字节 TAC 后端一面:分布式锁与直播点赞排行设计
- 轮次
- 一面
- 结果
- 一面已过
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 简单拷打实习经历(面试官顺着简历追问)。
- 介绍一下 AQS。
- 分布式锁有哪些实现方式?(追问:还有其他方式吗)
- 事务失效的原因有哪些?
- MySQL 事务的隔离级别以及实现方式?
- 手撕:链表中离最后一个节点距离为 n 的节点。
- TCP 协议的拥塞控制和可靠传输是怎么做的?
- 介绍一下你了解的 TCP 协议。
- 场景题:抖音直播点赞,统计排行榜 Top100 用户,接口和数据结构如何设计,才能保证高并发和实时性?
《参考解析》
AQS 是「一个 volatile 状态位 + 一个 CLH 变体等待队列 + 一套模板方法」。 核心字段只有 state(volatile int)和 head/tail 两个节点指针,state 的语义交给子类定义:ReentrantLock 用它表示重入次数,Semaphore 表示可用许可,CountDownLatch 表示剩余计数。获取失败时把当前线程包成 Node 用 CAS 挂到队尾,然后 LockSupport.park 阻塞;释放时改 state 并唤醒后继节点。它把「怎么抢、抢不到怎么排队」抽成模板方法(tryAcquire / tryRelease 由子类实现),把「排队、阻塞、唤醒、中断处理」这些容易写错的并发细节收敛在一处。追问常落在三点:独占与共享两套模式、公平锁为什么慢(每次都要检查队列前面有没有人)、以及条件变量 Condition 是怎么用单独的条件队列实现的。答的时候主动说清「state 是一个 volatile 字段加 CAS 保证原子性,队列本身靠 CAS 维护指针」,比背方法名有价值。
分布式锁的实现方式可以按「用什么存储做互斥」分四类。 一是 Redis:SET key value NX PX 加唯一 value,释放时用 Lua 比对 value 再删,避免误删别人的锁;要进一步做可重入、自动续期就用 Redisson,它的 watchdog 会在业务未结束时定期续期。二是 ZooKeeper:靠临时顺序节点,最小的那个节点拿到锁,前面的节点删除时通过 watch 唤醒后继,天然解决「客户端崩溃锁自动释放」和「等待公平」。三是数据库:唯一索引插入或 select ... for update 行锁,实现简单但性能差、易死锁,只适合低频场景。四是 etcd 这类基于 Raft 的一致性存储,用租约(lease)加事务比较实现。面试时要主动补上三个难点:锁的过期时间比业务耗时长怎么办(续期或看门狗)、主从切换导致的锁丢失(RedLock 的争议在于它对时钟依赖且无法给出强一致的证明)、以及锁只保证互斥不保证幂等。工程上更推荐的方向是「能不用分布式锁就不用」——用唯一约束、状态机 CAS、消息队列串行化来替代互斥。
事务失效的原因几乎都和 Spring AOP 代理有关。 常见几类:方法不是 public(CGLIB 代理不了 private/final/static 方法);同一个类内部自调用(this.method() 不走代理对象,注解不生效);类没有被 Spring 管理或者被 new 出来;异常被自己 catch 掉没有抛出,或者抛出的异常类型不在 rollbackFor 范围内(默认只对 RuntimeException 和 Error 回滚,受检异常不回滚);@Transactional 加在接口方法上但用了 CGLIB 代理;多线程里新开的线程不共享同一个事务上下文;以及传播行为设成 NOT_SUPPORTED / REQUIRES_NEW 导致预期外的提交。解决套路对应也就那几条:自调用改成注入自身代理或抽到另一个 Bean、明确 rollbackFor = Exception.class、异步逻辑单独拆事务、用 TransactionSynchronizationManager 或 TransactionTemplate 做编程式控制。
隔离级别与实现方式要分开答。 四个级别是读未提交、读已提交、可重复读、串行化,分别解决脏读、不可重复读、幻读(标准定义下)。MySQL InnoDB 的实现手段是三件套:undo log 加 MVCC 提供一致性读(RC 每次查询生成新 ReadView,RR 在事务第一次查询时生成并复用),间隙锁加 Next-Key Lock 在 RR 下防幻读,以及行锁本身保证写写互斥。要指出两个容易被追问的点:RR 下「当前读」(select ... for update、update)看到的是最新数据而不是快照;间隙锁只在 RR 下生效,RC 下没有间隙锁所以并发更高但会幻读,很多互联网业务因此把隔离级别降到 RC。再补一句 MVCC 的可见性判断依赖事务 ID 与活跃事务数组,回滚段里那条记录找不到就顺着 roll_pointer 往回找。
可靠传输和拥塞控制是 TCP 的两套独立机制,别混着讲。 可靠传输解决「不丢、不重、按序」:序号加确认(累积确认与 SACK)、超时重传(RTO 按 RTT 平滑估计加方差)、快速重传(收到三个重复 ACK 立即重传)以及校验和。拥塞控制解决「发多快」:慢启动(cwnd 指数增长到 ssthresh)、拥塞避免(线性增长)、快速恢复(三次重复 ACK 时 ssthresh 减半而不回到慢启动),再加上内核里的 BBR 这类基于带宽时延积的算法。追问常常是「四次挥手为什么是四次」「TIME_WAIT 有什么用」「为什么要有拥塞窗口和接收窗口两个窗口」——答案是前者是发送方对网络的估计、后者是接收方对自身缓冲的声明,实际发送量取最小值。
直播点赞排行榜是典型的「写多读少、允许近似」的高并发设计题。 数据侧用 Redis ZSet:ZINCRBY room:rank:<roomId> 1 <userId> 做原子自增,复杂度 O(log N),天然有序;单 key 热点的解法是按 userId 哈希分片成 N 个 ZSet,读的时候用 ZUNIONSTORE 聚合到临时 key 再 ZREVRANGE 0 99,或者让各分片各取 Top100 再归并——后者在「Top K 只关心头部」时更省。接口侧要做到:点赞请求先落 Redis(Lua 脚本保证「同一用户同一场次只计一次」),返回即成功,异步批量把增量刷进 DB 与离线数仓;查询走缓存并设短 TTL(比如 1 到 3 秒),排行榜这种数据允许秒级延迟,用本地缓存加定时刷新或长轮询/推送把读 QPS 打下来。再往上可以提近似的 HeavyKeeper、Count-Min Sketch 用于「Top100 之外的噪声用户」做预筛,以及大促时用「点赞消息进 MQ 削峰 + 分区消费 + 幂等去重」。面试官在这题上主要看你有没有区分「精确与近似、实时与准实时、计数与排序」这三组取舍,以及能不能把热点 key、幂等、降级说清楚。
链表手撕题(倒数第 n+1 个节点)的标准解法是双指针或一次遍历。 快指针先走 n+1 步,然后快慢一起走,快指针到尾时慢指针就是目标;等价写法是先求长度再走 len-n-1 步。要注意的边界:n 等于链表长度时删的是头节点,所以加哨兵节点最稳;n 非法(小于 1 或大于长度)要有判断。如果题目是「删除」而不是「返回」,记得改前驱的 next 指针并处理内存释放(C++)。这类题面试官往往只让你讲思路,讲的时候把「为什么快指针走 n+1 步而不是 n 步」和「哨兵节点解决了什么边界」点出来,比闷头写代码更能拿到分。