起因
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
排查过程
- 确保mapper办法对应的sql是对的
- 将应用
REQUIRES_NEW
的办法改为默认的REQUIRED
,发现能查问到platformUser - 在ut中应用其余办法查问插入的platformUser,发现能查问到
- mybatis配置加上日志,debug发现ut中的查问platformUserMapper.getByOpenIdAndBG发现没有打印sql
- 猜想可能是查问是用了mybatis缓存,勾销缓存发现能够查问到实在记录
- 剖析:
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
这个流传行为的了解就更粗浅了。
参考:
- 理解事务陷阱:https://www.ibm.com/developer...
- Spring五个事务隔离级别和七个事务流传行为:https://blog.csdn.net/caoxiao...
- Innodb中的事务隔离级别和锁的关系:https://tech.meituan.com/2014...
- Mybatis XML配置:https://mybatis.org/mybatis-3...
记得帮我点赞哦!
精心整顿了计算机各个方向的从入门、进阶、实战的视频课程和电子书,依照目录正当分类,总能找到你须要的学习材料,还在等什么?快去关注下载吧!!!
朝思暮想,必有回响,小伙伴们帮我点个赞吧,非常感谢。
我是职场亮哥,YY高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。听我说,提高多,程序人生一把梭
如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。
职场亮哥文章列表:更多文章
自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!