Redis进阶-细说分布式锁

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis进阶-细说分布式锁


Pre

Redis Version : 5.0.3

Redis进阶-核心数据结构进阶实战 中我们讲 strings 数据结构的时候,举了一个例子

事实上,要实现一把相对完善的分布式锁,需要注意的细节还是蛮多的,这里我们好好的梳理一把。


我们先来看段代码

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
 if (stock > 0) {
   int realStock = stock - 1;
   stringRedisTemplate.opsForValue().set("stock", realStock + "");
  }

redis中提前存储了一个key stock , value为 100

上述代码有问题吗?

是不是我们熟悉的超卖问题?

为啥会超卖? 假设同时有两个线程都执行到了 int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); , 比如都取到了stock为 100 , 然后继续执行后面的业务逻辑,到最后将扣减后的值set到redis中,应该剩98吧, 事实上呢? 你库存里的值是 99个… 卖到最后,是不是卖多了? 。。。。

那怎么办呢? 没有分布式经验的童鞋,可能会说 加把锁啊 云云

加锁后 变成了啥呢?

synchronized(this){
 int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
 if (stock > 0) {
   int realStock = stock - 1;
   stringRedisTemplate.opsForValue().set("stock", realStock + "");
  }
}

那 这样的代码还有问题吗?

  1. 性能问题
  2. 更为重要的是,如果你的应用是集群模式,好比 你有N个tomcat, 用户通过NG地址访问,你想想你的这个JVM级别的锁 ,还有啥用,一样会超卖…

这个时候你需要一把分布式锁,这里我们讨论的是如何使用redis实现分布式锁


分布式锁演进 V1

来, 上代码

String key = "STOCK_LOCK";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key,"ARTISAN_LOCK");
        if (!result){ // 如果未获取到锁,直接返回
            return "1001";
        }
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock + "");
        }
        stringRedisTemplate.delete(key);
        return "扣减成功";

我们来分析下, stringRedisTemplate.opsForValue().setIfAbsent(key,"ARTISAN_LOCK"); 这行代码就保证了只有一个线程能set成功 (redis 的工作线程是单线程的嘛 ), setIfAbsent 不存在才设置,如果有一个线程设置成功了,在这个线程未释放之前,其他线程是无法set成功的,所以其他线程返回false,直接return了。


分布式锁演进 V2

那这个代码严谨吗? ---------> 有的同学说,你这个中间要是出异常了,没有执行 stringRedisTemplate.delete(key);,那岂不是这把锁释放不了了,死锁了呀? 要不try catch finally ?

那代码变成如下

那,这样就完美了吗? 抛出异常的场景我们是处理了,在finally里释放。


分布式锁演进 V3

那假设在运行的过程中,还没有执行到finally , 这个时候tomcat挂了,但是锁已经set到redis里了 咋办? --------》 有的同学说, 简单啊 加个超时时间呗。

那还有问题吗? ----------》如果宕机时间发生在

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key,"ARTISAN_LOCK");
stringRedisTemplate.expire(key,5000,TimeUnit.SECONDS);

这两行代码之间,有怎么办? … 不会这么巧吧 …但理论上是存在的

继续聊


分布式锁演进 V4

本质上: 要把set key和 设置过期时间 搞成一个原子命令 .

低版本的Redis,你可能需要lua脚本,但是现在Redis提供了setnx 命令, spring也帮我们封装好了

最关键的一行代码

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key,"ARTISAN_LOCK",10,TimeUnit.SECONDS);

代码就变成了

对于一般的应用,并发不是很高,这个也足够用了,因为简单啊

但是如果在高并发下,那还有问题吗? 这样就满足所有场景了吗 ?

我们在设置key的时候,给key设置的过期时间是 10秒 ,也就说 10秒后,这个key会被redis给删除掉, 假设你的这个业务执行了15秒才执行完。当前业务还未执行结束,第二个线程的请求已经过来了,它也能加锁成功。 第二个线程继续执行,执行了5秒,你的第一个线程也执行完了,最后一步 删除key , 那第一个线程就把第二个线程加的锁给删掉了啊。。。。。

删了别的线程加的锁,并发一高,你这个锁就没啥用了哇。。。所以 还有另外一个原则: 加锁和解锁必须是同一个线程 .


分布式锁演进 V5

加锁和解锁必须是同一个线程 . 实现的话也简单,value 不写死,写成一个线程ID或者随机数等等 都行,删除key的时候,比较下,相等的话才删除

