三.吃透Mybatis源码-缓存的理解

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 对于Mybatis的缓存在上一章节《吃透Mybatis源码-Mybatis执行流程》我们有提到一部分,这篇文章我们对将详细分析一下Mybatis的一级缓存和二级缓存。

来来来,给俏如来扎起。感谢老铁们对俏如来的支持,2021一路有你,2022我们继续加油!你的肯定是我最大的动力

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢


前言

对于Mybatis的缓存在上一章节《吃透Mybatis源码-Mybatis执行流程》我们有提到一部分,这篇文章我们对将详细分析一下Mybatis的一级缓存和二级缓存。

一级缓存

市面上流行的ORM框架都支持缓存,不管是Hibernate还是Mybatis都支持一级缓存和二级缓存,目的是把数据缓存到JVM内存中,减少和数据库的交互来提高查询速度。同时MyBatis还可以整合三方缓存技术。

Mybatis一级缓默认开启,是SqlSession级别的,也就是说需要同一个SqlSession执行同样的SQL和参数才有可能命中缓存。如:
在这里插入图片描述
同一个SqlSession执行同一个SQL,发现控制台日志只执行了一次SQL记录,说明第二次查询是走缓存了。但是要注意的是,当SqlSession执行了delete,update,insert语句后,缓存会被清除。

那么一级缓存在哪儿呢?下面给大家介绍一个类。
在这里插入图片描述
Mybatis中提供的缓存都是Cache的实现类,但是真正实现缓存的是PerpetualCache,其中维护了一个Map<Object, Object> cache = new HashMap<Object, Object>() 结构来缓存数据。其他的缓存类采用了装饰模式对PerpetualCache做增强。比如:LruCache 在PerpetualCache 的基础上增加了最近最少使用的缓存清楚策略,当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use)。代码如下

public class LruCache implements Cache {
   
   
    //对 PerpetualCache 做装饰
  private final Cache delegate;

下面对其他的缓存类做一个介绍

  • PerpetualCache : 基础缓存类
  • LruCache : LRU 策略的缓存 当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use),eviction="LRU"(默 认)
  • FifoCache : FIFO 策略的缓存 当缓存到达上限时候,删除最先入队的缓存,配置eviction="FIFO"
  • SoftCache WeakCache :带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReference 和 WeakReference
  • SynchronizedCache : 同步缓存 基于 synchronized 关键字实现,解决并发问题
  • ScheduledCache : 定时调度的缓存,在进行 get/put/remove/getSize 等操作前,判断 缓存时间是否超过了设置的最长缓存时间(默认是 一小时),如果是则清空缓存--即每隔一段时间清 空一次缓存
  • SerializedCache :支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化
  • TransactionalCache :事务缓存,在二级缓存中使用,可一次存入多个缓存,移除多个缓存 。通过TransactionalCacheManager 中用 Map 维护对应关系。

一级缓存到底存储在哪儿?

一级缓存在SimpleExecutor 的父类 BaseExecutor 执行器中,如下

public abstract class BaseExecutor implements Executor {
   
   

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  //一级缓存
  protected PerpetualCache localCache;

PerpetualCache缓存类源码如下

public class PerpetualCache implements Cache {
   
   

  private final String id;
  //缓存
  private Map<Object, Object> cache = new HashMap<Object, Object>();

那么一级缓存在什么时候创建的?

在 BaseExecutor 中的构造器中创建了一级缓存,而执行器Executor 是保存在SqlSession中的,也就是说当创建SqlSession的时候,就会创建 SimpleExecutor,而在SimpleExecutor的构造器中会调用BaseExecutor的构造器来创建一级缓存。见:org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor

public class SimpleExecutor extends BaseExecutor {
   
   
    //执行器构造器
  public SimpleExecutor(Configuration configuration, Transaction transaction) {
   
   
      //调用父类构造器
    super(configuration, transaction);
  }

下面是 BaseExecutor 的执行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor

public abstract class BaseExecutor implements Executor {
   
   

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  //一级缓存
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;


  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;
  }

一级缓存怎么存储的?

一级缓存是在执行查询的时候会先走二级缓存,二级缓存么有就会走一级缓存,以及缓存没有就会走数据库查询,然后放入一级缓存和二级缓存。我们来看一下源码流程 ,见:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   
   
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //构建缓存的Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //执行查询
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里在尝试构建Cachekey ,cachekey时由:MappedStatement的id(如:cn.xx.xx.xxMapper.selectByid) ,分页,Sql,参数值一起构建而成的,一级二级缓存都是如此。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
   
   
    //开启了二级缓存才会存在Cache  
    Cache cache = ms.getCache();
    if (cache != null) {
   
   
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
   
   
        ensureNoOutParams(ms, 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);
  }

这里我们看到,在执行org.apache.ibatis.executor.CachingExecutor#query 查询的时候会先走二级缓存,二级缓存没有会继续调用 org.apache.ibatis.executor.BaseExecutor#query 查询,而BaseExecutor#query会尝试先走一级缓存

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;
  }

上面代码会先走一级缓存拿数据,如果一级缓存没有,就走数据库获取数据,然后加入一级缓存org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

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;
  }

到这里我们就看到了一级缓存和二级缓存的执行流程,注意的是:先执行二级缓存再执行一级缓存。

这里画一个一级缓存的图
在这里插入图片描述

二级缓存

第一步:二级缓存需要在mybatis-config.xml 配置中开启,如下

<setting name="cacheEnabled" value="true"/>

当然其实该配置默认是开启的,也就是默认会使用 CachingExecutor 装饰基本的执行器。
第二步骤:需要在mapper.xml中配置 < cache/>如下

<mapper namespace="cn.whale.mapper.StudentMapper">
    <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024" 
         eviction="LRU" 
         flushInterval="120000" 
         readOnly="false"/> 
...省略...

解释一下上面的配置,首先<cache/> 是在某个mapper.xml中指定的,也就是说二级缓存作用于当前的namespace.

