您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
由 Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
由 Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存
wy****
2025-06-20
IP归属:北京
19浏览
上一篇 [从根上理解 Mybatis 一级缓存](http://sd.jd.com/article/40791?shareId=53471&isHideShareButton=1) 我们介绍了一级缓存。本篇则是关于二级缓存,同样地,仍然需要关注它的实现原理,以及要考虑“为什么在已经有了一级缓存的情况下还需要设计二级缓存”的问题,在以后实际业务中的缓存设计提供借鉴和参考。 ### 1. 验证二级缓存 在上一篇帖子中的 User 和 Department 实体类依然要用,这里就不再赘述了,要启用二级缓存,需要在 Mapper.xml 文件中指定 **cache** 标签,如下: ```xml UserMapper.xml <select id="findAll" resultType="User"> select * from user </select> <cache /> ``` ```xml Department.xml <select id="findAll" resultType="entity.Department"> select * from department; </select> <cache readOnly="true"/> ``` 在 Department.xml 中的 **cache** 标签指定了 **readOnly** 属性,因为该配置相对比较重要,所以我们在这里把它讲解一下: **readOnly 默认为 false**,这种情况下通过二级缓存查询出来的数据会进行一次 **序列化深拷贝**。在这里大家需要回想一下介绍一级缓存时举的例子:一级缓存查询出来返回的是 **该对象的引用**,若我们对它修改,**再查询** 时触发一级缓存获得的便是 **被修改过的数据**。但是,二级缓存的序列化机制则不同,它获取到的是 **缓存深拷贝的对象**,这样对二级缓存进行修改操作不影响后续查询结果。 如果将该属性配置为 true 的话,那么它就会变得和一级缓存一样,返回的是对象的引用,这样做的好处是 **避免了深拷贝的开销**。 > 为什么会有这种机制呢?</p> > 因为二级缓存是 **Mapper级别** 的,不能保证其他 SqlSession 不对二级缓存进行修改,所以这也是一种保护机制。 我们验证一下这个例子,Department 和 User 的查询都执行了两遍(注意 **事务提交之后** 才能使二级缓存生效): ```java public static void main(String[] args) { InputStream xml = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 开启二级缓存需要在同一个SqlSessionFactory下,二级缓存存在于 SqlSessionFactory 生命周期,如此才能命中二级缓存 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml); SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class); System.out.println("----------department第一次查询 ↓------------"); List<Department> departments1 = departmentMapper1.findAll(); System.out.println("----------user第一次查询 ↓------------"); List<User> users1 = userMapper1.findAll(); // 提交事务,使二级缓存生效 sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class); System.out.println("----------department第二次查询 ↓------------"); List<Department> departments2 = departmentMapper2.findAll(); System.out.println("----------user第二次查询 ↓------------"); List<User> users2 = userMapper2.findAll(); sqlSession1.close(); sqlSession2.close(); } ``` Department 和 User 的同一条查询语句都执行了两遍,因为 Department 指定了 readOnly 为true,那么 **两次查询返回的对象均为同一个引用**,而 User 则反之,Debug 试一下:  #### cache 的其他属性 | 属性 | 描述 | 备注 | | ------------- | -------- | --------------------------------------------- | | eviction | 缓存回收策略 | 默认 LRU | | type | 二级缓存的实现类 | 默认实现 PerpetualCache | | size | 缓存引用数量 | 默认1024 | | flushInterval | 定时清除时间间隔 | 默认无 | | blocking | 阻塞获取缓存数据 | 若缓存中找不到对应的 key ,是否会一直阻塞,直到有对应的数据进入缓存。默认 false | 接下来我们测试验证下二级缓存的生效: ```java SqlSession sqlSession1 = sqlSessionFactory.openSession(); DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class); System.out.println("----------department第一次查询 ↓------------"); List<Department> departments1 = departmentMapper1.findAll(); // 使二级缓存生效 sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class); System.out.println("----------department第二次查询 ↓------------"); List<Department> departments2 = departmentMapper2.findAll(); ``` * 第一次 Query,会去数据库中查  * 第二次 Query,直接从二级缓存中取  ### 2. 二级缓存的原理 #### 二级缓存对象 Cache 在加载 Mapper 文件(`org.apache.ibatis.builder.xml.XMLConfigBuilder#mappersElement` 方法)时,定义了加载 cache 标签的步骤(`org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement` 方法),代码如下: ```java public class XMLMapperBuilder extends BaseBuilder { // ... private void configurationElement(XNode context) { try { // 若想要在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存 cacheRefElement(context.evalNode("cache-ref")); // 配置二级缓存 cacheElement(context.evalNode("cache")); // ... } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } } ``` 具体解析逻辑如下: ```java public class XMLMapperBuilder extends BaseBuilder { // ... private void cacheElement(XNode context) { if (context != null) { // 二级缓存实现类,默认 PerpetualCache,我们在一级缓存也提到过 String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); // 缓存清除策略,默认 LRU String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); // 定时清除间隔 Long flushInterval = context.getLongAttribute("flushInterval"); // 缓存引用数量 Integer size = context.getIntAttribute("size"); // readOnly上文我们提到过,默认 false boolean readWrite = !context.getBooleanAttribute("readOnly", false); // blocking 默认 false boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); // 创建缓存对象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } } ``` 我们继续看创建二级缓存对象的逻辑 `org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache`,可以发现,创建 Cache 对象使用了 **建造者模式**:  建造者 `CacheBuilder` 并没有被组合在任意一种缓存的实现类中,而是根据如下代码中 `implementation(valueOrDefault(typeClass, PerpetualCache.class))` 逻辑指定了要创建的缓存类型,并在 `build` 方法中使用反射创建对应实现类: ```java public class MapperBuilderAssistant extends BaseBuilder { // ... 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).implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size) .readWrite(readWrite).blocking(blocking).properties(props).build(); // 添加到全局配置中 configuration.addCache(cache); currentCache = cache; return cache; } } ``` 其中 `addDecorator(valueOrDefault(evictionClass, LruCache.class))` 逻辑添加了 **装饰器**,使用了 **装饰器模式**,将 `LruCache` 类型的装饰器添加到 `decorators` 中: ```java public class CacheBuilder { private final List<Class<? extends Cache>> decorators; public CacheBuilder addDecorator(Class<? extends Cache> decorator) { // 将 LruCache 装饰器添加到 decorators if (decorator != null) { this.decorators.add(decorator); } return this; } // ... } ``` 在 `CacheBuilder#build` 方法中,如下为封装装饰器的逻辑: ```java public class CacheBuilder { // ... public Cache build() { setDefaultImplementations(); // 反射创建 PerpetualCache Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); // 封装装饰器的逻辑 if (PerpetualCache.class.equals(cache.getClass())) { for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 初始化基础必要的装饰器 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } // 定时清空二级缓存 if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } // readOnly属性相关的读写缓存 if (readWrite) { cache = new SerializedCache(cache); } // 日志缓存和同步缓存(借助 ReentrantLock 实现) 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); } } } ``` 所有装饰器都在 `org.apache.ibatis.cache.decorators` 包下,唯独 `PerpetualCache` 在`org.apache.ibatis.cache.impl` 包下:  `PerpetualCache` 中不包含 `delegate` 属性表示装饰器,说明它将作为最基础的实现类被其他装饰器装饰,而其他装饰器中均含有 `delegate` 属性来装饰其他实现。 默认创建的二级缓存类型如下:  类关系图如下:  #### query 方法对二级缓存的应用 `org.apache.ibatis.executor.CachingExecutor#query` 方法使用了二级缓存,如下代码所示: ```java public class CachingExecutor implements Executor { // 事务缓存管理器 private final TransactionalCacheManager tcm = new TransactionalCacheManager(); @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 先获取二级缓存,该对象便是上文中创建的被装饰器装饰的 PerpetualCache Cache cache = ms.getCache(); if (cache != null) { // 判断是否需要清除缓存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // 从二级缓存中取 @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; } } // 没有二级缓存的话,执行的是我们在一级缓存中介绍的方法,要么取一级缓存,否则去数据库查 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } // ... } ``` 上述逻辑比较清晰,我们在上文中提到过,只有 **事务提交的时候才会将二级缓存保存**,但是其中有 `tcm.putObject(cache, key, list);` 逻辑,似乎在这里保存了二级缓存,而此时事务还未提交,这便需要我们一探究竟。它会执行到 `TransactionalCacheManager#putObject` 方法: ```java public class TransactionalCacheManager { private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } private TransactionalCache getTransactionalCache(Cache cache) { return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new); } } ``` `TransactionalCacheManager` 事务缓存管理器会创建并管理 `TransactionalCache` 对象,`TransactionalCache` 同样是 `Cache` 装饰器,它将装饰在 `SynchronizedCache` 上: ```java public class TransactionalCache implements Cache { // 被装饰对象,默认是 SynchronizedCache private final Cache delegate; // 该元素将保存在事务 commit 时被保存的键值对缓存 private final Map<Object, Object> entriesToAddOnCommit; @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } // ... } ``` `putObject` 执行时便是向 `entriesToAddOnCommit` 添加元素,记录二级缓存键值对,并没有真正添加到二级缓存 `PerpetualCache` 对象中。此外,`entriesToAddOnCommit` 的命名,也暗示了在事务提交时缓存才会被保存。那么接下来,便需要看一下事务提交逻辑。 在上文测试二级缓存的代码中,有 `sqlSession1.commit();` 逻辑。在事务提交时,它会走到 `CachingExecutor#commit` 方法,其中会调用到 `TransactionalCacheManager#commit` 方法,如下: ```java public class CachingExecutor implements Executor { // ... private final TransactionalCacheManager tcm = new TransactionalCacheManager(); @Override public void commit(boolean required) throws SQLException { // ... tcm.commit(); } } ``` 在该方法中,会遍历所有的事务缓存 `TransactionalCache`,并逐一调用它们的 `commit` 方法, ```java public class TransactionalCacheManager { private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } // ... ``` `commit` 方法会调用 `delegate.commit` 方法,而 `delegate` 为被装饰对象,最后便会将二级缓存记录: ```java public class TransactionalCache implements Cache { private final Map<Object, Object> entriesToAddOnCommit; public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } private void flushPendingEntries() { // 事务提交,将 entriesToAddOnCommit 中所有待添加的二级缓存添加 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); } } } private void reset() { clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } // ... } ``` #### 缓存失效 事务回滚是不是会使本次事务中相关的二级缓存失效呢? ```java public class TransactionalCache implements Cache { public void rollback() { unlockMissedEntries(); reset(); } private void reset() { clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { try { delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifying a rollback to the cache adapter. " + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } } // ... } ``` 的确如此,它会将未被缓存的元素清除 `reset()`,也会把在本次事务中操作过的数据在二级缓存中移除 `unlockMissedEntries()`。 那数据发生新增、修改或删除呢?同样会清除缓存 ```java public class CachingExecutor implements Executor { @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(); // 默认 flushCacheRequired 为 true if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } ``` 它将调用 `TransactionalCache#clear` 方法,将待生效的 `entriesToAddOnCommit` 二级缓存清除,并标记 `clearOnCommit` 为 true,在事务提交时,二级缓存会执行清除缓存的 `clear` 方法: ```java @Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); } public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } ``` #### 缓存生效范围 到这里,我们已经基本弄清楚二级缓存生效的原理了,那么接下来我们需要解释“为什么二级缓存是 Mapper 级别的?”其实也非常简单,看如下代码: ```java public class CachingExecutor implements Executor { @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 先获取二级缓存,该对象便是上文中创建的被装饰器装饰的 PerpetualCache Cache cache = ms.getCache(); // ... } // ... } ``` 在执行查询时,二级缓存 `Cache` 是在 `MappedStatement` 中获取的,Mapper 中每个 SQL 声明都对应唯一的 `MappedStatement`,当同一条 SQL 被执行时,它们都会去取同样的缓存,所以可以说它是 Mapper 级别的,说成 `MappedStatement` 级别更准确,二级缓存支持多个 `SqlSession` 共享。 #### 为什么要在事务提交后才生效? 在这里我们讨论一个问题:**为什么二级要在事务提交后才能生效呢**? 因为二级缓存可以在不同的 `SqlSession` 间生效,画个图你就明白了:  如果 **SqlSession1先修改了数据**,**再查询数据**,如果二级缓存在事务未提交时就生效,那么 SqlSession2 调用同样的查询时便会从 **二级缓存中获取数据**,但是此时 **SqlSession1回滚了事务**,那么此时就会导致 SqlSession2 从二级缓存获取的数据 **变成脏数据**,这就是为什么二级缓存要在事务提交后才能生效的原因。 ### 3. 为什么要扩展二级缓存? MyBatis 中设计一级缓存和二级缓存的目的是为了提高数据库访问的效率,但它们的作用范围和使用场景有所不同,各自有其特定的用途和优势。 **一级缓存** 默认开启,是基于 `SqlSession` 的,也就是说,它的作用范围仅限于一次数据库会话,所以当会话关闭后,缓存就会被清除。这意味着不同会话之间无法共享缓存数据。而 **二级缓存** 是基于 Mapper 级别的,需要显式配置开启,可以在多个 `SqlSession` 之间共享。当然也由于二级缓存的作用范围更广,因此需要更复杂的缓存失效策略和数据一致性管理,以避免数据不一致的问题。二级缓存的引入是为了在更大范围内(多个会话之间)提高数据访问的效率,特别是在读多写少的应用场景。 ### 4. 总结 * 二级缓存本质上是 `HashMap`,在 `PerpetualCache` 实现类中 * 二级缓存是 Mapper 级别的,可以在不同 `SqlSession` 间共享 * 特殊的 readOnly 标签,默认为 false,表示二级缓存中是被深拷贝的对象 * 二级缓存需要在事务提交后才能生效 * 执行 Insert、Delete、Update 语句会使 **当前 Mapper 下的二级缓存失效**
上一篇:三十分钟入门基础Go——Java小子版
下一篇:一分钟入门mcp开发
wy****
文章数
40
阅读量
9971
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
缓存之美:从根上理解 ConcurrentHashMap
本文将详细介绍 ConcurrentHashMap 构造方法、添加值方法和扩容操作等源码实现。ConcurrentHashMap 是线程安全的哈希表,此哈希表的设计主要目的是在最小化更新操作对哈希表的占用,以保持并发可读性,次要目的是保持空间消耗与 HashMap 相同或更好,并支持利用多线程在空表上高效地插入初始值。在 Java 8 及之后的版本,使用 CAS 操作、 synchronized
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
40
阅读量
9971
作者其他文章
01
高性能MySQL实战(一):表结构
01
缓存之美:从根上理解 ConcurrentHashMap
01
分布式服务高可用实现:复制
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号