草莓熊Lotso头像
关注
【Redis 进阶】分布式锁深度演进:从 SETNX 到 Redlock,一步步写对工业级分布式锁封面图

【Redis 进阶】分布式锁深度演进:从 SETNX 到 Redlock,一步步写对工业级分布式锁

在这里插入图片描述

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!

🎬 博主简介:

在这里插入图片描述


前言:

在分布式系统开发中,并发资源控制是绕不开的核心问题。单机时代,我们用 C++ 的 std::mutex、Java 的 synchronized 就能搞定进程内的线程安全;但当业务部署到多个服务器节点上,每个节点都是独立进程,共享同一份业务资源时,进程内锁就完全失效了。超卖、重复创建、资源竞争,各类并发问题随之而来。Redis 凭借单线程原子性、高性能、接入简单的特点,成了实现分布式锁最主流的方案。但很多开发者实现分布式锁,往往停留在「setnx 加锁、del 解锁」的最简版本,线上要么出现死锁、要么误解锁、甚至并发场景下互斥完全失效,坑非常多。本文就从最基础的版本开始,一步步迭代优化,拆解每个版本的问题与解法,覆盖过期时间、身份校验、Lua 原子解锁、看门狗续约、Redlock 高可用方案,帮你把分布式锁从「能用」写到「工业级可靠」。


一. 什么是分布式锁

1.1 单机锁的局限

我们熟悉的互斥锁,不管是 C++ 的 std::mutex 还是 Java 的 synchronized,本质都是进程内锁:只能保证同一个进程内部多个线程的执行顺序,出了这个进程就管不到了。

而分布式系统里,多个服务器节点跑在不同机器上,是完全独立的进程,共同操作同一个共享资源(比如车票库存、订单状态)。这时候单机锁完全失去作用,就会出现经典的并发问题。

最典型的就是车票超卖: 多个售票节点都执行「查询余票 → 大于 0 则扣减」的逻辑。没有全局互斥的话,两个节点同时查到剩余 1 张票,同时执行扣减,一张票就卖给了两个人。

1.2 分布式锁的核心思路

既然没有全局的共享内存,那就找一个全局共享的第三方系统,用一个标记来表示锁的状态,所有节点都来抢这个标记,抢到的才能操作资源。这就是分布式锁的本质。

Redis 是非常合适的载体:

  • 单线程模型,命令串行执行,天然保证原子性;
  • 纯内存操作,性能极高,锁操作开销极小;
  • 提供丰富的字符串、过期时间等原语,方便组装锁逻辑。

当然实现分布式锁不止 Redis 一种,Zookeeper、MySQL 也可以做,但 Redis 因为性能高、接入无侵入,是业务场景中最常用的方案。


二. 分布式锁的六步演进

分布式锁的发展史,本质就是不断解决问题的迭代史。我们从最简版本开始,一步步补全能力。

2.1 初版:SETNX 基础互斥

最朴素的实现,基于 SETNX(SET if Not eXists)命令:key 不存在时才能设置成功,key 已经存在则失败。

  • 加锁:以资源名称为 key,尝试设置,成功代表拿到锁;
  • 解锁:业务执行完毕,删除 key 释放锁。

C++ 风格伪代码:

// 加锁:成功返回 true,失败返回 false
bool try_lock(const std::string& lock_key) {
    // setnx 返回 1 表示设置成功,0 表示已存在
    return redis.setnx(lock_key, "1") == 1;
}

// 解锁
void unlock(const std::string& lock_key) {
    redis.del(lock_key);
}

致命缺陷:如果拿到锁的服务器突然崩溃、机器掉电,代码走不到解锁逻辑,这个 key 就永久留在 Redis 中,形成死锁。后续所有节点都拿不到锁,相关业务直接瘫痪。

2.2 第二版:引入过期时间,解决死锁

针对死锁,最直接的思路是给锁加一个过期时间,就算没人主动删除,时间到了系统自动删。

⚠️ 这里有一个高频踩坑点:绝对不能把设置值和设置过期分成两条命令写。

// ❌ 错误写法:两条命令不原子,中间崩溃依然会死锁
redis.setnx(lock_key, "1");
// 如果这里服务器崩了,expire 没执行,锁永久存在
redis.expire(lock_key, std::chrono::seconds(10));

✅ 正确做法:使用原子的 SET 命令,一条指令同时完成「设置值 + 不存在才设置 + 过期时间」。

