Mybatis学习笔记-Mabatis缓存

2023-10-27

Mybatis学习笔记

Mybatis缓存

​ 缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度

​ MyBatis提供了对缓存的支持,分为一级缓存和二级缓存,可以通过一下图解来理解:

在这里插入图片描述

​ 一级缓存是SqlSession级别的缓存,默认开启,在操作数据库是要构造SqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的SqlSession之间的数据缓存区域是互相不影响的。

​ 二级缓存是Mapper级别的缓存,多个SqlSession去操作通一个Mapper的SQL语句,多个SqlSession可以公用二级缓存,二级缓存是跨SqlSession的。

一级缓存
  • 我们在一个SqlSession中,对User表查询所有用户进行两次查询,查看他们发出SQL语句的情况。

    
     @Test
        public void selectTest(){
            SqlSession sqlSession = sessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = userMapper.selectList();
            users.forEach(user -> {
                System.out.println(user);
            });
    
            List<User> users1 = userMapper.selectList();
            users1.forEach(user -> {
                System.out.println(user);
            });
        }
    

在这里插入图片描述

​ 从上面的SQL语句执行结果打印可以看到,我们执行了两次的selectList方法,但是只打印了一次SQL语句的执行日志,可见第二次执行selectList时取得是缓存中的数据。当缓存中没有数据的时候,会执行SQL语句获取结果,然后将数据放到缓存中,当第二次执行同一个SQL语句时,就会取到缓存中的数据。

  • 同样时对user表进行两次查询,只不过之间进行一次update操作

     @Test
        public void selectTest(){
            SqlSession sqlSession = sessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = userMapper.selectList();
            users.forEach(user -> {
                System.out.println(user);
            });
    
            User u1 = users.get(0);
            u1.setUsername(u1.getUsername()+"-update");
            userMapper.update(u1);
    
            List<User> users1 = userMapper.selectList();
            users1.forEach(user -> {
                System.out.println(user);
            });
        }
    

    在这里插入图片描述

可以看到,当我们执行了一次update方法后,再次执行selectList时,我们又执行了一次数据库查询,由此可见,更新数据库信息,会导致缓存失效

  • 一级缓存示意图
    在这里插入图片描述
  • 一级缓存总结:
    • 第一次发起数据查询时,SqlSession会先去缓存中找是否有需要的数据,如果没有,从数据库中查询,得到数据后,将数据存储到SqlSession一级缓存中。
    • 如果中间SqlSession执行了commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的是为了让缓存中存储的是最新的信息,避免了脏读。
    • 第二次发起数据查询时,同样会先去缓存中查找是否有数据,如果有则直接返回结果,没有则和第一次查询的操作一致。
一级缓存原理探究与源码分析

​ 一级缓存是到底是什么?一级缓存何时备创建?一级缓存的工作流程是怎么样的?

​ 我们知道一级缓存是SqlSession层次的缓存,所有我们可以从SqlSession中出发,看看有没有和缓存相关的属性或方法

在这里插入图片描述
​ 从SqlSession的所有方法中看到,只有一个clearCache()方法和缓存有点关系,那我们就从这个方法开始,看下SqlSession的缓存是怎么回事。

在这里插入图片描述

​ 从SqlSession开始,流程最后走到了PrepetualCahe#clear()方法中,在这里调用cache.clear()方法,翻阅PrepetualCache类可以看到,cache实际上就是一个Map对象,调用cache.clear()方法,其实就是调用的Map的clear()方法。

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public boolean equals(Object o) {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if (this == o) {
            return true;
        } else if (!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}

​ 那么一级缓存是何时创建的呢?

​ 我们在使用SqlSession进行数据操作时,最终执行SQL请求的是Excutor接口中的query()方法,而在Excutor接口的抽象实现类BaseExector的query方法中,有如下一段代码

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler 		resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

​ 从上面的代码中,我们看到,在query方法中,在执行具体实现类的query方法之前,创建了一个CacheKey对象,而cacheKey是通过参数MappedStatement参数,SQL参数,RowBounds参数和ResultHandler参数来创建的

 public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(rowBounds.getOffset());
            cacheKey.update(rowBounds.getLimit());
            cacheKey.update(boundSql.getSql());
           	//****
            return cacheKey;
        }
    }

再query方法中,CacheKey对象被当作参数向后传递,在quer方法的重载方法中,首先会根据这个key在本地缓存中获取一次数据,如果存在数据,则返回该数据,如果不存在则调用queryFromDatabase()方法,从数据库中获取数据

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
			...
            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;
            }
           ...
            return list;
        }
    }

而在queryFromDatabase方法中,将获取到的数据添加到了本地缓存中

 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;
    }

从上面的代码中可以看到,MyBatis的一级缓存,SqlSession缓存是在执行Sql语句之后,查询到结果集并完成封装之后创建的,同时也可以看出一级缓存的工作流程。

  • 一级缓存原理探究与源码分析总结:
    • 一级缓存的本质是一个HashMap对象
    • 一级缓存是在执行SQL语句之前,根据MappedStatemtnt参数,SQL参数,ResultHandler参数和RowBounds参数,创建了CacheKey,然后在SQL语句执行完成之后,将数据添加到了localCache中,即HashMap集合中
    • 一级缓存的工作流程从SqlSession->Excutor#query()->BaseExcutor#query()->BaseExcutor#queryFromDataBase()->PerpetualCache#putObject(),最后在putObject()方法中完成缓存数据的添加
二级缓存

​ 和一级缓存不同,二级缓存是需要我们手动开启的。但是二级缓存的原理和一级缓存一样,第一次查询,将数据放入缓存中,第二次查询则会直接从缓存中取。MyBatis的二级缓存是在Mapper层次,而一级缓存是SqlSession层次的,也就是说多个SqlSession可以共享一个mapper的二级缓存。同样的,当SqlSession执行插入、修改、删除等操作时,会清空Mapper二级缓存。

  • 如何使用二级缓存:

    • 开启二级缓存

      首先我们需要在MyBatis的配置文件中开启二级缓存

      <settings>
              <!-- 1. 显示SQL语句 -->
              <setting name="logImpl" value="STDOUT_LOGGING"/>
              <!--2、开启二级缓存-->
              <setting name="cacheEnabled" value="true"/>
      </settings>
      
    • 然后在Mapper.xml中开启缓存

      <!--开启二级缓存-->
      <cache></cache>
      

      在mapper.xml中,我们只提供了一个空标签,但其实这里是可以配置的。在Mybatis中,PerpetualCache这个类是Mybatis的默认实现缓存功能的类。我们不写cache标签的type参数,就是使用默认的缓存。同时我们也可以实现Cache接口,来实现自定义的二级缓存。同时用于二级缓存的存储介质多样化,不一定是只存在内存中,也有可能存在硬盘中,所以我们从二级缓存再取数据的时候,可能需要反序列化操作,因此,mybatis中的所有数据实体类pojo需要实现Serializable接口。

  • 测试二级缓存

    • 测试二级缓存和SqlSession无关

       	@Test
          public void mapperCacheTest() {
              SqlSession sqlSession = sessionFactory.openSession(true);
              SqlSession sqlSession2 = sessionFactory.openSession(true);
              UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
      
              List<User> users = userMapper.selectList();
              users.forEach(user -> {
                  System.out.println(user);
              });
              System.out.println("=====================================");
              sqlSession.close();
      
              List<User> users1 = userMapper2.selectList();
              users1.forEach(user -> {
                  System.out.println(user);
              });
          }
      

      控制台打印结果:

      在这里插入图片描述
      从控制台打印结果中可以看到,即使我们已经关闭了sqlSession,但是sqlSession2执行查询时乃未打印SQL查询语句。

    • 执行commit操作,验证二级缓存是否清空

      	@Test
          public void mapperCacheTest() {
              SqlSession sqlSession = sessionFactory.openSession(true);
              SqlSession sqlSession2 = sessionFactory.openSession(true);
              SqlSession sqlSession3 = sessionFactory.openSession(true);
              UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
              UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
      
              List<User> users = userMapper.selectList();
              users.forEach(user -> {
                  System.out.println(user);
              });
              System.out.println("=====================================");
              sqlSession.close();
      
              User u1 = users.get(0);
              u1.setUsername(u1.getUsername().replace("-update",""));
              userMapper3.update(u1);
      
      
              List<User> users1 = userMapper2.selectList();
              users1.forEach(user -> {
                  System.out.println(user);
              });
          }
      

      控制台打印结果:

    在这里插入图片描述
    从打印结果中,我们看到,执行完update语句后,我们再次查询时,又执行了一次select语句,由此可见,当我们进行了commit()操作后,Mapper中的二级缓存会被清空

  • 二级缓存整合redis

    上面我们使用的是mybatis自带的二级缓存,但是这个缓存是单服务器工作的,无法实现分布式缓存,即不同服务器不能共享缓存。为了解决这个问题,就得找一个分布式缓存,专门来存储缓存数据,这样不同得服务器得缓存数据都存在该处,缓存也从该处取。

    • mybatis与redis整合

      之前我们提到过,mybatis的默认缓存类是PrepetualCache,它实现了Cache接口,而我们如果要实现自己的缓存逻辑,我们也只需要实现Cache接口即可。PrepetualCache类的缓存逻辑,无法实现分布式缓存,因此,mybatis提供了一个针对Cache接口的redis实现类,该类存在于mybatis-redis包中

      • 实现

        • 引入jar包的坐标

          <dependency>
          	<groupId>org.mybatis.caches</groupId>
           	<artifactId>mybatis-redis</artifactId>
           	<version>1.0.0-beta2</version>
          </dependency>
          
        • 在Mapper.xml中配置cache标签的type属性值

          <!--开启二级缓存-->
          <cache type="org.mybatis.caches.redis.RedisCache"></cache>
          
        • redis配置 redis.properties

          注意,此处redis的配置文件的名称不能随意修改,固定未redis.properties

          host=192.168.2.107
          port=6379
          connectionTimeout=5000
          password=
          database=0
          
        • 测试

           	@Test
              public void redisCacheTest() {
                  SqlSession sqlSession = sessionFactory.openSession(true);
                  SqlSession sqlSession2 = sessionFactory.openSession(true);
                  SqlSession sqlSession3 = sessionFactory.openSession(true);
                  UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
                  UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
                  UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
          
                  List<User> users = userMapper.selectList();
                  users.forEach(user -> {
                      System.out.println(user);
                  });
                  System.out.println("=====================================");
                  sqlSession.close();
          
                  User u1 = users.get(0);
                  u1.setUsername(u1.getUsername().replace("-update", ""));
                  userMapper3.update(u1);
          
          
                  List<User> users1 = userMapper2.selectList();
                  users1.forEach(user -> {
                      System.out.println(user);
                  });
              }
          

          在这里插入图片描述

        可以看到关闭了进行了commit()操作后,我们进行第二次查询未发送SQL语句,此时第二次查询从redis中进行了获取

