AP模式(Redis)的分布式锁分析以及实现

分布式CAP理论

在介绍分布式锁之前,先说一下CAP理论。因为现在提到分布式系统一定离不开CAP理论。C(Consistency)一致性、A(Availability)可用性、P(Partition tolerance)分区容错性。三者不能同时存在,由于P是必要因素,所以分为CP和AP两种模型。下面我们就根据AP和CP模型来分析一下分布式锁以及使用场景。

AP模型的分布式锁

AP模型是保证了在分布式系统中的高可用性。AP模型的分布式锁是基于Redis来实现的,适用于对数据的一致性要求不那么苛刻的场景中,可以保证高可用性。根据Redis存储的结构以及原理可以知道Redis无法保证主从节点数据的一致性,无法保证在主节点宕机时将所有数据自动同步到从节点中,因此在业务要求保证一致性的场景中,Redis的分布式锁会在主节点宕机的情况下丢失锁信息而出现重复上锁的极端情况。

Redis分布式锁原理

  1. SETNX:Redis的分布式锁主要是使用Redis的SETNX命令来完成上锁的操作。此条命令的官方解释为:只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。在设置成功时返回 1 , 设置失败时返回 0 。
  2. expire:Redis支持key设置过期时间,因此我们在设计锁的时候会设置一个过期时间来使得key有自动过期的机制。但是单纯的只设置过期时间会有问题,在下一个小结介绍问题所在。
  3. 续租:上面提到了单纯的设置过期时间会产生在持有锁的期间内逻辑没有处理完而自动释放锁的问题。例如当前线程在获得锁后,设置了过期时间为1秒,但是由于某些原因导致或者是代码的bug使得此次代码逻辑时间超过了1秒,这时导致锁被释放,而此时下一个线程重新获得了锁,导致最终业务受到影响,波及整个系统的数据问题。因此需要续租的机制来保证在当前线程没有执行完的时候不会自动释放锁,从而保证业务数据的安全。

Redis分布式锁的实现

一、 基于jedis分布式锁的实现
为了保证SETNX和expire的原子操作,可以通过redis的一条命令来完成。
在这里插入图片描述
上图中的NX和XX参数介绍一下,NX为如果key不存在则设置一个value。XX为只在键已经存在时, 才对键进行设置操作,XX为续租来做准备。此条命令可以省去了写lua脚本来保证setnx和expire的原子性操作。
二、基于redisson分布式锁的实现
redisson是redis的一个客户端,封装了基本的redis操作还有对分布式锁的支持,redisson实现了自动续租的操作,上手更加容易,操作简单。redisson的看门狗机制就是实现了对过期时间的自动续租功能,如果在业务中出现了死循环代码或者是处理时间过长的问题,会导致看门狗无限续租的情况出现,此时我们需要保证业务代码的健壮性以及增加对锁的监控手段,避免线上出现死锁问题导致排查困难。

Redis分布式锁代码

一、jedis的分布式锁代码,注解实现

@Around("lockPoint()")
    public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
        //获取RedisLock注解信息
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        RedisLock lockInfo = method.getAnnotation(RedisLock.class);
        String lockKey = lockInfo.value();
        if (StringUtils.isBlank(lockKey)) {
            throw new IllegalArgumentException("配置参数错误,lockKey不能为空!");
        }
        boolean lock = false;
        Object obj = null;
        try {
            // 获取锁的最大超时时间
            long maxSleepMills = System.currentTimeMillis() + lockInfo.maxSleepMills();
            while (!lock) {
                //持锁时间
                String keepMills = String.valueOf(System.currentTimeMillis() + lockInfo.keepMills());
                //上锁
                lock = jedisService.setNX(lockKey, keepMills, lockInfo.keepMills());
                // 得到锁,没有人加过相同的锁
                if (lock) {
                    logger.info("得到锁...");
                    obj = pjp.proceed();
                }
                // 已过期,并且getAndSet后旧的时间戳依然是过期的,可以认为获取到了锁
                else if (System.currentTimeMillis() > jedisService.get(lockKey) &&
                        (System.currentTimeMillis() > jedisService.getAndSet(lockKey, keepMills))) {
                    lock = true;
                    logger.info("得到锁...");
                    obj = pjp.proceed();
                }
                // 没有得到任何锁
                else {
                    // 继续等待获取锁
                    if (lockInfo.action().equals(RedisLock.LockFailAction.CONTINUE)) {
                        // 如果超过最大等待时间抛出异常
                        logger.info("稍后重新请求锁...");
                        if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
                            throw new TimeoutException("获取锁资源等待超时");
                        }
                        TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
                    } else {
                        // 放弃等待
                        logger.info("放弃锁...");
                        break;
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            // 如果获取到了锁,释放锁
            if (lock) {
                //锁没有过期就删除key
                if (System.currentTimeMillis() < (System.currentTimeMillis() + lockInfo.keepMills())) {
                    logger.info("释放锁...");
                    jedisService.delete(lockKey);
                }

            }
        }
        return obj;
    }
public boolean setNX(String key, String value, long time) {
        boolean res = false;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String set = jedis.set(key, value, NX, PX, time);
            if (StringUtils.isBlank(set)) {
                res = true;
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }
        return res;

    }
    private void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedisPool.returnResource(jedis);
        }
    }

一、redisson的分布式锁代码,注解实现

@Around("lockPoint()")
    public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
        //获取Lock注解信息
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        RedissonLock lockInfo = method.getAnnotation(RedissonLock.class);
        String lockKey = lockInfo.key();
        if (StringUtils.isBlank(lockKey)) {
            throw new IllegalArgumentException("配置参数错误,lockKey不能为空!");
        }
        long leaseTime = lockInfo.leaseTime();
        long waitTime = lockInfo.waitTime();
        TimeUnit unit = lockInfo.unit();
        Object obj = null;
        boolean lock = false;
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            //尝试去上锁
            if (leaseTime > 0) {
                //设置过期时间,到期自动释放的锁
                lock = rLock.tryLock(waitTime, leaseTime, unit);
            } else {
                //不设置过期时间,看门狗自动续租的锁
                lock = rLock.tryLock(waitTime, unit);
            }
            if (lock && rLock.isHeldByCurrentThread()) {
                logger.info("当前线程得到锁...");
                obj = pjp.proceed();
            }
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            if (lock && rLock.isHeldByCurrentThread()) {
                //当前线程是否持有此锁,持有就删除锁
                logger.info("释放锁...");
                rLock.unlock();

            }
        }
        return obj;
    }

AP模式分布式锁总结

以上是我对redis实现的分布式锁的一些介绍。redis锁的机制理解起来比较简单,现有的redisson客户端可以很好的支持分布式锁的操作,也基本满足了分布式锁的场景需要。redis分布式锁的最致命的问题就是无法保证数据的一致性,如果一旦主节点宕机,数据没有同步到从节点中,会出现再次上锁的问题,如果业务一定需要数据的一致性在高并发的场景下是不建议选择redis锁的实现,可以选择CP模型的zk或者etcd来实现分布式锁。以上的例子以及代码都是基于单机的redis来实现的,如有不足望大家指正。

PS:
在文章的最后为大家推荐一个公众号《架构之美》,上面会不定期的推荐一些技术文章,希望大家喜欢。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值