分布式锁的原子性问题

一、前言:你以为的"安全锁",可能漏洞百出

很多开发者认为,只要用 SET key value NX EX 加锁,再用 DEL key 解锁,就实现了"安全"的分布式锁。

但现实是:

  • 在高并发下,锁被提前释放
  • 业务逻辑被多个线程同时执行
  • 甚至出现 "A 线程删除了 B 线程的锁"

根本原因忽略了分布式锁操作中的"原子性"缺失问题。

本文将带你深入理解:什么是分布式锁的原子性?哪些环节容易出错?如何真正实现原子安全?


二、什么是"原子性"?为什么它对分布式锁至关重要?

原子性(Atomicity):一个操作要么全部成功,要么全部失败,中间不可被中断或观察到中间状态。

在分布式锁中,以下两个操作必须保证原子性:

操作 非原子风险
加锁 + 设置过期时间 若只 SET key value,忘记设 TTL → 死锁
校验持有者 + 删除锁 若先 GETDEL → 中间可能被篡改

⚠️ 一旦原子性被破坏,分布式锁的安全性将荡然无存!


三、常见非原子操作场景分析

场景 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

📌 关键优势

  • GETDEL 在 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 线程(避免业务耦合)

🔑 核心思想
任何涉及"读-改-写"的操作,在分布式环境下都必须原子化!


八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
ai_xiaogui7 小时前
【开源前瞻】从“咸鱼”到“超级个体”:谈谈 Panelai 分布式子服务器管理系统的设计架构与 UI 演进
服务器·分布式·架构·分布式架构·panelai·开源面板·ai工具开发
凯子坚持 c7 小时前
如何基于 CANN 原生能力,构建一个支持 QoS 感知的 LLM 推理调度器
分布式
飞升不如收破烂~8 小时前
Redis 分布式锁+接口幂等性使用+当下流行的限流方案「落地实操」+用户连续点击两下按钮的解决方案自用总结
数据库·redis·分布式
无心水8 小时前
分布式定时任务与SELECT FOR UPDATE:从致命陷阱到优雅解决方案(实战案例+架构演进)
服务器·人工智能·分布式·后端·spring·架构·wpf
Lansonli8 小时前
大数据Spark(八十):Action行动算子fold和aggregate使用案例
大数据·分布式·spark
invicinble9 小时前
对于分布式的原子能力
分布式
心态还需努力呀19 小时前
CANN仓库通信库:分布式训练的梯度压缩技术
分布式·cann
Coder_Boy_1 天前
基于SpringAI的在线考试系统-相关技术栈(分布式场景下事件机制)
java·spring boot·分布式·ddd