MyBatis-Plus插件: 分页-sql性能分析-乐观锁
2026/8/6 4:01:27 网站建设 项目流程

文章目录

    • @[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

@ConfigurationpublicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}}

DbType.MYSQL指定数据库类型,插件会根据不同数据库生成对应的分页 SQL(MySQL 使用LIMIT,Oracle 使用ROWNUM等)。

1.2 内置分页查询

使用selectPage()方法进行分页查询,传入Page对象指定分页参数:

@TestpublicvoidtestPage(){// 查询第1页,每页5条Page<User>page=newPage<>(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 接口定义方法
publicinterfaceUserMapperextendsBaseMapper<User>{/** * 根据年龄查询用户列表,分页显示 * @param page 分页对象,必须放在第一个参数位置,传入后自动分页 * @param age 年龄 */Page<User>selectPageVo(@Param("page")Page<User>page,@Param("age")Integerage);}

Page参数必须放在第一个位置,插件会自动识别并拼接分页 SQL。

1.4.2 编写 XML 映射文件

resources/com/xq/mapper/目录下创建UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.xq.mapper.UserMapper"><!--SQL片段,记录基础字段--><sqlid="BaseColumns">uid, name, age, email</sql><!--Page<User> selectPageVo(Page<User> page, Integer age);--><selectid="selectPageVo"resultType="com.xq.pojo.User">SELECT<includerefid="BaseColumns"></include>FROM tb_user WHERE age > #{age}</select></mapper>

XML 中只需要写业务 SQL,不需要写LIMIT,分页插件会自动追加。

1.4.3 测试自定义分页
@TestpublicvoidtestSelectPageVo(){Page<User>page=newPage<>(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:

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus</artifactId><version>3.1.1</version></dependency>

在 MyBatis 核心配置文件mybatis-config.xml中引入插件:

<configuration><plugins><!-- SQL 执行性能分析,开发环境使用,线上不推荐 --><plugininterceptor="com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor"><!-- sql 最大执行时长(毫秒),超时抛异常 --><propertyname="maxTime"value="100"/><!-- SQL 是否格式化输出,默认 false --><propertyname="format"value="true"/></plugin></plugins></configuration>
配置项说明
maxTimeSQL 最大允许执行时间(毫秒),超时抛出异常
format是否格式化输出 SQL,设为true可读性更好

配置后在applicationContext.xml中引入该配置文件即可生效。如果将maxTime设置过小,执行 SQL 时会直接报错,便于在开发阶段发现慢查询。


3 乐观锁插件

3.1 问题场景:并发修改导致数据覆盖

两个操作员同时读取了商品价格 100 元:

  • 小李将价格 +50,写入 150 元
  • 小王将价格 -30,写入 70 元

由于小王的写入在小李之后,小李的修改被覆盖,最终价格为 70 元(低于成本价 80 元)。

操作顺序操作人读取价格操作写入价格
1小李100+50150
2小王100-3070
期望结果---120
实际结果---70(小李的操作被覆盖)

针对这种并发冲突,有两种解决思路:

  • 悲观锁:小李操作时锁定数据,小王必须等小李操作完才能修改
  • 乐观锁:不加锁,但在更新时检查数据是否被其他人修改过,如果被修改过则更新失败

3.2 模拟修改冲突

3.2.1 建表与实体类
CREATETABLEt_product(idBIGINT(20)NOTNULLCOMMENT'主键ID',NAMEVARCHAR(30)NULLDEFAULTNULLCOMMENT'商品名称',priceINT(11)DEFAULT0COMMENT'价格',VERSIONINT(11)DEFAULT0COMMENT'乐观锁版本号',PRIMARYKEY(id));INSERTINTOt_product(id,NAME,price)VALUES(1,'外星人笔记本',100);
@Data@NoArgsConstructor@AllArgsConstructor@TableName("t_product")publicclassProduct{privateLongid;privateStringname;privateIntegerprice;privateIntegerversion;}
publicinterfaceProductMapperextendsBaseMapper<Product>{}
3.2.2 测试并发修改
@AutowiredProductMapperproductMapper;@TestpublicvoidtestConcurrentUpdate(){// 1、小李取出商品价格Productp1=productMapper.selectById(1L);System.out.println("小李取出的价格:"+p1.getPrice());// 2、小王取出商品价格Productp2=productMapper.selectById(1L);System.out.println("小王取出的价格:"+p2.getPrice());// 3、小李将价格加了50元,存入数据库p1.setPrice(p1.getPrice()+50);intresult1=productMapper.updateById(p1);System.out.println("小李修改结果:"+result1);// 4、小王将商品减了30元,存入数据库p2.setPrice(p2.getPrice()-30);intresult2=productMapper.updateById(p2);System.out.println("小王修改结果:"+result2);// 最终结果Productp3=productMapper.selectById(1L);// 价格被覆盖,结果为70System.out.println("最后的结果:"+p3.getPrice());}

未使用乐观锁时,小王的修改直接覆盖了小李的修改,最终价格为 70。

3.3 乐观锁实现原理

乐观锁通过version字段实现:

  1. 取出记录时,获取当前version
  2. 更新时,将version + 1作为更新条件
  3. 如果WHERE中的version与数据库中的不一致,说明数据已被其他人修改,更新失败(影响行数为 0)
-- 取出记录,version = 1SELECTid,name,price,versionFROMt_productWHEREid=1;-- 更新时携带 version 条件UPDATEt_productSETprice=?,version=version+1WHEREid=1ANDversion=1;

3.4 MyBatis-Plus 实现乐观锁

3.4.1 添加 @Version 注解

在实体类的版本号字段上添加@Version注解:

@Data@NoArgsConstructor@AllArgsConstructor@TableName("t_product")publicclassProduct{privateLongid;privateStringname;privateIntegerprice;@Version// 标识乐观锁版本号字段privateIntegerversion;}
3.4.2 添加乐观锁插件配置

在配置类中注册OptimisticLockerInnerInterceptor

@ConfigurationpublicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));// 乐观锁插件interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}}
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 失败重试优化

小王修改失败后,可以重新查询最新数据再次尝试更新,确保两次操作都生效:

@TestpublicvoidtestConcurrentVersionUpdate(){// 小李取数据Productp1=productMapper.selectById(1L);// 小王取数据Productp2=productMapper.selectById(1L);// 小李修改 +50p1.setPrice(p1.getPrice()+50);intresult1=productMapper.updateById(p1);System.out.println("小李修改的结果:"+result1);// 小王修改 -30p2.setPrice(p2.getPrice()-30);intresult2=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);// 老板看价格Productp3=productMapper.selectById(1L);// 最终结果:120(100 + 50 - 30)System.out.println("老板看价格:"+p3.getPrice());}

重试后小王基于最新价格 150 进行 -30 操作,最终价格为120,符合预期。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询