MyBatis-Plus插件: 分页-sql性能分析-乐观锁

### 文章目录

  • [@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 字段实现:

  1. 取出记录时,获取当前 version
  2. 更新时,将 version + 1 作为更新条件
  3. 如果 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,符合预期。

乐观锁适用场景: 读多写少的业务。如果写操作频繁,乐观锁会导致大量重试,此时应考虑使用悲观锁。