MyBatis缓存机制

MyBatis缓存机制

MyBatis是一个功能强大并且非常轻量的ORM框架,其中缓存机制也是其一大特性。MyBatis的缓存分为一级缓存与二级缓存。Mybatis的一级缓存,指的是SqlSession级别的缓存,默认开启;Mybatis的二级缓存,指的是SqlSessionFactory级别的缓存,需要配置。缓存是针对select来说的。

一级缓存

在应用执行过程中,有可能在一个session中运行多次相同的SQL,如果一直访问数据库则会有很大开销,MyBatis提供了一级缓存的方案优化这部分场景,如果一级缓存命中,则返回,避免了重复访问数据库,提高性能。

myb.jpg

一级缓存是SqlSession级别的缓存。在创建SqlSession的时候,MyBatis会为每个SqlSession创建一个Excutor,每个Executor中有一个LocalCache,当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

需要注意的是,由于一级缓存是SqlSession级别的缓存 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用; 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。

一级缓存的执行过程

bb851700.png

一级缓存的cachekey

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。

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
public class PerpetualCache implements Cache {

private String id;

private Map<Object, Object> cache = new HashMap<Object, Object>();

public PerpetualCache(String id) {
this.id = id;
}

public String getId() {
return id;
}

public int getSize() {
return cache.size();
}

public void putObject(Object key, Object value) {
cache.put(key, value);
}

public Object getObject(Object key) {
return cache.get(key);
}

public Object removeObject(Object key) {
return cache.remove(key);
}

public void clear() {
cache.clear();
}

public ReadWriteLock getReadWriteLock() {
return null;
}

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());
}

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

}

一级缓存是通过维护一个HashMap来实现缓存的,而为了达到key的一一对应,key的决定因素有很多。

  • 传入的statementId (mapper+方法名)
  • 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
  • 这次查询所产生的最终要传递给JDBC Preparedstatement的SQL语句
  • 传递给java.sql.Statement要设置的参数值

key的生成策略:id + offset + limit + sql + param value + environment id,这些值都相同,生成的key就相同

一级缓存读脏数据问题

前面我们知道一级缓存是SqlSession级别的,也说明了一级缓存只在数据库会话内部共享。如果在另一个会话对数据进行修改,那么原本那个会话的缓存是不会刷新的,这样会导致原本会话出现读脏数据的问题。

二级缓存

在一级缓存中,是SqlSession级别的缓存,最大共享范围是SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

myb2.png

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

通过图中可以知道,在开启二级缓存后,查询过程变为 二级缓存 - 一级缓存 - 数据库

二级缓存需要在MyBatis配置文件中手动开启

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

同时在mapper中配置 <cache> 也可以通过 <cache-ref> 将多个mapper共享一个二级缓存。

1
<cache-ref namespace="mapper.StudentMapper"/>

但是开启了二级缓存开关,并不代表查询语句查到的结果都会放置到Cache对象之中,最后还需要在<select>中加上缓存支持的配置useCache="true"

1
<select id="selectById" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">

二级缓存读脏数据问题

二级缓存虽然解决了一级缓存在不同SqlSession中读脏数据的问题,但是在涉及多表的时候同样会出现读脏数据的问题:当对两个不同表操作时,如果涉及不同的namespace,则有可能出现读脏数据的问题。但是可以通过<cache-ref>将两个mapper共享同一个二级缓存,不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

参考资料