在对数据库进行噼里啪啦的查询时,可能存在多次使用相同的SQL语句去查询数据库,并且结果可能还一样,这时,如果不采取一些措施,每次都从数据库查询,会造成一定资源的浪费,所以Mybatis中提供了一级缓存和二级缓存来解决这个问题,通过把第一次查询的结果保存在内存中,如果下次有同样的语句,则直接从内存中返回。

一级缓存

一级缓存的作用域在同一个SqlSession,也就是说两个一样的SQL语句,第一次会从数据库中获得,并保存在一个Map<Object, Object> 中,第二次会从这个Map中返回,Mybatis默认开启了一级缓存。

下面是代码演示

  1. public static void main( String[] args )
  2. {
  3. String resource = "mybatis-config.xml";
  4. try {
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
  7. SqlSession sqlSession = build.openSession();
  8. System.out.println(sqlSession.getMapper(IUserDao.class).select());
  9. System.out.println(sqlSession.getMapper(IUserDao.class).select());
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. }

在配置文件中加入日志实现类来打印执行日志

  1. <settings>
  2. <setting name="logImpl" value="STDOUT_LOGGING" />
  3. </settings>

通过日志可以发现,在第一次查询时构造了sql语句,从数据库查询,第二次并没有构造sql,直接返回了缓存的数据。

  1. Opening JDBC Connection
  2. Created connection 1928931046.
  3. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72f926e6]
  4. ==> Preparing: select * from tb_user
  5. ==> Parameters:
  6. <== Columns: user_tel, user_name, user_passwd
  7. <== Row: 1500, 123, 111
  8. <== Row: 1501, 123, 222
  9. <== Total: 2
  10. [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
  11. [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]

一级缓存源码分析

在BaseExecutor中有一个PerpetualCache类,而PerpetualCache很简单,其中Map<Object, Object>则是用来保存缓存





那就先可以假设,保存缓存时一定会调用其putObject方法,而取出缓存时一定调用getObject方法,通过debug可以看到,在第一次查询完成之后,会调用putObject方法,key为sql语句,value是结果.



调用putObject方法在BaseExecutor下的queryFromDatabase中

  1. private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  2. //先占位?
  3. this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
  4. List list;
  5. try {
  6. //从数据库查询
  7. list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  8. } finally {
  9. this.localCache.removeObject(key);
  10. }
  11. //吧结果保存在PerpetualCache中
  12. this.localCache.putObject(key, list);
  13. if (ms.getStatementType() == StatementType.CALLABLE) {
  14. this.localOutputParameterCache.putObject(key, parameter);
  15. }
  16. return list;
  17. }

而调用queryFromDatabase在query中,首先判断缓存中有没有数据,没有则调用queryFromDatabase从数据库查找,有就直接返回

  1. List list;
  2. try {
  3. ++this.queryStack;
  4. //从缓存中获取
  5. list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
  6. if (list != null) {
  7. this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  8. } else {
  9. //从数据库获取
  10. list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  11. }
  12. } finally {
  13. --this.queryStack;
  14. }

手动清除缓存

SqlSession提供了clearCache来清除缓存,当第一次查询玩之后,手动清除缓存,就会发现第二次也是从数据库中查找,原理也就是对PerpetualCache进行了clear();

  1. public static void main( String[] args )
  2. {
  3. String resource = "mybatis-config.xml";
  4. try {
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
  7. SqlSession sqlSession = build.openSession();
  8. System.out.println(sqlSession.getMapper(IUserDao.class).select());
  9. sqlSession.clearCache();
  10. System.out.println(sqlSession.getMapper(IUserDao.class).select());
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. }
  1. Opening JDBC Connection
  2. Created connection 1928931046.
  3. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72f926e6]
  4. ==> Preparing: select * from tb_user
  5. ==> Parameters:
  6. <== Columns: user_tel, user_name, user_passwd
  7. <== Row: 1500, 123, 111
  8. <== Row: 1501, 123, 222
  9. <== Total: 2
  10. [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
  11. ==> Preparing: select * from tb_user
  12. ==> Parameters:
  13. <== Columns: user_tel, user_name, user_passwd
  14. <== Row: 1500, 123, 111
  15. <== Row: 1501, 123, 222
  16. <== Total: 2
  17. [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]

并且Mybatis会在Insert、Update后都会自动删除缓存

但是会有一个数据不一致的问题,很多人称之为坑,举个数据不一致的例子,两个SqlSession,都保存了id为1500的数据,但是当SqlSession1更新了1500的密码后,SqlSession2在获取并没有从数据库获取,而是从缓存中获取,缓存中的是旧密码,就出现缓存不一直问题

  1. public static void main( String[] args )
  2. {
  3. String resource = "mybatis-config.xml";
  4. try {
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
  7. SqlSession sqlSession1 = build.openSession(true);
  8. SqlSession sqlSession2 = build.openSession(true);
  9. sqlSession1.getMapper(IUserDao.class).selectByTel("1500");
  10. sqlSession2.getMapper(IUserDao.class).selectByTel("1500");
  11. System.out.println("1500的密码---"+sqlSession2.getMapper(IUserDao.class).selectByTel("1500").getUserPass());
  12. sqlSession1.getMapper(IUserDao.class).upUserPass("1500","456");
  13. System.out.println("----"+sqlSession2.getMapper(IUserDao.class).selectByTel("1500"));
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  1. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
  2. Opening JDBC Connection
  3. Created connection 1780034814.
  4. ==> Preparing: select * from tb_user where user_tel=?
  5. ==> Parameters: 1500(String)
  6. <== Columns: user_tel, user_name, user_passwd
  7. <== Row: 1500, 123, 123
  8. <== Total: 1
  9. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
  10. Opening JDBC Connection
  11. Created connection 918312414.
  12. ==> Preparing: select * from tb_user where user_tel=?
  13. ==> Parameters: 1500(String)
  14. <== Columns: user_tel, user_name, user_passwd
  15. <== Row: 1500, 123, 123
  16. <== Total: 1
  17. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
  18. 1500的密码---123
  19. ==> Preparing: UPDATE tb_user set user_passwd=? where user_tel=?
  20. ==> Parameters: 456(String), 1500(String)
  21. <== Updates: 1
  22. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
  23. ----UserEntity{userTel='1500', userName='123', userPass='123'}

解决办法在配置文件中加入 <setting name="localCacheScope" value="STATEMENT"/>,原理是在BaseExecutor#query中判断了LocalCacheScope是不是等于STATEMENT,如果等于,则清除缓存,这就意味着每次查询完都清除缓存,间接的表示关闭了一级缓存

  1. if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  2. this.clearLocalCache();
  3. }

二级缓存

二级缓存作用于是Mapper中的namespace,脱离了SqlSession,二级缓存的总开个默认也是开启的,也就是很多人说在配置文件中加入<setting name="cacheEnabled" value="true"/>,脱裤子放屁,简直多此一举



既然有主开关了,那就还有个次开关,其实还有个次次开关,次开关就是在mapper文件中加入 <cache></cache>,即表示开启二级缓存



下面代码先演示

  1. public static void main( String[] args )
  2. {
  3. String resource = "mybatis-config.xml";
  4. try {
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
  7. SqlSession sqlSession1 = build.openSession();
  8. SqlSession sqlSession2 = build.openSession();
  9. System.out.println(sqlSession1.getMapper(IUserDao.class).selectByTel("1500"));
  10. //提交
  11. sqlSession1.commit();
  12. System.out.println(sqlSession2.getMapper(IUserDao.class).selectByTel("1500"));
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }
  1. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
  2. Opening JDBC Connection
  3. Created connection 1780034814.
  4. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6a192cfe]
  5. ==> Preparing: select * from tb_user where user_tel=?
  6. ==> Parameters: 1500(String)
  7. <== Columns: user_tel, user_name, user_passwd
  8. <== Row: 1500, 123, 789
  9. <== Total: 1
  10. UserEntity{userTel='1500', userName='123', userPass='789'}
  11. Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.5
  12. UserEntity{userTel='1500', userName='123', userPass='789'}
  13. Process finished with exit code 0

当第一次查询时,命中率为0.0(Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0),第二次为0.5(Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.5),所以从缓存中获取,但是如果吧其中 sqlSession1.commit();去掉,就发现两次都从数据库查找,命中率都为0.0,原因是二级缓存有一个TransactionalCacheManager来管理二级缓存,只有调用其commit()方法才会真正保存下来,在调用SqlSession的close或者commit方法时都会调用到CachingExecutor下的close和commit方法,这两个方法对缓存进行真正的提交。



二级缓存源码分析

在Configuration的newExecutor中判断了总开关是否打开,如果打开,则使用CachingExecutor

  1. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2. executorType = executorType == null ? this.defaultExecutorType : executorType;
  3. executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  4. Object executor;
  5. if (ExecutorType.BATCH == executorType) {
  6. executor = new BatchExecutor(this, transaction);
  7. } else if (ExecutorType.REUSE == executorType) {
  8. executor = new ReuseExecutor(this, transaction);
  9. } else {
  10. executor = new SimpleExecutor(this, transaction);
  11. }
  12. //判断总开关是否打开
  13. if (this.cacheEnabled) {
  14. executor = new CachingExecutor((Executor)executor);
  15. }
  16. Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
  17. return executor;
  18. }

查询时最终调用到在CachingExecutor的query方法,首先从MappedStatement中获取Cache,如果在Mapper文件中没有配置Cache标签,它会返回空,直接从数据库查询,如果配置了,还要判断标签上的useCache属性是不是为true,也就是刚才说的次次开关,可以增加useCache=“false”来关闭这个select的缓存。

然后从缓存事物管理器中获取缓存,没有的话从数据库查询,并且添加结果到缓存事物管理器,有就返回。

  1. public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  2. Cache cache = ms.getCache();
  3. if (cache != null) {
  4. //判断是否刷新缓存
  5. this.flushCacheIfRequired(ms);
  6. //标签上是的useCache属性是否为true
  7. if (ms.isUseCache() && resultHandler == null) {
  8. this.ensureNoOutParams(ms, boundSql);
  9. //从缓存中获取
  10. List<E> list = (List)this.tcm.getObject(cache, key);
  11. if (list == null) {
  12. list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  13. //添加到缓存
  14. this.tcm.putObject(cache, key, list);
  15. }
  16. return list;
  17. }
  18. }
  19. return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  20. }

cache配置

cache有几个属性值

  • 一 eviction:缓存回收策略,默认是LRU
  1. LRU(Least Recently Used),最近最少使用的,最长时间不用的对象

  2. FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们

  3. SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象

  4. WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象

  • flushInterval:刷新间隔,单位为毫秒
  • size:缓存中的个数
  • readOnly:是否只读
  • type:自定义缓存
  • blocking:是否阻塞

flushCache

在insert、update、delete下,flushCache是默认为true的,在执行后,会刷新缓存,而select为false,查询不刷新缓存,可以更具自己需求改过来

注意

其实在集群环境下,一级缓存和二级缓存显的有点无能为力,比如A查询了张三的数据保存在缓存中,此时如果还是A修改了张三的数据,那么可以做到刷新缓存,但是重点是B修改了张三的信息,而A此时无法知道张三信息已经改变,出现数据不一致情况,解决办法通过Redis等进行缓存,

带你了解MyBatis一二级缓存的更多相关文章

  1. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

  2. Mybatis一二级缓存的理解

        频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以 ...

  3. mybatis 源码分析(四)一二级缓存分析

    本篇博客主要讲了 mybatis 一二级缓存的构成,以及一些容易出错地方的示例分析: 一.mybatis 缓存体系 mybatis 的一二级缓存体系大致如下: 首先当一二级缓存同时开启的时候,首先命中 ...

  4. 认识Mybatis的一二级缓存

    认识Mybatis的一二级缓存 一次完整的数据库请求,首先根据配置文件生成SqlSessionFactory,再通过SqlSessionFactory开启一次SqlSession,在每一个SqlSes ...

  5. 手把手带你阅读Mybatis源码(三)缓存篇

    前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...

  6. Mybatis学习(6)动态加载、一二级缓存

    一.动态加载: resultMap可以实现高级映射(使用association.collection实现一对一及一对多映射),association.collection具备延迟加载功能. 需求: 如 ...

  7. Mybatis 二级缓存应用 (21)

    [MyBatis 二级缓存] 概述:一级缓存作用域为同一个SqlSession对象,而二级缓存用来解决一级缓存不能夸会话共享,作用范围是namespace级,可以被多个SqlSession共享(只要是 ...

  8. 通过源码分析MyBatis的缓存

    前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍 ...

  9. SpringBoot 下 mybatis 的缓存

    背景: 说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架.与 Spring 和 Struts 组成了 Java Web 开发的三剑客--- SSM.当然随着 Spr ...

随机推荐

  1. 02-Java基础语法【数据类型转换、运算符、方法入门】

    重点知识记录 01.数据类型转换 当数据类型不一样是,将会发生数据类型转换. 1)自动类型转换(隐式): 特点:代码不需要进行特殊处理,自动完成: 规则:数据范围从小到大:byte < shor ...

  2. Centos7下配置Apache的虚拟主机

    一.虚拟主机 虚拟主机是Apache提供的一个功能,通过虚拟主机拉雅在一台服务器上部署多个网站.虽然服务器的IP地址是相同的,但用户当用户使用不同的域名访问时,访问到的是不同的网站. 下面讲解Apac ...

  3. bbs论坛登录相关功能(2)

    昨天把注册功能页面做出来,接下来就是登录页面 登录功能: 1,用户账号,密码后台效验,错误信息在登录按钮右边显示 2.验证码,根据图片生成,点击图片刷新产生新的验证码 修改密码 注册 先把前端页面lo ...

  4. vue项目接入markdown

    vue 项目接入 markdown 最近做一个项目,需要在vue项目中接入 markdown 编辑器,其实这个好接,他没有什么特别的样式,男的就是图片的上传. 今天给大家推荐一个插件 :mavonEd ...

  5. pycharm2019.3安装以及激活

    最近很多的pycharm激活过期的,小伙伴们问我pycharm要怎么激活?这里就分享一下pycharm最新版本的安装以及激活吧!!! 首先先去官网(https://www.jetbrains.com/ ...

  6. 按需引入element-ui时修改.babelrc报错

    刚开始学习element-ui时用的都是完整引入,图省事,这次准备按需引入,以减小项目体积, 乙烯类npm 之后,到了修改 .babelrc 文件这一步(PS:vue-cli 2.0版本会有这个文件, ...

  7. C++ - cpprestsdk

    Windows 安装方法: CMake 1.32+,生成过程会将 vcpkg 下载好,配置到系统环境变量,然后用 vcpkg 安装依赖库(github 上有列出需要的依赖库). Github 上的示例 ...

  8. Laravel 部署到阿里云 / 腾讯云

    首先你需要一台阿里云/腾讯云服务器 安装系统选择 ubuntu 16.04 然后通过 ssh 登录远程服务器按下列步骤进行配置: 更新列表 apt-get update 安装语言包 sudo apt- ...

  9. 每天进步一点点------基础实验_13_有限状态机 :Mealy型序列检测器

    /********************************************************************************* * Company : * Eng ...

  10. iOS开发之使用 infer静态代码扫描工具

    infer是Facebook 的 Infer 是一个静态分析工具.可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题. 任何人都可以使用 infer 检测应用,可以将严重的 ...