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
一级缓存和二级缓存的原理与使用,有助于开发者根据业务场景灵活配置缓存策略,在提升系统性能的同时,确保数据的一致性与准确性。