5. 审核中心责任链 + 发布审核与搜索/向量同步全链路详解
本文档是 SmartLive 项目内容风控与数据异构链路的深度拆解,适合面试 10 分钟讲解版本。
涵盖:解耦审核流程(责任链模式) → 多节点决策流转 → 失败通知(MQ 下放) → 异构双写(ES 全文检索 / Milvus 向量引擎) → 同步强幂等(Redis tryConsumeOnce 防重放) → 死信补偿兜底。
1. 链路总览图
审核中心责任链:
发布审核与搜索 / 向量同步:
搜索 / 向量库异构同步细化:
2. 每一步的技术细节与面试追问
Step 1~4:审核责任链的运转 (AuditProcessChain)
做了什么:
- 用户发布动态(如博客、评论)后,业务不直接
Insert可见表,而是塞入一条AuditMessage进 RabbitMQ。 - 审核服务监听消息后记录一条
AuditTask到 MySQL 留存证据。 - 把该任务装载进
AuditProcessContext,循环推送给List<AuditProcessHandler>链式网。 - 从基础的 DFA 算法敏感词处理器,到接入外部大模型 AI 的鉴黄鉴暴处理器,再到疑似问题转给人工的处理器。任何一个 Handler 随时可终结上下文抛出结论(如
context.reject("涉嫌违规"))。
面试追问与回答:
| 问题 | 回答 |
|---|---|
审核逻辑为什么不用臃肿的 if-else,而是搞出一套责任链? | 为了严格的“开闭原则”和良好的可配置性。平台风控规则日新月异,明天可能加上“涉政匹配”、“黑名单画像校验”等;通过链式设计加上 @Order,团队只需新增并注入一个实现了 Handle 的 Bean 即可挂载上去生效,不动主干逻辑。 |
| 如果某个审核节点出 Bug 或者超时报错了,这条 UGC 的下场是什么? | 我在 handleAudit 主方法的 try-catch 里做了链路异常兜底,只要报错则强行将该内容降级为待审(调用 waitManual("异常转人工")),既不盲目放行引发事故,也不直接驳回引起客诉。 |
Step 5~6:审核驳回与模块异步通信
做了什么:
一旦遇到 REJECT,系统修改任务为驳回态,立刻封装一条 SystemNoticeCreateDTO 通过 RabbitMQ 的 ChatMqConstants.SYSTEM_NOTICE_EXCHANGE 扔给站内信模块处理,让用户及时感知发布失败的原因。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 把驳回原因通知给用户,为什么不用 Feign/RPC 直接调站内信服务? | 这不是核心依赖路径!审核一旦执行完成,其状态就已经算定局,若因为去同步调接口等待对方返回期间遭遇对方微服务宕机或超时异常进而导致审核方法回滚,属于因果倒置。MQ 让两者毫无耦合。 |
Step 7~10:双轨异构同步与核心强幂等 (Search / Milvus Sync)
做了什么:
- 当 UGC 通过审核(PASS)后,由主业务模块发布一次性同步事件(包含了
type和实体内容data)。 - 同步 ES:
EsSyncListener接客,先用DeliveryTag配合 Header 里的Message_ID利用redisService.tryConsumeOnce(idempotentKey)在 Redis 执行 SETNX 高速拦截。再把载荷丢给EsSyncStrategy策略去批量/单条写入倒排索引库。 - 同步 Milvus:
MilvusSyncListener接客,同样先利用 Redis 分布式锁占位防并发重放。再扔给MilvusSyncStrategy这个策略池里的组件(如ProductMilvusStrategy),提取出特征字段让大模型 Embeddings 并塞入高维向量数据库用于后续 RAG / 语意对话。 - 若执行失败,删除
IdempotentKey锁,重新对外抛出RuntimeException让 RabbitMQ 的重试机制介入。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 你的异构同步(MySQL 到 ES)为什么用 MQ,而不选 Canal 或 Logstash? | Canal 伪装从库抓取 Binlog 虽然 0 侵入代码,但由于我项目中的同步对象是一张“宽表大物”(比如博客还要连带作者、店铺详情),Canal 在拼装这种横跨几张表的数据时极尽复杂。我通过 MQ 从业务层触发时就顺手打包全整的一套 VO 对象过去同步,简单粗暴且定制化强。 |
| Redis 的强幂等锁如果有多个实例瞬间消费同一条消息,怎么保证唯一执行? | Redis setNx 操作天生拥有超强原子穿透能力。哪怕10个消费者实例同一毫秒拿到重传消息,只有第一个能拿到锁进行后续的 strategy.insert 动作并 ACK,剩余 9 个统统捕获并做 basicAck(false) 空弹跳出,防重放(Replay Attack/Retry)滴水不漏。 |
Strategy(策略模式)在这里起了什么至关重要的作用? | ES 库和向量库不止插一种东西(有店铺、有博客、有商品),它们字段甚至坐标结构天差地别。用 Factory 和 Strategy 设计模式做中间人分发,完美抹平业务线的差异,把臃肿的几十张表的判断直接多态化分解成独立的类去各自负责自己的解析插入。 |
Step 11:死信队列(兜底拦截)
做了什么:
若 ES 或 Milvus 因为网段不通或其他数据包畸形导致写入失败。由于向外抛出抛了 3 次(Spring AMQP 本地 Retry 耗尽),原封不动地将坏骨头丢弃至 SEARCH_DLX_EXCHANGE 指向的 DLQ_QUEUE,不再堵塞主干道。后台由专门的方法监听死信并告警记录在册,待查明人工点动恢复重塞入库。
3. 整条链路涉及的核心技术点汇总
┌─────────────────────────────────────────────────────┐
│ 技术点对照表 │
├──────────────┬──────────────────────────────────────┤
│ 架构设计 │ · 异构数据双写架构(DB -> ES / Milvus)│
│ │ · 容错降级降档(转译兜底人工防事故) │
├──────────────┼──────────────────────────────────────┤
│ 设计模式 │ · 责任链模式(Chain_of_Responsibility)│
│ │ · 策略模式(Strategy分发异构同构差异) │
├──────────────┼──────────────────────────────────────┤
│ 消息队列 MQ │ · Direct 投递业务到微服务分界线 │
│ │ · 消费端本地重试 + DLX 兜底信箱收集 │
│ │ · Redis SETNX 与 TTL 打造极致消费判重 │
├──────────────┼──────────────────────────────────────┤
│ 双库异构支撑 │ · Elasticsearch(处理高频聚合、距离筛选)│
│ │ · Milvus(承载大模型 RAG高维稠密搜索) │
└──────────────┴──────────────────────────────────────┘4. 面试 10 分钟讲述模板
讲述思路:以解耦引入内容风控的优雅实现,再过渡到复杂的异构重试与强幂等闭环。
开场(1 分钟)
"在 SmartLive 平台,UGC 创作和分发是一道重头戏体验。除了保证海量查询效率我接入了 ES 与 Milvus 两大外部引擎外,还必须保障非法内容 0 容忍。但审核流与庞大的更新同步流不仅慢还贼脆弱,我引入了 审核责任链机制 + MQ 分布式解耦异构同步 + Redis 强防重体系。这套设计原则只有一个:模块极致解耦下的数据最终一同步。"
风控体系:一条华丽的责任链(3 分钟)
"用户传上的动态如果都用
If-else调几个风控包做强判断,这坨代码一两个版本后就没法看了。我将其打散进一个基于 List 和@Order排列出来的责任链中(先测敏感词过滤、再切 AI 端检测、不行推给人工待定池),它天生具备了可插拔、免测试、无限延伸的微内核架构特质。并且其中任意一个环节因为外部网络原因报错崩溃了,我都不会暴露出来,而是利用 catch 包装成转人工状态进行优雅降级。"
消费解耦:一切非主路线都由 MQ 传达(2.5 分钟)
"审核一完毕,如果不过关,立刻给站内信交换机扔条包装信鸽,彻底断开两路 RPC 的生绑关联。如果过关了,就在发布端将封装好的业务全量大 VO 打破并广播,交由处于平台最后端的 ES 和 Milvus 双双订阅和拉取。"
异构防雷:极严苛的消费强幂等与死信归档(2.5 分钟)
"为了抗网络抖动,消息很有可能被塞入双份。两边消费端的入口,全用基于 Redis
tryConsumeOnce开辟的一把防重排他锁守住,拦截无效或重装复试流量丢进垃圾桶。拿到业务包后,利用策略工厂化解商品、内容等五花八门的业务包差异。同步时如果真写不进去外部库,连续抛出致命错误,交由 RabbitMQ 自身的 Dead-Letter 把这些烂账打包塞回一个专门的死信监听器里做持久报警,既不流失最后一次重读机会,也不造成排头队列的无尽拥堵。"
总结亮点(1 分钟)
"这条内容流经之路,精髓在于:责任链抽离了多重多变的业务判断枝干,MQ 分割了长流程的时间停顿,而重试策略夹击死信通道则做到了对大基盘异构同步事故的最佳掩护和复用。"
5. 高频追问速查表
| 追问方向 | 关键问题 | 核心回答 |
|---|---|---|
| 设计取舍 | 审核拒绝通知为什么不要 try-catch 后调库解决,还要大费周折发 MQ 给自己应用? | 微服务化要求权责分离。Chat 通讯模块有属于自己独立的库表表空间,跨模块调用库在规范上是不允许且牵连深重(分布式事务问题)。发一次 MQ 等同于完成领域边界的一次干净交付,不带拖延、不去关心。 |
| 同源一致 | 消息里同步只传 ID,然后到消费者那头去库里反查,这样不更好?为何传全量的大 VO? | 传 ID 虽然报文轻便,但反查时面对的是主库。如果主库由于刚刚的强更还没等主从复制过去(主从延迟),你马上从库去反查可能查不到;再或者该条数据极短时间内被人又秒删了,你查到空指针而造成索引同步挂断。我把“确定的现状(瞬时快照全量数据)”直接发过去落底,消减了状态穿透查的风险。 |
| 解偶拆分 | 为什么要设计 ES 有一个 Listener,Milvus 还有个一模一样的 Listener,合成一个不香? | 职责单一原则。ES 通常耗时在毫秒级,而部分提取内容去调用大模型模型转出 1536 维向量存给 Milvus 时,耗秒可能是 1 - 2 秒。把耗时悬殊、业务目标无关的逻辑揉进一台机器的单线消费者中,不但一挂双丢,还得被迫受限于短板效应发生队头阻塞。 |
6. 扩展:这条链路和八股的关联
风控与异构同步链路 ←→ 八股知识点映射
架构与微服务实战设计八股:
├── CQRS(命令查询职责分离)变体的实际应用与下沉同步设计
└── 基于事件驱动架构(EDA)消弥跨进程分布式事务(通过 MQ 解耦达到最终一致)
设计模式八股深潜:
├── 责任链模式的运用(通过 Filter 或 List<Handler> 实现动态表决插拔)
└── 策略模式搭配简单工厂(多态消除冗长逻辑的终极奥义)
双向消息驱动八股:
├── 消费链路中的幂等去重本质:分布式锁 SETNX 在 MessageID 发力
├── RabbitMQ 死信背后的重试循环与(Retry/BackOff策略机制)
└── Fanout/Topic 扇出到双消费实例时的订阅组分配设计隔离
主流搜索引擎/向量基底八股:
├── 为什么像 ES 这样的倒排无法实现精准自然语意命中?
└── 稠密多维向量 Milvus 与 RAG(检索增强生成架构)数据预处理的第一公里接入点一句话总结:内容安全与千库外延永远是个沉重的话题,但在微服务下,只要熟练掌握 "用责任链抽离无尽规则、借 MQ 扇出切断拥堵、靠 Redis 短路截断重放" 三板斧,所有耗时、变动、和扯皮的联动系统都能在不经意间顺畅调配。