10.5 MyBatis缓存机制 10.5.2二级缓存(mapper级别)

10.5 MyBatis缓存机制 10.5.2二级缓存(mapper级别)

二级缓存是mapper级别的缓存。使用二级缓存时,多个SqlSession使用同一个mapperSQL语句去操作数据库,得到的数据会存在二级缓存区域,它同样是使用HashMap进行数据存储的。

二级缓存和一级缓存的区别

相比一级缓存SqlSession,二级缓存的范围更大,多个SqlSession可以共享二级缓存中的数据,二级缓存是跨SqlSession的。
二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace。不同的SqlSession两次执行相同的namespace下的SQL语句,且向SQL中传递的参数也相同,即最终执行相同的SQL语句时,当第一个SqlSession调用close()方法关闭一级缓存时,第一次从数据库中査询到的数据会被保存到二级缓存,第二次查询时会从二级缓存中获取数据,不再去底层数据库查询,从而提高査询效率.

二级缓存需要手动开启

MyBatis默认没有开启二级缓存,需要Mybatis的配置文件的setting全局参数中配置开启二级缓存。

如何设置二级缓存

接下来测试MyBatis的二级缓存,所有代码和测试一级缓存的代码完全一样,只是需要在MyBatis配置文件中开启二级缓存.

在mybatis-config.xml中设置开启二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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">
<!-- XML 配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
......
<settings>
<!-- 开启二级缓存 -->
<setting
name="cacheEnabled"
value="true"/>
</settings>
......
</configuration>

setting标签的cacheEnabled属性的valuetrue时表示在此配置文件下开启二级缓存,该属性默认为false

二级缓存和命名空间绑定

MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件或者Mapper接口中。

  • 在映射文件中,命名空间就是XML根节点mappernamespace属性。
  • Mapper接口中,命名空间就是接口的全限定名称。

cache标签

在映射文件Mapper.xml中写入cache标签可以开启默认的二级缓存,代码如下:

1
2
3
4
5
6
7
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.UserMapper">
<cache/>
</mapper>

默认的二级缓存作用

默认的二级缓存会有如下作用:

  • 映射语句文件中的所有SELECT语句将会被缓存。
  • 映射语句文件中的所有INSERTUPDATEDELETE语句会刷新缓存。
  • 缓存会使用Least Recently Used(LRU 最近最少使用)策略来收回。
  • 根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用。
  • 缓存会被视为read/write(可读/可写)的,这意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

cache标签实例

cache标签中所有这些行为都可以通过cache标签的属性来进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.UserMapper">
<!-- 开启二级缓存
回收策略为先进先出
自动刷新时间60s
最多缓存512个引用对象
只读
-->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>

以上配置创建了一个LRU缓存,并每隔60秒刷新,最大存储512个对象,而且返回的对象被认为是只读的.

cache标签的属性

cache标签用来开启当前mappernamespace下的二级缓存。该标签的属性设置如下:

  • flushInterval属性。刷新间隔。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况下是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
  • size属性。缓存数目。可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。
  • readonly属性。只读。该属性可以被设置为truefalse
    • 设置为true表示只读,只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改。这提供了很重要的性能优势。
    • 可读写的缓存会返回缓存对象的拷贝(通过序列化)。这种方式会慢一些,但是安全,因此默认是false表示不是只读.
  • eviction属性。收回策略,默认为LRU。有如下几种:
    • LRU最近最少使用策略,移除最长时间不被使用的对象。
    • FIFO先进先出策略,按对象进入缓存的顺序来移除它们.
    • SOFT软引用策略,移除基于垃圾回收器状态和软引用规则的对象.
    • WEAK弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象.

使用二级缓存的的Java对象必须可序列化

使用二级缓存时,与查询结果映射的Java对象必须实现java.io.Serializable接口的序列化和反序列化操作,如果存在父类,其成员都需要实现序列化接口。实现序列化接口是为了对缓存数据进行序列化和反序列化操作,因为二级缓存数据存储介质多种多样,不一定在内存,有可能是硬盘或者远程服务器.

实例

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
E:\workspace_web2\MyTwoLevelCacheTest
├─src
│ ├─db.properties
│ ├─domain
│ │ ├─tb_user.sql
│ │ └─User.java
│ ├─fractory
│ │ └─SqlSessionFratoryTools.java
│ ├─log4j.xml
│ ├─mapper
│ │ ├─UserMapper.java
│ │ └─UserMapper.xml
│ ├─mybatis-config.xml
│ └─test
│ └─TwoLevelCacheTest.java
└─WebContent
└─WEB-INF
└─lib
├─ant-1.9.6.jar
├─ant-launcher-1.9.6.jar
├─asm-5.2.jar
├─cglib-3.2.5.jar
├─commons-logging-1.2.jar
├─javassist-3.22.0-CR2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
├─mysql-connector-java-5.1.44-bin.jar
├─ognl-3.1.15.jar
├─slf4j-api-1.7.25.jar
└─slf4j-log4j12-1.7.25.jar

