不能直接用 sync.map 做分布式计数器,因为其仅限单进程内存,多实例下数据不共享,导致计数非全局一致;应使用 redis + lua 实现原子操作,并严格配置连接池、超时及缓存策略。

为什么不能直接用 sync.Map 做分布式计数器
因为 sync.Map 只在单进程内存内有效,微服务一拆成多个实例,每个实例的 sync.Map 就各自维护一份数据,完全不共享。你看到的“计数”只是某一个副本的局部值,根本不是全局一致的。
常见错误现象:压测时 QPS 上去了,但总量对不上;服务重启后计数归零;灰度发布期间两个版本服务写入冲突,数值跳变或丢失。
- 别试图用 HTTP 轮询其他实例来“同步”计数——网络延迟、失败重试、并发写入竞争会让逻辑失控
- 别把计数存在本地文件或 SQLite —— 多实例同时写会损坏,且无原子性保障
- 如果业务允许“最终一致性”,可以接受几秒延迟,那方案选型和强一致完全不同,得先明确 SLA
用 Redis + Lua 实现原子递增最稳妥
Redis 的 INCR、INCRBY 本身是原子操作,但微服务里常需要“带条件递增”(比如只在用户未签到时 +1)或“复合操作”(如计数+写日志),这时裸命令不够用,必须上 Lua 脚本。
Go 侧推荐用 redis.UniversalClient(来自 github.com/go-redis/redis/v8),它自动适配单节点、哨兵、集群模式,避免自己处理拓扑切换。
- 脚本必须用
redis.Eval执行,不要拼接命令再Do()—— 中间网络断开会导致部分执行成功、部分失败 - Lua 脚本里禁止调用非纯函数(如
math.random),否则集群模式下哈希槽路由可能出错 - key 设计建议带业务前缀和分片字段,例如
"counter:order:202405",避免所有请求打到同一个 slot
示例:按用户 ID 分片的签到计数
const incrIfNotExists = `
if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 0 then
redis.call("HINCRBY", KEYS[1], ARGV[1], 1)
redis.call("HSET", KEYS[1], ARGV[1], ARGV[2])
return 1
else
return 0
end`
注意 Redis 连接池和超时配置
默认连接池太小(10)、超时太长(5s),高并发下容易阻塞或拖垮整个服务。微服务里 Redis 不是辅助组件,它是关键路径上的依赖,必须按数据库级别对待。
-
MinIdleConns至少设为 5–10,避免突发流量时频繁建连 -
MaxRetries建议设为 0(禁用自动重试),否则幂等没做好时,一次失败请求可能被重试多次,导致计数翻倍 -
ReadTimeout和WriteTimeout都应控制在 200ms 内,超过就快速失败,由上层决定降级(如返回默认值或走本地缓存) - 集群模式下务必开启
ReadOnly选项,读请求尽量打到 slave,减轻 master 压力
本地缓存只能做旁路,不能替代 Redis
有些团队加一层 freecache 或 bigcache 缓存计数结果,想减少 Redis 请求。这没问题,但必须清楚:缓存只是加速读,写操作永远以 Redis 为准,且要处理好失效问题。
- 不要用 “写本地缓存 + 异步刷 Redis” 模式——异步失败时数据永久丢失
- 缓存 key 和 Redis key 必须严格一致,否则更新时漏掉某一边就产生脏数据
- 如果业务要求“精确计数”,缓存 TTL 建议 ≤ 1s;若允许误差,可设 10s,但得在文档里写明误差范围
- 缓存击穿风险高(比如某个热 key 突然大量请求),要用
singleflight拦住重复回源
真正难的不是写代码,是定义清楚“这个计数器到底要多准、能容忍多久延迟、失败时怎么兜底”。这些不聊透,技术方案再漂亮也扛不住线上真实流量。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











