2026 春招后端面经:字节、阿里、腾讯 20 场面试真题汇总

一、前言

在这里插入图片描述

这篇文章汇总了 20 场面试中遇到的全部技术真题,包括算法、八股、项目深挖和系统设计。所有答案均经过面试后的复盘整理,并结合了实际工作中的理解,建议收藏后反复阅读

阅读指南:文章很长(约 1.5 万字),建议先看最后的【高频考点总结】,再有针对性地查漏补缺。


二、面试概况速览

公司 岗位 技术栈 面试轮次 结果
字节跳动 后端开发 Go 3 技术面 + 1 HR 面(共投递 2 个部门) Offer
阿里巴巴 Java 后端 Java 3 技术面 + 1 交叉面 + 1 HR 面(共 2 个事业部) Offer
腾讯 后台开发 C++/Go 2 技术面 + 1 总监面 + 1 HR 面(共 2 个 BG) Offer

三、字节跳动(7 场面试真题)

字节面试的特点是:算法题量大、注重工程实践、喜欢深挖项目细节。每一面几乎都有 1-2 道 Medium 难度的算法题。

3.1 字节一面:基础 + 算法

题目 1:手撕 LRU Cache
要求:实现一个满足 O(1) 时间复杂度的 LRU 缓存机制。

点击查看详细解答

核心思路:HashMap + 双向链表。HashMap 保证查询 O(1),双向链表保证插入/删除 O(1)。

class LRUCache {
    class Node {
        int key, val;
        Node prev, next;
        Node(int k, int v) { key = k; val = v; }
    }
    
    private int capacity;
    private Map<Integer, Node> map;
    private Node head, tail; // 伪头部和伪尾部节点
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        moveToHead(node);
        return node.val;
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.val = value;
            moveToHead(node);
        } else {
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            addToHead(newNode);
            if (map.size() > capacity) {
                Node tail = removeTail();
                map.remove(tail.key);
            }
        }
    }
    
    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }
    
    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private Node removeTail() {
        Node res = tail.prev;
        removeNode(res);
        return res;
    }
}

面试官追问

  1. 为什么要用双向链表而不是单向链表?
    :删除节点时需要找到前驱节点,双向链表是 O(1),单向链表是 O(n)。
  2. 如果要求线程安全,怎么改造?
    :可以使用 Collections.synchronizedMap 包裹 HashMap,或者使用 LinkedHashMapremoveEldestEntry 方法配合 ReentrantReadWriteLock

题目 2:Redis 持久化机制对比

点击查看详细解答

Redis 提供两种持久化方式:RDB(快照)AOF(日志)

特性 RDB AOF
原理 定时 fork 子进程生成内存快照 记录每条写命令到日志文件
文件体积 紧凑,经过压缩 较大,可配置重写机制
恢复速度 快(直接加载二进制) 慢(需要重放命令)
数据安全 可能丢失最后一次快照后的数据 取决于刷盘策略(always/everysec/no)
性能影响 fork 时可能有短暂阻塞 持续写盘,但一般可接受

混合持久化(Redis 4.0+)

  • AOF 重写时,先以 RDB 格式记录当前内存状态,再追加期间的增量 AOF 命令。
  • 结合了 RDB 的快速恢复和 AOF 的细粒度数据安全。

答题要点:一定要提到 fork() 的 Copy-On-Write 机制,以及 AOF 重写的 bgrewriteaof 过程。


3.2 字节二面:中间件 + 分布式

题目 3:Kafka 为什么能做到高吞吐?

点击查看详细解答

Kafka 的高吞吐设计是多维度优化的结果:

  1. 顺序写磁盘:Kafka 的消息是追加到日志文件末尾,磁盘的顺序写性能接近内存随机写(约 600MB/s),避免了随机寻道。
  2. PageCache 机制:Kafka 重度依赖操作系统的 PageCache。生产者写入时先写到 PageCache,由 OS 异步刷盘;消费者读取时优先从 PageCache 读,实现零拷贝
  3. 零拷贝(Zero-Copy):传统 IO 需要 4 次拷贝(磁盘→内核→应用→Socket→网卡),Kafka 使用 sendfile() 系统调用,直接从 PageCache 发送到网卡,减少 2 次拷贝和 2 次上下文切换。
  4. 批量处理:Producer 支持批量发送(batch.sizelinger.ms),Consumer 支持批量拉取,减少网络 RTT。
  5. 分区并行:Topic 分为多个 Partition,不同 Partition 可以并行读写,水平扩展消费能力。
  6. 稀疏索引:每个日志段(LogSegment)维护稀疏索引,快速定位消息位置,避免全量扫描。

延伸:如果面试官问 Kafka 和 RocketMQ 的区别,重点答 Kafka 适合日志/大数据场景(高吞吐),RocketMQ 适合金融/事务场景(低延迟+事务消息)

题目 4:分布式事务的解决方案

