由 Mybatis 源碼暢談軟件設計(七):從根上理解 Mybatis 一級緩存
本篇我們來講 一級緩存,重點關注它的實現原理:何時生效、生效范圍和何時失效,在未來設計緩存使用時,提供一些借鑒和參考。
1. 準備工作
定義實體
public class Department {
public Department(String id) {
this.id = id;
}
private String id;
/**
* 部門名稱
*/
private String name;
/**
* 部門電話
*/
private String tel;
/**
* 部門成員
*/
private Set<User> users;
}
public class User {
private String id;
private String name;
private Integer age;
private LocalDateTime birthday;
private Department department;
}
定義 Mapper.xml
DepartmentMapper.xml,兩條 SQL:一條根據 ID 查詢;一條清除緩存,標記了 fulshCache 標簽,將其設置為 true 后,只要語句被調用,都會將本地緩存和二級緩存清空(默認值為 false)
<select id="findById" resultType="Department">
select * from department
where id = #{id}
</select>
<select id="cleanCathe" resultType="int" flushCache="true">
select count(department.id) from department;
</select>
UserMapper.xml,聯表查詢用戶信息:
<select id="findAll" resultMap="userMap">
select u.*, td.id, td.name as department_name
from user u
left join department td
on u.department_id = td.id
</select>
2. 一級緩存
一級緩存的生效范圍 SqlSession 級別的,不同 SqlSession 間不共享緩存,它默認情況下是啟用的。主要作用是減少在同一個查詢 SQL 會話中對數據庫的重復查詢,從而提高性能。以如下用例為例:
public static void main(String[] args) throws IOException {
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 開啟二級緩存需要在同一個SqlSessionFactory下,二級緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級緩存
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
SqlSession sqlSession = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查詢 ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println("----------department一級緩存生效,控制臺看不見SQL ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
}
可以發現在第二次查詢時,一級緩存生效,控制臺沒有出現SQL:

而我們清空下一級緩存再試試:
public static void main(String[] args) throws IOException {
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 開啟二級緩存需要在同一個SqlSessionFactory下,二級緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級緩存
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
SqlSession sqlSession = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查詢 ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println("----------department一級緩存生效,控制臺看不見SQL ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println("----------清除一級緩存 ↓------------");
departmentMapper.cleanCathe();
System.out.println("----------清除后department再一次查詢,SQL再次出現 ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
}
控制臺日志很清晰,清除緩存后又重新查了一遍:

接下來我們看一下不同 SqlSession 間一級緩存是否共享,創建一個新的 SqlSession sqlSession1 執行相同的SQL:
public static void main(String[] args) throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查詢 ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println("----------sqlSession1下department執行相同的SQL,控制臺出現SQL ↓------------");
departmentMapper1.findById("18ec781fbefd727923b0d35740b177ab");
}
如控制臺日志所示,可以發現在不同的 SqlSession 下不共享一級緩存:

3. 一級緩存原理
一級緩存在查詢方法 org.apache.ibatis.executor.BaseExecutor#query 中生效,如下所示:
public abstract class BaseExecutor implements Executor {
// ...
// 一級緩存
protected PerpetualCache localCache;
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.");
}
// 判斷是否刷新本地緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 判斷一級緩存是否存在,存在則直接作為結果返回,否則查詢數據庫
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();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 一級緩存占位
localCache.putObject(key