关于java:记录一次mybatis缓存和事务传播行为导致ut挂的排查过程

40次阅读

共计 4481 个字符,预计需要花费 12 分钟才能阅读完成。

起因

rhea 我的项目有两个 ut 始终都是挂的,之前也通过几个共事排查过,然而都没有找到解决办法,缓缓的这个问题就搁置了。因为之前负责 rhea 我的项目的共事到职,我长期接手了这个我的项目,刚好最近来了一个新共事在做新的性能开发的时候遇到了这个问题,于是我就接了一个锅,最终证实这个锅很好玩。

rhea 是一个典型的应用 mybatis orm 的 springboot 我的项目,咱们应用 h2 内存数据库做单元测试,每个单元测试都在一个事务内,都由 Transactional 进行注解。testGetBGWechatAccountByOpenid 这个 ut 的外围调用链如下

<!–more–>

调用深度较深,并且有多处应用到了事务,其中 BasePlatformUserService.insert 这个办法用到了Propagation.REQUIRES_NEW,也就是图中最左边的这个链路中最终插入了一个 PlatformUser

ut 代码如下:

    @Test
        @Transactional
    public void testGetBGWechatAccountByOpenid() {OpenidRo openidRo = OpenidRo.builder()
            .openid(openidZmall)
            .appId(appIdZmall)
            .unionid(unionid)
            .openAppId(openAppId)
            .platformCategory(PlatformCategoriesEnum.Zmall.getValue())
            .service(ServicesEnum.Server.getValue())
            .serviceBusinessGroupId(serviceBusinessGroup2)
            .alived(false)
            .build();
        RheaAccount rheaAccount = platformUserService.getAccountByOpenId(openidRo);
        Assert.assertEquals(rheaAccount.getPhone(), phone2);

        RheaPlatformUser platformUser = platformUserMapper.getByOpenIdAndBG(openidZmall, appIdZmall, serviceBusinessGroup2, ServicesEnum.Server.getValue());
        Assert.assertEquals(rheaAccount.getId(), platformUser.getAccountId());

    }

然而在 ut 外面应用 getByOpenIdAndBG 查问 platformUser 却是 null 导致最终 platformUser.getAccountId()这个办法抛出了 NPE。

常识储备

排查这个问题会用到以下两个知识点

  • 事务流传行为 -Propagation
  • mybatis 缓存
  • 事务和 mybatis Session 的关联

事务流传行为

