前言
Mybatis
會(huì)為每次的查詢結(jié)果進(jìn)行緩存,緩存根據(jù)作用范圍劃分為一級(jí)况褪、二級(jí)緩存撕贞,基于Mybatis
自帶的緩存機(jī)制,可以減少去數(shù)據(jù)庫(kù)執(zhí)行查詢的次數(shù)测垛,縮減開銷捏膨,以提升效率。本文將通過(guò)實(shí)驗(yàn)的方式食侮,來(lái)分析一級(jí)脊奋、二級(jí)緩存的作用范圍,以及緩存在何時(shí)被銷毀疙描。
配置日志
為了更好的觀察Mybatis
下每條語(yǔ)句的執(zhí)行流程诚隙,首先配置為其配置日志功能,Mybatis
支持多種主流的日志框架起胰,這里選擇LOG4J
久又。首先在maven
上下載LOG4J
的jar
包巫延,這里選擇的版本為
log4j-1.2.17.jar
將其加入項(xiàng)目目錄下,并設(shè)置添加為Library
地消,然后創(chuàng)建一個(gè)名為log4j.properties
的配置文件(注意名稱是約定好的炉峰,不可更改),添加如下配置脉执。
- log4j.properties
# 全局日志配置
log4j.rootLogger=DEBUG, stdout
log4j.logger.org.mybatis=DEBUG
# MyBatis 日志配置
#log4j.logger.org.entity.PersonMapper=TRACE
# 控制臺(tái)輸出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
重要的是設(shè)置日志級(jí)別為DEBUG
疼阔,該級(jí)別下可以輸出包括ERROR
等所有級(jí)別的日志信息,輸出位置設(shè)置為標(biāo)準(zhǔn)輸出stdout
半夷,即控制臺(tái)即可婆廊。
接下來(lái)為Mybatis
設(shè)置所使用的日志框架, 將以下內(nèi)容添加到Mybatis
的配置文件中
- mybatis-config.xml
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
...
</configuration>
- 在數(shù)據(jù)庫(kù)中創(chuàng)建一個(gè)
Person
表
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
| age | int(11) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
- 插入如下數(shù)據(jù)
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | TOM | 26 |
| 2 | Ben | 41 |
- 創(chuàng)建對(duì)應(yīng)的
entity
public class Person {
private int id;
private String name;
private int age;
public Person(){};
public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
在PersonMapper.xml
中書寫一個(gè)簡(jiǎn)單的根據(jù)id
查詢個(gè)人信息的sql
- PersonMapper.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="entity.PersonMapper">
<select id="selectById" resultType="entity.Person" parameterType="int">
SELECT *
FROM Person
WHERE id = #{id}
</select>
</mapper>
- PersonMapper.java
public interface PersonMapper {
Person selectById(int id);
}
一切配置好就可以檢驗(yàn)下日志是否配置成功了。
- Test.java
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
}
}
- 控制臺(tái)輸出
DEBUG [main] - Created connection 1337335626.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4fb61f4a]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
日志成功跟蹤了整個(gè)流程巫橄,配置成功淘邻。
一級(jí)緩存
使用Mybatis
時(shí),我們會(huì)通過(guò)sqlSessionFactory
來(lái)獲得一個(gè)sqlSession
實(shí)例湘换,該sqlSession
實(shí)例象征著一次和Mysql Server
的連接宾舅,我們?cè)谶@個(gè)sqlSession
下將sql
發(fā)送給Mysql Server
并執(zhí)行它,Mybatis
的一級(jí)緩存的作用范圍便是當(dāng)前的sqlSession
下彩倚,現(xiàn)在讓我們?cè)偻粋€(gè)sqlSession
下執(zhí)行兩次對(duì)id=1
的記錄的查詢筹我。
- Test.java
package entity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
Person person2 = personMapper.selectById(2);
System.out.println(person2);
}
}
觀察日志輸出結(jié)果
DEBUG [main] - Created connection 1337335626.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4fb61f4a]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
Person{id=1, name='TOM', age=26}
可以發(fā)現(xiàn),雖然執(zhí)行了兩次對(duì)id=1
的查詢帆离,但是實(shí)際上只查詢了一次崎溃,因?yàn)樵诘谝淮尾樵兒螅?code>Mybatis幫我們對(duì)查詢結(jié)果進(jìn)行了緩存。
之前我們說(shuō)了一級(jí)緩存的作用范圍是同一個(gè)sqlSession
下盯质,現(xiàn)在讓我們?cè)賰蓚€(gè)不同的session
下執(zhí)行查詢工作。
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
//
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper personMapper2 = session2.getMapper(PersonMapper.class);
Person person2 = personMapper2.selectById(1);
System.out.println(person2);
}
}
查看日志輸出結(jié)果
DEBUG [main] - Created connection 1337335626.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4fb61f4a]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 168907708.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a1153bc]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
可以看到概而,因?yàn)椴辉谝粋€(gè)session
下呼巷,所以緩存沒(méi)有派上用場(chǎng),因此查詢了兩次赎瑰。Mybatis
中的一級(jí)緩存是默認(rèn)開啟的王悍,且采用了LRU
算法,因此會(huì)淘汰掉最近最久未使用的查詢結(jié)果餐曼,除此之外压储,我們也可以手動(dòng)的執(zhí)行commit()
語(yǔ)句來(lái)清空緩存。
commit()
清空一級(jí)緩存Test.java
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
session.commit();
Person person2 = personMapper.selectById(1);
System.out.println(person2);
}
}
- 輸出日志
DEBUG [main] - Created connection 1337335626.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4fb61f4a]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
可以看到源譬,當(dāng)調(diào)用session.commit()
之后集惋,再執(zhí)行id=1
的查詢語(yǔ)句時(shí),又去數(shù)據(jù)庫(kù)查詢了一次踩娘,說(shuō)明緩存被清空了刮刑。類似的執(zhí)行delete
,update
,insert
語(yǔ)句時(shí)也會(huì)清空緩存,因?yàn)樗鼈儠?huì)隱式的調(diào)用commit
語(yǔ)句。這所以這些操作會(huì)清空緩存的原因也很簡(jiǎn)單雷绢,因?yàn)檫@些語(yǔ)句都對(duì)數(shù)據(jù)庫(kù)表中的記錄進(jìn)行修改泛烙,如果不清空緩存,那么下一次操作就會(huì)拿到臟數(shù)據(jù)翘紊。
二級(jí)緩存
除了session
范圍內(nèi)的一級(jí)緩存蔽氨,Mybatis
還提供了二級(jí)緩存,與一級(jí)緩存默認(rèn)開啟不同帆疟,二級(jí)緩存需要手動(dòng)開啟鹉究,開啟的方式也很簡(jiǎn)單,只要在PersonMapper.xml
內(nèi)添加一行<cache/>
標(biāo)簽即可鸯匹。
<?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="entity.PersonMapper">
<cache/>
<select id="selectById" resultType="entity.Person" parameterType="int">
SELECT *
FROM Person
WHERE id = #{id}
</select>
</mapper>
從這里我們可以初步猜測(cè)坊饶,二級(jí)緩存的作用范圍是在同一種mapper
下,也就是說(shuō)在同一個(gè)namespace
下殴蓬,我們知道當(dāng)利用對(duì)PersonMapper
這個(gè)接口生成動(dòng)態(tài)代理對(duì)象匿级,利用該對(duì)象進(jìn)行執(zhí)行具體的查詢操作時(shí),會(huì)傳入一個(gè)PersonMapper.class
染厅。而這個(gè)PersonMapper.class
與namespace="entity.PersonMapper"
這個(gè) xml
文件是一一對(duì)映的痘绎。
PersonMapper personMapper = session.getMapper(PersonMapper.class);
因此可以簡(jiǎn)單的說(shuō),只要是同一個(gè)PersonMapper.class
生成的動(dòng)態(tài)代理對(duì)象肖粮,都會(huì)將查詢結(jié)果緩存到同一個(gè)空間中去孤页。
除了在Mapper.xml
標(biāo)注使用緩存,我們還要在Mybatis
的配置文件中開啟緩存功能
- Mybatis-config.xml
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="cacheEnabled" value="true"/>
</settings>
...
</configuration>
在具體實(shí)驗(yàn)之前涩馆,我們首先要明白行施,一級(jí)緩存的作用范圍要小于二級(jí)緩存,因此在執(zhí)行具體的查詢時(shí)魂那,都會(huì)先去一級(jí)緩存(內(nèi)存中)進(jìn)行查找蛾号,一級(jí)緩存沒(méi)有找到的時(shí)候,才會(huì)去二級(jí)緩存查找涯雅。為此我們?cè)O(shè)計(jì)如下的測(cè)試方法
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
//session.close();
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper personMapper2 = session2.getMapper(PersonMapper.class);
Person person2 = personMapper2.selectById(1);
System.out.println(person2);
}
}
我們之前說(shuō)了只要是同一個(gè)Mapper.class
生成的動(dòng)態(tài)代理對(duì)象鲜结,公用同一個(gè)緩存空間,因此利用2
個(gè)不同的sqlSession
生成了2
個(gè)不同的動(dòng)態(tài)代理對(duì)象活逆,因此因?yàn)椴还蚕硪患?jí)緩存精刷,會(huì)去二級(jí)緩存中嘗試獲取結(jié)果,如果我們之前推論無(wú)誤的話,person2
會(huì)直接從二級(jí)緩存中存取,而不會(huì)去數(shù)據(jù)庫(kù)查詢淑蔚。
- 執(zhí)行結(jié)果
DEBUG [main] - Created connection 1388278453.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@52bf72b5]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
DEBUG [main] - Cache Hit Ratio [entity.PersonMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 464887938.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1bb5a082]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
根據(jù)日志可以發(fā)現(xiàn),依舊執(zhí)行了2次查詢工作误算,并沒(méi)有訪問(wèn)到二級(jí)緩存仰美,是我們的推論有問(wèn)題嗎?實(shí)際上我們要考慮一個(gè)重要的問(wèn)題儿礼,就是緩存結(jié)果的時(shí)機(jī)咖杂,在之前討論一級(jí)緩存的時(shí)候,很明顯是執(zhí)行完一次查詢蚊夫,就會(huì)把結(jié)果放進(jìn)緩存里诉字,而實(shí)際上在二級(jí)緩存里,只有一個(gè)sqlSession
結(jié)束以后知纷,會(huì)把本次查詢的結(jié)果打包存進(jìn)緩存中壤圃,為什么要這么做?因?yàn)橐患?jí)緩存的結(jié)果是存在內(nèi)存里琅轧,而二級(jí)緩存實(shí)際上是將結(jié)果存在磁盤里(所以你的對(duì)象實(shí)體還需要支持序列化N樯),因此如果每次查詢完就存到磁盤里乍桂,會(huì)產(chǎn)生大量的隨機(jī) IO
冲杀,開銷過(guò)大,因此會(huì)將每次查詢結(jié)果等本次sqlSession
結(jié)束后再一次性放到二級(jí)緩存里睹酌。因此权谁,只要在一個(gè)sqlSession
,手動(dòng)調(diào)用close()
方法即可憋沿。
public class Test {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
Person person1 = personMapper.selectById(1);
System.out.println(person1);
session.close();
SqlSession session2 = sqlSessionFactory.openSession();
PersonMapper personMapper2 = session2.getMapper(PersonMapper.class);
Person person2 = personMapper2.selectById(1);
System.out.println(person2);
}
}
- 輸出結(jié)果
DEBUG [main] - Created connection 1388278453.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@52bf72b5]
DEBUG [main] - ==> Preparing: SELECT * FROM Person WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Person{id=1, name='TOM', age=26}
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@52bf72b5]
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@52bf72b5]
DEBUG [main] - Returned connection 1388278453 to pool.
DEBUG [main] - Cache Hit Ratio [entity.PersonMapper]: 0.5
Person{id=1, name='TOM', age=26}
可見現(xiàn)在二級(jí)緩存起作用了旺芽,解釋下 Cache Hit Ratio [entity.PersonMapper]: 0.5
,即緩存命中率辐啄,第一次沒(méi)有命中采章,第二次命中了,因此1/2=0.5.