分布式事务解决方案:最佳实践(零基础入门)

异步回调迷宫
2025-12-18 05:23
阅读 2016

大家好,我是你们的老朋友,一名在大厂干了三年后端开发的工程师,平时也在 B 站做技术分享。最近收到不少粉丝私信,问:“分布式事务到底该怎么学?是不是只有架构师才用得上?”

其实不是!随着微服务架构的普及,哪怕你只是写一个简单的订单-库存系统,也可能遇到“钱扣了,货没减”的问题——这就是典型的分布式事务一致性问题。

我当初刚开始接触微服务时,也是一脸懵:本地事务明明好好的,怎么一拆服务就出问题?踩过不少坑之后,我才明白:分布式事务不是高深莫测的理论,而是一套可落地、可调试、有工具支持的工程实践。

所以今天这篇教程,我就用最通俗的语言、最贴近实战的方式,手把手带你从零理解并实现一个基于 Spring Boot 的分布式事务方案。无论你是刚学完 Spring Boot 的小白,还是正在转型微服务的开发者,都能看懂、能跑通、能用到项目里!


一、什么是分布式事务?为什么需要它?

1.1 本地事务 vs 分布式事务

在单体应用中,我们操作数据库通常这样写:

@Transactional
public void transfer() {
    accountDao.decreaseBalance("A", 100);
    accountDao.increaseBalance("B", 100);
}

只要加上 @Transactional,这两条 SQL 要么都成功,要么都回滚——这叫本地事务,由数据库自己保证 ACID。

但如果你把账户服务拆成两个微服务:

  • order-service:创建订单并扣款
  • inventory-service:减少库存

这时候,两个操作分别发生在不同的数据库甚至不同的服务器上。数据库无法跨服务协调事务,于是可能出现:

  • 钱扣了 ✅
  • 库存没减 ❌
    或者反过来。

这就叫分布式事务问题:如何让多个独立的服务,在逻辑上像一个整体一样“全成功或全失败”。

💡 开发心得:很多初学者以为分布式事务是“高级功能”,其实它本质是业务正确性的基本要求。不要等到线上出问题才去补!


二、环境准备:搭建你的第一个微服务项目

我们要用 Spring Boot 快速搭建两个服务。推荐使用 start.spring.io 初始化项目。

所需依赖(两个服务都要加)

依赖项 用途
Spring Web 提供 REST API
Spring Data JPA 操作数据库
H2 Database 内存数据库(方便演示)
Lombok 减少样板代码

📌 注意:生产环境请用 MySQL/PostgreSQL,这里用 H H2 是为了免配置。

项目结构

distributed-tx-demo/
├── order-service/     # 订单服务
└── inventory-service/ # 库存服务

启动配置(application.yml)

order-service(端口 8081):

server:
  port: 8081
spring:
  datasource:
    url: jdbc:h2:mem:orderdb
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

inventory-service(端口 8082):

server:
  port: 8082
spring:
  datasource:
    url: jdbc:h2:mem:inventorydb
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

启动两个服务,确保能独立运行。


三、核心概念:主流分布式事务方案对比

目前业界主流方案有三种,我们用表格对比一下:

方案 原理 优点 缺点 适用场景
两阶段提交 (2PC) 协调者统一提交/回滚 强一致性 性能差、阻塞风险 传统金融系统
TCC (Try-Confirm-Cancel) 业务层实现补偿逻辑 灵活、性能好 开发复杂度高 高并发电商
Saga 模式 事件驱动 + 补偿事务 易理解、适合长流程 最终一致性 订单履约、物流

🚨 新手注意:不要一上来就追求强一致性!大多数互联网场景用“最终一致性”就够了。

我们今天重点讲 Saga 模式 + 本地消息表,因为它:

  • 实现简单
  • 无需引入额外中间件(如 Seata)
  • 完全基于 Spring Boot 生态

四、实战:用“本地消息表”实现 Saga 模式

4.1 设计思路

我们在 order-service 中:

  1. 创建订单
  2. 同时写一条“待发送”的消息到本地消息表
  3. 异步发送消息给 inventory-service
  4. 如果库存扣减成功,标记消息为“已发送”
  5. 如果失败,定时重试(直到成功或人工干预)

💡 这就是典型的 “可靠消息最终一致性” 模式。

4.2 数据库表设计

order-service 的数据库

-- 订单表
CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id VARCHAR(50),
    product_id VARCHAR(50),
    amount DECIMAL(10,2)
);

-- 本地消息表
CREATE TABLE message_outbox (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(100),   -- 如 "DECREASE_INVENTORY"
    payload TEXT,              -- JSON 格式的事件数据
    status VARCHAR(20),        -- PENDING / SENT
    created_at TIMESTAMP,
    sent_at TIMESTAMP
);

4.3 关键代码实现

步骤1:创建订单 + 写消息(同一个本地事务)

@Service
@Transactional
public class OrderService {

    @Autowired
    private OrderRepository orderRepo;

    @Autowired
    private MessageOutboxRepository messageRepo;

