Mybatis一级缓存和二级缓存

一级缓存

一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSessionDefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
MyBatis的一级缓存指的是在一个Session域内,Session为关闭的时候执行的查询会根据SQL为key被缓存,单独使用MyBatis而不继承Spring,使用原生的MyBatis的SqlSessionFactory来构造sqlSession查询,是可以使用以及缓存的。
当参数不变的时候只进行一次查询,参数变更以后,则需要重新进行查询,而清空缓存以后,参数相同的查询过的SQL也需要重新查询,当执行SQL时两次查询中间发生增删改操作,则SqlSession的缓存清空。
如果集成Spring是没有使用一级缓存。原因是一个sqlSession,但是实际上因为我们的dao继承SqlSessionDaoSupport,而SqlSessionDaoSupport内部sqlSession的实现是使用用动态代理实现的,这个动态代理sqlSessionProxy使用一个模板方法封装select()等操作,每一次select()查询都会自动先执行openSession(),执行完后调用close()方法,相当于生成一个新的session实例,所以我们无需手动的去关闭这个session(),当然也无法使用MyBatis的一级缓存,也就是说MyBatis的一级缓存在Spring中是没有作用的。

官方文档

MyBatis SqlSession provides you with specific methods to handle transactions programmatically. But when using MyBatis-Spring your beans will be injected with a Spring managed SqlSession or a Spring managed mapper. That means that Spring will always handle your transactions.
You cannot call SqlSession.commit(), SqlSession.rollback() or SqlSession.close() over a Spring managed SqlSession. If you try to do so, a UnsupportedOperationException exception will be thrown. Note these methods are not exposed in injected mapper classes.

二级缓存

二级缓存,又叫自定义缓存,实现Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。

1
2
3
4
5
public class Configuration {
// ...
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
// ...
}

每次构建SqlSessionFactory对象时都会创建新的Configuration对象,因此,二级缓存的生命周期与SqlSessionFactory是相同的。在创建每个MapperedStatement对象时,都会根据其所属的namespace名称空间,给其分配Cache缓存对象。
二级缓存同样执行增删查改操作,会清空缓存。
二级缓存就是global caching,它超出session范围之外,可以被所有sqlSession共享,它的实现机制和MySQL的缓存一样,开启它只需要在MyBatis的配置文件开启settings里。

1
<setting name="cacheEnabled" value="true"/>

在相应的Mapper文件里:

1
2
3
4
5
6
7
8
9
<mapper namespace="dao.userdao">
... select statement ...
<!-- Cache 配置 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true" />
</mapper>

需要注意的是global caching的作用域是针对Mapper#Namespace而言的,也就是说只在有在这个Namespace内的查询才能共享这个cache。下面是官方文档的介绍:

It’s important to remember that a cache configuration and the cache instance are bound to the namespace of the SQL Map file. Thus, all statements in the same namespace as the cache are bound by it.

注意

  1. 映射语句文件中的所有select语句将会被缓存。
  2. 映射语句文件中的所有insertupdatedelete语句会刷新缓存。
  3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
  4. 缓存会根据指定的时间间隔来刷新。
  5. 缓存会存储1024个对象。

如果二级缓存想要命中实现,则必须要将上一次sqlSession commit之后才能生效,不然将不会命中。原因:两个不同的session必须提交前面一个session才能缓存生效的,原因是因为MyBatis的缓存会被一个Transactioncache类包装住,所有的cache#putObject全部都会被暂时存到一个map里,等事务提交以后,这个map里的缓存对象才会被真正的cache类执行putObject操作。这么设计的原因是防止事务执行过程中出异常导致回滚,如果getobject后直接put进缓存,万一发生回滚,就很容易导致MyBatis缓存被脏读。

Cache

在MyBatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。如下图。

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

FifoCache

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

LoggingCache

输出缓存命中的日志信息

LruCache

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

ScheduledCache

调度缓存,负责定时清空缓存

SerializedCache

缓存序列化和反序列化存储

SoftCache

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

SynchronizedCache

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

WeakCache

基于弱引用实现的缓存管理策略
另外,还有一个特殊的装饰器TransactionalCache(事务性的缓存)

