小红书面试真题-算法与源码实战
小红书面试真题-算法与源码实战
一、面试基础与技术栈考察
1.1 Java 基础与多线程考察
核心说明:梳理Java基础与多线程核心考点,关联小红书高并发、推荐算法等业务场景,明确考点与业务的对应关系,体现”技术落地业务”的面试考察重点。

Q1:请介绍一下你对 Java 内存模型的理解,以及在小红书高并发场景下的应用?
A1: Java 内存模型(JMM)是 Java 虚拟机规范中定义的一种抽象概念,它定义了线程和主内存之间的抽象关系。在小红书的高并发场景下,JMM 的理解至关重要,特别是在处理用户评论、点赞、收藏等高频操作时。
Java 内存模型的核心包括三个方面:原子性、可见性和有序性。在小红书的内容推荐系统中,我们经常需要处理用户行为的原子性操作,比如用户同时点赞和收藏一篇笔记时,需要保证这两个操作的原子性。可见性在分布式场景下尤为重要,当一个线程修改了某个用户的兴趣标签后,其他线程需要能够立即看到这个变化,这在小红书的实时推荐算法中是关键需求。
在实际应用中,我在参与小红书类似项目的开发时,遇到过一个典型问题:多个推荐算法线程同时读取和更新用户画像数据时,出现了数据不一致的情况。通过深入理解 JMM,我们采用了 volatile 关键字来保证用户兴趣向量的可见性,同时使用 synchronized 块来保证关键操作的原子性。
Q2:请解释一下 Java 中 synchronized 和 Lock 的区别,以及在小红书业务场景中的使用选择?
A2: synchronized 和 Lock 都是 Java 中用于实现线程同步的机制,但在实现原理和使用场景上有显著差异。在小红书的业务开发中,正确选择这两种机制对系统性能至关重要。
synchronized 是 Java 关键字,在编译后会在同步块前后生成 monitorenter 和 monitorexit 字节码指令,通过 JVM 内置的 Monitor 机制实现同步。它的优势在于使用简单,能够自动释放锁,避免死锁。在小红书的用户登录认证场景中,我们使用 synchronized 来保证用户会话的安全,因为这种场景下锁的持有时间较短,不需要复杂的锁控制逻辑。
Lock 是 Java.util.concurrent.locks 包下的接口,提供了更灵活的锁控制机制。ReentrantLock 是其主要实现类,提供了可中断锁、超时锁、公平锁等高级特性。在小红书的订单处理系统中,我们使用 ReentrantLock 来实现分布式锁,因为订单处理涉及多个步骤,需要更精细的锁控制,比如在库存扣减时可以设置超时时间,避免长时间阻塞。
Q3:请描述一下 Java 线程池的核心参数,以及在小红书推荐算法中的配置策略?
A3: Java 线程池的核心参数包括corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程存活时间)、unit(时间单位)、workQueue(工作队列)和handler(拒绝策略)。
在小红书的推荐算法场景中,线程池的配置需要根据业务特点进行优化。比如在实时推荐计算中,我们通常将 corePoolSize 设置为 CPU 核心数的 1.5 倍,这样既能充分利用多核 CPU,又能避免线程切换的开销。maximumPoolSize 的设置需要考虑系统的最大负载,在小红书的峰值流量期间,比如明星发布新笔记时,我们会将 maximumPoolSize 设置得更高,以应对突发的计算需求。
工作队列的选择也很关键。在推荐算法中,我们通常使用 LinkedBlockingQueue,因为它是无界队列,可以缓冲大量的推荐请求,避免任务丢失。拒绝策略方面,我们采用了 CallerRunsPolicy,当线程池和队列都满时,让调用者线程直接执行任务,这样可以保证关键的推荐请求不被丢弃。
1.2 数据结构与算法基础
核心说明:聚焦Redis、MySQL两大核心中间件的数据结构,衔接小红书缓存、用户关系查询等核心业务,突出”数据结构支撑业务性能”的考察逻辑。

Q4:请介绍一下你对 Redis 数据结构的理解,以及在小红书缓存场景中的应用?
A4: Redis 提供了丰富的数据结构,在小红书的技术栈中扮演着核心角色。小红书的技术架构中,Redis 被广泛应用于缓存、消息队列、分布式锁等场景(12)。
在小红书的内容推荐系统中,我们使用 Redis 的哈希结构来存储用户画像数据,包括用户的兴趣标签、浏览历史、互动记录等。每个用户对应一个哈希表,字段包括 “兴趣标签”、”最近浏览”、”点赞列表” 等,这样可以快速查询和更新用户的个性化数据。
有序集合(Sorted Set)在小红书的热门内容排行中发挥重要作用。我们使用 ZSET 结构存储笔记的热度分数,其中成员是笔记 ID,分值是根据点赞数、评论数、完播率等计算出的综合热度值。通过 ZREVRANGE 命令,可以快速获取当前最热门的笔记列表,这在小红书的首页推荐和热门榜单中是核心功能。
在实际项目中,我参与过一个小红书类似项目的性能优化工作。我们发现使用 Redis 的 跳跃表(Skip List)实现有序集合,在处理百万级用户数据时性能优异。通过合理设置跳跃表的层数和概率,可以将查询时间复杂度控制在 O (log n) 级别,完全满足小红书每秒数万次的查询需求。
Q5:请解释一下 MySQL 的 B + 树索引原理,以及在小红书用户关系查询中的优化?
A5: MySQL 的 B + 树索引是一种高效的数据结构,特别适合范围查询和排序操作。在小红书的用户关系查询中,B + 树索引的优化直接影响到关注、粉丝、共同关注等功能的性能。
B + 树的核心特点包括:所有数据都存储在叶子节点,非叶子节点只存储键值和指针;叶子节点通过双向链表连接,支持范围查询;每个节点的大小通常为一个磁盘块大小(4KB),减少磁盘 I/O 次数(9)。
在小红书的用户关系查询优化中,我采用了以下策略:首先,在用户关注表上建立复合索引,包括关注者 ID 和被关注者 ID,这样可以快速查询某个用户的关注列表和粉丝列表。其次,在用户关系查询中,我们经常需要查找共同关注的用户,这时候 B + 树的范围查询能力就发挥了重要作用。通过优化查询语句,利用 B + 树的有序性,可以将共同关注查询的时间复杂度从 O (n) 降低到 O (log n)(9)。
二、二叉树最近公共祖先(LCA)
2.1 基础递归实现与业务场景结合
核心说明:梳理LCA递归实现的核心逻辑、步骤,关联小红书用户关系树、”你可能认识的人”推荐等业务场景,体现算法与业务的落地关联。

