分布式与高并发进阶指南:实战排行榜精选
摘要
消息队列通过异步通信实现系统解耦、流量削峰与最终一致性,主流场景包括异步处理、应
第五部分:消息队列 —— 异步削峰的解耦神器
说到分布式系统,消息队列绝对是绕不开的基础设施。它的核心价值在于,通过异步通信这种“延迟满足”的方式,完美地解决了应用之间的解耦、流量的削峰填谷,以及最终一致性的问题。
5.1 典型使用场景
先来聊聊它最常用的几个场景,你一定会觉得“原来如此”:
- 异步处理: 比如用户注册成功后,系统需要发邮件和信息通知。如果同步执行,用户注册的响应时间会变长,体验很差。正确做法是,把这些耗时的操作丢进消息队列,主流程直接返回“注册成功”,用户感受就快多了。
- 应用解耦: 想象一下,订单系统创建订单后,需要通知库存系统扣减库存、积分系统增加积分。如果直接通过RPC调用,任何一个下游系统出问题,都会导致订单失败。而通过消息队列,订单系统只需要发布一个“订单创建”事件,下游系统自行订阅处理,互不干扰。
- 流量削峰: 这是消息队列的“王牌”场景。比如秒杀系统在瞬间涌入海量请求,数据库根本扛不住。解决方案就是先把请求一股脑丢进队列,后端服务再根据自己的处理能力慢慢地从队列里取任务执行。这就像水库,在暴雨时蓄水,在干旱时放水,从而保护了下游的“农田”(数据库)。
- 日志收集: 各个服务产生的日志,以往都是直接写到本地文件,管理和查询很头疼。现在,每个服务都把日志异步发送到MQ,统一由Logstash等工具消费并写入Elasticsearch,实现集中化管理和分析。
5.2 主流MQ对比
市面上的消息队列选择不少,每个都有自己的脾气秉性。

