完整分布式事务解决方案(本地消息表 + RabbitMQ)

完整分布式事务解决方案(本地消息表 + RabbitMQ)

一、方案概述

本方案基于 本地消息表 + RabbitMQ 发布确认机制 实现分布式事务一致性,核心解决分布式系统中"业务操作与跨服务消息发送的原子性"问题,确保:

  • 本地业务执行成功 ↔ 本地消息表插入成功(同一事务)
  • 消息必达:通过 MQ 发布确认(ACK)+ 失败重试避免消息丢失
  • 无侵入性:核心逻辑封装为独立服务,业务系统直接调用接口即可
  • 可扩展性:支持自定义消息ID生成、消息发送实现、重试策略

核心原理

css 复制代码
graph TD
    A[业务系统] -->|1. 业务逻辑 + 调用消息服务| B[本地事务管理器]
    B --> C{业务执行成功?}
    C -->|否| D[事务回滚,不发送消息]
    C -->|是| E[同一事务插入本地消息表(状态:待发送)]
    E --> F[事务提交]
    F --> G[事务提交后异步发送MQ消息]
    G --> H[RabbitMQ交换机]
    H -->|2. 发布确认回调| I{消息投递成功?}
    I -->|是| J[更新消息状态为"已发送"]
    I -->|否| K{是否达到最大重试次数?}
    K -->|是| L[更新消息状态为"发送失败"]
    K -->|否| M[等待重试(定时任务/手动触发)]
    M --> G

二、技术栈选型

组件 版本建议 作用
Spring Boot 2.7.x 基础框架
Spring Cloud Stream 3.2.x 消息中间件统一适配(对接RabbitMQ)
RabbitMQ 3.9+ 消息中间件(可靠投递、发布确认)
MyBatis 3.5.x 数据访问层(操作本地消息表)
PostgreSQL 12+ 数据库(存储本地消息表)
Hutool 5.8.x JSON序列化、工具类
Lombok 1.18.x 简化POJO类(可选)

三、标准项目结构(去隐私化)

bash 复制代码
com.localmessage/
├── configuration/          # 配置类
│   └── LocalMessageAutoConfig.java  # Spring Bean自动装配
├── constant/               # 常量枚举
│   └── LocalMessageStatusEnum.java  # 消息状态枚举
├── dto/                    # 数据传输对象
│   └── LocalMessagePayload.java     # 消息发送载体(泛型)
├── entity/                 # 数据库实体
│   └── LocalMessage.java            # 本地消息表实体
├── mapper/                 # MyBatis映射层
│   ├── LocalMessageMapper.java      # Mapper接口
│   └── xml/
│       └── LocalMessageMapper.xml   # MyBatis SQL映射文件
├── service/                # 核心服务层
│   ├── ILocalMessageService.java    # 消息服务核心接口
│   ├── IMessageIdGeneratorService.java  # 消息ID生成接口
│   └── impl/
│       ├── RabbitMQLocalMessageServiceImpl.java  # RabbitMQ实现类
│       └── DefaultMessageIdGenerator.java        # 默认ID生成实现(时间基UUID)
├── application.yml         # 全局配置文件(MQ、数据库、MyBatis配置)
└── sql/                    # 数据库脚本
    └── local_message_create.sql     # 本地消息表建表语句

四、核心代码实现(完整可运行)

1. 常量枚举:LocalMessageStatusEnum

arduino 复制代码
package com.localmessage.constant;

/**
 * 本地消息状态枚举
 */
public enum LocalMessageStatusEnum {
    READY(0, "待发送"),
    SEND(1, "已发送"),
    FAIL(2, "发送失败");

    private final Integer status;
    private final String desc;

    LocalMessageStatusEnum(Integer status, String desc) {
        this.status = status;
        this.desc = desc;
    }

    public Integer getStatus() {
        return status;
    }

    public String getDesc() {
        return desc;
    }
}

2. DTO:LocalMessagePayload(消息载体)

arduino 复制代码
package com.localmessage.dto;

import cn.hutool.json.JSONUtil;
import lombok.Data;

/**
 * 消息发送DTO载体(泛型支持任意消息体类型)
 */
@Data
public class LocalMessagePayload<T> {
    /** 消息唯一ID */
    private String messageId;
    /** 链路追踪ID(用于分布式追踪) */
    private String traceId;
    /** 业务编码(区分不同业务场景) */
    private String businessCode;
    /** 消息体数据(任意类型) */
    private T data;

