ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

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

2020-06-16 11:07:09  阅读:788  来源: 互联网

标签:lock Redis AP key lockInfo lockKey 分布式


分布式CAP理论

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

AP模型的分布式锁

AP模型的分布式锁是基于Redis来实现的。Redis集群在分布式系统中是一种AP模型,无法保证在主节点宕机时自动完成数据一致性的同步操作,因此在业务要求保证一致性的场景中,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:

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

标签:lock,Redis,AP,key,lockInfo,lockKey,分布式
来源: https://blog.csdn.net/qq_41401800/article/details/106764515

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有