点击查看详细解答
方案 原理 适用场景 缺点
2PC 准备阶段 + 提交阶段 传统数据库 XA 同步阻塞、单点故障、脑裂
3PC 引入预提交 + 超时机制 对 2PC 的改进 实现复杂,网络分区仍有问题
TCC Try-Confirm-Cancel 电商、金融(Seata) 业务侵入大,Confirm/Cancel 需幂等
本地消息表 事务内写业务表+消息表,定时扫描 最终一致性 需要定时任务,延迟较高
MQ 事务消息 半消息 + 回调检查 RocketMQ 原生支持 需要 MQ 支持事务消息
Saga 长事务拆分 + 补偿 微服务长流程 隔离性差,需处理脏读

字节面试官特别追问:如果 TCC 的 Confirm 阶段网络超时了怎么办?
:TCC 框架(如 Seata)会不断重试 Confirm,因此 Confirm/Cancel 必须保证幂等性。同时需要设置事务日志表,记录当前分支事务状态,避免重复执行。


3.3 字节三面:系统设计 + 算法

题目 5:设计一个短链(Short URL)服务

点击查看详细解答

QPS 预估:读 QPS 10万+,写 QPS 1万+。

核心设计

  1. 短码生成算法

    • 方案 A:自增 ID + Base62:利用数据库自增主键或 Redis INCR,转为 62 进制(a-zA-Z0-9)。优点是单调递增、无冲突;缺点是短码有规律,可能被遍历。
    • 方案 B:Hash 算法:对长链取 MD5/ MurmurHash,取前 6 位。可能冲突,需要处理碰撞。
    • 方案 C:预生成:离线生成大量短码放入池子,使用时取出,性能最好。

    推荐:生产环境用 Redis 分布式自增 + Base62,并加入随机盐值打乱顺序。

  2. 存储设计

    • 关系型数据库(MySQL/TiDB)存储长链→短链映射。
    • Redis 缓存热点数据,过期时间 7 天。
    • 为了防止缓存击穿,使用布隆过滤器先判断短码是否存在。
  3. 跳转优化

    • 301 永久重定向:浏览器会缓存,减少服务端压力,但无法统计点击次数。
    • 302 临时重定向:可以统计 UV/PV,但服务端压力大。
    • 折中:首次 302 并设置 Cache-Control: max-age=3600,后续一段时间内浏览器缓存。
  4. 分库分表

    • 按短码 Hash 分 16 个库,每个库 64 张表。
    • 或者使用 TiDB 直接分布式存储。

面试官追问:如果短链被恶意攻击(遍历短码),怎么防护?

  • 短码长度从 6 位增加到 8 位,扩大空间。
  • 接入风控系统,对异常 IP 限流。
  • 敏感长链增加访问密码或有效期。

题目 6:算法题——最长无重复字符的子串

public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>();
    int left = 0, max = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (map.containsKey(c)) {
            left = Math.max(left, map.get(c) + 1);
        }
        map.put(c, right);
        max = Math.max(max, right - left + 1);
    }
    return max;
}

时间复杂度 O(n),空间复杂度 O(min(m, n)),其中 m 为字符集大小。


3.4 字节四面:Go 语言深度

题目 7:Go 的 GMP 模型详解

点击查看详细解答

G(Goroutine):用户态轻量级线程,初始栈仅 2KB,由 Go Runtime 管理。
M(Machine):操作系统线程,由 OS 调度,M 必须绑定 P 才能执行 G。
P(Processor):逻辑处理器,维护一个本地可运行 G 队列(LRQ),数量默认等于 CPU 核心数。

调度过程

  1. 新建 G 时,优先放入当前 P 的 LRQ。
  2. 如果 LRQ 满了,则将一半 G 放入全局队列 GRQ。
  3. M 执行 G 时发生阻塞(如系统调用),M 会与 P 分离,P 去绑定空闲的 M 或新建 M,保证其他 G 能继续执行。
  4. Work Stealing:如果 P 的 LRQ 空了,会尝试从 GRQ 或其他 P 的 LRQ 偷取 G。

面试官追问:Go 的 Goroutine 比 Java 线程快在哪里?

  • 调度器:Go 是 M:N 调度,用户态切换仅需 200ns;Java Thread 是 1:1 映射到内核线程,切换需 1-2μs。
  • 栈管理:Goroutine 栈动态伸缩(2KB~1GB),Java 线程栈固定 1MB,内存占用高。
  • GC 优化:Go 的并发标记清除对 Goroutine 的栈进行精准扫描,暂停时间短。

题目 8:Channel 的底层实现

点击查看详细解答

Channel 在 Go 源码中由 hchan 结构体实现:

type hchan struct {
    qcount   uint           // 当前队列中的元素个数
    dataqsiz uint           // 环形队列大小
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 元素大小
    closed   uint32         // 是否已关闭
    elemtype *_type         // 元素类型
    sendx    uint           // 发送索引
    recvx    uint           // 接收索引
    recvq    waitq          // 等待接收的 Goroutine 队列( sudog 链表)
    sendq    waitq          // 等待发送的 Goroutine 队列
    lock     mutex          // 互斥锁,保护 hchan
}