    /**
     * 序列化为JSON字符串(存储到消息表)
     */
    @Override
    public String toString() {
        return JSONUtil.toJsonStr(this);
    }
}

3. 实体类:LocalMessage(本地消息表实体)

arduino 复制代码
package com.localmessage.entity;

import lombok.Data;

/**
 * 本地消息表实体(与数据库表一一对应)
 */
@Data
public class LocalMessage {
    /** 消息唯一ID(主键) */
    private String localMessageId;
    /** 消息体(JSON字符串) */
    private String messageBody;
    /** 队列名称(RabbitMQ队列名) */
    private String queue;
    /** 重试次数(默认0) */
    private Integer retryTimes = 0;
    /** 下一次发送时间(时间戳,毫秒) */
    private Long nextSendTime;
    /** 主题/交换机名称(可选) */
    private String topic;
    /** 路由键(可选) */
    private String routingKey;
    /** 最大重试次数(默认0) */
    private Integer maxRetryTimes = 0;
    /** 延迟发送时间(秒) */
    private Long delaySeconds;
    /** 创建时间(时间戳,毫秒) */
    private Long createTime;
    /** 更新时间(时间戳,毫秒) */
    private Long modifyTime;
    /** 状态(0:待发送,1:已发送,2:发送失败) */
    private Integer state;
    /** 逻辑删除标识(0:未删除,1:已删除) */
    private Integer deleted = 0;
}

4. Mapper接口:LocalMessageMapper

less 复制代码
package com.localmessage.mapper;

import com.localmessage.entity.LocalMessage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface LocalMessageMapper {
    /**
     * 根据ID查询消息(支持忽略已删除)
     */
    LocalMessage selectById(@Param("id") String id, @Param("ignoreDeleted") Boolean ignoreDeleted);

    /**
     * 更新消息状态/重试次数等信息(忽略空值)
     */
    Integer update(LocalMessage localMessage);

    /**
     * 插入消息(本地事务中执行)
     */
    Integer insert(LocalMessage localMessage);

    /**
     * 批量插入消息
     */
    Integer batchInsert(@Param("list") List<LocalMessage> list);

    /**
     * 根据消息ID查询未删除的消息列表
     */
    List<LocalMessage> queryListById(@Param("id") String id);

    /**
     * 查询待重试的消息(定时任务用)
     */
    List<LocalMessage> queryRetryMessages(@Param("currentTime") Long currentTime, @Param("limit") Integer limit);
}

