面灵AI→

杭州初创小厂 Java 后端一面:DDD、用户画像建表与责任链

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 说说你对 DDD 的理解,和传统 MVC 的区别。
  2. 情感陪伴 AI 的任务驱动、任务调度怎么设计?
  3. 情感陪伴 AI 的用户画像,数据库表如何设计?
  4. 现有任务表按批次查询、任务失败需要整批重跑。如果要支持单独重跑单个任务,如何修改任务表字段?
  5. 拼团项目如何防超卖?解释为什么要用乐观锁。
  6. 项目中为什么选用责任链?如何用一条责任链串联完整业务链路?
  7. 责任链如何实现并行?如果任务依赖前面多个任务的结果,该怎么设计?
  8. 反问:为什么面试没有八股?对本次面试的评价?为什么大量考察数据库设计?

《参考解析》

DDD 与 MVC 不是替代关系,而是两把不同的刀。 MVC 解决的是「请求怎么从入口走到数据层」:Controller 接参、Service 编排、DAO 落库,按技术职责分层,适合业务规则简单、以 CRUD 为主的系统。DDD 解决的是「业务规则放在哪」:先划分限界上下文,再在上下文内建聚合(聚合根保证一致性边界)、实体与值对象,用领域服务承载跨聚合逻辑,用领域事件解耦副作用,最后才通过应用服务对接外部。判断标准是业务复杂度:如果改一个规则要在三四个 Service 里同步改,说明规则散落在技术层,适合往领域对象里收;如果只是表单存取,硬套 DDD 只会多出一堆贫血对象和转换代码。两者可以组合:外层仍是 MVC 的接口层,内层用 DDD 建模。答题时最好拿自己项目举例,说明哪个聚合、聚合根是谁、哪些不变量由聚合根守住。

任务调度:先分类,再谈调度器。 先把任务拆成几类:一次性任务(生成一次画像、发一次提醒)、周期性任务(每日画像更新、定时回访)、事件触发任务(用户发消息后抽取记忆)、长任务(批量重算)。每类任务的语义不同:一次性任务要幂等键防重复投递,周期任务要错峰和去重(同一用户同一周期只跑一次),事件任务要削峰和顺序保证(同一会话的消息尽量串行)。实现上,轻量阶段用「任务表 + 定时扫描 + 状态机」就够:状态从 pending → running → success/failed,用 SELECT ... FOR UPDATE SKIP LOCKED 或乐观锁抢占,失败按重试次数退避;规模上来后再引入消息队列(延迟消息做定时、死信队列兜失败)或调度框架(XXL-Job、Temporal)。关键设计点是超时检测与补偿:抢了任务但进程挂掉,要有租约(lease)过期后重新可抢,而不是永久卡在 running。还要有可观测性:每个任务有 traceId、执行耗时、失败原因,方便定位。

用户画像建表:别做成一张大宽表。 推荐拆成四层:用户基础表(user_id、昵称、年龄、职业等稳定属性);画像标签表(user_id、tag_key、tag_value、weight、confidence、source、updated_at),用「纵表」支持标签动态扩展,避免每加一个标签就改表结构;行为记录表(user_id、event_type、object_id、occurred_at、context),只追加不更新,作为画像的原始事实;标签定义表(tag_key、含义、值域、计算口径、更新频率),把「标签怎么算」从代码里抽出来。查询侧再按需做宽表或倒排索引(如「喜欢露营的杭州用户」),用离线任务物化,别在请求链路上现算。情感陪伴场景还要单独考虑记忆:短期记忆(会话上下文)和长期记忆(用户偏好、重要事件)分开存,长期记忆要带来源与时间,冲突时按「新近 + 置信度 + 用户显式确认」定优先级。隐私上,敏感属性要标注来源与用途,支持删除与导出。

任务表改成支持单个任务重跑。 核心是把「批次」从执行单元降级为分组标签。表上增加:task_id(单任务唯一主键,雪花号或 uuid)、batch_id(批次号,仅用于聚合查询)、task_status(pending/running/success/failed/skipped)、retry_count、next_retry_at、execute_time、lease_until、idempotency_key、error_msg。执行器从「按 batch_id 捞一批」改成「按 task_status + next_retry_at 捞单条」,用 FOR UPDATE SKIP LOCKED 抢占,抢到就写 lease_until。重跑时只把目标 task_id 的状态重置为 pending,不影响同批次其他任务;如果要整批重跑,就按 batch_id 批量重置。还要加唯一约束防止重复建任务(如 batch_id + biz_key),以及重跑次数上限,避免死循环。

拼团防超卖:乐观锁适合「冲突不极端」的场景。 超卖的本质是「读库存 → 判断 → 扣减」不是原子操作,并发下多个请求都读到 1,然后都扣成功。方案有三类:数据库乐观锁(UPDATE stock SET count = count - 1, version = version + 1 WHERE id = ? AND count >= 1,靠影响行数判断成败);数据库悲观锁(SELECT ... FOR UPDATE,串行化但吞吐低);Redis 预扣 + Lua 原子脚本(高并发首选,再异步落库对账)。另外可以加数据库唯一约束兜底(同一用户同一团只能下一单)。选乐观锁的理由:拼团库存变更不是极端高频,冲突率低,乐观锁无锁等待、无死锁风险、实现简单;缺点是冲突高时大量失败重试,反而放大数据库压力。答题时要补「失败怎么办」:有限次重试 + 熔断,或者直接把请求打到 Redis 预扣,别让重试风暴压垮数据库。

责任链适合「流程节点多、可增删、每个节点只关心自己的判断」。 典型是下单链路:参数校验 → 风控 → 库存判断 → 优惠计算 → 创建订单 → 扣减库存 → 发消息。每个处理器实现同一接口(handle(context)),链式传递同一个上下文对象,任一节点可以中断并返回失败原因。好处是解耦和可插拔(新增一个「限购校验」不用改主流程),坏处是链路不直观、排查困难,所以要加日志与链路追踪,并控制链长。实现上用数组 + 顺序执行即可,别过度设计成动态编排。

责任链的并行与依赖汇聚。 原生责任链是串行的,想并行要先把「无依赖的节点」识别出来:比如风控、优惠计算、库存查询可以并行,创建订单必须等它们全部完成。做法是把链改成 DAG:用线程池/CompletableFuture 并发执行无依赖节点,用 CompletableFuture.allOf 或 CountDownLatch 做屏障,全部成功后再进入汇总节点;任一失败则取消其余任务并走补偿。如果依赖关系复杂(多个前置、条件分支、超时降级),纯责任链就不合适了,应该换成状态机或工作流引擎(如 Temporal、XXL-Job 编排、自研 DAG 引擎),把节点、依赖、重试、超时都建模成配置。答题时要点出取舍:并行能降耗时,但会放大下游压力、增加一致性复杂度(部分成功怎么办),所以只对幂等、可补偿的节点做并行。