sync.Map不适合实时票数统计,因其Load+Store非原子操作,会导致并发投票丢失;应改用atomic.Int64配固定key映射或分项锁,配合Redis幂等校验与缓存失效策略保障一致性。

为什么 sync.Map 不适合实时票数统计?
直接用 sync.Map 存储每个选项的票数,看似线程安全,实际在高并发下会掩盖数据竞争——因为 sync.Map.Load + sync.Map.Store 不是原子操作。用户 A 和 B 同时投同一选项,可能都读到旧值 5,各自加 1 后写回 6,最终只+1 而非+2。
- 真正需要的是「读-改-写」原子性,
sync.Map不提供CompareAndSwap或Add类接口 - 若用
sync.Mutex包裹 map,锁粒度太粗,所有投票请求串行化,QPS 直接受限 - 更合理的选择是:对每个投票项单独配一把锁(如
map[string]*sync.Mutex),或改用atomic.Int64配合固定 key 映射
如何用 atomic.Int64 安全累加票数?
前提是选项 ID 是已知且有限的(比如预设的 "option_a"、"option_b"),这样可以预先初始化一组 atomic.Int64 变量,避免运行时动态创建带来的竞态和内存抖动。
示例结构:
var voteCounts = map[string]*atomic.Int64{
"option_a": &atomic.Int64{},
"option_b": &atomic.Int64{},
"option_c": &atomic.Int64{},
}
- 投票时调用
voteCounts[optionID].Add(1),底层是 CPU 的LOCK XADD指令,无锁且原子 - 读取总数用
voteCounts[optionID].Load(),不阻塞,也不需 defer unlock - 注意:不能对不存在的 key 动态 new
*atomic.Int64并存入 map——map 写操作本身非原子,需额外锁保护 map 结构
Gin 路由里怎么避免重复投票?
单纯靠前端禁用按钮不可信,后端必须做幂等校验。常见做法是把 userID + pollID 组成唯一键,存入 Redis 的 SET 或用 redis.SetNX。
立即学习“go语言免费学习笔记(深入)”;
- 推荐用
redis.Client.SetNX(ctx, "voted:"+pollID+":"+userID, "1", time.Hour*24),成功返回 true 才执行计票 - 不要用
GET+SET两步,中间可能被并发请求插入 - 如果业务允许“一人多投”,但限制“每选项一票”,则 key 应为
"voted:"+pollID+":"+userID+":"+optionID - Gin 中建议把校验逻辑封装进中间件,而不是堆在 handler 里,便于复用和测试
统计结果要不要缓存?缓存失效怎么处理?
每次查票都扫一遍 Redis 或 DB 是浪费资源。但缓存不是简单加个 time.Sleep,关键在失效策略。
- 写操作(投票)后立即
redis.Del(ctx, "result:"+pollID),让下次读命中空,触发重建 - 读操作先
redis.Get(ctx, "result:"+pollID),未命中再聚合voteCounts或查 DB,最后redis.SetEX回填,TTL 设为 5–30 秒视业务容忍度而定 - 别用「定时刷新缓存」,容易出现脏数据窗口;也别用「更新时双删」,Redis 删除失败会导致永久不一致
- 如果投票结束,立刻
DEL缓存并设置一个poll:123:closed标记,后续请求跳过缓存直查最终结果
复杂点在于选项动态增减和跨服务部署时的原子性——这时候单靠 atomic.Int64 不够,得上分布式锁或事务型存储。但对大多数内部投票系统,按上述方式已足够稳。


















