Mybatis缓存

缓存

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

缓存结构

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

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

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

缓存继承关系:

Mybatis缓存_第1张图片
mybatis缓存总体分为三大类:基本缓存、淘汰算法缓存、装饰器缓存

Mybatis缓存_第2张图片

一级缓存

一级缓存也叫本地缓存,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。

 


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) localCache.getObject(key) : null;
if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    // 真正的查询流程
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List 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来执行查询,
再走到一级缓存。最后把结果缓存起来,返回给用户。

Mybatis缓存_第3张图片

二级缓存开启方式

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


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

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

2.在Mapper.xml配置 标签:


       eviction="LRU" 
       flushInterval="120000" 
       readOnly="false"/> 

cache属性详解:

Mybatis缓存_第4张图片

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

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

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

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

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