// ✅ 正确写法:原子加锁
bool try_lock(const std::string& lock_key, 
              const std::chrono::seconds& ttl) {
    // NX 表示不存在才设置,EX 表示设置过期秒数
    return redis.set(lock_key, "1", ttl, 
                    sw::redis::UpdateType::NOT_EXIST);
}

对应 Redis 命令:SET key value NX EX seconds

到这里解决了死锁问题,但很快又出现了新的问题。

2.3 第三版:加入身份校验,防止误解锁

问题场景:解锁操作是可以被任何人调用的。 比如服务器 A 拿到了锁,因为代码 bug、或者逻辑异常,服务器 B 也执行了解锁,直接把别人的锁删掉了。 更隐蔽的场景:锁已经自动过期释放了,A 还以为自己持有锁,这时候 B 成功拿到锁,A 接着执行解锁,直接把 B 刚拿到的锁删了。 没有校验的解锁,是非常严重的安全隐患。

解决方案:锁的 value 不存固定值,存入唯一身份标识,比如服务器 UUID + 线程 ID。解锁的时候先校验:这个锁是不是自己加的,是自己的才删。

// 加锁时存入本机唯一标识
bool try_lock(const std::string& lock_key,
              const std::string& worker_id,
              const std::chrono::seconds& ttl) {
    return redis.set(lock_key, worker_id, ttl, 
                    sw::redis::UpdateType::NOT_EXIST);
}

// 解锁时先校验身份
void unlock(const std::string& lock_key,
            const std::string& worker_id) {
    auto val = redis.get(lock_key);
    if (val.has_value() && val.value() == worker_id) {
        redis.del(lock_key);
    }
}

遗留问题:GET 和 DEL 是两条独立命令,中间不是原子的。 极端竞态条件下:A 刚 GET 完确认是自己的锁,正要执行 DEL 的瞬间,锁正好到期,B 成功加了锁,然后 A 的 DEL 才执行,直接把 B 刚拿到的锁删掉了。 问题根源还是:校验和删除不是原子操作。

2.4 第四版:Lua 脚本实现原子解锁

这是工业级分布式锁的标准方案。 Redis 支持内嵌 Lua 脚本,并且整个 Lua 脚本的执行是原子的:Redis 会把脚本当成一条命令串行执行,中间不会插入任何其他客户端的请求。官方也明确说明,Lua 脚本是 Redis 事务的更好替代方案。

把「校验身份 + 删除锁」的逻辑写到一个 Lua 脚本里,就能保证整个解锁操作原子完成。

解锁 Lua 脚本:

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

逻辑非常简洁:取出 key 对应的值,和传入的工作节点 ID 对比,一致就删除锁,否则返回 0 表示解锁失败。 C++ 的 redis-plus-plus、Java 的 Jedis 等主流客户端,都支持直接加载并执行 Lua 脚本,传入 key 和参数即可。

到这一步,常规业务场景的分布式锁就已经比较可靠了:不会死锁、不会误删别人的锁、解锁操作原子安全。

2.5 第五版:看门狗动态续约

还有一个经典的两难问题:过期时间到底设多久?

  • 设太短:业务逻辑还没执行完,锁就过期释放了,后续操作失去互斥保护;
  • 设太长:服务器崩溃后,锁要很久才会自动释放,这段时间资源一直被占用,影响可用性。

业务执行时长本身就是不确定的,固定值永远调不到完美。 更好的方案是动态续约 + 看门狗线程,这也是工业级实现的标配:

  1. 加锁时先设置一个较短的初始过期时间,比如 1 秒;
  2. 后台启动一个 看门狗(watchdog) 线程,定期巡检(比如剩 300ms 时),任务还没完成就把过期时间重新续上 1 秒;
  3. 只要任务在运行,锁就会被持续续约,不会中途释放;
  4. 如果服务器崩溃,看门狗线程也跟着挂掉,没人续约,锁在短时间内就会自动过期释放。

相当于「少量多次」的思路,用一个后台线程的极小开销,同时解决了「业务没做完锁就没了」和「崩溃后锁释放慢」两个问题。

2.6 第六版:Redlock 红锁,解决单点故障

前面的方案都基于单个 Redis 节点。很多人会想:配个主从 + 哨兵不就高可用了? 但锁场景有个隐蔽的问题:主从同步存在延迟。加锁命令刚到主节点,还没同步到从节点,主节点就挂了,从节点升级成新主,这个锁数据就丢了。这时候就可能出现两个客户端同时拿到锁,互斥直接失效。

对于锁可靠性要求极高的场景,Redis 作者提出了 Redlock(红锁)算法,核心是多数派冗余。