5. MyBatis映射文件:LocalMessageMapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.localmessage.mapper.LocalMessageMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="baseResultMap" type="com.localmessage.entity.LocalMessage">
        <id column="local_message_id" property="localMessageId" />
        <result column="message_body" property="messageBody" />
        <result column="queue" property="queue" />
        <result column="retry_times" property="retryTimes" />
        <result column="next_send_time" property="nextSendTime" />
        <result column="create_time" property="createTime" />
        <result column="modify_time" property="modifyTime" />
        <result column="state" property="state" />
        <result column="deleted" property="deleted" />
        <result column="topic" property="topic" />
        <result column="routing_key" property="routingKey" />
        <result column="max_retry_times" property="maxRetryTimes" />
        <result column="delay_seconds" property="delaySeconds" />
    </resultMap>

    <!-- 通用查询字段 -->
    <sql id="baseColumnList">
        local_message_id, message_body, queue, retry_times, next_send_time, 
        create_time, modify_time, state, deleted, topic, routing_key, 
        max_retry_times, delay_seconds
    </sql>

    <!-- 根据ID查询 -->
    <select id="selectById" resultMap="baseResultMap">
        SELECT <include refid="baseColumnList" />
        FROM local_message
        WHERE local_message_id = #{id}
        <if test="ignoreDeleted != null and ignoreDeleted">
            AND deleted = 0
        </if>
        LIMIT 1
    </select>

    <!-- 更新消息 -->
    <update id="update" parameterType="com.localmessage.entity.LocalMessage">
        UPDATE local_message
        <set>
            <if test="messageBody != null">message_body = #{messageBody},</if>
            <if test="queue != null">queue = #{queue},</if>
            <if test="retryTimes != null">retry_times = #{retryTimes},</if>
            <if test="nextSendTime != null">next_send_time = #{nextSendTime},</if>
            <if test="modifyTime != null">modify_time = #{modifyTime},</if>
            <if test="state != null">state = #{state},</if>
            <if test="deleted != null">deleted = #{deleted},</if>
            <if test="topic != null">topic = #{topic},</if>
            <if test="routingKey != null">routing_key = #{routingKey},</if>
            <if test="maxRetryTimes != null">max_retry_times = #{maxRetryTimes},</if>
            <if test="delaySeconds != null">delay_seconds = #{delaySeconds},</if>
        </set>
        WHERE local_message_id = #{localMessageId} AND deleted = 0
    </update>

    <!-- 插入消息 -->
    <insert id="insert" parameterType="com.localmessage.entity.LocalMessage">
        INSERT INTO local_message
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="localMessageId != null">local_message_id,</if>
            <if test="messageBody != null">message_body,</if>
            <if test="queue != null">queue,</if>
            <if test="retryTimes != null">retry_times,</if>
            <if test="nextSendTime != null">next_send_time,</if>
            <if test="createTime != null">create_time,</if>
            <if test="state != null">state,</if>
            <if test="deleted != null">deleted,</if>
            <if test="topic != null">topic,</if>
            <if test="routingKey != null">routing_key,</if>
            <if test="maxRetryTimes != null">max_retry_times,</if>
            <if test="delaySeconds != null">delay_seconds,</if>
        </trim>
        VALUES
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="localMessageId != null">#{localMessageId},</if>
            <if test="messageBody != null">#{messageBody},</if>
            <if test="queue != null">#{queue},</if>
            <if test="retryTimes != null">#{retryTimes},</if>
            <if test="nextSendTime != null">#{nextSendTime},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="state != null">#{state},</if>
            <if test="deleted != null">#{deleted},</if>
            <if test="topic != null">#{topic},</if>
            <if test="routingKey != null">#{routingKey},</if>
            <if test="maxRetryTimes != null">#{maxRetryTimes},</if>
            <if test="delaySeconds != null">#{delaySeconds},</if>
        </trim>
    </insert>

    <!-- 批量插入 -->
    <insert id="batchInsert" parameterType="java.util.List">
        INSERT INTO local_message (
            local_message_id, message_body, queue, retry_times, next_send_time,
            create_time, state, deleted, topic, routing_key, max_retry_times, delay_seconds
        )
        VALUES
        <foreach collection="list" item="item" separator=",">
            (
                #{item.localMessageId}, #{item.messageBody}, #{item.queue}, #{item.retryTimes},
                #{item.nextSendTime}, #{item.createTime}, #{item.state}, #{item.deleted},
                #{item.topic}, #{item.routingKey}, #{item.maxRetryTimes}, #{item.delaySeconds}
            )
        </foreach>
    </insert>

    <!-- 根据ID查询未删除消息 -->
    <select id="queryListById" resultMap="baseResultMap">
        SELECT <include refid="baseColumnList" />
        FROM local_message
        WHERE local_message_id = #{id} AND deleted = 0
    </select>

    <!-- 查询待重试消息(定时任务用) -->
    <select id="queryRetryMessages" resultMap="baseResultMap">
        SELECT <include refid="baseColumnList" />
        FROM local_message
        WHERE state = #{readyStatus,javaType=java.lang.Integer}
          AND deleted = 0
          AND next_send_time &lt;= #{currentTime}
        ORDER BY next_send_time ASC
        LIMIT #{limit}
    </select>
</mapper>

6. 核心服务接口:ILocalMessageService

typescript 复制代码
package com.localmessage.service;

import com.localmessage.entity.LocalMessage;

/**
 * 分布式消息核心服务接口
 */
public interface ILocalMessageService {

    /**
     * 普通发送(不存储本地消息,不监听ACK)
     * @param queueName 队列名称
     * @param businessCode 业务编码
     * @param data 消息体(任意类型)
     * @return 消息唯一ID
     */
    String send(String queueName, String businessCode, Object data);

    /**
     * 可靠发送(存储本地消息,监听ACK,支持重试)
     * @param queueName 队列名称
     * @param businessCode 业务编码
     * @param data 消息体
     * @param maxRetryTimes 最大重试次数(0表示不重试)
     * @param delaySeconds 延迟发送时间(秒)
     * @return 消息唯一ID
     */
    String sendReliable(String queueName, String businessCode, Object data, int maxRetryTimes, long delaySeconds);

