实际应用场景(大模型生成)
场景设定:设计淘宝“双十一”零点秒杀一个超热门商品(比如 茅台)的后端系统
-
业务挑战:
- 超高峰值流量:在零点钟声敲响的第一秒,可能会有数百万用户同时点击“抢购”按钮。
- 数据强一致性:库存只有 1000 瓶,绝对不能超卖。即卖出第 1001 瓶。
- 高可用性:系统不能崩溃。
- 公平性:尽量保证先到先得。
实施过程:一场精心编排的并发“战役”
战役前线:流量接入与削峰 (API 网关 & 消息队列)
-
用户请求入口:
- 用户的请求首先会经过阿里云的 CDN 和多层负载均衡 (SLB) ,这部分负责抵御 DDoS 攻击和进行初步的流量分发。
- 流量最终到达API 网关集群(比如基于 Nginx/OpenResty 自研的网关)。
- (应用) : 反向代理, 负载均衡器
-
API 网关的初步拦截:
- 网关层会执行一个前置的、粗粒度的限流。比如通过令牌桶算法,判断这一秒的总请求数是不是已经超过了系统能承受的最大阈值(比如 200 万次)。超过的请求,会直接返回“系统繁忙,请稍后再试”,根本不会进入后端。
- (应用) : 令牌桶
-
异步化解耦 (核心步骤) :
-
对于通过了限流的请求,API 网关不会直接调用后端的秒杀服务。
-
它会做一件极其重要的事:把这个抢购请求(包含 user_id, item_id 等信息)序列化后,以极快的速度扔进一个 Apache Kafka 或 阿里云 RocketMQ (消息队列) 的 Topic 里,比如 seckill_request_topic。
-
然后,网关立刻给用户返回一个“排队中,请稍后查看结果”的友好提示。
-
(应用) : 分布式消息系统 (Kafka/RabbitMQ)
-
为什么这么做?
- 削峰填谷:把第一秒涌入的数百万请求,变成了一个平稳流入的消息流。后端系统可以按照自己的节奏去消费,避免了被瞬间流量打垮。
- 解耦:接入层和处理层分离,任何一方升级或故障,不影响另一方。
-
战役中场:核心库存处理 (分布式锁 & 缓存)
现在,请求已经变成了 Kafka 里的消息。我们有一组专门的 Go 服务(秒杀处理服务)来消费这些消息。
-
消费消息:
- 秒杀处理服务作为一个消费者组,从 Kafka 中拉取抢购消息。
- (应用) : Go并发 - Worker Pool: 我们可以用一个协程池来并发地处理这些消息,以提高吞吐量。
-
库存预检查 (缓存层) :
- 服务拿到一个请求后,不会立刻去查数据库。
- 它会先访问一个分布式缓存 (Redis) ,检查一个特殊的库存标记位,比如 GET stock:maotai:flag。
- 如果这个标记位显示“已售罄”,服务会直接丢弃这个请求,连锁都不用抢了。这可以过滤掉绝大部分无效请求。
- (应用) : 服务器端缓存 (Redis)
-
争抢分布式锁 (最关键的一步) :
- 如果缓存显示还有库存,现在就进入了最关键的“抢锁”环节。
- 服务会尝试获取一个针对这个商品的分布式锁,比如用 Redis 的 SET stock_lock:maotai user_id NX PX 1000 命令。
- NX 保证了只有一个客户端能设置成功。
- PX 1000 设置了一个较短的超时时间(1秒),防止因某个节点宕机而导致死锁。
- (应用) : 分布式并发原语 - 分布式锁 (用 Redis 实现) , 强一致性
-
扣减库存 (数据库操作) :
-
成功抢到锁的那个 goroutine,获得了唯一的操作库存的权限。
-
它会去数据库 (MySQL) 里,执行一个事务:
- SELECT stock FROM items WHERE item_id = 'maotai' FOR UPDATE; (悲观锁,再次确认库存)
- 如果 stock > 0,则 UPDATE items SET stock = stock - 1 WHERE item_id = 'maotai';
- INSERT INTO orders ...; (创建订单)
- COMMIT;
-
操作完成后,立刻释放分布式锁 (DEL stock_lock:maotai)。
-
(应用) : SQL 数据库, ACID 事务
-
-
更新缓存标记:
- 如果这次扣减导致库存变成了 0,那么这个 goroutine 还需要负责去更新 Redis 里的那个标记位 SET stock:maotai:flag "sold_out"。
战役收尾:结果通知与数据同步
-
结果通知:
- 秒杀处理服务在创建订单成功后,会把结果(“抢购成功,订单号是xxx”)再发送到另一个 Kafka Topic 里,比如 seckill_result_topic。
- 有一个专门的通知服务,会订阅这个 Topic,然后通过WebSocket或移动推送,把结果实时地推送给用户。
- 对于失败的用户,可以在一段时间后统一告知“已售罄”。
- (应用) : WebSocket
-
数据同步与分析:
- 订单数据、用户行为日志等,都会被持续地写入 Kafka。
- 下游的大数据平台 (MaxCompute) 会消费这些消息,进行批处理和流处理,用于生成报表、风控分析、实时大屏等。
- (应用) : 批处理与流处理
这个过程中用到的其他“Go并发”知识
- errgroup: 在一个服务内部,如果需要同时调用多个下游依赖(比如同时查询用户信息和风控评分),会用 errgroup 来进行并发编排,并确保一个失败,全部取消。
- context: 贯穿全程! 从 API 网关接收到请求的那一刻起,就会创建一个带 trace_id 和超时时间的 Context。这个 Context 会被一路传递到 Kafka 消息体中,再到秒杀处理服务,再到数据库调用,实现了全链路追踪和超时控制。
- singleflight: 在查询一些不那么频繁变化的热点数据时(比如商品的静态描述信息),会用 singleflight 来防止缓存击穿。
这个例子,几乎把你笔记里所有重要的分布式和并发概念都串联起来了。它清晰地展示了在面对极端并发挑战时,一个大型系统是如何通过分层、解耦、异步化和各种并发原语的组合来保证系统的稳定性和数据一致性的。
场景设定:设计B站/腾讯视频,支撑一个千万播放量、百万评论的爆款视频
-
业务挑战:
- 海量读取 (Read Heavy) :一个热门视频,可能会有数十万 QPS(每秒请求数)来读取它的评论。数据库根本扛不住。
- 海量存储:一个视频可能有几百万条评论,所有视频加起来是千亿甚至万亿级别。单张 MySQL 表无法存储。
- 写入/读取的实时性:用户刚发表的评论,需要近乎实时地被其他人看到。
- 复杂的查询需求:需要按时间排序、按热度(点赞数)排序、支持分页。
实施过程:一场“读写分离”与“多级缓存”的艺术
第一阶段:评论的写入 (Write Path) —— “异步化、分而治之”
当一个用户点击“发表评论”时:
-
API 网关接收请求:
- 请求 POST /api/v2/comment/add 到达 B 站的 API 网关。
- 网关进行基础的认证(通过 JWT 确认用户身份)、鉴权(用户是否被禁言)、内容安全审核(调用 AI 服务检测违规内容)。
- (应用) : API 网关, 无状态架构
-
评论写入服务 (Go) :
-
网关将合法的请求转发给后端的 “评论写入服务” 。
-
这个服务不直接写主数据库!它会做两件事:
- 写入消息队列 (Kafka/RocketMQ) :将完整的评论内容(comment_id, video_id, user_id, content, timestamp)打包成一条消息,发送到 comment_add_topic。这是为了后续的数据同步和分析。
- 写入“评论审核库” (MySQL) :将评论写入一个专门的、结构简单的审核数据库,状态为“审核中”。这个库只负责存储新评论,写入压力相对可控。
-
写入成功后,立刻给前端返回“发表成功”。
-
(应用) : 分布式消息系统, SQL数据库 (用于写入)
-
-
异步处理与数据同步:
-
有一个后台的 “评论同步服务” (消费者),会订阅 comment_add_topic。
-
它拿到新评论数据后,会将其写入真正的主存储系统。
-
主存储的选择:对于评论这种数据,传统 MySQL 很难水平扩展。大厂通常会选择列式 NoSQL 数据库,如 HBase 或 Cassandra。
- 分区/分片:数据会以 video_id 作为分区键 (Partition Key) 进行水平分区。所有同一个视频的评论,都会被路由到同一个分片上,便于按视频聚合查询。
- (应用) : NoSQL (宽列), 数据分区 (水平分区)
-
第二阶段:评论的读取 (Read Path) —— “层层设防,多级缓存”
当成千上万的用户刷新评论区时:
-
第一道防线:客户端缓存 (Client-Side Cache)
- App/浏览器可能会在本地缓存第一页的评论。当用户短时间内反复进入页面,可以直接从本地加载,连网络请求都不发。
- (应用) : 客户端缓存
-
第二道防线:CDN 缓存
- 对于极度热门且不常变化的数据(比如一个视频的“精选评论”或“置顶评论”),可以把获取这些评论的 API 接口配置到 CDN 上,缓存几十秒。
- (应用) : CDN
-
第三道防线:分布式缓存集群 (Redis) —— 主力防线
-
这是最核心的缓存层。当请求穿透 CDN 到达源站的 “评论读取服务” (Go) 时,服务绝对不会直接去查 HBase/Cassandra。
-
它会先去 Redis 集群里查询。
-
缓存设计:
- 按页缓存:Redis 中的 Key 设计成 comment:video_id:page_1:by_time,Value 就是第一页评论列表的 JSON 字符串。
- 数据结构:可以用 String 存预先序列化好的 JSON,也可以用 ZSet (有序集合) 来存储评论,score 是时间戳或点赞数,便于排序和分页。
-
绝大多数(99.9%)的读请求,都会在这一层被命中并直接返回。
-
(应用) : 服务器端缓存 (Redis)
-
-
第四道防线:本地内存缓存 (groupcache / freecache)
- 在“评论读取服务”的进程内部,还可以再加一层本地内存缓存。
- 当服务A从 Redis 加载了 video_id:123 的第一页评论后,它会把这个结果在自己的内存里再缓存几秒钟。
- 如果下一秒,又有 100 个请求被负载均衡器打到了服务A上,要查询同一个视频的同一页评论,那么这 100 个请求会直接命中本地内存缓存,连 Redis 都不用访问了!
- (应用) : 内存缓存 (应用内) , groupcache (如果需要节点间协作)
-
终极防线:数据源 (HBase/Cassandra)
-
只有当所有缓存层都未命中时(比如一个冷门视频的第一条评论,或者热门视频的缓存刚好过期),请求才会到达最终的数据源。
-
此时,会触发 “回源” 逻辑:
- 获取分布式锁 (用 etcd 或 Redis 实现),防止缓存击穿。第一个拿到锁的请求去 HBase/Cassandra 查询数据。
- 查询到数据后,重建各级缓存(写入本地内存、写入 Redis)。
- 释放锁,并把数据返回给用户。
-
(应用) : 分布式锁 (etcd/Redis) , 缓存击穿防护 (SingleFlight)
-
这个架构中 Go 并发原语的应用
- WaitGroup / errgroup: 在“评论读取服务”中,可能需要聚合评论数据和UP主的附加信息,这时会用 errgroup 并发地调用评论数据源和用户服务。
- sync.Pool: 在进行大量的 JSON 序列化/反序列化时,会用 sync.Pool 来复用 encoder/decoder 和 buffer,降低 GC 压力。
- Channel: 在服务内部,用于不同组件间的异步消息传递,比如将需要更新缓存的任务扔给一个专门的 goroutine 去处理。
这个场景,完美地展示了为了支撑超高读取 QPS,一个系统是如何通过层层缓存来构建纵深防御体系的。写入时通过异步化和分区来分散压力,读取时则想尽一切办法避免访问底层存储。
好几个月以来,非常喜欢让大模型陪我学习或者是指导我学习,虽然不能保证完全的正确性,但我还是黏上了它,分不开了啊属于是🤣你们如果觉得内容有误或者与自己的理解不同,请尽量先坚持自己的想法,这些内容仅供参考