Mybatis缓存

该文章是综合在网上一些对mybatis二级缓存的原理解析。

 

像大多数的持久化框架一样,Mybatis也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。Mybatis 中缓存分为一级缓存,二级缓存。

一级缓存

一级缓存只是相对于同一个SqlSession而言。如果在参数和SQL完全一样的情况下,使用同一个SqlSession对象调用一个Mapper方法,只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。

一级缓存的生命周期有多长?

  • MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  • 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
  • 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
  • SqlSession中执行了commit操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 

源码解析

由前面Mybatis执行过程的分析可以知道,Mybatis是将任务交给Executor,在由Executor交给Handler实现增删改查。

在BaseExecutor中,发现Executor执行查询前,会先去查询缓存。

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        //设置一级缓存的key
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                //判断resultHandler是否为空,为空就从缓存中根据一级缓存的KEY获取值
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                //判断缓存是否为空
                if (list != null) {
                    //操作本地缓存输出
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    //如果为空,就执行去数据库查询
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

 且BaseExecutor中由两个属性

 protected PerpetualCache localCache;
 protected PerpetualCache localOutputParameterCache;

而PerpetualCache是接口Cache的实现类

它用一个Map来维护缓存

private Map<Object, Object> cache = new HashMap();

再来看一下key的产生方法

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(rowBounds.getOffset());
            cacheKey.update(rowBounds.getLimit());
            cacheKey.update(boundSql.getSql());
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            Iterator var8 = parameterMappings.iterator();

            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            if (this.configuration.getEnvironment() != null) {
                cacheKey.update(this.configuration.getEnvironment().getId());
            }

            return cacheKey;
        }
    }

所以statementId 、 rowBounds 、传递给JDBC的SQL 和 rowBounds.limit决定key中的hashcode。

 

二级缓存

二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession 的。 


从上面三张图中我们得出结论,一级缓存是sqlsession级别、二级缓存是Mapper级别。mybatis定义了【Cache】接口,支持自定义缓存,同时还支持集成第三方缓存库,现在为了更细粒度的控制缓存,更多的集成【ehcache】、【redis】。

那么mybatis的二级缓存主要是在Executor对象上来做文章,当mybatis发现你在mybatis.xml配置文件中设置cacheEnabled=true时,mybatis在创建sqlsession时创建Executor对象,同时会对Executor加上装饰者【CacheExecutor】。CacheExecutor对于查询请求,会判断application级别的二级缓存是否有缓存结果,如果有查询结果则直接返回,如果没有再交给查询器Executor实现类,也就是【SimpleExecutor】来执行查询。再就是缓存结果,返回给用户。

 

配置方式

1.在主配置文件中加入cacheEnabled=true配置,其实这里不用配置,默认情况下打开的。

<settings>
        <setting name="cacheEnabled" value="true" />
</settings>

2.再在映射配置文件中加入<cache>配置

<!--
    eviction LRU
    flushInterval缓存时间,以毫秒为单位
    size缓存大小
    readOnly如果为false的话,缓存对象必须是可序列化的-->
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="120000"
           size="1024"
           readOnly="true"/>

这里eviction属性有三个值可选:

 LRU:(Least Recently Used),最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近做少被使用的缓存记录清除掉,然后添加新的记录;

FIFO:(First in first out),先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉;

Scheduled:指定时间间隔清空算法,该算法会以指定的某一个时间间隔将Cache缓存中的数据清空;

3.statement语句的配置

flushCache

在映射文件的statement标签,可以设置是否刷新缓存。

flushCache = "true" 时可以刷新当前的二级缓存。

在默认情况下:

  • select语句:flushCache是false,也就是默认情况下,select语句是不会刷新缓存的。如果设置成true,那么每次查询都会去数据库查询,意味着查询的二级缓存失效;

  • insert、update、delete语句:flushCache是true,也就是默认情况下,增删改是会刷新缓存的。

useCache

默认情况下是true,即该statement使用二级缓存;当​​​​​​statement中设置userCache=false,可以禁用当前select语句的二级缓存,即每次查询都是去数据库中查询。

 

Mybatis整合使用ehcache

为什么使用第三方缓存框架?

Mybatis它是一个持久层的框架,不是一个缓存框架。所以说它本身的缓存机制不是很好,不能支持分布式,所以需要对它进行整合,整合其他的分布式缓存框架,ehcache和redis都可以。

整合缓存框架

使用mybatis默认的二级缓存时,在映射文件中只写了一个cache,其中就是默认使用的mybatis默认的二级缓存,也就是这个实现类,如果整合其他缓存框架的话,那么就需要改变了。

         1.添加ehcache的jar包

         2.添加ehcache的配置文件,配置文件如下

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 2     xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
 3     <!-- 缓存数据要存放的磁盘地址 -->
 4     <diskStore path="F:\develop\ehcache" />
 5     <!-- diskStore:指定数据在磁盘中的存储位置。  defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略 
 6         以下属性是必须的:  maxElementsInMemory - 在内存中缓存的element的最大数目  maxElementsOnDisk 
 7         - 在磁盘上缓存的element的最大数目,若是0表示无穷大  eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断 
 8          overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 以下属性是可选的:  timeToIdleSeconds 
 9         - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大 
10          timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大 diskSpoolBufferSizeMB 
11         这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.  diskPersistent 
12         - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。  diskExpiryThreadIntervalSeconds 
13         - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作  memoryStoreEvictionPolicy 
14         - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出) -->
15 
16     <defaultCache maxElementsInMemory="1000"
17         maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false"
18         timeToIdleSeconds="120" timeToLiveSeconds="120"
19         diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU">
20     </defaultCache>
21 </ehcache>

          3.在映射文件中设置cache标签的type

这样就整合完成了。

 

二级缓存的优缺点

优点:对于查询多,commit少且用户对查询结果实时性要求不高的业务,此时采用mybatis二级缓存技术降低数据库访问量,提高访问速度。

缺点:二级缓存也有很多弊端,从MyBatis默认二级缓存是关闭的就可以看出来。

二级缓存是建立在同一个namespace下的,如果对表的操作查询可能有多个namespace,那么得到的数据就是错误的。

举个简单的例子:

订单和订单详情,orderMapper、orderDetailMapper。在查询订单详情时我们需要把订单信息也查询出来,那么这个订单详情的信息被二级缓存在orderDetailMapper的namespace中,这个时候有人要修改订单的基本信息,那就是在orderMapper的namespace下修改,他是不会影响到orderDetailMapper的缓存的,那么你再次查找订单详情时,拿到的是缓存的数据,这个数据其实已经是过时的。

 

根据以上,想要使用二级缓存时需要想好两个问题:

1)对该表的操作与查询都在同一个namespace下,其他的namespace如果有操作,就会发生数据的脏读。

2)对关联表的查询,关联的所有表的操作都必须在同一个namespace。

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值