为什么 MyBatis 要缓存

缓存在互联网系统中是非常重要的, 其主要作用是将数据保存到内存中, 当用户查询数据时, 优先从缓存容器中获取数据,而不是频繁地从数据库中查询数据,从而提高查询性能。而在 ORM 框架中引入缓存的目的就是为了减少读取数据库的次数,从而提升查询的效率。

在 MyBatis 中存在两种缓存,一个在事务内部使用的一级缓存,另一个可以全局使用的二级缓存。

一级缓存

一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个 SqlSession 中,跨 SqlSession 是无效的。

需要注意的是:MyBatis 中一级缓存是默认开启的,不需要任何的配置。

既然一级缓存的作用域只对同一个 SqlSession 有效,那么一级缓存应该存储在哪里比较合适是呢?

当然是 SqlSession 内是最合适的,下面我们看看 SqlSession 的唯一实现类 DefaultSqlSession,如下图所示:

我们知道,SqlSession 只提供对外接口,实际执行 sql 的就是 Executor。
下面是查询的执行流程:

每个 SqlSession 实例中都有一个 Executor 对象,Executor 对象执行查询操作。

Executor有多个实现,无论哪种实现,查询的时候都会执行父类 BaseExecutor 的 query 方法。

  @Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {//获取SQLBoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);//创建一级缓存return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}

从这里我们可以看到,在查询之前就会去 localCache 中根据 CacheKey 对象来获取缓存,获取不到才会调用下面的 query 方法中的 queryFromDatabase 方法。

接下来看一下缓存的 key 是怎么创建的。
缓存的 key 使用 CacheKey 表示。MyBatis 使用 createCacheKey 方法创建 CacheKey 对象,如下所示:

  @Overridepublic 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());//Mapper接口的全限定类名+方法名cacheKey.update(rowBounds.getOffset());//查询数据的偏移量cacheKey.update(rowBounds.getLimit());//查询条数cacheKey.update(boundSql.getSql());//原始SQL语句List<ParameterMapping> 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 #176cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}

创建缓存 key 会经过一系列的 update 方法,update 方法由一个 createKey 对象来执行,这个 update 方法最终由 updateList 的 list 来把五个值存进去,对照上面的代码和下面的图示,我们可以理解这五个值是什么了,如下图:


CacheKey 集合了 SQL 语句、SQL 参数、调用的 Mapper 方法名、SQL 分页参数、Environment 对象信息。当比较两个 CacheKey 是否相等时,上述这些数据都要参与比较,而且 CacheKey 对象哈希值也是基于这些数据生成的。因此 CacheKey 重写了 hashCode(),equals()。

下面看一下 cacheKey.update 方法就可以明白 hashCode(),equals() 是如何做的了。

  //每次向CacheKey添加数据时,都要调用该方法public void update(Object object) {//计算入参对象的hash值int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++;checksum += baseHashCode;baseHashCode *= count;//计算CacheKey对象哈希值//hashCode()方法返回的就是hashcode值//multiplier = 37hashcode = multiplier * hashcode + baseHashCode;//updateList是List对象,用于保存原始数据,比较两个CacheKey是否相等,要比较updateList中的数据updateList.add(object);}

创建完 CacheKey 之后,我们继续进入 query 方法:

 @SuppressWarnings("unchecked")@Overridepublic <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) {//检测当前Executor是否已经被关闭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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482//如果设置localCacheScope=STATEMENT,则清空缓存clearLocalCache();}}return list;}

在 query 方法中,如果查不到的话,就去数据库查询,在 queryFromDatabase 中会对 localCache 进行写入,localCache 对象的 put 方法最终会交给 Map 进行存放,如下所示:

  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;}
  @Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}

二级缓存

一级缓存因为只能在同一个 SqlSession 中共享,所以会有一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。

二级缓存默认是关闭的,开启二级缓存需要三步操作,如下:

1、在 mybatis-config 中有一个全局配置属性,这个不配置也行,因为默认就是 true。

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

2、在 Mapper 映射文件内需要配置缓存标签:

<cache/>
或
<cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>

3、在select查询语句标签上配置useCache属性,如下:

<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true">select * from lw_user</select>

以上配置第1点是默认开启的,也就是说我们只要配置第 2 点就可以打开二级缓存了,而第 3 点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。

不过开启二级缓存的时候有两点需要注意:

1、需要 commit 事务之后才会生效
2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)

如果不实现序列化接口则会报错误。

二级缓存是通过 CachingExecutor 对象来实现的,接下来我们来看看这个对象都有些什么。


我们看到 CachingExecutor 中只有 2 个属性,Executor 就不用说了,因为 CachingExecutor 本身就是 Executor 的包装器,所以属性 TransactionalCacheManager 可以确定就是用来管理二级缓存的,我们再进去看看 TransactionalCacheManager 对象是如何管理缓存的:


TransactionalCacheManager 中维护了一个 HashMap 来存储缓存。HashMap 中的 value 是一个TransactionalCache 对象,继承了 Cache,我们在进去看看。

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;private final Set<Object> entriesMissedInCache;

我们可以看到其中也维护了一个 Map, 这个 Map 是暂存区真正用来暂存数据的地方, 而 delegate 属性代表的便是真正的缓存区, 有了与缓存区之间的关联, 在提交事务的时候, 就可以方便的把暂存区的数据刷新到缓存区了。

介绍完事务管理器, 暂存区, 缓存区之间的结构关系, 我们来通过源码看下二级缓存进行查询的过程,如下:

  @Overridepublic <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);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache = ms.getCache();//获取二级缓存//全局配置文件默认开启,假如 Mapper.xml 文件没有开启二级缓存,这里就会是 null 值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) {//如果二级缓存没有获取到,就回去执行原来的 Executor 中的 query 方法,也就是会再去读取一级缓存list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//二级缓存存储时会先保存在一个临时的属性中,直到事务提交才会保存到二级缓存//存储二级缓存tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。所以我们看一下 commit 方法:

在提交的方法中, 我们会把暂存区中的所有内容刷新到缓存区中。

在我们调用 sqlSession.commit() 方法的时候, 也会调用当前会话持有的缓存执行器的 commit() 方法, 缓存执行器会执行事务缓存管理器的 commit() 方法。看一下事务缓存管理器的提交的源码, 在事务缓存管理器的 commit() 方法中, 会调用事务缓存管理器所有暂存区 (TransactionalCache) 的 commit() 方法。

在 TransactionalCache 的 commit() 方法中, 如果有未提交的更新操作( clearOnCommit 为 true), 则要清空缓存区, 因为更新后, 缓存区的数据便是不准确的了。随后调用 flushPendingEntries 和 reset 两个方法, flushPendingEntries 方法负责把所有暂存区的内容刷新到缓存中。而 reset 方法则负责把本地暂存区清空, 同时把 clearOnCommit 置为 false。

public class TransactionalCache implements Cache:
private final Cache delegate;//指向缓存区(链条式的 Cache 实现类)
private boolean clearOnCommit;//执行更新后 clearOnCommit 将变为 true
private final Map<Object, Object> entriesToAddOnCommit;//本地暂存
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();
}