发送/接收流程

  • 有缓冲 Channel:数据先写入环形队列 buf,如果 buf 满了,发送者 G 进入 sendq 等待。
  • 无缓冲 Channel:直接交换数据,发送者和接收者必须同时就绪。
  • 关闭 Channel:设置 closed=1,唤醒 recvqsendq 中所有 G。向已关闭 Channel 发送会 panic,接收会返回零值+false。

关键考点select 语句在编译时会转换为 selectgo 函数,采用随机轮询策略选择可用的 case,避免饥饿。


3.5 字节五面:MySQL 深度优化

题目 9:线上慢 SQL 如何排查与优化?

点击查看详细解答

排查步骤

  1. 定位慢 SQL

    • 开启 MySQL 慢查询日志:slow_query_log=1,设置阈值 long_query_time=1
    • 使用 mysqldumpslowpt-query-digest 分析日志。
    • 实时查看:SHOW PROCESSLISTperformance_schema
  2. 分析执行计划

    • 使用 EXPLAIN ANALYZE(MySQL 8.0.18+)查看实际执行时间和行数。
    • 关注 type(是否出现 ALL 全表扫描)、key(是否用到索引)、Extra(是否 Using filesort/Using temporary)。
  3. 常见优化手段

    • 索引优化:遵循最左前缀原则,避免索引失效(如函数操作、类型隐式转换、like ‘%xx’)。
    • 覆盖索引:查询字段全部在索引中,避免回表。
    • 分页优化LIMIT 100000, 10 改为先查 ID 再 JOIN,或使用覆盖索引+子查询。
    • Join 优化:小表驱动大表,确保 Join 字段有索引,必要时使用 STRAIGHT_JOIN 强制顺序。
    • SQL 改写:避免 SELECT *,拆分大事务,批量插入改为 INSERT ... VALUES (), ()

真实案例:曾遇到一条 UPDATE 慢 SQL,原因是 WHERE 条件中对索引字段使用了 DATE(create_time) 函数,导致索引失效。改为范围查询 create_time BETWEEN '2026-01-01' AND '2026-02-01' 后,从 12s 降到 10ms。


3.6 字节六面:高并发架构

题目 10:限流算法对比与实现

点击查看详细解答
算法 原理 优点 缺点
计数器 固定窗口计数 简单 窗口边界突发流量(临界问题)
滑动窗口 细分为多个子窗口 平滑临界问题 内存占用略高
令牌桶 匀速放令牌,请求取令牌 允许一定突发流量 实现稍复杂
漏桶 请求入桶,匀速漏出 绝对平滑 无法应对突发流量

令牌桶 Go 实现

type TokenBucket struct {
    rate       int64 // 每秒放令牌数
    capacity   int64 // 桶容量
    tokens     int64 // 当前令牌数
    lastUpdate int64 // 上次更新时间(毫秒)
    mu         sync.Mutex
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    
    now := time.Now().UnixMilli()
    elapsed := now - tb.lastUpdate
    tb.tokens += elapsed * tb.rate / 1000
    if tb.tokens > tb.capacity {
        tb.tokens = tb.capacity
    }
    tb.lastUpdate = now
    
    if tb.tokens > 0 {
        tb.tokens--
        return true
    }
    return false
}

分布式限流:单机限流用 Guava RateLimiter 或上述代码;分布式场景用 Redis + Lua 脚本 实现令牌桶,或直接使用 Sentinel。

题目 11:如何设计一个高并发秒杀系统?

点击查看详细解答

核心挑战:高并发读(库存查询)、高并发写(扣减库存)、超卖问题。

分层架构

  1. 前端层

    • 静态资源 CDN 化,按钮置灰(点击后 N 秒内不可点)。
    • 验证码/答题机制,错峰流量(削峰)。
  2. 网关层

    • Nginx 限流:limit_req_zone 限制单 IP 请求频率。
    • 黑名单过滤:提前识别刷单 IP。
  3. 应用层

    • Redis 预减库存:活动开始前将库存加载到 Redis,使用 DECR 原子扣减。
    • 异步下单:Redis 扣减成功后,发送 MQ 消息,异步创建订单。
    • 库存回补:如果订单 30 分钟未支付,Redis 库存 INCR 回补。
  4. 数据层

    • 数据库扣减使用 乐观锁

      UPDATE product SET stock = stock - 1, version = version + 1 
      WHERE id = 1 AND stock > 0 AND version = #{version}
      
    • 或者使用 MySQL 的 WHERE stock > 0 原子判断。

防超卖关键:Redis 的 DECR 是单线程原子操作,不会超卖。但需要注意 Redis 和 DB 的最终一致性,一般通过 MQ 保证。


3.7 字节七面:项目深挖

题目 12:一致性 Hash 在你项目中怎么用的?

点击查看详细解答

