美文网首页
Mybatis中的一二级缓存

Mybatis中的一二级缓存

作者: LENN123 | 来源:发表于2020-04-29 17:54 被阅读0次

前言

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.

相关文章

网友评论

      本文标题:Mybatis中的一二级缓存

      本文链接:https://www.haomeiwen.com/subject/lxdnwhtx.html