Mybatis是一款優秀的持久層架構,具有較高的SQL靈活性,支援進階映射(一對一,一對多),動态SQL,延遲加載和緩存等特性。他避免了幾乎所有的JDBC代碼和手動設定參數以及擷取結果集,可以使用簡單的XML或注解來配置和映射原生類型。為了減少和資料庫的互動次數,減少系統開銷,以及提高系統效率,Mybatis提供了緩存機制,接下來我們聊一聊Mybatis的緩存機制。
一、緩存
首先我們先說一下緩存,緩存是将一些臨時資料存儲到我們的記憶體中。由于我們伺服器的記憶體是有限的,是以說可以存儲一些資料量小的資料,不可能将所有的資料都放到伺服器的記憶體中。緩存的速率較快,使用友善。
那為什麼會使用緩存呢?我們開發的系統,大部分的功能需求都是對資料庫的增删改查,其中查詢的功能占據多數。當我們使用者想要進行查詢資料時,需要每次都要通路資料庫,他會加大對資料庫的壓力,使其速率降低。是以,我們的緩存就孕育而生了,這樣我們隻查詢資料庫一次,就會将資料加載到伺服器的記憶體中,以後再次查詢時,直接通路伺服器的記憶體,不需要每次都通路資料庫,不僅僅減少了資料庫的壓力,還提高了查詢速率。
二、Mybatis一級緩存
首先介紹一些Mybatis的緩存機制,Mybatis 包含一個非常強大的查詢緩存特性,它可以非常友善地配置和定制。Mybatis系統中預設定義了兩級緩存:一級緩存、二級緩存。預設情況下,隻有一級緩存(SqlSession級别的緩存,也稱為本地緩存)開啟。二級緩存需要手動開啟和配置,他是基于namespace級别的緩存。
概念
Mybatis一級緩存,首先他是預設開啟的,他是一個SqlSession裡面的所有查詢操作都會儲存到緩存中,但是要注意一點,如果在同一個SqlSession會話中,兩個的查詢中間有了 insert 、update或delete的操作,那麼之前的查詢緩存就會被清空。
圖檔來源于網路,侵删
緩存原理
第一次擷取資料時,先從資料庫中查詢指定資料。将資料緩存到一級緩存中【Map結構,key:hashCode+查詢的SqlId+編寫的sql查詢語句+參數】。以後在同一個SqlSession中,擷取相同資料時,會先從一級緩存中擷取資料,如一級緩存中沒有指定資料,再從資料庫中擷取。
不同條件下測試情況
首先我們要準備一下相關的環境:
定義EmployeeMapper接口:
public interface EmployeeMapper {
/**
* 按照動态條件擷取員工資訊【if&where】
*/
public List<Employee> selectEmpByEmp(Employee employee);
}
定義EmployeeMapper.xml的映射檔案:
<?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="ssm.mapper.EmployeeMapper">
<sql id="select_employee">
SELECT
id,
last_name,
email,
salary,
dept_id
FROM
tbl_employee
</sql>
<select id="selectEmpByEmp" resultType="employee">
<include refid="select_employee"></include>
<where>
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null">
AND last_name = #{lastName}
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="salary != null">
AND salary = #{salary}
</if>
</where>
</select>
</mapper>
編寫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>
<properties resource="db.properties"></properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 開啟延遲加載 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 設定加載的資料是按需加載-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 開啟二級緩存-->
<!-- <setting name="cacheEnabled" value="true"/>-->
</settings>
<typeAliases>
<package name="ssm.pojo"/>
</typeAliases>
<environments default="development">
<!-- 設定連接配接資料庫環境-->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 設定映射檔案路徑-->
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
在測試類中提供擷取我們的sqlSessionFactory的方法:
public SqlSessionFactory getSqlSessionFactory(){
SqlSessionFactory sqlSessionFactory = null;
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
return sqlSessionFactory;
}
接下來我們測試一下不同條件下的Mybatis的一級緩存:
1、在相同的SqlSession會話中重複兩次相同的條件查詢操作:
public void testCache1(){
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee empParam = new Employee();
empParam.setId(2);
empParam.setLastName("chengcheng2");
List<Employee> empList = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList) {
System.out.println("employee1 = " + employee1);
}
// sqlSession.close();
System.out.println("======================================");
List<Employee> empList2 = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList2) {
System.out.println("employee1 = " + employee1);
}
}
結果顯示:
DEBUG 11-21 15:48:28,077 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 15:48:28,100 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 15:48:28,121 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
======================================
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
結果顯而易見,當在同一個SqlSession中重複兩次相同的查詢操作,隻有第一次進行通路資料庫,第二次通路直接調用緩存資料。
2、當相同的SqlSession中兩次查詢操作間執行了任意一次增删改操作:
public void testCache2(){
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee empParam = new Employee();
empParam.setId(2);
empParam.setLastName("chengcheng2");
List<Employee> empList = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList) {
System.out.println("employee1 = " + employee1);
}
System.out.println("======================================");
//同一個SqlSession兩次查詢期間執行了任何一次增删改操作
mapper.updateEmpByEmp(empParam);
System.out.println("======================================");
List<Employee> empList2 = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList2) {
System.out.println("employee1 = " + employee1);
}
}
輸出結果顯示:
DEBUG 11-21 16:14:50,658 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,681 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,703 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
======================================
DEBUG 11-21 16:14:50,705 ==> Preparing: update tbl_employee SET last_name = ? where id = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,705 ==> Parameters: chengcheng2(String), 2(Integer) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,707 <== Updates: 1 (BaseJdbcLogger.java:137)
======================================
DEBUG 11-21 16:14:50,707 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,708 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:14:50,709 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
結論:當在相同的SqlSession中進行查詢操作後執行了任意一次增删改操作,再次查詢還需要重新通路資料庫。
3、當相同的SqlSession中兩次查詢操作間進行了事務的送出操作:
public void testCache3(){
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee empParam = new Employee();
empParam.setId(2);
empParam.setLastName("chengcheng2");
List<Employee> empList = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList) {
System.out.println("employee1 = " + employee1);
}
// 同一個SqlSession兩次查詢期間送出了事務
sqlSession.commit();
System.out.println("======================================");
List<Employee> empList2 = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList2) {
System.out.println("employee1 = " + employee1);
}
}
輸出結果顯示:
DEBUG 11-21 16:23:09,349 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:23:09,378 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:23:09,405 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
======================================
DEBUG 11-21 16:23:09,407 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:23:09,408 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:23:09,409 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
結論:當在相同的SqlSession中進行查詢操作後執行了事務的送出,再次查詢還需要重新通路資料庫。
4、當在不同的SqlSession中進行兩次相同的條件查詢操作:
public void testCache4(){
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee empParam = new Employee();
empParam.setId(2);
empParam.setLastName("chengcheng2");
List<Employee> empList = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList) {
System.out.println("employee1 = " + employee1);
}
System.out.println("======================================");
//不同的SqlSession對應不同的一級緩存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmployeeMapper mapper2 = sqlSession2.getMapper(EmployeeMapper.class);
List<Employee> empList2 = mapper2.selectEmpByEmp(empParam);
for (Employee employee1 : empList2) {
System.out.println("employee1 = " + employee1);
}
}
輸出結果顯示:
DEBUG 11-21 16:07:31,964 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:07:31,987 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:07:32,007 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
======================================
DEBUG 11-21 16:07:32,020 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-21 16:07:32,020 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-21 16:07:32,022 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
結論:當在不同SqlSession中進行兩次相同條件的查詢操作,兩次查詢操作都通路資料庫。
失效條件
最後我們做一下小總結,Mybatis一級緩存失效的條件:
1)不同的SqlSession對應不同的一級緩存
2)同一個SqlSession但是查詢條件不同
3)同一個SqlSession兩次查詢期間執行了任何一次增删改操作
4)同一個SqlSession兩次查詢期間手動清空了緩存
5)同一個SqlSession兩次查詢期間送出了事務
三、Mybatis二級緩存
概念
二級緩存(second level cache),全局作用域緩存。二級緩存是預設不開啟的,需要手動配置。
開啟步驟
使用Mybatis二級緩存的步驟:
1、全局配置檔案中開啟二級緩存<setting name="cacheEnabled" value="true"/>
在mybatis-config.xml配置檔案中進行配置,具體如下代碼14行所示:
<?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>
<properties resource="db.properties"></properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 開啟延遲加載 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 設定加載的資料是按需加載-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 開啟二級緩存-->
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<package name="ssm.pojo"/>
</typeAliases>
<environments default="development">
<!-- 設定連接配接資料庫環境-->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 設定映射檔案路徑-->
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
2、需要使用二級緩存的映射檔案處使用cache配置緩存<cache />,我們以EmployeeMapper.xml為例進行配置,見代碼的第6行:
<?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="ssm.mapper.EmployeeMapper">
<cache></cache>
<sql id="select_employee">
SELECT
id,
last_name,
email,
salary,
dept_id
FROM
tbl_employee
</sql>
<select id="selectEmpByEmp" resultType="employee">
<include refid="select_employee"></include>
<where>
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null">
AND last_name = #{lastName}
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="salary != null">
AND salary = #{salary}
</if>
</where>
</select>
</mapper>
3、注意:POJO需要實作Serializable接口,以Employee實體類為例,如下代碼所示:
public class Employee implements Serializable {
private static final long serialVersionUID = 1917969510394772466L;
private Integer id;
private String lastName;
private String email;
private Double salary;
//員工所屬部門資訊
private Dept dept;
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", salary=" + salary +
", dept=" + dept +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
public Employee() {
}
public Employee(Integer id, String lastName, String email, Double salary, Dept dept) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.salary = salary;
this.dept = dept;
}
}
4、關閉sqlSession或送出sqlSession時,将資料緩存到二級緩存,具體詳見測試代碼。
緩存原理
第一次擷取資料時,從資料庫中查詢指定資料。并先将資料緩存至一級緩存,再關閉或送出SqlSession時,将資料緩存至二級緩存。以後在擷取資料時,先從一級緩存中擷取資料,如擷取不到資料;再從二級緩存中擷取資料【如未擷取到資料】,則從資料庫中擷取資料【并重複上述操作】。
代碼測驗
我們來進行驗證,根據上述步驟開啟二級緩存,使用相同的SqlSessionFactory建立出兩個不同的SqlSession對象,然後中間進行第一個SqlSession的送出。再進行第二個SqlSession對象的查詢操作,代碼如下:
public void testCache2(){
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee empParam = new Employee();
empParam.setId(2);
empParam.setLastName("chengcheng2");
List<Employee> empList = mapper.selectEmpByEmp(empParam);
for (Employee employee1 : empList) {
System.out.println("employee1 = " + employee1);
}
// SqlSession送出了事務
sqlSession.commit();
System.out.println("======================================");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmployeeMapper mapper2 = sqlSession2.getMapper(EmployeeMapper.class);
List<Employee> empList2 = mapper2.selectEmpByEmp(empParam);
for (Employee employee1 : empList2) {
System.out.println("employee1 = " + employee1);
}
}
輸出結果顯示:
DEBUG 11-22 14:29:33,339 Cache Hit Ratio [ssm.mapper.EmployeeMapper]: 0.0 (LoggingCache.java:60)
DEBUG 11-22 14:29:34,138 ==> Preparing: SELECT id, last_name, email, salary, dept_id FROM tbl_employee -- SFDSF WHERE id = ? AND last_name = ? (BaseJdbcLogger.java:137)
DEBUG 11-22 14:29:34,161 ==> Parameters: 2(Integer), chengcheng2(String) (BaseJdbcLogger.java:137)
DEBUG 11-22 14:29:34,180 <== Total: 1 (BaseJdbcLogger.java:137)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
======================================
WARN 11-22 14:29:34,186 As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 (SerialFilterChecker.java:46)
DEBUG 11-22 14:29:34,188 Cache Hit Ratio [ssm.mapper.EmployeeMapper]: 0.5 (LoggingCache.java:60)
employee1 = Employee{id=2, lastName='chengcheng2', email='[email protected]', salary=100000.0, dept=null}
結論:二級緩存生效,當不同的SqlSession進行查詢操作,中間進行送出操作後,第二次查詢走緩存。
相關屬性
我們的相關屬性的配置在二級緩存的映射檔案處使用的cache處進行配置,以第一個緩存清除政策為例,其他的在其基礎上隻修改<cache>的屬性,具體如下:
eviction:緩存清除【回收】政策
LRU – 最近最少使用的:移除最長時間不被使用的對象。
FIFO – 先進先出:按對象進入緩存的順序來移除它們。
<?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="ssm.mapper.EmployeeMapper">
<cache eviction="LRU"></cache>
<sql id="select_employee">
SELECT
id,
last_name,
email,
salary,
dept_id
FROM
tbl_employee
</sql>
<select id="selectEmpByEmp" resultType="employee">
<include refid="select_employee"></include>
<where>
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null">
AND last_name = #{lastName}
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="salary != null">
AND salary = #{salary}
</if>
</where>
</select>
</mapper>
flushInterval:重新整理間隔,機關毫秒
預設情況是不設定,也就是沒有重新整理間隔,緩存僅僅調用語句時重新整理。<cache flushInterval="60"></cache>
size:引用數目,正整數
代表緩存最多可以存儲多少個對象,太大容易導緻記憶體溢出。<cache size="512"></cache>
readOnly:隻讀,true/false
<cache readOnly="true"></cache>
總結
由于在更新時會重新整理緩存, 是以需要注意使用場合:查詢頻率很高, 更新頻率很低時使用, 即經常使用 select, 相對較少使用delete, insert, update。緩存會被視為讀/寫緩存,這意味着擷取到的對象并不是共享的,可以安全地被調用者修改,而不幹擾其他調用者或線程所做的潛在修改。進行緩存可以很大程度上減少資料庫的壓力,并且可以提高查詢速率。