项目背景:我们有一个图片存储服务,使用多台服务器做缓存。为了应对服务器扩容/缩容,采用了一致性 Hash。

原理

  • 将节点和数据的 Key 都映射到一个 2^32 的环上。
  • 数据顺时针找到的第一个节点即为其存储节点。
  • 引入虚拟节点(如 150 个虚拟节点/物理节点)解决数据倾斜问题。

代码核心

public class ConsistentHash<T> {
    private final TreeMap<Long, T> circle = new TreeMap<>();
    private final int virtualNodes;
    
    public void add(T node) {
        for (int i = 0; i < virtualNodes; i++) {
            long hash = hash(node.toString() + i);
            circle.put(hash, node);
        }
    }
    
    public T get(Object key) {
        if (circle.isEmpty()) return null;
        long hash = hash(key);
        if (!circle.containsKey(hash)) {
            Map.Entry<Long, T> entry = circle.ceilingEntry(hash);
            if (entry == null) entry = circle.firstEntry();
            return entry.getValue();
        }
        return circle.get(hash);
    }
}

面试官追问:如果节点宕机,缓存失效怎么解决?
:采用 Hash 环 + 主从备份。每个物理节点在环上有主虚拟节点和从虚拟节点,主节点宕机时,请求顺时针找到从节点。或者结合 Hot Key 本地缓存(如 Caffeine)做兜底。


四、阿里巴巴(7 场面试真题)

阿里面试特点是:Java 源码问得极深、重视分布式架构设计、喜欢问开放性的场景题

4.1 阿里一面:Java 基础

题目 13:HashMap 源码详解,1.7 和 1.8 的区别?

点击查看详细解答

核心参数

  • 默认初始容量:16
  • 负载因子:0.75
  • 树化阈值:8(链表长度≥8 且 数组长度≥64 时转红黑树)
  • 反树化阈值:6

1.7 vs 1.8 关键差异

特性 JDK 1.7 JDK 1.8
数据结构 数组 + 链表 数组 + 链表 + 红黑树
插入方式 头插法 尾插法
扩容 先扩容再插入 先插入再扩容
Hash 算法 4 次位运算 + 5 次异或 1 次位运算 + 1 次异或(高 16 位异或低 16 位)
并发安全 头插法导致死循环 尾插法不会死循环,但仍不线程安全

Hash 计算

// JDK 1.8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

目的:让高 16 位也参与运算,减少低位相同导致的冲突。

扩容机制

  • 容量变为 2 倍,元素位置要么在原索引,要么在原索引 + 旧容量。
  • 判断依据:e.hash & oldCap == 0 则位置不变,否则位置 = 原位置 + oldCap。

线程安全替代ConcurrentHashMap(1.8 使用 CAS + synchronized,锁粒度为链表头节点)。


4.2 阿里二面:JVM 与调优

题目 14:线上服务 Full GC 频繁,如何排查和解决?

点击查看详细解答

排查步骤

  1. 确认 GC 情况

    • jstat -gcutil PID 1000 查看各代占用和 GC 次数。
    • 开启 GC 日志:-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
  2. 分析 GC 日志

    • 使用 GCViewer 或 GCEasy 可视化分析。
    • 关注 Full GC 触发原因:老年代空间不足 / Metaspace 不足 / System.gc() 调用。
  3. 常见原因与解决

现象 原因 解决方案
老年代快速增长 内存泄漏 / 大对象过多 MAT/Arthas 分析堆 dump,修复泄漏
Minor GC 后大量对象进入老年代 对象生命周期过长 / Survivor 区太小 调大 SurvivorRatio,检查代码
Metaspace OOM 动态生成类过多(如 CGLIB/反射) 增大 Metaspace,检查动态代理
System.gc() 触发 代码显式调用 / NIO 直接内存回收 添加 -XX:+DisableExplicitGC

调优案例
某服务使用 Kafka 消费者,每次拉取 500 条消息处理,导致 Young GC 后大量对象晋升老年代。解决方案:

  • 减少单次拉取量到 100 条。
  • 调大新生代(-Xmn)从 512M 到 1.5G。
  • 使用 G1 垃圾收集器替代 CMS,设置 -XX:MaxGCPauseMillis=200

G1 调优参数

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45

4.3 阿里三面:分布式框架

题目 15:Dubbo 的 SPI 机制与 JDK SPI 有什么区别?

点击查看详细解答

JDK SPI

  • 通过 ServiceLoader 加载 META-INF/services/ 下的接口全限定名文件。
  • 缺点:一次性加载所有实现类,无法按需加载;不支持别名和 IOC/AOP。

Dubbo SPI

  • 配置文件路径:META-INF/dubbo/META-INF/dubbo/internal/
  • 支持别名:如 @SPI("dubbo") 指定默认扩展名。
  • 按需加载:根据 URL 中的参数(如 protocol=dubbo)动态选择实现类。
  • 支持 IOC 和 AOP
    • IOC:通过 ExtensionFactory 注入依赖(如 AdaptiveExtensionFactory)。
    • AOP:使用 Wrapper 类对扩展点进行包装,实现 AOP(如 ProtocolFilterWrapper)。

