news 2026/6/14 0:15:52

Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

分布式系统中,多个服务实例常常需要竞争同一资源——比如扣减库存、分配订单号、更新公共配置。如果没有协调机制,并发操作就会导致数据错乱。这时候就需要分布式锁:一把所有实例都认可的“钥匙”,谁拿到谁才能操作资源,操作完再还回去。

Redis 是实现分布式锁最流行的工具之一。但它并非一把SETNX就万事大吉——死锁、误删、锁过期、主从切换导致的锁丢失……这些都是生产环境里的真实大坑。本文从单实例的简单锁开始,一步步演化到生产级的 Redlock 算法,用 Python 把每个坑都踩一遍,再把它填平。

1. 分布式锁的本质与基本要求

一把合格的分布式锁需要满足:

  • 互斥性:同一时刻只有一个客户端持有锁。

  • 防死锁:即使持有锁的客户端崩溃,锁也能被释放(通过过期时间)。

  • 解铃还须系铃人:只有持锁的客户端才能释放锁,不能误删别人的锁。

  • 高可用:锁服务本身不能是单点,否则服务宕机锁就全部失效。

  • 容错性:在部分节点故障时仍能正确加锁、解锁。

Redis 单线程、高性能、自带过期,天然适合做锁。但要满足上述全部要求,需要经过精心设计。

2. 第一代:SETNX —— 朴素的起点

最原始的想法:SETNX(SET if Not eXists),键不存在则设置成功,表示获取锁;键已存在则失败,表示锁被别人占用。释放锁就是DEL键。

# 终端 A127.0.0.1:6379>SETNX lock:order:1001 A(integer)1# 加锁成功# 终端 B(同时)127.0.0.1:6379>SETNX lock:order:1001 B(integer)0# 加锁失败,锁已被 A 持有

Python 版本:

importredis r=redis.Redis(host='localhost',port=6379,decode_responses=True)def acquire_lock_v1(lock_name, holder_id):"""第一代:SETNX 加锁"""returnr.setnx(lock_name, holder_id)def release_lock_v1(lock_name):"""第一代:直接 DEL 释放""" r.delete(lock_name)# 使用ifacquire_lock_v1('lock:order:1001','client_A'): print('A 获取锁成功')# 执行业务...release_lock_v1('lock:order:1001')else: print('A 获取锁失败')

致命问题

  1. 死锁:如果客户端 A 在DEL之前崩溃,锁永远无法释放。其他客户端永远拿不到锁。

  2. 误删:客户端 B 无法区分锁是谁的,可能直接DEL掉 A 持有的锁。

3. 第二代:加过期时间 + 持有者标识

3.1 防止死锁——给锁加 TTL

SET key value EX seconds NX(Redis 2.6.12+)一条命令完成加锁并设置过期:

def acquire_lock_v2(lock_name, holder_id,expire=30):"""第二代:加锁带过期时间"""returnr.set(lock_name, holder_id,nx=True,ex=expire)# 使用ifacquire_lock_v2('lock:order:1001','client_A',expire=30): print('A 获取锁成功,30 秒后自动释放')

即使客户端崩溃,锁也会在 30 秒后自动释放,不会死锁。

3.2 防止误删——释放时校验持有者

释放前先检查锁的值是否与自己的holder_id匹配。但“检查 + 删除”两步不是原子的,需要 Lua 脚本:

release_lock_lua="""ifredis.call("GET", KEYS[1])==ARGV[1]thenreturnredis.call("DEL", KEYS[1])elsereturn0end""" release_lock=r.register_script(release_lock_lua)def acquire_and_release(): lock_name='lock:order:1001'holder='client_A'ifr.set(lock_name, holder,nx=True,ex=30): print(f'{holder} 获取锁成功')# 业务逻辑...result=release_lock(keys=[lock_name],args=[holder])print(f'释放结果: {result}')# 1 表示成功删除else: print('获取锁失败')acquire_and_release()

还有问题吗?有。如果业务执行时间超过了锁的过期时间(30 秒),锁自动释放了,其他客户端拿到锁,原客户端还在操作,就破坏了互斥性。这就需要锁续期

4. 第三代:锁续期(Watchdog)

