mybatis源码解析Mybatis 技术干货

阿里P7JAVA大牛带你一起来看MyBatis(四)

2018-01-21  本文已影响218人  5f1df32e8c44

经过前面三章的瞎扯,我们已经能够使用配置文件或注解的方式对数据表中的数据进行CRUD操作。前三节都在我的头条号中,可以进去看前面的

今天我们主要来聊一聊MyBatis中的缓存,一提到缓存,不知道各位脑子里首先会想到什么。比如好像听过“缓存”这个词,熟悉而又陌生的感觉,或许能够想到大学时《计算机组成原理》中的一些概念,又或者会想到hibernate框架中好像也有缓存的概念。总之,可能有人不是很明白“缓存“是啥。

无论大家熟不熟悉,都没关系,就认为缓存是存储东西的地方,至于具体存什么,怎么存,我们暂且先不关心。

那在Mybatis中为何需要缓存?怎么用?意义何在?别着急,我们先来回顾一下Mybatis的查询操作,如下面这段代码

这段代码比较简单,是用配置文件的方式实现根据id查询数据表的信息,然后将其封装到一个User对象中,这个流程可以用下图表示。注意,当执行session.selectOne时,需要去操作数据库,也就是去查询数据表中的对应记录。

那么如果需要再次查询id为"1"的数据,按照我们正常的理解是不是依旧是这个流程?如果按照这样的理解,流程就如下图所示

不过这就带来一个问题,假设id为"1"的这条记录里面的数据没有变化,再次去查询数据库显然不太合适,因为频繁的操作数据库加大数据库服务器的压力,从而影响系统的性能,这就有点像咱们《数据库连接池》那篇文章提到的数据库连接对象Connection的感觉。总之,既然数据表中的记录没有变化,而你又需要用到这条记录,一直去查询数据库不妥。

有问题就得解决,怎么办呢?不妨这样,在第一次查询完id为"1"的记录之后,我们将user1对象保存起来,然后第二次查询,先看下有没有id为"1"的记录对应的对象,如果有,咱们就直接拿过来用,如果没有再去查询数据库,流程如下图所示

写了一段伪代码,但是是可行的。现在问题在于,这个容器是啥?在Java开发中,要想存储对象类型的数据,我们用什么?大家应该很容易想到,集合。没错,用集合,但是对于集合家族中的那么多实现类,咱们选用哪个呢?ArrayList?LinkedList?Set?Map?咱们得有根据,不能瞎用对不对?从需求上来看,要存储User对象,看似哪一种集合都可以,因为都可以存,但别忘了,在第二次进行查询时,如果该集合中已经有了该对象,还需要进行取,而且要准确地取到数据,所以List集合应该要pass,为啥?先不谈是否覆盖,是否有序的问题,要想准确地取到数据,当然是选用key-value的形式,也就是根据key来进行取值,既方便又快捷。这里属于Java基础的部分,我们就不把话题带偏咯,这章主要聊的是“缓存”,要是对于集合部分比较薄弱的同学,可以关注下我们的《Java快速入门》课程。那到底选用什么呢?看来只有HashMap最合适咯,那咱们就用HashMap来存储。此时流程中的伪代码可以变换一下,如下图所示

到这里,我们需要关注的是key的值怎么生成,因为当第二次查询时,需要根据这个key来找map集合中是否存在对应的对象,而且该key要唯一,实际上有很多生成策略,无非是经过一些值的拼接和转换,比如根据uuid生成一串值然后加上sql语句等等,总之保证该key唯一即可。

OK,这时候有人会想,那是不是我们还得创建HashMap对象进行对象的保存?肯定不用啦,想一想这种繁琐又常用的操作,当然交给MyBatis来做,当然,MyBatis可以支持缓存,但是不是专门做缓存的,这点咱们有个印象。

上述把User对象保存到HashMap中供后面重复查询,这个过程就是Mybatis中“缓存”的基本原理,也就是在我们查询数据之后,为了避免查询重复的记录,需要将查询的结果保存起来,用什么保存呢?用HashMap对象。

不过在MyBatis中分为一级缓存和二级缓存,一级缓存也称为session级缓存,二级缓存也称为mapper级缓存,说白了,也就是作用域不同,啥意思?意思就是保存范围不一样,一级范围稍微小点,二级范围稍微大点,具体我们还是以一个小demo为例,感受一下一级缓存和二级缓存。这里我们用配置文件的方式进行演示,因为有前面三章的学习,有些过程比较简单,我们就不作过多说明咯,直接文字+截图。

