1. 为什么我们需要乐观锁?
在电商、金融等业务系统中,账户余额变更、库存扣减这类操作是最典型的并发冲突场景。想象一个场景:某商品库存仅剩1件,此时两个用户同时点击购买。如果系统简单地执行UPDATE product SET stock=stock-1 WHERE id=100,很可能会导致超卖——两个请求都读到stock=1,最终库存变为-1。
传统解决方案是使用悲观锁(如SELECT FOR UPDATE),但这会带来性能瓶颈。我在某次大促中亲眼见过,一个热门商品的详情页因为频繁加锁导致数据库连接池耗尽。而乐观锁的核心思想是:假设冲突不常发生,只在提交时检测冲突。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MyBatis-Plus乐观锁实现原理
2.1 版本号机制设计
MyBatis-Plus通过@Version注解实现乐观锁。其工作流程如下:
- 读取数据时获取version字段值(假设为1)
- 更新时执行:
UPDATE table SET ..., version=version+1 WHERE id=xxx AND version=1 - 若受影响行数为0,说明version已被其他事务修改
这种设计有三大优势:
- 无锁等待:不像悲观锁会阻塞其他事务
- 实现简单:只需添加一个version字段
- 通用性强:适用于所有需要并发控制的场景
2.2 与悲观锁的性能对比
通过JMeter压测某库存服务接口,结果对比如下:
| 并发用户数 | 悲观锁QPS | 乐观锁QPS | 错误率 |
|---|---|---|---|
| 50 | 1200 | 2800 | 0.1% |
| 100 | 800 | 2500 | 0.3% |
| 200 | 400 | 2300 | 0.8% |
注意:乐观锁的错误率来自冲突时的更新失败,这在实际业务中是可接受的,通常配合重试机制使用
3. 完整集成指南(基于MyBatis-Plus 3.5.17)
3.1 实体类配置
java复制@Data
public class Account {
private Long id;
private BigDecimal balance;
@Version
private Integer version; // 必须使用包装类型
}
关键细节:
- version字段必须用Integer/Long等包装类型,不可用int/long
- 初始值建议设为0或1(数据库设置DEFAULT值)
3.2 数据库表设计
sql复制CREATE TABLE `account` (
`id` BIGINT NOT NULL,
`balance` DECIMAL(10,2) DEFAULT 0.00,
`version` INT DEFAULT 0, -- 关键字段
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
3.3 Spring Boot配置
application.yml中需开启MP的乐观锁插件:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 如果使用逻辑删除
configuration:
map-underscore-to-camel-case: true
Java配置类:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
4. 实战中的六个关键问题
4.1 高并发下的重试策略
当更新失败时(受影响行数=0),推荐采用指数退避重试:
java复制@Transactional
public boolean deductBalance(Long accountId, BigDecimal amount) {
int retry = 0;
while (retry < MAX_RETRY) {
Account account = accountMapper.selectById(accountId);
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
account.setBalance(account.getBalance().subtract(amount));
if (accountMapper.updateById(account) > 0) {
return true;
}
Thread.sleep((long) Math.pow(2, retry) * 100); // 指数退避
retry++;
}
throw new ConcurrentUpdateException("操作过于频繁,请稍后重试");
}
4.2 批量操作的坑
批量更新时(如updateBatchById),每个记录的version是独立比较的。可能出现部分成功部分失败的情况。解决方案:
- 在Service层拆分为单条更新
- 使用自定义批量SQL(需手动处理version)
xml复制<update id="batchUpdate">
<foreach collection="list" item="item" separator=";">
UPDATE account
SET balance=#{item.balance}, version=version+1
WHERE id=#{item.id} AND version=#{item.version}
</foreach>
</update>
4.3 与逻辑删除的冲突
如果同时使用@TableLogic逻辑删除,执行顺序是:
- 先检查乐观锁条件
- 再执行逻辑删除更新
这意味着被逻辑删除的记录也会触发乐观锁检查。建议在查询时自动过滤已删除记录:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新
return interceptor;
}
5. 生产环境监控方案
5.1 冲突率监控
通过切面记录乐观锁失败次数:
java复制@Aspect
@Component
@Slf4j
public class OptimisticLockAspect {
@AfterThrowing(pointcut = "execution(* com..service.*.*(..))", throwing = "ex")
public void afterThrowing(OptimisticLockingFailureException ex) {
Metrics.counter("db.optimistic_lock.failure").increment();
log.warn("乐观锁冲突: {}", ex.getMessage());
}
}
5.2 报警阈值设置
建议当冲突率超过以下阈值时报警:
- 普通业务:>5%
- 核心交易:>1%
Prometheus配置示例:
yaml复制groups:
- name: db.rules
rules:
- alert: HighOptimisticLockConflict
expr: rate(db_optimistic_lock_failure_total[1m]) / rate(db_update_requests_total[1m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "高乐观锁冲突率 ({{ $value }})"
6. 进阶:分布式场景下的扩展
在分布式系统中(如使用Sharding-JDBC),乐观锁仍然有效,但需要注意:
- 版本号必须全局唯一:不能用数据库自增,推荐使用Snowflake算法生成
- 重试策略需要跨服务:可能需要引入分布式事务框架
- 监控要聚合所有实例数据
我在实际项目中采用的做法是:
- 版本号改用64位长整型
- 重试时通过Seata的AT模式保证最终一致性
- 通过ELK收集各实例日志统一分析
