脑抽研究生Go并发-6-实际应用场景(大模型生成)-淘宝“双十一”零点秒杀,支撑一个千万播放量、百万评论的爆款视频

82 阅读11分钟

实际应用场景(大模型生成)

场景设定:设计淘宝“双十一”零点秒杀一个超热门商品(比如 茅台)的后端系统

  • 业务挑战

    1. 超高峰值流量:在零点钟声敲响的第一秒,可能会有数百万用户同时点击“抢购”按钮。
    2. 数据强一致性:库存只有 1000 瓶,绝对不能超卖。即卖出第 1001 瓶。
    3. 高可用性:系统不能崩溃。
    4. 公平性:尽量保证先到先得。

实施过程:一场精心编排的并发“战役”

战役前线:流量接入与削峰 (API 网关 & 消息队列)
  1. 用户请求入口

    • 用户的请求首先会经过阿里云的 CDN多层负载均衡 (SLB) ,这部分负责抵御 DDoS 攻击和进行初步的流量分发。
    • 流量最终到达API 网关集群(比如基于 Nginx/OpenResty 自研的网关)。
    • (应用) : 反向代理, 负载均衡器
  2. API 网关的初步拦截

    • 网关层会执行一个前置的、粗粒度的限流。比如通过令牌桶算法,判断这一秒的总请求数是不是已经超过了系统能承受的最大阈值(比如 200 万次)。超过的请求,会直接返回“系统繁忙,请稍后再试”,根本不会进入后端。
    • (应用) : 令牌桶
  3. 异步化解耦 (核心步骤)

    • 对于通过了限流的请求,API 网关不会直接调用后端的秒杀服务。

    • 它会做一件极其重要的事:把这个抢购请求(包含 user_id, item_id 等信息)序列化后,以极快的速度扔进一个 Apache Kafka 或 阿里云 RocketMQ (消息队列) 的 Topic 里,比如 seckill_request_topic。

    • 然后,网关立刻给用户返回一个“排队中,请稍后查看结果”的友好提示。

    • (应用) : 分布式消息系统 (Kafka/RabbitMQ)

    • 为什么这么做?

      • 削峰填谷:把第一秒涌入的数百万请求,变成了一个平稳流入的消息流。后端系统可以按照自己的节奏去消费,避免了被瞬间流量打垮。
      • 解耦:接入层和处理层分离,任何一方升级或故障,不影响另一方。
战役中场:核心库存处理 (分布式锁 & 缓存)

现在,请求已经变成了 Kafka 里的消息。我们有一组专门的 Go 服务(秒杀处理服务)来消费这些消息。

  1. 消费消息

    • 秒杀处理服务作为一个消费者组,从 Kafka 中拉取抢购消息。
    • (应用) : Go并发 - Worker Pool: 我们可以用一个协程池来并发地处理这些消息,以提高吞吐量。
  2. 库存预检查 (缓存层)

    • 服务拿到一个请求后,不会立刻去查数据库
    • 它会先访问一个分布式缓存 (Redis) ,检查一个特殊的库存标记位,比如 GET stock:maotai:flag。
    • 如果这个标记位显示“已售罄”,服务会直接丢弃这个请求,连锁都不用抢了。这可以过滤掉绝大部分无效请求。
    • (应用) : 服务器端缓存 (Redis)
  3. 争抢分布式锁 (最关键的一步)

    • 如果缓存显示还有库存,现在就进入了最关键的“抢锁”环节。
    • 服务会尝试获取一个针对这个商品的分布式锁,比如用 Redis 的 SET stock_lock:maotai user_id NX PX 1000 命令。
    • NX 保证了只有一个客户端能设置成功。
    • PX 1000 设置了一个较短的超时时间(1秒),防止因某个节点宕机而导致死锁。
    • (应用) : 分布式并发原语 - 分布式锁 (用 Redis 实现) , 强一致性
  4. 扣减库存 (数据库操作)

    • 成功抢到锁的那个 goroutine,获得了唯一的操作库存的权限。

    • 它会去数据库 (MySQL) 里,执行一个事务:

      1. SELECT stock FROM items WHERE item_id = 'maotai' FOR UPDATE; (悲观锁,再次确认库存)
      2. 如果 stock > 0,则 UPDATE items SET stock = stock - 1 WHERE item_id = 'maotai';
      3. INSERT INTO orders ...; (创建订单)
      4. COMMIT;
    • 操作完成后,立刻释放分布式锁 (DEL stock_lock:maotai)。

    • (应用) : SQL 数据库, ACID 事务

  5. 更新缓存标记

    • 如果这次扣减导致库存变成了 0,那么这个 goroutine 还需要负责去更新 Redis 里的那个标记位 SET stock:maotai:flag "sold_out"。
战役收尾:结果通知与数据同步
  1. 结果通知

    • 秒杀处理服务在创建订单成功后,会把结果(“抢购成功,订单号是xxx”)再发送到另一个 Kafka Topic 里,比如 seckill_result_topic。
    • 有一个专门的通知服务,会订阅这个 Topic,然后通过WebSocket移动推送,把结果实时地推送给用户。
    • 对于失败的用户,可以在一段时间后统一告知“已售罄”。
    • (应用) : WebSocket
  2. 数据同步与分析

    • 订单数据、用户行为日志等,都会被持续地写入 Kafka。
    • 下游的大数据平台 (MaxCompute) 会消费这些消息,进行批处理流处理,用于生成报表、风控分析、实时大屏等。
    • (应用) : 批处理与流处理

