最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring Boot 事務(wù)實(shí)戰(zhàn)之如何解決 DB 與 MQ 的"雙寫(xiě)不一致"問(wèn)題

 更新時(shí)間:2025年12月10日 10:10:29   作者:qq_25624705  
本文結(jié)合代碼案例探討如何利用Spring的 TransactionSynchronizationManager 實(shí)現(xiàn)事務(wù)提交后觸發(fā)(Trigger After Commit)機(jī)制,優(yōu)雅解決數(shù)據(jù)庫(kù)與消息隊(duì)列的"雙寫(xiě)一致性"問(wèn)題,感興趣的朋友跟隨小編一起看看吧

摘要:在分布式系統(tǒng)中,“先存數(shù)據(jù)庫(kù)還是先發(fā)消息”是一個(gè)經(jīng)典的架構(gòu)難題。特別是在 IM 系統(tǒng)的多媒體消息處理場(chǎng)景中,如果處理順序不當(dāng),不僅會(huì)導(dǎo)致對(duì)象存儲(chǔ)(OSS)中產(chǎn)生無(wú)法回收的“孤兒文件”,還會(huì)引發(fā)并發(fā)重復(fù)處理的問(wèn)題。本文結(jié)合代碼案例,探討如何利用 Spring 的 TransactionSynchronizationManager 實(shí)現(xiàn) 事務(wù)提交后觸發(fā) (Trigger After Commit) 機(jī)制,優(yōu)雅解決數(shù)據(jù)庫(kù)與消息隊(duì)列的“雙寫(xiě)一致性”問(wèn)題。

1. 引言:一個(gè)看似簡(jiǎn)單的順序問(wèn)題

在“信令與媒體分離”的架構(gòu)中,核心流程通常如下:API 服務(wù)收到消息 -> 落庫(kù)(標(biāo)記為 Pending) -> 異步通知 Worker 搬運(yùn)文件。

這一流程涉及兩個(gè)異構(gòu)系統(tǒng)的寫(xiě)操作:

  • DB Write:將消息元數(shù)據(jù)寫(xiě)入 MySQL。
  • MQ Write:將搬運(yùn)任務(wù)發(fā)布到 NATS。

在實(shí)際開(kāi)發(fā)中,直覺(jué)性的代碼編寫(xiě)往往會(huì)陷入以下誤區(qū):

誤區(qū)一:先發(fā)消息,后入庫(kù)

// ? 錯(cuò)誤示范
natsPublisher.publish(task); // 1. 消息發(fā)出,Worker 開(kāi)始下載轉(zhuǎn)存
messageRepository.save(message); // 2. 數(shù)據(jù)庫(kù)報(bào)錯(cuò)(如字段超長(zhǎng)、唯一鍵沖突)
// 后果:DB 回滾,業(yè)務(wù)無(wú)記錄,但 OSS 中產(chǎn)生了一個(gè)永遠(yuǎn)無(wú)法被引用的“孤兒文件”。

誤區(qū)二:在事務(wù)內(nèi)發(fā)消息

// ? 錯(cuò)誤示范
@Transactional
public void handle() {
    messageRepository.save(message);
    natsPublisher.publish(task); 
    // 3. 代碼執(zhí)行完畢,但在事務(wù)提交(Commit)的一瞬間數(shù)據(jù)庫(kù)連接斷開(kāi)
}
// 后果:Worker 收到任務(wù)并完成處理,但在回調(diào)更新?tīng)顟B(tài)時(shí)發(fā)現(xiàn) DB 中不存在該記錄。

2. 核心方案:事務(wù)提交后的“驚險(xiǎn)一躍”

為了保證 “只有數(shù)據(jù)庫(kù)確確實(shí)實(shí)持久化成功了,才去觸發(fā)異步任務(wù)”,最佳實(shí)踐是利用 Spring 框架提供的事務(wù)同步機(jī)制。

以下是優(yōu)化后的代碼實(shí)現(xiàn):

2.1 主業(yè)務(wù)邏輯

