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

SpringBoot防止消息重復(fù)消費的方法步驟

 更新時間:2026年05月29日 09:27:44   作者:海棠Flower未眠  
這段文章詳細介紹了消息隊列中消息重復(fù)消費的問題及解決方案,重點講解了Redis唯一ID防重、數(shù)據(jù)庫唯一索引防重、業(yè)務(wù)狀態(tài)機防重和全局唯一約束防重四種方案,適合不同業(yè)務(wù)場景和并發(fā)量的需求,需要的朋友可以參考下

在項目開發(fā)中難免會用到一些消息隊列,例如經(jīng)常使用的MQ,在 MQ 消息隊列的生產(chǎn)實踐中,消息丟失、消息重復(fù)、消息積壓是三大核心難題。其中消息重復(fù)消費是100% 必然發(fā)生的問題,不屬于 Bug,而是 MQ 機制特性。 很多同學(xué)開發(fā)的訂單、支付、積分、物流系統(tǒng)。

經(jīng)常出現(xiàn):

• 同一訂單多次扣款 

• 同一筆積分多次發(fā)放 

• 重復(fù)生成訂單、重復(fù)發(fā)貨 

• 重復(fù)回調(diào)、重復(fù)更新數(shù)據(jù)   

所有問題的根源只有一個:沒有做好消息冪等性。

那么應(yīng)該怎么處理消息冪等呢?今天分享一下如何使用SpringBoot 做消息冪等。

一、為什么會出現(xiàn)消息重復(fù)消費?

MQ 設(shè)計核心原則:寧可重復(fù),絕不丟失。為了保證消息可靠性,MQ 會開啟重試機制,直接導(dǎo)致重復(fù)消費。

1.1、重復(fù)原因

 消費者 ACK 超時
消費者業(yè)務(wù)執(zhí)行成功,但返回 ACK 確認時網(wǎng)絡(luò)抖動、超時,MQ 未收到確認,判定消費失敗,重新投遞消息。(生產(chǎn)最高頻)

 消費者異常退出
業(yè)務(wù)執(zhí)行一半、執(zhí)行成功后程序宕機、重啟,未完成 ACK,觸發(fā) MQ 重試。

生產(chǎn)者重復(fù)投遞
生產(chǎn)者重試機制、接口重發(fā)、網(wǎng)絡(luò)重傳,導(dǎo)致發(fā)送多條相同消息。

MQ 集群故障切換
主從切換、節(jié)點重啟、分區(qū)重平衡,導(dǎo)致消息重復(fù)分發(fā)。

結(jié)論:所有 MQ 項目,必須強制做冪等,沒有例外。

二、什么是消息冪等性?

冪等性:接口/業(yè)務(wù)執(zhí)行 1 次 和執(zhí)行 N 次,最終業(yè)務(wù)結(jié)果完全一致,不會產(chǎn)生臟數(shù)據(jù)、重復(fù)數(shù)據(jù)、異常數(shù)據(jù)。

MQ 冪等核心目標:保證同一條消息,只會生效一次,多次消費無副作用。

所有冪等方案的核心抓手:唯一消息標識(msgId、orderId、tradeId、businessId)。

三、冪等方案

針對不同業(yè)務(wù)場景、不同并發(fā)量級,整理業(yè)界通用 4 套方案,從輕量到厚重,從通用到專用,按需選用。

3.1、方案一:Redis 唯一ID防重

3.1.1、核心原理

利用 Redis SETNX 原子命令,實現(xiàn)消息唯一占用:

  1.  每條消息攜帶全局唯一 msgId

  2.  消費前嘗試根據(jù) msgId 占坑(SETNX)

  3. 占坑成功:首次消費,執(zhí)行業(yè)務(wù)邏輯

  4. 占坑失?。褐貜?fù)消息,直接 ACK 丟棄

  5. 設(shè)置過期時間,避免 Redis 死數(shù)據(jù)堆積

3.1.2、完整代碼

