MyBatis二级缓存源码解析
文本主要目的为掌握二级缓存的使用场景、熟悉其执行结构、以及执行过程源码
二级缓存概述
二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。在流程上是先访问二级缓存,在访问一级缓存。
二缓存需求
二级缓存是一个完整的缓存解决方案,那应该包含哪些功能呢?这里我们分为核心功能和非核心功能两类:
存储【核心功能】
即缓存数据库存储在哪里?常用的方案如下:
-
内存:最简单就是在内存当中,不仅实现简单,而且速度快。内存弊端就是不能持久化,且容易有限。
-
硬盘:可以持久化,容量大。但访问速度不如内存,一般会结合内存一起使用。
-
第三方集成:在分布式情况,如果想和其它节点共享缓存,只能第三方软件进行集成。比如Redis.
溢出淘汰【核心功能】
无论哪种存储都必须有一个容易,当容量满的时候就要进行清除,清除的算法即溢出淘汰机制。常见算法如下:
-
FIFO:先进先出
-
LRU:最近最少使用
-
WeakReference: 弱引用,将缓存对象进行弱引用包装,当Java进行gc的时候,不论当前的内存空间是否足够,这个对象都会被回收
-
SoftReference:软件引用,基机与弱引用类似,不同在于只有当空间不足时GC才才回收软引用对象。
其它功能
-
过期清理:指清理存放数据过久的数据
-
线程安全:保证缓存可以被多个线程同时使用
-
写安全:当拿到缓存数据后,可对其进行修改,而不影响原本的缓存数据。通常采取做法是对缓存对象进行深拷贝。
二级缓存责任链设计
这么多的功能,如何才能简单的实现,并保证它的灵活性与扩展性呢?这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:
-
设置缓存
-
获取缓存
-
清除缓存
-
获取缓存数量
然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。
这样设计有以下优点:
-
职责单一:各个节点只负责自己的逻辑,不需要关心其它节点。
-
扩展性强:可根据需要扩展节点、删除节点,还可以调换顺序保证灵活性。
-
松耦合:各节点之间不没强制依赖其它节点。而是通过顶层的Cache接口进行间接依赖。
二级缓存的使用
缓存空间声明
二级默认缓存默认是不开启的,需要为其声明缓存空间才可以使用,通过@CacheNamespace 或 为指定的MappedStatement声明。声明之后该缓存为该Mapper所独有,其它Mapper不能访问。如需要多个Mapper共享一个缓存空间可通过@CacheNamespaceRef 或进行引用同一个缓存空间。@CacheNamespace 详细配置见下表:
配置 | 说明 |
implementation | 指定缓存的存储实现类,默认是用HashMap存储在内存当中 |
eviction | 指定缓存溢出淘汰实现类,默认LRU ,清除最少使用 |
flushInterval | 设置缓存定时全部清空时间,默认不清空。 |
size | 指定缓存容量,超出后就会按eviction指定算法进行淘汰 |
readWrite | true即通过序列化复制,来保证缓存对象是可读写的,默认true |
blocking | 为每个Key的访问添加阻塞锁,防止缓存击穿 |
properties | 为上述组件,配置额外参数,key对应组件中的字段名。 |
|
|
注:Cache中责任链条的组成即通过@CacheNamespace 指导生成。具体逻辑详兔崽子CacheBuilder
缓存其它配置
除@CacheNamespace 还可以通过其它参数来控制二缓存
字段 | 配置域 | 说明 |
cacheEnabled |
| 二级缓存全局开关,默认开启 |
useCache | <select|update|insert|delete> | 指定的statement是否开启,默认开启 |
flushCache | <select|update|insert|delete> | 执行sql前是否清空当前二级缓存空间,update默认true。query默认false |
二级缓存的命中条件
二级缓存的命中场景与一级缓存类似,不同在于二级可以跨会放使用,还有就是二级缓存的更新,必须是在会话提交之后。
为什么要提交之后才能命中缓存?
如上图两个会话在修改同一数据,当会话二修改后,在将其查询出来,假如它实时填充到二级缓存,而会话一就能过缓存获取修改之后的数据,但实质是修改的数据回滚了,并没真正的提交到数据库。
所以为了保证数据一至性,二级缓存必须是会话提交之才会真正填充,包括对缓存的清空,也必须是会话正常提交之后才生效。
二级缓存结构
为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都一个唯一的事物缓存管理 器。所以这里暂存区也可叫做事物缓存。
最后我们通过下图来了解会话、暂存区、二级缓存空间的关系:
二级缓存执行流程
原本会话是通过Executor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截。以嵌入二级缓存相关逻辑。
查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
请注意,这里的查询是实时从缓存空间读取的,而变更,只会记录在暂存区
更新操作update
当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。
如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
提交操作commit
当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。