要朝这java程序员发展或者真心有兴趣的。可以找我要一些java的学习视频Java学习交流群:450936584,这个是免费的,希望同学找我要的时候不要有理所应当的态度,毕竟都是我的心血,希望你是真的有一颗想要学好java的心,我也会尽所能的去帮助你成为一名优秀的程序员。

1.基础工作的准备

1)创建Java项目导入相关jar包

2)数据库依然用原来的

3)创建数据表t_users对应的实体类User

4)新建SqlMapConfig.xml文件并写好基本的配置信息

5)新建userMapper.xml文件,并把crud标签都写完

6)将userMapper.xml文件交给SqlMapConfig.xml核心配置文件管理

7)准备好MyBatisUtils工具类

8)编写测试类TestCache查询id为1的记录,这里我们将User对象的hashCode也打印出来

9)运行结果

2.一级缓存(sqlSession级缓存)

1)初识一级缓存

前面基础的工作已经搭建完成,下面我们就来看Mybatis中的“一级缓存”,也称为“sqlSession级缓存”,也就是该缓存的作用范围是sqlSession,具体怎么理解,我们一会再说,先用起来看看,那怎么用呢?一级缓存默认是开启的,也就是不用做额外的配置,直接用就行,那咱们来模拟一个重复查询的场景

需求:在同一个sqlSession下,两次查询id为1的记录,分别得到对应的User对象并打印出hashCode值

说明:考虑到有人可能没用过log4j,这里就不用日志去查看执行过程咯,暂且用hashCode的方式比较两个对象是否相等,实际上hashCode也可以重写,也不能一定说明两个对象是否相等,但是这个案例中是没问题的,咱们为了演示效果。

若hashCode值不一样,说明两次都查询了数据库得到了不同的User对象;如果hashCode值一样,则说明第二次直接用的第一次查询到的缓存结果

结论:两次的hashCode值一样,说明第二次没有向数据库发送查询请求,而是直接拿到的缓存中的数据,其实开启日志功能可以看得更明显,第一次查询会向数据库发送sql语句,而第二次却不会。由于考虑到不是每位同学都接触过log4j,这里就不用log4j演示咯。

流程:为了更加清晰地了解这个过程,我们用一幅图来加深一下理解

说明:之所以说一级缓存又称为sqlSession级缓存,是因为当sqlSession进行commit,clearCache,close等操作时,缓存中的数据会被清空

2)当sqlSession关闭时

从下图中可以看出,sqlSession被close后,直接会报错,这点很容易理解吧?资源都被关了,还用个屁 ,还谈什么缓存,恰好也印证了一级缓存是sqlSession级缓存

此时有同学会想,既然sqlSession关闭了,那就再创建一个呗,行,咱们创建一个,再运行看看

运行是不报错了,但是得到的是不同的hashCode,说明两次查询都向数据库发送了sql语句,并没有从缓存中拿,很明显,两个不同的sqlSession分别拥有各自的HashMap缓存区,更加说明了一级缓存是sqlSession级别的。那如果用的是不同的sqlSession,又想共用同一块缓存区,怎么办呢?这就涉及到二级缓存,之前我们说二级缓存的范围更大,也就是这个含义,二级缓存我们一会再看。

3)当sqlSession进行clear时

可以发现,clearCache之后,两次拿到的是不同的hashCode,说明什么?说明两次都向数据库发送了sql语句,都已经clear缓存了,第二次查询的时候肯定拿不到数据咯。

4)当sqlSession进行commit提交时

可以看出来,进行update操作commit操作后,也是两次查询都向数据库发送了sql语句,这点很容易想通,因为数据表中id为1的记录数据值都已经发生了变化,缓存中的数据肯定要清空,不然就与数据表中的数据不一致了,所以要重新向数据表中拿数据,然后还是存储到一级缓存中。

3.二级缓存(mapper级缓存)

前面我们看得出来,当有两个不同的sqlSession时,显然一级缓存是有局限性的,这时候需要用到二级缓存,也就是说sqlSession缓存范围嫌小了,需要更大的范围来存储。这时候我们不禁会想,那二级缓存用的存储容器是什么呢?同样,也是HashMap。

1)初识二级缓存

一级缓存是默认开启的,无需我们做什么配置。但是二级缓存不同,需要我们手动开启才可以使用,它的开启分两个步骤

a.在SqlMapConfig文件中开启二级缓存

b.在userMapper文件中开启二级缓存

第一步实际上是可以省略的,因为默认是开启的状态,怎么做呢?直接把这段话复制到SqlMapConfig中,当然不写也没事,默认是开启的状态

