1\. 寫在前頭
這篇文章主要講一級緩存,它的作用範圍和源碼分析
(本來想把一二級緩存合在一起,發現太長了)
2\. 準備工作
2.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;
}
2.2 Mapper.xml檔案中要用的SQL
- DepartmentMapper.xml,兩條SQL,一條根據ID比對,一條清除緩存,注意fulshCache标簽
<select id="findById" resultType="Department">
select * from department
where id = #{id}
</select>
<!-- flushCache 所有namespace 的一級緩存 和 目前namespace 的二級緩存均會清除 預設是false-->
<select id="cleanCathe" resultType="int" flushCache="true">
select count(department.id) from department;
</select>
- UserMapper.xml,簡簡單單的查詢所有的user
<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>
3\. 一級緩存
- 一級緩存是基于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
- 我們清空下一級緩存再試試
xml檔案中flushCache标簽 會清除所有namespace 的一級緩存 和 目前namespace 的二級緩存均會清除 預設是false
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");
}
- 控制台日志很清晰,清除緩存後又重新查了一遍
3.1 一級緩存失效的情況
3.1.1 不同SQLSession下同一條SQL一級緩存不生效
- 建立一個新的sqlSession1執行相同的SQL,發現不同SQLSession下不共享一級緩存
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");
3.1.2 兩次相同查詢SQL間有Insert、Delete、Update語句出現
- 因為Insert、Delete、Update的flushCache标簽 預設為 true ,執行它們時,必然會導緻一級緩存的清空,進而引發之前的一級緩存不能繼續使用的情況(這跟我們上邊清除一級緩存的SQL例子一緻)
3.1.3 調用sqlSession.clearCache()方法
- 這個方***将一級緩存清除,效果是一樣的
3.2 一級緩存源碼:緩存被儲存在了哪裡?
3.2.1 該如何找打它的位置
- Mybatis頂層的緩存是接口Cache,檢視它的實作類
- 發現大部分實作類的包都是decorators(裝飾器),隻有PerpetualCache是Impl,是以我們确定的說,它就是我們要找的緩存實作類,點進去看看,發現隻是組合了HashMap...
public class PerpetualCache implements Cache {
private final String id;
// 看這裡
private final Map<Object, Object> cache = new HashMap<>();
...
}
複制代碼
- 那這個PerpetualCache被放在哪裡呢? 我們想到了一級緩存是基于SQLSession,那我們去DefaultSQLSession,它預設的實作類裡看看
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
...
}
複制代碼
- 發現并沒有哇!DefaultSqlSession還有兩個東西,Configuration是全局的配置,這裡邊兒應該是沒有,那我們隻能再去Executor裡看看了
- 發現它是個接口,實作類有一個CachingExecutor!立馬點進去!
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
...
}
複制代碼
- 發現還是沒有???
- 但是Executor還有一個BaseExecutor,最後一家了,再在沒有關了Idea睡覺了
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// o??!! 不就在這呢嘛,小老弟
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
...
}
- 它來了,原來在這藏着呢呀,行了,這把知道它的位置了,我們直接看SQL執行的時候是怎麼存的,怎麼取的吧!
3.2.2 query()方法
- BaseExecutor的query()方法,看看注釋,很簡單
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();
}
deferredLoads.clear();
// 全局localCacheScope設定為statement,則清空一級緩存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
3.2.3 寫兩條Sql,Debug看一下
System.out.println("----------department第一次查詢 ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
複制代碼
- 哎,很對,第一次果然去資料庫裡查了
- 哎,更對了,第二次果然取得緩存
- 好嘛,真簡單呀
3.3 注意:一級緩存的查詢結果被修改後,竟然...
- 竟然會對之後取出的一級緩存有影響,測試下看看
System.out.println("----------department第一次查詢 ↓------------");
Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
System.out.println(department);
department.setName("方圓把名字改了");
System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
System.out.println(departmentMapper.findById("18ec781fbefd727923b0d35740b177ab"));
- 第一次查詢結果name為null,之後我們修改它的name,第二次查詢取緩存的結果是更改name結果之後的
- 這是因為存放的資料其實是對象的引用,導緻第二次從一級緩存中查詢到的資料,就是我們剛剛改過的資料
3.4 文末
- 一級緩存是基于SQLSession的,不同SQLSession間不共享一級緩存
- 執行Insert、Delete、Update語句會使一級緩存失效
- 一級緩存在底層被存放在了BaseExecutor中,本質上就是個HashMap
- 一級緩存存放的資料其實是對象的引用,若對它進行修改,則之後取出的緩存為修改後的資料