【Mybatis】一二级缓存的源码研究

Mybatis的一级缓存

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
在这里插入图片描述

一级缓存简介

1、默认开启
2、作用域: 一级缓存 是SqlSession 级别的 , 回话之间共享
3、增加 修改 默认为true , 查询默认为false, 在XML可以用 flushCache=true 来进行清空缓存
4、也就是说当执行update操作的时候, 一级缓存就失效了
5、用hashmap存储

一级缓存的缺点

一级缓存是SqlSession 级别的 , 也就是别的会话进行了update操作, 其他会话并不知道, 这是其他会话就读取的是脏数据
•线程不安全

一级缓存的生命周期有多长?

a、MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
b、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
c、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
d、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

怎么判断两次查询是完全相同的查询?

2.1 传入的statementId
2.2 查询时要求的结果集中的结果范围
2.3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
2.4 传递给java.sql.Statement要设置的参数值

验证Mybatis的一级缓存功能

    @Test
    public void testStart() throws IOException {
        //1、mybatis主配置文件
        String config = "mybatis-config.xml";
        //2、读取配置文件
        InputStream in = Resources.getResourceAsStream(config);
        //3、创建sqlSessionFactory对象,目的是获取sqlSession
        SqlSessionFactory factory =new SqlSessionFactoryBuilder().build(in);
        //4、获取SqlSession,SqlSession能执行sql语句
        SqlSession session = factory.openSession();
        //5、执行sqlSession的selectList()
        List<Object> studentList = session.selectList("com.bjpowernode.dao.StudentDao.selectStudents");
        List<Object> studentList2 = session.selectList("com.bjpowernode.dao.StudentDao.selectStudents");
        //6、循环输出查询结果
        studentList.forEach(student-> System.out.println(student));
        studentList2.forEach(student-> System.out.println(student));
        //7、关闭SqlSession,释放资源
        session.close();

    }

可以看出我们用的是一个sqlSession,现在我们打印日志

Created connection 1278852808.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
==>  Preparing: select id,name,email,age from student
==> Parameters: 
<==    Columns: id, name, email, age
<==        Row: 1, 梁佳宝, ljb@163.com, 25
<==        Row: 2, 马珂, make@163.com, 26
<==        Row: 3, 闫伟强, ywq@163.com, 21
<==        Row: 4, 赫于富, hyf@163.com, 22
<==        Row: 5, 吉莹, jy@163.com, 23
<==        Row: 6, 赵晓东, zxd@163.com, 24
<==      Total: 6
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
Returned connection 1278852808 to pool.

Process finished with exit code 0

可以从日志看出,就查了一次数据库

一级缓存源码分析

在这里插入图片描述
从localCache中获取数据也就是一级缓存

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

如果缓存中没有那么就从数据库中查询

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }
//查询后的值,放入到缓存中
        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }

二级缓存

SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开席需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了,

二级缓存简介

• 默认不开启
• 作用域: namespace 中共享 , 跨会话 跨线程
• sqlsession 在提交的时候才会存入二级缓存
• 二级缓存命中率更高
• 什么时候开启二级缓存
• 以查询为主的namespace , 因为增删改会频繁清空二级缓存
• 一般分布式 高并发的项目都使用第三方插件管理, 独立的服务器作为缓存服务器
• 存储缓存的也是hashmap

在这里插入图片描述

二级缓存开启与不开启对比

二级缓存不开启

   @Test
    public void testStart() throws IOException {
        //1、mybatis主配置文件
        String config = "mybatis-config.xml";
        //2、读取配置文件
        InputStream in = Resources.getResourceAsStream(config);
        //3、创建sqlSessionFactory对象,目的是获取sqlSession
        SqlSessionFactory factory =new SqlSessionFactoryBuilder().build(in);
        //4、获取SqlSession,SqlSession能执行sql语句
        SqlSession session = factory.openSession();
        SqlSession session2 = factory.openSession();
        //5、执行sqlSession的selectList()
        List<Object> studentList = session.selectList("com.bjpowernode.dao.StudentDao.selectStudents");
        List<Object> studentList2 = session.selectList("com.bjpowernode.dao.StudentDao.selectStudents");
        List<Object> studentList3 =session2.selectList("com.bjpowernode.dao.StudentDao.selectStudents");
        //6、循环输出查询结果
        studentList.forEach(student-> System.out.println(student));
        studentList2.forEach(student-> System.out.println(student));
        studentList3.forEach(student-> System.out.println(student));
        //7、关闭SqlSession,释放资源
        session.close();

    }
