一、引言:为什么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 二级缓存工作流程(流程图可视化)
核心流程解读:
- 客户端发起查询请求,首先获取SqlSession,再通过SqlSession获取目标Mapper接口;
- 优先判断二级缓存是否开启:若未开启,直接查询一级缓存;若已开启,先检查二级缓存中是否存在目标数据;
- 若二级缓存命中,直接返回数据,无需查询数据库;若未命中,查询一级缓存;
- 若一级缓存命中,直接返回数据;若未命中,执行SQL查询数据库;
- 数据库查询结果会先存入一级缓存,待当前事务提交后,再同步到二级缓存(确保数据一致性);
- 最终将数据返回给客户端。
2.3 二级缓存核心组件架构(架构图)
核心组件说明:
- Configuration:MyBatis核心配置类,存储所有Mapper的缓存配置信息;
- MapperCache:每个Mapper接口对应一个独立的缓存空间,由Configuration统一管理;
- CachingExecutor:缓存执行器,是二级缓存的核心执行组件,负责拦截查询请求,实现缓存的查询、存储逻辑;
- Cache接口:缓存的顶层接口,定义了缓存的基本操作(get、put、clear等),其实现类分为两类:
- 本地缓存实现:PerpetualCache(默认,基于HashMap的永久缓存)、LruCache(基于LRU淘汰策略)、FifoCache(基于FIFO淘汰策略)等;
- 分布式缓存实现:RedisCache、EhCache等,用于解决集群环境下的缓存共享问题;
- BaseExecutor:基础执行器,负责实际的SQL执行,CachingExecutor通过装饰BaseExecutor实现缓存功能的增强。
2.4 二级缓存的核心特性
- 懒加载特性:二级缓存的数据并非在应用启动时初始化,而是在第一次查询后才存入缓存;
- 事务一致性:只有当当前事务提交后,查询结果才会存入二级缓存;若事务回滚,缓存不会更新,避免脏数据;
- 可配置性:支持自定义缓存淘汰策略、缓存过期时间、缓存介质等;
- 灵活性:可通过注解或XML配置细粒度控制缓存的启用/禁用(接口级、方法级)。
三、二级缓存启用与基础配置:从0到1搭建
3.1 启用二级缓存的前置条件
要正确启用二级缓存,需满足以下3个核心条件:
- 全局配置中开启二级缓存开关(cacheEnabled=true);
- 目标Mapper接口中声明使用二级缓存(通过注解@CacheNamespace或XML标签);
- 缓存的实体类必须实现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:二级缓存命中场景
- 启动应用,访问Swagger地址:http://localhost:8080/swagger-ui.html;
- 调用
/api/users/{id}接口(如id=1),观察控制台日志:
- 第一次调用:会打印SQL执行日志(
SELECT id,username,age,email,create_time FROM user WHERE id=?),说明缓存未命中,查询数据库; - 第二次调用同一接口(id=1):控制台无SQL执行日志,说明缓存命中,直接从二级缓存获取数据;
- 验证跨SqlSession共享:重启应用(清除内存缓存),通过Postman多次调用同一接口,观察到只有第一次执行SQL,后续均命中缓存(因为不同Postman请求对应不同SqlSession,却共享了Mapper级缓存)。
4.4.2 测试2:二级缓存失效场景(增删改操作)
- 先调用
/api/users/1接口,确保缓存命中(第二次调用无SQL); - 调用
/api/users(PUT)接口,更新id=1的用户信息(如修改age为26); - 再次调用
/api/users/1接口,观察控制台:会重新执行SQL,说明更新操作触发了二级缓存清空,缓存失效; - 核心原理:MyBatis的二级缓存默认会在执行INSERT/UPDATE/DELETE操作时,自动清空当前Mapper的缓存空间,确保缓存数据与数据库一致。
4.4.3 测试3:手动清空缓存场景
- 调用
/api/users/1接口,确保缓存命中; - 调用
/api/users/clear-cache接口,手动清空用户Mapper的二级缓存; - 再次调用
/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> {
}
测试验证:
- 调用
/api/users/1接口,缓存命中; - 等待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 分布式环境下本地缓存的局限性
在集群/分布式环境中,每个应用节点的二级缓存都是本地内存缓存,存在以下问题:
- 缓存不一致:节点A更新数据后,仅清空自身缓存,节点B的缓存仍为旧数据,导致数据脏读;
- 缓存冗余:每个节点都存储一份缓存数据,浪费内存资源;
- 缓存穿透风险:若某个节点缓存未命中,会查询数据库并更新本地缓存,但其他节点仍可能重复查询数据库。
解决方案:使用分布式缓存(如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 分布式缓存测试验证
- 启动两个应用实例(端口分别为8080和8081);
- 访问8080实例的
/api/users/1接口,第一次执行SQL,数据存入Redis; - 访问8081实例的
/api/users/1接口,观察控制台无SQL执行,说明从Redis缓存获取数据(跨节点共享成功); - 调用8080实例的
/api/users(PUT)接口更新用户信息; - 再次访问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注解,或事务回滚),数据仅存入一级缓存,不会同步到二级缓存。
解决方案
- 确保查询方法添加@Transactional(readOnly = true)注解,保证事务正常提交;
- 避免在事务未提交的情况下跨SqlSession查询。
7.3 坑点3:分布式环境下本地缓存导致数据不一致
问题现象
集群环境中,节点A更新数据后,节点B查询仍获取旧数据。
原因分析
使用本地缓存(如PerpetualCache)时,每个节点的缓存独立存储,节点A更新数据后仅清空自身缓存,节点B的缓存未更新,导致数据不一致。
解决方案
- 分布式环境必须使用分布式缓存(如Redis、EhCache集群),替代本地缓存;
- 确保所有节点共享同一缓存介质,增删改操作能全局清空缓存。
7.4 坑点4:缓存穿透(查询不存在的数据)
问题现象
频繁查询不存在的用户ID(如id=999),每次都执行SQL查询数据库,导致数据库压力增大。
原因分析
二级缓存默认不会缓存“空结果”,若查询不存在的数据,每次都会穿透到数据库。
解决方案
- 在Service层添加空结果缓存逻辑:查询结果为空时,也存入缓存(值为null或空对象);
- 使用布隆过滤器(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)缓存过期瞬间,大量并发请求穿透到数据库,导致数据库压力激增。
原因分析
热点数据缓存过期时,若同时有大量请求查询该数据,会同时穿透到数据库,造成“缓存击穿”。
解决方案
- 互斥锁方案:缓存未命中时,获取分布式锁(如Redis分布式锁),只有一个请求能查询数据库,其他请求等待锁释放后从缓存获取数据;
- 热点数据永不过期:对于核心热点数据,设置缓存永不过期,通过定时任务后台更新缓存数据。
示例代码(互斥锁方案):
@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的生成逻辑在BaseExecutor的createCacheKey方法中,核心代码如下:
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个核心因素共同决定,缺一不可:
- Mapper接口+方法名(
ms.getId()):区分不同Mapper、不同方法的查询; - 分页参数(
offset+limit):区分同一方法不同分页的查询(如查询第1页和第2页的数据,Key不同); - 原始SQL语句(
boundSql.getSql()):区分不同SQL的查询(即使同一方法,若SQL不同,Key也不同); - SQL参数值(
parameterValue):区分同一SQL不同参数的查询(如查询id=1和id=2的用户,Key不同); - 环境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:事务性缓存的具体实现,封装了真实的缓存(如PerpetualCache、RedisCache),提供延迟提交、事务回滚时清空缓存的功能;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 事务性缓存的核心流程
- 查询时:先从真实缓存获取数据,未命中则记录Key到
entriesMissedInCache; - 存入缓存时:数据先存入临时容器
entriesToAddOnCommit,不直接写入真实缓存; - 事务提交(
commit):
- 若标记
clearOnCommit=true(如执行了增删改操作),先清空真实缓存; - 将
entriesToAddOnCommit中的数据写入真实缓存; - 重置事务性缓存状态;
- 事务回滚(
rollback):
- 移除真实缓存中
entriesMissedInCache记录的Key(避免脏数据); - 清空
entriesToAddOnCommit,放弃未提交的缓存数据; - 重置事务性缓存状态。
通过这一流程,确保了:
- 只有事务提交的最终数据才会存入二级缓存,避免其他SqlSession读取临时数据;
- 事务回滚时,所有未提交的缓存操作都会被撤销,保证缓存数据与数据库一致。
8.4 增删改操作清空缓存的底层逻辑
前文提到,增删改(INSERT/UPDATE/DELETE)操作会自动清空当前Mapper的二级缓存。这一逻辑的底层实现的是:**增删改对应的MappedStatement默认配置了flushCache="true"**,触发CachingExecutor的flushCacheIfRequired方法,最终清空事务性缓存。
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 清空缓存的源码流程
- 执行增删改操作时,调用
CachingExecutor的update方法:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 检查是否需要清空缓存(增删改默认flushCache="true",触发清空)
flushCacheIfRequired(ms);
// 委托基础执行器执行增删改SQL
return delegate.update(ms, parameterObject);
}
flushCacheIfRequired方法调用TransactionalCacheManager的clear方法:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache); // 清空当前缓存空间的事务性缓存
}
}
TransactionalCacheManager的clear方法调用TransactionalCache的clear方法:
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);
}
}
TransactionalCache的clear方法标记clearOnCommit=true,延迟到事务提交时清空真实缓存:
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
核心逻辑总结:增删改操作通过flushCache="true"触发缓存清空,清空操作先标记状态,延迟到事务提交时执行,确保事务一致性——避免增删改操作未提交时,其他SqlSession仍读取到旧缓存数据。
8.5 源码剖析总结:二级缓存的完整工作链路
结合上述源码分析,梳理二级缓存的完整工作链路(从请求发起至数据返回):
- 客户端发起查询请求,通过
SqlSession获取Mapper接口代理对象; SqlSession内部获取CachingExecutor(缓存执行器);CachingExecutor的query方法被调用,先获取当前Mapper的缓存空间(Cache);- 检查缓存配置:若开启二级缓存且当前查询允许使用缓存(
useCache="true"),则生成缓存CacheKey; - 通过
TransactionalCache查询缓存:
- 命中:直接返回缓存数据;
- 未命中:委托基础执行器(
SimpleExecutor)查询数据库;
- 基础执行器执行SQL,从数据库获取数据,同时存入一级缓存;
- 查询结果存入
TransactionalCache的临时容器(entriesToAddOnCommit); - 事务提交时,
TransactionalCache将临时容器中的数据同步到真实缓存(二级缓存); - 后续同一查询请求(CacheKey一致)可直接从二级缓存获取数据,无需查询数据库;
- 若执行增删改操作,触发缓存清空,标记
clearOnCommit=true,事务提交时清空二级缓存。
九、二级缓存的适用场景与禁用场景
9.1 适用场景
二级缓存的核心价值是“减少重复查询,提升性能”,适合以下场景:
- 查询频率高、数据变更少的静态数据:
- 字典表(如性别字典、学历字典)、配置表(如系统参数配置);
- 热点静态数据(如商品分类、地区信息)。
- 只读或读写频率极低的业务数据:
- 历史订单查询(订单创建后很少修改)、用户档案查询(基本信息很少变更)。
- 分布式环境下的共享缓存需求:
- 集群部署的应用,需要多个节点共享缓存数据,减少数据库压力。
- 响应时间要求高的接口:
- 高频查询接口(如首页数据查询、商品详情页查询),通过缓存降低响应时间。
9.2 禁用场景
以下场景禁用二级缓存,避免数据不一致或性能反优化:
- 数据变更频繁的业务数据:
- 订单表(频繁创建、更新订单)、库存表(高频扣减库存);
- 原因:增删改操作会频繁清空缓存,缓存命中率极低,反而增加缓存管理开销。
- 实时性要求高的数据:
- 实时交易数据(如股票价格、实时订单数量)、实时统计数据;
- 原因:缓存存在延迟(即使设置过期时间,也无法保证实时性),可能导致用户读取到旧数据。
- 含有动态参数的查询:
- 复杂条件查询(如多条件筛选、动态排序),缓存Key多样性极高,缓存命中率极低;
- 原因:缓存空间被大量低命中的Key占用,浪费内存资源。
- 分布式事务场景:
- 跨库事务、分布式事务(如微服务间的事务);
- 原因:分布式事务提交存在延迟,可能导致不同节点的缓存数据不一致。
- 大数据量查询:
- 一次性查询大量数据(如导出报表、批量查询);
- 原因:缓存数据体积大,占用大量内存,且查询频率低,缓存价值低。
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 核心价值回顾
- 跨SqlSession共享数据:作用域为Mapper接口级别,解决了一级缓存无法跨会话共享的局限性;
- 降低数据库压力:高频查询请求直接命中缓存,减少SQL执行次数,降低数据库QPS峰值;
- 提升接口响应速度:缓存数据存储在内存/分布式缓存中,查询速度远快于数据库,降低接口响应时间;
- 高度可配置:支持自定义缓存实现、淘汰策略、过期时间等,适配不同业务场景;
- 兼容分布式环境:通过集成Redis等分布式缓存,支持集群节点共享缓存,适配微服务架构。
10.2 最佳实践总结
- 基础配置规范:
- 全局开启二级缓存(
cache-enabled: true); - 实体类必须实现
Serializable接口; - Mapper接口通过
@CacheNamespace或<cache>标签开启缓存。
- 缓存策略选择:
- 静态数据(字典表):使用只读缓存(
readWrite = false),设置较长过期时间; - 热点数据(商品详情):使用阻塞缓存(
blocking = true),避免缓存击穿; - 分布式环境:必须使用Redis等分布式缓存,替代本地缓存;
- 一般业务数据:使用LRU淘汰策略(默认),设置合理的过期时间(如30秒-5分钟)。
- 性能优化技巧:
- 合理设置缓存容量(
size):避免缓存过大占用过多内存; - 避免缓存穿透:空结果也存入缓存,结合布隆过滤器拦截无效参数;
- 避免缓存击穿:热点数据使用互斥锁或永不过期策略;
- 避免缓存雪崩:缓存过期时间添加随机值,避免大量缓存同时过期。
- 数据一致性保障:
- 增删改操作默认开启
flushCache="true",自动清空缓存; - 分布式环境确保所有节点共享同一缓存介质;
- 事务未提交时,缓存数据不会同步到二级缓存,避免脏读。
- 避坑指南:
- 不缓存数据变更频繁、实时性要求高的数据;
- 不缓存大数据量、低命中的查询;
- 分布式环境不使用本地缓存,避免数据不一致;
- 确保缓存Key的唯一性,避免不同查询命中同一缓存。
10.3 未来展望
MyBatis二级缓存作为成熟的缓存方案,在传统单体应用和微服务架构中均有广泛应用。随着云原生、分布式架构的普及,二级缓存的核心发展方向是:
- 更深度的分布式缓存集成:与云原生缓存服务(如Redis Cluster、Aliyun Tair)无缝集成;
- 智能化缓存策略:结合AI动态调整缓存过期时间、容量,提升缓存命中率;
- 缓存与ORM框架的更深度融合:简化分布式缓存配置,降低开发者使用成本。
通过本文的学习,相信读者已全面掌握MyBatis二级缓存的底层原理、实战配置、源码逻辑及最佳实践。在实际开发中,应根据业务场景灵活选择缓存策略,既要充分发挥缓存的性能优势,也要确保数据一致性,真正做到“性能与稳定兼得”。