这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战java
为了提高持久层数据查询的性能,MyBatis提供了一套缓存机制,根据缓存的做用范围可分为:一级缓存和二级缓存。一级缓存默认是开启的,做用域为SqlSession,也称做【回话缓存/本地缓存】。二级缓存默认是关闭的,须要手动开启,做用域为namespace。本篇文章暂且不讨论二级缓存,仅从源码的角度分析一下MyBatis一级缓存的实现原理。 算法
咱们已经知道,SqlSession是MyBatis对外提供的,操做数据库的惟一接口。当咱们从SqlSessionFactory打开一个新的回话时,一个新的SqlSession实例将被建立。SqlSession内置了一个Executor,它是MyBatis提供的操做数据库的执行器,当咱们执行数据库查询时,最终会调用Executor.query()
方法,它在查询数据库前会先判断是否命中一级缓存,若是命中就直接返回,不然才真的发起查询操做。 数据库
咱们直接看Executor.query()
方法,它首先会根据请求参数ParamMap解析出要执行的SQL语句BoundSql,而后建立缓存键CacheKey,而后调用重载方法。缓存
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 根据参数解析出要执行的SQL
BoundSql boundSql = ms.getBoundSql(parameter);
// 建立缓存键
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 执行查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
复制代码
所以咱们重点看query重载方法,它首先会向ErrorContext报告本身正在作查询,而后判断是否须要清空缓存,若是你在SQL节点配置了flushCache="true"
则不会使用一级缓存。以后就是尝试从一级缓存中获取结果,若是命中缓存则直接返回,不然调用queryFromDatabase
查询数据库,在queryFromDatabase
方法中,会再将查询结果存入一级缓存。markdown
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
try {
// 试图从一级缓存中获取数据
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);
}
}
return list;
}
复制代码
所以,咱们重点看localCache对象。 app
localCache是PerpetualCache类的实例,译为【永久缓存】,由于它不会主动删除缓存,可是会在事务提交或执行更新方法时清空缓存。PerpetualCache是缓存接口Cache的子类,分析子类前,咱们应该先看它的接口。 ide
以下是Cache接口,它的职责很简单,就是维护结果集缓存,对缓存提供了【增删查】的API。源码分析
public interface Cache {
// 获取缓存惟一标识符
String getId();
// 添加缓存项
void putObject(Object key, Object value);
// 根据缓存键获取缓存
Object getObject(Object key);
// 根据缓存键删除缓存
Object removeObject(Object key);
// 清空缓存数据
void clear();
}
复制代码
PerpetualCache的实现很是简单, 内部使用一个HashMap容器来维护缓存,Key存放的是CacheKey,Value存放的是结果集,代码就不贴了,以下是它的属性。post
public class PerpetualCache implements Cache {
// 缓存惟一表示
private final String id;
// 使用HashMap做为数据缓存的容器
private final Map<Object, Object> cache = new HashMap<>();
}
复制代码
CacheKey是MyBatis提供的缓存键,为何要为缓存键单独写一个类呢?由于MyBatis判断是否命中缓存,条件很是多,并非一个简单的字符串就能够搞定的,所以才有了CacheKey。 性能
如何判断查询可否命中缓存?有哪些条件须要判断呢?总结以下:
以上五个条件,必须同时知足,才能命中缓存。并且,像【填充的参数】这类数据是不固定的,所以CacheKey使用一个List
来存放这些条件,以下是它的属性:
可否命中缓存,就看CacheKey是否相等了。为了提高equals
方法的性能,避免每次挨个比较updateList,CacheKey使用hashcode
来保存哈希值,哈希值是根据每一个条件对象参与计算得出的,MyBatis提供了一套算法,尽量的让哈希值分散。
调用update
方法能够添加条件对象,源码以下:
public void update(Object object) {
// 计算单个对象的哈希值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 条件对象个数递增
count++;
// 累加校验和
checksum += baseHashCode;
// 基础哈希值乘以条件数量,使哈希值尽量分散
baseHashCode *= count;
// 最终哈希值,再乘以质数17,仍是为了分散
hashcode = multiplier * hashcode + baseHashCode;
// 添加条件对象到List
updateList.add(object);
}
复制代码
equals
方法用于判断两个CacheKey是否相等,为了提高性能,会先进行一系列的简单校验,最后才是按个匹配每一个条件对象。
@Override
public boolean equals(Object object) {
// 前置一系列校验省略...
// 以上步骤,都是为了提高equals的性能。最终仍是比较updateList的每一项
for (int i = 0; i < updateList.size(); i++) {
// 依次比较updateList各项条件
}
return true;
}
复制代码
若是CacheKey相等,则表明命中缓存。
前面已经说过,调用query
方法时,首先会建立CacheKey,根据CacheKey判断可否命中缓存,最后,咱们看一下CacheKey的建立过程。
createCacheKey
方法位于BaseExecutor
,CacheKey的建立过程并不复杂,就是实例化一个CacheKey对象,而后将须要匹配的条件调用update
方法保存下来。
上文已经说过判断可否命中缓存的五大条件了,所以它须要MappedStatement获取StatementID、须要parameterObject获取请求参数、须要RowBounds获取分页信息、须要BoundSql获取要执行的SQL。
/** * * @param ms 执行的SQL节点封装对象 * @param parameterObject 参数,通常是ParamMap * @param rowBounds 分页信息 * @param boundSql 绑定的SQL * @return */
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
// 实例化缓存键
CacheKey cacheKey = new CacheKey();
// StatementId要一致,必须调用的是同一个Mapper的同一个方法
cacheKey.update(ms.getId());
// 分页信息,查询的数据范围要一致
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 执行的SQL要一致,动态SQL的缘由,SQL都不一致,确定不能命中缓存
cacheKey.update(boundSql.getSql());
/** * 除了上述基本的四项,还要匹配全部的参数。 * 针对同一个查询方法,传入的参数不一样,确定也不能命中缓存。 * 下面是从参数ParamMap中获取对应参数的过程。 */
cacheKey.update(参数);
// 校验运行环境,查询的数据源不一样,也不能命中缓存。
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
复制代码
CacheKey建立完毕后,就是从PerpetualCache中判断是否已经存在缓存数据了,若是命中缓存就直接取缓存结果,避免查询数据库,提高查询性能。
继续看BaseExecutor的其余代码你会发现,PerpetualCache虽然本身不会主动清理缓存,可是只要执行了update
语句、或者事务提交/回滚都会清空缓存。
一级缓存是基于SqlSession的,当一个会话被打开时,它会同时建立一个Executor,Executor内部持有一个PerpetualCache,PerpetualCache底层使用一个HashMap容器来维护缓存结果集。HashMap中Key存储的是CacheKey,它是MyBatis提供的缓存键,由于判断是否命中缓存涉及的条件很是多,所以CacheKey使用一个List来保存条件对象,只有当全部的条件都匹配时,才能命中缓存。
MyBatis的一级缓存其实仍是比较鸡肋的,在多会话的场景下存在脏数据的问题,SessionA读了一遍数据,SessionB修改了该数据,SessionA再去读仍然是旧数据。不过,若是你使用Spring整合MyBatis就不用担忧这个问题了。