上一篇 [MyBatis 框架原理 2:SqlSession 运行过程][1] 介绍了 MyBatis 的工作流程, 其中涉及到了 MyBatis 缓存的使用, 首先回顾一下工作流程图:
如果开启了二级缓存, 数据查询执行过程就是首先从二级缓存中查询, 如果未命中则从一级缓存中查询, 如果也未命中则从数据库中查询. MyBatis 的一级和二级缓存都是基于 Cache 接口的实现, 下面先来看看 Cache 接口和其各种实现类.
Cache 接口及常用装饰器
- public interface Cache {
- String getId();
- // 缓存中添加数据, key 为生成的 CacheKey,value 为查询结果
- void putObject(Object key, Object value);
- // 查询
- Object getObject(Object key);
- // 删除
- Object removeObject(Object key);
- // 清空缓存
- void clear();
- // 获取缓存数量
- int getSize();
- // 获取读写锁
- ReadWriteLock getReadWriteLock();
- }
Cache 接口位于 MyBatis 的 cache 包下, 定义了缓存的基本方法, 其实现类采用了装饰器模式, 通过实现类的组装, 可以实现操控缓存的功能. cache 包结构如下:
PerpetualCache 是 Cache 接口的实现类, 通过内部的 HashMap 来对缓存进行基本的操作, 通常配合装饰器类一起使用.
BlockingCache 装饰器: 保证只有一个线程到数据库中查询指定 key 的数据, 如果该线程在 BlockingCache 中未查找到数据, 就获取 key 对应的锁, 阻塞其他查询这个 key 的线程, 通过其内部 ConcurrentHashMap 来实现, 源码如下:
- public class BlockingCache implements Cache {
- // 阻塞时长
- private long timeout;
- private final Cache delegate;
- //key 和 ReentrantLock 对象一一对应
- private final ConcurrentHashMap<Object, ReentrantLock> locks;
- @Override
- public Object getObject(Object key) {
- // 获取 key 的锁
- acquireLock(key);
- // 根据 key 查询
- Object value = delegate.getObject(key);
- // 如果命中缓存, 释放锁, 未命中则继续持有锁
- if (value != null) {
- releaseLock(key);
- }
- return value;
- }
- @Override
- // 从数据库获取结果后, 将结果放入 BlockingCache, 然后释放锁
- public void putObject(Object key, Object value) {
- try {
- delegate.putObject(key, value);
- } finally {
- releaseLock(key);
- }
- }
- ...
FifoCache 装饰器: 先入先出规则删除最早的缓存, 通过其内部的 Deque 实现.
LruCache 装饰器: 删除最近使用最少的缓存, 通过内部的 LinkedHashMap 实现.
SynchronizedCache 装饰器: 同步 Cache.
LoggingCache 装饰器: 提供日志功能, 记录和输出缓存命中率.
SerializedCache 装饰器: 序列化功能.
CacheKey
CacheKey 对象是用来确认缓存项的唯一标识, 由其内部 ArrayList 添加的所有对象来确认两个 CacheKey 是否相同, 通常 ArrayList 内将添加 MappedStatement 的 id,SQL 语句, 用户传递给 SQL 语句的参数以及查询结果集范围 RowBounds 等, CacheKey 源码如下:
- public class CacheKey implements Cloneable, Serializable {
- ...
- private final int multiplier;
- 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>();
- }
- // 向 updateLis 中添加对象
- public void update(Object object) {
- int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
- count++;
- checksum += baseHashCode;
- baseHashCode *= count;
- hashcode = multiplier * hashcode + baseHashCode;
- updateList.add(object);
- }
- @Override
- // 重写 equals 方法判断 CacheKey 是否相同
- public boolean equals(Object object) {
- if (this == object) {
- return true;
- }
- if (!(object instanceof CacheKey)) {
- return false;
- }
- final CacheKey cacheKey = (CacheKey) object;
- if (hashcode != cacheKey.hashcode) {
- return false;
- }
- if (checksum != cacheKey.checksum) {
- return false;
- }
- if (count != cacheKey.count) {
- return false;
- }
- // 比较 updateList 中每一项
- 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;
- }
- }
一级缓存
一级缓存是 session 级别缓存, 只存在当前会话中, 在没有任何配置下, MyBatis 默认开启一级缓存, 当一个 SqlSession 第一次执行 SQL 语句和参数查询时, 将生成的 CacheKey 和查询结果放入缓存中, 下一次通过相同的 SQL 语句和参数查询时, 就会从缓存中获取, 当进行更新或者插入操作时, 一级缓存会进行清空. 在上一篇中说到, MayBatis 进行一级缓存查询和写入是由 BaseExecutor 执行的, 源码如下:
初始化缓存:
一级缓存是 Cache 接口的 PerpetualCache 实现类对象
- public abstract class BaseExecutor implements Executor {
- ...
- 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<DeferredLoad>();
- // 一级缓存初始化
- this.localCache = new PerpetualCache("LocalCache");
- this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
- this.closed = false;
- this.configuration = configuration;
- this.wrapper = this;
- }
- ...
生成 CacheKey
key 在 CachingExecutor 中生成, CacheKey 的 updateList 中放入了 MappedStatement, 传入 SQL 的参数, 结果集范围 rowBounds 和 boundSql:
- 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);
- }
将查询结果和 CacheKey 放入缓存:
- private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
- List<E> list;
- // 缓存中放入 CacheKey 和占位符
- localCache.putObject(key, EXECUTION_PLACEHOLDER);
- try {
- // 在数据库中查询操作
- list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
- } finally {
- localCache.removeObject(key);
- }
- // 缓存中放入 CacheKey 和结果集
- localCache.putObject(key, list);
- if (ms.getStatementType() == StatementType.CALLABLE) {
- localOutputParameterCache.putObject(key, parameter);
- }
- // 返回结果
- return list;
- }
再次执行相同查询条件时从缓存获取结果:
- 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) {
- // issue #482
- clearLocalCache();
- }
- }
- return list;
- }
更新操作时清空缓存:
- 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);
- }
通过以下代码验证下, 分别开两个 session 进行相同的查询, 第一个 session 查询两次:
- public void testSelect() {
- SqlSession sqlSession = sqlSessionFactory.openSession();
- User user = sqlSession.selectOne("findUserById", 1);
- System.out.println(user);
- User user2 = sqlSession.selectOne("findUserById", 1);
- System.out.println(user2);
- sqlSession.close();
- System.out.println("sqlSession closed!===================================");
- // 新建会话
- SqlSession sqlSession2 = sqlSessionFactory.openSession();
- User user3 = sqlSession2.selectOne("findUserById", 1);
- System.out.println(user3);
- sqlSession2.close();
- }
把日志设置为 DEBUG 级别得到运行日志:
- DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
- DEBUG [main] - ==> Parameters: 1(Integer)
- DEBUG [main] - <== Total: 1
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
- DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.MySQL.jdbc.JDBC4Connection@16022d9d]
- DEBUG [main] - Closing JDBC Connection [com.MySQL.jdbc.JDBC4Connection@16022d9d]
- DEBUG [main] - Returned connection 369241501 to pool.
- sqlSession closed!===================================
- DEBUG [main] - Opening JDBC Connection
- DEBUG [main] - Checked out connection 369241501 from pool.
- DEBUG [main] - Setting autocommit to false on JDBC Connection [com.MySQL.jdbc.JDBC4Connection@16022d9d]
- DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
- DEBUG [main] - ==> Parameters: 1(Integer)
- DEBUG [main] - <== Total: 1
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
- DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.MySQL.jdbc.JDBC4Connection@16022d9d]
- DEBUG [main] - Closing JDBC Connection [com.MySQL.jdbc.JDBC4Connection@16022d9d]
- DEBUG [main] - Returned connection 369241501 to pool.
第一次会话中, 虽然查询了两次 id 为 1 的用户, 但是只执行了一次 SQL, 关闭会话后开启一次新的会话, 再次查询 id 为 1 的用户, SQL 再次执行, 说明了一级缓存只存在 SqlSession 中, 不同 SqlSession 不能共享.
二级缓存
二级缓存是 Mapper 级别缓存, 也就是同一 Mapper 下不同的 session 共享二级缓存区域.
只需要在 xml 映射文件中增加 cache 标签或 cache-ref 标签标签就可以开启二级缓存, cache-ref 标签配置的是共享其指定 Mapper 的二级缓存区域. 具体配置信息如下:
blocking : 是否使用阻塞缓存
readOnly : 是否只读
eviction: 缓存策略, 可指定 Cache 接口下装饰器类 FifoCache,LruCache,SoftCache 和 WeakCache
flushInterval : 自动刷新缓存时间
size : 设置缓存个数
type : 设置缓存类型, 用于自定义缓存类, 默认为 PerpetualCache
二级缓存是在 MyBatis 的解析配置文件时初始化, 在 XMLMapperBuilder 中将缓存配置解析:
- private void cacheElement(XNode context) throws Exception {
- if (context != null) {
- // 指定默认类型为 PerpetualCache
- String type = context.getStringAttribute("type", "PERPETUAL");
- Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
- // 默认缓存策略为 LruCache
- String eviction = context.getStringAttribute("eviction", "LRU");
- Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
- Long flushInterval = context.getLongAttribute("flushInterval");
- Integer size = context.getIntAttribute("size");
- boolean readWrite = !context.getBooleanAttribute("readOnly", false);
- boolean blocking = context.getBooleanAttribute("blocking", false);
- Properties props = context.getChildrenAsProperties();
- // 委托 builderAssistant 构建二级缓存
- builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
- }
- }
构建过程:
- public Cache useNewCache(Class<? extends Cache> typeClass,
- Class<? extends Cache> evictionClass,
- Long flushInterval,
- Integer size,
- boolean readWrite,
- boolean blocking,
- Properties props) {
- Cache cache = new CacheBuilder(currentNamespace)
- // 设置缓存类型, 默认为 PerpetualCache
- .implementation(valueOrDefault(typeClass, PerpetualCache.class))
- // 设置缓存策略, 默认使用 LruCache 装饰器
- .addDecorator(valueOrDefault(evictionClass, LruCache.class))
- // 设置刷新时间
- .clearInterval(flushInterval)
- // 设置大小
- .size(size)
- // 设置是否只读
- .readWrite(readWrite)
- .blocking(blocking)
- .properties(props)
- .build();
- configuration.addCache(cache);
- currentCache = cache;
- return cache;
- }
最终得到默认的二级缓存对象结构为:
CachingExecutor 将初始化的 Cache 对象用 TransactionalCache 包装后放入 TransactionalCacheManager 的 Map 中, 下面代码中的 tcm 就是 TransactionalCacheManager 对象, CachingExecutor 执行二级缓存操作过程:
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
- throws SQLException {
- // 从 Configuration 的 MappedStatement 中获取二级缓存
- Cache cache = ms.getCache();
- if (cache != null) {
- // 判断是否需要刷新缓存, SELECT 不刷新, INSERT|UPDATE|DELETE 刷新缓存
- flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- ensureNoOutParams(ms, boundSql);
- @SuppressWarnings("unchecked")
- // 从二级缓存中获取数据
- List<E> list = (List<E>) tcm.getObject(cache, key);
- if (list == null) {
- // 委托 BaseExecutor 查询
- 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);
- }
通过之前一级缓存的例子验证二级缓存, 只需要在 UserMapper 映射文件中加入 cache 标签, 并且让相关 POJO 类实现 java.io.Serializable 接口, 运行得到日志:
- DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
- DEBUG [main] - ==> Parameters: 1(Integer)
- DEBUG [main] - <== Total: 1
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
- DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.MySQL.jdbc.JDBC4Connection@5c072e3f]
- DEBUG [main] - Closing JDBC Connection [com.MySQL.jdbc.JDBC4Connection@5c072e3f]
- DEBUG [main] - Returned connection 1543974463 to pool.
- sqlSession closed!===================================
- DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
User [id=1, username = 小明, birthday=null, sex=1, address = 四川成都]
不同 session 查询同一条记录时, 总共只执行了一次 SQL 语句, 并且日志打印出了缓存的命中率, 这时候不同 session 已经共享了二级缓存区域.
[1]: https://www.cnblogs.com/abcboy/p/9656302.HTML
来源: https://www.cnblogs.com/abcboy/p/9688961.html