《看不懂你打我系列》之 —— 极简mybatis缓存

mybatis版本:3.4.6

题外话

先说点题外话,作为技术工作者,学习框架或技术,我觉得有3个方面需要去思考。

  1. 是什么?

    这个东西是什么?要怎么使用?在什么场景下需要使用?

  2. 为什么?

    为什么有这个东西,它能解决什么问题?

  3. 怎么做到的?

    框架或技术,就是工具。是工具,就有不好使的时候。工具不好使了,怎么办呢?

    修!

    不懂原理的话怎么修?所以,要至少了解它的一些基本原理与思路,了解它是如何起作用的,才能够游刃有余,玩弄它于股掌

昨天花了一天时间温习mybatis,包括它的使用以及源码的大致流程。真心体会到了授课老师所说的 :“源码要是分析到位了,那是停不下来啊,直接怼到底儿”。

昨晚12点本来上床睡了,脑子里还在想mybatis二级缓存的事,想着想着突然觉得一个地方堵住了,不对劲,还睡积极,起来怼。于是乎,手脚麻利的下了床,开了电脑,搞到1点才给搞满意。昨晚做梦好像自己也在IDEA中的各个源码文件中穿梭,给我脑袋整的晕乎乎的,真是醉了。

接下来,归纳总结一下mybatis缓存的那点事,权当个人的学习记录,也顺便练练写作,话不多说,直接开怼。

正餐

mybatis的缓存有2种:一级缓存,二级缓存

按照上述3个方面进行叙述即是

  1. 是什么?

    mybatis缓存,将数据库的查询结果,保存到内存(或者硬盘),以便下次执行相同查询时,不经过数据库,直接从内存中取出结果

  2. 为什么?

    对于重复的查询,它能够提高响应速度,并减轻数据库的访问压力。适用于对响应时间要求高,而数据实时性要求不高的情况下。缓存需要考虑到数据一致性的问题,即缓存中的数据,和数据库中的数据有可能不是一致的,所以缓存一般会设置一个刷新间隔,或者执行某些操作时,会刷新缓存。

  3. 怎么做到的?

    mybatis中的缓存的核心类是 PerpetualCache,其底层是通过一个HashMap来保存数据的。欲知后事如何,请听下面分解。

先来总结一下mybatis的大致运行流程,以及所涉及到的类

运行流程

  1. 利用Xpath语法解析全局配置文件
  2. 解析全局配置,如
    • 是否开启二级缓存
    • 是否开启延迟加载
    • 是否使用插件(plugin)
    • 是否设置类型别名(typeAlias)
    • 数据库连接信息
  3. 解析mapper映射文件,将每个CRUD标签,封装为一个MappedStatement
  4. 所有的信息封装到Configuration对象(这是一个重量级对象)
  5. 将Configuration封装到SqlSessionFactory中
  6. 每次调用SqlSessionFactory开启一个SqlSession
  7. 每个SqlSession,会持有一个私有的Executor对象,以及共享的Configuration对象
  8. 每次操作,选取一个MappedStatement,由Executor执行。包括SQL语句组装,参数解析,返回结果解析等操作

用一张比较粗糙的图来表示就是

关键的类

  • Configuration

    封装了所有的属性

  • MappedStatement

    一个CRUD标签,对应一个MappedStatement

  • Executor体系

    mybatis核心执行器,通过Executor来执行数据库操作

在这里插入图片描述

  • SqlSource体系

    封装CRUD标签的一个SqlSource,用于组装SQL语句

在这里插入图片描述

  • SqlNode体系

    以树状形式,存储动态SQL标签,一个SqlSource拥有一个rootSqlNode,每个SqlNode有一个apply方法,在Executor执行时用于拼接SQL语句,SqlNode的设计用到的是设计模式中的组合模式。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vKLjQS8V-1573208102870)(C:\Users\Vergi\Desktop\sqlNode.png)]

  • StatementHandler

    生成Statement,设置参数,执行查询

  • ParameterHandler

    设置参数时,进行参数解析,类型转换等

  • ResultSetHandler

    获取结果集,并进行结果封装

一级缓存

  • 作用范围:SqlSession(默认)
  • 持有者:BaseExecutor
  • 默认开启(其实是无法关闭的,但可以通过一些方法来使一级缓存失效)
  • 清除缓存:
    • 在同一个SqlSession里执行 增 删 改 操作时(不必提交),会清除一级缓存
    • SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存
    • 对mapper.xml中的某个CRUD标签设置属性flushCache=true (这样会导致该标签的一级缓存和二级缓存都失效)
    • 在全局配置文件中设置 <setting name="localCacheScope" value="STATEMENT"/>,这样会使一级缓存失效,二级缓存不受影响

