深入骨髓!MyBatis二级缓存实战指南

简介: 本文全面解析MyBatis二级缓存的核心原理与实践应用。作为Mapper级别的缓存机制,二级缓存能有效降低数据库压力,提升查询性能。文章详细介绍了二级缓存的启用配置、工作流程、源码实现及事务一致性机制,并针对分布式环境提出了Redis集成方案。同时总结了适用场景与禁用场景,提供缓存策略选择建议,强调数据一致性的保障措施。最后给出最佳实践指南,包括缓存容量设置、性能优化技巧及常见问题解决方案,帮助开发者合理利用二级缓存实现性能优化。

一、引言:为什么MyBatis二级缓存是性能优化的关键?

在Java后端开发中,数据库交互是性能瓶颈的核心来源之一。MyBatis作为主流的持久层框架,提供了两级缓存机制来减轻数据库压力、提升查询效率。其中,一级缓存(SqlSession级缓存)默认开启,但其作用域仅限于单个SqlSession,无法实现跨会话共享数据,在分布式或多用户场景下局限性显著。

而二级缓存(Mapper级缓存)作为一级缓存的补充,作用域提升至Mapper接口级别,支持多个SqlSession共享缓存数据,尤其适用于查询频率高、数据变更少的场景(如字典表、配置表查询)。据统计,合理使用二级缓存可使高频查询接口的响应时间降低50%以上,数据库QPS峰值减少30%-60%,是企业级应用性能优化的关键手段。

二、MyBatis二级缓存核心原理:一文读懂底层逻辑

2.1 二级缓存的核心定义与作用域

MyBatis二级缓存是Mapper接口级别的缓存,即同一个Mapper接口下的所有方法共享同一个缓存空间。其核心作用是:当多个SqlSession查询同一Mapper下的同一数据时,只需第一次查询数据库,后续查询可直接从缓存中获取数据,避免重复执行SQL语句。

这里需要明确区分一级缓存与二级缓存的核心差异,避免混淆:

特性 一级缓存(SqlSession级) 二级缓存(Mapper级)
作用域 单个SqlSession 单个Mapper接口(跨SqlSession)
默认状态 开启(不可关闭) 关闭(需手动开启)
数据存储位置 JVM内存(SqlSession内部) JVM内存/分布式缓存(如Redis)
共享性 仅当前SqlSession内共享 同一应用内所有SqlSession共享
失效场景 SqlSession关闭/提交/回滚/增删改 对应Mapper执行增删改/缓存过期/手动清理

2.2 二级缓存工作流程(流程图可视化)

image.png

核心流程解读:

  1. 客户端发起查询请求,首先获取SqlSession,再通过SqlSession获取目标Mapper接口;
  2. 优先判断二级缓存是否开启:若未开启,直接查询一级缓存;若已开启,先检查二级缓存中是否存在目标数据;
  3. 若二级缓存命中,直接返回数据,无需查询数据库;若未命中,查询一级缓存;
  4. 若一级缓存命中,直接返回数据;若未命中,执行SQL查询数据库;
  5. 数据库查询结果会先存入一级缓存,待当前事务提交后,再同步到二级缓存(确保数据一致性);
  6. 最终将数据返回给客户端。

2.3 二级缓存核心组件架构(架构图)

image.png

核心组件说明:

  1. Configuration:MyBatis核心配置类,存储所有Mapper的缓存配置信息;
  2. MapperCache:每个Mapper接口对应一个独立的缓存空间,由Configuration统一管理;
  3. CachingExecutor:缓存执行器,是二级缓存的核心执行组件,负责拦截查询请求,实现缓存的查询、存储逻辑;
  4. Cache接口:缓存的顶层接口,定义了缓存的基本操作(get、put、clear等),其实现类分为两类:
  • 本地缓存实现:PerpetualCache(默认,基于HashMap的永久缓存)、LruCache(基于LRU淘汰策略)、FifoCache(基于FIFO淘汰策略)等;
  • 分布式缓存实现:RedisCache、EhCache等,用于解决集群环境下的缓存共享问题;
  1. BaseExecutor:基础执行器,负责实际的SQL执行,CachingExecutor通过装饰BaseExecutor实现缓存功能的增强。

2.4 二级缓存的核心特性

  1. 懒加载特性:二级缓存的数据并非在应用启动时初始化,而是在第一次查询后才存入缓存;
  2. 事务一致性:只有当当前事务提交后,查询结果才会存入二级缓存;若事务回滚,缓存不会更新,避免脏数据;
  3. 可配置性:支持自定义缓存淘汰策略、缓存过期时间、缓存介质等;
  4. 灵活性:可通过注解或XML配置细粒度控制缓存的启用/禁用(接口级、方法级)。

三、二级缓存启用与基础配置:从0到1搭建

3.1 启用二级缓存的前置条件

要正确启用二级缓存,需满足以下3个核心条件:

  1. 全局配置中开启二级缓存开关(cacheEnabled=true);
  2. 目标Mapper接口中声明使用二级缓存(通过注解@CacheNamespace或XML标签);
  3. 缓存的实体类必须实现Serializable接口(确保数据可序列化存储,尤其是使用分布式缓存时)。

3.2 环境准备(Maven依赖配置)

本案例基于JDK17、SpringBoot3.2.0、MyBatis-Plus3.5.5(最新稳定版)搭建,核心依赖如下(pom.xml):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.0</version>
       <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.jam.demo</groupId>
   <artifactId>mybatis-second-level-cache-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>mybatis-second-level-cache-demo</name>
   <description>MyBatis二级缓存实战案例</description>
   
   <properties>
       <java.version>17</java.version>
       <mybatis-plus.version>3.5.5</mybatis-plus.version>
       <fastjson2.version>2.0.45</fastjson2.version>
       <guava.version>32.1.3-jre</guava.version>
       <mysql.version>8.0.33</mysql.version>
   </properties>
   
   <dependencies>
       <!-- SpringBoot核心依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       
       <!-- MyBatis-Plus依赖(含MyBatis核心) -->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       
       <!-- MySQL驱动 -->
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <version>${mysql.version}</version>
           <scope>runtime</scope>
       </dependency>
       
       <!-- Lombok(日志、getter/setter) -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.30</version>
           <scope>provided</scope>
       </dependency>
       
       <!-- Swagger3(接口文档) -->
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>2.2.0</version>
       </dependency>
       
       <!-- FastJSON2(JSON处理) -->
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       
       <!-- Guava(集合工具) -->
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </dependency>
   </dependencies>
   
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

3.3 全局配置(开启二级缓存)

通过application.yml配置MyBatis-Plus核心参数,同时开启二级缓存:

spring:
 # 数据源配置(MySQL8.0)
 datasource:
   url: jdbc:mysql://localhost:3306/mybatis_cache_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis-Plus配置
