字节跳动 - AI搜索推荐后端开发
- 日期: 2025-12-22
- 岗位描述
- 1.以 AI 技术为驱动,主导或参与信息流推荐、多模态搜索、智能内容中台建设;
- 2.参与亿级内容分发系统架构设计与优化,结合 AI 技术提升搜推一体化效果;
- 3.构建 AI 能力中台,解决内容理解、生成、合规等内容生态的共性技术需求;
- 4.参与 SaaS 应用的生产调优,解决大规模场景下系统的性能和稳定性问题。
- 面试时长: 45分钟
这个岗位从职责描述或者是任职要求上,应该是更偏向 AI 业务应用的搜推方向,面试整体过程中双方的 预期没有对齐,即没有想好要招什么人,也没想匹配对应的描述。更像是一个面向 校招的常规后端,没有任何 AI 方向的交流、也没有任何搜推方向的交流,也没有考虑到 社招 3-4 年背景 所需要考察的面试能力。还是希望面试的三方都做好对齐,找什么样的人,考察什么样的能力,提高筛选效率。
01 最近项目复盘
02 消息平台的消息分发如何确保 仅投递一次
"仅投递一次"(Exactly-Once) 是消息分发的核心诉求,需从 生产端、中间件、消费端 三层设计保障,结合电商营销消息场景落地。
1. 生产端:幂等 + 事务保障消息不重复发送
- 本地事务表 + 消息队列双写:发送营销消息前,先在
MySQL写入 "消息发送日志"(含msg_id、状态=待发送、业务唯一标识),并与业务操作(如营销任务触发)放在同一事务 - 幂等发送:生产端为每条消息生成全局唯一
msg_id,发送前校验本地日志,避免重复提交;若发送超时,基于msg_id重试,保证消息至少投递到 MQ
2. 中间件:精准投递 + 状态追踪
- 选用支持 "消息轨迹 + 死信队列" 的 MQ(如
RocketMQ/Kafka) - 开启 MQ 的 "消息确认机制"(
Producer Ack),确保消息成功写入 MQ 集群 - 对未确认的消息,基于
msg_id做有限重试(如 3 次),失败则进入死信队列人工介入
3. 消费端:幂等处理 + 事务确认
这是 "仅投递一次" 的核心环节,营销消息推送场景落地如下:
- 消费幂等:消费端以
msg_id或 "业务唯一标识(如用户 ID + 营销活动 ID)" 为键,在Redis/MySQL中记录 "已消费" 状态,消费前先校验,避免重复处理 - 消费事务:消费端处理消息(如推送至邮件网关)后,先更新本地消费状态(已消费),再向 MQ 发送 "消费确认"(
ACK);若处理失败,不发送ACK,MQ 会重试投递 - 重试兜底:设置消费重试次数(如 5 次),超过次数则进入死信队列,避免无限重试导致重复投递
关键补充
营销消息推送允许 "最终一致",若极端场景下出现重复消息,可通过业务层去重兜底:比如用户收到重复推送时,前端展示层基于 "消息唯一标识" 去重,不影响用户体验。
03 MySQL 的事务隔离级别
MySQL 默认事务隔离级别为 REPEATABLE READ(可重复读),基于 InnoDB 存储引擎的 MVCC(多版本并发控制) 实现,4 个隔离级别从低到高如下
| 隔离级别 | 核心定义 | 解决的问题 | 存在的问题 | 底层实现 |
|---|---|---|---|---|
READ UNCOMMITTED(读未提交) | 能读取其他事务未提交的数据 | - | 脏读、不可重复读、幻读 | 不加锁,直接读取最新数据 |
READ COMMITTED(读已提交) | 只能读取其他事务已提交的数据 | 脏读 | 不可重复读、幻读 | MVCC,每次查询生成新的 Read View |
REPEATABLE READ(可重复读) | 同一事务内多次读取同一数据,结果一致 | 脏读、不可重复读 | 幻读(InnoDB 已通过 Next-Key Lock 解决) | MVCC,事务启动时生成唯一 Read View,全程复用 |
SERIALIZABLE(串行化) | 最高级别,事务串行执行 | 所有并发问题 | 性能极差,完全丧失并发性 | 对所有读取的行加锁,读写互斥 |
核心考点补充
- 脏读(Dirty Read):读取到其他事务未提交的脏数据(如 A 事务修改数据但未提交,B 事务读取该数据,A 事务回滚后 B 读到无效数据)
- 不可重复读(Non-Repeatable Read):同一事务内多次读取同一数据,结果不同(如 A 事务读取数据后,B 事务修改并提交,A 再次读取数据变化)
- 幻读(Phantom Read):同一事务内多次执行同一查询,返回的行数不同(如 A 事务查询 "用户 ID>100 的记录",B 事务插入一条 ID=101 的记录,A 再次查询多了一条)
- InnoDB 优势:
REPEATABLE READ通过 Next-Key Lock(行锁 + 间隙锁) 解决了幻读,是 MySQL 的核心优势
04 TCP 的 4 次挥手
TCP 是面向连接的可靠传输协议,"4 次挥手" 是关闭 TCP 连接的标准流程,核心解决 "双方数据都传输完成后,安全关闭连接" 的问题
为什么需要 4 次?
TCP 是 全双工通信(双方可同时收发数据),关闭连接需分别确认 "发送端无数据" 和 "接收端无数据"
- 第一次挥手:客户端告知服务端 "我不发数据了"(
FIN=1) - 第二次挥手:服 务端确认 "知道你不发了,但我可能还在发数据"(
ACK=1) - 第三次挥手:服务端告知客户端 "我也不发数据了"(
FIN=1) - 第四次挥手:客户端确认 "知道你也不发了,连接可关闭"(
ACK=1)
核心状态变化
- 客户端:
FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT(等待 2MSL,确保服务端收到最后一个 ACK)→CLOSED - 服务端:
CLOSE_WAIT→LAST_ACK→CLOSED
TIME_WAIT 的作用
- 客户端发送最后一个
ACK后,会等待 2 倍 MSL(MSL = 报文最大生存时间,默认 1 分钟) - 避免最后一个
ACK丢失,服务端重发FIN时客户端能再次响应 - 确保本次连接的所有报文都从网络中消失,避免新连接收到旧连接的残留报文
05 40 亿个整数去重 (范围 -2^32~+2^32)
针对 40 亿个覆盖 -2³¹ ~ 2³¹-1(-2147483648 ~ 2147483647)的 32 位有符号整数去重,BitMap 是最优方案,以下从核心原理、实现、优化及面试追问展开解析
一、经典 BitMap 方案 - 内存充足时
1. 核心原理
BitMap 本质是 "用位代字节" 的空间优化策略:
- 32 位有符号整数全集共 2³² = 4,294,967,296 个唯一值
- 每个值对应 1 个比特位(
0=不存在、1=存在) - 仅需 512MB 内存(4,294,967,296 bit ÷ 8 ÷ 1024 ÷ 1024)即可存储全集状态,远优于 哈希表 的 32GB+
- 因 负数 无法直接作为数组下标,需 偏移映射:
- 偏移量 = 整数 + 2,147,483,648(2³¹)
- 将最小值
-2,147,483,648映射为0、最大值2,147,483,647映射为4,294,967,295 - 反向映射用 "整数 = 偏移量 - 2,147,483,648" 还原
2. 实现步骤与伪代码
数据选 型:选用无符号字节数组(1 字节 = 8 bit,存储 8 个整数状态,最小可寻址单元,避免浪费)
简洁伪代码:
常量定义:
OFFSET = 2^31 (2147483648)
BITMAP_SIZE = 2^32 / 8 = 512MB
步骤一: 初始化
创建字节数组 bitmap[BITMAP_SIZE],全部置 0
步骤二: 标记存在的整数
for 每个整数 num:
偏移量 = num + OFFSET
字节索引 = 偏移量 / 8
位索引 = 偏移量 % 8
bitmap[字节索引] 的第 位索引 位 置为 1
步骤三: 输出去重结果
for i = 0 to BITMAP_SIZE-1:
if bitmap[i] == 0: 跳过
for bit = 0 to 7:
if bitmap[i] 的第 bit 位为 1:
原整数 = (i * 8 + bit) - OFFSET
输出 原整数
3. 边界处理与性能
边界处理:
- 计算偏移量必须用 64 位整数,避免 32 位溢出
- 内存分配失败则降级为 分块方案
bitIndex 0-7对应字节最低位到最高位,保证映射一致
性能分析:
- 时间复杂度:
O(n+M)(n=40 亿输入,M=5.36e8 遍历 BitMap),线性高效 - 单线程处理 40 亿整数约 10-20 秒,取决于 IO 效率