前言
缓存主要用来提高查询效率。以计算机的 CPU 为例,CPU 具有三级缓存,性能依次降低,优先从一级缓存查询,一级缓存未命中时再从二级缓存查询,二级缓存未命中时再从三级缓存查询。
MyBatis 官网在缓存一节中提到:Mybatis 具有全局性的二级缓存。也许这也是网上一些资料说 MyBatis 具有二级缓存的来源。类比 CPU 三级缓存,乍一看 MyBatis 的二级缓存似乎也是这样使用的。经过仔细分析,MyBatis 官网提到的二级缓存并不是这么回事。
MyBatis 中的缓存分为 Session 和 Statement 两种类型,然而这两种类型的缓存并没有什么关系,如果 MyBatis 用 Statement 类型表述二级缓存,也许更为精确。这也告诉我们,即便是官网上的描述,也不一定准确,文档的撰写者和开发者很有可能不是同一人,因此从源码入手能够理解更为深刻。
然而 MyBatis 和 Spring 对比,其注释的量几乎可以忽略不计,好在 MyBatis 的设计也比较小巧优秀,可读性较强。这节我将对 MyBatis 的缓存进行分析,力争通过这一篇文章大家就能理解 MyBatis 的缓存机制。话不多说,我们开始今天的内容。
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 会根据配置使用不同的实现,各实现具体如下。
从上面 Cache 的表格中我们可以看到,MyBatis 使用装饰器模式定义了很多 Cache 的实现,以 LogingCache 为例,我们看下 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; } ... 省略部分方法 }
LogingCache 类对目标 Cache 进行装饰,持有目标 Cache 的引用,当调用 Cache 接口方法时委托给目标 Cache 处理,LogingCache 类对#getObject方法进行增强,记录了请求次数,命中次数,并且打印了日志。其他装饰器的实现和 LogingCache 类似。
如何在 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 对语句级别的缓存进行支持。关于 Executor,不了解的小伙伴可以点击 MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)查看。查看 CachingExecutor 关键代码如下。
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 使用缓存加快查询速度。
对于大型项目,我们通常会拆分为多个微服务,并且每个服务部署多份,如果仍然使用 MyBatis 的缓存,很容易导致相同的查询条件在 A 服务器上的查询结果和在 B 服务器上查询的结果不一致,如果一定要使用缓存加快查询速度,建议使用 Redis 做集中式缓存。对于小型单机部署的服务,直接使用 MyBatis 的缓存则没有问题。为了关闭 Executor 和语句对应的缓存,我们可以进行如下的配置。
<configuration> <settings> <setting name="cacheEnabled" value="false"/> <setting name="localCacheScope" value="STATEMENT"/> </settings> </configuration>
cachedEnabled 用来禁用语句的缓存,localCacheScope 设置为 STATEMENT 则可以将 Executor 中的缓存在查询后清空。
总结
本篇主要介绍了 MyBatis 对缓存的抽象、如何在 MyBatis 中配置缓存以及 MyBatis 底层对缓存的使用。从上面的分析中,我们并没有看到 MyBatis 有类似先查询一级缓存、再查二级缓存的逻辑,而是在 Executor 中使用缓存避免循环引用以及加快查询,因此 MyBatis 官网对二级缓存的描述可能不够准确。学习中,我们还是不能人云亦云,即便是官网的描述可能也不一定正确,需要我们时刻保持怀疑态度。