
这些 “锁失效” 的场景,你是不是也遇到过?
作为软件开发同行,你有没有过这样的经历:明明在代码里加了分布式锁,线上却还是出了幺蛾子 ——
上周有个朋友找我吐槽,他们电商项目做秒杀活动,为了防止库存超卖,专门加了 Redis 分布式锁,结果活动刚开始 10 分钟,就出现了 3 笔超卖订单,用户投诉不断,他加班到凌晨才勉强修复;还有个同事更无奈,写的分布式锁没思考 “重入性”,自己调用自己的接口时,锁直接把正常流程卡住,导致服务熔断,被领导批评了好几天。
实则不止他们,我在社群里做过小调查,大致有 60% 的开发同学,第一次用分布式锁都会踩坑 —— 要么是锁没锁住导致数据不一致,要么是锁太 “死” 导致性能瓶颈,甚至还有人由于锁的过期时间设置不当,引发了更严重的线上故障。今天咱们就好好聊聊,分布式锁到底该怎么设计,才能避开那些让人头疼的坑。
为什么分布式锁会 “坑” 住这么多开发者?
在聊解决方案之前,咱们先搞清楚一个问题:明明单机锁(列如 Java 的 synchronized、ReentrantLock)用得好好的,为什么到了分布式系统里,就必须用分布式锁?又为什么分布式锁这么容易出问题?
第一得明确分布式锁的核心作用 —— 当多个服务节点(列如微服务架构里的 A 服务、B 服务,或者同一服务的多个实例)同时操作同一份数据时,分布式锁能保证 “同一时间只有一个节点能操作”,避免出现数据不一致(列如超卖、超扣、数据重复插入)。而单机锁只能管自己所在的 “那一台机器”,管不了其他节点,所以在分布式架构里根本不够用。
但分布式锁的难点,恰恰在于 “分布式” 这三个字:
- 网络不确定性:Redis、ZooKeeper 这些实现分布式锁的中间件,和业务服务之间靠网络通信,一旦网络延迟、丢包,就可能出现 “锁没加上却以为加上了”“锁该释放却没释放” 的问题;
- 并发场景复杂:高并发下(列如秒杀、大促),大量请求同时抢锁,容易出现 “锁竞争”“死锁”,甚至把中间件压垮;
- 边界情况多:列如锁过期了业务还没执行完怎么办?释放锁时不小心把别人的锁删了怎么办?这些细节如果没思考到,锁就等于 “形同虚设”。
也正是由于这些难点,许多同学只学了分布式锁的 “基础用法”(列如 Redis 的 setNx),却没搞懂背后的 “坑”,结果一到线上就出问题。
3 个核心坑 + 对应的落地方案,附代码示例
结合我做分布式系统开发 5 年的经验,以及前面提到的 “朋友踩坑案例”,咱们把分布式锁最容易踩的 3 个坑拆解清楚,每个坑都给对应的解决方案,代码直接能用。
坑 1:只加基础锁,没防 “锁过期”,业务没跑完锁就没了
问题场景:有个同学用 Redis 实现分布式锁,设置了 10 秒过期时间,结果某次业务逻辑(列如调用第三方支付接口)由于网络慢,15 秒才执行完 —— 这时候锁已经自动释放了,其他请求趁机抢锁,导致同一笔订单被两次扣款。
解决方案:给锁加 “自动续期”(也叫 “看门狗” 机制),核心逻辑是:当业务没执行完,锁快过期时,自动延长锁的过期时间。
代码示例(Java + Redisson):
Redisson 是 Redis 官方推荐的分布式锁框架,自带 “看门狗” 机制,不用自己写续期逻辑:
// 1. 初始化Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 2. 获取分布式锁(锁的key要唯一,列如“order:lock:123”,123是订单ID)
RLock lock = redissonClient.getLock("order:lock:" + orderId);
try {
// 3. 加锁:等待时间3秒(没抢到锁就等3秒),自动释放时间30秒(默认看门狗续期)
boolean isLocked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (isLocked) {
// 4. 执行核心业务(列如扣库存、创建订单)
orderService.createOrder(orderId);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 5. 释放锁(必须在finally里,防止业务抛异常导致锁没释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
关键说明:Redisson 的 “看门狗” 会每隔 10 秒(默认是锁过期时间的 1/3)检查一次,如果业务还在执行,就把锁的过期时间延长到 30 秒,直到业务执行完释放锁。
坑 2:释放锁时 “误删别人的锁”,自己没锁却删了别人的锁
问题场景:有个同学用 Redis 的 “setNx + expire” 实现锁,释放锁时直接用 “del key”—— 结果某次业务执行慢,锁过期后被别人抢走,他的代码执行完,却把别人的锁删了,导致多个请求同时操作数据。
解决方案:给锁加 “唯一标识”(列如当前线程 ID、服务节点 IP),释放锁前先判断 “是不是自己的锁”,再删。
代码示例(Java + RedisTemplate):
如果不用 Redisson,自己实现时必须加唯一标识:
// 1. 定义锁的key和唯一标识(列如“线程ID+服务IP”)
String lockKey = "order:lock:" + orderId;
String lockValue = Thread.currentThread().getId() + ":" + InetAddress.getLocalHost().getHostAddress();
// 2. 加锁:setNx(不存在才加锁)+ expire(设置过期时间),必须原子操作
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(isLocked)) {
try {
// 执行核心业务
orderService.createOrder(orderId);
} finally {
// 3. 释放锁:先判断是不是自己的锁,再删除(用Lua脚本保证原子性)
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Integer.class),
Collections.singletonList(lockKey),
lockValue);
}
}
关键说明:用 Lua 脚本执行 “判断 + 删除”,是由于 Redis 执行 Lua 脚本是原子性的,能避免 “判断完是自己的锁,还没删,锁就过期被别人抢走” 的问题。
坑 3:没思考 “锁重入”,自己调用自己导致死锁
问题场景:有个同学写的接口 A 加了分布式锁,接口 A 又调用了接口 B,接口 B 也加了同一把锁 —— 结果接口 A 加锁后,调用接口 B 时,由于自己已经持有锁,却又要抢同一把锁,导致死锁。
解决方案:用 “可重入锁”,核心是记录 “当前锁的持有次数”,同一线程抢锁时,次数 + 1,释放时次数 – 1,次数为 0 时才真正删除锁。
代码示例(Java + Redisson):
Redisson 的 RLock 本身就是可重入锁,不用额外开发:
RLock lock = redissonClient.getLock("order:lock:" + orderId);
try {
// 第一次加锁:持有次数变成1
lock.tryLock(3, 30, TimeUnit.SECONDS);
orderService.createOrder(orderId); // 接口A的业务
callInterfaceB(orderId); // 调用接口B
} finally {
// 释放锁:持有次数先减1,减到0时删除锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
// 接口B的方法,同样加同一把锁
private void callInterfaceB(String orderId) {
RLock lock = redissonClient.getLock("order:lock:" + orderId);
try {
// 同一线程再次加锁:持有次数变成2,不会死锁
lock.tryLock(3, 30, TimeUnit.SECONDS);
orderService.updateOrderStatus(orderId); // 接口B的业务
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 释放后,持有次数变成1
}
}
}
关键说明:Redisson 内部用 “Hash 结构” 存储锁,key 是锁的名称,field 是线程唯一标识,value 是持有次数,所以能实现可重入。
总结呼吁:分布式锁不是 “加了就行”,要思考 3 个关键
今天咱们聊了分布式锁的 3 个核心坑,以及对应的解决方案 —— 实则本质上,分布式锁的设计不是 “能跑就行”,而是要思考 “边界情况”:
- 锁过期了业务没跑完?用 “看门狗” 自动续期;
- 释放锁时怕删错?加 “唯一标识”,用 Lua 脚本原子删除;
- 自己调用自己怕死锁?用 “可重入锁” 记录持有次数。
最后想跟大家说:作为软件开发人员,咱们写代码不仅要 “实现功能”,更要 “思考稳定性”—— 尤其是分布式系统,一个小小的锁设计不当,就可能导致线上故障,影响用户体验甚至造成损失。
如果你在项目里也踩过分布式锁的坑,或者有更好的实现方案,欢迎在评论区分享你的经历和思路;如果这篇文章帮你理清了思路,也别忘了点赞 + 收藏,下次遇到锁问题时,直接拿出来参考!