Go面试官:如何设计百万级别的消息推送系统?

发布时间:2026/7/15 5:28:36

Go面试官:如何设计百万级别的消息推送系统?
两年前我去一家做海外电商的公司面试。面试官是位看起来比我大不了几岁的技术负责人话不多但问问题的方式很特别。他递给我一张纸上面写着一行字“午夜 12 点给 100 万用户发一条推送。你怎么设计”我心想“这题我准备过Kafka 嘛。” 于是开始流畅地讲我的方案用消息队列解耦、异步消费、多个 Worker 并发处理……他听完没有点头也没有摇头只是接着问“如果 Firebase 突然对你们限流了怎么办”“如果一个 Worker 发完消息但还没来得及确认就挂了用户收到两条推送怎么办”“如果市场部说这次要发 1 亿用户你的方案还成立吗”我愣在那里。我准备了一个“答案”但我的“答案”在真正的“问题”面前显得那么单薄。那次面试我大概没通过。但那次对话让我彻底改变了对“大规模系统”的理解。今天我想用 Go 语言把那次面试里学到的设计思想重新讲一遍。第一轮别把请求和推送绑在一起面试官的第一个问题其实是考察我是否理解“同步 vs 异步”的边界。我当时的代码其实写了类似这样的东西funcSendCampaignHandler(w http.ResponseWriter,r*http.Request){users:getAllUsers()for_,user:rangeusers{pushService.Send(user.DeviceToken,Black Friday Sale!)}}这段代码的问题非常明显它把一次 HTTP 请求的生命周期和 100 万次外部调用的生命周期强行绑在了一起。结果就是请求超时、内存爆炸、服务不可用。正确的第一步是解耦。typeCampaignJobstruct{IDstringMessagestringUserIDs[]stringCreatedAt time.Time}funcSendCampaignHandler(w http.ResponseWriter,r*http.Request){varreq CampaignRequest// 解析请求...job:CampaignJob{ID:uuid.New().String(),Message:req.Message,UserIDs:req.UserIDs,}// 放入队列立即返回producer.Produce(job)w.WriteHeader(http.StatusAccepted)}这个改动看似简单但它改变了整个系统的性质推送的“命令”和“执行”被彻底分离了。API 层只需要保证“任务被可靠地记录下来”而不用关心它什么时候完成。第二轮Worker 池——控制节奏而不是被节奏控制面试官接着问“如果你的 Worker 消费太快把下游的推送服务打死了怎么办”这个问题让我意识到有队列还不够消费速率必须可控。在 Go 里我们可以用rate.Limiter来精确控制每个 Worker 的消费速度typePushWorkerstruct{Limiter*rate.Limiter// 控制 QPSClient*push.Client}func(w*PushWorker)Run(){formsg:rangeconsumerChannel{// 等待令牌如果没有则阻塞err:w.Limiter.Wait(context.Background())iferr!nil{continue}w.send(msg)}}更重要的是Worker 的数量应该是可动态调整的。当队列深度增加时我们可以自动扩缩 Worker 实例——这在 Kubernetes 里可以通过 HPAHorizontal Pod Autoscaler基于队列长度来实现。第三轮让重试变得无害面试官问了一个很刁钻的问题“如果一个 Worker 在发送成功之后、确认消息之前崩溃了会发生什么”答案是消息会被重新投递用户会收到两条通知。这不是理论上的风险这是分布式系统里每天都在发生的事。解决这个问题的方法是引入幂等性。在发送之前记录这条通知的唯一 IDtypeNotificationstruct{IDstring// 全局唯一UserIDstringDeviceTokenstringMessagestring}func(w*PushWorker)sendWithIdempotency(notif Notification)error{// 尝试在 Redis 中记录这条通知的 IDTTL 7 天ok,err:w.redis.SetNX(notif.ID,processed,7*24*time.Hour).Result()iferr!nil{returnerr}if!ok{// 已经处理过了直接跳过returnnil}// 真正发送returnw.Client.Send(notif.DeviceToken,notif.Message)}SetNXSet if Not eXists是 Redis 实现分布式锁和幂等性最常用的原子操作。有了它无论重试多少次用户都只会收到一次通知。第四轮批量发送——把 100 万变成 2000面试官又抛出一个问题“如果你的推送服务每次调用只能发一条100 万次调用光网络开销就很大了。怎么办”答案很简单批量发送。大多数推送服务都支持批量 APIfunc(w*PushWorker)sendBatch(notifications[]Notification)error{batch:make([]push.BatchItem,len(notifications))fori,n:rangenotifications{batch[i]push.BatchItem{DeviceToken:n.DeviceToken,Payload:n.Message,}}_,err:w.Client.SendBatch(batch)returnerr}假设一次批量最多发 500 条100 万条通知就只需要 2000 次网络调用。这不仅仅是“快”的问题它极大地降低了 API 限流的概率。第五轮处理失败——死信队列DLQ面试官接着问“如果一个设备 Token 是无效的你重试多少次也没用。这种消息怎么处理”这就是死信队列Dead Letter Queue, DLQ存在的意义。constMaxRetries5func(w*PushWorker)processWithDLQ(notif Notification){err:w.sendWithIdempotency(notif)iferrnil{return}retries:w.retryStore.Increment(notif.ID)ifretriesMaxRetries{// 放入死信队列等待人工处理w.dlqProducer.Send(notif)return}// 指数退避重试time.Sleep(time.Duration(math.Pow(2,float64(retries)))*time.Second)w.processWithDLQ(notif)}DLQ 是一个非常重要的“安全网”。它保证了系统不会因为一小部分“坏消息”而被卡住也让运维团队有一个地方去排查那些永远发不出去的推送。第六轮如果目标是 1 亿用户呢面试官最后抛出的问题是最有杀伤力的“如果这次不是 100 万而是 1 亿用户呢你的 Kafka 里要放 1 亿条消息吗”这是一个关于架构可扩展性的问题。答案是不要一次性生成所有任务而是动态分页生成。funcGenerateCampaignJob(campaignIDstring){pageSize:10000lastID:for{users:getUserBatch(lastID,pageSize)iflen(users)0{break}job:CampaignJob{ID:uuid.New().String(),UserIDs:users,}producer.Produce(job)lastIDusers[len(users)-1].ID// 控制生成速率避免瞬间洪峰time.Sleep(100*time.Millisecond)}}这样即使是 1 亿用户系统也不会一次性扛下巨大的内存和网络压力而是像“水龙头”一样平稳地把任务释放到队列里。结语那次面试教会我的事那次面试我大概没能拿到 offer。但那个下午那个面试官追问的每一个问题都在我后来的职业生涯里反复回响。他让我明白了一件事设计大规模系统不是设计一个“能处理 100 万请求”的系统而是设计一个“在面对 100 万个不确定性时依然能保持稳定”的系统。以下是那次面试之后我每次设计推送系统时都会带上的清单异步解耦别让 HTTP 请求等 Worker 处理完。限流消费用rate.Limiter保护下游服务。幂等性用SetNX或数据库唯一键让重试无害。批量发送减少网络调用提升吞吐量。死信队列DLQ给“坏消息”一个妥善的归宿。动态分页别一次性生成所有任务面对海量用户时尤是如此。这些思想不只在“推送”这个场景有用。在支付系统、订单系统、数据同步系统里它们同样适用。如果你正在准备一场系统设计的面试我希望这个故事能帮你少走一些弯路。毕竟面试官真正想看的不是你背了多少“最佳实践”而是你面对一个“简单问题”时能不能看到它背后的“复杂现实”。

相关新闻

RNS (Residue Number System):从并行加速到量子计算的现代算术基石

RNS (Residue Number System):从并行加速到量子计算的现代算术基石

2026/7/15 5:28:36

1. 剩余数系统:打破传统计算的枷锁第一次听说剩余数系统(RNS)时,我脑海中浮现的是一堆数字碎片。但当我真正理解它的工作原理后,才发现这简直是为现代计算量身定制的数学魔术。想象一下,你要处理一个235位的…

STM32Cube.AI模型量化与C代码生成:嵌入式AI部署实战指南

STM32Cube.AI模型量化与C代码生成:嵌入式AI部署实战指南

2026/7/15 5:28:36

在实际嵌入式AI项目中,最关键的挑战往往不是训练模型,而是如何将训练好的神经网络模型高效部署到资源受限的STM32微控制器上。STM32Cube.AI作为ST官方提供的免费AI模型优化器,能够将TensorFlow、PyTorch、ONNX等主流框架训练的模型转换为适合…

计算机毕业设计之旅游系统的设计与实现

计算机毕业设计之旅游系统的设计与实现

2026/7/15 5:28:36

经济快速发展带动下的旅游产业发展,在我国越来越被更多的人所追求。人们对获取旅游信息有很高的需求,在互联网发达的今天,制作一个旅游线路预定的网站十分有必要。该设计根据前人开发经验,基于JSP(Hypertext Preproces…

【Java 8 新特性】Java LocalDate 实战:从基础操作到高级应用

【Java 8 新特性】Java LocalDate 实战:从基础操作到高级应用

2026/7/15 7:08:41

1. LocalDate基础入门LocalDate是Java 8中引入的一个不可变日期类,专门用于处理不带时间的纯日期(格式为yyyy-MM-dd)。它彻底解决了传统Date和Calendar类的设计缺陷,比如线程安全问题、月份从0开始等反人类设计。创建LocalDate对象…

重磅发布!用Scratch复刻《我的世界》:从零到一的编程探险

重磅发布!用Scratch复刻《我的世界》:从零到一的编程探险

2026/7/15 7:08:41

1. 为什么用Scratch复刻《我的世界》?十年前我第一次接触Scratch时,完全没想到这个拖拽积木的编程工具能做出像《我的世界》(MC)这样复杂的游戏。直到看到国外一个12岁孩子用Scratch做的3D迷宫,我才意识到图形化编程的…

攀枝花3路公交:穿越北岸的老资历线路运营解析

攀枝花3路公交:穿越北岸的老资历线路运营解析

2026/7/15 7:08:41

攀枝花公交3路:穿越偏僻北岸的老资历线路全解析在攀枝花这座山城,公交线路如同城市的血脉,连接着各个区域。其中3路公交作为一条"老资历"线路,以其几乎未改过线的稳定性和穿越偏僻北岸的独特路线,成为了解这…

Windows系统如何获取苹果苹方字体:从字体原理到实战应用的完整指南

Windows系统如何获取苹果苹方字体:从字体原理到实战应用的完整指南

2026/7/15 7:08:41

Windows系统如何获取苹果苹方字体:从字体原理到实战应用的完整指南 【免费下载链接】PingFangSC PingFangSC字体包文件、苹果平方字体文件,包含ttf和woff2格式 项目地址: https://gitcode.com/gh_mirrors/pi/PingFangSC 你是否厌倦了Windows系统默…

基于文件共享见证的故障转移集群高可用文件服务器部署指南

基于文件共享见证的故障转移集群高可用文件服务器部署指南

2026/7/15 7:08:40

1. 文件共享见证的核心价值在传统的高可用方案中,磁盘见证(Disk Witness)是最常见的仲裁资源配置方式。但实际运维中经常会遇到这样的困境:当企业没有共享存储设备时,传统的磁盘见证方案就难以实施。这正是文件共享见证…

eino框架中AI推理过程透明化的架构设计与实现

eino框架中AI推理过程透明化的架构设计与实现

2026/7/15 6:58:40

eino框架中AI推理过程透明化的架构设计与实现 【免费下载链接】eino The ultimate LLM/AI application development framework in Go. 项目地址: https://gitcode.com/GitHub_Trending/ei/eino 在构建现代AI应用时,模型透明性已成为开发者关注的核心议题。ei…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/14 10:03:09

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/13 20:43:19

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/15 0:26:43

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载:…

【LINUX】驱动

【LINUX】驱动

2026/7/15 0:08:14

【LINUX驱动】【字符设备】【中断】【Platform】【网课 设备树】【GPIO】【PINCTRL】【INPUT】【IIC】【SPI】【网络驱动】【屏幕驱动】【一 设备树】【二 内核模块编译】【三 基本驱动框架】【四 Platform总线设备驱动框架】【五 驱动子系统】【六 综合】

【1982-2026】全国高精度建筑轮廓|村级精度|SHP矢量

【1982-2026】全国高精度建筑轮廓|村级精度|SHP矢量

2026/7/15 0:08:14

🔍 数据简介 本次分享1982-2026年全国村级精度建筑轮廓矢量数据,覆盖全国各省市区县,到村级别精细,为2026年最新实时采集成果,非网传仅60/77个城市的老旧数据。 数据含带高度/不带高度双版本,单体建筑边界精…

【1975-2026】全国水系水路数据|河流/水库/运河|SHP矢量

【1975-2026】全国水系水路数据|河流/水库/运河|SHP矢量

2026/7/15 0:08:14

🔍 数据简介 本次分享1975-2026年全国高精度水系水路矢量数据,覆盖全国全域,包含河流、水系、水库、运河、湿地、冰川、沟渠等全类别水文要素。 数据集包含双层矢量图层,字段分类清晰、要素齐全,支持2013-2026逐年完整…