Mybatis 查询结果 List 对List修改后再次查询,结果与数据库不一致
 使用 Mybatis 查询,结果为对象的 List ,修改List内的参数后,使用相同参数再次查询,发现查询结果与数据库不一致,而是第一次查询结果操作后的对象列表。
 根据问题现象可以发现,相同查询条件下,第二次查询使用了第一次的查询结果,而且两次查询是在同一方法的for循环内执行,第一次的对象肯定会被GC回收,所以应该有某种缓存机制存在,那么只可能是 Mybatis 实现了某种缓存机制。
举例:
mysql建表语句
CREATE TABLE `t_user` (
  `rid` bigint NOT NULL COMMENT '主键',
  `username` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
  PRIMARY KEY (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
实体类
@Data
@TableName("t_user")
public class User {
    @TableId
    private Long rid;
    @TableField("username")
    private String username;
}
mapper类
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
service接口
public interface UserService extends IService<User> {
    List<User> getAllUserList();
    void userList();
    void userListAddr();
}
service接口实现类
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    //@Transactional
    public List<User> getAllUserList() {
        List<User> firstList = list();
        firstList.forEach(item -> item.setUsername("老王头"));
        //修改完成之后,去 dao 中查询用户列表  然后返回
        return list();
    }
    @Override
    public void userList() {
        //查询列表
        final List<User> oneList = list();
        oneList.forEach(item -> log.info("第一次查询的userName:{} \n",  item.getUsername()));
        //修改数据
        oneList.forEach(item -> item.setUsername("猫猫身上有毛毛"));
        oneList.forEach(item -> log.info("修改后的userName:{} \n", item.getUsername()));
        //重新查询
        List<User> secondList = list();
        secondList.forEach(item -> log.info("重新查询的userName:{} \n",  item.getUsername()));
    }
    @Override
    //@Transactional
    public void userListAddr() {
        //查询列表
        final List<User> oneList = list();
        log.info("oneList 第一次查询内存地址:{} \n",System.identityHashCode(oneList));
        //修改数据
        oneList.forEach(item -> item.setUsername("猫猫身上有毛毛"));
        log.info("oneList 将数据进行修改后的内存地址:{} \n",System.identityHashCode(oneList));
        //先声明一个对象
        List<User> secondList = new ArrayList<>();
        log.info("secondList 刚创建后的内存地址:{} \n",System.identityHashCode(secondList));
        //查询数据库 打印 hashcode
        secondList = list();
        log.info("secondList 写入数据后的内存地址:{} \n",System.identityHashCode(secondList));
    }
}
主启动类
@SpringBootApplication
@MapperScan(basePackages = {"com.zhubayi.mybatiscache.mapper"})
public class MybatisCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisCacheApplication.class, args);
    }
}
测试类
@SpringBootTest
class MybatisCacheApplicationTests {
    @Autowired
    private UserService userService;
    @Test
    void contextLoads() {
    }
    @Test
    public void test01(){
        System.out.println(userService.getAllUserList());
    }
    @Test
    public void test02(){
        userService.userList();
    }
    @Test
    public void test03(){
        userService.userListAddr();
    }
}
配置文件
spring:
  datasource:
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/learning?serverTimeZone=UTC
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
运行test01方法
发现查询了两次数据库
 
加上@Transactional然后再运行
 
 
 只查询了一次数据库。
 查询出来的数据先被缓存,然后修改列表时,修改的其实是缓存数据的引用
 当再次查询时,取缓存中的数据,由于缓存中的数据已经被修改
 取出来的数据理所当然,已经是修改过了
 原因:
 MyBatis 默认开启了一级缓存,它会缓存查询结果,导致在同一个事务内,从缓存中读取数据而不是从数据库中实际查询。
解决办法:
 1.设置mybatis的1级缓存级别为statement
 
 2.在方法外开启事务:如果可能,可以将查询和更新拆分成不同的方法,然后在需要的地方使用 @Transactional 注解来开启事务,这样可以更好地控制事务的边界。
 3.手动清除缓存:在修改数据后,手动调用 MyBatis 的 clearCache 方法,以清除一级缓存,确保后续查询从数据库中重新获取数据。
问题总结
使用 Mybatis 时,要结合具体场景注意缓存使用问题。
Mybatis 缓存机制简介
 MyBatis 有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
一级缓存
定义:一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。
一级缓存的缺点:使用一级缓存的时候,由于缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话、或者分布式环境、或者本地对查询结果进行了增删改(本问题的场景)的情况下,会出现脏数据的问题。
一级缓存级别调整:MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选,如下所示:
| 缓存级别 | 处理方式 | 
|---|---|
| session 级别的缓存(默认) | 在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中获取 | 
| statement 级别的缓存 | 每次查询结束都会清掉一级缓存;将一级缓存的级别设为 statement 级别可避免脏数据问题 | 
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步,开启了二级缓存后,还需要将要缓存的entity实现Serializable接口。
如果 MyBatis 使用了二级缓存,并且你 Mapper 和 select 语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。













![java八股文面试[java基础]——浅拷贝和深拷贝](https://img-blog.csdnimg.cn/68d591e834284170bd20a61d0df0a9eb.png)





