聊聊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"/>
1

# 测试

# 先决条件

数据库结构如下:

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;
1
2
3
4
5
6
7

UserMapper.java

public interface UserMapper {
    User selectUserById(Integer id);

    void insert(User user);

    void update(User user);
}
1
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>
1
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));
    }
}
1
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.
1
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));
    }
}
1
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.
1
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));
    }
}
1
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.
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

显然: 一级缓存只在SqlSession中内部共享

# 源码解析

下回分解

参考文档 《聊聊Mybatis缓存机制》 mybatis官方文档

上次更新: 2020/09/05, 14:09:00
最近更新
01
RabbitMQ简介
10-27
02
聊聊Java多态
10-21
03
JVM垃圾回收器
10-16
更多文章>