MyBatis缓存模块深度解析
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=falseusername: rootpassword: rootmybatis:mapper-locations: classpath:/mapper/*.xmlconfiguration: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 {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@Testpublic 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<>();@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic 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 {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@Testpublic 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<>();@Overridepublic Object getObject(Object key) {return delegate.getObject(key);}@Overridepublic 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) {@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {eldestKey = eldest.getKey();}return tooBig;}};}@Overridepublic 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());}@Overridepublic 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;}@Overridepublic synchronized void putObject(Object key, Object value) {delegate.putObject(key, value);}@Overridepublic 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)