MyBatis 探究

编程入门 行业动态 更新时间:2024-10-15 18:29:03

<a href=https://www.elefans.com/category/jswz/34/1769966.html style=MyBatis 探究"/>

MyBatis 探究

示例代码:

1、MyBatis 的工作流程分析

    通过MyBatis 探究 | 一、MyBatis应用分析与最佳实践(一)使用到了Mybatis的编程式使用的方法,我们回顾一下Mybatis的主要工作流程:
    首先在 MyBatis 启动的时候我们要去解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了我们怎么控制 MyBatis 的行为,和我们要对数据库下达的指令, 也就是我们的 SQL 信息。我们会把它们解析成一个 Configuration 对象。
    接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接:这个就是 SqlSession 对象。
    我们要获得一个会话,必须有一个会话工厂 SqlSessionFactory 。 SqlSessionFactory 里面又必须包含我们的所有的配置信息,所以我们会通过一个 Builder 来创建工厂类。
    我们知道,MyBatis 是对 JDBC 的封装,也就是意味着底层一定会出现 JDBC 的一些核心对象,比如执行 SQL 的 Statement,结果集 ResultSet。在 Mybatis 里面,SqlSession 只是提供给应用的一个接口,还不是 SQL 的真正的执行对象。
    我们查看SqlSession的默认实现类DefaultSqlSession。

public class DefaultSqlSession implements SqlSession {...//全局的mybatis的配置文件解析类,会把mybatis-config.xml里的所有配置信息以及拓展信息存入该类中private final Configuration configuration;//sqlsession持有的执行器。private final Executor executor;...
}

    SqlSession 持有了一个 Executor 对象,用来封装对数据库的操作。在执行器 Executor 执行 query 或者 update 操作的时候我们创建一系列的对象, 来处理参数、执行 SQL、处理结果集,这里我们把它简化成一个对象:StatementHandler, 在阅读源码的时候我们再去了解还有什么其他的对象。
    所以总结一下mybatis的主要工作流程为:

2、MyBatis 架构分层与模块划分

    在 MyBatis 的主要工作流程里面,不同的功能是由很多不同的类协作完成的,它们分布在 MyBatis jar 包的不同的 package 里面。
    我们来看一下 MyBatis 的 jar 包。jar 包结构是这样的(21 个包):
└── org
    └── apache
        └── ibatis
          ├── annotations
          ├── binding
          ├── builder
          ├── cache
          ├── cursor
          ├── datasource
          ├── exceptions
          ├── executor
          ├── io
          ├── javassist
          ├── jdbc
          ├── lang
          ├── logging
          ├── mapping
          ├── ognl
          ├── parsing
          ├── plugin
          ├── reflection
          ├── scripting
          ├── session
          ├── transaction
          ├── type
    跟 Spring 一样,MyBatis 按照功能职责的不同,所有的 package 可以分成不同的工作层次。
    我们可以把 MyBatis 的工作流程类比成餐厅的服务流程。
    第一个是跟客户打交道的服务员,它是用来接收程序的工作指令的,我们把它叫做接口层。
    第二个是后台的厨师,他们根据客户的点菜单,把原材料加工成成品,然后传到窗口。这一层是真正去操作数据的,我们把它叫做核心层。
    最后就是餐厅也需要有人做后勤(比如清洁、采购、财务),来支持厨师的工作和整个餐厅的运营。我们把它叫做基础层。
    来看一下这张图,我们根据刚才的分层,和大体的执行流程,做了这么一个总结。

2.1、接口层

    首先接口层是我们打交道最多的。核心对象是 SqlSession,它是上层应用和 MyBatis 打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

2.2、核心处理层

    接下来是核心处理层。既然叫核心处理层,也就是跟数据库操作相关的动作都是在这一层完成的。
    核心处理层主要做了这几件事:

  • 把接口中传入的参数解析并且映射成 JDBC 类型;
  • 解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;
  • 执行 SQL 语句;
  • 处理结果集,并映射成 Java 对象。

    插件也属于核心层,这是由它的工作方式和拦截的对象决定的。

