incr 在 go 多实例下是原子的,但需避免本地加法、get+set 模拟、集群 key 路由不一致;sync.mutex 和 atomic.int64 仅限单进程,无法跨实例共享状态,故不能用于分布式计数。

INCR 在 Go 多实例下是原子的,但前提是别绕开 Redis 自己搞本地加法、别用 GET + SET 模拟自增、别在集群里让 key 路由到不同节点。
为什么 sync.Mutex 或 atomic.Int64 不能用于分布式计数
它们只锁住当前进程内的 goroutine。两个 Go 实例同时启动,各自用 atomic.AddInt64(&counter, 1),结果就是两个都从 0 开始加,生成重复值。这不是代码写得不够“并发安全”,而是根本不在同一个共享内存域里。
- 本地变量自增后才写 Redis:中间可能被其他实例覆盖,计数跳变或丢失
- 用数据库自增主键当计数器:写放大严重,
INSERT冲突概率随并发上升,TPS 掉一个数量级 - 自己实现带时间戳的 ID 拼接但没统一时区:
time.Now().Format("200601021504")用本地时区,跨机器可能倒序或重复
go-redis/v9 中正确调用 INCR 的姿势
别拼字符串命令,也别用老版本 redigo。v9 把原子性封装进类型安全的函数里,错误能明确返回,上下文可控制超时。
- 必须传带超时的
context.Context,例如ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - 调用
client.Incr(ctx, "counter:order"),检查.Err()是否为nil,网络断开、key 类型错误(比如之前存了非数字字符串)、Redis 只读模式都会在这里暴露 - 首次使用前想设默认值?用
client.SetNX(ctx, "counter:order", "0", 0),不是SET,避免覆盖已有值 - 如果 key 不存在时希望直接加 1 而不报错,
client.IncrBy(ctx, "counter:order", 1)更稳妥——它对空 key 默认从 0 开始加
Redis 集群下 key 路由不一致导致 INCR 失效
集群模式中,INCR "counter:order" 可能被哈希到节点 A,而另一个请求 INCR "counter:user" 被哈希到节点 B。但如果你的业务逻辑依赖“同一类计数器必须落在同一 slot”,那就得强制路由。
- 给 key 加大括号,例如
INCR "counter:{order}",Redis 集群只对{}内的内容做哈希,保证所有{order}相关操作落到同一分片 - 不要用随机后缀如
"counter:" + uuid.NewString(),这会让每个 key 散列到不同节点,彻底失去原子性保障 - 验证是否路由一致:连上任意节点执行
CLUSTER KEYSLOT "counter:{order}"和CLUSTER KEYSLOT "seq:{order}",看返回 slot 编号是否相同
Redis 不可用时怎么不崩、不重复、不卡住
硬抛错或 fallback 到内存计数器是最常见错误——前者让请求失败,后者让多个实例各自从 0 开始,ID 爆炸式重复。
- 启动时预热:用
client.MGet(ctx, "counter:a", "counter:b", "counter:c")一次性拉取多个初始值,存进sync.Map作只读快照,降级时读这个快照+本地偏移 - 降级开关用
atomic.Bool控制,别用 mutex,避免锁竞争拖慢主流程 - 失败后先
time.Sleep(10 * time.Millisecond)再重试,防止 Redis 恢复前瞬间大量重试压垮连接池 - 允许跳号:比如 Redis 不可用期间,直接从当前快照值 +1000 开始分配,比强保连续更实际
真正难的不是写对 INCR,而是想清楚“当 Redis 挂了 3 秒,你的订单号是继续发还是停?重复和跳号,哪个业务更能容忍?”——这个判断比任何代码都关键。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