mybatis-plus:
 # 映射文件路径(若使用XML配置SQL)
 mapper-locations: classpath:mapper/*.xml
 # 实体类别名包
 type-aliases-package: com.jam.demo.entity
 # 全局配置
 global-config:
   db-config:
     # 主键策略(自增)
     id-type: auto
 # 配置二级缓存(等价于mybatis-config.xml中的<setting name="cacheEnabled" value="true"/>)
 configuration:
   # 开启二级缓存(默认false,必须手动开启)
   cache-enabled: true
   # 一级缓存相关配置(默认开启,此处仅作说明)
   local-cache-scope: SESSION
   # 日志打印(便于调试缓存命中情况)
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# Swagger3配置
springdoc:
 api-docs:
   path: /api-docs
 swagger-ui:
   path: /swagger-ui.html
   operationsSorter: method

3.4 Mapper接口级缓存配置(核心步骤)

3.4.1 注解方式(推荐,简洁高效)

在Mapper接口上添加@CacheNamespace注解,声明使用二级缓存:

package com.jam.demo.mapper;

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

/**
* 用户Mapper接口(开启二级缓存)
* 注解说明:@CacheNamespace(implementation = PerpetualCache.class) 表示使用默认的本地缓存实现
* @author ken
*/

@Mapper
@CacheNamespace(implementation = org.apache.ibatis.cache.impl.PerpetualCache.class)
public interface UserMapper extends BaseMapper<User>
{
}

3.4.2 XML方式(适用于传统XML配置SQL场景)

在Mapper.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.jam.demo.mapper.UserMapper">
   <!-- 开启二级缓存,使用默认配置 -->
   <cache/>
   
   <!-- 自定义SQL示例(若有) -->
   <select id="selectUserById" resultType="com.jam.demo.entity.User">
       SELECT id, username, age, email, create_time FROM user WHERE id = #{id}
   </select>
</mapper>

3.5 实体类实现Serializable接口

缓存数据需要进行序列化存储(尤其是分布式缓存场景),因此实体类必须实现Serializable接口:

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
* 用户实体类
* 注意:必须实现Serializable接口,否则二级缓存无法存储
* @author ken
*/

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
@Schema(description = "用户实体")
public class User implements Serializable {
   private static final long serialVersionUID = 1L;

   @TableId(type = IdType.AUTO)
   @Schema(description = "用户ID(自增)")
   private Long id;

   @Schema(description = "用户名")
   private String username;

   @Schema(description = "年龄")
   private Integer age;

   @Schema(description = "邮箱")
   private String email;

   @Schema(description = "创建时间")
   private LocalDateTime createTime;
}

3.6 数据库表结构(MySQL8.0)

创建用户表,用于后续实战测试:

