关注我,从零开始构建基础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设施