@万字长文 | MyBatis 缓存到底是怎么回事?
关注“Java后端技术全栈”
回复“面试”获取全套面试资料
在Web应用中,缓存组件是一个核心组成部分,起到不可或缺的作用.常见的解决方案包括Redis、memcached等缓存中间件,它们能够有效地阻止大量直接发送至数据库的请求,从而缓解数据库的压力.针对关键业务逻辑,MyBatis系统也会特意提供相应的功能模块.通过在应用框架层面增添缓存功能,既能减轻数据库的压力,又能提高查询效率,双重优势显而易见.从技术架构角度来看,MyBatis系统的缓存机制主要由一级缓存与二级缓冲机制共同组成.其中一级和二级缓存在设计上都遵循Cache接口的标准实现模式.基于此基础,本章将深入介绍几种典型的Cache接口实现方案,随后重点阐述一级与二级缓存的具体实现细节
本文主要内容:

Mybatis缓存体系结构
Mybatis相关的缓存功能都集中于cache目录下,在之前的文章中已经进行了介绍。今天才对这个主题进行了深入探讨。其中包含有顶层接口Cache以及仅有的一个默认实现方式PerpetualCache。
下面是Cached的类图:

既然PerpetualCache是默认实现类,那么我们就从他下手。
PerpetualCache
该对象会被创建出来,并因此被称为基础缓存。然而缓存还具备多种额外功能:例如回收策略、日志记录以及定时刷新等功能。如果有需求的话就可以将这些功能附加到基础缓存上;如果不希望附加,则不进行添加。这是否让人联想到一种常见的设计模式——装饰器模式?PerpetualCache相当于这种模式下的ConcreteComponent。
装饰器模式是一种在原有对象功能不发生变化的前提下,在其基础上附加特定的功能,并提供了一种比传统继承更具弹性的替代方案;这种模式旨在通过扩展现有对象的功能来实现模块化设计的目的。
除了缓存功能之外,在我的batis框架中还定义了许多装饰器;这些装饰器不仅能够实现基础功能如缓存管理,并且还可以通过它们结合其他组件或策略来扩展应用的能力。
这些缓存是怎么分类的呢?
所有的缓存可以大体归为三类:基本类缓存、淘汰算法缓存、装饰器缓存。
下面把每个缓存进行详细说明和对比:

