得物Golang一面:混合分页与Redis集群扩缩容
- 轮次
- 一面
- base
- 长沙
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍,然后问项目
- 场景题:前端一个翻页列表,每页要同时展示两个表的混合数据,并且要求按照两个表混合后的时间排序输出,如何设计?
- Kafka 为什么性能好?
- 缓存三剑客和解决方案
- Redis 集群的扩缩容,slot 如何迁移?
- inner join 和 left join 有什么区别?
- 介绍 B+ 树
- MQ 的作用
- 聚簇索引和非聚簇索引
- 非聚簇索引存的什么数据
- MVCC 是什么,有什么作用
《参考解析》
两个表混合分页排序的设计:这是典型的异构数据源归并分页,核心矛盾是「排序键分散在两张表,无法用单表 limit 完成全局有序」。按数据规模和实时性分三档给方案。第一档是同库同实例,直接用 UNION ALL 把两张表按统一字段(时间、类型标识)投影成同构结果集,外层套 ORDER BY create_time DESC LIMIT ?, ?;缺点是无法走单一索引,深分页会退化成全量排序。第二档是数据量大、需要深分页,走游标分页:客户端带上上一页最后一条的 (create_time, id),两张表各自用 WHERE create_time < ? OR (create_time = ? AND id < ?) 各取 N 条,应用层做归并排序取前 N 条返回,并把新的游标带回去;这样每张表都能命中 (create_time, id) 索引,只取 N 条而不是全量,代价是只能顺序翻页、不能跳页,且并发写入时可能出现边界重复(用 id 做 tie-breaker 可以保证稳定顺序)。第三档是搜索/聚合场景,把两张表通过 binlog 同步进 ES 或一张冗余宽表,由它统一承载排序和分页,应用层按返回 id 回源补详情。无论哪一档,都要在业务层明确分页一致性策略(快照、去重)和最大页数限制。回答时给出「数据量 → 选方案」的判断依据,比只背一种写法分高得多。
Kafka 为什么性能好:五个点。一是顺序写:分区日志是 append-only,顺序写磁盘的吞吐接近内存随机写的数量级,避免随机 IO。二是充分利用操作系统页缓存,读写都走 page cache,不自己维护堆内缓存,重启也不丢缓存。三是零拷贝:消费端用 sendfile 直接把页缓存数据送到网卡,省掉内核态到用户态的两次拷贝。四是批量与压缩:生产者按 batch.size/linger.ms 攒批发送,broker 以消息集(record batch)为单位落盘和传输,配合 lz4/zstd 压缩,显著降低 IO 和网络开销。五是分区并行 + 稀疏索引:topic 按分区横向扩展,生产/消费并行;每个分段文件配 .index 稀疏索引,按 offset 二分定位,读取不用全扫。补充两点常被追问的:Kafka 的副本同步用 ISR 机制,acks=all 时只有 ISR 全部写入才算成功;分区数决定消费并行度上限,但分区过多会增加元数据与再均衡开销。
缓存三剑客:穿透——查询根本不存在的数据,缓存永远不命中,请求全打到 DB(多为恶意攻击)。解法:缓存空值(带短过期时间)、布隆过滤器前置拦截、参数合法性校验。击穿——某个热 key 恰好在过期瞬间失效,海量请求同时涌向 DB。解法:互斥锁/单飞(只让一个请求回源,其余等待)、热点数据逻辑过期不物理过期(后台异步续期)、预热。雪崩——大量 key 在同一时刻集中过期,或缓存集群整体不可用。解法:过期时间加随机抖动、多级缓存(本地 + Redis)、缓存集群高可用与限流降级、DB 侧兜底保护。回答完三类后主动补一句工程口径:缓存与 DB 的一致性用「先更新 DB 再删缓存 + 延迟双删 + 订阅 binlog 补偿」的组合,并接受最终一致。
Redis 集群扩缩容与 slot 迁移:Redis Cluster 把键空间划分为 16384 个槽,槽按 CRC16(key) mod 16384 分配,节点负责一部分槽位。扩容时用 redis-cli --cluster add-node 加节点(初始槽数为 0),然后用 reshard 从源节点迁移指定数量的槽:对每个槽,先 SETSLOT <slot> MIGRATING <target> 标记源节点、目标节点 IMPORTING,再逐个 key MIGRATE 搬运(--cluster-use-empty-masters 之类参数控制),全部搬完后用 SETSLOT <slot> NODE <target> 在两端及各节点广播新的槽映射;缩容则是反向操作,槽搬空后再 del-node。迁移期间客户端访问会收到 ASK(临时重定向到目标节点,本客户端要发 ASKING)或 MOVED(槽归属已变更,需刷新本地槽缓存),成熟的客户端会自动处理这两种重定向,MOVED 还会触发拓扑刷新。实践注意:优先迁移大 key 会阻塞,用 --cluster-pipeline 和限速;迁移期间业务侧要接受访问延迟上升;扩缩容前先确认没有正在执行的多 key 命令(跨槽的 mget 在集群下会直接报错,键要用 hash tag 控制同槽)。
inner join 与 left join:INNER JOIN 只返回两表都匹配上的行;LEFT JOIN 以左表为驱动表,左表所有行都保留,右表没有匹配时右表字段补 NULL。工程上的两个常见坑:一是把左连接的过滤条件下在 WHERE 里会让结果退化成内连接(WHERE b.status = 1 会过滤掉 NULL 行),正确做法是把右表条件写进 ON;二是多表左连接时驱动表选择影响性能,MySQL 8.0 之前会做连接顺序优化,必要时用 STRAIGHT_JOIN 手动指定。
B+ 树:三层结构要点:非叶子节点只存键和子节点指针、不存数据,所以单页能放几百个键,三到四层就能索引上千万行;所有数据都在叶子层,叶子节点之间用双向链表串联,天然支持范围扫描和 ORDER BY;树是平衡的,任何查询的 IO 次数相同,性能稳定可预期;插入删除通过分裂与合并维持平衡,页填充因子(InnoDB 默认约 15/16)留出余量。对比 B 树:B 树每个节点都存数据,单页能放的键更少、树更高,且范围查询要中序遍历回溯,不如 B+ 树的链表扫描高效。
MQ 的作用:异步(把非核心链路比如发通知、写日志从主流程摘出去,缩短响应时间)、解耦(上下游只依赖消息格式,新增消费方不用改生产者)、削峰填谷(大促瞬时流量先进队列,消费者按自身能力匀速消费)。代价也要说:系统复杂度上升、需要处理消息丢失/重复/顺序问题、端到端一致性只能靠最终一致(本地消息表、事务消息、对账补偿)。
聚簇索引与非聚簇索引:InnoDB 的聚簇索引就是主键索引,叶子节点直接存放整行数据,一张表只有一个聚簇索引;如果没有显式主键,InnoDB 会选一个唯一非空索引,再没有就隐式生成 6 字节 rowid。非聚簇索引(二级索引)的叶子节点存的是索引列的值 + 主键值,不存整行;因此用二级索引查询非索引列时必须拿主键回表到聚簇索引再取一次数据,这也是「覆盖索引」能显著提速的原因。工程建议:主键用自增或趋势递增的 bigint,避免随机 UUID 造成页分裂和随机写放大;二级索引要控制数量,每个索引都会带来写放大。
MVCC:多版本并发控制,目的是让读不加锁也能得到一致性快照,实现「读写不阻塞」。InnoDB 的实现是三件套:隐藏列(DB_TRX_ID 最近修改事务 id、DB_ROLL_PTR 指向 undo log 中的历史版本)、undo log 版本链、ReadView(记录生成快照时活跃的事务 id 集合,配合 min_trx_id/max_trx_id 判断某版本对本事务是否可见)。RC 隔离级别下每条 SELECT 都重建 ReadView,所以能看到已提交的最新数据;RR 下只在第一次读时建 ReadView 并复用,因此可重复读。它的作用:解决脏读和不可重复读(快照读),降低锁竞争提升并发;但当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)读的是最新版本并要加锁,幻读要靠间隙锁(Next-Key Lock)在 RR 下解决——这是常被追问的收尾点。