Mybatis缓存探索,查询集合后修改内容,再次执行sql查询结果发现是被修改过的
起因
事情是这样的,昨天晚上同学跟我说遇到一个问题,大概是这么描述的
A:"我写了个神奇的BUG 我从数据库查询一个列表 list1 "
A: “然后对这个列表进行了筛选,删掉了一部分数据”
A:" 结果我 return 的时候又重新去数据库查询了一遍 "
A:"神奇的是返回的结果是我筛选后的结果,要不是同事要 复制我的代码用,到现在都没发现这个问题 "
我:”怎么可能,一定是你们弄错了,给我看你代码“
A:复现代码ing… 执行… 结果显示并没有这种现象
我:“你看吧,一定是你弄错了,mybatis要是有这种bug早就没人用了”
A:“不是的,昨天我和张三,李四,都看到了,明天给你看我们之前的代码”
第二天。。。
A:“代码… 输出结果… 你看吧,我就说有这个问题”
我:陷入沉思…
A:“好像跟缓存有关系”
他的代码大概是这样的
@Override
@Transactional
public List<User> userList (){//去数据库查询列表List<User> firstList = userDao.selectAll(); //把所有用户名修改成 "老王头"firstList.forEach(item -> item.setName("老王头"));//修改完成之后,去 dao 中查询用户列表 然后返回return userDao.selectAll();
}
然后我就开始思考,什么情况下会出现这种情况,想出两个情况可能会有这个情况
- 他使用了缓存,正确的写完代码后才改成不正确的,在次调用直接取的上次的数据
- 引用传递导致的现象
首先第一种,缓存,理论上会有可能出现这种情况,但是他的代码已经颁到生产环境,并且运行了N天,应该可以排除先执行正确代码之后缓存的现象
ps:不知道引用传递,百度一下,很多文章的
然后第二种,引用传递,按理说,第二次查询之后直接返回,并没有跟 第一个集合有什么交道
再次陷入沉思…
继续观察推测,大胆猜想,两张情况都有可能造成这种现象,但是目前来看,两种情况都不满足
那么,如果是两个情况结合起来呢!
查询出来的数据先被缓存,然后修改列表时,修改的其实是缓存数据的引用
当再次查询时,取缓存中的数据,由于缓存中的数据已经被修改
取出来的数据理所当然,已经是修改过了
OK,推理完成,那么接下来要进行论证了,毕竟纸上谈兵,不如真枪实弹练一练,开始验证
首先复现代码
@Overridepublic List<ProjectEntity> projectList() {//查询列表final List<ProjectEntity> oneList = projectDao.findAll();oneList.forEach(item -> log.info("第一次查询的ProjectName:{} \n", item.getProjectName()));//修改数据oneList.forEach(item -> item.setProjectName("猫猫身上有毛毛"));oneList.forEach(item -> log.info("修改后的ProjectName:{} \n", item.getProjectName()));//重新查询List<ProjectEntity> secondList = projectDao.findAll();secondList.forEach(item -> log.info("重新查询的ProjectName:{} \n", item.getProjectName()));return null;}
让我们来看下输出结果
2021-05-14 13:42:23.393 第一次查询的ProjectName:小小橙 2021-05-14 13:42:23.393 第一次查询的ProjectName:大大橘 2021-05-14 13:42:23.393 第一次查询的ProjectName:花花牛 2021-05-14 13:42:23.394 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:42:23.394 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:42:23.394 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:42:23.406 重新查询的ProjectName:小小橙 2021-05-14 13:42:23.406 重新查询的ProjectName:大大橘 2021-05-14 13:42:23.406 重新查询的ProjectName:花花牛
!!! Why??? 为什么没有复现
秉着 代码不具有随机性 想法,再看一下
果然任何是一个细节都是关键的,对比后发现,A同学的方法是开启了事务的,那么接下来我们修改代码继续测试
@Transactional@Overridepublic List<ProjectEntity> projectList() {//查询列表final List<ProjectEntity> oneList = projectDao.findAll();oneList.forEach(item -> log.info("第一次查询的ProjectName:{} \n", item.getProjectName()));//修改数据oneList.forEach(item -> item.setProjectName("猫猫身上有毛毛"));oneList.forEach(item -> log.info("修改后的ProjectName:{} \n", item.getProjectName()));//重新查询List<ProjectEntity> secondList = projectDao.findAll();secondList.forEach(item -> log.info("重新查询的ProjectName:{} \n", item.getProjectName()));return null;}
让我们来看下输出结果
2021-05-14 13:50:46.001 第一次查询的ProjectName:小小橙 2021-05-14 13:50:46.001 第一次查询的ProjectName:大大橘 2021-05-14 13:50:46.001 第一次查询的ProjectName:花花牛 2021-05-14 13:50:46.002 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:50:46.002 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:50:46.002 修改后的ProjectName:猫猫身上有毛毛 2021-05-14 13:50:46.002 重新查询的ProjectName:猫猫身上有毛毛 2021-05-14 13:50:46.002 重新查询的ProjectName:猫猫身上有毛毛 2021-05-14 13:50:46.002 重新查询的ProjectName:猫猫身上有毛毛
- Ok 已经复现了,接下来验证我们的猜测,把 oneList 和 secondList 的 内存地址进行打印,进行对比
- 如果真的是缓存的话,那么第二次查询应该不会再查询数据库了吧,我们把sql查询日志也打印一遍
- 开启事务和不开启事务分别进行对比输出的日志
- Go Now !
修改后的代码
@Transactional@Overridepublic List<ProjectEntity> projectList() {//查询列表final List<ProjectEntity> oneList = projectDao.findAll();log.info("oneList 第一次查询内存地址:{} \n",System.identityHashCode(oneList));//修改数据oneList.forEach(item -> item.setProjectName("猫猫身上有毛毛"));log.info("oneList 将数据进行修改后的内存地址:{} \n",System.identityHashCode(oneList));//先声明一个对象List<ProjectEntity> secondList = new ArrayList<>();log.info("secondList 刚创建后的内存地址:{} \n",System.identityHashCode(secondList));//查询数据库 打印 hashcodesecondList = projectDao.findAll();log.info("secondList 写入数据后的内存地址:{} \n",System.identityHashCode(secondList));return null;}
执行结果
2021-05-14 14:05:50.240 【测试开始】:2021-05-14 14:05:50 050
2021-05-14 14:05:50.274 ==> Preparing: select id, code, project_name, ji_zu_lei_xing, address, employee_code, employee_name, data_count, create_time, create_name, update_time, update_name, del_flag from project
2021-05-14 14:05:50.384 ==> Parameters:
2021-05-14 14:05:50.401 <== Total: 3
2021-05-14 14:05:50.402 oneList 第一次查询内存地址:114565630 2021-05-14 14:05:50.402 oneList 将数据进行修改后的内存地址:114565630 2021-05-14 14:05:50.402 secondList 刚创建后的内存地址:920320548 2021-05-14 14:05:50.403 secondList 写入数据后的内存地址:114565630 2021-05-14 14:05:50.406 【测试结束】:2021-05-14 14:05:50 050 【耗时】:0s167ms
从打印出的日志可以看到:
先去数据库查询出 3条记录放入 list
这时候打印出的 oneList : 114565630
然后将集合中数据进行修改,这时 oneList :114565630,可以看到虽然内容发生了改变,但是内存地址并没有法师变化
然后声明一个新的集合 secondlist: 920320548 很明显,新创建的对象,跟 oneList 的内存地址不是同一个
然后见证奇迹的时候到了,重新执行 projectDao.findAll(); secondlist:114565630,“=” 赋值,内存地址发生变化, 并且没有打印Sql日志
果然它出现了,重新调用了查询,但是并没有执行 slq 去数据库查询,而是直接去 114565630 这个内存地址取出来我们已经修改过的集合
到这基本上就确定我们的想法是正确的,查询到的数据被缓存再内存中,我们修改集合时,因为对象是引用传递,我们每次修改的都是内存中的对象,当我们再次查询相同的sql语句,并没有去数据库查询,而是直接去内存地址取数据,所以就看到了我们明明打印的事直接调用查询的方法,却得到了我们处理过的数据
你以为文章到这里就结束了?不不不,还有最后一点要确认,
到底是不是 @Transactional 惹的祸
好,我们把 @Transactional 去掉,在运行一下
作为一个优秀的程序员一定要亲眼所见在下定论,上代码,看日志
//@Transactional@Overridepublic List<ProjectEntity> projectList() {//查询列表final List<ProjectEntity> oneList = projectDao.findAll();log.info("oneList 第一次查询内存地址:{} \n",System.identityHashCode(oneList));//修改数据oneList.forEach(item -> item.setProjectName("猫猫身上有毛毛"));log.info("oneList 将数据进行修改后的内存地址:{} \n",System.identityHashCode(oneList));//先声明一个对象List<ProjectEntity> secondList = new ArrayList<>();log.info("secondList 刚创建后的内存地址:{} \n",System.identityHashCode(secondList));//查询数据库 打印 hashcodesecondList = projectDao.findAll();log.info("secondList 写入数据后的内存地址:{} \n",System.identityHashCode(secondList));return null;}
2021-05-14 14:18:50.753 【测试开始】:2021-05-14 14:18:50 050
2021-05-14 14:18:50.780 ==> Preparing: select id, code, project_name, ji_zu_lei_xing, address, employee_code, employee_name, data_count, create_time, create_name, update_time, update_name, del_flag from project
2021-05-14 14:18:50.885 ==> Parameters:
2021-05-14 14:18:50.902 <== Total: 3
2021-05-14 14:18:50.903 oneList 第一次查询内存地址:810898134 2021-05-14 14:18:50.903 oneList 将数据进行修改后的内存地址:810898134 2021-05-14 14:18:50.903 secondList 刚创建后的内存地址:599203108 2021-05-14 14:18:50.903 ==> Preparing: select id, code, project_name, ji_zu_lei_xing, address, employee_code, employee_name, data_count, create_time, create_name, update_time, update_name, del_flag from project
2021-05-14 14:18:50.903 ==> Parameters:
2021-05-14 14:18:50.906 <== Total: 3
2021-05-14 14:18:50.907 secondList 写入数据后的内存地址:1280730191 2021-05-14 14:18:50.909 【测试结束】:2021-05-14 14:18:50 050 【耗时】:0s157ms
简单分析一下
- 第一次查询数据,记录内存地址
- 讲数据进行修改,内存地址并未发生变化,变化的知识内存内的数据
- 新建一对象,会声明新的内存地址
- 然后查询数据库,看到了 sql 已经被打印,并且赋值后的内存地址也跟第一次查询的内存地址不一样
好,打完收工
Mybatis缓存探索,查询集合后修改内容,再次执行sql查询结果发现是被修改过的相关推荐
- python执行sql查询脚本并填写到excel,执行SQL查询脚本
static void Main(string[] args) { Console.WriteLine("输入用户编号:"); string cusernum = Console. ...
- java批量执行查询sql语句_详解MyBatis直接执行SQL查询及数据批量插入
一.直接执行SQL查询: 1.mappers文件节选 ${paramSQL} 2.DAO类节选 public interface SomeDAO{ List getInstanceModel(@Par ...
- MyBatis直接执行SQL查询及批量插入数据
转:http://www.cnblogs.com/mabaishui/archive/2012/06/20/2556500.html 一.直接执行SQL查询: 1.mappers文件节选 <re ...
- 在VS Code中执行SQL查询,是怎样一种体验?
上次,我们演示了"如何使用Nuget包XPlot.Plotly.Interactive在.NET Interactive notebook中绘制图表". 这次,我们使用Nuget包 ...
- DBeaver 给id设置为键 以及执行sql查询语句
一.设置键 可以看到在information表中,设置id为主键. 步骤: 1.点到 约束 => 右键后 新建约束 2.点击id这个字段 => 确定 二.执行sql查询语句 目标 : 查询 ...
- 如何优雅的将Mybatis日志中的Preparing与Parameters转换为可执行SQL
原文链接 疫情期间大家宅在家里是不是已经快憋出"病"了~~ 公司给开了VPN,手机电脑都能连,手机装上APP测试包,就能干活了,所以walking从2020.02.01入京以来,已 ...
- 提交PR后修改内容并合并commit
解决的问题 PR提交后,发现PR内容需要进一步修改,但是希望避免出现多次commit记录. # git clone 个人仓 git clone https://gitee.com/ljrcore/xx ...
- Mybatis——注入执行sql查询、更新、新增以及建表语句
文章目录 前言 案例 dao和mapper编写 XXXmapper.xml编写 编写业务层代码,进行注入调用 额外扩展--创建表语句 前言 在平时的项目开发中,mybatis应用非常广泛,但一般都是直 ...
- mysql查询集合中的特定数据_快速查询List中指定的数据
时间:2017/5/15 作者:李国君 题目:快速查询List中指定的数据 背景:当List中保存了大量的数据时,用传统的方法去遍历指定的数据肯定会效率低下,有一个方法就是类似于数据库查询那样,根据索 ...
最新文章
- css左固定右自适应常用方法
- 小学生计算机课堂实践的重要性,多媒体在小学教学中的重要性
- 数据结构C#版笔记--树与二叉树
- 《jQuery、jQuery UI及jQuery Mobile技巧与示例》——3.3 技巧:生成类名
- 高内聚低耦合_高渗透环氧树脂灌浆料
- java为学生s1的age属性_求解java程序,题目如下
- 杜甫在线演唱《奇迹再现》、兵马俑真人还原……用AI技术打破次元壁的大谷来参加腾讯全球数字生态大会啦!...
- python安装scipy数次失败,之后安装Anaconda后使用sublime切换python解释器后解决pip无法安装scipy问题
- k近邻算法_机器学习算法之——K最近邻(k-Nearest Neighbor,KNN)分类算法原理讲解...
- 在简历中使用STAR法则
- Greenplum 调优--数据分布法则 - 分布列与分区的选择
- 短信宝发送短信验证码
- 旷视科技2023校园招聘提前批正式启动!
- 亲密关系的本质是分享:如何把握暧昧的窗口期?
- Jenkins ——The server rejected the connection
- 1.2样本空间和随机事件
- HTML-6.表单学习—如何做一个注册(登录)网页
- 安卓开发 从入门到转业 (一)
- 解决虚拟机启动黑屏无法进入系统
- 怎么开淘宝网店?淘宝网开店流程图解,淘宝开店教程!
热门文章
- 我喜欢Balsamiq Mockups的三大理由
- web课程设计网页规划与设计:摄影/拍摄/相片网站设计——摄影网(12页)HTML+CSS+JavaScript div+css网页html成品学生作业
- 【PAT甲级A1003 】Emergency (25分)(c++)
- pytorch_BCELoss的reduction参数理解
- 提升工作效率的常用mac命令行
- Mapper 重命名问题
- HarmonyOS与Android的全面对比
- matlab 点顺时针排序,按顺时针顺序排序点?
- C++ 语言禁止派生类 - final specifier
- ECN Trade:QE3仅是镇定剂,美好前景只是幻想