You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中实现数据库更新与Kafka消息推送的原子性事务方案

数据库更新与Kafka消息推送的分布式事务解决方案

针对数据库更新完成后、Kafka消息推送前服务器崩溃导致的数据不一致问题,最优落地方案是本地消息表+定时补偿,这是业内成熟的分布式事务一致性方案,无需引入复杂中间件,易实现且稳定性高。

核心思路

借助数据库事务保证业务数据更新与消息记录的原子性,再通过定时任务补偿未成功发送的消息,最终实现“要么两者都成功,要么通过重试/回滚保证一致性”。

具体实现步骤

1. 创建本地消息表

在业务数据库中新增一张与业务表同库的消息记录表,字段示例:

CREATE TABLE `business_message` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
  `business_id` VARCHAR(64) NOT NULL COMMENT '关联业务ID',
  `message_content` TEXT NOT NULL COMMENT 'Kafka消息内容',
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待发送,1-已发送,2-发送失败',
  `retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
);

2. 修改业务逻辑

将数据库更新与消息记录插入放在同一个数据库事务中,确保两者原子性:

@Transactional(rollbackFor = Exception.class)
public void handleBusiness() {
    // 1. 执行数据库记录更新
    updateRecord();
    
    // 2. 插入待发送的Kafka消息到本地消息表
    BusinessMessage message = new BusinessMessage();
    message.setBusinessId("xxx"); // 关联当前业务唯一标识
    message.setMessageContent("kafka message content");
    message.setStatus(0); // 标记为待发送
    businessMessageMapper.insert(message);
    
    // 3. 尝试推送Kafka消息
    try {
        pushMessageToKafka(message.getMessageContent());
        // 推送成功,更新消息状态为已发送
        message.setStatus(1);
        businessMessageMapper.updateById(message);
    } catch (Exception e) {
        // 推送失败,不更新状态,留待定时任务补偿
        log.error("推送Kafka消息失败,业务ID:{}", message.getBusinessId(), e);
    }
}

3. 定时补偿任务

编写定时任务,定期扫描本地消息表中待发送或重试中的消息,重新推送:

@Component
public class MessageCompensateTask {
    @Autowired
    private BusinessMessageMapper businessMessageMapper;
    @Autowired
    private KafkaProducer kafkaProducer;
    
    // 每5分钟执行一次,可根据业务调整间隔
    @Scheduled(cron = "0 */5 * * * ?")
    public void compensateUnsentMessages() {
        // 查询状态为待发送、且创建时间超过1分钟的消息(避免刚插入就重试)
        List<BusinessMessage> messages = businessMessageMapper.selectUnsentMessages();
        
        for (BusinessMessage message : messages) {
            if (message.getRetryCount() >= 3) { // 最大重试次数,可配置
                // 超过重试次数,标记为发送失败并触发告警
                message.setStatus(2);
                businessMessageMapper.updateById(message);
                log.error("消息重试次数耗尽,业务ID:{}", message.getBusinessId());
                // 此处可添加告警逻辑,如通知运维人员
                continue;
            }
            
            try {
                kafkaProducer.pushMessageToKafka(message.getMessageContent());
                // 推送成功,更新状态和重试次数
                message.setStatus(1);
                message.setRetryCount(message.getRetryCount() + 1);
                businessMessageMapper.updateById(message);
            } catch (Exception e) {
                // 推送失败,仅更新重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                businessMessageMapper.updateById(message);
                log.error("补偿推送Kafka消息失败,业务ID:{},重试次数:{}", message.getBusinessId(), message.getRetryCount(), e);
            }
        }
    }
}

4. 处理Pod崩溃场景

  • 如果Pod在数据库更新和消息插入成功后、推送消息前崩溃:重启后定时任务会扫描到这条待发送的消息,自动重试推送,保证消息最终发送成功。
  • 如果Pod在推送消息成功后、未更新消息状态就崩溃:重启后定时任务会再次推送,此时需要消费者端实现幂等性(比如通过业务ID或消息ID去重),避免重复消费导致业务异常。

注意事项

  • 本地消息表必须与业务表在同一个数据库,才能利用数据库事务保证原子性。
  • 消费者端必须实现幂等性,这是分布式场景下的必备要求。
  • 定时任务的执行间隔、最大重试次数需根据业务场景调整,避免对系统造成过大压力。

内容的提问来源于stack exchange,提问作者Aki T

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 00:07:12