去年秋招季,很多学弟学妹问我B站后端开发方向笔试到底考什么。说实话,B站这几年校招笔试的题目风格变化不小,2023届的笔试卷A更偏向“基础+工程”的组合,而不是纯粹刷LeetCode。如果你只刷题不补基础,很容易在选择题上翻车;如果你只背八股不写代码,编程题又会暴露真实水平。我花了几个晚上把这套卷子的考点和答题思路重新捋了一遍,写下这篇复盘,希望能给准备后端开发岗的同学一些参考。
这套卷子整体分为三个部分:客观选择题、编程题和简答设计题。考察范围覆盖计算机网络、操作系统、数据库、Java/Go语言基础、并发编程、算法与数据结构,以及一道典型的系统设计题。接下来我按题型拆解,把高频考点、典型陷阱和实战经验都讲清楚。
1. 这套卷子的题型分布:选择题、编程题、设计题各占多少
1.1 整体题型与分值占比
先说结论:整套卷子满分100分,客观题占了40分左右,编程题30分,设计题20分,剩下10分是一些简单问答和逻辑推理。这个分值结构意味着,就算你编程题全部AC,客观题考砸了也一样挂。
我根据印象整理了一份题型分布表,帮助大家直观感受:
| 题型 | 题量 | 单题分值 | 考察重点 |
|---|---|---|---|
| 单选/多选 | 20题 | 2分 | 网络、OS、数据库、语言基础、并发 |
| 编程题 | 2题 | 15分/题 | 算法与数据结构、代码完整性 |
| 简答设计题 | 1题 | 20分 | 系统设计、方案权衡、工程思维 |
| 逻辑/数学题 | 5题 | 2分 | 概率、排列组合、推理 |
从分值能看出,B站并不想招一个只会背答案的人。客观题考的是你大学四年有没有认真上课,编程题考的是有没有刷题手感,设计题考的则是能不能把知识串起来解决实际问题。这套组合下来,能比较真实地反映一个候选人的计算机基础扎实程度。
1.2 为什么B站会把设计题放进笔试
很多公司笔试只做选择题和算法题,B站却很早就开始在设计题上做文章。原因在于后端开发岗位日常工作离不开系统设计。比如一个视频播放量统计,看起来简单,但真正设计时要考虑数据量大不大、要不要实时、缓存和DB怎么同步、排行榜怎么更新。这些问题用笔试来筛选,比面试时临时问更高效,也能刷掉一批“只会写CRUD”的简历选手。
设计题通常给一个业务场景,让你写接口定义、数据表结构、缓存方案、或者画出架构流程。难倒很多人的不是不会写代码,而是不知道从何处下手。我后面单独用一个章节讲这套卷子的设计题思路。
2. 选择题高频坑:网络、数据库与并发编程的典型陷阱
2.1 TCP三次握手和四次挥手:考的不是背,是细节
这道题几乎每年笔试都有,但大部分人只背了“三次握手四次挥手”的流程,考试时以为稳了,结果栽在细节上。
我记得卷子里有一道多选,问“关于TCP四次挥手,下列描述正确的是”。选项里有几个特别容易混淆的:
- 服务器收到FIN后,先进入CLOSE_WAIT状态,再继续发送未发送完的数据。
- 主动关闭方进入TIME_WAIT状态后,需要等待2MSL才能关闭。
- 如果服务器同时收到FIN和ACK,可以合并为三次挥手。
- 四次挥手一定是客户端先发起。
第四个选项错得很经典,因为四次挥手的发起方不一定是客户端,任何一端都可以主动关闭。第三个也是对的,如果通信双方同时关闭,或者服务端在收到FIN后没有数据要发了,ACK和FIN可以合并发送,这就是“三次挥手”的由来。
这个考点本身不难,难在平时只记流程不思考原因。比如TIME_WAIT为什么是2MSL?因为要保证最后一个ACK能到达对方,如果丢了对端会重发FIN,2MSL足够一个报文最大生命周期内重发一次。知道这个底层原因,很多干扰选项一眼就能看穿。
2.2 数据库索引失效:B+树之外的判断逻辑
数据库也是重头戏。题目会给你一条SQL,问它能不能命中某个联合索引(user_id, create_time, status)。比如:
where user_id = 1 and status = 2能用到部分索引,但只有user_id能走索引,status不行,因为联合索引最左前缀原则,跳过了create_time,status无法使用索引。where create_time > '2023-01-01' and user_id = 1在MySQL优化器有优化的情况下可能调整顺序,但笔试默认按联合索引定义顺序判断,所以只能命中user_id。where status = 2彻底失效,因为没有从最左列开始。
还有一道题考select * from table where user_id like '%123',这种左模糊查询会导致索引失效,因为B+树无法根据前缀匹配定位。很多人会误以为加个索引就万事大吉,实际上索引的参与条件和列的顺序、查询条件、数据分布都强相关。
我的建议是复习时不要只背“最左前缀”“不要用函数”这些口诀,要理解B+树的查找过程:索引是一棵排序树,查询只能从最左列开始逐列比较,如果跳过或者对列做了计算,排序信息就丢失了。理解了这一点,索引失效的题目就是送分题。
2.3 并发编程:从锁到内存可见性
卷子里有一道关于Java并发经典的题目,问volatile能保证什么。选项有原子性、可见性、有序性、持久性。很多人知道volatile能保证可见性和有序性,但不能保证原子性。这就是坑。
volatile解决的是内存可见性和指令重排序问题,它不会对count++这种复合操作做任何原子性保护。所以多线程环境下用volatile做计数器,结果必错。考场上经常会把volatile和synchronized放在一起考,要记住volatile不是锁,它无法保证多个线程互斥。
还有一道问ThreadLocal的原理,选项里说“ThreadLocal是存在Thread里面的一个Map”,这个描述不完全对,但选“每个线程有自己的副本”就对了。ThreadLocal本质上是在每个Thread对象里维护一个ThreadLocalMap,所以线程隔离。但要注意内存泄漏问题,ThreadLocalMap的key是弱引用,value是强引用,如果线程池复用线程,value可能一直无法回收。笔试可能不会考这么深,但面试会问。
如果是Go方向的同学,B站也有Go后端的卷子,重点会放到goroutine、channel、sync.Mutex和原子操作上。核心思路是一样的:并发问题的本质是共享可变状态的管理,搞清楚锁、信号、内存模型的边界就够了。
3. 编程题实战:一道“最长无重复子串”的完整解题过程
编程题一共两道,分值不低。第一道是比较经典的滑动窗口题,第二道是自拟结构的设计题。这里我用一道示例题来复盘这类题目的满分写法,题目是“给定一个字符串,找出其中不含有重复字符的最长子串长度”。
3.1 题目描述与输入输出约定
输入一个字符串,例如abcabcbb,输出最长无重复子串的长度。对于abcabcbb,最长无重复子串是abc,长度3。
约定:字符串只包含英文字母、数字、符号和空格,长度范围0到50000。要求时间复杂度O(n),空间复杂度O(min(m,n)),其中m是字符集大小。
如果你只写了暴力解法,能过一部分用例,但数据量大的case会超时。笔试题要求的是能跑完所有测试点。
3.2 暴力解与问题分析
暴力解很简单,枚举左边界i和右边界j,用Set判断区间内是否有重复字符。复杂度O(n^2),对于长度5万的字符串完全不可行。
所以关键思路是如何在向右滑动右指针时,快速判断左指针该移动到什么位置。
3.3 滑动窗口优化及Java实现
滑动窗口的思路是用左右两个指针维护一个不重复窗口。右指针每次向右移动一个字符,如果遇到重复字符,就不断收缩左指针直到窗口内没有重复。这样每个字符最多被访问两次,时间复杂度O(n)。
但笔试时要求代码整洁,直接使用HashMap记录每个字符最后出现的位置,左指针跳转更快。逻辑如下:
- 初始化一个HashMap,记录字符到它最后一次出现的位置的下一个位置,也就是窗口的左边界候选。
- 遍历字符串,设当前位置i的字符为c。
- 如果map里已经有c,则将左边界left更新为max(left, map.get(c)),确保窗口内没有重复。
- 把c和下一个位置i+1存入map,更新答案ans为max(ans, i-left+1)。
Java代码:
import java.util.HashMap; public class LongestSubstringWithoutRepeating { public int lengthOfLongestSubstring(String s) { HashMap<Character, Integer> lastPos = new HashMap<>(); int left = 0; int ans = 0; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (lastPos.containsKey(c)) { left = Math.max(left, lastPos.get(c)); } lastPos.put(c, i + 1); ans = Math.max(ans, i - left + 1); } return ans; } }这段代码的关键是lastPos.get(c)存储的是“下一个位置”而不是当前下标,这样遇到重复字符时,左边界可以一次性跳到重复字符之后,不需要一步一步收缩。代码简洁,也避免了Set动态删除的繁琐。
3.4 笔试现场的边界条件处理
这道题最容易错的不是主逻辑,而是边界:
- 空字符串:输入长度为0,循环不会执行,返回0,不会错。
- 字符串里全是相同字符,比如
bbbbbb,每次遇到重复,left直接跳到当前字符的下一个位置,ans始终为1。 - 字符串里全是不同字符,比如
abcdef,lastPos每个都不重复,ans最终等于全串长度。 - 字符包含空格的,比如
ab c abc,空格也算一个字符,charAt可以直接处理。
另外,笔试平台可能要求自己写输入输出。题目里给了String参数,部分平台还要求读标准输入。如果你用Scanner读整行,注意next和nextLine的区别,别因为读取方式不对导致空字符串无法测试。
这道题在LeetCode上叫“无重复字符的最长子串”,属于热门题。但考场上的关键是快速写出正确且整洁的代码,然后抽几分钟跑一下边界case。如果你能一次性通过所有提交,编程题这部分就稳了。
4. 设计题该怎么写:以“视频热门榜”为例的答题框架
设计题是B站笔试的特色,这道题很考验工程能力。卷子里的设计题场景是“设计一个视频热门榜”,要求每天展示点击量TOP100的视频,支持小时级更新。题目没有给出明确的用户量和数据规模,所以答题的第一步是做合理的假设。
4.1 先做需求边界和量级估算,别急着画架构图
很多人看到设计题,上来就画Nginx、Redis、MQ、数据库四件套,这是大忌。评分标准首先看你的需求分析是否到位,如果没有估算,方案就没有依据。
我当时是这么写的:
- 假设B站日活用户1亿,每天产生100亿次播放行为。
- 视频总量1亿条,热门榜只取前100。
- 按小时更新,也就是每小时需要从新增播放记录里汇总一次,生成新的榜单。
这个规模说明:如果每次实时扫描全量视频,根本吃不住。所以必须采用离线计算+结果缓存的方式。比如每小时跑一次定时任务,读取过去1小时的所有播放事件,合并到当天的累计播放次数里,然后计算Top100,写入Redis缓存。用户请求热门榜时,直接读缓存,不查数据库。
4.2 接口与数据模型设计
接口设计不是写RESTful CRUD,而是要体现业务含义。我写的接口是:
GET /api/hot-list?date=2023-09-01&type=all:返回某天热门视频列表。- 响应字段包括
video_id, rank, score, play_count。
数据表方面,我设计了video_play_count表,字段:
video_idbigintdatedatehourintplay_countbigint- 主键是
(video_id, date, hour)
这样每天每小时一行记录。最终榜单的score可以定义为播放量+点赞量加权的得分,权重不同,视频的热度值也不同。
4.3 缓存、异步与降级方案
热门榜列表写入Redis的zset,key是hot_rank:{date},member是video_id,score是热度值。用户请求直接ZREVRANGE hot_rank:2023-09-01 0 99。这样既支持排序,又支持分页。
异步方面,播放事件通过消息队列传递,比如Kafka。服务端收到播放请求后写入MQ,由消费者异步累加到对应小时bucket里,不影响主流程。这样即使高峰期播放量暴增,核心接口也能稳定。
降级方案也不能少。如果Redis挂了,接口可以回退到MySQL查预计算好的榜单表,虽然性能差点,但不会直接报错。如果MySQL也挂了,就返回HTTP 503,同时告警。这些细节在笔试里能明显拉开差距。
4.4 笔试评分到底看什么
据我后来和参与过校招的朋友交流,设计题评分主要看四件事:是否做量级估算、是否设计合理的数据存储、是否有缓存和异步思想、是否提到容灾降级。能把四件事说清楚,就算方案不是最优,依然能拿高分。
最重要的是不要写成“用户点击+MySQL查询”的小作业,要让人感受到你有系统设计意识。B站业务体量很大,笔试考察的就是你有没有“大流量下依然稳定”的概念。
5. 我在笔试现场的三个教训:时间分配、输入格式与心态
5.1 时间分配方案:40-30-20-10
B站这套卷子一般给90分钟。我最开始准备写选择+编程+设计,但实际容易纠结选择题,导致后面紧张。
复盘后我总结的时间分配是:
- 前10分钟快速浏览全部题目,标记出会做和不会做的题。
- 客观题最多用40分钟,遇到卡壳超过2分钟的先跳过。
- 编程题用30分钟,每题最多15分钟,如果15分钟没有完整思路,先写暴力解法保底。
- 最后10分钟处理设计题或检查已答内容。
设计题虽然分值20分,但如果编程题没完成,损失更大。合理取舍很关键。
5.2 输入输出处理:本地明明AC,线上全错
我遇到过最惨的现场事故:编程题在IDE里跑得好好的,提交到笔试平台却0分。后来发现是输入输出格式错了。很多笔试平台要求从标准输入读取,用Scanner读取时,如果题目有多行输入,不能只用一个nextInt,要循环读取。
我给你一个通用做法:在本地写题时,一定要先看题目给的示例输入是不是包含多行,然后写一个完整读取的模板。比如:
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); String line = sc.nextLine(); // 如果有多行,用 while (sc.hasNextLine()) 循环读 System.out.println(lengthOfLongestSubstring(line)); } }注意:如果一行可能有空格,要用nextLine()而不是next()。如果读数字用nextInt(),读下一行时注意换行符残留。这个小细节经常把人搞崩。
5.3 不会的题先跳,但别留空白
选择题不会可以先用排除法蒙一个,空着跟蒙一个都可能错,但蒙一个好歹有四分之一概率。编程题如果没思路,把输入输出框架和暴力解写上,也能拿部分分。设计题哪怕只有思路没细节,写下关键点和流程,也能让阅卷人看到你的分析过程。
最怕的是心态崩了,从头到尾死磕一道难题,最后简单题也没时间做。校招笔试不是竞赛,没有“解出难题才算赢”的说法,把该拿的分拿满就够了。
5.4 选择题不确定时怎么蒙
多选选择题如果怕错选扣分,可以采取保守策略:不确定的选项不选。单选的话,排除两个明显错误后,在剩余两个里按第一直觉选择,不要反复改。我做过测试,第一直觉的正确率往往高于反复修改后的答案。
6. 笔试后的查漏补缺:后端校招核心知识点对照清单
题目做完不是结束,真正的成长来自对照知识点清单查漏补缺。我每次笔试完都会整理一份“哪里不会补哪里”的表格,这也是我能从秋招小白到收获Offer的关键方法。
6.1 知识点清单表格
| 分类 | 核心知识点 | 校招常见考法 |
|---|---|---|
| 数据结构 | 数组、链表、栈、队列、哈希、二叉树、堆 | 算法题直接考察,选择题考复杂度分析 |
| 算法 | 二分、双指针、滑动窗口、DFS/BFS、动态规划、贪心 | 编程题高频,需达到手写无bug |
| 计算机网络 | TCP/UDP、HTTP/HTTPS、DNS、TCP拥塞控制、常见状态码 | 选择题、简答题 |
| 操作系统 | 进程/线程、调度算法、死锁、虚拟内存、IO多路复用 | 选择题,面试常问 |
| 数据库 | 索引、事务隔离级别、MVCC、锁、SQL优化 | 选择题、设计题 |
| Java基础 | 集合源码、异常、泛型、反射、JVM内存模型、垃圾回收 | 选择题、面试 |
| 并发编程 | synchronized、volatile、AQS、线程池、CAS | 选择题、设计题 |
| 分布式基础 | Redis、消息队列、分布式锁、一致性哈希、幂等 | 设计题核心 |
| 系统设计 | 量级估算、接口设计、存储选型、缓存策略、降级限流 | 设计题 |
| Linux | 文件系统、常用命令、进程查看 | 部分选择题会涉及 |
6.2 优先级与复习路线建议
如果你是现在才开始准备,我的建议是:
- 算法题每天保持2-3道,不要贪多,但一定要把每道题的最优解想清楚,尤其是“为什么这样写”以及“能不能再优化”。
- 计算机网络和数据库放在第二优先级,因为面试和笔试都会遇到。重点看TCP状态转换、HTTP缓存机制、索引底层的B+树、事务隔离级别。
- 并发编程和JVM是拉开差距的点,但分值不一定高。可以放到笔试前一周集中刷。
- 系统设计不需要看太多理论,把“秒杀系统”“短链系统”“排行榜系统”三四个经典案例吃透,学会套模板即可。
- 每场笔试后一定要复盘。我最开始因为懒,考完就丢,结果下次遇到类似考点还是错。后来逼着自己做知识图谱,把薄弱点标红,下一次复习先看红点。
这套B站2023校招笔试卷A,难度整体中等偏上,不是那种纯刷题就能过的卷子。它要求你对计算机基础有系统性的理解,同时具备解决真实工程问题的思维。如果你能把这份清单里的知识点按优先级过一遍,再做两三套模拟题,相信你也能在笔试中稳定发挥。
最后再分享一个小技巧:笔试前把可能会用到的模板代码提前准备好,比如滑动窗口模板、TopK模板、链表反转模板、树的遍历模板,但务必自己敲一遍再背。这样遇到类似题目,能省下大把推导时间,把精力留给真正需要思考的部分。祝大家都能拿到心仪的Offer。