4 年 Java 后端面 B 站:Redis 与 MySQL 深挖,三面 OC
- 轮次
- 三轮技术面+HR面
- 结果
- 已OC
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面(45 分钟,三个方向深挖)
- RDB 和 AOF 的区别是什么?生产环境怎么选?
- RDB 的 fork 操作会阻塞主线程吗?
- AOF 重写期间,新写入的命令怎么办?
- Redis Cluster 的槽位是怎么分配的?MOVED 和 ASK 重定向的区别?
- redo log、undo log、binlog 三者的区别和联系?
- 为什么需要两阶段提交?如果不做会怎样?
二面(50 分钟,已通过)
- 系统设计:设计一个高可用的直播弹幕系统。
- 项目深挖:直播互动系统的 QPS 架构分析——哪个方法耗时最多、为什么、怎么优化?
三面(交叉面,40 分钟,已通过)
- 线上故障排查:讲一个真实的 P1 级事故,说明排查过程与事后改进。
HR 面(20 分钟)
- HR 面后拿到 OC。
《参考解析》
RDB 与 AOF 的区别,生产环境怎么选
RDB 是某个时刻的全量快照(二进制压缩格式),AOF 是命令追加日志(可读文本,Redis 7 起是多文件)。三个维度的对比最实用:
- 数据安全:RDB 按
save 900 1 / 300 10 / 60 10000这类条件触发,两次快照之间宕机会丢掉这段时间的数据;AOF 的appendfsync有always(每条命令 fsync,最安全、吞吐最差)、everysec(后台线程每秒 fsync,最多丢 1 秒)、no(交给 OS,通常 30 秒量级)三档。 - 恢复速度与体积:RDB 是内存镜像,加载快、文件小;AOF 要逐条重放命令,文件大、恢复慢,但它的写入更平滑、不会像 fork 快照那样造成内存与 IO 尖峰。
- 适用场景:RDB 适合做冷备、跨机房搬运、主从全量同步(
psync后主节点发 RDB);AOF 适合对丢数据敏感的主库。
生产上的主流选择是混合持久化:appendonly yes + aof-use-rdb-preamble yes(Redis 4.0 起),AOF 重写时前半部分写成 RDB 格式、后半部分追加增量命令,重启时先加载 RDB 部分再重放增量,兼顾恢复速度和「最多丢 1 秒」。再补一句会更加分:Redis 7.0 把 AOF 改成了 multi-part 结构(一个 base 文件 + 若干 incr 文件 + manifest 清单),重写期间新命令直接写新的 incr 文件,靠 manifest 的原子替换完成切换,比老版本追加写更干净。
RDB 的 fork 会阻塞主线程吗
会,但要分两段说清楚。fork() 这个系统调用本身是在主线程里同步执行的,内核要复制父进程的页表并建立子进程结构,阻塞时长与页表大小正相关——几个 GB 的实例通常是微秒到毫秒级,内存特别大(几十 GB)或者开了透明大页时可能到几十毫秒,这期间 Redis 完全不处理命令;所以监控里 info stats 的 latest_fork_usec 是要看的指标,实践也建议把单实例内存控制在 10GB 上下、并设 vm.overcommit_memory=1。
fork 之后的快照写入完全由子进程负责,主线程继续处理请求,不阻塞。内存也不会瞬间翻倍,因为父子进程靠**写时复制(Copy-On-Write)**共享同一份物理内存页,只有某一方真的写入某页时才复制那一页。但要注意两个反直觉的点:一是「不翻倍」的前提是写入少,如果 fork 期间业务写入很猛(比如大批量 SET/EXPIRE),被复制的页会很多,实际内存增长可能接近翻倍,这就是要留 maxmemory 余量的原因;二是子进程只读不写,所以真正触发复制的是主线程。
AOF 重写期间新写入的命令怎么办
AOF 重写的目的不是「读旧 AOF 做压缩」,而是带着当前内存数据 fork 出子进程,直接生成一份新的最小命令集。问题是子进程在生成新文件的这段时间里,主线程还在接受写入,这些新命令不能丢,也不能写进正在被替换的旧文件。
做法是双缓冲:主线程写命令时,除了写正常的 aof_buf(保证当前 AOF 可用),如果此时正在重写,还会同时追加到 aof_rewrite_buf。子进程写完后通知父进程,父进程把 aof_rewrite_buf 里的增量补到新 AOF 末尾(老版本的实现是父进程通过管道把差异写给子进程,让它追加到临时文件),最后 rename() 原子替换旧文件,新命令也就无缝接上了。两个缓冲区分工明确:aof_buf 保证正常写入不丢,aof_rewrite_buf 保证重写窗口内的增量不丢。
Redis 7 的 multi-part AOF 把这件事做得更简单:base 文件是 fork 时的快照,重写期间的新命令写进一个新的 incr 文件,重写完成后更新 manifest 清单指向「新 base + 重写期间产生的 incr」,随后删除不再被引用的旧文件——整个过程没有「把增量补写到临时文件」这一步,替换是清单级的原子操作。
Redis Cluster 的槽位与 MOVED、ASK
Redis Cluster 把整个键空间划分成 16384 个槽,路由规则是 CRC16(key) mod 16384,每个主节点负责其中一段连续的槽。为什么是 16384 而不是 65536?因为集群节点之间要周期性交换心跳,心跳里带着自己负责槽位的位图:16384 个槽的位图是 2KB,65536 个槽要 8KB,在大规模集群里心跳包体积和带宽就成了问题;而 16384 个槽配到上千个节点,平均每节点也就十几个槽,够用。集群最少需要 3 主 3 从(主节点要过半数投票才能判定故障转移)。
MOVED 和 ASK 都是重定向,区别在于槽的归属是不是永久变了:
MOVED:这个槽已经确定迁移到(或本来就属于)另一个节点,客户端应该更新本地路由表(槽位缓存)以后直接发新节点。它的语义是「你以后都别来找我了」。ASK:这个槽正在迁移中,key 可能还在源节点、也可能已经到了目标节点。源节点找不到这个 key 时回ASK,客户端这次要临时重定向到目标节点,并且必须先发一条ASKING命令(否则目标节点会因为「槽不属于我」而回 MOVED),但不要更新路由表——迁移完之前槽的归属还是源节点。
迁移过程由 redis-cli --cluster reshard 驱动,按槽逐个用 MIGRATE 把 key 搬过去,期间客户端靠这两种重定向正常工作。另外,多 key 命令和事务要求所有 key 在同一个槽,所以键名里常用 hash tag({user:1000}:name)来强制同槽。
redo log、undo log、binlog 的区别,以及为什么要两阶段提交
三者的定位:redo log 是 InnoDB 的物理逻辑日志(记录「某数据页做了什么修改」),负责崩溃恢复时的前滚,保证已提交事务的持久性;undo log 是 InnoDB 的逻辑日志,记录反向操作,支撑事务回滚和 MVCC 版本链;binlog 是 Server 层的逻辑日志(statement/row/mixed 三种格式),用于主从复制和时间点恢复。前两个在 InnoDB 引擎层、循环写、有固定大小;binlog 在 Server 层、追加写、不设上限。
两阶段提交是为了让 redo 和 binlog 保持一致。反证最清楚:先写 redo 再写 binlog——redo 写完(事务已可恢复)后宕机,binlog 没落盘,重启后主库有这笔数据,但从库和备份里没有,主从不一致;先写 binlog 再写 redo——binlog 写完就宕机,主库恢复后把这笔数据丢了,从库却已经收到并应用,同样不一致。所以顺序被固定为:redo 写盘并置 prepare → 写 binlog → redo 置 commit。
崩溃恢复的判定逻辑也随之确定:扫描 redo,遇到 prepare 状态的事务,用它的 XID 到 binlog 里找对应的完整事务——binlog 完整就提交(前滚),不完整就回滚。另外要把两阶段提交和刷盘参数分开说:innodb_flush_log_at_trx_commit=1 决定 redo 每次提交都 fsync,sync_binlog=1 决定 binlog 每次提交都 fsync,「双一」最安全但每次提交两次同步刷盘,所以 MySQL 用组提交把多个事务的 fsync 合并成一次来摊薄开销。
高可用直播弹幕系统的设计与 P1 故障排查
弹幕系统的特点是:读极多、写极多、消息有房间内顺序要求、延迟要求苛刻(端到端 P99 通常要求 200ms 内)、流量在头部房间极度倾斜。分四层设计:
- 接入层:WebSocket/长连接网关(Netty 或 Go),按
roomId一致性哈希把同一房间的连接尽量落到同一网关进程,网关本身无状态(连接信息 + 房间订阅关系放 Redis / 注册中心),支持水平扩容;客户端断线重连带lastMsgId做增量拉取。 - 消息层:写请求先进 Kafka,按
roomId分区保证同房间消息有序,同时起到削峰和上下游解耦的作用;消费端做安全审核、敏感词过滤、限流风控,通过后再进入广播环节。 - 存储层:最近 N 条弹幕放 Redis(
ZSET以毫秒时间戳为 score,或用List+LTRIM),历史弹幕落 HBase/Cassandra 这类写优化的宽表,「进房看历史弹幕」走 Redis,翻旧弹幕走冷存储。 - 推送层:广播型只推给在线连接。大房间不能一条弹幕一次全量广播,要做合并推送(把 100ms 窗口内的弹幕聚合成一批下发)和写扩散降级(超过阈值只推采样后的消息,或让客户端改为按频率拉取)。
高可用还体现在降级预案上:Redis 或 Kafka 异常时关闭弹幕(或降为只读历史)、按用户维度和房间维度双重限流(令牌桶)、网关熔断慢下游、热点房间单独隔离资源。
线上 P1 事故的讲法,面试官想听的是「定位路径 + 数据 + 事后改进」,而不是「重启就好了」。一个真实例子:大促流量比预估高 3 倍,Redis 连接池被打满,接口大面积超时报警。排查顺序是先看监控确认故障范围和起始时间点,再查连接池指标(活跃连接数打满、等待队列堆积),接着用 redis-cli --hotkeys(或采样 MONITOR、看单 key QPS 监控)发现是某个直播间成了热 key,所有请求都打到同一个分片上。
处置:先做本地缓存兜住热点(Caffeine,过期时间给 510 秒,接受短暂不一致,优先保可用),再把热 key 拆成多个副本(5 倍留冗余、压测常态化、核心链路(弹幕/礼物/支付)用不同的 Redis 实例做资源隔离、限流前置到网关、热 key 探测做成自动化的(客户端上报 + 服务端统计),而不是靠出事时人工发现。key:{0..9} 随机后缀,读时随机取一个)把压力散到不同分片,同时把连接池上限、超时时间调大并给非核心接口加限流和熔断。恢复后复盘要落到机制上:容量评估按峰值 3