缓存实现类源码
PerpetualCache源码
PerpetualCache 是一个基于基本功能的缓存类,在实现过程中采用了 HashMap 来完成缓存功能。
public class PerpetualCache implements Cache {
private final String id;
//使用Map作为缓存
private Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
// 存储键值对到 HashMap
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
// 查找缓存项
@Override
public Object getObject(Object key) {
return cache.get(key);
}
// 移除缓存项
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
//清空缓存
@Override
public void clear() {
cache.clear();
}
//部分代码省略
}
代码解释
上面完整地列出了PerpetualCache的所有代码,并也被称作基本缓存系统。相对而言这一部分实现较为简单。随后我们将通过使用装饰器类来增强这一核心功能的功能表现力使其变得更为丰富。
LruCache
LruCache即是一种基于LRU(最近最少使用)算法设计的缓存实现类。它能够有效提高数据访问效率。
除了之外,在MyBatis中还采用了FIFO策略的FifoCache缓存。虽然没有提供LFU(即Least Frequently Used)缓存这一算法。然而LFU是一种常见的缓存机制。如若感兴趣,则可自行扩展。
接下来,我们来看一下 LruCache 的实现。
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
/* * 初始化 keyMap,注意,keyMap 的类型继承自 LinkedHashMap,
* 并覆盖了 removeEldestEntry 方法
*/
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// 覆盖 LinkedHashMap 的 removeEldestEntry 方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 获取将要被移除缓存项的键值
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
// 存储缓存项
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
// 刷新 key 在 keyMap 中的位置
keyMap.get(key);
// 从被装饰类中获取相应缓存项
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
// 从被装饰类中移除相应的缓存项
return delegate.removeObject(key);
}
//清空缓存
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
// 存储 key 到 keyMap 中
keyMap.put(key, key);
if (eldestKey != null) {
// 从被装饰类中移除相应的缓存项
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
// 省略部分代码
}
代码解释
通过查看上述代码可以看出,LruCache 的 keyMap 属性是实现 LRU 策略的核心要素之一,并直接继承自 LinkedHashMap 类型。该属性不仅实现了 removeEldestEntry 方法的功能(即删除最古老的条目),还能够维持键值对的插入顺序。当向系统中添加一个新的键值对时,在 LinkedHashMap 中会按照先来先得的原则排列这些键值对
在LinkedHashMap内部结构中,tail指针始终指向最新的数据项,而head指针则指示最早被放置的数据项,即最久未被访问的数据项.通常情况下,Default情况下,Default情况下, Linked HashMap主要用于存储数据并按照插入顺序排列.如果要采用LinkedHashMap来实现LRU缓存机制,必须在构造函数中指定accessOrder属性为true,这样系统就会根据访问频率来维护数据顺序.
例如,在上述代码中Object.defineProperty方法中有一条指令是keyMap.get(key),其目的是更新key对应的键值对在LinkedHashMap中的存储位置。LinkedHashMap会将该键值对放置在队列末尾(tail),队列末端代表最近被访问或加入的数据项。除了设置accessOrder为true外还必须实现removeEldestEntry功能。LinkedHashMap在新增键值对时会触发该操作从而判断是否需要移除过时的数据项。
在代码部分中,在被装饰类达到其容量上限之前。随后会从 keyMap 中删除该键并将其记录到 eldestKey字段。然后cycleKeyList 方法负责将 eldestKey传递给该被装饰类中的removeOldestEntry方法。从而清除掉相应的缓存项。
BlockingCache
BlockingCache 具有阻塞特性,并且该特性是由 Java 的重入锁实现的。在同一时间点上,在 BlockingCache 中仅允许一个线程访问指定 key 对应的缓存项;其余所有试图在此时访问该 key 对应缓存项的线程都将被阻塞。
下面我们来看一下 BlockingCache 的源码。
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
}
@Override
public void putObject(Object key, Object value) {
try {
// 存储缓存项
delegate.putObject(key, value);
} finally {
// 释放锁
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
// 请 // 请求锁
acquireLock(key);
Object value = delegate.getObject(key);
// 若缓存命中,则释放锁。需要注意的是,未命中则不释放锁
if (value != null) {
// 释放锁
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// 释放锁
releaseLock(key);
return null;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();
// 存储 <key, Lock> 键值对到 locks 中
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
// 尝试加锁
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("...");
}
} catch (InterruptedException e) {
throw new CacheException("...");
}
} else {
// 加锁
lock.lock();
}
}
private void releaseLock(Object key) {
// 获取与当前 key 对应的锁
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
// 释放锁
lock.unlock();
}
}
// 省略部分代码
}
代码解释
当查询缓存时
反观 BlockingCache 类的注释中提到该类相对简单明了
It sets a lock over a cache key when the element is not found in cache.
This way, other threads will wait until this element is filled instead of hitting the database.
代码解释
这段文字的核心内容是描述一种缓存机制的行为模式:当指定的 key 对应的缓存元素尚未存在时, BlockingCache 将基于锁机制进行加锁操作,从而阻止其他线程直接访问数据库系统.这种机制通过阻塞未找到对应元素的线程,以确保数据一致性的同时减少对数据库资源的竞争.
在上述代码实现中, removeObject 方法的逻辑设计存在明显问题,该方法仅负责释放锁机制,而完全忽略了被装饰对象自身需要执行的清除对应缓存项的操作.这种设计思路为何?建议大家深入思考,具体原因将在后续分析二级缓存相关机制时进行详细阐述.
CacheKey
为了提升查询速度而采用缓存机制,在MyBatis中引入了缓存技术以减少对数据库的压力
那么 key 是什么类型的变量?比如说是字符串类型或是其他数据类型?大家通常会考虑使用 SQL 语句来作为 key。但这与实际情况不符。
比如:
SELECT * FROM author where id > ?
代码解释
当d值大于1且id大于10时(d > 1 和 id > 10),查询结果可能会出现差异)。因此,在这种情况下我们不能仅以单一的SQL语句作为键值(key)。通过进一步分析可以发现,在线处理参数会直接影响查询结果的变化(影响查询结果的因素包括但不限于运行时参数),因此我们的key应综合考虑影响查询结果的各种因素(参数)。此外(此外),如果进行分页查询也会导致最终的结果发生变化(进而产生不同的查询结果),因此key还需要涵盖分页相关的参数。综上所述,在这种情况下我们不能仅使用单一的SQL语句作为键值。相反地,在MySQL数据库中我们应当采用一种复合对象来覆盖所有可能影响查询结果的关键因素(parameter)。在MyBatis框架中这种复合对象就是所谓的CacheKey。
下面来看一下它的定义。
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
// 乘子,默认为37
private final int multiplier;
// CacheKey 的 hashCode,综合了各种影响因子
private int hashcode;
// 校验和
private long checksum;
// 影响因子个数
private int count;
// 影响因子集合
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
// 省略其他方法
}
代码解释
如上,除了 multiplier 是恒定不变的 ,其他变量将在更新操作中被修改。
下面看一下更新操作的代码。
/** 每当执行更新操作时,表示有新的影响因子参与计算 */
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 自增 count
count++;
// 计算校验和
checksum += baseHashCode;
// 更新 baseHashCode
baseHashCode *= count;
// 计算 hashCode
hashcode = multiplier * hashcode + baseHashCode;
// 保存影响因子
updateList.add(object);
}
代码解释
每当新影响因子引入计算时,
hashcode 和 checksum 会变得更加复杂无规律可寻。
这样做的结果是降低了冲突的概率,
使得 CacheKey 在缓存中的合理分布避免了碰撞冲突。
CacheKey 最终必须作为键存入 HashMap 中,
因此它必须实现 equals 和 hashCode 方法。
下面我们来看一下这两个方法的实现。
public boolean equals(Object object) {
// 检测是否为同一个对象
if (this == object) {
return true;
}
// 检测 object 是否为 CacheKey
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
// 检测 hashCode 是否相等
if (hashcode != cacheKey.hashcode) {
return false;
}
// 检测校验和是否相同
if (checksum != cacheKey.checksum) {
return false;
}
// 检测 coutn 是否相同
if (count != cacheKey.count) {
return false;
}
// 如果上面的检测都通过了,下面分别对每个影响因子进行比较
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
public int hashCode() {
// 返回 hashcode 变量
return hashcode;
}
代码解释
equals方法在检测逻辑上较为严格,在CacheKey中对多个成员变量进行了检测,并从而实现了两者的相等性要求;而hashCode方法相对简单,在实现时只需返回对应的hashcode值即可。
对于CacheKey而言,在此阶段我们已初步了解。CacheKey在一级和二级缓存中会被采用,在未来我们仍会关注它的应用情况。
好吧,终于把源码缓存实现类的源码拔完了。
下面我们在来说说一级缓存和二级缓存。
一级缓存
主要内容:

一级缓存也称为本地缓存(LocalCache),而Mybatis则采用会话级别(SqlSession)的方式进行一级缓存实现。由于其设计特性,默认启用一级 cache 通常被认为是一种合理的选择。对于我们的开发项目来说,默认情况下无需额外配置即可实现这一功能。然而,在特殊需求下如果不想要关闭这一功能则可以设置 localCacheScope=statement 来完成相应的操作。
如何关闭一级缓存呢?
在BaseExecutor的中,请看下面代码:

为什么说是SqlSession层面缓存?
就是一级缓存的生命周期和一个SqlSession对象的生命周期一样。
下面这段中,就会使用到一级缓存。
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectUserById", 1);
User user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectUserById", 1);
代码解释
结果输出:

用两张图来总结:
第一次:查数据库,放入到缓存中。

第二次:直接从缓存中获取。

下面这段代码中就使用不到缓存
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println("第一次查询");
System.out.println(userMapper.selectById(1));
User user = new User();
user.setUserName("tian111");
user.setId(1);
userMapper1.updateAuthorIfNecessary(user);
System.out.println("第二次查询");
System.out.println(userMapper.selectById(1));
代码解释
输出结果:

用三张图来总结:
第一次查询:sqlSession1查询数据库,放入到缓存中。

修改:sqlSession2执行修改操作,请注意这里被保存为sqlSession自身本地缓存。

第二次查询:sqlSession1第二次查询。

记住是一级缓存只能是同一个SqlSession对象就行了。
一级缓存维护在哪里的?
由于一级缓存机制的运行周期与SqlSession一致,在这种情况下我们是否推测该缓存机制确实被维护在SqlSession内部?
该类(即DefaultSqlSession)作为Sq
SQL
session的基础类,在其架构中仅包含两个核心属性用于存储缓存数据。
private final Configuration configuration;
private final Executor executor;
代码解释
configuration是全局的,肯定不会放缓存。
不得不将希望寄托在Executor上。尽管它只是一个接口协议,并且我们也可以深入探究其具体实现:

另外这里有个BaseExecutor。有各类也得瞄瞄。一看居然有东西。
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//熟悉的家伙,基本缓存
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
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;
}
}
代码解释
再看看BaseExecutor类图:

