天天看點

Java八股文:Mybatis緩存你了解多少?

作者:尚矽谷教育

Mybatis是一款優秀的持久層架構,具有較高的SQL靈活性,支援進階映射(一對一,一對多),動态SQL,延遲加載和緩存等特性。他避免了幾乎所有的JDBC代碼和手動設定參數以及擷取結果集,可以使用簡單的XML或注解來配置和映射原生類型。為了減少和資料庫的互動次數,減少系統開銷,以及提高系統效率,Mybatis提供了緩存機制,接下來我們聊一聊Mybatis的緩存機制。

一、緩存

首先我們先說一下緩存,緩存是将一些臨時資料存儲到我們的記憶體中。由于我們伺服器的記憶體是有限的,是以說可以存儲一些資料量小的資料,不可能将所有的資料都放到伺服器的記憶體中。緩存的速率較快,使用友善。

那為什麼會使用緩存呢?我們開發的系統,大部分的功能需求都是對資料庫的增删改查,其中查詢的功能占據多數。當我們使用者想要進行查詢資料時,需要每次都要通路資料庫,他會加大對資料庫的壓力,使其速率降低。是以,我們的緩存就孕育而生了,這樣我們隻查詢資料庫一次,就會将資料加載到伺服器的記憶體中,以後再次查詢時,直接通路伺服器的記憶體,不需要每次都通路資料庫,不僅僅減少了資料庫的壓力,還提高了查詢速率。

二、Mybatis一級緩存

首先介紹一些Mybatis的緩存機制,Mybatis 包含一個非常強大的查詢緩存特性,它可以非常友善地配置和定制。Mybatis系統中預設定義了兩級緩存:一級緩存、二級緩存。預設情況下,隻有一級緩存(SqlSession級别的緩存,也稱為本地緩存)開啟。二級緩存需要手動開啟和配置,他是基于namespace級别的緩存。

概念

Mybatis一級緩存,首先他是預設開啟的,他是一個SqlSession裡面的所有查詢操作都會儲存到緩存中,但是要注意一點,如果在同一個SqlSession會話中,兩個的查詢中間有了 insert 、update或delete的操作,那麼之前的查詢緩存就會被清空。

Java八股文:Mybatis緩存你了解多少?

圖檔來源于網路,侵删

緩存原理

第一次擷取資料時,先從資料庫中查詢指定資料。将資料緩存到一級緩存中【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。緩存會被視為讀/寫緩存,這意味着擷取到的對象并不是共享的,可以安全地被調用者修改,而不幹擾其他調用者或線程所做的潛在修改。進行緩存可以很大程度上減少資料庫的壓力,并且可以提高查詢速率。