用ConcurrentHashMap做投票计数是底线选择,因其单key操作原子;需配合LongAdder替代AtomicInteger提升吞吐,内存计数后异步落库,并用本地快照防重启丢失。

Java里用ConcurrentHashMap做投票计数,别碰HashMap
多线程环境下直接用HashMap累加票数,大概率出现漏票或重复计数——它不是线程安全的,put和get之间可能被其他线程打断。用ConcurrentHashMap是底线选择,它对单个key的操作(比如computeIfAbsent、merge)是原子的。
常见错误现象:HashMap在压力测试下总票数明显低于实际请求量;本地单线程跑没问题,一上并发就对不上数。
- 用
ConcurrentHashMap<string integer></string>存选项ID → 票数,别自己加synchronized块封装普通Map - 累加时优先用
merge(key, 1, Integer::sum),比先get再put更安全 - 如果需要实时读取全部计数结果,直接调用
entrySet()即可,不用额外同步
用户重复投票怎么拦:用ConcurrentHashMap<string set>></string>记IP+设备指纹
只靠前端限制或session拦截不可靠——用户清cookie、换浏览器、用不同设备都能绕过。服务端必须存一份“谁投过哪一票”的映射,且得支持高并发写入。
使用场景:一个投票活动允许每人限投1票,但要兼容PC/手机/微信内嵌等多种访问方式。
立即学习“Java免费学习笔记(深入)”;
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- 键用
投票活动ID + 用户标识拼接(如"vote_123_ip:192.168.1.100_ua:chrome"),避免跨活动冲突 - 值用
ConcurrentHashMap的computeIfAbsent初始化ConcurrentSkipListSet(比HashSet线程安全,且支持并发迭代) - 不要用
contains+add两步判断,改用add返回值判断是否首次插入
AtomicInteger不够用:当需要“增1并返回旧值”时得换LongAdder
单纯统计总票数用AtomicInteger可以,但一旦要支持“投完立刻返回当前累计数”且并发量大(比如每秒上千请求),incrementAndGet()会成为瓶颈——所有线程争抢同一个CAS变量。
性能影响:压测时QPS卡在3k左右就上不去,CPU在Unsafe.compareAndSwapInt上空转严重。
- 改用
LongAdder,它内部用分段累加+最终合并,吞吐量能提升5–10倍 - 注意:
LongAdder.sum()是非实时快照,如果业务要求绝对精确的“投完立刻看到最新总数”,仍需配合内存屏障或短暂同步 - 别在循环里频繁调用
sum(),缓存到局部变量里用
MySQL写入延迟导致数据不一致:先内存计数,再异步落库
每次投票都同步写数据库,不仅慢,还容易因DB抖动丢失数据。真实线上系统必须把计数和持久化解耦——内存里先算准,再用消息队列或定时批量刷库。
容易踩的坑:用@Scheduled每5秒刷一次,但没做去重,同一笔投票被重复写入;或者没处理应用重启时内存计数丢失。
- 内存计数结构用
ConcurrentHashMap+LongAdder组合,启动时从DB加载初始值 - 落库用
INSERT ... ON DUPLICATE KEY UPDATE,主键设为vote_id + user_id,避免重复插入 - 加个简单的本地文件快照(比如每分钟写一次
/tmp/vote-count-backup.json),重启时优先加载它,再补差量
真正麻烦的是“撤回投票”和“多选改单选”这类操作,它们会让计数逻辑从加法变成带条件的减法+加法,这时候光靠原子类不够,得引入轻量级状态机或版本号控制。细节藏在具体业务规则里,没法套模板。

















