被mybatis一级缓存坑了

背景

项目中出现了这样一个问题,就是select出来的数据和数据库里的数据不一样,就非常的奇怪,发现原来是mybatis的缓存导致的,经过查询资料发现这是mybatis的一级缓存。

下面介绍了问题出现的场景以及解决办法

场景

User类:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer age;
    private String name;
    private Long id;
    private Date createTime;
}

Mapper类:

@Mapper
public interface UserMapper {
    @Results(value = {
            @Result(property = "id", column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
            @Result(property = "age", column = "age", javaType = Integer.class, jdbcType = JdbcType.INTEGER),
            @Result(property = "name", column = "name", javaType = String.class, jdbcType = JdbcType.VARCHAR),
            @Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.DATE)
    })
    @Select("SELECT id, age, name, create_time FROM user WHERE id = #{id}")
    User selectUser(Long id);
}
一个普通的bean
@Slf4j
@Component
public class MyBean {

    @Autowired
    private UserMapper userMapper;

    public void test1() {
        User user = userMapper.selectUser(1L);
        log.info("user:{}", user);
        user.setAge(3); // 更新其中一个属性
        user = userMapper.selectUser(1L);
        log.info("user:{}", user);
    }

    @Transactional
    public void test2() {
        test1();
    }
}
配置类:
@Configuration
@MapperScan("cn.eagle.li.mybatis.cache.session")
@EnableTransactionManagement
public class Config {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

    @Bean(name = "sqlSessionFactory")
    @ConditionalOnMissingBean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    }

    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public DataSource dataSource() {
        MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("root");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8");
        return dataSource;
    }
}
测试类
@Slf4j
public class Main {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(Config.class);
        MyBean myBean = context.getBean(MyBean.class);
        myBean.test1();
        log.info("==================");
        myBean.test2();
    }
}
运行结果:

1966 [main] INFO  c.e.li.mybatis.cache.session.MyBean - user:User(age=2, name=3, id=1, createTime=Thu Nov 04 00:00:00 CST 2021) 
1996 [main] INFO  c.e.li.mybatis.cache.session.MyBean - user:User(age=2, name=3, id=1, createTime=Thu Nov 04 00:00:00 CST 2021) 
1996 [main] INFO  c.e.l.m.c.session.DataSourceMain - ================== 
2046 [main] INFO  c.e.li.mybatis.cache.session.MyBean - user:User(age=2, name=3, id=1, createTime=Thu Nov 04 00:00:00 CST 2021) 
2047 [main] INFO  c.e.li.mybatis.cache.session.MyBean - user:User(age=3, name=3, id=1, createTime=Thu Nov 04 00:00:00 CST 2021) 

可以看到两个方法的代码内容是一样的,只不过第二个方法上加了一个事务的注解

第一个方法中,两次从数据库选出的结果是一样的;而在第二个方法中,两次从数据库选出的结果是不一样的(age的值)

可以猜测是@Transactional+user.setAge(3);导致的结果不一样

原因

经过调试,是下面的这行代码的原因,BaseExecutor.query如下:

上面两张图片分别是不带事务带事务执行到第二个查询的时候经过的地方,可以看出带事务的方法,到这里的时候,直接从localCache取出来了,这就是原因所在。

大家可以去看一下localCache是什么时候被清理掉了,其实带事务的会等到事务结束之后才会清理掉;而不带事务的把每一个查询当成一个事务,所以每个查询后就被清理到了。

解法

  1. 就是不要修改查询出来的类,如下:
    public void test1() {
        User user = userMapper.selectUser(1L);
        log.info("user:{}", user);
        User user2 = User.builder().name(user.getName()).age(3).id(user.getId()).build();
        log.info("user:{}", user);
    }
  1. 当然,也可以把一级缓存关掉,如下配置:
mybatis-spring-config.xml 文件如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
</configuration>
    @Bean(name = "sqlSessionFactory")
    @ConditionalOnMissingBean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setConfigLocation(new ClassPathResource("mybatis-spring-config.xml")); // 这里加载配置文件
        return sessionFactory.getObject();
    }

其实就是每次查询后,都把localCache给清理掉了,原理如下:

参考

聊聊MyBatis缓存机制

posted @ 2022-02-01 19:56  eaglelihh  阅读(670)  评论(0编辑  收藏  举报