一、前言:你以为的"安全锁",可能漏洞百出
很多开发者认为,只要用 SET key value NX EX 加锁,再用 DEL key 解锁,就实现了"安全"的分布式锁。
但现实是:
- 在高并发下,锁被提前释放
- 业务逻辑被多个线程同时执行
- 甚至出现 "A 线程删除了 B 线程的锁"
根本原因 :忽略了分布式锁操作中的"原子性"缺失问题。
本文将带你深入理解:什么是分布式锁的原子性?哪些环节容易出错?如何真正实现原子安全?
二、什么是"原子性"?为什么它对分布式锁至关重要?
原子性(Atomicity):一个操作要么全部成功,要么全部失败,中间不可被中断或观察到中间状态。
在分布式锁中,以下两个操作必须保证原子性:
| 操作 | 非原子风险 |
|---|---|
| 加锁 + 设置过期时间 | 若只 SET key value,忘记设 TTL → 死锁 |
| 校验持有者 + 删除锁 | 若先 GET 再 DEL → 中间可能被篡改 |
⚠️ 一旦原子性被破坏,分布式锁的安全性将荡然无存!
三、常见非原子操作场景分析
场景 1️⃣:分步设置锁和过期时间(已过时,但仍有项目在用)
java
// 危险!非原子操作
redis.set("lock", "1");
redis.expire("lock", 30); // ← 如果这行没执行(如 JVM Crash),锁永不过期!
✅ 正确做法 :使用 SET key value NX EX seconds 原子命令(Redis 2.6.12+ 支持)
java
redis.set("lock", "unique_id", SetOption.SET_IF_ABSENT, Duration.ofSeconds(30));
✅ 这一步现代 Redis 客户端基本已解决。
场景 2️⃣:解锁时未原子校验持有者(最常见误删根源!)
java
// 错误示范:非原子解锁
String current = redis.get("lock");
if ("my_unique_id".equals(current)) {
redis.del("lock"); // ← GET 和 DEL 之间可能被其他线程插入!
}
竞态条件模拟:
| 时间 | 线程 A | 线程 B |
|---|---|---|
| T1 | GET lock → 返回 "A" |
--- |
| T2 | --- | 线程 A 的锁过期,B 获取锁(value="B") |
| T3 | DEL lock(以为是自己的) |
--- |
| T4 | 锁被 A 删除,但实际属于 B! | --- |
💥 结果:B 的锁被 A 误删,C 线程趁机进入 → 并发安全失效!
四、终极解决方案:Lua 脚本保证原子性
Redis 从 2.6 版本起支持 Lua 脚本 ,其执行具有原子性------整个脚本运行期间不会被其他命令打断。
✅ 安全解锁 Lua 脚本:
Lua
-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
📌 关键优势:
GET和DEL在 Redis 内部原子执行- 外部无法在中间插入任何操作
- 只有 value 匹配的持有者才能删除锁
五、生产级 Java 实现(Spring Boot)
1. 定义解锁脚本
java
@Component
public class SafeDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptText(
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
"return redis.call('DEL', KEYS[1]) " +
"else return 0 end"
);
UNLOCK_SCRIPT.setResultType(Long.class);
}
2. 安全加锁 & 解锁方法
java
/**
* 原子加锁(含过期时间)
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(result);
}
/**
* 原子解锁:仅当 value 匹配时才删除
*/
public void unlock(String lockKey, String requestId) {
redisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
requestId);
}
}
3. 业务使用示例
java
public void processOrder(String orderId) {
String lockKey = "lock:order:" + orderId;
String requestId = UUID.randomUUID().toString(); // 唯一标识!
try {
if (!lock.tryLock(lockKey, requestId, 30)) {
throw new RuntimeException("获取锁失败");
}
// 执行订单处理...
doProcess(orderId);
} finally {
lock.unlock(lockKey, requestId); // 安全释放
}
}
✅ 至此,加锁和解锁均满足原子性要求!
六、其他原子性陷阱与规避
陷阱 1:锁续期(Watchdog)非原子
- 续期操作
EXPIRE key本身是原子的 - 但"判断是否需要续期 + 执行 EXPIRE"不是原子的
- 解决方案:由独立线程定期续期(如 Redisson 的 Watchdog)
陷阱 2:可重入锁的计数更新
- 若实现可重入锁,需记录重入次数
INCR/DECR操作需与持有者校验结合- 推荐:直接使用 Redisson,避免重复造轮子
七、总结:分布式锁原子性三要素
要构建一个真正安全的分布式锁,必须确保以下操作的原子性:
| 操作 | 安全实现方式 |
|---|---|
| 加锁 + 设 TTL | SET key value NX EX seconds |
| 解锁前校验持有者 | Lua 脚本(GET + DEL 原子) |
| 锁续期 | 独立 Watchdog 线程(避免业务耦合) |
🔑 核心思想 :
任何涉及"读-改-写"的操作,在分布式环境下都必须原子化!
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!