• Redis 分布式锁一步步优化过程


    Redis 官方文档

    点击查看 Redis 中文官方文档

    列表模式 http://redis.cn/topics/

    举个案例

    高并发秒杀场景中,看一下一段代码,会发生哪些问题?

    @Slf4j
    @RestController
    public class GoodController {
        private static final String KEY = "goods:001";
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @RequestMapping("/goods")
        public String buyGoods() {
    
            String result = stringRedisTemplate.opsForValue().get(KEY);
    
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
    
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY,Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件",realNum);
                return "你已经成功秒杀了商品,还剩余"+realNum+"件";
            } 
            
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
            return "活动已经售罄,欢迎下次光临";
        }
    }
    
    • 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

    肯定会发生超卖(重复卖同一件商品)问题,所以这里可以加锁保证数据安全性,可以使用 Synchornized、ReentantLock 锁,但是推荐使用 ReentantLock 锁,它有一个 tryLock() 方法可以尝试加锁,不会死等。从而可以做一些自己的业务操作,改进之后如下:

    	@RequestMapping("/goods2")
        public String buyGoods() {
    
            if (lock.tryLock()) {
                try {
                    String result = stringRedisTemplate.opsForValue().get(KEY);
                    int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                    if (num > 0) {
                        int realNum = num - 1;
                        stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                        log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件", realNum);
                        return "你已经成功秒杀了商品,还剩余" + realNum + "件";
                    }
                    log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
                    return "活动已经售罄,欢迎下次光临";
                } finally {
                    lock.unlock();
                }
            } else {
                // do_something....
            }
            return "程序结束";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试效果如下:

    2022-09-14 11:10:26.028  INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余92022-09-14 11:10:26.125  INFO 92342 --- [o-9292-exec-185] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余82022-09-14 11:10:26.148  INFO 92342 --- [o-9292-exec-109] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余72022-09-14 11:10:26.157  INFO 92342 --- [o-9292-exec-179] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余62022-09-14 11:10:26.183  INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余52022-09-14 11:10:26.189  INFO 92342 --- [io-9292-exec-93] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余42022-09-14 11:10:26.192  INFO 92342 --- [io-9292-exec-74] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余32022-09-14 11:10:26.198  INFO 92342 --- [io-9292-exec-67] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余22022-09-14 11:10:26.203  INFO 92342 --- [o-9292-exec-192] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余12022-09-14 11:10:26.207  INFO 92342 --- [io-9292-exec-43] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余02022-09-14 11:10:26.210  INFO 92342 --- [o-9292-exec-110] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>活动已经售罄,欢迎下次光临.
    2022-09-14 11:10:26.212  INFO 92342 --- [io-9292-exec-89] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>活动已经售罄,欢迎下次光临.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    以上虽然能够解决单机在 Redis 上的安全行,但是如果是分布式微服务就不能保证了,所以最终还是要使用 Redis 来做分布式锁。

    那么这里就是用 Nginx 做负载均衡转发。

    安装和配置 nginx 请看:nginx 安装 nginx配置

    重新修改后代码如下:

        @RequestMapping("/goods4")
        public String buyGoods() {
    
            // 加上分布式锁
            String randStr = UUID.randomUUID().toString();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
            if (!flag) {
                return "抢锁失败,请进行重试...";
            }
    
            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
    
                // 正常执行业务之后直接释放锁
                stringRedisTemplate.delete(REDIS_KEY);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    改成用分布式锁来保证数据安全性,但是这里还有一个问题,就是如果业务执行不正常或者不没有释放锁,就会导致所有请求执行不了,所以必须要把释放锁这个操作放到 finally 操作快里面执行,无论如何都要释放掉锁。

    那么重新改进后的代码如下:

        @RequestMapping("/goods5")
        public String buyGoods() {
    
            try {
                // 加上分布式锁
                String randStr = UUID.randomUUID().toString();
                Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
                if (!flag) {
                    return "抢锁失败,请进行重试...";
                }
    
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 正常执行业务之后直接释放锁
                stringRedisTemplate.delete(REDIS_KEY);
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    但是发现这都是正常操作所以可以保证正常释放掉锁,此时如果程序刚好执行到 try 代码块的时候,还没来得及执行 finally 语句块,然后就停电了关机了,那么此时也就释放不了这把锁。

    所以这里还需要对锁加入失效时间,保证宕机之后能够正常释放掉锁。修改过后的代码如:

        @RequestMapping("/goods5")
        public String buyGoods() {
    
            try {
                // 加上分布式锁
                String randStr = UUID.randomUUID().toString();
                Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
    
                // 加上锁过期时间
                stringRedisTemplate.expire(REDIS_KEY,30L, TimeUnit.SECONDS);
                if (!flag) {
                    return "抢锁失败,请进行重试...";
                }
    
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 正常执行业务之后直接释放锁
                stringRedisTemplate.delete(REDIS_KEY);
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    加上了过期时间,但是还是有个问题,那就是加锁和设置锁失效时间不是一个原子操作,如果加锁成功,然后又停电宕机了,没有给锁设置过期时间,那么锁又会释放不成功。

    将加锁和设置锁失效时间两个步骤换成一个操作,如下所示:

    
        @RequestMapping("/goods6")
        public String buyGoods() {
    
            try {
                String randStr = UUID.randomUUID().toString();
                // 加上分布式锁、设置锁过期时间
                Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
    
                if (!flag) {
                    return "抢锁失败,请进行重试...";
                }
    
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 正常执行业务之后直接释放锁
                stringRedisTemplate.delete(REDIS_KEY);
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    现在已经保证了加锁的正确性了,但是还存在一个巨大的问题,就是误删问题,因为你给锁设置的超时时间是 30s,假设业务处理时间需要消耗 40s,那么此时第一个还在处理业务,超过了 30s,锁直接失效释放了,此时第二个线程就会加锁成功,等到第一个线程执行完之后,执行 finally 块释放锁,就把第二个线程的锁给释放了,这就是误删问题,非常严重。那么怎么解决呢?

    是不是可以在释放前先获取到锁,然后判断这把锁是不是自己的呢?是自己的就可以释放,不是自己的就不能释放,修改之后如下:

        @RequestMapping("/goods6")
        public String buyGoods() {
    
            String randStr = UUID.randomUUID().toString();
            try {
                // 加上分布式锁、设置锁过期时间
                Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
    
                if (!flag) {
                    return "抢锁失败,请进行重试...";
                }
    
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 加上锁判断、是否允许释放条件
                if (stringRedisTemplate.opsForValue().get(REDIS_KEY).equals(randStr)) {
    
                    // 正常执行业务之后直接释放锁
                    stringRedisTemplate.delete(REDIS_KEY);
                }
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    但是此时,判断锁和释放锁的步骤不是一个原子操作,还是可能存在时间先后的差异,也会导致误删锁的问题,所以需要保证判断和释放锁必须是一个原子操作,如下所示:

        @RequestMapping("/goods7")
        public String buyGoods() {
    
            String randStr = UUID.randomUUID().toString();
            try {
                // 加上分布式锁、设置锁过期时间
                Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);
    
                if (!flag) {
                    return "抢锁失败,请进行重试...";
                }
    
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 通过 Lua 脚本删除分布式锁的 key
    
                Jedis jedis = RedisUtils.getJedis();
    
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                try {
                    Object result = jedis.eval(script, Collections.singletonList(REDIS_KEY), Collections.singletonList(randStr));
                    // 这里记得 toString() 否则不会相等
                    if ("1".equals(result.toString())) {
                        log.info(">>>>>>>>>del lock_key success!!!");
                    } else {
                        log.info(">>>>>>>>del lock_key error... ");
                    }
                } finally {
                    if (Objects.nonNull(jedis)) {
                        // 关闭资源
                        jedis.close();
                    }
                }
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    RedisUtils 工具类如下:

    
    public class RedisUtils {
    
        private static JedisPool jedisPool = null;
    
        /**
         * 创建 Jedis 连接池
         */
        static {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(20);
            jedisPoolConfig.setMaxIdle(10);
            jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379);
        }
    
        /**
         * 从 JedisPool 中获取到 Jedis 对象
         */
        public static Jedis getJedis() {
            if (Objects.nonNull(jedisPool)) {
                return jedisPool.getResource();
            }
            throw new RuntimeException("连接池创建失败...");
        }
    }
    
    
    • 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

    使用到了传说中的 lua 脚本。点击查看 lua 脚本 或者 中文文档

    if redis.call("get",KEYS[1]) == ARGV[1]
    then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    经过修改之后发现还是会存在一定问题,Redis 集群是 AP 模式,不像 Zookeeper 是 CP 模式,如下:

    在这里插入图片描述在这里插入图片描述

    而我们的 Redis 有点像 Eureka 集群,如下:

    在这里插入图片描述

    因为 Redis 之间通过异步方式通信,所以如果 master 宕机了,并且此时 master 还没有把锁状态同步给从节点,异步复制失败,导致锁丢失。此时上面写的代码又不能保证数据安全性了,所以这个时候只能采用 RedLock,天生解决这种问题,RedLock 红锁的具体实现是交给 Redisson 、并且 Redisson 还是 Java 语言编写的。

        @RequestMapping("/goods7")
        public String buyGoods() {
    
            RLock lock = redisson.getLock(REDIS_KEY);
            // 加锁
            lock.lock();
            try {
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 直接调用 unlock() 解锁即可
                lock.unlock();
            }
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    但是这里还是存在一个问题,就是解锁不能解锁别人的锁,需要判断能不能解锁才可以解锁,修改之后如下:

    
        @RequestMapping("/goods9")
        public String buyGoods() {
    
            // 直接上 Redisson
            RLock lock = redisson.getLock(REDIS_KEY);
            // 加锁
            lock.lock();
            try {
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
            } finally {
                // 先判断自己是否能去释放这把锁
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    // 直接调用 unlock() 解锁即可
                    lock.unlock();
                }
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    • 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

    经过压测后数据正常,如下:

    在这里插入图片描述

    但是最终发现还是会存在一个非常严重的问题,就是你的锁失效时间具体要怎么填写?你怎么知道要设置多长的过期时间,这个是要和你的处理业务时间联系再一起,通常会经过 Jmeter 工具测试 qps,然后看平均耗时,但是这个还不是一个好的解决方案。

    其实我们可以为这个过期时间续命,或者说是刷新过期时间,在你处理业务的时候,后台线程去刷新这个过期时间。

    下面先插个小插曲,先来了解下 Redisson 的东西

    Redisson 文档: http://redis.cn/topics/

    Redis 分布式锁:http://redis.cn/topics/distlock.html

    Github 地址: https://github.com/redisson/redisson

    传统的基于 setnx 分布式锁有什么缺点,如下图示?

    在这里插入图片描述

    在这里插入图片描述

    上述图片讲述了基于 setnx 传统的分布式锁的缺点,那么怎么解决呢?

    Redis 中就提供了 RedLock 算法,用来实现基于多个实例的分布式锁,锁由多个实例维护,这样即使你有实例发生故障,锁变量依旧还存在,客户端还可以继续完成锁操作。RedLock 算法是实现高可靠用分布式锁的一种有效解决方案,可以在实际开发中应用。

    在多主机群模式中,需要部署几台 Redis 服务器,由计算容错率公式推到:

    N(需部署 Redis 台数) = 2 * X(宕机的 Redis 台数) + 1(奇数+1),

    假设我么允许有 1 台机器宕机,那么最少部署 2 * 1+1 = 3 台 Redis 服务器就可以保证高可用集群
    假设我么允许有 2 台机器宕机,那么最少部署 2 * 1+1 = 5 台 Redis 服务器就可以保证高可用集群

    +1 的操作是用最少的开销做到高可用集群,+2 的话虽然也可以,但是你需要多准别一台服务器,实现的效果和 +1 实现的效果是一样的。

    采用 docker 部署三台 Redis 服务器

    docker 命令如下:

    docker run -p 6381:6379 --name redis-master-1 -d redis:6.0.7
    docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.7
    docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.7
    
    • 1
    • 2
    • 3

    配置类如下:

    @ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
    @Data
    public class RedisPropertiesAutoConfiguration {
    
        private int database;
    
        /**
         * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
         */
        private int timeout;
        private String password;
        private String mode;
    
        /**
         * 池配置
         */
        private RedisPoolProperties pool;
    
        /**
         * 单机信息配置
         */
        private RedisSingleProperties single;
    }
    
    @Data
    public class RedisPoolProperties {
    
        private int maxIdle;
        private int minIdle;
        private int maxActive;
        private int maxWait;
        private int connTimeout;
        private int soTimeout;
    
        /**
         * 池大小
         */
        private  int size;
    }
    
    @Data
    public class RedisSingleProperties {
        private  String address1;
        private  String address2;
        private  String address3;
    }
    
    • 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

    然后配置三个 Redisson 客户端,如下:

    
    @Configuration
    @EnableConfigurationProperties(RedisPropertiesAutoConfiguration.class)
    public class CacheConfiguration {
    
        @Autowired
        RedisPropertiesAutoConfiguration redisPropertiesAutoConfiguration;
    
        @Bean
        RedissonClient redissonClient1() {
            Config config = new Config();
            String node = redisPropertiesAutoConfiguration.getSingle().getAddress1();
            node = node.startsWith("redis://") ? node : "redis://" + node;
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(node)
                    .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                    .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                    .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
            if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
                serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
            }
            return Redisson.create(config);
        }
    
        @Bean
        RedissonClient redissonClient2() {
            Config config = new Config();
            String node = redisPropertiesAutoConfiguration.getSingle().getAddress2();
            node = node.startsWith("redis://") ? node : "redis://" + node;
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(node)
                    .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                    .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                    .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
            if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
                serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
            }
            return Redisson.create(config);
        }
    
        @Bean
        RedissonClient redissonClient3() {
            Config config = new Config();
            String node = redisPropertiesAutoConfiguration.getSingle().getAddress3();
            node = node.startsWith("redis://") ? node : "redis://" + node;
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(node)
                    .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                    .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                    .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
            if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
                serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
            }
            return Redisson.create(config);
        }
    
    • 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

    最终代码如下:

    
        @RequestMapping("/goods10")
        public String buyGoods() {
    
            // 直接上 Redisson
            RLock lock1 = redissonClient1.getLock(REDIS_KEY);
            RLock lock2 = redissonClient2.getLock(REDIS_KEY);
            RLock lock3 = redissonClient3.getLock(REDIS_KEY);
    
            RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
            boolean isLockBoolean;
            try {
                // 开始加锁
                isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
                 if (isLockBoolean) {
                     String result = stringRedisTemplate.opsForValue().get(KEY);
                     int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                     if (num > 0) {
                         int realNum = num - 1;
                         stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                         log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                         return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                     }
                     log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
                 }
            } catch (Exception e) {
                e.printStackTrace();
            }
            finally {
                // 先判断自己是否能去释放这把锁
                if (redLock.isLocked() && redLock.isHeldByCurrentThread()) {
                    // 直接调用 unlock() 解锁即可
                    redLock.unlock();
                }
            }
    
            return "活动已经售罄,欢迎下次光临"+serverPort;
        }
    
    
    • 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

    这样下面数据安全性保证已经提高到 99% 了。现在就剩下最后一个锁续命问题了。

    分布式锁在 Redis 中存的数据格式如下所示:

    在这里插入图片描述

    在 Redisson 的实现中,它会额外开一个线程定期检查线程是否还持有这把锁,如果有则延迟锁过期时间,定期检查默认是每 1/3 的锁时间检查一次,如果锁还持有,那么就会刷新过期时间。而这个额外的线程就叫做 Watch Dog

    摘取官网的一段话如下所示:

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    所以最后一个缓存锁续命问题 Redisson 也帮我们实现了,那么现在我们这个分布式锁应该算是比较完整的了。后续的 Redisson 源码分析请看另一篇文章。

    所以实现分布式锁优先推荐 Redisson 去实现,使用传统的 setnx 需要自己解决很多问题。

  • 相关阅读:
    .Net Core 3.1 解决数据大小限制
    [2023年度回顾总结]凡是过往,皆为序章
    Linux程序设计(上)
    HTML期末作业,基于html实现中国脸谱传统文化网站设计(5个页面)
    MySQL NDB Cluster 分布式架构搭建 自定义启动、重启和关闭集群Shell脚本
    用自己的fullpage模拟出字节校招的fullpage
    数组传参(一维数组、二维数组)
    架构道术-企业选择Dubbo作为分布式服务框架的10个理由
    创建型模式-原型模式(五)
    北斗导航 | RTD、RTK完好性之B值、VPL与HPL计算(附B值计算matlab源代码)
  • 原文地址:https://blog.csdn.net/qq_35971258/article/details/126847774