完整分布式事务解决方案(本地消息表 + 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 <= #{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();
}
}
九、核心注意事项
- 事务一致性 :
sendReliable方法必须加@Transactional,且业务代码也需在同一事务中(确保业务和消息表插入原子性) - MQ配置 :必须开启
publisher-confirm-type: correlated,否则发布确认回调无效 - 消息幂等性 :消费者端需实现幂等处理(通过
messageId去重),避免重复消费 - 索引优化:本地消息表必须添加状态、下次发送时间索引,否则定时任务查询效率低
- 重试策略:采用指数退避算法,避免高并发重试导致MQ拥堵
- 监控告警:建议对"发送失败"状态的消息添加监控告警,及时人工介入处理
- 逻辑删除:消息删除采用逻辑删除,避免误删导致问题排查困难
十、扩展建议
- 多消息中间件支持 :实现
ILocalMessageService接口,扩展Kafka、RocketMQ等实现 - 自定义ID生成器 :实现
IMessageIdGeneratorService,替换为雪花算法(适合需要有序ID的场景) - 消息监控平台:开发管理平台,支持消息查询、手动重试、状态统计
- 死信队列:对"发送失败"的消息,可转发到死信队列,避免占用正常队列资源
- 异步通知:添加消息发送结果回调(如HTTP回调),通知业务系统消息发送状态
总结
本方案基于"本地消息表 + RabbitMQ"实现分布式事务,具有高可靠、低侵入、易扩展的特点,已覆盖大部分分布式场景的消息一致性需求。核心优势:
- 无分布式事务中间件依赖,部署简单
- 消息必达机制,避免消息丢失
- 事务同步设计,确保业务与消息原子性
- 完整的重试和监控机制,便于问题排查
所有代码已去隐私化,可直接复制到项目中,替换配置文件中的数据库、MQ连接信息即可运行!