01阿里云二面
根据口述整理,补充了面试中适合展开的回答口径。
面试问题#
- 视频 Feed 流最核心的设计是什么
- 消息队列异常时怎么降级
- 消息队列怎么做高可用和持久化
- 切片和数组的区别
- 缓存击穿和缓存雪崩
- Go map 底层如何实现
- defer 的实现和返回值修改
- Go 协程间通信和 Channel 容量设计
- 两个有序数组的中位数
- 根据字符串中的数字重新排序
参考答案(AI 生成)#
以下答案由 AI 生成,仅供面试复盘参考。
项目#
视频 Feed 流最核心的设计是什么#
可直接说: 我觉得最核心的是复合游标分页。Feed 流的难点在于数据持续新增、删除、热度变化,同时还要保证翻页稳定、性能稳定。单纯 offset 深分页会随着页码变大越来越慢,也容易在新内容插入后出现重复或跳过;复合游标用排序字段加唯一主键定位下一页,更适合动态 Feed。
典型设计是 (score, id) 或 (created_at, id):
SELECT id, title, score, created_at
FROM videos
WHERE (
score < ?
OR (score = ? AND id < ?)
)
ORDER BY score DESC, id DESC
LIMIT ?;这样设计的重点:
score或created_at决定主要排序。id作为唯一排序补充,让同分、同时间下的顺序保持稳定。- cursor 里保存上一页最后一条的排序字段和
id,下一页从这个位置继续查。 - 索引匹配排序字段和主键,例如
(score DESC, id DESC)或(created_at DESC, id DESC)。
如果 Feed 有个性化推荐分数,可以把推荐结果先写入 Redis ZSet 或临时结果表,再用 (rank_score, video_id) 做复合游标。面试里可以补一句:复合游标解决的是“高并发动态列表下的稳定翻页和深分页性能”。
消息队列异常时怎么降级#
可直接说: MQ 是异步链路,异常时我会按业务重要程度做降级。强依赖结果的核心链路走同步直写或本地消息表兜底;弱依赖链路先落库记录任务状态,再后台补偿;非核心通知类任务可以延迟处理。
在项目里可以这样回答:
- 正常路径:业务先写 MySQL,再写 outbox 本地消息表,worker 投递 MQ,消费者异步处理。
- MQ 异常:worker 投递失败后更新
retry_count和next_retry_at,按指数退避重试。 - MQ 长时间异常:熔断 MQ 投递,核心业务改成直写 MySQL 或同步调用关键逻辑,同时保留 outbox 记录,等 MQ 恢复后补偿下游。
- Redis 异常:Feed 缓存读取失败时降级读 MySQL,配合本地短 TTL 缓存、限流和
singleflight合并回源,保护数据库。 - 恢复阶段:MQ 或 Redis 恢复后,后台任务按顺序补偿未完成消息,并通过幂等键避免重复生效。
面试官追问自动降级直写时,可以答:核心写链路可以直写权威存储,异步链路保留 outbox 任务用于恢复后补偿。这样用户请求先成功,异步一致性由后台任务继续保证。
消息队列怎么做高可用和持久化#
可直接说: MQ 高可用要同时保证生产端投递、Broker 存储和消费端确认。RabbitMQ 里会开启生产者 confirm、队列持久化、消息持久化、手动 ACK,并使用 quorum queue 或镜像队列提高可用性。
RabbitMQ 关键配置和机制:
- 交换机和队列持久化:声明 exchange、queue 时设置
durable=true,Broker 重启后元数据还能恢复。 - 消息持久化:发布消息时设置
delivery_mode=2或 persistent,让消息写入磁盘。 - 生产者确认:开启 publisher confirm,Broker 落盘或副本确认后给生产者 ACK,失败时生产者重试或写 outbox 补偿。
- 消费者确认:使用 manual ack,业务处理成功后再 ACK;失败时
nack/requeue或进入死信队列。 - 高可用队列:优先考虑 quorum queue,通过 Raft 多副本复制保证主节点故障后可以切换。
- 死信和重试:配置 DLX、TTL、最大重试次数,把异常消息转入死信队列做人工排查或补偿。
常见优化参数和思路:
prefetch控制单个消费者 unacked 消息数,让消息在消费者之间更均衡地分配。- 批量发布和 confirm 批量等待,提高吞吐。
- 消息体控制大小,大对象放对象存储,MQ 里只传引用。
- 队列按业务拆分,核心队列和低优先级队列隔离。
- 配置 lazy queue 或 quorum queue 时结合业务延迟要求,权衡吞吐、落盘和恢复能力。
- 监控积压量、publish/ack 速率、磁盘水位、内存水位、消费者数量和死信数量。
这里可以强调:RabbitMQ 的可靠性依赖持久化和确认机制共同完成,业务侧仍然要有 outbox、重试和消费幂等。
八股#
切片和数组的区别#
可直接说: 数组是固定长度的值类型,长度属于类型的一部分;切片是对底层数组的一段引用,包含指针、长度和容量三个字段。
关键点:
- 数组长度固定,
[3]int和[4]int是不同类型。 - 数组赋值会复制整个数组。
- 切片长度可变,扩容时可能分配新底层数组。
- 切片传参复制的是 slice header,底层数组共享。
缓存击穿和缓存雪崩#
可直接说: 缓存击穿是热点 key 失效后,大量请求同时打到数据库;缓存雪崩是大量 key 同时失效或缓存集群故障,请求大面积打到数据库。
解决思路:
- 击穿:热点 key 加互斥锁、
singleflight、逻辑过期、后台刷新。 - 雪崩:TTL 加随机值、缓存预热、限流降级、多级缓存、Redis 高可用。
Go map 底层如何实现#
可直接说: Go map 底层是哈希表,核心结构是 hmap 和桶 bmap。每个桶最多放 8 个 key/value,并保存 top hash 用于快速比较;哈希冲突时使用溢出桶,数据增长后触发渐进式扩容。
关键点:
- 通过 hash 定位 bucket。
- bucket 内比较 top hash,再比较 key。
- 冲突过多时挂 overflow bucket。
- 扩容采用渐进搬迁,读写过程中逐步迁移旧桶数据。
defer 的实现和返回值修改#
可直接说: defer 会把延迟调用记录挂到当前 goroutine 或当前函数调用链上,函数返回前按后进先出的顺序执行。现代 Go 对简单 defer 做了开放编码优化,减少运行时分配和链表操作开销。
返回值执行顺序:
- 先给返回值赋值。
- 再执行 defer。
- 最后真正返回给调用方。
所以命名返回值可以被 defer 修改:
func f() (x int) {
defer func() {
x = 2
}()
return 1
}这个函数最终返回 2。匿名返回值场景下,defer 修改局部变量通常只影响该局部变量,返回结果已经在 defer 前完成赋值。
Go 协程间通信和 Channel 容量设计#
可直接说: Go 常用 Channel 做 goroutine 间通信,也会配合 context 做取消、超时和生命周期控制。Channel 容量为 0 是无缓冲通道,发送和接收必须同步配对;容量大于 0 是有缓冲通道,可以临时承接生产和消费的速度差。
容量设计原则:
- 用无缓冲 Channel:强调同步交接、事件通知、严格背压,比如任务完成信号。
- 用有缓冲 Channel:生产者和消费者速度有波动,需要削峰或提升吞吐。
- 容量大小按消费者处理速度、可接受排队延迟、内存占用来估算。
- 工程上常从小容量开始,比如
CPU 核数 * 每个 worker 可接受积压数,再结合压测调整。 - Channel 适合作为有界队列使用,容量过大会隐藏下游处理慢的问题。
算法#
两个有序数组的中位数#
可直接说: 简单方案是双指针合并到中位数位置,时间复杂度 O(m+n),空间可以做到 O(1);进阶方案是二分切分较短数组,时间复杂度 O(log(min(m,n)))。
双指针思路:
- 设总长度为
m+n,只遍历到第total/2个位置。 - 每次取两个数组当前较小值,记录
prev和cur。 - 总长度为奇数返回
cur,偶数返回(prev+cur)/2。
二分思路:
- 在较短数组上二分切分点
i。 - 另一个数组切分点
j = (m+n+1)/2 - i。 - 满足
leftA <= rightB && leftB <= rightA时找到正确切分。 - 奇数返回左半部分最大值,偶数返回左半最大值和右半最小值的平均值。
根据字符串中的数字重新排序#
题目:给定字符串数组,按照字符串中的数字排序,带多个数字时拼接为一个数,无数字视为 0。
示例:
输入:[aab2nn1, dd8jh, n1n, xyz]
数字:[21, 8, 1, 0]
输出:[xyz, n1n, dd8jh, aab2nn1]思路:
- 遍历每个字符串,提取其中所有数字字符并拼接。
- 无数字时数值记为
0。 - 按提取出的数字升序排序。
- 数字相同时按原始顺序稳定排序,避免同 key 顺序抖动。
Go 写法:
type item struct {
s string
num int
idx int
}
func extractNum(s string) int {
n := 0
has := false
for _, ch := range s {
if ch >= '0' && ch <= '9' {
has = true
n = n*10 + int(ch-'0')
}
}
if !has {
return 0
}
return n
}
func sortByNumber(arr []string) []string {
items := make([]item, 0, len(arr))
for i, s := range arr {
items = append(items, item{s: s, num: extractNum(s), idx: i})
}
sort.SliceStable(items, func(i, j int) bool {
return items[i].num < items[j].num
})
ans := make([]string, len(items))
for i, it := range items {
ans[i] = it.s
}
return ans
}