根据V4存在的问题,我们来看下代码

那有的童鞋会问,如果 在finally 中 执行到if 挂了。。。并没有执行delete咋办? 理论上是有可能发生的, 其实也不要紧,我们set key的时候,设置了一个超时时间, 那最多锁10秒嘛 ,不会死锁。 也能接受。

如果你非得要想改这个地方,把查询和delete弄成一个原子命令,lua脚本就排上用场了。

这里我们不展开了。

到这里,一把相对完善的锁,就OK了。

关于到底设置多长的过期时间合适, 这个不好讲了, 1秒中是长是短 ,1分钟呢? 要权衡一下。 那有没有更好的办法呢?


终极版-分布式锁演进(Redisson ) V6

针对v5中存在的问题, 虽然解决了 加锁和解锁都是同一个线程, 但是还是有点小bug , 比如 你给key设置了过期时间为10秒, 但你的方法执行了15秒,方法还没执行完,锁已经被redis干掉了。。。另外一个线程就可以拿到锁,继续干活了。 多个线程同时执行,还是有潜在的bug出现。

超时的问题,你设置多长时间都不合适…

真的要彻底解决,咋弄呢? -------》 可不可以给锁续命? 没执行完就给锁延期呗。 说起来简单,实现起来有点复杂了。。。

简单来说,后台弄个定时任务,检测这个锁是否存在,存在的话延长时间,不存在的话就是被删掉了,不考虑即可。

好在Redisson提供了这个牛逼的功能。

Code

@Bean
    public Redisson redisson() {
        // 此为单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.18.130:6379").
                setConnectionMinimumIdleSize(10).setDatabase(0);
        /*config.useClusterServers()
                .addNodeAddress("redis://192.168.0.61:8001")
                .addNodeAddress("redis://192.168.0.62:8002")
                .addNodeAddress("redis://192.168.0.63:8003")
                .addNodeAddress("redis://192.168.0.61:8004")
                .addNodeAddress("redis://192.168.0.62:8005")
                .addNodeAddress("redis://192.168.0.63:8006");*/
        return (Redisson) Redisson.create(config);
    }
@RequestMapping("/deduct_stock")
    public String deductStock() throws InterruptedException {
        String lockKey = "STOCK_LOCK";
        // 获取锁
        RLock redissonLock = redisson.getLock(lockKey);
        try {
            // 加锁,实现锁续命功能
            redissonLock.lock();
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock + "");
            }
        }finally {
            // 释放锁
            redissonLock.unlock();
        }
        return "扣减成功";
    }

总结一下 三部曲

  1. 第一步:获取锁 RLock redissonLock = redisson.getLock(lockKey);
  2. 第二步: 加锁,实现锁续命功能 redissonLock.lock();
  3. 第三步:释放锁 redissonLock.unlock();

Redisson分布式锁实现原理


源码分析

Redis进阶- Redisson分布式锁实现原理及源码解析


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
NoSQL 算法 安全
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
Redlock 算法-主从redis分布式锁主节点宕机锁丢失的问题
155 0
|
1月前
|
NoSQL 关系型数据库 MySQL
分布式锁(redis/mysql)
分布式锁(redis/mysql)
64 1
|
1月前
|
NoSQL Java Redis
如何通俗易懂的理解Redis分布式锁
在多线程并发的情况下,我们如何保证一个代码块在同一时间只能由一个线程访问呢?
39 2
|
3天前
|
存储 NoSQL Java
基于Redis实现分布式锁
基于Redis实现分布式锁
27 0
|
4天前
|
NoSQL Java Redis
Redis入门到通关之分布式锁Rediision
Redis入门到通关之分布式锁Rediision
|
4天前
|
NoSQL 关系型数据库 MySQL
Redis入门到通关之Redis实现分布式锁
Redis入门到通关之Redis实现分布式锁
|
1月前
|
人工智能 监控 NoSQL
【万字长文 一文搞定】Redis:从新手村到大师殿堂的奥德赛之旅 9种实现分布式锁的全技术指南
【万字长文 一文搞定】Redis:从新手村到大师殿堂的奥德赛之旅 9种实现分布式锁的全技术指南
83 4
|
1月前
|
消息中间件 存储 NoSQL
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
|
NoSQL Java 关系型数据库
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁
|
存储 canal 缓存