前言

在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如 Redis 也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存。自然的,作为一款优秀的 ORM 框架,MyBatis 中又岂能少得了缓存!

MyBatis 缓存

MyBatis 中的缓存相关类都在 cache 包下面,而且定义了一个顶级接口 Cache,默认只有一个实现类 PerpetualCache,PerpetualCache 中是内部维护了一个 HashMap 来实现缓存。
image.png
需要注意的是 decorators 包下面的所有类也实现了 Cache 接口,那么为什么我还是要说 Cache 只有一个实现类呢?其实看名字就知道了,这个包里面全部是装饰器,也就是说这其实是装饰器模式的一种实现。我们随意打开一个查看
image.png
可以看到,最终都是调用了 delegate 来实现,只是将部分功能做了增强,其本身都需要依赖 Cache 的唯一实现类 PerpetualCache(因为装饰器内需要传入 Cache 对象,故而只能传入 PerpetualCache 对象,因为接口是无法直接 new 出来传进去的)
image.png
在 MyBatis 中存在两种缓存,即一级缓存二级缓存

Mybatis 的一级缓存

先说 Mybatis 的一级缓存,因为这是如果不手动配置,他是自己默认开启的一级缓存,一级缓存只是相对于同一个 SqlSession 而言,跨 SqlSession 是无效的。参数和 SQL 完全一样的情况下,使用同一个 SqlSession 对象调用一个 Mapper 方法,往往只执行一次 SQL,因为使用 SelSession 第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。
来画个图表示一下一级缓存
2021-05-31-15-32-44-989511.png
那面试官肯定会说,直接从数据库查不就行了,为啥要一级缓存呢?
当使用 MyBatis 开启一次和数据库的会话时, MyBatis 会创建出一个 SqlSession 对象表示一次与数据库之间的信息传递,在执行 SQL 语句的过程中,们可能会反复执行完全相同的查询语句,如果不采取一些措施,每一次查询都会查询一次数据库,而如果在极短的时间内做了很多次相同的查询操作,那么这些查询返回的结果很可能相同。为了减轻数据库的开销,所以 Mybatis 默认开启了一级缓存。
SqlSession 一级缓存的工作流程:

  1. 对于某个查询,根据 statementId,params,rowBounds 来构建一个 key 值,根据这个 key 值去缓存 Cache 中取出对应的 key 值存储的缓存结果
  2. 判断从 Cache 中根据特定的 key 值取的数据数据是否为空,即是否命中;
  3. 如果命中,则直接将缓存结果返回;
  4. 如果没命中:
    1. 去数据库中查询数据,得到查询结果;
    2. 将 key 和查询到的结果分别作为 key,value 对存储到 Cache 中;
    3. 将查询结果返回;

一级缓存的不足:
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:

  1. session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
  2. 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 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
image.png

Mybatis 的二级缓存一般如果不对他进行设置,他是不会开启的,那怎么能够开启二级缓存呢?
1.MyBatis 配置文件

1
2
3
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>

2.MyBatis 要求返回的 POJO 必须是可序列化的
3.Mapper 的 xml 配置文件中加入 标签

1
2
3
4
5
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>

基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。可用的清除策略有:

1
2
3
4
5
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
  • PERPETUAL : 选择 PERPETUAL 来命名缓存,暗示这是一个最底层的缓存,数据一旦存储进来,永不清除.好像这种缓存不怎么受待见。
  • FIFO : 先进先出:按对象进入缓存的顺序来移除它们
  • LRU : 最近最少使用的:移除最长时间不被使用的对象。
  • SOFT : 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK : 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

大家虽然看着 PERPETUAL 排在了第一位,但是它可不是默认的,在 Mybatis 的缓存策略里面,默认的是 LRU 。
PERPETUAL :
源代码如下:

1
2
3
4
5
6
7
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
}

看着是不是有点眼熟,它怎么就只是包装了 HashMap ?
既然使用 HashMap,那么必然就会有 Key,那么他们的 Key 是怎么设计的?
CacheKey:

1
2
3
4
5
6
7
8
9
10
11
12
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode; //用于表示CacheKey的哈希码
private long checksum; //总和校验,当出现复合key的时候,分布计算每个key的哈希码,然后求总和
private int count;//当出现复合key的时候,计算key的总个数
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;//当出现复合key的时候,保存每个key
}

至于内部如何初始化,如何进行操作,有兴趣的可以去阅读一下源码,导入个源码包,打开自己看一下。
FIFO: 先进先出缓冲淘汰策略

1
2
3
4
5
6
7
8
9
10
11
12
public class FifoCache implements Cache {

private final Cache delegate; //被装饰的Cache对象
private final Deque<Object> keyList;//用于记录key 进入缓存的先后顺序
private int size;//记录了缓存页的上限,超过该值需要清理缓存(FIFO)

public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
}

在 FIFO 淘汰策略中使用了 Java 中的 Deque,而 Deque 一种常用的数据结构,可以将队列看做是一种特殊的线性表,该结构遵循的先进先出原则。Java 中,LinkedList 实现了 Queue 接口,因为 LinkedList 进行插入、删除操作效率较高。
看完这个源码的时候,是不是就感觉源码其实也没有那么难看懂,里面都是已经掌握好的知识,只不过中间做了一些操作,进行了一些封装。
LRU : 最近最少使用的缓存策略
需要看的源码则是在 Mybatis 中的源码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LruCache implements Cache {

private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;//记录最少被使用的缓存项key

public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);//重新设置缓存的大小,会重置KeyMap 字段 如果到达上限 则更新eldestKey
}
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
// 删除最近未使用的key
cycleKeyList(key);
}
}

SOFT: 基于垃圾回收器状态和软引用规则的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class SoftCache implements Cache {
//在SoftCache 中,最近使用的一部分缓存项不会被GC回收,这就是通过将其value添加到
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//引用队列,用于记录GC回收的缓存项所对应的SoftEntry对象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
//底层被修饰的Cache 对象
private final Cache delegate;
//连接的个数,默认是256
private int numberOfHardLinks;

public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}

public void putObject(Object key, Object value) {
// 清除被GC回收的缓存项
removeGarbageCollectedItems();
// 向缓存中添加缓存项
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
public Object getObject(Object key) {
Object result = null;
// 查找对应的缓存项
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
if (softReference != null) {
result = softReference.get();
// 已经被GC 回收
if (result == null) {
// 从缓存中清除对应的缓存项
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
public void clear() {
synchronized (hardLinksToAvoidGarbageCollection) {
// 清理强引用集合
hardLinksToAvoidGarbageCollection.clear();
}
// 清理被GC回收的缓存项
removeGarbageCollectedItems();
delegate.clear();
}
//其中指向key的引用是强引用,而指向value的引用是弱引用
private static class SoftEntry extends SoftReference<Object> {
private final Object key;

SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}
}

WEAK : 基于垃圾收集器状态和弱引用规则的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WeakCache implements Cache {
private final Deque<Object> hardLinksToAvoidGarbageCollection;
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
private int numberOfHardLinks;

public WeakCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
}

WeakCache 在实现上与 SoftCache 几乎相同,只是把引用对象由 SoftReference 软引用换成了 WeakReference 弱引用。