一级缓存的作用范围仅在同一个SqlSession,它是通过BaseExecutor中的一个localCache属性来实现的,这个localCache其实是一个PerpetualCache类的实例,其内部就是一个普通的HashMap。下面通过一次查询来具体解释

//测试代码
public class SimpleTest {

    private SqlSessionFactory factory;

    @Before
    public void init() throws IOException {
        String resource = "v1/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        factory = new SqlSessionFactoryBuilder().build(inputStream);
    }
    @Test
    public void testFindById(){
        SqlSession sqlSession = factory.openSession();
        User u1 = sqlSession.selectOne("findUserById", 1);
        System.out.println(u1);
        User u2 = sqlSession.selectOne("findUserById", 1);
        System.out.println(u2);
    }
}

先看factory.openSession()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ICAsnMO-1573208102871)(C:\Users\Vergi\Desktop\mybatis-opensession1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QamU72R0-1573208102872)(C:\Users\Vergi\Desktop\mybatis-opensession.png)]

new了一个Executor出来,Configuration中的默认ExecutorType为SIMPLE

//Configuration源码片段
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFaS4xwt-1573208102873)(C:\Users\Vergi\Desktop\mybatis-newExecutor.png)]

而默认情况下,cacheEnabled是开启的

//Configuration源码片段
protected boolean cacheEnabled = true;

最后这个默认生成出来的Executor就是被CachingExecutor所包装的SimpleExecutor(这里是装饰器模式,但我觉得解释为包装更好理解)

放个Debug图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUBzoG0i-1573208102873)(C:\Users\Vergi\Desktop\Executor Debug图.png)]

再放一个Executor类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRrrGTOo-1573208102874)(C:\Users\Vergi\Desktop\Executor类图.png)]

BaseExecutor是一个抽象类,这里的设计运用了模板方法模式。而CachingExecutor只是作为一个装饰者而存在的,它主要的功能是帮助维护二级缓存(二级缓存实际是与MappedStatement关联起来的),真正的执行,还是委托给SimpleExecutor去做的。而一级缓存,维护在BaseExecutor中。

下面执行一次查询,看看Executor做了什么,我们的入口是SqlSession的selectOne方法

User u1 = sqlSession.selectOne("findUserById", 1);

最终来到DefaultSqlSession中的selectList方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5O80tUJ-1573208102876)(C:\Users\Vergi\Desktop\SqlSession#selectList.png)]

进入到CachingExecutor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IeQBZ68z-1573208102876)(C:\Users\Vergi\Desktop\CachingExecutor#query1.png)]

这里的CacheKey是与二级缓存相关的,先留着,后面再说,继续往下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nu3pP1gX-1573208102877)(C:\Users\Vergi\Desktop\CachingExecutor#query2.png)]

SimpleExecutor中的query方法是从BaseExecutor继承来的,并没有重写,所以进入BaseExecutor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hkSHHuh-1573208102878)(C:\Users\Vergi\Desktop\BaseExecutor#query.png)]

下面进入queryFromDatabase,真正从数据库里查数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YWL4iHMu-1573208102878)(C:\Users\Vergi\Desktop\BaseExecutor#queryFromDB.png)]

第一次查询的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vaahSknZ-1573208102880)(C:\Users\Vergi\Desktop\queryResult1.png)]

下面在同一个SqlSession中,执行第二次相同查询

直接进到BaseExecutor中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlTm1qiT-1573208102880)(C:\Users\Vergi\Desktop\BaseExecutor#queryHitCache.png)]

再看看此时localCache,里面是有值的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Awff322a-1573208102881)(C:\Users\Vergi\Desktop\localCacheHit.png)]

看看执行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzBjN3Fy-1573208102881)(C:\Users\Vergi\Desktop\result2.png)]

只执行了一次SQL查询,第二次确实是走了一级缓存

我们再看一下BaseExecutor中的localCache

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfyJg5fg-1573208102882)(C:\Users\Vergi\Desktop\BaseExecutor@LocalCache.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hfBLwPZm-1573208102883)(C:\Users\Vergi\Desktop\PerpetualCache.png)]

一级缓存实际就是这个PerpetualCache,一个带有id的HashMap

而二级缓存的最底层实现,其实也是这个PerpetualCache,只不过二级缓存使用了装饰器模式,一层一层地对PerpetualCache进行了包装,且持有者也不再是BaseExecutor,而是MappedStatement。

下面开始二级缓存,GOGOGO

