注意:所有文章除特别说明外,转载请注明出处.
MyBatis缓存机制
[TOC]
MyBatis的缓存机制以及流程
mybatis提供了缓存机制减轻数据库压力,提高数据库性能
mybatis的缓存分为两级:==一级缓存、二级缓存==
一级缓存
一级缓存是SqlSession级别的缓存,缓存的数据只在SqlSession内有效
在操作数据库的时候需要先创建SqlSession会话对象,在对象中有一个HashMap用于存储缓存数据,此HashMap是当前会话对象私有的,别的SqlSession会话对象无法访问,每一个 session 会话都会有各自的缓存,这缓存是局部的,也就是所谓的一级缓存
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache
配置:
开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效
1 | <setting name="localCacheScope" value="SESSION"/> |
具体流程:
1.第一次执行select完毕会将查到的数据写入SqlSession内的HashMap中缓存起来
2.第二次执行select会从缓存中查数据,如果select相同切传参数一样,那么就能从缓存中返回数据,不用去数据库了,从而提高了效率‘
源码
首先需要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession
SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList
- SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法
- 在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因
- SqlSession的insert方法和delete方法,都会统一走update的流程
- 每次执行update前都会清空localCache
==BaseExecutor==: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
1 | protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; |
在一级缓存的介绍中提到对Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后发现Local Cache是BaseExecutor内部的一个成员变量,如下代码所示。
1 | public abstract class BaseExecutor implements Executor {protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; |
==Cache==: MyBatis中的Cache接口,提供了和缓存相关的最基本的操作
BaseExecutor成员变量之一的==PerpetualCache,是对Cache接口最基本的实现==,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作
注意事项:
1.如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前SqlSession缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读
2.当一个SqlSession结束后那么他里面的一级缓存也就不存在了,mybatis默认是开启一级缓存,不需要配置t
3.mybatis的缓存是基于[namespace:sql语句:参数]来进行缓存的,意思就是,SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]作为key,查询返回的语句作为value保存的
二级缓存
二级缓存是mapper级别的缓存,同一个namespace公用这一个缓存,所以对SqlSession是共享的
就是同一个namespace的mappe.xml,当多个SqlSession使用同一个Mapper操作数据库的时候,得到的数据会缓存在同一个二级缓存区, 如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中
1.二级缓存默认是没有开启的。需要在setting全局参数中配置开启二级缓存
1 | <setting name="cacheEnabled" value="true"/> |
2.在MyBatis的映射XML中配置cache或者 cache-ref 。
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。
1 | <cache/> |
- type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过
- eviction: 定义回收的策略,常见的有FIFO,LRU。
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
1 | <cache-ref namespace="mapper.StudentMapper"/> |
在userMapper.xml中配置:
1 | <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>当前mapper下所有语句开启二级缓存 |
这里配置了一个LRU缓存,并每隔60秒刷新,最大存储512个对象,而却返回的对象是只读的
若想禁用当前select语句的二级缓存,添加useCache=”false”修改如下:
1 | <select id="getCountByName" parameterType="java.util.Map" resultType="INTEGER" statementType="CALLABLE" useCache="false"> |
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库
开启二级缓存后,会使用==CachingExecutor==装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示
具体流程:
1.当一个sqlseesion执行了一次select后,在关闭此session的时候,会将查询结果缓存到二级缓存
2.当另一个sqlsession执行select时,首先会在他自己的一级缓存中找,如果没找到,就回去二级缓存中找,找到了就返回,就不用去数据库了,从而减少了数据库压力提高了性能
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache
然后是判断是否需要刷新缓存,代码如下所示:
1
flushCacheIfRequired(ms);
在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示:
1
2
3
4
5
6private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}MyBatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。
TransactionalCacheManager中持有了一个Map,代码如下所示:
1 | private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>(); |
这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。
TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:
1 | void clear() { |
CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。
1 | if (ms.isUseCache() && resultHandler == null) { |
之后会尝试从tcm中获取缓存的列表。
1 | List<E> list = (List<E>) tcm.getObject(cache, key); |
在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Map集合,这个主要是为了统计命中率。
1 | Object object = delegate.getObject(key);if (object == null) { |
CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。
1 | if (list == null) { |
tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。
1 | void putObject(Object key, Object object) { |
从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示:
1 | void commit(boolean force) { try { |
因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。
1 | void commit(boolean required) throws SQLException { |
会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。
1 | public void commit() { if (clearOnCommit) { |
看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:
1 | private void flushPendingEntries() { for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { |
在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。
后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了这个函数,代码如下所示:
1 | private void flushCacheIfRequired(MappedStatement ms) |
useCache和flushCache
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
1 | `<select id=``"selectUserByUserId"` `useCache=``"false"` `resultType=``"com.ys.twocache.User"` `parameterType=``"int"``>`` ``select * from user where id=#{id}``</select>` |
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数据库中获取。
在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。
设置statement配置中的flushCache=”true” 属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
1 | `<select id=``"selectUserByUserId"` `flushCache=``"true"` `useCache=``"false"` `resultType=``"com.ys.twocache.User"` `parameterType=``"int"``>`` ``select * from user where id=#{id}``</select>` |
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读
注意事项:
1.如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前mapper缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读
2.mybatis的缓存是基于[namespace:sql语句:参数]来进行缓存的,意思就是,SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]作为key,查询返回的语句作为value保存的。
二级缓存整合ehcache
以上是mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了1服务器,查询后的缓存就会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个缓存
在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后无论有多少台服务器,我们都能从缓存中获取数据,即mybaits和第三方框架ehcache的整合
mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。mybatis本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。ehcache分布式缓存就可以,mybatis提供了一个针对cache接口的ehcache实现类,这个类在mybatis和ehcache的整合包中。
①、导入 mybatis-ehcache 整合包(最上面的源代码中包含有)
②、在全局配置文件 mybatis-configuration.xml 开启缓存
1 | `<!--开启二级缓存 -->``<settings>`` ``<setting name=``"cacheEnabled"` `value=``"true"``/>``</settings>` |
③、在 xxxMapper.xml 文件中整合 ehcache 缓存
将如下的类的全类名写入
1 | `<!-- 开启本mapper的namespace下的二级缓存`` ``type:指定cache接口的实现类的类型,不写type属性,mybatis默认使用PerpetualCache`` ``要和ehcache整合,需要配置type为ehcache实现cache接口的类型``-->``<cache type=``"org.mybatis.caches.ehcache.EhcacheCache"``></cache>` |
④、配置缓存参数
在 classpath 目录下新建一个 ehcache.xml 文件,并增加如下配置:
1 | `<?xml version=``"1.0"` `encoding=``"UTF-8"``?>``<ehcache xmlns:xsi=``"http://www.w3.org/2001/XMLSchema-instance"`` ``xsi:noNamespaceSchemaLocation=``"../config/ehcache.xsd"``>` ` ``<diskStore path=``"F:\develop\ehcache"``/>` ` ``<defaultCache`` ``maxElementsInMemory=``"10000"`` ``eternal=``"false"`` ``timeToIdleSeconds=``"120"`` ``timeToLiveSeconds=``"120"`` ``maxElementsOnDisk=``"10000000"`` ``diskExpiryThreadIntervalSeconds=``"120"`` ``memoryStoreEvictionPolicy=``"LRU"``>`` ``<persistence strategy=``"localTempSwap"``/>`` ``</defaultCache>`` ` `</ehcache>` |
diskStore:指定数据在磁盘中的存储位置。
defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用
以下属性是必须的:
maxElementsInMemory - 在内存中缓存的element的最大数目
maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要 根据timeToIdleSeconds,timeToLiveSeconds判断
overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
diskExpiryThreadIntervalSeconds - 磁盘缓存的 清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)