[DLM] Redis achieve reentrant Distributed Lock

I. Introduction

  Before writing an article , "elaborate Distributed Lock" describes three implementations of distributed lock, but the lock on implementing distributed Redis Lua script to achieve, did not describe the custom Distributed Lock notes and problems that need attention. This article is to explain in detail how to use Redis achieve reentrant distributed lock.

Second, the program

Deadlock

  When a client gets the lock is successful, if it crashes cause it no longer and Redis node communication, then it would have been holding the lock, causing other clients never get a lock, so there must be an automatic lock release time.
  We need to ensure setnx command and expire command atomically executed, or if the client performs after setnx get the lock, then the client is down, then the lock is not set expiration time, lead to other clients never get locked .

Other thread lock is released

  If you do not use any treatment that is simple to achieve SETNX Redis distributed lock, will encounter a problem: If a thread lock C1 to obtain, but the business processing time is too long, thread lock before the completion of the processing business has C1 has not expired, then thread to acquire the lock C2, C2 threads in the thread during the complete business execution processing business C1 releases the lock operation, but this time the business is still processing thread thread C2 C1 C2 thread releases the lock, leading business processing thread C2 does not actually lock provided protection mechanisms; Similarly thread C2 C3 thread may release the lock, leading to serious problems.
  Therefore, each thread releases the lock can only be released when their lock that locks must be the owner of a mark, and also the need to ensure atomic operation to release the lock.
  Judgment mark owner (value is the same), the same can be removed only when the lock is released when, while taking advantage of Lua scripts to achieve atomic operations, the script is as follows:

  1. if redis.call("get", KEYS[1]) == ARGV[1] then 
  2. return redis.call("del", KEYS[1]) 
  3. else 
  4. return 0 
  5. end 

可重入问题

可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,如果没有可重入锁的支持,在第二次尝试获得锁时将会进入死锁状态。
这里有两种解决方案:

  1. 客户端在获得锁后保存value(拥有者标记),然后释放锁的时候将value和key同时传过去。
  2. 利用ThreadLocal实现,获取锁后将Redis中的value保存在ThreadLocal中,同一线程再次尝试获取锁的时候就先将 ThreadLocal 中的 值 与 Redis 的 value 比较,如果相同则表示这把锁所以该线程,即实现可重入锁。

这里的实现的方案是基于单机Redis,之前说的集群问题这里暂不考虑。

三、编码

我们通过自定义分布式锁注解+AOP可以更加方便的使用分布式锁,只需要在加锁的方法上加上注解即可。
Redis分布式锁接口

/**
 * Redis分布式锁接口
 * Created by 2YSP on 2019/9/20.
 */
public interface IRedisDistributedLock {

  /**
   *
   * @param key
   * @param requireTimeOut 获取锁超时时间 单位ms
   * @param lockTimeOut 锁过期时间,一定要大于业务执行时间 单位ms
   * @param retries 尝试获取锁的最大次数
   * @return
   */
  boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries);

  /**
   * 释放锁
   * @param key
   * @return
   */
  boolean release(String key);

}

Redis 分布式锁实现类

/**
 * Redis 分布式锁实现类
 * Created by 2YSP on 2019/9/20.
 */
@Slf4j
@Component
public class RedisDistributedLockImpl implements IRedisDistributedLock {

  /**
   * key前缀
   */
  public static final String PREFIX = "Lock:";
  /**
   * 保存锁的value
   */
  private ThreadLocal<String> threadLocal = new ThreadLocal<>();

  private static final Charset UTF8 = Charset.forName("UTF-8");
  /**
   * 释放锁脚本
   */
  private static final String UNLOCK_LUA;

  /*
   * 释放锁脚本,原子操作
   */
  static {
    StringBuilder sb = new StringBuilder();
    sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
    sb.append("then ");
    sb.append("    return redis.call(\"del\",KEYS[1]) ");
    sb.append("else ");
    sb.append("    return 0 ");
    sb.append("end ");
    UNLOCK_LUA = sb.toString();
  }

  @Autowired
  private RedisTemplate redisTemplate;

