深势科技后端一面:WorkflowState 与显式状态机、上下文控制、点赞并发与分表 Top10
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍,并介绍项目。
- 你项目里的
workflowState存储什么内容? - 显式状态机有什么优势?相比链式调用,明显的优点在哪?
- 假设业务复杂度变高,7 个节点不够用了,你有什么方案?
- Agent 项目的 Redis 和 MySQL 里分别存什么?
- 有哪些方式可以控制上下文的长度?
- 说一下代码检查自动修复的闭环逻辑。
- build node 执行 npm 拉起子进程,并发请求很多时怎么处理?
- 库存扣减成功,但用户券记录插入失败,库存怎么办?
- 连续「点赞→取消→点赞→取消」并发操作,怎么保证最终点赞状态和点赞数正确?
- 分库分表以后,查询多个分表的合计点赞 Top10 怎么操作?如果某个月是热点月,怎么办?
- 四种限流的实现方法是什么?固定窗口的使用场景是什么?
- ConcurrentHashMap 在 Java 7 和 Java 8 里有什么区别?
- 手撕:用
CountDownLatch模拟主线程等待多个子线程的结果。 - 介绍线程池的参数,等待队列里存什么?线程什么时候销毁?
- MySQL 不定义自增主键,会不会自动生成一个主键?
- Redis 哨兵模式 1 主 1 从 1 哨兵,发现 Master 宕机后会不会自动选主?过一段时间还能继续写数据吗?
- 算法:递增数组,求两数之和等于 target 的数对数量。
《参考解析》
显式状态机 vs 链式调用:链式(一个函数调下一个)把流程写死在调用栈里,中断后无法恢复、无法在中间插入人工审批、错误处理只能靠异常层层上抛;显式状态机把「当前节点、已完成步骤、中间产物、错误信息」持久化成数据,每个节点只依赖 state,于是可以暂停 / 恢复 / 重试 / 分支,也能做可视化与审计。workflowState 里通常存:当前节点 id、已完成节点与各自输出(或引用)、上下文摘要、重试计数、幂等键、时间戳。
节点数不够时的扩展方案:把节点定义从代码里抽出来做 DSL / 配置(DAG 或图),用统一执行器按拓扑跑,节点注册表 + 插件式加载新节点;对长流程再做子图 / 子工作流复用;关键是「控制流与业务逻辑分离」,否则每加一个节点都要改调度代码。
控制上下文长度的手段按层列:轮次裁剪(保留 system + 最近 N 轮)、摘要压缩(对历史做滚动摘要)、检索式注入(长期记忆抽成事实,按需检索)、结构化裁剪(工具结果只留关键字段,原文外置可回查)、分阶段处理(map-reduce / 子 agent 各自持小上下文再汇总),以及靠模型侧 prompt caching 降低重复前缀的成本(不缩短长度但降费用)。
高并发拉起子进程是资源管理题:进程数要有上限(信号量 / 队列 + worker pool),超限排队或拒绝;给每个子进程设超时与 kill(防止僵尸)、限制 CPU/内存与输出大小、复用长驻 worker 而不是每次都 npm 冷启、把构建产物缓存起来(依赖锁文件 hash 作为 key)。还要处理孤儿进程与清理(进程组 kill)。
「库存扣了、券没插进去」是跨资源事务题:同一库内就放进一个本地事务;跨服务用 TCC(Try 预占库存 → Confirm 确认券 → Cancel 回滚)或本地消息表 / outbox(把「扣库存」和「待插券」写在同一事务,异步补偿插券,失败重试 + 对账),或 Saga(每步定义补偿动作)。核心是「不允许出现不可补偿的中间态」,以及必须有对账兜底。
点赞并发:幂等 + 原子。用 Redis 的 Set/Hash 记 (userId, targetId) 状态并配合 Lua 做原子 toggle,计数用增量计数器而不是「查一遍再算」;最终一致性上把变更写进消息队列顺序消费落库,落库用唯一索引 (user_id,target_id) 防重复;读时以缓存为准并定期用数据库校对。要强调「连续 toggle 的最终状态由最后一次成功写入决定,计数与状态必须一起原子更新」。
分库分表 Top10:两种做法——各分片本地求 Top10 再归并(N 路归并,堆排序),或者把点赞数写进独立的计数表 / Redis Sorted Set 做全局榜;热点月导致单分片倾斜时,先做热点隔离(单独库 / 单独缓存)、再考虑按热点 key 二次拆分(加时间或业务维度分片键),以及把榜单做成准实时(离线算 + 增量更新)。
四种限流:固定窗口(简单、有临界突刺,适合粗粒度日配额)、滑动窗口(按时间片加权,平滑一些、内存换精度)、令牌桶(允许突发、长期按速率)、漏桶(严格匀速,适合保护脆弱下游)。另外要点出「并发数限流(信号量)」与分布式的实现(Redis + Lua,或网关层)。
ConcurrentHashMap:JDK7 是 Segment 分段锁 + HashEntry 数组 + 链表,锁段;JDK8 取消 Segment,用 CAS 插入空桶 + synchronized 锁桶头,链表长度超阈值转红黑树,扩容支持多线程协助迁移(ForwardingNode),size 用 CounterCell 分散。线程池参数:核心数、最大数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略;队列里存的是待执行的 Runnable;线程数超过核心数时非核心线程空闲存活时间到了被回收,核心线程默认不回收(allowCoreThreadTimeOut 可改),shutdown 后任务跑完才退、shutdownNow 打断并返回未执行任务。
MySQL 不定义主键:InnoDB 会自动选一个「非空唯一索引」做聚簇索引,如果连唯一索引都没有,就建一个隐藏的 6 字节 ROW_ID(全局自增,不是每表独立),此时你无法访问它,页分裂与回表都会更难控制——所以生产上应该显式定义自增或雪花主键。
哨兵 1 主 1 从 1 哨兵:哨兵本身要多数派投票(quorum 与 majority),单个哨兵在主观下线后只能达到 sdown,要 odown 需要 quorum,failover 还要多数 sentinel 授权——1 个哨兵无法完成真正的自动切换,会卡在「已下线但不能提升从库」;而且没有可提升的从库时更谈不上切换,期间对 master 的写会失败。生产至少要 3 个哨兵、1 主 2 从。这个题就是考「哨兵需要奇数多数派」这个常识。
两数之和数对(递增数组):双指针,l=0, r=n-1,和小于 target 就 l++,大于就 r--,相等时计数并同时移动(注意重复元素的组合计数:若要求返回数对个数而不是组合,要按相同值的个数乘起来算),O(n) 时间 O(1) 空间。相比哈希表法,递增条件给了双指针更优的空间复杂度。