分布式事务解决方案:从线上事故到最佳实践的血泪总结

代码温度计
2025-12-17 23:48
阅读 261

作者:小红书推荐算法工程师(Vim党,Java重度用户,正在偷偷投简历)

上周五晚上十点半,我坐在工位上盯着监控面板上那条突兀的红色曲线——订单状态不一致的异常量在短短十分钟内暴涨了300%。心里一凉,手里的冰美式差点洒键盘上。这已经是本月第三次因为分布式事务没处理好导致的数据不一致问题了。

说实话,作为在小红书干了三年多的“老油条”,我本以为自己对这类问题已经见怪不怪了。但这次不一样,因为产品经理小王刚刚在群里@我说:“这个bug影响了双11大促的核销链路,老板很生气。”当时我真的想砸电脑——毕竟谁不想周末好好躺平呢?

不过抱怨归抱怨,问题还是要解决。趁着周末两天时间,我把整个分布式事务的方案重新梳理了一遍,也顺便整理了这篇血泪总结。希望能帮到同样在分布式系统泥潭里挣扎的兄弟们。

为什么分布式事务这么难搞?

先说说我司的业务场景。我们做的是推荐系统的用户增长模块,涉及到用户行为记录、积分发放、优惠券核销等多个微服务。典型的场景就是:用户完成某个任务 → 记录行为日志 → 发放积分 → 发放优惠券。

看起来很简单对吧?但在分布式环境下,任何一个环节失败都会导致数据不一致。比如:

  • 行为日志写入成功,但积分服务挂了
  • 积分发放成功,但优惠券服务超时
  • 网络抖动导致重复请求

最要命的是,这些服务都跑在不同的数据库实例上,传统的数据库事务根本hold不住。

记得去年双11期间,我们就因为这个问题导致几万个用户的积分没有正常发放。运维同事半夜打电话给我,声音都在颤抖:“兄弟,用户投诉电话都要被打爆了!”

方案选型:从2PC到Saga的踩坑之旅

刚开始遇到这个问题时,我和团队第一反应就是用两阶段提交(2PC)。毕竟教科书上都是这么写的嘛!

// 当时天真的代码示例
@Transactional
public void completeTask(UserTask task) {
    behaviorService.logBehavior(task);     // 服务A
    pointService.awardPoints(task);       // 服务B  
    couponService.issueCoupon(task);      // 服务C
}

结果可想而知,性能直接炸裂。2PC的同步阻塞特性让整个链路的响应时间从200ms飙到了2s+,用户体验直接崩盘。产品经理看监控数据的时候脸都绿了。

后来我们尝试了**TCC(Try-Confirm-Cancel)**模式。这个方案理论上很美好:先预留资源,再确认或取消。

// TCC接口定义
public interface PointTccService {
    @TwoPhaseBusinessAction(name = "awardPoints", commitMethod = "confirmAward", rollbackMethod = "cancelAward")
    public boolean tryAwardPoints(@BusinessActionContextParameter(paramName = "userId") String userId, 
                                 @BusinessActionContextParameter(paramName = "points") int points);
    
    public boolean confirmAward(BusinessActionContext context);
    public boolean cancelAward(BusinessActionContext context);
}

但实际落地的时候发现,TCC对业务侵入性太强了。每个业务逻辑都要拆分成try/confirm/cancel三个方法,代码复杂度直线上升。更惨的是,我们的测试同学抱怨说:“你们这个TCC逻辑太复杂了,我都不知道怎么写测试用例!”

最后我们转向了Saga模式,这也是目前我们生产环境的主力方案。

Saga模式:最终一致性的救星

Saga的核心思想很简单:把一个长事务拆分成多个本地事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,就按相反顺序执行补偿操作。

在我们的场景中,Saga流程大概是这样的:

  1. 记录行为日志(本地事务)
  2. 发放积分(本地事务)
  3. 发放优惠券(本地事务)

如果第3步失败,就先撤销积分,再删除行为日志。

基于事件驱动的Saga实现

我们最终采用了事件驱动的方式来实现Saga。主要原因是:

  • 解耦各个服务之间的直接调用
  • 更容易实现异步处理
  • 便于监控和重试

具体的架构设计如下:

// Saga协调器
@Component
public class TaskCompletionSagaOrchestrator {
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void startSaga(UserTask task) {
        // 发布Saga开始事件
        eventPublisher.publishEvent(new SagaStartedEvent(task));
        
        // 触发第一个步骤
        eventPublisher.publishEvent(new LogBehaviorRequestedEvent(task));
    }
    
    @EventListener
    public void handleLogBehaviorCompleted(LogBehaviorCompletedEvent event) {
        if (event.isSuccess()) {
            // 继续下一步
            eventPublisher.publishEvent(new AwardPointsRequestedEvent(event.getTask()));
        } else {
            // 失败,开始补偿
            startCompensation(event.getTask(), CompensationStep.NONE);
        }
    }
    
    @EventListener  
    public void handleAwardPointsCompleted(AwardPointsCompletedEvent event) {
        if (event.isSuccess()) {
            eventPublisher.publishEvent(new IssueCouponRequestedEvent(event.getTask()));
        } else {
            startCompensation(event.getTask(), CompensationStep.LOG_BEHAVIOR);
        }
    }
    
    // ... 其他事件处理器
}

每个服务只需要监听自己关心的事件,处理完成后发布下一个事件或者补偿事件。这样整个流程就变成了一个状态机。

消息队列的选择

说到事件驱动,肯定离不开消息队列。我们在RocketMQ和Kafka之间纠结了很久,最终选择了RocketMQ,主要原因:

  1. 事务消息支持:RocketMQ的事务消息机制完美契合我们的需求
  2. 延迟消息:补偿操作经常需要延迟执行,RocketMQ原生支持
  3. 消费重试:内置的重试机制比我们自己实现要稳定得多