Created connection 1278852808.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
==>  Preparing: select id,name,email,age from student
==> Parameters: 
<==    Columns: id, name, email, age
<==        Row: 1, 梁佳宝, ljb@163.com, 25
<==        Row: 2, 马珂, make@163.com, 26
<==        Row: 3, 闫伟强, ywq@163.com, 21
<==        Row: 4, 赫于富, hyf@163.com, 22
<==        Row: 5, 吉莹, jy@163.com, 23
<==        Row: 6, 赵晓东, zxd@163.com, 24
<==      Total: 6
Opening JDBC Connection
Created connection 1431467659.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5552768b]
==>  Preparing: select id,name,email,age from student
==> Parameters: 
<==    Columns: id, name, email, age
<==        Row: 1, 梁佳宝, ljb@163.com, 25
<==        Row: 2, 马珂, make@163.com, 26
<==        Row: 3, 闫伟强, ywq@163.com, 21
<==        Row: 4, 赫于富, hyf@163.com, 22
<==        Row: 5, 吉莹, jy@163.com, 23
<==        Row: 6, 赵晓东, zxd@163.com, 24
<==      Total: 6
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4c39bec8]
Returned connection 1278852808 to pool.
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5552768b]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5552768b]
Returned connection 1431467659 to pool.

Process finished with exit code 0

从日志中可以看出进行了二次的查询

二级缓存开启

(1)对实体类进行序列化

@Data
public class Student implements Serializable {
    //属性名和列名一样
    private Integer id;
    private String name;
    private String email;
    private Integer age;
}

(2)在映射文件中开启二级缓存
在这里插入图片描述
(3)在mybatis-config.xml中开启二级缓存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
        <setting name="cacheEnabled" value="true" />
        .....
    </settings>
    ....
</configuration>

(4)

 @Test
    public void testSecondCache() throws IOException {
        //1、mybatis主配置文件
        String config = "mybatis-config.xml";
        //2、读取配置文件
        InputStream in = Resources.getResourceAsStream(config);
        //3、创建sqlSessionFactory对象,目的是获取sqlSession
        SqlSessionFactory factory =new SqlSessionFactoryBuilder().build(in);
        //4、获取SqlSession,SqlSession能执行sql语句
        SqlSession session = factory.openSession();
        //会话过程中第一次发送请求,从数据库中得到结构
        //得到结果之后,mybatis自动将这个查询结果放入到当前用户的一级缓存
        StudentDao studentMapper = session.getMapper(StudentDao.class);
        Student student = studentMapper.selectOne();
        System.out.println("第一次查询到的学生="+student);
        //触发MyBatis框架从当前一级缓存中的Dept对象保存到二级缓存
        session.commit();;
        //改成Session.close();效果相同
        SqlSession session2 = factory.openSession();
        StudentDao studentMapper2 = session2.getMapper(StudentDao.class);
        Student student1 = studentMapper2.selectOne();
        System.out.println("第二次查询得到学生对象="+student1);
        session2.commit();
    }

(5)日志查看

Created connection 249034932.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@ed7f8b4]
==>  Preparing: select id,name,email,age from student
==> Parameters: 
<==    Columns: id, name, email, age
<==        Row: 1, 梁佳宝, ljb@163.com, 25
<==        Row: 2, 马珂, make@163.com, 26
<==        Row: 3, 闫伟强, ywq@163.com, 21
<==        Row: 4, 赫于富, hyf@163.com, 22
<==        Row: 5, 吉莹, jy@163.com, 23
<==        Row: 6, 赵晓东, zxd@163.com, 24
<==      Total: 6
Cache Hit Ratio [com.bjpowernode.dao.StudentDao]: 0.0
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.bjpowernode.dao.StudentDao]: 0.3333333333333333
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)
Student(id=1, name=梁佳宝, email=ljb@163.com, age=25)
Student(id=2, name=马珂, email=make@163.com, age=26)
Student(id=3, name=闫伟强, email=ywq@163.com, age=21)
Student(id=4, name=赫于富, email=hyf@163.com, age=22)
Student(id=5, name=吉莹, email=jy@163.com, age=23)
Student(id=6, name=赵晓东, email=zxd@163.com, age=24)

二级缓存源码分析

在这里插入图片描述

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // 有没有配置全局缓存空间, 有没有声明缓存空间
        Cache cache = ms.getCache();
        if (cache != null) {
            //即使查询也会去情况缓存
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                //通过缓存管理器获取缓存中的值,为什么要传入cache 因为缓存管理器中有很多cache
                List<E> list = (List)this.tcm.getObject(cache, key);\
                //没有数据,然后查询数据库
                if (list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    //然后再次填充到暂存区
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

这个清理的话也是清理的暂存区(暂存区)

  private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) {
            this.tcm.clear(cache);
        }

    }

//通过缓存管理器获取缓存中的值,为什么要传入cache 因为缓存管理器中有很多cache

List<E> list = (List)this.tcm.getObject(cache, key);

直接从缓存中查找

    public Object getObject(Object key) {
        Object object = this.delegate.getObject(key);
        if (object == null) {
            // 防止缓存穿透
            this.entriesMissedInCache.add(key);
        }
//如果 标记为清除了,即使缓存中有也会返回Null
        return this.clearOnCommit ? null : object;
    }

如果缓存中没有查找要查询的数据,那么就要到数据库进行查找

list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

然后再次添加到暂存区

   this.tcm.putObject(cache, key, list);

当我们进行提交的时候,它会把暂存区的数据进行提交

    public void commit() {
        this.commit(false);
    }

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 29
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TGB-Earnest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值