redis分布式锁的实现

一些参数说明
  1. DynmaicConfigurerBean:项目中配置,方便获取动态属性。
  2. 分布式锁相关的属性有:分布式锁的超时时间、重试次数、睡眠时间等
  3. 超时时间默认1s,重试次数默认5次,睡眠时间默认200ms
流程图

在这里插入图片描述

流程图及关键函数说明
  1. long expires = System.currentTimeMillis() + timeout + 1;

    设置分布式锁的超时时间

  2. redisClient.setNX(lockKey, expiresStr)

    尝试获取锁,键为锁的名字,值为超时时间

    如果获取成功就返回。

    没有获取会走下面逻辑:分析

/**
 1. 对于以下部分场景考虑的【重要注释】:
 2. ① 某一时刻所有线程都检测到之前已经成功获取锁的线程已经持有锁。
 3.   此时,如果检测到已持有锁的线程已经锁超时,那么他们可以通过CAS机制竞争锁,可以保证只有一个线程获取锁。
 4. ② 某一时刻,之前有一个线程已经获取锁了,剩余的线程,会发现锁的生命周期结束时间大于当前时间,因此不会获取锁。
 5. ③判断过期时间的好处:如果在发布时的时候,或者因为某些原因机器崩溃,假设碰巧没有解锁,那么就会产生死锁,判断过期时间也有自动检测死锁的功能。
 6. ④ 对于多线程getSet会不会让锁的生存周期结束时间更新?首先,即使更新,分布式锁的判断依据是是否获取锁,并不关心超时的情况,并且对于已经获取锁
 7.    的线程会解锁。即使已经获取锁的线程超时了,但是对应key值的value值已经被重新赋值生命周期,但是因为既然是锁竞争,走到这里他们的生存周期的截止时间
 8.    都是相近的,也没有什么问题。
 */
  1. String currentValueStr = redisClient.get(lockKey);
Long.parseLong(currentValueStr) < System.currentTimeMillis()

没有获取到锁则获取该锁的超时时间,判断超时时间有没有超过当前时间
  1. String oldValueStr = redisClient.getSet(lockKey, expiresStr);

    如果已经小于当前时间,说明获得锁的线程并没有及时释放锁,则去cas抢锁,抢锁成功,则返回

  2. 没有抢到,则重试次数加1,重试次数大于阈值,则退出,否则睡眠一段时间再重试。

代码实现
@Component
public class DistributedLock {
    private Logger logger = LoggerFactory.getLogger(DistributedLock.class);

    /** 动态配置中分布式锁是否生效的标识 */
    private static final String distributedLockValid = "distributedLockValid";
    /** 动态配置中分布式锁的超时时间 */
    private static final String distributedLockTimeout = "distributedLockTimeout";
    /** 动态配置中分布式锁的重试次数 */
    private static final String distributedLockRetryCount = "distributedLockRetryCount";
    /** 动态配置中分布式锁的睡眠时间 */
    private static final String distributedLockSleepTime = "distributedLockSleepTime";

    @Autowired
    private DynmaicConfigurerBean dynmaicConfigurerBean;

    @Resource(name = "redisClient")
    private RedisClient redisClient;