// 1. 準(zhǔn)備階段:預(yù)生成任務(wù)(純內(nèi)存操作,無(wú)副作用)
// 此時(shí)并沒(méi)有真正發(fā)送 NATS 消息,只是構(gòu)建了對(duì)象
List<MediaTransferTask> mediaTasks = prepareMediaTransferTasks(msg, ids.sessionId());
// 2. 構(gòu)建消息實(shí)體
WxMessage message = buildMessage(msg, accountId, ids.sessionId(), ids.senderId());
try {
    // 【核心步驟 A】數(shù)據(jù)庫(kù)落庫(kù) (Source of Truth)
    // 這是唯一的“事實(shí)來(lái)源”。如果這里失敗,后續(xù)一切都不應(yīng)發(fā)生。
    messageRepository.save(message);
    log.info("Message saved: id={}, wxid={}", message.getId(), message.getWxid());
    // 3. 發(fā)布會(huì)話(huà)更新事件 (內(nèi)存事件或 MQ)
    SessionUpdateEvent event = SessionUpdateEvent.builder()
        .accountId(accountId)
        // ... build params
        .build();
    sessionEventPublisher.publishSessionUpdate(event);
    // 【核心步驟 B】注冊(cè)事務(wù)回調(diào)
    // 關(guān)鍵點(diǎn):這里不是立即發(fā)送,而是“預(yù)約”發(fā)送
    publishMediaTransferTasksAfterCommit(mediaTasks);
    return ProcessResult.success();
} catch (DataIntegrityViolationException e) {
    // 【并發(fā)場(chǎng)景的保護(hù)】
    // 如果兩個(gè)線(xiàn)程同時(shí)處理同一條消息(如網(wǎng)絡(luò)重放或客戶(hù)端重試),
    // 數(shù)據(jù)庫(kù)的唯一索引會(huì)拋出此異常。
    // 由于消息發(fā)送邏輯在事務(wù)提交后執(zhí)行,失敗的線(xiàn)程事務(wù)回滾,
    // 因此“afterCommit”鉤子不會(huì)被觸發(fā),完美避免了 Worker 重復(fù)搬運(yùn)文件。
    log.debug("Duplicate message (concurrent): {}", msg.getMessageId());
    return ProcessResult.duplicate();
}

2.2 事務(wù)同步器的實(shí)現(xiàn)

publishMediaTransferTasksAfterCommit 方法利用了 Spring 的 TransactionSynchronizationManager 來(lái)掛載回調(diào)。
private void publishMediaTransferTasksAfterCommit(List<MediaTransferTask> tasks) {
    if (CollectionUtils.isEmpty(tasks)) {
        return;
    }
    // 判斷當(dāng)前是否在事務(wù)中
    if (TransactionSynchronizationManager.isActualTransactionActive()) {
        // 注冊(cè)同步器
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 【真正發(fā)送的時(shí)機(jī)】
                // 只有當(dāng) DB 事務(wù)成功 Commit 后,這一行才會(huì)執(zhí)行
                // 此時(shí) DB 里一定有數(shù)據(jù),Worker 回調(diào)一定能成功
                tasks.forEach(mediaTransferPublisher::publishMediaTransfer);
                log.debug("Async media tasks published after commit: size={}", tasks.size());
            }
        });
    } else {
        // 如果不在事務(wù)中(比如非事務(wù)方法調(diào)用),則立即發(fā)送(降級(jí)策略)
        tasks.forEach(mediaTransferPublisher::publishMediaTransfer);
    }
}

3. 深度解析:方案優(yōu)勢(shì)

3.1 杜絕“孤兒資源”

通過(guò) afterCommit 鉤子,嚴(yán)格保證了因果關(guān)系:因(DB落庫(kù)成功) -> 果(觸發(fā)搬運(yùn))。 如果 messageRepository.save(message) 因?yàn)槿魏卧颍I(yè)務(wù)校驗(yàn)失敗、數(shù)據(jù)庫(kù)異常)導(dǎo)致事務(wù)回滾,afterCommit 回調(diào)將永遠(yuǎn)不會(huì)被執(zhí)行,NATS 消息也就不會(huì)發(fā)出,從而從源頭上避免了 OSS 資源的浪費(fèi)。

3.2 天然的冪等性防護(hù)

