用 redis 分布式锁可解决多 go 进程定时任务重复执行问题,核心是通过 set key value ex seconds nx 原子命令实现唯一执行权控制,value 必须为唯一标识,过期时间设为任务最长执行时间的 2–3 倍,并用 lua 脚本安全删锁。

用 Redis 分布式锁控制任务唯一执行
多个 Go 进程同时拉起定时任务时,cron 本身不带集群协调能力,直接跑就会重复触发。核心解法不是改调度逻辑,而是加一层“谁有资格执行”的判断——用 Redis 实现轻量级分布式锁最常用也最可控。
常见错误现象:redis: nil(连接未初始化)、ERR value is not an integer or out of range(SET 命令参数错位)、锁没设过期时间导致死锁。
- 用
SET key value EX seconds NX原子写入,value 必须是唯一标识(比如主机名+pid),方便后续校验和清理 - 过期时间(EX)建议设为任务最长执行时间的 2–3 倍,太短可能任务还没完锁就丢了,太长会拖慢故障恢复
- 不要用
DEL key直接删锁——得先EVAL脚本比对 value 再删,否则可能误删别人持有的锁 - Go 里推荐用
github.com/go-redis/redis/v9,它的SetNX+SetEX组合不如原生命令原子,老老实实用Do调SET
// 示例:获取锁
ok, err := rdb.Do(ctx, "SET", "task:send_report", os.Getenv("HOSTNAME")+"-"+strconv.Itoa(os.Getpid()), "EX", "300", "NX").Bool()
if err != nil || !ok {
return // 没抢到锁,跳过本次执行
}
选对触发时机:避免 cron + 锁双重延迟
很多人把 cron 和分布式锁叠在一起,结果发现任务总在预期时间之后几秒甚至几十秒才执行——问题出在“每个节点都按 cron 触发,再争锁”,争锁失败的节点白白空转,还占资源。
更稳的做法是只让一个节点负责“触发调度”,其他节点只负责“执行”或“跳过”。典型结构:一个中心调度器(哪怕只是个固定 IP 的 Go 进程)按 cron 推送任务 ID 到 Redis List 或 Pub/Sub,所有工作节点监听并竞争执行权。
- 如果用
redis.PubSub,注意 Go 客户端默认不重连,断连后收不到新消息,得手动监听Subscribe返回的ErrClosed并重建连接 - 用 List(
LPUSH+BRPOP)更可靠,但要注意BRPOP超时设置,别卡太久;任务体建议用 JSON 字符串,字段至少含id、timeout、retry_count - 别在 cron 表达式里写
* * * * *让所有节点每秒都去抢锁——这是典型的“高频率低收益”设计,压 Redis 还容易触发限流
任务幂等性不能全靠锁兜底
锁只能保证“同一时刻最多一个实例执行”,但挡不住网络分区、进程崩溃、Redis 故障这些导致的“锁丢失+任务重试”。所以真正关键的是任务自身必须可重入。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
常见错误现象:发两次邮件、扣两次余额、生成两个重复订单号——表面看是锁没生效,其实是业务逻辑没做幂等校验。
- 给每次任务执行生成唯一
run_id(比如uuid.New().String()),并在执行前写入 Redis:SETNX task_run:<code>task_name:daterun_id,过期时间设为任务周期的 2 倍 - 关键操作前查这个 key,存在则直接 return;成功后记得用
DEL清理(注意仍是带 value 校验的删除) - 数据库写操作务必加唯一索引(比如
UNIQUE(task_type, date, biz_id)),让 DB 层兜最后一道底,别依赖应用层判断 - 避免用时间戳或日期字符串当幂等 key——不同节点时钟不同步会导致误判
etcd vs Redis 怎么选
有人觉得 etcd 天然支持租约和监听,比 Redis 更适合做分布式任务协调。实际落地中,Redis 更快上手、更低延迟,而 etcd 在强一致和故障恢复上更稳——选哪个取决于你的容忍边界。
性能影响明显:etcd 的 Lease.Grant + Put + KeepAlive 链路比 Redis 的 SET 多 2–3 倍 RT,集群规模大了以后,心跳压力会体现在 etcd leader 节点 CPU 上。
- 如果任务间隔 > 30 秒、允许最多一次重复、运维已有一套 Redis 集群——优先用 Redis,别为了“技术正确”换组件
- 如果任务要求严格单例(比如财务对账)、能接受 100ms 级延迟、团队熟悉 etcd 生态——用
go.etcd.io/etcd/client/v3的Session封装很省心 - 别混用:同一个任务既往 Redis 写锁又往 etcd 写租约,等于多维护一套状态,出问题时排查路径翻倍
真正麻烦的从来不是选哪个中间件,而是锁续期时机没对齐任务进度、或者幂等 key 设计漏掉了业务维度。这些细节一错,换啥都白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










