Mybatis缓存

Posted snail-gao

tags:

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

缓存

缓存是一般ORM框架都有的功能,目的就是提高查询的效率和减少数据库的压力。

缓存结构

Mybatis源码中与缓存相关的类都在cache包中,其中有一个Cache接口,默认实现类PerpetualCache,他是由HashMap实现的,是基础缓存。

Mybatis的缓存功能是采用装饰器模式实现的。

装饰器模式:在不改变原对象的基础上,将功能附加到对象上,提供了比继承更有弹性的代替方案。

缓存继承关系:

技术图片
mybatis缓存总体分为三大类:基本缓存、淘汰算法缓存、装饰器缓存

技术图片

一级缓存

一级缓存也叫本地缓存,Mybatis的一级缓存实在会话层进行缓存的。Mybatis的一级缓存默认是开启的,不需要任何的配置。伪关闭方法提高缓存级别(localCacheScope设置为STATEMENT,只针对statement有效)

BaseExecutor的query()

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
}

mybatis执行的流程里面,缓存对象PerpetualCache是哪个对象维护的呢?

Mybatis一级缓存是与SqlSession共存亡的,所以就不需要为SqlSession编号、再根据SqlSession的编号去查询对应的缓存了。

DefaultSqlSession里面有两个对象属性: Configuration和Executor

其中Configuration是全局的,不属于SqlSession,所以缓存维护在Executor里面--实际上他维护在基本执行器SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有PrepetualCache。

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
}

同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再去查询数据库。但不同的会话里面,执行相同的SQL,也会去查询数据库语句,不走一级缓存。

一级缓存验证

首先关闭二级缓存,localCacheScope设置为SESSION。

 <!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="SESSION"/>

1.在同一个session中共享

UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));
System.out.println(mapper.selectOne(1));

2.不同session中不能共享

SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));

一级缓存在BaseExecutor的query()--queryFromDatabase()中存入。在queryFromDatabase之前会get()。

//从缓存中获取数据(key是CacheKey)
//一级缓存和二级缓存的CacheKey是同一个
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    // 真正的查询流程
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 先占位
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 默认Simple
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 移除占位符
        localCache.removeObject(key);
    }
    // 写入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

3.同一个会话中,update(包括delete)会导致一级缓存清空

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    //先执行缓存清空操作
    clearLocalCache();
    return doUpdate(ms, parameter);
}

QA:只有更新才会清空缓存吗?查询会清空缓存吗?怎么清空?

一级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的,如果是query只有select标签的flushCache=true才清空。

一级缓存的工作范围是一个会话。如果跨回话,出现什么问题?

4.其他会话更新会导致当前会话读到的数据是过时的数据(不能跨会话共享)

//会话2更新数据
UserMapper mapper2 = session.getMapper(UserMapper.class);
mapper.updateById(user);
session.commit();
//会话1读取到过时的数据,一级缓存不能夸会话共享
System.out.println(mapper1.selectOne(1));
不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在查询到过时数据的问题。如果要解决这个问题,需要开启二级缓存。

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题。

QA:如果开启了二级缓存,是在一级缓存前面还是后面执行呢?怎么维护的?


作为一个作用范围更广的缓存,可定在SqlSession的外层,不然做不到SqlSession共享。

而一级缓存是在SqlSession内部的,所以是在一级缓存前面执行,只有二级缓存找不到才会去一级缓存找。

那么二级缓存在哪里维护的呢? 跨会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,所以应该在BaseExecutor之外创建。但只有二级缓存开启后才能加载这个对象。

实际上Mybatis使用了一个装饰器类(CachingExecutor)来维护。

如果启用了二级缓存。Mybatis在创建Executor对象的时候会对Executor进行装饰。

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

技术图片

二级缓存开启方式

1.在mybatis-config.xml中配置了(默认true)

<setting name="cacheEnable" value="true"/>

只要开启了二级缓存,都会使用CachingExecutor装饰基本的执行器(SIMPLE、REUSE、BATCH)

二级缓存默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要使用二级缓存,还要单独配置。

2.在Mapper.xml配置标签:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
       size="1024" <!--最大缓存个数,默认1024-->
       eviction="LRU" <!--缓存策略-->
       flushInterval="120000" <!-- 自动刷新时间,未配置时只有调用时刷新 -->
       readOnly="false"/> <!-- 默认false,改为true可读可写,对象必须支持序列化 -->

cache属性详解:

技术图片

Mapper.xml配置了之后。select()会被存储。update()、delete()、insert()会刷新缓存。

QA:如果cacheEnable=true,Mapper.xml没有配置标签,还会走二级缓存吗?还会使用CachingExecutor包装对象吗?

只要cacheEnable=true基本执行器就会被装饰。有没有配置<cache>,决定了在启用的时候能不能创建mapper这个Cache对象,最终会影响到CachingExecutor query方法里面的判断。
也就是说,此时会被装饰,但没有cache对象,依然不会走二级缓存。

QA:如果一个Mapper需要开启二级缓存,但是这里面的某些查询方法对数据实时性要求很高,不需要二级缓存,怎么办?

可以在单个Statement ID上显示关闭二级缓存(默认是true)

<select id="selectUser" resultMap="BaseResultMap" useCache="false">

CachingExecutor query方法的判断:

// cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
    // flushCache="true" 清空一级二级缓存 >>
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 获取二级缓存
        // 缓存通过 TransactionalCacheManager、TransactionalCache 管理
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
            list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            // 写入二级缓存
            tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
    }
}
二级缓存验证
UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));
//事务不提交的情况下,二级缓存不会写入
session.commit();
UserMapper mapper2 = session2.getMapper(userMapper.class);
System.out.println(mapper2.selectOne(1));

QA:为什么事务不提交,二级缓存不生效?

    因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache的getObject()、putObject和commit方法,TransactionCache
里面又持有真正的Cache对象,列入被层层装饰的PerpetualCache对象。
    在putObject的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit()方法被调用的时候才会调用flushPendingEntries()真正写入缓存。
他就是在DefaultSqlSession调用commit()的时候被调用的。

~~~java
public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    // 真正写入二级缓存
    flushPendingEntries();
    reset();
}

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

QA:为什么增删改会清空缓存?

在CachingExecutor的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面渠道的flushCache的值。而增删改操作的flush属性默认为true.

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
}

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // 增删改查的标签上有属性:flushCache="true" (select语句默认是false)
    // 一级二级缓存都会被清理
    if (cache != null && ms.isFlushCacheRequired()) {
        tcm.clear(cache);
    }
}

也就是说,如果不需要清空二级缓存,可以把flushCache属性修改成false(这样会造成过时数据的问题)。

二级缓存的使用场景

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

2、如果多个namespace中有针对性同一个表的操作,比如user表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。

所以推荐在一个Mapper里面只操作单表的情况使用。

QA:怎么让多个namespace共享一个二级缓存?

跨namespace的缓存共享的问题,可以使用来解决:

<cache-ref namespace="com.xxx.xxx.dao.UserMapper"/>

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

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

使用第三方作为二级缓存

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

例如集成redis做二级缓存:

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

pom.xml文件依赖:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

Mapper.xml配置,type使用RedisCache:

<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

redis.properties配置:

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

Redis作为二级缓存的验证(需要安装Redis客户端):RedisManager

当然在分布式环境中也可以单独的使用缓存服务,不使用Mybatis自带的二级缓存。


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

Mybatis关于复杂的SQL查询的处理&Mybatis的缓存机制

Mybatis 学习笔记总结

推荐学java——MyBatis高级

推荐学java——MyBatis高级

推荐学java——MyBatis高级

推荐学java——MyBatis高级