  • type : 代表的是使用什么类型的缓存,只要是实现了 Cache 接口的实现类都可以
  • size :缓存的个数,默认是1024 个对象
  • eviction : 缓存剔除策略 ,LRU – 最近最少使用的:移除最长时间不被使用的对象(默认);FIFO – 先进先出:按对象进入缓存的顺序来移除它们 ;SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象;WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
  • flushInterval :定时自动清空缓存间隔 自动刷新时间,单位 ms,未配置时只有调用时刷新
  • readOnly :缓存时候只读
  • blocking :是否使用可重入锁实现 缓存的并发控制 true,会使用 BlockingCache 对 Cache 进行装饰 默认 false

Mapper.xml 配置了之后,select()会被缓存。update()、delete()、insert() 会刷新缓存,下面是测试案例
在这里插入图片描述

可以看到,这里使用了2个SqlSesion 2次执行了相同的SQL,参数相同,看控制台日志只执行了一次SQL,说明是命中的二级缓存。因为满足条件:同一个 namespace下的相同的SQL被执行,尽管使用的SqlSession不是同一个。

但是你可能注意到一个细节,就是session.commit() 为什么要提交事务呢?这就要说到二级缓存的存储结构了,如果不执行commit是不会写入二级缓存的。在 CachingExecutor 中有一个属性private final TransactionalCacheManager tcm = new TransactionalCacheManager(); 看名字肯能够看出二级缓存和事务有关系。结构如下

public class CachingExecutor implements Executor {
   
   

  private final Executor delegate;
  //二级缓存,通过TransactionalCacheManager来管理
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 中维护了一个 HashMap()

public class TransactionalCacheManager {
   
   
  //二级缓存的HashMap
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

在TransactionCache中维护了一个 Map<Object, Object> entriesToAddOnCommit;

public class TransactionalCache implements Cache {
   
   

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  //二级缓存临时存储
  private final Map<Object, Object> entriesToAddOnCommit;

  ...省略...
  //写入二级缓存
  @Override
  public void putObject(Object key, Object object) {
   
   
    entriesToAddOnCommit.put(key, object);
  }

当执行查询的时候,从数据库查询出来数据回写入TransactionalCache的entriesToAddOnCommit中,我们来看一下二级缓存写入的流程,见:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
   
   
      //如果mapper.xml配置了 <cache/> 就会创建 Cache
    Cache cache = ms.getCache();
    if (cache != null) {
   
   
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
   
   
        ensureNoOutParams(ms, 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);
  }

如果mapper.xml配置了 就会创建 Cache,Cache不为null,才会走到二级缓存的流程,此时代码来到org.apache.ibatis.cache.TransactionalCacheManager#putObject

public class TransactionalCacheManager {
   
   
  //存储二级缓存
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

    public void putObject(Cache cache, CacheKey key, Object value) {
   
   
    //通过cache为key拿到 TransactionalCache ,把数据put进去
    getTransactionalCache(cache).putObject(key, value);
  }

存储数据的是TransactionalCache ,见org.apache.ibatis.cache.decorators.TransactionalCache#putObject

public class TransactionalCache implements Cache {
   
   

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //正在的二级缓存存储位置
  private final Cache delegate;
  private boolean clearOnCommit;
  //临时的二级缓存存储位置
  private final Map<Object, Object> entriesToAddOnCommit;

  @Override
  public void putObject(Object key, Object object) {
   
   
    entriesToAddOnCommit.put(key, object);
  }

我们看到,数据写到了 TransactionalCache#entriesToAddOnCommit 一个Map中。只有在执行commit的时候数据才会真正写入二级缓存。

我们来看下SqlSession.commit方法是如何触发二级缓存真正的写入的,见:org.apache.ibatis.session.defaults.DefaultSqlSession#commit()

  @Override
  public void commit() {
   
   
    commit(false);
  }

