Mybatis缓存初知

目录

文章目录

前言

1. 一级缓存

1.1. 什么是一级缓存

1.2. 测试1-同一个SqlSession

1.3.  测试2-不同的SqlSession

1.4. 测试3-刷新缓存

 1.5. 总结

2. 二级缓存

2.1. 什么是二级缓存

2.2. 配置二级缓存

2.2.1. 全局开关

2.2.2. 分开关

2.2.3. 代码配置

2.2.4. cache 标签配置详解

总结


前言

缓存的重要性是不言而喻的。使用缓存,我们可以避免频繁的与数据库进行交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存对性能的提高更明显。
mybatis 也提供了对缓存的支持,分为一级缓存和二级缓存。但是在默认的情况下,只开启一级缓存(一级缓存是对同一个 SqlSession 而言的)。代码在(Idea创建简单的MyBatis项目
)基础上进行修改


1. 一级缓存

1.1. 什么是一级缓存

同一个 SqlSession 对象, 在参数和 SQL 完全一样的情况先, 只执行一次 SQL 语句(如果缓存没有过期)。
因为使用SelSession第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

1.2. 测试1-同一个SqlSession

StudentMapperTest.java 添加下面代码
@Test
    public void oneSqlSession() {
        SqlSession sqlSession = null;
        try {
            sqlSession = sqlSessionFactory.openSession();

            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            // 执行第一次查询
            System.out.println("=============开始同一个 Sqlsession 的第一次查询============");
            List<Student> students = studentMapper.selectAll();
            for (int i = 0; i < students.size(); i++) {
                System.out.println(students.get(i));
            }
            System.out.println("=============开始同一个 Sqlsession 的第二次查询============");
            // 同一个 sqlSession 进行第二次查询
            List<Student> stus = studentMapper.selectAll();
            Assert.assertEquals(students, stus);
            for (int i = 0; i < stus.size(); i++) {
                System.out.println("stus:" + stus.get(i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }

执行上面代码,进行了两次查询, 使用相同的 SqlSession,得到以下结果

在日志和输出中:
第一次查询发送了 SQL 语句,后返回了结果;
第二次查询没有发送 SQL 语句,直接从内存中获取了结果。
而且两次结果输入一致,同时断言两个对象相同也通过。

1.3.  测试2-不同的SqlSession

StudentMapperTest.java 添加下面代码

 @Test
    public void differSqlSession() {
        SqlSession sqlSession1 = null;
        SqlSession sqlSession2 = null;
        try {
            sqlSession1 = sqlSessionFactory.openSession();

            StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
            // 执行第一次查询
            System.out.println("=============开始不同 Sqlsession 的第一次查询============");
            List<Student> students = studentMapper.selectAll();
            for (int i = 0; i < students.size(); i++) {
                System.out.println(students.get(i));
            }
            System.out.println("=============开始不同 Sqlsession 的第二次查询============");
            // 从新创建一个 sqlSession2 进行第二次查询
            sqlSession2 = sqlSessionFactory.openSession();
            StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
            List<Student> stus = studentMapper2.selectAll();
            // 不相等
            Assert.assertNotEquals(students, stus);
            for (int i = 0; i < stus.size(); i++) {
                System.out.println("stus:" + stus.get(i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (sqlSession1 != null) {
                sqlSession1.close();
            }
            if (sqlSession2 != null) {
                sqlSession2.close();
            }
        }
    }

 执行上面代码,进行了两次查询, 分别使用SqlSession1和SqlSession2进行相同的查询,得到以下结果

 从日志中可以看到两次查询都分别从数据库中取出了数据。 虽然结果相同, 但两个是不同的对象。

1.4. 测试3-刷新缓存

刷新缓存是清空这个 SqlSession 的所有缓存, 不单单是某个键。

StudentMapper.java 添加selectByPrimaryKey()方法

package com.fang.mybatis.mapper;

import com.fang.mybatis.entity.Student;

import java.util.List;

public interface StudentMapper {

    /**
     *
     * @return
     */
    List<Student> selectAll();

    Student selectByPrimaryKey(int id);
}

 StudentMapper.xml添加下面配置,如果没有配置 flushCache=“true”,结果还是第二个不发 SQL 语句。

<select id="selectByPrimaryKey" flushCache="true" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from student
        where student_id=#{student_id, jdbcType=INTEGER}
</select>

 StudentMapperTest.java 添加下面代码

@Test
    public void sameSqlSessionNoCache() {
        SqlSession sqlSession = null;
        try {
            sqlSession = sqlSessionFactory.openSession();

            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            // 执行第一次查询
            Student student = studentMapper.selectByPrimaryKey(1);
            System.out.println("=============开始同一个 Sqlsession 的第二次查询============");
            // 同一个 sqlSession 进行第二次查询
            Student stu = studentMapper.selectByPrimaryKey(1);
            Assert.assertEquals(student, stu);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }

 执行上面代码,得到以下结果,第一次, 第二次都发送了 SQL 语句, 同时, 断言两个对象相同出错。

 1.5. 总结

1.在同一个 SqlSession 中, Mybatis 会把执行的方法参数通过算法生成缓存的键值, 将键值和结果存放在一个 Map 中, 如果后续的键值一样, 则直接从 Map 中获取数据;
2.不同的 SqlSession 之间的缓存是相互隔离的;
3.用一个 SqlSession, 可以通过配置使得在查询前清空缓存;
4.任何的 UPDATE, INSERT, DELETE 语句都会清空缓存。

2. 二级缓存

2.1. 什么是二级缓存

二级缓存存在于 SqlSessionFactory 生命周期中。

2.2. 配置二级缓存

在 mybatis 中, 二级缓存有全局开关分开关。全局开关默认开启,一般配置是为了方便团队知道已经使用了二级缓存。

2.2.1. 全局开关

全局开关, 在 mybatis-config.xml 中如下配置,默认是为 true, 即默认开启总开关。

<settings>
  <!--全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 -->
  <setting name="cacheEnabled" value="true"/>
</settings>

2.2.2. 分开关

分开关就是说在 *Mapper.xml 中开启或关闭二级缓存, 默认是不开启的,如果要使用二级缓存cache标签一定要配置。

若要使用进行如下配置:

<!--在当前 Mapper.xml文件开启二级缓存-->
<cache/>

或者自定义cache标签参数

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

eviction:清除策略为FIFO缓存,先进先出原则,默认的清除策略是 LRU
flushInterval:属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量
size:最多可以存储结果对象或列表的引用数
readOnly:只读属性,可以被设置为 true 或 false。

2.2.3. 代码配置

1. StudentMapper.xml 进行如下配置:

<cache readOnly="false"/>

 StudentMapper.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="com.fang.mybatis.mapper.StudentMapper">
    <cache readOnly="false"/>
    <resultMap id="BaseResultMap" type="com.fang.mybatis.entity.Student">
        <id column="student_id" jdbcType="INTEGER" property="studentId" />
        <result column="name" jdbcType="VARCHAR" property="name" />
        <result column="phone" jdbcType="VARCHAR" property="phone" />
        <result column="email" jdbcType="VARCHAR" property="email" />
        <result column="sex" jdbcType="TINYINT" property="sex" />
        <result column="locked" jdbcType="TINYINT" property="locked" />
        <result column="gmt_created" jdbcType="TIMESTAMP" property="gmtCreated" />
        <result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified" />
    </resultMap>

    <sql id="Base_Column_List">
      student_id, name, phone, email, sex, locked, gmt_created, gmt_modified
    </sql>

    <select id="selectAll" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from student
    </select>

    <select id="selectByPrimaryKey"  parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from student
        where student_id=#{student_id, jdbcType=INTEGER}
    </select>

</mapper>

 2.实体类实现序列化

Student 实现序列化接口

package com.fang.mybatis.entity;

import java.io.Serializable;
import java.util.Date;

public class Student implements Serializable {
    private static final long serialVersionUID = -4852658907724408209L;

    private Integer studentId;

    private String name;

    private String phone;

    private String email;

    private Byte sex;

    private Byte locked;

    private Date gmtCreated;

    private Date gmtModified;
    /**
     * 以下部分为setter和getter, 省略
     */
    public Integer getStudentId() {
        return studentId;
    }

    public void setStudentId(Integer studentId) {
        this.studentId = studentId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        //this.name = (name == null) ? null : name.trim();
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
        //this.phone = (phone == null) ? null : phone.trim();
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
        //this.email = (email == null) ? null : email.trim();
    }

    public Byte getSex() {
        return sex;
    }

    public void setSex(Byte sex) {
        this.sex = sex;
    }

    public Byte getLocked() {
        return locked;
    }

    public void setLocked(Byte locked) {
        this.locked = locked;
    }

    public Date getGmtCreated() {
        return gmtCreated;
    }

    public void setGmtCreated(Date gmtCreated) {
        this.gmtCreated = gmtCreated;
    }

    public Date getGmtModified() {
        return gmtModified;
    }

    public void setGmtModified(Date gmtModified) {
        this.gmtModified = gmtModified;
    }

    @Override
    public String toString() {
        return "Student{" +
                "studentId=" + studentId +
                ", name='" + name + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                ", sex=" + sex +
                ", locked=" + locked +
                ", gmtCreated=" + gmtCreated +
                ", gmtModified=" + gmtModified +
                '}';
    }
}

3. StudentMapperTest.java 添加下面代码

 @Test
    public void secendLevelCacheTest() {

        // 获取 SqlSession 对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //  获取 Mapper 对象
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 使用 Mapper 接口的对应方法,查询 id=2 的对象
        Student student = studentMapper.selectByPrimaryKey(2);
        // 更新对象的名称
        student.setName("奶茶");
        // 再次使用相同的 SqlSession 查询id=2 的对象
        Student student1 = studentMapper.selectByPrimaryKey(2);
        /* Assert.assertEquals("奶茶", student1.getName());
        // 同一个 SqlSession 使用缓存, 则得到的对象都一样的
        Assert.assertEquals(student, student1);*/

        sqlSession.close();

        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        Student student2 = studentMapper1.selectByPrimaryKey(2);
        Student student3 = studentMapper1.selectByPrimaryKey(2);
        Assert.assertEquals("奶茶", student2.getName());
        Assert.assertNotEquals(student3, student2);

        sqlSession1.close();
    }

 执行上面代码,得到以下结果

 


 

第一阶段:

1.在第一个 SqlSession 中, 查询出 student 对象, 此时发送了 SQL 语句;
2.student更改了name 属性;(先查二级缓存,再查一级缓存,再查数据库;即使在一个sqlSession中,也会先查二级缓存)
3.SqlSession 再次查询出 student1 对象, 此时不发送 SQL 语句, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是没有命中。 然后查一级缓存,在一级缓存中查到了。
4.由于是一级缓存, 因此, 此时两个对象是相同的。
5.调用了 sqlSession.close(), 此时将数据序列化并保持到二级缓存中。

第二阶段:

1.新创建一个 SqlSession 对象;
2.查询出 student2 对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时是第 3 次3.进行查询,但只有一个命中, 因此 命中率 1/3=0.333333;

4.查询出 student3 对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时是第4次进行查询,这次加上一次次命中共两次, 因此 命中率 2/4=0.5;
5.由于 readOnly=“true”, 因此 student2 和 student3 都是反序列化得到的, 为相同的实例。
 

2.2.4. cache 标签配置详解

查看 dtd 文件, 可以看到如下约束:

<!ELEMENT cache (property*)>
<!ATTLIST cache
type CDATA #IMPLIED
eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED
>

从中可以看出:

cache 中可以出现任意多个 property子元素;
cache 有一些可选的属性 type, eviction, flushInterval, size, readOnly, blocking.

1.type

type 用于指定缓存的实现类型, 默认是PERPETUAL, 对应的是 mybatis 本身的缓存实现类 org.apache.ibatis.cache.impl.PerpetualCache。

后续如果我们要实现自己的缓存或者使用第三方的缓存, 都需要更改此处。

2.eviction

eviction 对应的是回收策略, 默认为 LRU。
LRU: 最近最少使用, 移除最长时间不被使用的对象。
FIFO: 先进先出, 按对象进入缓存的顺序来移除对象。
SOFT: 软引用, 移除基于垃圾回收器状态和软引用规则的对象。
WEAK: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象。

3.flushInterval

flushInterval 对应刷新间隔, 单位毫秒, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。
如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据。

4.size

size 对应为引用的数量,即最多的缓存对象数据, 默认为 1024。

5.readOnly

readOnly 为只读属性, 默认为 false
false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。
true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。

6.blocking

blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache 进行封装。
使用 BlockingCache 会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。

 

总结

由于在更新时会刷新缓存, 因此需要注意使用场合:查询频率很高, 更新频率很低时使用, 即经常使用 select, 相对较少使用delete, insert, update。
缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响。但刷新缓存是刷新整个 namespace 的缓存, 也就是你 update 了一个, 则整个缓存都刷新了。最好在 「只有单表操作」 的表的 namespace 使用缓存, 而且对该表的操作都在这个 namespace 中。 否则可能会出现数据不一致的情况。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis 缓存是一个常见的面试题。下面是一些常见的 MyBatis 缓存面试问题和答案: 1. 什么是 MyBatis 缓存? MyBatis 缓存是一个用于提高系统性能的机制,它可以在查询数据库时缓存结果,并在后续的相同查询中直接返回缓存的结果,减少数据库访问次数。 2. MyBatis 缓存有哪些类型? MyBatis 缓存有两种类型:一级缓存(本地缓存)和二级缓存(全局缓存)。 3. 什么是一级缓存?如何开启和关闭一级缓存? 一级缓存是 MyBatis 默认开启的,它是在 SqlSession 的生命周期内有效的,可以通过配置文件或编程方式关闭一级缓存。 4. 什么是二级缓存?如何开启和关闭二级缓存? 二级缓存是全局共享的缓存,可以被多个 SqlSession 共享。要开启二级缓存,需要在 MyBatis 配置文件中进行相应的配置。关闭二级缓存也是在配置文件中设置。 5. MyBatis 的二级缓存如何实现缓存更新和失效? MyBatis 的二级缓存使用了基于触发器的机制来实现缓存更新和失效。当数据发生变化时,会触发相应的更新操作,保证缓存数据的一致性。 6. MyBatis 缓存的实现原理是什么? MyBatis 缓存的实现原理是通过将查询结果缓存在内存中,使用一个 Map 结构来存储查询结果。当需要查询时,首先检查缓存是否存在对应的结果,如果存在则直接返回缓存结果,否则执行数据库查询操作,并将结果放入缓存。 这些问题涵盖了 MyBatis 缓存的基本概念和实现原理。希望对你有所帮助!如果你还有其他问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值