MyBatis 的缓存机制是提升数据库访问性能的重要手段,它通过减少对数据库的查询次数,显著提高应用的响应速度。本文将深入分析 MyBatis 的一级缓存和二级缓存,包括它们的工作原理、配置方法以及如何避免缓存带来的数据一致性问题。
MyBatis 提供了两级缓存机制:
1. 一级缓存:也称为本地缓存,是 SqlSession 级别的缓存。每个 SqlSession 都有自己的缓存,不同的 SqlSession 之间的缓存相互隔离。
2. 二级缓存:也称为全局缓存,是 SqlSessionFactory 级别的缓存。所有由同一个 SqlSessionFactory 创建的 SqlSession 共享二级缓存。
缓存机制的基本工作流程如下:
查询请求 --> 二级缓存 --> 一级缓存 --> 数据库
当执行查询时,MyBatis 首先检查二级缓存,如果未命中则检查一级缓存,最后才会查询数据库。这种多级缓存的设计可以最大程度地减少数据库访问,提高性能。
一级缓存是 SqlSession 级别的缓存,它存在于一个 SqlSession 的生命周期内。当在同一个 SqlSession 中执行相同的 SQL 查询时,MyBatis 会直接从缓存中获取结果,而不是再次查询数据库。
一级缓存的核心实现是 PerpetualCache 类,它是一个简单的基于 HashMap 的缓存实现。
源码关键代码:
// BaseExecutor.java
@Override
public List 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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List list;
try {
queryStack++;
// 先从本地缓存中查询
list = resultHandler == null ? (List ) localCache.getObject(key) : null;
if (list != null) {
// 处理缓存结果
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// 清空延迟加载列表
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 如果是 STATEMENT 作用域,查询后清空缓存
clearLocalCache();
}
}
return list;
}
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List 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;
}
一级缓存会在以下情况下失效:
一级缓存是默认开启的,无需额外配置。如果需要禁用一级缓存,可以将 localCacheScope 设置为 STATEMENT:
二级缓存是全局缓存,作用域是整个 SqlSessionFactory。多个 SqlSession 可以共享同一个二级缓存,这使得二级缓存的生命周期更长,缓存命中率更高。
二级缓存的默认实现也是 PerpetualCache,但 MyBatis 允许集成第三方缓存框架,如 Ehcache、Redis 等。
缓存层级结构:
CachingExecutor (二级缓存)
↓
BaseExecutor (一级缓存)
↓
JDBC
源码关键代码:
// CachingExecutor.java
@Override
public List 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);
@SuppressWarnings("unchecked")
// 从二级缓存获取数据
List list = (List ) tcm.getObject(cache, key);
if (list == null) {
// 缓存未命中,委托给底层执行器查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将结果放入缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有启用二级缓存,直接委托给底层执行器
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存需要手动配置才能启用:
全局配置:
映射文件配置:
在映射文件中添加
SELECT * FROM users WHERE id = #{id}
自定义缓存配置:
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
参数说明:
MyBatis 支持集成第三方缓存,例如 Ehcache:
添加依赖:
org.mybatis.caches
mybatis-ehcache
1.2.1
配置映射文件:
下面是一个完整的流程图,展示了 MyBatis 缓存机制的工作流程:
查询请求
|
v
CachingExecutor 是否启用二级缓存?
|
+--- 是 ---> 是否命中二级缓存?
| |
| +--- 是 ---> 返回缓存结果
| |
| +--- 否 ---> 委托给 BaseExecutor
|
+--- 否 ---> 直接委托给 BaseExecutor
|
v
BaseExecutor 查询一级缓存
|
+--- 命中 ---> 返回缓存结果
|
+--- 未命中 ---> 查询数据库
|
v
将结果存入一级缓存
|
v
返回结果给 CachingExecutor
|
v
将结果存入二级缓存(如果启用)
虽然缓存可以显著提高性能,但也可能带来数据一致性问题。以下是几种常见的问题及解决方案:
问题描述:当一个事务修改了数据,但另一个事务读取了旧的缓存数据时,就会发生脏读。
解决方案:
问题描述:当数据在数据库中被修改,但缓存中的数据没有及时更新时,会导致数据不一致。
解决方案:
问题描述:在分布式系统中,多个服务实例可能共享同一份数据,但各自维护自己的缓存,导致缓存不一致。
解决方案:
1. 合理使用一级缓存:一级缓存是 SqlSession 级别的,适合在同一个事务中多次查询相同数据的场景。
2. 谨慎使用二级缓存:二级缓存是全局的,适合缓存一些不经常变化的数据,如字典表、配置信息等。
3. 设置合理的缓存参数:根据业务需求设置缓存的大小、刷新间隔和淘汰策略。
4. 避免缓存大数据量:缓存大量数据会占用内存,降低系统性能。
5. 注意事务边界:确保在事务提交或回滚后,缓存状态与数据库一致。
MyBatis 的缓存机制是一把双刃剑,合理使用可以显著提高系统性能,但如果使用不当,可能会导致数据一致性问题。通过深入理解一级缓存和二级缓存的工作原理,合理配置缓存参数,并采取有效的数据一致性保障措施,可以充分发挥缓存的优势,同时避免其带来的问题。
在实际开发中,需要根据业务场景权衡缓存的使用,对于实时性要求高、数据变化频繁的业务,应该谨慎使用缓存;而对于读多写少、数据变化不频繁的业务,缓存可以带来明显的性能提升。