共计 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
排查过程
- 确保 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 高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。
听我说,提高多,程序人生一把梭
如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。
职场亮哥文章列表:更多文章
自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!