核心架构拷问 FAQ
本页收录关于项目深层架构选型、高并发场景、微服务协同与工程取舍的高频拷问,适合面试前最后一轮通读。
0. 先看这页最有说服力的 FAQ 硬数据
这页之所以值得看,不是因为问题“听起来高级”,而是这些问题都能在当前代码里找到直接证据:
18 个@FeignClient:说明跨服务调用是主链路,不是边角能力30 个@RabbitListener:说明 MQ 已经承担了异步解耦、补偿、广播、同步等核心职责26 个@XxlJob处理器:说明定时扫描、热榜重建、订单兜底都已经体系化4个 Gateway 前置过滤器:AuthFilter / XssFilter / ValidateCodeFilter / BlackListUrlFilter4类向量空间:店铺 / 商品 / 博客 / 评价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 个@RabbitListener26 个@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处理心跳与连接保活ChannelGroup和userChannels管理在线用户RemoteChatService负责消息持久化和会话同步- MQ 负责广播与异步扩散
所以不用 Spring WebSocket 的根因不是“它不行”,而是: 当前 IM 这条链路已经拆成了 实时连接层 + 持久化层 + 广播层,Netty 更适合做这一层控制。
8. 为什么用 ZSet 存列表,String 存详情,不统一都用 Hash?
这里不是为了“多用几种 Redis 结构”,而是按读写模型拆分:
- 列表层(ZSet):适合按时间、热度、距离、排序做范围查询
- 详情层(String):直接存完整 JSON,读取完整对象成本最低
- 计数层(String / Hash / 独立 Key):适合原子增减和高频写入
如果统一都用 Hash,会遇到两个问题:
- 列表排序能力弱,很多范围读取还是得靠应用层处理
- 对详情页这种“总是读取完整对象”的场景,Hash 并不比 String 更自然
所以当前项目本质上是: 按访问模式选结构,而不是按“统一风格”选结构。
9. AbstractInteractionStrategy 为什么值得讲?
这一块不是单纯的“用了模板方法模式”,而是把多类互动行为的 ES 同步统一收口了。
AbstractInteractionStrategy 做了 5 件统一的事:
- 拉取源业务对象
- 获取业务域
- 获取动作类型
- 获取资源类型编码
- 构建统一
UserResourceMessage并投递 RabbitMQ
这样各个子类只需要实现少量钩子方法,就能接入同一套 ES 用户资源同步链路。
当前 interaction 模块里已经形成了多套策略工厂:
LikeStrategyFactoryStarStrategyFactoryFollowStrategyFactoryReviewStrategyFactoryCommentStrategyFactoryResourceStrategyFactoryHotRankStrategyFactory
所以它真正的价值不是“设计模式写得优雅”,而是: 当互动类型增多时,不需要让每个业务都自己手写一套 MQ 投递和 ES 同步协议。
10. 如何高效地同步 Redis 中的大量互动数据到 MySQL?
这个问题的本质不是“怎么批量刷库”,而是: 如何在不拖慢主业务的前提下,把高频热数据安全回写到 MySQL。
当前实现走的是双轨异步思路:
- 同步轨(Sync):把 Redis 高频数据批量回刷到 MySQL
- 计算轨(Calc):对热榜、得分、候选集做增量重算
并且在 SyncDataServiceImpl 里,当前已经不是 4 个任务并发,而是 6 个任务并发:
likecommentstarreviewfollowfans
关键优化点包括:
- RENAME 原子快照:把热数据切到快照 Key,再异步回刷,避免和在线写入互相阻塞
- Pipeline 批量查询:减少 Redis 往返
- CompletableFuture.runAsync` 并发执行:提升回刷吞吐
所以这一块真正值得讲的是: 项目没有把“高频互动写入”直接压回数据库,而是设计成了 Redis 实时承载 + 定时异步回刷 + 热榜计算分轨运行 的结构。