MySQL 乐观锁的实际落地:避免并发更新冲突的 3 种实现方式

  • 在高并发业务场景(如秒杀库存扣减、订单状态更新)中,数据库并发更新冲突是核心痛点------例如秒杀时"库存超卖"、订单"同时支付和取消"导致状态异常。悲观锁(SELECT FOR UPDATE)虽能解决冲突,但会导致线程阻塞、性能下降,甚至引发死锁;而乐观锁基于"无锁假设",通过"更新时校验条件"实现冲突控制,适合读多写少、高并发的业务场景。本文将详解乐观锁的 3 种落地方式(版本号字段、时间戳字段、CAS 原生对比),结合秒杀、订单场景提供可直接落地的代码案例,并分析高并发下的性能差异与死锁规避技巧。

一、乐观锁核心原理与适用场景

核心原理

乐观锁假设"并发更新冲突概率低",核心逻辑是:

  • 读取数据时不加锁,仅记录数据的"版本/状态标识";
  • 更新数据时,通过 WHERE条件校验"标识是否未被修改";
  • 若校验通过则更新,若失败则说明数据已被其他线程修改,触发应用层重试。

适用场景

  • 高并发读、低并发写的场景(如秒杀库存扣减、商品库存更新);
  • 避免长事务阻塞的场景(如订单状态更新,无需锁定整个记录);
  • 对性能要求高、可接受"少量重试"的场景(冲突率 < 10%)。

前置测试环境

为复现并发场景,先创建 2 张核心表并插入测试数据:

二、三种乐观锁实现方式实战

方式 1:版本号字段实现(最稳定、推荐)

核心原理

在表中新增 version 整数字段,初始值为 1:

  • 读取数据时,同步获取 version值;
  • 更新数据时,WHERE条件必须包含"当前读取的 version",且更新时将 version + 1;
  • 若更新影响行数为 0,说明数据已被修改,触发重试。

场景 1:秒杀库存扣减(防超卖)

核心 SQL(数据库层)

应用层落地代码(Java + MyBatis 示例)

复制代码
   // 1. 库存扣减服务接口
   public interface SeckillStockService {
       /**
        * 秒杀库存扣减(乐观锁+重试)
        * @param goodsId 商品ID
        * @param retryTimes 最大重试次数
        * @return 是否扣减成功
        */
       boolean deductStock(Long goodsId, int retryTimes);
   }

   // 2. 实现类
   @Service
   @Transactional(rollbackFor = Exception.class)
   public class SeckillStockServiceImpl implements SeckillStockService {
       @Resource
       private SeckillStockMapper stockMapper;

       @Override
       public boolean deductStock(Long goodsId, int retryTimes) {
           // 递归终止条件:重试次数为0
           if (retryTimes <= 0) {
               return false;
           }

           // 步骤1:读取当前库存和版本号(无锁)
           SeckillStock stock = stockMapper.selectByGoodsId(goodsId);
           if (stock == null || stock.getStockNum() <= 0) {
               return false; // 库存为空,直接失败
           }

           // 步骤2:执行乐观锁更新
           int updateRows = stockMapper.deductStockByVersion(
               goodsId, 
               stock.getVersion()
           );

           // 步骤3:判断更新结果
           if (updateRows > 0) {
               return true; // 扣减成功
           } else {
               // 冲突,重试(休眠10ms避免高频重试)
               try {
                   Thread.sleep(10);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
               }
               return deductStock(goodsId, retryTimes - 1); // 递归重试
           }
       }
   }

   // 3. MyBatis Mapper 接口
   public interface SeckillStockMapper {
       // 根据商品ID查询库存(无锁)
       @Select("SELECT id, goods_id, stock_num, version FROM seckill_stock WHERE goods_id = #{goodsId}")
       SeckillStock selectByGoodsId(@Param("goodsId") Long goodsId);

       // 版本号乐观锁扣减库存
       @Update("UPDATE seckill_stock SET stock_num = stock_num - 1, version = version + 1 " +
               "WHERE goods_id = #{goodsId} AND version = #{version} AND stock_num > 0")
       int deductStockByVersion(@Param("goodsId") Long goodsId, @Param("version") Integer version);
   }

场景 2:订单状态更新(防并发修改)

复制代码
-- 订单状态更新 SQL(版本号乐观锁)
UPDATE order_info
SET status = 2, version = version + 1  -- 2=已支付
WHERE order_id = 9001 AND version = #{version} AND status = 1; -- 1=待支付

版本号实现优缺点

方式 2:时间戳字段实现(轻量化)

核心原理

利用 update_time 时间戳字段替代版本号:

  • 读取数据时记录 update_time;
  • 更新时 WHERE条件包含"读取的 update_time",同时更新 update_time 为当前时间;
  • 依赖时间戳的唯一性判断数据是否被修改。

场景:订单状态并发更新(轻量化场景)

核心 SQL

