完整分布式事务解决方案(本地消息表 + 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连接信息即可运行!

相关推荐
彭于晏Yan26 分钟前
Redisson分布式锁
spring boot·redis·分布式
GetcharZp1 小时前
Git 命令行太痛苦?这款 75k Star 的神级工具,让你告别“合并冲突”恐惧症!
后端
Victor3562 小时前
MongoDB(69)如何进行增量备份?
后端
Victor3562 小时前
MongoDB(70)如何使用副本集进行备份?
后端
千寻girling2 小时前
面试官 : “ 说一下 Python 中的常用的 字符串和数组 的 方法有哪些 ? ”
人工智能·后端·python
ywf12153 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端
LSTM973 小时前
C# 实战:轻松提取 PDF 文件中的文字内容
后端
PFinal社区_南丞4 小时前
Skills与脚本:当智能遇上死板,一场编程界的"冰与火之歌"
后端
树上有只程序猿4 小时前
低代码何时能出个“秦始皇”一统天下?我是真学不动啦!
前端·后端·低代码
2501_921649494 小时前
期货 Tick 级数据与基金净值历史数据 API 接口详解
开发语言·后端·python·websocket·金融·区块链