redis分布式锁是最小可行解,因其通过set key value ex seconds nx原子抢锁、lua脚本校验value解锁、ttl按p99×2.5设定,满足任务不重跑、宕机可漂移、代码改动小三大核心需求。

不能靠 time.Ticker 或 cron.AddFunc 直接多实例部署,否则每个节点都执行一遍——这不是“分布式”,是“重复触发”。必须引入外部协调机制,核心是「全局唯一执行权」+「故障自动漂移」。
为什么 Redis 分布式锁是最小可行解
多数团队卡在“要不要上 etcd / XXL-JOB”,其实真正在意的就三件事:任务不重跑、宕机后能接上、代码改动小。Redis 满足全部:SET key value EX seconds NX 原子抢锁,value 写 os.Getenv("HOSTNAME") + "-" + strconv.Itoa(os.Getpid()),TTL 设为任务最长执行时间的 2–3 倍(比如任务最多跑 40s,TTL 至少设 120s)。
常见错误:
-
redis: nil:客户端未初始化或连接池耗尽,别在抢锁前才 dial - 用
DEL key直接删锁:可能误删别人持有的锁,必须走 Lua 脚本校验 value - TTL 设成固定 30s:任务偶尔卡住 35s 就触发并发,得按 P99 执行时长 × 2.5 来定
抢锁时机决定延迟和资源消耗
别让所有节点每秒都调 cron.Every(1 * time.Second) 然后争锁——Redis 连接打满、CPU 白耗、还容易被频控。真正稳的做法是「中心触发 + 多节点竞争」:
- 只部署一个轻量调度器(哪怕只是个固定 IP 的 Go 进程),按 cron 表达式算出下次触发时间戳,
LPUSH task:queue推送 JSON 消息 - Worker 节点用
BRPOP task:queue 1监听,抢到消息后立刻执行SETNX锁校验 - 避免用
Pub/Sub:Go 客户端默认断连不重试,消息丢了就没了;List可持久化、支持重试
删锁必须用 Lua,且不能 defer 无条件删
释放锁不是 rdb.Del(ctx, "task:send_report") 一行完事。得用原子脚本:
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
客户端传入当前节点的 value,返回 1 才算成功。更关键的是:别在函数开头 defer unlock()——如果根本没抢到锁,defer 会删掉别人刚 set 的值。
正确姿势:
- 抢锁成功后,记录当前 value 到局部变量
- 任务执行完或 panic recover 后,再用该 value 调 Lua 脚本删锁
- context 超时退出时,同样要校验 value 再删,不能跳过
幂等性设计不能只靠锁
锁只能防“同时执行”,挡不住网络分区、Redis 故障、进程 OOM 这些导致的“锁丢 + 任务重试”。所以业务层必须自己扛:
- INSERT 不用
INSERT IGNORE,改用INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或带WHERE NOT EXISTS的 UPSERT - 状态更新加条件:比如关订单,不是
UPDATE orders SET status = 'closed' WHERE id = ?,而是UPDATE orders SET status = 'closed' WHERE id = ? AND status = 'pending' - 任务元数据外置:把
task_id、run_at、exec_node存 Redis Hash 或数据库,方便排查谁在什么时候跑了什么
最易被忽略的点:cron 表达式解析默认用 time.Local,但 K8s pod 里没 TZ 环境变量,结果变成 UTC——你本地测试是凌晨 2 点跑,线上其实是早上 10 点。启动时务必 os.Setenv("TZ", "Asia/Shanghai") 并显式传 location 给解析器。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










