Mybatis缓存探索,查询集合后修改内容,再次执行sql查询结果发现是被修改过的

本文讲述了在使用MyBatis时遇到的一个奇怪问题,即在事务中对数据库查询结果进行修改后,再次查询返回的仍是修改后的数据,而非数据库原始数据。经过分析,确定是事务结合缓存导致的现象。通过打印内存地址和SQL日志,证实了在事务环境下,修改后的数据被缓存,再次查询时直接从缓存取数据,从而解释了这一现象。最后,移除事务后,问题不再复现,进一步确认了事务在这其中的作用。
摘要由CSDN通过智能技术生成

起因

事情是这样的,昨天晚上同学跟我说遇到一个问题,大概是这么描述的

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

然后我就开始思考,什么情况下会出现这种情况,想出两个情况可能会有这个情况

  1. 他使用了缓存,正确的写完代码后才改成不正确的,在次调用直接取的上次的数据
  2. 引用传递导致的现象

首先第一种,缓存,理论上会有可能出现这种情况,但是他的代码已经颁到生产环境,并且运行了N天,应该可以排除先执行正确代码之后缓存的现象

ps:不知道引用传递,百度一下,很多文章的

然后第二种,引用传递,按理说,第二次查询之后直接返回,并没有跟 第一个集合有什么交道

再次陷入沉思…

继续观察推测,大胆猜想,两张情况都有可能造成这种现象,但是目前来看,两种情况都不满足

那么,如果是两个情况结合起来呢!

查询出来的数据先被缓存,然后修改列表时,修改的其实是缓存数据的引用

当再次查询时,取缓存中的数据,由于缓存中的数据已经被修改

取出来的数据理所当然,已经是修改过了

OK,推理完成,那么接下来要进行论证了,毕竟纸上谈兵,不如真枪实弹练一练,开始验证

首先复现代码


    @Override
    public 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
	@Override
    public 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:猫猫身上有毛毛 
  1. Ok 已经复现了,接下来验证我们的猜测,把 oneList 和 secondList 的 内存地址进行打印,进行对比
  2. 如果真的是缓存的话,那么第二次查询应该不会再查询数据库了吧,我们把sql查询日志也打印一遍
  3. 开启事务和不开启事务分别进行对比输出的日志
  4. Go Now !

修改后的代码

    @Transactional
    @Override
    public 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));

        //查询数据库 打印 hashcode
        secondList = 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

从打印出的日志可以看到:

  1. 先去数据库查询出 3条记录放入 list

  2. 这时候打印出的 oneList : 114565630

  3. 然后将集合中数据进行修改,这时 oneList :114565630,可以看到虽然内容发生了改变,但是内存地址并没有法师变化

  4. 然后声明一个新的集合 secondlist: 920320548 很明显,新创建的对象,跟 oneList 的内存地址不是同一个

  5. 然后见证奇迹的时候到了,重新执行 projectDao.findAll(); secondlist:114565630,“=” 赋值,内存地址发生变化, 并且没有打印Sql日志

    果然它出现了,重新调用了查询,但是并没有执行 slq 去数据库查询,而是直接去 114565630 这个内存地址取出来我们已经修改过的集合

到这基本上就确定我们的想法是正确的,查询到的数据被缓存再内存中,我们修改集合时,因为对象是引用传递,我们每次修改的都是内存中的对象,当我们再次查询相同的sql语句,并没有去数据库查询,而是直接去内存地址取数据,所以就看到了我们明明打印的事直接调用查询的方法,却得到了我们处理过的数据

你以为文章到这里就结束了?不不不,还有最后一点要确认,

到底是不是 @Transactional 惹的祸

好,我们把 @Transactional 去掉,在运行一下

作为一个优秀的程序员一定要亲眼所见在下定论,上代码,看日志

    //@Transactional
    @Override
    public 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));

        //查询数据库 打印 hashcode
        secondList = 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

简单分析一下

  1. 第一次查询数据,记录内存地址
  2. 讲数据进行修改,内存地址并未发生变化,变化的知识内存内的数据
  3. 新建一对象,会声明新的内存地址
  4. 然后查询数据库,看到了 sql 已经被打印,并且赋值后的内存地址也跟第一次查询的内存地址不一样

好,打完收工

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值