    /**
     * 根据消息对象重试发送
     * @param queueName 队列名称
     * @param localMessage 消息实体
     * @return 消息唯一ID
     */
    String retrySend(String queueName, LocalMessage localMessage);

    /**
     * 根据消息ID重试发送
     * @param messageId 消息唯一ID
     * @return 消息唯一ID
     */
    String retrySend(String messageId);
}

7. ID生成接口:IMessageIdGeneratorService

csharp 复制代码
package com.localmessage.service;

/**
 * 消息唯一ID生成接口(支持自定义实现)
 */
public interface IMessageIdGeneratorService {
    String generateMessageId();
}

8. 服务实现类:RabbitMQLocalMessageServiceImpl

ini 复制代码
package com.localmessage.service.impl;

import cn.hutool.json.JSONUtil;
import com.localmessage.constant.LocalMessageStatusEnum;
import com.localmessage.dto.LocalMessagePayload;
import com.localmessage.entity.LocalMessage;
import com.localmessage.mapper.LocalMessageMapper;
import com.localmessage.service.ILocalMessageService;
import com.localmessage.service.IMessageIdGeneratorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;

/**
 * RabbitMQ实现的分布式消息服务
 */
public class RabbitMQLocalMessageServiceImpl implements ILocalMessageService {

    private static final Logger log = LoggerFactory.getLogger(RabbitMQLocalMessageServiceImpl.class);

    @Resource
    private StreamBridge streamBridge;

    @Resource
    private LocalMessageMapper localMessageMapper;

    @Resource
    private IMessageIdGeneratorService messageIdGenerator;