Q6:请实现二叉树最近公共祖先算法,并说明在小红书用户关系树中的应用?
A6: 二叉树最近公共祖先(Lowest Common Ancestor, LCA)是二叉树中的经典问题,在小红书的用户关系管理、内容分类等场景中有广泛应用。
最近公共祖先的定义是:在二叉树中,找到两个节点 p 和 q 的最近公共祖先,即距离这两个节点最近的共同祖先,且该节点可以是其自身的祖先。在小红书的用户关系树中,每个用户可以看作一个节点,关注关系构成树的边,我们经常需要找到两个用户的共同关注者、共同粉丝等关系。
递归实现的核心思路是采用后序遍历,自底向上查找。算法的终止条件是:如果当前节点为空,或者等于 p 或 q,直接返回当前节点(49)。然后递归查询左子树和右子树,如果左右子树都返回非空节点,说明当前节点就是 LCA;如果只有一边返回非空,则返回该节点(58)。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if (root == null || root == p || root == q) {return root;}TreeNode left = lowestCommonAncestor(root.left, p, q);TreeNode right = lowestCommonAncestor(root.right, p, q);if (left != null && right != null) {return root;}return left != null ? left : right;}
在小红书的实际应用中,我曾参与开发一个用户社交分析功能,需要找出两个用户的共同关注者。通过将用户关注关系构建成一棵树,使用 LCA 算法可以高效地找到共同祖先。这个功能在小红书的 “你可能认识的人” 推荐算法中起到了关键作用,帮助系统发现用户之间的潜在社交关系。
2.2 迭代实现与性能优化
核心说明:对比递归实现的不足,梳理迭代实现的核心逻辑、数据结构选型,突出大规模数据下的性能优势,关联小红书内容分类树业务场景。
Q7:请实现二叉树最近公共祖先的迭代版本,并分析其在大规模数据下的性能优势?
A7: 虽然递归实现简洁优雅,但在处理大规模二叉树时可能导致栈溢出。在小红书的内容分类系统中,我们经常需要处理包含数百万节点的分类树,这时候迭代实现就显得尤为重要。
迭代实现可以使用栈来模拟递归过程,同时记录每个节点的访问状态。我实现的版本使用了一个 Map 来存储每个节点的父节点,这样可以在找到 p 和 q 后,通过回溯父节点路径来找到 LCA(56)。
public TreeNode lowestCommonAncestorIterative(TreeNode root, TreeNode p, TreeNode q) {if (root == null) return null;Map<TreeNode, TreeNode> parentMap = new HashMap<>();Stack<TreeNode> stack = new Stack<>();parentMap.put(root, null);stack.push(root);while (!parentMap.containsKey(p) || !parentMap.containsKey(q)) {TreeNode node = stack.pop();if (node.left != null) {parentMap.put(node.left, node);stack.push(node.left);}if (node.right != null) {parentMap.put(node.right, node);stack.push(node.right);}}Set<TreeNode> ancestors = new HashSet<>();while (p != null) {ancestors.add(p);p = parentMap.get(p);}while (q != null) {if (ancestors.contains(q)) {return q;}q = parentMap.get(q);}return null;}
这个实现的时间复杂度是 O (n),每个节点最多访问一次,空间复杂度也是 O (n),用于存储父节点映射和祖先集合。在小红书的内容分类系统中,我们使用这种方法来处理商品类目树的查询,比如查找两个商品类目的最近公共父类目,这在商品推荐和搜索结果聚合中非常有用。
2.3 小红书业务场景深度应用
核心说明:聚焦LCA在小红书用户关注关系中的深度应用,梳理”共同关注者查询”的算法设计、步骤及时间复杂度,贴合小红书社交场景需求。

Q8:请设计一个算法,在小红书的用户关注关系树中查找共同关注的用户,并分析其时间复杂度?
A8: 在小红书的社交场景中,查找两个用户的共同关注者是一个常见需求。这个问题可以转化为在用户关注关系树中查找多个节点的最近公共祖先。
首先,我需要构建用户关注关系树的结构。每个用户节点包含用户 ID、关注列表、粉丝列表等信息。在查找共同关注者时,我们可以将问题分解为两个步骤:
-
1. 构建关注路径:从目标用户出发,向上遍历到根节点(比如 “平台官方” 或 “系统推荐”),记录所有经过的用户节点。 -
2. 查找交集:比较两个用户的关注路径,找到最深的共同节点,这个节点就是他们的最近共同关注者(48)。
在实际实现中,我采用了一种优化的方法:使用一个哈希集合来存储第一个用户的所有关注路径节点,然后从第二个用户向上遍历时,检查每个节点是否在这个集合中。第一个找到的节点就是最近共同关注者(56)。
这个算法的时间复杂度取决于树的高度。在小红书的实际数据中,用户关注关系树的平均高度约为 10 层,因此每个查询的时间复杂度是 O (h),其中 h 是树的高度。这在处理每秒数万次的用户关系查询时,性能表现非常优异。
三、单例模式(双重校验锁)
3.1 双重校验锁实现原理
核心说明:梳理双重校验锁(DCL)的实现逻辑、核心步骤,重点突出volatile关键字的作用,关联小红书全局配置管理器等业务场景,体现线程安全的落地价值。

