概述
在现代微服务架构中,消息队列是实现服务间异步通信的重要组件。本文将通过一个完整的 Go 语言实践项目,介绍消息队列的核心概念以及 AWS SNS(Simple Notification Service)和 SQS(Simple Queue Service)的使用方法,并使用 localstack 模拟 AWS 服务。
消息队列基础概念
什么是消息队列?
消息队列是一种应用程序间的通信方法,它允许应用程序通过发送和接收消息来进行通信,并使用队列来存储消息。消息队列的核心特点包括:
- 异步通信:发送方不需要等待接收方处理完成
- 解耦:生产者和消费者之间松耦合
- 可靠性:消息持久化存储,确保不丢失
- 可扩展性:支持多个消费者并行处理
AWS SNS 与 SQS 简介
AWS SNS (Simple Notification Service)
SNS 是 AWS 提供的发布-订阅消息服务,支持多种订阅类型和基于属性的消息过滤
AWS SQS (Simple Queue Service)
SQS 是 AWS 提供的消息队列服务,提供标准队列和 FIFO 队列,支持消息持久化存储和消息传递。
项目架构设计
我们的项目实现了一个基于 SNS-SQS 的商城消息处理系统,包含订单、支付、库存三种类型的消息的生产和消费,项目架构如下图:
graph TB
subgraph "生产者服务"
A[订单服务
生产者] B[支付服务
生产者] C[库存服务
生产者] end subgraph "AWS SNS 主题" D[订单主题
sns-order-topic] E[支付主题
sns-payment-topic] F[库存主题
sns-inventory-topic] end subgraph "AWS SQS 队列" G[订单队列
sqs-order-queue] H[支付队列
sqs-payment-queue] I[库存队列
sqs-inventory-queue] end subgraph "消费者服务" J[订单消息
消费者] K[支付消息
消费者] L[库存消息
消费者] end A --> D B --> E C --> F D --> G E --> H F --> I G --> J H --> K I --> L classDef producer fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef sns fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef sqs fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef consumer fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px class A,B,C producer class D,E,F sns class G,H,I sqs class J,K,L consumer
生产者] B[支付服务
生产者] C[库存服务
生产者] end subgraph "AWS SNS 主题" D[订单主题
sns-order-topic] E[支付主题
sns-payment-topic] F[库存主题
sns-inventory-topic] end subgraph "AWS SQS 队列" G[订单队列
sqs-order-queue] H[支付队列
sqs-payment-queue] I[库存队列
sqs-inventory-queue] end subgraph "消费者服务" J[订单消息
消费者] K[支付消息
消费者] L[库存消息
消费者] end A --> D B --> E C --> F D --> G E --> H F --> I G --> J H --> K I --> L classDef producer fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef sns fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef sqs fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef consumer fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px class A,B,C producer class D,E,F sns class G,H,I sqs class J,K,L consumer
消息类型设计
我们定义了三种主要的消息类型:
go
type Message struct {
subject string // 消息主题
content string // 消息内容
msgType string // 消息类型:order, payment, inventory
}
核心代码实现
1. 创建 localstack 容器
我们将在本地创建一个 localstack 的 docker 容器,用于模拟 AWS 服务。创建 docker-compose.yml 文件,代码如下:
yml
version: "3.8"
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=sns,sqs
- DEFAULT_REGION=us-east-1
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
# volumes 字段用于将主机目录或文件挂载到容器内,实现数据共享或持久化,常用于需要管理 Docker 的服务。
# 这里将主机的 /var/run/docker.sock 挂载到容器,允许容器内的进程与主机 Docker 守护进程通信。
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
2. 启动 localstack 容器
执行下列命令启动 localstack 容器:
bash
# -d 表示以"后台(detached)模式"运行容器,让命令执行后立即返回,容器在后台持续运行。这样终端不会被占用。
docker-compose up -d
3. demo 代码
go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"sync"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sns"
snstypes "github.com/aws/aws-sdk-go-v2/service/sns/types"
"github.com/aws/aws-sdk-go-v2/service/sqs"
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
)
type Message struct {
subject string
content string
msgType string
}
func main() {
ctx := context.Background()
// 初始化 AWS 服务
snsClient, sqsClient := initializeAWSServices(ctx)
// 定义主题和队列名称
topicNames := []string{"sns-order-topic", "sns-payment-topic", "sns-inventory-topic"}
queueNames := []string{"sqs-order-queue", "sqs-payment-queue", "sqs-inventory-queue"}
// 创建主题并获取 ARN
// ARN(Amazon Resource Name)可以唯一标识一个主题或队列。
topicArns, err := createTopics(snsClient, topicNames)
if err != nil {
log.Fatalf("创建主题失败: %v", err)
}
// 列出所有主题
if err := listTopics(snsClient); err != nil {
log.Fatalf("列出主题失败: %v", err)
}
// 创建队列并获取 URL 和 ARN
queueUrls, queueArns, err := createQueuesAndGetArns(sqsClient, queueNames)
if err != nil {
log.Fatalf("创建队列失败: %v", err)
}
// 订阅队列到主题
if err := subscribeQueuesToTopics(snsClient, topicArns, queueArns); err != nil {
log.Fatalf("订阅失败: %v", err)
}
// 准备测试消息
messages := prepareTestMessages()
// 启动消息处理系统
startMessageSystem(ctx, snsClient, sqsClient, topicArns, queueUrls, messages)
}
// 初始化 AWS SNS 和 SQS 客户端
func initializeAWSServices(ctx context.Context) (*sns.Client, *sqs.Client) {
snsClient, err := createSNSClient()
if err != nil {
log.Fatalf("创建 SNS 客户端失败: %v", err)
}
sqsClient, err := createSQSClient()
if err != nil {
log.Fatalf("创建 SQS 客户端失败: %v", err)
}
return snsClient, sqsClient
}
// 批量创建 SNS 主题
func createTopics(snsClient *sns.Client, topicNames []string) ([]string, error) {
var topicArns []string
for _, name := range topicNames {
arn, err := createTopic(snsClient, name)
if err != nil {
return nil, fmt.Errorf("创建主题 %s 失败: %v", name, err)
}
topicArns = append(topicArns, arn)
}
return topicArns, nil
}
// 批量创建 SQS 队列并获取 ARN
func createQueuesAndGetArns(sqsClient *sqs.Client, queueNames []string) ([]string, []string, error) {
var queueUrls, queueArns []string
for _, name := range queueNames {
// 创建队列
queueUrl, err := createSQSQueue(sqsClient, name)
if err != nil {
return nil, nil, fmt.Errorf("创建队列 %s 失败: %v", name, err)
}
queueUrls = append(queueUrls, queueUrl)
// 获取队列 ARN
queueArn, err := getQueueArn(sqsClient, queueUrl)
if err != nil {
return nil, nil, fmt.Errorf("获取队列 %s ARN 失败: %v", name, err)
}
queueArns = append(queueArns, queueArn)
}
return queueUrls, queueArns, nil
}
// 订阅队列到主题
func subscribeQueuesToTopics(snsClient *sns.Client, topicArns, queueArns []string) error {
for i, topicArn := range topicArns {
_, err := snsClient.Subscribe(context.TODO(), &sns.SubscribeInput{
TopicArn: aws.String(topicArn),
Protocol: aws.String("sqs"),
Endpoint: aws.String(queueArns[i]),
})
if err != nil {
return fmt.Errorf("订阅队列 %s 到主题 %s 失败: %v", queueArns[i], topicArn, err)
}
fmt.Printf("✓ 队列 %s 订阅到主题 %s 成功\n", queueArns[i], topicArn)
}
return nil
}
// 准备测试消息
func prepareTestMessages() []Message {
return []Message{
// 订单相关消息
{"订单创建", "用户 #12345 创建了新订单", "order"},
{"订单创建", "用户 #67890 创建了新订单", "order"},
{"订单创建", "用户 #12345 取消了新订单", "order"},
{"订单完成", "订单 #12345 已完成,感谢您的购买!", "order"},
{"订单完成", "订单 #67890 已完成,感谢您的购买!", "order"},
// 支付相关消息
{"支付成功", "订单 #12345 支付成功,金额: $99.99", "payment"},
{"支付成功", "订单 #67890 支付成功,金额: $49.50", "payment"},
{"支付失败", "订单 #24680 支付失败,金额: $120.00", "payment"},
// 库存相关消息
{"库存更新", "商品 #ABC123 库存减少 1 件", "inventory"},
{"库存更新", "商品 #XYZ789 库存减少 2 件", "inventory"},
{"库存更新", "商品 #LMN456 库存减少 5 件", "inventory"},
// 物流相关消息
{"物流通知", "订单 #12345 已发货,预计 3 天内到达", "shipping"},
{"物流通知", "订单 #67890 已发货,预计 2 天内到达", "shipping"},
{"物流通知", "订单 #24680 已发货,预计 5 天内到达", "shipping"},
}
}
// 启动消息处理系统
func startMessageSystem(ctx context.Context, snsClient *sns.Client, sqsClient *sqs.Client,
topicArns, queueUrls []string, messages []Message) {
var wg sync.WaitGroup
messageTypes := []string{"order", "payment", "inventory"}
// 启动消费者
wg.Add(len(queueUrls))
for i, queueUrl := range queueUrls {
consumerName := fmt.Sprintf("%s 消费者", messageTypes[i])
go receiveMessage(ctx, &wg, sqsClient, queueUrl, consumerName)
}
// 启动发布者
wg.Add(len(topicArns))
for i, topicArn := range topicArns {
filteredMessages := filterMessagesByType(messages, messageTypes[i])
go publishMessages(snsClient, &wg, topicArn, filteredMessages)
}
// 等待所有任务完成
fmt.Println("⏳ 等待所有任务完成...")
wg.Wait()
fmt.Println("✓ 程序结束")
}
func filterMessagesByType(messages []Message, msgType string) []Message {
var filtered []Message
for _, msg := range messages {
if msg.msgType == msgType {
filtered = append(filtered, msg)
}
}
return filtered
}
// 发布消息到 SNS 主题
func publishMessages(snsClient *sns.Client, wg *sync.WaitGroup, topicArn string, messages []Message) {
defer wg.Done()
fmt.Printf("📤 开始向主题 %s 发布 %d 条消息\n", topicArn, len(messages))
for i, msg := range messages {
_, err := publishMessage(snsClient, topicArn, msg)
if err != nil {
log.Printf("❌ 发布消息 %d 失败: %v", i+1, err)
} else {
fmt.Printf("✓ 发布消息 %d 成功: %s (类型: %s)\n", i+1, msg.content, msg.msgType)
}
time.Sleep(1 * time.Second)
}
fmt.Printf("✓ 主题 %s 消息发布完成\n", topicArn)
}
// 创建 SNS 客户端
func createSNSClient() (*sns.Client, error) {
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion("us-east-1"),
config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: "http://localhost:4566",
SigningRegion: region,
}, nil
})),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
)
if err != nil {
return nil, err
}
return sns.NewFromConfig(cfg), nil
}
// 创建 SNS 主题
func createTopic(snsClient *sns.Client, topicName string) (string, error) {
ctx := context.TODO()
topicAttributes := map[string]string{}
result, err := snsClient.CreateTopic(ctx, &sns.CreateTopicInput{
Name: aws.String(topicName),
Attributes: topicAttributes,
})
if err != nil {
return "", err
}
return *result.TopicArn, nil
}
// 列出所有 SNS 主题
func listTopics(snsClient *sns.Client) error {
ctx := context.TODO()
result, err := snsClient.ListTopics(ctx, &sns.ListTopicsInput{})
if err != nil {
return err
}
fmt.Println("📋 主题列表:")
for i, topic := range result.Topics {
fmt.Printf(" %d. %s\n", i+1, *topic.TopicArn)
}
return nil
}
// 发布单条消息到 SNS 主题
func publishMessage(snsClient *sns.Client, topicArn string, message Message) (string, error) {
ctx := context.TODO()
result, err := snsClient.Publish(ctx, &sns.PublishInput{
TopicArn: aws.String(topicArn),
Message: aws.String(message.content),
Subject: aws.String(message.subject),
MessageAttributes: map[string]snstypes.MessageAttributeValue{
"MessageType": {
DataType: aws.String("String"),
StringValue: aws.String(message.msgType),
},
},
})
if err != nil {
return "", err
}
return *result.MessageId, nil
}
// 创建 SQS 客户端
func createSQSClient() (*sqs.Client, error) {
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion("us-east-1"),
config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: "http://localhost:4566",
SigningRegion: region,
}, nil
})),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
)
if err != nil {
return nil, err
}
return sqs.NewFromConfig(cfg), nil
}
// 创建 SQS 队列
func createSQSQueue(sqsClient *sqs.Client, queueName string) (string, error) {
ctx := context.TODO()
result, err := sqsClient.CreateQueue(ctx, &sqs.CreateQueueInput{
QueueName: aws.String(queueName),
})
if err != nil {
return "", err
}
return *result.QueueUrl, nil
}
// 获取 SQS 队列的 ARN
func getQueueArn(sqsClient *sqs.Client, queueUrl string) (string, error) {
ctx := context.TODO()
attributesOutput, err := sqsClient.GetQueueAttributes(ctx, &sqs.GetQueueAttributesInput{
QueueUrl: aws.String(queueUrl),
AttributeNames: []sqstypes.QueueAttributeName{
sqstypes.QueueAttributeNameQueueArn,
},
})
if err != nil {
return "", err
}
queueArn, ok := attributesOutput.Attributes[string(sqstypes.QueueAttributeNameQueueArn)]
if !ok {
return "", fmt.Errorf("未能获取队列 ARN 属性")
}
return queueArn, nil
}
// 从 SQS 队列接收并处理消息
func receiveMessage(ctx context.Context, wg *sync.WaitGroup, sqsClient *sqs.Client, sqsQueueUrl string, routineName string) {
defer wg.Done()
fmt.Printf("🎧 %s 开始监听队列: %s\n", routineName, sqsQueueUrl)
// 设置超时和空消息计数,避免无限等待
timeout := time.After(60 * time.Second)
emptyCount := 0
maxEmptyCount := 10 // 连续10次没有消息就退出
for {
select {
case <-ctx.Done():
fmt.Printf("🛑 %s 停止监听\n", routineName)
return
case <-timeout:
fmt.Printf("⏰ %s 超时退出\n", routineName)
return
default:
result, err := sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(sqsQueueUrl),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 1,
})
if err != nil {
log.Printf("❌ %s 接收消息失败:%v", routineName, err)
continue
}
if len(result.Messages) == 0 {
emptyCount++
if emptyCount >= maxEmptyCount {
fmt.Printf("📭 %s 连续 %d 次没有收到消息,退出监听\n", routineName, maxEmptyCount)
return
}
continue
}
// 重置空消息计数
emptyCount = 0
fmt.Printf("📨 %s 收到 %d 条消息\n", routineName, len(result.Messages))
for _, message := range result.Messages {
var bodyMap map[string]interface{}
if err := json.Unmarshal([]byte(*message.Body), &bodyMap); err == nil {
fmt.Printf("📋 %s 收到消息:\n", routineName)
for k, v := range bodyMap {
fmt.Printf(" %s: %v\n", k, v)
}
} else {
fmt.Printf("📄 %s 消息内容: %s\n", routineName, *message.Body)
}
// 删除已处理的消息
_, err := sqsClient.DeleteMessage(ctx, &sqs.DeleteMessageInput{
QueueUrl: aws.String(sqsQueueUrl),
ReceiptHandle: message.ReceiptHandle,
})
if err != nil {
log.Printf("❌ %s 删除消息失败:%v", routineName, err)
} else {
fmt.Printf("✓ %s 消息已删除\n", routineName)
}
}
}
}
}
运行项目
bash
go run main.go
看到如下的结果则说明项目运行符合预期。
bash
✅ order 消费者 消息已删除
✅ 发布消息 5 成功: order 消息2, 订单 #67890 已完成,感谢您的购买! (类型: order)
📨 order 消费者 收到 1 条消息
🔥 order 消费者 收到消息:
Type: Notification
MessageId: 7e64b6d8-5243-4423-a178-62c09370319d
TopicArn: arn:aws:sns:us-east-1:000000000000:sns-order-topic
Message: order 消息2, 订单 #67890 已完成,感谢您的购买!
Timestamp: 2025-10-09T04:34:29.290Z
UnsubscribeURL: http://localhost.localstack.cloud:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:000000000000:sns-order-topic:517ef0a3-6b93-4f9b-bf50-d35283df3abd
Subject: 订单完成
SignatureVersion: 1
MessageAttributes: map[MessageType:map[Type:String Value:order]]
Signature: Fxbtz/YXXR1OwIYnFxMXKhkKAaakTSKjM1zcEwKHOwyCWPNpWlCtSHH0T6iAQU6+XCDD+RJLmS6dOI/Eh8YH7FC8mXyEuB92csY+XHFRGJbP2n0ljL1T+G10DiVNa8/uRUJPr+xnf9yBeJCeCWN6aghxTrzjcrGWx7v+dYfWhdJciOirC6rusXkmj+N0YmdlIrAu/txtOVZGiI6TkOLBwkki89Q8FFU5/l/lpepdfuUnPlfThbqYfz4TpFmxE4KkJL4hQRSlEn7d1kzvPrwrqles8+ZTDhgx0xm2AZbk+To6gNA2QbEogJHANwYUgoZJP1pBuZdcgZBEBB0Tp82oJw==
SigningCertURL: http://localhost.localstack.cloud:4566/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem
✅ order 消费者 消息已删除