CREATE DATABASE IF NOT EXISTS mybatis_cache_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mybatis_cache_demo;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
 `username` varchar(50) NOT NULL COMMENT '用户名',
 `age` int(11) DEFAULT NULL COMMENT '年龄',
 `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 插入测试数据
INSERT INTO `user` (`username`, `age`, `email`) VALUES
('zhangsan', 25, 'zhangsan@example.com'),
('lisi', 30, 'lisi@example.com'),
('wangwu', 35, 'wangwu@example.com');

四、基础实战案例:二级缓存的查询与失效验证

4.1 核心业务层实现(Service)

4.1.1 Service接口

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;

/**
* 用户服务接口
* @author ken
*/

@Schema(description = "用户服务接口")
public interface UserService extends IService<User> {
   /**
    * 根据ID查询用户(测试二级缓存命中)
    * @param id 用户ID
    * @return 用户信息
    */

   @Operation(summary = "根据ID查询用户", description = "测试二级缓存命中情况")
   User getUserById(@Parameter(description = "用户ID") Long id);

   /**
    * 更新用户信息(测试二级缓存失效)
    * @param user 用户信息
    * @return 是否更新成功
    */

   @Operation(summary = "更新用户信息", description = "测试二级缓存失效情况")
   boolean updateUser(@Parameter(description = "用户信息") User user);

   /**
    * 清空用户Mapper的二级缓存
    */

   @Operation(summary = "清空用户二级缓存", description = "手动清理当前Mapper的二级缓存")
   void clearUserCache();
}

4.1.2 Service实现类

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;

/**
* 用户服务实现类
* @author ken
*/

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

   @Resource
   private UserMapper userMapper;

   @Resource
   private SqlSessionFactory sqlSessionFactory;

   /**
    * 根据ID查询用户(测试二级缓存命中)
    * 注意:查询方法需保证事务一致性,此处使用默认事务(REQUIRED)
    */

   @Override
   @Transactional(readOnly = true)
   public User getUserById(Long id) {
       // 参数校验
       if (ObjectUtils.isEmpty(id)) {
           log.error("查询用户失败:用户ID不能为空");
           throw new IllegalArgumentException("用户ID不能为空");
       }
       log.info("执行用户查询,用户ID:{}", id);
       // 调用MyBatis-Plus的BaseMapper方法查询
       return userMapper.selectById(id);
   }

   /**
    * 更新用户信息(测试二级缓存失效)
    * 增删改操作会自动清空当前Mapper的二级缓存,确保数据一致性
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public boolean updateUser(User user)
{
       // 参数校验
       if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {
           log.error("更新用户失败:用户信息或用户ID不能为空");
           throw new IllegalArgumentException("用户信息或用户ID不能为空");
       }
       log.info("执行用户更新,用户信息:{}", user);
       // 执行更新操作
       int rows = userMapper.updateById(user);
       return rows > 0;
   }

   /**
    * 清空用户Mapper的二级缓存(手动清理)
    */

   @Override
   public void clearUserCache() {
       // 获取SqlSession,通过Configuration获取UserMapper的缓存空间并清理
       try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
           sqlSession.getConfiguration().getCache(UserMapper.class.getName()).clear();
           log.info("用户Mapper二级缓存已清空");
       } catch (Exception e) {
           log.error("清空用户Mapper二级缓存失败", e);
           throw new RuntimeException("清空缓存失败", e);
       }
   }
}

4.2 控制层实现(Controller)

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
* 用户控制器(测试二级缓存)
* @author ken
*/

@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户查询、更新及缓存测试接口")
@Slf4j
public class UserController {

   @Resource
   private UserService userService;

   /**
    * 根据ID查询用户
    */

   @GetMapping("/{id}")
   @Operation(summary = "根据ID查询用户", description = "测试二级缓存命中:多次调用观察SQL执行次数")
   public ResponseEntity<User> getUserById(
           @Parameter(description = "用户ID")
@PathVariable Long id) {
       User user = userService.getUserById(id);
       return ObjectUtils.isEmpty(user) ?
               new ResponseEntity<>(HttpStatus.NOT_FOUND) :
               new ResponseEntity<>(user, HttpStatus.OK);
   }

   /**
    * 更新用户信息
    */

   @PutMapping
   @Operation(summary = "更新用户信息", description = "测试二级缓存失效:更新后再次查询会重新执行SQL")
   public ResponseEntity<Boolean> updateUser(@Parameter(description = "用户信息") @RequestBody User user) {
       boolean success = userService.updateUser(user);
       return new ResponseEntity<>(success, HttpStatus.OK);
   }

   /**
    * 清空用户二级缓存
    */

   @PostMapping("/clear-cache")
   @Operation(summary = "清空用户二级缓存", description = "手动清理缓存后,查询会重新执行SQL")
   public ResponseEntity<String> clearUserCache() {
       userService.clearUserCache();
       return new ResponseEntity<>("缓存清空成功", HttpStatus.OK);
   }
}

4.3 启动类

package com.jam.demo;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
* 应用启动类
* @author ken
*/

@SpringBootApplication
@MapperScan("com.jam.demo.mapper") // 扫描Mapper接口
@EnableTransactionManagement // 开启事务管理(默认已开启,此处显式声明)
@OpenAPIDefinition(
       info = @Info(
               title = "MyBatis二级缓存实战API",
               description = "MyBatis二级缓存的查询、失效及手动清理测试接口",
               version = "1.0.0"
       )
)
public class MybatisSecondLevelCacheDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(MybatisSecondLevelCacheDemoApplication.class, args);
   }
}

4.4 测试验证:缓存命中与失效场景

4.4.1 测试1:二级缓存命中场景

  1. 启动应用,访问Swagger地址:http://localhost:8080/swagger-ui.html;
  2. 调用/api/users/{id}接口(如id=1),观察控制台日志:
  • 第一次调用:会打印SQL执行日志(SELECT id,username,age,email,create_time FROM user WHERE id=?),说明缓存未命中,查询数据库;
  • 第二次调用同一接口(id=1):控制台无SQL执行日志,说明缓存命中,直接从二级缓存获取数据;
  1. 验证跨SqlSession共享:重启应用(清除内存缓存),通过Postman多次调用同一接口,观察到只有第一次执行SQL,后续均命中缓存(因为不同Postman请求对应不同SqlSession,却共享了Mapper级缓存)。

4.4.2 测试2:二级缓存失效场景(增删改操作)

  1. 先调用/api/users/1接口,确保缓存命中(第二次调用无SQL);
  2. 调用/api/users(PUT)接口,更新id=1的用户信息(如修改age为26);
  3. 再次调用/api/users/1接口,观察控制台:会重新执行SQL,说明更新操作触发了二级缓存清空,缓存失效;
  4. 核心原理:MyBatis的二级缓存默认会在执行INSERT/UPDATE/DELETE操作时,自动清空当前Mapper的缓存空间,确保缓存数据与数据库一致。

4.4.3 测试3:手动清空缓存场景

  1. 调用/api/users/1接口,确保缓存命中;
  2. 调用/api/users/clear-cache接口,手动清空用户Mapper的二级缓存;
  3. 再次调用/api/users/1接口,会重新执行SQL,说明缓存已被手动清理。

五、二级缓存进阶配置:自定义缓存策略与过期时间

5.1 核心配置参数说明

@CacheNamespace注解支持多个配置参数,用于自定义缓存行为,核心参数如下:

参数名 类型 作用说明 默认值
implementation Class<? extends Cache> 指定缓存实现类(本地缓存/分布式缓存) PerpetualCache(本地)
eviction Class<? extends Cache> 指定缓存淘汰策略(当缓存满时,如何删除数据) LruCache(LRU策略)
flushInterval long 缓存过期时间(毫秒),超过时间后自动清空缓存 0(永不过期)
size int 缓存最大容量(最多存储多少条数据) 1024
readWrite boolean 是否支持读写缓存(true:缓存数据可修改;false:缓存数据只读,适合静态数据) true
blocking boolean 是否支持阻塞缓存(当缓存未命中时,是否阻塞其他请求,避免并发查询数据库) false

5.2 配置1:设置缓存过期时间与淘汰策略

需求:设置缓存过期时间为30秒,淘汰策略为FIFO(先进先出):

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.annotations.Mapper;

/**
* 用户Mapper接口(自定义缓存策略)
* @author ken
*/

@Mapper
@CacheNamespace(
       implementation = PerpetualCache.class, // 基础缓存实现
       eviction
= FifoCache.class, // 淘汰策略:FIFO
       flushInterval
= 30000, // 缓存过期时间:30秒
       size = 512, // 缓存最大容量:512条
       readWrite = true, // 支持读写
       blocking = false // 不支持阻塞
)
public interface UserMapper extends BaseMapper<User> {
}

测试验证:

  1. 调用/api/users/1接口,缓存命中;
  2. 等待30秒后,再次调用同一接口,观察到SQL重新执行,说明缓存已过期失效。

5.3 配置2:只读缓存(适合静态数据)

对于字典表、配置表等静态数据(几乎不修改),可设置为只读缓存,提升性能:

@CacheNamespace(
       implementation = PerpetualCache.class,
       readWrite
= false, // 只读缓存
       flushInterval = 86400000 // 过期时间:1天
)
public interface DictMapper extends BaseMapper<Dict> {
}

注意:只读缓存的实体类无需实现Serializable接口(因为数据不会被修改,无需序列化传输),但建议统一实现,避免后续修改时遗漏。

5.4 配置3:阻塞缓存(解决并发穿透)

当缓存未命中时,阻塞缓存会让后续请求等待,直到第一个请求查询数据库并更新缓存后,再释放其他请求,避免大量并发请求直接冲击数据库:

@CacheNamespace(
       implementation = PerpetualCache.class,
       blocking
= true // 开启阻塞缓存
)
public interface ProductMapper extends BaseMapper<Product> {
}

适用场景:热点数据查询(如商品详情页),高并发场景下可显著降低数据库压力。

六、分布式环境下的二级缓存:集成Redis实现缓存共享

6.1 分布式环境下本地缓存的局限性

在集群/分布式环境中,每个应用节点的二级缓存都是本地内存缓存,存在以下问题:

  1. 缓存不一致:节点A更新数据后,仅清空自身缓存,节点B的缓存仍为旧数据,导致数据脏读;
  2. 缓存冗余:每个节点都存储一份缓存数据,浪费内存资源;
  3. 缓存穿透风险:若某个节点缓存未命中,会查询数据库并更新本地缓存,但其他节点仍可能重复查询数据库。

解决方案:使用分布式缓存(如Redis)替代本地缓存,实现所有节点共享缓存数据。

6.2 集成Redis实现分布式二级缓存

6.2.1 添加Redis依赖

在pom.xml中添加Redis相关依赖:

<!-- Spring Data Redis -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis-Redis缓存集成包(官方提供) -->
<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-redis</artifactId>
   <version>1.0.0-beta2</version>
</dependency>

6.2.2 配置Redis连接信息(application.yml)

spring:
 # Redis配置
 redis:
   host: localhost
   port: 6379
   password: # 若Redis无密码,留空
   database: 0
   timeout: 3000ms
   lettuce:
     pool:
       max-active: 8
       max-idle: 8
       min-idle: 0
       max-wait: -1ms

6.2.3 配置Mapper使用Redis缓存

修改UserMapper接口的@CacheNamespace注解,指定缓存实现类为RedisCache:

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.cache.redis.RedisCache;
import org.apache.ibatis.annotations.Mapper;

/**
* 用户Mapper接口(集成Redis分布式缓存)
* @author ken
*/

@Mapper
@CacheNamespace(implementation = RedisCache.class)
public interface UserMapper extends BaseMapper<User>
{
}

6.2.4 自定义RedisCache配置(可选,优化缓存行为)

MyBatis-Redis默认配置可能无法满足需求(如缓存过期时间、Redis序列化方式),可自定义RedisCache实现类:

package com.jam.demo.cache;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.redis.RedisCache;
import org.apache.ibatis.cache.redis.RedisCallback;
import org.apache.ibatis.cache.redis.RedisConfig;
import org.apache.ibatis.cache.redis.RedisSerializer;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* 自定义Redis缓存实现(优化序列化和过期时间)
* @author ken
*/

public class CustomRedisCache implements Cache {

   private final String id;
   private final JedisPool jedisPool;
   private final RedisSerializer<Object> serializer;
   private final long expireTime; // 缓存过期时间(毫秒)
   private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

   /**
    * 构造方法(MyBatis会自动传入缓存ID,即Mapper接口全限定名)
    * @param id 缓存ID(com.jam.demo.mapper.UserMapper)
    */

   public CustomRedisCache(String id) {
       if (id == null) {
           throw new IllegalArgumentException("Cache instance requires an ID");
       }
       this.id = id;
       // 加载Redis配置(默认读取mybatis-config.xml中的redis配置)
       RedisConfig redisConfig = RedisConfigBuilder.build();
       this.jedisPool = new JedisPool(redisConfig.getPoolConfig(),
               redisConfig.getHost(),
               redisConfig.getPort(),
               redisConfig.getTimeout(),
               redisConfig.getPassword(),
               redisConfig.getDatabase());
       // 使用FastJSON2序列化(替代默认的Java序列化)
       this.serializer = new FastJson2RedisSerializer<>(Object.class);
       // 设置缓存过期时间:60秒
       this.expireTime = 60000;
   }

   @Override
   public String getId() {
       return id;
   }

   /**
    * 存入缓存(带过期时间)
    */

   @Override
   public void putObject(Object key, Object value) {
       try (Jedis jedis = jedisPool.getResource()) {
           byte[] keyBytes = serializer.serialize(key);
           byte[] valueBytes = serializer.serialize(value);
           // 存入Redis并设置过期时间
           jedis.setex(keyBytes, (int) (expireTime / 1000), valueBytes);
       } catch (Exception e) {
           throw new RuntimeException("Redis put object failed", e);
       }
   }

   /**
    * 从缓存获取数据
    */

   @Override
   public Object getObject(Object key) {
       try (Jedis jedis = jedisPool.getResource()) {
           byte[] keyBytes = serializer.serialize(key);
           byte[] valueBytes = jedis.get(keyBytes);
           return valueBytes != null ? serializer.deserialize(valueBytes) : null;
       } catch (Exception e) {
           throw new RuntimeException("Redis get object failed", e);
       }
   }

   /**
    * 移除缓存数据
    */

   @Override
   public Object removeObject(Object key) {
       try (Jedis jedis = jedisPool.getResource()) {
           byte[] keyBytes = serializer.serialize(key);
           return jedis.del(keyBytes) > 0;
       } catch (Exception e) {
           throw new RuntimeException("Redis remove object failed", e);
       }
   }

   /**
    * 清空缓存(对应Mapper的增删改操作)
    */

   @Override
   public void clear() {
       try (Jedis jedis = jedisPool.getResource()) {
           // 模糊匹配当前Mapper的所有缓存key(前缀为id+":")
           String pattern = id + ":*";
           jedis.keys(pattern).forEach(key -> jedis.del(key));
       } catch (Exception e) {
           throw new RuntimeException("Redis clear cache failed", e);
       }
   }

   /**
    * 获取缓存大小(可选实现)
    */

   @Override
   public int getSize() {
       try (Jedis jedis = jedisPool.getResource()) {
           String pattern = id + ":*";
           return jedis.keys(pattern).size();
       } catch (Exception e) {
           throw new RuntimeException("Redis get size failed", e);
       }
   }

   @Override
   public ReadWriteLock getReadWriteLock() {
       return readWriteLock;
   }

   /**
    * FastJSON2序列化实现
    * @param <T> 序列化对象类型
    */

   static class FastJson2RedisSerializer<T> implements RedisSerializer<T> {

       private final Class<T> clazz;

       public FastJson2RedisSerializer(Class<T> clazz) {
           this.clazz = clazz;
       }

       @Override
       public byte[] serialize(T t) {
           if (t == null) {
               return new byte[0];
           }
           return com.alibaba.fastjson2.JSON.toJSONBytes(t);
       }

       @Override
       public T deserialize(byte[] bytes) {
           if (bytes == null || bytes.length == 0) {
               return null;
           }
           return com.alibaba.fastjson2.JSON.parseObject(bytes, clazz);
       }
   }

   /**
    * Redis配置构建器(读取application.yml配置)
    */

   static class RedisConfigBuilder {
       public static RedisConfig build() {
           // 实际项目中可通过Spring上下文获取配置,此处简化实现
           RedisConfig config = new RedisConfig();
           config.setHost("localhost");
           config.setPort(6379);
           config.setTimeout(3000);
           config.setDatabase(0);
           return config;
       }
   }
}

6.2.5 启用自定义Redis缓存

修改UserMapper接口的@CacheNamespace注解:

@Mapper
@CacheNamespace(implementation = com.jam.demo.cache.CustomRedisCache.class)
public interface UserMapper extends BaseMapper<User>
{
}

6.2.6 分布式缓存测试验证

  1. 启动两个应用实例(端口分别为8080和8081);
  2. 访问8080实例的/api/users/1接口,第一次执行SQL,数据存入Redis;
  3. 访问8081实例的/api/users/1接口,观察控制台无SQL执行,说明从Redis缓存获取数据(跨节点共享成功);
  4. 调用8080实例的/api/users(PUT)接口更新用户信息;
  5. 再次访问8081实例的/api/users/1接口,会重新执行SQL,说明更新操作清空了Redis中的缓存,确保数据一致性。

七、企业级实战避坑指南:二级缓存的常见问题与解决方案

7.1 坑点1:实体类未实现Serializable接口,导致缓存存储失败

问题现象

启动应用后,调用查询接口报错:java.io.NotSerializableException: com.jam.demo.entity.User

原因分析

MyBatis二级缓存默认使用Java序列化存储数据,若实体类未实现Serializable接口,无法完成序列化,导致缓存存储失败。

解决方案

确保所有需要缓存的实体类实现Serializable接口,并生成serialVersionUID:

public class User implements Serializable {
   private static final long serialVersionUID = 1L;
   // 其他字段...
}

7.2 坑点2:事务未提交,缓存无法共享

问题现象

在同一个测试方法中,通过两个SqlSession查询同一数据,第二个SqlSession仍执行SQL,未命中缓存。

原因分析

MyBatis的二级缓存数据是在事务提交后才会存入缓存空间;若事务未提交(如测试方法未加@Transactional注解,或事务回滚),数据仅存入一级缓存,不会同步到二级缓存。

解决方案

  1. 确保查询方法添加@Transactional(readOnly = true)注解,保证事务正常提交;
  2. 避免在事务未提交的情况下跨SqlSession查询。

7.3 坑点3:分布式环境下本地缓存导致数据不一致

问题现象

集群环境中,节点A更新数据后,节点B查询仍获取旧数据。

原因分析

使用本地缓存(如PerpetualCache)时,每个节点的缓存独立存储,节点A更新数据后仅清空自身缓存,节点B的缓存未更新,导致数据不一致。

解决方案

  1. 分布式环境必须使用分布式缓存(如Redis、EhCache集群),替代本地缓存;
  2. 确保所有节点共享同一缓存介质,增删改操作能全局清空缓存。

7.4 坑点4:缓存穿透(查询不存在的数据)

问题现象

频繁查询不存在的用户ID(如id=999),每次都执行SQL查询数据库,导致数据库压力增大。

原因分析

二级缓存默认不会缓存“空结果”,若查询不存在的数据,每次都会穿透到数据库。

解决方案

  1. 在Service层添加空结果缓存逻辑:查询结果为空时,也存入缓存(值为null或空对象);
  2. 使用布隆过滤器(Bloom Filter)预处理,拦截不存在的ID查询,避免穿透到数据库。

示例代码(Service层空结果缓存):

@Override
@Transactional(readOnly = true)
public User getUserById(Long id) {
   if (ObjectUtils.isEmpty(id)) {
       throw new IllegalArgumentException("用户ID不能为空");
   }
   // 自定义缓存key(可使用Mapper接口名+方法名+参数)
   String cacheKey = "UserMapper:getUserById:" + id;
   // 从Redis获取缓存(此处简化,实际可通过RedisTemplate操作)
   User user = redisTemplate.opsForValue().get(cacheKey);
   if (!ObjectUtils.isEmpty(user)) {
       log.info("从缓存获取用户数据,ID:{}", id);
       return user;
   }
   // 缓存未命中,查询数据库
   user = userMapper.selectById(id);
   // 空结果也存入缓存,有效期5分钟
   redisTemplate.opsForValue().set(cacheKey, user, 5, TimeUnit.MINUTES);
   return user;
}

7.5 坑点5:缓存击穿(热点数据过期)

问题现象

某个热点数据(如热门商品ID=100)缓存过期瞬间,大量并发请求穿透到数据库,导致数据库压力激增。

原因分析

热点数据缓存过期时,若同时有大量请求查询该数据,会同时穿透到数据库,造成“缓存击穿”。

解决方案

  1. 互斥锁方案:缓存未命中时,获取分布式锁(如Redis分布式锁),只有一个请求能查询数据库,其他请求等待锁释放后从缓存获取数据;
  2. 热点数据永不过期:对于核心热点数据,设置缓存永不过期,通过定时任务后台更新缓存数据。

示例代码(互斥锁方案):

@Override
@Transactional(readOnly = true)
public User getHotUserById(Long id) {
   if (ObjectUtils.isEmpty(id)) {
       throw new IllegalArgumentException("用户ID不能为空");
   }
   String cacheKey = "UserMapper:getHotUserById:" + id;
   User user = redisTemplate.opsForValue().get(cacheKey);
   
   // 缓存未命中,获取分布式锁
   if (ObjectUtils.isEmpty(user)) {
       String lockKey = "lock:user:" + id;
       boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
       if (locked) {
           try {
               // 再次检查缓存(避免锁等待期间其他请求已更新缓存)
               user = redisTemplate.opsForValue().get(cacheKey);
               if (ObjectUtils.isEmpty(user)) {
                   // 查询数据库并更新缓存
                   user = userMapper.selectById(id);
                   redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
               }
           } finally {
               // 释放锁
               redisTemplate.delete(lockKey);
           }
       } else {
           // 未获取到锁,等待100ms后重试
           Thread.sleep(100);
           return getHotUserById(id);
       }
   }
   return user;
}

八、二级缓存底层源码剖析:从源码看缓存工作机制

8.1 核心入口:CachingExecutor的query方法

MyBatis的二级缓存功能通过CachingExecutor(缓存执行器)实现,其query方法是缓存查询的核心入口。该方法通过拦截查询请求,先查询二级缓存,未命中再委托基础执行器(如SimpleExecutor)查询数据库,最终将结果同步到二级缓存。完整源码及逐行解读如下:

package org.apache.ibatis.executor;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cache.TransactionalCacheManager;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.List;

/**
* 缓存执行器:二级缓存的核心实现载体,通过装饰器模式增强基础执行器
* @author ken
*/

public class CachingExecutor implements Executor {
   private final Executor delegate; // 被装饰的基础执行器(真正执行SQL的组件)
   private final TransactionalCacheManager tcm = new TransactionalCacheManager(); // 事务性缓存管理器

   @Override
   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
                            ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException
{
       // 1. 获取当前MappedStatement对应的二级缓存(即Mapper接口的缓存空间)
       // MappedStatement:封装SQL映射信息,每个SQL标签/注解对应一个MappedStatement
       Cache cache = ms.getCache();
       
       // 2. 若缓存存在(当前Mapper开启了二级缓存),则优先查询缓存
       if (cache != null) {
           // 检查是否需要清空缓存(如当前SQL配置了flushCache="true",强制清空)
           flushCacheIfRequired(ms);
           
           // 仅查询方法(SELECT)且配置了useCache="true"(默认true)时,才查询二级缓存
           if (ms.isUseCache() && resultHandler == null) {
               // 验证参数是否可缓存(避免因参数不可序列化导致缓存失败)
               ensureNoOutParams(ms, boundSql);
               
               // 3. 从二级缓存中获取数据(此处通过事务性缓存获取,保证事务一致性)
               @SuppressWarnings("unchecked")
               List<E> list = (List<E>) tcm.getObject(cache, key);
               
               // 4. 缓存未命中:委托基础执行器查询数据库
               if (list == null) {
                   list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                   
                   // 5. 将查询结果存入二级缓存(临时存储,事务提交后才正式生效)
                   tcm.putObject(cache, key, list);
               }
               return list;
           }
       }
       
       // 6. 若未开启二级缓存,直接委托基础执行器查询数据库
       return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
   }

   /**
    * 检查是否需要清空缓存
    * 规则:增删改操作(INSERT/UPDATE/DELETE)默认配置flushCache="true",会强制清空当前Mapper的二级缓存
    */

   private void flushCacheIfRequired(MappedStatement ms) {
       Cache cache = ms.getCache();
       if (cache != null && ms.isFlushCacheRequired()) {
           tcm.clear(cache); // 清空事务性缓存
       }
   }

   /**
    * 确保没有输出参数(存储过程相关,避免不可序列化的输出参数导致缓存失败)
    */

   private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
       if (ms.getStatementType() == StatementType.CALLABLE) {
           for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
               if (parameterMapping.getMode() != ParameterMode.IN) {
                   throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId());
               }
           }
       }
   }

   // 其他方法(update/commit/rollback等)省略...
}

核心逻辑总结:

  • CachingExecutor通过装饰器模式包装基础执行器,拦截所有查询请求;
  • 先判断当前Mapper是否开启二级缓存(cache != null),再检查是否需要清空缓存、是否允许使用缓存;
  • 缓存命中则直接返回数据,未命中则委托基础执行器查询数据库,查询结果存入“事务性缓存”;
  • 只有当事务提交后,事务性缓存中的数据才会正式同步到二级缓存,确保数据一致性。

8.2 缓存Key的生成机制:为什么同一查询会命中缓存?

二级缓存的命中核心是“缓存Key的唯一性”——只有当两次查询的Key完全一致时,才能命中缓存。MyBatis通过CacheKey类生成缓存Key,其生成规则由多个核心因素决定,确保“同一查询”的判定准确性。

8.2.1 CacheKey的生成源码

CacheKey的生成逻辑在BaseExecutorcreateCacheKey方法中,核心代码如下:

package org.apache.ibatis.executor;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.List;

public abstract class BaseExecutor implements Executor {
   @Override
   public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
       if (ms.isCacheEnabled()) {
           CacheKey cacheKey = new CacheKey();
           
           // 1. 加入Mapper接口+方法名(如:com.jam.demo.mapper.UserMapper.selectById)
           cacheKey.update(ms.getId());
           
           // 2. 加入分页参数(RowBounds的offset和limit,确保不同分页的查询不命中同一缓存)
           cacheKey.update(rowBounds.getOffset());
           cacheKey.update(rowBounds.getLimit());
           
           // 3. 加入SQL语句(确保不同SQL的查询不冲突)
           cacheKey.update(boundSql.getSql());
           
           // 4. 加入SQL参数(确保同一SQL不同参数的查询不命中同一缓存)
           Configuration configuration = ms.getConfiguration();
           List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
           if (!parameterMappings.isEmpty()) {
               TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
               for (ParameterMapping parameterMapping : parameterMappings) {
                   Object value;
                   String propertyName = parameterMapping.getProperty();
                   if (boundSql.hasAdditionalParameter(propertyName)) {
                       value = boundSql.getAdditionalParameter(propertyName);
                   } else if (parameterObject == null) {
                       value = null;
                   } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                       value = parameterObject;
                   } else {
                       // 反射获取参数对象的属性值(支持JavaBean/Map参数)
                       MetaObject metaObject = configuration.newMetaObject(parameterObject);
                       value = metaObject.getValue(propertyName);
                   }
                   cacheKey.update(value); // 加入参数值
               }
           }
           
           // 5. 加入环境信息(多环境部署时,确保不同环境的缓存不冲突)
           if (configuration.getEnvironment() != null) {
               cacheKey.update(configuration.getEnvironment().getId());
           }
           
           return cacheKey;
       }
       return null;
   }

   // 其他方法省略...
}

8.2.2 CacheKey的核心构成因素

缓存Key由以下5个核心因素共同决定,缺一不可:

  1. Mapper接口+方法名(ms.getId()):区分不同Mapper、不同方法的查询;
  2. 分页参数(offset+limit):区分同一方法不同分页的查询(如查询第1页和第2页的数据,Key不同);
  3. 原始SQL语句(boundSql.getSql()):区分不同SQL的查询(即使同一方法,若SQL不同,Key也不同);
  4. SQL参数值(parameterValue):区分同一SQL不同参数的查询(如查询id=1和id=2的用户,Key不同);
  5. 环境ID(environment.getId()):区分不同环境(如开发环境、测试环境、生产环境)的缓存。

示例:两个查询请求若满足以下条件,则缓存Key一致,可命中二级缓存:

  • 调用同一Mapper接口的同一方法(如UserMapper.selectById);
  • 分页参数相同(如均查询第1页,每页10条);
  • SQL语句相同(如SELECT id,username FROM user WHERE id=?);
  • 参数值相同(如均传入id=1);
  • 运行在同一环境(如生产环境)。

8.3 事务性缓存:为什么二级缓存要等事务提交后才生效?

前文提到,查询结果会先存入“事务性缓存”,而非直接存入二级缓存。这一设计的核心目的是保证数据一致性——避免事务未提交时,其他SqlSession读取到未确认的临时数据。

8.3.1 核心组件:TransactionalCache与TransactionalCacheManager

  • TransactionalCache:事务性缓存的具体实现,封装了真实的缓存(如PerpetualCacheRedisCache),提供延迟提交、事务回滚时清空缓存的功能;
  • TransactionalCacheManager:事务性缓存管理器,管理多个TransactionalCache实例(一个缓存空间对应一个TransactionalCache)。

8.3.2 TransactionalCache核心源码解读

package org.apache.ibatis.cache.decorators;

import org.apache.ibatis.cache.Cache;

import java.util.HashMap;
import java.util.Map;

/**
* 事务性缓存:延迟提交缓存数据,事务回滚时清空缓存
* @author ken
*/

public class TransactionalCache implements Cache {
   private final Cache delegate; // 真实的缓存实现(如PerpetualCache、RedisCache)
   private boolean clearOnCommit; // 标记是否在提交时清空缓存
   private final Map<Object, Object> entriesToAddOnCommit; // 待提交的缓存条目(事务提交后才存入真实缓存)
   private final Map<Object, Boolean> entriesMissedInCache; // 记录缓存未命中的Key(用于后续清理)

   public TransactionalCache(Cache delegate) {
       this.delegate = delegate;
       this.clearOnCommit = false;
       this.entriesToAddOnCommit = new HashMap<>();
       this.entriesMissedInCache = new HashMap<>();
   }

   /**
    * 从缓存获取数据:优先查真实缓存,未命中则记录到entriesMissedInCache
    */

   @Override
   public Object getObject(Object key) {
       // 1. 从真实缓存获取数据
       Object object = delegate.getObject(key);
       
       // 2. 未命中时,记录该Key(事务回滚时需清理这些Key的临时数据)
       if (object == null) {
           entriesMissedInCache.put(key, Boolean.TRUE);
       }
       
       // 3. 若标记为"提交时清空",则返回null(相当于缓存已清空)
       return clearOnCommit ? null : object;
   }

   /**
    * 存入缓存:先存入临时容器entriesToAddOnCommit,不直接写入真实缓存
    */

   @Override
   public void putObject(Object key, Object value) {
       entriesToAddOnCommit.put(key, value);
   }

   /**
    * 清空缓存:标记clearOnCommit=true,延迟到提交时执行
    */

   @Override
   public void clear() {
       clearOnCommit = true;
       entriesToAddOnCommit.clear(); // 清空待提交的缓存条目
   }

   /**
    * 事务提交:将临时缓存条目同步到真实缓存
    */

   public void commit() {
       // 1. 若标记为"提交时清空",则先清空真实缓存
       if (clearOnCommit) {
           delegate.clear();
       }
       
       // 2. 将待提交的缓存条目写入真实缓存
       flushPendingEntries();
       
       // 3. 重置状态,准备下一次事务
       reset();
   }

   /**
    * 事务回滚:放弃待提交的缓存条目,清理未命中的Key
    */

   public void rollback() {
       // 1. 移除真实缓存中可能已存在的、未命中的Key(避免脏数据)
       unlockMissedEntries();
       
       // 2. 重置状态
       reset();
   }

   /**
    * 将待提交的缓存条目写入真实缓存
    */

   private void flushPendingEntries() {
       for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
           delegate.putObject(entry.getKey(), entry.getValue());
       }
       
       // 3. 对于缓存未命中的Key,若真实缓存中存在,则移除(避免脏数据)
       for (Object key : entriesMissedInCache.keySet()) {
           if (!entriesToAddOnCommit.containsKey(key)) {
               delegate.putObject(key, null);
           }
       }
   }

   /**
    * 重置事务性缓存状态
    */

   private void reset() {
       clearOnCommit = false;
       entriesToAddOnCommit.clear();
       entriesMissedInCache.clear();
   }

   /**
    * 移除真实缓存中未命中的Key(事务回滚时)
    */

   private void unlockMissedEntries() {
       for (Object key : entriesMissedInCache.keySet()) {
           delegate.removeObject(key);
       }
   }

   // 其他方法(getId、getSize等)省略...
}

8.3.3 事务性缓存的核心流程

  1. 查询时:先从真实缓存获取数据,未命中则记录Key到entriesMissedInCache
  2. 存入缓存时:数据先存入临时容器entriesToAddOnCommit,不直接写入真实缓存;
  3. 事务提交(commit):
  • 若标记clearOnCommit=true(如执行了增删改操作),先清空真实缓存;
  • entriesToAddOnCommit中的数据写入真实缓存;
  • 重置事务性缓存状态;
  1. 事务回滚(rollback):
  • 移除真实缓存中entriesMissedInCache记录的Key(避免脏数据);
  • 清空entriesToAddOnCommit,放弃未提交的缓存数据;
  • 重置事务性缓存状态。

通过这一流程,确保了:

  • 只有事务提交的最终数据才会存入二级缓存,避免其他SqlSession读取临时数据;
  • 事务回滚时,所有未提交的缓存操作都会被撤销,保证缓存数据与数据库一致。

8.4 增删改操作清空缓存的底层逻辑

前文提到,增删改(INSERT/UPDATE/DELETE)操作会自动清空当前Mapper的二级缓存。这一逻辑的底层实现的是:**增删改对应的MappedStatement默认配置了flushCache="true"**,触发CachingExecutorflushCacheIfRequired方法,最终清空事务性缓存。

8.4.1 增删改操作的MappedStatement配置

MyBatis在解析映射文件或注解时,会为增删改操作默认设置flushCache="true"

  • XML映射文件:<insert><update><delete>标签默认flushCache="true"
  • 注解方式:@Insert@Update@Delete注解默认flushCache="true"

可通过手动设置flushCache="false"关闭这一功能(不推荐,会导致数据不一致):

<!-- 不推荐:关闭增删改的缓存清空功能 -->
<update id="updateUser" flushCache="false">
   UPDATE user SET username=#{username} WHERE id=#{id}
</update>

8.4.2 清空缓存的源码流程

  1. 执行增删改操作时,调用CachingExecutorupdate方法:

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
   // 检查是否需要清空缓存(增删改默认flushCache="true",触发清空)
   flushCacheIfRequired(ms);
   // 委托基础执行器执行增删改SQL
   return delegate.update(ms, parameterObject);
}

  1. flushCacheIfRequired方法调用TransactionalCacheManagerclear方法:

private void flushCacheIfRequired(MappedStatement ms) {
   Cache cache = ms.getCache();
   if (cache != null && ms.isFlushCacheRequired()) {
       tcm.clear(cache); // 清空当前缓存空间的事务性缓存
   }
}

  1. TransactionalCacheManagerclear方法调用TransactionalCacheclear方法:

public class TransactionalCacheManager {
   private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

   public void clear(Cache cache) {
       getTransactionalCache(cache).clear();
   }

   private TransactionalCache getTransactionalCache(Cache cache) {
       // 每个缓存空间对应一个TransactionalCache实例
       return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
   }
}

  1. TransactionalCacheclear方法标记clearOnCommit=true,延迟到事务提交时清空真实缓存:

@Override
public void clear() {
   clearOnCommit = true;
   entriesToAddOnCommit.clear();
}

核心逻辑总结:增删改操作通过flushCache="true"触发缓存清空,清空操作先标记状态,延迟到事务提交时执行,确保事务一致性——避免增删改操作未提交时,其他SqlSession仍读取到旧缓存数据。

8.5 源码剖析总结:二级缓存的完整工作链路

结合上述源码分析,梳理二级缓存的完整工作链路(从请求发起至数据返回):

  1. 客户端发起查询请求,通过SqlSession获取Mapper接口代理对象;
  2. SqlSession内部获取CachingExecutor(缓存执行器);
  3. CachingExecutorquery方法被调用,先获取当前Mapper的缓存空间(Cache);
  4. 检查缓存配置:若开启二级缓存且当前查询允许使用缓存(useCache="true"),则生成缓存CacheKey
  5. 通过TransactionalCache查询缓存:
  • 命中:直接返回缓存数据;
  • 未命中:委托基础执行器(SimpleExecutor)查询数据库;
  1. 基础执行器执行SQL,从数据库获取数据,同时存入一级缓存;
  2. 查询结果存入TransactionalCache的临时容器(entriesToAddOnCommit);
  3. 事务提交时,TransactionalCache将临时容器中的数据同步到真实缓存(二级缓存);
  4. 后续同一查询请求(CacheKey一致)可直接从二级缓存获取数据,无需查询数据库;
  5. 若执行增删改操作,触发缓存清空,标记clearOnCommit=true,事务提交时清空二级缓存。

九、二级缓存的适用场景与禁用场景

9.1 适用场景

二级缓存的核心价值是“减少重复查询,提升性能”,适合以下场景:

  1. 查询频率高、数据变更少的静态数据:
  • 字典表(如性别字典、学历字典)、配置表(如系统参数配置);
  • 热点静态数据(如商品分类、地区信息)。
  1. 只读或读写频率极低的业务数据:
  • 历史订单查询(订单创建后很少修改)、用户档案查询(基本信息很少变更)。
  1. 分布式环境下的共享缓存需求:
  • 集群部署的应用,需要多个节点共享缓存数据,减少数据库压力。
  1. 响应时间要求高的接口:
  • 高频查询接口(如首页数据查询、商品详情页查询),通过缓存降低响应时间。

9.2 禁用场景

以下场景禁用二级缓存,避免数据不一致或性能反优化:

  1. 数据变更频繁的业务数据:
  • 订单表(频繁创建、更新订单)、库存表(高频扣减库存);
  • 原因:增删改操作会频繁清空缓存,缓存命中率极低,反而增加缓存管理开销。
  1. 实时性要求高的数据:
  • 实时交易数据(如股票价格、实时订单数量)、实时统计数据;
  • 原因:缓存存在延迟(即使设置过期时间,也无法保证实时性),可能导致用户读取到旧数据。
  1. 含有动态参数的查询:
  • 复杂条件查询(如多条件筛选、动态排序),缓存Key多样性极高,缓存命中率极低;
  • 原因:缓存空间被大量低命中的Key占用,浪费内存资源。
  1. 分布式事务场景:
  • 跨库事务、分布式事务(如微服务间的事务);
  • 原因:分布式事务提交存在延迟,可能导致不同节点的缓存数据不一致。
  1. 大数据量查询:
  • 一次性查询大量数据(如导出报表、批量查询);
  • 原因:缓存数据体积大,占用大量内存,且查询频率低,缓存价值低。

9.3 细粒度控制:方法级缓存启用/禁用

若同一Mapper接口中,部分方法适合缓存、部分不适合,可通过useCache参数细粒度控制(优先于接口级配置)。

9.3.1 XML方式

通过<select>标签的useCache属性控制:

<!-- 启用缓存(默认,可省略) -->
<select id="selectById" resultType="com.jam.demo.entity.User" useCache="true">
   SELECT id, username, age, email FROM user WHERE id = #{id}
</select>

<!-- 禁用缓存(数据变更频繁的方法) -->
<select id="selectUserByCondition" resultType="com.jam.demo.entity.User" useCache="false">
   SELECT id, username, age, email FROM user
   WHERE username LIKE CONCAT('%', #{username}, '%')
</select>

9.3.2 注解方式

通过@Options注解的useCache属性控制:

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

/**
* 用户Mapper接口(方法级缓存控制)
* @author ken
*/

@Mapper
@CacheNamespace(implementation = org.apache.ibatis.cache.impl.PerpetualCache.class)
public interface UserMapper extends BaseMapper<User>
{

   // 禁用缓存
   @Options(useCache = false)
   @Select("SELECT id, username, age, email FROM user WHERE username LIKE CONCAT('%', #{username}, '%')")
   List<User> selectUserByCondition(String username);
}

十、总结:MyBatis二级缓存的核心价值与最佳实践

10.1 核心价值回顾

  1. 跨SqlSession共享数据:作用域为Mapper接口级别,解决了一级缓存无法跨会话共享的局限性;
  2. 降低数据库压力:高频查询请求直接命中缓存,减少SQL执行次数,降低数据库QPS峰值;
  3. 提升接口响应速度:缓存数据存储在内存/分布式缓存中,查询速度远快于数据库,降低接口响应时间;
  4. 高度可配置:支持自定义缓存实现、淘汰策略、过期时间等,适配不同业务场景;
  5. 兼容分布式环境:通过集成Redis等分布式缓存,支持集群节点共享缓存,适配微服务架构。

10.2 最佳实践总结

  1. 基础配置规范:
  • 全局开启二级缓存(cache-enabled: true);
  • 实体类必须实现Serializable接口;
  • Mapper接口通过@CacheNamespace<cache>标签开启缓存。
  1. 缓存策略选择:
  • 静态数据(字典表):使用只读缓存(readWrite = false),设置较长过期时间;
  • 热点数据(商品详情):使用阻塞缓存(blocking = true),避免缓存击穿;
  • 分布式环境:必须使用Redis等分布式缓存,替代本地缓存;
  • 一般业务数据:使用LRU淘汰策略(默认),设置合理的过期时间(如30秒-5分钟)。
  1. 性能优化技巧:
  • 合理设置缓存容量(size):避免缓存过大占用过多内存;
  • 避免缓存穿透:空结果也存入缓存,结合布隆过滤器拦截无效参数;
  • 避免缓存击穿:热点数据使用互斥锁或永不过期策略;
  • 避免缓存雪崩:缓存过期时间添加随机值,避免大量缓存同时过期。
  1. 数据一致性保障:
  • 增删改操作默认开启flushCache="true",自动清空缓存;
  • 分布式环境确保所有节点共享同一缓存介质;
  • 事务未提交时,缓存数据不会同步到二级缓存,避免脏读。
  1. 避坑指南:
  • 不缓存数据变更频繁、实时性要求高的数据;
  • 不缓存大数据量、低命中的查询;
  • 分布式环境不使用本地缓存,避免数据不一致;
  • 确保缓存Key的唯一性,避免不同查询命中同一缓存。

10.3 未来展望

MyBatis二级缓存作为成熟的缓存方案,在传统单体应用和微服务架构中均有广泛应用。随着云原生、分布式架构的普及,二级缓存的核心发展方向是:

  1. 更深度的分布式缓存集成:与云原生缓存服务(如Redis Cluster、Aliyun Tair)无缝集成;
  2. 智能化缓存策略:结合AI动态调整缓存过期时间、容量,提升缓存命中率;
  3. 缓存与ORM框架的更深度融合:简化分布式缓存配置,降低开发者使用成本。

通过本文的学习,相信读者已全面掌握MyBatis二级缓存的底层原理、实战配置、源码逻辑及最佳实践。在实际开发中,应根据业务场景灵活选择缓存策略,既要充分发挥缓存的性能优势,也要确保数据一致性,真正做到“性能与稳定兼得”。

目录
相关文章
|
2天前
|
云安全 监控 安全
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
966 5
|
13天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
1101 41
|
9天前
|
机器学习/深度学习 人工智能 数据可视化
1秒生图!6B参数如何“以小博大”生成超真实图像?
Z-Image是6B参数开源图像生成模型,仅需16GB显存即可生成媲美百亿级模型的超真实图像,支持中英双语文本渲染与智能编辑,登顶Hugging Face趋势榜,首日下载破50万。
673 39
|
13天前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
776 69
大厂CIO独家分享:AI如何重塑开发者未来十年
|
9天前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
479 30
|
16天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
945 59
Meta SAM3开源:让图像分割,听懂你的话
|
6天前
|
弹性计算 网络协议 Linux
阿里云ECS云服务器详细新手购买流程步骤(图文详解)
新手怎么购买阿里云服务器ECS?今天出一期阿里云服务器ECS自定义购买流程:图文全解析,阿里云服务器ECS购买流程图解,自定义购买ECS的设置选项是最复杂的,以自定义购买云服务器ECS为例,包括付费类型、地域、网络及可用区、实例、镜像、系统盘、数据盘、公网IP、安全组及登录凭证详细设置教程:
205 114