1. MyBatisPlus乐观锁插件的工作机制
乐观锁作为并发控制的重要手段,在MyBatisPlus中通过@Version注解和OptimisticLockerInterceptor拦截器协同实现。其核心原理可以概括为:在读取数据时获取版本号,更新时自动校验版本号一致性。
1.1 版本号字段的识别与处理
MyBatisPlus通过实体类的@Version注解识别版本号字段。这个注解有几个关键特性:
- 支持Integer/Long/Timestamp等数值类型字段
- 每个实体类最多只能有一个@Version注解字段
- 字段值由MyBatisPlus自动维护,开发者不应手动赋值
源码中对应的处理逻辑位于com.baomidou.mybatisplus.core.metadata.TableInfo类的initVersionFieldIfNeed()方法。该方法会在应用启动时扫描实体类,检测@Version注解并缓存版本字段信息。
实际开发中常见的一个坑是忘记在数据库表中创建对应的version字段。虽然应用启动不会报错,但执行更新操作时会抛出SQL异常。
1.2 SQL语句的动态改写
OptimisticLockerInterceptor拦截器会在update操作执行前介入,通过processUpdate()方法实现SQL改写。核心改写逻辑包括:
- 检测当前操作是否需要乐观锁处理(有版本字段且非null值)
- 在WHERE条件中追加版本号条件(原SQL的WHERE条件后加上AND version=oldVersion)
- 在SET语句中追加版本号自增(原SET语句后加上version=version+1)
java复制// 简化后的改写逻辑示例
String originalSql = "UPDATE user SET name=? WHERE id=?";
String newSql = "UPDATE user SET name=?, version=version+1 WHERE id=? AND version=?";
这种改写方式确保了:
- 版本号校验和更新是原子操作
- 即使原SQL没有WHERE条件也能正常工作
- 与其他拦截器(如分页、逻辑删除)兼容
2. 乐观锁冲突的检测与处理机制
2.1 更新结果验证逻辑
MyBatisPlus通过检查SQL执行影响行数来判断乐观锁是否冲突:
- 影响行数为1:更新成功,版本号已递增
- 影响行数为0:版本号不匹配,数据已被其他事务修改
在OptimisticLockerInnerInterceptor中,相关验证逻辑如下:
java复制if (effect == 0) {
throw new OptimisticLockException("乐观锁更新失败,记录已被其他事务修改");
}
2.2 异常处理最佳实践
实际项目中建议采用以下方式处理乐观锁冲突:
- 捕获OptimisticLockException异常
- 获取最新数据并重新执行业务逻辑
- 设置最大重试次数避免死循环
java复制int retry = 3;
while (retry-- > 0) {
try {
userService.updateById(user);
break;
} catch (OptimisticLockException e) {
user = userService.getById(user.getId());
// 重新计算业务数据...
}
}
在分布式系统中,单纯依靠乐观锁可能不够,通常需要结合分布式锁或状态机设计来保证强一致性。
3. 插件扩展点与自定义实现
3.1 自定义版本号生成策略
默认的版本号自增策略(version+1)可以通过实现IVersionOptimisticLock接口来扩展:
java复制public class TimestampVersionLock implements IVersionOptimisticLock {
@Override
public Object nextVersion(Object currentVersion) {
return System.currentTimeMillis();
}
}
// 配置使用自定义策略
mybatisPlusInterceptor.setOptimisticLocker(new TimestampVersionLock());
这种扩展适用于需要更细粒度时间戳控制的场景,但要注意高并发下可能的时间戳冲突问题。
3.2 与其他插件的执行顺序
MyBatisPlus多个拦截器是通过责任链模式组织的,执行顺序很重要。乐观锁插件应该在其他更新相关插件之后执行:
java复制MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 先添加其他拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 最后添加乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
错误的顺序可能导致SQL改写异常,比如分页插件在乐观锁之后执行会导致分页条件被错误处理。
4. 性能优化与生产实践
4.1 批量操作的优化处理
默认情况下,乐观锁插件会对每个update语句单独处理。对于批量更新操作(如updateBatchById),这会导致多次版本校验和SQL改写。可以通过以下方式优化:
- 自定义批量更新方法,在Service层实现批量版本校验
- 使用@SqlParser(filter=true)临时关闭SQL解析
- 对于确定无并发冲突的批量操作,直接使用baseMapper执行
java复制@Transactional
public void batchUpdate(List<User> users) {
// 预先检查所有记录的版本号
Map<Long, Integer> versions = users.stream()
.collect(Collectors.toMap(User::getId, User::getVersion));
List<User> dbUsers = listByIds(versions.keySet());
for (User dbUser : dbUsers) {
if (dbUser.getVersion() != versions.get(dbUser.getId())) {
throw new OptimisticLockException();
}
}
// 执行批量更新(不使用自动乐观锁)
baseMapper.updateBatchById(users);
}
4.2 监控与统计实现
生产环境建议添加乐观锁冲突监控:
java复制@Aspect
@Component
public class OptimisticLockMonitor {
@AfterThrowing(pointcut = "execution(* com..service.*.*(..))",
throwing = "ex")
public void monitorLockConflict(OptimisticLockException ex) {
Metrics.counter("optimistic.lock.conflict").increment();
// 发送告警或记录详细日志...
}
}
关键监控指标应包括:
- 冲突率(冲突次数/总更新次数)
- 冲突热点数据(哪些记录频繁冲突)
- 重试成功率
这些数据可以帮助调整业务逻辑或考虑改用悲观锁方案。
5. 源码级深度解析
5.1 关键类结构分析
MyBatisPlus乐观锁实现涉及的核心类:
-
OptimisticLockerInnerInterceptor:主拦截器- 继承自
InnerInterceptor抽象类 - 通过
beforeUpdate()方法介入SQL执行流程
- 继承自
-
VersionMeta:版本号元数据- 缓存@Version字段信息
- 提供字段类型检查和值获取方法
-
SqlParserHelper:SQL解析工具- 识别需要乐观锁处理的SQL语句
- 辅助构建新的SQL语句
5.2 SQL改写算法细节
完整的SQL改写过程包括:
- 参数位置重计算:
java复制// 原SQL参数位置: [nameParam, idParam]
// 改写后参数位置:[nameParam, idParam, versionParam]
- WHERE条件拼接:
sql复制/* 原WHERE */ WHERE id = ?
/* 新WHERE */ WHERE id = ? AND version = ?
- SET语句追加:
sql复制/* 原SET */ SET name = ?
/* 新SET */ SET name = ?, version = version + 1
这种改写需要精确处理参数位置偏移,是插件中最复杂的部分。
5.3 与MyBatis原生机制的协作
乐观锁插件通过MyBatis的Interceptor接口挂接到执行流程中。关键执行点:
Executor.update()方法被调用时- 通过
Plugin.wrap()创建的代理链 - 最终调用
StatementHandler执行SQL
整个过程保持了与MyBatis其他组件的良好兼容性,不会影响缓存、事务等基础功能。
6. 常见问题排查指南
6.1 版本号不更新的情况排查
如果发现版本号没有按预期更新,检查以下方面:
- 实体类字段未加@Version注解
- 数据库字段名与实体类字段名不匹配
- 拦截器未正确配置(缺少OptimisticLockerInnerInterceptor)
- 使用了自定义SQL(@Update注解)而未手动处理版本号
可以通过开启MyBatisPlus的SQL日志来确认最终执行的SQL语句:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
6.2 与逻辑删除的冲突解决
当同时使用乐观锁和逻辑删除时,可能出现WHERE条件顺序问题。解决方案:
- 确保拦截器顺序正确(先逻辑删除,后乐观锁)
- 自定义SQL注入器统一处理条件
- 对于复杂场景,使用@SqlParser注解临时禁用自动处理
java复制@SqlParser(filter = true)
public void updateWithComplexCondition(User user) {
userMapper.updateById(user);
}
6.3 分布式环境下的注意事项
在分布式系统中使用乐观锁需要额外考虑:
- 时钟同步问题(如果使用时间戳作为版本号)
- 跨服务更新的版本一致性
- 与分布式事务的配合使用
建议方案:
- 采用集中式版本号服务
- 结合业务状态机设计
- 关键操作添加分布式锁作为补充
7. 扩展应用场景
7.1 实现软状态控制
除了并发控制,乐观锁机制还可用于实现状态机控制:
java复制@Version
private Integer status; // 0-待处理 1-处理中 2-已完成
public void markProcessing() {
if (this.status != 0) {
throw new IllegalStateException();
}
this.status = 1;
}
这种模式可以确保状态转换的原子性,避免重复处理等问题。
7.2 审计日志的乐观记录
结合乐观锁实现无锁的审计日志记录:
java复制@Version
private Long recordVersion;
public void addLog(String content) {
// 不需要加锁,通过版本号冲突检测并发修改
auditLogMapper.insert(new AuditLog(content));
this.recordVersion = getNewVersion();
}
这种方式特别适合高频读、低频写的审计场景。
7.3 与缓存的一致性保障
在使用Redis等缓存时,可以通过版本号实现缓存一致性:
- 查询时同时获取数据版本号
- 更新时校验缓存中的版本号
- 数据变更时淘汰对应缓存
java复制@CacheEvict(key = "'user:' + #user.id",
condition = "#result > 0")
public int updateWithCache(User user) {
return userMapper.updateById(user);
}
这种模式可以有效避免缓存雪崩和脏读问题。
