4. Feed 推送与滚动读取 + 互动双轨同步全链路详解
本文档是 SmartLive 项目核心链路的深度拆解,适合面试 10 分钟讲解版本。
涵盖:推拉结合 Feed 流 → 解决深翻页(Redis ZSet 按分值滚动) → 策略模式解耦聚合详情 → 高并发点赞(Redis 高速缓存) → XXL-JOB 定时刷盘(最终一致性)。
1. 链路总览图
Feed 推送与写扩散:
关注 Feed 推送与滚动读取:
互动双轨同步:
2. 每一步的技术细节与面试追问
Step 1~3:动态发布的推拉流设计
做了什么:
- 用户发布商品/博客后,异步发消息给 MQ 的
INTERACT_FEED_EXCHANGE。 FollowListener消费 MQ,防重判重后搜集粉丝列表,将动态标识(不含详情的大内容,仅含指针)推送(Push) 到各粉丝的个人信箱 Redis ZSet 内。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 为什么发动态不直接写 MySQL 关系表? | 给 1 万个粉丝发动态就要 Insert 1 万条记录的话,关系型数据库抗不住这样的"写扩散"压力,会严重拖慢链路。 |
| Feed 流的推(Push)模式和拉(Pull)模式怎么选?本系统用的哪种? | 本项目基于实际粉丝量(中小V为主)体量,采用 Push为主的推拉结合模式 或者说是 存指针(ID)不存实体(Content)。ZSet 里只发极轻的 ID,读取时再去关系库或缓存里反查内容聚合成型,既杜绝写库瓶颈又优化内存占用。 |
| 把消息塞进 Redis 信箱的时候如果 MQ 被重复消费怎么办? | 在消费者入口通过在 Redis 内写入 IdempotentKey (MessageID) 进行强一致的并发消费去重,只执行一次 Push。 |
Step 4:Redis ZSet 滚动分页(Scroll Pagination)
做了什么:
利用 ZSet 数据结构,Score 存毫秒时间戳。 由于存在同一毫秒生成多条动态的极端可能,读取时需要前端传递 max(上一页最小 Score)和 offset(偏移累进值)。
面试追问与回答:
| 问题 | 回答 |
|---|---|
传统的 Limit / Offset 为什么不能用于 Feed 流的分页? | 深翻页(Deep Paging)问题:Limit 1000000, 10 会扫描百外行丢弃后取 10 条,全表扫描拖垮性能;同时,Feed 流是实时变动的,采用传统切页,有新数据插入时会导致老数据被人往前挤而产生重复拉取或漏读。 |
| 基于 Score 的 ZREVRANGEBYSCORE 能彻底解决漏读吗?如果是同一秒的? | 因为同一毫秒内可插入 2 条记录(Score 并列),采用类似 Elasticsearch Scroll 的游标(Offset)机制补偿。每次往回查的时候:只要当前遍历值=minTime,下页请求的入参 Offset 就在本次基础上+1,完美截断掉上一轮读过的末尾并列项。 |
Step 5~6:消除 N+1 查询与策略模式组装(聚合层)
做了什么:
- 从 Redis 反解出
{shopId}:{action}:{bizType}:{id}之后,不立刻去for循环里掉表查询。 - 按不同
bizType分组把 IDCollect出来。 - 定义
ResourceStrategyFactory获取到对应实体域(商品域、博客域)的方法:resourceStrategy.getResourceList。 - 将多条碎片组合成 In 查询(批量查询Batch) 把实体一次性查出并在内存映射为
Map,然后装配进FeedVO中反给客户端。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 为什么这里费尽心机搞分组和 Batch 查询? | 如果 10 条 Feed 含 10 个不同实体,按行单个反查数据库需要发起 10 次网络 IO 往返(典型的 N+1 问题),在 Feed 流下会引发灾难性 RT(延迟)。 |
| 引入策略(Strategy)模式的好处是什么? | 消除庞大的 if-else。后续平台增加“视频 Feed”、“活动 Feed”,只要新增实现 ResourceStrategy 并注册到工厂,核心的聚合引擎代码(feedServiceImpl)无需改动任何一行。遵循开闭原则。 |
Step 7~12:互动双轨(Redis高频拦截 + XXL-JOB 兜底持久化)
做了什么:
系统面临高并发“顺手点赞、顺手收藏”的突发场景:
- 轨 1(拦截写风暴): 用户点赞,业务直接在 Redis 内用
Set结构SADD用户身份(防止重复点赞),并操作全局计数器,立即返回。数据库毫无波澜。 - 轨 2(Write-Behind异步延时): XXL-JOB(如:
interactionSyncAllDataJob)每 10 分钟拉起,启动多线程executorService分别执行:刷点赞、刷评论、刷粉丝等独立子任务。把内存增量映射为 MySQL 的批量UPDATE或INSERT ... ON DUPLICATE KEY UPDATE以覆盖长效储存。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 为什么不直接用并行的 MQ 异步写,而是设计定时 XXL-JOB 兜底? | 点赞具有极高的”波动性与覆盖性“(一秒被点赞100次最终只表现为数量+100),如果是发 MQ 会给队列造成巨量积压;而缓存在 Redis 等定时任务“按批”抓取,将无数次细碎的修改天然"折叠(Merge)"为一条 Update,是极致削峰填谷的思想体现(类似内存池)。 |
| Redis 宕机或者没来得及同步会导致点赞丢失吗? | 会。但在 CAP 定理中本模块极端贴合 AP 舍弃强一致 C。允许极低概率点赞被丢(大V差几条赞无关紧要),从而换取十倍乃至百倍以上的无感知吞吐性能。如果涉及金钱不能丢,必须切回“MQ持久化强事务”方案(如订单秒杀中的 MQ 链路)。 |
3. 整条链路涉及的核心技术点汇总
┌─────────────────────────────────────────────────────┐
│ 技术点对照表 │
├──────────────┬──────────────────────────────────────┤
│ Redis │ · ZSet (翻页) Score + Offset 机制 │
│ │ · String/Hash/Set (互动打点与状态去重)│
│ │ · 拦截 99% 写入(Write-Behind 异步写回│
├──────────────┼──────────────────────────────────────┤
│ RabbitMQ │ · 解耦推送模型主流程 │
│ │ · tryConsumeOnce(消费强幂等校验) │
├──────────────┼──────────────────────────────────────┤
│ MySQL │ · Batch Batch(批读取代替 N+1 ) │
│ │ · 最终数据持久化地 │
├──────────────┼──────────────────────────────────────┤
│ 架构流派 │ · 推拉结合 (存ID为主的数据信箱) │
│ │ · 双轨模型 (Redis第一层, 异步拉替MQ缓冲│
├──────────────┼──────────────────────────────────────┤
│ 设计模式 │ · 策略模式(Strategy + Factory) │
│ │ · 模板方法(抽象层处理重复逻辑) │
├──────────────┼──────────────────────────────────────┤
│ 性能优化 │ · SISMEMBER / SADD O(1)时延拦截 │
│ │ · ZREVRANGEBYSCORE 滑动指标不扫前表 │
└──────────────┴──────────────────────────────────────┘4. 面试 10 分钟讲述模板
讲述思路:以痛点引入,先主流程(看动态),后高频写(发互动)。
开场(1 分钟)
"在 SmartLive 项目中,除了交易,另一大硬实力就是内容与互动。为了抗住类似微博那样的海量粉丝 Feed 流刷新和热门作品的并发点赞,我设计了基于 Redis 滚动分页的轻推流模型,以及 XXL-JOB 支撑的高速双轨互动体系。此设计的核心目标是:拒绝全表深扫,根治 N+1 查询,拦截无效突发写库,高度解耦。"
主流程:Feed 流派发机制(2.5 分钟)
"大V派发动态时,如果同步写,光查寻粉丝就能拖死主线程,所以我采用 MQ 发出业务事件,后台 Listener 去把动态指针(仅包含业务类别与ID)异步 Push 到粉丝基于 Redis 保证的 ZSet 信箱。以 Timestamp 为 Score,天然排序防乱序。"
难点攻克:滚动分页避免深扫 O(n)(2 分钟)
"传统的下拉加载做 Limit/Offset 在数据多时必然慢,我改为 Elasticsearch 那套游标(Scroll)思路运行在 ZSet 上。前台每次把时间戳
max和同分offset上报回来。通过ZREVRANGEBYSCORE只往特定的时间截面下游走,这样不管用户往下滑 100 页还是 1000 页,耗时死死压在 O(1) 级别常量。"
难点攻克:策略聚合消减 N+1(1.5 分钟)
"为了渲染详情,不能拿 Redis 里的 ID 直接查库(那是典型的 N+1)。我用 Java 策略加简单工厂,按
bizType归纳出一个列表组,最终用一条 SQL(也就是 Batch in)拿到详情后合并到返回 VO 数组的指定槽位里。"
异常处理与双规兜底体系:点赞同步(2 分钟)
"所有互动比如收藏评论全部禁止直接打库。用户点赞一瞬间,Redis Set 就已经写下记录并返回。剩下的持久化,我利用 XXL-JOB 定期执行的多线程子任务在后台将其批量汇总为一个 Update 发给 MySQL(Write-Behind机制),通过极端折叠合并请求,完全屏蔽了脉冲式的高频打库。"
总结亮点(1 分钟)
"该链路做到了把重业务化整为碎片(通过 MQ + 游标),和把碎片业务并拢为大块(通过策略批量 In + 定时同步落地折叠),可以说是架构在吞吐能力压榨上的极致应用。"
5. 高频追问速查表
| 追问方向 | 关键问题 | 核心回答 |
|---|---|---|
| 模式选型 | 大V如果是李佳琦粉丝千万级怎么发? | 千万粉的话纯 Push 会撑爆 Redis 集群,应当引入真正的推拉结合:超级大V发动态存“发件箱”(Pull),普通用户存“收件箱”(Push)。本阶段属于百万人级中低体量的 Push 为主模型,但也做了指针化极大缩减内存。 |
| 深翻页补偿 | Offset 为何能防同一毫秒发好几条的Bug? | Redis 是有序 Zset 的。在遍历元素判定 Score = MaxScore 且在上一轮查过了时,Offset自加1,下一页取范围的头N个并列项将被自动剪接丢弃从 N+1 的项读起。 |
| 防重复写入 | MQ 如果宕机复活导致重复发怎么办? | 推送用的是 RabbitMQ,使用我配置在 Redis 的 tryConsumeOnce(根据MessageID)锁定防重,重入即踢掉。 |
| 一致性权衡 | 点赞丢了有补偿么?还是任其丢失? | 本设计是高度 AP 的 Write-behind(先响应后排队)架构。如果有极端高要求,可以额外增加一份“流水记录”进 MQ/Kafka 或 AOF 进行回放补录,但对于单纯点赞,权衡之下允许万分之一的抖动。 |
| 并发 | 你的批量聚合(策略模式组装详细)是用多线程吗? | 只是在业务内部的 HashMap Collect 及一条 In SQL而已,如果在更巨构网关化体系下可用 CompletableFuture 将查不同领域的 RPC 查并行发出。 |
6. 扩展:这条链路和八股的关联
推流互动链路 ←→ 八股知识点映射
Redis 八股:
├── 数据结构深入应用(ZSet 排行及聚合特性)
├── Redis 命令(ZREVRANGEBYSCORE / SISMEMBER / SADD)
└── 高并发写与内存换硬盘(Write-behind Caching 模式)
设计模式八股:
├── 策略模式(开闭原则增加 Feed 详情支持类型)
└── 工厂模式(对外抹平各种 bizType 怎么分库获取的具体执行)
MQ / 并发八股:
├── 消息中间件解耦与防止长调用雪崩
└── 接口幂等性通用设计(利用 Redis SETNX/MessageID去重)
MySQL 数据库八股:
├── 深翻页性能衰退原理与其规避方案(基于游标键索引走主键回表)
├── 大批量 In 查询替换连续 Select 的 IO 资源节约法则
└── 锁机制(把脉冲的行锁升级为异步按批的宽容锁)
架构思想:
├── Push 推演与 Pull 拉演(架构取舍 Trade-off能力)
├── 批量思想(Batch / Bulk / Fold)聚合打捆能力
└── C与AP的权衡取舍:订单业务走(ACID); 点赞业务走(BASE)一句话总结:Feed 和互动的核心就是 "异步推流解耦 + ZSet滚动游标防全表 + 策略聚集查防N+1 + 缓存异步收口拒强写"。理解上述逻辑你就不再是那种只会
Limit offset拼接和满地图调Mapper.insert的初级兵了。