    /**
     * 分布式锁-加锁[ms]级别加锁
     * 
     * @param logHead 日志头
     * @param lockKey key
     * @return lockBean
     */
    public LockBean lock(String logHead, String lockKey) {
        try {
            logger.info(logHead + "DistributedLock.lock lockKey=" + lockKey);

            // 获取动态配置参数
            boolean isValid = dynmaicConfigurerBean.getBoolean(distributedLockValid, false);
            int timeout = dynmaicConfigurerBean.getInt(distributedLockTimeout, 1000); // 默认1s
            int retryCount = dynmaicConfigurerBean.getInt(distributedLockRetryCount, 5); // 默认重试5次
            int sleepTime = dynmaicConfigurerBean.getInt(distributedLockSleepTime, 200); // 默认睡眠时间200ms
            logger.info(logHead + "lock-分布式锁动态配置,isValid=" + isValid + ",timeout[ms]=" + timeout +
                    ",retryCount=" + retryCount + ",sleepTime[ms]=" + sleepTime);

            if (isValid) { // 动态配置生效
                /**
                 * 增加锁的重试次数,减少对上游的影响
                 * add by dlb 2017年8月8日11:05:43
                 */
                int count = 0;
                while (true) {
                    // 设置锁的到期时间,这是一个锁的生命周期的结束时刻
                    long expires = System.currentTimeMillis() + timeout + 1;
                    String expiresStr = String.valueOf(expires);

                    if (redisClient.setNX(lockKey, expiresStr) == true) { // 加锁成功,表示本线程已经获取到锁
                        logger.info(logHead + "lock-加锁成功[2],lockKey=" + lockKey + ",timeout[ms]=" + timeout);
                        return new LockBean(2, expires);
                    }

                    /**
                     * 以下代码是对于以下部分场景考虑的【重要注释】:
                     * ① 某一时刻所有线程都检测到之前已经成功获取锁的线程已经持有锁,那么都会走到这儿来。
                     *   此时,如果检测到已持有锁的线程已经锁超时,那么他们可以通过CAS机制竞争锁,可以保证只有一个线程获取锁。
                     * ② 某一时刻,如果所有线程走到这里,之前有一个线程已经获取锁了,剩余的线程,会发现锁的生命周期结束时间大于当前时间,因此不会获取锁。
                     * ③ 这个地方,能不能对于没有获取锁的直接返回?如果在发布时的时候,或者因为某些原因机器崩溃,假设碰巧没有解锁,那么就会产生死锁,这个
                     *    地方也有自动检测死锁的功能。
                     * ④ 对于多线程getSet会不会让锁的生存周期结束时间更新?首先,即使更新,分布式锁的判断依据是是否获取锁,并不关心超时的情况,并且对于已经获取锁
                     *    的线程会解锁。即使已经获取锁的线程超时了,但是对应key值的value值已经被重新赋值生命周期,但是因为既然是锁竞争,走到这里他们的生存周期的截止时间
                     *    都是相近的,也没有什么问题。
                     */
                    String currentValueStr = redisClient.get(lockKey);
                    if (!Strings.isNullOrEmpty(currentValueStr) && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                        // redis保证设置新值,并且返回上一个版本的值
                        String oldValueStr = redisClient.getSet(lockKey, expiresStr);
                        if (!Strings.isNullOrEmpty(oldValueStr) && oldValueStr.equals(currentValueStr)) {
                            logger.info(logHead + "lock-加锁成功[2],lockKey=" + lockKey + ",timeout[ms]=" + timeout);
                            return new LockBean(2, expires);
                        }
                    }

                    count++;
                    logger.info(logHead + "分布式锁抢锁次数=" + count + ",lockKey=" + lockKey);

                    if (count > retryCount) { // 超过重试次数直接退出循环
                        logger.info(logHead + "分布式锁超过重试次数,lockKey=" + lockKey + ",次数=" + count
                                + ",sleepTime[ms]=" + sleepTime);
                        break;
                    }

                    Thread.sleep(sleepTime); // 睡一会,然后再次尝试抢锁
                }

                logger.info(logHead + "lock-目标已经被加锁[1],lockKey=" + lockKey + ",timeout[ms]=" + timeout);
                return new LockBean(1, null);
            } else { // 动态配置没有失效,不会影响业务系统的执行结果
                logger.info(logHead + "lock-分布式锁动态配置没有生效[0],lockKey=" + lockKey + ",isValid=" + isValid);
                return new LockBean(0, null);
            }
        } catch (Exception e) {
            logger.error(logHead + "DistributedLock.lock.error[-1]", e);
            return new LockBean(-1, null);
        }
    }

    /**
     * 分布式锁-解锁
     * @param logHead 日志头
     * @param lockKey key
     */
    public void unlock(String logHead, String lockKey, LockBean lockBean) {
        try {
            /**
             * 如果之前已经获取锁成功了,并且业务执行时间在锁的超时返回之内,
             * 那么本线程可以释放锁,否则本线程不要释放锁,需要等待其他在超时时间以后
             * 获取锁的线程释放锁,以防止出现对同一单号的连续解锁反应。
             */
            if (lockBean.getCode() == 2 && (System.currentTimeMillis() <= lockBean.getTime())) {
                redisClient.del(lockKey); // 解锁
                logger.info(logHead + "unlock-解锁成功,lockKey=" + lockKey);
            } else {
                logger.info(logHead + "unlock-该线程已经业务超时,不进行解锁,lockKey=" + lockKey);
            }
        } catch (Exception e) {
            logger.error(logHead + "unlock-解锁失败,lockKey=" + lockKey, e);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值