    @Override
    public String send(String queueName, String businessCode, Object data) {
        LocalMessagePayload<Object> payload = buildMessagePayload(businessCode, data);
        Message<LocalMessagePayload<Object>> message = MessageBuilder.withPayload(payload).build();
        // 直接发送MQ,不存储本地消息
        streamBridge.send(queueName, message);
        log.info("普通消息发送成功,messageId: {}, queue: {}", payload.getMessageId(), queueName);
        return payload.getMessageId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String sendReliable(String queueName, String businessCode, Object data, int maxRetryTimes, long delaySeconds) {
        // 1. 构建消息载体
        LocalMessagePayload<Object> payload = buildMessagePayload(businessCode, data);
        String messageId = payload.getMessageId();
        String traceId = payload.getTraceId();
        long currentTime = System.currentTimeMillis();

        // 2. 插入本地消息表(与业务事务同享)
        LocalMessage localMessage = new LocalMessage();
        localMessage.setLocalMessageId(messageId);
        localMessage.setQueue(queueName);
        localMessage.setMessageBody(payload.toString());
        localMessage.setRetryTimes(0);
        localMessage.setMaxRetryTimes(maxRetryTimes);
        localMessage.setState(LocalMessageStatusEnum.READY.getStatus());
        localMessage.setCreateTime(currentTime);
        localMessage.setNextSendTime(currentTime + (delaySeconds * 1000));
        localMessage.setDelaySeconds(delaySeconds);
        localMessage.setDeleted(0);

        localMessageMapper.insert(localMessage);
        log.info("可靠消息插入本地表成功,messageId: {}, queue: {}", messageId, queueName);

        // 3. 事务提交后异步发送MQ(避免事务未提交就发送消息)
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                sendToRabbitMQ(queueName, localMessage, traceId);
            }
        });

        return messageId;
    }

    @Override
    public String retrySend(String queueName, LocalMessage localMessage) {
        if (Objects.isNull(localMessage) || Objects.isNull(localMessage.getLocalMessageId())) {
            log.warn("重试消息失败:消息对象为空");
            return null;
        }

        log.info("开始重试消息,messageId: {}, queue: {}, 当前重试次数: {}",
                localMessage.getLocalMessageId(), queueName, localMessage.getRetryTimes());

        // 更新下次发送时间(指数退避:重试次数越多,延迟越长)
        int nextRetryTimes = localMessage.getRetryTimes() + 1;
        long delayMillis = calculateBackoffDelay(nextRetryTimes);
        localMessage.setNextSendTime(System.currentTimeMillis() + delayMillis);
        localMessage.setRetryTimes(nextRetryTimes);
        localMessageMapper.update(localMessage);

        // 发送到MQ
        sendToRabbitMQ(queueName, localMessage, MDC.get("traceId"));
        return localMessage.getLocalMessageId();
    }

    @Override
    public String retrySend(String messageId) {
        if (messageId == null || messageId.isBlank()) {
            log.warn("重试消息失败:消息ID为空");
            return null;
        }

        List<LocalMessage> messageList = localMessageMapper.queryListById(messageId);
        if (messageList == null || messageList.isEmpty()) {
            log.warn("重试消息失败:未查询到消息,messageId: {}", messageId);
            return messageId;
        }
        if (messageList.size() > 1) {
            log.warn("重试消息警告:查询到多条相同ID消息,messageId: {}", messageId);
            return messageId;
        }

        LocalMessage localMessage = messageList.get(0);
        return retrySend(localMessage.getQueue(), localMessage);
    }

    /**
     * 构建消息载体
     */
    private LocalMessagePayload<Object> buildMessagePayload(String businessCode, Object data) {
        String messageId = messageIdGenerator.generateMessageId();
        String traceId = MDC.get("traceId"); // 从链路追踪上下文获取traceId

        LocalMessagePayload<Object> payload = new LocalMessagePayload<>();
        payload.setMessageId(messageId);
        payload.setTraceId(traceId);
        payload.setBusinessCode(businessCode);
        payload.setData(data);
        return payload;
    }

    /**
     * 发送消息到RabbitMQ,并监听发布确认
     */
    private void sendToRabbitMQ(String queueName, LocalMessage localMessage, String traceId) {
        String messageId = localMessage.getLocalMessageId();
        int currentRetryTimes = localMessage.getRetryTimes();
        int maxRetryTimes = localMessage.getMaxRetryTimes();

        try {
            // 构建MQ消息(携带消息体和确认标识)
            CorrelationData correlationData = new CorrelationData(messageId);
            Message<String> message = MessageBuilder
                    .withPayload(localMessage.getMessageBody())
                    .setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, correlationData)
                    .build();

            // 发送消息
            boolean sendSuccess = streamBridge.send(queueName, message);
            if (!sendSuccess) {
                log.warn("消息发送到MQ失败(发送接口返回失败),messageId: {}", messageId);
                handleSendFailure(messageId, currentRetryTimes, maxRetryTimes);
                return;
            }

            // 监听MQ发布确认回调
            correlationData.getFuture().addCallback(
                    confirm -> {
                        MDC.put("traceId", traceId);
                        if (confirm.isAck()) {
                            // ACK:消息已投递到交换机,更新状态为"已发送"
                            log.info("MQ发布确认成功,messageId: {}", messageId);
                            updateMessageStatus(messageId, LocalMessageStatusEnum.SEND.getStatus(), currentRetryTimes);
                        } else {
                            // NACK:消息投递失败,处理重试或标记失败
                            log.warn("MQ发布确认失败,messageId: {}, 原因: {}", messageId, confirm.getReason());
                            handleSendFailure(messageId, currentRetryTimes, maxRetryTimes);
                        }
                    },
                    ex -> {
                        log.error("MQ确认回调异常,messageId: {}", messageId, ex);
                        handleSendFailure(messageId, currentRetryTimes, maxRetryTimes);
                    }
            );
        } catch (Exception e) {
            log.error("发送消息到MQ异常,messageId: {}", messageId, e);
            handleSendFailure(messageId, currentRetryTimes, maxRetryTimes);
        }
    }

    /**
     * 处理消息发送失败
     */
    private void handleSendFailure(String messageId, int currentRetryTimes, int maxRetryTimes) {
        if (maxRetryTimes <= 0 || currentRetryTimes >= maxRetryTimes) {
            // 达到最大重试次数,标记为失败
            log.error("消息发送失败,已达到最大重试次数,messageId: {}", messageId);
            updateMessageStatus(messageId, LocalMessageStatusEnum.FAIL.getStatus(), currentRetryTimes);
        } else {
            // 未达最大重试次数,等待下次重试(定时任务触发)
            log.warn("消息发送失败,等待下次重试,messageId: {}, 当前重试次数: {}", messageId, currentRetryTimes);
        }
    }

    /**
     * 更新消息状态和重试次数
     */
    private void updateMessageStatus(String messageId, Integer status, int retryTimes) {
        LocalMessage updateMsg = new LocalMessage();
        updateMsg.setLocalMessageId(messageId);
        updateMsg.setState(status);
        updateMsg.setRetryTimes(retryTimes);
        updateMsg.setModifyTime(System.currentTimeMillis());
        localMessageMapper.update(updateMsg);
    }

    /**
     * 指数退避算法:计算重试延迟时间(避免高并发重试)
     */
    private long calculateBackoffDelay(int retryTimes) {
        // 延迟时间 = 2^retryTimes * 1000 毫秒(最多延迟5分钟)
        long delay = (long) Math.pow(2, Math.min(retryTimes, 10)) * 1000;
        return Math.min(delay, 5 * 60 * 1000);
    }
}