所有的缓存对象的操作与维护都是由Executor器执行来完成的,一级缓存由BaseExecutor(包含SimpleExecutorReuseExecutorBatchExecutor三个子类)负责维护,二级缓存由CachingExecutor负责维护。因此需要注意的是,配置二级缓存不代表MyBatis就会使用二级缓存,还需要确保在创建SqlSession的过程中,MyBatis创建是CachingExecutor类型的执行器。

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
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;

/**
* 缓存接口
* 给缓存供应商的SPI(Service Provider Interface)
* 一个Cache的实例将为名称空间被创建
* Cache接口的实现类必须有一个具有String类型参数的构造方法,用于接收Cache对象的id,作为其唯一标识
*
* mybatis将以namespace作为id调用这个构造函数创建对象
*
*
*/
public interface Cache {

/**
* 获取缓存对象的唯一标识
* @return
*/
String getId();

/**
* 保存key/value到缓存对象中
* key可以是任何对象,但一般是CacheKey对象
* value是查询结果,为List类型
* @param key
* @param value
*/
void putObject(Object key, Object value);

/**
* 从缓存对象中获取key对应的value
* @param key
* @return
*/
Object getObject(Object key);

/**
* 可选的方法,没有被核心框架调用,移除key对应的value
* @param key
* @return
*/
Object removeObject(Object key);

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

/**
* 获取缓存对象中存储的键/值对的数量
* 可选的方法,没有被框架核心调用
*/
int getSize();

/**
* 获取读写锁
* 可选的方法,从3.2.6起这个方法不再被框架核心调用
* 任何需要的锁,都必须由缓存供应商提供
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}

PerpetualCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
* 永久缓存 Cache接口实现类
* Cache接口只有这唯一一个基础实现,其他实现类全都是装饰模式持有另一个缓存对象
*
*
*/
public class PerpetualCache implements Cache {
// 缓存对象的唯一标识
private String id;
// 对象内部维护的HashMap
private Map<Object, Object> cache = new HashMap<Object, Object>();
....
}

在MyBatis中,PerpetualCache是唯一的Cache接口的基础实现。它内部维护一个HashMap,所有的缓存操作,其实都是对这个HashMap的操作。

缓存装饰器

LruCache最近最少使用的回收策略

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
package org.apache.ibatis.cache.decorators;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;

/**
* 基于最近最少使用算法的回收策略
*
*
*/
public class LruCache implements Cache {

// 代理的缓存对象
private final Cache delegate;
// 此Map的key和value都是要添加的键值对的键
private Map<Object, Object> keyMap;
// 最老的key
private Object eldestKey;

public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}

....

public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;

protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}

@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}

....

/**
* 循环keyList
* @param key
*/
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}

}

LruCache内部维护一个Map

1
private Map<Object, Object> keyMap;

它的实际类型是LinkedHashMap<Object,Object>的匿名子类,子类重写removeEldesEntry(),用于获取在达到容量限制时被删除的key。

LruCache#setSize

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;

protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}

在每次调用setSize(),都会创建一个新的该类型的对象,同时指定其容量大小。第三个参数为true代表Map中的键值对列表要按照访问顺序排序,每次被方位的键值对都会被移动到列表尾部(值为false时按照插入顺序排序)。

LruCache#putObject

1
2
3
4
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}

在每次给代理缓存对象添加完键值对后,都会调用cycleKeyList()进行一次检查。

LruCache#cycleKeyList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /**
* 循环keyList
* @param key
*/
private void cycleKeyList(Object key) {
// 把刚刚给代理缓存对象中添加的key,同时添加到keyMap中
keyMap.put(key, key);
// 如果eldestKey不为null,则代表keyMap内部删除eldestKey这个key
if (eldestKey != null) {
// 同样把代理缓存对象中key为eldestKey的键值对删除即可
delegate.removeObject(eldestKey);
eldestKey = null;
}
}

LruCache把新添加的键值对的键添加到keyMap中,如果发现keyMap内部删除一个key,则同样把代理缓存对象中相同的key删除。
LruCache就是以这种方式实现最近最少访问回收算法的。

