MySQL + Redis 分布式事务消息中间件:保证最终一致性

MySQL + Redis 分布式事务消息中间件:保证最终一致性

摘要

在分布式系统中,MySQL 和 Redis 的数据一致性是一个经典难题。本文将深入探讨如何通过自研消息中间件,基于本地消息表和事务消息机制,实现 MySQL 事务和 Redis 缓存的最终一致性,并提供生产级的完整解决方案。

目录

  1. 分布式一致性问题
  2. 技术方案对比
  3. 核心架构设计
  4. 本地消息表方案
  5. 事务消息中间件实现
  6. 生产级代码实现
  7. 异常场景处理
  8. 性能优化
  9. 监控与运维
  10. 实战案例
  11. 最佳实践
  12. 总结

1. 分布式一致性问题

1.1 典型场景

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│           MySQL + Redis 数据不一致典型场景                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景 1:先更新 MySQL,再更新 Redis                          │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐         │
│  │ 更新MySQL │ ───▶ │  提交成功 │ ───▶ │ 更新Redis│         │
│  │          │      │          │      │   失败   │ ❌       │
│  └──────────┘      └──────────┘      └──────────┘         │
│  结果:MySQL 已更新,Redis 未更新,数据不一致               │
│                                                             │
│  场景 2:先删除 Redis,再更新 MySQL                          │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐         │
│  │ 删除Redis │ ───▶ │  删除成功 │ ───▶ │ 更新MySQL│         │
│  │          │      │          │      │   失败   │ ❌       │
│  └──────────┘      └──────────┘      └──────────┘         │
│  结果:Redis 已删除,MySQL 未更新,数据不一致               │
│                                                             │
│  场景 3:并发读写问题                                        │
│  线程 A:更新 MySQL ───▶ 删除 Redis                         │
│  线程 B:读取 Redis(旧数据)───▶ 写入 Redis(覆盖新数据)  │
│  结果:Redis 中是旧数据,数据不一致                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 核心问题

问题本质:

  • 跨系统操作无法保证原子性
  • 网络异常、服务宕机导致部分操作失败
  • 并发场景下的竞态条件

解决目标:

  • ✅ 保证 MySQL 和 Redis 的最终一致性
  • ✅ 可靠的消息投递(至少一次)
  • ✅ 幂等性处理
  • ✅ 异常可回滚或重试
  • ✅ 性能影响最小化

2. 技术方案对比

2.1 常见方案

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│              分布式一致性方案对比                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  方案             优点              缺点          适用场景    │
│  ─────────────────────────────────────────────────────────  │
│  延迟双删      简单易实现        不保证一致性    低一致性要求 │
│                                  并发问题                    │
│                                                              │
│  Canal监听     解耦性好          延迟较大        数据同步     │
│  Binlog        侵入性小          需要额外组件    日志场景     │
│                                                              │
│  本地消息表    强一致性          代码侵入性强    高一致性要求 │
│                可靠性高          性能有影响      订单/支付    │
│                                                              │
│  TCC           强一致性          实现复杂        金融场景     │
│                可控性好          性能开销大      核心业务     │
│                                                              │
│  Saga          适合长事务        补偿逻辑复杂    跨服务流程   │
│                灵活性好          不保证隔离性    工作流       │
│                                                              │
│  消息中间件    解耦性强          依赖MQ组件      异步场景     │
│  (本文方案)    可靠性高          学习成本         高并发      │
│                扩展性好                                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.2 本文方案选型

我们采用 本地消息表 + 事务消息中间件 的混合方案:

  • 本地消息表保证消息可靠性
  • 定时任务扫描待发送消息
  • MQ 保证消息投递
  • 消费端幂等性处理
  • 支持消息补偿和重试

3. 核心架构设计

3.1 整体架构

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│                消息最终一致性中间件架构                        │
└──────────────────────────────────────────────────────────────┘

                    ┌─────────────────┐
                    │  业务服务层      │
                    │  (Controller)   │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │   业务逻辑层     │
                    │   (Service)     │
                    └────────┬────────┘
                             │
            ┌────────────────┼────────────────┐
            │                │                │
   ┌────────▼────────┐  ┌───▼────────┐  ┌───▼────────┐
   │ 事务消息发送器   │  │ MySQL 操作 │  │ 业务处理   │
   │ TransactionMsg  │  │            │  │            │
   └────────┬────────┘  └───┬────────┘  └────────────┘
            │               │
            │      ┌────────▼────────┐
            │      │  MySQL 事务      │
            │      │  ┌────────────┐  │
            │      │  │ 业务数据表 │  │
            │      │  └────────────┘  │
            │      │  ┌────────────┐  │
            │      │  │ 消息表     │  │  ← 本地消息表
            │      │  │ (msg_log)  │  │
            │      │  └────────────┘  │
            │      └─────────────────┘
            │               │
            └───────────────┤
                            │ 提交成功
                    ┌───────▼────────┐
                    │  消息扫描器     │
                    │  (Scheduler)   │
                    │  • 扫描待发送  │
                    │  • 重试失败    │
                    └───────┬────────┘
                            │
                    ┌───────▼────────┐
                    │   消息队列      │
                    │  (RabbitMQ)    │
                    └───────┬────────┘
                            │
            ┌───────────────┼───────────────┐
            │               │               │
   ┌────────▼────────┐ ┌───▼────────┐ ┌───▼────────┐
   │  缓存更新消费者  │ │ 其他消费者 │ │ 其他消费者 │
   │                 │ │            │ │            │
   │  • 幂等性检查   │ │            │ │            │
   │  • 更新Redis    │ │            │ │            │
   │  • ACK确认      │ │            │ │            │
   └────────┬────────┘ └────────────┘ └────────────┘
            │
   ┌────────▼────────┐
   │   Redis Cache   │
   │                 │
   └─────────────────┘

