MyBatis 缓存机制源码深度解析:一级缓存与二级缓存
- 一、一级缓存
- 1.1 逻辑位置与核心源码解析
- 1.2 一级缓存容器:PerpetualCache
- 1.3 createCacheKey 方法与缓存命中
- 1.4 命中与失效时机
- 1.5 使用方式
- 二、二级缓存
- 2.1 逻辑位置与核心源码解析
- 2.2 查询流程、命中与失效时机
- 2.3 使用方式
- 三、一级缓存与二级缓存的差异

在 Java 开发领域,MyBatis 作为主流的持久层框架,其缓存机制是提升系统性能的关键利器。MyBatis 提供的一级缓存和二级缓存,通过不同的策略与实现,有效减少数据库访问次数。
本文基于 MyBatis 3.4.6 版本,结合关键源码,深入解析两种缓存的原理、使用及差异。
一、一级缓存
1.1 逻辑位置与核心源码解析
一级缓存又称会话级缓存,其核心逻辑主要存在于org.apache.ibatis.executor.BaseExecutor类中。BaseExecutor是 MyBatis 执行器的基础抽象类,负责管理一级缓存相关操作。最外层执行的query方法,包含了缓存的核心逻辑,而doQuery方法则是具体的数据库查询操作,由BaseExecutor的子类(如SimpleExecutor、ReuseExecutor等)实现。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 构造缓存keyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 执行查询逻辑return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());// ...List<E> list;try {queryStack++;// 优先从一级缓存(localCache)中获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 处理存储过程的输出参数缓存handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 一级缓存未命中,执行数据库查询(queryFromDatabase有具体的数据库查询逻辑)list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}// ...return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 在本地缓存中标记查询执行中,防止循环引用导致的无限递归localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 执行实际的数据库查询操作list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}// 将查询结果存入一级缓存localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}
上述代码中,query方法先判断缓存相关条件,尝试从localCache获取数据。
若缓存未命中,则调用queryFromDatabase方法执行数据库查询,并将结果存入一级缓存。
1.2 一级缓存容器:PerpetualCache
一级缓存的数据存储在org.apache.ibatis.cache.PerpetualCache类实例中。PerpetualCache是一个基于 HashMap 实现的简单缓存类,用于存储缓存数据。
public class PerpetualCache implements Cache {private final String id;// 使用HashMap存储缓存数据,key为缓存键,value为缓存值private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}// ...
}
PerpetualCache通过putObject、getObject等方法实现对缓存数据的操作,BaseExecutor通过操作该实例来管理一级缓存。
1.3 createCacheKey 方法与缓存命中
CacheKey是缓存的唯一标识,MyBatis 通过createCacheKey方法生成CacheKey对象。该方法位于org.apache.ibatis.executor.BaseExecutor类,其核心逻辑是将 SQL 语句、参数、RowBounds 等信息进行哈希计算,生成唯一的缓存键。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// 设置Mapper语句的唯一标识cacheKey.update(ms.getId());// 添加分页查询的偏移量cacheKey.update(rowBounds.getOffset());// 添加分页查询的限制数量cacheKey.update(rowBounds.getLimit());// 添加SQL语句本身cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();// 优先从SQL绑定参数中获取值if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {// 通过反射获取参数对象对应属性的值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 将参数值添加到CacheKey中cacheKey.update(value);}}if (configuration.getEnvironment() != null) {// 添加环境ID到CacheKey中cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}
当再次执行相同 SQL 查询时,若生成的CacheKey与缓存中已存在的CacheKey一致,且在同一个SqlSession内,就会触发一级缓存命中,直接从缓存获取数据。
1.4 命中与失效时机
-
命中时机:在同一个
SqlSession中,执行的SQL语句、输入参数完全相同(即生成的CacheKey相同)时,一级缓存命中。 -
失效时机:
SqlSession关闭,或执行insert、update、delete操作时,一级缓存会失效。以update操作的源码为例,在org.apache.ibatis.executor.BaseExecutor类的update方法中:public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 调用该方法清空一级缓存clearLocalCache();return doUpdate(ms, parameter); }执行
update操作时,会调用clearLocalCache方法清空当前SqlSession的一级缓存,保证数据一致性。insert和delete操作也有类似逻辑。
1.5 使用方式
一级缓存默认开启,无需额外配置。在同一个SqlSession中,MyBatis 会自动管理缓存的读写与失效。以下是一个简单的 demo 案例代码:
public class CacheTest {private SqlSessionFactory sessionFactory;/*** 加载配置文件。并且初始化SqlSessionFactory*/@Beforepublic void before() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);}@Testpublic void testGetById() {SqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// 第一次查询,会执行数据库查询User user1 = userMapper.getUserById(6);// 第二次查询,由于在同一个SqlSession且查询条件相同,会命中一级缓存User user2 = userMapper.getUserById(6);System.out.println(user1 == user2); // 输出true}
}

