前言
🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。
🍊支持作者: 点赞👍、关注💖、留言💌~
在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,我们还可以利用中间件(例如 Redis)来充当缓存。 MyBatis 作为一款优秀的 ORM 框架,也用到了缓存,那么今天咱们就一起来聊一聊 Mybatis 的一级缓存和二级缓存。
Mybatis 的一级缓存
首先我们先来看一张图片👇
我们在开发项目的过程中,如果我们开启了 Mybatis 的 SQL 语句打印,我们就会经常看到这句话:Creating a new SqlSession,其实这就是我们常说的 Mybatis 的一级缓存。
Mybatis 的一级缓存也就是在执行一次 SQL 查询或者 SQL 更新之后,这条 SQL 语句并不会消失,而是被 MyBatis 缓存起来,当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是再次执行SQL命令。一级缓存又被称为 SqlSession 级别的缓存,在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的 SqlSession 之间的缓存数据区(HashMap)是互相不影响的。
在我们的应用系统的运行期间,我们可能在一次数据库会话中,执行多次查询条件相同的 SQL 语句,那么针对此情况,你来设计的话你会如何考虑呢?没错,就是加缓存,MyBatis 也是这样去处理的,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,造成数据库的压力,以提高性能。具体执行过程如下图所示👇
SqlSession 是一个接口,提供了一些 CRUD 的方法,而 SqlSession 的默认实现类是 DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,也就是上图的 Local Cache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。这时候可能有小伙伴要说了:我还在控制台上见到了“Closing non transactional SqlSession ”这句话,那我每次创建的 SqlSession 到最后都被关闭了,那我还缓存个毛线了 😥
事请当然不会像我们想象的那样,我们继续往下看👇
🍊 getSqlSession 源码
🍊 closeSqlSession 源码
我们使用官方的解释来说 closeSqlSession 方法就是:检查作为参数传递的 SqlSession 是否由 Spring TransactionSynchronizationManage 管理。如果不是,则关闭它,否则它只更新引用计数器,并在托管事务结束时让 Spring 调用关闭回调。简单点来说就是“如果我们方法是开启事物的,则当前事物内是获取的同一个 sqlSession,否则每次都是获取不同的 sqlSession”,所以我们也并不需要担心无法获取到对应的缓存。这时候有些小伙伴可能又有疑问了:Mybatis 的一级缓存什么情况下会过期呢?各位稍安勿躁,我们接着往下看👇
我们一开始就说了,Mybatis 的一级缓存是存在 sqlSession 里面的,毫无疑问当 sqlSession 被清空或者关闭的时候缓存就没了(在不开启事物的情况下,每次都会关闭 sqlSession);除此之外,在执行 insert、update、delete 的时候也会清空缓存。我们通过源码可以发现 sqlSession 的 insert 和 delete 方法的本质都是执行的 update 方法 👇
我们再来看看 update 的源码👇
执行到 this.clearLocalCache(); 的时候,缓存就已经被清理掉了,也就是说此时 Mybatis 的一级缓存就过期了🧐
我们说了这么多,相信各位小伙伴也了解到了 MyBatis 一级缓存的相关内容,不过 MyBatis 的一级缓存最大的共享范围就是一个 SqlSession 内部,那么如果多个 SqlSession 需要共享缓存该怎么办呢?没错!这时候就需要 MyBatis 的二级缓存登场了 😎
Mybatis 的二级缓存
如果需要多个 SqlSession 共享缓存,则需要我们开启二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示👇
当二级缓存开启后,同一个命名空间(namespace)所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,我们可以将其理解成一个全局变量。当开启二级缓存后,数据的查询执行流程就变为了:二级缓存 → 一级缓存 → 数据库。关于查询的执行流程,我们可以通过源码加以佐证,在 CachingExecutor 文件下的 query 方法很容易就看到了,如果开启二级缓存那就走二级缓存,否则就走一级缓存,如下图所示👇
Mybatis 的二级缓存不像一级缓存默认就是开启的,我们需要在对应的 Mapper 文件里面加上 cache 标签,手动开启 Mybatis 的二级缓存👇
我们可以看到 cache 标签有多个属性,我们先来一起看一下这些属性都分别代表了什么含义:
- type:指定自定义缓存的全类名(一般我们可以使用该 Mapper 文件的全路径作为 type 值)。
- readOnly:是否只读。true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据,同时 MyBatis 为了加快获取数据的速度,直接就会将数据在缓存中的引用交给用户,虽然速度快变快了,但是安全性却降低了。如果不设置该属性的话,则默认为读写。
- size:缓存存放多少个元素。
- blocking:若缓存中找不到对应的key,是否会一直阻塞(blocking),直到有对应的数据进入缓存。
- flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不刷新。
- eviction: 缓存回收策略,回收策略共有以下四种
LRU:最近最少回收,移除最长时间不被使用的对象(默认值)
FIFO:先进先出,按照缓存进入的顺序来移除它们
SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
WEAK:弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
🍊 解析 cache 标签的 cacheElement 方法源码
不知道各位小伙伴知不知道 Mybatis 的二级缓存应用了什么设计模式呢?其中最明显的就是应用了装饰器模式~
既然是装饰器模式,那肯定不止一两种装饰器😄 Mybatis 的源码中一共提供了多种装饰器,比如LruCache、ScheduledCache、LoggingCache 等等,我们通过类名就大概能猜到他们的作用👇
这里有一点是需要注意的:其实他们并不是 cache 的实现类,真正的实现类只有 PerpetualCache ,红框里面的类都是对 PerpetualCache 的包装。
我们了解了缓存装饰器,我们再来看看设置标准装饰器的源码👇
看完这块代码,心理就是一个字:爽!! 能把装饰器模式用的如此精妙,也真是没谁了。该说不说,只要能把这块源码理解通透,那装饰器模式就真的完全掌握了😉
通过上面的源码,我们知道 Mybatis 的二级缓存默认就是可读可写的缓存,它会用 SynchronizedCache 进行装饰,我们来看来SynchronizedCache 的 putObject 方法👇
这也就是为什么二级缓存的实体一定要实现序列化接口的原因了,当然如果将二级缓存设置为只读的缓存ÿjava基础数据缓存0c;那么也就不需要实现序列化接口了。
最后我们回归实际,在分布式架构盛行的当下,我们该如何选择使用哪种缓存呢?其实答案也很简单:除非对性能要求特别高,否则一级缓存和二级缓存都不建议使用,Mybatis 的一级缓存和二级缓存都是基于本地的,分布式环境下必然会出现脏读。
虽然 Mybatis 的二级缓存可以通过实现 Cache 接口集中管理缓存,避免出现脏读的情况,但是有一定的开发成本,并且在多表查询时,使用不当极有可能会出现脏数据~
小结
本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇
希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/2143.html