自适应扩展(Adaptive)

@SPI
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
}

@Adaptive 会动态生成代理类,根据 URL 参数决定调用哪个实现。

面试官追问:Dubbo 的服务导出(Export)流程是怎样的?

  1. 解析 @Service 注解,封装为 ServiceConfig
  2. 调用 Protocol.export(),经过 ProtocolFilterWrapper(构建过滤器链)和 ProtocolListenerWrapper
  3. 打开 Server(Netty),监听端口。
  4. 向注册中心(Nacos/ZK)注册服务 URL。

4.4 阿里四面:消息队列

题目 16:RocketMQ 的事务消息和顺序消息如何实现?

点击查看详细解答

事务消息
基于 两阶段提交 + 事务回查 机制。

  1. 半消息(Half Message):生产者发送事务消息,MQ 将其标记为"暂不能投递",消费者不可见。
  2. 执行本地事务:生产者执行本地业务(如扣减库存)。
  3. 提交或回滚
    • 本地事务成功:发送 Commit,MQ 将半消息变为可投递。
    • 本地事务失败:发送 Rollback,MQ 删除半消息。
  4. 事务回查:如果生产者超时未响应,MQ 会定时回查生产者本地事务状态(默认 1 分钟一次,最多 15 次)。

顺序消息

  • 全局顺序:一个 Topic 只有一个队列,性能差,很少使用。
  • 分区顺序:同一业务 ID(如订单 ID)的消息发送到同一个队列,利用队列的 FIFO 特性保证顺序。
  • 实现:生产者发送时通过 MessageQueueSelector 根据 Sharding Key 选择队列;消费者使用 MessageListenerOrderly,加锁消费。

面试官追问:如果消费者集群消费顺序消息,怎么保证顺序?
:Broker 会对队列加分布式锁(Rebalance 时分配队列),同一时刻只有一个消费者实例消费该队列。消费失败时暂停当前队列消费,而不是跳过,避免乱序。


4.5 阿里五面:Redis 深度

题目 17:Redis 分布式锁,Redisson 的看门狗机制?

点击查看详细解答

基础分布式锁

SET lock_key unique_value NX PX 30000
  • NX:仅当 key 不存在时设置。
  • PX:设置过期时间,防止死锁。
  • unique_value:释放时通过 Lua 脚本判断是否为当前线程的锁。

Redisson 实现
Redisson 使用 hash 结构存储锁:

KEY: myLock
VALUE: {
    "UUID:threadId": 1  // 重入次数
}

看门狗(Watch Dog)机制

  1. 加锁时,如果未指定 leaseTime,Redisson 默认设置 30 秒过期。
  2. 加锁成功后,启动一个后台线程(看门狗),每隔 10 秒(leaseTime/3)检查锁是否仍被持有。
  3. 如果持有,则续期到 30 秒。
  4. 如果业务执行完毕,主动解锁,看门狗停止。

关键源码

