1. MyBatis 缓存概述
1.1 什么是缓存
在软件系统中,缓存(Cache)指的是将耗时或频繁访问的结果临时存储在一个快速访问的介质(通常是内存)中,以减少重复计算或重复IO,从而提升系统性能。
类比:缓存就像你做笔记,把重点记下来,下次复习直接翻笔记而不是重新读整本书。
缓存的好处:
-
减少数据库压力:避免相同 SQL 重复访问数据库
-
提升响应速度:从内存读取数据的速度远高于从数据库读取
-
节省系统资源:减少网络传输、数据库连接开销
1.2 缓存的分类
MyBatis 中的缓存分为两类:
缓存级别 | 存储范围 | 生命周期 | 默认开启 | 线程安全 | 应用场景 |
---|---|---|---|---|---|
一级缓存 | SqlSession 级别 | SqlSession 存活期 | ✅ 开启 | ❌ 不保证线程安全 | 单次会话内多次相同查询 |
二级缓存 | Mapper 级别(或命名空间级别) | 应用程序运行期 | ❌ 默认关闭 | ✅ 线程安全 | 跨会话共享数据 |
1.3 MyBatis 缓存的设计背景
在 MyBatis 1.x 时代,缓存功能较弱,只提供了简单的一级缓存。随着项目规模增大和性能优化需求增加,MyBatis 2.x/3.x 引入了更完善的缓存体系:
-
多层缓存:一级缓存(本地缓存)+ 二级缓存(全局缓存)
-
装饰器模式:让缓存支持可插拔的功能(日志、同步、LRU/FIFO 淘汰等)
-
可扩展性:支持自定义缓存实现(如整合 Redis、EhCache)
设计目标:
-
减少数据库访问次数
-
保持数据一致性(更新/删除后及时清理缓存)
-
支持多种缓存策略(LRU、FIFO、SOFT、WEAK)
-
方便扩展与定制
1.4 MyBatis 缓存生命周期
用一张图直观感受 MyBatis 缓存的生命周期(以一级缓存为例):
┌───────────────┐
│ 第一次查询 │
└──────┬────────┘
│
▼
[执行 SQL 查询]
│
▼
[结果存入缓存]
│
▼
┌───────────────┐
│ 第二次查询 │
└──────┬────────┘
│
[直接命中缓存返回]
-
一级缓存:SqlSession 创建 → 执行查询 → 存入缓存 → 相同SqlSession内再次查询时命中 → SqlSession关闭或提交/回滚事务后清空
-
二级缓存:Mapper 配置
<cache/>
→ 查询结果序列化存储 → 跨 SqlSession 共享 → 任何更新操作都会清空对应命名空间的缓存
1.5 缓存失效策略
缓存并不是永久有效的,MyBatis 在以下情况下会清空缓存:
-
执行
INSERT
、UPDATE
、DELETE
操作 -
手动调用
clearCache()
方法 -
SqlSession 关闭或提交/回滚事务
-
二级缓存的过期时间(
<cache eviction="LRU" flushInterval="60000">
)到期
1.6 MyBatis 缓存与其他缓存的区别
-
与 Redis 的区别:MyBatis 缓存是进程内缓存,存储在 JVM 内存中,不支持跨服务节点共享;Redis 是分布式缓存,可以多节点共享。
-
与 Hibernate 二级缓存的区别:Hibernate 的二级缓存是 ORM 的核心机制,而 MyBatis 的缓存是可选的增强功能。
2. 一级缓存实战详解
2.1 一级缓存回顾
一级缓存是 MyBatis 默认开启的本地缓存,作用范围是同一个 SqlSession
。
同一个 SqlSession
内,相同的 SQL 语句、相同的参数、相同的查询条件,第一次查询会执行 SQL 并缓存结果,第二次查询会直接命中缓存而不访问数据库。
类比:同一个人(SqlSession)在短时间内问你同样的问题,你只会回答一次(执行SQL),之后直接用上次的答案(缓存结果)。
2.2 项目环境准备
我们用 Spring Boot + MyBatis 搭建一个简单环境,数据库表为 user
:
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(50)
);
INSERT INTO user VALUES (1, 'Tom', 'tom@example.com');
依赖配置(pom.xml):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useSSL=false
username: root
password: root
mybatis:
mapper-locations: classpath:/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志
2.3 Mapper 接口与 XML
UserMapper.java:
public interface UserMapper {
User selectById(Long id);
}
UserMapper.xml:
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
2.4 一级缓存命中案例
我们在同一个 SqlSession
内执行两次相同的查询:
@SpringBootTest
public class CacheTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
public void testFirstLevelCacheHit() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询
User u1 = mapper.selectById(1L);
System.out.println("第一次查询结果:" + u1);
// 第二次查询(同一个SqlSession,相同SQL和参数)
User u2 = mapper.selectById(1L);
System.out.println("第二次查询结果:" + u2);
System.out.println("是否同一对象:" + (u1 == u2));
}
}
}
日志输出:
==> Preparing: SELECT * FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, username, email
<== Row: 1, Tom, tom@example.com
第一次查询结果:User{id=1, username='Tom', email='tom@example.com'}
第二次查询结果:User{id=1, username='Tom', email='tom@example.com'}
是否同一对象:true
🔍 结论:
-
只有第一次查询执行了 SQL
-
第二次查询直接命中缓存(
u1
和u2
是同一个对象)
2.5 一级缓存失效场景
场景1:不同 SqlSession
@Test
public void testFirstLevelCacheDifferentSession() {
try (SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User u1 = mapper1.selectById(1L);
User u2 = mapper2.selectById(1L);
}
}
📌 原因:一级缓存是 SqlSession
级别的,不同会话互不共享。
场景2:执行更新操作
@Test
public void testFirstLevelCacheClearOnUpdate() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.selectById(1L); // 第一次查询
// 执行更新
session.update("com.example.mapper.UserMapper.updateEmail",
Map.of("id", 1L, "email", "new@example.com"));
mapper.selectById(1L); // 第二次查询
}
}
📌 原因:MyBatis 会在执行 INSERT/UPDATE/DELETE
时清空本地缓存,以保证数据一致性。
场景3:手动清空缓存
session.clearCache(); // 手动清空
2.6 实战小结
-
一级缓存默认开启,无需额外配置
-
作用范围是
SqlSession
,生命周期随SqlSession
而结束 -
缓存命中条件:相同 SQL + 相同参数 + 相同环境
-
缓存失效的常见原因:
-
不同
SqlSession
-
执行更新操作
-
手动调用
clearCache()
-
提交/回滚事务
-
3. 一级缓存源码解析
3.1 一级缓存的核心入口
MyBatis 一级缓存的逻辑主要集中在 BaseExecutor.query()
方法中,所有的 Executor
(SimpleExecutor
、ReuseExecutor
、BatchExecutor
)都会继承它。
核心类:
-
Executor
:执行器接口,定义了query()
、update()
等方法。 -
BaseExecutor
:抽象类,一级缓存逻辑的核心实现。 -
PerpetualCache
:一级缓存的默认实现,底层用HashMap
存储数据。
3.2 流程时序图
下面的时序图展示了一次查询在 MyBatis 一级缓存中的执行路径(假设命中缓存):
调用方(Client)
│
▼
UserMapper.selectById()
│
▼
MapperMethod.execute()
│
▼
Executor.query()
│
▼
BaseExecutor.query()
│ ┌───────────────────────────────┐
│ │ 1. 生成 CacheKey │
│ │ 2. 从 localCache 获取数据 │
│ │ 3. 命中则直接返回 │
│ │ 4. 未命中则执行查询并缓存结果 │
│ └───────────────────────────────┘
▼
返回结果
如果未命中缓存,BaseExecutor.query()
会调用 doQuery()
(由具体 Executor 实现)执行真正的 SQL 查询,并把结果放入缓存。
3.3 源码解析
BaseExecutor.query()
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 一级缓存
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 如果 flushCache=true(如更新操作后),清空一级缓存
clearLocalCache();
}
queryStack++;
try {
// 先从缓存中查
List<E> list = (List<E>) localCache.getObject(key);
if (list != null) {
// 命中缓存
return list;
}
// 未命中则执行 SQL 查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
// 放入缓存
localCache.putObject(key, list);
return list;
} finally {
queryStack--;
}
}
📌 关键点:
-
缓存对象:
localCache
就是PerpetualCache
实例 -
命中条件:缓存命中取决于
CacheKey
是否相同 -
缓存清理:
ms.isFlushCacheRequired()
通常在更新操作后为true
,会清空缓存
CacheKey 的生成
CacheKey
是 MyBatis 判断两次查询是否相同的关键,它包含了:
-
MappedStatement.id
(SQL 唯一标识) -
RowBounds
(分页信息) -
SQL 语句本身
-
参数值
-
Environment.id
(环境ID)
源码:
CacheKey key = new CacheKey();
key.update(ms.getId());
key.update(rowBounds.getOffset());
key.update(rowBounds.getLimit());
key.update(boundSql.getSql());
for (Object param : boundSql.getParameterMappings()) {
key.update(paramValue);
}
📌 你可以理解 CacheKey
是SQL 查询的唯一身份证。
PerpetualCache 的实现
PerpetualCache
是 MyBatis 一级缓存的最简单实现,内部就是一个 HashMap
:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public void clear() {
cache.clear();
}
}
📌 特点:
-
简单直接,无淘汰策略
-
不保证线程安全(因为一级缓存作用范围是单个 SqlSession,通常无需加锁)
3.4 关键调用链总结
一次查询命中一级缓存的完整调用链:
MapperMethod.execute()
└──> Executor.query()
└──> BaseExecutor.query()
├── 生成 CacheKey
├── localCache.getObject(key) 命中
└── 返回缓存结果
4. 二级缓存实战案例
4.1 二级缓存回顾
-
作用范围:Mapper(命名空间)级别
-
跨会话共享:不同
SqlSession
、同一个 Mapper,查询结果可以共享 -
默认状态:关闭,需要手动开启
-
数据存储:对象会被序列化存储在缓存中(必须实现
Serializable
)
4.2 开启二级缓存
在 MyBatis 中开启二级缓存需要三步:
1. MyBatis 全局配置
确保全局配置允许二级缓存(mybatis-config.xml
或 Spring Boot 的 application.yml
):
mybatis:
configuration:
cache-enabled: true # 开启全局缓存开关
2. Mapper XML 中声明 <cache/>
在 UserMapper.xml
中添加:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启当前Mapper的二级缓存 -->
<cache eviction="LRU" <!-- 缓存淘汰策略 -->
flushInterval="60000" <!-- 刷新间隔(毫秒) -->
size="512" <!-- 缓存条目数 -->
readOnly="true"/> <!-- 只读缓存(可共享同一引用) -->
<select id="selectById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
⚠ 注意:二级缓存存储的对象必须实现
java.io.Serializable
,否则会抛出序列化异常。
3. 实体类实现 Serializable
@Data
public class User implements Serializable {
private Long id;
private String username;
private String email;
}
4.3 二级缓存命中测试
@SpringBootTest
public class SecondLevelCacheTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
public void testSecondLevelCache() {
try (SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
// 第一次查询(走数据库)
User u1 = mapper1.selectById(1L);
System.out.println("第一次查询结果:" + u1);
// 提交事务后,一级缓存数据才会写入二级缓存
session1.commit();
// 第二次查询(不同SqlSession,但同一个Mapper)
User u2 = mapper2.selectById(1L);
System.out.println("第二次查询结果:" + u2);
System.out.println("是否同一对象引用:" + (u1 == u2));
}
}
}
4.4 日志分析
假设第一次执行 SQL 时:
==> Preparing: SELECT * FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, username, email
<== Row: 1, Tom, tom@example.com
第一次查询结果:User{id=1, username='Tom', email='tom@example.com'}
第二次查询(不同 SqlSession
)时:
Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5
第二次查询结果:User{id=1, username='Tom', email='tom@example.com'}
📌 结论:
-
第一次查询走数据库
-
第二次查询命中二级缓存,没有执行 SQL
-
u1 == u2
为false
,因为二级缓存存储的是序列化对象,每次取出都会反序列化成新对象
4.5 一级缓存 vs 二级缓存对比表
对比项 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession 级别 | Mapper(namespace)级别 |
生命周期 | SqlSession 生命周期 | 应用运行期(或缓存过期时间) |
默认状态 | 开启 | 关闭 |
是否可跨会话共享 | 否 | 是 |
存储位置 | JVM 内存(当前会话) | JVM 内存(全局共享) |
对象存储形式 | 原对象引用 | 序列化对象 |
线程安全性 | 无需关心(单线程会话) | 需要线程安全(内部已加锁) |
适用场景 | 会话内重复查询 | 跨会话频繁查询 |
清空时机 | 更新操作、事务提交/回滚、手动清理 | 更新操作、flushInterval 到期、手动清理 |
📌 小结
-
二级缓存能显著减少数据库访问,但会增加内存消耗,并存在缓存过期一致性问题
-
默认存储在 JVM 内存,可以通过自定义实现(如 Redis)扩展到分布式缓存
-
开启二级缓存后,一级缓存依然存在,查询时会先查一级缓存,再查二级缓存
5 二级缓存源码解析
5.1 二级缓存的核心参与者
在 MyBatis 中,二级缓存的执行流程主要由以下几个类协作完成:
类名 | 作用 |
---|---|
CachingExecutor | Executor 装饰器,在执行查询/更新时增加二级缓存逻辑 |
TransactionalCacheManager | 管理 Mapper 级别缓存的事务提交/回滚行为 |
TransactionalCache | 二级缓存的事务包装,提交事务前先缓存在临时区,提交后批量写入 |
PerpetualCache | 二级缓存的底层实现(默认),存储数据的核心 Map |
CacheKey | 缓存键的唯一标识 |
5.2 二级缓存命中流程图
下面的流程图展示了 查询请求 在二级缓存的执行路径(假设命中二级缓存):
调用方(Client)
│
▼
MapperMethod.execute()
│
▼
CachingExecutor.query()
│
├── 判断是否开启二级缓存
│
├── 生成 CacheKey
│
├── 从二级缓存获取数据
│ │
│ ├── 命中:直接返回
│ └── 未命中:执行 BaseExecutor.query()
│ ├── 命中一级缓存 → 返回
│ └── 未命中 → 查询数据库 → 写入一级缓存
│
▼
返回结果
5.3 源码入口:CachingExecutor.query()
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 获取当前 Mapper 的二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
// 是否需要清空缓存(更新操作后)
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key); // 从二级缓存读取
if (list == null) {
// 未命中缓存,执行查询
list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
// 暂存到事务缓存中,等待提交事务时写入二级缓存
tcm.putObject(cache, key, list);
}
return list;
}
}
// 没有配置二级缓存或不使用缓存
return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
📌 关键逻辑:
-
ms.getCache()
:获取 Mapper 对应的二级缓存实例 -
tcm.getObject()
:从事务缓存管理器中获取数据 -
tcm.putObject()
:先放入事务缓存,等待事务提交时写入全局二级缓存 -
delegate.query()
:委托给真实的 Executor(如 SimpleExecutor)执行查询
5.4 事务缓存管理器:TransactionalCacheManager
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
📌 设计要点:
-
TransactionalCacheManager
不直接操作真实缓存,而是操作TransactionalCache
-
所有写入缓存的操作先进入
TransactionalCache
,等事务提交时批量写入,保证缓存和数据库的一致性
5.5 事务缓存包装类:TransactionalCache
public class TransactionalCache implements Cache {
private final Cache delegate; // 真正的二级缓存
private final Map<Object, Object> entriesToAddOnCommit = new HashMap<>();
@Override
public Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public void putObject(Object key, Object value) {
// 先放入临时缓存,等事务提交时再批量写入
entriesToAddOnCommit.put(key, value);
}
public void commit() {
// 将临时缓存批量写入真实缓存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
clearOnCommit();
}
private void clearOnCommit() {
entriesToAddOnCommit.clear();
}
}
📌 设计优点:
-
避免事务未提交就缓存数据,防止脏数据被其他会话读取
-
保证缓存与数据库的数据一致性
5.6 二级缓存的调用链
一次二级缓存命中的调用链:
MapperMethod.execute()
└──> CachingExecutor.query()
├──> TransactionalCacheManager.getObject()
│ └──> TransactionalCache.getObject()
└──> 命中缓存 → 返回结果
5.7 二级缓存与一级缓存的关系
查询时的优先级:
-
先查一级缓存(SqlSession 级别)
-
如果一级缓存未命中,才查二级缓存(Mapper 级别)
-
如果二级缓存也未命中,再查数据库,并写入一级缓存
-
事务提交时,一级缓存中的结果写入二级缓存
📌 小结
-
二级缓存逻辑由
CachingExecutor
装饰器统一管理 -
缓存写入采用事务延迟提交机制(
TransactionalCache
) -
查询优先级:一级缓存 > 二级缓存 > 数据库
-
二级缓存解决了跨会话共享数据的问题,但带来了分布式一致性挑战,分布式部署时通常结合 Redis 等外部缓存
6. 装饰器模式与缓存装饰器源码解析
6.1 装饰器模式回顾
定义
装饰器模式(Decorator Pattern)是一种结构型设计模式,用于在运行时为对象动态地添加功能,而无需修改其源码。
关键特征:
-
继承同一个接口(或抽象类)
-
内部持有被装饰对象的引用
-
对外表现和被装饰对象一致,但可在方法前后添加新行为
通用结构:
Component(接口)<---- ConcreteComponent(真实对象)
▲
│
Decorator(抽象装饰器)
▲
│
ConcreteDecoratorX / Y(具体装饰器)
6.2 MyBatis 缓存的装饰器体系
MyBatis 的缓存接口:
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
}
核心思想:
-
PerpetualCache
是最基础的实现(一个 HashMap) -
其余缓存功能(LRU、日志、同步、事务等)都是装饰器
-
装饰器可以层层嵌套
6.2.1 UML 类图
Cache(接口)
▲
│
----------------------
│ │
PerpetualCache 装饰器抽象类(无)
▲
----------------------------------
│ │ │
LruCache LoggingCache SynchronizedCache
│
其他淘汰策略(FifoCache, SoftCache, WeakCache)
📌 注意:MyBatis 没有专门的 Decorator
抽象类,而是让每个装饰器直接实现 Cache
接口,并在内部持有一个 Cache
对象。
6.3 常用缓存装饰器源码解析
6.3.1 LruCache(最近最少使用淘汰)
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
📌 特点:
-
使用
LinkedHashMap
的访问顺序模式 -
超过容量时,自动移除最久未访问的元素
6.3.2 LoggingCache(带日志功能)
public class LoggingCache implements Cache {
private final Cache delegate;
protected final Log log;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(getId());
}
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + (hits * 100 / requests) + "%");
}
return value;
}
}
📌 特点:
-
统计缓存命中率
-
记录日志,方便调试
6.3.3 SynchronizedCache(线程安全)
public class SynchronizedCache implements Cache {
private final Cache delegate;
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized void putObject(Object key, Object value) {
delegate.putObject(key, value);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
}
📌 特点:
-
为所有方法加
synchronized
,确保多线程环境安全
6.3.4 装饰器叠加示例
假设你在 Mapper XML 中配置:
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
eviction="LRU"
size="512"
readOnly="false"/>
MyBatis 实际上会构建这样的装饰链:
SynchronizedCache(
LoggingCache(
LruCache(
PerpetualCache
)
)
)
执行顺序:
-
最外层:
SynchronizedCache
→ 保证线程安全 -
中间层:
LoggingCache
→ 打印命中率 -
内层:
LruCache
→ 实现淘汰策略 -
最底层:
PerpetualCache
→ 存放数据
6.4 装饰器模式的优点在 MyBatis 中的体现
-
开放封闭原则:无需修改
Cache
接口或基础实现,就能动态扩展功能 -
灵活组合:可以自由叠加缓存特性(线程安全、淘汰策略、日志等)
-
可扩展性强:用户可自定义
Cache
装饰器并无缝集成
6.5 小结
-
MyBatis 缓存的增强功能全靠装饰器模式实现
-
装饰器之间可以灵活组合
-
默认二级缓存链中至少会有
SynchronizedCache
和LoggingCache
-
这种设计比继承更灵活,更符合开闭原则
7. 缓存失效策略与一致性问题
7.1 缓存失效的根源
无论是一级缓存还是二级缓存,核心问题都是:
缓存中的数据和数据库中的真实数据可能不一致。
主要原因:
-
更新操作(
INSERT
、UPDATE
、DELETE
)导致数据库数据变更,但缓存还存旧数据。 -
缓存淘汰:容量满了,旧数据被移除,但重新查询时可能触发不必要的 SQL。
-
多节点 / 分布式环境:A 节点更新了数据并清空了缓存,但 B 节点的缓存还没清。
7.2 MyBatis 缓存失效策略
场景 | 一级缓存 | 二级缓存 |
---|---|---|
同一个 SqlSession 内执行更新操作 | 缓存清空 | 缓存清空 |
不同 SqlSession 执行更新操作 | 无影响(因为本来隔离) | 缓存清空 |
commit() 提交事务 | 缓存清空 | 缓存清空 |
执行 flushCache=true 的查询 | 缓存清空 | 缓存清空 |
Mapper 中手动调用 clearCache() | 缓存清空 | 缓存清空 |
容量满(LRU、FIFO 等淘汰策略触发) | 淘汰部分缓存 | 淘汰部分缓存 |
📌 结论:
-
一级缓存失效是“小范围、短生命周期”的
-
二级缓存失效是“全局、持久化”的
7.3 时序图:缓存与数据库交互
示例:二级缓存失效的流程(更新场景)
Client SqlSession1 SqlSession2 Cache DB
| | | | |
|---查询----->| | | |
| |---查缓存---------->| | |
| |<--命中/未命中------| | |
| |---查DB-------------------------------->| |
| |<-------------------数据----------------| |
| |---写入二级缓存---->| | |
|<--返回数据--| | | |
|---更新数据->| | | |
| |---清空二级缓存---->| | |
| |---更新DB-------------------------------->| |
| |<--OK-----------------------------------| |
关键点:
-
MyBatis 在执行更新后,会立即调用
clear()
清空二级缓存 -
一级缓存同样会被
clearSession()
清空
7.4 跨节点一致性问题
单机
-
二级缓存可以保证在同一个 JVM 内一致性(因为更新会清空全局缓存实例)
集群 / 分布式
-
问题:节点 A 更新数据并清空本地缓存,节点 B 的缓存依然是旧的
-
解决思路:
-
不启用二级缓存(最稳妥)
-
分布式缓存方案(Redis、Memcached)
-
缓存消息通知机制(更新时发送消息通知其他节点清缓存)
-
7.5 常见失效场景表
失效场景 | 描述 | 解决方案 |
---|---|---|
事务提交 | commit 时清空缓存 | 业务允许的情况下可延迟提交或减少事务范围 |
flushCache=true 查询 | 查询时强制清空缓存 | 仅在确实需要最新数据时开启 |
多节点部署 | 节点间缓存不同步 | 使用分布式缓存或消息通知 |
大量更新 | 缓存频繁被清空,命中率下降 | 缓存粒度更细,或减少不必要更新 |
淘汰策略触发 | 缓存容量不足 | 增大容量或优化热点数据命中 |
7.6 缓存一致性建议
-
一级缓存建议开启(读已提交的事务情况下,其他视情况而定)
-
二级缓存单机可用,集群需谨慎
-
高一致性需求场景(金融、库存)优先查询数据库
-
低一致性需求场景(新闻、商品列表)可以缓存为主
-
分布式部署配合 Redis + MyBatis 二级缓存插件(如 mybatis-redis)