共计 1921 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点
在构建消息中转服务时,开发者常面临几个核心挑战:

- 协议多样性 :不同客户端可能使用 HTTP、WebSocket、gRPC 等不同协议接入,需要统一处理
- 消息堆积 :在电商秒杀等场景下,瞬时流量可能导致消息积压,传统同步处理模式容易崩溃
- 顺序保证 :订单状态变更等业务场景要求消息必须有序处理
- 可靠性 :网络抖动或服务重启时,如何避免消息丢失
以电商秒杀为例,当万人同时抢购时,消息中转服务需要具备:
– 每秒万级消息的处理能力
– 99.99% 的可用性
– 毫秒级延迟
架构设计
我们采用分层架构设计,各层职责明确:
graph TD
A[接入层] -->| 协议转换 | B[路由层]
B -->| 消息分发 | C[持久层]
C --> D[(Kafka)]
C --> E[(Redis)]
协议选型对比
通过基准测试对比两种主流协议(测试环境:4 核 8G 云主机,payload 1KB):
| 协议类型 | QPS | 平均延迟 | CPU 占用 |
|---|---|---|---|
| REST | 12,000 | 15ms | 65% |
| gRPC | 28,000 | 5ms | 45% |
虽然 gRPC 性能更优,但最终我们选择支持双协议:
– 对外提供 REST API(兼容性更好)
– 内部服务间采用 gRPC 通信
核心实现
异步处理器(Go 实现)
// 带缓冲的通道处理消息
func NewAsyncProcessor(capacity int) *AsyncProcessor {
return &AsyncProcessor{msgChan: make(chan Message, capacity), // 建议缓冲大小为 QPS 的 2 倍
workers: runtime.NumCPU() * 2, // 按 CPU 核数启动 worker}
}
// 消息处理主循环
func (p *AsyncProcessor) Run() {
var wg sync.WaitGroup
for i := 0; i < p.workers; i++ {wg.Add(1)
go func() {defer wg.Done()
for msg := range p.msgChan {if err := process(msg); err != nil {metrics.ErrorCount.Inc()
p.retryQueue.Add(msg)
}
metrics.ProcessedCount.Inc()}
}()}
wg.Wait()}
幂等处理(Redis Lua 脚本)
-- KEYS[1]: 消息 ID
-- ARGV[1]: 当前时间戳
-- 返回值:1- 首次处理 0- 重复消息
local exists = redis.call('EXISTS', KEYS[1])
if exists == 1 then
return 0
end
redis.call('SETEX', KEYS[1], 86400, ARGV[1])
return 1
生产考量
Kafka 分片策略
通过测试不同分片数对吞吐量的影响(相同硬件条件):
- 3 个分区:15,000 msg/s
- 6 个分区:28,000 msg/s
- 12 个分区:35,000 msg/s
- 24 个分区:38,000 msg/s
建议:
– 分区数 = 消费者数量 × 1.5
– 单个分区不超过 50MB/s
监控指标设计
关键 Prometheus 指标:
metrics:
- name: message_queue_size
help: Current messages in processing queue
type: gauge
- name: process_duration_seconds
help: Time spent processing a message
type: histogram
buckets: [0.1, 0.5, 1, 2]
避坑指南
Goroutine 管理
常见内存泄漏场景:
- 未设置超时的阻塞 channel 操作
- 未回收的 timer/ticker
解决方案:
// 使用 context 控制生命周期
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case msg := <-ch:
// 正常处理
case <-ctx.Done():
return errors.New("timeout")
}
Redlock 实现
注意事项:
- 至少 3 个独立的 Redis 实例
- 时钟漂移不超过 TTL 的 10%
- 重试间隔 = TTL × 0.3
推荐配置:
lock, err := redsync.NewRedlock([]redsync.Pool{pool1, pool2, pool3},
redsync.WithRetryDelay(100*time.Millisecond),
redsync.WithTries(3),
)
思考题
如何设计跨机房消息同步方案?可以考虑:
1. 双写 + 冲突解决(CRDT 数据结构)
2. 基于 Raft 的日志复制
3. 机房级消息队列镜像
欢迎在评论区分享你的方案。
正文完