初始化数据库表

/MyTwoLevelCacheTest/src/domain/tb_user.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 取消外键约束
SET FOREIGN_KEY_CHECKS = 0;
# 创建数据库表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` char(2) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入数据
INSERT INTO `tb_user` VALUES ('1', '小明', '男', '21');
INSERT INTO `tb_user` VALUES ('2', '小王', '男', '22');
INSERT INTO `tb_user` VALUES ('3', '小丽', '女', '18');
INSERT INTO `tb_user` VALUES ('4', '小芳', '女', '18');
INSERT INTO `tb_user` VALUES ('5', '小王', '男', '22');
# 回复外键检查
SET FOREIGN_KEY_CHECKS=1;

数据库配置文件

db.properties:

/MyTwoLevelCacheTest/src/db.properties mysql5.x配置
1
2
3
4
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root
/MyTwoLevelCacheTest/src/db.properties mysql8.x配置
1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf-8
username=root
password=root

log4j.xml

/MyTwoLevelCacheTest/src/log4j.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<log4j:configuration>
<appender name="STDOUT"
class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%5p [%t] %m%n" />
</layout>
</appender>
<logger name="mapper.UserMapper">
<level value="DEBUG" />
</logger>
<root>
<level value="ERROR" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>

mybatis-config.xml

/MyTwoLevelCacheTest/src/mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<properties resource="db.properties" />
<settings>
<!-- 设置日志实现 -->
<setting name="logImpl" value="log4j" />
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true" />
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="pooled">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml" />
</mappers>
</configuration>

User.java

/MyTwoLevelCacheTest/src/domain/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package domain;
public class User {
private Integer id;
private String name;
private String sex;
private Integer age;
public User()
{
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

工具类

/MyTwoLevelCacheTest/src/fractory/SqlSessionFactoryTools.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package fractory;

import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

mapper.xml

/MyTwoLevelCacheTest/src/mapper/UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="mapper.UserMapper">

<!-- ############## 下面的代码放在mapper.xml文件中 ############### -->
<!-- flushInterval:刷新时间间隔,默认刷新,只在执行SQL语句时刷新 -->
<!-- size:缓存数目,默认1024 -->
<!-- readOnly:是否只读,true为只读,默认false -->
<!-- eviction:回收策略,默认LRU(最近最少使用), -->
<!-- FIFO(先进先出), -->
<!-- SORT(软引用:移除基于垃圾回收器的状态和软引用规则) -->
<!-- WEAK(弱引用,更积极的移除基于垃圾回收器的窗台和弱引用规则的对象) -->
<cache flushInterval="60000" size="512" readOnly="true"
eviction="LRU" />
<select id="selectUserById" parameterType="int"
resultType="domain.User">
select * from tb_user where id=#{id}
</select>
<delete id="deleteUserById" parameterType="int">
delete from tb_user where id=#{id}
</delete>
</mapper>

mapper接口

/MyTwoLevelCacheTest/src/mapper/UserMapper.java
1
2
3
4
5
6
7
8
package mapper;

import domain.User;

public interface UserMapper {
User selectUserById(int id);
void deleteUserById(int id);
}

测试类

/MyTwoLevelCacheTest/src/test/TwoLevelCacheTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TwoLevelCacheTest {
public static void main(String[] args)
{
// 获取会话
SqlSession sqlSession = SqlSessionFratoryTools.getSqlSession();
// 获取mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 查询用户
User user = userMapper.selectUserById(1);
System.out.println(user);
System.out.println("--------------------------------------------");
// 关闭会话,清空一级缓存.
sqlSession.close();
// 获取一个新的会话
sqlSession = SqlSessionFratoryTools.getSqlSession();
// 获取新的代理对象
userMapper = sqlSession.getMapper(UserMapper.class);
// 再次查询该用户
user = userMapper.selectUserById(1);
System.out.println(user);
}
}

运行结果:

DEBUG [main] Cache Hit Ratio [mapper.UserMapper]: 0.0
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <==      Total: 1
User [id=1, name=小明, sex=男, age=21]
--------------------------------------------
DEBUG [main] Cache Hit Ratio [mapper.UserMapper]: 0.5
User [id=1, name=小明, sex=男, age=21]

代码详解

仔细观察MyBatis的执行结果,日志中有几条以Cache Hit Ratio 开头的语句,这行日志后面输出的值为当前执行方法的缓存命中率。在第一次査询id1User对象时,执行了一条select语句,接下来调用SqlSessionclose()方法,该方法会关闭SqlSession一级缓存,同时会将査询数据保存到二级缓存中。
当第二次获取id1User对象时,重新获得的一级缓存SqlSession中并没有缓存任何对象,但是因为启用了二级缓存,当MyBatis在一级缓存中没有找到id1User对象时,会去二级缓存中查找,所以不会再次执行select语句。