Fork me on GitHub

MyBatis缓存机制

注意:所有文章除特别说明外,转载请注明出处.

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
2
public abstract class BaseExecutor implements Executor {protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;

==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
    6
    private 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
2
3
4
@Overridepublic void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}

CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。

1
2
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);

之后会尝试从tcm中获取缓存的列表。

1
List<E> list = (List<E>) tcm.getObject(cache, key);

在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Map集合,这个主要是为了统计命中率。

1
2
3
Object object = delegate.getObject(key);if (object == null) {
entriesMissedInCache.add(key);
}

CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。

1
2
3
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116}

tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。

1
2
3
4
@Overridepublic void putObject(Object key, Object object) {

entriesToAddOnCommit.put(key, object);
}

从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示:

1
2
@Overridepublic void commit(boolean force) {    try {
executor.commit(isCommitOrRollbackRequired(force));

因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。

1
2
3
4
@Overridepublic void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}

会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。

1
2
3
4
5
6
public void commit() {    if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}

看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:

1
2
3
4
5
private void flushPendingEntries() {    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
................
}

在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 整合包(最上面的源代码中包含有)

  img

②、在全局配置文件 mybatis-configuration.xml 开启缓存

1
`<!--开启二级缓存  -->``<settings>``    ``<setting name=``"cacheEnabled"` `value=``"true"``/>``</settings>`

③、在 xxxMapper.xml 文件中整合 ehcache 缓存

将如下的类的全类名写入的type属性中

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(先进先出)

本文标题:MyBatis缓存机制

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:07:02

原始链接:http://bangjinhu.github.io/undefined/MyBatis缓存机制/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.