当前位置: 首页 > news >正文

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)

设计目标:

  1. 减少数据库访问次数

  2. 保持数据一致性(更新/删除后及时清理缓存)

  3. 支持多种缓存策略(LRU、FIFO、SOFT、WEAK)

  4. 方便扩展与定制


1.4 MyBatis 缓存生命周期

用一张图直观感受 MyBatis 缓存的生命周期(以一级缓存为例):

       ┌───────────────┐│   第一次查询   │└──────┬────────┘│▼[执行 SQL 查询]│▼[结果存入缓存]│▼┌───────────────┐│   第二次查询   │└──────┬────────┘│[直接命中缓存返回]
  • 一级缓存:SqlSession 创建 → 执行查询 → 存入缓存 → 相同SqlSession内再次查询时命中 → SqlSession关闭或提交/回滚事务后清空

  • 二级缓存:Mapper 配置 <cache/> → 查询结果序列化存储 → 跨 SqlSession 共享 → 任何更新操作都会清空对应命名空间的缓存


1.5 缓存失效策略

缓存并不是永久有效的,MyBatis 在以下情况下会清空缓存:

  1. 执行 INSERTUPDATEDELETE 操作

  2. 手动调用 clearCache() 方法

  3. SqlSession 关闭或提交/回滚事务

  4. 二级缓存的过期时间(<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

  • 第二次查询直接命中缓存(u1u2 是同一个对象)


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 + 相同参数 + 相同环境

  • 缓存失效的常见原因:

    1. 不同 SqlSession

    2. 执行更新操作

    3. 手动调用 clearCache()

    4. 提交/回滚事务

3. 一级缓存源码解析

3.1 一级缓存的核心入口

MyBatis 一级缓存的逻辑主要集中在 BaseExecutor.query() 方法中,所有的 ExecutorSimpleExecutorReuseExecutorBatchExecutor)都会继承它。

核心类:

  • 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--;}
}

📌 关键点

  1. 缓存对象localCache 就是 PerpetualCache 实例

  2. 命中条件:缓存命中取决于 CacheKey 是否相同

  3. 缓存清理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);
}

📌 你可以理解 CacheKeySQL 查询的唯一身份证


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 == u2false,因为二级缓存存储的是序列化对象,每次取出都会反序列化成新对象


4.5 一级缓存 vs 二级缓存对比表

对比项一级缓存二级缓存
作用范围SqlSession 级别Mapper(namespace)级别
生命周期SqlSession 生命周期应用运行期(或缓存过期时间)
默认状态开启关闭
是否可跨会话共享
存储位置JVM 内存(当前会话)JVM 内存(全局共享)
对象存储形式原对象引用序列化对象
线程安全性无需关心(单线程会话)需要线程安全(内部已加锁)
适用场景会话内重复查询跨会话频繁查询
清空时机更新操作、事务提交/回滚、手动清理更新操作、flushInterval 到期、手动清理


📌 小结

  • 二级缓存能显著减少数据库访问,但会增加内存消耗,并存在缓存过期一致性问题

  • 默认存储在 JVM 内存,可以通过自定义实现(如 Redis)扩展到分布式缓存

  • 开启二级缓存后,一级缓存依然存在,查询时会先查一级缓存,再查二级缓存

5 二级缓存源码解析

5.1 二级缓存的核心参与者

在 MyBatis 中,二级缓存的执行流程主要由以下几个类协作完成:

类名作用
CachingExecutorExecutor 装饰器,在执行查询/更新时增加二级缓存逻辑
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);
}

📌 关键逻辑

  1. ms.getCache():获取 Mapper 对应的二级缓存实例

  2. tcm.getObject():从事务缓存管理器中获取数据

  3. tcm.putObject():先放入事务缓存,等待事务提交时写入全局二级缓存

  4. 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 二级缓存与一级缓存的关系

查询时的优先级:

  1. 先查一级缓存(SqlSession 级别)

  2. 如果一级缓存未命中,才查二级缓存(Mapper 级别)

  3. 如果二级缓存也未命中,再查数据库,并写入一级缓存

  4. 事务提交时,一级缓存中的结果写入二级缓存


📌 小结

  • 二级缓存逻辑由 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 中的体现

  1. 开放封闭原则:无需修改 Cache 接口或基础实现,就能动态扩展功能

  2. 灵活组合:可以自由叠加缓存特性(线程安全、淘汰策略、日志等)

  3. 可扩展性强:用户可自定义 Cache 装饰器并无缝集成


