Redis进阶-细说分布式锁

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容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 Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
12天前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
48 16
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
61 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
49 1
|
1月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
80 4
|
1月前
|
缓存 NoSQL Ubuntu
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
56 3
|
1月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
|
机器学习/深度学习 缓存 NoSQL
|
缓存 NoSQL Java
为什么分布式一定要有redis?
1、为什么使用redis 分析:博主觉得在项目中使用redis,主要是从两个角度去考虑:性能和并发。当然,redis还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis。
1364 0
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
77 6