鎖超時(shí)發(fā)現(xiàn)parallelStream并行流線(xiàn)程上下文坑解決
detached entity passed to persist問(wèn)題
就我之前因?yàn)樵谔幚韏pa持久化對(duì)象上下文時(shí),spring jpa關(guān)于線(xiàn)程池異步執(zhí)行導(dǎo)致detached entity passed to persist問(wèn)題排查和解決
我這邊有個(gè)批量插入用戶(hù)OpenUser和應(yīng)用OpenApp關(guān)聯(lián)關(guān)系數(shù)據(jù)的操作,由于耗時(shí)較長(zhǎng)時(shí)間,所以準(zhǔn)備用線(xiàn)程池異步執(zhí)行操作,然而卻遇到了一個(gè)jpa的detached entity passed to persist問(wèn)題,我這邊的操作是批量保存一個(gè)OpenAppUser關(guān)聯(lián)關(guān)系表,所以需要先獲得對(duì)應(yīng)OpenUser和OpenApp的引用,再設(shè)置到關(guān)聯(lián)對(duì)象OpenAppUser里,然后在保存,我這邊是先通過(guò)userRepository.findById(userId)獲取到OpenUser,然后openAppUser.setOpenUser(openUser),在執(zhí)行appUserRepository.save(openAppUser);時(shí)發(fā)生了如標(biāo)題上的錯(cuò)誤,說(shuō)是OpenUser對(duì)象處于游離態(tài),無(wú)法保存。
經(jīng)過(guò)排查,我這邊是因?yàn)镺penAppUser類(lèi)里設(shè)置了@ManyToOne(cascade = CascadeType.ALL)級(jí)聯(lián)OpenUser,所以在保存OpenAppUser的時(shí)候會(huì)級(jí)聯(lián)操作OpenUser,本來(lái)在沒(méi)有開(kāi)線(xiàn)程異步的情況下,因?yàn)镺penUser之前通過(guò)findById查出來(lái)了,所以在jpa的PersistenceContext里是有該OpenUser的脫管對(duì)象的,這時(shí)候就不會(huì)報(bào)錯(cuò),而在線(xiàn)程異步的情況下context里確沒(méi)有該脫管對(duì)象了
(這里說(shuō)明一下,為啥不開(kāi)線(xiàn)程有,開(kāi)了線(xiàn)程沒(méi)有?)因?yàn)閟pring-boot默認(rèn)jpa.open-in-view=true,會(huì)使用ThreadLocal在當(dāng)前線(xiàn)程里保存EntityManager上下文信息,所以在整個(gè)controller里都是使用的同一個(gè)context
PersistenceContext持久性上下文有兩種類(lèi)型
- 事務(wù)范圍的持久性上下文;當(dāng)我們?cè)谑聞?wù)中執(zhí)行任何操作時(shí),EntityManager 會(huì)檢查持久性上下文。 如果存在,則將使用它。否則,它將創(chuàng)建一個(gè)持久性上下文
- 擴(kuò)展范圍的持久性上下文;擴(kuò)展持久性上下文可以跨越多個(gè)事務(wù)。我們可以在沒(méi)有事務(wù)的情況下持久化實(shí)體,但不能在沒(méi)有事務(wù)的情況下刷新它。
在@PersistenceContext注解里type可以指定范圍:PersistenceContextType.TRANSACTION;PersistenceContextType.EXTENDED
而當(dāng)我們用線(xiàn)程池異步的時(shí)候,拿不到之前的EntityManager的配置信息,而spring jpa repository默認(rèn)的方法上都會(huì)自帶一個(gè)事務(wù),所以在執(zhí)行完userRepository.findById(userId)獲取到OpenUser之后,會(huì)commit,而commit操作會(huì)clear掉EntityManager里保存的脫管對(duì)象OpenUser,等到appUserRepository.save(openAppUser);保存的時(shí)候,由于引用的OpenUser已經(jīng)沒(méi)有在PersistenceContext上下文里了,不是脫管對(duì)象了(具體可以看EntityState entityState = getEntityState( entity, entityName, entityEntry, source );里面的實(shí)現(xiàn),有幾種判斷條件,是不是脫管對(duì)象,有沒(méi)有id、version等等屬性),就會(huì)報(bào)detached entity passed to persist這個(gè)異常
所以根據(jù)實(shí)際情況,我們只要參考o(jì)pen-in-view=true產(chǎn)生對(duì)應(yīng)的OpenEntityManagerInViewInterceptor攔截器改造一下自己線(xiàn)程里的PersistenceContext上下文生效范圍,就可以解決該異常了
parallelStream并行流
parallelStream并行流給我的印象就是會(huì)讀不到父線(xiàn)程的上下文的,所以應(yīng)該在父線(xiàn)程里的事務(wù)和在parallelStream里的事務(wù)應(yīng)該是區(qū)分的,而不是共用同一個(gè)事務(wù)的,然而今天因?yàn)橐粋€(gè)鎖超時(shí)的問(wèn)題,發(fā)現(xiàn)并沒(méi)有那么簡(jiǎn)單,下面我們一步一步來(lái)驗(yàn)證。
鎖超時(shí)場(chǎng)景
具體的業(yè)務(wù)我不講了,就說(shuō)下偽代碼
@PostMapping("/saveUser")
@Transactional
public void saveUser(@RequestBody List<Complex> list) {
list.parallelStream().forEach(complex->{
Integer appId = complex.getAppId();
Integer userId = complex.getUserId();
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "insert ignore into open_app_user (app_id, open_id, user_status, creator, modifier, create_time, modify_time, status, version) values ("+appId+","+userId+",0,1,1,now(),now(),1,1)";
int id = jdbcTemplate.update(con -> con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS), keyHolder);
});
//todo 業(yè)務(wù)邏輯...
}這里我有個(gè)批量保存的邏輯,需要先保存一個(gè)中間表open_app_user表(該表app_id和open_id是聯(lián)合唯一鍵)獲得id,拿到用戶(hù)的open_app_user_id后再進(jìn)行其他業(yè)務(wù)邏輯,這里按我原來(lái)的理解是雖然我在controller的方法上加了@Transactional注解,但是parallelStream里的事務(wù)應(yīng)該都是獨(dú)立的,不會(huì)是同一個(gè)事務(wù),所以即使有數(shù)據(jù)重復(fù),第一個(gè)線(xiàn)程插入后,第二個(gè)線(xiàn)程也只會(huì)插入失?。ú粫?huì)報(bào)錯(cuò),因?yàn)槲壹恿薸gnore),所以即使并行也不會(huì)有問(wèn)題的,然而卻發(fā)生了鎖超時(shí)的問(wèn)題。
查看鎖超時(shí)以及定位的操作可以看我前面的文章,通過(guò)查找mysql的 http://www.dbjr.com.cn/article/259480.htm
select * from information_schema.INNODB_TRX; select * from performance_schema.data_lock_waits; select * from performance_schema.data_locks;
定位到了這里,然而我也百思不得其解,為啥會(huì)鎖超時(shí)呢,這里應(yīng)該都是馬上執(zhí)行就馬上釋放了啊,難道是其中的事務(wù)沒(méi)有提交?
因?yàn)楝F(xiàn)在都是spring的聲明式事務(wù)管理,spring是在有@Transactional注解的情況下,執(zhí)行完了才提交事務(wù),在沒(méi)有@Transactional注解的情況下,每個(gè)方法都差不多可以理解成原子,比如我上面的jdbcTemplate.update()這個(gè)方法就是一個(gè)事務(wù),執(zhí)行完了就直接提交事務(wù)了。
驗(yàn)證
因?yàn)閟pring是把事務(wù)上下文放在ThreadLocal里了,主要是用TransactionSynchronizationManager這個(gè)類(lèi)來(lái)管理,所以我寫(xiě)了一個(gè)demo來(lái)進(jìn)行驗(yàn)證
@GetMapping("/get")
@Transactional
public String get() {
List<Complex> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Complex(1, 1));
}
list.parallelStream().forEach(complex->{
Map<Object, Object> resourceMap = TransactionSynchronizationManager.getResourceMap();
System.err.println("count:"+resourceMap.size());
Integer appId = complex.getAppId();
Integer userId = complex.getUserId();
String sql = "insert ignore into open_app_user (app_id, open_id, user_status, creator, modifier, create_time, modify_time, status, version) values ("+appId+","+userId+",0,1,1,now(),now(),1,1)";
int update = jdbcTemplate.update(sql);
});
return "hello, world! ";
}有趣的事情發(fā)生了,我在注釋掉@Transactional注解時(shí),代碼里resourceMap.size()返回的內(nèi)容是竟然不一樣,因?yàn)槲业膌ist有10條記錄,差不多就是10個(gè)并行,然而我的輸出卻是:
count:1
count:0
count:0
count:0
count:0
count:0
count:0
count:0
count:0
count:0
沒(méi)有注釋掉@Transactional注解時(shí),輸出是:
count:2
count:0
count:0
count:0
count:0
count:0
count:0
count:0
count:0
count:0
并且還會(huì)出現(xiàn)鎖超時(shí)的現(xiàn)象,奇怪的地方就是為啥我用的parallelStream會(huì)有線(xiàn)程上下文里的值,我并沒(méi)有做什么操作,而且10個(gè)并行里只有一個(gè)(這里并不是說(shuō)明固定只有一次,下面會(huì)說(shuō)明)獲得了線(xiàn)程上下文的信息
測(cè)試
我又進(jìn)一步測(cè)試,偽代碼改成:
@GetMapping("/get")
public void get() {
List<Complex> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Complex(1, 1));
}
ThreadLocal local = new ThreadLocal();
local.set("parent_set_value");
list.parallelStream().forEach(complex->{
System.err.println(local.get());
});
}結(jié)果如我所料,輸出為:
parent_set_value
null
null
null
null
null
null
null
null
null
使用parallelStream并不完全都是另開(kāi)了線(xiàn)程,其中有一個(gè)是屬于主線(xiàn)程的,可以使用System.err.println(Thread.currentThread().getName());查看當(dāng)前線(xiàn)程的名稱(chēng),我發(fā)現(xiàn)parallelStream會(huì)把當(dāng)前主線(xiàn)程也作為一個(gè)執(zhí)行線(xiàn)程去執(zhí)行任務(wù)
后面我再去了解了一下parallelStream的實(shí)現(xiàn),在這個(gè)方法上的注解里第一句話(huà)有個(gè)單詞是possibly,是“可能”返回并行流,原來(lái)參與并行處理的線(xiàn)程有主線(xiàn)程以及ForkJoinPool中的worker線(xiàn)程,所以parallelStream是有兩種情況的,一是可能只一個(gè)線(xiàn)程并發(fā)執(zhí)行,二是多個(gè)線(xiàn)程并行執(zhí)行,而我這里導(dǎo)致鎖超時(shí),就是因?yàn)橛玫搅酥骶€(xiàn)程,所以在并行插入的時(shí)候,有個(gè)處理有事務(wù)上下文,導(dǎo)致一直沒(méi)有提交事務(wù)(@Transactional注釋方法的方法沒(méi)有跑完,這里也不可能跑完),所以其他線(xiàn)程的插入就一直等待這個(gè),產(chǎn)生了鎖超時(shí)報(bào)錯(cuò)
以上就是鎖超時(shí)發(fā)現(xiàn)parallelStream并行流線(xiàn)程上下文坑解決的詳細(xì)內(nèi)容,更多關(guān)于parallelStream并行流線(xiàn)程坑的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot 接收List 入?yún)⒌膸追N方法
本文主要介紹了springboot 接收List 入?yún)⒌膸追N方法,本文主要介紹了7種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03
Java 并發(fā)編程學(xué)習(xí)筆記之核心理論基礎(chǔ)
編寫(xiě)優(yōu)質(zhì)的并發(fā)代碼是一件難度極高的事情。Java語(yǔ)言從第一版本開(kāi)始內(nèi)置了對(duì)多線(xiàn)程的支持,這一點(diǎn)在當(dāng)年是非常了不起的,但是當(dāng)我們對(duì)并發(fā)編程有了更深刻的認(rèn)識(shí)和更多的實(shí)踐后,實(shí)現(xiàn)并發(fā)編程就有了更多的方案和更好的選擇。本文是對(duì)并發(fā)編程的核心理論做了下小結(jié)2016-05-05
Java中數(shù)組的創(chuàng)建與傳參方法(學(xué)習(xí)小結(jié))
這篇文章主要介紹了Java中數(shù)組的創(chuàng)建與傳參方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
Spring?IOC?xml方式進(jìn)行工廠(chǎng)Bean操作詳解
這篇文章主要介紹了Spring?IOC?xml方式進(jìn)行工廠(chǎng)Bean操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01
SpringBoot Controller Post接口單元測(cè)試示例
今天小編就為大家分享一篇關(guān)于SpringBoot Controller Post接口單元測(cè)試示例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
mybatis動(dòng)態(tài)生成sql語(yǔ)句的實(shí)現(xiàn)示例
在MyBatis中,動(dòng)態(tài)SQL是一個(gè)非常重要的特性,它允許我們根據(jù)條件動(dòng)態(tài)地生成SQL語(yǔ)句,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-11-11
Spring覆蓋容器中Bean的注解如何實(shí)現(xiàn)@OverrideBean
文章介紹了在項(xiàng)目開(kāi)發(fā)中如何通過(guò)偷梁換柱的方式重寫(xiě)Spring容器中的內(nèi)置Bean,并指出了需要注意的兩點(diǎn):1. 對(duì)應(yīng)的Bean應(yīng)基于接口注入;2. 如果不是基于接口注入,可以使用同包名同類(lèi)名的方式重寫(xiě)(可能存在潛在問(wèn)題,不推薦),文章還強(qiáng)調(diào)了“基于接口編程”的好處2025-01-01
mybatis執(zhí)行批量更新batch update 的方法(oracle,mysql兩種)
這篇文章主要介紹了mybatis執(zhí)行批量更新batch update 的方法,提供oracle和mysql兩種方法,非常不錯(cuò),需要的朋友參考下2017-01-01

