[toc]
| 单词 | 词性 | 音标 | 含义 |
|---|---|---|---|
| Optimistic | adj. | /ˌɒptɪˈmɪstɪk/ | 乐观的(乐观锁假设并发冲突很少,更新时才校验) |
| Lock | n. | /lɒk/ | 锁(乐观锁通过版本号机制避免并发更新互相覆盖) |
| Version | n. | /ˈvɜːʃn/ | 版本号(@Version 注解的字段,每次更新 +1,用来判断数据是否被改过) |
| Concurrent | adj. | /kənˈkʌrənt/ | 并发的(多个请求同时操作同一条数据) |
Lecture10-乐观锁
一、问题:并发更新会丢数据
假设两个管理员同时改同一条员工数据的工资:
- 张三查到工资 8000,准备改成 9000
- 李四同时查到工资 8000,准备改成 10000
- 李四先提交成功(工资变成 10000),张三后提交(工资变成 9000)
结果:张三的提交覆盖了李四的修改,李四的更新"丢了"。这种问题叫丢失更新。
二、乐观锁的思路
基本原则:任何人都必须基于最新版修改数据
乐观锁不锁数据,而是在表里加一个 version(版本号)字段,更新时校验:
- 更新前,把读到的
version一起带上:WHERE id=? AND version=旧值 - 更新成功后,
version自动 +1 - 如果更新时
version已经变了(当前版本落后,说明被别人改过),影响行数为 0,本次更新失败,由业务决定是否重试
三、配置乐观锁
1、准备:给表加 version 字段
ALTER TABLE t_emp ADD COLUMN version INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号';
2、实体类加 @Version
@Data
@TableName("t_emp")
public class Employee {
@TableId(type = IdType.AUTO)
private Integer empId;
// ...其他字段
@Version // 乐观锁版本号字段
private Integer version;
}
3、配置类中添加乐观锁插件
在第 7 章的分页配置类中追加一行:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 分页插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁插件
return interceptor;
}
}
注意导包:
com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor
四、使用示例
1、正常更新(version 自动 +1)
@Test
public void testOptimisticLock() {
// 1. 查出来(拿到当前 version)
Employee employee = employeeMapper.selectById(1);
System.out.println("当前版本 version = " + employee.getVersion());
// 2. 修改工资
employee.setEmpSalary(new BigDecimal("9000.00"));
// 3. 更新:框架自动带上 version 条件,成功后 version+1
int rows = employeeMapper.updateById(employee);
System.out.println("rows = " + rows);
}
==> Preparing: UPDATE t_emp SET emp_no=?,emp_name=?,emp_salary=?,...,version=? WHERE emp_id=? AND version=?
==> Parameters: ..., 2(Integer), 1(Integer), 1(Integer)
<== Updates: 1
观察 SQL:SET ... version=2 + WHERE emp_id=? AND version=1。框架自动做了三件事:
- SET 部分:
version自动 +1 - WHERE 部分:自动带上「version = 读到的旧值」
- 只有影响行数为 1 才算更新成功
2、模拟并发:第二次更新失败
@Test
public void testOptimisticLockConflict() {
// 模拟两个事务同时读到了同一条数据(version 都是 1)
Employee employeeA = employeeMapper.selectById(1); // version=1
Employee employeeB = employeeMapper.selectById(1); // version=1
// A 先更新成功:version 变成 2
employeeA.setEmpSalary(new BigDecimal("9500.00"));
int rowsA = employeeMapper.updateById(employeeA);
System.out.println("A 更新影响行数 = " + rowsA); // 1
// B 再用旧 version 更新:条件 version=1 已经不满足
employeeB.setEmpSalary(new BigDecimal("10000.00"));
int rowsB = employeeMapper.updateById(employeeB);
System.out.println("B 更新影响行数 = " + rowsB); // 0,更新失败!
}
==> Preparing: UPDATE t_emp SET ...,version=? WHERE emp_id=? AND version=?
==> Parameters: ..., 2(Integer), 1(Integer), 1(Integer) ← A 的更新,version 1→2
<== Updates: 1
==> Preparing: UPDATE t_emp SET ...,version=? WHERE emp_id=? AND version=?
==> Parameters: ..., 2(Integer), 1(Integer), 1(Integer) ← B 的更新,version 已变成 2,条件不满足
<== Updates: 0 ← B 失败
3、失败后的处理:重试
业务上检测到影响行数为 0,重新读取最新数据再试一次:
@Test
public void testOptimisticLockRetry() {
Employee employeeA = employeeMapper.selectById(1);
Employee employeeB = employeeMapper.selectById(1);
// A 先更新成功
employeeA.setEmpSalary(new BigDecimal("9500.00"));
employeeMapper.updateById(employeeA);
// B 更新失败
employeeB.setEmpSalary(new BigDecimal("10000.00"));
int rowsB = employeeMapper.updateById(employeeB);
if (rowsB == 0) {
System.out.println("B 更新失败,重新读取最新数据再试一次...");
// 重新查询:拿到最新的 version=2
Employee latest = employeeMapper.selectById(1);
latest.setEmpSalary(new BigDecimal("10000.00"));
int rowsRetry = employeeMapper.updateById(latest);
System.out.println("重试结果 rows = " + rowsRetry); // 1,成功
}
}
现实项目里,重试逻辑通常配合「提示用户:数据已被他人修改,请刷新后再试」一起用。
五、注意事项
@Version字段类型推荐Integer/Long/Date/Timestamp- 乐观锁只对
updateById(entity)和update(entity, wrapper)生效 共同点是都需要传入 entity 实体类对象,而关键就是实体类对象中包含 version 值 - 逻辑删除字段
is_deleted与乐观锁version互不干扰,可以共存 - 每次更新都会带上 version 条件,必须保证 version 有值(查出再改);直接 new 一个对象 updateById 时,version 为 null 会导致条件异常,要注意
六、本章小结
- 丢失更新:并发下后提交的覆盖先提交的
- 乐观锁三步:加 version 列 → 实体加 @Version → 配置类加乐观锁插件
- 更新时自动带上 version 条件,失败影响行数为 0,业务自己处理重试
- 几乎零成本接入,是并发安全的基础配置
下一章学习 Service 层:IService 与链式操作,正式进入三层架构开发。