You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从ConcurrentHashMap提取计数避免漏算/重复计算的实现是否正确?

问题排查

已知代码缺陷

  1. 明显笔误:incrementCount方法中操作的transactionsPerId和你定义的成员变量transactionsPerUser名称不一致,真实运行的代码首先要修正这个问题,否则会直接编译报错。
  2. 调度并发隐患:如果你自定义了定时任务的调度线程池支持并发执行,或者给该方法加了@Async注解,当上一次刷库任务执行超过1秒还没结束时,下一秒的调度任务会同时启动,两个并行任务可能读取到同一批计数数据重复写入数据库,你遇到的计数偏多的情况很大概率和这个问题有关。
    同时你的遍历逻辑存在空指针风险:两个并行任务同时遍历到同一个用户ID时,只有第一个调用remove的线程能拿到有效值,第二个线程拿到null会直接触发空指针。
  3. 数据库操作没有幂等保障:如果你的updateCount方法加了失败重试逻辑,当出现数据库执行成功但网络超时的情况时,重试会导致同一批计数被重复累加,最终结果偏多。如果没有重试逻辑,操作失败时会直接丢失该批次计数,导致结果偏少。

额外注意点

LongAdder#sum()在高并发更新场景下返回的是近似值,sum过程中新增的增量可能不会被统计到,不过这个问题只会导致计数偏少,和你遇到的结果偏多的情况不符,可以放在后面排查。

修复方案

  1. 先确认DAO层的更新逻辑必须是累加而非直接覆盖,SQL应该写成:
    UPDATE user_statistics SET transaction_count = transaction_count + ? WHERE user_id = ?
    
    如果是直接把字段设置为传入的计数值,多实例部署时一定会出现计数互相覆盖的问题。
  2. 确认定时任务是单线程串行调度:Spring的@Scheduled默认使用单线程池,上一个任务没执行完不会触发下一个调度,不要给该方法加@Async注解,也不要配置自定义多线程调度池,避免并发调度。如果是多实例部署,要给刷库方法加分布式锁,确保同一时间只有一个实例在执行刷库操作。
  3. 优化刷库的遍历逻辑,避免空指针:
    @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);
        }
    }
    
  4. 增加重试幂等校验:如果需要加失败重试,每次刷库生成唯一的批次号,数据库中记录已执行成功的批次号,重试前先判断该批次是否已经执行过,避免重复累加。

以上问题修复后如果还是存在计数偏多的情况,可以排查上层业务是否有请求重复调用的问题,比如网关重试、接口重试导致同一个事务被多次计数。

内容的提问来源于stack exchange,提问作者Anonymous Beaver

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 23:39:01