Mybatis源码解析--缓存
背景
Mybatis作为一个持久层框架,极大的简化了使用JDBC对数据库操作的繁琐流程,只需定义接口编写SQL
模板即可操作数据库,消除了手动获取连接、创建Statement
、设置参数、解析结果集等。Mybatis除了对数据库操作进行简化之外,还在性能上做了优化,提供了缓存。开始之前,先思考下面这个场景:
一般用户会向应用发起一个请求,通过应用处理之后会响应一个预期结果给用户。设想一下由于业务比较复杂,可能这次请求需要对数据库访问十几次才能获取完本次业务处理所需数据,而一般我们的应用和数据库会分别部署在同一局域网的两台机器上。请求处理示意图如下:
上面每次都要从数据库获取数据,如果存在对同一数据获取多次的场景就会有性能上的损耗;这种情况下,如果加个缓存,将查询的结果临时缓存起来,每次先从缓存获取,如果有结果直接使用该结果;缓存中没有,再查询数据库并放入缓存。这样整个请求处理过程中数据的获取一部分走缓存一部分走数据库从而提高响应效率。
Mybatis缓存的抽象
Mybatis内部设计了两级缓存
Mybatis考虑到这种情况,所以提供了一级缓存和二级缓存机制以提高系统的性能
缓存接口的定义
public interface Cache { // 获取当前缓存的标识 String getId(); // 向缓存中存放数据 void putObject(Object key, Object value); // 从缓存中获取数据 Object getObject(Object key); // 从缓存中移除该key的数据 Object removeObject(Object key); // 清空缓存 void clear(); // 当前缓存已存放数据的大小 int getSize(); default ReadWriteLock getReadWriteLock() { return null; } }
内置的缓存实现
以上实现可以分为缓存的实现类(PerpetualCache)和对其他缓存的装饰类(除PerpetualCache的缓存类)
一级缓存
上图是在没有开启二级缓存时Mybatis的缓存架构(即仅有一级缓存),下面我们看一下具体代码
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // 如果开启二级缓存会使用CachingExecutor对原Executor进行包装 if (cacheEnabled) { executor = new CachingExecutor(executor); } // Mybatis插件功能对Executor的增强 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
此处我们只讨论一级缓存所以我们认为二级缓存没有开启且没有对Executor做任何增强
protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); // 初始化一级缓存 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
在创建Executor时会在内部初始化
PerpetualCache
作为一级缓存
PerpetualCache具体实现
public class PerpetualCache implements Cache { private final String id; private final Map<Object, Object> cache = new HashMap<>(); public PerpetualCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public int getSize() { return cache.size(); } @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } @Override public void clear() { cache.clear(); } }
可以看到
PerpetualCache
是通过HashMap
实现的
一级缓存的生命周期
Mybatis在对数据库进行操作时会创建一个SqlSession
对象(SqlSession是接口,真正创建的是DefaultSqlSession),该SqlSession
会持有一个Executor
的引用(创建DefaultSqlSession需要创建一个Executor),在创建Executor
时会在内部创建PerpetualCache
对象(该对象内部使用HashMap实现),所以这3者的关系图如下:
所以当会话结束时,SqlSession对象及其内部Executor对象及PerpetualCache会一并释放掉。
CacheKey的定义与设计
如下图所示,MyBatis定义了一个org.apache.ibatis.cache.Cache
接口作为其Cache
提供者的SPI(Service Provider Interface) ,所有的MyBatis内部的Cache
缓存,都应该实现这一接口。MyBatis定义了一个PerpetualCache实现类实现了Cache
接口,实际上,在SqlSession对象里的Executor对象内维护的Cache类型实例对象,就是PerpetualCache子类创建的。
(MyBatis内部还有很多Cache接口的实现,一级缓存只会涉及到这一个PerpetualCache子类,Cache的其他实现将会放到二级缓存中介绍)。
我们知道,Cache最核心的实现其实就是一个Map,将本次查询使用的特征值作为key,将查询结果作为value存储到Map中。
现在最核心的问题出现了:怎样来确定一次查询的特征值?
换句话说就是:怎样判断某两次查询是完全相同的查询?
也可以这样说:如何确定Cache中的key值?
MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
传入的
statementId
查询时要求的结果集中的结果范围 (结果的范围通过
rowBounds.offset
和rowBounds.limit
表示);这次查询所产生的最终要传递给
JDBC java.sql.Preparedstatement
的Sql语句字符串(boundSql.getSql()
)传递给
java.sql.Statement
要设置的参数值
现在分别解释上述四个条件:
传入的
statementId
,对于MyBatis而言,你要使用它,必须需要一个statementId
,它代表着你将执行什么样的Sql
;MyBatis自身提供的分页功能是通过
RowBounds
来实现的,它通过rowBounds.offset
和rowBounds.limit
来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页;由于MyBatis底层还是依赖于
JDBC
实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC
而言,也是完全一致的查询才行。而对于JDBC
而言,两次查询,只要传入给JDBC
的SQL
语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。上述的第3个条件正是要求保证传递给
JDBC
的SQL
语句完全一致;第4条则是保证传递给JDBC
的参数也完全一致;3、4讲的有可能比较含糊,举一个例子:
<select id="selectByCritiera" parameterType="java.util.Map" resultMap="BaseResultMap"> select employee_id,first_name,last_name,email,salary from louis.employees where employee_id = #{employeeId} and first_name= #{firstName} and last_name = #{lastName} and email = #{email} </select>如果使用上述的"
selectByCritiera
"进行查询,那么,MyBatis会将上述的SQL
中的#{}
都替换成?
如下:select employee_id,first_name,last_name,email,salary from louis.employees where employee_id = ? and first_name= ? and last_name = ? and email = ?MyBatis最终会使用上述的
SQL
字符串创建JDBC
的java.sql.PreparedStatement
对象,对于这个PreparedStatement
对象,还需要对它设置参数,调用setXXX()
来完成设值,第4条的条件,就是要求对设置JDBC
的PreparedStatement
的参数值也要完全一致。
即3、4两条MyBatis最本质的要求就是:
调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
综上所述,CacheKey由以下条件决定:
statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值
CacheKey的创建
对于每次的查询请求,Executor都会根据传递的参数信息以及动态生成的SQL语句,将上面的条件根据一定的计算规则,创建一个对应的CacheKey对象。
我们知道创建CacheKey的目的,就两个:
-
根据CacheKey作为key,去Cache缓存中查找缓存结果;
-
如果查找缓存命中失败,则通过此CacheKey作为key,将从数据库查询到的结果作为value,组成key,value对存储到Cache缓存中。
CacheKey的构建被放置到了Executor接口的实现类BaseExecutor中,定义如下:
/** * 所属类: org.apache.ibatis.executor.BaseExecutor * 功能 : 根据传入信息构建CacheKey */ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { // 1.检查Executor是否已关闭 if (closed) { throw new ExecutorException("Executor was closed."); } // 2.创建CacheKey对象并设置属性 CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); 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.update(value); } } if (configuration.getEnvironment() != null) { cacheKey.update(configuration.getEnvironment().getId()); } // 返回创建的CacheKey return cacheKey; }
CacheKey的hashcode生成算法
刚才已经提到,Cache接口的实现,本质上是使用的HashMap<k,v>,而构建CacheKey的目的就是为了作为HashMap<k,v>中的key值。而HashMap是通过key值的hashcode 来组织和存储的,那么,构建CacheKey的过程实际上就是构造其hashCode的过程。下面的代码就是CacheKey的核心hashcode生成算法,感兴趣的话可以看一下:
public void updateAll(Object[] objects) { for (Object o : objects) { update(o); } }
public void update(Object object) { //1. 得到对象的hashcode; int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); //对象计数递增 count++; checksum += baseHashCode; //2. 对象的hashcode 扩大count倍 baseHashCode *= count; //3. hashCode * 拓展因子(默认37)+拓展扩大后的对象hashCode值 hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); }
二级缓存
二级缓存的划分
Mybatis并不是简单整个Application只有一个Cache缓存对象,他将缓存划分的更细,既是Mapper级别的,即每个Mapper都会拥有一个Cache对象,具体如下
-
为每个Mapper分配一个Cache缓存对象(使用
<cache>
标签配置) -
多个Mapper共享一个Cache缓存对象(使用
<cache-ref>
标签配置)
为每个Mapper分配一个Cache缓存对象(使用<cache>
标签配置)
Mybatis将Application级别缓存细分到Mapper级别,即对每个Mapper.xml,如果在其中使用了<cache>标签,则Mybatis会为这个Mapper创建一个Cache缓存对象,如下图所示:
对于namespace1和namespace2两个Mapper,都配置了
<cache>
会分别为这两个Mapper根据配置信息创建一个Cache对象;而namespace3的Mapper,没有配置<cache>
标签,则对于Mapper namespace3就没有对应的二级缓存,定义如下:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
多个Mapper共享一个Cache缓存对象(使用<cache-ref>
标签配置)
如果想让多个Mapper共享一个Cache的话,需要使用<cache-ref namespace="">
标签来指定这个Mapper使用哪个Mapper的Cache对象
Mapper namespace2的
<cache-ref>
定义的namespace="namespace1",说明Mapper namespace2将使用Mapper namespace1中的缓存对象,这时要求namespace1中一定要有<cache>
标签的定义,定义如下:
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
二级缓存生效条件
Mybatis
对二级缓存的支持的粒度很细,他会指定某一条查询语句是否使用二级缓存
虽然在Mapper中配置了<cache>
,并且为此Mapper分配了Cache对象,这并不代表我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象中,我们必须指定Mapper中的某条选择语句是否支持缓存。即如下所示,在<select>
标签中配置useCache=true
,Mapper才会对此Select的查询支持缓存特性,否则,不会对此select查询,不会经过缓存。
总之,要想使某条select查询支持二级缓存,必须同时保证一下条件同时满足:
-
MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
-
该select语句所在的Mapper,配置了
<cache>
或<cached-ref>
节点,并且有效 -
该select语句的参数 useCache=true
二级缓存实现的选择
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache
缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache
接口实现,用户是需要实现org.apache.ibatis.cache.Cache
接口,然后将Cache
实现类配置在<cache type="">
节点的type
属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached
的集成,总之,使用MyBatis的二级缓存有三个选择:
1.MyBatis自身提供的缓存实现;
2. 用户自定义的Cache接口实现;
3.跟第三方内存缓存库的集成;