复制代码
-- 时间戳乐观锁更新订单状态
UPDATE order_info
SET status = 3, update_time = NOW()  -- 3=已取消
WHERE order_id = 9001 
  AND update_time = #{updateTime}  -- 读取时的更新时间
  AND status = 1; -- 仅允许修改待支付订单
  1. 应用层代码(关键片段)
  2. 时间戳实现优缺点

方式 3:CAS(Compare and Swap)原生实现(极简)

核心原理

无需额外字段,直接对比"更新前的核心字段值"(如库存数、状态值),本质是"值对比"的乐观锁:

  • 更新时 WHERE条件包含"更新前的字段值";
  • 适合简单场景(如仅校验库存数、状态值)。

场景:秒杀库存扣减(极简场景)

核心 SQL

应用层代码(关键片段)

CAS 实现优缺点

三、高并发性能对比与分析

为验证三种方式的性能,模拟 1000/10000/100000 并发请求(秒杀库存扣减,初始库存 1000),测试指标包括成功扣减率平均耗时冲突重试次数 ,结果如下:

性能结论

  1. 版本号方式:稳定性和成功率最高,是核心业务的首选;
  2. CAS 方式:执行效率略高,但冲突率最高,仅适合极简场景;
  3. 时间戳方式:折中方案,轻量化但受精度影响,适合非核心业务。

四、死锁与冲突规避技巧

乐观锁本身基于"无锁"设计,不会直接引发死锁,但高并发下的"重试+事务"仍可能导致性能问题或隐性冲突,以下是核心规避技巧:

合理设置重试机制(避免无限重试)

  • 重试次数:核心业务建议 3-5 次,非核心业务 1-3 次;
  • 重试间隔:采用"指数退避"策略(如首次 10ms,第二次 20ms,第三次 40ms),避免高频重试加剧数据库压力;
  • 代码示例(指数退避重试):

缩小事务范围(避免长事务)

  • 乐观锁更新操作应作为"最小事务"执行,避免事务中包含查询、远程调用等耗时操作;
  • 反例(错误):

正例(正确):

控制并发量级(避免数据库过载)

  • 应用层限流:通过 Redis 分布式锁、令牌桶算法限制并发请求数(如秒杀接口限制每秒 1000 次请求);
  • 数据库层限流:设置 max_connections、innodb_thread_concurrency等参数,避免数据库连接池耗尽;

避免"幻读"(合理设置事务隔离级别)

  • 乐观锁建议使用 READ COMMITTED隔离级别(MySQL 默认是 REPEATABLE READ),减少 MVCC 版本链压力,降低冲突概率;
  • 设置方式:

核心字段加唯一索引(防重复提交)

  • 秒杀场景:为 goods_id + user_id添加唯一索引,防止同一用户重复秒杀;
  • 订单场景:为 order_id添加主键索引,避免重复更新;

失败兜底策略(避免用户体验差)

  • 乐观锁重试失败后,提供兜底方案(如"当前抢购人数过多,请稍后再试"),而非直接返回"失败";
  • 核心业务可结合消息队列(如 RocketMQ)实现"异步重试",确保最终一致性。

五、最佳实践总结

核心原则

优先版本号方式:稳定性最高,适配所有高并发核心业务;

拒绝过度设计:简单场景无需引入版本号,CAS 或时间戳即可满足需求;

重试≠万能:乐观锁的核心是"低冲突假设",若冲突率 > 20%,应考虑拆分业务(如分库分表)而非增加重试次数;

监控大于优化:通过慢查询日志、Prometheus 监控乐观锁的"重试次数""失败率",及时发现并发瓶颈。

乐观锁的落地核心是"扬长避短"------利用其无锁特性提升高并发性能,同时通过合理的重试、事务、限流策略规避冲突,最终在"性能"与"数据一致性"之间找到最佳平衡点。

相关推荐
無法複制2 小时前
Centos7安装MySQL8.0
linux·mysql
zhujian826372 小时前
二十八、【鸿蒙 NEXT】orm框架
数据库·华为·sqlite·harmonyos·orm框架
Dxy12393102162 小时前
PostgreSQL与MySQL有哪些区别:从架构到应用场景的深度解析
mysql·postgresql·架构
小码吃趴菜2 小时前
MySQL远程连接
数据库·mysql
被星1砸昏头2 小时前
高级爬虫技巧:处理JavaScript渲染(Selenium)
jvm·数据库·python
王文搏2 小时前
MySQL 常用函数用法速查(含解释与示例)
数据库·mysql·adb
liux35282 小时前
MySQL高可用架构全面解析:MHA原理、部署与运维实践(九)
mysql·高可用
信创天地2 小时前
国产关系型数据库部署与权限管理实战:人大金仓、达梦、南大通用、华为GaussDB
数据库·华为·gaussdb
l1t2 小时前
psql 中的流水线操作(PostgreSQL 18)
数据库·人工智能·postgresql