  @Override
  public void commit(boolean force) {
   
   
    try {
   
   
    //调用执行器提交事务
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
   
   
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
   
   
      ErrorContext.instance().reset();
    }
  }

代码来到org.apache.ibatis.executor.CachingExecutor#commit

@Override
  public void commit(boolean required) throws SQLException {
   
   
    //提交事务
    delegate.commit(required);
    //调用org.apache.ibatis.cache.TransactionalCacheManager#commit提交事务
    tcm.commit();
  }

代码来到org.apache.ibatis.cache.TransactionalCacheManager#commit

public void commit() {
   
   
    for (TransactionalCache txCache : transactionalCaches.values()) {
   
   
      //调用 TransactionalCache#commit
      txCache.commit();
    }
  }

代码来到org.apache.ibatis.cache.decorators.TransactionalCache#commit

public class TransactionalCache implements Cache {
   
   

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //真正的二级缓存存储位置,本质是一个 PerpetualCache
  private final Cache delegate;
  //临时存储二级缓存
  private final Map<Object, Object> entriesToAddOnCommit;

  public void commit() {
   
   
    if (clearOnCommit) {
   
   
      delegate.clear();
    }
    //这里在写入缓存,保存到TransactionalCache中的delegate字段,本质是一个PerpetualCache
    flushPendingEntries();
    //把entriesToAddOnCommit清除掉
    reset();
  }

  private void flushPendingEntries() {
   
   
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
   
   
          //从entriesToAddOnCommit中拿到临时的缓存数据,写入缓存,最终会写入PerpetualCache#cache字段中
          delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
   
   
          if (!entriesToAddOnCommit.containsKey(entry)) {
   
   
            delegate.putObject(entry, null);
          }
        }
   }

    private void reset() {
   
   
        clearOnCommit = false;
        //清除entriesToAddOnCommit
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
  }

所以我们总结一下二级缓存的写入流程,二级缓存通过 TransactionalCacheManager中的一个Map<Cache, TransactionalCache>管理的,当执行query查询处数据的时候,会把数据写入TransactionalCache中的 Map<Object, Object> entriesToAddOnCommit 中临时存储。当执行commit的时候才会把entriesToAddOnCommit中的数据写入TransactionalCache中的 Cache delegate ,其本质和一级缓存一样,也是一个 PerpetualCache

当我们做第二次query的时候会尝试通过 TransactionalCacheManager#getObject 从二级缓存获取数据

public class TransactionalCacheManager {
   
   

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  //获取二级缓存
  public Object getObject(Cache cache, CacheKey key) {
   
   
    return getTransactionalCache(cache).getObject(key);
  }

然后会从 TransactionalCache中的delegate中获取缓存

public class TransactionalCache implements Cache {
   
   

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
 //二级缓存
  private final Cache delegate;
  ...省略...

  @Override
  public Object getObject(Object key) {
   
   
    // issue #116
    //从二级缓存获取数据
    Object object = delegate.getObject(key);
    if (object == null) {
   
   
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
   
   
      return null;
    } else {
   
   
      return object;
    }
  }

所以记得,二级缓存一定要commit才会起作用。下面花了一个一级缓存和二级缓存的结构图
在这里插入图片描述

三方缓存框架

除了使用Mybatis自带的缓存,也可以使用第三方缓存方式,比如:比如 ehcache 和 redis 下面以Redis为例 ,首先导入mybatis整合redis的依赖

<dependency>
     <groupId>org.mybatis.caches</groupId>
     <artifactId>mybatis-redis</artifactId> 
     <version>1.0.0-beta2</version> 
 </dependency>

第二步骤:在mapper.xml配置缓存

<cache type="org.mybatis.caches.redis.RedisCache" 
    eviction="FIFO" 
    flushInterval="60000" 
    size="512" readOnly="true"/>

这里type使用了RedisCache,RedisCache也是实现了Cache接口的,接着我们需要配置Redis的链接属性,默认RedisCache类会读取名字为 : redis.properties 的配置文件

host=127.0.0.1
password=123456
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

再次执行测试代码,查看Redis效果如下
在这里插入图片描述
博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
13天前
|
SQL Java 数据库连接
一文细说Mybatis八大核心源码
以上 是V哥给大家整理的8大核心组件的全部内容,为什么说选择 Java 就是选择未来,真正爱 Java 的人,一定喜欢深入研究,学习源码只是第一步,要有一杆子捅到操作系统才够刺激。
|
17天前
|
SQL 缓存 Java
|
1月前
|
缓存 Java 数据库连接
MyBatis三级缓存实战:高级缓存策略的实现与应用
MyBatis三级缓存实战:高级缓存策略的实现与应用
36 0
MyBatis三级缓存实战:高级缓存策略的实现与应用
|
1月前
|
XML 缓存 Java
MyBatis二级缓存解密:深入探究缓存机制与应用场景
MyBatis二级缓存解密:深入探究缓存机制与应用场景
67 2
MyBatis二级缓存解密:深入探究缓存机制与应用场景
|
1月前
|
存储 缓存 Java
探秘MyBatis缓存原理:Cache接口与实现类源码分析
探秘MyBatis缓存原理:Cache接口与实现类源码分析
38 2
探秘MyBatis缓存原理:Cache接口与实现类源码分析
|
1月前
|
XML Java 数据库连接
探秘MyBatis:手写Mapper代理的源码解析与实现
探秘MyBatis:手写Mapper代理的源码解析与实现
21 1
|
1月前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
33 1
深入源码:解密MyBatis数据源设计的精妙机制
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
14 0
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
10 0
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
11 0