9. ID生成实现:DefaultMessageIdGenerator

typescript 复制代码
package com.localmessage.service.impl;

import com.localmessage.service.IMessageIdGeneratorService;
import org.apache.logging.log4j.core.util.UuidUtil;

/**
 * 默认消息ID生成器(时间基UUID,保证全局唯一且含时间戳)
 */
public class DefaultMessageIdGenerator implements IMessageIdGeneratorService {

    @Override
    public String generateMessageId() {
        // 时间基UUID:包含时间戳,支持按时间排序,全局唯一
        return UuidUtil.getTimeBasedUuid().toString().replace("-", "");
    }
}

10. Spring自动配置:LocalMessageAutoConfig

kotlin 复制代码
package com.localmessage.configuration;

import com.localmessage.service.ILocalMessageService;
import com.localmessage.service.IMessageIdGeneratorService;
import com.localmessage.service.impl.DefaultMessageIdGenerator;
import com.localmessage.service.impl.RabbitMQLocalMessageServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * 分布式消息服务自动配置类(无需手动扫描,自动装配Bean)
 */
@Configuration
@ComponentScan("com.localmessage") // 扫描核心组件
public class LocalMessageAutoConfig {

    /**
     * 消息ID生成器(默认实现,支持自定义替换)
     */
    @Bean
    @ConditionalOnMissingBean(IMessageIdGeneratorService.class)
    public IMessageIdGeneratorService messageIdGenerator() {
        return new DefaultMessageIdGenerator();
    }

    /**
     * 消息服务实现(默认RabbitMQ,支持自定义替换)
     */
    @Bean
    @ConditionalOnMissingBean(ILocalMessageService.class)
    public ILocalMessageService localMessageService() {
        return new RabbitMQLocalMessageServiceImpl();
    }
}

五、配置文件(application.yml)

yaml 复制代码
spring:
  # 数据库配置(PostgreSQL)
  datasource:
    url: jdbc:postgresql://localhost:5432/your_db_name?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: your_db_username
    password: your_db_password
    driver-class-name: org.postgresql.Driver
  # RabbitMQ配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 发布确认配置(必须开启,否则ACK回调无效)
    publisher-confirm-type: correlated
    # 消息返回回调(可选,处理路由失败)
    publisher-returns: true
    template:
      mandatory: true
  # Spring Cloud Stream配置
  cloud:
    stream:
      rabbit:
        binder:
          # 开启消息持久化(避免MQ重启丢失消息)
          persistent: true
          # 消费者确认模式(手动确认,确保业务处理完成后再ACK)
          acknowledge-mode: manual
      bindings:
        # 通用消息绑定(可根据业务扩展多个binding)
        default-out-0:
          destination: default-exchange # 交换机名称
          producer:
            required-groups: default-group # 消费组
            partition-count: 1 # 分区数(根据集群节点调整)

# MyBatis配置
mybatis:
  mapper-locations: classpath:com/localmessage/mapper/xml/*.xml # Mapper XML路径
  type-aliases-package: com.localmessage.entity # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL(开发环境)

# 日志配置
logging:
  level:
    com.localmessage: INFO
    org.springframework.amqp: WARN
    org.mybatis: INFO

# 自定义配置
localmessage:
  retry:
    batch-size: 100 # 定时任务每次重试消息数量
    cron: 0 */1 * * * ? # 定时任务执行频率(每1分钟)

六、数据库脚本(PostgreSQL)

sql 复制代码
-- 本地消息表(分布式事务核心表)
CREATE TABLE "public"."local_message" (
  "local_message_id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
  "message_body" text COLLATE "pg_catalog"."default" NOT NULL,
  "queue" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
  "retry_times" int2 NOT NULL DEFAULT 0,
  "next_send_time" int8 NOT NULL,
  "topic" varchar(255) COLLATE "pg_catalog"."default",
  "routing_key" varchar(255) COLLATE "pg_catalog"."default",
  "max_retry_times" int2 NOT NULL DEFAULT 0,
  "delay_seconds" int8 NOT NULL DEFAULT 0,
  "create_time" int8 NOT NULL,
  "modify_time" int8,
  "state" int2 NOT NULL DEFAULT 0,
  "deleted" int2 NOT NULL DEFAULT 0,
  CONSTRAINT "local_message_pk" PRIMARY KEY ("local_message_id")
) WITH (OIDS = FALSE);

