MyBatis 缓存

所有 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 一级缓存实现

一级缓存通过 BaseExecutorlocalCache 属性实现。

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笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据