代碼中對(duì) DataIntegrityViolationException 的捕獲處理是該方案的另一大亮點(diǎn)。 在分布式場(chǎng)景下,消息重復(fù)投遞是常見(jiàn)現(xiàn)象。

  • 無(wú)保護(hù)模式:若未加控制,兩個(gè)線(xiàn)程可能都會(huì)發(fā)出 NATS 消息,導(dǎo)致 Worker 下載上傳兩次同樣的圖片,浪費(fèi)帶寬和計(jì)算資源。
  • 事務(wù)同步模式:數(shù)據(jù)庫(kù)的唯一約束(Unique Key)充當(dāng)了“守門(mén)員”。第二個(gè)線(xiàn)程在 save 時(shí)會(huì)因沖突被拒絕,隨之事務(wù)回滾。由于事務(wù)未成功提交, 其注冊(cè)的 afterCommit 鉤子自動(dòng)失效。最終,只有搶鎖成功的線(xiàn)程才會(huì)發(fā)出唯一的一條異步任務(wù)。

4. 兜底策略:應(yīng)對(duì)“反向不一致”

雖然該方案解決了“有文件沒(méi)記錄”的問(wèn)題,但理論上仍存在極低概率的“反向不一致”:DB 提交成功了,但在執(zhí)行 afterCommit 發(fā)送 NATS 消息的一瞬間,服務(wù)宕機(jī)或斷電。

此時(shí),數(shù)據(jù)庫(kù)中存在一條狀態(tài)為 PENDING 的記錄,但永遠(yuǎn)不會(huì)有 Worker 來(lái)處理它。

為了達(dá)到金融級(jí)的一致性,系統(tǒng)應(yīng)補(bǔ)充一個(gè)兜底補(bǔ)償機(jī)制

  • **定時(shí)任務(wù) (Compensation Job)**:每隔一定周期(如 5 分鐘)掃描一次消息表。
  • 篩選條件create_time < 5分鐘前 AND media_status = 'PENDING'。
  • 補(bǔ)償動(dòng)作:重新構(gòu)建 MediaTransferTask 并補(bǔ)發(fā)到 NATS。

5. 總結(jié)

在處理“數(shù)據(jù)庫(kù)事務(wù)”與“外部系統(tǒng)調(diào)用(MQ/RPC)”混合的業(yè)務(wù)場(chǎng)景時(shí),“事務(wù)同步器(Transaction Synchronization)” 是 Spring 體系中解決雙寫(xiě)一致性問(wèn)題的利器。

通過(guò)這一模式的重構(gòu),系統(tǒng)實(shí)現(xiàn)了:

  • 資源一致性:杜絕了 OSS 孤兒文件。
  • 并發(fā)安全性:利用數(shù)據(jù)庫(kù)鎖自動(dòng)解決并發(fā)任務(wù)重復(fù)發(fā)布問(wèn)題。
  • 邏輯嚴(yán)密性:確保狀態(tài)流轉(zhuǎn)嚴(yán)格遵循業(yè)務(wù)時(shí)序。

核心原則:先落庫(kù),再提交,回調(diào)之中發(fā)消息。

