Mybatis 緩存原理
Mybatis 緩存原理
本文來自拉鉤 java 高薪訓練營,如果文章寫的不好,看不懂可以找我要課程視頻,不收費。
- 只願在編程道路上,尋求志同道合的碼友。v:15774135883
1 Mybatis 緩存機制
Mybatis 提供了一級、二級緩存。
- 一級緩存:線程級別的緩存,也稱為
本地緩存
或sqlSession級別的緩存
,一級緩存是默認存在的,同一個會話中,查詢兩次相同的操作就會從緩存中取。- 二級緩存:
全局範圍的緩存
;除了當前sqlSession
能用外,其他的也可以使用。二級緩存默認也是開啟的,只需要在 mapper 文件中寫一個即可實現,二級緩存的實現需要 pojo 實現序列化的接口,否則會出錯
搭建工程
快速搭建一個項目,以便更加深入的了解原理。
- 創建一個普通 maven 項目即可。(就不做演示了)
- 依賴中的 mysql 版本換成你們數據庫對應的版本,我的是 8.0
<dependencies>
<!--mybatis坐標-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--mysql驅動坐標-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
<scope>runtime</scope>
</dependency>
<!--單元測試坐標-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
<!-- Mybatis整合redis做緩存 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<!-- 分頁助手 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>3.7.5</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>
實體類
@Table(name = "user")
public class User implements Serializable {
@Id //對應的是註解id
@GeneratedValue(strategy = GenerationType.IDENTITY) //設置主鍵的生成策略
private Integer id;
private String username;
// get set 省略
}
mapper
public interface IUserMapper {
//更新用戶
@Update("update user set username = #{username} where id = #{id}")
public void updateUser(User user);
//根據id查詢用戶
@Options(useCache = true)
@Select({"select * from user where id = #{id}"})
public User findUserById(Integer id);
}
配置文件 sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--給實體類的全限定類名給別名-->
<typeAliases>
<!--給單獨的實體起別名-->
<!-- <typeAlias type="com.lagou.pojo.User" alias="user"></typeAlias>-->
<!--批量起別名:該包下所有的類的本身的類名:別名還不區分大小寫-->
<package name="com.lagou.pojo"/>
</typeAliases>
<!--environments:運行環境-->
<environments default="development">
<environment id="development">
<!--當前事務交由JDBC進行管理-->
<transactionManager type="JDBC"></transactionManager>
<!--當前使用mybatis提供的連接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///2022_xx_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="317311"/>
</dataSource>
</environment>
</environments>
<!--引入映射配置文件-->
<mappers>
<!-- <mapper class="com.lagou.mapper.IUserMapper"></mapper>-->
<package name="com.lagou.mapper"/>
</mappers>
</configuration>
**輸出日誌: **log4j.properties
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=c:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout
測試類:
public class CacheTest {
private IUserMapper userMapper;
private SqlSession sqlSession;
private SqlSessionFactory sqlSessionFactory;
@Before
public void before() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
userMapper = sqlSession.getMapper(IUserMapper.class);
}
@Test
public void test1(){
// 第一次查詢id為1的用戶 發出sql 查詢的結果,存入緩存中
User user1 = userMapper.findUserById(1);
System.out.println(user1);
//第⼆次查詢,由於是同⼀個sqlSession,會在緩存中查詢結果
//如果有,則直接從緩存中取出來,不和數據庫進⾏交互
User user2 = userMapper.findUserById(1);
System.out.println(user2);
System.out.println(user1==user2);
}
}
查看控制台打印情況:
- 同樣是對 user 表進⾏兩次查詢,只不過兩次查詢之間進⾏了⼀次 update 操作。
@Test
public void test2() {
//第⼀次查詢,發出sql語句,並將查詢的結果放⼊緩存中
User u1 = userMapper.findUserById(1);
System.out.println(u1);
//第⼆步進⾏了⼀次更新操作,sqlSession.commit()
User user = new User();
user.setId(1);
user.setUsername("tom");
userMapper.updateUser(user);
//第⼆次查詢,由於是同⼀個sqlSession.commit(),會清空緩存信息
//則此次查詢也會發出sql語句
User u2 = userMapper.findUserById(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情況:
總結
- 第⼀次發起查詢⽤戶 id 為 1 的⽤戶信息,先去找
緩存中
是否有 id 為 1 的⽤戶信息,如果沒有
,從 數據庫查詢⽤戶信息。得到⽤戶信息,將⽤戶信息存儲到⼀級緩存
中。- 如果中間
sqlSession去執⾏commit操作
(執⾏插⼊、更新、刪除),則會清空
SqlSession 中的 ⼀ 級緩存,這樣做的⽬的為了讓緩存中存儲的是最新的信息,避免臟讀
。- 第⼆次發起查詢⽤戶 id 為 1 的⽤戶信息,先去找緩存中是否有 id 為 1 的⽤戶信息,緩存中有,直 接從 緩存中獲取⽤戶信息
一級緩存源碼探究
⼀級緩存到底是什麼?⼀級緩存什麼時候被創建、⼀級緩存的⼯作流程是怎樣的?帶着如下問題來探究
- ⼤家可以這樣想,上⾯我們⼀直提到⼀級緩存,那麼提到⼀級緩存就繞不開 SqlSession,所以索性我們 就直接從 SqlSession,看看有沒有創建緩存或者與緩存有關的屬性或者⽅法
查看⼀圈,發現上述所有⽅法中,好像只有
clearCache()
和緩存沾點關係,那麼就直接從這個⽅法⼊⼿吧,分析源碼時
,我們要看它(此類)是誰,它的⽗類和⼦類分別⼜是誰,對如上關係了解了,你才會對這個類有更深的認識,分析了⼀圈,你可能會得到如下這個流程圖
- 找到
clearCache()
也就是說,緩存其實就是 本地存放的⼀個 map 對象,每⼀個 SqISession 都會存放⼀個 map 對象的引⽤,那麼這個 cache 是何 時創建的呢?
你覺得最有可能創建緩存的地⽅是哪⾥呢?我覺得是
Executo
r,為什麼這麼認為?因為 Executor 是 執⾏器,⽤來執⾏ SQL 請求,⽽且清除緩存的⽅法也在Executor中執⾏
,所以很可能緩存的創建也很 有可 能在 Executor 中,看了⼀圈發現 Executor 中有⼀個 createCacheKey ⽅法,這個⽅法很像是創 建緩存的 ⽅法啊,跟進去看看,你發現 createCacheKey ⽅法是由 BaseExecutor 執⾏的,代碼如下
CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql語句的所在位置包名+類名+ SQL名稱
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具體的SQL語句
cacheKey.update(boundSql.getSql());
//後⾯是update 了 sql中帶的參數
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
創建緩存 key 會經過⼀系列的 update ⽅法,udate ⽅法由⼀個 CacheKey 這個對象來執⾏的,這個 update ⽅法最終由 updateList 的 list 來把五個值存進去,對照上⾯的代碼和下⾯的圖示,你應該能 理解 這五個值都是什麼了?
這⾥需要注意⼀下最後⼀個值,configuration.getEnvironment().getId()這是什麼,這其實就是 定義在 mybatis-config.xml 中的標籤,⻅如下。
那麼我們回歸正題,那麼創建完緩存之後該⽤在何處呢?總不會憑空創建⼀個緩存不使⽤吧?絕對不會 的,經過我們對⼀級緩存的探究之後,我們發現⼀級緩存更多是⽤於查詢操作,畢竟⼀級緩存也叫做查 詢緩存吧,為什麼叫查詢緩存我們⼀會⼉說。我們先來看⼀下這個緩存到底⽤在哪了,我們跟蹤到
BaseExecutor
的 query ⽅法如下:
2 ⼆級緩存
⼆級緩存的原理和⼀級緩存原理⼀樣,第⼀次查詢,會將數據放⼊緩存中,然後第⼆次查詢則會直接去 緩存中取。但是⼀級緩存是基於 sqlSession 的,⽽⼆級緩存是基於 mapper ⽂件的 namespace 的,也 就 是說多個 sqlSession 可以共享⼀個 mapper 中的⼆級緩存區域,並且如果兩個 mapper 的 namespace 相 同,即使是兩個 mapper,那麼這兩個 mapper 中執⾏ sql 查詢到的數據也將存在相同的⼆級緩存區域 中
如何使⽤⼆級緩存
開啟⼆級緩存 和⼀級緩存默認開啟不⼀樣,⼆級緩存需要我們⼿動開啟 ⾸先在全局配置⽂件 sqlMapConfig.xml ⽂件中加⼊如下代碼:
<!--開啟二級緩存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在 UserMapper.xml ⽂件中開啟 緩存
<!--開啟⼆級緩存-->
<cache></cache>
像我們所使用的是註解開發,沒有 mapper.xml 文件,可以用註解實現開啟二級緩存
開啟了⼆級緩存後,還需要將要
緩存的pojo實現Serializable接⼝
,為了將緩存數據取出執⾏反序列化操作
,因為⼆級緩存數據存儲介質多種多樣,不⼀定只存在內存中,有可能存在硬盤中,如果我們要再取 這個緩存的話,就需要反序列化了。所以 mybatis 中的pojo
都去實現Serializable
接⼝
測試
⼀、測試⼆級緩存和 sqlSession ⽆關
@Test
public void SecondLevelCache() {
//根據 sqlSessionFactory 產⽣ session
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
//第⼀次查詢,發出sql語句,並將查詢的結果放⼊緩存中
User user1 = mapper1.findUserById(1);
sqlSession1.close(); //清空一級緩存
//第⼆次查詢,即使sqlSession1已經關閉了,這次查詢依然不發出sql語句
User user2 = mapper2.findUserById(1);
System.out.println(user1 == user2);
}
可以看出上⾯兩個不同的 sqlSession,第⼀個關閉了,第⼆次查詢依然不發出 sql 查詢語句
⼆、測試執⾏ commit()操作,⼆級緩存數據清空
@Test
public void SecondLevelCache() {
//根據 sqlSessionFactory 產⽣ session
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
//第⼀次查詢,發出sql語句,並將查詢的結果放⼊緩存中
User user1 = mapper1.findUserById(1);
sqlSession1.close(); //清空一級緩存
User user2 = mapper2.findUserById(1);
//執⾏更新操作,commit()
User user = new User();
user.setId(1);
user.setUsername("lisi");
mapper3.updateUser(user);
sqlSession3.commit();
//第⼆次查詢,由於上次更新操作,緩存數據已經清空(防⽌數據臟讀),這⾥必須再次發出sql語
System.out.println(user1 == user2);
}
查看控制台情況:
useCache 和 flushCache
- userCache: 禁⽤⼆級緩存,直接從數 據 庫中獲取
- flushCache=”true」屬性,默認情況下為 true,即刷新緩存,如果改成 false 則 不 會刷新。使⽤緩存時如果⼿動修改數據庫表中的查詢數據會出現臟讀。(一般不設置)
3 ⼆級緩存整合 redis
- 主要解決 分佈式緩存
上⾯我們介紹了 mybatis ⾃帶的⼆級緩存,但是這個緩存是單服務器⼯作,⽆法實現分佈式緩存。 那麼 什麼是分佈式緩存呢?假設現在有兩個服務器 1 和 2,⽤戶訪問的時候訪問了 1 服務器,查詢後的緩 存就 會放在 1 服務器上,假設現在有個⽤戶訪問的是 2 服務器,那麼他在 2 服務器上就⽆法獲取剛剛那個 緩 存,如下圖所示:
為了解決這個問題,就得找⼀個分佈式的緩存,專⻔⽤來存儲緩存數據的,這樣不同的服務器要緩存數 據都往它那⾥存,取緩存數據也從它那⾥取,如下圖所示
如上圖所示,在⼏個不同的服務器之間,我們使⽤第三⽅緩存框架,將緩存都放在這個第三⽅框架中, 然後⽆論有多少台服務器,我們都能從緩存中獲取數據。 這⾥我們介紹
mybatis與redis的整合
。 剛剛提到過,mybatis提供了⼀個cache接⼝,如果要實現⾃⼰的緩存邏輯,實現cache接⼝開發即可
。 mybatis 本身默認實現了⼀個,但是這個緩存的實現⽆法實現分佈式緩存,所以我們要⾃⼰來實現。 redis 分佈式緩存就可以,mybatis提供了⼀個針對cache接⼝的redis實現類,該類存在mybatis-redis包 中 實現
添加依賴:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
修改二級緩存實現 二級緩存
- 下面是 xml 的方式
- 但是我們文章是基於註解的方式,在 mapper 中修改
<cache type="org.mybatis.caches.redis.RedisCache" />
redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
測試:
執行一個沒有清空二級緩存的操作,比如兩次查詢
- 可以看到 已經存儲到redis中了。
源碼分析:
RedisCache和⼤家普遍實現Mybatis的緩存⽅案⼤同⼩異,⽆⾮是實現Cache接⼝,並使⽤jedis操作緩 存;不過該項⽬在設計細節上有⼀些區別;
RedisCache在mybatis啟動的時候,由MyBatis的CacheBuilder創建,創建的⽅式很簡單,就是調⽤ RedisCache的帶有String參數的構造⽅法,即RedisCache(String id);⽽在RedisCache的構造⽅法中, 調⽤了 RedisConfigu rationBuilder 來創建 RedisConfig 對象,並使⽤ RedisConfig 來創建JedisPool。 RedisConfig類繼承了 JedisPoolConfig,並提供了 host,port等屬性的包裝,簡單看⼀下RedisConfig的 屬性
RedisConfig對象是由RedisConfigurationBuilder創建的,簡單看下這個類的主要⽅法:
- 核⼼的⽅法就是parseConfiguration⽅法,該⽅法從classpath中讀取⼀個redis.properties⽂件:
接下來看看Cache中最重要的兩個⽅法:putObject和getObject,通過這兩個⽅法來查看mybatis-redis 儲存數據的格式:
可以很清楚的看到,mybatis-redis在存儲數據的時候,是使⽤的hash結構,把cache的id作為這個hash 的key (cache的id在mybatis中就是mapper的namespace);這個mapper中的查詢緩存數據作為 hash 的field,需要緩存的內容直接使⽤SerializeUtil存儲,SerializeUtil和其他的序列化類差不多,負責 對象 的序列化和反序列化;