拒绝过度设计:Spring Boot 3.4.5 下 MySQL 分布式事务与高并发优化的选型博弈
上周收到一个紧急的技术复盘需求,某电商核心交易链路在双11预热期间出现严重的数据库连接池耗尽问题。表象是订单创建接口响应时间从 200ms 飙升至 8s,底层日志显示大量Connection pool exhausted异常,且伴随部分订单状态不一致。
团队内部针对“如何用 MySQL 支撑高并发下的分布式一致性”产生了激烈分歧。有人主张引入 Seata 全链路治理,有人坚持优化现有 SQL,还有人提议用 Redis 完全绕过 MySQL 写路径。作为架构负责人,我需要在 48 小时内给出一个兼顾稳定性、可维护性与性能的最优解,而不是盲目追逐最新技术栈。
一、需求分析:痛点在于“一致性”与“吞吐量”的零和博弈
我们的业务场景具有鲜明特征:高并发读写、强一致性要求、事务边界跨越多个微服务。具体指标如下:
- 核心功能:订单创建需保证库存扣减、资金冻结、订单入库三件事要么全成功,要么全回滚;支付回调需幂等处理。
- 非功能需求:P99 延迟 < 500ms,QPS 峰值达到 5000+,MySQL 连接数不超过 200,CPU 使用率 < 70%。
这个需求的核心矛盾在于:MySQL 本身是单进程模型,虽然支持 InnoDB 行级锁和 MVCC,但在分布式环境下,跨服务的事务协调必然带来额外的网络往返和锁等待。如果完全依赖 MySQL 原生事务,长事务会拖垮连接池;如果引入重型分布式事务框架,又会引入新的复杂度和性能损耗。
二、方案对比:三种主流路径的优劣权衡
面对上述挑战,我们评估了三种技术选型方案:
| 方案 | 技术栈 | 优点 | 缺点 | 适用场景 |
|------|--------|------|------|----------|
| A: 本地事务 + 最终一致性 | Spring Transaction + RocketMQ 事务消息 | 简单可靠,无额外中间件依赖,性能最优 | 存在短暂不一致窗口,需复杂补偿机制 | 对实时一致性要求不高的场景 |
| B: 轻量级分布式事务 | Seata AT 模式 | 对业务代码侵入小,透明化事务,开发效率高 | 全局锁性能损耗大,高并发下容易成为瓶颈,运维复杂 | 中低并发、对一致性要求较高的内部系统 |
| C: 柔性事务(TCC/基于重试) | 自研状态机 + 本地消息表 + 定时补偿 | 性能可控,无全局锁,最终一致性有保障 | 开发成本高,需改造业务代码为 Try/Confirm/Cancel | 高并发、高性能要求的交易核心链路 |
为什么我们不选 Seata AT?
虽然 Seata 1.7.x 版本在性能上有所优化,但在我们 5000 QPS 的压测环境下,全局锁的等待时间导致 P99 延迟经常突破 1s。官方文档推荐 Seata 用于“中低并发”场景,这与我们的业务现实不符。尽管它是业界热门选择,但在高并发 MySQL 优化场景下,反而会成为系统的瓶颈。
为什么我们不选纯本地事务?
电商订单场景要求“钱货两清”,用户侧不能接受订单显示“支付成功”但实际未扣减库存的情况。虽然本地消息表方案能实现最终一致性,但用户感知的延迟和错误恢复体验较差,不符合我们的产品标准。
最终选型:C 方案——柔性事务架构
我们选择了基于本地消息表 + 状态机重试 + 幂等性设计的柔性事务方案。核心思路是:将分布式事务拆分为本地事务 + 异步消息,通过状态流转保证最终一致性。这套方案在 2026 年的分布式系统架构演进中被证明是高性能场景下的优选,它避免了全局锁,同时保证了数据一致性。
三、核心实现:本地消息表与幂等性设计的协同
3.1 架构设计
整个流程分为三步:
- 下单阶段:在同一个本地事务中,写入订单记录 + 写入消息表(状态=PENDING)。
- 发送阶段:定时任务扫描 PENDING 状态的消息,发送到 RocketMQ,发送成功后更新消息表状态为 SENT。
- 执行阶段:下游服务消费消息,执行业务逻辑(扣库存、冻结资金),并返回处理结果。若失败,消息重投。
关键设计点在于幂等性。每个订单生成全局唯一 ID(基于雪花算法),所有写操作都基于该 ID 进行判断,防止重复执行。
3.2 关键代码实现
订单创建接口(Spring Boot 3.4.5):
```java
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageTableMapper messageMapper;
public Order createOrder(CreateOrderRequest request) {
// 1. 生成全局唯一订单ID
String orderId = IdGenerator.generateOrderId();
// 2. 写入订单记录
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
// 3. 写入消息表(本地事务保证原子性)
MessageTable message = new MessageTable();
message.setBizId(orderId);
message.setMessageType("ORDER_CREATED");
message.setPayload(JSON.toJSONString(request));
message.setStatus(MessageStatus.PENDING);
message.setCreateTime(new Date());
messageMapper.insert(message);
return order;
}
}
```
消息发送任务(基于 Quartz 调度):
```java
@Component
public class MessageSendTask {
@Autowired
private MessageTableMapper messageMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
// 每 5 秒扫描一次,发送 PENDING 状态的消息
@Scheduled(fixedDelay = 5000)
public void sendMessage() {
List pendingMessages = messageMapper.selectByStatus(MessageStatus.PENDING);
for (MessageTable message : pendingMessages) {
try {
// 发送消息到 RocketMQ
rocketMQTemplate.syncSend("order-topic",
MessageBuilder.withPayload(message.getPayload()).build());
// 更新消息状态为已发送
messageMapper.updateStatus(message.getId(), MessageStatus.SENT);
} catch (Exception e) {
// 发送失败,保持 PENDING 状态,下次重试
log.error("Send message failed, bizId: {}", message.getBizId(), e);
}
}
}
}
```
下游消费方(幂等性保证):
```java
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener {
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Override
public void onMessage(String payload) {
CreateOrderRequest request = JSON.parseObject(payload, CreateOrderRequest.class);
String orderId = request.getOrderId();
// 幂等性检查:通过订单状态判断是否已处理
Order order = orderMapper.selectByOrderId(orderId);
if (order == null || order.getStatus() == OrderStatus.DONE) {
log.warn("Order already processed, skip. orderId: {}", orderId);
return;
}
try {
// 1. 扣减库存
inventoryService.deductStock(request.getSkuId(), request.getQuantity());
// 2. 冻结资金
paymentService.freezeAmount(request.getUserId(), request.getAmount());
// 3. 更新订单状态
order.setStatus(OrderStatus.DONE);
orderMapper.updateById(order);
} catch (Exception e) {
// 业务失败,触发补偿或人工介入
log.error("Order processing failed, orderId: {}", orderId, e);
throw new RuntimeException(e);
}
}
}
```
3.3 索引与 SQL 优化
针对高并发场景,我们对消息表进行了专项优化:
```sql
-- 消息表索引设计
CREATE TABLEmessage_table(idbigint NOT NULL AUTO_INCREMENT,biz_idvarchar(64) NOT NULL,message_typevarchar(32) NOT NULL,payloadtext NOT NULL,statustinyint NOT NULL DEFAULT 0 COMMENT '0:PENDING 1:SENT 2:FAILED',create_timedatetime NOT NULL,update_timedatetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEYuk_biz_id(biz_id), -- 防止重复发送
KEYidx_status_create_time(status,create_time) -- 定时任务扫描优化
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
关键优化点:
- 联合索引:
(status, create_time)让定时任务扫描时走索引,避免全表扫描。 - 唯一约束:
biz_id唯一索引保证幂等性,从数据库层面防止重复处理。 - 状态机设计:消息状态流转为
PENDING → SENT → DONE,避免状态混乱。
四、效果复盘:性能数据与上线观察
该系统上线后,我们进行了为期一周的灰度观察和压力测试,关键指标如下:
- QPS 提升:订单创建接口 QPS 从 1200 提升至 4800,接近目标值的 96%。
- 延迟改善:P99 延迟从 8s 降至 320ms,远低于 500ms 的目标。
- 连接池稳定:MySQL 活跃连接数稳定在 80-120 之间,未出现连接池耗尽告警。
- CPU 使用率:应用服务器 CPU 使用率从 85% 降至 65%,留有充足余量应对突发流量。
- 数据一致性:上线一周内,未出现订单状态不一致问题,消息表补偿机制成功处理了 3 次下游服务短暂不可用场景。
与引入 Seata AT 模式的对照组相比,柔性事务方案在相同硬件配置下,吞吐量提升了 2.4 倍,延迟降低了 60%。这验证了在高并发场景下,“去全局锁化”的重要性。
当然,这套方案并非完美。它要求业务代码具备一定的幂等性设计能力,且需要维护消息表的状态流转。但在我们的业务约束下,这是性价比最高的选择。
写在最后
MySQL 优化从来不是一蹴而就的,它需要在一致性、可用性和分区容忍性之间做出权衡。分布式事务的选择更是如此——没有银弹,只有最适合场景的解法。希望这篇实战复盘能为你在高并发场景下的架构选型提供参考。
你在实际项目中有遇到类似问题吗?欢迎在评论区分享你的经验和解决方案。