MyBatis 缓存机制分析
缓存主要用来提高查询效率。以计算机的 CPU 为例,CPU 具有三级缓存,性能依次降低,优先从一级缓存查询,一级缓存未命中时再从二级缓存查询,二级缓存未命中时再从三级缓存查询。
MyBatis 缓存抽象
最简单的缓存使用 Map 即可实现,然而由于需要支持不同的使用场景,因此 MyBatis 将缓存抽象出一个 Cache 接口,定义如下:
public interface Cache {
// 获取当前缓存的标识符
String getId();
// 存入缓存对象
void putObject(Object key, Object value);
// 获取缓存对象
Object getObject(Object key);
// 移除缓存对象
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取缓存的对象数量
int getSize();
// 废弃的接口,3.2.6 版本开始不再使用
default ReadWriteLock getReadWriteLock() {
return null;
}
}
可以看到 Cache 主要提供的功能就是添加、移除对象,MyBatis 会根据配置使用不同的实现,各实现具体如下:
缓存类型 | 特点 |
PerpetualCache | 永久存储对象的缓存,使用 Map 实现 |
BlockingCache | 缓存装饰器,使用 CountDownLatch 实现,支持阻塞式获取,获取不到值时当前线程会被阻塞,直到其他线程存入值 |
FifoCache | 缓存装饰器,使用 Deque 实现,最大存储 1024 个对象,超过最大值时使用先进先出的方式移除旧对象 |
LoggingCache | 缓存装饰器,获取对象时会打印日志,对命中率进行简单的统计 |
LruCache | 缓存装饰器,使用 LinkedHashMap 实现,最大存储 1024 个对象,超过最大值时使用最近最少使用的方式移除旧对象 |
ScheduledCache | 缓存装饰器,存储的对象具有一个小时的生命周期,存取或者移除对象时会将过期的对象移除 |
SerializedCache | 缓存装饰器,存放的对象必须实现 Serializable 以支持序列化,存放的是对象序列话后的数组,存取前后会进行序列化和反序列化操作 |
SoftCache | 缓存装饰器,使用 SoftReference 实现,以便内存不够时进行垃圾回收 |
SynchronizedCache | 缓存装饰器,方法前添加 synchronized,每个线程需要获取到锁才能存取对象 |
TransactionalCache | 缓存装饰器,commit 时才把对象刷新到目标 Cache 中 |
WeakCache | 缓存装饰器,使用 WeakReference 实现,每次垃圾回收都会清空缓存对象 |
从上面 Cache 的表格中我们可以看到,MyBatis 使用装饰器模式定义了很多 Cache 的实现,以 LoggingCache 为例,我们看下 MyBatis 对装饰器模式的使用。
public class LoggingCache implements Cache {
// 日志对象
private final Log log;
// 目标 Cache
private final Cache delegate;
// 请求获取对象的数量
protected int requests = 0;
// 命中数量
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(getId());
}
@Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
// 获取命中率
private double getHitRatio() {
return (double) hits / (double) requests;
}
... 省略部分方法
}
LoggingCache 类对目标 Cache 进行装饰,持有目标 Cache 的引用,当调用 Cache 接口方法时委托给目标 Cache 处理,LoggingCache 类对#getObject方法进行增强,记录了请求次数,命中次数,并且打印了日志。其他装饰器的实现和 LoggingCache 类似。
如何在 MyBatis 中配置缓存
根据上面的内容,我们知道,MyBatis 中具有多种类型的 Cache,那么 MyBatis 到底使用哪个作为缓存实现呢?这是根据配置来的。
全局配置
MyBatis xml 配置文件或 Configuration 类中可以对缓存进行全局配置,以 xml 为例可以配置如下:
<configuration>
<settings>
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
</configuration>
请注意:这两个配置理解为开关更为合适,然而很难直观的通过这两个字段理解其含义,MyBatis 将其分别修改为 statementCacheEnabled、clearSessionCacheOnFinish 也许更为恰当。
首先我们要知道 MyBatis 中每个 SqlSession 和 SQL 语句分别有一个对应的 Cache,有了这个背景知识之后我们再来看这两个字段的含义。
- cacheEnabled: 语句级 Cache 的开关,可取值 true 或 false ,用于开启或关闭语句的缓存。
- localCacheScope:可取值为 SESSION 或 STATEMENT,如果为 STATEMENT 则 SqlSession 每次查询结束都会清空 对应的 Cache。
Mapper 配置
除了全局配置,MyBatis 还可以在 Mapper 的 xml 文件中对每个语句使用的 Cache 进行配置。
<mapper namespace="com.zzuhkp.blog.mybatis.UserMapper">
<cache type="" blocking="" eviction="" flushInterval="" readOnly="" size=""/>
<cache-ref namespace=""/>
</mapper>
每个 mapper 中的所有语句共享一个 Cache。使用的这个 Cache 可以通过 cache 或 cache-ref 节点来配置,cache-ref 节点的 namespace 属性可以指定使用哪个命名空间 mapper 下的 Cache,cache 节点相对复杂,会影响使用到的具体 Cache 类型,下面对其可以配置的属性进行详细介绍:
- type:使用的 Cache 具体类型,如果不指定则默认为 PerpetualCache。
- blocking:表示是否使用 BlockingCache 装饰 Cache,可取值为 true 或 false,如果不指定则默认为 false。
- eviction:缓存清除策略,可取值为 FIFO、LRU、SOFT、WEAK,如果不指定则默认为 LRU,使用 LruCache 装饰 Cache。
- flushInterval:缓存刷新间隔,单位 ms,如果设置了则使用 ScheduledCache 装饰 Cache。
- readOnly:可取值 true 或 false ,如果设置为 true 则使用 SerializedCache 装饰 Cache,如果不指定则默认为 flase。
- size:指定可缓存的对象的数量,FifoCache 或 LruCache 使用。
可以看到,cache 节点中的很多属性将影响 MyBatais 创建 Cache 的装饰器。
cache 或 cache-ref 只是配置 mapper 的语句中使用的缓存类型,那么每个查询都会使用缓存吗?MyBatis 在每个语句中提供了灵活配置的方式,具体如下:
<mapper namespace="com.zzuhkp.blog.mybatis.UserMapper">
<select|insert|update|delete flushCache="true" useCache="true"/>
</mapper>
Mapper xml 配置文件中 select、insert、update、delete 每个节点都可以设置 flushCache、useCache 属性:
- flushCache:SqlSession 执行查询或更新前是否清空 SqlSession 和语句对应的缓存,默认非 select 语句执行前清空缓存。
- useCache:是否使用语句对应的 Cache 缓存查询结果。
MyBatis 缓存底层使用分析
这节对 MyBatis 底层对缓存的使用进行一个分析,为了了解 MyBatis 如何使用缓存的,我们可以从配置入手。全局配置都会存放到 Configuration 中,查看 cacheEnabled 的使用位置如下:
public class Configuration {
// 创建新的 Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
... 省略部分代码
Executor executor;
... 省略部分代码
if (cacheEnabled) {
// 启用 Statement 级别的缓存
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
Configuration 提供了实例化 Executor 的方法#newExecutor,当设置 cachedEnabled 为 true 时 MyBatis 会创建一个 CachingExecutor,由这个 Executor 对语句级别的缓存进行支持。
public class CachingExecutor implements Executor {
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 语句中的 Cache,值为 Mapper xml 文件中 cache 或 cache-ref 节点配置的缓存对象
Cache cache = ms.getCache();
if (cache != null) {
// 如果配置了 flushCache 则先清空语句对应的缓存
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, 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);
}
}
CachingExecutor 执行查询或更新时,如果 Mapper xml select|insert|update|delete 语句中配置了 flushCache 会先清空语句对应的缓存,如果配置了 useCache 为 true 则执行查询时优先从缓存中获取结果,从数据库查询到结果后则会放入缓存中。
Configuration 中还有一个 localCacheScope 配置,查看其使用位置如下:
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
... 省略非关键代码
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 先清空缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 优先从缓存获取
list = resultHandler == null ? (List<E>) 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) {
... 省略非关键代码
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
}
BaseExecutor 查询的逻辑和 CachingExecutor 类似,只是 CachingExecutor 使用的是 STATEMENT 类型的缓存,而 BaseExecutor 自身持有一个 Cache 对象,这个对象就是 SESSION 类型的缓存。
BaseExecutor 执行查询前如果 select|insert|update|delete 语句中设置了 flushCache 也会先清空缓存,查询时优先从缓存获取数据,缓存中没有结果时再进行数据库查询。查询后如果配置中的缓存作用域设置的是 STATEMENT,则会清空 BaseExecutor 中的缓存。
MyBatis 缓存使用场景
综合上面的分析,我们可以得出 MyBatis 中对缓存的使用有两个场景。
- BaseExecutor 使用缓存避免循环引用查询。
- CachingExecutor 使用缓存加快查询速度。
<configuration>
<settings>
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
</configuration>
cachedEnabled 用来禁用语句的缓存,localCacheScope 设置为 STATEMENT 则可以将 Executor 中的缓存在查询后清空。
参考: |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了