SpringBoot事務(wù)異步調(diào)用引發(fā)的bug解決
前言
日常開發(fā)中有沒有遇到這種場(chǎng)景,save一條數(shù)據(jù)后發(fā)起一次異步調(diào)用,舉個(gè)例子,假設(shè)我們以mysql組件和xxl-job組件為例,創(chuàng)建一條數(shù)據(jù)導(dǎo)出任務(wù),創(chuàng)建后默認(rèn)啟動(dòng)任務(wù)。那么邏輯可能大致為三步。
- 創(chuàng)建導(dǎo)出任務(wù)(DB.EXPORT_TASK)
- 創(chuàng)建導(dǎo)出任務(wù)歷史記錄(DB.EXPORT_TASK_HISTORY)
- 觸發(fā)導(dǎo)出任務(wù)(XXL-JOB)
因?yàn)樾枰瑫r(shí)要?jiǎng)?chuàng)建導(dǎo)出任務(wù)和導(dǎo)出任務(wù)歷史兩條記錄,所以代碼中需要通常要添加事務(wù)
@Service public class TaskService { ? ? @Transactional(rollbackFor = Exception.class) ? ? public String saveExportTask() { ? ? ? ? // 1. save export task ? ? ? ? // 2. save export task history? ? ? ? ? // 3. execute xxl-job ? ? } }
外層controller層只需要調(diào)用service的方法即可
@RestController public class TaskController { @Resource TaskService taskService; @PostMapping public String save() { taskService.saveExportTask(); } }
我們使用了xxl-job去觸發(fā)任務(wù)是一個(gè)異步調(diào)用的過程,當(dāng)xxl-job回調(diào)執(zhí)行器去執(zhí)行時(shí)可能需要根據(jù)job_id獲取到導(dǎo)出任務(wù)的配置,通過查詢db獲取任務(wù)詳情,比如導(dǎo)出地址了,導(dǎo)出規(guī)范等等。
看似非常和諧的場(chǎng)面,實(shí)際執(zhí)行起來則會(huì)出現(xiàn)任務(wù)不存在的問題。問題的根源其實(shí)也很好理解,就是因?yàn)樵诋惒椒椒ɡ镒隽送降氖戮蜁?huì)出現(xiàn)這種問題,當(dāng)?shù)谝徊經(jīng)]有執(zhí)行完,第三步的回調(diào)方法已經(jīng)到執(zhí)行器了,也就是說一個(gè)任務(wù)還沒存到數(shù)據(jù)庫(kù),執(zhí)行這個(gè)任務(wù)時(shí)去數(shù)據(jù)庫(kù)查該任務(wù)的明細(xì)肯定會(huì)報(bào)任務(wù)不存在異常了。
那么如何解決呢。
代碼拆分
最簡(jiǎn)單的一個(gè)方案,web應(yīng)用通常劃分為controller、service、dao層那么幾層,業(yè)務(wù)邏輯按規(guī)范寫在service層,我們把發(fā)起異步調(diào)用的方法挪到controller層,service只做數(shù)據(jù)庫(kù)操作,servcie執(zhí)行完事務(wù)提交完,再同步發(fā)起異步調(diào)用豈不就繞開了這個(gè)問題。
@RestController public class TaskController { @Resource TaskService taskService; @PostMapping public String save() { taskService.saveExportTask(); // 3. execute xxl-job } }
如果秉持著代碼和人有一個(gè)能跑就行的原則,此時(shí)已經(jīng)結(jié)束戰(zhàn)斗了,對(duì)于秉持著該原則且有點(diǎn)代碼潔癖同學(xué)頂多也就是把觸發(fā)任務(wù)的動(dòng)作封裝到一個(gè)觸發(fā)service里調(diào)用。
TransactionSynchronizationManager事務(wù)回調(diào)
當(dāng)然還是有很多同學(xué)對(duì)待技術(shù)是追求極致精神的,那么有沒有優(yōu)雅的方式去解決這個(gè)問題,那就要看springboot的事務(wù)回調(diào)能力了。
TransactionSynchronizationManager 事務(wù)同步器,從new TransactionSynchronization()可實(shí)現(xiàn)的方法上即可管中窺豹可見一斑,我們完全可以通過實(shí)現(xiàn)歇歇方法實(shí)現(xiàn)事務(wù)完成后回調(diào)的邏輯。
直接上代碼舉例子
@Service public class TaskService { ? ? @Transactional(rollbackFor = Exception.class) ? ? public String saveExportTask() { ? ? ? ? // 1. save export task ? ? ? ? // 2. save export task history? ? ? ? ? TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter(){ ? ? ? ? ? ? public void afterCommit(){ ? ? ? ? ? ? ? ? System.out.println("commit!!!"); ? ? ? ? ? ? ? ? // 3. execute xxl-job ? ? ? ? ? ? } ? ? ? ? }); ? ? } }
這樣一來就可以保證是在事務(wù)結(jié)束之后去執(zhí)行xxl-job的任務(wù)。
@TransactionalEventListener注解要和事務(wù)事件監(jiān)控
TransactionalEventListener,自 Spring 4.2 以來,可以使用基于注釋的配置為提交后事件(或更一般的事務(wù)同步事件,例如回滾)定義偵聽器。本質(zhì)上是基于核心 spring中的事件處理。使用這種方法可以避免對(duì) TransactionSynchronizationManager 的硬編碼。
首先需要自定義監(jiān)聽器
@Component public class TaskEventListener { ? ?@Autowired ? ?private TaskService taskService; ? ?@TransactionalEventListener ? ?public void handleOrderCreatedEvent(TaskCreatedEvent event) { ? ? ? Task task = event.getTask(); ? ? ? // 處理訂單創(chuàng)建事件 ? ? ? try { ? ? ? ? ?taskService.processOrder(task); ? ? ? } catch (Exception e) { ? ? ? ? ?// 處理失敗,拋出異常,事務(wù)回滾 ? ? ? ? ?throw new RuntimeException(e); ? ? ? } ? ?} ? ?@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) ? ?public void handleOrderCompletedEvent(TaskCompletedEvent event) { ? ? ? Task task = event.getTask(); ? ? ? // 處理訂單完成事件 ? ? ? try { ? ? ? ? ?taskService.sendOrderConfirmationEmail(task); ? ? ? } catch (Exception e) { ? ? ? ? ?// 處理失敗,不影響事務(wù) ? ? ? ? ?e.printStackTrace(); ? ? ? } ? ?} }
定義事件
public class TaskCompletedEvent { ? ?private Task task; ? ?public TaskCompletedEvent(Task task) { ? ? ? this.task = task; ? ?} ? ?public Task getTask() { ? ? ? return task; ? ?} }
@TransactionalEventListener注解要和@Transactional注解配合使用,確保在事務(wù)完成后才會(huì)觸發(fā)回調(diào)方法。@TransactionalEventListener注解也可以指定回調(diào)方法的觸發(fā)時(shí)機(jī),可以選擇在事務(wù)提交后觸發(fā)(默認(rèn))或在事務(wù)回滾后觸發(fā)。
到此這篇關(guān)于SpringBoot事務(wù)異步調(diào)用引發(fā)的bug解決的文章就介紹到這了,更多相關(guān)SpringBoot事務(wù)異步調(diào)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用Java實(shí)現(xiàn)全國(guó)天氣預(yù)報(bào)的api接口調(diào)用示例
查詢天氣預(yù)報(bào)在APP中常用的一個(gè)常用功能,本文實(shí)例講述了java調(diào)用中國(guó)天氣網(wǎng)api獲得天氣預(yù)報(bào)信息的方法。分享給大家供大家參考。2016-10-10在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程
這篇文章主要介紹了在Ubuntu系統(tǒng)下安裝JDK和Tomcat的教程,這樣便是在Linux系統(tǒng)下搭建完整的Java和JSP開發(fā)環(huán)境,需要的朋友可以參考下2015-08-08MyBatis-plus報(bào)錯(cuò)Property ‘sqlSessionFactory‘ or 
這篇文章主要給大家介紹了MyBatis-plus 報(bào)錯(cuò) Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required的兩種解決方法,如果遇到相同問題的朋友可以參考借鑒一下2023-12-12Mybatis中where標(biāo)簽與if標(biāo)簽結(jié)合使用詳細(xì)說明
mybatis中if和where用于動(dòng)態(tài)sql的條件拼接,在查詢語(yǔ)句中如果缺失某個(gè)條件,通過if和where標(biāo)簽可以動(dòng)態(tài)的改變查詢條件,下面這篇文章主要給大家介紹了關(guān)于Mybatis中where標(biāo)簽與if標(biāo)簽結(jié)合使用的詳細(xì)說明,需要的朋友可以參考下2023-03-03