news 2026/9/12 8:02:13

多线程环境下count更新的并发问题与解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多线程环境下count更新的并发问题与解决方案

1. 问题场景还原:多线程更新count的典型困境

上周排查一个生产环境问题时,发现合同统计模块的月度汇总数据频繁出现异常。具体表现为:系统每天凌晨会启动10个线程并行处理各分公司的合同数据,每个线程负责更新对应分公司的当月合同总数。按业务逻辑,北京分公司12月应该显示352份合同,但数据库里却记录了327——少了整整25条记录。

这种count统计不一致的问题,在涉及以下场景时几乎必然出现:

  • 统计型数据的实时更新(如订单数、库存量)
  • 定时任务并行处理分区数据
  • 高并发下的计数器场景

关键现象:无论重复运行多少次,最终结果总是小于预期值,且差额随机

2. 根因分析:你以为的原子操作其实不原子

2.1 伪原子操作的真相

大多数开发者会这样实现count更新(MyBatis-Plus示例):

// 错误示范1:先查后改 Contract contract = contractMapper.selectById(contractId); contract.setCount(contract.getCount() + 1); contractMapper.updateById(contract); // 错误示范2:直接运算更新 contractMapper.update( new UpdateWrapper<Contract>() .setSql("count = count + 1") .eq("id", contractId) );

看似第二种方式用了SQL表达式应该没问题?其实在MyBatis-Plus 3.5.x版本中,这两种方式在多线程环境下都会出问题。原因在于:

  1. 事务隔离级别陷阱:默认的REPEATABLE_READ隔离级别下,两个线程可能同时读到相同的count值
  2. SQL执行间隙:setSql生成的UPDATE table SET count=count+1语句在数据库层面确实是原子的,但MyBatis-Plus的插件机制可能会将其拆分为多个步骤
  3. 版本控制缺失:没有使用乐观锁等并发控制机制

2.2 并发测试验证

通过以下测试代码可以稳定复现问题:

@SpringBootTest class ConcurrentUpdateTest { @Autowired private ContractService contractService; @Test void testConcurrentUpdate() throws InterruptedException { // 初始count=0 Contract contract = new Contract().setCount(0); contractMapper.insert(contract); // 启动100个线程并发+1 CountDownLatch latch = new CountDownLatch(100); for (int i = 0; i < 100; i++) { new Thread(() -> { contractService.incrementCount(contract.getId()); latch.countDown(); }).start(); } latch.await(); // 结果通常小于100 Contract result = contractMapper.selectById(contract.getId()); System.out.println("Final count: " + result.getCount()); } }

3. 解决方案对比:从临时补丁到终极方案

3.1 方案1:同步锁(不推荐)

public synchronized void incrementCount(Long id) { // 更新逻辑 }

缺点

  • 性能杀手,完全丧失多线程优势
  • 分布式环境失效

3.2 方案2:数据库悲观锁(适用特定场景)

@Transactional public void incrementCount(Long id) { Contract contract = contractMapper.selectById(id, new QueryWrapper<Contract>().last("FOR UPDATE")); contract.setCount(contract.getCount() + 1); contractMapper.updateById(contract); }

适用场景

  • 写多读少
  • 允许较高延迟

3.3 方案3:MyBatis-Plus乐观锁(推荐基础方案)

  1. 实体类添加@Version注解:
public class Contract { @Version private Integer version; // 其他字段 }
  1. 更新时自动校验版本:
public void incrementCount(Long id) { Contract contract = contractMapper.selectById(id); contract.setCount(contract.getCount() + 1); contractMapper.updateById(contract); // 自动校验version }

优势

  • 轻量级并发控制
  • 失败后自动重试

3.4 方案4:CAS+重试机制(高并发最优解)

public void safeIncrement(Long id, int maxRetry) { for (int i = 0; i < maxRetry; i++) { Contract contract = contractMapper.selectById(id); int oldCount = contract.getCount(); int newCount = oldCount + 1; boolean updated = contractMapper.update( new UpdateWrapper<Contract>() .eq("id", id) .eq("count", oldCount) .set("count", newCount) ) > 0; if (updated) break; } }

核心原理

  • Compare And Swap原子操作
  • 类似Java中的AtomicInteger实现

4. MyBatis-Plus专项优化技巧

4.1 批量更新的性能陷阱

即使使用乐观锁,这样的批量更新仍然有问题:

list.forEach(item -> { item.setCount(item.getCount() + 1); mapper.updateById(item); });

正确姿势

List<Long> ids = list.stream().map(Contract::getId).collect(Collectors.toList()); mapper.update( new UpdateWrapper<Contract>() .setSql("count = count + 1") .in("id", ids) );

4.2 版本号管理的隐藏坑

发现很多团队这样用乐观锁:

// 反例:手动设置version contract.setVersion(contract.getVersion() + 1); mapper.updateById(contract);

正确做法

  • 永远让MyBatis-Plus自动管理version
  • 在实体类中version字段添加@Version注解即可

4.3 分库分表下的特殊处理

当数据分布在多个库时,需要额外处理:

@DS("sharding") // 动态数据源注解 public void distributedIncrement(Long id) { // 使用分布式锁或Seata等方案 }

5. 生产环境真实案例复盘

某电商平台促销活动期间,库存扣减出现超卖。排查发现其"已售数量"统计用的是普通update:

UPDATE product SET sold=sold+1 WHERE id=?

最终解决方案

  1. 短期应急:改用SELECT FOR UPDATE
  2. 长期方案:
    • 商品表添加version字段
    • 更新SQL改为:
      UPDATE product SET sold=sold+1, version=version+1 WHERE id=? AND version=?
  3. 架构升级:
    • 热点商品采用Redis原子计数器
    • 异步同步到数据库

性能对比

方案TPS错误率
原生update12008.7%
乐观锁8500%
Redis+异步56000%

6. 扩展思考:不同场景的选型建议

  1. 低频更新(如点赞数):

    • 直接使用方案3的乐观锁
    • 配合@Retryable注解实现自动重试
  2. 高频更新(如秒杀库存):

    • Redis原子操作 + 定时持久化
    • 考虑Redisson的分布式锁
  3. 财务级精确统计

    • 事务日志+补偿机制
    • 采用TCC模式

实际项目中,我在处理物流订单状态统计时,最终采用了组合方案:

  • 实时显示:Redis INCR
  • 每日对账:跑批修复数据库统计值
  • 关键报表:独立统计引擎

这种设计使得日均200万次的更新请求,数据库压力几乎为零,且统计误差控制在万分之一内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 8:01:40

dsh 后台运行指南:终端关闭不断线,nohup/tmux/systemd 实战

最近我把 DeepSeek Harness&#xff08;命令行工具 dsh&#xff09;当成主力智能体框架来用&#xff0c;每天都要在上面起对话、挂多轮任务、跑连接外部环境的自动化流程。用得越深就越发现一个问题&#xff1a;只要本地开的终端窗口被关掉&#xff0c;或者 SSH 远程连接闪断一…

作者头像 李华
网站建设 2026/9/12 8:00:51

C++ STL容器核心解析与高效使用指南

1. C容器概述&#xff1a;STL的核心武器库在C标准模板库(STL)中&#xff0c;容器是存储和管理数据的核心组件。作为从1998年便纳入C标准的经典设计&#xff0c;STL容器历经二十余年发展已成为每个C开发者必须掌握的技能。不同于原始数组的固定大小和手动管理&#xff0c;STL容器…

作者头像 李华
网站建设 2026/9/12 7:58:18

Hunyuan3D-2 本地部署实操教程:图像转3D,5分钟出第一个模型

Hunyuan3D-2 本地部署实操教程&#xff1a;图像转3D&#xff0c;5分钟出第一个模型 【免费下载链接】Hunyuan3D-2 High-Resolution 3D Assets Generation with Large Scale Hunyuan3D Diffusion Models. 项目地址: https://gitcode.com/GitHub_Trending/hu/Hunyuan3D-2 …

作者头像 李华
网站建设 2026/9/12 7:57:43

Kafka消息积压故障分析与实战处理指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华