Spring Boot整合MyBatis Plus分页查询性能调优实战

Spring Boot整合MyBatis Plus分页查询性能调优实战

大家好,今天我们来聊聊Spring Boot整合MyBatis Plus进行分页查询的性能调优。分页查询是Web应用中非常常见的需求,但如果处理不当,很容易成为性能瓶颈。我们将从多个角度深入探讨如何优化MyBatis Plus的分页查询,包括SQL层面、MyBatis Plus配置、以及一些通用的优化策略。

一、分页查询的基础:MyBatis Plus介绍与配置

MyBatis Plus(简称MP)是MyBatis的增强工具,它在MyBatis的基础上只做增强不做改变,简化开发、提高效率。其中,分页插件是MP的核心功能之一。

1.1 引入MyBatis Plus依赖

首先,在Spring Boot项目中引入MyBatis Plus的依赖。在pom.xml文件中添加以下依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>  <!-- 使用最新稳定版本 -->
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

1.2 配置MyBatis Plus拦截器

接下来,配置MyBatis Plus的分页拦截器。创建一个配置类,例如MybatisPlusConfig.java

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

这个配置类注册了一个MybatisPlusInterceptor,并向其中添加了PaginationInnerInterceptor,这是MP的分页核心拦截器。

1.3 编写Mapper接口与XML文件

假设我们有一个User实体类,以及对应的UserMapper接口:

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("user") // 对应数据库表名
public class User {
    @TableId(type = IdType.AUTO) // 主键自增
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 可以自定义SQL方法,也可以使用MP提供的默认方法
}

UserMapper继承了BaseMapper<User>,MP会自动提供常用的CRUD方法。 如果需要自定义SQL,可以在UserMapper.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.example.demo.mapper.UserMapper">

    <!-- 自定义SQL示例 -->
    <select id="selectUserByName" resultType="com.example.demo.entity.User">
        SELECT * FROM user WHERE name = #{name}
    </select>

</mapper>

1.4 使用Page对象进行分页查询

在Service层,我们可以使用MP提供的Page对象进行分页查询:

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public IPage<User> getUserPage(int pageNum, int pageSize) {
        Page<User> page = new Page<>(pageNum, pageSize); // 当前页,每页记录数
        return userMapper.selectPage(page, null); // 第一个参数是Page对象,第二个参数是查询条件Wrapper,null表示无条件
    }
}

Page对象包含了当前页码、每页记录数等信息。selectPage方法会将查询结果封装到Page对象中,包括总记录数、总页数等。

二、SQL层面优化:避免全表扫描

最直接有效的优化方式就是优化SQL语句,避免全表扫描,充分利用索引。

2.1 添加合适的索引

对于经常用于查询条件的字段,务必添加索引。例如,如果经常根据name字段进行查询,则需要添加name字段的索引。

ALTER TABLE user ADD INDEX idx_name (name);

2.2 避免使用SELECT *

尽量只查询需要的字段,避免使用SELECT *。这样可以减少数据传输量,提高查询效率。 修改Mapper XML文件或者使用MP提供的Wrapper对象指定需要查询的字段。

  • 修改Mapper XML文件:
<select id="selectUserPage" resultType="com.example.demo.entity.User">
    SELECT id, name, age FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
    </where>
    LIMIT #{offset}, #{pageSize}
</select>

然后在UserMapper.java中定义对应的方法:

IPage<User> selectUserPage(Page<User> page, @Param("name") String name);
  • 使用MP提供的Wrapper对象:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public IPage<User> getUserPage(int pageNum, int pageSize, String name) {
        Page<User> page = new Page<>(pageNum, pageSize);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "name", "age"); // 指定查询字段
        if (name != null && !name.isEmpty()) {
            queryWrapper.like("name", name);
        }
        return userMapper.selectPage(page, queryWrapper);
    }
}

2.3 使用EXPLAIN分析SQL语句

使用EXPLAIN命令分析SQL语句的执行计划,可以帮助我们判断SQL语句是否使用了索引,以及是否进行了全表扫描。

EXPLAIN SELECT * FROM user WHERE name = '张三';

通过分析EXPLAIN的结果,可以了解SQL语句的性能瓶颈,并进行相应的优化。 EXPLAIN 结果中的 type 字段很重要:

  • system: 表只有一行记录,这是const类型的特例,平时不会出现,这个可以忽略不计。
  • const: 表示通过索引一次就找到了,const用于比较primary key 或者 unique索引。因为只匹配一行数据,所以很快。
  • eq_ref: 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键 或 唯一索引。
  • ref: 非唯一性索引扫描,返回匹配某个单独值的所有行。
  • range: 只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等查询。
  • index: Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然使用了索引,但是扫描了整个索引)。
  • ALL: Full Table Scan,即全表扫描,意味着mysql需要从头到尾去查找所需要的行。通常情况下这需要增加索引来进行优化。

一般来说,我们需要尽量避免ALLindex类型的出现。

2.4 优化LIKE查询

LIKE查询如果以%开头,会导致索引失效,进行全表扫描。尽量避免这种情况。

  • 如果必须使用LIKE查询,尽量将%放在后面:
SELECT * FROM user WHERE name LIKE '张三%';  -- 使用索引
  • 如果%必须放在前面,可以考虑使用全文索引或者其他搜索技术(例如Elasticsearch)。

