### 文章目录
- [@toc](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1 分页插件](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.1 添加分页插件配置](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.2 内置分页查询](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.3 Page 对象常用方法](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.4 自定义 XML 分页](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.4.1 Mapper 接口定义方法](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.4.2 编写 XML 映射文件](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [1.4.3 测试自定义分页](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [2 SQL 性能分析插件](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3 乐观锁插件](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.1 问题场景:并发修改导致数据覆盖](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.2 模拟修改冲突](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.2.1 建表与实体类](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.2.2 测试并发修改](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.3 乐观锁实现原理](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.4 MyBatis-Plus 实现乐观锁](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.4.1 添加 @Version 注解](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.4.2 添加乐观锁插件配置](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.4.3 测试乐观锁效果](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
- [3.5 失败重试优化](#文章目录 @[toc] 1 分页插件 1.1 添加分页插件配置 1.2 内置分页查询 1.3 Page 对象常用方法 1.4 自定义 XML 分页 1.4.1 Mapper 接口定义方法 1.4.2 编写 XML 映射文件 1.4.3 测试自定义分页 2 SQL 性能分析插件 3 乐观锁插件 3.1 问题场景:并发修改导致数据覆盖 3.2 模拟修改冲突 3.2.1 建表与实体类 3.2.2 测试并发修改 3.3 乐观锁实现原理 3.4 MyBatis-Plus 实现乐观锁 3.4.1 添加 @Version 注解 3.4.2 添加乐观锁插件配置 3.4.3 测试乐观锁效果 3.5 失败重试优化)
1 分页插件
MyBatis-Plus 内置了分页插件,配置后即可使用,无需引入额外依赖。
1.1 添加分页插件配置
创建配置类,注册 PaginationInnerInterceptor:
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
DbType.MYSQL 指定数据库类型,插件会根据不同数据库生成对应的分页 SQL(MySQL 使用 LIMIT,Oracle 使用 ROWNUM 等)。
1.2 内置分页查询
使用 selectPage() 方法进行分页查询,传入 Page 对象指定分页参数:
java
@Test
public void testPage() {
// 查询第1页,每页5条
Page<User> page = new Page<>(1, 5);
userMapper.selectPage(page, null);
// 获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页显示的条数:" + page.getSize());
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
}
selectPage() 的第二个参数为条件构造器,传入 null 表示不附加查询条件。
1.3 Page 对象常用方法
| 方法 | 返回类型 | 说明 |
|---|---|---|
getRecords() |
List<T> |
获取当前页的数据列表 |
getCurrent() |
long |
获取当前页码 |
getSize() |
long |
获取每页显示条数 |
getTotal() |
long |
获取总记录数 |
getPages() |
long |
获取总页数 |
hasPrevious() |
boolean |
是否有上一页 |
hasNext() |
boolean |
是否有下一页 |
1.4 自定义 XML 分页
内置的 selectPage() 只能做简单的全表分页。如果需要带自定义条件的分页查询,可以在 Mapper 接口中定义方法,配合 XML 编写 SQL,分页逻辑由插件自动处理。
1.4.1 Mapper 接口定义方法
java
public interface UserMapper extends BaseMapper<User> {
/**
* 根据年龄查询用户列表,分页显示
* @param page 分页对象,必须放在第一个参数位置,传入后自动分页
* @param age 年龄
*/
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
}
Page 参数必须放在第一个位置,插件会自动识别并拼接分页 SQL。
1.4.2 编写 XML 映射文件
在 resources/com/xq/mapper/ 目录下创建 UserMapper.xml:
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xq.mapper.UserMapper">
<!--SQL片段,记录基础字段-->
<sql id="BaseColumns">uid, name, age, email</sql>
<!--Page<User> selectPageVo(Page<User> page, Integer age);-->
<select id="selectPageVo" resultType="com.xq.pojo.User">
SELECT <include refid="BaseColumns"></include> FROM tb_user WHERE age > #{age}
</select>
</mapper>
XML 中只需要写业务 SQL,不需要写 LIMIT,分页插件会自动追加。
1.4.3 测试自定义分页
java
@Test
public void testSelectPageVo() {
Page<User> page = new Page<>(1, 5);
userMapper.selectPageVo(page, 20);
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页显示的条数:" + page.getSize());
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
}
2 SQL 性能分析插件
PerformanceInterceptor 用于输出每条 SQL 的执行时间,可设置最大执行时长,超时则抛出异常。该插件仅用于开发环境调试,不建议在生产环境使用。
注意: 高版本的 MyBatis-Plus(3.2+)已移除此插件。如需使用,需将 MyBatis-Plus 版本降至 3.1.x:
xml<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.1.1</version> </dependency>
在 MyBatis 核心配置文件 mybatis-config.xml 中引入插件:
xml
<configuration>
<plugins>
<!-- SQL 执行性能分析,开发环境使用,线上不推荐 -->
<plugin interceptor="com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor">
<!-- sql 最大执行时长(毫秒),超时抛异常 -->
<property name="maxTime" value="100" />
<!-- SQL 是否格式化输出,默认 false -->
<property name="format" value="true" />
</plugin>
</plugins>
</configuration>
| 配置项 | 说明 |
|---|---|
maxTime |
SQL 最大允许执行时间(毫秒),超时抛出异常 |
format |
是否格式化输出 SQL,设为 true 可读性更好 |
配置后在 applicationContext.xml 中引入该配置文件即可生效。如果将 maxTime 设置过小,执行 SQL 时会直接报错,便于在开发阶段发现慢查询。
3 乐观锁插件
3.1 问题场景:并发修改导致数据覆盖
两个操作员同时读取了商品价格 100 元:
- 小李将价格 +50,写入 150 元
- 小王将价格 -30,写入 70 元
由于小王的写入在小李之后,小李的修改被覆盖,最终价格为 70 元(低于成本价 80 元)。
| 操作顺序 | 操作人 | 读取价格 | 操作 | 写入价格 |
|---|---|---|---|---|
| 1 | 小李 | 100 | +50 | 150 |
| 2 | 小王 | 100 | -30 | 70 |
| 期望结果 | - | - | - | 120 |
| 实际结果 | - | - | - | 70(小李的操作被覆盖) |
针对这种并发冲突,有两种解决思路:
- 悲观锁:小李操作时锁定数据,小王必须等小李操作完才能修改
- 乐观锁:不加锁,但在更新时检查数据是否被其他人修改过,如果被修改过则更新失败
3.2 模拟修改冲突
3.2.1 建表与实体类
sql
CREATE TABLE t_product (
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
price INT(11) DEFAULT 0 COMMENT '价格',
VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
);
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本', 100);
java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_product")
public class Product {
private Long id;
private String name;
private Integer price;
private Integer version;
}
java
public interface ProductMapper extends BaseMapper<Product> {
}
3.2.2 测试并发修改
java
@Autowired
ProductMapper productMapper;
@Test
public void testConcurrentUpdate() {
// 1、小李取出商品价格
Product p1 = productMapper.selectById(1L);
System.out.println("小李取出的价格:" + p1.getPrice());
// 2、小王取出商品价格
Product p2 = productMapper.selectById(1L);
System.out.println("小王取出的价格:" + p2.getPrice());
// 3、小李将价格加了50元,存入数据库
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1);
System.out.println("小李修改结果:" + result1);
// 4、小王将商品减了30元,存入数据库
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("小王修改结果:" + result2);
// 最终结果
Product p3 = productMapper.selectById(1L);
// 价格被覆盖,结果为70
System.out.println("最后的结果:" + p3.getPrice());
}
未使用乐观锁时,小王的修改直接覆盖了小李的修改,最终价格为 70。
3.3 乐观锁实现原理
乐观锁通过 version 字段实现:
- 取出记录时,获取当前
version值 - 更新时,将
version + 1作为更新条件 - 如果
WHERE中的version与数据库中的不一致,说明数据已被其他人修改,更新失败(影响行数为 0)
sql
-- 取出记录,version = 1
SELECT id, name, price, version FROM t_product WHERE id = 1;
-- 更新时携带 version 条件
UPDATE t_product SET price = ?, version = version + 1 WHERE id = 1 AND version = 1;
3.4 MyBatis-Plus 实现乐观锁
3.4.1 添加 @Version 注解
在实体类的版本号字段上添加 @Version 注解:
java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_product")
public class Product {
private Long id;
private String name;
private Integer price;
@Version // 标识乐观锁版本号字段
private Integer version;
}
3.4.2 添加乐观锁插件配置
在配置类中注册 OptimisticLockerInnerInterceptor:
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;
}
}
3.4.3 测试乐观锁效果
配置完成后,再次执行并发修改测试,MyBatis-Plus 生成的 SQL 会自动携带 version 条件:
| 步骤 | 操作人 | 执行 SQL | 结果 |
|---|---|---|---|
| ① | 小李查询 | SELECT id,name,price,version FROM t_product WHERE id=? |
price=100, version=0 |
| ② | 小王查询 | SELECT id,name,price,version FROM t_product WHERE id=? |
price=100, version=0 |
| ③ | 小李修改 | UPDATE t_product SET name=?, price=150, version=1 WHERE id=1 AND version=0 |
成功,version 变为 1 |
| ④ | 小王修改 | UPDATE t_product SET name=?, price=70, version=1 WHERE id=1 AND version=0 |
失败,version 已不是 0 |
小王的更新因 version 不匹配而失败(影响行数为 0),最终价格为 150。
3.5 失败重试优化
小王修改失败后,可以重新查询最新数据再次尝试更新,确保两次操作都生效:
java
@Test
public void testConcurrentVersionUpdate() {
// 小李取数据
Product p1 = productMapper.selectById(1L);
// 小王取数据
Product p2 = productMapper.selectById(1L);
// 小李修改 +50
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1);
System.out.println("小李修改的结果:" + result1);
// 小王修改 -30
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("小王修改的结果:" + result2);
if (result2 == 0) {
// 失败重试:重新获取最新数据(含最新 version),再次更新
p2 = productMapper.selectById(1L);
p2.setPrice(p2.getPrice() - 30);
result2 = productMapper.updateById(p2);
}
System.out.println("小王修改重试的结果:" + result2);
// 老板看价格
Product p3 = productMapper.selectById(1L);
// 最终结果:120(100 + 50 - 30)
System.out.println("老板看价格:" + p3.getPrice());
}
重试后小王基于最新价格 150 进行 -30 操作,最终价格为 120,符合预期。
乐观锁适用场景: 读多写少的业务。如果写操作频繁,乐观锁会导致大量重试,此时应考虑使用悲观锁。