MyBatis二级缓存解密:深入探究缓存机制与应用场景
MyBatis的二级缓存是一种跨会话的缓存机制,可以提高数据库访问的性能和效率。本文将深入研究MyBatis二级缓存,详细解析其工作原理、配置方式和使用场景。我们将探讨二级缓存的生命周期、作用范围、失效策略等关键特性,以及如何正确配置和优化二级缓存以提高系统性能。
在上文《探秘MyBatis缓存原理:Cache接口与实现类源码分析》中,我们已经介绍了 MyBatis 的 Cache 接口以及对应的实现类。其中的 PerpetualCache 是 MyBatis 缓存的最基础的实现类,底层通过 HashMap 存储数据,其他的实现类都属于装饰器,基于 PerpetualCache 的各个方面进行增强,各个实现类的理论和实现我们学习过后,本文我们就来探究一下,MyBatis 真正的缓存机制是怎么样的!
MyBatis 缓存机制
MyBatis 为提高其数据库查询性能,提供了两层缓存机制(只针对查询做缓存),包括一级缓存和二级缓存。
- ⼀级缓存:将查询到的数据存储到 SqlSession 中,所以只对本 SqlSession 有效。范围比较小,只对于一次 SQL 会话。
- ⼆级缓存:将查询到的数据存储到 SqlSessionFactory 中。范围比较大,针对于整个数据库级别。
MyBatis 也可以集成其它第三方缓存:比如基于 Java 开发的 EhCache、基于 C 语言开发的 Memcache等。
二级缓存
二级缓存也叫全局缓存。数据存放在 SqlSessionFactory 的 Configuration 中,只要是同一个工厂中对象创建的 SqlSession,在进行查询时都能共享数据。一般在项目中只有一个 SqlSessionFactory 对象,所以二级缓存的数据是全项目共享的,它可以在多个会话之间共享缓存数据,有效减少数据库访问次数,提高系统性能和响应速度。
默认情况下,MyBatis 在解析 SQL XML 文件的时候,会为每个 XML 文件创建一个二级缓存,这个 XML 中的多个查询结果都共用这同一个 Cache,当然也可以通过 <cache-ref>
标签来配置多个 XML 复用同一个 Cache,通常会在多表查询的时候会指定同一个 Cache,这是为了避免脏数据。
因为在对数据库进行更新的时候会清理当前 MappedStatement 引用的 Cache,但是如果别的 XML 文件中的 MappedStatement 的查询也使用到了这个表,那么别的 XML 的 Cache 中就会出现脏数据了。
但是如果都指定同一个 Cache,那么在更新的时候会频繁的清理当前的 Cache,而且是全部清理,这样显然不是很好,所以我们有必要更改 MyBatis 底层的缓存清除策略。
- 作用范围:跨会话缓存,二级缓存是跨多个 SqlSession 的缓存机制,可以在不同的会话之间共享缓存数据。
- 生命周期:应用级别缓存,二级缓存的生命周期与应用程序的生命周期相同,可以在整个应用程序中共享数据。
- 默认开启:二级缓存可以通过 MyBatis 配置文件中的
<setting name="cacheEnabled" value="true"/>
控制,默认就是 true,可以理解为默认开启,但是还是需要对应的 Mapper XML 配置<cache>
标签后才能生效。 - 缓存策略:二级缓存的配置通常配置在映射文件(Mapper XML 文件)的
<mapper>
标签内部。配置<cache>
元素,可用于配置二级缓存的属性,如缓存类型、缓存的大小、刷新间隔等。在映射文件添加<cache />
标签,该映射文件下的所有方法都支持二级缓存。该标签有 size 属性,可以设置缓存中的对象数量,默认是 1024 个。 - 缓存命中:当执行查询时,MyBatis 会先检查二级缓存中是否存在相应的结果。如果存在,则直接从缓存中返回结果,而不需要访问数据库。
- 缓存失效:对于更新、插入或删除操作,会导致相应的缓存失效,需要重新查询更新后的结果,同一个 XML 中的 SQL 是共用同一个缓存的,失效的时候是指这个 XML 中的多个查询 SQL 都会失效,多个 MappedStatement 引用的同一个 Cache。
- 数据同步问题:由于二级缓存是跨会话的,可能会出现数据不一致的情况。在更新或删除数据时,需要及时清除相应的缓存,以保持数据的一致性。
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是:二级缓存 -> 一级缓存 -> 数据库。
验证二级缓存
MyBatis 配置文件(可省略)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 此项配置可以省略,因为默认就是 true -->
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<typeAlias type="world.xuewei.mybatis.entity.Account" alias="Account"/>
</typeAliases>
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://**.**.**/learn?useSSL=false&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/AccountMapper.xml"/>
</mappers>
</configuration>
Mapper 映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 配置 namespace -->
<mapper namespace="world.xuewei.mybatis.dao.AccountDao">
<!-- 需要明确指定此标签,表示开启二级缓存 -->
<cache/>
<!-- useCache="true" 可以省略 -->
<select id="getAll" resultType="Account" useCache="true">
select * from account
</select>
</mapper>
在 MyBatis 的映射 XML 中配置 cache 或者 cache-ref 都可以。
-
<cache>
标签用于声明这个 namespace 使用二级缓存,并且可以自定义配置。type
:cache 使用的类型,默认是PerpetualCache
,这在一级缓存中提到过。eviction
:定义回收的策略,常见的有 FIFO,LRU。flushInterval
:配置一定时间自动刷新缓存,单位是毫秒。size
:最多缓存对象的个数,默认 1024 个。readOnly
:是否只读,若配置可读写,则需要对应的实体类能够序列化。blocking
:若缓存中找不到对应的 key,是否会一直 blocking,直到有对应的数据进入缓存。
-
<cache-ref>
代表引用别的命名空间的 Cache 配置,两个命名空间的操作使用的是同一个 Cache。例如:
<cache-ref namespace="mapper.StudentMapper"/>
测试实体(Serializable)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
private Integer id;
private String name;
private String password;
}
数据库实体必须要实现 Serializable 接口,否则会抛出异常:
Error committing transaction. Cause: org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: world.xuewei.mybatis.entity.Account
测试类
public class Cache2Test {
private SqlSession sqlSession1;
private SqlSession sqlSession2;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession1 = sessionFactory.openSession();
sqlSession2 = sessionFactory.openSession();
}
/**
* 验证二级缓存,多个 SqlSession 有效
*/
@Test
public void testGetAll() {
AccountDao accountDao1 = sqlSession1.getMapper(AccountDao.class);
AccountDao accountDao2 = sqlSession2.getMapper(AccountDao.class);
accountDao1.getAll().forEach(System.out::println);
// 处理这里很关键,SqlSession 提交或关闭的时候才会将数据保存到二级缓存
sqlSession1.commit();
System.out.println("======================================");
accountDao2.getAll().forEach(System.out::println);
sqlSession2.commit();
}
}
通过观察日志的打印情况,可以看出,在第二次执行相同的查询方法时,就已经是从缓存中直接拿到的数据了,并且打印出了缓存的命中率 0.5。
清理二级缓存
执行 update、insert 或 delete 操作
@Test
public void testGetAll() {
AccountDao accountDao1 = sqlSession1.getMapper(AccountDao.class);
AccountDao accountDao2 = sqlSession2.getMapper(AccountDao.class);
accountDao1.getAll().forEach(System.out::println);
accountDao1.delete(1);
sqlSession1.commit();
System.out.println("======================================");
accountDao2.getAll().forEach(System.out::println);
sqlSession2.commit();
}
配置 flushCache=“true”
<select id="getAll" resultType="Account" flushCache="true">
select * from account
</select>
关闭二级缓存
<settings>
<!-- 默认是 true -->
<setting name="cacheEnabled" value="false"/>
</settings>
实现原理
CachingExecutor 创建时机
开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示:
这是一个典型的装饰器模式,通过 CachingExecutor 为 Executor 增强缓存的功能。在源码中如何提现的呢?首先根据我们原有的知识,Configuration 类是 MyBatis 的第一大核心类,这个类有一个职能就是可以创建其他的核心对象,包括 Executor:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 创建一个原始的 Executor,类型可能为 BatchExecutor、ReuseExecutor、SimpleExecutor,默认为 SimpleExecutor。
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// cacheEnabled 默认就是 true
if (cacheEnabled) {
// 通过 CachingExecutor 来装饰原始的 Executor
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
二级缓存实现流程
SqlSession 将 SQL 语句的处理和执行交给 Executor 去处理,如果是查询语句,那么肯定会交给 Executor#query 方法处理,而这里的 Executor 就是刚刚得到的使用 CachingExecutor 装饰后的 Executor。所以我们可以将关注点放在 CachingExecutor#query 方法,以下是 CachingExecutor 的源码:
public class CachingExecutor implements Executor {
// 非常典型的装饰器模式
private final Executor delegate;
// 存储缓存的工具,稍后我们会仔细分析
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 插入、更新、删除操作会清空当前 MappedStatement 的缓存
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建缓存的 Key,其实就是调用 BaseExecutor 的方法,和一级缓存的 Key 一样。
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
// 这种查询方式也会清空 MappedStatement 缓存
flushCacheIfRequired(ms);
return delegate.queryCursor(ms, parameter, rowBounds);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// 判断 Mapper XML 中是否配置了 <cache/> 标签
if (cache != null) {
// 判断是否需要刷新缓存
flushCacheIfRequired(ms);
// 此查询 SQL 是否配置了 useCache="true" 属性
if (ms.isUseCache() && resultHandler == null) {
// 用来处理存储过程的,暂时不用考虑
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从二级缓存中拿,会把获取值的职责一路传递,最终到 PerpetualCache。如果没有查到,会把 key 加入 Miss 集合,这个主要是为了统计命中率。
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 没拿到就查询数据库
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 然后存放在二级缓存,等待提交(注意此时还没有保存到二级缓存,还需要一个提交的操作)
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有配置 <cache/> 则直接调用原始 Executor 查询数据库
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
private void flushCacheIfRequired(MappedStatement ms) {
// 判断 Mapper XML 中是否配置了 <cache/> 标签
Cache cache = ms.getCache();
// 如果配置了 <cache/> 并且此查询语句配置了 flushCache="true" 就会清空缓存
if (cache != null && ms.isFlushCacheRequired()) {
// 清空缓存
tcm.clear(cache);
}
}
// 省略其他方法...
}
在上面的 query 方法中的 Cache cache = ms.getCache();
是获取当前 MappedStatement 配置的缓存对象。
本质上是装饰器模式的使用(无限套娃),具体的装饰链是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
以下是具体这些 Cache 实现类的介绍,他们的组合为 Cache 赋予了不同的能力。
SynchronizedCache
:同步 Cache,实现比较简单,直接使用 synchronized 修饰方法。LoggingCache
:日志功能,装饰类,用于记录缓存的命中率,如果开启了 DEBUG 模式,则会输出命中率日志。SerializedCache
:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的 Copy,用于保存线程安全。LruCache
:采用了 Lru 算法的 Cache 实现,移除最近最少使用的 Key/Value。PerpetualCache
:作为为最基础的缓存类,底层实现比较简单,直接使用了 HashMap。
在上面的 query 方法中,有这样的逻辑:
if (ms.isUseCache() && resultHandler == null) {
// ...
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(...);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
表达的含义是先调用 tcm 的 getObject 查询缓存,如果能查询到数据直接返回,否则调用 delegate 的查询,并将结果保存在 tcm。那么这个 tcm 可以说就是用来存储数据的。找到 tcm 的定义如下:
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
查询源码后可以看到:TransactionalCacheManager 维护了一个 transactionalCaches 这个 HashMap,键就是每个 MappedStatement 的 Cache,值就是 TransactionalCache。这个 TransactionalCache 也是 PerpetualCache 的装饰器,提供了事务性缓存的功能。事务性缓存确保缓存中的数据与数据库中的数据保持一致,只有在事务成功提交后,缓存中的数据才会被更新或者删除。
关于 TransactionalCache 的介绍可以查询这篇文章:《探秘MyBatis缓存原理:Cache接口与实现类源码分析》
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// 注意这里!当第一次添加数据的时候,会用 TransactionalCache 再次装饰一下 MappedStatement 的 Cache 当做 Map 的 value
// 这也就是 TransactionalCache 是怎么和 MappedStatement 的 Cache 产生联系的原因
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
二级缓存创建过程
刚刚我们获取到了一个套娃的 PerpetualCache,那么这个 PerpetualCache 是什么时候创建的呢?这个就要联系到我们之前学到的知识《MyBatis初探:揭示初始化阶段的核心流程与内部机制》。
在构建 MappedStatement 的时候是由 XMLMapperBuilder 负责处理,在其核心的配置解析的方法(XMLMapperBuilder#configurationElement
)中就由一步是去解析<cache/>
和<cache-ref>
标签的。
稍微追踪一下源码即可发现,缓存的创建是由 MapperBuilderAssistant 类完成,这是一个典型的构建者模式:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
// 默认底层是 PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
// 默认换出策略是 LruCache
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
// 刷新时间
.clearInterval(flushInterval)
// 存储数据大小
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
这些属性其实就是对应的<cache>
标签的属性。
所以说,缓存存储对象 cache 是在 MyBatis 初始化 MappedStatement 的时候就创建好了,创建好后存储在 MappedStatement 对象,由 XMLMapperBuilder 负责解析,MapperBuilderAssistant 负责创建。
接下来我们仔细看一下 CacheBuilder 的 build 方法是如何构建出这个 Cache 对象的:
public Cache build() {
// 设置默认的缓存实现,如果 <cache> 标签没有指定 type,那么就用 PerpetualCache + LruCache
setDefaultImplementations();
// 通过反射来创建缓存对象
Cache cache = newBaseCacheInstance(implementation, id);
// 读取 <cache> 下的 <property> 增加额外的参数(内置缓存不用,通常用于搭配自定义缓存,例如:Redis、Ehcache 等)
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
// 添加装饰器,可以通过 <cache> 标签的 eviction 属性指定
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 根据 <cache> 标签配置的属性,设置装饰器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
private void setDefaultImplementations() {
if (implementation == null) {
// 默认就是 PerpetualCache + LruCache
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
// 设置缓存大小,其实就是赋值给 PerpetualCache 的 size 属性,默认 1024
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// 支持定时清理的缓存
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// 自动序列化的缓存
cache = new SerializedCache(cache);
}
// 添加日志功能的缓存
cache = new LoggingCache(cache);
// 同步的缓存
cache = new SynchronizedCache(cache);
if (blocking) {
// 阻塞式的缓存
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
MyBatis 缓存的查询顺序
先说结论:查询的时候先查询二级缓存,查不到再去查询一级缓存,再查不到则会查询数据库。
再默认的情况下(开启二级缓存),我们得到的 Executor 是 CachingExecutor,而这个类就是二级缓存的负责类。当用户执行查询操作的时候,会先由 CachingExecutor 的 query 处理,处理过程就是先查缓存,然后没查到的话调用委托的 Executor delegate
来处理查询,而这个 delegate 就是 SimpleExecutor,一级缓存的负责类就是这个 SimpleExecutor 的父类,即 BaseExecutor,所以结合一级缓存和二级缓存的实现原理可以得出上面这个结论。
- 点赞
- 收藏
- 关注作者
评论(0)