大家好啊,我是天天跟多线程、分布式 "死磕" 的后端博主~ 前几天帮同事查 BUG,发现他居然在分布式服务里用synchronized锁库存,结果并发一上来直接 "超卖",差点被产品追着打😂 这事儿让我意识到:很多同学对 "锁" 的认知还停留在单机阶段,一到分布式场景就掉坑。
今天咱们就从 "锁的前世今生" 说起,把分布式锁讲得明明白白 ------ 从区别到实现,再到 Redisson 怎么解决那些让人头大的 "过期、误删、重试" 问题,看完这篇,下次面试官问你分布式锁,你就能自信拿捏!
一、先搞懂:单机锁和分布式锁,根本不是一回事儿!
咱们先打个比方:
单机锁就像你家的房门钥匙------ 家里就你一个人用,钥匙放兜里,想开门就开,不用怕别人抢(单 JVM 进程内,线程间竞争锁,synchronized、ReentrantLock就能搞定)。
但如果是分布式系统,比如你做了个电商平台,部署了 3 台服务器(3 个 JVM),用户下单要扣库存。这时候 3 台机器里的线程都要改 "库存" 这个共享数据,相当于 "3 户人共用一个小区大门"------ 你家的钥匙(单机锁)肯定管不了另外两家啊!
这时候就需要分布式锁:它得是一把 "小区大门卡",3 台机器的线程都得凭这张卡开门,而且同一时间只能有一个线程拿到卡(保证互斥),还得防止 "卡丢了门一直锁着"(避免死锁)。
二、分布式锁有哪些实现方案?别瞎选!
市面上的分布式锁方案不少,但坑也多,咱们先排排雷:
方案 | 原理 | 优点 | 缺点 |
---|---|---|---|
数据库锁 | 用for update悲观锁 / 唯一索引 | 不用额外中间件 | 性能差!高并发下数据库扛不住 |
ZooKeeper | 基于临时有序节点 | 强一致性,自动释放锁 | 部署复杂,重连机制麻烦 |
Redis 锁 | 基于键值对的原子操作 | 轻量、高性能、易部署 | 需要自己处理过期、误删问题 |
显然,Redis 是后端最常用的选择------ 但千万别以为 "用 Redis 的 set 命令就能搞定",这里面的坑能让你调试到半夜!
三、Redis 的 set 命令:分布式锁的 "入门款",但要注意姿势
很多同学一开始会这么写:先用setnx key value(只有 key 不存在时才设置成功,保证互斥),再用expire key 10(给锁加过期时间,避免死锁)。
但这是错的! 因为setnx和expire是两步操作,中间如果服务器宕机,锁就没了过期时间,直接变成 "死锁"!
正确姿势是用Redis 的原子 set 命令:
sql
SET lock_key unique_value NX EX 10
拆解一下这几个参数的作用:
- NX:Only if the key does not exist(只有 key 不存在时才设置,保证同一时间只有一个线程拿到锁)
- EX:Set the expire time to seconds(设置过期时间,单位秒,避免死锁)
- unique_value:必须是唯一值!比如 "UUID + 线程 ID",后面解决 "误删问题" 全靠它
这行命令把 "加锁 + 设过期" 变成了一步原子操作,完美避免了 "加锁后宕机" 的死锁问题~ 但别急,这只是入门,还有两个大问题没解决:锁过期了怎么办?锁被别人误删了怎么办?
四、Redisson:分布式锁的 "真香工具",一键解决所有坑
手写 Redis 锁的同学,迟早会遇到这两个灵魂拷问:
- 我加了 10 秒过期锁,但业务逻辑要执行 20 秒,锁提前过期被别人抢了怎么办?
- 我执行完业务,要删锁的时候,怎么保证删的是自己的锁,不是别人的?
- 抢锁失败了,总不能直接返回吧?重试逻辑怎么写才优雅?
别慌!Redisson早就把这些问题封装好了,相当于给你一个 "现成的锁工具包",拿来就能用,还不用自己填坑~ 咱们逐个看它是怎么解决的。
1. 锁过期问题:"看门狗" 机制帮你续期
比如你给锁设了 30 秒过期,但业务逻辑要执行 1 分钟 ------ 这时候锁到期会被自动释放,别的线程就会抢锁,导致 "并发安全问题"。
Redisson 的解决办法是内置 "看门狗"(Watch Dog) :
- 当你用 Redisson 加锁时,如果没指定过期时间,它会默认给锁设 30 秒过期,同时启动一个 "看门狗线程"
- 这个线程会每隔 10 秒(30 秒的 1/3)检查一次:如果当前线程还持有锁(业务没执行完),就自动把锁的过期时间续到 30 秒
- 直到线程执行完业务,主动释放锁,"看门狗" 才会停止续期
相当于小区保安(看门狗)定期巡逻,看到你还在屋里(持有锁),就自动帮你把大门卡的有效期续上,再也不怕 "锁提前过期" 了!
代码示例(Spring Boot 中使用):
scss
// 获取Redisson客户端
RLock redissonLock = redissonClient.getLock("stock_lock");
try {
// 加锁:默认30秒过期,看门狗自动续期
redissonLock.lock();
// 执行业务:就算执行1分钟,锁也不会过期
updateStock();
} finally {
// 释放锁
redissonLock.unlock();
}
2. 锁误删问题:唯一标识 + Lua 脚本原子判断
假设这么个场景:
- 线程 A 加了 10 秒锁,但业务执行了 15 秒,锁到期自动释放
- 线程 B 趁机拿到锁,开始执行业务
- 这时候线程 A 的业务终于执行完了,它要释放锁 ------ 但此时的锁已经是线程 B 的了!如果 A 直接删锁,就会把 B 的锁误删,导致并发问题。
Redisson 怎么解决?给锁加唯一标识,释放前先判断:
- 加锁时,Redisson 会自动给锁值设为 "UUID: 线程 ID"(比如8f4d7b:1234),保证每个线程的锁值唯一
- 释放锁时,Redisson 不会直接删锁,而是先执行一段 Lua 脚本:
vbnet
-- 判断锁值是不是自己的,是才删除
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
- Lua 脚本是原子执行的,避免了 "判断 + 删除" 两步操作的中间空隙,完美防止 "误删别人的锁"
你不用自己写这段 Lua 脚本,Redisson 的unlock()方法已经帮你封装好了 ------ 调用就行,省心!
3. 抢锁失败重试问题:内置重试机制,不用自己写 while 循环
手写 Redis 锁时,抢锁失败了你得自己写 while 循环重试:
less
// 手写重试,麻烦又不优雅
while (!redisTemplate.opsForValue().setIfAbsent("lock", "value", 10, TimeUnit.SECONDS)) {
// 重试间隔:硬编码,不好维护
Thread.sleep(100);
}
Redisson 直接帮你搞定了重试逻辑,还支持配置重试次数和间隔:
java
// 抢锁失败后,最多重试3次,每次间隔500毫秒
boolean isLocked = redissonLock.tryLock(3, 500, TimeUnit.MILLISECONDS);
if (isLocked) {
try {
updateStock();
} finally {
redissonLock.unlock();
}
} else {
// 重试3次还没抢到锁,返回友好提示
return "当前下单人数过多,请稍后再试~";
}
甚至你还能自定义重试策略,比如 "指数退避重试"(重试间隔越来越长),Redisson 都支持 ------ 不用自己造轮子,香!
五、总结:分布式锁选 Redisson,准没错!
咱们回头捋一捋:
- 单机锁管不了分布式场景,得用分布式锁
- Redis 是分布式锁的主流选择,但手写会踩 "过期、误删、重试" 的坑
- Redisson 通过 "看门狗续期""唯一标识 + Lua 脚本""内置重试",把这些坑全填了,还封装得特别易用
最后给大家一个小建议:项目里别再手写 Redis 分布式锁了,直接集成 Redisson------ 几行代码搞定,还能少加班!
你们在项目中用分布式锁踩过哪些坑?比如 "锁没释放导致服务卡死""重试次数没调好导致性能差"?欢迎在评论区交流~ 觉得有用的话,点赞关注走一波,下次再跟大家扒更多后端干货!