Mybatis一级缓存
在系统代码的运行中,我们可能会在一个数据库会话中,执行多次查询条件完全相同的Sql,鉴于日常应用的大部分场景都是读多写少,这重复的查询会带来一定的网络开销,同时select查询的量比较大的话,对数据库的性能是有比较大的影响的。
如果是Mysql数据库的话,在服务端和Jdbc端都开启预编译支持的话,可以在本地JVM端缓存Statement,可以在Mysql服务端直接执行Sql,省去编译Sql的步骤,但也无法避免和数据库之间的重复交互。
Mybatis提供了一级缓存的方案来优化在数据库会话间重复查询的问题。实现的方式是每一个SqlSession中都持有了自己的缓存,一种是SESSION级别,即在一个Mybatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个statement有效。
Mybatis默认是有缓存的,默认的缓存粒度是Session,也可以设置成Statement粒度的。
在setting节点下设置
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 | SESSION | STATEMENT | SESSION |
---|---|---|---|
<setting name="localCacheScope" value="SESSION"/>
运用实例
@Test public void testSelect() { SqlSession session = null; try { session = MybatisUtil.getCurrentSession(); UserDao userDao = session.getMapper(UserDao.class); List<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(3); list.add(25); List<User> userList = userDao.queryList(list); System.out.println(JSON.toJSONString(userList)); List<User> userList1 = userDao.queryList(list); System.out.println(JSON.toJSONString(userList1)); } catch (Exception e) { // TODO: handle exception } finally { if (session != null) session.close(); } }
如果第二次执行userDao.queryList(list);依旧是需要去数据库查询的话,这样我们程序的效率低了,这毕竟是个重复操作,肯定是可以使用缓存减少与数据库的直接对话的。那我们就来通过DEBUG看看到底Mybatis缓存是怎么用的。
一次完成的查询过程之前已经说过了,在这里我就直接捡之前没有说到过的部分说了,直接看这个BaseExecutor里的query方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//在得到了封装好的Sql的boundSql对象
BoundSql boundSql = ms.getBoundSql(parameter);
//在这里创建一个CacheKey,作为缓存结果集的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
CacheKey的创建
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
//StatementID
cacheKey.update(ms.getId());
//sql的offset
cacheKey.update(rowBounds.getOffset());
//Sql的limit、
cacheKey.update(rowBounds.getLimit());
//Sql本身
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
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);
}
//Sql中的参数
cacheKey.update(value);
}
}
我们可以看到它将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终生成了CacheKey。
我们重要的是看看这个类重写的equals方法,这样,我们才知道什么情况下这个一级缓存才生效,才能取出被就缓存的封装结果集。
public boolean equals(Object object) {
if (this == object)
return true;
if (!(object instanceof CacheKey))
return false;
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode)
return false;
if (checksum != cacheKey.checksum)
return false;
if (count != cacheKey.count)
return false;
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null)
return false;
} else {
if (!thisObject.equals(thatObject))
return false;
}
}
return true;
}
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
除去hashcode,checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。结合update方法,我们可以得出下结论:也就是只要两条Sql的下列五个值相同,即可以认为是相同的Sql。
Statement Id + Offset + Limmit + Sql + Params
接着执行query方法,到了这个方法中。
@SuppressWarnings("unchecked")
public <E> List<E> 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<E> list;
try {
queryStack++;
//这里会在本地缓存里用CacheKey来查找,如果没找到就得需要去数据库中查询
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//存储过程用
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//从数据库中去查询,会把得到的结果存到本地缓存localCache中
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
//如果缓存的等级是STATEMENT,也就是说只对当前的STATEMNET有效的话,就需要清空本地缓存,也就是一级缓存失效了
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
localCache
这里我们需要介绍一下localCache
localCache是BaseExecutor的一个属性,是一个类型为PerpetualCache的成员变量。
那么PerpetualCache又是一个什么类呢,他是一个实现了Cache接口的类。
Mybatis的Cache接口非常简单,但是他有许多的实现类,这些实现类实现了Mybatis缓存的所有功能,非常的强大,这些实现类的设计采用了装饰模式,也是非常的巧妙,具体怎样设计会在Mybatis二级缓存中介绍。
PerpetulCache应该可以算是Mybatis中最简单的Cache实现类了,他基本上就是利用了一个HashMap来做缓存,没有难懂的地方。我们的一级缓存的查询结果最终就是存在一个HashMap中,
其中Key为CacheKey对象,Value是Mapper Method期望的返回结果。
Mybatis一级缓存是在SESSION级的,这也就意味着他感受不到其他SESSION对我们的缓存结果的改动,也就是说当我们执行了一次查询之后,生成了一个CacheKey,然后把封装的结果集存入PerpetulCache中,在第二次再执行这个查询之前,另一个SESSION更改了查询里的内容,然后我们再去读取数据时,这个数据已经被改动了,但是我们还是会去读取缓存中的数据。这样的表现,与Mysql隔离级别为RR(Repeatable Read )下的查询表现是一致的。
当然在同一个会话中更改了结果是不会出现这种情况的,因为在同一会话中,如果调用了update\delete\insert。实例上这三种都调用了update,看源码可知,那我们看看update做了什么
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
//执行更新数据的操作时,都会将本地一级缓存清掉。
clearLocalCache();
return doUpdate(ms, parameter);
}
从上述代码可以看出,因为每一次执行数据更改操作时,都会将当前会话中的一级缓存清空,所以,当再次查询的时候就无法从缓存中取数据了,就需要直接跟数据库对话,去数据库里取数据,即不会存在不可重复读问题了。
也就是说使用默认的一级缓存会存在不可重复读的问题,因为你的粒度是SESSION,是没法检查都另一个会话是否改变了我们的查询结果的,这将对数据的准确性带来很大的影响,所以在Mybatis中,我们应该把默认的一级缓存关掉,也就是说应该将一级缓存设为STATEMENT
<setting name="localCacheScope" value="STATEMENT"/>
Mybatis二级缓存
我们可以看到Mybatis一级缓存的粒度太粗了,一般情况是不建议使用的,所以Mybatis有了一个粒度更细的二级缓存。我们得有一个使得多个Session会话可以共享的缓存,这样就不会存在某一个会话修改了查询结果而另一个会话浑然不知的情况了。
在配置文件中设置开启二级缓存
首先我们解决如何开启二级缓存的问题,这个同样可以在配置文件中设置。
1.在settings节点中配置
<setting name="cacheEnabled" value="true"/>
这样就已经开启了二级缓存了,但是缓存要应用的话,就需要在相应的namespace中配置。
还记得我们在讲解Executor的时候讲过他的初始化过程吗
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 { //如果没有显式的设置,则使用SimpleExecutor,一般都是使用这个Executor executor = new SimpleExecutor(this, transaction); } //开启了二级缓存的话,这里就会为true,也就是说上述的配置对应的就是这个cacheEnabled的值 if (cacheEnabled) { //开启二级缓存 ,使用CahingExecutor装饰BaseExecutor的子类 ,注意,这里已经开始使用装饰模式了,不过这还只是开始。更溜的装饰模式还在后面 executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
2.在Mybatis Mapper XML中配置cache或者 cache-ref
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wangcc.mybatis.dao.UserDao"> <cache></cache> <resultMap type="User" id="UserMapper"> <result property="id" column="t_id"/> <result property="name" column="t_name"/> </resultMap> <select id="queryList" resultMap="UserMapper"> select * from t_user where t_id in <foreach collection="list" item="uId" index="index" open="(" separator="," close=")">#{uId}</foreach> </select> <insert id="insert" parameterType="User"> insert into t_user (t_name,address) values(#{name},#{address}) </insert> <insert id="batchInsert" > insert into t_user (t_name,address) values <foreach collection="list" item="user" separator=","> (#{user.name},#{user.address}) </foreach> </insert> </mapper><?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wangcc.mybatis.dao.UserDao"> <cache></cache> <resultMap type="User" id="UserMapper"> <result property="id" column="t_id"/> <result property="name" column="t_name"/> </resultMap> <select id="queryList" resultMap="UserMapper"> select * from t_user where t_id in <foreach collection="list" item="uId" index="index" open="(" separator="," close=")">#{uId}</foreach> </select> <insert id="insert" parameterType="User"> insert into t_user (t_name,address) values(#{name},#{address}) </insert> <insert id="batchInsert" > insert into t_user (t_name,address) values <foreach collection="list" item="user" separator=","> (#{user.name},#{user.address}) </foreach> </insert> <update id="udpate" parameterType="User"> update t_user set t_name=#{name,JdbcType=VARCHAR} , address=#{address,JdbcType=VARCHAR} where t_id=#{id} </update> </mapper>
实验
CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//得到cache 装饰模式
Cache cache = ms.getCache();
if (cache != null) {
//看是否需要清空缓存,默认select 不需要清除缓存,insert/update/delete 需要清除
flushCacheIfRequired(ms);
//
if (ms.isUseCache() && resultHandler == null) {
//这个存储过程会用到,目前不管他
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//这一行代码我们需要关注
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Cache cache=ms.getCache();
这一行代码得到的cache,是经过装饰模式的多次复用得到的Cache。
具体的执行链是
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
- SynchronizedCache: 同步Cache,实现比较简单,直接使用synchronized修饰方法。
- LoggingCache: 日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
- SerializedCache: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
- LruCache: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。
- PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
flushCacheIfRequired 清除缓存的方法
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 很明显我们可以推断出, select时,isFlushCacheRequired false ,OTHER true
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
tcm.getObject(cache, key);
CachingExecutor持有了TransactionalCacheManager对象,即tcm。
TransactionalCacheManager中持有了一个Map
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。
TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示。@Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); }
我们回到这个方法tcm.getObject(cache, key);
public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; }
Cache Hit Ratio [com.wangcc.mybatis.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 1270855946.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4bbfb90a]
==> Preparing: select *from t_user where t_id=?
==> Parameters: 1(Integer)
<== Columns: t_id, t_name, address
<== Row: 1, kobe, los
<== Total: 1
User(id=1, name=kobe, address=los)
Cache Hit Ratio [com.wangcc.mybatis.dao.UserDao]: 0.5
User(id=1, name=kobe, address=los)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4bbfb90a]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4bbfb90a]
Returned connection 1270855946 to pool.