二级缓存

  • 作用范围:mapper级别(可以跨SqlSession),一个mapper.xml即对应一个二级缓存,每个二级缓存,以mapper文件的namespace作为唯一标识

  • 持有者:MappedStatement

  • 默认关闭(其实全局配置文件中的cacheEnabled默认是true,这个是二级缓存总开关,默认是已经打开了的,然而必须要在mapper.xml中配置<cache/> 标签,才能开启该mapper.xml的二级缓存)

  • 对于SELECT节点对应的MappedStatement,默认是开启二级缓存,对其他节点(INSERT/DELETE/UPDATE),默认是关闭(这是由MappedStatement里的useCache属性控制的)

    贴一张丑陋的图,一个mapper.xml中的每个CRUD标签,都会被封装为MappedStatement,一个mapper.xml只有一个二级缓存,但是这个二级缓存是被MappedStatement(准确的说是SELECT节点)持有的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NELkilOU-1573208102883)(C:\Users\Vergi\Desktop\mybatis二级缓存.png)]

  • 放入二级缓存的数据,默认要实现Serializable接口,因为二级缓存的存储介质除了内存,还可能存在硬盘,所以存储的对象需要可序列化。当然,若把二级缓存的readOnly属性设为true,则对象数据可以不用实现Serializable接口

    • readOnly=true:返回缓存对象的引用。readOnly=true意在告诉用户,从缓存中取出数据后,不要对数据进行修改,而不是保证缓存的只读性(cache中存的是对象引用,取出数据后若执行修改,则会改变真正的对象,另外的用户再从cache中取对象,则会发现对象已经被修改)
    • readOnly=false:通过序列化,返回缓存对象的一份拷贝,速度上会慢一些,但是更加安全。用户取得数据后,执行修改,并不会影响到二级缓存中的对象
  • 执行一次查询后,需要进行提交,该次查询结果才会保存到二级缓存中

  • 存在问题:

    • 由于是每个namespace对应一个二级缓存(一个namespace就是一个mapper.xml),若mapperA.xml中全是对user表的操作,而在另一个mapperB.xml中也有少许对user表的操作,这2个mapper的二级缓存是互相独立的,然而mapperB若对user表执行了增删改,并提交,却不会刷新到mapperA的二级缓存,此时用mapperA去做查询,则可能取到脏数据。所以二级缓存的使用要特别小心,对同一个表的操作,尽量只放在一个mapper.xml中。
    • 细粒度控制不够好。引用一个经典的栗子,若一个电商网站,对商品信息进行缓存,又要求用户每次都能查询到最新的信息,mybatis二级缓存无法实现 “当某个商品信息发生变化,只刷新该缓存中该商品的信息,而不刷新其他商品的信息 ”。因为二级缓存是mapper级别的,一次刷新就会清掉缓存的所有信息。一个可能的解决措施:放弃二级缓存,在业务层改用可控制的缓存。

    mybatis中可以通过<cache-ref/>标签来使得多个mapper.xml共享同一个二级缓存

    也可以通过<cache/> 中的type属性,来使用自定义的缓存实现,或者第三方的缓存(如redis,ehcache,memcache)

先从mybatis解析配置文件时,来分析二级缓存的配置,入口是XMLConfigBuilder的parse方法,我们不再赘述,直接进入到解析mapper.xml的地方

直接进入到XMLMapperBuilder源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L8grhiyB-1573208102884)(C:\Users\Vergi\Desktop\XMLMapperBuilder1.png)]

看看对<cache/>标签的解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgygJ03e-1573208102884)(C:\Users\Vergi\Desktop\XMLMapperBuilder3.png)]

默认使用Perpetual作为缓存实现,并且采用LRU的方式进行替换,readOnly默认也是设为了false,这要求默认情况下,放入二级缓存的对象必须要可序列化

最后调用了MapperBuilderAssistant存储了这个cache(一个mapper文件对应一个builderAssistant,这个builderAssistant里面存了一个mapper的公共信息,如parameterMap,resultMap,mapper的cache)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1j3Ndt06-1573208102884)(C:\Users\Vergi\Desktop\useCache.png)]

我们看一下创建cache的过程,即CacheBuilder的build方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHcM98mA-1573208102885)(C:\Users\Vergi\Desktop\cacheBuilder.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eiXoagc3-1573208102885)(C:\Users\Vergi\Desktop\CacheMultiWrap.png)]

上一张DEBUG图,看看最后的Cache是什么样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gru6dVJr-1573208102886)(C:\Users\Vergi\Desktop\CacheAfterWrapped.png)]

