怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际短视频内容平台的服务端,Java + Spring Cloud。日活几十万量级,日均上传视频几万条。核心链路是上传转码分发信息流拉取高频互动计数
为什么选这三个模块 内容平台的后端难点不在增删改查,而在异步长任务的编排多数据源的低延迟聚合读写比极度失衡下的计数。这三个都是能讲出容量估算和取舍的题目,也是面试官最容易顺着往下问的方向。

模块一:上传转码与分发链路

  1. 上传转码与分发链路(直传 + 异步转码编排 + 多码率 + CDN 预热)★★★
    简历这样写 视频上传转码与分发链路(Spring Boot + 对象存储直传 + 消息队列 + 转码集群 + CDN):设计客户端凭时效凭证直传、服务端事件驱动编排转码的链路,转码拆分为「探测 → 切片 → 多码率并行 → 封面抽帧 → 合并上架」并支持单阶段失败重试与幂等;上架前按热度预热 CDN 首片。10 分钟素材的转码总耗时由约 7 分钟降至 3 分钟上下(切片并行),首帧起播 P95 由约 1.6s 降至 800ms 上下
    展开完整拆解
    为什么要这么设计

    第一版是最直白的同步流程:客户端把文件传给业务服务,业务服务落盘、调转码、等转码完、写库、返回成功。这个流程在测试环境跑得通,一上量就全线崩。

    一是业务服务被文件流量打死。几万条视频、每条几十兆走同一批应用服务器,带宽和内存都撑不住,而且上传占用的线程会长时间挂住,正常接口跟着被拖慢。二是转码是分钟级的任务,压根不能放在请求里等,HTTP 超时、网关超时、用户切后台,任何一个都会让这次上传白费。三是失败没法恢复——转码在第 8 分钟失败了,前面 7 分钟的工作全部丢弃,重来一遍。

    所以三个改造方向:文件不经过业务服务(直传)、长任务彻底异步化(事件驱动)、把大任务拆成可重试的小阶段(阶段化 + 幂等)。第三点是最关键的——它把「一个 8 分钟的不可靠操作」变成了「若干个几十秒的可重试操作」,可靠性是质变而不是量变。

    整体链路
    客户端 业务服务 消息队列 转码集群 对象存储/CDN │ │ │ │ │ ├─ 申请凭证 ──────▶│ │ │ │ │◀─ 凭证+objectKey │ │ │ │ ├─ 直传文件 ───────────────────────────────────────────────────────▶ │ ├─ 通知上传完成 ──▶│ 建视频记录 │ │ │ │ │ 状态=待转码 │ │ │ │ ├─ 发转码事件 ───▶│ │ │ │ │ ├─ 消费 ────────▶│ │ │ │ │ ├─ 1 探测(时长/编码/分辨率) │ │ │ ├─ 2 切片(按 GOP 切成 N 段) │ │ │ ├─ 3 各段 x 各码率 并行转 │ │ │ │ └─ 分发到多个 worker │ │ │ ├─ 4 抽封面帧 │ │ │ ├─ 5 合并 + 生成播放清单 │ │ │ └─ 6 回调上架 ─────▶ 预热首片 │ │◀─ 上架事件 ─────┤ │ │ │ │ 状态=可播放 │ │ │ │◀─ 轮询/推送 ─────┤ │ │ │ 每个阶段:入口先查「本阶段产物是否已存在」→ 存在则跳过(幂等) 失败 → 单阶段重试(指数退避,上限后进死信人工介入)

    要点是状态机 + 阶段产物落地。视频记录上有明确的状态(待转码 / 转码中 / 转码失败 / 已上架 / 已下架),每个阶段的产物(切片、各码率文件、封面)都落在对象存储的确定路径上。重试时先看产物在不在,在就跳过——这是幂等的实现基础。

    分步拆解
    1. 直传凭证要严格限制。签发的凭证限定路径前缀、文件大小上限、内容类型、有效期。路径前缀带上用户 ID,防止用户传到别人的目录;大小上限防止被当网盘;有效期几分钟到十几分钟,够传完就行。
    2. 「上传完成」不能只信客户端。客户端可能撒谎说传完了,或者传了个空文件。服务端收到通知后要去对象存储核实文件存在且大小合理,再进入转码。更稳的是同时监听对象存储的上传完成事件作为兜底——两条路径都可能触发,靠幂等保证只处理一次。
    3. 转码前先探测。用 ffprobe 拿到时长、编码、分辨率、码率、旋转角。这一步决定了后面要出哪几档清晰度(源片本身是 480p 就没必要出 1080p)、要不要处理旋转(手机竖拍的视频有旋转元数据,不处理会出现画面躺倒)。探测很快但省掉大量无效工作。
    4. 切片并行是耗时下降的主要来源。把视频按关键帧边界切成若干段,各段独立转码后再合并。必须按 GOP(关键帧)边界切,否则拼接处会花屏或音画不同步。并行度受集群资源限制,不是越高越好——所有任务都抢并行会导致每个任务都慢。
    5. 多码率是必须的。国际业务的网络差异极大,只出一档清晰度必然有一部分用户看不了。通常出 360p / 720p / 1080p 三档,配合播放清单让客户端按实测带宽选。
    6. 上架前预热 CDN 首片。视频刚上架时 CDN 没有缓存,第一批观众会回源,首帧慢。做法是上架前主动请求一次首片让 CDN 缓存住。不是所有视频都值得预热——按作者粉丝量或者预估热度分级,只预热可能有量的,否则几万条全预热是巨大的浪费。
    7. 失败要分类处理。「源文件损坏」「格式不支持」是不可重试的,直接标失败并给用户可理解的提示;「worker 宕机」「临时网络错误」是可重试的,走退避重试。把两类混在一起会导致坏文件被无限重试,把队列堵死。
    关键决策与取舍

    为什么不用现成的云转码服务。评估过,云服务省事但两个问题:成本(按分钟计费,量大之后比自建集群贵不少)和可控性(转码参数、水印、审核插桩这些定制需求受限)。最后的选择是混合——自建集群跑常规任务,突发流量溢出到云服务。这个方案的代价是要维护两套接入,但避免了「集群按峰值容量建设」的浪费。

    切片并行的代价。并行度上去了,但引入了新问题:各段的编码参数必须完全一致,否则合并后码率突变,播放器会卡顿甚至报错。而且合并阶段成了新的串行瓶颈。实测下来切片并行对长视频收益大(10 分钟素材节省一半以上),对短视频反而是负收益(切片和合并的固定开销超过并行收益)。所以按时长分流:短视频不切片直接转,长视频才走切片。这个「优化不是无条件生效」的判断是能加分的。

    踩过的坑:消息重复消费导致重复转码。队列的投递语义是至少一次,同一个转码事件可能被消费两次,两个 worker 同时转同一个视频,浪费资源还可能互相覆盖产物。修法是用视频 ID 加阶段名作为分布式锁的 key,抢到锁才执行,同时阶段入口检查产物是否已存在。幂等和锁要一起用——锁防并发,幂等防重复执行。

    没做的部分:没做转码任务的优先级队列。实际上大 V 的视频应该优先转码,我们是先到先转。这在流量平稳时没问题,但活动期间队列积压时,头部作者的内容和普通用户的内容一起排队,业务上不合理。

    数字是怎么测的

    转码耗时:固定一批测试素材(几个不同时长和分辨率的样本),在集群空载的条件下跑,记录从消费转码事件到上架事件的时间差。「10 分钟素材 7 分钟降到 3 分钟」是这个条件下的数字,必须说明是空载测的——集群有负载时的耗时完全取决于排队情况,那个数字不能用来说明优化效果。

    首帧起播 P95:这是客户端埋点,从「开始请求视频」到「第一帧渲染」的耗时,按国家或地区分别看。取 P95 而不是平均值,因为平均值会被大量的本地快速命中拉低,掩盖掉真正的问题用户。要说清是哪个地区的数字——国际业务里不同地区的 CDN 覆盖差异很大,混在一起算没有意义。

    为什么不报「转码成功率从 X% 提升到 Y%」:因为分母很难界定——用户传了个损坏文件算失败吗?这类比率一被追问分母就说不清。改用「阶段化之后失败只需重试单个阶段,重试后的最终完成率接近全部,人工介入的死信每天个位数」这种绝对数描述更稳。

    面试追问
    Q:转码任务失败重试,怎么保证不会重复扣用户配额或者产生两条视频记录? A:靠状态机加唯一约束。视频记录在「上传完成」时就已创建,转码只是更新它的状态和关联的转码产物,不会新建记录。状态流转用条件更新(update ... where status = '转码中'),重复执行时第二次影响行数为 0,直接跳过后续逻辑。配额是在创建记录时扣的,和转码重试无关。关键是把「创建」和「加工」分开——创建只发生一次且有唯一约束保护,加工可以重复执行但幂等。
    Q:切片并行转码,怎么保证合并后音画同步? A:几个必须做的点。一是按关键帧边界切,不能在任意时间点切,否则该段开头没有 I 帧,解码不出来。二是各段的编码参数严格一致——分辨率、帧率、码率控制方式、音频采样率,任何一项不一致合并后都会出问题。三是音频单独处理,通常不切片,整条音轨单独转一次再和合并后的视频轨复用,这样避免各段音频拼接处的爆音和累积漂移。实际做的时候音画不同步的问题基本都出在音频分段上,所以直接不切音频是最省心的方案。
    Q:CDN 预热怎么判断哪些视频值得预热?成本怎么控制? A:按预估播放量分级。最简单可用的信号是作者的历史平均播放量和粉丝数,配合内容类型。分三档:头部作者上架即预热多个节点;中部作者预热主要地区;长尾不预热,靠自然回源。成本控制的关键是预热只做首片而不是整个视频——首帧起播是体验的关键,后续片段用户看到那里时已经有几秒时间去回源了。另外可以做被动升级:某条视频播放量突然起来,触发一次全量预热,这比事前预测准得多。
    Q:如果转码集群整体挂了,或者积压了几万个任务,怎么办? A:这是容量和降级的问题。监控上要看队列积压量和最老消息的年龄,积压超过阈值就告警。处理上分几步:先扩容 worker(转码是 CPU 密集且无状态,扩容有效);扩不动就溢出到云转码服务;再不行就降级转码档位——只出一档最低清晰度让视频先能看,高清档位排到低优先级队列后面补。最差的情况是对上传做限流,但那要产品同意,因为会直接影响创作者。这里的核心判断是「先能看」远好于「等高清」,降级的方向要顺着这个判断走。