2.3、基础支持层

    最后一个就是基础支持层。基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、事务等等这些功能。
    这个就是 MyBatis 的主要工作流程和架构分层。接下来我们来学习一下基础层里面的一个主要模块,缓存。我们一起来了解一下 MyBatis 一级缓存和二级缓存的区别,和它们的工作方式,以及使用过程里面有什么注意事项。

3、MyBatis 缓存详解

    缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

3.1、缓存体系结构

    MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。
    除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。

“装饰者模式(Decorator Pattern)是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能)。”


    但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认 PerpetualCache)。
    所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

缓存实现类描述作用装饰条件
PerpetualCache缓存基本实现类默认是 PerpetualCache,也可以自定义比如 RedisCache、EhCache 等,具备基本功能的缓存类
LruCacheLRU 策略的缓存当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use)eviction=“LRU”(默认)
FifoCacheFIFO 策略的缓存当缓存到达上限时候,删除最先入队的缓存eviction=“FIFO”
SoftCache带清理策略的缓存通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReferenceeviction=“SOFT”
WeakCache带清理策略的缓存通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 WeakReferenceeviction=“WEAK”
LoggingCache带日志功能的缓存比如:输出缓存命中率基本
SynchronizedCache同步缓存基于 synchronized 关键字实现,解决并发问题基本
BlockingCache阻塞缓存通过在 get/put 方式中加锁,保证只有一个线程操 作缓存,基于 Java 重入锁实现blocking=true
SerializedCache支持序列化的缓存将对象序列化以后存到缓存中,取出时反序列化readOnly=false(默认)
ScheduledCache定时调度的缓存在进行 get/put/remove/getSize 等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存–即每隔一段时间清空一次缓存flushInterval 不为空
TransactionalCache事务缓存在二级缓存中使用,可一次存入多个缓存,移除多个缓存在TransactionalCach eManager 中用 Map 维护对应关系

    缓存对象在什么时候创建?什么情况下被装饰?
    我们要弄清楚这个问题,就必须要知道 MyBatis 的一级缓存和二级缓存的工作位置和工作方式的区别。

3.2、一级缓存

    MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。
    首先我们必须去弄清楚一个问题,在 MyBatis 执行的流程里面,涉及到这么多的对象,那么缓存 PerpetualCache 应该放在哪个对象里面去维护?如果要在同一个会话里面共享一级缓存,这个对象肯定是在 SqlSession 里面创建的,作为 SqlSession 的一个属性。
    DefaultSqlSession 里面有两个属性,Configuration 是全局的解析配置文件存储的实例,所以缓存只可能放在 Executor 执行器里面维护SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor 的构造函数中持有了 PerpetualCache。

public class DefaultSqlSession implements SqlSession {private final Configuration configuration;//sqlSession持有的执行器private final Executor executor;...
}
public abstract class BaseExecutor implements Executor {...//持有的cache实例protected PerpetualCache localCache;...
}

    在同一个会话(sqlSession)里面,多次执行相同的 SQL 语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。但是不同的会话里面,即使执行的 SQL 一模一样(通过一个 Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。

    接下来我们来验证一下,MyBatis 的一级缓存到底是不是只能在一个会话里面共享, 以及跨会话(不同 session)操作相同的数据会产生什么问题。

3.2.1、一级缓存验证

    (基于 mybatis-example 工程,注意演示一级缓存需要先关闭二级缓存, 并且localCacheScope 设置为 SESSION)

	<settings><!-- 关闭二级缓存 --><setting name="cacheEnabled" value="fasle"/><!-- 开启一级缓存 --><setting name="localCacheScope" value="SESSION"/></settings>

    判断是否命中缓存:如果再次发送 SQL 到数据库执行,说明没有命中缓存;如果直接打印对象,说明是从内存缓存中取到了结果。

  • 在同一个 session 中共享
public class MybatisExample {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证一级缓存Member member = mapper.selectByPrimaryKey(1L);System.out.println("---二次查询----");Member member1 = mapper.selectByPrimaryKey(1L);}
}

    控制台打印结果:

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1
---二次查询----

    可以看到第二次查询并没有执行对DB的查询操作。我们简单看下源码看下缓存是如何取值的,


