分布式事务解决方案:最佳实践(零基础入门)
大家好,我是你们的老朋友,一名在大厂干了三年后端开发的工程师,平时也在 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 中:
- 创建订单
- 同时写一条“待发送”的消息到本地消息表
- 异步发送消息给
inventory-service - 如果库存扣减成功,标记消息为“已发送”
- 如果失败,定时重试(直到成功或人工干预)
💡 这就是典型的 “可靠消息最终一致性” 模式。
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 测试流程
- 调用
POST http://localhost:8081/order/create - 查看
order-service数据库:订单 + 消息记录都存在 - 5秒内,
inventory-service收到请求,库存减少 - 消息状态变为
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 事务消息也不迟。
六、学习建议:下一步怎么走?
恭喜你已经掌握了分布式事务的核心思想!但别止步于此,我建议你:
- 动手改造:把上面的例子改成用 RabbitMQ/Kafka 实现,体验事务消息。
- 深入原理:读一读 Seata 的 AT 模式源码,理解全局锁、undo log。
- 扩展场景:尝试实现 TCC 模式(比如 Try 阶段冻结金额,Confirm 扣款,Cancel 解冻)。
- 压测验证:用 JMeter 模拟高并发下单,观察一致性表现。
📌 最后一句真心话:分布式事务没有银弹。选择方案要看业务容忍度——支付系统要强一致,点赞系统最终一致就行。别为了“技术酷”而过度设计!
结语
写这篇教程,是因为我见过太多新人被“分布式事务”这个词吓住,其实它不过是一系列工程技巧的组合。只要你理解“本地事务保原子,异步消息保最终一致”,就已经超越了80%的开发者。
希望这篇从零开始的实践指南,能帮你迈出微服务架构的第一步。如果你觉得有用,欢迎去 B 站搜我的账号(ID:后端老张),我会持续更新 Spring Cloud、DDD、性能优化等实战内容。
有问题评论区见,咱们下期再见!

评论 0