目錄
1.1 我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。
1.2 同樣是對user表進行兩次查詢,只不過兩次查詢之間進行了一次update操作。
2.1.3 在 mybatis-config.xml中開啓二級緩存
Mybatis 爲我們提供了一級緩存和二級緩存,可以通過下圖來理解:
- 一級緩存是SqlSession級別的緩存。在操作數據庫時需要構造sqlSession對象,在對象中有一個數據結構(HashMap)用於存儲緩存數據。不同的sqlSession之間的緩存數據區域(HashMap)是互相不影響的。
- 二級緩存是mapper級別的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的。二級緩存的底層也是HashMap。(同一個Mapper本質就是同一個命名空間)
1、一級緩存
我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。
1.1 我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。
@Test
public void testSelectOrderAndUserByOrderId(){
//根據 sqlSessionFactory 產生 sqlsession
SqlSession sqlSession = sessionFactory.openSession();
String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查詢,發出sql語句,並將查詢的結果放入緩存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第二次查詢,由於是同一個sqlSession,會在緩存中查找查詢結果
//如果有,則直接從緩存中取出來,不和數據庫進行交互
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制檯打印情況:
1.2 同樣是對user表進行兩次查詢,只不過兩次查詢之間進行了一次update操作。
@Test
public void testSelectOrderAndUserByOrderId(){
//根據 sqlSessionFactory 產生 sqlsession
SqlSession sqlSession = sessionFactory.openSession();
String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查詢,發出sql語句,並將查詢的結果放入緩存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第二步進行了一次更新操作,sqlSession.commit()
u1.setSex("女");
userMapper.updateUserByUserId(u1);
sqlSession.commit();
//第二次查詢,由於是同一個sqlSession.commit(),會清空緩存信息
//則此次查詢也會發出 sql 語句
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
控制檯打印情況:
1.3 一級緩存查詢過程
1、第一次發起查詢用戶id爲1的用戶信息,先去找緩存中是否有id爲1的用戶信息,如果沒有,從數據庫查詢用戶信息。得到用戶信息,將用戶信息存儲到一級緩存中。
2、如果中間sqlSession去執行commit操作(執行插入、更新、刪除),則會清空SqlSession中的一級緩存,這樣做的目的爲了讓緩存中存儲的是最新的信息,避免髒讀。
3、第二次發起查詢用戶id爲1的用戶信息,先去找緩存中是否有id爲1的用戶信息,緩存中有,直接從緩存中獲取用戶信息。
1.4 Mybatis與Spring整合導致一級緩存失效
我們使用Spring整合MyBatis的時候,就會發現我們的一級緩存失效,即如果1.1的代碼是運行在spring項目下的,那麼兩次查詢都會執行sql語句。
這是因爲spring整合Mybatis之後,sqlSession是要交給spring容器去管理的,在每一次執行sql之後,spring會將sqlSession關閉,所以一級緩存進而失效。
1.5 總結
一級緩存:與數據庫同一次會話(sqlSession)期間查詢到的數據會放在本地緩存中,如果以後要獲取相同的數據直接從緩存中獲取,不會再次向數據庫查詢數據
一個SqlSession擁有一個一級緩存,這個一級緩存是一個本地緩存。
myBatis一直開啓一級緩存,不同SqlSession的緩存,數據不可以共享
一級緩存還有失效的情況,一級緩存失效後,第二次查詢相同的語句就還需向數據庫發送sql
一級緩存失效情況:
- sqlSession不同
- 當sqlSession對象相同的時候,查詢的條件不同,原因是第一次查詢時候一級緩存中沒有第二次查詢所需要的數據
- 當sqlSession對象相同,兩次查詢之間進行了插入的操作
- 當sqlSession對象相同,手動清除了一級緩存中的數據
一級緩存的生命週期有多長?
- MyBatis在開啓一個數據庫會話時,會創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象。Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。
- 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,使PerpetualCache對象失效,以致一級緩存將不可用。
- 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用。
- SqlSession中執行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用
2、二級緩存
二級緩存的原理和一級緩存原理一樣,第一次查詢,會將數據放入緩存中,然後第二次查詢則會直接去緩存中取。但是一級緩存是基於 sqlSession 的,而 二級緩存是基於 mapper文件的namespace的,也就是說多個sqlSession可以共享一個mapper中的二級緩存區域,並且如果兩個mapper的namespace相同,即使是兩個mapper,那麼這兩個mapper中執行sql查詢到的數據也將存在相同的二級緩存區域中。
namespace默認就是Mapper的類的全限定名。所以同一個Mapper也就保證了是同一個namespace
2.1 二級緩存的使用
2.1.1 創建一個POJO Bean並序列化
由於二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,所以需要給緩存的對象執行序列化。(如果存儲在內存中的話,實測不序列化也可以的。)
public class Student implements Serializable{
private static final long serialVersionUID = 735655488285535299L;
private String id;
private String name;
private int age;
private Gender gender;
private List<Teacher> teachers;
setters&getters()....;
toString();
}
2.1.2 在映射文件中開啓二級緩存
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yihaomen.mybatis.dao.StudentMapper">
<!--開啓本mapper的namespace下的二級緩存-->
<!--
eviction:代表的是緩存回收策略,目前MyBatis提供以下策略。
(1) LRU,最近最少使用的,一處最長時間不用的對象
(2) FIFO,先進先出,按對象進入緩存的順序來移除他們
(3) SOFT,軟引用,移除基於垃圾回收器狀態和軟引用規則的對象
(4) WEAK,弱引用,更積極的移除基於垃圾收集器狀態和弱引用規則的對象。這裏採用的是LRU,
移除最長時間不用的對形象
flushInterval:刷新間隔時間,單位爲毫秒,這裏配置的是100秒刷新,如果你不配置它,那麼當
SQL被執行的時候纔會去刷新緩存。
size:引用數目,一個正整數,代表緩存最多可以存儲多少個對象,不宜設置過大。設置過大會導致內存溢出。
這裏配置的是1024個對象
readOnly:只讀,意味着緩存數據只能讀取而不能修改,這樣設置的好處是我們可以快速讀取緩存,缺點是我們沒有
辦法修改緩存,他的默認值是false,不允許我們修改
-->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
<resultMap id="studentMap" type="Student">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="gender" column="gender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
</resultMap>
<resultMap id="collectionMap" type="Student" extends="studentMap">
<collection property="teachers" ofType="Teacher">
<id property="id" column="teach_id" />
<result property="name" column="tname"/>
<result property="gender" column="tgender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result property="subject" column="tsubject" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
<result property="degree" column="tdegree" javaType="string" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<select id="selectStudents" resultMap="collectionMap">
SELECT
s.id, s.name, s.gender, t.id teach_id, t.name tname, t.gender tgender, t.subject tsubject, t.degree tdegree
FROM
student s
LEFT JOIN
stu_teach_rel str
ON
s.id = str.stu_id
LEFT JOIN
teacher t
ON
t.id = str.teach_id
</select>
<!--可以通過設置useCache來規定這個sql是否開啓緩存,ture是開啓,false是關閉-->
<select id="selectAllStudents" resultMap="studentMap" useCache="true">
SELECT id, name, age FROM student
</select>
<!--刷新二級緩存
<select id="selectAllStudents" resultMap="studentMap" flushCache="true">
SELECT id, name, age FROM student
</select>
-->
</mapper>
2.1.3 在 mybatis-config.xml中開啓二級緩存
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--這個配置使全局的映射器(二級緩存)啓用或禁用緩存-->
<setting name="cacheEnabled" value="true" />
.....
</settings>
....
</configuration>
2.1.4 測試
① 測試二級緩存和sqlSession 無關
@Test
public void testTwoCache(){
//根據 sqlSessionFactory 產生 session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
String statement = "com.ys.twocache.UserMapper.selectUserByUserId";
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
//第一次查詢,發出sql語句,並將查詢的結果放入緩存中
User u1 = userMapper1.selectUserByUserId(1);
System.out.println(u1);
sqlSession1.close();//第一次查詢完後關閉sqlSession
//第二次查詢,即使sqlSession1已經關閉了,這次查詢依然不發出sql語句
User u2 = userMapper2.selectUserByUserId(1);
System.out.println(u2);
sqlSession2.close();
}
可以看出上面兩個不同的sqlSession,第一個關閉了,第二次查詢依然不發出sql查詢語句。
② 測試執行 commit() 操作,二級緩存數據清空
@Test
public void testTwoCache(){
//根據 sqlSessionFactory 產生 session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
SqlSession sqlSession3 = sessionFactory.openSession();
String statement = "com.ys.twocache.UserMapper.selectUserByUserId";
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);
//第一次查詢,發出sql語句,並將查詢的結果放入緩存中
User u1 = userMapper1.selectUserByUserId(1);
System.out.println(u1);
sqlSession1.close();//第一次查詢完後關閉sqlSession
//執行更新操作,commit()
u1.setUsername("aaa");
userMapper3.updateUserByUserId(u1);
sqlSession3.commit();
//第二次查詢,由於上次更新操作,緩存數據已經清空(防止數據髒讀),這裏必須再次發出sql語句
User u2 = userMapper2.selectUserByUserId(1);
System.out.println(u2);
sqlSession2.close();
}
查看控制檯情況:
2.1.5 使用spring整合Mybatis時使用二級緩存
只需要在Mapper接口類上加一個@CacheNamespace註解就開啓二級緩存了。
@CacheNamespace
public interface demoMapper {
@Select("select * from demo")
public List<Map<String, Object>> list();
}
2.2 二級緩存的缺陷
MyBatis二級緩存使用的在某些場景下會出問題,來看一下爲什麼這麼說。
假設我有一條select語句(開啓了二級緩存):
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;
對於tableA與tableB的操作定義在兩個Mapper中,分別叫做MapperA與MapperB,即它們屬於兩個命名空間,如果此時啓用緩存:
- MapperA中執行上述sql語句查詢這6個字段
- tableB更新了col1與col2兩個字段
- MapperA再次執行上述sql語句查詢這6個字段(前提是沒有執行過任何insert、delete、update操作)
此時問題就來了,即使第(2)步tableB更新了col1與col2兩個字段,第(3)步MapperA走二級緩存查詢到的這6個字段依然是原來的這6個字段的值,因爲我們從CacheKey的3組條件來看:
- <select>標籤所在的Mapper的Namespace+<select>標籤的id屬性
- RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認爲0,limit默認爲Integer.MAX_VALUE
- <select>標籤中定義的sql語句
對於MapperA來說,其中的任何一個條件都沒有變化,自然會將緩存中的原結果返回。
這個問題對於MyBatis的二級緩存來說是一個無解的問題,因此使用MyBatis二級緩存有一個前提:必須保證所有的增刪改查都在同一個命名空間下才行。
最後說一下,MyBatis支持三種類型的二級緩存:
- MyBatis默認的緩存,type爲空,Cache爲PerpetualCache
- 自定義緩存
- 第三方緩存
彙總自:https://www.jianshu.com/p/2be932206c59
https://www.cnblogs.com/xrq730/p/6991655.html