MyBatis源码分析(五):MyBatis Cache分析

一、Mybatis缓存介绍

  在Mybatis中,它提供了一级缓存和二级缓存,默认的情况下只开启一级缓存,所以默认情况下是开启了缓存的,除非明确指定不开缓存功能。使用缓存的目的就是把数据保存在内存中,是应用能更快获取数据,避免与数据库频繁交互,特别是在查询比较多、命中率比较高的情况下,缓存就显得很重要。但是使用不得当,会产生脏数据。

二、目录

  • 一级缓存介绍及相关配置。
  • 一级缓存工作流程及源码分析。
  • 一级缓存总结。
  • 二级缓存介绍及相关配置。
  • 二级缓存源码分析。
  • 二级缓存总结。
  • 全文总结。

三、一级缓存介绍及相关配置

  一级缓存是有Session、Statement两种级别。Session级别,在同一个SqlSession中,执行相同的SQL查询,第一次从数据库里面找出来,然后写到缓存中,后续的查询就会从一级缓存中取出来。若是在一次数据库会话中发生了在修改操作后,再次执行的相同查询,会发现Myabtis查询了数据库,说明一级缓存失效;如果没有声明要刷新,而且缓存时间超时了,缓存会被清空。Statement级别,可以理解为支队当前执行的这个Statement有效。

  mybatis-config中关于缓存的部分配置:

1 <setting name="localCacheScope" value="SESSION"/>

四、一级缓存工作流程以及源码分析

执行流程大致为下图:

在上一篇文章中的BaseExecutor的query方法中有一断代码,根据算法生成的key,从缓存中查询是否存在需要查询的的对象,若为空,则从数据库中查询结果

 1     try {
 2       queryStack++;
 3       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;  //从缓存中查询
 4       if (list != null) {
 5         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
 6       } else {
 7         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); //从数据库中查询
 8       }
 9     } finally {
10       queryStack--;
11        }

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:

1 public class PerpetualCache implements Cache {
2
3   private final String id;
4
5   private Map<Object, Object> cache = new HashMap<>();
6
7   ... ...
8 }

SQL语句执行过程在上一篇文章中已经详细解释,BaseExecutor的query方法执行到最后会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因

 1 if (queryStack == 0) {
 2       for (DeferredLoad deferredLoad : deferredLoads) {
 3         deferredLoad.load();
 4       }
 5       // issue #601
 6       deferredLoads.clear();
 7       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
 8         // issue #482
 9         clearLocalCache();
10       }
11 }

接下来看insert和delete方法,它们都是走update方法

 1   @Override
 2   public int insert(String statement) {
 3     return insert(statement, null);
 4   }
 5
 6   @Override
 7   public int insert(String statement, Object parameter) {
 8     return update(statement, parameter);
 9   }
10
11   @Override
12   public int delete(String statement) {
13     return update(statement, null);
14   }
15
16   @Override
17   public int delete(String statement, Object parameter) {
18     return update(statement, parameter);
19   }
 1   @Override
 2   public int update(String statement, Object parameter) {
 3     try {
 4       dirty = true;
 5       MappedStatement ms = configuration.getMappedStatement(statement);
 6       return executor.update(ms, wrapCollection(parameter));
 7     } catch (Exception e) {
 8       throw ExceptionFactory.wrapException("Error updating database.  Cause: " +
 9     e, e);
10     } finally {
11       ErrorContext.instance().reset();
12     }
13    }

再转到BaseExecutor看update方法,最后把doUpdate交给了继承了BaseExecutor的Executor中

1 @Override
2 public int update(MappedStatement ms, Object parameter) throws SQLException {
3     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
4     if (closed) {
5       throw new ExecutorException("Executor was closed.");
6     }
7     clearLocalCache();  //清空了缓存
8     return doUpdate(ms, parameter);
9 }

再看到SimpleExecutor(默认)源码,清空了缓存,然后往数据库中执行更新操作,所以一级缓存发生更新操作时,会清空缓存

 1 @Override
 2 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 3     Statement stmt = null;
 4     try {
 5       Configuration configuration = ms.getConfiguration();
 6       StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
 7       stmt = prepareStatement(handler, ms.getStatementLog());
 8       return handler.update(stmt);
 9     } finally {
10       closeStatement(stmt);
11     }
12 }

然后都是上一篇介绍SQL执行过程的内容了。

五、一级缓存的总结

  • MyBatis一级缓存的生命周期和SqlSession一致。
  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

六、二级缓存介绍及相关配置

一级缓存中最大的共享范围是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要开启二级缓存。二级缓存开启的时候,调用Configuration的newExecutor方法,利用CachingExecutor装饰Executor,在进入一级缓存的查询前,先执行二级缓存的查询的流程。

mybatis-config中关于开启二级缓存的部分配置:

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

七、二级缓存源码分析

二级缓存开启,在一个SqlSession查询了对象,用另外一个SqlSession中查同样的数据,首先CachingExecutor先从二级缓存中查询

