前置知识
在了解一级缓存与二级缓存之前大家要大概了解一下SqlSession与executor
SqlSession
- 作为mybatis提供用户操作的门面类。用户一般与db的操作都直接使用sqlSession。例如selectOne,selectList,insert,update,delete等
executor
- 作为SqlSession背后的真正执行者,负责与db交互。并隐藏后续参数映射,结果集拼装等。直接返回给用户想要的数据
- 提供了一级缓存与二级缓存
二者的初始化流程
SqlSession sqlSession = factory.openSession(true);
sqlSession.selectOne("mapper.StudentMapper.getStudentByIdWithClassInfo","2");
factory.openSession(true)会初始化SqlSession对象与executor对象。openSession会进入到DefaultSqlSessionFactory.openSessionFromDataSource方法。
前文说了最终执行都是给executor执行。所以在这里需要构建一个executor。而executor是通过configuration.newExecutor(tx, execType)执行。流程中所使用4大组件是通过new的方式在configuration进行构造。目的为拦截器留口子。(写过Mybatis拦截器的同学都知道,在@Signature注解中,type字段的值就为这4个组件)。构建完executor后,返回DefaultSqlSession对象。此时sqlsession与exexcutor都构建完毕。
执行流程
这里重点从sqlSession到executor,中间包含一级缓存与二级缓存的流程。我们以上述的selectOne作为例子。如下图。
类的流转就是sqlSession->CachingExecutor->baseExecutor->具体的executor。更抽象来说其实就是sqlSession->具体的executor。而作者之所以这样写的原因是使用了装饰者模式与模版方法模式来满足一级缓存与二级缓存。
- CachingExecutor其实就是一个装饰者,内部持有executor对象。会先找寻一下是否有二级缓存,如果不存在则走到executor接口的query方法
- baseExecutor存在的原因是无论是simpleExecutor,reuseExecutor,都有一些通用代码,比如在查询时候的一级缓存。三个执行器都代码一样的close代码。,而差异只是在具体的查询,更新的代码,交给子类实现。这是模版方法模式的常见场景。
executor类结构
- Executor
- 顶层接口,定义了基本的数据库操作方法
- BaseExecutor
- 抽象类,继承自Executor并实现了大部分方法,主要实现了缓存管理和事物管理的方法。具体的查询,更新交给子类实现
- SimpleExecutor
- 继承自BaseExecutor,是默认配置使用PreparedStatement访问数据库,每次访问都创建新的PreparedStatement对象
- ReuseExecutor
- 继承自BaseExecutor,使用预编译的PreparedStatement访问数据库,会重用Statement对象
- BatchExecutor
- 继承自BaseExecutor,提供批量执行Sql语句的能力
- CachingExecutor
-
装饰器类,实现了Executor接口并持有Executor对象,添加了二级缓存的功能
-
而具体的executor配置在配置文件中defaultExecutorType配置,默认为SimpleExecutor。在加载配置文件时,读取该配置,configuration.newExecutor的时候会根据配置进行构建不同的executor
缓存详解
全景图
从上图可以看到当用户进行一个SQL查询时,整个流程为二级缓存->一级缓存->db库。
二级缓存
- 用法
- 使用Mybatis二级缓存一共分为两步。第一步是在配置文件中开启(默认不开启)
<settings> <setting name="cacheEnabled" value="true"/> </settings> - 第二步在xml中加入标签或者在mapper接口中加入@CacheNameSpace(但不能2个同时都加,否则启动会报错。并且如果你使用注解方式,在xml中编写的sql语句也无法命中缓存。原因就是xml中与接口中是2个cache对象。如果使用注解方式,则需要在xml中使用标签指向接口,表明二者是一个cache。这里真的挺坑的,所以建议大家在开发时,统一使用xml方式)。表示该mapper开启。
- 使用Mybatis二级缓存一共分为两步。第一步是在配置文件中开启(默认不开启)
mybatis使用了装饰者模式。通过一系列的cache实现类来实现序列化,定时删除,淘汰机制,事务管理等。
| 缓存名称 | 配置方式 | 作用 |
|---|---|---|
| TransactionalCache | 默认开启 | 作为缓存value的最外面一层。提供事务管理 |
| BlockingCache | 默认关闭,通过cache标签中blocking=true | 防止缓存击穿 |
| SynchronizedCache | 默认开启 | 通过synchronized关键字保证同步 |
| LoggingCache | 默认开启 | 记录缓存命中率 |
| SerializedCache | 默认开启 | 序列化和反序列化对象 |
| ScheduledCache | 默认关闭,通过cache标签中配置flushInterval=600(单位秒)配置 | 定时清理策略 |
| LruCache | 默认开启 | 淘汰策略的默认方式。其中个数默认为1024个 |
| FifoCache | 默认关闭,通过cache标签中eviction=“FIFO” | 淘汰策略的其中一种,个数也是1024个 |
| SoftCache | 默认关闭,通过cache标签中eviction=“SOFT” | 淘汰策略的其中一种,跟Java对象软引用生命周期一致 |
| WeakCache | 默认关闭,通过cache标签中eviction=“WEAK” | 淘汰策略的其中一种,跟Java对象弱引用生命周期一致 |
| PerpetualCache | 默认开启 | 存放缓存的具体地方,底层为一个hashMap。一级缓存底层也是这个 |
其中包装顺序也是从最外层TransacationCache到最底层的PerpetualCache
- 初始化流程
上述这些包装顺序都是configuraion初始化的时候填充的。
这里就是上述说的比较坑的地方。在解析xml方式的缓存时,也会触发注解方式的解析。最终结果就是同一个mapper。xml与接口是2个namespace
一个Mapper的xml一个cache。所以为什么二级缓存是基于mapper的
可以看到将最后的cache给add到了configuration的cache对象中。这就是为什么二级缓存是全局缓存。
我们可以使用第三方缓存来替换默认的PerpetutalCache。例如redisCache,步骤也很简单。1.继承cache接口,重写相关方法 2.cache标签配置中type配置为redisCache的全路径名
最后走到build方法,这里就会说明上述的顺序。
可以看到他装饰的顺序就是上图中cache的顺序。至于最前面的TransactionalCache是在具体存放的时候进行初始化的。setDefaultImplementaions时,会先初始化最底层的PerpetutalCache->LruCache(或者通过cache标签中的eviction,四选一)
后续就是SchEduledCache->serializedCache->loggingcache->synchronizedCache->blockingCache。与上图顺序一致
-
流程
- 当我们通过sqlsession进行查询时,会首先进入CachingExecutor。会先从二级缓存中查询,如果没有,则走后续查询。最后给放到缓存中。
所谓二级缓存。其实就是一个Key为Cache,value为TransactionalCache的对象。那面试中常见问题:二级缓存命中条件其实就是看我们key由哪些元素组成。
- 当我们通过sqlsession进行查询时,会首先进入CachingExecutor。会先从二级缓存中查询,如果没有,则走后续查询。最后给放到缓存中。
整个结构跟redis的hash结构类似。外层key是cache对象,用于获取TransactionalCache对象,而真正的value是通过TransactionalCache对象的key,也就是一个cacheKey对象。cacheKey里面包含如图的东西,那二级缓存生效条件也就是这里面的字段全部相同,包括:
- namespace(mapper.StudentMapper.getStudentByIdWithClassInfo)
- offset与limit(0,2147483647)
- sql语句
- 查询参数
- 环境信息(开发,测试,生效)
但其实漏了一项最重要的。事务提交/关闭。 二级缓存的value为TransactionalCache那必然跟事务有关系。 可以看到当查询完数据库,把缓存放进去过后仍然取不到。其中的奥妙我们看一下TransactionalCache的三个方法就知道了。
put方法是放进一个entriesToAddOnCommit map中
entriesToAddOnCommit 就类似于一个缓冲区。当我们put的时候数据放进缓冲区。只有当commit后,才将缓冲区的时候放进真正的缓存中而flushPendingEntries方法只有commit调用。而TransactionalCacheManager的close方法。被cacheExecutor的commit与close调用。所以二级缓存命中最重要的一个条件就是事务提交或者关闭。
- 缺陷
- 脏读问题
- 由于二级缓存是基于namespace的。在涉及多表查询时,被关联表无法感应到所属namespace的缓存变化,从而引发脏读。场景描述: 1.studentMapper.selectWithClassInfo。这个连表查询,包含了class信息。查询后放入缓存中 2.classMapper.updateClassInfo 3.studentMapper.selectWithClassInfo再次查询时候,会走到缓存。此时读的就不是最新的数据。
- 解决办法。使用Cache ref标签。让student与class使用同一个命名空间。那这样粒度就变粗了。所有在生产环境中并不推荐使用二级缓存。
- 脏读问题
- 总结
- 二级缓存namespace级别,且为全局缓存。
- 二级缓存生效条件为cacheKey的几个字段完全相同(stantment,offset+limit,sql语句,参数)加上事务提交/关闭
- 二级缓存多表查询时容易产生脏读。解决办法是使用cache ref标签让联表使用同一个namespace
- 二级缓存底层就是一个hashMap.使用装饰者模式,key最外层为BlockingCache。(如果配置,默认配置则为SynchronizedCache)。value最外层为TransactionalCache。底层都是PerpetualCache。
- 生产环境不建议使用。在当今分布式年代,说到底只是一个jvm缓存,当然你可以自己写一个redisCache取代底层的PerpetualCache。那为什么不直接用redis呢?
一级缓存
一级缓存就简单多了。它是基于sqlSession,原因就是它存在于BaseExecutor。而executor是在sqlSession初始化时候new出来的。 代码流程就是从上述cacheingExecuxtor的delegate.query后进入。可以看到以及缓存的key。也为cacheKey。所以他的命中条件与二级缓存类似。不同的是二级缓存要事务提交,一级缓存需要相同的sqlSession。一级缓存是默认开启的。但我们可以配置他的作用域
<setting name="localCacheScope" value="SESSION/STATEMENT"/>
二者的区别就是清理时机不同。
- 清理时机
- 事务提交,关闭
- 除查询语句
- 事务回滚
- 配置flushCache为true
- 当localCacheScope=STATEMENT,每次查询后都会清空
大家不用去死记这些场景。打开BaseExecutor类查看clearLocalCache的调用就可以很清晰的看到。
上述就是mybatis一级缓存,二级缓存的内容。写的比较多,代码流程比较简单,只要知道CachingExecutor是executor的装饰,其中包含二级缓存,而二级缓存使用了装饰者模式来实现它的相关功能,整个代码难度并不大。