微服务架构做到一定复杂度后,服务之间最难处理的不是怎么写代码,而是怎么通信。同步 HTTP 调用简单直观,但链路一长就会出现一个典型场景:A 调用 B,B 调用 C,C 又去调 D,结果 D 慢了几百毫秒,A 的线程池很快被占满,接口超时,客户端重试,流量翻倍,雪崩就是这么来的。于是大家开始找异步方案。在 AWS 生态里,最常用也最容易上手的答案就是 SNS + SQS。
这篇文章是“AWS SNS SQS 微服务架构”系列的第一篇。我会先把 SNS 和 SQS 的核心概念、适用边界、两者组合出来的扇出模式讲明白,再带你用 AWS CLI、Python boto3、Java SDK 完整跑通一条“订单事件同时分发到多个服务”的消息链路,最后给出生产环境最容易踩的坑和工程建议。
读完这篇文章,你应该能回答三个问题:第一,某个消息场景到底该用 SNS、SQS 还是两个都用;第二,怎么让一条 SNS 消息自动投递到多个 SQS 队列;第三,消息到了消费者手里为什么还会重复,线上该怎么防御。
1. 为什么微服务架构里要用 SNS + SQS
微服务之间的通信方式,从大方向上分只有两类:同步和异步。绝大多数团队最开始使用的都是同步 HTTP:服务 A 发起调用,阻塞等待服务 B 返回结果。
这种模式不是不好,而是对链路延时长、调用关系复杂的场景不友好。典型的问题有三个。
第一个是调用链放大。A 调用 B,B 调用 C,如果 C 挂了或变慢,A 的请求线程会被一直占用。流量高峰时,线程池被打满,后续请求全部排队。更糟的是客户端超时后通常会自动重试,重试流量再涌进来,系统很容易从一个小故障演变成大面积不可用。
第二个是下游强耦合。订单服务在代码里写了“通知服务”的地址,那它就是强依赖通知服务。通知服务重启、升级、甚至只是网络抖动,都可能让订单服务接口 5xx。业务上“通知失败”并不应该导致“下单失败”,但同步调用的代码会让这两件事强相关。
第三个是处理能力错配。下单是突发的,扣库存可能只要 5 毫秒,但发短信要调第三方接口,平均要 300 毫秒。用同步阻塞的方式,一个下单线程会被慢操作拖住,整个系统的吞吐就被最慢的那个环节锁死了。
当然,不是所有调用都应该异步化。实时查询类的操作,比如查订单详情、登录校验,天然需要同步拿到结果;而事件通知、状态变更传播、任务执行这类操作,并不要求调用方立刻拿到结果,这些才是异步消息的应用场景。在 AWS 上做异步,最常遇到的两个服务就是 SNS 和 SQS。
2. SNS 与 SQS 核心概念与定位
2.1 SQS:分布式消息队列
SQS(Simple Queue Service)是全托管的分布式消息队列。你不需要自己部署任何 MQ 服务,只需要创建队列,然后生产方发消息、消费方拉消息。
用生活场景来理解,SQS 就是一个快递柜。寄件人不用当面把东西交给收件人,而是放进快递柜;收件人什么时候有空,就什么时候去取。快递柜本身并不关心寄件人和收件人是否同时在线。这种解耦,就是队列最核心的价值。
SQS 有两种队列类型,选错会影响后续架构设计:
标准队列:默认类型,吞吐高,支持近似无限并发;但只保证至少一次投递,也就是说消费方有可能重复收到同一条消息。消息顺序也不保证。
FIFO 队列:提供先入先出的严格顺序语义,支持正好一次投递,但吞吐量受限,默认配额有限;FIFO 队列的命名必须以.fifo结尾。
在微服务架构里,绝大多数业务队列场景用标准队列就够;只有对消息顺序有硬要求的场景,比如交易流水回放、订单状态机流转,才必须用 FIFO。
2.2 SNS:发布订阅广播总线
SNS(Simple Notification Service)是发布/订阅模式的消息服务。它的核心组件是 Topic(主题)。生产者把消息发布到 Topic,Topic 再把消息推送给所有订阅者。
还是用生活场景理解,SNS 像广播电台的频道。电台只管发信号,谁拿着收音机调到这个频道,谁就能收到。发信号的广播台不需要知道有多少台收音机,也不管收音机是否开机。
SNS 支持的订阅端非常多:HTTP/HTTPS 端点、Email、短信、移动推送、SQS 队列、Lambda 函数等。在微服务架构中,最常用的两个订阅端是 SQS 队列和 Lambda 函数。SNS 本身不负责存储消息,也不保证消费者一定处理成功,它更像一个“事件交换机”,发布一次,转发多路。
2.3 SNS 与 SQS 的核心区别
很多新手分不清这两个服务,我用一张表把它们放在一起对比。
| 维度 | 同步 HTTP | SQS | SNS |
|---|---|---|---|
| 通信模式 | 一对一同步 | 一对一异步 | 一对多异步广播 |
| 调用结果 | 调用方阻塞等待 | 消费者自主拉取 | 订阅者各自接收 |
| 削峰能力 | 无 | 强 | 需要配合队列实现 |
| 故障隔离 | 弱 | 强 | 取决于订阅端 |
| 消息存储 | 无 | 默认保留 4 天 | 不存储,立即推送 |
| 时序保证 | 自然有序 | 标准无序,FIFO 有序 | 无序 |
| 适用场景 | 实时查询、强一致 | 任务队列、削峰填谷 | 事件通知、扇出分发 |
一个简单的记忆方式是:SQS 是“点对点”的消息管道,一条消息只会被一个消费者取走;SNS 是“广播”的喇叭,一条消息会复制给所有订阅者。
2.4 为什么是 SNS + SQS 组合
如果只用一个队列,解决的是“一个生产者给一个消费者解耦”的问题。但很多需求是“一个事件要同时影响多个服务”。
以订单创建为例:订单服务只需要发布一条“订单创建成功”的事件,但库存服务、通知服务、积分服务都需要这个事件,而且它们各自的处理速度不同。如果直接把事件写到某个下游队列里,其他下游就收不到;如果让订单服务挨个往三四个队列各发一遍,订单服务的代码就重复且耦合。
SNS + SQS 的组合正是解决这类“一对多”扇出需求的标准做法:SNS 主题把事件广播到多个 SQS 队列,每个队列对应一个消费者服务。生产者只需要跟 SNS 交互,消费者只需要跟自己的 SQS 队列交互,中间完全是解耦的。这个模式在 AWS 官方许多参考架构里都有出现,是微服务事件驱动设计的基础。
订单服务 → SNS(order-events) ├── SQS 队列 A → 库存服务 ├── SQS 队列 B → 通知服务 └── SQS 队列 C → 积分服务3. 环境准备与前置条件
要跑通本文示例,需要准备以下环境。
3.1 AWS 账号与 IAM 权限
你需要一个能访问 SNS 和 SQS 的 AWS 账号。不要在生产账号直接用管理员权限做实验,更稳妥的做法是创建一个最小权限 IAM 用户,或者使用 AWS CloudShell 内置的临时凭证。
本文示例需要的权限至少包括:
- sqs:CreateQueue
- sqs:SendMessage
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
- sqs:SetQueueAttributes
- sns:CreateTopic
- sns:Subscribe
- sns:Publish
- sns:GetTopicAttributes
如果只是本地试验,推荐使用 AWS CloudShell,它直接内置了 AWS CLI 和临时凭证,不用在本地配置密钥,安全性也更高。如果你在本地操作,则按下方方式配置。
3.2 安装并配置 AWS CLI
安装 AWS CLI 之后,执行:
aws configure按提示输入 Access Key、Secret Key、默认区域和输出格式。验证是否配置成功:
aws sts get-caller-identity看到 Account 和 Arn 输出,就说明配置完成。注意本文示例按us-east-1区域编写,如果你使用其他区域,请把命令和 ARN 中的区域替换成实际值。
3.3 Java 与 Python 运行环境
本文代码示例提供 Python(boto3)和 Java(AWS SDK for Java 2.x)两个版本。
Python 版只需要安装依赖:
pip install boto3Java 版需要 JDK 8 以上和 Maven,并在 pom.xml 中引入software.amazon.awssdk:sqs与software.amazon.awssdk:sns两个依赖。版本号不要照抄网上写死的数字,以 Maven 中央仓库当前稳定版本为准。如果使用 Spring Boot,可以通过依赖管理统一维护版本。
3.4 本地模拟选型:LocalStack
如果你不想在开发阶段连真实 AWS,可以用 LocalStack 在本地模拟 SNS/SQS。用 Docker 启动:
docker run --name localstack -d -p 4566:4566 localstack/localstackPython 代码里指定 endpoint 即可连接本地模拟环境:
import boto3 sqs = boto3.client( 'sqs', endpoint_url='http://localhost:4566', region_name='us-east-1', aws_access_key_id='test', aws_secret_access_key='test' )LocalStack 适合单元测试和本地联调,但它不是真正的 AWS 服务,某些高级特性可能缺失。涉及生产问题排查时,仍然要以真实环境为准。
4. 整体架构设计:订单事件扇出
下面进入实践。我们先定义一个足够典型的业务场景。
4.1 场景描述
假设有一个电商系统,用户提交订单后,订单服务需要触发三件事:
- 库存服务扣减库存;
- 通知服务发送短信和 App 推送;
- 积分服务给用户增加积分。
这三件事都不需要用户在请求里等待,消费速度也各不相同。其中发短信还要调第三方接口,最容易慢和失败。这就是典型的异步扇出场景。
4.2 事件流转
完整链路如下:
- 订单服务发布一条消息到 SNS 主题
order-events,消息内容是一段订单 JSON。 - SNS 将这条消息自动复制并推送到所有订阅了该主题的 SQS 队列。
- 库存、通知、积分三个服务各自从自己的队列中拉取消息,消费速度互不影响。
- 如果某个服务消费失败,消息不会丢失,而是在队列里保留,等待重试或进入死信队列。
这里的关键点是:订单服务只感知 SNS,不感知任何下游服务的地址和状态。新增一个下游服务时,订单服务一行代码都不用改。
4.3 为什么不用 Kafka
一部分读者会想:这个场景 Kafka 也能做。确实能,但两者侧重不同。
Kafka 是一个