- 在高并发业务场景(如秒杀库存扣减、订单状态更新)中,数据库并发更新冲突是核心痛点------例如秒杀时"库存超卖"、订单"同时支付和取消"导致状态异常。悲观锁(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; -- 仅允许修改待支付订单
- 应用层代码(关键片段)

- 时间戳实现优缺点

方式 3:CAS(Compare and Swap)原生实现(极简)
核心原理
无需额外字段,直接对比"更新前的核心字段值"(如库存数、状态值),本质是"值对比"的乐观锁:
- 更新时 WHERE条件包含"更新前的字段值";
- 适合简单场景(如仅校验库存数、状态值)。
场景:秒杀库存扣减(极简场景)
核心 SQL

应用层代码(关键片段)

CAS 实现优缺点

三、高并发性能对比与分析
为验证三种方式的性能,模拟 1000/10000/100000 并发请求(秒杀库存扣减,初始库存 1000),测试指标包括成功扣减率 、平均耗时 、冲突重试次数 ,结果如下:
性能结论
- 版本号方式:稳定性和成功率最高,是核心业务的首选;
- CAS 方式:执行效率略高,但冲突率最高,仅适合极简场景;
- 时间戳方式:折中方案,轻量化但受精度影响,适合非核心业务。
四、死锁与冲突规避技巧
乐观锁本身基于"无锁"设计,不会直接引发死锁,但高并发下的"重试+事务"仍可能导致性能问题或隐性冲突,以下是核心规避技巧:
合理设置重试机制(避免无限重试)
- 重试次数:核心业务建议 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 监控乐观锁的"重试次数""失败率",及时发现并发瓶颈。
乐观锁的落地核心是"扬长避短"------利用其无锁特性提升高并发性能,同时通过合理的重试、事务、限流策略规避冲突,最终在"性能"与"数据一致性"之间找到最佳平衡点。