3.1.2.1、Redis 工具類

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class MqIdempotentRedisUtil {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 消息冪等占坑
     * @param msgId 消息唯一ID
     * @param expireSeconds 過期時間(大于業(yè)務(wù)最大執(zhí)行時長)
     * @return true=首次消費,false=重復(fù)消費
     */
    public boolean tryLock(String msgId, long expireSeconds) {
        String key = "mq:idempotent:" + msgId;
        // SETNX 原子操作:不存在則設(shè)置,存在則返回false
        return stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "consumed", expireSeconds, TimeUnit.SECONDS);
    }
}

3.1.2.2、冪等消費者

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;

@Component
public class OrderMsgConsumer {

    @Resource
    private MqIdempotentRedisUtil idempotentRedisUtil;

    // 業(yè)務(wù)最大執(zhí)行時長5秒,鎖過期時間設(shè)30秒(預(yù)留緩沖)
    private static final long LOCK_EXPIRE_TIME = 30;

    @RabbitListener(queues = "order.pay.queue")
    public void consume(Message message, Channel channel) throws IOException {
        // 1. 獲取全局唯一消息ID(生產(chǎn)者必須傳遞)
        String msgId = message.getMessageProperties().getHeader("msgId");
        if (msgId == null || "".equals(msgId)) {
            // 無唯一ID,非法消息,直接丟棄
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }

        try {
            // 2. 冪等判斷:占坑失敗=重復(fù)消息
            boolean isFirstConsume = idempotentRedisUtil.tryLock(msgId, LOCK_EXPIRE_TIME);
            if (!isFirstConsume) {
                System.out.println("【重復(fù)消息丟棄】msgId:" + msgId);
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                return;
            }

            // 3. 核心業(yè)務(wù)邏輯(下單、支付、積分、物流等)
            doBusiness(msgId);

            // 4. 手動ACK確認消費成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 消費異常,拒絕消息,重回隊列重試
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            e.printStackTrace();
        }
    }

    private void doBusiness(String msgId) {
        // 模擬業(yè)務(wù)執(zhí)行
        System.out.println("【首次消費成功】處理消息:" + msgId);
    }
}

3.1.3、優(yōu)缺點分析

? 優(yōu)點:性能高、無數(shù)據(jù)庫壓力、適配所有MQ、代碼簡單、不侵入業(yè)務(wù)

? 缺點:依賴Redis,Redis宕機需降級兜底

?? 適用場景:絕大多數(shù)互聯(lián)網(wǎng)業(yè)務(wù)、中小高并發(fā)場景(通用首選)

3.2、方案二:數(shù)據(jù)庫唯一索引防重

3.2.1、原理

新建消息防重表,給 msgId 設(shè)置唯一索引,利用數(shù)據(jù)庫唯一約束實現(xiàn)冪等:

  1. 消費前先插入防重記錄
  2.  插入成功:首次消費,執(zhí)行業(yè)務(wù)
  3. 插入報錯(唯一沖突):重復(fù)消息,直接丟棄

3.2.2、表結(jié)構(gòu)設(shè)計