RedisCache源码分析

​ RedisCache是mybatis-redis提供的实现了Cache接口的类,实现了redis分布式缓存的逻辑。从RedisCache的构造方法中,我们可以看到,传入了一个id参数,这个id参数就是我们Mapper.xml中对用的namespace的属性;然后调用了RedisConfigurationBuilder来创建RedisConfig配置对象,在RedisConfiguraitonBuilder中,我们可以看到一些常量信息,包括redis.properties,这也是为什么redis的配置文件不能随便修改的原因。获取了RedisConfig配置后,就是获取JedisPool,redis的操作都是通过JedisPool中的jedis来实现的

 public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            this.id = id;
            RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
            pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
        }
    }
 private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();
    private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
    private static final String REDIS_RESOURCE = "redis.properties";
    private final String redisPropertiesFilename = System.getProperty("redis.properties.filename", "redis.properties");
 public void putObject(final Object key, final Object value) {
        this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
                return null;
            }
        });
    }

    public Object getObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
            }
        });
    }

然后我们可以看下,redisCache中的putObject()和getObject()方法,这两个方法中,详细的指出了,RedisCache的数据结构,就是哈希结构。

到这里,mybatis的缓存大概就学完了,若存在不正确的地方,麻烦各位大佬指正,谢谢~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Mybatis学习笔记-Mabatis缓存 的相关文章

