Skip to content

核心架构拷问 FAQ

本页收录关于项目深层架构选型、高并发场景、微服务协同与工程取舍的高频拷问,适合面试前最后一轮通读。

0. 先看这页最有说服力的 FAQ 硬数据

这页之所以值得看,不是因为问题“听起来高级”,而是这些问题都能在当前代码里找到直接证据:

  • 18 个 @FeignClient:说明跨服务调用是主链路,不是边角能力
  • 30 个 @RabbitListener:说明 MQ 已经承担了异步解耦、补偿、广播、同步等核心职责
  • 26 个 @XxlJob 处理器:说明定时扫描、热榜重建、订单兜底都已经体系化
  • 4 个 Gateway 前置过滤器:AuthFilter / XssFilter / ValidateCodeFilter / BlackListUrlFilter
  • 4 类向量空间:店铺 / 商品 / 博客 / 评价
  • Netty + Chat + MQ 三段式 IM 架构:说明即时通讯不是页面演示,而是完整链路

所以这页 FAQ 更像“面试官会怎么拷问这套代码”,而不是抽象八股问答。

1. AI 模块在这个项目里到底做了什么?

这个项目里的 AI 不只是“接一个聊天接口”,而是同时覆盖了 用户端问答检索商家端经营辅助 两条线:

  • 用户端:支持店铺 / 商品 / 评价 / 博客问答、流式 AI 对话、探店博客生成、消费评价生成
  • 商家端:支持差评回复、经营建议、营销文案生成、经营分析
  • 底层能力
    • 基于 Spring AI + Tool + RagService + 向量检索 实现业务数据型、结构化、可执行的 RAG
    • 拆成 店铺 / 商品 / 博客 / 评价 四类向量空间
    • 通过 Agent 路由与意图识别做决策,再按需调用不同工具链
    • Milvus 不可用时可回退到 SimpleVectorStore

所以它更像一层建立在业务知识底座上的 AI 增强能力,而不是孤立的聊天能力展示。

2. 为什么项目里同时用了 RabbitMQ 和 XXL-JOB?

两者职责不同,不是重复建设:

  • RabbitMQ:负责异步解耦和准实时处理,例如订单创建、库存扣减、审核投递、消息推送、支付回调后的后续动作
  • XXL-JOB:负责周期性扫描、补偿兜底和批处理,例如热榜重算、互动数据回刷、秒杀预热、订单超时处理、销量同步

从当前代码规模看,这种分工不是“理论设计”,而是已经落地:

  • 30 个 @RabbitListener
  • 26 个 @XxlJob 处理器

可以把它理解成: MQ 解决“事件驱动”,XXL-JOB 解决“定时调度与补偿兜底”。两者配合起来,才能同时兼顾实时性和最终一致性。

3. 为什么选 Milvus 而不是 Pinecone 或 pgvector?

项目中需要结合 AI 进行相似度检索(例如商品、店铺、博客、评价的语义召回),Milvus 更适合当前这类 开源、自部署、可控 的场景:

  • 相比 Pinecone:不依赖云服务商,数据和成本更可控
  • 相比 pgvector:当前项目已经是专门的向量检索副本思路,不希望把事务库和向量检索强绑在一起
  • 相比“必须依赖 Milvus 在线”:当前代码已经支持 SimpleVectorStore 降级,联调和演示更稳

所以这个选择不只是“追新”,而是基于当前项目需要 开源可控 + 多向量空间 + 可降级 的现实判断。

4. Feed 流查询为什么需要采用特殊的滚动分页?

在具有强社交属性的 Feed 瀑布流中,数据写入频率极高。如果采用传统的 LIMIT offset, size,当用户翻页时,只要头部插入了新动态,就会导致整体数据向后位移,用户将在下一页看到重复数据。

本项目采用基于 Redis ZSet 的滚动分页:

  • 每次查询以上一页最后一条记录的时间戳作为 score 锚点
  • 再配合 offset 处理同分值元素
  • 核心读取方式是 ZREVRANGEBYSCORE

这样做不是为了“写法特别”,而是为了在高频写入的 Feed 场景下,从数据结构上避免传统分页错位。

5. 分布式环境下的数据一致性是怎么保证的?

这个项目当前主方案不是“全链路 Seata 强事务”,而是:

RabbitMQ 消息可靠投递 + Redis 幂等 + ACK/NACK + 延迟 / 死信队列 + XXL-JOB 的最终一致性方案。

适用场景包括:

  • 下单成功后的后续动作
  • 审核系统状态回写
  • 数据同步到 ES / Milvus
  • 超时订单关闭与库存恢复
  • 钱包退款消息入账

