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的脏读问题有两种:
- 不同命名空间有自己的二级缓存,当某个二级缓存缓存了另一个命名空间中的缓存时,另一个空间中的缓存已经更新了,但之前被缓存的数据没有关联更新,导致出现脏读。可以配置参照缓存来解决
- 并发调用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缓存的主要内容,如果未能解决你的问题,请参考以下文章