public abstract class BaseExecutor implements Executor {...//mybatis所有的查询方法,不论是返回单条,list,map等等//最终均会走到BaseExecutor的query()方法中。public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {...List<E> list;try {queryStack++;//通过localCache获取结果集list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {//如果获取到的list不为空,处理后直接返回handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {//如果获取到的是空,则进行SQL查询list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}...return list;}...
}
  • 不同 session 不能共享
public class MybatisExample {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证一级缓存 不同会话Member member = mapper.selectByPrimaryKey(1L);System.out.println("---二次查询----");SqlSession session1 = sqlSessionFactory.openSession();MemberMapper mapper1 = session1.getMapper(MemberMapper.class);Member member1 = mapper1.selectByPrimaryKey(1L);}
}

    控制台打印结果:

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1
---二次查询----
==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1

    可以看到不同的会话之间一级缓存不共享。因为一级缓存是在Executor中声明的,而Executor又是在SqlSession中声明,不同的SqlSession对应不同的Executor,导致不同会话间一级缓存不共享。

  • 同一个会话中,update(包括delete)会导致一级缓存被清空
public class MybatisExample {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证一级缓存Member member = mapper.selectByPrimaryKey(1L);System.out.println("---执行更新方法----");int i = mapper.updateMemberById(member);System.out.println("---二次查询----");Member member1 = mapper.selectByPrimaryKey(1L);}
}

    控制台打印结果:

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1
---执行更新方法----
==>  Preparing: update member SET name = ?, phone = ? where id = ? 
==> Parameters: 张三(String), 157xxxxxxxx(String), 1(Integer)
<==    Updates: 1
---二次查询----
==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1

    我们再来看一下源码,看执行更新或者删除方法时,缓存为什么就没有了。

public abstract class BaseExecutor implements Executor {.../*** 在mybatis中,所有的delete方法,update方法,或者是insert方法,* 对于执行器来说均为update,最终会调用到该方法*/@Overridepublic 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.");}//在正式执行doUpdate()方法前,会先去调用clearLocalCache()方法//来清空一级缓存中保存的信息,这就是为什么一级缓存在执行写的方法后会失效。clearLocalCache();return doUpdate(ms, parameter);}...
}
  • 如果跨会话,会出现什么问题?其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)
public class MybatisExample {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证一级缓存跨会话导致的脏数据问题//先去查询用户信息Member member = mapper.selectByPrimaryKey(1L);System.out.println("修改前名称" + member.getName());//开启新的会话 来修改用户名称SqlSession session1 = sqlSessionFactory.openSession();MemberMapper mapper1 = session1.getMapper(MemberMapper.class);Member member2 = new Member();member2.setId(1);member2.setName("王五");mapper1.updateMemberById(member2);session1mit();//再次查询用户名称Member member1 = mapper.selectByPrimaryKey(1L);System.out.println("修改后名称" + member1.getName());}
}

    控制台打印结果:

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 157xxxxxxxx, {"sex":1,"age":10}
<==      Total: 1
修改前名称张三
==>  Preparing: update member SET name = ? where id = ? 
==> Parameters: 王五(String), 1(Integer)
<==    Updates: 1
修改后名称张三

    可以看到我们在操作数据库将用户的名称从张三改为王五了,但是因为同一个会话中还存在一级缓存,导致查询出来的结果仍然是张三,从而读取到了脏数据。

3.2.2、一级缓存的不足

    使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。

3.3、二级缓存

3.3.1、二级缓存介绍

    二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。
    思考一个问题:如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是在一级缓存之后呢?二级缓存是在哪里维护的呢?
    作为一个作用范围更广的缓存,它肯定是在 SqlSession 的外层,否则不可能被多个 SqlSession 共享。而一级缓存是在 SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
    第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor 之外创建一个对象。
    实际上 MyBatis 用了一个装饰器的类来维护,就是 CachingExecutor。如果启用了二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。
    CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

    一级缓存是默认开启的,那二级缓存怎么开启呢?

