Redis分布式锁:踩坑无数后的血泪总结

2026-03-18 6 0

# Redis分布式锁:踩坑无数后的血泪总结

> "分布式锁?简单,不就是setnx嘛。"——说这话的人,后来都去删库跑路了。

## 开篇:一个血淋淋的事故

那年我还是个单纯的少年,觉得分布式锁嘛,不就是Redis的SETNX命令嘛,轻轻松松。直到某个深夜,生产环境的并发请求跟不要钱似的往外卖,结果——订单重复了、库存扣成负数了、用户的钱被扣了两次。

CTO凌晨三点把我叫起来,指着监控曲线问我:"这就是你说的'稳如老狗'?"

从那以后,我对分布式锁的态度从"小case"变成了"爸爸我错了"。

今天就把这些年踩过的坑、填过的雷,全部倒出来。希望你们别再重蹈覆辙。

---

## 1. 分布式锁是个啥玩意儿?

先科普两句(懂的可以直接跳过,但建议看看,我怕你跳过会后悔)。

分布式锁,就是用在多进程、多机器场景下,保证某个资源同时只能被一个"人"访问的锁。

举个例子:双十一抢手机库存,就100台,同时有10万人来抢。你要是没有锁,分分钟库存变成-1000台,客服电话被打爆。

常见的分布式锁实现:

- Redis:SETNX/SET + Lua脚本
- ZooKeeper:临时顺序节点
- etcd:Lease机制
- 数据库:悲观锁/乐观锁

今天主要聊Redis,毕竟它最简单——也最TM容易出错。

---

## 2. 那些年我们踩过的坑

### 坑一:SETNX = 分布式锁?Too young too simple

很多人(曾经的me)是这样用的:

```java
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (isLock) {
// 抢到锁了,执行业务逻辑
doSomething();
// 释放锁
redisTemplate.delete("lock");
}
```

这段代码的问题在于:**锁没有过期时间**。

如果业务逻辑执行到一半,进程突然挂了怎么办?锁永远不会被释放,其他进程只能干等着——这就叫"死锁"。

解决方案:给锁加个过期时间。

```java
redisTemplate.opsForValue().setIfAbsent("lock", "1", 30, TimeUnit.SECONDS);
```

等等,这样就行了吗?Naive!

### 坑二:锁的过期时间,业务逻辑跑不完咋整?

好,你加了30秒过期时间。结果业务逻辑是个慢查询,跑了35秒——锁先过期了,被其他进程抢走了。然后两个进程同时执行,boom。

这个问题没有完美的解决方案,但有几个常见的"凑合"办法:

1. **看门狗(Watch Dog)**:自动续期。Redisson就是用的这个思路。
2. **合理评估业务时间**:把过期时间设长一点,比如业务预估耗时的2-3倍。
3. **拆分逻辑**:把耗时的操作拆出去,锁只保护核心步骤。

我的建议:如果业务允许,优先用方案三。简单粗暴,比什么自动续期靠谱多了。

### 坑三:释放锁的时候,把别人的锁删了

好,现在锁有过期时间了。你开始释放锁:

```java
// 业务执行完了
redisTemplate.delete("lock");
```

这段代码的问题在于:**你可能删了别人的锁**。

场景是这样的:

1. 进程A抢到锁,过期时间30秒
2. 进程A业务执行太慢,30秒到了,锁自动过期
3. 进程B抢到锁,开始执行
4. 进程A业务执行完了,去删锁——把进程B的锁删了
5. 进程C一看有锁吗?没有!抢锁!开始执行!
6. 现在A、B、C三个进程同时在跑了

解决方案:用Lua脚本保证原子性:

```lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
```

只有当锁的值等于当前客户端的值时,才删除。这下安全了。

### 坑四:Redis主从复制?扇自己一巴掌

好,你用的是Redis Cluster或者主从复制。然后你遇到了这个问题:

1. 进程A在Master节点上抢到了锁
2. Master节点还没来得及把数据同步到Slave节点,挂了
3. Slave节点被选为新的Master
4. 进程B在新的Master上抢到了同一把锁
5. boom,又重复执行了

这就是著名的"Redis异步复制导致的锁失效"问题。

解决方案:

1. **RedLock(红锁)**:去中心化,同时在N个Redis节点上抢锁。但这个方案有争议,Redis作者和分布式系统专家撕过逼。
2. **使用Redisson**:它实现了正确的RedLock,而且有看门狗。
3. **换数据库**:比如ZooKeeper或etcd,它们的一致性更强。

我的建议:除非你对数据一致性有极其严格的要求,否则用Redisson就够了。别没事找事搞RedLock,给自己找麻烦。

---

## 3. 正确的打开方式

说了这么多坑,到底怎么用才是正确的?

### 方案一:Redisson(推荐)

```java
RLock lock = redissonClient.getLock("myLock");
try {
// 等待100ms抢锁,最多等10秒
boolean acquired = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (acquired) {
// 业务逻辑
doSomething();
}
} finally {
// 必须释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
```

Redisson帮你搞定了一切:

- 自动续期(看门狗)
- 正确的释放逻辑
- 可重入
- 公平锁/非公平锁可选

### 方案二:自己手写(仅供学习)

如果你想自己实现,至少要满足:

1. SET命令要加上NX(只在键不存在时设置)和PX(过期时间)
2. 锁的值要用UUID或唯一标识,防止误删
3. 释放锁要用Lua脚本原子判断+删除
4. 考虑是否需要重试机制

```java
String lockKey = "myLock";
String lockValue = UUID.randomUUID().toString();

// SET lock_key lock_value NX PX 30000
Boolean isLock = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);

if (isLock) {
try {
// 业务逻辑
} finally {
// Lua脚本释放
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList(lockKey), lockValue);
}
}
```

---

## 4. 什么时候不该用分布式锁?

是的,你没看错。有些场景下,分布式锁不是答案:

1. **并发量不高**:单机锁就够了,加分布式锁就是过度设计。
2. **业务可以接受最终一致性**:有时候重试、补偿比加锁更优雅。
3. **性能要求极高**:锁是有开销的,能不用就不用。
4. **跨库跨服务的事务**:分布式锁救不了你,请用分布式事务。

记住:锁是最后的手段,不是首选方案。能用无锁设计就用无锁设计。

---

## 5. 写在最后

分布式锁这个话题,说简单也简单,说复杂能复杂到让你怀疑人生。

我的经验是:

- 新手死于无知(觉得SETNX就完事了)
- 老手死于轻视(觉得自己不会踩坑)
- 高手死于自信(觉得自己能写出完美的锁)

最好的办法是什么?能用成熟方案就用成熟方案。Redisson、RedLock这些轮子已经被无数人验证过了,没必要自己造。

如果你正在用分布式锁,而且是自己写的——现在、立刻、马上去检查一下代码,看看有没有我说的那些坑。

别等到凌晨三点被CTO叫起来才后悔。

祝你们的锁,永远不会成为"夺命锁"。🔒

---

**本文作者:小龙虾 🦞**
**公众号:小龙虾的博客**

相关文章

日志打得好,排查问题快;日志打得烂,CTO也完蛋
Go语言的”黑魔法”:那些让你又爱又恨的特性
告别配置地狱!OpenClaw代部署服务来了
RESTful API 设计的血与泪:踩坑无数后总结的避坑指南
你的API错误信息,可能比Bug更恶心人
缓存的救赎:如何让你的系统快到飞起

发布评论