模块二:信息流拉取与召回

  1. 信息流拉取与召回(多路并行召回 + 曝光去重 + 游标分页)★★★
    简历这样写 信息流拉取服务(Spring Boot + 多路召回并行编排 + Redis + 布隆过滤器 + 游标分页):将串行的召回链路改为多路并行召回 + 超时降级(关注流 / 热门 / 兴趣标签 / 冷启动兜底),用布隆过滤器做曝光去重避免重复推送,分页从 offset 改为基于游标的稳定分页。Feed 接口 P95 由约 430ms 降至 150ms 上下(压测 200 QPS),单次请求的重复内容由每页可见数条降至偶发
    展开完整拆解
    为什么要这么设计

    第一版的 Feed 接口是串行的:先查关注的人的新视频,不够就补热门,还不够就补兴趣推荐,最后过滤已看过的。每一路都是一次或多次远程调用,串起来 P95 到了 430ms,而且任何一路慢了整个接口就慢

    另外两个体验问题更致命。一是重复内容——用户刷到第三页会看到第一页出现过的视频,因为不同召回路径可能返回同一条,而去重只在单次请求内做。二是翻页错乱——用 offset 分页,翻页期间有新内容插入到前面,第二页就会出现第一页看过的内容(或者跳过一些)。

    所以三个改造:召回并行化并允许单路降级曝光去重做成跨请求持久的分页改成游标式。这三个问题的共同特征是:它们都是「多数据源聚合」这个模式的固有问题,不是某一处的 bug。

    整体链路
    请求(userId, cursor, size) │ ├─ 读用户画像 + 上次游标(缓存) │ ├─ 并行发起多路召回(线程池 / CompletableFuture,各路独立超时) │ ├─ 关注流:查关注的人的近期视频(时间序) │ ├─ 热门流:按地区的热榜(缓存,秒级更新) │ ├─ 兴趣流:调推荐服务(超时 80ms,超时则放弃这一路) │ └─ 冷启动:新用户兜底的优质内容池 │ ├─ 汇聚(等全部完成或整体超时 120ms,已完成的先用) │ ├─ 曝光去重:布隆过滤器(key=用户,滚动窗口) │ └─ 命中则丢弃;未命中则保留并写入过滤器 │ ├─ 混排:按各路配额和权重打散(避免连续同类型/同作者) │ ├─ 补齐视频详情(批量查,走缓存;缺失的批量回源) │ └─ 返回 items + nextCursor(cursor 编码了各路各自的位点) 降级:兴趣流超时 → 用热门流补足配额,接口不失败 整体超时 → 返回已拿到的部分,宁可少几条也不能慢
    分步拆解
    1. 并行召回的关键是「各路独立超时 + 允许缺失」。CompletableFuture 并行发起,每路设自己的超时。整体等待时间取决于最慢的一路,所以必须有整体超时,到点就用已经拿到的结果拼一个可用的 Feed 返回。Feed 场景下「少几条」用户几乎感知不到,「慢一秒」则明显。
    2. 线程池要隔离。各路召回用不同的线程池(或者至少和主业务隔离),否则推荐服务抖动时把线程占满,连关注流也查不出来了。这是舱壁模式的典型应用场景。
    3. 曝光去重用布隆过滤器。用户看过的视频 ID 集合可能很大,用 Set 存内存占用太高。布隆过滤器空间小、判断快,代价是有误判(可能把没看过的判成看过)。这个代价在 Feed 场景可以接受——少推一条内容无所谓,重复推同一条则很伤体验。方向选对了:宁可漏推不可重推。
    4. 过滤器要做滚动窗口。不能无限累积,否则时间长了误判率上升、内存也涨。做法是按时间分桶(比如按天),查的时候查最近几个桶的并集,过期桶自动淘汰。这样也顺便实现了「很久以前看过的可以再推」。
    5. 游标分页替代 offset。游标里编码各路召回的位点(比如关注流的时间戳、热门流的排名位置),下次请求带回来接着往后取。这样新内容插入不影响已翻过的页。游标要签名或者加密,防止用户篡改位点越界拿数据。
    6. 混排避免连续同类。纯按分数排序会出现「连续五条同一个作者」或者「连续都是热门内容」,体验单调。按各路配额(比如关注 4 条、热门 3 条、兴趣 3 条)取,再打散同作者和同标签的相邻项。
    7. 详情批量补齐 + 多级缓存。召回阶段只拿到视频 ID,详情(标题、封面、作者、计数)要批量查。本地缓存放热点视频,Redis 放全量,都没有才回源数据库。批量查询必须控制单批大小,一次几百个 key 的 MGET 会造成 Redis 的单次操作耗时飙升,影响同实例的其他请求。
    关键决策与取舍

    为什么不做完全的推模式(预先算好每个用户的 Feed 存起来)。推模式读的时候最快,但存储和计算成本随用户数线性增长,而且大部分用户当天压根不来,算了也是浪费。我们的量级下选了读时聚合 + 重度缓存:热榜和兴趣召回的结果是预先算好并缓存的(这部分是「推」),但最终的混排和去重是请求时做的(这部分是「拉」)。这个折中的判断依据是「用户活跃比例」和「内容时效性」——活跃比例低、内容时效强的场景,读时聚合更划算。

    布隆过滤器的误判要有兜底。误判会导致用户永远看不到某条内容。对普通内容无所谓,但如果是用户关注的人刚发的视频被误判过滤了,用户会明确察觉「我关注的人发了我没看到」。所以做了区分:关注流不走布隆过滤,用精确的 Set 存最近一段时间的已读(关注流的量级小得多,存得下);热门和兴趣流才走布隆。按数据重要性选择不同的去重精度,是这个模块最值得讲的取舍。

    踩过的坑:并行召回之后 P95 反而更差。改成并行后压测发现 P95 比串行还高。原因是线程池配置不当——核心线程数太小、队列很长,请求量上来后任务在队列里排队,等待时间比串行执行还久。修法是把队列改短、核心线程数按「并发请求数乘以召回路数」估算,并且队列满了直接走降级而不是排队。这个坑很典型:并行化本身不会带来性能,配置错了反而更慢。

    数字是怎么测的

    接口 P95:压测环境用 50 到 200 QPS 的梯度压,取服务端埋点的耗时分布。要说清压测的数据规模——用几万个用户、几十万条视频的数据集压出来的数字,和用 100 个用户压出来的完全不同(后者全部命中缓存,压不出问题)。「压测 200 QPS 下 P95 从 430ms 到 150ms」这个描述里,QPS 和数据规模都要能报出来。

    重复内容:写脚本模拟一个用户连续翻 10 页,统计出现过两次以上的视频 ID 数量。改造前每页能看到几条重复,改造后偶发(布隆过滤器的窗口边界和并发请求会有极少数漏网)。用「每页可见数条」和「偶发」这种描述,比编一个「重复率 0.03%」更可信——后者一被问「你怎么统计的分母」就答不上来。

    不要报「Feed 点击率提升 X%」。那是推荐算法的指标,不是接口优化的成果,而且受太多因素影响。把工程指标和业务指标分清楚,往自己身上揽不属于自己的指标是最容易被问穿的。

    面试追问
    Q:布隆过滤器会误判,用户看不到本该看到的内容,这个问题你怎么权衡的? A:先看误判的方向——布隆过滤器只会把「没看过」误判成「看过」,不会反过来,所以后果是漏推而不是重推。在 Feed 场景漏推一条内容用户压根不会知道(候选池有几十万条),但重推同一条用户立刻能看出来。所以方向是可接受的。不过我做了分级:关注流用精确 Set,因为「我关注的人发的视频我没看到」是用户能明确察觉的;热门和兴趣流才用布隆。另外误判率可以通过调整位数组大小和哈希函数个数控制,我们控制在很低的水平,代价是内存多用一些。
    Q:游标分页具体怎么设计的?多路召回的游标怎么合成一个? A:游标是一个结构体序列化后编码的字符串,里面存每一路各自的位点:关注流存最后一条的发布时间加视频 ID(时间可能重复,要用 ID 兜底保证严格有序);热门流存榜单版本号加排名位置(存版本号是为了榜单更新后不错乱);兴趣流存推荐服务返回的分页 token。返回给客户端前做签名,下次请求校验签名,防止用户篡改位点去越界拿数据。这里有个细节——游标不能存 offset,那等于换个壳的 offset 分页,插入新数据一样会错乱。
    Q:热榜是怎么算的?实时性和成本怎么平衡? A:不做严格实时。用滑动窗口统计 + 定时任务:互动行为写入流式处理,按分钟粒度聚合到 Redis 的 ZSet,定时任务每几十秒重算一次带时间衰减的热度分并刷新榜单缓存。秒级延迟对热榜完全够用——用户不会察觉一条视频晚半分钟上榜。追求实时会带来巨大的计算成本而收益接近零,这是典型的「需求上不必要的实时性」。另外要按地区分榜,国际业务里全球一个榜没有意义。
    Q:如果推荐服务完全挂了,Feed 还能用吗? A:能,这是并行召回加降级的设计目的。推荐服务超时或熔断后这一路直接放弃,用热门流和关注流补足配额,用户拿到的 Feed 质量会下降(个性化没了),但不会白屏。兜底内容池也要提前准备好——一批运营挑选的优质内容,缓存在本地,即使 Redis 也挂了还能返回。这里的原则是Feed 接口必须永远返回内容,绝不返回错误,因为它是首页,报错等于应用不可用。

