【MyBatis源码篇】一次搞清楚MyBatis的缓存功能是咋实现的,以及一二级缓存的原理🌈

缓存

在MyBatis中缓存是一个很重要的概念,缓存的存在可以提升我们的查询性能(非SQL性能),减少没有必要的SQL编译和结果的查询,缓存又分为两种,一级缓存、二级缓存。这两种缓存有不同的作用,各司其职,下面我就详细介绍一下吧 !

一、一级缓存

  • 作用域:一级缓存的作用域默认为SqlSession共享,只要操作是在同一个SqlSession内的就可以共享一级缓存
  • 缓存命中必要条件&命中场景
    • 相同的 SqlSession
    • 相同的StatementId 也就是相同的Mapper接口
    • 相同的 SQL 语句和 参数
    • 相同的 RowBounds 返回行数
  • 缓存何时会清空&&失效
    • 调用 sqlSessionclose() 方法
    • 未配置刷新缓存 flushCache=true
    • 未执行Update操作
    • 缓存的作用域不是 STATEMENT

一级缓存执行流程&调用过程图


一级缓存的实现逻辑

之前在 Executor 文章里说过,一级缓存是在 BaseExecutor 内部进行实现的,所有我将对 BaseExecutor 内部代码进行刨析 !

具体的执行流程可参考上述第二张图的调用流程

java 复制代码
/**
     * 执行器的查询方法
     */
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取动态sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 获取缓存信息描述对象 内部会有 statementId、分页参数、sql 这些参数
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 内部重载的查询方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

query方法

java 复制代码
/**
 * 缓存类、内部很简单只有一个HashMap用来缓存数据
 * key就是缓存的元数据、value就是缓存的泛型集合
 */
protected PerpetualCache localCache;
@Override
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();
        // 如果配置的缓存作用域为 Statement的话 则会清除缓存、可用于关闭一级缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // 清除缓存的操作
            clearLocalCache();
        }
    }
    return list;
}

PerpetualCache 一级缓存类

里边就一些 get、set、remove、size 操作 没什么好解释的

java 复制代码
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }
    
 	  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }   
}

小知识:cache 属性为什么不是 ConcurrentHashMap? 因为SqlSession本身就不是线程安全的,所以即便是ConcurrentHashMap也没啥意义

首先在DefaultSqlSession的源码中明确说了不是线程安全的:

java 复制代码
/**
*
* The default implementation for {@link SqlSession}.
* Note that this class is not Thread-Safe.
*
* @author Clinton Begin
*/

