[DLM] 레디 스는 잠금 분산 재진입을 달성

I. 서론

  기사 작성하기 전에 "잠금을 분산 정교한"를 분산 잠금의 세 가지 구현을 설명하지만, 달성하기 위해 분산 레디 스 루아 스크립트를 실행에 잠금, 잠금 노트와주의가 필요한 문제 분산 사용자 정의를 설명하지 않았다. 이 문서 레디 스 재진입 분산 잠금을 달성 사용하는 방법을 자세히 설명하는 것입니다.

둘째, 프로그램

교착 상태

  클라이언트가 더 이상 및 레디 스 노드 통신이 원인이 충돌하는 경우 잠금이 성공하지지면, 그것은 다른 클라이언트가 잠금을 결코 원인이 잠금을 보유했을 것이다, 그래서 자동 잠금 장치가 있어야합니다 시간을 놓습니다.
  우리는 확인해야합니다 setnx의 명령을하고 만료 원자 적으로 실행 명령을, 또는 클라이언트가 잠금을 얻을 setnx 후 수행하는 경우, 클라이언트는 다른 클라이언트에 잠겨 만나지 리드, 아래로 다음 잠금이 설정되지 않은 만료 시간입니다 .

다른 스레드 잠금이 해제됩니다

  당신이 SETNX 레디 스 분산 잠금 장치를 달성하기 위해 간단 어떤 치료를 사용하지 않는 경우 문제가 발생합니다 : 스레드 잠금 C1은 얻을 경우,하지만, 비즈니스 처리 시간이 너무 길면, 처리 사업의 완료 전에 나사 잠금 C1이 만료되지 않은 한, 실제로 제공 잠금 않습니다 다음 C1 잠금 동작을 해제 전체 비즈니스 실행 처리 사업 중 스레드에서 잠금 C2, C2 스레드를 취득 스레드, 이번에 사업은 여전히 비즈니스 처리 스레드 C2를 선도 스레드 스레드 C2 C1의 C2 스레드 해제 잠금 처리 보호 메커니즘은 유사하게 심각한 문제를 초래하여 잠금을 해제 할 수있다 C2 C3 스레드를 스레드.
  따라서 각 스레드는 잠금 마크의 소유자, 또한 필요해야합니다 그들의 잠금 잠금을 해제하기 위해 원자 작동을 보장하기 위해 때 잠금 만 해제 할 수 해제합니다.
  잠금이 해제 될 때 원자의 동작을 달성하기 위해 루아 스크립트를 이용하면서, 스크립트는 다음과 같다 때, 판정 마크 소유자 (값이 동일 함), 동일 만 제거 할 수있다 :

  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脚本来释放锁的时候一直不成功,参数没任何问题,而且我之前的文章就是用这个方法可以正确的释放锁。

추천

출처www.cnblogs.com/2YSP/p/11563448.html