zoukankan      html  css  js  c++  java
  • MyBatis缓存机制学习(一级缓存,二级缓存,二级缓存击中情况)

    简介

    MyBatis是常见的Java数据库访问层框架。

    一级缓存介绍

    在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

     
     

    每个SqlSession中持有了Executor(译:执行器),每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement(译:映射声明),在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户

    一级缓存配置

    如何使用MyBatis一级缓存。开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

     
    <setting name="localCacheScope" value="SESSION"/>
    

    一级缓存实验

    创建示例表student,创建对应的POJO类和增改的方法

     
    DROP TABLE IF EXISTS `student`;
    
    CREATE TABLE `student` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
      `age` tinyint(3) unsigned DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
    
    LOCK TABLES `student` WRITE;
    
    INSERT INTO `student` (`id`, `name`, `age`) VALUES (1,'点点',16),(2,'平平',16),(3,'美美',16),(4,'团团',16);
    
    UNLOCK TABLES;
    

    实验1

    开启一级缓存,范围为会话级别,调用三次getStudentById,代码如下所示

     
    public void getStudentById() throws Exception {
            SqlSession sqlSession = factory.openSession(true); // 自动提交事务
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            System.out.println(studentMapper.getStudentById(1));
            System.out.println(studentMapper.getStudentById(1));
            System.out.println(studentMapper.getStudentById(1));
        }
    
     

    只有第一次真正查询了数据库,后续的查询使用了一级缓存。

    实验2

    增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。

     
    @Test
    public void addStudent() throws Exception {
            SqlSession sqlSession = factory.openSession(true); // 自动提交事务
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            System.out.println(studentMapper.getStudentById(1));
            System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
            System.out.println(studentMapper.getStudentById(1));
            sqlSession.close();
    }
    
     

    在修改操作后执行的相同查询,查询了数据库,一级缓存失效。

    实验3

    开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。

     
    @Test
    public void testLocalCacheScope() throws Exception {
            SqlSession sqlSession1 = factory.openSession(true); 
            SqlSession sqlSession2 = factory.openSession(true); 
    
            StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
            StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    
            System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
            System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
            System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据");
            System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
            System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }
    
     

    sqlSession2更新了id为1的学生的姓名,从凯伦改为了小岑,但session1之后的查询中,id为1的学生的名字还是凯伦,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话内部共享。

    一级缓存工作流程

     

     
    核心类

    SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。
    Executor(执行器): SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。
    BaseExecutor: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
    Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后发现Local Cache是BaseExecutor内部的一个成员变量,如下代码所示。
    Cache: MyBatis中的Cache接口,提供了和缓存相关的最基本的操作

    过程描述

    1.首先需要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession
    2.在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数
    3.SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList
    4.SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。
    5.在上述代码中,会先根据传入的参数生成CacheKey
    6.将MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。
    7.CacheKey内容有:首先是成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中,同时重写了CacheKey的equals方法
    8.除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。

     
    Statement Id + Offset + Limmit + Sql + Params
    

    9.BaseExecutor的query方法继续往下走,如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。
    10.SqlSession的insert方法和delete方法,都会统一走update的流程.每次执行update前都会清空localCache。

    一级缓存的总结

    MyBatis一级缓存的生命周期和SqlSession一致。
    MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
    MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement

    二级缓存

    1.MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
    2.MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
    3.在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

    什么时候才能命中二级缓存,什么时候才能存到二级缓存里

    如果二级缓存想要命中实现,则必须要将上一次sqlSession commit之后才能生效,不然将不会命中,原因:两个不同的session必须提交前面一个session才能缓存生效的原因是因为mybatis的缓存会被一个transactioncache类包装住,所有的cache.putObject全部都会被暂时存到一个map里,等事务提交以后,这个map里的缓存对象才会被真正的cache类执行putObject操作。这么设计的原因是为了防止事务执行过程中出异常导致回滚,如果get到object后直接put进缓存,万一发生回滚,就很容易导致mybatis缓存被脏读




  • 相关阅读:
    领扣(LeetCode)七进制数 个人题解
    ie固定table单元格宽度
    js 阻止冒泡
    在jsp页面下, 让eclipse完全支持HTML/JS/CSS智能提示(转)
    WebStorm 6.0 与 7.0 注册码
    统制Highcharts中x轴和y轴坐标值的密度
    ie版本
    flash透明 处于最低
    eclipse svn --
    jquery---- 数组根据值进行删除
  • 原文地址:https://www.cnblogs.com/albertzhangyu/p/12931556.html
Copyright © 2011-2022 走看看