聊聊MyBatis缓存机制 — 一级缓存
# 原理
在Mybatis中,一级缓存默认启用,目的是为了优化执行多次查询条件完全相同的SQL,避免直接对数据库进行查询,提高性能。执行过程如图所示:

在SqlSession中存在着一个Executor引用,同时每个Executor中又有LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
# 配置
在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
<setting name="localCacheScope" value="SESSION"/>
# 测试
# 先决条件
数据库结构如下:
CREATE TABLE `user` (
`id` INT ( 10 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 20 ) COLLATE utf8_bin DEFAULT NULL,
`password` VARCHAR ( 20 ) COLLATE utf8_bin DEFAULT NULL,
`age` TINYINT ( 3 ) DEFAULT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 12 DEFAULT CHARSET = utf8 COLLATE = utf8_bin;
2
3
4
5
6
7
UserMapper.java
public interface UserMapper {
User selectUserById(Integer id);
void insert(User user);
void update(User user);
}
2
3
4
5
6
7
UserMapper.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="mapper.UserMapper">
<select id="selectUserById" parameterType="java.lang.Integer" resultType="User">
select * from user where id = #{id};
</select>
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into user(username, password, age) VALUES (#{username}, #{password}, #{age});
</insert>
<update id="update" parameterType="User">
update user set username = #{username}, password = #{password}, age = #{age} where id = #{id};
</update>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查询
启用一级缓存,调用三次selectUserById(4) 测试代码如下:
@Test
public void localCacheSelectTest() {
try(SqlSession session = sqlSessionFactory.openSession(true)) {
UserMapper userMapper = session.getMapper(UserMapper.class);
System.out.println(userMapper.selectUserById(4));
System.out.println(userMapper.selectUserById(4));
System.out.println(userMapper.selectUserById(4));
}
}
2
3
4
5
6
7
8
9
输出:
Created connection 1537772520.
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, 123, 123, 34
<== Total: 1
mapper.User@4f1bfe23 // 连接数据库查询,以下命中本地缓存
mapper.User@4f1bfe23
mapper.User@4f1bfe23
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5ba88be8]
Returned connection 1537772520 to pool.
2
3
4
5
6
7
8
9
10
11
显然,Mybatis在第一次查询时真正的访问数据库。
# 插入数据/更新数据
启用一级缓存,调用一次``selectUserById(4), 插入一条新的记录,再调用一次selectUserById(4)` 更新数据同理。测试代码:
@Test
public void localCacheUpdateTest() {
try(SqlSession session = sqlSessionFactory.openSession(true)) {
UserMapper userMapper = session.getMapper(UserMapper.class);
System.out.println(userMapper.selectUserById(4));
userMapper.insert(new User("张三","23",35));
System.out.println(userMapper.selectUserById(4));
}
}
2
3
4
5
6
7
8
9
输出:
Created connection 940087898.
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, 123, 123, 34
<== Total: 1
mapper.User@5adb0db3 // 数据库查询
==> Preparing: insert into user(username, password, age) VALUES (?, ?, ?); // 插入数据
==> Parameters: 张三(String), 23(String), 35(Integer)
<== Updates: 1
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, 123, 123, 34
<== Total: 1
mapper.User@6aa3a905 // 数据库查询
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@38089a5a]
Returned connection 940087898 to pool.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
由此可知:在插入或更新数据库操作后执行的相同查询,查询了数据库,一级缓存失效。
# 在不同的Session中执行查询和更新
开启两个SqlSession,在sqlSession_select中查询数据,使一级缓存生效,在sqlSession_update中更新数据库,验证一级缓存只在数据库会话内部共享。测试代码:
@Test
public void localCacheSessionUpdateAndSelectTest() {
try(SqlSession session_select = sqlSessionFactory.openSession(true);
SqlSession session_update = sqlSessionFactory.openSession(true)) {
UserMapper userMapper_select = session_select.getMapper(UserMapper.class);
UserMapper userMapper_update =session_update.getMapper(UserMapper.class);
System.out.println(userMapper_select.selectUserById(4));
System.out.println("userMapper_select 一级缓存生效: "+userMapper_select.selectUserById(4));
User user = userMapper_update.selectUserById(4); // userMapper_update 在session_update中更新
user.setUsername("Zhang san");
userMapper_update.update(user);
System.out.println("userMapper_select 产生了脏读 " + userMapper_select.selectUserById(4));
System.out.println("userMapper_update 执行查询 " + userMapper_update.selectUserById(4));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输出:
Opening JDBC Connection
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, Zhang san, 123, 34
<== Total: 1
mapper.User@70f59913
userMapper_select 一级缓存生效: mapper.User@70f59913
Opening JDBC Connection
Created connection 624795507.
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, Zhang san, 123, 34
<== Total: 1
==> Preparing: update user set username = ?, password = ?, age = ? where id = ?;
==> Parameters: Zhang san(String), 123(String), 34(Integer), 4(Integer)
<== Updates: 1
userMapper_select 产生了脏读 mapper.User@70f59913 ———————— userMapper_select 脏读
==> Preparing: select * from user where id = ?;
==> Parameters: 4(Integer)
<== Columns: id, username, password, age
<== Row: 4, Zhang san, 123, 34
<== Total: 1
userMapper_update 执行查询 mapper.User@6aa3a905
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@253d9f73]
Returned connection 624795507 to pool.
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4f0100a7]
Returned connection 1325465767 to pool.
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
显然: 一级缓存只在SqlSession中内部共享。
# 源码解析
下回分解