当前位置: 首页 > news >正文

mybatis 二级缓存失效_Mybatis 一二级缓存实现原理与使用指南

edb495ddc652e3f47e7567b8056acd6c.png

Mybatis 与 Hibernate 一样,支持一二级缓存。一级缓存指的是 Session 级别的缓存,即在一个会话中多次执行同一条 SQL 语句并且参数相同,则后面的查询将不会发送到数据库,直接从 Session 缓存中获取。二级缓存,指的是 SessionFactory 级别的缓存,即不同的会话可以共享。

缓存,通常涉及到缓存的写、读、过期(更新缓存)等几个方面,请带着这些问题一起来探究Mybatis关于缓存的实现原理吧。

提出问题:缓存的查询顺序,是先查一级缓存还是二级缓存?

本文以 SQL 查询与更新两个流程来揭开 Mybatis 缓存实现的细节。

温馨提示,建议在阅读本文之前先阅读笔者的另外几篇文章:
1)源码分析Mybatis MapperProxy初始化【图文并茂】
2)源码分析Mybatis MappedStatement的创建流程
3)【图文并茂】源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解
4)【图文并茂】Mybatis执行SQL的4大基础组件详解

1、从 SQL 查询流程看一二级缓存


温馨提示,本文不会详细介绍详细的 SQL 执行流程,如果对其感兴趣,可以查阅笔者的另外一篇文章:【图文并茂】源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解

1.1 创建Executor

具体实现由 Configuration 的 newExecutor 方法实现。

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) {                                                           // @1
      executor = new CachingExecutor(executor);                 // @2
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

代码@1:如果 cacheEnabled 为 true,表示开启缓存机制,缓存的实现类为 CachingExecutor,这里使用了经典的装饰模式,处理了缓存的相关逻辑后,委托给的具体的 Executor 执行。

cacheEnable 在实际的使用中通过在 mybatis-config.xml 文件中指定,例如:

<configuration>
    <settings>
        <setting name="cacheEnabled" value="true">
    settings>
configuration>

该值默认为true。

1.2 CachingExecutor#query

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);  // @1
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);   // @2return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);       // @3
}

代码@1:根据参数生成SQL语句。

代码@2:根据 MappedStatement、参数、分页参数、SQL 生成缓存 Key。

代码@3:调用6个参数的 query 方法。

缓存 Key 的创建比较简单,本文就只贴出代码,大家一目了然,大家重点关注组成缓存Key的要素。

BaseExecute#createCacheKey

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());
  List parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }if (configuration.getEnvironment() != null) {// issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }return cacheKey;
}

接下来重点看CachingExecutor的另外一个query方法。

CachingExecutor#query

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {
    Cache cache = ms.getCache();    // @1if (cache != null) {
      flushCacheIfRequired(ms);        // @2if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")
        List list = (List) tcm.getObject(cache, key);      // @3if (list == null) {                                                              // @4
          list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);    //@5
          tcm.putObject(cache, key, list); // issue #578 and #116                                                               // @6
        }return list;
      }
    }return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  //@7
}

代码@1:获取 MappedStatement 中的 Cache cache 属性。

代码@2:如果不为空,则尝试从缓存中获取,否则直接委托给具体的执行器执行,例如 SimpleExecutor (@7)。

代码@3:尝试从缓存中根据缓存 Key 查找。

代码@4:如果从缓存中获取的值不为空,则直接返回缓存中的值,否则先从数据库查询@5,将查询结果更新到缓存中。

这里的缓存即 MappedStatement 中的 Cache 对象是一级缓存还是二级缓存?通常在 ORM 类框架中,Session 级别的缓存为一级缓存,即会话结束后就会失效,显然这里不会随着 Session 的失效而失效,因为 Cache 对象是存储在于 MappedStatement 对象中的,每一个 MappedStatement 对象代表一个 Dao(Mapper) 中的一个方法,即代表一条对应的 SQL 语句,是一个全局的概念。

相信大家也会觉得,想继续深入了解 CachingExecutor 中使用的 Cache 是一级缓存还是二级缓存,了解 Cache 对象的创建至关重要。关于 MappedStatement 的创建流程,建议查阅笔者的另外一篇博文:源码分析Mybatis MappedStatement的创建流程。

本文只会关注 MappedStatement 对象流程中关于缓存相关的部分。

接下来将按照先二级缓存,再一级缓存的思路进行讲解。

1.2.1 二级缓存

1.2.1.1 MappedStatement#cache属性创建机制

从上面看,如果 cacheEnable 为 true 并且 MappedStatement 对象的 cache 属性不为空,则能使用二级缓存。

我们可以看到 MappedStatement 对象的 cache 属性赋值的地方为:MapperBuilderAssistant 的 addMappedStatement 方法,从该方法的调用链可以得知是在解析 Mapper 定义的时候就会创建。

b3e4a2a28a5e6ae2e6dc4dc682bb91e2.png
在这里插入图片描述

使用的 cache 属性为 MapperBuilderAssistant 的 currentCache,我们跟踪一下该属性的赋值方法:
public Cache useCacheRef(String namespace)

其调用链如下:

34cb6eab3f298fecbeff0c733ca000f3.png
在这里插入图片描述

可以看出是在解析 cacheRef 标签,即在解析 Mapper.xml 文件中的 cacheRef 标签时,即二级缓存的使用和 cacheRef 标签离不开关系,并且特别注意一点,其参数为 namespace,即每一个 namespace 对应一个 Cache 对象,在 Mybatis 的方法中,通常namespace 对一个 Mapper.java 对象,对应对数据库一张表的更新、新增操作。
public Cache useNewCache

其调用链如下图所示:

1add645d10b5772476b439c1c2c5b655.png
在这里插入图片描述
在解析 Mapper.xml 文件中的 cache 标签时被调用。
1.2.1.2 cache标签解析

接下来我们根据 cache 标签简单看一下 cache 标签的解析,下面以 xml 配置方式为例展开,基于注解的解析,其原理类似,其代码 XMLMapperBuilder 的 cacheElement 方法。

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");                                                      
      Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      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.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

从上面 cache 标签的核心属性如下:

  • type
    缓存实现类,可选择值:PERPETUAL、LRU 等,Mybatis 中所有的缓存实现类如下:

    5860d3c6bd39ada6b17131a0e6a7701a.png
    在这里插入图片描述
  • eviction
    移除算法,默认为 LRU。

  • flushInterval
    缓存过期时间。

  • size
    缓存在内存中的缓存个数。

  • readOnly
    是否是只读。

  • blocking
    是否阻塞,具体实现请看 BlockingCache。

1.2.1.3 cacheRef
acd118c48e71883970f4b73379658f18.png
在这里插入图片描述

cacheRef 只有一个属性,就是 namespace,就是引用其他 namespace 中的 cache。

Cache 的创建流程就讲解到这里,同一个 Namespace 只会定义一个 Cache。二级缓存的创建是在 *Mapper.xml 文件中使用了< cache/>、< cacheRef/>标签时创建,并且会按 NameSpace 为维度,为各个 MapperStatement 传入它所属的 Namespace 的二级缓存对象。

二级缓存的查询逻辑就介绍到这里了,我们再次回看 CacheingExecutor 的查询方法:
CachingExecutor#query

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {
    Cache cache = ms.getCache();    // @1if (cache != null) {
      flushCacheIfRequired(ms);        // @2if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")
        List list = (List) tcm.getObject(cache, key);      // @3if (list == null) {                                                              // @4
          list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);    //@5
          tcm.putObject(cache, key, list); // issue #578 and #116                                                               // @6
        }return list;
      }
    }return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  //@7
}

如果 MappedStatement 的 cache 属性为空,则直接调用内部的 Executor 的查询方法。也就是如果在 *.Mapper.xm l文件中未定义< cache/>或< cacheRef/>,则 cache 属性会为空。

1.2.2 一级缓存

Mybatis 根据 SQL 的类型共有如下3种 Executor类型,分别是 SIMPLE,  REUSE, BATCH,本文将以 SimpleExecutor为 例来对一级缓存的介绍。

BaseExecutor#query

public  List 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()) {   // @1
      clearLocalCache();
    }
    List list;try {
      queryStack++;                                                              
      list = resultHandler == null ? (List) localCache.getObject(key) : null;     // @2if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);   // @3
      }
    } 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;
  }

代码@1:queryStack:查询栈,每次查询之前,加一,查询返回结果后减一,如果为1,表示整个会会话中没有执行的查询语句,并根据 MappedStatement 是否需要执行清除缓存,如果是查询类的请求,无需清除缓存,如果是更新类操作的MappedStatemt,每次执行之前都需要清除缓存。

代码@2:如果缓存中存在,直接返回缓存中的数据。

代码@3:如果缓存未命中,则调用 queryFromDatabase 从数据中查询。

我们顺便看一下 queryFromDatabase 方法,再来看一下一级缓存的实现类。

 private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);   //@!try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);   // @2
    } finally {
      localCache.removeObject(key);                                                            // @3
    }
    localCache.putObject(key, list);                                                              // @4if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }return list;
  }

代码@1:先往本地缓存存入一个特定值,表示正在执行中。

代码@2:从数据中查询数据。

代码@3:先移除正在执行中的标记。

代码@4:将数据库中的值存储到一级缓存中。

可以看出一级缓存的属性为 localCache,为 Executor 的属性。如果大家看过笔者发布的这个 Mybatis 系列就能轻易得出一个结论,每一个 SQL 会话对应一个 SqlSession 对象,每一个 SqlSession 会对应一个 Executor 对象,故 Executor 级别的缓存即为Session 级别的缓存,即为 Mybatis 的一级缓存。

上面已经介绍了一二级缓存的查找与添加,在查询的时候,首先查询缓存,如果缓存未命中,则查询数据库,然后将查询到的结果存入缓存中。

下面我们来简单看看缓存的更新。

2、从SQL更新流程看一二级缓存


从更新的角度,更加的是关注缓存的更新,即当数据发生变化后,如果清除对应的缓存。

2.1 二级缓存

CachingExecutor#update

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);    // @1
    return delegate.update(ms, parameterObject);  // @2
}

代码@1:如果有必要则刷新缓存。

代码@2:调用内部的 Executor,例如 SimpleExecutor。

接下来重点看一下 flushCacheIfRequired 方法。

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
}

TransactionalCacheManager#clearpublic void clear(Cache cache) {
    getTransactionalCache(cache).clear();
}

TransactionalCacheManager 事务缓存管理器,其实就是对 MappedStatement 的 cache 属性进行装饰,最终调用的还是MappedStatement 的 getCache 方法得到其缓存对象然后调用 clear 方法,清空所有的缓存,即缓存的更新策略是只要namespace 的任何一条插入或更新语句执行,整个 namespace 的缓存数据将全部清空。

2.2 一级缓存的更新

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

其更新策略与二级缓存维护的一样。

一二级缓存的的新增、查询、更新就介绍到这里了,接下来对其进行一个总结。

3、总结


3.1 一二级缓存作用序列图

Mybatis 一二级缓存时序图如下:

afcf39dd4799c200735aa98695019716.png
在这里插入图片描述

3.2 如何使用二级缓存

1、在mybatis-config.xml中将cacheEnable设置为true。例如:

<configuration>
    <settings>
        <setting name="cacheEnabled" value="true">
    settings>
configuration>

不过该值默认为true。

2、在需要缓存的表操作,对应的 Dao 的配置文件中,例如 *Mapper.xml 文件中使用 cache、或 cacheRef 标签来定义缓存。

<?xml  version="1.0" encoding="UTF-8" ?>
mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.winterchen.dao.UserDao" >
  <insert id="insert" parameterType="com.winterchen.model.UserDomain">
    //省略
  insert>
  <select id="selectUsers" resultType="com.winterchen.model.UserDomain">
      //省略
  select>
  <cache type="lru" readOnly="true" flushInterval="3600000">cache>
mapper>

这样就定义了一个 Cache,其 namespace 为 com.winterchen.dao.UserDao。其中 flushInterval 定义该 cache 定时清除的时间间隔,单位为 ms。

如果一个表的更新操作、新增操作位于不同的 Mapper.xml 文件中,如果对一个表的操作的 Cache 定义在不同的文件,则缓存数据则会出现不一致的情况,因为 Cache 的更新逻辑是,在一个 Namespace 中,如果有更新、插入语句的执行,则会清除该 namespace 对应的 cache 里面的所有缓存。那怎么来处理这种场景呢?cacheRef 闪亮登场。

如果一个 Mapper.xml 文件需要引入定义在别的 Mapper.xml 文件中定义的 cache,则使用 cacheRef,示例如下:

<cacheRef "namespace" = "com.winterchen.dao.UserDao"/>

一级缓存默认是开启的,也无法关闭。

edb495ddc652e3f47e7567b8056acd6c.png

相关文章:

  • pycharm导入mysql_Pycharm创建Django项目讲解 python django
  • fanuc机器人控制柜接线_FANUC机器人系统镜像还原步骤
  • zabbix监控磁盘_zabbix监控cpu、内存、磁盘使用情况
  • 一直跳动的按钮插件_职场表格插件 Power Click功能介绍03:工作便签
  • 手机屏幕常见故障_华强北二手苹果手机面容损坏可修复原理(重磅,大家务必小心,莫贪小便宜)...
  • springcloud 创建子父项目_SpringCloud(四)- 父子项目
  • redis常用命令getex_详解Redis基本命令
  • 需要显卡还是cpu_组装电脑装机预算不足的情况下,选择高U低显还是高显低U?...
  • 云计算体系结构中soa构建层_云计算体系结构
  • 编译安装_CentOS 7 源码编译安装Python3.9
  • 着墨中文lisp登入_AUTOLISP程序的设计技巧
  • stegsolve保存的图片打不开_stegsolve.jar压缩包打开和使用方法
  • 设置input标签禁用_input使用小技巧
  • 日历对象导哪个包_日期对象到日历[Java]
  • golang 组播数据接收_组播概念3
  • 【前端学习】-粗谈选择器
  • bearychat的java client
  • Date型的使用
  • docker-consul
  • ES6 学习笔记(一)let,const和解构赋值
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • Java-详解HashMap
  • js继承的实现方法
  • maya建模与骨骼动画快速实现人工鱼
  • PAT A1120
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • spring security oauth2 password授权模式
  • vue中实现单选
  • webpack+react项目初体验——记录我的webpack环境配置
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 搞机器学习要哪些技能
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 简单数学运算程序(不定期更新)
  • 入门到放弃node系列之Hello Word篇
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 我看到的前端
  • Nginx实现动静分离
  • 函数计算新功能-----支持C#函数
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • # 数据结构
  • #pragam once 和 #ifndef 预编译头
  • #数学建模# 线性规划问题的Matlab求解
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (java)关于Thread的挂起和恢复
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (原創) 未来三学期想要修的课 (日記)
  • (转)winform之ListView
  • (转)负载均衡,回话保持,cookie
  • (转载)(官方)UE4--图像编程----着色器开发
  • ./和../以及/和~之间的区别
  • .net core Swagger 过滤部分Api