这个过程中用到的其他“Go并发”知识

  • errgroup: 在一个服务内部,如果需要同时调用多个下游依赖(比如同时查询用户信息和风控评分),会用 errgroup 来进行并发编排,并确保一个失败,全部取消。
  • context: 贯穿全程! 从 API 网关接收到请求的那一刻起,就会创建一个带 trace_id 和超时时间的 Context。这个 Context 会被一路传递到 Kafka 消息体中,再到秒杀处理服务,再到数据库调用,实现了全链路追踪超时控制
  • singleflight: 在查询一些不那么频繁变化的热点数据时(比如商品的静态描述信息),会用 singleflight 来防止缓存击穿。

这个例子,几乎把你笔记里所有重要的分布式和并发概念都串联起来了。它清晰地展示了在面对极端并发挑战时,一个大型系统是如何通过分层、解耦、异步化和各种并发原语的组合来保证系统的稳定性和数据一致性的。

场景设定:设计B站/腾讯视频,支撑一个千万播放量、百万评论的爆款视频

  • 业务挑战

    1. 海量读取 (Read Heavy) :一个热门视频,可能会有数十万 QPS(每秒请求数)来读取它的评论。数据库根本扛不住。
    2. 海量存储:一个视频可能有几百万条评论,所有视频加起来是千亿甚至万亿级别。单张 MySQL 表无法存储。
    3. 写入/读取的实时性:用户刚发表的评论,需要近乎实时地被其他人看到。
    4. 复杂的查询需求:需要按时间排序、按热度(点赞数)排序、支持分页。

实施过程:一场“读写分离”与“多级缓存”的艺术

第一阶段:评论的写入 (Write Path) —— “异步化、分而治之”

当一个用户点击“发表评论”时:

  1. API 网关接收请求

    • 请求 POST /api/v2/comment/add 到达 B 站的 API 网关
    • 网关进行基础的认证(通过 JWT 确认用户身份)、鉴权(用户是否被禁言)、内容安全审核(调用 AI 服务检测违规内容)。
    • (应用) : API 网关, 无状态架构
  2. 评论写入服务 (Go)

    • 网关将合法的请求转发给后端的 “评论写入服务”

    • 这个服务不直接写主数据库!它会做两件事:

      1. 写入消息队列 (Kafka/RocketMQ) :将完整的评论内容(comment_id, video_id, user_id, content, timestamp)打包成一条消息,发送到 comment_add_topic。这是为了后续的数据同步和分析
      2. 写入“评论审核库” (MySQL) :将评论写入一个专门的、结构简单的审核数据库,状态为“审核中”。这个库只负责存储新评论,写入压力相对可控。
    • 写入成功后,立刻给前端返回“发表成功”。

    • (应用) : 分布式消息系统, SQL数据库 (用于写入)

  3. 异步处理与数据同步

    • 有一个后台的 “评论同步服务” (消费者),会订阅 comment_add_topic。

    • 它拿到新评论数据后,会将其写入真正的主存储系统

    • 主存储的选择:对于评论这种数据,传统 MySQL 很难水平扩展。大厂通常会选择列式 NoSQL 数据库,如 HBaseCassandra

      • 分区/分片:数据会以 video_id 作为分区键 (Partition Key) 进行水平分区。所有同一个视频的评论,都会被路由到同一个分片上,便于按视频聚合查询。
      • (应用) : NoSQL (宽列), 数据分区 (水平分区)
第二阶段:评论的读取 (Read Path) —— “层层设防,多级缓存”

当成千上万的用户刷新评论区时:

  1. 第一道防线:客户端缓存 (Client-Side Cache)

    • App/浏览器可能会在本地缓存第一页的评论。当用户短时间内反复进入页面,可以直接从本地加载,连网络请求都不发。
    • (应用) : 客户端缓存
  2. 第二道防线:CDN 缓存

    • 对于极度热门不常变化的数据(比如一个视频的“精选评论”或“置顶评论”),可以把获取这些评论的 API 接口配置到 CDN 上,缓存几十秒。
    • (应用) : CDN
  3. 第三道防线:分布式缓存集群 (Redis) —— 主力防线

    • 这是最核心的缓存层。当请求穿透 CDN 到达源站的 “评论读取服务” (Go) 时,服务绝对不会直接去查 HBase/Cassandra。

    • 它会先去 Redis 集群里查询。

    • 缓存设计

      • 按页缓存:Redis 中的 Key 设计成 comment:video_id:page_1:by_time,Value 就是第一页评论列表的 JSON 字符串。
      • 数据结构:可以用 String 存预先序列化好的 JSON,也可以用 ZSet (有序集合) 来存储评论,score 是时间戳或点赞数,便于排序和分页。
    • 绝大多数(99.9%)的读请求,都会在这一层被命中并直接返回

    • (应用) : 服务器端缓存 (Redis)

  4. 第四道防线:本地内存缓存 (groupcache / freecache)

    • 在“评论读取服务”的进程内部,还可以再加一层本地内存缓存
    • 当服务A从 Redis 加载了 video_id:123 的第一页评论后,它会把这个结果在自己的内存里再缓存几秒钟。
    • 如果下一秒,又有 100 个请求被负载均衡器打到了服务A上,要查询同一个视频的同一页评论,那么这 100 个请求会直接命中本地内存缓存,连 Redis 都不用访问了!
    • (应用) : 内存缓存 (应用内) , groupcache (如果需要节点间协作)
  5. 终极防线:数据源 (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,一个系统是如何通过层层缓存来构建纵深防御体系的。写入时通过异步化分区来分散压力,读取时则想尽一切办法避免访问底层存储

好几个月以来,非常喜欢让大模型陪我学习或者是指导我学习,虽然不能保证完全的正确性,但我还是黏上了它,分不开了啊属于是🤣你们如果觉得内容有误或者与自己的理解不同,请尽量先坚持自己的想法,这些内容仅供参考