2.5 避免在WHERE子句中使用函数或表达式

WHERE子句中使用函数或表达式会导致索引失效。例如:

SELECT * FROM user WHERE YEAR(create_time) = 2023;  -- 索引失效

应该尽量避免这种情况,可以将函数或表达式移到等号右边:

SELECT * FROM user WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01';  -- 使用索引

三、MyBatis Plus配置优化:提升整体性能

除了SQL层面,MyBatis Plus的配置也会影响分页查询的性能。

3.1 开启二级缓存

MyBatis Plus默认支持二级缓存,可以减少数据库的访问次数,提高查询效率。

application.propertiesapplication.yml中开启二级缓存:

mybatis-plus.configuration.cache-enabled=true

需要在实体类上添加@KeySequence注解,指定缓存的键序列。 (这个注解在较新版本可能已经被移除,需要配置合适的缓存实现,如Redis)

import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("user")
//@KeySequence("seq_user")  // 适配 Oracle 等数据库的序列
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

注意: 二级缓存适用于读多写少的场景。如果数据更新频繁,使用二级缓存可能会导致数据不一致。

3.2 批量操作优化

对于批量插入、更新等操作,可以使用MyBatis Plus提供的批量操作方法,减少与数据库的交互次数。

  • 批量插入:
List<User> userList = new ArrayList<>();
// ... 填充userList
userService.saveBatch(userList);
  • 批量更新:
List<User> userList = new ArrayList<>();
// ... 填充userList
userService.updateBatchById(userList);

3.3 合理设置pageSize

pageSize(每页记录数)的选择也会影响分页查询的性能。

  • pageSize过小: 导致频繁的数据库交互,增加网络开销。
  • pageSize过大: 导致单次查询的数据量过大,增加内存开销。

应该根据实际情况选择合适的pageSize,通常建议在10-50之间。可以通过压测来确定最佳的pageSize

四、通用优化策略:减少数据库压力

除了SQL层面和MyBatis Plus配置,还可以从以下几个方面进行优化,减少数据库压力。

4.1 缓存热点数据

对于经常访问的热点数据,可以使用缓存(例如Redis)进行缓存,减少数据库的访问次数。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    private static final String USER_CACHE_PREFIX = "user:";

    public User getUserById(Long id) {
        String key = USER_CACHE_PREFIX + id;
        User user = redisTemplate.opsForValue().get(key);
        if (user == null) {
            user = userMapper.selectById(id);
            if (user != null) {
                redisTemplate.opsForValue().set(key, user);
            }
        }
        return user;
    }
}

4.2 读写分离

如果数据库读操作远多于写操作,可以考虑使用读写分离技术,将读操作和写操作分发到不同的数据库服务器上,提高系统的并发能力。

4.3 异步处理

对于非实时的操作,可以使用异步处理技术(例如消息队列),将操作放入队列中,由后台任务异步执行,减少对数据库的直接访问。

4.4 数据预热

在系统启动时,可以预先加载一些常用的数据到缓存中,避免用户首次访问时需要从数据库加载数据。

五、真实案例分析

假设我们有一个电商网站,需要分页展示商品列表。商品表product包含以下字段:

  • id: 商品ID(主键,自增)
  • name: 商品名称
  • category_id: 商品分类ID
  • price: 商品价格
  • create_time: 创建时间

用户经常根据商品名称和分类ID进行查询。

5.1 优化前的SQL语句

SELECT * FROM product WHERE name LIKE '%keyword%' AND category_id = 123 LIMIT 0, 20;

该SQL语句存在以下问题:

  • 使用了SELECT *,查询了所有字段。
  • LIKE '%keyword%'导致索引失效。

5.2 优化后的SQL语句

SELECT id, name, price FROM product WHERE name LIKE 'keyword%' AND category_id = 123 LIMIT 0, 20;

优化后的SQL语句:

  • 只查询需要的字段(id, name, price)。
  • LIKE查询改为LIKE 'keyword%',使用索引。

5.3 添加索引

namecategory_id字段添加索引:

ALTER TABLE product ADD INDEX idx_name (name);
ALTER TABLE product ADD INDEX idx_category_id (category_id);

或者创建联合索引:

ALTER TABLE product ADD INDEX idx_name_category_id (name, category_id);

选择联合索引还是单列索引,需要根据实际情况进行权衡。通常情况下,如果查询条件中同时包含了多个字段,使用联合索引的效率更高。

5.4 缓存热点商品数据

将经常访问的商品数据缓存到Redis中,减少数据库的访问次数。

5.5 压测与监控

在优化完成后,需要进行压测,验证优化效果。同时,需要对数据库进行监控,及时发现性能瓶颈。

六、总结与思考

本讲座我们深入探讨了Spring Boot整合MyBatis Plus进行分页查询的性能调优。从SQL层面、MyBatis Plus配置、以及通用优化策略等多个角度进行了分析。

记住,性能优化是一个持续的过程,需要不断地分析和调整。希望今天的分享能帮助大家在实际项目中更好地优化分页查询的性能。

实践是检验真理的唯一标准

本文介绍的各种优化手段需要根据实际场景进行选择和调整,通过监控和压测来验证优化效果,才能真正提升系统的性能。没有银弹,需要不断学习和实践。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注