5.3 可靠消息传递的关键概念
用好消息队列,必须理解几个核心概念,否则容易丢消息:
- 生产端确认: 生产者发送消息后,必须等Broker(消息服务器)返回一个确认信号(RabbitMQ的Publisher Confirm,Kafka的acks),才算是真的发送成功了。如果没有收到确认,生产者就要考虑重发。
- 消费端确认: 消费者从队列里拿到消息并处理完毕后,需要手动告诉Broker:“我搞定了,你可以删除了”(也就是手动ack)。如果因为某种原因(如消费者宕机)没有发送ack,Broker会认为处理失败,这条消息会重新排队,被其他消费者处理——这也就是“至少一次投递”的保证。
- 消息持久化: 消息默认存储在内存中,Broker重启就会丢失。必须配置将其写入磁盘,才能在Broker挂掉后恢复。
- 死信队列(DLQ): 对于那些因为各种原因(如业务报错)无法被正常消费的消息,队列会将其丢入一个“死信队列”。这相当于一个回收站,方便开发人员发现、排查和重试问题消息。
5.4 使用RocketMQ实现削峰(Ja va示例)
理论讲完,我们来看个实战例子——用RocketMQ实现秒杀请求的削峰。
生产者:秒杀请求入队
@PostMapping("/seckill")
public String seckill(Long userId, Long productId) {
// 简单校验库存(此处可先用Redis预减库存)
String orderId = UUID.randomUUID().toString();
// 发送延迟消息(例如10秒后处理支付超时)
Message msg = new Message("SEC_KILL_TOPIC", "order", (userId + ":" + productId).getBytes());
// 同步发送
SendResult result = producer.send(msg);
return "排队中";
}
消费者:处理订单创建
@RocketMQMessageListener(topic = "SEC_KILL_TOPIC", consumerGroup = "order_consumer")
public class OrderConsumer implements RocketMQListener {
@Override
public void onMessage(String message) {
// 1. 校验库存
// 2. 创建订单
// 3. 扣减数据库库存
// 注意:必须实现幂等性,防止重复消费
}
}
5.5 消息幂等性
既然MQ提供了“至少一次投递”的保证,就意味着消息可能会被重复投递。所以,消费者必须自己处理重复消息。实现幂等(同一个消息被消费多次,结果一样)的常见方式:
- 数据库唯一键约束: 将订单号作为数据库表的唯一索引,重复插入会失败,天然保证了幂等。
- Redis记录已处理的消息ID: 消费前先检查这个ID是否已在Redis中,用`SETNX`命令,如果成功,说明是第一次处理;如果失败,说明已经处理过了,直接跳过。
- 业务状态机: 处理前先检查业务状态。比如订单已经处于“已处理”状态,那么重复的消息就不再做任何操作。
第六部分:分布式事务 —— 跨数据的一致性难题
当业务跨越多个独立的数据源(比如数据库、Redis、消息队列)时,单机数据库提供的事务就无能为力了。分布式事务,就是用来解决这个全局一致性难题的,但它需要在性能和一致性之间做出艰难的取舍。
6.1 两阶段提交(2PC)与三阶段提交(3PC)
这两种是最经典的分布式事务协议,但各有明显缺陷。
- 2PC (Two-Phase Commit): 有个协调者,先问所有参与者“准备好了没”(Prepare阶段)。如果所有人都说“准备好了”,协调者就下令“提交”(Commit);如果有人说不,或者超时,就下令“回滚”。看起来很完美,但问题在于:Prepare阶段后,协调者如果崩溃,所有参与者都处于一种“悬空”状态,必须锁定资源等待,造成同步阻塞。而且协调者本身也是单点故障,一旦出问题,整个系统就瘫痪了。
- 3PC (Three-Phase Commit): 为了解决2PC的阻塞问题,3PC引入了超时机制和一个“预提交”阶段。虽然减少了阻塞范围,但它的设计复杂得多,实际生产环境中应用很少。
典型的实现是XA协议(MySQL、Oracle支持),但它性能较差,不适合高并发场景。
6.2 TCC(Try-Confirm-Cancel)
TCC是一种补偿型事务方案,更轻量级,也更灵活。它将一个完整的业务操作拆分为三个阶段:
- Try: 预留资源。比如冻结用户的100元库存。
- Confirm: 确认执行。实际扣减冻结的库存,完成业务。
- Cancel: 取消。如果Confirm失败,就释放Try阶段预留的资源。
优点: 性能高,由业务层控制粒度,非常灵活。
缺点: 对业务代码侵入性强,需要为每个操作编写Try、Confirm、Cancel三个接口,而且还要处理幂等和“悬挂”等复杂问题。
示例:转账服务 (A转给B 100元)
// Try:冻结A的100元,增加B的预收资金
void tryTransfer(String from, String to, int amount);
// Confirm:扣减A的冻结资金,将B的预收变为可用余额
void confirmTransfer(String from, String to, int amount);
// Cancel:解冻A的资金,回退B的预收
void cancelTransfer(String from, String to, int amount);
实现TCC时,Confirm和Cancel方法都必须保证幂等性。
6.3 本地消息表 + MQ (最终一致性)
这是目前最常用的最终一致性方案,因为它简单、不依赖复杂的分布式事务框架。以创建一个订单并扣减库存为例:
- 订单服务在自己的本地事务中做两件事:插入一条订单记录,同时插入一条消息记录(状态为“待发送”)。这两步操作在同一个数据库事务中,要么同时成功,要么同时失败。
- 后台有一个定时任务,不停地扫描消息表,把“待发送”的消息发送到MQ。
- 库存服务消费MQ消息,执行扣减库存。
- 库存处理成功后,通过一个回调接口通知订单服务,将对应的消息状态更新为“已处理”。
- 如果库存服务处理失败,或者MQ丢消息了,订单服务的定时任务会不断重试,直到成功或转人工处理。
这个方案的精妙之处在于,它利用本地事务保证了消息不会丢,同时又通过MQ实现了服务间的解耦。缺点是维护了一张消息表,且至少要保证MQ是“至少一次投递”。
6.4 Seata框架
Seata是阿里开源的分布式事务解决方案,它集成了AT(自动补偿)、TCC、Saga等多种模式。其中,AT模式最吸引人,因为它基本没有业务侵入。它通过袋里数据源,自动生成反向的回滚SQL(Undo Log),当全局事务失败时,自动回滚所有分支事务。
// Spring Boot中使用Seata AT模式
@GlobalTransactional
public void purchase(Long userId, Long productId, int count) {
orderService.createOrder(userId, productId, count); // 本地事务
stockService.deductStock(productId, count); // 远程调用
accountService.debitBalance(userId, count * price); // 远程调用
}
Seata会记录每个分支事务的Undo Log,一旦`purchase`方法中任何一个步骤失败,所有已经执行成功的步骤都会被自动反方向补偿。它本质上是对2PC的改进,但在性能和易用性上做了巨大提升。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。