前言
在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如 Redis 也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存。自然的,作为一款优秀的 ORM 框架,MyBatis 中又岂能少得了缓存!
MyBatis 缓存
MyBatis 中的缓存相关类都在 cache 包下面,而且定义了一个顶级接口 Cache,默认只有一个实现类 PerpetualCache,PerpetualCache 中是内部维护了一个 HashMap 来实现缓存。
需要注意的是 decorators 包下面的所有类也实现了 Cache 接口,那么为什么我还是要说 Cache 只有一个实现类呢?其实看名字就知道了,这个包里面全部是装饰器,也就是说这其实是装饰器模式的一种实现。我们随意打开一个查看
可以看到,最终都是调用了 delegate 来实现,只是将部分功能做了增强,其本身都需要依赖 Cache 的唯一实现类 PerpetualCache(因为装饰器内需要传入 Cache 对象,故而只能传入 PerpetualCache 对象,因为接口是无法直接 new 出来传进去的)。
在 MyBatis 中存在两种缓存,即一级缓存和二级缓存。
Mybatis 的一级缓存
先说 Mybatis 的一级缓存,因为这是如果不手动配置,他是自己默认开启的一级缓存,一级缓存只是相对于同一个 SqlSession 而言,跨 SqlSession 是无效的。参数和 SQL 完全一样的情况下,使用同一个 SqlSession 对象调用一个 Mapper 方法,往往只执行一次 SQL,因为使用 SelSession 第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。
来画个图表示一下一级缓存
那面试官肯定会说,直接从数据库查不就行了,为啥要一级缓存呢?
当使用 MyBatis 开启一次和数据库的会话时, MyBatis 会创建出一个 SqlSession 对象表示一次与数据库之间的信息传递,在执行 SQL 语句的过程中,们可能会反复执行完全相同的查询语句,如果不采取一些措施,每一次查询都会查询一次数据库,而如果在极短的时间内做了很多次相同的查询操作,那么这些查询返回的结果很可能相同。为了减轻数据库的开销,所以 Mybatis 默认开启了一级缓存。
SqlSession 一级缓存的工作流程:
- 对于某个查询,根据 statementId,params,rowBounds 来构建一个 key 值,根据这个 key 值去缓存 Cache 中取出对应的 key 值存储的缓存结果
- 判断从 Cache 中根据特定的 key 值取的数据数据是否为空,即是否命中;
- 如果命中,则直接将缓存结果返回;
- 如果没命中:
- 去数据库中查询数据,得到查询结果;
- 将 key 和查询到的结果分别作为 key,value 对存储到 Cache 中;
- 将查询结果返回;
一级缓存的不足:
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:
- session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
- statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存。
Mybatis 的二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的 MyBatis 使用了二级缓存,并且你的 Mapper 和 select 语句也配置使用了二级缓存,那么在执行 select 查询的时候,MyBatis 会先从二级缓存中取输入,其次才是一级缓存,即 MyBatis 查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
作为一个作用范围更广的缓存,它肯定是在 SqlSession 的外层,否则不可能被多个 SqlSession 共享。而一级缓存是在 SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor 之外创建一个对象。
实际上 MyBatis 用了一个装饰器的类来维护,就是 CachingExecutor。如果启用了二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
Mybatis 的二级缓存一般如果不对他进行设置,他是不会开启的,那怎么能够开启二级缓存呢?
1.MyBatis 配置文件
1 | <settings> |
2.MyBatis 要求返回的 POJO 必须是可序列化的
3.Mapper 的 xml 配置文件中加入 标签
1 | <cache type="org.apache.ibatis.cache.impl.PerpetualCache" |
基本上就是这样。这个简单语句的效果如下:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。可用的清除策略有:
1 | typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); |
- PERPETUAL : 选择 PERPETUAL 来命名缓存,暗示这是一个最底层的缓存,数据一旦存储进来,永不清除.好像这种缓存不怎么受待见。
- FIFO : 先进先出:按对象进入缓存的顺序来移除它们
- LRU : 最近最少使用的:移除最长时间不被使用的对象。
- SOFT : 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK : 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
大家虽然看着 PERPETUAL 排在了第一位,但是它可不是默认的,在 Mybatis 的缓存策略里面,默认的是 LRU 。
PERPETUAL :
源代码如下:
1 | public class PerpetualCache implements Cache { |
看着是不是有点眼熟,它怎么就只是包装了 HashMap ?
既然使用 HashMap,那么必然就会有 Key,那么他们的 Key 是怎么设计的?
CacheKey:
1 | public class CacheKey implements Cloneable, Serializable { |
至于内部如何初始化,如何进行操作,有兴趣的可以去阅读一下源码,导入个源码包,打开自己看一下。
FIFO: 先进先出缓冲淘汰策略
1 | public class FifoCache implements Cache { |
在 FIFO 淘汰策略中使用了 Java 中的 Deque,而 Deque 一种常用的数据结构,可以将队列看做是一种特殊的线性表,该结构遵循的先进先出原则。Java 中,LinkedList 实现了 Queue 接口,因为 LinkedList 进行插入、删除操作效率较高。
看完这个源码的时候,是不是就感觉源码其实也没有那么难看懂,里面都是已经掌握好的知识,只不过中间做了一些操作,进行了一些封装。
LRU : 最近最少使用的缓存策略
需要看的源码则是在 Mybatis 中的源码,
1 | public class LruCache implements Cache { |
SOFT: 基于垃圾回收器状态和软引用规则的对象
1 | public class SoftCache implements Cache { |
WEAK : 基于垃圾收集器状态和弱引用规则的对象
1 | public class WeakCache implements Cache { |
WeakCache 在实现上与 SoftCache 几乎相同,只是把引用对象由 SoftReference 软引用换成了 WeakReference 弱引用。