ScheduledCache调度缓存装饰器

它的内部维护2个字段,clearIntervallastClear

1
2
3
4
// 调用clear()清空缓存的时间间隔,单位毫秒,默认1小时
protected long clearInterval;
// 最后一次清空缓存的时间,单位毫秒
protected long lastClear;

在对代理的缓存对象进行任何操作之前,都会首先调用clearWhenStale()方法检查当前时间点是否需要清理一次缓存,如果需要则进行清理并返回true,否则返回false

ScheduledCache#clearWhenStale()

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 当缓存过期时调用clear方法进行清空
* @return 如果成功进行清理则返回true,否则返回false
*/
private boolean clearWhenStale() {
// 如果当前时间-最后一次清空时间>指定的时间间隔,则调用clear()进行清空
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}

SerializedCache序列化缓存装饰器

它负责在调用putObject(key,value)方法保存key/value时,把value序列化为字节数组;在调用getObject(key)获取value时,把获取到字节数组再反序列化回来。
使用此装饰器的前提是,所有要缓存的value必须实现Serializable接口,否则会抛出CacheException异常。

SerializedCache#putObject()

1
2
3
4
5
6
7
8
@Override
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}

SerializedCache#getObject()

1
2
3
4
5
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
return object == null ? null : deserialize((byte[]) object);
}

在反序列化时,SerializedCache使用内部定义的类CustomObjectInputStream,此类继承自ObjectInputStream,重写父类的resolveClass(),区别在于解析加载Class对象时使用的ClassLoader类加载器不同。

SerializedCache#CustomObjectInputStream

1
2
3
4
5
6
7
8
9
public static class CustomObjectInputStream extends ObjectInputStream {
public CustomObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
return Resources.classForName(desc.getName());
}
}

LoggingCache日志缓存装饰器

它内部维护2个字段,requests查询次数计数器和hits查询命中次数计数器

1
2
3
4
5
6
7
8
   // 日志记录器
private Log log;
// 代理的缓存对象
private Cache delegate;
// 每次调用getObject(key)查询时,此值+1
protected int requests = 0;
// 每次调用getObject(key)获取到的value不为null时,此值+1
protected int hits = 0;

在每次调用getObject()从缓存对象中查询值时,都会迭代这两个计数器,并且计算实时命中率,打印到日志中。

LoggingCache#getObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
// 打印当前缓存对象的命中率
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}

LoggingCache#getHitRatio()

1
2
3
4
5
6
7
/**
* 计算实时命中率
* @return
*/
private double getHitRatio() {
return (double) hits / (double) requests;
}

SynchronizedCache同步的缓存装饰器

这个装饰器比较简单,只是把所有操作缓存对象的方法上都加synchronized,用来避免多线程并发访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @Override
public synchronized int getSize() {
return delegate.getSize();
}

@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}

@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}

@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}

@Override
public synchronized void clear() {
delegate.clear();
}

FifoCache先进先出缓存回收策略装饰器

它内部维护一个不限容量的LinkedList,名称为keyList,在构造方法中被创建。

1
private LinkedList<Object> keyList;

FifoCache#FifoCache()

1
2
3
4
5
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<Object>();
this.size = 1024;
}

在调用putObject()添加缓存时,会在向代理的缓存对象中添加数据之前,调用cycleKeyList()进行一次验证,如果keyList超过限制长度,则进行回收。

FifoCache#putObject()

1
2
3
4
5
@Override
public void putObject(Object key, Object value) {
cycleKeyList(key);
delegate.putObject(key, value);
}

FifoCache#cycleKeyList()

1
2
3
4
5
6
7
8
9
10
private void cycleKeyList(Object key) {
// 把key添加到keyList中
keyList.addLast(key);
// 如果keyList超长,则移除第一个key,并获取被移除的key
// 之后从代理缓存对象中,删除这个key
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}

SoftCache软引用缓存装饰器

它利用JDK的SoftReference软引用,借助垃圾回收器进行缓存对象的回收。
通常使用的引用方式都是强引用,如:Object obj = new Object();只要引用变量obj!=null,那么Object对象永远不会被垃圾回收器回收。而软引用的引用方式是这样的。

