redis实现分布式锁 工具类

/**
 * @description: Redis 分布式锁
 * @author: cy
 * @create: 2020-07-10
 **/
@Slf4j
@Component
public class RedisLock {

  private final RedisTemplate<String, Object> redisTemplate;

  private final ThreadLocal<String> localKeys = new ThreadLocal<>();
  private final ThreadLocal<String> localValues = new ThreadLocal<>();


  private final static String LOCK_PREFIX = "REDIS_LOCK:";
  private final static long LOCK_SUCCESS = 1;
  private final static long RELEASE_SUCCESS = 1;
  private final static long LOCK_EXPIRED = -1;

  @Value("${application.lock-timeout}")
  private long timeout;

  //定义获取锁的lua脚本
  private final static DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>(
          "if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"pexpire\", KEYS[1], KEYS[3]) else return 0 end"
          , Long.class
  );

  //定义释放锁的lua脚本
  private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(
          "if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end"
          , Long.class
  );

  public RedisLock(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
  }

  /**
   * 清除本地线程变量,防止内存泄露
   */
  private void clean() {
    localValues.remove();
    localKeys.remove();
  }

  /**
   * 获取RedisKey
   * @param key 原始KEY,如果为空,自动生成随机KEY
   * @return
   */
  private String getRedisKey(String key) {
    //如果Key为空且线程已经保存,直接用,异常保护
    if (StringUtils.isEmpty(key) && !StringUtils.isEmpty(localKeys.get())) {
      return localKeys.get();
    }
    //如果都是空那就抛出异常
    if (StringUtils.isEmpty(key) && StringUtils.isEmpty(localKeys.get())) {
      throw new RuntimeException("key is null");
    }
    return LOCK_PREFIX + key;
  }

  public boolean lock(String key, String value) {
    return lock(key, value, timeout);
  }

  /**
   * 加锁
   * @param key Key
   * @param timeout 过期时间
   * @return
   */
  public boolean lock(String key, String value, long timeout) {
    try {
      long start = System.currentTimeMillis();
      final String redisKey = this.getRedisKey(key);
      //组装lua脚本参数
      List<String> keys = Arrays.asList(redisKey, value, String.valueOf(timeout));
      //执行脚本
      Long result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
      //存储本地变量
      if (result != null && LOCK_SUCCESS == result) {
        localValues.set(value);
        localKeys.set(redisKey);
        log.info("成功获取锁:" + Thread.currentThread().getName() + ", Status code:" + result);
        return true;
      } else {
        //重试获取锁
        log.debug("开始重试获取锁:" + Thread.currentThread().getName() + ", Status code reply:" + result);
        while(true) {
          try {
            //检测是否超时
            if (System.currentTimeMillis() - start > timeout) {
              return false;
            }
            //休眠一定时间后再获取锁,这里时间可以通过外部设置
            Thread.sleep(20);
            result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
            if(!StringUtils.isEmpty(result) && result == LOCK_SUCCESS) {
              localValues.set(value);
              localKeys.set(redisKey);
              log.info("成功获取锁:" + Thread.currentThread().getName() + ", Status code:" + result);
              return true;
            }
          } catch (Exception e) {
            log.error("获取锁异常:" + Thread.currentThread().getName(), e);
            break;
          }
        }
      }
    } catch (Exception e1) {
      log.error("获取锁异常:" + Thread.currentThread().getName(), e1);
    }
    return false;
  }

  /**
   * 释放KEY
   * @param key
   * @return
   */
  public boolean unlock(String key) {
    try {
      String localKey = localKeys.get();
      //如果本地线程没有KEY,说明还没加锁,不能释放
      if(StringUtils.isEmpty(localKey)) {
        log.error("释放锁失败: lock key not found");
        return false;
      }
      String redisKey = getRedisKey(key);
      //判断KEY是否正确,不能释放其他线程的KEY
      if(!StringUtils.isEmpty(localKey) && !localKey.equals(redisKey)) {
        log.error("释放锁失败: illegal key:" + key);
        return false;
      }
      //组装lua脚本参数
      List<String> keys = Arrays.asList(redisKey, localValues.get());
      // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
      Long result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys);
      //如果这里抛异常,后续锁无法释放
      if (result != null && RELEASE_SUCCESS == result) {
        log.info("释放锁成功:" + Thread.currentThread().getName() + ", Status code reply=" + result);
        return true;
      } else if (!StringUtils.isEmpty(result) && result == LOCK_EXPIRED) {
        //返回-1说明获取到的KEY值与requestId不一致或者KEY不存在,可能已经过期或被其他线程加锁
        // 一般发生在key的过期时间短于业务处理时间,属于正常可接受情况
        log.warn("释放锁异常:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result);
      } else {
        //其他情况,一般是删除KEY失败,返回0
        log.error("释放锁失败:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result);
      }
    } catch (Exception e) {
      log.error("释放锁失败异常", e);
    } finally {
      //清除本地变量
      this.clean();
    }
    return false;
  }
}
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页