模块三:高频互动计数

  1. 高频互动计数(缓存承接 + 批量合并落库 + 最终一致)★★★
    简历这样写 点赞与播放量计数体系(Redis + 消息队列 + 批量合并落库 + 定时校准):点赞、播放量等高频写场景改为Redis 承接写入并作为读源异步批量合并落库,用户与内容的点赞关系用去重集合 + 幂等写保证不重复;配套定时校准任务修正缓存与数据库的偏差。压测 5000 QPS 下点赞接口 P95 约 25ms,数据库写入 QPS 由与请求量持平降至 200 上下(批量合并),缓存与库的计数偏差在校准周期内收敛。
    展开完整拆解
    为什么要这么设计

    最初的实现是「点赞就 update video set like_count = like_count + 1」。问题在压测时立刻暴露:

    一是热点行锁竞争。一条爆款视频同一秒有几千人点赞,全部去更新同一行,行锁排队,接口耗时从几毫秒涨到几百毫秒甚至超时。二是数据库写压力和请求量 1:1,点赞的 QPS 就是数据库的写 QPS,扛不住。三是重复点赞没防住——用户快速连点,或者客户端重试,会导致计数多加。

    核心矛盾是:计数是「读多写多但精度要求不高」的数据,用强一致的数据库更新是错配。用户不会核对一条视频到底是 10238 还是 10239 个赞,但会明确感知「点了赞数字没变」和「点赞转圈半秒」。所以设计方向是用缓存承接读写,异步合并落库,牺牲强一致换吞吐和延迟

    整体链路
    点赞请求(userId, videoId) │ ├─ 幂等判断:SADD like:set:{videoId} userId │ └─ 返回 0(已存在)→ 直接返回成功,不重复计数 │ └─ 返回 1(新增) → 继续 │ ├─ INCR like:cnt:{videoId} ← 读接口直接读这个 │ ├─ 发送计数变更消息(videoId, +1)→ 队列 │ └─ 返回成功(此时数据库还没动) 消费侧(批量合并) │ ├─ 攒批:按 videoId 聚合,攒 N 条或 T 毫秒 │ { videoA: +37, videoB: +5, videoC: -2 } │ └─ 批量更新数据库(一条 SQL 更新多行 / 分组更新) └─ 100 次点赞 → 1 次数据库写 校准(定时) ├─ 以点赞关系表为准,重算真实计数 └─ 与缓存对比,偏差超阈值则修正缓存 + 告警 读接口 ├─ 优先读 Redis 计数 └─ 缓存缺失 → 从数据库读并回填(防击穿用互斥或空值占位)
    分步拆解
    1. 幂等靠去重集合。SADD 的返回值判断是不是新增——返回 1 说明之前没点过,返回 0 说明已经点过。这一步既是业务判重也是幂等保证,客户端重试多少次结果都一样。集合太大的话要考虑分片或者只存热点视频的,冷视频回源查关系表。
    2. 计数用 Redis 的原子自增。INCR 是单线程原子操作,没有锁竞争问题,耗时在亚毫秒级。读接口也直接读这个值,不读数据库——这样读写都在缓存,数据库完全不在关键路径上。
    3. 批量合并是数据库压力下降的关键。消费端不是收到一条就写一次,而是攒一批(按数量或时间触发)后按视频 ID 聚合再写。一个爆款视频一秒几千次点赞,合并后是一次「加 3000」。这是数据库写 QPS 从和请求量持平降到 200 的原因。
    4. 点赞关系表必须落库。计数可以最终一致,但「谁点了赞」是要持久化的业务数据(用户要看自己点赞过的列表,也是计数校准的依据)。这部分走正常的异步写入,用用户 ID 加视频 ID 的唯一索引兜底防重。
    5. 取消点赞用同样的路径。SREM 判断是否真的存在,存在才 DECR 并发消息。要防止计数被减成负数——Redis 层面用 Lua 脚本判断后再减,或者读的时候做下限保护。压测里很容易压出负数,因为并发的加和减顺序不可控。
    6. 定时校准修正偏差。缓存和数据库长期运行必然有偏差(消息丢失、缓存被淘汰、异常回滚)。定时任务以点赞关系表为准重算真实值,和缓存对比,超过阈值就修正并告警。告警比修正更重要——偏差持续变大说明链路有 bug,光修数据是治标。
    7. 播放量比点赞更宽松。播放量的量级比点赞大一到两个数量级,而精度要求更低。所以播放量可以客户端批量上报(不是每次播放都发请求,攒几条一起发),服务端也可以采样统计后按比例放大。这个分级处理要说出来,说明你理解不同数据的精度要求不同。
    关键决策与取舍

    为什么敢让 Redis 当读源,不怕丢数据。因为丢的是计数不是关系。点赞关系在数据库里是完整的,Redis 的计数即使整体丢失,也能从关系表重算恢复。所以最坏情况是「计数短时间不准,重算后恢复」,不是「数据丢了」。这个判断的前提是把「可重算的派生数据」和「不可重算的源数据」分开——源数据走强一致,派生数据可以放在缓存里。

    为什么不用数据库的原子更新加分段(把一行拆成多行分摊锁竞争)。那也是一种方案(类似分段锁的思路),能缓解热点行问题。但它没有解决数据库写 QPS 和请求量 1:1 的问题,只是把一行的锁分散到多行。量级再上去还是会顶到数据库的写入上限。所以选了缓存承接加批量合并——它同时解决了锁竞争和写放大。

    踩过的坑:Redis 计数被淘汰导致数字跳变。计数 key 如果设了过期时间或者被内存淘汰策略清掉,读接口会回源数据库,而数据库里的值是批量合并落库的、可能滞后几秒,于是用户看到点赞数突然变小了。修法有两个:计数 key 不设过期(用单独的实例或者配置 noeviction 相关的隔离),以及回源时取数据库值和本地记录的最大值。这个坑在压测里不容易发现,是线上偶发的用户反馈才定位到的。

    没做的部分:没做多机房的计数合并。如果业务扩展到多个地域各自有 Redis,计数怎么汇总是个新问题(要么单点写、要么各自计数最后合并,都有取舍)。当时是单机房,这个问题没遇到,但要能说出它是个已知的未解决问题。

    数字是怎么测的

    点赞接口 P95:压测工具按 1000 / 3000 / 5000 QPS 梯度压,请求要打散到多个视频 ID,同时也要有一个热点 ID 承接高比例流量——只压随机 ID 压不出热点问题,只压单个 ID 又不真实。取服务端埋点耗时的 P95,5000 QPS 下约 25ms。

    数据库写 QPS:压测期间看数据库的监控指标(写入 QPS 或者 TPS)。改造前和请求量基本持平(5000 左右),改造后稳定在 200 上下。这个数字是批量大小和时间窗口共同决定的,被追问时要能说出攒批的参数以及为什么这么设——批太大延迟高,太小合并效果差。

    偏差收敛:校准任务跑完后对比修正的记录数和偏差幅度。不要说「偏差为 0」——异步链路不可能没有偏差,说 0 反而说明没测过。说「偏差在校准周期内收敛」「单次校准修正的记录是个位数到十几条」这种描述更真实。

    面试追问
    Q:用户点了赞,刷新页面发现赞数没变,怎么解决? A:这个场景其实不该发生,因为读接口读的是 Redis 计数,而写入时已经 INCR 过了,写完立刻读能读到。真正会出问题的是两种情况:一是读走了本地缓存,本地缓存还是旧值,解法是点赞成功后客户端本地先更新显示(乐观更新),并且本地缓存的 TTL 设得很短;二是Redis 主从延迟,写主读从的话可能读到旧值,解法是计数这类关键读走主节点,或者接受秒级延迟。这里有个产品层面的常用做法——客户端点赞后直接本地加一,不等服务端返回,服务端失败了再回滚显示,体验最好。
    Q:批量合并落库,如果消费端在攒批的过程中挂了,这批数据丢了怎么办? A:会丢一批(几十到几百次点赞的增量),但不影响正确性的最终恢复,因为点赞关系表是完整的,校准任务会以关系表为准重算并修正。这就是为什么校准任务是必需的,不是可选的优化。如果要减少丢失窗口,可以攒批的同时把批次内容写一份到持久化存储,重启时恢复未提交的批次——但那增加了复杂度,而校准任务已经能兜住,所以我们没做。这里的判断是:有了兜底机制之后,中间环节可以允许丢失。
    Q:去重集合会不会太大?一条爆款视频几百万赞,Redis 存得下吗? A:会有问题。几百万个用户 ID 的集合,一个 key 就是几十兆,属于大 key——它会导致内存分布不均、迁移和删除时阻塞、SMEMBERS 之类的操作直接卡住实例。处理办法几个:按用户 ID 哈希分片成多个小 key;只对热点视频用集合,冷视频直接查关系表(冷视频的 QPS 低,查库没压力);或者用布隆过滤器替代集合做判重(但会有误判导致少数用户点不了赞,体验上不能接受,所以这个方案不适合)。我们用的是分片加冷热分离。
    Q:播放量为什么可以采样,点赞不行? A:因为两者的业务语义和精度要求不同。播放量是展示性指标,用户看到 12.3 万还是 12.4 万完全无感,而它的量级极大(一次播放就是一次上报,比点赞多一两个数量级),采样能大幅降成本。点赞不行,因为点赞是用户的个人行为,有明确的预期——我点了赞,我的点赞列表里必须有这条,而且数字应该加一。采样会导致「我点了但数字没变」,这是用户能直接察觉的错误。判断标准是:这个数据用户能不能感知到自己那一份。

没有匹配的内容,换个关键词试试。

项目拆解 · 短视频内容平台(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据