Mybatis相关:一级缓存&二级缓存&脏数据的产生

1.Mybatis的缓存

缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate一样,Mybatis也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

特点:缓存数据不可靠(可能会丢失),存放热点数据(经常使用的数据)

优点:缓存大部分存在内存中,查询速度快

  • 一级缓存基于SqlSession,
  • 二级缓存基于namespace,同一个namespace下所有数据可以共享

https://blog.csdn.net/zitian246/article/details/109086027这篇文章中介绍配置文件时曾说明namespace与二级缓存相关

一个mapper.xml用自己的namespace,另外也可以多个mapper(dao接口)共享同一个二级缓存namespace,即namespace取名可以相同

一级缓存与二级缓存调用顺序:

          获取数据先去二级缓存获取,如果有得到,写入(更新)一级缓存

          若没有得到,再去一级缓存,如果仍没有,再去数据库

          优先级: 二级缓存--->一级缓存--->数据库

2.一级缓存

一级缓存:基于SqlSession默认就是开启的

特点:

  • 查询第一次时,获取到数据写入一级缓存,再次查询时从缓存获取,不再执行sql语句
  • 若当前SqlSession发生修改、增加、删除动作时,就会立即把当前缓存的所有数据清空
  • 对于查询操作,只要SqlSession没有调用flush或者close方法,它就一直存在
 @Test //一级缓存
    public void firstLevelCacheTest(){
        //查询所有时 可以缓存数据 但是一旦发生增加 或者删除 修改 就要清空
        iStudentDao.findAllStudentWithScoreLazy();

        //查询第一次 有执行sql select * from student_tb where id = ?
        Student student1 = iStudentDao.findStudentById(2);
        System.out.println("student:" + student1);

        //查询第二次 没有执行sql 没有去数据库查数据 从一级缓存sqlSession获取数据
        Student student2 = iStudentDao.findStudentById(2);
        System.out.println("student:" + student2);

        student2.setAge(22);
        int num = iStudentDao.upateStudent(student2);
        System.out.println("num:"+num);

        //再次查询数据 需要去数据库查数据 为什么??
        //应为 当前sqlSession 发生 修改、删除、增加动作时,就会把当前缓存的所有数据清空
        Student student3 = iStudentDao.findStudentById(2);
        System.out.println("student3:"+student3);

        iStudentDao.findAllStudentWithScoreLazy();
    }

3.二级缓存

二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。(也可以说是,在同一namespace下,共享一块缓存空间,如果多个mapper (dao.xml)共享同一namesapce 则也共享一块缓存,二级缓存是跨sqlsession,多个sqlsession可以去二级缓存获取数据。即可以针对同一个dao接口或者同一个命名空间(namespace)创建多个SqlSession )

特点:

  • 只要发生增删改,就会将·同一命名空间(namespace)下的缓存清空
  • 使用二级缓存实体类必须实现序列化,否则报错
  • 使用查询语句,默认只写入一级缓存,只有调用close(),commit()方法,才会将数据提交到二级缓存,其他的sqlsession才能拿到,不再执行sql语句

3.1实现

     1.开启二级缓存(mybatis配置文件中开启)

 <settings>
        <!--开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

     2.在mapper的命名空间下配置缓存(xxx.xml)

