[MyBatis学习笔记] 三、Mybatis缓存
一、Mybatis缓存简介
以下内容来自mybatis官网对于缓存的介绍:
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行1:
<cache/>
这个简单语句的效果如下:
- 映射语句文件中的所有
select语句的结果将会被缓存。 - 映射语句文件中的所有
insert、update和delete语句会刷新缓存。 - 缓存会使用最近最少使用算法(
LRU,Least Recently Used)算法来清除不需要的缓存。 - 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的
1024个引用。 - 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
二、一级缓存
一级缓存是SqlSession级别的缓存,其底层存储结构是HashMap。
1、简单的源码分析
(1)存储结构
可通过SqlSession#clearCache()方法定位一级缓存的具体存储位置:

上述图示说明一级缓存的存储结构为HashMap。
(2)读操作(查询)工作过程
以下借助Executor#query(MappedStatement, Object, RowBounds, ResultHandler)来查看在查询时一级缓存是如何工作的:
// Executor#query(MappedStatement, Object, RowBounds, ResultHandler)
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
其实现类BaseExecutor的相关实现代码:
// BaseExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
其中,CacheKey由MappedStatement, Object, RowBounds以及BoundSql四个部分构成:
// BaseExecutor#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
......
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
......
}
继续执行query方法:
// BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
......
// 首先从localCache即一级缓存中查询
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);
}
......
}
BaseExecutor#queryFromDatabase()方法:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
......
try {
// 查询数据库,获取结果
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将数据库查询的结果写入缓存
localCache.putObject(key, list);
......
return list;
}
(3)写操作(增删改)工作过程
先说结论:进行增删改操作时,mybatis会刷新一级缓存。
以下以新增操作为例进行简单分析:
首先找到入口SqlSession.insert(String)及其实现类DefaultSqlSession.insert(String),调用情况如下:

接力棒交给了DefaultSqlSession#update(String,Object)(SqlSession中的insert、update、delete操作,最终都会进入此处,执行本方法):
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
// 最终调用的Executor#update方法
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
接下来进入BaseExecutor#update(MappedStatement, Object)方法:
// BaseExecutor
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);
}
由上述示例代码可见,mybatis在执行写入操作(增删改)前,会清空一级缓存。
2、测试代码
SqlSessionFactory sqlSessionFactory;
try (
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(in), StandardCharsets.UTF_8)
) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(isr);
}
try (
SqlSession session = sqlSessionFactory.openSession()
) {
UserMapper userMapper = session.getMapper(UserMapper.class);
User user1 = userMapper.findByIdNameAnn(new User(1, "lucy", null, null));
User user2 = userMapper.findByIdNameAnn(new User(1, "lucy", null, null));
System.out.println(user1 == user2);
}
输出结果为true,说明第二次查询的结果是从缓存中获取的。
三、二级缓存
二级缓存是Mapper级别的缓存。多个SqlSession操作同一个Mapper中的sql语句时,可共用二级缓存。二级缓存的存储介质有多种,其可存储在内存中,也可存储在硬盘中。
1、源码分析
由前文第一部分可知,在*Mapper.xml中添加<cache></cache>标签可开启二级缓存,故可以此处为突破口进行源码分析。
(1)二级缓存的加载过程
以下以SqlSessionFactory#build(Reader, String, Properties)方法作为入口查看映射文件*Mapper.xml是如何加载cache的:
SqlSessionFactoryBuilder:
STEP1:

XMLConfigBuilder:
STEP1:

STEP2:

STEP3:

XMLMapperBuilder:
STEP1(加载当前的Mapper.xml):

STEP2:

1)XMLMapperBuilder#cacheElement方法
STEP3:

MapperBuilderAssistant:
STEP1:

至此,当前Mapper所属的二级缓存加载完成。
2)XMLMapperBuilder#buildStatementFromContext方法

XMLStatementBuilder:


MapperBuilderAssistant#addMappedStatement:

由此可见,同一Mapper.xml中的所有statement共享一个二级缓存。
(2)二级缓存的执行过程
以下将通过SqlSession#selectList()作为入口,来分析二级缓存的执行过程。不过在此之前,我们先探究一下SqlSession是如何被创建的。
1)SqlSesssion中Executor的类型
可通过SqlSessionFactory.openSession()方法来创建SqlSession对象,openSession方法的具体实现(DefaultSqlSessionFactory.openSession())如下:
DefaultSqlSessionFactory:
STEP1:

STEP2(主要查看Executor是如何获取的):

Configuration:

由此可见,当缓存开启(mybatis-config中setting标签的cacheEnabled属性被设置为true——默认为true)时,实际的Executor对象时CachingExecutor。
2)selectList中二级缓存的执行过程
以DefaultSqlSession#selectList(String, Object, RowBounds)方法作为开始。
DefaultSqlSession:
STEP1:

由上一小节的分析可知,cacheEnabled属性被设置为true的情况下,此处的executor对象的具体实现为CachingExecutor类。
CachingExecutor:
query:

由此可见,mybatis中的查询步骤为:二级缓存(CachingExecutor中) -> 一级缓存(BaseExecutor中) -> 数据库
commit:

TransactionalCacheManager:

TransactionalCache:
getObject:

putObject:

commit:

flushPendingEntries:

由上述putObject及commit可见,查询数据库所得的结果,并未立即放入二级缓存中,而是先存入entriesToAddOnCommit集合中,直到commit方法被执行时,才由flushPendingEntries方法将结果写入二级缓存中。
clear:

2、简单的使用
(1)开启二级缓存
在*Mapper.xml中添加<cache></cache>。
或者在*Mapper接口中添加注解:@CacheNamespace。
3、分布式缓存:Mybatis-RedisCache
使用redis作为二级缓存。
需添加依赖:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
参考:
MyBatis缓存详解:一级缓存与二级缓存实战

本文深入探讨了MyBatis的一级和二级缓存机制。一级缓存是SqlSession级别的,使用HashMap存储,查询时首先检查一级缓存,未命中再查询数据库。增删改操作会清空一级缓存。二级缓存是Mapper级别,跨SqlSession共享,基于LRU算法,可配置。二级缓存执行流程涉及CachingExecutor,查询时先查二级缓存,再查一级缓存,最后查询数据库。更新时数据先暂存,提交时写入二级缓存。同时介绍了如何开启和使用二级缓存,以及Mybatis-RedisCache的分布式缓存方案。
343

被折叠的 条评论
为什么被折叠?