第二步,只需要加上在userMapper文件中加上一句即可,就这样?没错,就这样,但是这个标签中有一些属性,主要用于配置缓存的大小,刷新的间隔等等,具体用到时候咱们再去配置

接下来就是在代码中使用咯,怎么测试二级缓存呢?是不是看两次查询到的User对象是否一致,看hashCode?注意,这里就不能用hashCode作为演示了,因为无论是否拿的是缓存,两次的hashCode都不会一样,因为User对象经历了序列化和反序列化的操作。不信?不信我们来看看运行结果。之前测试一级缓存的时候必须是在同一个sqlSession对象中,如果是两个sqlSession则无效,那么二级缓存咱们就用两个sqlSession测试,很好理解吧?

可以发现,最后hashCode不一致,那是否说明两次都向数据库发送了sql语句?显然不是,那怎样证明呢?最好的方式是用日志追踪看下,但是还是考虑到大家不一定都接触过log4j,我们怎么做?其实可以这样,在第二次查询的时候打个断点,断点调试大家应该都接触过吧?然后进入Debug模式,如下图所示

从上图可以看出,进入Debug模式后,等待执行下一步,此时注意看下username的值为es9d,现在我们将数据表中的该值改为“es9d1”,然后继续执行,

可以看出,第二次查询拿到了修改后的数据值,说明什么?说明第二次查询也是向数据库发出了sql语句查询,即没有拿二级缓存中的数据,这不恰好说明了hashCode值不一样,所以都是向数据库发送了sql语句吗?的确两次都向数据库发送了sql语句,但是这不是从hashCode值不一样得出来的,主要问题在下面。

问题:的确是两个sqlSession,两次查询也都使用的两个sqlSession,要是这种情况割到一级缓存那边好理解,因为一级缓存只作用于sqlSession范围,但是我们已经开启了二级缓存,也就是说两个不同的sqlSession也应该可以共用更大的HashMap缓存的。再次检查一下是不是二级缓存没开启?发现没有问题啊。

问题的解决:既然没问题,那就说明还有细节没注意到

a.User类需要实现序列化接口,为啥?想一想,二级缓存的范围更大,不局限于sqlSession,也就是说User对象存储的HashMap介质可能会有很多,比如硬盘,网盘,这就难免需要进行对象的传输,如果不序列化,就无法进行反序列化,对于序列化和反序列化不清楚的同学可以找一下相关的资料

b.当进行了User user1 = session1.selectOne(statement, 1);第一次查询之后,一级缓存会起作用,将对象存储到一级缓存的HashMap中,那二级缓存呢?咱们得告诉二级缓存生效,也将查询出的结果对象存储到二级缓存的HashMap中对不?之前只是开启了二级缓存,现在要告诉人家你要存东西了,那怎么告诉呢?很简单,在第一次查询之后,执行一次commit或者关闭sqlSession1即可,如下图所示

此时,我们将上述比较生硬的验证办法再来一遍,在第二次查询那边打个断点,然后进入Debug模式,先看下第一次查询到的值

修改数据表中的username值为"es9d1"

然后继续执行第二次查询,查看结果

可以看出来,两次结果是一致的,说明第二次查询拿到的是缓存数据,这时候咱们看下后面的hashCode值,发现不相等,如果按照一级缓存那边的理论,则说明两个对象不相等,即第二次查询也是向数据库发出了sql语句,显然不成立。还是强调一下,其实这里用日志查看sql语句的发出会更加明显,有兴趣的同学可以用log4j自己观察一下。

2)二级缓存中的数据何时会没了?

之前我们说对于一级缓存而言,sqlSession的close,commit,clearCache都会清空缓存中的数据,那么二级缓存中的数据何时会被清空呢?对于sqlSession的close和clearCache显然不会影响到二级缓存的数据,因为二级缓存的作用域是Mapper级别的,也就是说Mapper中的insert,delete和update操作都会清空二级缓存中的数据。

为了演示,我们再来创建一个sqlSession,然后用其对数据进行update操作。

这时候,二级缓存中的数据会被清空,大家可以自己运行看下

4.补充

其实一般我们不用Mybatis自己的二级缓存,而是用第三方二级缓存技术,比如ehcache,用法也很简单,导入ehcache相关jar包,然后在userMapper.xml文件的标签中进行一下配置即可。标签中还有其他属性,比如flushInterval间隔多久刷新缓存,size最多存储多少个对象等等,大家在开发中可以根据实际场景进行相应的选择,也欢迎与我们一起沟通与探讨。

上一篇下一篇

猜你喜欢

热点阅读