如何用 Redpanda + 本地事务,实现“发消息 + 写 DB” 的强一致性!

关注我,从零开始构建基础IT设施
作者:旷野说
定位:Spring Boot 开发者速查手册 + 高并发系统设计认知指南
关键澄清:在分布式系统中,"发消息 + 写 DB" 的强一致性无法靠"同步调用"实现,必须借助事务消息、本地事务表或 Redpanda 的幂等生产者 + 消费幂等的组合策略------没有银弹,只有权衡。


如何用 Redpanda + 本地事务,实现"发消息 + 写 DB" 的强一致性?

我是羅蘭。

在"最右"的打赏系统中,有一个看似简单却致命的需求:

"用户打赏成功后,必须 100% 保证:DB 有记录,且消息已发出。"

早期我们天真地这样写:

java 复制代码
@Transactional
public void sendGift(GiftRequest req) {
    giftRecordMapper.insert(req); // 1. 写 DB
    redpandaTemplate.send("gift-sent", event); // 2. 发消息
}

结果呢?

  • DB 写成功,消息发送失败 → 消息丢失,下游无感知
  • 消息发成功,DB 提交失败(事务回滚)→ 消息重复,下游重复处理

"发消息 + 写 DB" 的原子性,成了资损的黑洞。

直到我们深入 Redpanda 的能力边界,结合本地事务表 + 定时扫表任务 ,才真正守住一致性底线------即使 Redpanda 宕机两小时,恢复后也能自动续发,零消息丢失


一、为什么"同步发消息"不可靠?

Spring 的 @Transactional 只能管理 JDBC 事务无法控制 Redpanda 的消息发送

  • 消息发送在 DB 提交前:若网络超时,你无法知道消息是否真失败,重试可能造成重复。
  • 消息发送在 DB 提交后 :DB 成功,但消息发送失败 → 消息永久丢失

💡 根本矛盾DB 与 Redpanda 是两个独立系统,无法跨系统原子提交。

我们必须换思路:不追求"一次成功",而要"失败可重试 + 重试不重复"


二、我们的方案:本地事务表 + Redpanda 幂等 + 消费幂等

步骤 1:建"消息事务表"(与业务同库)
sql 复制代码
CREATE TABLE `outbox_event` (
  `id` BIGINT AUTO_INCREMENT,
  `event_id` VARCHAR(64) NOT NULL COMMENT '唯一事件ID',
  `topic` VARCHAR(128) NOT NULL,
  `payload` JSON NOT NULL,
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待发送, 1=已发送',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_event_id` (`event_id`)
);
步骤 2:主流程:写 DB + 写消息表(同一个事务)
java 复制代码
@Service
public class GiftService {

    @Transactional
    public void sendGift(GiftRequest req) {
        // 1. 扣款(Redis Lua 保证原子性)
        if (!diamondService.tryDeduct(...)) throw ...;
        
        // 2. 写打赏记录
        giftRecordMapper.insert(req);
        
        // 3. 写消息到本地事务表(同事务!)
        outboxEventMapper.insert(
            new OutboxEvent(UUID.randomUUID().toString(), 
                           "gift-sent", 
                           toJson(new GiftEvent(req)))
        );
    }
}

关键消息与业务数据在同一个 DB 事务中,要么全成功,要么全失败。


三、消息积压在 DB,如何"恢复后自动续发"?

答案:一个独立的定时扫表任务(Polling-based Outbox Pattern)。

核心逻辑:
java 复制代码
@Component
public class OutboxEventPublisher {

    @Scheduled(fixedDelay = 1000) // 每秒扫描
    public void pollAndPublish() {
        List<OutboxEvent> events = outboxEventMapper.findPending(100);
        for (OutboxEvent event : events) {
            try {
                // 发送到 Redpanda(幂等)
                redpandaTemplate.send(event.getTopic(), event.getPayload()).get(2, SECONDS);
                // 标记为已发送
                outboxEventMapper.markAsSent(event.getId());
            } catch (Exception e) {
                log.warn("发送失败,下次重试", e); // 失败则下次继续
            }
        }
    }
}
场景:Redpanda 宕机 2 小时
  • 期间 :所有打赏仍正常处理,消息安全落在 outbox_event
  • 恢复后:扫表任务自动查出积压消息,逐条重试发送
  • 全程无需人工干预,系统自愈

这就是"消息积压在 DB,恢复后自动续发"的实现本质


四、三重保险:确保"不丢不重"

风险 防御措施
消息丢失 本地事务表 + 扫表重试,保证"至少发一次"
消息重复 Redpanda 幂等生产者 + 消费端幂等,保证"最多处理一次"
消费异常 DLQ + 人工补偿 + 监控告警
1. Redpanda 幂等生产者(防重复发送)
yaml 复制代码
spring:
  kafka:
    producer:
      enable-idempotence: true   # 幂等
      acks: all                  # 强一致
      retries: 2147483647        # 无限重试(安全)
2. 消费端幂等(最后一道防线)
java 复制代码
@KafkaListener(topics = "gift-sent")
public void handleGiftEvent(GiftEvent event) {
    if (processedEventService.exists(event.getEventId())) return; // 已处理则跳过
    // 执行业务
    notificationService.push(...);
    processedEventService.record(event.getEventId()); // 标记
}

五、性能与可靠性优化

问题 优化方案
扫表慢 status + created_at 联合索引
空轮询多 无积压时动态延长扫描间隔(1s → 5s)
主库压力 批量发送 + 独立线程池
积压告警 监控 status=0 的 count,超阈值告警

📌 我们的选择不引入 CDC(如 Debezium),用扫表保持架构简单,单机支撑 5K TPS。


强一致性实现口诀(羅蘭实战总结)

同库事务写消息,
扫表异步保可达;
Redpanda 幂等防重发,
消费幂等守底线;
不求一次就成功,
但求重试不翻车。


结语:可靠,往往藏在"笨办法"里

在追求"流式""实时""事件驱动"的今天,一个简单的定时扫表任务,反而成了我们最信赖的"消息守门人"

它不酷,但稳;

它不新,但可靠。

高并发系统的韧性,常常不在前沿技术里,而在这些"老派但有效"的设计中。

关注我,从零开始构建基础IT设施

相关推荐
二哈赛车手6 小时前
新人笔记---ApiFox的一些常见使用出错
java·笔记·spring
栗子~~7 小时前
JAVA - 二层缓存设计(本地缓冲+redis缓冲+广播所有本地缓冲失效) demo
java·redis·缓存
YDS8297 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— RAG知识库的搭建和接口实现
java·ai·springboot·agent·rag·deepseek
星星也在雾里7 小时前
PgBouncer 解决 PostgreSQL 连接数超限 + 可视化监控
数据库·postgresql
未若君雅裁8 小时前
MyBatis 一级缓存、二级缓存与清理机制
java·缓存·mybatis
AI人工智能+电脑小能手9 小时前
【大白话说Java面试题 第65题】【JVM篇】第25题:谈谈对 OOM 的认识
java·开发语言·jvm
阿维的博客日记9 小时前
Nacos 为什么能让配置动态生效?(涉及 @RefreshScope 注解)
java·spring
雨辰AI9 小时前
SpringBoot3 + 人大金仓读写分离 + 分库分表 + 集群高可用 全栈实战
java·数据库·mysql·政务
长城20249 小时前
关于MySql的ONLY_FULL_GROUP_BY问题
数据库·mysql·聚合列
常常有10 小时前
MySQL 底层执行原理:输入SQL语句到两阶段提交
数据库·sql·mysql