如果业务还在执行,锁快过期了,就自动延长过期时间。这就是Watchdog(看门狗)机制。Redisson(Java)中就有经典的看门狗实现。

我们在 Python 中用一个后台线程来实现:

importthreadingimporttimeimportuuid class RedisLockWithWatchdog:"""带看门狗的分布式锁""" def __init__(self, redis_client, lock_name,expire=30,renew_interval=None): self.redis=redis_client self.lock_name=lock_name self.holder=str(uuid.uuid4())self.expire=expire self.renew_interval=renew_interval or max(expire //3,1)# 每 expire/3 秒续期self._renew_thread=None self._stop_renew=threading.Event()def acquire(self):"""获取锁"""ifself.redis.set(self.lock_name, self.holder,nx=True,ex=self.expire): self._start_watchdog()returnTruereturnFalse def _start_watchdog(self):"""启动看门狗线程,定期续期""" self._stop_renew.clear()self._renew_thread=threading.Thread(target=self._watchdog_loop,daemon=True)self._renew_thread.start()def _watchdog_loop(self):"""续期循环""" renew_script=self.redis.register_script("""ifredis.call("GET", KEYS[1])==ARGV[1]thenreturnredis.call("EXPIRE", KEYS[1], ARGV[2])elsereturn0end""")whilenot self._stop_renew.wait(self.renew_interval): result=renew_script(keys=[self.lock_name],args=[self.holder, self.expire])ifresult: print(f'[看门狗] 续期成功,延长 {self.expire}s')else: print('[看门狗] 锁已不属于自己,停止续期')breakdef release(self):"""释放锁""" self._stop_renew.set()ifself._renew_thread: self._renew_thread.join(timeout=2)release_script=self.redis.register_script("""ifredis.call("GET", KEYS[1])==ARGV[1]thenreturnredis.call("DEL", KEYS[1])elsereturn0end""")returnrelease_script(keys=[self.lock_name],args=[self.holder])def __enter__(self):ifself.acquire():returnself raise Exception('获取锁失败')def __exit__(self, exc_type, exc_val, exc_tb): self.release()# 使用示例r=redis.Redis(host='localhost',port=6379,decode_responses=True)def process_order(): try: with RedisLockWithWatchdog(r,'lock:order:1001',expire=10)as lock: print('获取锁成功,开始处理订单...')time.sleep(15)# 模拟长业务,超过 expireprint('订单处理完成')except Exception as e: print(e)process_order()

输出:

获取锁成功,开始处理订单...[看门狗]续期成功,延长 10s[看门狗]续期成功,延长 10s 订单处理完成

即使业务执行了 15 秒(超过初始的 10 秒 TTL),锁也没有过期,因为看门狗在第 3 秒和第 6 秒续期了。

5. 第四代:Redlock —— 分布式锁的巅峰

5.1 单实例锁的致命缺陷

即使加了 Watchdog,上述方案仍有一个根本性缺陷:锁存储在单个 Redis 实例上,一旦这个实例宕机,锁就丢失了

如果使用主从架构,主节点宕机后从节点提升,但从节点可能还没同步到锁的数据,导致锁丢失,两个客户端同时认为拿到了锁。

Redlock是 Redis 作者 antirez 提出的算法,在多个独立的 Redis 节点上同时加锁,只要超过半数节点加锁成功,才认为获取锁成功。

5.2 Redlock 算法流程

  1. 获取当前时间戳(毫秒)。

  2. 依次向 N 个 Redis 节点(建议 5 个)请求加锁,使用相同的keyvalue和 TTL。

  3. 客户端设置一个超时时间(远小于锁的 TTL),如果某个节点没及时响应,则立即尝试下一个节点。

  4. 统计加锁成功的节点数。如果成功数 ≥ N/2 + 1(即大多数),且总耗时 < 锁的 TTL,则认为获取锁成功。

  5. 锁的有效时间 = 锁的 TTL - 总耗时。

  6. 如果加锁失败(未达到大多数或耗时过长),则向所有节点发送释放锁请求。

客户端 │ ├── 加锁请求 ──>Redis-1 ✅ ├── 加锁请求 ──>Redis-2 ✅ ├── 加锁请求 ──>Redis-3 ✅ ├── 加锁请求 ──>Redis-4 ❌(超时)└── 加锁请求 ──>Redis-5 ✅ │4/5 成功>半数 → 获取锁成功

5.3 Python Redlock 实现

我们使用redlock-py库(也可手写,这里展示原理):

importredlockimporttime# 配置 5 个独立的 Redis 节点(生产环境需要不同机器)# 这里演示:可在不同端口启动多个 Redisnodes=[{'host':'localhost','port':6379,'db':0},{'host':'localhost','port':6380,'db':0},{'host':'localhost','port':6381,'db':0},{'host':'localhost','port':6382,'db':0},{'host':'localhost','port':6383,'db':0},]# 创建 Redlock 实例dlm=redlock.Redlock(nodes,retry_count=3,retry_delay=0.2)# 加锁lock_name='lock:critical:task'my_lock=None try:# 尝试获取锁,TTL=10000ms (10s)my_lock=dlm.lock(lock_name,10000)ifmy_lock: print(f'获取 Redlock 成功! 锁值: {my_lock.resource}')# 执行业务...time.sleep(2)else: print('获取 Redlock 失败')except redlock.MultipleRedlockException as e: print(f'Redlock 异常: {e}')finally:ifmy_lock: dlm.unlock(my_lock)print('锁已释放')

手写 Redlock 核心逻辑(理解原理):

importuuidimporttimeimportredis class SimpleRedlock:"""简化版 Redlock 实现""" def __init__(self, nodes,retry_count=3,retry_delay=0.2):""" nodes:[{'host':'localhost','port':6379,'db':0},...]""" self.nodes=[redis.Redis(host=n['host'],port=n['port'],db=n.get('db',0),decode_responses=True,socket_timeout=0.5)forninnodes]self.quorum=len(self.nodes)//2+1self.retry_count=retry_count self.retry_delay=retry_delay def lock(self, resource,ttl_ms=10000):"""获取分布式锁""" value=str(uuid.uuid4())forattemptinrange(self.retry_count +1): start_time=int(time.time()*1000)success_count=0# 向所有节点尝试加锁fornodeinself.nodes: try:ifnode.set(resource, value,nx=True,px=ttl_ms): success_count+=1except Exception as e: print(f'节点 {node} 加锁异常: {e}')elapsed=int(time.time()*1000)- start_time# 加锁成功条件:多数节点成功,且耗时未超 TTLifsuccess_count>=self.quorum and elapsed<ttl_ms:return{'value':value,'validity':ttl_ms - elapsed# 剩余有效时间}# 加锁失败,释放已成功的节点self._unlock_all(resource, value)# 等待后重试ifattempt<self.retry_count: time.sleep(self.retry_delay *(attempt +1))# 退避returnNone def unlock(self, resource, value):"""释放锁""" self._unlock_all(resource, value)def _unlock_all(self, resource, value):"""向所有节点发送释放锁请求""" unlock_script="""ifredis.call("GET", KEYS[1])==ARGV[1]thenreturnredis.call("DEL", KEYS[1])elsereturn0end"""fornodeinself.nodes: try: node.eval(unlock_script,1, resource, value)except Exception: pass# 忽略释放失败(节点可能已宕机)# 使用 SimpleRedlock# 需要先启动 5 个 Redis 实例(不同端口)# 为演示,这里假设已存在redlock=SimpleRedlock(nodes,retry_count=2)lock_info=redlock.lock('lock:critical:task',ttl_ms=5000)iflock_info: print(f'获取锁成功,剩余有效时间: {lock_info["validity"]}ms')# 业务处理...time.sleep(1)redlock.unlock('lock:critical:task', lock_info['value'])print('锁已释放')else: print('获取锁失败')

6. Redlock 争议与最佳实践

Redlock 并非毫无争议。分布式系统专家 Martin Kleppmann 曾撰文指出 Redlock 存在安全性问题。核心争议在于:时钟跳跃GC 停顿可能导致锁失效。

  • 时钟跳跃:如果某个节点的系统时钟被 NTP 调整,可能导致锁提前过期。

  • GC 停顿:客户端在 GC(垃圾回收)时暂停,锁过期了业务还在执行。

反方建议:使用ZooKeeperetcd这类基于共识算法的强一致性存储来实现锁。

实际业界选择

  • 大多数互联网公司仍广泛使用 Redis 做分布式锁(简单、高性能、已有基础设施)。

  • 对一致性要求极高的场景(金融交易、订单状态机),建议使用 ZooKeeper/etcd。

  • 折中方案:使用redlock-py库并配合单调递增的 fencing token,每次获取锁返回一个递增的 token,资源服务可以通过比对 token 来拒绝过期的锁。

7. 动手试试

  1. Watchdog 模拟:设置锁过期 5 秒,业务睡眠 12 秒,观察看门狗续期效果。打印续期次数和最终锁释放状态。

  2. 并发竞争:启动 10 个线程同时竞争一个锁,统计获取成功的次数(应为 1),验证互斥性。

  3. Redlock 节点故障:用 Docker 启动 5 个 Redis,运行 SimpleRedlock,手动停掉其中 2 个节点,验证仍然可以加锁(3/5 > 半数)。

  4. 锁误删防护:用两个不同holder_id尝试释放对方的锁,验证 Lua 脚本正确拦截。

预期效果:看门狗延长锁 TTL;10 个线程只有一个获取锁成功;半数节点存活时 Redlock 仍能正常工作;误删锁被拒绝。

8. 总结

我们从一个简单的SETNX出发,一步步打造了一把生产级分布式锁:

分布式锁没有银弹,关键是根据业务场景选择合适的方案。对 90% 的场景,SET NX EX + Lua 释放 + Watchdog已经足够。对跨数据中心、金融级一致性要求的场景,再考虑 Redlock 或 ZooKeeper。

下一篇,我们将迎来 Redis 5.0 的重磅特性——Redis Stream,用它构建生产级的可靠消息队列。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/14 0:10:01

Photoshop纹理压缩神器:Intel Texture Works插件终极指南

Photoshop纹理压缩神器&#xff1a;Intel Texture Works插件终极指南 【免费下载链接】Intel-Texture-Works-Plugin Intel has extended Photoshop* to take advantage of the latest image compression methods (BCn/DXT) via plugin. The purpose of this plugin is to provi…

作者头像 李华
网站建设 2026/6/14 0:08:57

嵌入式LCD图片取模工具:支持JPG/BMP转C数组,带Qt源码可直接编译

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;Image2Lcd是专为嵌入式开发设计的图片转LCD数组工具&#xff0c;能将JPG、BMP等常见格式图片一键转换成适配单色、灰度、彩色LCD屏幕的C语言数组或二进制数据。工具提供水平/垂直扫描方向选择、图像缩放、灰度级…

作者头像 李华
网站建设 2026/6/14 0:08:01

103、视频连续 AF:Touch Tracking、目标跟踪与平滑过渡的工程实践

103、视频连续 AF:Touch Tracking、目标跟踪与平滑过渡的工程实践 一、从一次“追焦翻车”说起 去年Q2,我接手一个旗舰机项目,视频模式下用户点击屏幕锁定对焦目标后,只要被摄物体稍微加速移动,画面就开始“抽搐”——对焦马达像抽风一样来回拉风箱,偶尔还出现焦点突然跳…

作者头像 李华
网站建设 2026/6/14 0:07:02

SAP ABAP开发:别再硬编码了!用FI_PERIOD_CHECK函数优雅处理财务账期校验

SAP ABAP开发实战&#xff1a;FI_PERIOD_CHECK函数在财务账期校验中的高阶应用财务账期校验是SAP系统中至关重要的控制环节&#xff0c;直接关系到财务数据的准确性和合规性。许多ABAP开发者习惯直接查询T001B表进行账期判断&#xff0c;这种方式虽然直观&#xff0c;但存在诸多…

作者头像 李华
网站建设 2026/6/14 0:02:59

文件加密软件有哪些?硬核的文件加密软件排行榜前 5 公开推荐

企业文件泄露这事&#xff0c;真不是吓你。一份客户名单、一套技术图纸流出去&#xff0c;竞争对手直接抄底&#xff0c;半年白干不说&#xff0c;客户信任直接归零。所以文件加密软件现在是刚需&#xff0c;但市面上产品一大堆&#xff0c;到底选哪个&#xff1f;今天直接给大…

作者头像 李华