鎖超時(shí)發(fā)現(xiàn)parallelStream并行流線程上下文坑解決
detached entity passed to persist問題
就我之前因?yàn)樵谔幚韏pa持久化對(duì)象上下文時(shí),spring jpa關(guān)于線程池異步執(zhí)行導(dǎo)致detached entity passed to persist問題排查和解決
我這邊有個(gè)批量插入用戶OpenUser和應(yīng)用OpenApp關(guān)聯(lián)關(guān)系數(shù)據(jù)的操作,由于耗時(shí)較長(zhǎng)時(shí)間,所以準(zhǔn)備用線程池異步執(zhí)行操作,然而卻遇到了一個(gè)jpa的detached entity passed to persist問題,我這邊的操作是批量保存一個(gè)OpenAppUser關(guān)聯(lián)關(guān)系表,所以需要先獲得對(duì)應(yīng)OpenUser和OpenApp的引用,再設(shè)置到關(guān)聯(lián)對(duì)象OpenAppUser里,然后在保存,我這邊是先通過userRepository.findById(userId)獲取到OpenUser,然后openAppUser.setOpenUser(openUser),在執(zhí)行appUserRepository.save(openAppUser);時(shí)發(fā)生了如標(biāo)題上的錯(cuò)誤,說是OpenUser對(duì)象處于游離態(tài),無法保存。
經(jīng)過排查,我這邊是因?yàn)镺penAppUser類里設(shè)置了@ManyToOne(cascade = CascadeType.ALL)級(jí)聯(lián)OpenUser,所以在保存OpenAppUser的時(shí)候會(huì)級(jí)聯(lián)操作OpenUser,本來在沒有開線程異步的情況下,因?yàn)镺penUser之前通過findById查出來了,所以在jpa的PersistenceContext里是有該OpenUser的脫管對(duì)象的,這時(shí)候就不會(huì)報(bào)錯(cuò),而在線程異步的情況下context里確沒有該脫管對(duì)象了
(這里說明一下,為啥不開線程有,開了線程沒有?)因?yàn)閟pring-boot默認(rèn)jpa.open-in-view=true,會(huì)使用ThreadLocal在當(dāng)前線程里保存EntityManager上下文信息,所以在整個(gè)controller里都是使用的同一個(gè)context
PersistenceContext持久性上下文有兩種類型
- 事務(wù)范圍的持久性上下文;當(dāng)我們?cè)谑聞?wù)中執(zhí)行任何操作時(shí),EntityManager 會(huì)檢查持久性上下文。 如果存在,則將使用它。否則,它將創(chuàng)建一個(gè)持久性上下文
- 擴(kuò)展范圍的持久性上下文;擴(kuò)展持久性上下文可以跨越多個(gè)事務(wù)。我們可以在沒有事務(wù)的情況下持久化實(shí)體,但不能在沒有事務(wù)的情況下刷新它。
在@PersistenceContext注解里type可以指定范圍:PersistenceContextType.TRANSACTION;PersistenceContextType.EXTENDED
而當(dāng)我們用線程池異步的時(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)沒有在PersistenceContext上下文里了,不是脫管對(duì)象了(具體可以看EntityState entityState = getEntityState( entity, entityName, entityEntry, source );里面的實(shí)現(xiàn),有幾種判斷條件,是不是脫管對(duì)象,有沒有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攔截器改造一下自己線程里的PersistenceContext上下文生效范圍,就可以解決該異常了
parallelStream并行流
parallelStream并行流給我的印象就是會(huì)讀不到父線程的上下文的,所以應(yīng)該在父線程里的事務(wù)和在parallelStream里的事務(wù)應(yīng)該是區(qū)分的,而不是共用同一個(gè)事務(wù)的,然而今天因?yàn)橐粋€(gè)鎖超時(shí)的問題,發(fā)現(xiàn)并沒有那么簡(jiǎn)單,下面我們一步一步來驗(yàn)證。
鎖超時(shí)場(chǎng)景
具體的業(yè)務(wù)我不講了,就說下偽代碼
@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,拿到用戶的open_app_user_id后再進(jìn)行其他業(yè)務(wù)邏輯,這里按我原來的理解是雖然我在controller的方法上加了@Transactional注解,但是parallelStream里的事務(wù)應(yīng)該都是獨(dú)立的,不會(huì)是同一個(gè)事務(wù),所以即使有數(shù)據(jù)重復(fù),第一個(gè)線程插入后,第二個(gè)線程也只會(huì)插入失?。ú粫?huì)報(bào)錯(cuò),因?yàn)槲壹恿薸gnore),所以即使并行也不會(huì)有問題的,然而卻發(fā)生了鎖超時(shí)的問題。
查看鎖超時(shí)以及定位的操作可以看我前面的文章,通過查找mysql的 http://m.fzitv.net/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ù)沒有提交?
因?yàn)楝F(xiàn)在都是spring的聲明式事務(wù)管理,spring是在有@Transactional注解的情況下,執(zhí)行完了才提交事務(wù),在沒有@Transactional注解的情況下,每個(gè)方法都差不多可以理解成原子,比如我上面的jdbcTemplate.update()這個(gè)方法就是一個(gè)事務(wù),執(zhí)行完了就直接提交事務(wù)了。
驗(yàn)證
因?yàn)閟pring是把事務(wù)上下文放在ThreadLocal里了,主要是用TransactionSynchronizationManager這個(gè)類來管理,所以我寫了一個(gè)demo來進(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
沒有注釋掉@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ì)有線程上下文里的值,我并沒有做什么操作,而且10個(gè)并行里只有一個(gè)(這里并不是說明固定只有一次,下面會(huì)說明)獲得了線程上下文的信息
測(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并不完全都是另開了線程,其中有一個(gè)是屬于主線程的,可以使用System.err.println(Thread.currentThread().getName());查看當(dāng)前線程的名稱,我發(fā)現(xiàn)parallelStream會(huì)把當(dāng)前主線程也作為一個(gè)執(zhí)行線程去執(zhí)行任務(wù)
后面我再去了解了一下parallelStream的實(shí)現(xiàn),在這個(gè)方法上的注解里第一句話有個(gè)單詞是possibly,是“可能”返回并行流,原來參與并行處理的線程有主線程以及ForkJoinPool中的worker線程,所以parallelStream是有兩種情況的,一是可能只一個(gè)線程并發(fā)執(zhí)行,二是多個(gè)線程并行執(zhí)行,而我這里導(dǎo)致鎖超時(shí),就是因?yàn)橛玫搅酥骶€程,所以在并行插入的時(shí)候,有個(gè)處理有事務(wù)上下文,導(dǎo)致一直沒有提交事務(wù)(@Transactional注釋方法的方法沒有跑完,這里也不可能跑完),所以其他線程的插入就一直等待這個(gè),產(chǎn)生了鎖超時(shí)報(bào)錯(cuò)
以上就是鎖超時(shí)發(fā)現(xiàn)parallelStream并行流線程上下文坑解決的詳細(xì)內(nèi)容,更多關(guān)于parallelStream并行流線程坑的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IntelliJ IDEA基于Scala實(shí)現(xiàn)Git檢查工具
這篇文章主要介紹了如何使用Scala實(shí)現(xiàn)自定義的Git檢查工具,大家可以基于本文的示例進(jìn)行擴(kuò)展與實(shí)現(xiàn),也可以進(jìn)行其他應(yīng)用方向的嘗試,感興趣的可以了解下2023-08-08
IDEA導(dǎo)入eclipse項(xiàng)目并且部署到tomcat的步驟詳解
這篇文章主要給大家介紹了關(guān)于IDEA導(dǎo)入eclipse項(xiàng)目并且部署到tomcat的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
IDEA下lombok安裝及找不到get,set的問題的解決方法
這篇文章主要介紹了IDEA下lombok安裝及找不到get,set的問題的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
Java實(shí)現(xiàn)讀取Word模板文檔并替換內(nèi)容生成新文檔
Spring data elasticsearch使用方法詳解
@Valid和@Validated注解校驗(yàn)以及異常處理方式
遠(yuǎn)程調(diào)用@FeignClient注解屬性使用詳解