所以这就证明了,这个缓存是维护在SqlSession里。
一级缓存什么时候被清空?
当执行update操作、“insert”操作、“delete”操作以及flush the cache="true"操作时(包括update操作、“insert”操作、“delete”操作等),相应的一级缓存都会被清除。
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
代码解释
在进行update操作时, first-level cache项会被清除. 这个update被delete和insert操作所调用. 通过跟踪SqlSession中的insert, update和delete方法, 可以实现这一功能.

当使用LocalCacheScope中的STATEMENT时(即涉及一级缓存的操作),会触发一级缓存的清除操作。在BaseExecutor中的query方法将执行相应的操作:

事务提交回滚时,一级缓存会被清空。

flushCache="true"时,一级缓存会被清空。

一级缓存key是什么?

下面就是一级缓存key的创建过程
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
代码解释
id:com.tian.mybatis.mapper.UserMapper.selectById
key的生成策略:id、offset、limit、sql、param value及environment id等参数均保持一致(即所有参数取值完全相同),就会导致相同的key值。
示例:

一级缓存总结
一级缓存机制的生命周期与SqlSession对象的生命周期保持一致;因此,在SqlSession对象中定义的属性executor负责维护缓存数据
一级缓存默认开启。可以通过修改配置项把一级缓存关掉。
清空一级缓存的方式有:
update、insert、delete
flushCache="true"
commit、rollback
LocalCacheScope.STATEMENT
二级缓存
主要内容:

基于一级SqlSession的基础上构建了一个层级更为分明的层次化架构设计模式
依照MyBatis规范配置SqlSession时, 一级缓存不会出现 Concurrent issues. 相反地, 二级缓存却有所不同, 它能够在不同的命名空间中实现资源共享. 这种配置下可能会出现 Concurrent issues, 因此需要采取相应的措施来应对. 除了解决上述 Concurrent issues外, 二级缓存还会遇到与事务相关的挑战.
二级缓存如何开启?
配置项
<configuration>
<settings>
<setting name="cacheEnabled" value="true|false" />
</settings>
</configuration>
代码解释
配置为true以启用二级缓存;但必须在Mapper.xml中进行配置。
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
或者 简单方式
<cache/>
代码解释
对配置项属性说明:
设置为flushInterval=60, 每隔60秒重置缓存;该时间间隔为60秒,并非由定时器事件驱动触发(即被动触发),而是通过不依赖定时器轮询的方式实现。
设定大小参数为512位时,则表明队列的最大容量设置为512个元素,在超出该数值时会自动移除前端元素。其中该数值代表的是缓存键的数量,默认设置为1024位存储空间。
const readOnly = "true"; // 表示任何获取操作都将返回同一个实例
if (readOnly === "false") { // 则每一次都会生成该目标对象的一个副本
// 这类似于将目标对象进行序列化复制并返回一份副本
}
会被Default Least Recently Used (LRU) 算法所采用。
会被First In First Out (FIFO) 法则所采用
在Configuration类的newExecutor方法中是否开启二级缓存
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
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);
}
//是否开启二级缓存
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
代码解释
二级缓存借助CachingExecutor实现其功能机制:当缓存中存在结果时会直接返回;若无对应结果则会调用Executor进行计算。具体流程如下:在一级缓存未关闭的情况下优先查找一级缓存中的数据;若无发现相关结果则会转而查询数据库中的数据。
下面使用一张图来表示:

下面是源码:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获得 BoundSql 对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey 对象
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 查询
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 调用 MappedStatement#getCache() 方法,获得 Cache 对象,
//即当前 MappedStatement 对象的二级缓存。
Cache cache = ms.getCache();
if (cache != null) { // <2>
// 如果需要清空缓存,则进行清空
flushCacheIfRequired(ms);
//当 MappedStatement#isUseCache() 方法,返回 true 时,才使用二级缓存。默认开启。
//可通过@Options(useCache = false) 或 <select useCache="false"> 方法,关闭。
if (ms.isUseCache() && resultHandler == null) { // <2.2>
// 暂时忽略,存储过程相关
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);
}
代码解释
二级缓存key是如何生成的?
基于BaseExecutor类中的createCacheKey方法生成的
二级缓存范围
二级缓存有一个非常重要的空间划分策略:
namespace="com.tian.mybatis.mappers.UserMapper"
namespace="com.tian.mybatis.mappers.RoleMapper"
代码解释
即基于namespace的划分,在同一namespace内存在同一Cache space;而不同的namespace之间则各自拥有不同的Cache space。
比如:

在这个namespace下的二级缓存是同一个。
二级缓存什么时候会被清空?
每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。
事务不提交,二级缓存不生效?
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println("第一次查询");
User user = sqlSession.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1);
System.out.println(user);
//sqlSession.commit();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
System.out.println("第二次查询");
User user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1);
System.out.println(user2);
代码解释
由于Secondary Cache采用了TransactionManager(tcm)来管理,并随后调用了TransacTable.getObject(), putObject(), 和 commit()方法。
TransactionalCache内部还包含着真正实现的Cache对象,并如:经过多重封装的PrepetualCache
在putObject的时候,只是添加到entriesToAddOnCommit里面。
//TransactionalCache类中
@Override
public void putObject(Object key, Object object) {
// 暂存 KV 到 entriesToAddOnCommit 中
entriesToAddOnCommit.put(key, object);
}
代码解释
仅在调用conmit的情况下才会触发flushPendingEntries的执行,并最终将数据写入缓存中。DefaultSqlSession在进行commit操作时会直接执行该特定的commit操作。
//TransactionalCache类中
public void commit() {
//如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
flushPendingEntries();
// 重置
reset();
}
private void flushPendingEntries() {
// 将 entriesToAddOnCommit 刷入 delegate 中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 将 entriesMissedInCache 刷入 delegate 中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
// 重置 clearOnCommit 为 false
clearOnCommit = false;
// 清空 entriesToAddOnCommit、entriesMissedInCache
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
代码解释
为什么增删该操作会清空二级缓存呢?
因为在CachingExecutor的update方法中
@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();
// 是否需要清空缓存
//通过 @Options(flushCache = Options.FlushCachePolicy.TRUE) 或 <select flushCache="true"> 方式,
//开启需要清空缓存。
if (cache != null && ms.isFlushCacheRequired()) {
//调用 TransactionalCache#clear() 方法,清空缓存。
//注意,此时清空的仅仅,当前事务中查询数据产生的缓存。
//而真正的清空,在事务的提交时。这是为什么呢?
//还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改。
tcm.clear(cache);
}
}
代码解释
如何实现多个namespace的缓存共享?
关于多个namespace的缓存共享的问题,可以使用来解决。
比如:
<cache-ref namespace="com.tian.mybatis.mapper.RoleMapper"
代码解释
cache-ref用于表示引用别名的命名空间中的Cache配置;该两个命名空间的操作采用了同一个Cache配置。当关联表数量较少时或根据业务需求将表进行分组时可用。
「注意」 :在这种情况下,多个mapper的操作都会引起缓存刷新,所以这里的缓存的意义已经不是很大了。
如果将第三方缓存作为二级缓存?
Mybatis在不使用自带的二级缓存的情况下,在不使用自带的二级缓存的前提下(或者也可以认为是说),我们还可以通过使用Cache接口来自定义(或者说是自定义化)二级缓存。
添加依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
代码解释
redis基础配置项
host=127.0.0.1
port=6379
connectionTimeOut=5000
soTimeout=5000
datebase=0
代码解释
在我们的UserMapper.xml中添加
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
代码解释
RedisCache类图,Cache就是Mybatis中缓存的顶层接口。

二级缓存应用场景
当存在频繁被调用的查询请求时,并且用户对结果的实时性要求较低,则可以考虑使用MyBatis提供的二级缓存解决方案。该方法能够有效减少对数据库的总访问量,并提升整体应用性能。适用于那些涉及耗时较高的统计分析SQL语句及电话账单查询SQL语句等业务场景。
缓存查询顺序
先检查二级存储体内容。若未找到结果,则应持续关注一级存储体的状态;若当前的一级存储体处于未被开启状态,则需再次查看一级存储体;仍然没有找到对应的一级存储体内容后才到数据库进行数据获取操作

二级缓存总结
二级缓存开启方式有两步:
第一步:在全局配置中添加配置
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
代码解释
第二步,在Mapper中添加配置
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
代码解释
二级换已默认启用;但为确保每个Mapper都具备二级缓存功能,则需手动开启其二级缓存设置。
二级缓存的key和一级缓存的key是一样的。
每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。
我们可以继承第三方缓存来作为Mybatis的二级缓存。
总结
本文首先对Mybatis缓取体系的整体架构进行了深入探讨,并展开了对该组件核心模块——持久化组件的相关技术细节研究。具体而言,在对其一级取线与二级取线的工作原理进行了详细阐述的基础上,深入剖析了各个取线实现类的具体代码实现,并对其相关控制逻辑也进行了相应的说明。其中,在讨论到不同层次取线的应用场景时,默认情况下优先选择使用二级取线进行处理,并通过自定义的方式实现了对各层取线机制的有效管理与优化。此外,在这一过程中还明确了不同层次取线之间的交互关系以及各自的适用场景设定原则,在确保系统性能的同时实现了对资源消耗范围的有效控制
参考:http://www.tianxiaobo.com/2018/08/25/
推荐阅读
深入掌握Mybatis动态映射技术确实需要投入大量的时间和精力。
《为忙碌的人提供Java核心技术学习指南》.pdf
美女面试官问我:能说几个常见的Linux性能调优命令吗?