6.5 小结

  • MyBatis 缓存的增强功能全靠装饰器模式实现

  • 装饰器之间可以灵活组合

  • 默认二级缓存链中至少会有 SynchronizedCacheLoggingCache

  • 这种设计比继承更灵活,更符合开闭原则

7. 缓存失效策略与一致性问题

7.1 缓存失效的根源

无论是一级缓存还是二级缓存,核心问题都是:

缓存中的数据和数据库中的真实数据可能不一致

主要原因:

  1. 更新操作INSERTUPDATEDELETE)导致数据库数据变更,但缓存还存旧数据。

  2. 缓存淘汰:容量满了,旧数据被移除,但重新查询时可能触发不必要的 SQL。

  3. 多节点 / 分布式环境: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 的缓存依然是旧的

  • 解决思路:

    1. 不启用二级缓存(最稳妥)

    2. 分布式缓存方案(Redis、Memcached)

    3. 缓存消息通知机制(更新时发送消息通知其他节点清缓存)


7.5 常见失效场景表

失效场景描述解决方案
事务提交commit 时清空缓存业务允许的情况下可延迟提交或减少事务范围
flushCache=true 查询查询时强制清空缓存仅在确实需要最新数据时开启
多节点部署节点间缓存不同步使用分布式缓存或消息通知
大量更新缓存频繁被清空,命中率下降缓存粒度更细,或减少不必要更新
淘汰策略触发缓存容量不足增大容量或优化热点数据命中


7.6 缓存一致性建议

  1. 一级缓存建议开启(读已提交的事务情况下,其他视情况而定)

  2. 二级缓存单机可用,集群需谨慎

  3. 高一致性需求场景(金融、库存)优先查询数据库

  4. 低一致性需求场景(新闻、商品列表)可以缓存为主

  5. 分布式部署配合 Redis + MyBatis 二级缓存插件(如 mybatis-redis)

http://www.dtcms.com/a/331020.html

相关文章:

  • dolphinscheduler中任务输出变量的问题出现ArrayIndexOutOfBoundsException
  • MCP和Agent之间的区别和联系
  • vercel部署上线
  • lesson38:MySQL数据库核心操作详解:从基础查询到高级应用
  • 飞算JavaAI智慧零售场景实践:从用户洞察到供应链优化的全链路技术升级
  • UniApp 中使用 tui-xecharts插件(或类似图表库如 uCharts)
  • [ HTML 前端 ] 语法介绍和HBuilderX安装
  • 通过网页调用身份证阅读器http websocket方法-湖南步联科技美萍MP999A电子————仙盟创梦IDE
  • 15 ABP Framework 开发工具
  • Transformer网络结构解析
  • HTML <link rel=“preload“>:提前加载关键资源的性能优化利器
  • CNN - 卷积层
  • MicroVM-as-a-Service 后端服务架构设计与实现
  • 使用 Docker 部署 PostgreSQL
  • 加密货币交易所开发:如何打造安全、高并发的数字资产交易平台?
  • 基于ECharts和EdgeOne打造云上智能图表
  • 单体架构集训整理
  • css 属性@font-face介绍
  • 经典深度学习模型——LSTM【原理解释 代码(以2025年美赛C题为例)】
  • FreeRTOS-C语言指针笔记
  • 【入门级-C++程序设计:13、STL 模板:栈(stack)、队 列(queue)、 链 表(list)、 向 量(vector) 等容器】
  • gitlab的ci/cd变量如何批量添加
  • 【P81 10-7】OpenCV Python【实战项目】——车辆识别、车流统计(图像/视频加载、图像运算与处理、形态学、轮廓查找、车辆统计及显示)
  • 智能清扫新纪元:有鹿机器人如何用AI点亮我们的城市角落
  • Streamlit实现Qwen对话机器人
  • CVPR 2025 | 机器人操控 | RoboGround:用“掩码”中介表示,让机器人跨场景泛化更聪明
  • GaussDB数据库架构师修炼(十六) 如何选择磁盘
  • Helm-K8s包管理(三)新建、编辑一个Chart
  • k8s+isulad 重装
  • Seata学习(三):Seata AT模式练习