Appearance
MyBatis-Plus 乐观锁
乐观锁用于解决并发更新问题,MyBatis-Plus 通过 @Version 注解和乐观锁插件,一行配置即可实现。
一、为什么需要乐观锁?
并发更新问题:
线程 A:读取 user.age = 25
线程 B:读取 user.age = 25
线程 A:更新 age = 26 → 提交
线程 B:更新 age = 30 → 提交(覆盖了 A 的更新!)
结果:A 的更新丢失了,这就是并发更新覆盖问题。乐观锁解决思路:
① 数据表增加 version 字段(版本号)
② 读取数据时,获取当前 version
③ 更新时,WHERE 条件带上 version
④ 更新后 version + 1
⑤ 如果 version 不匹配(别人已经改过),更新失败
UPDATE user SET age = 26, version = 2 WHERE id = 1 AND version = 1
如果 version 已经是 2(被 B 改了),这次更新影响行数为 0,更新失败二、配置方式
2.1 添加乐观锁插件
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}2.2 实体类标注
java
@Data
public class User {
private Long id;
private String name;
private Integer age;
@Version
private Integer version; // 版本号字段
}2.3 数据库加字段
sql
ALTER TABLE user ADD COLUMN version INT DEFAULT 1 COMMENT '版本号';三、使用效果
java
// 正常更新
User user = userMapper.selectById(1L);
// user: {id=1, name="张三", age=25, version=1}
user.setAge(26);
int rows = userMapper.updateById(user);
// 执行 SQL:UPDATE user SET age = 26, version = 2
// WHERE id = 1 AND version = 1
// rows = 1(更新成功)
// 并发更新时
User userA = userMapper.selectById(1L); // version=1
User userB = userMapper.selectById(1L); // version=1
userA.setAge(26);
userMapper.updateById(userA); // 成功,version 变为 2
userB.setAge(30);
int rows = userMapper.updateById(userB);
// 执行 SQL:UPDATE user SET age = 30, version = 2
// WHERE id = 1 AND version = 1
// rows = 0(更新失败,version 已经是 2 了)四、完整示例
java
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
/**
* 扣减余额(带乐观锁)
*/
@Transactional
public boolean deductBalance(Long userId, BigDecimal amount) {
// 查询用户
User user = getById(userId);
if (user.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 扣减余额
user.setBalance(user.getBalance().subtract(amount));
// 更新(带乐观锁)
int rows = userMapper.updateById(user);
if (rows == 0) {
// 版本号不匹配,说明被其他人修改了
throw new RuntimeException("并发更新失败,请重试");
}
return true;
}
/**
* 带重试的乐观锁更新
*/
public boolean deductBalanceWithRetry(Long userId, BigDecimal amount) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
User user = getById(userId);
if (user.getBalance().compareTo(amount) < 0) {
return false;
}
user.setBalance(user.getBalance().subtract(amount));
int rows = userMapper.updateById(user);
if (rows > 0) {
return true;
}
// 更新失败,重试
}
throw new RuntimeException("更新失败,超过最大重试次数");
}
}五、乐观锁 vs 悲观锁
| 维度 | 乐观锁 | 悲观锁 |
|---|---|---|
| 实现方式 | @Version + 版本号 | SELECT ... FOR UPDATE |
| 锁粒度 | 无锁,版本号校验 | 行锁 |
| 并发性能 | 高 | 低 |
| 适用场景 | 读多写少,冲突概率低 | 写多,冲突概率高 |
| 失败处理 | 重试或提示用户 | 排队等待 |
| 实现复杂度 | 低 | 中 |
六、乐观锁原理
MyBatis-Plus 乐观锁执行流程:
① 数据查询
SELECT id, name, age, version FROM user WHERE id = 1
→ version = 1
② 更新时,拦截器修改 SQL
UPDATE user SET age = 30, version = 2 WHERE id = 1 AND version = 1
③ 执行更新
→ 如果 version 匹配 → 更新成功,rows = 1
→ 如果 version 不匹配 → 更新失败,rows = 0
④ 拦截器检查更新结果
→ rows = 0 → 抛 OptimisticLockException(可选配置)七、注意事项
① 仅支持 updateById 和 update(entity, wrapper)
自定义 SQL 不会自动加乐观锁
② 支持的数据类型
int, Integer, long, Long, Date, Timestamp, LocalDateTime
③ 批量更新不支持
updateBatchById 不会触发乐观锁
④ 版本号类型选择
- int:数字递增(推荐)
- LocalDateTime:时间戳版本
⑤ 不要手动修改 version
让插件自动管理,手动修改会导致逻辑混乱八、速记总结
乐观锁三步走:
① 数据库加字段:version INT DEFAULT 1
② 插件加拦截器:OptimisticLockerInnerInterceptor
③ 实体加注解:@Version
更新流程:
SELECT → version = 1
UPDATE SET ... AND version = 1(如果 version 变了,更新 0 行)
速记口诀:
版本号字段加注解,插件拦截改 SQL,
WHERE 条件带版本,更新失败需重试。九、面试要点
| 问题 | 答案要点 |
|---|---|
| 乐观锁原理? | 版本号机制,UPDATE 时 WHERE 带 version,不匹配则失败 |
| 乐观锁和悲观锁区别? | 乐观锁无锁,版本号校验;悲观锁行锁,SELECT FOR UPDATE |
| 乐观锁失败怎么办? | 重试机制或提示用户刷新 |
| 自定义 SQL 支持乐观锁吗? | 不支持,需要手动加 version 条件 |
