mybatis缓存

Posted hellohello

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mybatis缓存相关的知识,希望对你有一定的参考价值。

基本概念

一级缓存与session绑定,只存在于session生命周期内,从数据库中查询到的值会保存到一级缓存中,当session关闭后,会保存到二级缓存中,一级缓存默认是开启的。

二级缓存存在于sqlSessionFactory生命周期内,多个session共享二级缓存,二级缓存保障了在session关闭后,一级缓存不会浪费,不同mapper的二级缓存是分开管理的,互不影响。二级缓存需要配置才会启用

 

进行查询时,首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。

 

因为DefaultSqlSession是线程不安全的,所以规范就是不能对它进行并发调用,也就意味着一级缓存不存在并发安全问题,因为根本不会被并发调用

 

因为多个session共享二级缓存,所以二级缓存会被并发访问,所以需要解决并发问题和事务问题。

 

因为实际进行数据操作时,都是通过executor来执行的,所以一级和二级缓存都是在executor内处理。

 

mybatis默认使用的是CachingExecutor,它装饰了SimpleExecutor,而SimpleExecutor继承了BaseExecutor。因为一级缓存是默认开启的,所以会有CachingExecutor去装饰SimpleExecutor,不开启的话,对外暴露的就直接是SimpleExecutor了。

 

所以进行数据操作时,都是调用CachingExecutor的方法,内部会先进行二级缓存判断,没有缓存才调用SimpleExecutor的父类BaseExecutor的方法,BaseExecutor内部会进行一级缓存判断。

 

不管是一级还是二级缓存,都是Cache接口的实现类。

BaseExecutor内有一个localCache属性,是PerpetualCache类型的,它实现了Cache接口,内部使用HashMap<Object, Object>提供最基础的缓存功能。也就是说,所谓的一级缓存,就是一个PerpetualCache对象

 

对于上面这个HashMap,键值对中value对应查询结果,而key对应CacheKey对象,CacheKey的作用是对一个Object[]进行计算,根据计算得出的结果,重写自己的hashCode和equals方法。CacheKey用于一级和二级缓存,mybatis通过调用createCacheKey创建CacheKey,参与计算的有:查询id、用于分页的offset和limit、sql语句和参数

 

其他Cache实现类有具有 LRU 策略的缓存 LruCache,以及可保证线程安全的缓存 SynchronizedCache 和具备阻塞功能的缓存 BlockingCache 等。除了 PerpetualCache 之外,其他几类缓存实现类中都是使用装饰模式,内部有一个Cache类型的delegate属性记录了实际的缓存对象,外部提供缓存的操作策略,而实际操作缓存时使用delegate去读写。即最终读写的都是 PerpetualCache

关于二级缓存

CachingExecutor内从MappedStatement中获取二级缓存的Cache对象,而非由 CachingExecutor 创建。由于 MappedStatement 存在于全局配置中,可以被多个 CachingExecutor 获取到。所以二级缓存需要去解决线程安全问题。除此之外,多个事务共用一个二级缓存时,会有脏读问题。

解决线程安全问题

mybatis解决线程安全问题是通过 SynchronizedCache 装饰类解决,该装饰类会在 Cache 实例构造期间被添加上:

org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
org.apache.ibatis.session.Configuration#addMappers(java.lang.String)
org.apache.ibatis.binding.MapperRegistry#addMappers(java.lang.String)
org.apache.ibatis.binding.MapperRegistry#addMappers(java.lang.String, java.lang.Class<?>)
org.apache.ibatis.binding.MapperRegistry#addMapper
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseCache
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
org.apache.ibatis.mapping.CacheBuilder#build
org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators

看看userNewCache源码

Cache cache = new CacheBuilder(currentNamespace)
    .implementation(valueOrDefault(typeClass, PerpetualCache.class))
    .addDecorator(valueOrDefault(evictionClass, LruCache.class))
    .clearInterval(flushInterval)
    .size(size)
    .readWrite(readWrite)
    .blocking(blocking)
    .properties(props)
    .build();

默认就是使用LruCache去装饰PerpetualCache

 

接着看setStandardDecorators源码