  @Override
  public boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries) {
    //可重入锁判断
    String originValue = threadLocal.get();
    if (!StringUtils.isBlank(originValue) && isReentrantLock(key, originValue)) {
      return true;
    }
    String value = UUID.randomUUID().toString();
    long end = System.currentTimeMillis() + requireTimeOut;
    int retryTimes = 1;

    try {
      while (System.currentTimeMillis() < end) {
        if (retryTimes > retries) {
          log.error(" require lock failed,retry times [{}]", retries);
          return false;
        }
        if (setNX(wrapLockKey(key), value, lockTimeOut)) {
          threadLocal.set(value);
          return true;
        }
        // 休眠10ms
        Thread.sleep(10);

        retryTimes++;
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return false;
  }

  private boolean setNX(String key, String value, long expire) {
    /**
     * List设置lua的keys
     */
    List<String> keyList = new ArrayList<>();
    keyList.add(key);
    return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
      Boolean result = connection
          .set(key.getBytes(UTF8),
              value.getBytes(UTF8),
              Expiration.milliseconds(expire),
              SetOption.SET_IF_ABSENT);
      return result;
    });

  }

  /**
   * 是否为重入锁
   */
  private boolean isReentrantLock(String key, String originValue) {
    String v = (String) redisTemplate.opsForValue().get(key);
    return v != null && originValue.equals(v);
  }

  @Override
  public boolean release(String key) {
    String originValue = threadLocal.get();
    if (StringUtils.isBlank(originValue)) {
      return false;
    }
    return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
      return connection
          .eval(UNLOCK_LUA.getBytes(UTF8), ReturnType.BOOLEAN, 1, wrapLockKey(key).getBytes(UTF8),
              originValue.getBytes(UTF8));
    });
  }


  private String wrapLockKey(String key) {
    return PREFIX + key;
  }
}

分布式锁注解

@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {

  /**
   * 默认包名加方法名
   * @return
   */
  String key() default "";

  /**
   * 过期时间 单位:毫秒
   * <pre>
   *     过期时间一定是要长于业务的执行时间.
   * </pre>
   */
  long expire() default 30000;

  /**
   * 获取锁超时时间 单位:毫秒
   * <pre>
   *     结合业务,建议该时间不宜设置过长,特别在并发高的情况下.
   * </pre>
   */
  long timeout() default 3000;

  /**
   * 默认重试次数
   * @return
   */
  int retryTimes() default Integer.MAX_VALUE;

}

aop切片类

@Component
@Aspect
@Slf4j
public class RedisLockAop {

  @Autowired
  private IRedisDistributedLock redisDistributedLock;


  @Around(value = "@annotation(lock)")
  public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, DistributedLock lock) {
    // 加锁
    String key = getKey(proceedingJoinPoint, lock);
    Boolean success = null;
    try {
        success = redisDistributedLock
          .lock(key, lock.timeout(), lock.expire(), lock.retryTimes());
      if (success) {
        log.info(Thread.currentThread().getName() + " 加锁成功");
        return proceedingJoinPoint.proceed();
      }
      log.info(Thread.currentThread().getName() + " 加锁失败");
      return null;
    } catch (Throwable throwable) {
      throwable.printStackTrace();
      return null;
    } finally {
      if (success){
        boolean result = redisDistributedLock.release(key);
        log.info(Thread.currentThread().getName() + " 释放锁结果:{}",result);
      }
    }
  }

  private String getKey(JoinPoint joinPoint, DistributedLock lock) {
    if (!StringUtils.isBlank(lock.key())) {
      return lock.key();
    }
    return joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature()
        .getName();
  }
}

业务逻辑处理类

@Service
public class TestService {

  @DistributedLock(retryTimes = 1000,timeout = 1000)
  public String lockTest() {
    try {
      System.out.println("模拟执行业务逻辑。。。");
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
      return "error";
    }

    return "ok";
  }
}

四、测试

  1. 启动本地redis,启动项目
  2. 打开cmd,利用ab压力测试设置3个线程100个请求。

ab -c 3 -n 100 http://localhost:8000/lock/test

idea控制台输出如下:

 

enter description here
enter description here

 

至此大功告成,代码地址

ps: 遇到一个奇怪的问题,我用 RedisTemplate.execute(RedisScript script, List keys, Object... args) 这个方法,通过加载resource目录下的lua脚本来释放锁的时候一直不成功,参数没任何问题,而且我之前的文章就是用这个方法可以正确的释放锁。

Guess you like

Origin www.cnblogs.com/2YSP/p/11563448.html