3.3.2、开启二级缓存的方法

    第一步:在 mybatis-config.xml 中配置了(可以不配置,默认是 true),只要没有显式地设置 cacheEnabled=false,都会用 CachingExecutor 装饰基本的执行器。

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

    第二步:在 Mapper.xml 中配置<cache/>标签

<!-- 声明这个 namespace 使用二级缓存 --> 
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" 
size="1024" <!-- 最多缓存对象个数,默认 1024 --> 
eviction="LRU" <!-- 回收策略 --> 
flushInterval="120000" <!--自动刷新时间 ms,未配置时只有调用时刷新--> 
readOnly="false"/> <!--默认是 false(安全),改为 true 可读写时,对象必须支持序列化 -->

    cache 属性详解:

属性含义取值
type缓存实现类需要实现 Cache 接口,默认是 PerpetualCache
size最多缓存对象个数默认1024
eviction回收策略
(缓存淘汰算法)
LRU - 最近最少使用的:移除最长时间不被使用的对象(默认)。
FIFO - 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval定时自动清空缓存间隔自动刷新时间,单位 ms,未配置时只有调用时刷新
readOnly是否只读true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是 false。
改为 false 可读写时,对象必须支持序列化。
blocking是否使用可重入锁实现缓存的并发控制true,会使用 BlockingCache 对 Cache 进行装饰 默认 false

    Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert() 会刷新缓存。
    思考:如果 cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗? 还会出现 CachingExecutor 包装对象吗?
    只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置,决定了在启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query()方法里面的判断。
    如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办? 我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true):

<select id="selectByPrimaryKey" resultMap="BaseResultQueryMap" useCache="false">

    我们查看CachingExecutor的源码,和BaseExecutor逻辑一致,所有的update()、delete()、insert()在mybatis中均为update(),所有的select()方法对应的是query()方法,我们查看query()、update()方法,源码逻辑在注释上有声明。

public class CachingExecutor implements Executor {...@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里的cache标签是否存在,如果不存在,不进行二级缓存。if (cache != null) {//清空二级缓存,重新缓存flushCacheIfRequired(ms);//这里判断Mapper.xml里声明的语句中 useCache属性是否为true,如果为false不执行二级缓存的逻辑。if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);//先从二级缓存中获取@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);//如果二级缓存中不存在或者没有命中,则调用BaseExecutorif (list == null) {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);}/*** update()、delete()、insert() 会刷新缓存。*/@Overridepublic int update(MappedStatement ms, Object parameterObject) throws SQLException {//清空二级缓存flushCacheIfRequired(ms);return delegate.update(ms, parameterObject);}//清空二级缓存的方法private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);}}...
}
3.3.3、二级缓存验证

    (验证二级缓存需要先开启二级缓存)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis//DTD Mapper 3.0//EN" ".dtd" >
<mapper namespace="com.peng.mybatis.mapper.MemberMapper" ><!--针对member表 开启二级缓存--><cache />
</mapper>
<!--  是否使用二级缓存(后面会专门提到缓存,先记下)  是整个工程中所有映射器配置缓存的 开关,即是一个全局缓存开关  默认值 true-->
<setting name="cacheEnabled" value="true"/>

    验证二级缓存

public class MybatisExample {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证二级缓存Member member = mapper.selectByPrimaryKey(1L);//事务提交,二级缓存才会把本次结果缓存住//sessionmit();SqlSession session1 = sqlSessionFactory.openSession();MemberMapper mapper1 = session1.getMapper(MemberMapper.class);Member member1 = mapper1.selectByPrimaryKey(1L);}
}

    控制台打印

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 王五, 157xxxxxxxx, {"sex":1,"age":10}==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 王五, 157xxxxxxxx, {"sex":1,"age":10}

