@Transactional注解和Mybatis缓存问题(Mybatis 查询结果 List 对List修改后再次查询,结果与数据库不一致)
@Transactional注解与Mybatis缓存的影响(Mybatis 查询结果 List 对List修改后再次查询,结果与数据库不一致)
Mybatis 查询结果 List 对List修改后再次查询,结果与数据库不一致
使用 Mybatis 查询,结果为对象的 List ,修改List内的参数后,使用相同参数再次查询,发现查询结果与数据库不一致,而是第一次查询结果操作后的对象列表。
根据问题现象可以发现,相同查询条件下,第二次查询使用了第一次的查询结果,而且两次查询是在同一方法的for循环内执行,第一次的对象肯定会被GC回收,所以应该有某种缓存机制存在,那么只可能是 Mybatis 实现了某种缓存机制。
以下为代码举例:
@Transactional
public List<User> userList (){
List<User> firstList = userDao.selectAll();
firstList.forEach(item -> item.setName("老王头"));
//修改完成之后,去 dao 中查询用户列表 然后返回
return userDao.selectAll();
}
查询出来的数据先被缓存,然后修改列表时,修改的其实是缓存数据的引用
当再次查询时,取缓存中的数据,由于缓存中的数据已经被修改
取出来的数据理所当然,已经是修改过了
复现代码
@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;
}
输出结果:
第一次查询的ProjectName:小小橙
第一次查询的ProjectName:大大橘
第一次查询的ProjectName:花花牛
修改后的ProjectName:猫猫身上有毛毛
修改后的ProjectName:猫猫身上有毛毛
修改后的ProjectName:猫猫身上有毛毛
重新查询的ProjectName:小小橙
重新查询的ProjectName:大大橘
重新查询的ProjectName:花花牛
为什么没有复现,在看下边代码
@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;
}
输出结果:
第一次查询的ProjectName:小小橙
第一次查询的ProjectName:大大橘
第一次查询的ProjectName:花花牛
修改后的ProjectName:猫猫身上有毛毛
修改后的ProjectName:猫猫身上有毛毛
修改后的ProjectName:猫猫身上有毛毛
重新查询的ProjectName:猫猫身上有毛毛
重新查询的ProjectName:猫猫身上有毛毛
重新查询的ProjectName:猫猫身上有毛毛
以下把 oneList 和 secondList 的 内存地址进行打印,进行对比
如果真的是缓存的话,那么第二次查询应该不会再查询数据库了吧,我们把sql查询日志也打印一遍
开启事务和不开启事务分别进行对比输出的日志
@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;
}
执行结果
oneList 第一次查询内存地址:114565630
oneList 将数据进行修改后的内存地址:114565630
secondList 刚创建后的内存地址:920320548
secondList 写入数据后的内存地址:114565630
从打印出的日志可以看到:
先去数据库查询出 3条记录放入 list
这时候打印出的 oneList : 114565630
然后将集合中数据进行修改,这时 oneList :114565630,可以看到虽然内容发生了改变,但是内存地址并没有法师变化
然后声明一个新的集合 secondlist: 920320548 很明显,新创建的对象,跟 oneList 的内存地址不是同一个
然后见证奇迹的时候到了,重新执行 projectDao.findAll(); secondlist:114565630,“=” 赋值,内存地址发生变化, 并且没有打印Sql日志
果然它出现了,重新调用了查询,但是并没有执行 slq 去数据库查询,而是直接去 114565630 这个内存地址取出来我们已经修改过的集合接下来把 @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;
}
oneList 第一次查询内存地址:810898134
oneList 将数据进行修改后的内存地址:810898134
secondList 刚创建后的内存地址:599203108
secondList 写入数据后的内存地址:1280730191
问题总结
使用 Mybatis 时,要结合具体场景注意缓存使用问题。
Mybatis 缓存机制简介
MyBatis 有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
一级缓存
定义:一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。
一级缓存的缺点:使用一级缓存的时候,由于缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话、或者分布式环境、或者本地对查询结果进行了增删改(本问题的场景)的情况下,会出现脏数据的问题。
一级缓存级别调整:MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选,如下所示:
缓存级别 | 处理方式 |
---|---|
session | 级别的缓存(默认) 在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中获取 |
statement | 级别的缓存 每次查询结束都会清掉一级缓存;将一级缓存的级别设为 statement 级别可避免脏数据问题 |
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
如果 MyBatis 使用了二级缓存,并且你 Mapper 和 select 语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
更多推荐
所有评论(0)