二、二级缓存
2.1 逻辑位置与核心源码解析
MyBatis执行SQL时,整体流程是先经过CachingExecutor(最外层),最后才是其他Executor(BaseExecutor子类)CachingExecutor作为二级缓存的核心执行者,采用适配器模式,内部持有一个Executor对象(delegate),该delegate由具体的子类执行器(如SimpleExecutor)实例化,负责执行具体的数据库操作*MyBatis默认未开启二级缓存,需要在配置文件和映射文件中进行配置才能启用。二级缓存又称全局缓存,其核心逻辑存在于org.apache.ibatis.executor.CachingExecutor类
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache();// 判断二级缓存是否开启且可用if (cache != null) {// 处理可刷新缓存的情况,如执行增删改操作后需要刷新缓存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 优先从二级缓存中获取结果List<E> list = (List<E>) tcm.getObject(cache, key);if (list!= null) {return list;}}}// 二级缓存未命中,委托给具体的执行器执行查询return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}// 省略其他方法...
}
在CachingExecutor的query方法中,首先通过ms.getCache()获取当前 Mapper 语句对应的缓存对象,判断二级缓存是否开启。
若开启且可用,则尝试从二级缓存获取数据。
若未命中,则将查询委托给delegate,由delegate(如SimpleExecutor)调用BaseExecutor的相关方法执行具体数据库查询操作,并在事务提交后将结果写入二级缓存。
2.2 查询流程、命中与失效时机
-
查询流程:
MyBatis先在二级缓存中查找CacheKey对应的结果,若未命中,再检查一级缓存,若一级缓存也未命中,则执行数据库查询,查询结果先存入一级缓存,事务提交后存入二级缓存。 -
命中时机:
namespace相同,执行的SQL语句和输入参数相同(CacheKey相同),且事务已提交,数据已写入二级缓存时,二级缓存命中。 -
失效时机:执行
insert、update、delete操作,或手动调用方法,会清空当前namespace下的二级缓存。以update操作为例,在CachingExecutor类的update方法中,通过flushCacheIfRequired方法处理缓存刷新public int update(MappedStatement ms, Object parameterObject) throws SQLException {flushCacheIfRequired(ms);return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache!= null && ms.isFlushCacheRequired()) {tcm.clear(cache);} }当检测到当前
Mapper语句配置了需要刷新缓存(ms.isFlushCacheRequired()为true),就会通过TransactionalCacheManager的clear方法清空对应的二级缓存,实现缓存失效,保证数据一致性。insert和delete操作也会触发类似的缓存清空逻辑 。
2.3 使用方式
-
在
MyBatis的配置文件中开启二级缓存:<configuration><settings><setting name="cacheEnabled" value="true"/></settings> </configuration> -
在
Mapper映射文件中添加<cache>标签启用二级缓存,并可配置缓存属性:<mapper namespace="com.coderzpw.dao.UserMapper"><cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/><select id="getUserById" resultType="com.coderzpw.domain.User">SELECT * FROM user WHERE id = #{id}</select> </mapper> -
编写测试代码验证二级缓存效果:
User user1 = null; User user2 = null; try (SqlSession sqlSession1 = sessionFactory.openSession()) {UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);user1 = userMapper1.getUserById(6);sqlSession1.commit(); } try (SqlSession sqlSession2 = sessionFactory.openSession()) {UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);user2 = userMapper2.getUserById(6);sqlSession2.commit(); } System.out.println(user1 == user2); // 如果readOnly为true,这里会输出true
三、一级缓存与二级缓存的差异
| 对比项 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession级别,会话内有效,仅在当前SqlSession中共享 | namespace级别,全局有效,可在多个SqlSession间共享 |
| 开启方式 | 默认开启,无需配置 | 需要在配置文件和映射文件中配置开启 |
| 失效机制 | SqlSession关闭或执行增删改操作时,通过调用clearLocalCache清空缓存 | 执行增删改操作或手动调用方法,通过TransactionalCacheManager清空对应namespace下的缓存 |
| 实现核心类 | BaseExecutor、PerpetualCache | CachingExecutor、TransactionalCacheManager |
| 数据存储位置 | 存储在SqlSession对应的localCache中 | 存储在namespace对应的缓存区域,由TransactionalCacheManager管理 |
深入理解 MyBatis 一级缓存和二级缓存的原理与使用,有助于开发者根据业务场景灵活配置缓存策略,在提升系统性能的同时,确保数据的一致性与准确性。