起因

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@Documentedpublic @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高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。

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

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

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

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