分布式事务解决方案:最佳实践
上周五晚上十点半,我盯着屏幕上不断重试失败的订单状态,心里默默问候了产品经理祖宗十八代。这已经是本周第三次因为“超卖”问题被叫起来救火了。作为一个在当前公司摸爬滚打三年多的老油条,我其实早就想换个环境了——但在这之前,得先把这个烂摊子收拾干净。
说起来有点惭愧,虽然我 GitHub Copilot 付费用户的身份已经快两年了(别笑,这玩意儿真的能救命),平时写 CRUD 代码飞快,但一遇到分布式事务这种硬核问题,还是得老老实实翻文档、看源码。最近在研究 Rust,感觉内存安全和并发模型特别优雅,但现实是……我们后端还是 Java 的天下啊!
为什么分布式事务这么让人头大?
事情的起因很简单:我们的电商平台要做一个“下单即扣库存+创建订单+发优惠券”的业务流程。听起来很常规对吧?但在微服务架构下,这三个操作分别在三个不同的服务里,每个服务都有自己的数据库。
去年双11期间,我们就因为这个问题搞出了线上事故。用户下了单,库存扣了,但订单创建失败,优惠券没发出去。结果用户投诉说“钱付了但没收到货”,客服电话被打爆,CTO 在群里@所有人:“今晚不解决就别回家了”。
当时的临时方案就是人工对账 + 补偿脚本,但这种“人肉运维”的方式显然不可持续。作为经常参加技术分享会的老鸟,我深知这种问题必须用正规的分布式事务方案来解决。
踩坑历程:从两阶段提交到 Saga 模式
最开始,团队里有个新来的实习生建议用 XA 两阶段提交。我看着他天真的眼神,内心苦笑——这哥们还没被生产环境毒打过啊!
XA 协议理论上很完美,但实际上在高并发场景下性能堪忧。我们测试环境压测了一下,TPS 直接从 3000 降到了 300,数据库连接池直接被打满。而且一旦协调者挂了,整个事务就卡在那里,简直是运维噩梦。
// 别学这个!这只是展示 XA 的复杂性
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void createOrderWithXA(Order order) {
// 需要配置 XADataSource,每个数据库都要支持
// 还要考虑网络超时、资源锁定等问题
// 实际上我们很快就放弃了这个方案
}
后来我们尝试了基于消息队列的最终一致性方案。思路很简单:下单服务发送消息到 MQ,库存服务和优惠券服务消费消息进行处理。但如果消息发送失败怎么办?如果消费者处理失败怎么办?
这时候我想起了之前在某次技术分享会上听到的 TCC 模式(Try-Confirm-Cancel)。TCC 的核心思想是业务层面的两阶段提交:
- Try 阶段:预留资源(比如冻结库存)
- Confirm 阶段:确认执行(比如扣减冻结的库存)
- Cancel 阶段:取消预留(比如释放冻结的库存)
听起来很美好,但实现起来工作量巨大。每个业务操作都要写三套逻辑,而且要考虑各种异常情况。我们团队评估后觉得 ROI 太低,毕竟我们不是支付宝,没必要这么严谨。
最后我们选择了 Saga 模式,这是目前我们认为最适合我们业务场景的方案。
Saga 模式实战:用 Seata 实现
Saga 模式的核心思想是:将一个长事务拆分成多个本地事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,就依次执行前面步骤的补偿操作。
我们在项目中集成了 Seata(一款开源的分布式事务解决方案),配置相对简单,对业务代码侵入性也比较小。
1. 环境配置
首先在 pom.xml 中添加依赖:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
然后配置 application.yml:
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
2. 业务代码实现
关键是在主方法上加上 @GlobalTransactional 注解:
@Service
@Slf4j
public class OrderService {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private CouponClient couponClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createOrder(OrderRequest request) {
try {
// 1. 创建订单(本地事务)
Order order = orderRepository.save(buildOrder(request));
log.info("订单创建成功: {}", order.getId());
// 2. 扣减库存(远程调用,需要支持分布式事务)
boolean inventoryResult = inventoryClient.decreaseInventory(
request.getProductId(), request.getQuantity()
);
if (!inventoryResult) {
throw new RuntimeException("库存不足");
}
log.info("库存扣减成功");
// 3. 发放优惠券(远程调用)
boolean couponResult = couponClient.issueCoupon(request.getUserId());
if (!couponResult) {
log.warn("优惠券发放失败,但不影响主流程");
// 这里可以选择继续还是回滚,根据业务需求
}
return order;
} catch (Exception e) {
log.error("创建订单失败: {}", e.getMessage(), e);
throw e; // 触发全局回滚
}
}
}
每个参与方服务也需要配置 Seata,并实现对应的接口。以库存服务为例:
@RestController
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@PostMapping("/inventory/decrease")
public boolean decreaseInventory(@RequestBody InventoryRequest request) {
// 这个方法会被 Seata 代理,自动参与全局事务
return inventoryService.decrease(request.getProductId(), request.getQuantity());
}
}
3. 补偿机制设计
Saga 模式的关键在于补偿操作的设计。我们需要确保:
- 补偿操作是幂等的(多次执行结果一致)
- 补偿操作能够真正回滚业务状态
- 补偿操作本身也要考虑失败的情况
比如库存服务的补偿操作:
// 库存服务中的补偿逻辑
public void compensateDecreaseInventory(String productId, Integer quantity) {
// 幂等性检查:避免重复补偿
if (alreadyCompensated(productId, getCurrentGlobalTxId())) {
return;
}
try {
// 增加库存
inventoryRepository.increase(productId, quantity);
markAsCompensated(productId, getCurrentGlobalTxId());
} catch (Exception e) {
log.error("补偿库存失败", e);
// 这里可能需要人工介入或者重试机制
throw e;
}
}
生产环境经验总结
经过几个月的线上运行,我们总结了一些关键经验:
性能对比
| 方案 | TPS | 事务成功率 | 开发复杂度 | 运维复杂度 |
|---|---|---|---|---|
| XA 两阶段提交 | 300 | 99.9% | 低 | 高 |
| 消息队列最终一致性 | 2500 | 98.5% | 中 | 中 |
| TCC 模式 | 2000 | 99.99% | 高 | 高 |
| Saga (Seata) | 2200 | 99.8% | 中 | 中 |
安全意识不能忘
在实现分布式事务时,安全问题往往被忽视。我们特别注意了以下几点:
- 数据一致性验证:定期跑对账脚本,确保各服务间数据一致
- 权限控制:Seata 的注册中心和配置中心都要做访问控制
- 日志审计:所有事务操作都要记录详细日知,便于追踪问题
- 网络隔离:Seata Server 和业务服务之间使用内网通信
// 对账脚本示例(每天凌晨执行)
@Component
public class ReconciliationJob {
@Scheduled(cron = "0 0 2 * * ?")
public void reconcileOrdersAndInventory() {
List<Order> orders = orderRepository.findCompletedOrdersYesterday();
for (Order order : orders) {
Integer actualInventory = inventoryClient.getActualInventory(order.getProductId());
Integer expectedInventory = calculateExpectedInventory(order.getProductId());
if (!actualInventory.equals(expectedInventory)) {
log.error("库存不一致! productId: {}, actual: {}, expected: {}",
order.getProductId(), actualInventory, expectedInventory);
// 发送告警通知
alertService.sendAlert("库存数据不一致");
}
}
}
}
团队协作心得
实施分布式事务最大的挑战其实不是技术,而是团队协作。每个微服务团队都需要理解分布式事务的原理,按照约定的方式开发。我们建立了以下规范:
- 所有参与分布式事务的方法必须有明确的入参和出参
- 异常处理要统一,不能吞掉异常
- 接口文档要及时更新
- 压测时要模拟网络延迟和超时场景
写在最后
现在回想起来,那个周五晚上的加班虽然痛苦,但也促使我们真正解决了这个技术债。系统上线三个月来,再也没有出现过超卖问题,用户投诉率下降了 80%。
作为一个准备跳槽的程序员,这段经历让我深刻认识到:真正的工程能力不是会多少框架,而是在复杂约束下找到平衡点的能力。既要考虑技术的先进性,也要考虑团队的接受度;既要保证系统的可靠性,也要控制开发成本。
下周我要去参加一个 Java 技术分享会,准备把这次的经验分享给大家。说不定还能遇到合适的下家呢(笑)。
如果你也在为分布式事务头疼,不妨试试 Saga 模式。当然,具体方案还是要根据你的业务场景来选择。记住,没有最好的方案,只有最适合的方案。
最后,感谢我的 GitHub Copilot,虽然它不能帮我解决分布式事务的问题,但至少让我写 CRUD 代码的时候少敲了很多键盘,保护了我的腱鞘 😅

评论 0