// 看门狗续期
RFuture<Void> scheduleExpirationRenewal(long threadId) {
    Timeout task = commandExecutor.getConnectionManager().newTimeout(timeout -> {
        RFuture<Boolean> future = renewExpirationAsync(threadId);
        future.onComplete((res, e) -> {
            if (res) {
                scheduleExpirationRenewal(threadId); // 递归续期
            }
        });
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 10秒
}

红锁(RedLock):在 Redis 主从架构下,如果主节点宕机导致锁未同步到从节点,可能丢失锁。RedLock 算法要求在 N 个独立 Redis 节点上同时加锁,超过半数成功才算获取锁。但在实际生产环境中,RedLock 争议较大,很多公司直接使用 Redisson 的单节点锁 + 业务幂等兜底。


4.6 阿里六面:数据库架构

题目 18:分库分表方案设计,ShardingSphere 的原理?

点击查看详细解答

分库分表策略

策略 说明 适用场景
Hash 取模 对分片键取模 数据分布均匀,但扩容需迁移数据
范围分片 按时间或 ID 范围 容易扩容,但可能热点集中
标签分片 按地区/租户 多租户系统
混合分片 先范围再 Hash 兼顾扩容和均匀

分库分表问题

  1. 分布式 ID:雪花算法(Snowflake),注意时钟回拨问题(可用百度 UidGenerator 或美团 Leaf)。
  2. 跨分片查询:使用 ShardingSphere 的联邦查询,或异构索引表(ES/HBase)。
  3. 分页排序:先查各分片 Top N,再内存归并排序。
  4. 分布式事务:Seata AT 模式,或最终一致性(消息队列)。

ShardingSphere 原理

  • ShardingJDBC:JDBC 驱动层增强,对业务透明。解析 SQL → 改写 SQL(如分表后表名替换)→ 路由到真实数据源 → 执行 → 结果归并。
  • ShardingProxy:独立部署的数据库代理,支持任意语言,类似 MyCat。
  • SQL 解析:使用 ANTLR 生成语法树,支持 MySQL/PostgreSQL/Oracle 等。
  • 执行引擎:分为连接模式和内存模式。连接模式会复用连接(流式归并),内存模式会加载全部数据到内存(内存归并)。

4.7 阿里七面:HR 面 + 项目亮点

题目 19:你做过最有技术挑战的项目是什么?

点击查看回答思路

STAR 法则

  • S(背景):公司旧订单系统使用单体架构,高峰期(如大促)数据库 CPU 飙到 90%,接口 RT 超过 5s,频繁告警。
  • T(任务):负责订单系统的重构,目标是支撑 10 倍流量,接口 RT < 200ms。
  • A(行动)
    1. 数据库拆分:按用户 ID 水平分 16 库 64 表,使用 ShardingSphere。
    2. 缓存架构:引入 Redis Cluster,热点数据本地缓存(Caffeine)+ 分布式缓存二级架构。
    3. 异步化:订单创建后,非核心流程(如发短信、积分)改为 MQ 异步处理。
    4. 限流降级:接入 Sentinel,核心接口兜底。
    5. 压测调优:全链路压测发现 GC 问题,优化后 Young GC 从 200ms 降到 30ms。
  • R(结果):大促期间订单接口 QPS 从 2k 提升到 3w,RT 稳定在 80ms,零故障。

HR 追问:如果重来一次,你会怎么改进?
:会提前做全链路灰度,当时直接全量切流风险较高;另外会引入混沌工程,提前演练 Redis 集群宕机、DB 主从切换等故障场景。


五、腾讯(6 场面试真题)

腾讯面试特点是:C++ 和计算机基础问得深、网络协议必须精通、喜欢问操作系统和海量并发场景

5.1 腾讯一面:网络 + 操作系统

题目 20:TCP 三次握手和四次挥手,为什么是三次?

点击查看详细解答

三次握手

  1. SYN:客户端发送 SYN=1, seq=x,进入 SYN_SENT
  2. SYN+ACK:服务端发送 SYN=1, ACK=1, seq=y, ack=x+1,进入 SYN_RCVD
  3. ACK:客户端发送 ACK=1, seq=x+1, ack=y+1,双方进入 ESTABLISHED

为什么是三次?

  • 防止历史连接:如果网络中存在延迟的 SYN,客户端可以通过第三次 ACK 的上下文判断是否是当前连接(如 RST 机制)。
  • 同步双方初始序列号:双方都确认了自己的发送和接收能力正常。
  • 两次握手的问题:如果客户端的 SYN 重传了两次,服务端会建立两个连接,造成资源浪费。

四次挥手

  1. FIN:主动方发送 FIN=1, seq=u,进入 FIN_WAIT_1
  2. ACK:被动方发送 ACK=1, ack=u+1,进入 CLOSE_WAIT。此时被动方可能还有数据要发。
  3. FIN:被动方数据发完后,发送 FIN=1, seq=w,进入 LAST_ACK
  4. ACK:主动方发送 ACK=1, ack=w+1,进入 TIME_WAIT(等待 2MSL 后关闭)。

TIME_WAIT 作用

  • 保证最后一个 ACK 能到达对方(如果丢失,对方会重发 FIN)。
  • 等待 2MSL(Maximum Segment Lifetime)让网络中所有报文消失,防止下一个连接收到旧报文。

面试官追问:大量 CLOSE_WAIT 怎么解决?
:说明被动方(通常是服务端)没有正确关闭连接。检查代码是否漏了 close(),或者业务逻辑阻塞在 IO/数据库查询。


5.2 腾讯二面:系统编程

题目 21:进程、线程、协程的区别?Linux 的线程实现?

点击查看详细解答
特性 进程 线程 协程
资源占用 独立地址空间、文件描述符 共享进程资源,独立栈和寄存器 用户态,极轻量(几 KB 栈)
切换开销 大(需切换页表、TLB 刷新) 中(切换寄存器、栈) 小(纯用户态,无内核介入)
通信方式 IPC(管道、共享内存、Socket) 共享内存,需同步机制 直接读写共享变量
调度 内核调度 内核调度 用户态调度器
阻塞影响 不影响其他进程 阻塞会导致整个进程挂起 阻塞会切换协程,不影响其他协程

Linux 线程实现(NPTL)

  • Linux 没有严格区分进程和线程,统一用 task_struct 表示。
  • 线程是轻量级进程(LWP),通过 clone() 系统调用创建,共享 mm_struct(地址空间)、files_struct(文件描述符表)等。
  • 通过 pthread 库封装,底层调用 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)

面试官追问:线程栈在进程的哪个区域?
:线程栈位于进程的堆栈区,但每个线程有独立的栈空间(默认 8MB,可通过 ulimit -s 查看)。Linux 使用 mmap 为线程分配栈,并设置 guard page(保护页)防止栈溢出。


5.3 腾讯三面:高性能服务器

题目 22:epoll 的原理,为什么比 select/poll 快?

点击查看详细解答

select/poll 的缺点

  • select 有 FD_SETSIZE 限制(默认 1024)。
  • 每次调用都需要将 fd 集合从用户态拷贝到内核态。
  • 内核需要遍历所有 fd,检查是否有事件发生,时间复杂度 O(n)。
  • 返回后,用户态也需要遍历所有 fd,找到就绪的。

epoll 的优化

  1. 红黑树存储 fd:使用 epoll_ctl 增删改 fd,只需一次用户态→内核态拷贝。红黑树保证增删改 O(log n)。
  2. 就绪链表(rdllist):内核通过回调函数(ep_poll_callback),在设备就绪时将 fd 加入就绪链表,无需遍历。
  3. mmap 共享内存:epoll 的 epoll_event 数组使用 mmap 映射到用户空间,避免了 epoll_wait 返回时的拷贝。
  4. 返回即就绪epoll_wait 直接返回就绪的 fd 数组,时间复杂度 O(1)(就绪数量)。

LT(水平触发)vs ET(边缘触发)

  • LT:只要 fd 还有数据,每次 epoll_wait 都会返回。编程简单,但可能重复触发。
  • ET:仅在状态变化时触发一次(如从无数据→有数据)。需要一次性读写完所有数据(配合非阻塞 IO),性能更高但编程复杂。

代码示例

int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];

ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);

