所有 insert/update/delete
语句都会导致缓存被清除。
1. 一级缓存
一级缓存是针对数据库会话的,用于优化在一次数据库会话里多次执行同样的 SQL。
有两种范围:
- SESSION:会话级,默认的。
- STATEMENT:语句级,每次执行完 mapper 中的语句后都清除一级缓存,其实就是禁用这个语句的一级缓存。
如果要在全局更改一级缓存的范围,需要在 MyBatis 的配置文件中设置:
<setting name="localCacheScope" value="STATEMENT"/>
2. 二级缓存
二级缓存是针对 Mapper 级别的,默认是启用的,但生效的话要对每个 Mapper 进行配置,Mapper 里没有配置的不使用二级缓存。
注意:如果在多个 Mapper 中存在对同一个表的操作,那么这几个 Mapper 的缓存数据可能会出现不一致现象。
<!-- 不启用的话在配置文件中指定如下 -->
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
<!-- Mapper 文件里配置 cache 元素以生效 -->
<mapper namespace="...UserMapper">
<!-- 默认对该 Mapper 文件里的所有查询使用二级缓存 -->
<cache/>
<!-- 该select语句不使用缓存 -->
<select id="selectAbc" useCache="false">
...
</select>
</mapper>
3. 源码实现
3.1 一级缓存实现
一级缓存通过 BaseExecutor
的 localCache
属性实现。
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 缓存基本是 STATEMENT 的,在执行完后清除缓存。
// issue #482
clearLocalCache();
}
}
return list;
}
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;
}
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 事务提交时清理一级缓存
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
}
3.2 二级缓存实现
二级缓存通过 CachingExecutor 和 MappedStatement 关联的 cache 对象来实现。
二级缓存的特点是跟事务相关,事务提交成功了缓存才生效,事务回滚或不提交则缓存不生效;事务执行过程中查询得到的数据封装在 TransactionalCache
,由 TransactionalCacheManager
维护,CachingExecutor
会在事务提交、回滚的地方调用 TransactionalCacheManager
的方法来控制事务性缓存是否提交到二级缓存。
public class Configuration {
protected boolean cacheEnabled = true; // 默认启用二级缓存
// 每个 SqlSession 会持有一个 Executor
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) {
// 启用了二级缓存的,用 CachingExecutor 包装具体的 Executor
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
CachingExecutor
维护二级缓存。
public class CachingExecutor implements Executor {
private Executor delegate;
// 管理事务执行过程中产生的查询数据
private TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
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) { // Mapper 配置了 cache 才起作用
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
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;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public void commit(boolean required) throws SQLException {
// 提交数据库事务
delegate.commit(required);
// 提交缓存
tcm.commit();
}
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
}finally {
if (required) {
//
tcm.rollback();
}
}
}
}
注意:上面的 Cache cache = ms.getCache();
标明这个 cache 是跟随 MappedStatement 的,多个 SqlSession 执行同一个 MappedStatement 时就能共享这个缓存。
一次事务操作可能涉及多个 Mapper 的缓存,TransactionalCacheManager
维护了多个 Mapper 缓存及其事务性缓存的关系。
public class TransactionalCacheManager {
// Cache 是 Mapper 的二级缓存,
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// 用 目标缓存 初始化事务性缓存
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
TransactionalCache
事务性缓存,持有对目标缓存的引用,根据事务是否提交来决定是否提交到目标缓存。事务结束后总会被清空。
public class TransactionalCache implements Cache {
private Cache delegate;
private boolean clearOnCommit;
private Map<Object, Object> entriesToAddOnCommit;
private Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries(); // 提交到目标缓存
reset(); // 清空
}
private void flushPendingEntries() {
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();
}
}
二级缓存为什么要先封装在 TransactionalCache、跟随事务提交而提交?
因为事务里可能先修改了数据、然后查询出最新的数据,因此必须是事务提交了才能提交到二级缓存,否则事务未提交或事务回滚、其他会话可能会看到脏数据。
4. 缓存存储
4.1 存储属性配置
缓存元素可以这样配置:
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true" />
其属性含义如下:
-
eviction
:缓存的淘汰算法,可选值有”LRU”、”FIFO”、”SOFT”、”WEAK”,缺省值是LRU。 -
flashInterval
:指缓存过期时间,单位为毫秒,60000即为60秒,缺省值为空,即只要容量足够,永不过期。 -
size
:指缓存多少个对象,默认值为1024
。 -
readOnly
:是否只读,如果为true
,则所有相同的sql语句返回的是同一个对象(有助于提高性能,但并发操作同一条数据时,可能不安全),如果设置为false
,则相同的sql,后面访问的是cache的克隆副本。
4.2 底层存储实现
PerpetualCache:直接封装了 HashMap 作为存储,无限容量。
包装器缓存:封装了其他的 Cache 实现,提供额外的逻辑。
-
BlockingCache:为每个 key 分配一个 ReentrantLock 锁,访问 key 要在持有锁的前提下进行。
-
FifoCache:缓存容量满时遵循 FIFO 清除最老的 key 。借助 LinkedList 来实现 FIFO 队列。
-
LoggingCache:每次访问 key 时可以输出缓存的命中率的缓存。维护了对缓存的请求次数、命中次数。
-
LruCache:利用 LinkedHashMap 来实现 最近最少使用 的特性。
-
ScheduledCache:实现了定期清除所有 key 的特性。在每次访问时判断是否需要先清除。维护了最近清除时间、清除间隔。
-
SerializedCache:把值序列化为字节数组来存储的缓存。
-
SynchronizedCache:基于 synchronized 关键字来实现的线程安全的缓存。
-
TransactionalCache:提供了 commit/rollback 方法的可与事务协作的缓存,是事务执行过程中的数据的临时存储,事务提交后才提交到目标缓存。
-
SoftCache:把键值封装在 SoftReference 的子类,利用软引用队列 ReferenceQueue 来跟踪被回收的 key,每次访问底层缓存之前先清理被回收的缓存条目。利用 LinkedList 保持对一定的缓存条目的强引用,防止其被 GC。
-
WeakCache:与 SoftCache 类似,只是把键值封装在 WeakReference 的子类。
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}
5. 小结
MyBatis 的缓存默认是采用本地缓存,在目前应用程序一般采用多实例、分布式部署的情况下,容易出现脏数据,尽量不要使用其缓存,如果要使用,尽量使用集中式缓存。
二级缓存是针对 Mapper 的 namespace 的,尽量保证对一张表的访问只在一个 Mapper 里。否则 Mapper A 关联的缓存缓存了 T 表的查询数据, Mapper B 对 T 表进行更新,导致 A 的缓存数据变成脏数据。
之前碰到的一个 MyBatis 缓存踩坑的场景:取数线程从数据库获取一批未处理的任务,然后提交到一个线程池进行处理,处理完后再取下一批,直至所有未处理的任务处理完成。因为取数线程每次都只负责读操作、更新操作是由线程池里的线程执行的,因此取数线程关联的 SqlSession 的一级缓存对数据进行了缓存,导致数据会被重复处理。修复也简单,取数线程在任务处理完成后清除缓存或者把取数 SQL 的缓存级别设置为 STATEMENT
。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。