我的理解主要是两方面:

  1. 首先由于JDBC的`Connection对象本身不是线程安全的,而session中又只有一个connection,所以不是线程安全的
  2. 一级缓存 由于一级缓存是session级别的,所以如果多个线程同时使用session,当线程A进行了插入操作未完成,但是此时线程B进行查询并缓存了数据,这是就出现了一级缓存与数据库数据不一致的问题。

为什么SqlSession不是线程安全的 转自简书 作者: 没有花的雪


二、二级缓存

  • 作用域:整个应用都可以用,而且可以进行跨线程使用
    • 二级缓存的命中率会更高,适合缓存一些修改较少的数据
  • 存在的问题:因为作用域是整个应用,所以要控制缓存的大小,以免造成OOM的发生
  • 命中条件
    • 要开启二级缓存
    • 必须要手动提交 sqlSession.commit()
    • 和一级缓存一致 (除了一致的SqlSession)
  • 二级缓存的扩展性功能
    • 存储:可存储在硬盘、内存
    • 溢出淘汰机制 FIFO (First In First Out)
    • LRU 最近最少使用
    • 过期清理
    • 序列化
    • 线程安全
    • 命中率统计

二级缓存执行流程图

二级缓存源码解析

二级缓存的入口 CachingExecutor

1、获取缓存

java 复制代码
/**
* 缓存管理器,全局唯一
*/
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
/**
     * 执行查询的方法
     */
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 获取缓存key
    Cache cache = ms.getCache();
    if (cache != null) {
        // 是否刷新缓存
        flushCacheIfRequired(ms);
        // 开启缓存并且不能手动指定结果集处理器
        if (ms.isUseCache() && resultHandler == null) {
            // 校验存储过程中是否有OUT返回值 如果有的话就抛个异常出去,提示说需要关闭缓存!
            ensureNoOutParams(ms, boundSql);
            // 从缓存管理器中获取数据 代码如下
            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);
}

再看tcm.getObject(cache, key);

java 复制代码
/**
   * 获取对象
   */
public Object getObject(Cache cache, CacheKey key) {
    // getTransactionalCache(cache) 可以理解为 map.get(cache)
    return getTransactionalCache(cache).getObject(key);
}
/**
* 最后调用的 getObject(key) 方法
*/
@Override
public Object getObject(Object key) {
    // 上边讲到的责任链 会一层一层往下掉用 最终到达 PerpetualCache 中
    Object object = delegate.getObject(key);
    // 如果得到的是空的话就添加一个空对象、目的是为了防止缓存穿透,会在调用commit方法时提交到真正的二级缓存中去
    if (object == null) {
        entriesMissedInCache.add(key);
    }
    // 如果调用了 clear方法则返回空
    if (clearOnCommit) {
        return null;
    } 
    // 返回结果
    else {
        return object;
    }
}

2、提交事务

java 复制代码
/**
 * 提交事务
 */
@Override
public void commit(boolean required) throws SQLException {
    // 调用BaseExecutor的commit 提交事务
    delegate.commit(required);
    // 提交缓存暂存区的数据至真正的二级缓存 调用TransactionalCacheManager的方法
    tcm.commit();
}
/**
* 循环Map中的值 调用TransactionalCache的commit()方法
* 这个map就是缓存的暂存区Map 
* private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
*/
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();
    }
}
/**
* TransactionalCache的commit()方法
*/
public void commit() {
    // 如果调用clear方法 清空二级缓存
    if (clearOnCommit) {
        delegate.clear();
    }
    // 提交到二级缓存
    flushPendingEntries();
    // 清空暂存区
    reset();
}

二级缓存有这么多的附加功能 MyBatis是如何实现的 ?

MyBatis在实现这些附加功能的时候采用了设计模式: 装饰者模式+责任链模式

使用责任链模式的好处:只有一个顶级接口,在接口的实现类里一层套一层,层层套娃,某个层不需要关心下一个层需要实现什么,提升了代码的层次感。如图:

此图就是缓存的附加功能实现方式,顺序也是如此 (有些功能是需要开启才可以添加进去的)

源码如下

  • 入口 CacheBuilder
java 复制代码
/**
     * 构建一个Cache对象
     *
     * @return 装饰完成的cache对象
     */
public Cache build() {
    // 设置默认的缓存实现 如果为null的话默认的缓存对象就是 PerpetualCache
    setDefaultImplementations();
    // 创建责任链顶端的Cache对象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 设置 LruCache
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 装饰 Cache
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}
  • 实现 装饰&责任链的主方法
java 复制代码
/**
     * 装饰缓存对象和责任链赋值
     *
     * @param cache 最中的缓存对象
     * @return 装饰后的缓存对象
     */
private Cache setStandardDecorators(Cache cache) {

    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        // 过期清理缓存区
        if (clearInterval != null) {
            // 过期清理缓存区
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {
            // 对象序列化
            cache = new SerializedCache(cache);
        }
        // 统计命中率
        cache = new LoggingCache(cache);
        // 线程同步缓存区
        cache = new SynchronizedCache(cache);
        // 第一个 简单阻塞
        if (blocking) {
            cache = new BlockingCache(cache);
        }
        // 因为是责任链所以要倒着看
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

最终该Cache对象会存放于 MappedStatement 中 供其他用到缓存的地方使用


为什么二级缓存虚需要提交之后才可以命中

tex 复制代码
因为二级缓存是跨线程使用的,但是在数据库中两个不同的事务是互相不可见的,正因为有了二级缓存这个东西就导致了他们是可见的,如果不提交就可以命中缓存的话,就可能导致脏读的情况发生 !
相关推荐
向前看-1 小时前
验证码机制
前端·后端
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
AskHarries5 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion6 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp6 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder7 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚8 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心8 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴9 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲9 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端