while (1) {
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listen_fd) {
            // accept 新连接
        } else {
            // 处理 IO,ET 模式下需循环 read 直到 EAGAIN
        }
    }
}

5.4 腾讯四面:算法 + 数据库

题目 23:合并 K 个升序链表

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;
    PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
    
    for (ListNode node : lists) {
        if (node != null) pq.offer(node);
    }
    
    ListNode dummy = new ListNode(0);
    ListNode cur = dummy;
    
    while (!pq.isEmpty()) {
        ListNode node = pq.poll();
        cur.next = node;
        cur = cur.next;
        if (node.next != null) pq.offer(node.next);
    }
    return dummy.next;
}

时间复杂度 O(N log K),N 为总节点数,K 为链表数。优先队列大小为 K,每次插入/弹出 O(log K)。

题目 24:MySQL 的 InnoDB 索引结构,为什么用 B+ 树?

点击查看详细解答

B+ 树特点

  1. 数据都在叶子节点:非叶子节点只存索引键值和指针,可以容纳更多索引,树更矮,IO 次数更少。
  2. 叶子节点有序且链表连接:方便范围查询(如 BETWEENORDER BY)和全表扫描。
  3. 查询稳定:任何查询都要走到叶子节点,时间复杂度稳定 O(log n)。

对比其他结构

  • Hash:等值查询快 O(1),但不支持范围查询和排序。
  • 二叉树/AVL/红黑树:树太高,磁盘 IO 次数多(一个节点一次 IO)。
  • B 树:非叶子节点也存数据,导致一个页存储的键值少,树更高;且范围查询需要中序遍历,效率低。

聚簇索引 vs 非聚簇索引

  • 聚簇索引:叶子节点存整行数据(InnoDB 主键索引)。
  • 非聚簇索引(二级索引):叶子节点存主键值,需要回表查询整行数据。
  • 覆盖索引:查询字段都在二级索引中,无需回表(如 SELECT name, age FROM user WHERE name = 'xx',如果索引是 (name, age))。

5.5 腾讯五面:微服务架构

题目 25:微服务治理中的熔断、降级、限流

点击查看详细解答
机制 触发条件 目的 实现
熔断 错误率/慢调用超过阈值 防止故障扩散,给下游恢复时间 Sentinel、Hystrix(已停更)
降级 系统负载过高 / 依赖服务不可用 保证核心功能可用,牺牲非核心 返回默认值、静态页面
限流 QPS/并发数超过阈值 保护系统不被流量压垮 令牌桶、漏桶、滑动窗口

熔断器状态机

  1. Closed:正常状态,请求通过,统计错误率。
  2. Open:错误率超过阈值,熔断器打开,请求直接失败(快速失败)。
  3. Half-Open:经过一段休眠时间后,允许少量请求通过试探。
    • 如果成功:切换到 Closed。
    • 如果失败:回到 Open。

Sentinel 熔断策略

  • 慢调用比例:RT 超过阈值的比例。
  • 异常比例:异常数占总请求数的比例。
  • 异常数:单位时间内的异常数量。

降级实战
某电商系统,推荐服务挂了时:

  • 熔断:停止调用推荐服务,避免拖垮商品详情页。
  • 降级:返回"热门推荐"的缓存数据(静态兜底),保证页面能打开。

5.6 腾讯六面:总监面

题目 26:设计一个支持百万并发的 IM(即时通讯)系统

点击查看详细解答