在CachingExecutor的query方法里,首先从MappedStatement中获取配置初始化时赋予的Cache,本质上是装饰器模式的使用,具体的装饰链是:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

 1 @Override
 2 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 3       throws SQLException {
 4     Cache cache = ms.getCache(); //获取配置初始化时的cache 5     if (cache != null) {
 6       flushCacheIfRequired(ms); //判断是否刷新
 7       if (ms.isUseCache() && resultHandler == null) {
 8         ensureNoOutParams(ms, boundSql); //主要是用来处理存储过程的
 9         @SuppressWarnings("unchecked")
10         List<E> list = (List<E>) tcm.getObject(cache, key); //从tcm中获取缓存的列表,把获取值的职责一路传递,最终到PerpetualCache11         if (list == null) {
12           list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
13           tcm.putObject(cache, key, list); //如果查询到数据,则调用tcm.putObject方法,往缓存中放入值;不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中
14         }
15         return list;
16       }
17     }
18     return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
19 }

以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。

  • SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

MyBatis的CachingExecutor持有了TransactionalCacheManager,上面的tcm,它里面有一个Map,这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。

1 private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。TransactionalCache里面有一个clear方法

1 @Override
2 public void clear() {
3     clearOnCommit = true;
4     entriesToAddOnCommit.clear();
5 }

CachingExecutor里面的commit方法

1 @Override
2 public void commit(boolean required) throws SQLException {
3     delegate.commit(required);
4     tcm.commit();
5 }

TransactionalCachingManager的commit方法

1 public void commit() {
2     for (TransactionalCache txCache : transactionalCaches.values()) {
3       txCache.commit();
4     }
5 }

TransactionalCache的commit方法

1 public void commit() {
2     if (clearOnCommit) {
3       delegate.clear();
4     }
5     flushPendingEntries();
6     reset();
7 }

TransactionalCache的flushPendingEntries方法,在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。

1 private void flushPendingEntries() {
2     for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
3       delegate.putObject(entry.getKey(), entry.getValue());
4     }
5     ......
6 }

后续的查询操作会重复执行这套流程。如果是 insert | update | delete 的话,会统一进入CachingExecutor的update方法

1 private void flushCacheIfRequired(MappedStatement ms) 

二级缓存执行后会执行一级缓存操作。

八、二级缓存总结

  • MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  • MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  • 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

九、总结

本文对介绍了MyBatis一二级缓存的基本概念,并从应用及源码的角度对MyBatis的缓存机制进行了分析。最后对MyBatis缓存机制做了一定的总结,个人建议MyBatis缓存特性在生产环境中进行关闭,单纯作为一个ORM框架使用可能更为合适。

参考:聊聊MyBatis缓存机制

原文地址:https://www.cnblogs.com/magic-sea/p/11210654.html

时间: 2024-10-28 11:30:01

MyBatis源码分析(五):MyBatis Cache分析的相关文章

了解mybatis源码手写mybatis

一:mybatis概述 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录. 二:手写mybatis 实现思路:: 1创建SqlSessionFactory实例 2:实例化过程中,加载配置

【mybatis源码学习】mybatis的参数处理

一.mybatis的参数处理以及参数取值 1.单个参数 mybatis不做任何处理 取值方式: ? #{参数名/任意名} <!-- Employee getEmpById(Integer id); --> <select id="getEmpById" resultType="com.mxc.bean.Employee"> select * from employee where id=#{id} </select> 2.多个参数

【mybatis源码学习】mybatis的插件功能

一.mybatis的插件功能可拦截的目标 org.apache.ibatis.executor.parameter.ParameterHandler org.apache.ibatis.executor.resultset.ResultSetHandler org.apache.ibatis.executor.statement.StatementHandler org.apache.ibatis.executor.Executor 二.Mybatis的插件功能接入步骤 1.实现接口:org.a

MyBatis源码分析-SQL语句执行的完整流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

Mybatis源码分析

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

MyBatis源码分析-MyBatis初始化流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis初始化的过程也就是创建Configura

mybatis专题(三)-----mybatis源码学习

源码分析概述 源码包分析 下载地址 MyBatis 源码下载地址:https://github.com/mybatis/mybatis-3 导入过程 1. 下载MyBatis的源码 2. 检查maven的版本,必须是3.25以上,建议使用maven的最新版本 3. mybatis的工程是maven工程,在开发工具中导入,工程必须使用jdk1.8以上版本: 4. 把mybatis源码的pom文件中true,全部改为false,或者直接删除这行: 5. 在工程目录下执行 mvn clean inst

MyBatis源码浅析

什么是MyBatis MyBatis是支持定制化SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手工设置参数以及抽取结果集.MyBatis 使用简单的 XML 或注解来配置和映射基本体,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录. MyBatis简单示例 虽然在使用MyBatis时一般都会使用XML文件,但是本文为了分析程序的简单性,简单的测试程序将不包含XML配置

MyBatis 源码篇-日志模块2

上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一个 jdbc package,package 中的内容如下图所示: BaseJdbcLogger 是一个抽象类,它是 jdbc package 下其他类的父类,类继承关系如下图所示: BaseJdbcLogger 类中定义了一些公共集合和简单的工具方法,提供给子类使用. BaseJdbcLogger

MyBatis 源码篇-MyBatis-Spring 剖析

本章通过分析 mybatis-spring-x.x.x.jar Jar 包中的源码,了解 MyBatis 是如何与 Spring 进行集成的. Spring 配置文件 MyBatis 与 Spring 集成,在 Spring 配置文件中配置了数据源.SqlSessionFactory.自动扫描 MyBatis 中的 Mapper 接口.事务管理等,这部分内容都交由 Spring 管理.部分配置内容如下所示: <?xml version="1.0" encoding="U