随机推荐

  • Nginx的proxy_buff和slice分片结合导致内存无法回收

    Nginx域名配置文件 wrk压测内存的使用情况 内存一直被消耗占用 而且无法释放 入下图所示 解决方案 采用多域名的方式 采用多server调度的方式 一个用于做开启proxy buff 一个用于做切片
  • 无向图的最短路径----迪杰斯特拉算法----(包括:花费问题,物资问题,条数问题)

    迪杰斯特拉算法如果不熟悉的话从这里开始看 如果已经明白了迪杰斯特拉算法而想知道花费问题 城市之间的物资问题 最短路径条数问题的朋友可以往下翻 一 迪杰斯特拉算法讲解 算法思想是从起点开始 找到一条起点能到达顶点中的边权最小的那个点 然后从这
  • 小米手机侧边栏怎么打开_小米3怎样开启全能侧边栏 开启侧边栏教程

    有没有觉得手机经常要退出运行程序的界面才能进另一个应用界面呢 有木有感觉很麻烦 小编今天给大伙分享一个小米3开启快捷的全能侧边栏的小妙招 让你的操作不再繁琐 首先 需要下载一个小软件 全能侧边栏 apk 本方法不需要手机ROOT 软件说明
  • linux非常实用的命令和技巧

    1 串行 单核CPU 进程一个个执行 2 并行 单核CPU 感觉多个进程同一时间执行 3 并发 多核CPU 同一时间有多个进程执行 4 apt get install f修复所有软件包的依赖关系 可以提高软件包的安装成功率 5 ls 2 g
  • ‘str‘ object is not callable

    r HttpRequest http request url item url data item data cookies item Cookie http method item method auth item auth 用reque
  • 华为手机怎样才算激活了_手机电池寿命,到底使用多久才算正常

    最近这几天 同事叫我给他更换华为P9的电池 目前是充不上电 使用时间短 非常不稳定 他的P9是2016年10月份左右购买的 已经使用2年多一点点 先给大家科普一下 手机电池的寿命问题 电池寿命 锂离子电池只能充放电500次 相信绝大部分消费
  • 阿里云ECS漏洞处理(centos): NetworkManager 安全更新

    影响说明 软件 NetworkManager tui 1 10 2 14 el7 5 命中 NetworkManager tui version less than 1 1 12 0 8 el7 6 路径 usr bin nmtui 软件
  • 【Three.js】第八章 Fullscreen and resizing 全屏和调整大小

    08 Fullscreen and resizing 全屏和调整大小 介绍 我们的画布目前有一个固定的分辨率800x600 项目中不一定需要 WebGL 填满整个屏幕 但如果您想要身临其境的体验 填满整屏的体验可能会更好 首先 我们想让画布
  • win10电脑服务器在哪个文件夹下,Win10桌面背景在哪个文件夹?Win10桌面背景所在文件夹介绍...

    最近有Win10用户反映 之前电脑有设了张很好看的桌面背景 但后来不小心给换成了别的 现在想换回来 却不知道要在哪个文件夹找那张桌面背景 用户为此非常困恼 那么 Win10桌面背景在哪个文件夹呢 下面 我们就一起往下看看Win10桌面背景所
  • 计算机图标被选定无法取消,我电脑桌面上图标全被选中的状态,去不掉怎么处理?...

    1 鼠标右键单机 我的电脑 属性 高级 性能 设置 勾选下方的 在桌面上为图标标签使用阴影 OK 2 右键桌面空白处 在 排列图标 里去掉 锁定桌面的web项目 OK 3 到 控制面板 里 用户帐户 中重新建立一个新帐户 使用新帐户登陆即可
  • 超详细的ARM架构安装Mysql8.0

    一 安装环境 系统 Mac Linux版本 CentOS 9 二 安装 1 先卸载MariaDB 在CentOS中默认安装有MariaDB 是MySQL的一个分支 主要由开源社区维护 CentOS 7及以上版本已经不再使用MySQL数据库
  • 迁移学习&finetune详解

    文章目录 一 为什么要用迁移学习 二 几种方式 三 三种方式的对比 四 具体训练策略建议 一 为什么要用迁移学习 1 站在巨人的肩膀上 在已经上线的基础模型的效果可以接受的情况下 表明模型有效 可以分辨数据集基础特征 没有必要重复造轮子 每
  • MySQL中删除id为最小的数据

    方法1 delete from 表名 where id in select id from select min id id from 表名 c1 t1 方法2 delete from 表名 order by id asc limit 1
  • 链表随机指针

    我卡住的一个原因是 我曾想便利两次 想在第二遍的同时把两个链表分离开 结果失败了 必须三遍 以为random指针很可能指到前面已经断开的节点 而这个还是不用哈希的方式 因此如果前面的链表断开了 就都乱套了 Definition for a
  • Android Studio模拟器启动后不停闪烁(已玄学解决)

    问题描述 Android Studio模拟器启动后不停闪烁 解决方法 右侧点击Device Manager打开设备管理 点击修改标志 将Graphics 图样 换成Software 软件 点击Finish 这个方法是网上找的 但是不好使TA
  • Linux命令 - cp命令

    Linux命令 cp命令 cp 是copy的缩写 Linux中 cp命令用来复制文件或者目录 一般情况下 shell会设置一个别名 在命令行下复制文件时 如果目标文件已经存在 就会询问是否覆盖 不管你是否使用 i参数 但是如果是在shell
  • python图像差分法目标检测_运动目标检测(4)—背景差分法

    背景减法利用图像序列中的当前帧和事先确定的背景参考模型间的差异比较 来确定运动物体位置 是一种基于统计学原理的运动目标检测的方法 这种方法的性能取决于背景建模技术 Gloyer等人使用单高斯模型的思路 但常常不能准确地描述背景模型 1999
  • Android调用系统发送短信界面

    很多软件都有分享的功能 不少是支持短信分享的 其实就是调用系统发送短信的Activity 代码实现非常简单 发送短信 param smsBody private void sendSMS String smsBody Uri smsToUr
  • Elasticsearch基本概念及CRUD常用操作

    Elasticsearch基本概念及CRUD常用操作 一 介绍 二 REST 风格 三 索引CRUD 3 1 创建索引 3 2 查询索引 3 3 删除索引 四 文档CRUD 4 1 添加文档 4 1 1 普通添加 随机ID 4 1 2 普通
  • Mybatis学习笔记-Mabatis缓存

    Mybatis学习笔记 Mybatis缓存 缓存就是内存中的数据 常常来自对数据库查询结果的保存 使用缓存 我们可以避免频繁的与数据库进行交互 进而提高响应速度 MyBatis提供了对缓存的支持 分为一级缓存和二级缓存 可以通过一下图解来理