所有 insert/update/delete
语句都会导致缓存被清除。
一级缓存是针对数据库会话的,用于优化在一次数据库会话里多次执行同样的 SQL。
有两种范围:
如果要在全局更改一级缓存的范围,需要在 MyBatis 的配置文件中设置:
<setting name="localCacheScope" value="STATEMENT"/>
二级缓存是针对 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>
一级缓存通过 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(); } } }
二级缓存通过 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、跟随事务提交而提交?
因为事务里可能先修改了数据、然后查询出最新的数据,因此必须是事务提交了才能提交到二级缓存,否则事务未提交或事务回滚、其他会话可能会看到脏数据。
缓存元素可以这样配置:
<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的克隆副本。
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; } }
MyBatis 的缓存默认是采用本地缓存,在目前应用程序一般采用多实例、分布式部署的情况下,容易出现脏数据,尽量不要使用其缓存,如果要使用,尽量使用集中式缓存。
二级缓存是针对 Mapper 的 namespace 的,尽量保证对一张表的访问只在一个 Mapper 里。否则 Mapper A 关联的缓存缓存了 T 表的查询数据, Mapper B 对 T 表进行更新,导致 A 的缓存数据变成脏数据。
之前碰到的一个 MyBatis 缓存踩坑的场景:取数线程从数据库获取一批未处理的任务,然后提交到一个线程池进行处理,处理完后再取下一批,直至所有未处理的任务处理完成。因为取数线程每次都只负责读操作、更新操作是由线程池里的线程执行的,因此取数线程关联的 SqlSession 的一级缓存对数据进行了缓存,导致数据会被重复处理。修复也简单,取数线程在任务处理完成后清除缓存或者把取数 SQL 的缓存级别设置为 STATEMENT
。
欢迎关注我的微信公众号: coderbee笔记 ,可以更及时回复你的讨论。