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 等,具备基本功能的缓存类 | 无 |
LruCache | LRU 策略的缓存 | 当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use) | eviction=“LRU”(默认) |
FifoCache | FIFO 策略的缓存 | 当缓存到达上限时候,删除最先入队的缓存 | eviction=“FIFO” |
SoftCache | 带清理策略的缓存 | 通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReference | eviction=“SOFT” |
WeakCache | 带清理策略的缓存 | 通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 WeakReference | eviction=“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 探究
发布评论