MyBatis缓存详解

Posted yybinger

tags:

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

缓存体系结构

缓存一般是ORM框架都会提供的功能,目的就是提升查询效率和减少数据库的压力。跟Hibernate一样,MyBatis也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

MyBatis跟缓存相关的类都在cache包里面,其中有一个Cache接口,只有一个默认的实现类PerpetualCache,它使用HashMap实现的。

除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等。

如果对装饰器不懂的,可以去看一下设计模式中的装饰者模式的资料。

但无论怎么装饰,最后使用的还是最基本的实现类PerpetualCache。

所有的缓存实现类总体上可以分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

缓存实现类 描述 作用 装饰条件
基本缓存 缓存基本实现类 默认是PerpetualCache,也可以自定义比如RedisCache、EhCache等,具备基本功能的缓存类
LruCache LRU策略的缓存 当缓存达到上限时,删除最近最少使用的缓存(Least Recently Use) eviction="LRU"(默认)
FifoCache FIFO策略的缓存 当缓存到达上限时,删除最先入队的缓存 eviction="FIFO"

SoftCache

WeakCache

带清理策略的缓存 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存,基于SoftReference和WeakReference

eviction="SOFT"

eviction="WEAK"

LoggingCache 带日志功能的缓存 比如:输出缓存命中率 基本
SynchronizedCache 同步缓存 基于synchronized关键字实现,解决并发问题 基本
BlockingCache 阻塞缓存 通过在get/put方式中加锁,保证只有一个县城操作缓存,基于Java重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 将对象序列化后放到缓存中,取出时反序列化 readOnly=false(默认)
ScheduledCache 定时调度的缓存 在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存。即每隔一段时间清空一次缓存 flushInterval不为空
TransactionCache 事物缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 在TransactionCacheManager中用Map维护对应关系

 

 

 

 

 

 

 

 

 

 

 

一级缓存

一级缓存也叫本地缓存,MyBatis的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何配置。

在MyBatis执行的流程里面,涉及到那么多的对象,那么缓存PerpetualCache应该放到哪个对象里面去维护?如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession里面创建的,作为SqlSession的一个属性。

SqlSession只有一个默认实现类DefaultSqlSession,DefaultSqlSession里面只有两个属性,Configuration是全局的,所以缓存只可能放在Executor里面维护--SimpleExecutor/ReUseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache。

在同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再发送SQL到数据库,但是不同的会话之间,即使执行相同的SQL语句(同一个Mapper的同一个方法的相同参数),也不能使用到一级缓存。

技术图片

查看BaseExecutor的query源代码:

 1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
 3     if (closed) {
 4       throw new ExecutorException("Executor was closed.");
 5     }
 6     if (queryStack == 0 && ms.isFlushCacheRequired()) {
 7       clearLocalCache();
 8     }
 9     List<E> list;
10     try {
11       queryStack++;
12       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
13       if (list != null) {
14         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
15       } else {
16         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
17       }
18     } finally {
19       queryStack--;
20     }
21     if (queryStack == 0) {
22       for (DeferredLoad deferredLoad : deferredLoads) {
23         deferredLoad.load();
24       }
25       // issue #601
26       deferredLoads.clear();
27       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
28         // issue #482
29         clearLocalCache();
30       }
31     }
32     return list;
33 }
34 
35 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
36     List<E> list;
37     localCache.putObject(key, EXECUTION_PLACEHOLDER);
38     try {
39       list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
40     } finally {
41       localCache.removeObject(key);
42     }
43     localCache.putObject(key, list);
44     if (ms.getStatementType() == StatementType.CALLABLE) {
45       localOutputParameterCache.putObject(key, parameter);
46     }
47     return list;
48 }

看到,一级缓存是在BaseExecutor的query()--queryFromDatabase()中存入,在getFromDatabase()之前会get()。

而在同一个会话中,update(包括delete)会导致一级缓存被清空:

1 public int update(MappedStatement ms, Object parameter) throws SQLException {
2     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
3     if (closed) {
4       throw new ExecutorException("Executor was closed.");
5     }
6     // 清空一级缓存
7     clearLocalCache();
8     return doUpdate(ms, parameter);
9 }

一级缓存的不足

当有两个会话时,第一个会话先从数据库中查出一条记录放到缓存中了,这时第二个会话对这条记录进行了update,由于一级缓存只能在同一个会话中共享,所以此时第一个会话中的缓存中的数据不会变,导致会话一第二次查询时直接从缓存拿到了脏数据。如果要解决这个问题,就要用到二级缓存。

二级缓存

 二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个Mapper中的同一个方法,都可以共享),生命周期和应用同步。

作为一个作用范围更广的缓存,他肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。而一级缓存是在SqlSession内部的,所以,二级缓存是工作在一级缓存之前的,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