    发现即使开启了二级缓存,仍然是查询两次这是为什么呢?我们把sessionmit()的注释打开,再次执行后控制台打印

==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 王五, 157xxxxxxxx, {"sex":1,"age":10}

    发现这时候已经是只查询一次了。
    思考:为什么事务不提交,二级缓存不生效?
    因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了 TransactionalCache 的getObject()、putObject和 commit()方法,TransactionalCache 里面又持有了真正的 Cache 对象,比如是经过层层装饰的 PerpetualCache。
    在 putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的 commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。它就是在 DefaultSqlSession 调用 commit()的时候被调用的。源码部分,我们首先来看从二级缓存中获取数据时调用的tcm.getObject(cache, key)方法

  @Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {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.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);}

    tcm.getObject(cache, key);方法的实现

public class TransactionalCacheManager {...public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);}...
}

    最后会调用到TransactionalCache的getObject()方法,其源码如下:

public class TransactionalCache implements Cache {private final Cache delegate;private boolean clearOnCommit;private final Map<Object, Object> entriesToAddOnCommit;@Overridepublic Object getObject(Object key) {//从delegate中获取到对应的数据Object object = delegate.getObject(key);if (object == null) {entriesMissedInCache.add(key);}if (clearOnCommit) {return null;} else {return object;}}
}

    通过上述代码我们了解到,获取二级缓存中的数据,是通过调用TransactionalCache类中的delegate获取到的。下面我们再回到CachingExecutor的query()方法,重点关注tcm.putObject(cache, key, list)方法。

@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {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.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);}

    打开tcm.putObject(cache, key, list)方法的实现最后会调用到TransactionalCache的putObject()方法.

public class TransactionalCache implements Cache {private final Cache delegate;private boolean clearOnCommit;private final Map<Object, Object> entriesToAddOnCommit;@Overridepublic void putObject(Object key, Object object) {entriesToAddOnCommit.put(key, object);}
}

    发现putObject()方法存入的地方并不是获取数据时候的delegate,而是一个entriesToAddOnCommit的Map。回到我们写的代码中的sessionmit()方法,找到该方法的实现,最后会调用到TransactionalCache的commit()方法,源码如下:

public class TransactionalCache implements Cache {private final Cache delegate;private boolean clearOnCommit;private final Map<Object, Object> entriesToAddOnCommit;public void commit() {if (clearOnCommit) {delegate.clear();}//存入delegateflushPendingEntries();reset();}private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {//将暂存在entriesToAddOnCommit中的数据放入delegate中。delegate.putObject(entry.getKey(), entry.getValue());}for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}
}

    所以这就是为什么二级缓存在不执行commit()的时候,二级缓存其实是不生效的。

    验证二级缓存在同一namespace下发生增删改操作时,二级缓存会被刷新。