// RocketMQ事务消息示例
@Transactional
public void sendLogBehaviorMessage(UserTask task) {
    Message message = new Message("TASK_SAGA_TOPIC", 
        "LOG_BEHAVIOR", 
        JSON.toJSONString(task).getBytes());
    
    TransactionSendResult sendResult = transactionMQProducer.sendMessageInTransaction(message, task);
    
    if (sendResult.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
        throw new RuntimeException("事务消息发送失败");
    }
}

生产环境的最佳实践

经过几次线上事故的洗礼,我们总结出了一些分布式事务的最佳实践。

1. 幂等性是生命线

这是我被教育得最惨的一点。由于网络问题或者重试机制,同一个请求可能会被处理多次。如果不做幂等处理,用户的积分可能被发10次!

我们的解决方案是在每个关键操作前先检查是否已经执行过:

@Service
public class PointServiceImpl implements PointService {
    
    @Autowired
    private PointRecordRepository pointRecordRepository;
    
    @Transactional
    public boolean awardPoints(String userId, int points, String businessId) {
        // 检查是否已经处理过
        if (pointRecordRepository.existsByBusinessId(businessId)) {
            log.info("业务ID {} 已经处理过,跳过", businessId);
            return true;
        }
        
        // 执行业务逻辑
        updateUserPoints(userId, points);
        
        // 记录处理记录
        PointRecord record = new PointRecord();
        record.setBusinessId(businessId);
        record.setUserId(userId);
        record.setPoints(points);
        pointRecordRepository.save(record);
        
        return true;
    }
}

2. 补偿操作要谨慎设计

补偿操作本身也可能失败!所以我们给补偿操作加上了最大重试次数人工干预机制

// 补偿操作重试配置
@Component
public class CompensationRetryConfig {
    
    public static final int MAX_COMPENSATION_RETRY = 3;
    public static final long COMPENSATION_RETRY_DELAY = 5 * 60 * 1000; // 5分钟
    
    @Bean
    public RetryTemplate compensationRetryTemplate() {
        RetryTemplate template = new RetryTemplate();
        
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(COMPENSATION_RETRY_DELAY);
        template.setBackOffPolicy(backOffPolicy);
        
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(MAX_COMPENSATION_RETRY);
        template.setRetryPolicy(retryPolicy);
        
        return template;
    }
}

当补偿操作重试3次都失败后,会将任务推送到人工处理队列,运维同事可以通过管理后台手动处理。

3. 监控和告警不能少

现在我们的监控面板上有几个关键指标:

指标名称 阈值 告警方式
Saga执行失败率 > 0.1% 企业微信+短信
补偿操作触发次数 > 10次/分钟 企业微信
未完成Saga数量 > 100 企业微信+电话

这些监控帮我们提前发现了好几次潜在的问题。比如有一次数据库连接池配置有问题,导致补偿操作大量失败,但因为有及时告警,我们在影响用户之前就解决了问题。

开源方案对比

在选型过程中,我们也调研了不少开源方案。这里分享一下我们的对比结果:

方案 优点 缺点 适用场景
Seata 支持多种模式,文档完善 对Spring Cloud依赖较强 新项目,技术栈统一
ByteTCC 轻量级,侵入性小 社区活跃度一般 简单TCC场景
Eventuate Tram Saga模式成熟 学习成本高 复杂业务流程
自研方案 完全可控,贴合业务 开发维护成本高 有足够技术储备

我们最终选择自研,主要是因为业务场景比较特殊,而且团队有足够的时间和人力投入。但如果是一个新团队或者时间紧迫的项目,我会推荐直接用Seata

性能优化的那些事儿

分布式事务最大的痛点就是性能。经过几轮优化,我们的Saga执行时间从最初的800ms降到了现在的120ms。

主要优化点包括:

  1. 异步化:非关键路径的操作异步执行
  2. 批量处理:同一用户的多个操作合并处理
  3. 缓存预热:热点数据提前加载到缓存
  4. 数据库优化:合理的索引设计和SQL优化

特别是数据库层面,我们做了很多工作:

  • 为Saga状态表添加了复合索引 (user_id, status, created_time)
  • 使用读写分离减轻主库压力
  • 关键表定期归档历史数据
-- Saga状态表索引优化
CREATE INDEX idx_saga_user_status_time ON saga_execution (user_id, status, created_time);

写在最后

折腾了这么久,终于把分布式事务的问题基本解决了。上周上线后,数据一致性问题再也没有出现过,产品经理小王也在群里夸我们“给力”。

不过说实话,分布式事务这个话题真的没有银弹。每个方案都有自己的适用场景和trade-off。关键是要根据自己的业务特点来选择合适的方案。

现在我已经开始考虑换个环境了(懂的都懂)。这三年在小红书学到了很多,特别是在分布式系统设计方面。如果你也在为分布式事务头疼,或者对推荐算法感兴趣,欢迎私信交流!

对了,我们的一些核心代码已经在GitHub上开源了(公司允许的部分),地址就不在这里贴了,感兴趣的可以私信问我。毕竟作为一个Vim党,能在IDE里看到别人star自己的代码,还是挺开心的。

记住:在分布式世界里,一致性、可用性、性能,你最多只能同时拥有两个。 但通过合理的设计和取舍,我们总能找到那个最适合自己的平衡点。


本文纯属个人经验分享,如有雷同,那说明我们都踩过同样的坑。文中提到的技术方案已在小红书生产环境稳定运行6个月以上,QPS峰值达到5000+,数据一致性保证在99.99%以上。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