MyBatis 缓存原理解析相关推荐

  1. mybatis延迟加载原理解析

    延迟加载前言: 在很多真实的实战的业务场景中,由于业务的复杂度,都会让我们进行过多的进行一些连接查询,在数据量少的时候,我们或许感受不到查询给我们带来的效率影响,在数据量和业务复杂的时候我们进行过多的 ...

  2. MyBatis插件原理解析及自定义插件实践

    一.插件原理解析 首先,要搞清楚插件的作用.不管是我们自定义插件,还是用其他人开发好的第三方插件,插件都是对MyBatis的四大核心组件:Executor,StatementHandler,Param ...

  3. RecycleView 缓存原理解析

    前言 下面让我们剖析一下RecycleView 缓存原理. RecycleView 缓存类型 缓存容器 需要创建布局 需要重新绑定 存入时机 取出时机 mAttachedScrap 否 否 Recyc ...

  4. Mybatis逻辑分页原理解析RowBounds

    物理分页Mybatis插件原理分析(三)分页插件 Mybatis提供了一个简单的逻辑分页使用类RowBounds(物理分页当然就是我们在sql语句中指定limit和offset值),在DefaultS ...

  5. mybatis缓存原理

    缓存概述 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持: 一级缓存基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 ...

  6. 清空缓存的命令_超详细的mysql数据库查询缓存原理解析、涉及命令、流程分析等...

    概述 mysql查询缓存在数据库优化可以起到很大的作用,今天主要针对这一块做一个总结,下面一起来看看吧~ 一.缓存条件,原理 MySQL Query Cache是用来缓存我们所执行的SELECT语句以 ...

  7. Mybatis——缓存原理和分析

    摘要 主要是分析的一下Mybatis的一级缓存和二级缓存的原理和源码的操作.每一次在和数据库进行会话的过程中,MyBatis 都会创建一个SqlSession对象.同一次会话期间,只要是查询过的数据, ...

  8. MyBatis缓存技术(一级缓存、二级缓存)

    MyBatis缓存技术(一级缓存.二级缓存) MyBatis的缓存分类 一级缓存 一级缓存的作用域是一个SqlSession.MyBatis默认开启一级缓存.在同一个SqlSession中,执行相同的 ...

  9. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

最新文章

  1. 《UML面向对象设计基础》—第1章1.2节信息/实现隐藏
  2. 和平精英微信和qq不是一个服务器,和平精英微信和QQ玩家能不能一起玩?微信和QQ怎么开黑建房[图]...
  3. android 开发时遇到的环境问题3--eclipse整个项目工程报错
  4. php运行资源库,如何在sublime上运行php
  5. Visual studio内存泄露检查工具--BoundsChecker
  6. Spark大数据平台
  7. PB级数据实时分析,ClickHouse到底有多彪悍?
  8. 视打击微软 力挺国产红旗Linux
  9. mysql 节假日判断_sql 节假日判断(春节、中秋、国庆、周末等)
  10. 如何防止服务器被攻击?
  11. 基于STM32蓝牙无线手环脉搏心率计步器体温监测设计
  12. 使用 HTML CSS 和 JavaScript 创建星级评分系统
  13. python字符串常用操作方法(一)
  14. [推荐]中国联通推出3G新套餐,基本套餐最低46元
  15. 分享Python简短代码,实现TXT转换MySQL文件。
  16. 3.7 倒计时计时器——全部代码
  17. DatawhaleGit-Model:假设检验3-分类数据的检验
  18. AR技术揭秘:如何实现虚拟与现实的完美融合?
  19. 钢笔墨水能否代替打印机墨水_UV打印机的墨水选择
  20. Linux 操作文本内容命令

热门文章

  1. verilog 语言实现任意分频
  2. FFmpeg —— Linux下进行编译配置(硬件加速编解码)
  3. C++黑马视频教程对应的课件
  4. x265探索与研究(九):compressFrame()函数
  5. linux做m3u8推流服务器,linux搭建nginx流服务器,OBS推流,VCL拉流播放
  6. 贾樟柯赵涛宣布结婚:8月已领证 威尼斯拍婚照
  7. 电工技术(7)—正弦量的向量表示法
  8. 去哪儿网怎么沦为骗子的平台了,一步步揭开去哪儿网欺骗消费者的把戏
  9. ansys workbench 中模态分析的solution information都包含什么信息?
  10. 正割、余割、正弦、余弦、正切、余切之间的关系的公式 sec、csc与sin、cos、tan、cot之间的各种公式...