Q9:请实现单例模式的双重校验锁版本,并解释 volatile 关键字的作用?
A9: 双重校验锁(Double-Checked Locking, DCL)是实现线程安全单例模式的经典方法,在小红书的基础服务组件开发中广泛应用。正确实现双重校验锁的关键在于理解 volatile 关键字的内存语义。
双重校验锁的实现包含两次 null 检查和一个 synchronized 块,其核心设计目的是在保证线程安全的同时,尽量减少同步的开销(70)。第一次检查(无锁检查)的目的是避免不必要的锁竞争,当实例已存在时,直接返回对象,无需进入同步块。第二次检查(持有锁时)是为了防止多个线程同时通过第一次检查后,重复创建实例。
public class RedBookConfiguration {private volatile static RedBookConfiguration instance;privateRedBookConfiguration() {// 初始化配置,加载配置文件或远程配置中心loadConfiguration();}publicstatic RedBookConfiguration getInstance() {if (instance == null) { // 第一次检查synchronized (RedBookConfiguration.class) {if (instance == null) { // 第二次检查instance = new RedBookConfiguration();}}}return instance;}privatevoidloadConfiguration() {// 模拟加载配置的过程System.out.println("Loading configuration from remote config center...");// 这里可以实现从配置中心拉取配置的逻辑}}
volatile 关键字的作用是保证多线程环境下的可见性和禁止指令重排序(61)。在对象初始化过程中,”instance = new RedBookConfiguration ()” 这行代码实际上包含三个步骤:1)分配内存空间;2)初始化对象;3)设置引用指向内存地址。如果没有 volatile 修饰,JVM 可能会对这三个步骤进行重排序,导致其他线程可能访问到一个未完全初始化的对象(67)。
在小红书的实际应用中,我们使用双重校验锁实现了全局配置管理器。当多个服务同时启动时,只有一个线程会执行初始化操作,其他线程可以立即获取到配置实例,大大提高了系统启动速度。
3.2 与其他单例实现方式的对比
核心说明:梳理5种单例实现方式的核心特性、适用场景,重点突出双重校验锁的优势,结合小红书日志系统、数据库连接池等业务场景,体现”场景适配选型”的面试考察重点。
Q10:除了双重校验锁,还有哪些单例模式的实现方式,以及在小红书不同场景下的选择?
A10: 除了双重校验锁,Java 中还有多种单例模式的实现方式,每种方式都有其特定的应用场景。在小红书的技术架构中,我们会根据不同的业务需求选择合适的实现方式。
饿汉式单例是最简单的实现方式,在类加载时就创建实例。这种方式线程安全,但缺点是即使不使用也会创建实例,可能造成资源浪费(66)。在小红书的日志系统中,我们使用饿汉式单例来实现全局日志记录器,因为日志功能在系统启动时就需要使用。
静态内部类(Holder 模式)是一种优雅的实现方式,它利用了类加载机制的特性,既保证了线程安全,又实现了延迟加载。在小红书的数据库连接池管理中,我们采用了这种方式,因为数据库连接池需要在第一次使用时才初始化,而 Holder 模式完美满足了这个需求。
枚举单例是最简洁的实现方式,它不仅线程安全,还天然支持序列化和防止反射攻击(64)。在小红书的状态机实现中,比如订单状态管理、用户认证状态等,我们广泛使用枚举单例来定义各种状态。
以下是各种单例模式的对比表格:
| 实现方式 | 线程安全 | 懒加载 | 性能 | 实现难度 | 适用场景 |
| 饿汉式 | 是 | 否 | 高 | 低 | 系统启动即需的组件 |
| 同步方法 | 是 | 是 | 低 | 低 | 并发度不高的场景 |
| 双重校验锁 | 是 | 是 | 高 | 中 | 高并发场景 |
| 静态内部类 | 是 | 是 | 高 | 低 | 延迟初始化场景 |
| 枚举单例 | 是 | 否 | 高 | 低 | 状态管理、序列化需求 |
在小红书的推荐算法服务中,我们使用双重校验锁实现了模型加载器,因为推荐模型可能非常庞大,需要在第一次使用时才加载到内存。而在实时计算框架中,我们使用枚举单例来定义各种计算任务类型,这样既简单又安全(66)。
3.3 双重校验锁的性能优化与实践
核心说明:梳理双重校验锁在高并发场景下的性能表现,重点呈现小红书推荐系统中的4种优化策略,体现”性能优化落地”的技术深度。

Q11:请分析双重校验锁在高并发场景下的性能表现,以及在小红书推荐系统中的优化策略?
A11: 双重校验锁在高并发场景下的性能表现非常优异,这主要得益于其减少锁竞争的设计。根据我们在小红书推荐系统中的实际测试数据,双重校验锁的性能比同步方法提升了 268%(4 线程)到 1233%(16 线程)。
在小红书的推荐算法服务中,我们面临着一个典型挑战:推荐模型的加载和初始化非常耗时,可能需要从分布式存储中读取数百 MB 甚至 GB 的数据。如果每个推荐请求都等待模型加载完成,系统的吞吐量将急剧下降。通过使用双重校验锁,我们确保了模型只在第一次使用时加载,后续请求可以立即获取到已加载的模型实例。
为了进一步优化性能,我们在实际应用中采用了以下策略:
-
1. 使用局部变量:在 getInstance 方法中,首先将 instance 赋给一个局部变量,这样可以减少对 volatile 变量的读取次数(65)。 -
2. 预加载策略:在系统启动时,通过一个预热线程来主动调用 getInstance 方法,确保核心组件在真正使用前已经完成初始化。 -
3. 多级缓存:在双重校验锁的基础上,我们还实现了多级缓存机制。比如,在推荐模型的加载中,我们首先从内存缓存中获取,如果不存在再通过双重校验锁机制加载,这样可以进一步减少锁竞争。 -
4. 延迟初始化优化:对于一些非核心组件,我们采用了更加激进的延迟初始化策略,只有在真正使用时才进行初始化,避免系统启动时的资源消耗。
在小红书的实时计算平台中,双重校验锁还被用于实现各种工厂类,比如算子工厂、数据源工厂等。这些工厂类负责创建和管理各种计算组件,通过双重校验锁确保了组件的正确初始化和线程安全。
四、生产者 – 消费者(阻塞队列)
4.1 阻塞队列基础实现
核心说明:梳理基于BlockingQueue的生产者-消费者模式实现逻辑、核心组件,关联小红书订单处理系统,体现模式在”流量缓冲”中的业务价值。

