• 【分布式id和分布式锁】分布式id实现方案、分布式锁的使用场景、实现方案、注意事项_Redis03


    1、分布式id

    • Redis可以实现分布式id,原理就是利用redis的 incr命令实现ID的原子性自增
    127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
    OK
    127.0.0.1:6379> incr seq_id      // 增加1,并返回递增后的数值
    (integer) 2
        
    ------------------------------------------------------------------------------------------------------------
    @Service
    public class SequenceFactory {
        @Autowired
        RedisTemplate<StringString> redisTemplate;
    
        /**
         * @param key
         * @param value
         * @param expireTime
         * @Title: set
         * @Description: set cache.
         */
        public void set(String key, int value, Date expireTime) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
            counter.set(value);
            counter.expireAt(expireTime)}
    
        /**
         * @param key
         * @param value
         * @param timeout
         * @param unit
         * @Title: set
         * @Description: set cache.
         */
        public void set(String key, int value, long timeout, TimeUnit unit) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
            counter.set(value);
            counter.expire(timeout, unit)}
    ------------------------------------------------------------------------------------------------------------
        /**
         * @param key
         * @return
         * @Title: generate
         * @Description: Atomically increments by one the current value.
         */
        public long generate(String key) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory())return counter.incrementAndGet()}
    
        /**
         * @param key
         * @return
         * @Title: generate
         * @Description: Atomically increments by one the current value.
         */
        public long generate(String key, Date expireTime) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
            counter.expireAt(expireTime)return counter.incrementAndGet()}
    
        /**
         * @param key
         * @param increment
         * @return
         * @Title: generate
         * @Description: Atomically adds the given value to the current value.
         */
        public long generate(String key, int increment) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory())return counter.addAndGet(increment)}
    
        /**
         * @param key
         * @param increment
         * @param expireTime
         * @return
         * @Title: generate
         * @Description: Atomically adds the given value to the current value.
         */
        public long generate(String key, int increment, Date expireTime) {
            RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
            counter.expireAt(expireTime)return counter.addAndGet(increment)}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 用redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF
      • RDB会定时打一个快照进行持久化:假如连续自增redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况
      • AOF会对每条写命令进行持久化:即使Redis挂掉也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长

    2、redis实现的分布式锁

    1. 概念理解

    • 关于分布式锁,一般有三种选择:

      • 1、redis
      • 2、zk
      • 3、DB锁(悲观锁、乐观锁)
    • 另外几种模式都无法避免两个问题:

      • 1、异步数据丢失。
      • 2、脑裂问题。
    • 所以redis官方针对这种情况提出了红锁(Redlock)的概念:

      • 假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。
    • 分布式锁使用场景:多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)。

      • 分布式部署后,单机锁还是出现超卖现象,需要分布式锁。
      • Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。

    2. 解决方案

    • 1)redis单机下使用set key value EX seconds NX + 手写Redis自动续期demo防止误解锁。
    • 2)redis集群下可以使用redlock算法,有现成的jar包Redisson。

    3. 常见的面试题

    • Redis除了拿来做缓存,你还见过基于Redis的什么用法?
    • Redis做分布式锁的时候有需要注意的问题?
    • 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
    • 集群模式下,比如主从模式,有没有什么问题呢?
      • 主 Redis 创建了锁,但是还没有来得及同步给从,然后主挂了,从成为新的主的时候并没有之前创建的锁。
    • 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈。
    • Redis分布式锁如何续期?看门狗知道吗?

    4. 逐步分析需注意得要点

    • 问题1:代码出现异常的话,可能无法释放锁必须要在代码层面finally释放锁
      • 解决方法:try…finally…
    • 问题2:部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块没办法保证解锁,这个key没有被删除。
      • 解决方法:需要加入一个过期时间限定key
    • 问题3:设置key+过期时间分开了,必须要合并成一行具备原子性。
      • 可能会导致什么结果:存在的问题是setnx和expire设置过期时间是两步操作非原子性操作,当某线程执行setnx得到了锁,还没来得及设置过期时间时,redis节点挂掉了,这把锁就永久有效了,其他线程就无法再获得锁了
      • 解决方法:写到一行,就可以保证原子性
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10LTimeUnit.SECONDS)
    • 1
    • 问题4:张冠李戴,删除了别人的锁
      • 解决方法:只能自己删除自己的,不许动别人的
    • 问题5:finally块的判断 + del删除操作不是原子性的
      • 解决方案:用lua脚本、用redis自身的事务
    • 问题6:确保RedisLock过期时间大于业务执行时间的问题
      • 解决方案:直接上RedLock之Redisson落地实现

    在这里插入图片描述

    5. Redisson怎么玩?

    
    @Autowired
    private Redisson redisson;
    
    public static final String REDIS_LOCK = "REDIS_LOCK"@GetMapping("/doSomething")
    public String doSomething(){
    
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock()try {
            //doSomething
        }finally {
        	//添加后,更保险
    		if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
        		redissonLock.unlock()}
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    6. 手写Redis自动续期demo

    1)加锁解锁方法
    package com.yubin.case5;
    import com.yubin.RedisLock;
    import redis.clients.jedis.Jedis;
    
    import java.time.LocalTime;
    import static com.yubin.LockConstants.*;
    
    public class LockCase5 extends RedisLock {
    
        public LockCase5(Jedis jedis, String lockKey) {
            super(jedis, lockKey);
    
        }
    
        @Override
        public void lock() {
            while (true) {
                String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 30);
                if (OK.equals(result)) {
                    System.out.println("线程id:"+Thread.currentThread().getId() + "加锁成功!时间:"+LocalTime.now());
    
                    //开启定时刷新过期时间
                    isOpenExpirationRenewal = true;
                    scheduleExpirationRenewal();//调用父类中的定时续期方法
                    break;
                }
                System.out.println("线程id:"+Thread.currentThread().getId() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
                //休眠10秒
                sleepBySencond(10);
            }
        }
    
        @Override
        public void unlock() {
            System.out.println("线程id:"+Thread.currentThread().getId() + "解锁!时间:"+LocalTime.now());
    
            String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                        "return redis.call('del', KEYS[1]) " +
                                        "else " +
                                        "return 0 " +
                                        "end";
            jedis.eval(checkAndDelScript, 1, lockKey, lockValue);//执行解锁的lua脚本
          
            isOpenExpirationRenewal = false;//标识位置为false,锁续期的for循环推出;
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    2)锁续期方法
    package com.yubin;
    
    import redis.clients.jedis.Jedis;
    
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    
    public abstract class RedisLock implements Lock {
    
        protected Jedis jedis;
        protected String lockKey;
        protected String lockValue;
        protected volatile boolean isOpenExpirationRenewal = true;
    
        public RedisLock(Jedis jedis,String lockKey) {
            this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
        }
    
        public RedisLock(Jedis jedis, String lockKey, String lockValue) {
            this.jedis = jedis;
            this.lockKey = lockKey;
            this.lockValue = lockValue;
        }
    
        public void sleepBySencond(int sencond){
            try {
                Thread.sleep(sencond*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 开启定时刷新
         */
        protected void scheduleExpirationRenewal(){
            Thread renewalThread = new Thread(new ExpirationRenewal());
            renewalThread.start();
        }
    
        @Override
        public void lockInterruptibly(){}
    
        @Override
        public Condition newCondition() {
            return null;
        }
    
        @Override
        public boolean tryLock() {
            return false;
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit){
            return false;
        }
    
    
        /**
         * 刷新key的过期时间,每隔10秒续期30秒
         */
        private class ExpirationRenewal implements Runnable{
            @Override
            public void run() {
                //isOpenExpirationRenewal续期标识位用volatile修饰,保证可见性
                while (isOpenExpirationRenewal){
                    System.out.println("执行延迟失效时间中...");
    
                    String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                            "return redis.call('expire',KEYS[1],ARGV[2]) " +
                            "else " +
                            "return 0 end";
                    jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");//执行续期的lua脚本
    
                    //休眠10秒(保证每10秒钟续期30秒)
                    sleepBySencond(10);
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    7. redis分布式锁总结回顾

    •  synchronized单机版oK,上分布式
      ​ nginx分布式微服务单机锁不行
      ​ 取消单机锁,上Redis分布式锁setnx
      ​ 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
      ​ 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
      ​ 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行
      ​ 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
      ​ Redis集群环境下,我们自己写的也不oK,因为不能保证锁续期问题。直接上RedLock之Redisson落地实现
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

  • 相关阅读:
    大学生数学建模赛题解析及优秀论文-2021电工杯A题高铁牵引供电系统运行数据分析及等值建模(附Python代码)
    软件测试员必看!数据库知识mysql查询语句大全
    Python多进程之分享(multiprocessing包)
    RabbitMQ实现延迟消息
    【ARM Coresight SoC-400/SoC-600 专栏导读】
    广州华锐互动:VR互动实训内容编辑器助力教育创新升级
    面试官:为什么忘记密码要重置而不是告诉你原密码?
    算法入门(五):对数器与递归复杂度计算
    DP(动态规划)【2】 最大连续子列和 最长不降子序列
    疯狂星期四的营销策略是什么?如何复制?
  • 原文地址:https://blog.csdn.net/weixin_38963649/article/details/126160778