MyBatis中的缓存相关类都在cache包下面,而且定义了一个顶级接口Cache,默认只有一个实现类PerpetualCache,PerpetualCache中是内部维护了一个HashMap来实现缓存。
在MyBatis中存在两种缓存,即一级缓存和二级缓存。
一级缓存:一级缓存也称本地缓存,session级别的缓存
二级缓存:二级缓存是全局缓存,是一个基于namespace级别的缓存,作用域更为广泛,不局限于一个sqlSession,可以在多个sqlSession之间共享,mybatis的二级缓存默认也是开启的,但是由于它的作用域是namespace,所以还需要在mapper.xml中开启才能生效,只需要加入cache标签即可。
作用顺序为二级缓存>一级缓存>数据库。
一级缓存作用域仅限于同一sqlSession中,无法感知到其他sqlSession中的数据变更,所以极易产生脏数据,而二级缓存可以通过cache-ref让多个mapper.xml共享一个namespace,从而实现缓存共享,但多表联动查询的时候会比较麻烦,由此,在生产环境的时候可以关闭一级缓存(statement级别),有需要的话可以打开二级缓存。如果是分布式应用的话, 这里我们不建议开启
一级缓存
一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个SqlSession中,跨SqlSession是无效的。在同一个 SqlSession中,两次执行相同的 sql 查询,第二次不再从数据库查询。
MyBatis中一级缓存是默认开启的,不需要任何配置。有两个级别的选项:SESSION
或者STATEMENT
,默认是SESSION
级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT
级别,可以理解为缓存只对当前执行的这一个Statement
有效。
配置:
<setting name="localCacheScope" value="SESSION"/>
原理:一级缓存采用 Hashmap 存储,mybatis 执行查询时,从缓存中查询,如果缓存中没有从数据库查询。如果该 SqlSession 执行 clearCache()提交 或者增加 删除 修改操作。
在query
操作时,这个缓存的Hashmap的key生成方法是:
CacheKey cacheKey = new CacheKey();
//MappedStatement的Id
cacheKey.update(ms.getId());
//分页
cacheKey.update(rowBounds.getOffset());
//SQL的limit
cacheKey.update(rowBounds.getLimit());
//SQL本身
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);
即:将MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。
Statement Id + Offset + Limmit + Sql + Params
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
在insert/delete/update
操作时:
SqlSession的insert
方法和delete
方法,都会统一走update
的流程,代码如下所示:
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
而update
方法也是委托给了Executor
执行。BaseExecutor
的执行方法如下所示:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空本地缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
每次执行update前都会清空localCache。
一级缓存失效的四种情况:
- 不同的SqlSession对应不同的一级缓存
- 同一个SqlSession但查询条件不同
- 同一个SqlSession两次查询期间执行了任何一次增删改操作
- 同一个SqlSession两次查询期间手动清空了缓存
总结
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二级缓存配置
要正确的使用二级缓存,需完成如下配置的。
在MyBatis的配置文件中开启二级缓存。
<setting name="cacheEnabled" value="true"/>
在MyBatis的映射XML中配置cache或者 cache-ref 。
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。
<cache/>
type
:cache使用的类型,默认是PerpetualCache
,这在一级缓存中提到过。eviction
: 定义回收的策略,常见的有FIFO,LRU。flushInterval
: 配置一定时间自动刷新缓存,单位是毫秒。size
: 最多缓存对象的个数。readOnly
: 是否只读,若配置可读写,则需要对应的实体类能够序列化。blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。cache-ref
代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="mapper.StudentMapper"/>
总结
- MyBatis的二级缓存相对于一级缓存来说,实现了
SqlSession
之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。 - MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。