	/*** 测试二级缓存* @throws IOException */public static void testTransactionalCache() throws IOException {String resource = "mybatis-config.xml"; //定位到mybatis全局配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//通过配置文件,构建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);System.out.println("-----session第一次查询-----");//开启sqlSession会话SqlSession session = sqlSessionFactory.openSession();MemberMapper mapper = session.getMapper(MemberMapper.class);//验证二级缓存mapper.selectByPrimaryKey(1L);//事务提交,二级缓存才会把本次结果缓存住sessionmit();System.out.println("-----session2执行insert-----");SqlSession session2 = sqlSessionFactory.openSession();MemberMapper mapper2 = session2.getMapper(MemberMapper.class);Member member2 = new Member();member2.setName("王二");member2.setPhone("13703308122");MemberDetail memberDetail = new MemberDetail();memberDetail.setAge(1);memberDetail.setSex(1);member2.setDetail(memberDetail);mapper2.insertSelective(member2);session2mit();System.out.println("-----session1第二次查询-----");SqlSession session1 = sqlSessionFactory.openSession();MemberMapper mapper1 = session1.getMapper(MemberMapper.class);mapper1.selectByPrimaryKey(1L);}

    控制台打印结果

-----session第一次查询-----
==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 15711074825, {"sex":1,"age":2121}
-----session2执行insert-----
==>  Preparing: insert into member ( name, phone, detail ) values ( ?, ?, ? ) 
==> Parameters: 王二(String), 13703308122(String), {"age":1,"sex":1}(String)
<==    Updates: 1
-----session1第二次查询-----
==>  Preparing: select id, name, phone, detail from member where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, phone, detail
<==        Row: 1, 张三, 15711074825, {"sex":1,"age":2121}

    可以看到在同一namespace下发生增删改操作时,二级缓存会被刷新。为什么增删改操作会清空缓存?我们继续看下源码,在CachingExecutor类的update()方法里

  public int update(MappedStatement ms, Object parameterObject) throws SQLException {//这里调用了清空二级缓存的方法flushCacheIfRequired(ms);return delegate.update(ms, parameterObject);}

    实际调用到TransactionalCache的clear()方法,将clearOnCommit标识至为true。

  @Overridepublic void clear() {//将clearOnCommit标识至为trueclearOnCommit = true;entriesToAddOnCommit.clear();}

    然后在sessionmit()方法中,因为clearOnCommit已经至为true,故会调用delegate.clear()方法将二级缓存的map清空,所以在二级缓存中,当进行增删改操作时,会把当前namespace的二级缓存清空。

  public void commit() {if (clearOnCommit) {delegate.clear();}flushPendingEntries();reset();}
3.3.4、二级缓存的使用场景

    一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

  • 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
  • 如果多个 namespace 中有针对于同一个表的操作,如果在一个namespace 中刷新了缓存,另一个 namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个 Mapper 里面只操作单表的情况使用

PS:如果要让多个 namespace 共享一个二级缓存,应该怎么做?
跨 namespace 的缓存共享的问题,可以使用来解决:

<cache-ref namespace="com.peng.mybatis.mapper.PurchaseLogMapper"/>

    cache-ref 代表引用别的命名空间的 Cache 配置,两个命名空间的操作使用的是同一个 Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。
    注意:在这种情况下,多个 Mapper 的操作都会引起缓存刷新,缓存的意义已经不大。

3.4、一二级缓存在分布式、多线程下的坑

    通过源码我们可以看到,不论一级缓存还是二级缓存,都是本地内存级别的Map存储缓存数据,如果在分布式情况下本地内存之间是不共享的,可能会因为开启一二级缓存从而导致脏读的问题。

3.4.1、一级缓存的坑

    分布式场景:服务A中存储有一级缓存的数据,服务B进行了数据的修改,服务A无法得知数据已经修改,如果在同一会话中的话,再去读取缓存数据,从而导致仍然是旧数据,从而导致脏读。
    多线程场景:在多线程场景中,一般一个线程在开启事务的情况下即可认为是一个sqlSession会话,线程A和线程B中的一级缓存不共享,故同样有可能导致脏读。
    故一般在实际场景中,我们往往会关闭一级缓存,如果有需求,会使用外部缓存服务来完成响应的缓存机制,比如redis

3.4.2、二级缓存的坑

    分布式场景:比如 服务A中有存储二级缓存,但是服务B进行了增删改的操作,服务A并没有感知到从而依旧读取的是旧数据。
    多线程场景:因为二级缓存是namespace级别,在多线程下二级缓存仍然是共享的,故不会出现脏读。

3.4.3、二级缓存使用第三方缓存服务

PS:二级缓存官方支持第三方缓存服务,我们以redis为例,简单介绍下引入第三方缓存。

    pom文件引入依赖

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

    Mapper.xml 配置,type 使用 RedisCache:

<cache type="org.mybatis.caches.redis.RedisCache"  />

    在项目中添加redis.properties

host=localhost
port=50005
password=T9gN1Vn9c500ip2q
database=0

    在测试完二级缓存中,会看到redis中存有一条缓存数据。

更多推荐

MyBatis 探究

本文发布于:2024-02-27 17:54:29,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1707644.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:MyBatis

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!