Springboot 的 Transactional 的实现蕴含两局部,一个局部是事务流传行为,一个局部是数据库隔离级别,代码如下:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {@AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;
    
    ...

数据库隔离级别默认是 Isolation.DEFAULT,也就是应用数据库本身的隔离级别,Mysql 的默认隔离级别是 REPEATABLE_READ 可反复读,Oracle 的默认事务隔离级别是读已提交 READ_COMMITTED。具体的隔离级别不在此探讨。咱们须要关注事务的流传行为,也就是 Propagation。Propagation 实现如下:

public enum Propagation {REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {this.value = value;}

    public int value() {return this.value;}
}

这里咱们只是用到了 REQUIRED 和 REQUIRED_NEW,REQUIRED 也是默认的流传行为,这两个流传行为的区别在于:

  • REQUIRED:默认的 spring 事务流传级别,应用该级别的特点是,如果上下文中曾经存在事务,那么就退出到事务中执行,如果以后上下文中不存在事务,则新建事务执行。所以这个级别通常 能满足解决大多数的业务场景
  • REQUIRED_NEW:从字面即可晓得,new,每次都要一个新事务,该流传级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行以后新建事务实现当前,上下文事务复原再执行。

    • 只有在被调用办法中的数据库操作须要保留到数据库中,而不论笼罩事务的后果如何时,才应该应用 REQUIRES_NEW 事务属性
    • 举个栗子:假如尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证谬误、资金不足或其余起因,不论交易是否失败,这条信息都须要被长久化。如果没有对审计办法应用 REQUIRES_NEW 属性,审计记录就会连同尝试执行的交易一起回滚。应用 REQUIRES_NEW 属性能够确保不论初始事务的后果如何,审计数据都会被保留

mybatis 缓存

Mybatis-config.xml 中能够配置 mybatis 的本地缓存范畴 localCacheScope。

mybatis 官网解释:MyBatis 利用本地缓存机制(Local Cache)避免循环援用(circular references)和减速反复嵌套查问。默认值为 SESSION,这种状况下会缓存一个会话中执行的所有查问。若设置值为 STATEMENT,本地会话仅用在语句执行上,对雷同 SqlSession 的不同调用将不会共享数据。

用文言解释:

  • SESSION 范畴的缓存:在同一个 SqlSession 中屡次查问会缓存的 mapper 中的办法,通过验证,key 是单个查询方法

    • 间断查问则后续的查问会应用第一个查问的缓存后果——debug 时无奈找到查问的 sql 日志
    • 间断的查问则会理论执行每个查问操作——能够找到每个查问的 sql 日志
    • 间断的定义是:在以后 Session 中执行 DML 操作,或者开启了其余 Session 执行了 DML 操作,都认为是间断
  • STATEMENT 范畴的缓存:实质是不应用缓存

在新版本的 mysql 中数据库本身有本人的缓存,咱们并不需要 Mybatis 的缓存,而且 Mybatis 不是最底层的缓存,因为多个 Session 的存在,往往导致一些问题。

批改 mybatis 的默认缓存范畴能够在 Mybatis-config.xml 中退出以下配置:

<!-- 设置缓存作用域,它决定是否应用 mybatis 的缓存。零碎默认值是 SESSION,为了不应用 mybatis 缓存,设置为 STATEMENT -->
<setting name="localCacheScope" value="STATEMENT"/>

应用以下配置能够打印出 mybatis 执行时的操作 log 和 sql 语句:

<setting name="logImpl" value="STDOUT_LOGGING" />

事务和 mybatis Session 的关联

开启一个新的事务并且在新的事务中首次执行 mybatis 操作时会开启新的 mybatis Session,因而在 REQUIRES_NEW 中执行 mybatis 操作肯定会开启新的 Session

排查过程

  1. 确保 mapper 办法对应的 sql 是对的
  2. 将应用 REQUIRES_NEW 的办法改为默认的REQUIRED,发现能查问到 platformUser
  3. 在 ut 中应用其余办法查问插入的 platformUser,发现能查问到
  4. mybatis 配置加上日志,debug 发现 ut 中的查问 platformUserMapper.getByOpenIdAndBG 发现没有打印 sql
  5. 猜想可能是查问是用了 mybatis 缓存,勾销缓存发现能够查问到实在记录
  6. 剖析:REQUIRES_NEW开启的新事务中开启的新 Session 插入的记录并没有突破老 Session 缓存的查问后果,因而在老 Session 中应用雷同的查问语句是查问不到实在记录的

具体的 debug 日志如下:

红框中的就是最外层的事务开启的老 session,绿色框是两头 REQUIRES_NEW 新事务中开启的新 Session。所以对于红框这个 Session 而言,它并不知道曾经产生了 DML 操作,因而在后续持续查问时会应用最开始的查问后果,也就是 null。

这种问题通常产生在 getOrCreate 操作中。

解决

去掉 Mybatis 层面的缓存

<!-- 设置缓存作用域,它决定是否应用 mybatis 的缓存。零碎默认值是 SESSION,为了不应用 mybatis 缓存,设置为 STATEMENT -->
<setting name="localCacheScope" value="STATEMENT"/>

解决这个问题对于 REQUIRES_NEW 这个流传行为的了解就更粗浅了。


参考:

  1. 理解事务陷阱:https://www.ibm.com/developer…
  2. Spring 五个事务隔离级别和七个事务流传行为:https://blog.csdn.net/caoxiao…
  3. Innodb 中的事务隔离级别和锁的关系:https://tech.meituan.com/2014…
  4. Mybatis XML 配置:https://mybatis.org/mybatis-3…

记得帮我点赞哦!

精心整顿了计算机各个方向的从入门、进阶、实战的视频课程和电子书,依照目录正当分类,总能找到你须要的学习材料,还在等什么?快去关注下载吧!!!

朝思暮想,必有回响,小伙伴们帮我点个赞吧,非常感谢。

我是职场亮哥,YY 高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。

听我说,提高多,程序人生一把梭

如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。

职场亮哥文章列表:更多文章

自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!

正文完
 0