【Redis八股|第8篇】Redis 分布式锁原理与 Redisson 使用
前言
前面我们学习了 Redis 的数据类型、缓存问题、过期策略、内存淘汰和持久化。
这一篇进入 Redis 在项目实战中非常常见的场景:分布式锁。
在单体项目中,如果多个线程同时修改同一份资源,可以使用 Java 本地锁,比如:
synchronizedReentrantLock但是在分布式项目中,服务可能部署多台机器。
比如:
订单服务实例 A 订单服务实例 B 订单服务实例 C如果多个实例同时处理同一个订单,本地锁就不够用了。
这时候就需要分布式锁。
Redis 是实现分布式锁的常见方案之一。
这一篇我们就从为什么需要分布式锁、Redis 如何实现分布式锁、手写锁有哪些问题,以及 Redisson 怎么用这几个角度来完整梳理。
一、为什么需要分布式锁?
先看一个库存扣减场景。
假设商品库存只有 1 件。
用户 A 和用户 B 同时下单。
如果没有控制,可能发生:
用户 A 查询库存:1 用户 B 查询库存:1 用户 A 扣减库存:库存变成 0 用户 B 扣减库存:库存变成 -1这就出现了超卖。
在单体项目中,我们可以使用:
synchronized比如:
publicsynchronizedvoidcreateOrder(LongproductId){// 查询库存// 扣减库存// 创建订单}但是如果项目部署了多个实例:
实例 A 有自己的 JVM 实例 B 有自己的 JVMsynchronized只能锁住当前 JVM 内部的线程,锁不住其他机器上的线程。
所以分布式环境需要一个所有实例都能访问到的锁。
Redis 就可以作为这个共享锁中心。
二、什么是分布式锁?
分布式锁可以理解为:
在分布式系统中,让多个服务实例争抢同一把锁,只有拿到锁的实例才能执行关键业务。
比如多个订单服务实例都访问 Redis:
实例 A 尝试加锁 lock:product:1001 成功 实例 B 尝试加锁 lock:product:1001 失败 实例 C 尝试加锁 lock:product:1001 失败只有实例 A 可以执行扣减库存。
执行完后,实例 A 删除锁。
然后其他实例才有机会获取锁。
三、Redis 实现分布式锁的核心命令
Redis 实现分布式锁的核心命令是:
setlock:product:1001 value nx ex30这里:
| 参数 | 含义 |
|---|---|
lock:product:1001 | 锁 key |
value | 锁 value |
nx | key 不存在时才设置成功 |
ex 30 | 30 秒后自动过期 |
这条命令的意思是:
只有锁不存在时才能加锁成功,并且锁会在 30 秒后自动释放。
为什么要用nx?
因为锁只能被一个线程或实例持有。
为什么要设置过期时间?
防止服务宕机后锁永远不释放。
四、手写 Redis 分布式锁示例
简单代码:
publicvoidcreateOrder(LongproductId){StringlockKey="lock:product:"+productId;Booleansuccess=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"1",30,TimeUnit.SECONDS);if(!Boolean.TRUE.equals(success)){thrownewRuntimeException("系统繁忙,请稍后再试");}try{// 查询库存// 判断库存是否充足// 扣减库存// 创建订单}finally{stringRedisTemplate.delete(lockKey);}}逻辑是:
- 先尝试加锁
- 加锁失败,说明有其他线程正在处理
- 加锁成功,执行核心业务
- 最后释放锁
这个版本可以表达基本思路,但还不够安全。
五、手写锁的问题一:误删别人的锁
假设锁过期时间是 30 秒。
线程 A 获取锁后,业务执行了 40 秒。
0 秒:线程 A 加锁成功,锁 30 秒过期 30 秒:锁自动过期 31 秒:线程 B 加锁成功 40 秒:线程 A 执行完 finally,删除锁这时线程 A 删除的其实是线程 B 的锁。
这就是误删锁问题。
六、解决误删锁:value 使用唯一标识
加锁时,value 不要写死成"1",而应该使用唯一标识。
比如:
Stringvalue=UUID.randomUUID().toString();加锁:
Booleansuccess=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,value,30,TimeUnit.SECONDS);释放锁时,先判断 value 是否是自己的:
StringredisValue=stringRedisTemplate.opsForValue().get(lockKey);if(value.equals(redisValue)){stringRedisTemplate.delete(lockKey);}这样只有锁 value 和自己保存的一致时,才删除。
七、手写锁的问题二:判断和删除不是原子操作
上面的释放锁代码还有问题:
StringredisValue=stringRedisTemplate.opsForValue().get(lockKey);if(value.equals(redisValue)){stringRedisTemplate.delete(lockKey);}这里分成两步:
- 查询锁 value
- 删除锁
这两个操作不是原子的。
可能出现:
线程 A 查询 value,发现是自己的 锁刚好过期 线程 B 获取锁 线程 A 执行 delete,删除了线程 B 的锁所以释放锁需要保证:
判断 value + 删除 key是一个原子操作。
通常可以用 Lua 脚本实现。
八、Lua 脚本释放锁
Lua 脚本可以让 Redis 原子执行多条命令。
释放锁脚本:
ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end含义:
如果锁 key 的 value 等于当前线程的 value 就删除锁 否则不删除Java 中执行 Lua 脚本可以保证判断和删除在 Redis 中原子完成。
这样就避免了误删其他线程锁的问题。
九、手写锁的问题三:锁过期时间不好设置
锁必须设置过期时间。
但过期时间设置多少合适?
如果设置太短:
业务还没执行完,锁先过期了会导致并发问题。
如果设置太长:
服务宕机后,其他线程要等很久才能重新获取锁体验不好。
这就是手写 Redis 锁比较麻烦的地方。
需要有一种机制:
如果业务还没执行完,自动给锁续期。
这就是 Redisson 的看门狗机制。
十、Redisson 是什么?
Redisson 是一个 Redis 客户端框架,它基于 Redis 实现了很多分布式工具。
比如:
- 分布式锁
- 分布式集合
- 分布式计数器
- 分布式信号量
- 延迟队列
我们最常用的就是 Redisson 分布式锁。
它帮我们处理了很多手写锁容易出错的问题,比如:
- 加锁
- 解锁
- 锁续期
- 可重入
- 误删锁
- 等待锁
所以实际项目中,比起自己手写 Redis 分布式锁,更推荐使用 Redisson。
十一、Spring Boot 引入 Redisson
添加依赖:
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.27.2</version></dependency>配置 Redis 地址:
spring:redis:host:localhostport:6379不同项目版本配置可能略有差异,具体以项目 Spring Boot 和 Redisson 版本为准。
十二、Redisson 分布式锁使用示例
@AutowiredprivateRedissonClientredissonClient;publicvoidcreateOrder(LongproductId){RLocklock=redissonClient.getLock("lock:product:"+productId);booleansuccess=false;try{success=lock.tryLock(3,30,TimeUnit.SECONDS);if(!success){thrownewRuntimeException("系统繁忙,请稍后再试");}// 查询库存// 判断库存是否充足// 扣减库存// 创建订单}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException("获取锁失败");}finally{if(success&&lock.isHeldByCurrentThread()){lock.unlock();}}}这里:
tryLock(3,30,TimeUnit.SECONDS)表示:
最多等待 3 秒获取锁 获取成功后锁 30 秒自动过期释放锁:
lock.unlock();isHeldByCurrentThread()用来判断当前线程是否持有这把锁,避免误解锁。
十三、Redisson 看门狗机制
Redisson 有一个很重要的机制:看门狗。
如果加锁时没有手动指定过期时间,Redisson 默认会给锁设置一个过期时间,比如 30 秒。
如果业务还没执行完,Redisson 会自动给锁续期。
比如:
lock.lock();Redisson 会自动续期,直到业务执行完并调用:
lock.unlock();这就避免了业务执行时间太长导致锁自动过期的问题。
不过如果你使用:
lock.tryLock(3,30,TimeUnit.SECONDS)手动指定了锁过期时间,通常就不会触发自动续期机制。
所以实际使用时要理解这点。
十四、Redisson 可重入锁
Redisson 的RLock是可重入锁。
可重入的意思是:
同一个线程已经持有锁后,可以再次获取同一把锁。
比如:
lock.lock();try{methodA();}finally{lock.unlock();}如果methodA()内部又尝试获取同一把锁,同一个线程可以再次获取成功。
这和 Java 中的ReentrantLock思想类似。
十五、分布式锁使用注意事项
1. 锁粒度不要太大
锁 key 应该尽量精确。
比如扣减商品库存:
lock:product:1001不要直接使用:
lock:product如果所有商品共用一把锁,会导致并发能力很差。
2. 锁内逻辑要尽量短
加锁后执行的业务越长,锁持有时间越长,其他请求等待越久。
所以锁内只放必要的核心逻辑。
3. 一定要释放锁
使用try-finally保证释放锁。
try{// 业务逻辑}finally{lock.unlock();}4. 注意锁和事务的关系
如果业务中有数据库事务,要注意锁释放时机。
有时候锁释放了,但数据库事务还没提交,其他线程读到旧数据,可能出现问题。
这种情况要结合事务提交时机设计。
十六、Redis 分布式锁能保证绝对安全吗?
严格来说,Redis 分布式锁并不是所有场景下都绝对安全。
比如 Redis 主从切换时,可能出现锁还没同步到从节点,主节点宕机,从节点升级为主节点,导致其他客户端又获取到锁。
所以在极高一致性要求场景中,需要更严谨的方案。
但在很多普通业务场景下,Redisson 分布式锁已经能满足需求。
如果是金融级强一致业务,可能要结合数据库锁、事务、唯一索引、消息队列等方案综合处理。
十七、面试标准回答
如果面试官问:
Redis 分布式锁怎么实现?有什么问题?Redisson 做了什么?
可以这样回答:
Redis 分布式锁的核心是使用set key value nx ex seconds命令。nx保证只有 key 不存在时才能加锁成功,ex设置过期时间,防止服务宕机后锁永远不释放。
释放锁时不能直接删除 key,因为可能误删别人的锁。一般会给锁设置唯一 value,释放时判断 value 是否一致,一致才删除。判断和删除必须保证原子性,通常用 Lua 脚本实现。
手写 Redis 锁还会遇到锁过期时间不好设置的问题。如果业务执行时间超过锁过期时间,锁可能提前释放。Redisson 提供了更完善的分布式锁实现,支持可重入、自动续期、Lua 脚本解锁等能力。它的看门狗机制可以在业务未执行完时自动给锁续期。
实际项目中,不推荐自己手写复杂分布式锁,通常使用 Redisson。
十八、常见问题总结
1. Redis 分布式锁为什么要设置过期时间?
防止服务宕机后锁无法释放,导致死锁。
2. 为什么 value 要唯一?
为了防止释放锁时误删别人的锁。
3. 为什么解锁要用 Lua 脚本?
因为判断 value 和删除 key 必须是原子操作。
4. Redisson 看门狗是什么?
看门狗会在业务未执行完时自动给锁续期,避免锁提前过期。
5. Redisson 锁是可重入的吗?
是的,RLock支持可重入。
6. 分布式锁可以完全替代数据库事务吗?
不能。
分布式锁解决并发互斥问题,事务解决数据一致性提交和回滚问题,它们关注点不同。
十九、实际开发建议
使用 Redis 分布式锁时,可以注意下面几点:
- 优先使用 Redisson,不要轻易手写复杂锁
- 锁 key 要有业务含义
- 锁粒度要尽量小
- 锁内业务逻辑要尽量短
- 一定用
try-finally释放锁 - 解锁前确认当前线程持有锁
- 注意锁过期时间和业务执行时间
- 强一致场景不要只依赖 Redis 锁
- 数据库唯一索引、事务、乐观锁也可以配合使用
- 高并发核心链路要压测验证
二十、总结
这一篇主要学习了 Redis 分布式锁原理和 Redisson 使用。
分布式锁是为了解决多服务实例同时操作共享资源的问题。Redis 实现分布式锁的核心命令是set key value nx ex seconds。
手写 Redis 锁需要注意误删锁、原子释放锁、锁过期时间、服务宕机等问题。释放锁时需要唯一 value,并使用 Lua 脚本保证判断和删除的原子性。
实际项目中更推荐使用 Redisson。Redisson 提供了可重入锁、自动续期、线程安全释放锁等能力,比手写 Redis 锁更可靠。
下一篇我们继续学习 Redis 主从复制、哨兵和集群。