    public void createOrder(String userId, String productId, BigDecimal amount) {
        // 1. 保存订单
        Order order = new Order(userId, productId, amount);
        orderRepo.save(order);

        // 2. 写入本地消息表(同事务!)
        String payload = "{\"orderId\":\"" + order.getId() + 
                         "\",\"productId\":\"" + productId + "\"}";
        MessageOutbox message = new MessageOutbox("DECREASE_INVENTORY", payload, "PENDING");
        messageRepo.save(message);
    }
}

✅ 关键点:订单和消息写入同一个数据库事务,保证原子性!

步骤2:异步发送消息(定时任务轮询)

@Component
public class MessageSender {

    @Autowired
    private MessageOutboxRepository messageRepo;

    @Autowired
    private RestTemplate restTemplate; // 用于调用 inventory-service

    @Scheduled(fixedDelay = 5000) // 每5秒扫描一次
    public void sendPendingMessages() {
        List<MessageOutbox> pendingMessages = messageRepo.findByStatus("PENDING");
        for (MessageOutbox msg : pendingMessages) {
            try {
                // 调用库存服务
                restTemplate.postForObject(
                    "http://localhost:8082/inventory/decrease",
                    new InventoryRequest(msg.getPayload()),
                    String.class
                );
                // 标记为已发送
                msg.setStatus("SENT");
                msg.setSentAt(LocalDateTime.now());
                messageRepo.save(msg);
            } catch (Exception e) {
                // 失败不处理,下次继续重试
                log.warn("Failed to send message {}, will retry...", msg.getId());
            }
        }
    }
}

记得在主类加 @EnableScheduling 开启定时任务。

步骤3:库存服务接口

@RestController
public class InventoryController {

    @PostMapping("/inventory/decrease")
    public ResponseEntity<String> decreaseInventory(@RequestBody InventoryRequest request) {
        // 解析 payload,扣减库存
        // (简化逻辑,实际应加锁、校验库存等)
        inventoryService.decrease(request.getProductId(), 1);
        return ResponseEntity.ok("success");
    }
}

4.4 测试流程

  1. 调用 POST http://localhost:8081/order/create
  2. 查看 order-service 数据库:订单 + 消息记录都存在
  3. 5秒内,inventory-service 收到请求,库存减少
  4. 消息状态变为 SENT

🔁 如果库存服务宕机?没关系!定时任务会不断重试,直到恢复。


五、常见问题 & 避坑指南

Q1:消息重复消费怎么办?

:在 inventory-service 中实现幂等性。例如:

// 库存服务加一张“已处理事件”表
CREATE TABLE processed_events (event_id VARCHAR(100) PRIMARY KEY);

// 处理前先查是否处理过
if (processedEventRepo.existsById(eventId)) {
    return; // 直接返回,不重复处理
}
// 否则正常处理,并记录 event_id

💡 开发心得:所有分布式系统都要考虑幂等!这是基本素养。


Q2:定时轮询效率低,有没有更实时的方式?

:可以用 Spring 的 @TransactionalEventListener

@Transactional
public void createOrder(...) {
    // ... 保存订单和消息
    applicationEventPublisher.publishEvent(new OrderCreatedEvent(orderId));
}

@TransactionalEventListener
public void onOrderCreated(OrderCreatedEvent event) {
    // 在事务提交后立即发送消息
    sendMessage(event);
}

这样避免了轮询,但要注意:如果发送失败,仍需兜底重试机制。


Q3:消息表会不会越来越大?

:会!建议:

  • 定期归档已发送的消息(如7天前的)
  • 或使用 TTL(Time-To-Live)自动清理(H2 不支持,MySQL 可用)

Q4:为什么不用 RocketMQ/Kafka 的事务消息?

:很好!但对新手来说:

  • 需要部署消息队列
  • 学习成本高
  • 本地消息表方案零外部依赖,更适合入门

等你熟悉后再升级到 MQ 事务消息也不迟。


六、学习建议:下一步怎么走?

恭喜你已经掌握了分布式事务的核心思想!但别止步于此,我建议你:

  1. 动手改造:把上面的例子改成用 RabbitMQ/Kafka 实现,体验事务消息。
  2. 深入原理:读一读 Seata 的 AT 模式源码,理解全局锁、undo log。
  3. 扩展场景:尝试实现 TCC 模式(比如 Try 阶段冻结金额,Confirm 扣款,Cancel 解冻)。
  4. 压测验证:用 JMeter 模拟高并发下单,观察一致性表现。

📌 最后一句真心话:分布式事务没有银弹。选择方案要看业务容忍度——支付系统要强一致,点赞系统最终一致就行。别为了“技术酷”而过度设计!


结语

写这篇教程,是因为我见过太多新人被“分布式事务”这个词吓住,其实它不过是一系列工程技巧的组合。只要你理解“本地事务保原子,异步消息保最终一致”,就已经超越了80%的开发者。

希望这篇从零开始的实践指南,能帮你迈出微服务架构的第一步。如果你觉得有用,欢迎去 B 站搜我的账号(ID:后端老张),我会持续更新 Spring Cloud、DDD、性能优化等实战内容。

有问题评论区见,咱们下期再见!

评论 0

最热最新
暂无评论
异步回调迷宫Lv.1
0
影响力
0
文章
0
粉丝