3.2 核心流程

markdown 复制代码
【生产者流程】

开始业务事务
     │
     ├─ 1. 执行业务操作(更新用户信息)
     │
     ├─ 2. 插入本地消息表(状态:待发送)
     │      - 消息ID(全局唯一)
     │      - 业务数据
     │      - 消息类型
     │      - 状态:PENDING
     │
提交 MySQL 事务
     │
     ├─ 成功 ──▶ 3. 异步发送到 MQ
     │              │
     │              ├─ 成功 ──▶ 更新消息状态为 SENT
     │              │
     │              └─ 失败 ──▶ 定时任务重试
     │
     └─ 失败 ──▶ 回滚(消息表也回滚)


【消费者流程】

接收 MQ 消息
     │
     ├─ 1. 幂等性检查
     │      - 检查消息是否已处理
     │      - 使用分布式锁
     │
     ├─ 2. 执行业务逻辑
     │      - 更新 Redis 缓存
     │      - 其他业务操作
     │
     ├─ 3. 记录消费状态
     │      - 插入消费记录表
     │      - 防止重复消费
     │
     ├─ 成功 ──▶ ACK 消息
     │
     └─ 失败 ──▶ NACK 消息(触发重试)

4. 本地消息表方案

4.1 数据库设计

sql 复制代码
-- 本地消息表
CREATE TABLE `transaction_message` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `message_id` VARCHAR(64) NOT NULL COMMENT '消息唯一ID(UUID)',
    `business_key` VARCHAR(128) NOT NULL COMMENT '业务唯一键',
    `message_type` VARCHAR(50) NOT NULL COMMENT '消息类型',
    `message_body` TEXT NOT NULL COMMENT '消息内容(JSON)',
    `status` VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '消息状态:PENDING/SENT/CONSUMED/FAILED',
    `retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
    `max_retry` INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
    `next_retry_time` DATETIME NULL COMMENT '下次重试时间',
    `error_msg` TEXT NULL COMMENT '错误信息',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_id` (`message_id`),
    KEY `idx_status_retry` (`status`, `next_retry_time`),
    KEY `idx_business_key` (`business_key`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地事务消息表';

-- 消息消费记录表(消费端)
CREATE TABLE `message_consume_log` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `message_id` VARCHAR(64) NOT NULL COMMENT '消息ID',
    `consumer_name` VARCHAR(100) NOT NULL COMMENT '消费者名称',
    `consume_status` VARCHAR(20) NOT NULL COMMENT '消费状态:SUCCESS/FAILED',
    `consume_time` DATETIME NOT NULL COMMENT '消费时间',
    `error_msg` TEXT NULL COMMENT '错误信息',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_consumer` (`message_id`, `consumer_name`),
    KEY `idx_consume_time` (`consume_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息消费日志表';

-- 业务表示例(用户表)
CREATE TABLE `user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL,
    `email` VARCHAR(100),
    `phone` VARCHAR(20),
    `status` TINYINT NOT NULL DEFAULT 1,
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 实体类定义

java 复制代码
package com.example.transaction.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * 事务消息实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransactionMessage {

    private Long id;

    /**
     * 消息唯一ID
     */
    private String messageId;

    /**
     * 业务唯一键(用于幂等性)
     */
    private String businessKey;

    /**
     * 消息类型
     */
    private String messageType;

    /**
     * 消息内容(JSON格式)
     */
    private String messageBody;

    /**
     * 消息状态
     */
    private MessageStatus status;

    /**
     * 重试次数
     */
    private Integer retryCount;

    /**
     * 最大重试次数
     */
    private Integer maxRetry;

    /**
     * 下次重试时间
     */
    private LocalDateTime nextRetryTime;

    /**
     * 错误信息
     */
    private String errorMsg;

    /**
     * 创建时间
     */
    private LocalDateTime createdAt;

    /**
     * 更新时间
     */
    private LocalDateTime updatedAt;

    /**
     * 消息状态枚举
     */
    public enum MessageStatus {
        PENDING("待发送"),
        SENT("已发送"),
        CONSUMED("已消费"),
        FAILED("失败");

        private final String description;

        MessageStatus(String description) {
            this.description = description;
        }

        public String getDescription() {
            return description;
        }
    }
}

/**
 * 消息消费日志实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageConsumeLog {

    private Long id;

    /**
     * 消息ID
     */
    private String messageId;

    /**
     * 消费者名称
     */
    private String consumerName;

    /**
     * 消费状态
     */
    private ConsumeStatus consumeStatus;

    /**
     * 消费时间
     */
    private LocalDateTime consumeTime;

    /**
     * 错误信息
     */
    private String errorMsg;

    public enum ConsumeStatus {
        SUCCESS, FAILED
    }
}

/**
 * 用户实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long id;
    private String username;
    private String email;
    private String phone;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

5. 事务消息中间件实现

5.1 Maven 依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>transaction-message-middleware</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot AMQP (RabbitMQ) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!-- Spring Boot Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- Redisson (分布式锁) -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.25.0</version>
        </dependency>

        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Apache Commons Lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

5.2 配置文件

yaml 复制代码
spring:
  application:
    name: transaction-message-middleware

  # MySQL 配置
  datasource:
    url: jdbc:mysql://localhost:3306/transaction_msg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

  # Redis 配置
  data:
    redis:
      host: localhost
      port: 6379
      password: redis123
      database: 0
      timeout: 60s
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5

  # RabbitMQ 配置
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin123
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual
        retry:
          enabled: true
          max-attempts: 3
          initial-interval: 3000

# MyBatis Plus 配置
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  type-aliases-package: com.example.transaction.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 事务消息配置
transaction:
  message:
    # 消息扫描配置
    scanner:
      enabled: true
      cron: "0/10 * * * * ?"  # 每10秒扫描一次
      batch-size: 100          # 每次扫描数量
    # 重试配置
    retry:
      max-attempts: 3
      initial-interval: 5000   # 首次重试间隔(毫秒)
      multiplier: 2.0          # 重试间隔倍数
    # 消息过期时间
    expire-hours: 72           # 72小时后自动清理

# 日志配置
logging:
  level:
    root: INFO
    com.example: DEBUG
    com.rabbitmq: DEBUG

5.3 RabbitMQ 配置

java 复制代码
package com.example.transaction.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    // 交换机名称
    public static final String TRANSACTION_EXCHANGE = "transaction.exchange";

    // 队列名称
    public static final String CACHE_UPDATE_QUEUE = "cache.update.queue";
    public static final String CACHE_UPDATE_ROUTING_KEY = "cache.update";

    // 死信队列
    public static final String DLX_EXCHANGE = "dlx.exchange";
    public static final String DLX_QUEUE = "dlx.queue";

    /**
     * 交换机
     */
    @Bean
    public DirectExchange transactionExchange() {
        return ExchangeBuilder
                .directExchange(TRANSACTION_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 缓存更新队列
     */
    @Bean
    public Queue cacheUpdateQueue() {
        return QueueBuilder
                .durable(CACHE_UPDATE_QUEUE)
                .deadLetterExchange(DLX_EXCHANGE)
                .deadLetterRoutingKey("dlx")
                .build();
    }

    /**
     * 绑定
     */
    @Bean
    public Binding cacheUpdateBinding() {
        return BindingBuilder
                .bind(cacheUpdateQueue())
                .to(transactionExchange())
                .with(CACHE_UPDATE_ROUTING_KEY);
    }

    /**
     * 死信交换机
     */
    @Bean
    public DirectExchange dlxExchange() {
        return ExchangeBuilder
                .directExchange(DLX_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 死信队列
     */
    @Bean
    public Queue dlxQueue() {
        return QueueBuilder
                .durable(DLX_QUEUE)
                .build();
    }

    /**
     * 死信绑定
     */
    @Bean
    public Binding dlxBinding() {
        return BindingBuilder
                .bind(dlxQueue())
                .to(dlxExchange())
                .with("dlx");
    }

    /**
     * 消息转换器(使用 JSON)
     */
    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * RabbitTemplate 配置
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(messageConverter());

        // 消息发送确认
        template.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                System.err.println("消息发送失败: " + cause);
            }
        });

        // 消息返回确认
        template.setReturnsCallback(returned -> {
            System.err.println("消息被退回: " + returned.getMessage());
        });

        return template;
    }

    /**
     * 监听器容器工厂
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(messageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setPrefetchCount(10);
        return factory;
    }
}

6. 生产级代码实现

6.1 事务消息管理器

java 复制代码
package com.example.transaction.manager;

import com.example.transaction.entity.TransactionMessage;
import com.example.transaction.mapper.TransactionMessageMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 事务消息管理器
 */
@Slf4j
@Component
public class TransactionMessageManager {

    private final TransactionMessageMapper messageMapper;
    private final RabbitTemplate rabbitTemplate;
    private final ObjectMapper objectMapper;

    public TransactionMessageManager(
            TransactionMessageMapper messageMapper,
            RabbitTemplate rabbitTemplate,
            ObjectMapper objectMapper) {
        this.messageMapper = messageMapper;
        this.rabbitTemplate = rabbitTemplate;
        this.objectMapper = objectMapper;
    }

    /**
     * 发送事务消息(在业务事务内调用)
     *
     * @param businessKey  业务唯一键
     * @param messageType  消息类型
     * @param messageBody  消息内容(对象)
     * @param exchange     交换机
     * @param routingKey   路由键
     * @return 消息ID
     */
    @Transactional(rollbackFor = Exception.class)
    public String sendTransactionMessage(
            String businessKey,
            String messageType,
            Object messageBody,
            String exchange,
            String routingKey) {

        try {
            // 1. 生成消息ID
            String messageId = UUID.randomUUID().toString();

            // 2. 序列化消息体
            String messageBodyJson = objectMapper.writeValueAsString(messageBody);

            // 3. 插入本地消息表(在业务事务内)
            TransactionMessage message = TransactionMessage.builder()
                    .messageId(messageId)
                    .businessKey(businessKey)
                    .messageType(messageType)
                    .messageBody(messageBodyJson)
                    .status(TransactionMessage.MessageStatus.PENDING)
                    .retryCount(0)
                    .maxRetry(3)
                    .nextRetryTime(LocalDateTime.now())
                    .build();

            messageMapper.insert(message);

            log.info("本地消息表插入成功 - messageId: {}, businessKey: {}", messageId, businessKey);

            // 4. 业务事务提交后,异步发送消息到MQ
            // 注意:这里需要在事务提交后执行
            sendToMQ(messageId, messageBodyJson, exchange, routingKey);

            return messageId;

        } catch (Exception e) {
            log.error("发送事务消息失败", e);
            throw new RuntimeException("发送事务消息失败", e);
        }
    }

    /**
     * 发送消息到 MQ
     */
    public void sendToMQ(String messageId, String messageBody, String exchange, String routingKey) {
        try {
            rabbitTemplate.convertAndSend(exchange, routingKey, messageBody, message -> {
                message.getMessageProperties().setMessageId(messageId);
                return message;
            });

            // 更新消息状态为已发送
            messageMapper.updateStatus(messageId, TransactionMessage.MessageStatus.SENT);

            log.info("消息发送到MQ成功 - messageId: {}", messageId);

        } catch (Exception e) {
            log.error("消息发送到MQ失败 - messageId: {}", messageId, e);

            // 更新错误信息
            messageMapper.updateError(messageId, e.getMessage());

            // 计算下次重试时间(指数退避)
            TransactionMessage msg = messageMapper.selectById(messageId);
            if (msg != null && msg.getRetryCount() < msg.getMaxRetry()) {
                long delaySeconds = (long) Math.pow(2, msg.getRetryCount()) * 5;
                LocalDateTime nextRetryTime = LocalDateTime.now().plusSeconds(delaySeconds);
                messageMapper.updateNextRetryTime(messageId, nextRetryTime);
            }
        }
    }

    /**
     * 标记消息已消费
     */
    public void markAsConsumed(String messageId) {
        messageMapper.updateStatus(messageId, TransactionMessage.MessageStatus.CONSUMED);
        log.info("消息标记为已消费 - messageId: {}", messageId);
    }

    /**
     * 增加重试次数
     */
    public void incrementRetryCount(String messageId) {
        messageMapper.incrementRetryCount(messageId);
    }
}

6.2 消息扫描器

java 复制代码
package com.example.transaction.scheduler;

import com.example.transaction.config.RabbitMQConfig;
import com.example.transaction.entity.TransactionMessage;
import com.example.transaction.manager.TransactionMessageManager;
import com.example.transaction.mapper.TransactionMessageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 事务消息扫描器
 * 定时扫描待发送和失败的消息,进行重试
 */
@Slf4j
@Component
public class TransactionMessageScanner {

    private final TransactionMessageMapper messageMapper;
    private final TransactionMessageManager messageManager;

    @Value("${transaction.message.scanner.batch-size:100}")
    private int batchSize;

    @Value("${transaction.message.retry.max-attempts:3}")
    private int maxRetryAttempts;

    public TransactionMessageScanner(
            TransactionMessageMapper messageMapper,
            TransactionMessageManager messageManager) {
        this.messageMapper = messageMapper;
        this.messageManager = messageManager;
    }

    /**
     * 扫描待发送的消息
     */
    @Scheduled(cron = "${transaction.message.scanner.cron:0/10 * * * * ?}")
    public void scanPendingMessages() {
        log.debug("开始扫描待发送消息...");

        try {
            // 查询待发送的消息(状态为PENDING,且重试时间已到)
            List<TransactionMessage> messages = messageMapper.selectPendingMessages(
                    LocalDateTime.now(),
                    batchSize
            );

            if (messages.isEmpty()) {
                return;
            }

            log.info("找到 {} 条待发送消息", messages.size());

            for (TransactionMessage message : messages) {
                try {
                    // 检查是否超过最大重试次数
                    if (message.getRetryCount() >= maxRetryAttempts) {
                        log.warn("消息超过最大重试次数,标记为失败 - messageId: {}", message.getMessageId());
                        messageMapper.updateStatus(
                                message.getMessageId(),
                                TransactionMessage.MessageStatus.FAILED
                        );
                        continue;
                    }

                    // 增加重试次数
                    messageManager.incrementRetryCount(message.getMessageId());

                    // 重新发送到 MQ
                    messageManager.sendToMQ(
                            message.getMessageId(),
                            message.getMessageBody(),
                            RabbitMQConfig.TRANSACTION_EXCHANGE,
                            RabbitMQConfig.CACHE_UPDATE_ROUTING_KEY
                    );

                    log.info("消息重新发送成功 - messageId: {}, retryCount: {}",
                            message.getMessageId(), message.getRetryCount() + 1);

                } catch (Exception e) {
                    log.error("消息重新发送失败 - messageId: {}", message.getMessageId(), e);
                }
            }

        } catch (Exception e) {
            log.error("扫描待发送消息失败", e);
        }
    }

    /**
     * 清理过期消息
     */
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
    public void cleanExpiredMessages() {
        log.info("开始清理过期消息...");

        try {
            LocalDateTime expireTime = LocalDateTime.now().minusHours(72);
            int deletedCount = messageMapper.deleteExpiredMessages(expireTime);

            log.info("清理过期消息完成,删除 {} 条记录", deletedCount);

        } catch (Exception e) {
            log.error("清理过期消息失败", e);
        }
    }
}

6.3 消息消费者

java 复制代码
package com.example.transaction.consumer;

import com.example.transaction.config.RabbitMQConfig;
import com.example.transaction.entity.MessageConsumeLog;
import com.example.transaction.manager.TransactionMessageManager;
import com.example.transaction.mapper.MessageConsumeLogMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * 缓存更新消费者
 */
@Slf4j
@Component
public class CacheUpdateConsumer {

    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;
    private final ObjectMapper objectMapper;
    private final MessageConsumeLogMapper consumeLogMapper;
    private final TransactionMessageManager messageManager;

    private static final String CONSUMER_NAME = "CacheUpdateConsumer";
    private static final String LOCK_KEY_PREFIX = "msg:lock:";

    public CacheUpdateConsumer(
            StringRedisTemplate redisTemplate,
            RedissonClient redissonClient,
            ObjectMapper objectMapper,
            MessageConsumeLogMapper consumeLogMapper,
            TransactionMessageManager messageManager) {
        this.redisTemplate = redisTemplate;
        this.redissonClient = redissonClient;
        this.objectMapper = objectMapper;
        this.consumeLogMapper = consumeLogMapper;
        this.messageManager = messageManager;
    }

    /**
     * 消费缓存更新消息
     */
    @RabbitListener(queues = RabbitMQConfig.CACHE_UPDATE_QUEUE)
    public void handleCacheUpdate(Message message, Channel channel) throws IOException {
        String messageId = message.getMessageProperties().getMessageId();
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        log.info("接收到缓存更新消息 - messageId: {}", messageId);

        RLock lock = null;
        try {
            // 1. 幂等性检查(使用分布式锁)
            String lockKey = LOCK_KEY_PREFIX + messageId;
            lock = redissonClient.getLock(lockKey);

            if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                log.warn("获取锁失败,消息可能正在处理 - messageId: {}", messageId);
                // NACK 消息,稍后重试
                channel.basicNack(deliveryTag, false, true);
                return;
            }

            // 2. 检查消息是否已处理
            if (isMessageConsumed(messageId)) {
                log.info("消息已处理,跳过 - messageId: {}", messageId);
                // ACK 消息
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 3. 解析消息内容
            String messageBody = new String(message.getBody());
            JsonNode jsonNode = objectMapper.readTree(messageBody);

            String businessKey = jsonNode.get("businessKey").asText();
            String operation = jsonNode.get("operation").asText();

            log.info("处理缓存操作 - businessKey: {}, operation: {}", businessKey, operation);

            // 4. 执行缓存操作
            switch (operation) {
                case "UPDATE" -> updateCache(businessKey, jsonNode.get("data"));
                case "DELETE" -> deleteCache(businessKey);
                default -> log.warn("未知操作类型: {}", operation);
            }

            // 5. 记录消费日志
            saveConsumeLog(messageId, MessageConsumeLog.ConsumeStatus.SUCCESS, null);

            // 6. 标记消息已消费
            messageManager.markAsConsumed(messageId);

            // 7. ACK 消息
            channel.basicAck(deliveryTag, false);

            log.info("消息处理成功 - messageId: {}", messageId);

        } catch (Exception e) {
            log.error("消息处理失败 - messageId: {}", messageId, e);

            // 记录失败日志
            saveConsumeLog(messageId, MessageConsumeLog.ConsumeStatus.FAILED, e.getMessage());

            // NACK 消息,触发重试
            channel.basicNack(deliveryTag, false, true);

        } finally {
            // 释放锁
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 检查消息是否已消费
     */
    private boolean isMessageConsumed(String messageId) {
        return consumeLogMapper.existsByMessageIdAndConsumer(messageId, CONSUMER_NAME) > 0;
    }

    /**
     * 更新缓存
     */
    private void updateCache(String key, JsonNode data) {
        try {
            String value = objectMapper.writeValueAsString(data);
            redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
            log.info("缓存更新成功 - key: {}", key);
        } catch (Exception e) {
            log.error("缓存更新失败 - key: {}", key, e);
            throw new RuntimeException("缓存更新失败", e);
        }
    }

    /**
     * 删除缓存
     */
    private void deleteCache(String key) {
        Boolean deleted = redisTemplate.delete(key);
        log.info("缓存删除{} - key: {}", deleted ? "成功" : "失败", key);
    }

    /**
     * 保存消费日志
     */
    private void saveConsumeLog(String messageId, MessageConsumeLog.ConsumeStatus status, String errorMsg) {
        try {
            MessageConsumeLog log = MessageConsumeLog.builder()
                    .messageId(messageId)
                    .consumerName(CONSUMER_NAME)
                    .consumeStatus(status)
                    .consumeTime(LocalDateTime.now())
                    .errorMsg(errorMsg)
                    .build();

            consumeLogMapper.insert(log);
        } catch (Exception e) {
            log.error("保存消费日志失败", e);
        }
    }
}

6.4 业务服务层

java 复制代码
package com.example.transaction.service;

import com.example.transaction.config.RabbitMQConfig;
import com.example.transaction.entity.User;
import com.example.transaction.manager.TransactionMessageManager;
import com.example.transaction.mapper.UserMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 用户服务
 */
@Slf4j
@Service
public class UserService {

    private final UserMapper userMapper;
    private final StringRedisTemplate redisTemplate;
    private final TransactionMessageManager messageManager;
    private final ObjectMapper objectMapper;

    private static final String USER_CACHE_PREFIX = "user:";

    public UserService(
            UserMapper userMapper,
            StringRedisTemplate redisTemplate,
            TransactionMessageManager messageManager,
            ObjectMapper objectMapper) {
        this.userMapper = userMapper;
        this.redisTemplate = redisTemplate;
        this.messageManager = messageManager;
        this.objectMapper = objectMapper;
    }

    /**
     * 更新用户信息(带缓存同步)
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(Long userId, String email, String phone) {
        try {
            log.info("开始更新用户信息 - userId: {}", userId);

            // 1. 查询用户
            User user = userMapper.selectById(userId);
            if (user == null) {
                throw new RuntimeException("用户不存在");
            }

            // 2. 更新用户信息
            user.setEmail(email);
            user.setPhone(phone);
            userMapper.updateById(user);

            log.info("MySQL 更新成功 - userId: {}", userId);

            // 3. 构建消息体
            String businessKey = USER_CACHE_PREFIX + userId;
            Map<String, Object> messageBody = new HashMap<>();
            messageBody.put("businessKey", businessKey);
            messageBody.put("operation", "UPDATE");
            messageBody.put("data", user);

            // 4. 发送事务消息(在同一事务内)
            messageManager.sendTransactionMessage(
                    businessKey,
                    "USER_UPDATE",
                    messageBody,
                    RabbitMQConfig.TRANSACTION_EXCHANGE,
                    RabbitMQConfig.CACHE_UPDATE_ROUTING_KEY
            );

            log.info("用户更新完成 - userId: {}", userId);

        } catch (Exception e) {
            log.error("更新用户失败 - userId: {}", userId, e);
            throw new RuntimeException("更新用户失败", e);
        }
    }

    /**
     * 查询用户(先查缓存,缓存未命中查数据库)
     */
    public User getUser(Long userId) {
        String cacheKey = USER_CACHE_PREFIX + userId;

        try {
            // 1. 查询缓存
            String cachedUser = redisTemplate.opsForValue().get(cacheKey);
            if (cachedUser != null) {
                log.info("缓存命中 - userId: {}", userId);
                return objectMapper.readValue(cachedUser, User.class);
            }

            // 2. 缓存未命中,查询数据库
            log.info("缓存未命中,查询数据库 - userId: {}", userId);
            User user = userMapper.selectById(userId);

            if (user != null) {
                // 3. 写入缓存
                String userJson = objectMapper.writeValueAsString(user);
                redisTemplate.opsForValue().set(cacheKey, userJson, 1, TimeUnit.HOURS);
            }

            return user;

        } catch (Exception e) {
            log.error("查询用户失败 - userId: {}", userId, e);
            // 缓存失败,降级到数据库查询
            return userMapper.selectById(userId);
        }
    }

    /**
     * 删除用户(带缓存清理)
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteUser(Long userId) {
        try {
            log.info("开始删除用户 - userId: {}", userId);

            // 1. 删除数据库记录
            userMapper.deleteById(userId);

            log.info("MySQL 删除成功 - userId: {}", userId);

            // 2. 发送缓存删除消息
            String businessKey = USER_CACHE_PREFIX + userId;
            Map<String, Object> messageBody = new HashMap<>();
            messageBody.put("businessKey", businessKey);
            messageBody.put("operation", "DELETE");

            messageManager.sendTransactionMessage(
                    businessKey,
                    "USER_DELETE",
                    messageBody,
                    RabbitMQConfig.TRANSACTION_EXCHANGE,
                    RabbitMQConfig.CACHE_UPDATE_ROUTING_KEY
            );

            log.info("用户删除完成 - userId: {}", userId);

        } catch (Exception e) {
            log.error("删除用户失败 - userId: {}", userId, e);
            throw new RuntimeException("删除用户失败", e);
        }
    }
}

6.5 Mapper 接口

java 复制代码
package com.example.transaction.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.transaction.entity.TransactionMessage;
import org.apache.ibatis.annotations.*;

import java.time.LocalDateTime;
import java.util.List;

@Mapper
public interface TransactionMessageMapper extends BaseMapper<TransactionMessage> {

    /**
     * 查询待发送的消息
     */
    @Select("SELECT * FROM transaction_message " +
            "WHERE status = 'PENDING' " +
            "AND next_retry_time <= #{now} " +
            "AND retry_count < max_retry " +
            "ORDER BY created_at ASC " +
            "LIMIT #{limit}")
    List<TransactionMessage> selectPendingMessages(
            @Param("now") LocalDateTime now,
            @Param("limit") int limit
    );

    /**
     * 更新消息状态
     */
    @Update("UPDATE transaction_message " +
            "SET status = #{status}, updated_at = NOW() " +
            "WHERE message_id = #{messageId}")
    int updateStatus(
            @Param("messageId") String messageId,
            @Param("status") TransactionMessage.MessageStatus status
    );

    /**
     * 更新错误信息
     */
    @Update("UPDATE transaction_message " +
            "SET error_msg = #{errorMsg}, updated_at = NOW() " +
            "WHERE message_id = #{messageId}")
    int updateError(
            @Param("messageId") String messageId,
            @Param("errorMsg") String errorMsg
    );

    /**
     * 更新下次重试时间
     */
    @Update("UPDATE transaction_message " +
            "SET next_retry_time = #{nextRetryTime}, updated_at = NOW() " +
            "WHERE message_id = #{messageId}")
    int updateNextRetryTime(
            @Param("messageId") String messageId,
            @Param("nextRetryTime") LocalDateTime nextRetryTime
    );

    /**
     * 增加重试次数
     */
    @Update("UPDATE transaction_message " +
            "SET retry_count = retry_count + 1, updated_at = NOW() " +
            "WHERE message_id = #{messageId}")
    int incrementRetryCount(@Param("messageId") String messageId);

    /**
     * 删除过期消息
     */
    @Delete("DELETE FROM transaction_message " +
            "WHERE created_at < #{expireTime} " +
            "AND status IN ('SENT', 'CONSUMED', 'FAILED')")
    int deleteExpiredMessages(@Param("expireTime") LocalDateTime expireTime);
}

@Mapper
public interface MessageConsumeLogMapper extends BaseMapper<MessageConsumeLog> {

    /**
     * 检查消息是否已消费
     */
    @Select("SELECT COUNT(*) FROM message_consume_log " +
            "WHERE message_id = #{messageId} " +
            "AND consumer_name = #{consumerName} " +
            "AND consume_status = 'SUCCESS'")
    int existsByMessageIdAndConsumer(
            @Param("messageId") String messageId,
            @Param("consumerName") String consumerName
    );
}

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

7. 异常场景处理

7.1 异常场景分析

复制代码
┌──────────────────────────────────────────────────────────────┐
│                  异常场景及处理方案                            │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  场景 1:MySQL 提交失败                                       │
│  ────────────────────────────────────────────────────────   │
│  现象:业务操作和消息表都回滚                                │
│  处理:无需处理,数据一致                                     │
│  结果:✅ 数据一致                                            │
│                                                              │
│  场景 2:MySQL 提交成功,MQ 发送失败                          │
│  ────────────────────────────────────────────────────────   │
│  现象:消息表状态为 PENDING                                   │
│  处理:定时扫描器重试发送                                     │
│  结果:✅ 最终一致                                            │
│                                                              │
│  场景 3:消息发送成功,消费失败                                │
│  ────────────────────────────────────────────────────────   │
│  现象:MQ 消息未 ACK                                         │
│  处理:RabbitMQ 自动重新投递                                 │
│  结果:✅ 消费重试直到成功                                     │
│                                                              │
│  场景 4:消息重复消费                                         │
│  ────────────────────────────────────────────────────────   │
│  现象:MQ 消息重复投递                                        │
│  处理:消费端幂等性检查(消费记录表 + 分布式锁)              │
│  结果:✅ 重复消息被过滤                                      │
│                                                              │
│  场景 5:Redis 宕机                                           │
│  ────────────────────────────────────────────────────────   │
│  现象:缓存操作失败                                           │
│  处理:消费失败,消息重新入队,等待 Redis 恢复后重试          │
│  结果:✅ Redis 恢复后最终一致                                │
│                                                              │
│  场景 6:服务重启                                             │
│  ────────────────────────────────────────────────────────   │
│  现象:可能有消息处理到一半                                   │
│  处理:MQ 未 ACK 的消息重新投递,分布式锁防止重复处理         │
│  结果:✅ 重启后继续处理                                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

7.2 补偿机制

java 复制代码
package com.example.transaction.compensation;

import com.example.transaction.entity.TransactionMessage;
import com.example.transaction.mapper.TransactionMessageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 消息补偿处理器
 */
@Slf4j
@Component
public class MessageCompensationHandler {

    private final TransactionMessageMapper messageMapper;

    public MessageCompensationHandler(TransactionMessageMapper messageMapper) {
        this.messageMapper = messageMapper;
    }

    /**
     * 人工补偿失败的消息
     */
    public void compensateFailedMessages() {
        log.info("开始补偿失败消息...");

        // 查询失败的消息
        List<TransactionMessage> failedMessages = messageMapper.selectList(
                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TransactionMessage>()
                        .eq("status", TransactionMessage.MessageStatus.FAILED)
                        .ge("created_at", LocalDateTime.now().minusHours(24))
        );

        log.info("找到 {} 条失败消息待补偿", failedMessages.size());

        for (TransactionMessage message : failedMessages) {
            try {
                // 重置消息状态,允许重新发送
                message.setStatus(TransactionMessage.MessageStatus.PENDING);
                message.setRetryCount(0);
                message.setNextRetryTime(LocalDateTime.now());
                message.setErrorMsg(null);

                messageMapper.updateById(message);

                log.info("消息已重置为待发送 - messageId: {}", message.getMessageId());

            } catch (Exception e) {
                log.error("补偿消息失败 - messageId: {}", message.getMessageId(), e);
            }
        }

        log.info("消息补偿完成");
    }
}

8. 性能优化

8.1 批量处理优化

java 复制代码
package com.example.transaction.service;

import com.example.transaction.entity.User;
import com.example.transaction.manager.TransactionMessageManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 批量操作服务
 */
@Slf4j
@Service
public class BatchOperationService {

    private final UserService userService;
    private final TransactionMessageManager messageManager;

    public BatchOperationService(
            UserService userService,
            TransactionMessageManager messageManager) {
        this.userService = userService;
        this.messageManager = messageManager;
    }

    /**
     * 批量更新用户
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateUsers(List<User> users) {
        log.info("开始批量更新用户,数量: {}", users.size());

        for (User user : users) {
            userService.updateUser(user.getId(), user.getEmail(), user.getPhone());
        }

        log.info("批量更新用户完成");
    }
}

8.2 性能监控

java 复制代码
package com.example.transaction.monitor;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;

/**
 * 消息性能监控
 */
@Component
public class MessageMetrics {

    private final Counter messageSentCounter;
    private final Counter messageConsumedCounter;
    private final Counter messageFailedCounter;
    private final Timer messageSendTimer;
    private final Timer messageConsumeTimer;

    public MessageMetrics(MeterRegistry meterRegistry) {
        this.messageSentCounter = meterRegistry.counter("message.sent.total");
        this.messageConsumedCounter = meterRegistry.counter("message.consumed.total");
        this.messageFailedCounter = meterRegistry.counter("message.failed.total");
        this.messageSendTimer = meterRegistry.timer("message.send.duration");
        this.messageConsumeTimer = meterRegistry.timer("message.consume.duration");
    }

    public void recordMessageSent() {
        messageSentCounter.increment();
    }

    public void recordMessageConsumed() {
        messageConsumedCounter.increment();
    }

    public void recordMessageFailed() {
        messageFailedCounter.increment();
    }

    public Timer.Sample startSendTimer() {
        return Timer.start();
    }

    public void recordSendDuration(Timer.Sample sample) {
        sample.stop(messageSendTimer);
    }

    public Timer.Sample startConsumeTimer() {
        return Timer.start();
    }

    public void recordConsumeDuration(Timer.Sample sample) {
        sample.stop(messageConsumeTimer);
    }
}

9. 实战案例:电商订单系统

9.1 订单服务

java 复制代码
package com.example.transaction.demo;

import com.example.transaction.config.RabbitMQConfig;
import com.example.transaction.manager.TransactionMessageManager;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 订单服务示例
 */
@Slf4j
@Service
public class OrderService {

    private final TransactionMessageManager messageManager;

    public OrderService(TransactionMessageManager messageManager) {
        this.messageManager = messageManager;
    }

    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(CreateOrderRequest request) {
        log.info("开始创建订单 - userId: {}, productId: {}",
                request.getUserId(), request.getProductId());

        // 1. 创建订单记录(省略具体实现)
        Long orderId = saveOrder(request);

        // 2. 扣减库存(省略具体实现)
        deductStock(request.getProductId(), request.getQuantity());

        // 3. 发送消息通知(更新缓存、发送通知等)
        Map<String, Object> messageBody = new HashMap<>();
        messageBody.put("orderId", orderId);
        messageBody.put("userId", request.getUserId());
        messageBody.put("productId", request.getProductId());
        messageBody.put("quantity", request.getQuantity());
        messageBody.put("amount", request.getAmount());

        messageManager.sendTransactionMessage(
                "order:" + orderId,
                "ORDER_CREATED",
                messageBody,
                RabbitMQConfig.TRANSACTION_EXCHANGE,
                "order.created"
        );

        log.info("订单创建成功 - orderId: {}", orderId);
        return orderId;
    }

    private Long saveOrder(CreateOrderRequest request) {
        // 省略实现
        return System.currentTimeMillis();
    }

    private void deductStock(Long productId, Integer quantity) {
        // 省略实现
    }

    @Data
    public static class CreateOrderRequest {
        private Long userId;
        private Long productId;
        private Integer quantity;
        private BigDecimal amount;
    }
}

10. 最佳实践

10.1 设计原则

css 复制代码
┌──────────────────────────────────────────────────────────┐
│            事务消息系统设计最佳实践                        │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  1. 消息设计                                              │
│     ✓ 消息体包含完整业务数据                              │
│     ✓ 消息 ID 全局唯一(UUID)                           │
│     ✓ 业务键用于幂等性判断                                │
│                                                          │
│  2. 事务管理                                              │
│     ✓ 消息表操作必须在业务事务内                          │
│     ✓ MQ 发送在事务提交后异步执行                         │
│     ✓ 使用 @Transactional 注解                          │
│                                                          │
│  3. 可靠性保证                                            │
│     ✓ 消费端幂等性处理(消费记录表 + 分布式锁)           │
│     ✓ 消息重试机制(指数退避)                            │
│     ✓ 死信队列处理异常消息                                │
│                                                          │
│  4. 性能优化                                              │
│     ✓ 批量扫描消息(分页)                                │
│     ✓ 合理设置重试间隔                                    │
│     ✓ 异步处理,避免阻塞                                  │
│                                                          │
│  5. 监控告警                                              │
│     ✓ 监控消息发送成功率                                  │
│     ✓ 监控消息消费延迟                                    │
│     ✓ 告警失败消息                                        │
│                                                          │
└──────────────────────────────────────────────────────────┘

11. 总结

11.1 方案优势

强一致性保证:利用本地消息表,保证 MySQL 事务和消息的原子性

高可靠性:消息不会丢失,支持重试和补偿

可扩展性:易于添加新的消费者和业务场景

幂等性:通过消费记录表和分布式锁保证幂等

可观测性:完整的日志和监控

11.2 适用场景

  • 订单系统(订单创建、支付、发货)
  • 用户系统(注册、登录、信息变更)
  • 库存系统(库存扣减、补货)
  • 营销系统(优惠券发放、积分变更)

相关推荐
BullSmall2 小时前
MySQL8.0连接数查询全攻略
mysql
跟着珅聪学java2 小时前
Jedis SetParams教程:参数化设置 Redis 键值对
数据库·redis·缓存
潇I洒2 小时前
Ubuntu Linux 24.04 安装MySQL 8.4.7
linux·数据库·mysql·ubuntu
milanyangbo2 小时前
像Git一样管理数据:深入解析数据库并发控制MVCC的实现
服务器·数据库·git·后端·mysql·架构·系统架构
jwybobo20072 小时前
redis7.x源码分析:(9) 内存淘汰策略
linux·c++·redis
JH30732 小时前
Redisson vs Jedis vs Lettuce
java·redis
菜鸟小九2 小时前
mysql运维(读写分离)
运维·数据库·mysql
菜鸟小九3 小时前
mysql运维(分库分表)
运维·数据库·mysql
蜂蜜黄油呀土豆3 小时前
MySQL Undo Log 深度解析:表空间、MVCC、回滚机制与版本演进全解
数据库·mysql·innodb·redo log·mvcc·undo log·事务日志