<!--配置缓存
        1.开启<cache></cache>
        2.配置参数
        - flushinterval` 缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值
- `readOnly`: 是否只读;**true 只读**,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。**读写(默认)**:MyBatis 觉得数据可能会被修改
- `size` : 缓存存放多少个元素
- `type`: 指定自定义缓存的全类名(实现Cache 接口即可)
- `blocking`: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
    -->
   <cache flushInterval="10000" eviction="LRU" size="1000" readOnly="false" blocking="false"></cache>

eviction:缓存回收策略,有如下几种回收策略:

  • LRU(常用,默认) -最近最少回收,移除最长时间不被使用的对象
  • FIFO(不常用)-先进先出,按照缓存进入的顺序来移除他们
  • SOFT(了解)-软引用,移除基于垃圾回收器状态和软引用规则的对象
  • WEAK(了解)-弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象

默认是 LRU 最近最少回收策略

  • flushinterval 缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值

  • readOnly: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改

  • size : 缓存存放多少个元素

  • type: 指定自定义缓存的全类名(实现Cache 接口即可)

  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

   3.使用

    对于sql语句默认就开启了,不用另外配置,也可在此关闭缓存,这一条sql不使用不缓存

    <!--
        useCache="true" 使用缓存 一般不需要配置 默认就开启true        
    -->
    <select id="findStudentById" resultType="Student" useCache="true" >
        select * from  student_tb where id = #{id}
    </select>

     4.测试

 @Test  //二级缓存
    public void secondLevelCacheTest(){
        //查询第一次  执行sql语句select * from student_tb where id = ?
        Student student1 = iStudentDao.findStudentById(2);
        System.out.println("student:" + student1);

        //查询第二次,没有执行sql 从一级缓存sqlSession中获取,因为二级缓存中没有
        Student student2 = iStudentDao.findStudentById(2);
        System.out.println("student2:"+student2);

        //如果不执行这句话  下面的代码仍会执行sql
        //调用close(),commit()方法 才会将数据提交到二级缓存,其他的sqlsession才能拿到
        //sqlSession.close();
        sqlSession.commit();

        // studentDao2   studentDao 来自于不同的sqlSession
        iStudentDao2 = sqlSessionFactory.openSession(true).getMapper(IStudentDao.class);
        Student student3 = iStudentDao2.findStudentById(2);
        System.out.println("student3:"+student3);

    }

4.一级缓存与二级缓存的调用顺序与区别(重要)

一级缓存 在sqlSession 中
二级缓存 多个sqlSession 可以二级缓存 二级缓存是以namesapce 进行划分 多个mapper 可以共享一个二级缓存

调用顺序:先查二级缓存,再查一级缓存,最后查数据库

共同点:查询缓存数据 , 只要发生增删改 立即将 一级二级全部清除

5.问题的产生,脏数据

从上面的分析,我们可以知道缓存的调用顺序,试想一下这种情况:

使用二级缓存,当有一个session发生修改时,将二级缓存清空了,然而另一个session缓存过以前的查询结果(此时二级缓存没数据,去一级缓存却拿到了之前的),则可能产生脏数据。

如上图:

  1. 第一步sqlsession1 去查询id=1的数据,并写入了sqlsession1的一级缓存与共同的二级缓存
  2. 第二步sqlsession2 去查询id=1的数据,发现二级缓存有数据,就不执行sql,并将其写入sqlsession2的一级缓存
  3. 第三步sqlsession2执行修改id=1的数据,清空了二级缓存sqlsession2的一级缓存
  4. 第四步sqlsession1 再去查询id=1的数据,二级缓存没有,但却从sqlsession1的一级缓存读到了数据,但这却是修改之前的,就产生了脏数据
public class DirtyData {

    public static void main(String[] args) throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //获取SqlSession就是一个会话连接 mybatis默认是开启事务的
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);

        IStudentDao iStudentDao1 = sqlSession1.getMapper(IStudentDao.class);
        System.out.println("使用sqlSession1查询");
        Student student1 = iStudentDao1.findStudentById(2);
        System.out.println("student1:" + student1);


        //sqlSession1.commit();


        //从二级缓存中读取不再执行sql
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
        IStudentDao iStudentDao2 = sqlSession2.getMapper(IStudentDao.class);
        System.out.println("使用sqlSession2 查询");
        Student student2 = iStudentDao2.findStudentById(2);
        System.out.println("student2:"+student2);


        student2.setAge(222);
        iStudentDao2.upateStudent(student2);
        //提交,使二级缓存失效
        //sqlSession2.commit();

        //因为二级缓存失效  iStudentDao1先去二级缓存存取,失效后用一级缓存
        //此时能拿到数据但不是 session2修改后的最新数据,所以是脏数据
        student1 = iStudentDao1.findStudentById(2);
        System.out.println("student1:" + student1);
    }
}

5.1解决方案1:关闭一级缓存,并将查询的sqlsession改为自动提交

在spring中也不一定发生,创建时一般为单例的mapper,每次查询都会创建新的sqlsession(若开启事务则是同一sqlsession),所以可以避免,不用担心。

  public static void main(String[] args) {

        InputStream inputStream = null;
        SqlSession sqlSession1 = null;
        SqlSession sqlSession2 = null;
        try {
            // 1.读取配置文件
            inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");

            // 2.配置连接功工厂
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

            // 3.获取sqlSession 就是一个会话 连接  mybatis 默认是开启事务的
            sqlSession1 = sqlSessionFactory.openSession(true);
            IStudentDao iStudentDao1 = sqlSession1.getMapper(IStudentDao.class);
            System.out.println("使用sqlSession1 查询");
             Student student1 =  iStudentDao1.findStudentById(63);
            System.out.println("student1:"+student1);



            //sqlSession1.close();




            sqlSession2 = sqlSessionFactory.openSession();
            IStudentDao iStudentDao2 = sqlSession2.getMapper(IStudentDao.class);
            System.out.println("使用sqlSession2 查询");
            Student student2 = iStudentDao2.findStudentById(63);
            System.out.println("student2:"+student2);


            student2 = iStudentDao2.findStudentById(63);
            System.out.println("student2:"+student2);

            // 在 session2中修改数据  此时二级缓存失效
            student2.setHeight(333);
            iStudentDao2.updateStudent(student2);
            sqlSession2.commit();

            Thread.sleep(4000);

            // 因为二级缓存实现,iStudentDao1 先去二级缓存取,失效改用取一级缓存,此时能拿到数据但是不是    session2中修改数据的最新数据产生 脏数据
           Student student3 =  iStudentDao1.findStudentById(63);
           System.out.println("student3:"+student3);



        } catch (Exception e) {
            e.printStackTrace();
            // 有异常回滚
            sqlSession1.rollback();

        }finally {
            // 5.释放资源
            sqlSession1.close();
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

5.2解决方案2:不使用缓存

<select id="findStudentById" parameterType="java.lang.Integer" resultType="com.baidu.entity.Student"  useCache="false" >

        <include refid="selectAll"></include> where id = #{id}

    </select>

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值