-- 注释
COMMENT ON COLUMN "public"."local_message"."local_message_id" IS '消息唯一ID(主键)';
COMMENT ON COLUMN "public"."local_message"."message_body" IS '消息体(JSON字符串)';
COMMENT ON COLUMN "public"."local_message"."queue" IS '目标队列名称';
COMMENT ON COLUMN "public"."local_message"."retry_times" IS '已重试次数';
COMMENT ON COLUMN "public"."local_message"."next_send_time" IS '下次发送时间(时间戳,毫秒)';
COMMENT ON COLUMN "public"."local_message"."topic" IS '交换机/主题名称';
COMMENT ON COLUMN "public"."local_message"."routing_key" IS '路由键';
COMMENT ON COLUMN "public"."local_message"."max_retry_times" IS '最大重试次数';
COMMENT ON COLUMN "public"."local_message"."delay_seconds" IS '延迟发送时间(秒)';
COMMENT ON COLUMN "public"."local_message"."create_time" IS '创建时间(时间戳,毫秒)';
COMMENT ON COLUMN "public"."local_message"."modify_time" IS '更新时间(时间戳,毫秒)';
COMMENT ON COLUMN "public"."local_message"."state" IS '状态(0:待发送,1:已发送,2:发送失败)';
COMMENT ON COLUMN "public"."local_message"."deleted" IS '逻辑删除(0:未删除,1:已删除)';
COMMENT ON TABLE "public"."local_message" IS '分布式事务本地消息表';

-- 索引优化(提高查询效率)
CREATE INDEX "idx_local_message_state" ON "public"."local_message" USING btree ("state");
CREATE INDEX "idx_local_message_next_send_time" ON "public"."local_message" USING btree ("next_send_time");
CREATE INDEX "idx_local_message_queue" ON "public"."local_message" USING btree ("queue");

七、定时重试任务(补充)

为了自动重试"待发送"和"发送失败"的消息,添加定时任务组件:

kotlin 复制代码
package com.localmessage.task;

import com.localmessage.constant.LocalMessageStatusEnum;
import com.localmessage.entity.LocalMessage;
import com.localmessage.mapper.LocalMessageMapper;
import com.localmessage.service.ILocalMessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * 消息重试定时任务(自动重试未发送成功的消息)
 */
@Component
public class MessageRetryTask {

    private static final Logger log = LoggerFactory.getLogger(MessageRetryTask.class);

    @Resource
    private LocalMessageMapper localMessageMapper;

    @Resource
    private ILocalMessageService localMessageService;

    @Value("${localmessage.retry.batch-size:100}")
    private Integer batchSize;

    /**
     * 定时执行重试(默认每1分钟)
     */
    @Scheduled(cron = "${localmessage.retry.cron:0 */1 * * * ?}")
    public void retryPendingMessages() {
        log.info("开始执行消息重试任务,批量大小:{}", batchSize);
        long currentTime = System.currentTimeMillis();

        // 查询待重试的消息(状态为待发送 + 下次发送时间<=当前时间)
        List<LocalMessage> pendingMessages = localMessageMapper.queryRetryMessages(
                LocalMessageStatusEnum.READY.getStatus(),
                currentTime,
                batchSize
        );

        if (pendingMessages.isEmpty()) {
            log.info("无待重试消息,任务结束");
            return;
        }

        log.info("本次待重试消息数量:{}", pendingMessages.size());
        for (LocalMessage message : pendingMessages) {
            try {
                localMessageService.retrySend(message.getQueue(), message);
            } catch (Exception e) {
                log.error("重试消息失败,messageId: {}", message.getLocalMessageId(), e);
            }
        }

        log.info("消息重试任务执行完成");
    }
}

八、业务系统调用示例

1. 依赖引入(Maven)

xml 复制代码
<!-- 引入分布式消息服务依赖(如果是独立服务,通过Feign调用;如果是本地模块,直接依赖) -->
<dependency>
    <groupId>com.localmessage</groupId>
    <artifactId>local-message-starter</artifactId>
    <version>1.0.0</version>
