从ConcurrentHashMap提取计数避免漏算/重复计算的实现是否正确?
问题排查
已知代码缺陷
- 明显笔误:
incrementCount方法中操作的transactionsPerId和你定义的成员变量transactionsPerUser名称不一致,真实运行的代码首先要修正这个问题,否则会直接编译报错。 - 调度并发隐患:如果你自定义了定时任务的调度线程池支持并发执行,或者给该方法加了
@Async注解,当上一次刷库任务执行超过1秒还没结束时,下一秒的调度任务会同时启动,两个并行任务可能读取到同一批计数数据重复写入数据库,你遇到的计数偏多的情况很大概率和这个问题有关。
同时你的遍历逻辑存在空指针风险:两个并行任务同时遍历到同一个用户ID时,只有第一个调用remove的线程能拿到有效值,第二个线程拿到null会直接触发空指针。 - 数据库操作没有幂等保障:如果你的
updateCount方法加了失败重试逻辑,当出现数据库执行成功但网络超时的情况时,重试会导致同一批计数被重复累加,最终结果偏多。如果没有重试逻辑,操作失败时会直接丢失该批次计数,导致结果偏少。
额外注意点
LongAdder#sum()在高并发更新场景下返回的是近似值,sum过程中新增的增量可能不会被统计到,不过这个问题只会导致计数偏少,和你遇到的结果偏多的情况不符,可以放在后面排查。
修复方案
- 先确认DAO层的更新逻辑必须是累加而非直接覆盖,SQL应该写成:
如果是直接把字段设置为传入的计数值,多实例部署时一定会出现计数互相覆盖的问题。UPDATE user_statistics SET transaction_count = transaction_count + ? WHERE user_id = ? - 确认定时任务是单线程串行调度:Spring的
@Scheduled默认使用单线程池,上一个任务没执行完不会触发下一个调度,不要给该方法加@Async注解,也不要配置自定义多线程调度池,避免并发调度。如果是多实例部署,要给刷库方法加分布式锁,确保同一时间只有一个实例在执行刷库操作。 - 优化刷库的遍历逻辑,避免空指针:
@Scheduled(every = "1s") public void sendAggregatedStatisticsToDatabase() { Iterator<Map.Entry<String, LongAdder>> iterator = transactionsPerUser.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, LongAdder> entry = iterator.next(); // 先移除再获取值,确保后续新增的计数会进入下一批次 iterator.remove(); long count = entry.getValue().sum(); statisticsDAO.updateCount(entry.getKey(), count); } } - 增加重试幂等校验:如果需要加失败重试,每次刷库生成唯一的批次号,数据库中记录已执行成功的批次号,重试前先判断该批次是否已经执行过,避免重复累加。
以上问题修复后如果还是存在计数偏多的情况,可以排查上层业务是否有请求重复调用的问题,比如网关重试、接口重试导致同一个事务被多次计数。
内容的提问来源于stack exchange,提问作者Anonymous Beaver
相关产品推荐
相关产品推荐

