拼多多服务端开发一面面经:并发幂等、Kafka 积压与向量库索引
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
项目与中间件
- 详细介绍实习项目里涉及到的并发和幂等是怎么实现的?
- 为什么要用并发和幂等?
- 消息的顺序性怎么保证?
- 中间件是内部开发的吗?
- Kafka 的原理是什么?
- 有 topic 发生消息积压怎么办?
- 怎么确定 partition 分区数?
知识问答助手与向量数据库
- 知识问答助手具体做了哪些内容?
- 「不暴露内部业务代码」是什么意思?为什么要加这个?
- 你的项目中 Chroma 的作用是什么?
- 它与 Redis 的使用场景怎么区分?
- 智能体运行调度出错了怎么办?
- 向量数据库怎么更新?
- 向量数据库的索引结构是什么?
- 你还了解其他索引结构吗?
MySQL
- MySQL 的主从同步机制是怎样的?
- 结合一个插入的例子再讲一遍。
- 怎么保证数据的一致性?
开放题
- 怎么没投算法岗?
- 实习的地方有 offer 吗?
- 了解 PDD 的氛围和工作模式吗?你怎么看待?
算法
- 数组中能组成三角形的三条边有多少组?
- 一道较长的动态规划题。
《参考解析》
并发与幂等:先答动机,再落实现
「为什么用」比「怎么用」更容易露怯,先想清楚动机:并发是为了把互相独立的 IO 等待叠起来(批量拉取、并行调用多个下游、异步写日志),把串行耗时压到最长的那一段;代价是资源竞争、顺序被打乱、错误扩散,所以必须有边界(并发度、超时、熔断)。幂等是为了让「重试」这件事安全——网络超时、上游重发、消息至少一次投递都会造成重复,没有幂等就会重复扣款、重复建单、重复发券。
实现上要能说出具体手段,并按场景分:数据库唯一约束(业务唯一键建唯一索引,重复插入直接失败,这是最可靠的一道闸);幂等表 / 去重表(请求 id 或业务键 + 状态,先插入占位再执行业务,靠主键冲突判重);状态机(订单只能从「待支付」到「已支付」,重复回调因为状态不匹配被忽略);分布式锁或 SETNX 占位(Redis 拿锁 + 过期时间,用完释放,注意锁续期与误删);乐观锁版本号(update ... where version = ?)。回答时最好把「客户端请求 id → 服务端幂等键 → 数据库唯一约束」这条链完整说出来,并交代幂等键的生成规则和保留时长。
消息顺序性怎么保证
Kafka 只保证同一 partition 内有序,跨 partition 不保证,所以顺序性的做法是让需要保序的消息进同一个 partition:发消息时用业务键(订单 id、用户 id)做 key,Kafka 按 key 哈希到固定分区;如果全局保序要求高,就把 topic 设成单分区(吞吐会受限)。
生产端还要注意:开启幂等生产者(enable.idempotence=true,配 acks=all 与重试)避免重试造成的重复与乱序,必要时用 max.in.flight.requests.per.connection=1 彻底串行化。消费端同样要保序——一个 partition 只由一个消费者消费,但消费者内部如果用了线程池并行处理就会破坏顺序,常见做法是按 key 分发到内存队列(同一 key 固定进同一个队列,各队列单线程消费)。最后补一句兜底:即使链路全程保序,业务侧仍要幂等,因为顺序错乱和重复都可能出现在异常路径上。
消息积压怎么处理
先定位再动手,积压无非三种成因:消费能力不足(消费逻辑里有慢调用、每条都同步写库)、突发流量(上游批量导入、大促)、消费者挂了或一直在 rebalance。
常用的处置手段按代价排序:临时扩容消费者实例(注意实例数超过 partition 数就没有收益)、提高单次拉取批量与并行度、优化消费逻辑(批量写、去掉同步远程调用、把非关键逻辑异步化)、把积压消息转存到临时 topic 用另一组消费者并行消费(先把积压清掉、保证线上延迟)、必要时新增 partition 提升并行度(注意新增分区会改变 key 的哈希落点,破坏既有顺序性)。事后要有监控基线:消费 lag、消费速率与生产速率的比值、告警阈值,别等用户投诉。
分区数怎么确定
先算目标吞吐:单分区能扛的写入/消费速率(受 broker 磁盘、网络、复制开销影响,通常按几 MB/s 量级估),用峰值吞吐除以单分区能力,再乘 1.5~2 倍留余量,同时保证分区数不小于期望的消费者并行度上限。还要考虑代价:分区越多,元数据与文件句柄越多,leader 选举和 rebalance 越慢,端到端延迟也可能上升;分区数只能增不能减。工程上通常按「业务峰值吞吐 + 未来一年增长」定一个偏保守的值,并按 key 分布检查有没有热点分区。
Chroma 与 Redis 的使用场景区分
Chroma 是向量数据库:存 embedding 与原文 metadata,提供相似度检索、metadata 过滤和持久化,服务的是「语义召回」——RAG 的知识库这一层。Redis 是内存 KV / 数据结构服务:更快的读写、支持过期、原子操作,适合做缓存(热数据、embedding 结果)、会话状态、分布式锁、限流计数和消息队列。
两者会同时出现在一个 RAG 系统里但职责不同:Redis 缓存的是「同一个问题答过的结果」「query 的向量」「热点文档」,命中就少一次向量检索和一次模型调用;Chroma 负责真正的召回。要补一句边界:Redis 也有向量检索能力(RediSearch),但定位是「已经有 Redis、数据量不大时的顺带方案」,持久化、索引类型和过滤能力都不如专业向量库;反过来 Chroma 不适合当缓存,它的定位是持久化的检索存储。
智能体调度出错了怎么办
分三层处理。单步失败:工具调用超时或报错时,把错误信息作为观测回灌给模型让它换路,而不是直接终止;读类操作可以带退避重试,写类操作必须带幂等键、重试前先查真实状态。流程失败:每步把状态(计划、已完成步骤、中间产物、预算)落检查点,进程挂掉能从最后一步续跑,而不是从头再来。整体失败:设置最大步数、token 预算与总超时,超限就降级(返回部分结果、切到固定流程模板、转人工),并把失败分类(模型侧/工具侧/环境侧)记录下来用于归因。核心原则是:可恢复的重试、不可恢复的降级、所有异常都要有终止条件,不能让调度器无限循环。
向量数据库的更新与索引结构
更新上要区分两种操作:内容没变的元数据修改用 metadata 更新即可;文档内容变更最好是「按文档 id 删除旧 chunk 再写入新 chunk」(upsert + 删除),因为切分变了以后 chunk 与旧向量无法一一对应。工程上再配一层版本管理:文档带版本号与来源,写入时做幂等,删除用软删除标记再后台重建索引,避免读请求读到删了一半的知识库。更新完要能验证——拿一批已知答案的问题回归,确认召回率没有下降。
索引结构常见的三类:HNSW(分层小世界图,逐层跳转逼近近邻,查询快、召回高,内存占用大,构建慢,是多数向量库的默认);IVF 系列(先用聚类把向量分桶,查询时只扫最近的若干个桶;配上 PQ 乘积量化把向量压成短码,就是 IVF-PQ,省内存但损失精度);DiskANN / Vamana(面向磁盘的图索引,单机放不下时用);数据量很小或要求精确结果时就是 Flat 暴力检索(100% 召回,延迟随数据量线性增长)。除了向量索引,还应该知道倒排索引(关键词检索、bm25)、B+ 树(关系型数据库的范围查询)、LSM 树(写多读少的 KV,如 RocksDB)、哈希索引(等值查询)、位图索引(低基数列)。向量检索补一路关键词倒排做混合召回,能显著改善专有名词和数字的召回。
MySQL 主从同步与一致性
流程是:主库把变更写进 binlog;从库的 IO 线程连上主库(由主库的 dump 线程推),把收到的日志写进本地 relay log;从库的 SQL 线程(并行复制时是 worker 线程)读 relay log 并在从库重放,从而追平数据。binlog 有三种格式——STATEMENT(记 SQL,函数与随机值可能不一致)、ROW(记行变更,最安全、日志量大,现在基本是默认)、MIXED(按语句自动选)。
结合一条 insert 讲:客户端提交事务 → 主库写 binlog 并更新存储引擎 → 事务提交返回;dump 线程把这条 event 推给从库 → 从库写 relay log → SQL 线程执行同一条插入 → 从库数据可见。默认的异步复制在这里就返回了,主库挂了可能丢最后几个事务;因此有半同步复制(至少一个从库确认收到日志才返回)、组复制 / MGR(基于 Paxos,多数派确认)和 GTID(全局事务 id,方便自动定位位点与切换)。
一致性要分两个问题答:主从之间——用 sync_binlog=1 + innodb_flush_log_at_trx_commit=1 保证主库不丢、用半同步/组复制降低从库滞后,业务上允许脏读的走从库、要求强一致的读走主库(比如写后立刻读、支付状态查询);缓存与库之间——先写库再删缓存(Cache Aside),配延迟双删或订阅 binlog 投递失效消息,把不一致窗口压到最小。
两道算法题
数组中的三角形个数:三条边 a ≤ b ≤ c 能组成三角形的充要条件是 a + b > c。先把数组排序,固定最长边 c(下标 k,从 2 到 n−1),在 [0, k−1] 上用双指针找满足 a + b > c 的数对:若 nums[i] + nums[j] > nums[k],则 i 从当前到 j−1 的每个取值都能与 j 组成合法数对,一次累加 j − i 后 j 左移;否则 i 右移。整体 O(n²),比三重枚举的 O(n³) 好得多。边界要交代:等于的情况(a + b == c)不算三角形,重复元素按「下标不同的三元组」计数。
动态规划题:题目描述的是一道偏长的 DP(题干往往把状态、转移和约束写在场景里)。这类题的方法是三步——定义状态(dp[i][j] 表示什么含义,必须能一句话说清)、写转移(当前状态从哪些更小的状态推来,注意取 max/min 还是累加)、定边界与遍历顺序(初始化、遍历方向要保证依赖项已经算好)。如果状态维度太高,再考虑滚动数组压空间或用前缀和/单调队列优化转移。