在执行查询,向二级缓存中塞数据时,还会在最外层包裹一个TransactionalCache,它的作用是暂存查询结果,在session提交后再统一将查询结果put到PerpetualCache中

再上一张粗糙的图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ps2Mr5Fb-1573208102886)(C:\Users\Vergi\Desktop\caches.png)]

下面分别在2个SqlSession,进行相同的查询,来看看二级缓存究竟是神魔鬼

	@Test
	public void testLevel2Cache(){
		SqlSession sqlSession = factory.openSession();
		UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
		User userById = userMapper.findUserById(1);
		System.out.println(userById);
		//注意需要提交后才会将结果保存到二级缓存
		//若不提交,则还是会查询2次数据库
		sqlSession.close();

		SqlSession sqlSession2 = factory.openSession();
		userMapper = sqlSession2.getMapper(UserMapper.class);
		User user2 = userMapper.findUserById(1);
		System.out.println(user2);
	}

进行第一次查询,GOGOGO

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Py1Ux0GR-1573208102887)(C:\Users\Vergi\Desktop\Level2Cache.png)]

注意这个CacheKey,它是根据MappedStatement,入参,rowBounds等生成的一个key。会用于将这次查询结果插入到HashMap中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0fdBliZx-1573208102887)(C:\Users\Vergi\Desktop\Level2Cache2.png)]

此时看,从MappedStatement拿到的Cache已经不是null了,是一个SynchronizedCache实例,第一次查,二级缓存中没数据,故委托给SimpleExectutor,查询后,通过tcm.putObject(cache,key,list) 这一句,将查询结果保存到二级缓存(但此时并没有真正刷新到二级缓存中,要提交后才会刷新进去)。我们可以进去看一下这个方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lr2raBgn-1573208102888)(C:\Users\Vergi\Desktop\TransactionCacheManager.png)]

getTransactionalCache这个方法,实际是对cache进行一层包装(装饰器模式 )

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7nQVbc9-1573208102888)(C:\Users\Vergi\Desktop\TransactionalCacheManager2.png)]

我们之前说,二级缓存的最底层,是一个PerpetualCache,其内部就是一个HashMap,而这个TransactionalCache,它自己维护一个Map

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbVxNv3j-1573208102889)(C:\Users\Vergi\Desktop\TransactionalCache.png)]

查询完后,是将查询结果暂存在这个entriesToAddOnCommit里面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MxyokgWm-1573208102889)(C:\Users\Vergi\Desktop\TransactionalCache2.png)]

而从二级缓存中取数据,是取的PerpetualCache中HashMap里的数据。所以未提交时,查询结果仍然在这个entriesToAddOnCommit里,在提交时,才会将entriesToAddOnCommit里的数据,插入到PerpetualCache中HashMap里(通过调用delegate的putObject方法,一层一层地将数据传递下去)

我们可以看看SqlSession.commit

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5TUvc5LP-1573208102890)(C:\Users\Vergi\Desktop\sqlSession#commit.png)]

会调用executor的commit

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q49ldxB2-1573208102890)(C:\Users\Vergi\Desktop\executor#commit.png)]

再调用这个TransactionalCacheManager的commit

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ugTQs1Jm-1573208102890)(C:\Users\Vergi\Desktop\transactionalCacheManager3.png)]

会对该TransactionalCacheManager下的所有TransactionalCache,调用commit

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnCEHVW5-1573208102891)(C:\Users\Vergi\Desktop\transactionalCache3.png)]

这时,才会将暂存的数据刷新到二级缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FfLfhnJ-1573208102891)(C:\Users\Vergi\Desktop\flushEntriesToAdd.png)]

执行第二次查询,GOGOGO

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fPLaSXGj-1573208102892)(C:\Users\Vergi\Desktop\cacheSecond.png)]

可见第二次查询时,从二级缓存中取得了数据

看一下执行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ep9O1AVO-1573208102892)(C:\Users\Vergi\Desktop\result3.png)]

第二次查询命中了二级缓存,两次查询只执行了一次SQL

总结一下:

其实没太多东西,简单来说就是mybatis的Executor体系,以及Cache体系,需要注意一级缓存和二级缓存各自的缺陷。

Executor的设计用到了模板方法模式,装饰器模式

Cache的设计就是典型的装饰器模式

参考链接:

通过源码分析mybatis缓存

关于mybatis二级缓存的readOnly属性

使用redis做mybatis的二级缓存的优点

mybatis一级缓存存在的意义?

mybatis一级缓存踩坑记录

  • 45
    点赞
  • 214
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值