if (size != null && metaCache.hasSetter("size")) {
    metaCache.setValue("size", size);
}
if (clearInterval != null) {
    cache = new ScheduledCache(cache);
    ((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
    cache = new SerializedCache(cache);
}
    cache = new LoggingCache(cache);
    cache = new SynchronizedCache(cache);
if (blocking) {
    cache = new BlockingCache(cache);
}

到这里可以发现:缓存的装饰模式嵌套多层 SynchronizedCache > LoggingCache > LruCache > PerpetualCache

所以对同一个Mapper对象(同一个命名空间下)的方法调用,不会有并发问题。

 

以上创建完cache之后,接着为命名空间下的多个Method创建对应的MappedStatement,

org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement

内使用之前创建的cache,也保存一份引用到新建的MappedStatement中(每个MappedStatement代表了这个命名空间下的一个方法),通过MappedStatement可以访问到之前创建的cache,即访问当前命名空间的cache对象(二级缓存)

 

总结以上代码:解析配置文件时,解析每个mapper,对每个mapper,即每个命名空间都创建一个Cache对象,保存到Configuration#caches中(namespace -> chache对象),这就是每个命名空间对应的二级缓存了,同时也保存一份引用到命名空间下的多个MappedStatement中

以上mybatis通过SynchronizedCache装饰二级缓存,内部每个方法都加了synchronized关键字,解决了并发问题。剩余还有一个脏读问题

解决脏读问题

mybatis的脏读问题有两种:

  1. 不同命名空间有自己的二级缓存,当某个二级缓存缓存了另一个命名空间中的缓存时,另一个空间中的缓存已经更新了,但之前被缓存的数据没有关联更新,导致出现脏读。可以配置参照缓存来解决
  2. 并发调用mapper时,会对同一个二级缓存进行读写,所以可能读取到另一个线程中还没有提交的数据,出现脏读。mybatis内置使用TransactionalCache来解决脏读问题,内部使用一个额外的map来保存事务中未提交的数据,当事务提交时,才将这个map中的数据迁移到二级缓存中,从而避免了脏读。

详解第二种脏读解决思路:

CachingExecutor中有一个TransactionalCacheManager对象,内部能保存多个TransactionalCache,这又是一种Cache的实现类,用于来解决脏读问题

CachingExecutor内进行查询时,通过传入的MappedStatement获取到对应命名空间下的二级缓存,此时是一个SynchronizedCache。然后通过TransactionalCacheManager将其再装饰成TransactionalCache对象。后续使用这个装饰后的对象进行缓存读写。

TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache

TransactionalCache内和其他Cache实现类一样,使用一个delegate属性记录被装饰对象外,还加了一个成员属性entriesToAddOnCommit,用于记录未提交的数据。也就是说事务内的中间数据保存到了entriesToAddOnCommit而不是实际的二级缓存中,所以其他线程中对这些数据不可见。

 

当executor commit或者close时,会触发TransactionalCacheManager的commit,从而触发所有TransactionalCache对象的commit方法,会将entriesToAddOnCommit中的数据写入到delegate中。即事务要提交的数据都提交到了二级缓存中,其他线程就能读取到了。

以上可以看出,当一个事务提交了,二级缓存就会发生变化,就可能导致另一个事务中出现“不可重复度”问题。这是mybatis中由二级缓存带来的不可避免的问题,开发中需要注意一下。

关于一级缓存

mybatis有一个特性:session关闭时,一级缓存会转换为二级缓存。

 这实际是通过TransactionalCacheManager来实现的,从数据库查询得来的数据,会保存到BaseExecutor#localCache中,也会保存到二级缓存对应的TransactionalCache中,也就是说保存了两次引用,当session关闭时,BaseExecutor#localCache会被直接清空,然后触发TransactionalCacheManager#close方法,内部会把TransactionalCache中保留的缓存TransactionalCache#entriesToAddOnCommit迁移到delegate,即二级缓存中,就有了一级缓存转换为二级缓存的效果了。

以上是关于mybatis缓存的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis关于复杂的SQL查询的处理&Mybatis的缓存机制

Mybatis 学习笔记总结

推荐学java——MyBatis高级

推荐学java——MyBatis高级

推荐学java——MyBatis高级

推荐学java——MyBatis高级