1
SoftReference ref = new SoftReference(new Object());

引用变量ref引用SoftReference对象(这属于强引用),再由SoftReference对象内部引用new Object(这属于软引用)。

SoftCache数据结构

1
2
3
4
5
6
7
8
9
10
// 强引用集合,最近一此查询命中的对象,其引用会被加入此集合的头部
// 集合采取先进先出策略,当长度超出指定size时,删除尾部元素
private final LinkedList<Object> hardLinksToAvoidGarbageCollection;
// 此队列保存被垃圾回收器回收的对象所在的Reference对象
// 垃圾回收器在进行内存回收时,会把Reference对象内的引用变量置为null,同时将Reference对象加入队列中
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
// 代理的缓存对象
private final Cache delegate;
// 强引用集合长度限制,可通过setSize方法设置,默认为256
private int numberOfHardLinks;

SoftCache在写缓存之前,会先调用removeGarbageCollectedItems()删除已经被垃圾回收器回收的key/value,之后想缓存对象中写入SoftEntry类型的对象(定义在SoftCache的内部,是SoftReference类的子类)。

SoftCache#putObject()

1
2
3
4
5
@Override
public void putObject(Object key, Object value) {
removeGarbageCollectedItems();
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}

SoftCache#removeGarbageCollectedItems()

1
2
3
4
5
6
7
8
9
10
/**
* 从缓存对象中删除已经被垃圾回收器回收的value对象对应的key
*/
private void removeGarbageCollectedItems() {
SoftEntry sv;
// 弹出队列中的所有key,依次删除
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}

SoftCache在读缓存时,是直接读取的。这样存在一个问题,缓存的value对象已经被垃圾回收器回收,但是该对象的软引用对象还存在,这种情况下要删除缓存对象中,软引用对象对应的key。另外,每次调用getObject()查询到缓存对象中的value还未被回收时,都会把此对象的引用临时加入强引用集合,这样确保该对象不会被回收。这种机制保证访问频次越搞的value对象,被回收的几率越小。

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
@Override
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);
// 引用变量为null,说明不存在key指定的键值对
if (softReference != null) {
// 键值对存在的情况下,获取软引用变量引用的对象
result = softReference.get();
// 如果引用的value已经被回收,则删除缓存对象中的key
if (result == null) {
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
// 缓存的value对象没有被回收,且这次访问到了,则把此对象引用加入强引用集合
// 使其不会被回收
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}

WeakCache弱引用缓存装饰器

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

TransactionalCache事务性缓存

TransactionalCache比其他Cache对象多出2个方法:commit()rollback()TransactionalCache对象内部存在暂存区,所有对缓存对象的写操作都不会直接作用于缓存对象,而是被保存在暂存区,只有调用TransactionalCache#commit(),所有的更新操作才会真正同步到缓存对象中。(二级缓存先执行commit()才能使用的原因)
这样的话,就会存在一个问题,如果事务被设置为自动提交(autoCommit=true)的话,写操作会更新RDBMS(关系型数据库管理系统),但不会清空缓存对象(因为自动提交不会调用commit()),这样会产生数据库与缓存中数据不一致的情况。如果缓存没有过期失效的机制,那么问题会很严重。

TransactionalCache数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原始的被代理的Cache缓存对象
private Cache delegate;
// 调用commit()方法时是否清空缓存,初始为false
// 如果此值为true,则调用commit时会进行清空缓存的操作
// 只有事务中包含更新操作时,此值才会为true
// 否则只需要覆盖指定key/value的更新即可,(覆盖分为删除和添加两步操作)
private boolean clearOnCommit;
// 在commit时需要进行的添加操作
// 调用putObject方法时添加到这里
private Map<Object, AddEntry> entriesToAddOnCommit;
// 在commit时需要进行的移除操作,
// 调用removeObject时添加到这里
private Map<Object, RemoveEntry> entriesToRemoveOnCommit;

之所以把putObject操作分为删除和添加两步,可能是因为有的缓存的添加逻辑是:如果key已存在,则不允许添加,抛出异常。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×