redis防死锁本质是设计规避:必须用set key value nx ex原子命令加锁,禁用分步setnx+expire;解锁须用lua脚本校验value后删除,避免误删;value需唯一(如uuid),ttl应略大于业务最大耗时但不宜过长。

Redis 本身不是“死锁防护器”,它不自动检测或解除死锁;所谓“防死锁”,本质是靠设计规避——比如强制设置过期时间、用 Lua 脚本保证加解锁原子性、避免手动 DEL 导致误删。Golang 微服务里拿 Redis 做分布式锁,关键不在“能不能锁”,而在“锁得稳不稳、放得准不准”。
为什么 setnx + expire 不够用?
常见错误写法:SETNX key 1 成功后再 EXPIRE key 30 —— 这两步非原子,中间若进程崩溃,key 就永久存在,形成“假死锁”。
正确做法必须一步到位:SET key value EX 30 NX(v2.6.12+ 支持)。Go 客户端如 github.com/redis/go-redis/v9 的 Set 方法可直接传 redis.SetOptions{NX: true, EX: 30}。
- 不要拆成两次 Redis 请求
- value 必须唯一(推荐用 UUID 或
os.Hostname() + pid + rand),否则解锁时无法验证所有权 - 过期时间(EX)要明显大于业务最大耗时,但不宜过大(比如设 10 分钟就容易堆积残留锁)
解锁为什么不能只用 DEL?
直接 DEL key 是最危险的操作:A 加了锁,B 在 A 还没执行完时超时释放,结果把 A 的锁删了 —— 这叫“误删”,破坏独占性。
必须校验 value 再删除,且整个过程需原子执行。标准解法是 Lua 脚本:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Go 中调用示例:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
script := redis.NewScript(luaScript)_, err := script.Run(ctx, rdb, []string{lockKey}, lockValue).Result()
- 脚本里用
GET对比 value,匹配才DEL,否则返回 0 - 不要自己拼字符串去 eval,用客户端提供的
Script封装更安全 - 如果脚本返回非 1,说明解锁失败(可能已过期或被别人覆盖),不应静默忽略
高并发下锁等待策略怎么选?
微服务里常见两种模式:TryLock(立即返回)和 Lock(阻塞重试)。选哪种取决于业务容忍度。
例如库存扣减,用户请求不能无限等待 —— 应用 TryLock,失败直接返回 “请稍后重试”;而定时任务调度则可接受短时阻塞,用带 backoff 的轮询重试。
- 轮询间隔别固定(比如都 sleep 10ms),要用指数退避:
time.Sleep(time.Millisecond * time.Duration(math.Pow(2, float64(attempt)))) - 总超时时间必须严格控制(比如最多等 500ms),否则拖垮整个请求链路
- 别在 for-select 里无限制 retry,一定要有 ctx.Done() 退出机制
Redlock 真有必要吗?
单节点 Redis 锁在大多数业务场景下足够用(比如订单创建、用户积分更新)。Redlock 适合对一致性要求极高的金融类场景,但它带来显著复杂度:
- 至少 3 个独立 Redis 实例(不能是主从,得是不同物理节点)
- 加锁耗时 = 多数节点响应时间 + 网络抖动,延迟上升明显
- 客户端需自行处理多数派判定、锁续期、故障剔除逻辑
- go-redis 官方不内置 Redlock,得引入
github.com/cenkalti/redlock-go等第三方库
真正容易被忽略的点是:锁 key 的命名空间。比如用 "order_lock:1001" 没问题,但若多个服务共用一个 Redis,又没加前缀,"lock:user123" 可能被另一个系统意外覆盖或误删 —— 所有 key 必须带 service prefix,比如 "svc_order:lock:user123"。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