到此這篇關(guān)于Spring Boot 事務(wù)實(shí)戰(zhàn)之如何優(yōu)雅解決 DB 與 MQ 的"雙寫(xiě)不一致"問(wèn)題的文章就介紹到這了,更多相關(guān)springboot db與mq雙寫(xiě)不一致內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Core核心類(lèi)庫(kù)的功能與應(yīng)用實(shí)踐分析

    Spring Core核心類(lèi)庫(kù)的功能與應(yīng)用實(shí)踐分析

    本文詳細(xì)介紹了SpringCore核心類(lèi)庫(kù)的功能、應(yīng)用實(shí)踐和底層原理,SpringCore提供了控制反轉(zhuǎn)(IOC)、依賴(lài)注入(DI)、Bean管理以及JNDI、定時(shí)任務(wù)等企業(yè)級(jí)功能,文章通過(guò)多個(gè)Java示例展示了SpringCore的應(yīng)用,感興趣的朋友跟隨小編一起看看吧
    2024-12-12
  • SpringBoot項(xiàng)目jar發(fā)布后如何獲取jar包所在目錄路徑

    SpringBoot項(xiàng)目jar發(fā)布后如何獲取jar包所在目錄路徑

    這篇文章主要介紹了SpringBoot項(xiàng)目jar發(fā)布后如何獲取jar包所在目錄路徑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Java冪等性校驗(yàn)解決重復(fù)點(diǎn)擊的六種實(shí)現(xiàn)方式

    Java冪等性校驗(yàn)解決重復(fù)點(diǎn)擊的六種實(shí)現(xiàn)方式

    在分布式系統(tǒng)中,冪等性(Idempotency)是一個(gè)非常重要的概念,,冪等性操作指的是:無(wú)論這個(gè)操作執(zhí)行多少次,結(jié)果都應(yīng)該是相同的,這是為了避免重復(fù)執(zhí)行操作引起數(shù)據(jù)的不一致,本文給大家介紹了Java冪等性校驗(yàn)解決重復(fù)點(diǎn)擊的六種實(shí)現(xiàn)方式,需要的朋友可以參考下
    2025-04-04
  • SpringBoot彩色日志配置方式

    SpringBoot彩色日志配置方式

    這篇文章主要介紹了SpringBoot彩色日志配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • Java時(shí)間處理詳細(xì)教程與最佳實(shí)踐案例

    Java時(shí)間處理詳細(xì)教程與最佳實(shí)踐案例

    本文系統(tǒng)講解Java時(shí)間處理,涵蓋基礎(chǔ)概念、傳統(tǒng)API(Date/Calendar/SimpleDateFormat)、Java8新API(java.time包)及高級(jí)應(yīng)用,提供代碼案例與最佳實(shí)踐,適合開(kāi)發(fā)者全面掌握時(shí)間操作技巧,感興趣的朋友一起看看吧
    2025-08-08
  • SpringBoot項(xiàng)目中自定義線(xiàn)程池的方法步驟

    SpringBoot項(xiàng)目中自定義線(xiàn)程池的方法步驟

    在Spring Boot項(xiàng)目中,合理配置和使用線(xiàn)程池對(duì)于提升應(yīng)用程序性能、優(yōu)化資源利用和保證系統(tǒng)穩(wěn)定性至關(guān)重要,本文將詳細(xì)介紹如何在Spring Boot中自定義線(xiàn)程池,包括配置方式、參數(shù)調(diào)優(yōu)、使用方法和最佳實(shí)踐,需要的朋友可以參考下
    2025-09-09
  • 基于Java和Lua實(shí)現(xiàn)IP鎖定功能

    基于Java和Lua實(shí)現(xiàn)IP鎖定功能

    本文主要介紹了?Lua?語(yǔ)言的特點(diǎn)、應(yīng)用場(chǎng)景、基本語(yǔ)法,以及使用?Redis?+?Lua?腳本實(shí)現(xiàn)限制?IP?多次輸入錯(cuò)誤密碼功能,包括?Lua?腳本示例、參數(shù)解釋、邏輯流程、執(zhí)行方式,并給出了?Java?應(yīng)用示例及代碼解釋,還闡述了參數(shù)傳遞和使用,需要的朋友可以參考下
    2025-01-01
  • Java與MySQL時(shí)間不一致問(wèn)題解決

    Java與MySQL時(shí)間不一致問(wèn)題解決

    本文主要介紹了Java與MySQL時(shí)間不一致問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • idea如何在service窗口中顯示多個(gè)服務(wù)

    idea如何在service窗口中顯示多個(gè)服務(wù)

    這篇文章主要介紹了idea如何在service窗口中顯示多個(gè)服務(wù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-09-09
  • Mybatis-Plus樂(lè)觀(guān)鎖配置流程

    Mybatis-Plus樂(lè)觀(guān)鎖配置流程

    這篇文章主要介紹了Mybatis-Plus樂(lè)觀(guān)鎖配置使用流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作有一定的參考學(xué)習(xí)價(jià)值,感興趣的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧
    2024-01-01

最新評(píng)論

额尔古纳市| 永宁县| 全椒县| 全州县| 长岛县| 丽江市| 双峰县| 南汇区| 泾源县| 吉水县| 辽阳市| 丹巴县| 雷州市| 杭锦后旗| 景宁| 沾化县| 徐水县| 湘阴县| 仲巴县| 乌兰察布市| 陇南市| 庆元县| 兴义市| 泽州县| 南安市| 光泽县| 綦江县| 盐津县| 饶平县| 乐至县| 沾益县| 宜君县| 开原市| 磐安县| 阿坝县| 深水埗区| 开封市| 济源市| 柳州市| 牟定县| 那曲县|