【字节跳动|跨境电商】一面复盘|Redis 跳表 + 线程池调优 + 一致性设计 + LRU 实现

面试公司:字节跳动(跨境电商)
面试岗位:后端开发工程师
面试形式:电话面
面试时长:约 45 分钟
面试轮次:第一轮技术面


✨ 面试整体节奏:

这场字节跳动一面整体节奏中等偏快,主要围绕项目展开,过程中穿插 Java 基础、并发编程、Redis 数据结构和系统设计相关问题,最后还加了一道手撕 LRU 算法。

面试官非常注重细节,很多问题会顺着我的项目展开深入提问,比如 Redis 的具体数据结构、线程池的参数配置细节等,建议提前把项目做过的内容尽量准备得扎实一点。


✅ 面试题目逐题整理与解析:

1. 请你做一个简短的自我介绍

考察点:沟通表达、自我认知
回答建议: 介绍自己的学校、实习经历、项目亮点,突出与 JD 匹配的点。


2. 详细介绍一下你最近做的项目

考察点:项目深度与技术选型思考
回答建议: 聚焦核心业务逻辑、技术难点、迭代过程,强调自己负责的部分,适当引出 Redis 缓存、MySQL/ES 数据一致性等后续技术点。


3. Redis 中的 ZSet 是怎么实现的?底层结构是什么?

考察点:Redis 数据结构原理
答案: ZSet 是有序集合,底层由 跳表(skip list)+ 哈希表 实现。跳表用于有序性、范围查询;哈希表用于通过 member 快速定位。


4. 跳表是怎么实现的?时间复杂度是多少?

考察点:数据结构设计、空间换时间思想
答案: 跳表通过多层索引提升查找效率,最坏时间复杂度是 O(log n),插入/删除也是 O(log n),空间复杂度是 O(n)。
使用随机算法控制层数,使其平均性能接近平衡树,但实现更简单。


5. ZSet 中如何按 Key 查询?查询复杂度是多少?

考察点:API 与实现的理解
答案: 如果是通过 member 查询分数,哈希表复杂度是 O(1);如果是按 score 范围查询,使用跳表,复杂度是 O(log n + m),m 是结果数量。

这里还有一个“by 什么”的没听清,可能是 ZRANGEBYSCOREZRANK 等指令,建议面试前复习一下 Redis Sorted Set 的常用命令和时间复杂度。


6. 如果一个业务要先写 MySQL 再写 ES,怎么保证数据一致性?

考察点:分布式一致性、最终一致性设计
答案:

  • 理想方案是使用 消息队列 做削峰解耦,通过异步刷新 ES,业务先写 MySQL 成功,再投递一条 MQ 消息,消费者刷新 ES。
  • 宕机问题需要通过消息重试 + 死信队列兜底,核心思路是实现最终一致性。
  • 如果强一致需求,可考虑 二阶段提交、分布式事务(如 Seata),但一般业务倾向使用最终一致性方案。

7. Java 中你用过哪些集合?常用场景?

考察点:集合框架理解
答案:

  • 常用集合:ArrayList(顺序、随机读)、HashMap(KV 存储)、LinkedList(频繁插入删除)
  • 根据业务特性选用不同集合。

8. 你用过哪些并发集合?各自适用于什么场景?

考察点:并发编程基础
答案:

  • ConcurrentHashMap:线程安全的 HashMap,分段锁或 CAS 实现,适用于高并发读写场景。
  • CopyOnWriteArrayList:读多写少场景,写操作开销大但读操作无锁。

9. ThreadLocal 能将变量传递给子线程吗?

考察点:ThreadLocal 的使用边界
答案: 普通 ThreadLocal 不支持线程间传值,但 InheritableThreadLocal 可以把父线程的值传给子线程。
但在线程池中,线程是复用的,InheritableThreadLocal 可能出现脏数据问题。推荐使用 TransmittableThreadLocal(TTL) 来解决线程池中变量传递问题。


10. Java 线程池怎么创建?有哪些参数?

考察点:线程池使用与调优
答案:
通过 Executors 工厂类或自定义 ThreadPoolExecutor。推荐使用后者以便自定义参数:

new ThreadPoolExecutor(
  corePoolSize,
  maximumPoolSize,
  keepAliveTime,
  TimeUnit.SECONDS,
  new LinkedBlockingQueue<>(),
  new ThreadFactoryBuilder().setNameFormat("custom-pool-%d").build(),
  new ThreadPoolExecutor.AbortPolicy()
);

主要参数解释:

  • 核心线程数(corePoolSize)
  • 最大线程数(maximumPoolSize)
  • 队列容量(workQueue)
  • 线程存活时间(keepAliveTime)
  • 拒绝策略(rejectionHandler)

11. 如何设置线程池大小?如果是 CPU 密集型和 IO 密集型任务呢?

考察点:性能调优与系统资源评估
答案:

  • CPU 密集型:线程数 = CPU 核数 + 1
  • IO 密集型:线程数 = CPU 核数 /(1 - 阻塞系数),阻塞系数通常为 0.8,则线程数约为 CPU 核心数 * 5

举例:8 核 CPU,阻塞系数 0.8,则线程数 ≈ 8 / (1 - 0.8) = 40
(回答为略大于 40 是合理的)


12. 手撕代码:实现一个 LRU 缓存

考察点:数据结构组合、缓存设计
思路: 使用 HashMap + 双向链表,O(1) 实现 get/put。

class LRUCache {
    class Node {
        int key, value;
        Node prev, next;
        Node(int k, int v) { key = k; value = v; }
    }

    private final int capacity;
    private Map<Integer, Node> map;
    private Node head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new Node(0, 0); tail = new Node(0, 0);
        head.next = tail; tail.prev = head;
    }

    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        remove(node); insertToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) remove(map.get(key));
        if (map.size() == capacity) remove(tail.prev);
        Node node = new Node(key, value);
        insertToHead(node);
        map.put(key, node);
    }

    private void insertToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    private void remove(Node node) {
        map.remove(node.key);
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}

总结 & 面试复盘

这轮面试整体是围绕“项目 + 基础 + 设计”展开的,Redis 和线程池考察较深入,跳表、ZSet 的复杂度、线程传值细节问得挺细,说明面试官在考察你是否真正理解工具的底层原理和使用场景

我的项目讲得还算 OK,但在分布式事务、线程池参数等问题上回答略微模糊,后续复盘时建议重点补:

  • Redis 数据结构底层 + 命令时间复杂度
  • 分布式一致性方案(MQ、事务)
  • ThreadLocal 子线程传值问题
  • 线程池调优与 IO 密集场景分析
  • 常见数据结构手撕题(LRU、LFU、单调栈)

最后:

如果你也在准备字节/美团/京东等后端开发岗位的面试,欢迎留言交流。我会持续整理各大厂真实面经、高频八股和项目拆解,希望大家都能早日拿下心仪的 offer !

你可能感兴趣的:(redis,数据库,缓存)