Mybatis一级缓存

Mybatis一级缓存的配置方式:

1
<setting name="localCacheScope" value="SESSION"/>

value有两个值可选:
session:缓存对一次会话中所有的执行语句有效,也就是SqlSession级别的。
statement:缓存只对当前执行的这一个Statement有效。

BaseExecutor

一级缓存中对缓存的查询和写入是在Executor中完成的,以BaseExecutor为例,查看query方法:

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 abstract class BaseExecutor implements Executor {

private static final Log log = LogFactory.getLog(BaseExecutor.class);

protected Transaction transaction;
protected Executor wrapper;

protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache; //缓存
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;

protected int queryStack;
private boolean closed;

......

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//构建CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);//调用了下面的query方法
}

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//如果queryStack为0或者并且有必要刷新缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();//清空本地缓存
}
List<E> list;
try {
queryStack++;
//从缓存中获取数据,key的类型为CacheKey
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//处理本地缓存输出参数
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {//如果获取结果为空,从数据库中查找
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
//如果是STATEMENT级别的缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清空缓存
clearLocalCache();
}
}
return list;
}
}

  1. 从BaseExecutor的成员变量中,可以看到有一个类型为PerpetualCache变量名为localCache的字段,缓存就是用它来实现的。PerpetualCache类的成员变量也很简单,包含一个id和一个HashMap,缓存数据就存储在HashMap中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class PerpetualCache implements Cache {  

    private final String id;

    private Map<Object, Object> cache = new HashMap<Object, Object>();//使用一个map做存储

    get set方法省略
    ......
    }
  2. 在BaseExecutor的quey方法中,有一个构建CacheKey的语句,既然缓存数据存储在HashMap中,那么数据格式一定是键值对的形式,这个CacheKey就是HashMap中的key,value是数据库返回的数据。

    1
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  3. 第二个query方法中,当执行查询时,首先通过localCache.getObject(key)从缓存中获取数据,如果获取的数据为空,再从数据库中查找。

1
2
//从缓存中获取数据,key的类型为CacheKey  
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
1
2
//如果获取结果为空,从数据库中查找  
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  1. 如果开启了flushcache,将会清空缓存
1
2
3
4
//如果queryStack为0或者并且有必要刷新缓存  
if(queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();//清空本地缓存
}

配置flushcache:

1
2
3
<select id="getStudent" parameterType="String" flushCache="true">    
SQL
</select>
  1. 如果一级缓存的级别为Statement,将会清空缓存,这也是如果设置一级缓存的级别为Statement时缓存只对当前执行的这一个Statement有效的原因:
1
2
3
4
5
//如果是STATEMENT级别的缓存  
if(configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
//清空缓存
clearLocalCache();
}

配置方式:

1
<setting name="localCacheScope" value="STATEMENT"/>

CacheKey如何产生的

  1. 在query方法中,调用了createCacheKey方法生成CacheKey,然后多次调用了cachekey的update方法,将标签的ID、分页信息、SQL语句、参数等信息作为参数传入:
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
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//设置ID,也就是标签所在的Mapper的namespace + 标签的id
cacheKey.update(ms.getId());
//偏移量,Mybatis自带分页类RowBounds中的属性
cacheKey.update(rowBounds.getOffset());
//每次查询大小,同样是Mybatis自带分页类RowBounds中的属性
cacheKey.update(rowBounds.getLimit());
//标签中定义的SQL语句
cacheKey.update(boundSql.getSql());
//获取参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// 处理SQL中的参数
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
  1. 通过源码,看一下CacheKey的update方法,update方法中记录了调用update传入参数的次数、每个传入参数的hashcode之和checksum、以及计算CacheKey的成员变量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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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;//hashcode
private long checksum;//update方法中传入参数的hashcode之和
private int count; //调用update方法向updatelist添加参数的的次数
private List<Object> updateList;//调用update传入的参数会被放到updateList

public CacheKey() {
//初始化
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}

public CacheKey(Object[] objects) {
this();
updateAll(objects);
}

public int getUpdateCount() {
return updateList.size();
}

public void update(Object object) {
//获取参数的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//计数
count++;
//hash值累加
checksum += baseHashCode;
//更新hash,hash=hash*count
baseHashCode *= count;
//计算CacheKey的hashcode
hashcode = multiplier * hashcode + baseHashCode;
//将参数添加到updateList
updateList.add(object);
}
}
  1. CacheKey中的成员变量的作用是什么呢,接下来看一下它的equals方法,CacheKey中重写了equals方法,CacheKey中的成员变量其实就是为了判断两个CacheKey的实例是否相同:

如果满足以下条件,两个CacheKey将判为不相同:

  • 要比较的对象不是CacheKey的实例

  • CacheKey对象中的hashcode不相同、count不相同、checksum不相同(它们之间是或的关系)

  • CacheKey对象的updateList成员变量不相同

总结:

如果Statement Id + Offset + Limmit + Sql + Params 都相同将被认为是相同的SQL,第一次将CacheKey作为HashMap中的key,数据库返回的数据作为value放入到集合中,第二次查询时由于被认为是相同的SQL,HashMap中已经存在该SQL的CacheKey对象,可直接从localCache中获取数据来实现mybatis的一级缓存。

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
@Override
public boolean equals(Object object) {
//如果对象为空
if (this == object) {
return true;
}
//如果不是CacheKey的实例
if (!(object instanceof CacheKey)) {
return false;
}

final CacheKey cacheKey = (CacheKey) object;
//如果hashcode值不相同
if (hashcode != cacheKey.hashcode) {
return false;
}
// 如果checksum不相同
if (checksum != cacheKey.checksum) {
return false;
}
//如果count不相同
if (count != cacheKey.count) {
return false;
}
//对比两个对象的updatelist中的值是否相同
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
//如果有不相同的,返回false
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}

总结:

(1)mybatis的一级缓存是SqlSession级别的,不同的SqlSession不共享缓存;

(2)mybatis一级缓存是通过HashMap实现的,在PerpetualCache中定义,没有容量控制;

(3)分布式环境下使用一级缓存,数据库写操作会引起脏数据问题;

参考

五月的仓颉:【MyBatis源码解析】MyBatis一二级缓存

凯伦:聊聊MyBatis缓存机制