这里最关键的不是“用了 MQ”,而是几层兜底一起上:

  • 发送端:MqMessageSendUtils + Confirm 回调 + 本地最多 3 次重试
  • 路由失败:ReturnCallback 兜底排查
  • 消费端:messageId + Redis 幂等键
  • 失败场景:ACK/NACK + 死信 / 延迟队列

也就是说,这个项目强调的是: 高并发链路优先保证吞吐和最终收敛,而不是所有场景都上强事务。

6. 各种缓存并发与提单安全问题是怎么处理的?

项目中统一定义了 CacheClient 做热点缓存治理,也在核心链路里接入了 RedissonClient

  • 缓存击穿:热点店铺或博客详情采用 逻辑过期,优先返回旧值,再异步重建
  • 缓存穿透:对数据库不存在的空结果集采用 缓存空对象
  • 缓存雪崩:批量缓存引入 随机 TTL 抖动
  • 并发防重:普通下单场景通过 RedissonClient.getLock("order:" + userId) + tryLock() 做用户级防重

所以这个项目的缓存层不是只有“查 Redis”,而是已经把热点读、空值、过期、并发下单这几类问题拆开处理了。

7. Netty WebSocket 为什么不用 Spring WebSocket?

这个项目里的即时通讯不是单纯“有个聊天窗口”,而是要同时解决:

  • 长连接在线状态
  • 活跃会话同步
  • 消息实时分发
  • 持久化落库
  • MQ 广播

当前实现选择了 Netty + WebSocket,因为它更适合做细粒度控制:

  • IdleStateHandler 处理心跳与连接保活
  • ChannelGroupuserChannels 管理在线用户
  • RemoteChatService 负责消息持久化和会话同步
  • MQ 负责广播与异步扩散

所以不用 Spring WebSocket 的根因不是“它不行”,而是: 当前 IM 这条链路已经拆成了 实时连接层 + 持久化层 + 广播层,Netty 更适合做这一层控制。

8. 为什么用 ZSet 存列表,String 存详情,不统一都用 Hash?

这里不是为了“多用几种 Redis 结构”,而是按读写模型拆分:

  • 列表层(ZSet):适合按时间、热度、距离、排序做范围查询
  • 详情层(String):直接存完整 JSON,读取完整对象成本最低
  • 计数层(String / Hash / 独立 Key):适合原子增减和高频写入

如果统一都用 Hash,会遇到两个问题:

  1. 列表排序能力弱,很多范围读取还是得靠应用层处理
  2. 对详情页这种“总是读取完整对象”的场景,Hash 并不比 String 更自然

所以当前项目本质上是: 按访问模式选结构,而不是按“统一风格”选结构。

9. AbstractInteractionStrategy 为什么值得讲?

这一块不是单纯的“用了模板方法模式”,而是把多类互动行为的 ES 同步统一收口了。

AbstractInteractionStrategy 做了 5 件统一的事:

  1. 拉取源业务对象
  2. 获取业务域
  3. 获取动作类型
  4. 获取资源类型编码
  5. 构建统一 UserResourceMessage 并投递 RabbitMQ

这样各个子类只需要实现少量钩子方法,就能接入同一套 ES 用户资源同步链路。

当前 interaction 模块里已经形成了多套策略工厂:

  • LikeStrategyFactory
  • StarStrategyFactory
  • FollowStrategyFactory
  • ReviewStrategyFactory
  • CommentStrategyFactory
  • ResourceStrategyFactory
  • HotRankStrategyFactory

所以它真正的价值不是“设计模式写得优雅”,而是: 当互动类型增多时,不需要让每个业务都自己手写一套 MQ 投递和 ES 同步协议。

10. 如何高效地同步 Redis 中的大量互动数据到 MySQL?

这个问题的本质不是“怎么批量刷库”,而是: 如何在不拖慢主业务的前提下,把高频热数据安全回写到 MySQL。

当前实现走的是双轨异步思路:

  • 同步轨(Sync):把 Redis 高频数据批量回刷到 MySQL
  • 计算轨(Calc):对热榜、得分、候选集做增量重算

并且在 SyncDataServiceImpl 里,当前已经不是 4 个任务并发,而是 6 个任务并发

  • like
  • comment
  • star
  • review
  • follow
  • fans

关键优化点包括:

  • RENAME 原子快照:把热数据切到快照 Key,再异步回刷,避免和在线写入互相阻塞
  • Pipeline 批量查询:减少 Redis 往返
  • CompletableFuture.runAsync` 并发执行:提升回刷吞吐

所以这一块真正值得讲的是: 项目没有把“高频互动写入”直接压回数据库,而是设计成了 Redis 实时承载 + 定时异步回刷 + 热榜计算分轨运行 的结构。