被mybatis一级缓存坑了
背景
项目中出现了这样一个问题,就是select出来的数据和数据库里的数据不一样,就非常的奇怪,发现原来是mybatis的缓存导致的,经过查询资料发现这是mybatis的一级缓存。
下面介绍了问题出现的场景以及解决办法
场景
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是什么时候被清理掉了,其实带事务的等到事务结束之后才会清理掉;而不带事务的把每一个查询当成一个事务,所以每个查询后就被清理到了。
解法
- 就是不要修改查询出来的类,如下:
 
    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);
    }
- 当然,也可以把一级缓存关掉,如下配置:
 
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一级缓存坑了的更多相关文章
- MyBatis 一级缓存避坑
		
MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选: package org.apache.ibatis.session; /** * @autho ...
 - Mybatis一级缓存和二级缓存总结
		
1:mybatis一级缓存:级别是session级别的,如果是同一个线程,同一个session,同一个查询条件,则只会查询数据库一次 2:mybatis二级缓存:级别是sessionfactory级别 ...
 - Mybatis一级缓存和二级缓存 Redis缓存
		
一级缓存 Mybatis的一级缓存存放在SqlSession的生命周期,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对 ...
 - MyBatis 一级缓存与二级缓存
		
MyBatis一级缓存 MyBatis一级缓存默认开启,一级缓存为Session级别的缓存,在执行以下操作时一级缓存会清空 1.执行session.clearCache(); 2.执行CUD操作 3. ...
 - MyBatis一级缓存引起的无穷递归
		
MyBatis一级缓存引起的无穷递归 引言: 最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取 ...
 - mybatis一级缓存详解
		
mybatis缓存分为一级缓存,二级缓存和自定义缓存.本文重点讲解一级缓存 一:前言 在介绍缓存之前,先了解下mybatis的几个核心概念: * SqlSession:代表和数据库的一次会话,向用户提 ...
 - 0065 MyBatis一级缓存与二级缓存
		
数据库中数据虽多,但访问频率却不同,有的数据1s内就会有多次访问,而有些数据几天都没人查询,这时候就可以将访问频率高的数据放到缓存中,就不用去数据库里取了,提高了效率还节约了数据库资源 MyBatis ...
 - 关于mybatis 一级缓存引发的问题
		
场景: 由于在一个方法中存在多个不同业务操作 private void insertOrUpdateField(CompanyReport entity) { //计算并数据 calcReportDa ...
 - MyBatis一级缓存(转载)
		
<深入理解mybatis原理> MyBatis的一级缓存实现详解 及使用注意事项 http://demo.netfoucs.com/luanlouis/article/details/41 ...
 
随机推荐
- 【剑指Offer】扑克牌顺子 解题报告(Python)
			
[剑指Offer]扑克牌顺子 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews 题目描 ...
 - D. Persistent Bookcase(Codeforces Round #368 (Div. 2))
			
D. Persistent Bookcase time limit per test 2 seconds memory limit per test 512 megabytes input stand ...
 - 「HAOI2016」找相同字符
			
知识点: SA,线段树,广义 SAM 原题面 Loj Luogu 给定两字符串 \(S_1, S_2\),求出在两字符串中各取一个子串,使得这两个子串相同的方案数. 两方案不同当且仅当这两个子串中有一 ...
 - uniCloud爬虫获取网页数据
			
'use strict'; let request = require('request') let cheerio = require('cheerio'); //爬虫 let iconv = re ...
 - CapstoneCS5265|TYPEC转HDMI 4K60HZ转换方案设计|CS5265功能介绍
			
芯片简介描述:CS5265集成了一个DP1.4的转换器HDMI2.0转换.此外,CC控制器还用于CC通信,以实现DP Alt模式. CS5265是一种高度集成的单芯片,适用于多个细分市场和显示应用,如 ...
 - <数据结构>XDOJ332.二叉排序树的判定
			
问题与解答 问题描述 给定一个二叉树,判断其是否是一个有效的二叉排序树. 假设一个二叉排序树具有如下特征: 结点的左子树只包含小于当前结点的树. 结点的右子树只包含大于当前结点的树. 所有左子树和右子 ...
 - select 1 from 是什么意思?有什么作用?
			
参考:https://www.douban.com/note/518373959/ 一.select 1 from 的作用1.select 1 from mytable 与 select anycol ...
 - Oracle 查询NULL字段/空字符串
			
简单记录一下: 工作中需要查询某个字段值为空的数据, 最开始查询的时候按照以前的思路 : 1.where 字段名=NULL,点击F8,未查到结果: 2.where 字段名='',点击F8,未查到结果: ...
 - centos7-collabora-office(在线文档编辑)
			
1.wget https://www.collaboraoffice.com/repos/CollaboraOnline/CODE-centos7/repodata/repomd.xml.key &a ...
 - [Flask] Flask问题集(后端模板渲染项目)
			
1.redirect和render_template的区别? redirect:重定向,会改变url render_template:模板渲染,用模板来渲染当前页,不会改变url 2.关于 'g' 对 ...