抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

MYBATIS缓存模块

mybatis缓存

​ MyBatis作为一个强大的持久层框架,缓存是其必不可少的功能之一,MyBatis中的缓存是两层结构的,分为一级缓存,二级缓存,但本质上市相同的,它们使用的都是Cache接口的实现。

​ MyBatis缓存的实现是基于Map的,从缓存里面读写数据是缓存模块的核心基础功能

​ 除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添加缓存情况策略(fifo、LRU),序列化功能,日志能力和定时清空能力等

​ 附加功能可以以任意的组合附加到核心基础功能之上

装饰者模式

mybatis 缓存模块使用了装饰器模式

​ Mybatis缓存的核心模块就是在缓存中读写数据,但是除了在缓存中读写数据wait,还有其他的附加功能,这些附加功能可以任意的加在缓存这个核心功能上。

​ 加载附加功能可以有很多种方法,动态代理或者继承都可以实现,但是附加功能存在多种组合,用这两种方法,会导致生成大量的子类,所以mybatis选择使用装饰器模式.(灵活性、扩展性)

类结构

Mybatis 数据源的实现主要是在 cache包:

Cache 接口

一级缓存和二级缓存都是通过cache接口来实现的

Cache 接口是缓存模块的核心接口,定义了缓存的基本操作

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
/**
* 缓存核心接口
*/
public interface Cache {

/**
* 获取缓存ID
*/
String getId();

/**
* 设置缓存
*/
void putObject(Object key, Object value);

/**
* 根据Key获取缓存
*/
Object getObject(Object key);

/**
*
* 删除缓存
*/
Object removeObject(Object key);

/**
* 清空缓存
*/
void clear();

/**
* 获取缓存大小
*/
int getSize();

/**
* 获取读写锁
* @return A ReadWriteLock
*/
default ReadWriteLock getReadWriteLock() {
return null;
}

}

缓存实现类PerpetualCache

在缓存模块使用了装饰器模式PerpetualCache在其中扮演ConcreteComponent 角色,使用 HashMap来实现 cache 的相关操作

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 缓存的核心实现类
* 在装饰者模式中扮演了 ConcreteComponent(具体组件) 角色
* 使用HashMao实现了缓存
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
/**
* 缓存的ID
*/
private final String id;
/**
* 缓存的主体对象 hashMap
*/
private Map<Object, Object> cache = new HashMap<>();

/**
* 构造方法
* @param id
*/
public PerpetualCache(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}

/**
* 获取缓存的大小
* @return
*/
@Override
public int getSize() {
return cache.size();
}

/**
* 涉资缓存
* @param key 缓存key
* @param value 缓存的Value
*/
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

/**
* 获取缓存
* @param key The key
* @return
*/
@Override
public Object getObject(Object key) {
return cache.get(key);
}

/**
* 删除缓存
* @param key The key
* @return
*/
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}

/**
* 清空缓存
*/
@Override
public void clear() {
cache.clear();
}

@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}

Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}

@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}

缓存项CacheKey

为什么需要一个复杂的对象表示缓存项的key?通常来说表示一个对象的key可以用一个String对象,为什么不可以吗?

​ 在cache中唯一确定一个缓存项需要使用缓存项的key,Mybatis中因为涉及到动态SQL等多方面因素,其缓存项的key不等仅仅通过一个String表示,所以MyBatis 提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。

​ 怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从CacheKey说起。
我们先看一下CacheKey的数据结构:

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
/**
* 缓存项cacheKey
*
* @author Clinton Begin
*/
public class CacheKey implements Cloneable, Serializable {

private static final long serialVersionUID = 1146682552656046210L;
//定义为Null的缓存key
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
//默认的乘数
private static final int DEFAULT_MULTIPLYER = 37;
//默认的hashCode
private static final int DEFAULT_HASHCODE = 17;
//定义乘数变量
private final int multiplier;
//hashCode
private int hashcode;
//校验码
private long checksum;
//统计
private int count;
//更新列表
private List<Object> updateList;

/**
* 构造方法
*/
public CacheKey() {
//设置hashCode
this.hashcode = DEFAULT_HASHCODE;
//设置乘数
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
//更新列表 为空的list
this.updateList = new ArrayList<>();
}

/**
* 构造方法
*
* @param objects 需要缓存的数据
*/
public CacheKey(Object[] objects) {
//调用空的构造方法初始化缓存key
this();
//更新所有数据
updateAll(objects);
}
}

equals方法

其中最重要的是第41行的updateList这个属性,为什么这么说,因为HashMap的Key是CacheKey,而HashMap的get方法是先判断hashCode,在hashCode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断。

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
/**
* 生成的equals方法
* <p>
* 如果是CacheKey
* 先比较CacheKey的hashCode
* 一样在比较 校验码
* 还一样比较更新次数
* 还是一样比较更新的每个值的hashCode
* 保证每一个比较的一致性
*
* @param object
* @return
*/
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}

final CacheKey cacheKey = (CacheKey) object;
//比较cachekey的hashCode
if (hashcode != cacheKey.hashcode) {
return false;
}
//比较校验码
if (checksum != cacheKey.checksum) {
return false;
}
//比较更新次数
if (count != cacheKey.count) {
return false;
}
//比较每个值的hashCode
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}

进行equals的时候经过了几次比较

  • 对象不为空
  • 对象是CacheKey类型
  • cacheKey的hashcode一致
  • cacheKey的校验码checksum一致
  • cacheKey的更新次数count一致
  • updateList列表中每一个数据的hashCode一致

经过这样负责的比较,保证每一个cacheKey是完全一致才会计算equals一致。

更新方法update

如何生产hashcode

17是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,
那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。
而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。
而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,
并使用常数 31, 33, 37, 39 和 41 作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),
那么这几个数就被作为生成hashCode值得备选乘数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 更新数据
*
* @param object
*/
public void update(Object object) {
//获取hashCode 为null 则hashCode是1 否则是计算出来的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//统计更新次数
count++;
//校验码更新
checksum += baseHashCode;
//计算对象初始的hashCode 防止hash冲突
baseHashCode *= count;
/**
* hashCode更新方法
* newHashCode = oldHashCode*乘数(31,33,37,39,41)中选择一个+ObjectHashCode
* 每次迭代乘以 乘数
*/
hashcode = multiplier * hashcode + baseHashCode;
//将缓存对象添加到更新列表
updateList.add(object);
}

缓存增强

PerpetualCache是基类,其它实现的Cache的类都是对基类的扩
展,也就是装饰来包裹真实的对象。扩展了类的功能,也可以说是附加了一些方法。使得具有很好的灵活性

用于装饰PerpetualCache的标准装饰器共有8个(全部在 org.apache.ibatis.cache.decorators包中):

  • BlockingCache:是阻塞版本的缓存装饰器,它保证只有一个线程到数据库中查找指定key对应的数据。

  • FifoCache:先进先出算法,缓存回收策略

  • LoggingCache:输出缓存命中的日志信息

  • LruCache:最近最少使用算法,缓存回收策略

  • ScheduledCache:调度缓存,负责定时清空缓存

  • SerializedCache:缓存序列化和反序列化存储

  • SoftCache:基于软引用实现的缓存管理策略

  • SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问

  • WeakCache:基于弱引用实现的缓存管理策略

  • TransactionalCache:事务性的缓存

一级缓存和二级缓存

  • 一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中
    (BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以
    一级缓存的生命周期与SqlSession是相同的。
  • 二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配
    置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保
    存在Configuration核心配置对象中

注意

二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis
会根据配置自动追加一系列装饰器。

Cache对象之间的引用顺序

SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache

评论