工作原理:

  1. 部署 N 个完全独立的 Redis 主节点(通常取奇数,比如 5 个),互相之间没有主从关系;
  2. 客户端依次向每个节点尝试加锁;
  3. 如果超过半数的节点加锁成功(5 个节点至少成功 3 个),且总耗时小于锁过期时间,整体判定为加锁成功;
  4. 解锁时,向所有节点都发送解锁命令。

容错能力:少数节点挂掉完全不影响,只要多数节点存活,锁的状态就正确可靠。 当然代价也更高:需要部署多套独立 Redis 实例,资源成本上升。一般业务场景,主从 + 哨兵架构其实足够,只有对一致性要求极高的场景才需要 Redlock。

源码视角:设计思想拆解

站在 C/C++ 系统编程的视角看,分布式锁的每一步演进,都对应着经典的工程权衡思路。

  1. 原子性是分布式锁的根基

分布式环境没有共享内存,没有进程级互斥原语,所有互斥都必须依赖第三方存储系统的原子能力。 Redis 单线程串行执行所有命令,是它能做分布式锁的核心基础。无论是 SETNX 还是 Lua 脚本,本质都是利用「单线程不会插队」的特性,保证操作边界的原子性。

  1. Lua 为什么优于事务

Redis 事务也能打包命令,但事务只能批量顺序执行,不能做条件分支判断。而解锁逻辑是典型的「先校验再执行」的分支逻辑,事务根本做不了。 Lua 脚本相当于把业务逻辑下发到服务端执行,并且全程原子,表达能力比事务强得多。这也是官方更推荐用 Lua 替代事务做复杂逻辑的原因。

  1. 看门狗的工程哲学

看门狗本质是用后台巡检线程,换取时间维度的弹性。 这种模式非常通用:TCP 保活定时器、服务心跳、租约续约,本质都是同一个思路。用非常小的额外开销,解决了「固定超时永远调不准」的难题。

  1. Redlock 的共识思想

Redlock 用的是最朴素的多数派共识:分布式系统里单个节点永远不可靠,用冗余换可靠。 不需要复杂的一致性算法,只要超过半数节点达成一致,整体状态就有效。这也是很多分布式系统最基础的容错思路。

扩展:更多锁类型

上文讲的是最基础的互斥锁,基于 Redis 还可以实现更复杂的锁类型,也是面试高频考点:

  1. 读写锁:读共享、写互斥。分别维护读计数和写锁,读操作累加计数,写操作完全互斥。
  2. 公平锁:严格按请求顺序获取锁,先来先得。一般结合队列 + 通知机制实现。
  3. 可重入锁:同一个线程可以多次加同一把锁,内部维护引用计数,加锁计数 +1,解锁计数 -1,到 0 才真正释放。

这些都是在基础互斥锁的思路上扩展出来的,核心原理依然是 Redis 的原子原语。

核心考点总结

  1. 本质定位:分布式锁解决多节点下的全局资源互斥问题,Redis 基于单线程原子性成为最主流实现方案。
  2. 演进路线与对应坑点
    1. SETNX 初版:逻辑最简单,但服务器崩溃会造成死锁;
    2. 过期时间:解决死锁,必须使用 SET NX EX 原子命令,禁止 SETNX + EXPIRE 分开写;
    3. 身份校验:防止误删他人的锁,但 GET + DEL 非原子仍存在竞态风险;
    4. Lua 脚本解锁:原子完成校验 + 删除,是工业级标准实现;
    5. 看门狗续约:动态续期,同时解决业务未执行完和故障释放慢的问题;
    6. Redlock:多独立节点多数派共识,解决单节点故障导致的锁失效。
  3. 核心原则:涉及判断 + 操作的多步逻辑,必须保证原子性,这是分布式锁正确性的根基。
  4. 方案选型:一般业务主从 + 哨兵足够,极高可靠性场景使用 Redlock。

结语:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:分布式锁看起来简单,一个 setnx 就能跑起来,但真要做到生产环境可靠,中间要踩的坑非常多。从死锁到误解锁,从非原子操作到单点故障,每一步优化都是针对真实的线上问题。其实技术演进大多都是这个规律:先做一个能用的最简版本,遇到一个问题解决一个问题,一步步迭代到工业级。理解了每一步为什么这么做,而不是只记住最终答案,遇到线上问题才能快速定位根因,也才能根据自己的业务场景,选出最合适的方案,而不是盲目上最复杂的。

✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど

在这里插入图片描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2503_91389547/article/details/166645695

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--