直接用 sync.atomicint64 不行,因其仅保证单机线程安全,分布式环境下各实例维护独立计数器,无法实现全局唯一递增,易导致订单号重复或乱序,引发下游系统错误。

为什么直接用 sync.AtomicInt64 不行
单机场景下,sync.AtomicInt64 确实快且线程安全,但分布式环境里,多个服务实例各自维护一份原子变量,根本无法达成全局一致。你看到的“计数”只是某台机器的局部值,不是全局唯一递增序列。更危险的是,如果业务依赖该计数生成订单号、流水号或分片键,不同节点产出重复或乱序 ID,下游系统会直接出错。
Redis + Lua 是最实用的折中方案
不用强一致共识协议(如 Raft),也能在大多数场景下做到高吞吐、低延迟、全局单调递增。关键在于把「取值 + 自增」封装成原子操作,避免竞态。Redis 的 INCR 本身是原子的,但若需带过期时间、初始值控制或批量预分配(比如一次拿 1000 个号防频繁打 Redis),就得靠 Lua 脚本兜底。
示例:预分配一段连续 ID 并设置过期
local key = KEYS[1]
local step = tonumber(ARGV[1])
local expire_sec = tonumber(ARGV[2])
local base = redis.call("INCRBY", key, step)
return {base - step + 1, base, redis.call("EXPIRE", key, expire_sec)}
Go 调用时用 redis.Eval 执行该脚本,返回起始和结束 ID。注意:step 别设太大(如超 10000),否则某实例崩溃会导致大量 ID 浪费;也别太小(如 1),否则 Redis 成为瓶颈。
- 必须用
EVAL或EVALSHA,不能拆成GET+SET,否则失去原子性 - Redis 实例建议启用
maxmemory-policy volatile-lru,避免计数 key 挤占内存 - 客户端连接池大小建议 ≥50,否则高并发下
dial timeout错误频发
etcd v3 的 CompareAndSwap 更适合强一致性要求
当你的业务不能容忍任何 ID 重复(比如金融类凭证编号),且能接受写入吞吐下降 3–5 倍,etcd 是比 Redis 更稳的选择。它基于 Raft,天然支持线性一致性读写,cmp 字段可校验当前值是否等于预期,失败则重试。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
Go 中用官方 go.etcd.io/etcd/client/v3:
resp, err := cli.Txn(ctx).If(
clientv3.Compare(clientv3.Value(key), "=", currentVal),
).Then(
clientv3.OpPut(key, strconv.FormatInt(currentVal+1, 10)),
).Commit()
但要注意:
- 每次调用都走 Raft 日志落盘,P99 延迟通常在 10–50ms 级别,不适合每秒万级计数请求
-
currentVal必须先Get一次,再参与Compare,网络分区时可能拿到陈旧值,需配合WithSerializable()或WithRequireLeader() - etcd 集群节点数建议为奇数(3 或 5),偶数节点不提升容错能力,反而降低可用性
别忽略本地缓存与降级逻辑
无论选 Redis 还是 etcd,网络抖动或中间件故障时,计数器不可用会导致整个业务链路阻塞。必须设计降级路径:
- 本地用
sync.Pool缓存已预取的 ID 段,故障时从池中取,撑住 30–60 秒 - 降级到时间戳 + 机器 ID + 序列号(类似 Snowflake),但要注意时钟回拨——用
github.com/google/uuid的UUIDv7或自研逻辑做兜底 - 记录
counter_fallback_total{type="redis",status="fail"}这类 Prometheus 指标,故障恢复后人工核对 ID 是否跳变或重复
真正难的从来不是“怎么实现递增”,而是“怎么在部分失败时仍保持语义正确”。ID 一旦发出去,就收不回来了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










