Mybatis中的一二级缓存

前言

Mybatis会为每次的查询结果进行缓存,缓存根据作用范围划分为一级、二级缓存,基于Mybatis自带的缓存机制,可以减少去数据库执行查询的次数,缩减开销,以提升效率。本文将通过实验的方式,来分析一级、二级缓存的作用范围,以及缓存在何时被销毁。

配置日志

为了更好的观察Mybatis下每条语句的执行流程,首先配置为其配置日志功能,Mybatis支持多种主流的日志框架,这里选择LOG4J。首先在maven上下载LOG4Jjar包,这里选择的版本为

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.classnamespace="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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容