架构分层

  1. 接入层

    • 长连接网关:使用 Netty/Go 实现 WebSocket/TCP 网关,维护用户连接。
    • 负载均衡:LVS + Nginx 做四层负载,网关层无状态,可水平扩展。
    • 连接管理:一个网关实例维护 50W 长连接,100W 并发需 2-4 台网关。
  2. 路由层

    • 用户在线状态:使用 Redis 存储 user_id -> gateway_ip:port 映射。
    • 消息路由:用户 A 发消息给用户 B,先查 Redis 找到 B 所在的网关,通过 RPC 转发。
  3. 业务层

    • 消息存储:写扩散 vs 读扩散。
      • 写扩散(推):群消息发给所有成员,每人存一份。适合小群(<500人)。
      • 读扩散(拉):群消息只存一份,成员拉取。适合大群(如千人群)。
    • 消息顺序:利用 snowflake 算法生成全局递增 ID,或按会话分片保证单聊/群聊有序。
  4. 存储层

    • 近期消息:Redis 缓存最近 7 天消息。
    • 历史消息:HBase/TiDB 存储,按时间分片。
    • 未读数:Redis Hash 结构,HINCRBY user:1001 unread 1
  5. 高可用

    • 网关多活部署,客户端支持断线重连和心跳(30s/次)。
    • 消息至少投递一次(ACK 机制),幂等性通过 msg_id 去重。

面试官追问:如果用户量从百万扩展到亿级,怎么设计?

  • 按用户 ID 做地理分区(如国内分华北、华东、华南),不同区域部署独立集群。
  • 引入 MQTT 协议 替代 WebSocket,更适合弱网环境。
  • 使用 QUIC 协议 替代 TCP,解决队头阻塞和连接迁移问题。

六、高频考点总结(面试必背)

6.1 算法题(出现频率 Top 5)

  1. LRU Cache(手撕代码,必须熟练)
  2. 合并 K 个有序链表(优先队列经典题)
  3. 最长无重复字符子串(滑动窗口)
  4. 手撕线程池/生产者消费者(并发编程)
  5. 岛屿数量/拓扑排序(BFS/DFS)

6.2 八股文(每场必问)

领域 核心考点
Java HashMap、ConcurrentHashMap、JUC 包、线程池参数、JVM GC
Go GMP 调度、Channel 底层、Context、内存管理、Goroutine 泄漏排查
MySQL 索引优化、事务隔离级别、MVCC、锁(行锁/间隙锁/临键锁)、主从同步
Redis 数据类型、持久化、分布式锁、缓存穿透/击穿/雪崩、集群模式
Kafka 高吞吐原理、ISR 机制、消息可靠性、消费者组 Rebalance
网络 TCP/UDP、HTTP/2/3、HTTPS 握手、QUIC、epoll、零拷贝
OS 进程线程协程、内存管理(页表/TLB)、IO 多路复用、Linux 常用命令
分布式 CAP/Base、分布式事务、一致性算法(Raft/Paxos)、分库分表

6.3 项目深挖(总监面必问)

  • 项目的技术难点量化指标(QPS、RT、数据量)。
  • 如果流量扩大 10 倍,架构如何调整?
  • 做过的最失败/最有成就感的技术决策是什么?
  • 线上故障的排查过程和复盘(STAR 法则)。

七、面试心得与建议

7.1 准备阶段

  1. 算法:LeetCode 200 题打底,重点刷 Hot 100 和剑指 Offer。字节特别爱考滑动窗口、二叉树、回溯、并查集
  2. 八股:不要死记硬背,结合源码和实际场景理解。比如看 JDK 源码时,要问自己"如果我是设计者,会怎么解决并发问题?"。
  3. 项目:准备 2-3 个深度项目,能讲清楚背景、方案对比、技术细节、数据指标、后续优化

7.2 面试技巧

  1. 先讲思路,再写代码:算法题先沟通暴力解,再优化,体现思维过程。
  2. 引导面试官:如果某个知识点你很熟,回答时故意留一点"钩子",引导面试官追问。比如提到 Redis 分布式锁时,主动提"不过主从架构下 RedLock 也有争议"。
  3. 不会就坦诚:遇到不会的,可以说"这个我没有深入研究过,但我了解相关的 XX,您看我可以从那个角度分析吗?"

7.3 心态调整

  • 春招是一场持久战,我最多一天面 3 场,晚上复盘到 12 点。面试挂掉很正常,重要的是复盘
  • 建议用 Notion/飞书文档记录每道题,形成自己的"面试题库"。

八、结语

以上就是我 2026 年春招 20 场面试的全部真题汇总

技术面试的本质是:证明你能解决实际问题。 八股是基础,项目是亮点,算法是门槛,系统设计是分水岭。

如果你觉得这篇文章对你有帮助,欢迎 点赞、收藏、转发!有任何问题可以在评论区留言,我会尽量回复。

后续计划:我会陆续发布《Redis 源码深度解析》、《从零实现一个 RPC 框架》、《Kafka 高性能设计源码级分析》等系列文章,欢迎关注!


推荐阅读

更多推荐