[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>
参考: