在上文《探秘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,当然也可以通过 标签来配置多个 XML 复用同一个 Cache,通常会在多表查询的时候会指定同一个 Cache,这是为了避免脏数据。
因为在对数据库进行更新的时候会清理当前 MappedStatement 引用的 Cache,但是如果别的 XML 文件中的 MappedStatement 的查询也使用到了这个表,那么别的 XML 的 Cache 中就会出现脏数据了。
但是如果都指定同一个 Cache,那么在更新的时候会频繁的清理当前的 Cache,而且是全部清理,这样显然不是很好,所以我们有必要更改 MyBatis 底层的缓存清除策略。
- 作用范围:跨会话缓存,二级缓存是跨多个 SqlSession 的缓存机制,可以在不同的会话之间共享缓存数据。
- 生命周期:应用级别缓存,二级缓存的生命周期与应用程序的生命周期相同,可以在整个应用程序中共享数据。
- 默认开启:二级缓存可以通过 MyBatis 配置文件中的控制,默认就是 true,可以理解为默认开启,但是还是需要对应的 Mapper XML 配置标签后才能生效。
- 缓存策略:二级缓存的配置通常配置在映射文件(Mapper XML 文件)的 标签内部。配置 元素,可用于配置二级缓存的属性,如缓存类型、缓存的大小、刷新间隔等。在映射文件添加 标签,该映射文件下的所有方法都支持二级缓存。该标签有 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 都可以。
- 标签用于声明这个 namespace 使用二级缓存,并且可以自定义配置。
type
:cache 使用的类型,默认是PerpetualCache
,这在一级缓存中提到过。eviction
:定义回收的策略,常见的有 FIFO,LRU。flushInterval
:配置一定时间自动刷新缓存,单位是毫秒。size
:最多缓存对象的个数,默认 1024 个。readOnly
:是否只读,若配置可读写,则需要对应的实体类能够序列化。blocking
:若缓存中找不到对应的 key,是否会一直 blocking,直到有对应的数据进入缓存。
- 代表引用别的命名空间的 Cache 配置,两个命名空间的操作使用的是同一个 Cache。
例如:
测试实体(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
)中就由一步是去解析和标签的。
稍微追踪一下源码即可发现,缓存的创建是由 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 是在 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,所以结合一级缓存和二级缓存的实现原理可以得出上面这个结论。