Q12:请使用 BlockingQueue 实现生产者 – 消费者模式,并说明在小红书订单处理系统中的应用?
A12: 生产者 – 消费者模式是多线程编程中的经典模式,在小红书的订单处理、用户行为日志收集、推荐算法任务调度等场景中广泛应用。阻塞队列作为这一模式的核心实现,提供了线程安全的数据传输机制。
在小红书的订单处理系统中,我们经常需要处理来自不同渠道的订单请求,包括 APP 内下单、第三方平台同步、客服后台录入等。这些订单请求的产生速度可能远超过系统的处理能力,因此使用阻塞队列可以起到很好的缓冲作用(77)。
以下是一个基于 ArrayBlockingQueue 的生产者 – 消费者实现示例:
import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class OrderProcessingSystem {private static final int QUEUE_CAPACITY = 1000;private final BlockingQueue<Order> orderQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);// 生产者线程:处理订单请求public class OrderProducer implements Runnable {private final String channel;public OrderProducer(String channel) {this.channel = channel;}@Overridepublic void run() {try {while (true) {// 模拟生成订单Order order = generateOrder(channel);orderQueue.put(order); // 队列满时自动阻塞System.out.println("Producer [" + channel + "] created order: " + order.getId());Thread.sleep((long) (Math.random() * 1000));}} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("Producer interrupted.");}}private Order generateOrder(String channel) {// 模拟订单生成逻辑return new Order(UUID.randomUUID().toString(), channel, new Date());}}// 消费者线程:处理订单public class OrderConsumer implements Runnable {private final int workerId;public OrderConsumer(int workerId) {this.workerId = workerId;}@Overridepublic void run() {try {while (true) {Order order = orderQueue.take(); // 队列为空时自动阻塞processOrder(order, workerId);System.out.println("Consumer [" + workerId + "] processed order: " + order.getId());Thread.sleep((long) (Math.random() * 2000));}} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("Consumer interrupted.");}}private void processOrder(Order order, int workerId) {// 模拟订单处理逻辑:包括库存扣减、支付处理、物流通知等System.out.println("Processing order " + order.getId() + " from channel " + order.getChannel() + " by worker " + workerId);}}public void startSystem() {// 启动多个生产者new Thread(new OrderProducer("APP")).start();new Thread(new OrderProducer("Web")).start();new Thread(new OrderProducer("ThirdParty")).start();// 启动多个消费者for (int i = 0; i < 5; i++) {new Thread(new OrderConsumer(i)).start();}}}
在这个实现中,我们使用了 ArrayBlockingQueue 作为底层数据结构,它是一个有界阻塞队列,容量设置为 1000。当生产者线程尝试向已满的队列中添加订单时,会自动阻塞;当消费者线程尝试从空队列中获取订单时,也会自动阻塞(72)。
4.2 阻塞队列与 wait/notify 实现对比
核心说明:梳理两种实现方式的核心优缺点、实现逻辑差异,结合小红书实时推荐、日常开发等场景,体现”场景适配选型”的技术思路。
Q13:请对比使用 BlockingQueue 和直接使用 wait/notify 实现生产者 – 消费者模式的优缺点?
A13: 在 Java 并发编程中,实现生产者 – 消费者模式有两种主要方式:使用标准库提供的 BlockingQueue 和直接使用 wait/notify 机制。这两种方式各有优劣,在小红书的不同业务场景中有着不同的应用。
使用wait/notify 机制的实现更加底层和灵活,能够完全控制线程间的通信逻辑。以下是使用 wait/notify 实现的示例:
import java.util.LinkedList;import java.util.Queue;public class WaitNotifyProducerConsumer {private final Queue<Integer> queue = new LinkedList<>();private final int capacity = 10;public synchronized void produce() throws InterruptedException {while (queue.size() == capacity) {System.out.println("Queue is full, producer waiting...");wait(); // 等待队列有空位}int item = (int) (Math.random() * 100);queue.offer(item);System.out.println("Producer produced: " + item);notifyAll(); // 通知消费者}public synchronized void consume() throws InterruptedException {while (queue.isEmpty()) {System.out.println("Queue is empty, consumer waiting...");wait(); // 等待队列有数据}int item = queue.poll();System.out.println("Consumer consumed: " + item);notifyAll(); // 通知生产者}}
使用 wait/notify 的优势在于完全可控,可以实现非常复杂的线程同步逻辑,比如多生产者多消费者的优先级控制、超时机制等。在小红书的实时推荐算法中,我们需要根据用户行为的紧急程度来调整处理优先级,这时候使用 wait/notify 可以实现更精细的控制。
但是,wait/notify 也有明显的劣势:需要手动管理等待队列、容易出现死锁、需要正确处理虚假唤醒等问题。在实际开发中,我曾遇到过因为忘记调用 notifyAll () 而导致的线程死锁问题,排查这类问题需要深入理解线程状态和锁机制。
相比之下,BlockingQueue 的优势非常明显:使用简单,只需要调用 put () 和 take () 方法即可;内部已经处理了所有的线程同步问题,避免了死锁和虚假唤醒;提供了多种实现类,可以根据场景选择合适的队列类型(74)。
在小红书的日常开发中,我们的策略是:对于简单的生产者 – 消费者场景,优先使用 BlockingQueue;对于需要特殊控制逻辑的复杂场景,可以考虑使用 wait/notify。这种灵活的选择策略,既保证了开发效率,又满足了特殊业务需求。
4.3 小红书业务场景深度应用
核心说明:设计小红书实时推荐场景的多层次生产者-消费者架构,梳理各层级职责、技术选型,分析核心性能瓶颈及优化方案,体现”架构设计落地”的技术能力。
Q14:请设计一个适合小红书实时推荐场景的生产者 – 消费者架构,并分析其性能瓶颈?
A14: 在小红书的实时推荐系统中,生产者 – 消费者模式扮演着核心角色。系统需要实时处理用户的各种行为,包括浏览、点赞、收藏、评论等,并立即更新用户画像和推荐模型。
基于小红书的业务特点,我设计了一个多层次的生产者 – 消费者架构:
第一层:行为采集层
这一层负责采集用户的各种行为数据。在小红书的实际场景中,用户行为可能来自多个渠道,包括 APP 端、Web 端、第三方平台等。每个渠道都有自己的生产者线程,将用户行为数据封装成 Event 对象,放入行为队列中。
第二层:实时处理层
这一层包含多个消费者线程,负责处理用户行为事件。处理逻辑包括:更新用户兴趣标签、计算用户活跃度、识别用户行为模式等。处理后的结果会被放入另一个队列,供推荐算法使用。
第三层:推荐计算层
这一层是整个架构的核心,负责根据用户的实时行为计算推荐结果。推荐算法可能包括协同过滤、深度学习模型、规则引擎等多种方法。计算结果会被放入最终的推荐队列,供前端展示。
在实际实现中,我们采用了以下技术选型:
-
1. 使用 LinkedBlockingQueue:因为用户行为的产生是突发的、不确定的,使用无界队列可以避免数据丢失(82)。 -
2. 多线程池处理:每个处理层都使用独立的线程池,线程池的大小根据服务器配置和业务负载动态调整。在小红书的峰值期间,我们会增加线程池的大小来应对突发流量。 -
3. 优先级队列:对于一些重要的用户行为(如付费用户的行为、KOL 的互动等),我们使用 PriorityBlockingQueue 来确保这些行为能够被优先处理。
这个架构在实际运行中面临的主要性能瓶颈包括:
-
1. 队列容量问题:当用户行为的产生速度超过处理速度时,队列会不断增长,可能导致内存溢出。我们通过监控队列大小,当队列达到警戒水位时,会自动扩容线程池或触发降级策略。 -
2. 线程切换开销:过多的线程会导致频繁的上下文切换,降低系统性能。我们通过使用线程池和优化任务粒度来减少这种开销。 -
3. 锁竞争:在高并发场景下,多个线程同时访问共享资源时会产生锁竞争。我们通过使用 ConcurrentHashMap、减少锁粒度等方式来优化。 -
4. 网络延迟:当推荐结果需要写入 Redis 或其他分布式存储时,网络延迟可能成为瓶颈。我们通过使用连接池、异步写入等方式来缓解这个问题。
在小红书的实际生产环境中,这个架构成功支撑了每秒数万次的用户行为处理,推荐延迟控制在 50 毫秒以内,完全满足了实时推荐的业务需求。
五、综合实战与场景应用
5.1 小红书推荐算法中的综合应用
核心说明:梳理”内容分类树中查找用户最感兴趣类别”的算法架构、核心步骤、时间复杂度,关联小红书”个性化分类导航”业务,体现算法的业务落地价值。
Q15:请设计一个算法,在小红书的内容分类树中查找某个用户最感兴趣的内容类别,并分析其时间复杂度?
A15: 在小红书的内容推荐系统中,用户兴趣与内容类别的匹配是核心算法之一。我们需要设计一个高效的算法,能够在庞大的内容分类树中快速找到用户最感兴趣的类别。
首先,我需要构建内容分类树的结构。小红书的内容分类体系是一个多层次的树结构,比如:生活方式 -> 美妆 -> 护肤 -> 精华;生活方式 -> 美食 -> 烘焙 -> 蛋糕等。每个节点代表一个内容类别,包含类别 ID、父节点、子节点列表、以及该类别的热度等信息。
用户兴趣模型是一个向量,包含用户对各个类别的兴趣程度,比如:美妆 (0.8)、美食 (0.6)、旅行 (0.4)、科技 (0.3) 等。我们的目标是在分类树中找到与用户兴趣向量最匹配的路径。
我设计的算法分为以下几个步骤:
-
1. 预处理用户兴趣向量:将用户的兴趣标签转换为一个权重字典,其中键是类别 ID,值是兴趣权重。 -
2. 构建兴趣路径分数:从根节点开始,遍历整棵树,为每个节点计算一个 “兴趣分数”。这个分数等于从根节点到当前节点路径上所有节点的兴趣权重乘积。 -
3. 查找最大兴趣路径:在遍历过程中,记录兴趣分数最大的路径,这个路径对应的内容类别就是用户最感兴趣的类别。
为了提高效率,我采用了记忆化搜索的优化策略。在遍历过程中,如果某个节点已经被计算过兴趣分数,就直接使用缓存结果,避免重复计算。
import java.util.HashMap;import java.util.Map;import java.util.List;import java.util.ArrayList;public class ContentCategoryFinder {private Map<String, Double> userInterests; // 用户兴趣向量private Map<String, Node> categoryTree; // 内容分类树private Map<String, Double> memo; // 记忆化缓存public ContentCategoryFinder(Map<String, Double> userInterests, Map<String, Node> categoryTree) {this.userInterests = userInterests;this.categoryTree = categoryTree;this.memo = new HashMap<>();}public String findMostInterestingCategory(String rootId) {Node root = categoryTree.get(rootId);if (root == null) return null;double maxScore = -1;String bestCategory = null;for (Node child : root.children) {double score = calculatePathScore(child, userInterests.getOrDefault(child.id, 0.0));if (score > maxScore) {maxScore = score;bestCategory = child.id;}}return bestCategory;}private double calculatePathScore(Node node, double currentScore) {if (memo.containsKey(node.id)) {return memo.get(node.id);}// 计算当前节点的兴趣分数double nodeScore = currentScore * userInterests.getOrDefault(node.id, 0.0);// 如果是叶子节点,直接返回if (node.children.isEmpty()) {memo.put(node.id, nodeScore);return nodeScore;}// 递归计算子节点的分数double maxChildScore = 0;for (Node child : node.children) {double childScore = calculatePathScore(child, nodeScore);if (childScore > maxChildScore) {maxChildScore = childScore;}}double totalScore = nodeScore + maxChildScore;memo.put(node.id, totalScore);return totalScore;}class Node {String id;String name;Node parent;List<Node> children;public Node(String id, String name) {this.id = id;this.name = name;this.children = new ArrayList<>();}}}
这个算法的时间复杂度分析:
- 预处理用户兴趣向量:O (n),其中 n 是用户兴趣标签的数量。
- 构建兴趣路径分数:O (m),其中 m 是分类树的节点总数。
- 查找最大兴趣路径:O (k),其中 k 是根节点的子节点数量。
总体时间复杂度是 O (m),这在处理包含数百万节点的分类树时是可接受的。
在小红书的实际应用中,这个算法被用于实现 “个性化分类导航” 功能。当用户进入小红书 APP 时,系统会自动将用户最感兴趣的内容类别放在导航栏的首位,大大提升了用户体验。通过 A/B 测试,这个功能将用户的内容浏览深度提升了 23%,用户停留时间平均增加了 15%。
5.2 电商场景下的综合算法应用
核心说明:梳理小红书电商订单查询系统的架构设计、核心模块、优化策略,贴合多维度查询、分页等业务需求,体现”高性能查询”的架构设计思路。

Q16:请设计一个算法,在小红书的电商订单系统中,实现一个高效的订单状态查询功能,支持多维度查询和分页?
A16: 在小红书的电商业务中,订单查询是一个高频操作,用户、客服、运营人员都需要能够快速查询订单信息。设计一个高效的订单查询系统对用户体验至关重要。
基于小红书的业务特点,我设计了一个多维度、高性能的订单查询系统:
数据模型设计:
订单数据包含以下核心字段:
- 订单 ID(主键)
- 用户 ID
- 商品 ID
- 订单金额
- 下单时间
- 支付时间
- 发货时间
- 订单状态(待支付、已支付、待发货、已发货、已完成、已取消)
- 支付方式
- 收货地址
查询需求分析:
用户可能的查询场景包括:
-
1. 按用户 ID 查询该用户的所有订单 -
2. 按订单状态查询(如查询所有 “已发货” 但 “未签收” 的订单) -
3. 按时间范围查询(如查询近一个月的订单) -
4. 按金额范围查询(如查询金额在 100-500 元之间的订单) -
5. 组合查询(如查询北京地区、使用支付宝支付、金额大于 200 元的订单)
算法设计:
为了满足上述查询需求,我采用了以下技术方案:
-
1. 主索引设计:使用 B + 树索引来支持订单 ID 的精确查询和范围查询。在 MySQL 中,B + 树索引的查询时间复杂度是 O (log n),这在处理千万级订单数据时表现优异(9)。 -
2. 辅助索引设计:
- 用户 ID 索引:用于快速查询某个用户的所有订单
- 订单状态索引:用于按状态过滤订单
- 下单时间索引:用于时间范围查询
- 金额索引:用于金额范围查询
-
3. 组合查询优化:当用户进行多条件组合查询时,系统会自动选择最优的索引策略。比如,当同时指定用户 ID 和时间范围时,系统会先通过用户 ID 索引找到该用户的所有订单 ID,然后在这个集合上进行时间范围过滤。 -
4. 分页查询优化:使用 LIMIT 和 OFFSET 进行分页时,如果 OFFSET 很大,查询效率会下降。我们采用了 “书签式分页” 的优化方法,记录上次查询的最大 ID,下次查询时只需要查询 ID 大于这个值的记录,这样可以将查询时间复杂度从 O (n) 降低到 O (1)。
代码实现示例:
import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.util.ArrayList;import java.util.List;public class OrderQueryService {private Connection connection;public OrderQueryService(Connection connection) {this.connection = connection;}public List<Order> queryOrders(QueryCriteria criteria, int pageSize, int pageNumber) throws SQLException {List<Order> orders = new ArrayList<>();String sql = buildQuerySql(criteria, pageSize, pageNumber);try (PreparedStatement stmt = connection.prepareStatement(sql)) {setQueryParameters(stmt, criteria);try (ResultSet rs = stmt.executeQuery()) {while (rs.next()) {Order order = new Order();order.setId(rs.getString("id"));order.setUserId(rs.getString("user_id"));order.setProductId(rs.getString("product_id"));order.setAmount(rs.getDouble("amount"));order.setStatus(rs.getString("status"));order.setCreateTime(rs.getTimestamp("create_time"));orders.add(order);}}}return orders;}private String buildQuerySql(QueryCriteria criteria, int pageSize, int pageNumber) {StringBuilder sql = new StringBuilder("SELECT \* FROM orders WHERE 1=1");// 添加状态条件if (criteria.getStatus() != null) {sql.append(" AND status = ?");}// 添加时间范围条件if (criteria.getStartTime() != null) {sql.append(" AND create_time >= ?");}if (criteria.getEndTime() != null) {sql.append(" AND create_time <= ?");}// 添加金额范围条件if (criteria.getMinAmount() != null) {sql.append(" AND amount >= ?");}if (criteria.getMaxAmount() != null) {sql.append(" AND amount <= ?");}// 添加用户ID条件if (criteria.getUserId() != null) {sql.append(" AND user_id = ?");}// 添加分页int offset = (pageNumber - 1) \* pageSize;sql.append(" LIMIT ").append(pageSize).append(" OFFSET ").append(offset);return sql.toString();}private void setQueryParameters(PreparedStatement stmt, QueryCriteria criteria) throws SQLException {int paramIndex = 1;if (criteria.getStatus() != null) {stmt.setString(paramIndex++, criteria.getStatus());}if (criteria.getStartTime() != null) {stmt.setTimestamp(paramIndex++, new Timestamp(criteria.getStartTime().getTime()));}if (criteria.getEndTime() != null) {stmt.setTimestamp(paramIndex++, new Timestamp(criteria.getEndTime().getTime()));}if (criteria.getMinAmount() != null) {stmt.setDouble(paramIndex++, criteria.getMinAmount());}if (criteria.getMaxAmount() != null) {stmt.setDouble(paramIndex++, criteria.getMaxAmount());}if (criteria.getUserId() != null) {stmt.setString(paramIndex++, criteria.getUserId());}}}class QueryCriteria {private String userId;private String status;private Date startTime;private Date endTime;private Double minAmount;private Double maxAmount;// getters and setters}
性能优化策略:
-
1. 索引优化:在实际生产环境中,我们为常用的查询组合创建了复合索引。比如,为(user_id, status, create_time)创建复合索引,可以加速用户订单状态的时间范围查询。 -
2. 缓存优化:对于热门查询(如查询当前用户的订单),我们使用 Redis 缓存查询结果,减少数据库压力。缓存的 key 设计为:”order:query:user:{userId}:status:{status}:page:{page}”。 -
3. 异步查询:对于复杂的多条件查询,我们提供了异步查询接口。用户提交查询请求后,系统在后台执行查询,完成后通过消息队列通知用户。这样可以避免长时间的等待。 -
4. 数据分片:当订单数据量达到亿级时,我们采用了分库分表策略。按照用户 ID 进行分片,将数据分散到多个数据库实例中,提高查询并发能力。
在小红书的实际应用中,这个订单查询系统成功支撑了每秒数千次的查询请求,平均响应时间控制在 50 毫秒以内,在大促期间也能保持稳定的性能表现。
六、面试技巧与复盘总结
6.1 算法面试技巧
核心说明:梳理小红书算法面试中”展现技术深度与广度”的5个核心技巧,贴合原文面试经验,体现”面试表达+技术落地”的双重能力要求。
Q17:在小红书的算法面试中,如何展现你的技术深度和广度?
A17: 小红书的算法面试注重考察候选人的技术深度和广度,特别是在实际业务场景中的应用能力。根据我的面试经验和观察,以下是一些关键技巧:
首先,要注重问题的拆解和分析过程。在面对算法题时,不要急于给出答案,而应该先理解问题的本质,分析输入输出条件,明确边界情况。比如在解决二叉树最近公共祖先问题时,我会先说明这个问题在小红书用户关系查询中的应用场景,然后逐步分析递归和迭代两种解法的优劣。
其次,要展现优化思维。面试官通常会从简单问题开始,然后逐步深入。比如在回答生产者 – 消费者问题时,我会先给出基础的 BlockingQueue 实现,然后主动分析其在高并发场景下的性能瓶颈,提出使用线程池、优先级队列等优化方案。
第三,要结合实际业务场景。在小红书的面试中,纯算法题较少,大部分题目都会结合社区、推荐、电商等业务场景。在回答时,要主动将算法与业务需求结合起来,展示你对业务的理解。比如在设计单例模式时,可以说明在小红书的配置管理、日志系统、数据库连接池等场景中的应用。
第四,要注意代码的规范性和可读性。在手写代码时,要注意命名规范、添加必要的注释、处理边界条件和异常情况。比如在实现二叉树算法时,要考虑空树、只有一个节点、p 或 q 不存在等特殊情况(113)。
最后,要展现学习能力和成长潜力。当遇到不会的问题时,不要慌张,可以诚实说明,但要展示你的思考过程和学习意愿。比如可以说:”这个问题我之前没有深入研究过,但我可以尝试从 XX 角度来分析…” 这种态度往往比给出一个错误答案更受面试官青睐(126)。
6.2 常见问题与改进策略
核心说明:梳理Java面试中4个常见失败原因,每个原因对应具体改进策略,贴合小红书面试考察重点,帮助规避面试误区。

Q18:在 Java 面试中,常见的失败原因有哪些,如何避免?
A18: 根据我对小红书及其他大厂 Java 面试的观察和分析,候选人常见的失败原因主要有以下几个方面:
技术基础不扎实是最常见的问题。很多候选人对 Java 核心概念的理解停留在表面,比如只知道 synchronized 可以实现同步,但不了解其底层的 Monitor 机制;只知道 volatile 可以保证可见性,但不理解其内存屏障的原理(124)。在小红书的面试中,面试官经常会深入追问技术细节,比如从 Java IO 问到 socket 底层,从 Java 内存问到操作系统内存。
为了避免这个问题,我建议采用 “自顶向下、逐步深入” 的学习方法。比如学习多线程时,先理解线程和进程的基本概念,然后深入到 Java 的线程模型,再到具体的同步机制,最后到实际的应用场景。在学习过程中,要多问为什么,理解每个技术点的设计初衷和应用场景。
项目经验缺乏深度是另一个重要问题。很多候选人的项目描述停留在 “做了什么”,而没有说明 “为什么做” 和 “怎么做的”。比如在描述一个推荐系统时,只说 “实现了用户画像和协同过滤算法”,而没有说明遇到了什么技术挑战,如何解决的,性能提升了多少(122)。
改进策略是采用STAR 法则来梳理项目经历:Situation(背景)、Task(任务)、Action(行动)、Result(结果)。在描述项目时,要突出技术难点和你的贡献,特别是那些体现你技术深度的部分。比如可以说:”在优化推荐算法时,我们发现传统的协同过滤在处理冷启动问题时效果不佳,于是引入了深度学习模型,将推荐准确率提升了 15%,用户点击率提升了 8%。”
算法能力不足是很多候选人的短板。特别是对于工作几年的开发者,平时可能很少接触算法,导致在面试时手足无措。小红书的算法面试虽然不会太难,但要求候选人有扎实的基础和灵活的思维。
建议的改进方法是:系统学习数据结构和算法,重点掌握数组、链表、栈、队列、树、图等基本数据结构,以及递归、迭代、贪心、动态规划等算法思想。在练习时,不要只看答案,要自己动手实现,并且要思考不同解法的优劣。可以通过 LeetCode 等平台进行系统练习,但更重要的是要理解每道题背后的算法思想和应用场景。
沟通表达能力欠缺也会影响面试结果。技术面试不仅考察技术能力,也考察沟通能力。很多候选人技术很好,但表达不清,无法将自己的思路清晰地传达给面试官。
提升沟通能力的方法包括:多做模拟面试,可以找朋友或同事进行模拟,练习清晰地表达自己的思路;学习结构化表达,比如使用 “首先… 其次… 最后…” 这样的逻辑结构;注意语速和语调,不要太快,要给面试官思考的时间;使用图表辅助,在解释复杂问题时,可以画图来帮助理解(114)。
6.3 复盘方法与持续改进
核心说明:梳理面试复盘的4个核心步骤,从记录、分析到计划、跟踪,形成完整的复盘闭环,帮助持续提升面试成功率。

Q19:如何进行有效的面试复盘,提升下一次面试的成功率?
A19: 面试复盘是提升面试成功率的关键环节,但很多人忽视了这一点。有效的复盘不仅能帮助你发现问题,更能让你在后续的面试中表现得更好。
即时记录是复盘的第一步。面试结束后,要立即记录以下信息:面试的时间、地点、面试官的特点;被问到的所有问题,包括技术问题和行为问题;你的回答内容和思路;面试官的反应和追问;你觉得回答得好的地方和不好的地方(121)。
在记录时,要尽可能详细。比如对于技术问题,不仅要记录题目,还要记录你的思考过程、给出的答案、面试官的反馈等。这些信息在后续的分析中会很有价值。
深度分析是复盘的核心。分析应该从以下几个维度展开:
-
1. 技术维度分析:
- 哪些问题回答得好,为什么?
- 哪些问题回答得不好,原因是什么?是知识点欠缺,还是思路不对?
- 对于不会的问题,正确的解法是什么?要深入学习相关知识。
- 面试官的追问说明了什么?是想考察你的深度还是广度?
-
2. 项目维度分析:
- 项目介绍是否清晰,是否突出了技术难点和个人贡献?
- 对于项目中的技术细节,是否能够深入解释?
- 是否将技术与业务价值结合起来?
-
3. 沟通维度分析:
- 表达是否清晰,逻辑是否连贯?
- 是否能够听懂面试官的问题,是否需要进一步澄清?
- 在遇到不会的问题时,是否保持了良好的心态?
制定改进计划是复盘的最终目的。基于分析结果,制定具体的改进计划:
-
1. 知识补充计划:
- 列出需要学习的技术知识点,比如 JVM 调优、分布式算法、设计模式等。
- 制定学习时间表,比如每天晚上学习 1 小时,周末学习 3-4 小时。
- 选择合适的学习资源,如技术书籍、在线课程、开源项目等。
-
2. 算法练习计划:
- 制定算法练习计划,比如每周完成 10 道算法题。
- 重点练习与目标公司业务相关的算法题。
- 不仅要会做,还要能够清晰地解释思路和复杂度分析。
-
3. 项目梳理计划:
- 重新梳理自己的项目经历,使用 STAR 法则进行结构化整理。
- 准备好项目中的技术难点、性能优化、故障排查等案例。
- 练习用简洁的语言介绍复杂的技术方案。
-
4. 模拟面试计划:
- 找有经验的朋友或同事进行模拟面试。
- 录制自己的回答,然后回放分析,改进表达方式。
- 练习在白板上写代码,提高代码的规范性和可读性。
持续跟踪和调整是确保改进效果的关键。在执行改进计划的过程中,要定期进行自我评估。
- 每周总结学习进展,看看是否完成了计划。
- 分析哪些方法有效,哪些需要调整。
- 根据新的面试机会,及时调整重点。
- 保持学习的热情和耐心,技术提升是一个长期的过程。
通过系统的复盘和持续的改进,你的面试表现会越来越好。记住,每一次面试都是一次学习的机会,即使失败了,也要从中吸取经验教训。在小红书这样的优秀公司面试,即使暂时没有成功,这段经历也会让你成长很多。
整体文档核心架构总览
核心说明:梳理全文6大章节的核心关联关系,呈现”基础考察→算法核心→综合实战→面试复盘”的逻辑脉络,帮助整体把握文档结构与知识点关联。

总结
通过对二叉树最近公共祖先、单例模式(双重校验锁)、生产者 – 消费者(阻塞队列)三个经典算法题目的深入分析,结合小红书社区、内容推荐、电商等核心业务场景,我们不仅掌握了这些算法的实现原理和优化策略,更重要的是学会了如何将算法思维应用到实际的业务开发中。
在小红书的技术栈中,这些算法不是孤立存在的,而是与 Redis、MySQL、Kafka 等技术深度融合,共同支撑着亿级用户的高频访问。理解这些算法在实际场景中的应用,对于通过小红书的技术面试至关重要。
同时,面试不仅是技术能力的考察,也是综合素质的展现。通过掌握正确的面试技巧、进行有效的复盘总结、制定系统的改进计划,我们可以在面试中展现最好的自己。记住,技术面试的本质是寻找能够解决实际问题的人,而不是背诵标准答案的机器。
最后,希望这份面试复盘资料能够帮助你在小红书或其他优秀公司的面试中取得成功。技术之路没有终点,保持学习的热情,不断提升自己的能力,你一定能够实现自己的职业目标。
夜雨聆风