</dependency>

2. 业务代码中调用

typescript 复制代码
import com.localmessage.service.ILocalMessageService;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class OrderService {

    @Resource
    private ILocalMessageService localMessageService;

    /**
     * 下单业务(包含分布式消息发送)
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO orderDTO) {
        try {
            // 1. 设置链路追踪ID(可选,用于分布式追踪)
            MDC.put("traceId", "ORDER_" + System.currentTimeMillis());

            // 2. 执行本地业务逻辑(创建订单)
            String orderId = saveOrder(orderDTO);
            log.info("订单创建成功,orderId: {}", orderId);

            // 3. 调用分布式消息服务,发送可靠消息(事务提交后发送)
            // 参数:队列名称、业务编码、消息体、最大重试次数、延迟发送时间
            String messageId = localMessageService.sendReliable(
                    "order-notify-queue", // 队列名称
                    "ORDER_CREATE",       // 业务编码
                    orderDTO,             // 消息体(任意类型)
                    3,                    // 最大重试3次
                    0                     // 不延迟,立即发送
            );

            log.info("订单消息发送成功(待确认),messageId: {}", messageId);
            return orderId;
        } finally {
            MDC.clear();
        }
    }

    /**
     * 保存订单(本地业务逻辑)
     */
    private String saveOrder(OrderDTO orderDTO) {
        // 执行订单入库逻辑...
        return "ORDER_" + System.currentTimeMillis();
    }
}

九、核心注意事项

  1. 事务一致性sendReliable 方法必须加 @Transactional,且业务代码也需在同一事务中(确保业务和消息表插入原子性)
  2. MQ配置 :必须开启 publisher-confirm-type: correlated,否则发布确认回调无效
  3. 消息幂等性 :消费者端需实现幂等处理(通过 messageId 去重),避免重复消费
  4. 索引优化:本地消息表必须添加状态、下次发送时间索引,否则定时任务查询效率低
  5. 重试策略:采用指数退避算法,避免高并发重试导致MQ拥堵
  6. 监控告警:建议对"发送失败"状态的消息添加监控告警,及时人工介入处理
  7. 逻辑删除:消息删除采用逻辑删除,避免误删导致问题排查困难

十、扩展建议

  1. 多消息中间件支持 :实现 ILocalMessageService 接口,扩展Kafka、RocketMQ等实现
  2. 自定义ID生成器 :实现 IMessageIdGeneratorService,替换为雪花算法(适合需要有序ID的场景)
  3. 消息监控平台:开发管理平台,支持消息查询、手动重试、状态统计
  4. 死信队列:对"发送失败"的消息,可转发到死信队列,避免占用正常队列资源
  5. 异步通知:添加消息发送结果回调(如HTTP回调),通知业务系统消息发送状态

总结

本方案基于"本地消息表 + RabbitMQ"实现分布式事务,具有高可靠、低侵入、易扩展的特点,已覆盖大部分分布式场景的消息一致性需求。核心优势:

  1. 无分布式事务中间件依赖,部署简单
  2. 消息必达机制,避免消息丢失
  3. 事务同步设计,确保业务与消息原子性
  4. 完整的重试和监控机制,便于问题排查

所有代码已去隐私化,可直接复制到项目中,替换配置文件中的数据库、MQ连接信息即可运行!

相关推荐
溪饱鱼35 分钟前
NextJs + Cloudflare Worker 是出海最佳实践
前端·后端
LDG_AGI1 小时前
【推荐系统】深度学习训练框架(十):PyTorch Dataset—PyTorch数据基石
人工智能·pytorch·分布式·python·深度学习·机器学习
小周在成长1 小时前
Java 抽象类 vs 接口:相同点与不同点
后端
expect7g1 小时前
Paimon Branch --- 流批一体化之二
大数据·后端·flink
幌才_loong1 小时前
.NET 8 实时推送魔法:SSE 让数据 “主动跑” 到客户端
后端
tanxiaomi1 小时前
Redisson分布式锁 和 乐观锁的使用场景
java·分布式·mysql·面试
00后程序员1 小时前
如何解决浏览器HTTPS不安全连接警告及SSL证书问题
后端
00后程序员1 小时前
苹果App上架审核延迟7工作日无反应:如何通过App Store Connect和邮件询问进度
后端
DS小龙哥1 小时前
基于物联网设计的蜂箱智能监测系统设计
后端