前面讲的那些遇到缓存相关的就跳过去了,有点不厚道,那就拿这篇文章就来填坑吧。
为啥要有缓存
我们知道数据库中的数据是存储在磁盘上的,磁盘 IO 的效率比直接在内存中查询低好几个数量级。所以如果可以在内存中将查询的结果缓存起来下次一样的 SQL 语句来查询时直接将内存中的结果返回即可,可以极大的提高系统吞吐量。所以就有了这边文章要介绍的一级缓存和二级缓存。
一级缓存
一级缓存的作用域只在当前会话生命周期内,即不同的 SqlSession
不能共享缓存中的数据。
实现方式
通过前面的介绍我们知道,SqlSession
的查询请求会委托给 Executor
完成,其中 CachingExecutor
是为二级缓存准备的,先不管它。所以查询请求的入口就是 BaseExecutor#query
方法,一起来看看它的逻辑
通过在 BaseExecutor
内维护一个 PerpetualCache
类型的 localCache
属性作为本地缓存。其实现了 MyBatis 的 Cache
接口,通过一个 HashMap
来存储数据。所以一级缓存的生命周期和当前 SqlSession
会话的生命周期一致。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
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;
}
如果缓存中没有就去 DB 中查询,并将查询后的结果放入本地缓存 localCache
中。
调用时序图
如何清除缓存
除了上面看到的通过一些配置属性来清除缓存外,在 Executor
的 update
方法中也会清除本地缓存。我们知道 SqlSession
的增删改操作都会委托给 Executor#update
方法,所以只要当前会话执行过一次增删改操作之前的一级缓存都会被清除。
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);
}
总结
- 一级缓存的生命周期和
SqlSession
一致 - 有多个
SqlSession
或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为STATEMENT
- 可以设置
statement
的flushCache="true"
来强制清除缓存
二级缓存
不同于一级缓存的作用域,二级缓存可以做到在多个 SqlSession
中共享数据。
实现方式
通过前面的介绍我们知道,二级缓存是通过 CachingExecutor
来实现的,所以想要开启二级缓存首先需要在配置文件中加上 <setting name="cacheEnabled" value="true"/>
,这样 SqlSession
所持有的 Executor
实例才是 CachingExecutor
。先看一下其工作流程图:
同一个 namespace
下的缓存数据可以在多个 SqlSession
中共享。
namespace 是啥
其实就是 mapper.xml
的 <cache/>
节点,对应着 MappedStatement
的 cache
属性,不同的 mapper.xml
之间可通过配置 <cache-ref namespace="{another_mapper_namespace}"/>
使得不同的 MappedStatement
持有同一个 Cache
实例。所以想查询某个 statement
时使用二级缓存还需要在其所在的 mapper.xml
文件中配置 cache
或 cache-ref
节点。
show me the code
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
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); //⑤
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- 代码①:从
MappedStatement
中获取Cache
实例 - 代码②:判断是否需要清除缓存,即和一级缓存一样,如果当前
ms
配置了flushCache="true"
则会清除二级缓存 - 代码③:当前
ms
配置了useCache="true"
才会查询二级缓存,所以想使用二级缓存的条件又多了一个😺 - 代码④:通过
tcm
取cache
中的缓存数据,tcm
即TransactionalCacheManager
,待会再介绍它 - 代码⑤:如果二级缓存中没有查到则走👆一级缓存的查询逻辑,并将查询结果通过
tcm
放入cache
所对应的TransactionalCache
的entriesToAddOnCommit
中。注意:此时从并未加入二级缓存,需要执行commit
方法后其他sqlSession
才能在二级缓存中查询到
TransactionalCacheManager
每一个 CachingExecutor
实例都会持有一个 TransactionalCacheManager
变量 tcm
,所有与二级缓存相关的操作也都是通过该类完成的。
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
//清除缓存
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
//查询缓存
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
//将缓存加入待提交 map
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) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCache
从上面 TransactionalCacheManager
的介绍中我们可以看到其通过 transactionalCaches
变量维护着每一个 Cache
实例和与之对应的TransactionalCache
,并将二级缓存相关的操作都交给 TransactionalCache
来处理。
public class TransactionalCache implements Cache {
private final Cache delegate;
private boolean clearOnCommit;
//待提交缓存 map
private final Map<Object, Object> entriesToAddOnCommit;
//未命中缓存的 key 集合
private final Set<Object> entriesMissedInCache;
//对 mappedStatement.cache 进行包装
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
//查询缓存
public Object getObject(Object key) {
//查询 mappedStatement.cache 里的缓存数据
Object object = delegate.getObject(key);
if (object == null) {
//缓存未命中加入 entriesMissedInCache 集合
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
//将从 DB 或一级缓存中查询的数据加入待提交 map
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
//事务提交
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//处理 entriesToAddOnCommit 和 entriesMissedInCache
flushPendingEntries();
reset();
}
//事务回滚
public void rollback() {
//释放 entriesMissedInCache 中的数据
unlockMissedEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//将待提交缓存数据提交至 mappedStatement.cache 中,这样其他 sqlSession 也能从缓存中查询出数据了
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//未命中缓存的 cacheKey 也加入 mappedStatement.cache 中,value 设为 null
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
//删除二级缓存中 value 为 null 的数据
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
// 省略其他方法......
}
从上面代码可以了解到 TransactionalCache
是 MappedStatement.cache
的包装,对二级缓存的操作实际是通过 MappedStatement.cache
来完成的,这样才能达到不同 SqlSession
在同一个 namespace
下缓存数据共享的目的。
总结
- 通过在
MappedStatement
中保存缓存信息,二级缓存相比于一级缓存的作用域更大,能做到在不同SqlSession
之间共享同一个namespace
下的缓存数据 - 增删改操作会清空所在
namespace
下的全部缓存,因为insert
update
delete
的 SQL 语句对应的mappedStatemet.flushCacheRequired
属性默认为true
- 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻
我的看法
现在应用基本都是分布式部署的,所以 MyBatis 的一二级缓存并不能很好的发挥作用,用不好反而会带来一下问题,所以还是拿他作为一个 ORM 框架即可。缓存方案可以采用第三方集中式的缓存中间件,例如 Redis
、Memcached
即可。