分布式计数器不能用 sync.mutex,因其仅限单机原子性;跨进程需 redis incr/expire 等方案保证一致性与生命周期管理。

为什么不能直接用 sync.Mutex 做分布式计数器
本地加锁能保原子性,但跨进程、跨机器时完全失效。比如你起 3 个 Go 服务实例,各自用 sync.Mutex 统计请求量,最后汇总的总数大概率比真实值小——因为每个实例只在自己内存里加,彼此不通信。这不是并发问题,是分布问题。
真正要解决的是:多个独立运行的 Go 进程,如何对同一个逻辑计数器(如“今日 PV”)做安全、一致、低延迟的增减操作。
- 单机
sync/atomic或sync.Mutex只适用于单进程场景,一上 Kubernetes 就失效 - 数据库自增字段(如 PostgreSQL
UPDATE ... RETURNING)可行,但吞吐上不去,尤其高频率计数(如每秒万级)会成瓶颈 - Redis 的
INCR/DECR是最常用解法:命令原子、网络延迟可控、支持过期、有集群模式
用 redis.Client.Incr 实现带过期的计数器
Go 生态主流 Redis 客户端是 github.com/redis/go-redis/v9,它的 Incr 方法直接对应 Redis 原生命令,天然原子。
关键点不是“能不能加”,而是“加完怎么管生命周期”。比如统计“每分钟请求数”,必须自动清零,否则数字越滚越大。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 不要先
INCR再EXPIRE:两步非原子,中间若进程崩溃,key 永久存在 - 正确做法:对新 key 使用
SET key 1 EX 60 NX,失败则用INCR;但 Go 客户端没直接封装这个组合,得手写redis.Pipeline或 Lua 脚本 - 更简单稳妥的做法:用
INCR+Expire(注意是Expire不是SetEx),并设置NX参数(仅当 key 不存在时设过期)——client.Expire(ctx, key, ttl, redis.ExpireOptions{NX: true})
示例片段:
ctx := context.Background()
key := "counter:minute:20240520:1423"
val, err := client.Incr(ctx, key).Result()
if err != nil {
// 处理连接/超时错误
}
_ = client.Expire(ctx, key, time.Minute, redis.ExpireOptions{NX: true})
高吞吐下避免 Redis 成为瓶颈的三个动作
单个 Redis 实例扛不住每秒几万次 INCR?不是 Redis 不行,是用法容易踩坑。
- 禁用短连接:每次调用都
redis.NewClient→ 连接池耗尽、TLS 握手压垮 CPU。必须复用全局*redis.Client实例,并配置合理PoolSize(默认 10 太小,建议 50–200) - 慎用 pipeline:批量
INCR看似省 RTT,但实际把压力集中到单次 TCP 包,还可能因某条失败导致整批回滚。除非业务明确允许“尽力而为”,否则单命令更稳 - 考虑分片:把一个逻辑计数器拆成 N 个物理 key(如
counter:total:shard:0到shard:7),写时随机选 shard,读时 SUM。能线性提升吞吐,且不增加一致性复杂度
本地缓存 + 异步回写适合什么场景
如果计数器允许少量误差(比如监控大盘误差
本质是用空间换性能:每个 Go 实例维护一个内存计数器 + 定时器,攒够 N 次或到时间就一次性 INCRBY 到 Redis。
- 别用
time.Ticker直接触发回写:多个 goroutine 并发写同一 key,仍需加锁协调 - 推荐方案:每个 shard 对应一个独立 goroutine,用
time.AfterFunc延迟提交,提交后立即重置定时器;写操作只往 channel 发信号,由 goroutine 统一聚合 - 风险点:进程崩溃会导致未提交计数丢失。所以只适用于“统计类”而非“库存类”场景
真正难的从来不是“怎么加一”,而是“加在哪”“加几次”“加完谁负责清理”。Redis key 生命周期、客户端连接复用、分片策略——这些细节堆起来,才决定分布式计数器在生产里跑不跑得稳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