CREATE TABLE mq_message_record (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
    msg_id VARCHAR(64) NOT NULL COMMENT '消息唯一ID',
    business_type VARCHAR(32) COMMENT '業(yè)務(wù)類型',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE INDEX uk_msg_id (msg_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'MQ消息防重表';

3.2.3、代碼實例

import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class MqIdempotentDbService {

    @Resource
    private MqMessageRecordMapper messageRecordMapper;

    @Transactional(rollbackFor = Exception.class)
    public boolean isFirstConsume(String msgId, String businessType) {
        try {
            // 插入防重記錄
            MqMessageRecord record = new MqMessageRecord();
            record.setMsgId(msgId);
            record.setBusinessType(businessType);
            messageRecordMapper.insert(record);
            return true;
        } catch (DuplicateKeyException e) {
            // 唯一索引沖突,重復(fù)消息
            return false;
        }
    }
}

3.2.4、優(yōu)缺點分析

? 優(yōu)點:不依賴中間件、事務(wù)一致性極強、絕對可靠、可作為Redis降級方案

? 缺點:高并發(fā)下數(shù)據(jù)庫壓力大、性能低于Redis

?? 適用場景:核心金融、支付、賬務(wù)場景、Redis宕機降級兜底

3.3、方案三:業(yè)務(wù)狀態(tài)機+樂觀鎖

3.3.1、原理

針對訂單、支付、退款、物流等有明確狀態(tài)流轉(zhuǎn)的業(yè)務(wù),無需額外中間件,依靠業(yè)務(wù)狀態(tài)實現(xiàn)天然冪等。

狀態(tài)流轉(zhuǎn)示例:待支付(1) → 已支付(2) → 已發(fā)貨(3) → 已完成(4)

核心邏輯:僅允許狀態(tài)正向流轉(zhuǎn),已變更狀態(tài)禁止重復(fù)更新。

3.3.2、代碼示例

-- 樂觀鎖更新:僅待支付訂單可更新為已支付
UPDATE order_info 
SET status = 2, pay_time = NOW() 
WHERE order_id = #{orderId} AND status = 1;
@Service
public class OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Transactional(rollbackFor = Exception.class)
    public boolean paySuccess(Long orderId) {
        // 更新行數(shù)=0 說明:訂單已處理,重復(fù)消費
        int rows = orderMapper.updateOrderStatus(orderId, 1, 2);
        return rows > 0;
    }
}

3.3.3、優(yōu)缺點分析

? 優(yōu)點:零額外存儲、零開銷、業(yè)務(wù)貼合度最高、絕對冪等

? 缺點:僅適用于有狀態(tài)業(yè)務(wù),無狀態(tài)業(yè)務(wù)無法使用

?? 適用場景:訂單、支付、退款、積分變動、會員權(quán)益變更

3.4、方案四:全局唯一約束

部分業(yè)務(wù)可直接依靠業(yè)務(wù)唯一主鍵實現(xiàn)冪等,例如:

  • 支付流水號唯一
  •  訂單ID唯一
  • 退款單號唯一

插入數(shù)據(jù)時直接判斷主鍵是否存在,存在則放棄操作,適配簡單的新增類消息業(yè)務(wù)。

四、注意事項

4.1:鎖過期時間小于業(yè)務(wù)執(zhí)行時間

若業(yè)務(wù)執(zhí)行需要10秒,鎖只設(shè)置5秒,會導(dǎo)致鎖提前失效,重復(fù)消息穿透。

? 解決方案:鎖過期時間 = 業(yè)務(wù)最大耗時 * 3 倍預(yù)留緩沖

4.2:先執(zhí)行業(yè)務(wù),再做冪等判斷

致命錯誤!并發(fā)場景下會導(dǎo)致兩條消息同時執(zhí)行業(yè)務(wù),冪等完全失效。

? 正確順序:冪等判斷 > 執(zhí)行業(yè)務(wù) > 手動ACK

4.3:使用自動ACK(自動確認)

自動ACK會導(dǎo)致業(yè)務(wù)未執(zhí)行完成就確認消息,異常時無法重試,且冪等邏輯失效。

? 生產(chǎn)強制:所有核心業(yè)務(wù)MQ,必須手動ACK

4.4:msgId重復(fù)、為空

生產(chǎn)者未生成全局唯一ID,使用隨機ID、局部ID,導(dǎo)致冪等判斷錯亂。

? 規(guī)范:生產(chǎn)者統(tǒng)一生成 全局唯一 msgId(UUID/雪花算法)

4.5:Redis鎖執(zhí)行完立即刪除

高并發(fā)瞬時重復(fù)消息,會出現(xiàn)刪鎖后瞬間穿透,建議依靠過期時間自動失效,不手動刪鎖。

五、總結(jié)

  1.  MQ重復(fù)消費是必然現(xiàn)象,核心原因是ACK超時、程序異常、集群切換、生產(chǎn)者重發(fā)。
  2. 消息冪等核心:唯一消息ID + 消費前置防重判斷。
  3.  通用最優(yōu)方案:Redis SETNX 原子防重,適配所有MQ場景。
  4.  核心業(yè)務(wù)兜底:數(shù)據(jù)庫唯一索引、業(yè)務(wù)狀態(tài)機樂觀鎖。
  5. 生產(chǎn)規(guī)范:手動ACK、合理鎖過期時間、前置防重、全局唯一msgId。

消息冪等性是后端開發(fā)的必備核心能力,也是面試高頻考點、生產(chǎn)環(huán)境硬性要求。很多線上臟數(shù)據(jù)、資金問題、業(yè)務(wù)異常,根源都不是業(yè)務(wù) Bug,而是忽略了 MQ 重復(fù)消費的特性。

掌握這幾套冪等方案,足以應(yīng)對 訂單、支付、積分、物流、通知 所有業(yè)務(wù)場景,徹底解決線上消息重復(fù)問題,讓你的項目穩(wěn)定性提升一個層級。

以上就是SpringBoot防止消息重復(fù)消費的方法步驟的詳細內(nèi)容,更多關(guān)于SpringBoot防止消息重復(fù)消費的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java MyBatis 多表查詢詳解

    Java MyBatis 多表查詢詳解

    這篇文章主要給大家介紹了關(guān)于MyBatis如何實現(xiàn)多表查詢(多對一、一對多)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)
    2021-09-09
  • java線程池的四種創(chuàng)建方式詳細分析

    java線程池的四種創(chuàng)建方式詳細分析

    這篇文章主要介紹了java線程池的四種創(chuàng)建方式詳細分析,連接池是創(chuàng)建和管理一個連接的緩沖池的技術(shù),這些連接準備好被任何需要它們的線程使用
    2022-07-07
  • java弱口令檢測機制解析

    java弱口令檢測機制解析

    這篇文章主要介紹了java弱口令檢測機制,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Springboot使用cache緩存過程代碼實例

    Springboot使用cache緩存過程代碼實例

    這篇文章主要介紹了Springboot使用cache緩存過程代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-06-06
  • Java switch使用原理及實例解析

    Java switch使用原理及實例解析

    這篇文章主要介紹了Java switch使用及實例解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-02-02
  • ManyToMany單向、雙向:@JoinTable的使用

    ManyToMany單向、雙向:@JoinTable的使用

    這篇文章主要介紹了ManyToMany單向、雙向:@JoinTable的使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java中的ScheduledThreadPoolExecutor定時任務(wù)詳解

    Java中的ScheduledThreadPoolExecutor定時任務(wù)詳解

    這篇文章主要介紹了Java中的ScheduledThreadPoolExecutor詳解,??ScheduledThreadPoolExecutor?繼承自?ThreadPoolExecutor,它主要用來在給定的延遲之后運行任務(wù),或者定期執(zhí)行任務(wù),ScheduledThreadPoolExecutor?的功能與?Timer?類似<BR>,需要的朋友可以參考下
    2023-12-12
  • java自定義驗證器的實現(xiàn)示例

    java自定義驗證器的實現(xiàn)示例

    在對外暴露接口中,我們通常會對入?yún)⑦M行驗證,比如一些字符串非空判斷等,本文主要介紹了java自定義驗證器的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下
    2024-01-01
  • 談?wù)剬ava多態(tài)性的一點理解

    談?wù)剬ava多態(tài)性的一點理解

    多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發(fā)出的方法調(diào)用在編程時并不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發(fā)出的方法調(diào)用到底是哪個類中實現(xiàn)的方法,必須在由程序運行期間才能決定
    2017-08-08
  • java開發(fā)命名規(guī)范總結(jié)

    java開發(fā)命名規(guī)范總結(jié)

    包名的書寫規(guī)范 (Package)推薦使用公司或機構(gòu)的頂級域名為包名的前綴,目的是保證各公司/機構(gòu)內(nèi)所使用的包名的唯一性。包名全部為小寫字母,且具有實際的區(qū)分意義
    2013-10-10

最新評論

宜君县| 湖北省| 宁远县| 武鸣县| 明溪县| 旅游| 镇江市| 都兰县| 永嘉县| 渝北区| 平泉县| 邯郸县| 安陆市| 金阳县| 客服| 吴桥县| 迭部县| 新竹县| 石景山区| 邹城市| 汝州市| 元阳县| 阿城市| 沅陵县| 二连浩特市| 分宜县| 河津市| 武隆县| 唐河县| 剑河县| 溧水县| 龙南县| 本溪市| 镇宁| 西青区| 林芝县| 图木舒克市| 泸西县| 成都市| 瑞安市| 玛沁县|