二级缓存放在哪个对象中维护呢?要跨会话共享的话,SqlSession本身和它里面的Executor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor对象的时候会对Executor进行装饰。

CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有就委派交给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,再走到一级缓存的流程,最后会把结果缓存起来,并且返回给用户。

技术图片

 开启二级缓存的方法

第一步:在MyBatis的全局配置文件mybatis-config.xml中配置:

1 <settings>
2     <setting name="cacheEnabled" value="true"/>
3 </settings>

 

只要没有显示的设置cacheEnabled=false,都会用CachingExecutor装饰基本的执行器。

第二步:在Mapper.xml中配置<cache/>标签:

 1 <!-- 声明这个namespace使用二级缓存 
 2     size: 最多缓存对象个数,默认1024
 3     eviction: 回收策略,默认LRU
 4     flushInterval: 自动刷新时间 ms,未配置时只有调用时刷新
 5     readOnly: 默认是false(安全),改为true时可读写,对象必须支持序列化
 6 -->
 7 <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
 8        size="1024"
 9        eviction="LRU"
10        flushInterval="120000"
11        readOnly="false"/>

 

 cache属性详解:

属性 含义 取值
type 缓存实现类 需要实现Cache接口,默认是PerpetualCache
size 最多缓存对象个数 默认1024
eviction 回收策略(缓存淘汰算法)

LRU - 最近最少使用的:移除最长时间不被使用的对象(默认)。

FIFO - 先进先出:按对象进入缓存的顺序来移除他们。

SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象。

WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

flushInterval 定时自动清空缓存间隔 自动刷新时间,单位ms,未配置时只有调用时刷新
readOnly 是否只读

true:只读缓存:会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。

false:读写缓存:会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是false。对象必须支持序列化

blocking 是否使用可重入锁实现缓存的并发控制 true,会使用BlockingCache对Cache进行装饰。默认是false。

 

 

 

 

 

 

 

 

 

Mapper.xml中配置了<cache/>之后,select()会被缓存,insert()、update()、delete()会刷新缓存。

如果mybatis-config.xml中配置了cacheEnabled=true,Mapper.xml中没有配置<cache/>,还有二级缓存吗?

只要cacheEnabled设置为true,BaseExecutor就会被装饰为CachingExecutor。Mapper.xml中有没有配置<cache/>,决定了在启动的时候会不会创建这个mapper的Cache对象,最终会影响到CachingExecutor的query()方法里面的判断:

 1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 2       throws SQLException {
 3     Cache cache = ms.getCache();
 4     if (cache != null) {
 5       flushCacheIfRequired(ms);
 6       if (ms.isUseCache() && resultHandler == null) {
 7         ensureNoOutParams(ms, boundSql);
 8         @SuppressWarnings("unchecked")
 9         List<E> list = (List<E>) tcm.getObject(cache, key);
10         if (list == null) {
11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
12           tcm.putObject(cache, key, list); // issue #578 and #116
13         }
14         return list;
15       }
16     }
17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
18 }

 

如果某些查询方法对数据的实时性要求很高,不需要二级缓存,可以在单个StatementId上显示关闭二极缓存useCache="false"(默认是true):

1 <select id="selectByPk" resultMap="BaseResultMap" useCache="false">
2     select * from emp where empno = #{empNo}
3 </select>

 

什么时候开启二级缓存

1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就是去了意义 。

2、如果多个namespace中有针对于同一个表的操作,比如emp表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper里面只操作单表的情况使用。

跨namespace的缓存共享问题,可以使用<cache-ref>标签来解决:

<cache-ref namespace="test.EmployeeMapper"/>

 

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是用一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:在这种情况下,多个Mapper的操作都会引起缓存的刷新,缓存的意义已经不大了。

第三方缓存做二级缓存

除了MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级缓存。

MyBatis官方提供了一些第三方缓存集成方式,比如ehcache和redis:

https://github.com/mybatis/redis-cache

 pom文件中引入依赖:

1 <dependency>
2     <groupId>org.mybatis.caches</groupId>
3     <artifactId>mybatis-redis</artifactId>
4     <version>1.0.0-beta2</version>
5 </dependency>

 

Mapper.xml配置,type使用RedisCache:

1 <cache type="org.mybatis.caches.redis.RedisCache"
2    size="1024"
3    eviction="LRU"
4    flushInterval="120000"
5    readOnly="false"/>

 

redis.properties配置:

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

 

 

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

Yii2片段缓存详解

Mybatis的缓存机制详解

《深入理解mybatis原理6》 MyBatis的一级缓存实现详解 及使用注意事项

MyBatis缓存详解

mybatis 详解------ 一级缓存二级缓存

mybatis缓存机制详解 #yyds干货盘点#