前言
Mybatis
会为每次的查询结果进行缓存,缓存根据作用范围划分为一级、二级缓存,基于Mybatis
自带的缓存机制,可以减少去数据库执行查询的次数,缩减开销,以提升效率。本文将通过实验的方式,来分析一级、二级缓存的作用范围,以及缓存在何时被销毁。
配置日志
为了更好的观察Mybatis
下每条语句的执行流程,首先配置为其配置日志功能,Mybatis
支持多种主流的日志框架,这里选择LOG4J
。首先在maven
上下载LOG4J
的jar
包,这里选择的版本为
log4j-1.2.17.jar
将其加入项目目录下,并设置添加为Library
,然后创建一个名为log4j.properties
的配置文件(注意名称是约定好的,不可更改),添加如下配置。
- log4j.properties
# 全局日志配置
log4j.rootLogger=DEBUG, stdout
log4j.logger.org.mybatis=DEBUG
# MyBatis 日志配置
#log4j.logger.org.entity.PersonMapper=TRACE
# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
重要的是设置日志级别为DEBUG
,该级别下可以输出包括ERROR
等所有级别的日志信息,输出位置设置为标准输出stdout
,即控制台即可。
接下来为Mybatis
设置所使用的日志框架, 将以下内容添加到Mybatis
的配置文件中
- mybatis-config.xml
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
...
</configuration>
- 在数据库中创建一个
Person
表
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
| age | int(11) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
- 插入如下数据
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | TOM | 26 |
| 2 | Ben | 41 |
- 创建对应的
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
中书写一个简单的根据id
查询个人信息的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);
}
一切配置好就可以检验下日志是否配置成功了。
- 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);
}
}
- 控制台输出
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}
日志成功跟踪了整个流程,配置成功。
一级缓存
使用Mybatis
时,我们会通过sqlSessionFactory
来获得一个sqlSession
实例,该sqlSession
实例象征着一次和Mysql Server
的连接,我们在这个sqlSession
下将sql
发送给Mysql Server
并执行它,Mybatis
的一级缓存的作用范围便是当前的sqlSession
下,现在让我们再同一个sqlSession
下执行两次对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);
}
}
观察日志输出结果
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}
可以发现,虽然执行了两次对id=1
的查询,但是实际上只查询了一次,因为在第一次查询后,Mybatis
帮我们对查询结果进行了缓存。
之前我们说了一级缓存的作用范围是同一个sqlSession
下,现在让我们再两个不同的session
下执行查询工作。
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);
}
}
查看日志输出结果
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}
可以看到,因为不在一个session
下,所以缓存没有派上用场,因此查询了两次。Mybatis
中的一级缓存是默认开启的,且采用了LRU
算法,因此会淘汰掉最近最久未使用的查询结果,除此之外,我们也可以手动的执行commit()
语句来清空缓存。
commit()
清空一级缓存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}
可以看到,当调用session.commit()
之后,再执行id=1
的查询语句时,又去数据库查询了一次,说明缓存被清空了。类似的执行delete
,update
,insert
语句时也会清空缓存,因为它们会隐式的调用commit
语句。这所以这些操作会清空缓存的原因也很简单,因为这些语句都对数据库表中的记录进行修改,如果不清空缓存,那么下一次操作就会拿到脏数据。
二级缓存
除了session
范围内的一级缓存,Mybatis
还提供了二级缓存,与一级缓存默认开启不同,二级缓存需要手动开启,开启的方式也很简单,只要在PersonMapper.xml
内添加一行<cache/>
标签即可。
<?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>
从这里我们可以初步猜测,二级缓存的作用范围是在同一种mapper
下,也就是说在同一个namespace
下,我们知道当利用对PersonMapper
这个接口生成动态代理对象,利用该对象进行执行具体的查询操作时,会传入一个PersonMapper.class
。而这个PersonMapper.class
与namespace="entity.PersonMapper"
这个 xml
文件是一一对映的。
PersonMapper personMapper = session.getMapper(PersonMapper.class);
因此可以简单的说,只要是同一个PersonMapper.class
生成的动态代理对象,都会将查询结果缓存到同一个空间中去。
除了在Mapper.xml
标注使用缓存,我们还要在Mybatis
的配置文件中开启缓存功能
- Mybatis-config.xml
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="cacheEnabled" value="true"/>
</settings>
...
</configuration>
在具体实验之前,我们首先要明白,一级缓存的作用范围要小于二级缓存,因此在执行具体的查询时,都会先去一级缓存(内存中)进行查找,一级缓存没有找到的时候,才会去二级缓存查找。为此我们设计如下的测试方法
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);
}
}
我们之前说了只要是同一个Mapper.class
生成的动态代理对象,公用同一个缓存空间,因此利用2
个不同的sqlSession
生成了2
个不同的动态代理对象,因此因为不共享一级缓存,会去二级缓存中尝试获取结果,如果我们之前推论无误的话,person2
会直接从二级缓存中存取,而不会去数据库查询。
- 执行结果
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}
根据日志可以发现,依旧执行了2次查询工作,并没有访问到二级缓存,是我们的推论有问题吗?实际上我们要考虑一个重要的问题,就是缓存结果的时机,在之前讨论一级缓存的时候,很明显是执行完一次查询,就会把结果放进缓存里,而实际上在二级缓存里,只有一个sqlSession
结束以后,会把本次查询的结果打包存进缓存中,为什么要这么做?因为一级缓存的结果是存在内存里,而二级缓存实际上是将结果存在磁盘里(所以你的对象实体还需要支持序列化!),因此如果每次查询完就存到磁盘里,会产生大量的随机 IO
,开销过大,因此会将每次查询结果等本次sqlSession
结束后再一次性放到二级缓存里。因此,只要在一个sqlSession
,手动调用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);
}
}
- 输出结果
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}
可见现在二级缓存起作用了,解释下 Cache Hit Ratio [entity.PersonMapper]: 0.5
,即缓存命中率,第一次没有命中,第二次命中了,因此1/2=0.5.