您的位置:首页 > 其它

如何一文快速了解 Mybatis 的缓存机制

2020-04-21 20:22 686 查看

文章目录

  • 2.2、一级缓存分析
  • 2.3、测试一级缓存的清空
  • 3、二级缓存工作原理
  • 3.3、测试二级缓存
  • 4、二级缓存应用场景
  • 5、二级缓存局限性:
  • 1、Mybatis 缓存机制概述

      像大多数的持久化框架一样,Mybatis 也提供了缓存机制,通过缓存机制来减少对数据库的查询次数,从而提高系统性能。

    • 什么是缓存:存在于内存中的临时数据。

    • 适用于缓存的数据:经常查且不经常改,最重要的是数据的正确与否对最终结果影响不大的。
               如:金融相关的国际汇率等数据如果缓存在内存中,而与数据库不同步的话,就会对国际金融造成很大的影响。
               如:网页页面上的点赞数,只要实现了最终一致性,则短期内地出现与数据库不同步的现象,不会造成很大的影响,

    • 不适用于缓存的数据:经常改变的数据。数据的正确与否对最终结果影响很大的。
               如:商品的库存、银行的汇率、股市的牌价。
        Mybatis 中的缓存如下分为一级缓存,二级缓存。

    2、一级缓存工作原理

    2.1、证明一级缓存的存在

      一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。

    2.1.1、编写用户持久层 Dao 接口

    /**
    *
    * <p>Title: IUserDao</p>
    `* <p>Description: 用户的业务层接口</p>
    */
    public interface IUserDao {
    /**
    * 根据 id 查询
    * @param userId
    * @return
    */
    User findById(Integer userId);
    }

    2.1.2 编写用户持久层映射文件

      在statement中设置useCache=true,开启本条sql查询的缓存读取

    <?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="per.cjh.dao.IUserDao">
    <!-- 根据 id 查询 -->
    <select id="findById" resultType="UsEr" parameterType="int" useCache="true">
    select * from user where id = #{uid}
    </select>
    </mapper>

    2.1.3、编写测试方法

    /**
    *
    * <p>Title: MybastisCRUDTest</p>
    * <p>Description: 一对多的操作</p>
    */
    public class UserTest {
    private InputStream in ;
    private SqlSessionFactory factory;
    private SqlSession session;
    private IUserDao userDao;
    @Test
    public void testFindById() {
    //6.执行操作
    User user = userDao.findById(41);
    System.out.println("第一次查询的用户:"+user);
    User user2 = userDao.findById(41);
    System.out.println("第二次查询用户:"+user2);
    System.out.println(user == user2);
    }
    @Before//在测试方法执行之前执行
    public void init()throws Exception {
    //1.读取配置文件
    in = Resources.getResourceAsStream("SqlMapConfig.xml");
    //2.创建构建者对象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    //3.创建 SqlSession 工厂对象
    factory = builder.build(in);
    //4.创建 SqlSession 对象
    session = factory.openSession();
    //5.创建 Dao 的代理对象
    userDao = session.getMapper(IUserDao.class);
    }
    @After//在测试方法执行完成之后执行
    public void destroy() throws Exception{
    //7.释放资源
    session.close();
    in.close();
    }
    }

    测试结果如下:

    方法调用如下:

      从上面的方法调用和运行结果我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是 Mybatis 提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id为41的记录时,并没有发出sql语句从数据库中查询数据,而是从一级缓存中查询。

    2.2、一级缓存分析

      一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供的一块区域中。该区域的结构是一个Map。当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否有,有的话直接拿出来用。
      当SqlSession对象被销毁时,该区域即缓存消失了。

      第一次发起查询sql查询用户id为1的用户,先去找缓存中是否有id为1的用户,如果没有,再去数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
      如果sqlsession执行了commit操作(插入,更新,删除),会清空sqlsession中的一级缓存,避免脏读
      第二次发起查询id为1的用户,缓存中如果找到了,直接从缓存中获取用户信息
    mybatis默认支持一级缓存。

    2.3、测试一级缓存的清空

    /**
    * 测试一级缓存的清空
    */
    @Test
    public void testFirstLevelCache(){
    User user1 = userDao.findById(41);
    System.out.println(user1);
    // 清空一级缓存方法1:sqlSession.close();
    //再次获取 SqlSession 对象
    // 清空一级缓存方法2:sqlSession = factory.openSession();
    // 清空一级缓存方法3:sqlSession.clearCache();
    sqlSession.clearCache();//清空一级缓存方法3
    userDao = sqlSession.getMapper(IUserDao.class);
    User user2 = userDao.findById(41);
    System.out.println(user2);
    System.out.println(user1 == user2);
    }
    /**
    * 测试缓存的同步
    */
    @Test
    public void testClearlCache(){
    //1.根据 id 查询用户
    User user1 = userDao.findById(41);
    System.out.println(user1);
    //2.更新用户信息
    user1.setUsername("update user clear cache");
    user1.setAddress("北京市海淀区");
    userDao.updateUser(user1);
    //3.再次查询 id 为 41 的用户
    User user2 = userDao.findById(41);
    System.out.println(user2);
    System.out.println(user1 == user2);
    }

      分析:当执行sqlSession.close()后,再次获取sqlSession并查询id=41的User对象时,又重新执行了sql语句,从数据库进行了查询操作。当两次相同查询中间有更新语句时,前后两次获得的对象引用不同,即一级缓存已经被清除。

    3、二级缓存工作原理

      二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

    3.1、二级缓存结构图

      sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。
      如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。
      sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
    在mybatis的主配置文件设置二级缓存开关 , 还要在具体的接口mapper.xml中开启二级缓存

    3.2、二级缓存的开启和关闭

    mybatis二级缓存需要手动开启。

    3.2.1、在Mybatis主配置文件开启二级缓存

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

      因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。为 true 代表开启二级缓存;为false 代表不开启二级缓存。

    3.2.2、需要将映射的javapojo类实现序列化接口

      当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,目的是为了通过序列化的方式在缓存中保存对象。如:class Student implements Serializable{ }

    3.2.3、配置相关的Mapper映射文件

      于需要开启二级缓存的Mapper配置文件配置下面信息:

    <!--开启本Mapper的namespace下的二级缓存-->
    <cache eviction="LRU" flushInterval="10000"/>

    cache标签的4个属性简介:
      1、eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
       (1) LRU(Least Recently Used),最近最少使用的,最长时间不用的对象。
       (2) FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们。
       (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象。
       (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,移除最长时间不用的对形象。
      2、flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当SQL被执行的时候才会去刷新缓存。
      3、size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
      4、readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,他的默认值是false,不允许我们修改。

    3.2.4、配置 statement 上面的 useCache 属性

    <!-- 根据 id 查询 -->
    <select id="findById" resultType="user" parameterType="int" useCache="true">
    select * from user where id = #{uid}
    </select>

      将 UserDao.xml 映射文件中的标签中设置 useCache=”true”代表当前这个 statement 要使用二级缓存,如果不使用二级缓存可以设置为 false。
      注意:针对每次查询都需要最新数据的 sql,要设置成 useCache=false,禁用该条sql从二级缓存取数据。

    3.3、测试二级缓存

    public class SecondLevelCacheTest {
    private InputStream in;
    private SqlSessionFactory factory;
    @Before//用于在测试方法执行之前执行
    public void init()throws Exception{
    //1.读取配置文件,生成字节输入流
    in = Resources.getResourceAsStream("SqlMapConfig.xml");
    //2.获取 SqlSessionFactory
    factory = new SqlSessionFactoryBuilder().build(in);
    }
    @After//用于在测试方法执行之后执行
    public void destroy()throws Exception{
    in.close();
    }
    /**
    * 测试二级缓存
    */
    @Test
    public void testSecondLevelCache(){
    SqlSession sqlSession1 = factory.openSession();
    IUserDao dao1 = sqlSession1.getMapper(IUserDao.class);
    User user1 = dao1.findById(41);
    System.out.println(user1);
    sqlSession1.close();//一级缓存消失
    
    SqlSession sqlSession2 = factory.openSession();
    IUserDao dao2 = sqlSession2.getMapper(IUserDao.class);
    User user2 = dao2.findById(41);
    System.out.println(user2);
    sqlSession2.close();
    System.out.println(user1 == user2);
    }
    }

      经过上面的测试,我们发现执行了两次查询,并且在执行第一次查询后,我们关闭了一级缓存,再去执行第二次查询时,我们发现并没有对数据库发出 sql 语句,所以此时的数据就只能是来自于我们所说的二级缓存。虽然数据来自于二级缓存,但是前后两次user1 和 user2的引用并不一样。可知二级缓存区域存放的是pojo对象序列化之后的数据,而不是存放对象。存放的数据类似于:{“id”:41,“username”:“陈乾”,“address”:“北京”}这样子的Map(JSON)类型。

    4、二级缓存应用场景

      对于访问多的查询请求并且用户对查询结果实时性要求不高的情况下,可采用mybatis二级缓存,降低数据库访问量,提高访问速度,如电话账单查询根据需求设置相应的flushInterval:刷新间隔时间,比如三十分钟,24小时等。

    5、二级缓存局限性:

      mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。

    • 点赞
    • 收藏
    • 分享
    • 文章举报
    cjh-gdAcm997 发布了37 篇原创文章 · 获赞 5 · 访问量 3869 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: