热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

h2作为mysql缓存_MyBatis的一级缓存竟然还会引来麻烦?

端午假期相信不少小伙伴都在偷偷学习吧(说好了放假一起玩耍呢,结果又背着我学习),这不,刚过了端午,我的一个沙雕程序猿圈子里就

端午假期相信不少小伙伴都在偷偷学习吧(说好了放假一起玩耍呢,结果又背着我学习),这不,刚过了端午,我的一个沙雕程序猿圈子里就有人讨论起来问题了,这个问题聊起来好像挺麻烦,但实际上问题是很简单的,下面咱来讨论下这个问题。

原问题

MyBatis 一级缓存与 SpringFramework 的声明式事务有冲突吗?在 Service 中开启事务,连续查询两次同样的数据,结果两次查询的结果不一致。

—— 使用 Mapper 的 selectById 查出来实体,然后修改实体的属性值,然后再 selectById 一下查出来实体,对比一下之前查出来的,发现查出来的是刚才修改过的实体,不是从数据库查出来的。

—— 如果不开启事务,则两次请求查询的结果是相同的,控制台打印了两次 SQL 。

初步分析

讲道理,看到这个问题,我一下子就猜到是 MyBatis 一级缓存重复读取的问题了。

MyBatis 的一级缓存默认开启,属于 SqlSession 作用范围。在事务开启的期间,同样的数据库查询请求只会查询一次数据库,之后重复查询会从一级缓存中获取。当不开启事务时,同样的多次数据库查询都会发送数据库请求。

上面的都属于基础知识了,不多解释。重点是,他修改的实体是直接从 MyBatis 的一级缓存中查询出来的。咱都知道,查询出来的这些实体肯定属于对象,拿到的是对象的引用,咱在 Service 里修改了,一级缓存中相应的也就会被影响。由此可见,这个问题的核心原因也就很容易找到了。

问题复现

为了展示这个问题,咱还是简单复现一下场景吧。

工程搭建

咱使用 SpringBoot + mybatis-spring-boot-starter 快速构建出工程,此处 SpringBoot 版本为 2.2.8 ,mybatis-spring-boot-starter 的版本为 2.1.2 。

pom

核心的 pom 依赖有 3 个:

org.springframework.boot spring-boot-starter-weborg.mybatis.spring.boot mybatis-spring-boot-starter 2.1.2com.h2database h2 1.4.199

数据库配置

数据库咱们依然选用 h2 作为快速问题复现的数据库,只需要在 application.properties 中添加如下配置,即可初始化一个 h2 数据库。顺便的,咱把 MyBatis 的配置也简单配置好:

spring.datasource.driver-class-name=org.h2.Driverspring.datasource.url=jdbc:h2:mem:mybatis-transaction-cachespring.datasource.username=saspring.datasource.password=saspring.datasource.platform=h2spring.datasource.schema=classpath:sql/schema.sqlspring.datasource.data=classpath:sql/data.sqlspring.h2.console.settings.web-allow-others=truespring.h2.console.path=/h2spring.h2.console.enabled=truemybatis.type-aliases-package=com.linkedbear.demo.entitymybatis.mapper-locations=classpath:mapper/*.xml

初始化数据库

上面咱使用了 datasource 的 schema 和 data 初始化数据库,那自然的就应该有这两个 .sql 文件。

schema.sql :

create table if not exists sys_department ( id varchar(32) not null primary key, name varchar(32) not null);

data.sql :

insert into sys_department (id, name) values ('idaaa', 'testaaa');insert into sys_department (id, name) values ('idbbb', 'testbbb');insert into sys_department (id, name) values ('idccc', 'testccc');insert into sys_department (id, name) values ('idddd', 'testddd');

编写测试代码

咱使用一个最简单的单表模型,快速复现场景。

entity

新建一个 Department 类,并声明 id 和 name 属性:

public class Department { private String id; private String name; // getter setter toString ......}

mapper

MyBatis 的接口动态代理方式可以快速声明查询的 statement ,咱只需要声明一个 findById 即可:

@Mapperpublic interface DepartmentMapper { Department findById(String id);}

mapper.xml

对应的,接口需要 xml 作为照应:(此处并没有使用注解式 Mapper )

select * from sys_department where id = #{id}

service

Service 中注入 Mapper ,并编写一个需要事务的 update 方法,模拟更新动作:

@Servicepublic class DepartmentService { @Autowired DepartmentMapper departmentMapper; @Transactional(rollbackFor = Exception.class) public Department update(Department department) { Department temp = departmentMapper.findById(department.getId()); temp.setName(department.getName()); Department temp2 = departmentMapper.findById(department.getId()); System.out.println("两次查询的结果是否是同一个对象:" + temp == temp2); return temp; }}

controller

Controller 中注入 Service ,并调用 Service 的 update 方法来触发测试:

@RestControllerpublic class DepartmentController { @Autowired DepartmentService departmentService; @GetMapping("/department/{id}") public Department findById(@PathVariable("id") String id) { Department department = new Department(); department.setId(id); department.setName(UUID.randomUUID().toString().replaceAll("-", "")); return departmentService.update(department); }}

主启动类

主启动类中不需要什么特别的内容,只需要记得开启事务就好:

@EnableTransactionManagement@SpringBootApplicationpublic class MyBatisTransactionCacheApplication { public static void main(String[] args) { SpringApplication.run(MyBatisTransactionCacheApplication.class, args); }}

运行测试

以 Debug 方式运行 SpringBoot 的主启动类,在浏览器中输入 http://localhost:8080/h2 输入刚才在 application.properties 中声明的配置,即可打开 h2 数据库的管理台。

执行 SELECT * FROM SYS_DEPARTMENT ,可以发现数据已经成功初始化了:

5f31663862d44ae72c4ceea72437088f.png


image.png

下面测试效果,在浏览器中输入 http://localhost:8080/department/idaaa ,控制台中打印的结果为 true,证明 MyBatis 的一级缓存生效,两次查询最终得到的实体类对象一致。

解决方案

对于这个问题的解决方案,其实说白了,就是关闭一级缓存。最常见的几种方案列举一下:

  • 全局关闭:设置 mybatis.configuration.local-cache-scope=statement
  • 指定 mapper 关闭:在 mapper.xml 的指定 statement 上标注 flushCache="true"
  • 另类的办法:在 statement 的 SQL 上添加一串随机数(过于非主流。。。)select * from sys_department where #{random} = #{random}

原理扩展

其实到这里,问题就已经解决了,但先不要着急,思考一个问题:为什么声明了 local-cache-scope 为 statement ,或者mapper 的 statement 标签中设置 flushCache=true ,一级缓存就被禁用了呢?下面咱来了解下这背后的原理。

一级缓存失效的原理

在 DepartmentService 中,执行 mapper.findById 的动作,最终会进入到 DefaultSqlSession 的 selectOne中:

public T selectOne(String statement) { return this.selectOne(statement, null);}@Overridepublic T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. List list = this.selectList(statement, parameter); if (list.size() == 1) { return list.get(0); } else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; }}

可见 selectOne 的底层是调用的 selectList ,之后 get(0) 取出第一条数据返回。

selectList 的底层会有两个步骤:获取 MappedStatement → 执行查询,如下代码中的 try 部分:

public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}

执行 query 方法,来到 BaseExecutor 中,它会执行三个步骤:获取预编译的 SQL → 创建缓存键 → 真正查询

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

这里面的缓存键是有一定设计的,它的结构可以简单的看成 “ statementId + SQL + 参数 ” 的形式,根据这三个要素,就可以唯一的确定出一个查询结果。

到了这里面的 query 方法,它就带着这个缓存键,执行真正的查询动作了,如下面的这段长源码:(注意看源码中的注释)

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } // 如果statement有设置flushCache="true",则查询之前先清理一级缓存 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List list; try { queryStack++; // 先检查一级缓存 list = resultHandler == null ? (List) localCache.getObject(key) : null; if (list != null) { // 如果一级缓存中有,则直接取出 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 一级缓存没有,则查询数据库 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); // 如果全局配置中有设置local-cache-scope=statement,则清除一级缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list;}

上面的注释中,可以发现,只要上面的三个解决方案,任选一个配置,则一级缓存就会失效,分别分析下:

  • 全局设置 local-cache-scope=statement ,则查询之后即便放入了一级缓存,但存放完立马就给清了,下一次还是要查数据库;
  • statement 设置 flushCache="true" ,则查询之前先清空一级缓存,还是得查数据库;
  • 设置随机数,如果随机数的上限足够大,那随机到相同数的概率就足够低,也能类似的看成不同的数据库请求,那缓存的 key 都不一样,自然就不会匹配到缓存。



推荐阅读
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • Oracle10g备份导入的方法及注意事项
    本文介绍了使用Oracle10g进行备份导入的方法及相关注意事项,同时还介绍了2019年独角兽企业重金招聘Python工程师的标准。内容包括导出exp命令、删用户、创建数据库、授权等操作,以及导入imp命令的使用。详细介绍了导入时的参数设置,如full、ignore、buffer、commit、feedback等。转载来源于https://my.oschina.net/u/1767754/blog/377593。 ... [详细]
  • web.py开发web 第八章 Formalchemy 服务端验证方法
    本文介绍了在web.py开发中使用Formalchemy进行服务端表单数据验证的方法。以User表单为例,详细说明了对各字段的验证要求,包括必填、长度限制、唯一性等。同时介绍了如何自定义验证方法来实现验证唯一性和两个密码是否相等的功能。该文提供了相关代码示例。 ... [详细]
  • PDO MySQL
    PDOMySQL如果文章有成千上万篇,该怎样保存?数据保存有多种方式,比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择,做We ... [详细]
  • 分享css中提升优先级属性!important的用法总结
    web前端|css教程css!importantweb前端-css教程本文分享css中提升优先级属性!important的用法总结微信门店展示源码,vscode如何管理站点,ubu ... [详细]
  • 1.Listener是Servlet的监听器,它可以监听客户端的请求、服务端的操作等。通过监听器,可以自动激发一些操作,比如监听在线的用户的数量。当增加一个HttpSession时 ... [详细]
  • SQL Server 2008 到底需要使用哪些端口?
    SQLServer2008到底需要使用哪些端口?-下面就来介绍下SQLServer2008中使用的端口有哪些:  首先,最常用最常见的就是1433端口。这个是数据库引擎的端口,如果 ... [详细]
  • 折腾个半死,数据库初始化设置不当报错 ORA01078: failure in proces...
    2019独角兽企业重金招聘Python工程师标准[oraclelocalhost~]$sqlplusassysdba提示Connectedtoanidleinstance.连 ... [详细]
  • Oracle Database 10g许可授予信息及高级功能详解
    本文介绍了Oracle Database 10g许可授予信息及其中的高级功能,包括数据库优化数据包、SQL访问指导、SQL优化指导、SQL优化集和重组对象。同时提供了详细说明,指导用户在Oracle Database 10g中如何使用这些功能。 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • 如何在php文件中添加图片?
    本文详细解答了如何在php文件中添加图片的问题,包括插入图片的代码、使用PHPword在载入模板中插入图片的方法,以及使用gd库生成不同类型的图像文件的示例。同时还介绍了如何生成一个正方形文件的步骤。希望对大家有所帮助。 ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
  • Struts2+Sring+Hibernate简单配置
    2019独角兽企业重金招聘Python工程师标准Struts2SpringHibernate搭建全解!Struts2SpringHibernate是J2EE的最 ... [详细]
  • PHP输出缓冲控制Output Control系列函数详解【PHP】
    后端开发|php教程PHP,输出缓冲,Output,Control后端开发-php教程概述全景网页源码,vscode如何打开c,ubuntu强制解锁,sts启动tomcat慢,sq ... [详细]
author-avatar
手机用户2602921555
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有