Skip to content

难点踩坑与解决方案

本页作为网站详细版页面维护,适合单独承接踩坑复盘、故障定位、工程取舍与面试追问展开。

0. 先看这页最有说服力的难点数据

这页最值得先讲的,不是“我遇到了很多坑”,而是这些坑为什么一定会在这套代码里出现:

  • 18 个 @FeignClient:说明跨服务调用很多,状态边界和幂等问题一定会暴露
  • 30 个 @RabbitListener:说明异步消息已经进入主链路,不是一两个边角队列
  • 26 个 @XxlJob 处理器:说明项目里存在大量“定时扫描、补偿、重建、兜底”需求
  • 4 类向量检索空间:店铺 / 商品 / 博客 / 评价,意味着 AI 链路一定会面对外部依赖和副本收敛问题
  • 6 类审核策略:用户 / 店铺 / 博客 / 商品 / 评论 / 评价,意味着治理链路天然复杂
  • Netty + Chat + MQ 三段式实时通讯:意味着“实时推送”和“消息持久化”必须拆开看

所以这页想讲的不是“常见后端问题合集”,而是: 这些坑为什么会在这套代码里真实出现,以及我是怎么把它们收敛下来的。

1. 缓存与一致性

1.1 社交计数(点赞/收藏)的高频写穿透

  • 挑战:内容曝光时会出现突发的高频点赞 / 取消点赞动作,起初直接双写 Redis + MySQL 导致极高的 DB 事务开销,甚至死锁频发。
  • 解决方案:引入 "Redis Hash 增量原子更新 + 定时快照批量归档" 方案。
    1. 所有互动计数及状态实时累加在 Redis 的特定前缀缓存中。
    2. XXL-JOB 每隔 5 分钟执行一次快照归档任务:使用 RENAME 指令将当前全量热数据原子重命名为归档 Key。
    3. 异步线程消费归档 Key 并在应用层做状态融合聚合后,按照 ON DUPLICATE KEY UPDATE 批量 Upsert 回写 MySQL,彻底解耦读写路径。

1.2 高频更新场景下 Redis 缓存与数据库的一致性窗口问题

  • 挑战:项目里同时存在详情缓存、列表缓存、计数缓存、热榜缓存,店铺、商品、博客、评价等对象又都可能被高频更新。如果简单采用“查库后写缓存”或“更新时同步覆盖所有缓存”,不仅写放大严重,还会让热点数据在并发下频繁抖动,甚至出现列表和详情短时间不一致。
  • 解决方案:采用 "写库后删缓存 + 逻辑过期后台重建 + 批量缓存管理" 的组合方案。
    1. 详情读链路统一走 CacheClient,对热点数据采用“空值缓存防穿透 + 逻辑过期防击穿 + 互斥锁异步重建”。
    2. 写路径以数据库为准:店铺、商品、博客等更新成功后,不直接在业务代码里强行重写所有缓存,而是删除详情 Key、列表 Key、热榜 Key 或触发刷新方法。
    3. 对需要批量召回的列表场景,再引入 RedisMultiCacheManager 做统一管理,把“批量回填、逻辑过期、空值缓存、异步重建”收敛到基础设施层。

2. 交易与补偿

2.1 高并发秒杀场景下的“超卖”与“少卖”治理

  • 挑战:单靠数据库行锁扣减库存会导致 CPU 飙升、连接池爆满;改为 Redis 预扣后,又在极端网络抖动或 JVM 宕机时,出现了用户重复抢购,以及订单生成后超时未支付导致的“少卖”现象。
  • 解决方案:引入 "Redis Lua 原子校验预扣 + RabbitMQ 延迟队列闭环" 方案。
    1. 使用 Lua 脚本将“一人一单校验”与“预扣减库存”封装为原子操作并在 Redis 中执行,将绝大多数无效流量阻拦在缓存层。
    2. Lua 扣减成功后立即通过 MQ 发送异步消息,后端工作服务异步消费消息完成真实订单落库,实现削峰。
    3. 针对超时不支付情况,投递含订单 ID 的 TTL 延迟死信消息。消费者收到后核实支付状态,若未支付则自动关单、回滚 MySQL 真实库存,并补偿 Redis 容量表,形成库存状态闭环。

2.2 订单取消、退款、过期任务同时存在时的状态边界问题

  • 挑战:同一笔订单可能同时面临用户主动取消、用户主动退款、延迟队列超时取消、定时任务过期兜底等多条路径。如果没有严格的状态边界,就容易出现重复回滚库存、重复发送退款消息、钱包重复入账这类严重问题。
  • 解决方案:按 "状态校验优先,补偿动作后置" 的原则收敛所有入口。
    1. cancelrefund、延迟消息消费者、过期任务都先判断订单旧状态,只允许合法状态迁移;未支付订单只做关单与库存恢复,已支付订单才进入退款补偿链。
    2. 库存恢复、销量回滚、退款 MQ、钱包入账这几步不再散落在不同入口里,而是由订单服务统一编排。
    3. 代码里又额外拆出了 orderSoonExpireJobHandlerorderExpireJobHandler 两类任务,把“临期提醒”和“到期未使用自动退款”分开治理,避免把所有兜底逻辑揉在一处。

3. 消息与最终一致性

3.1 RabbitMQ 异步链路里的消息一致性与重复消费问题

  • 挑战:项目里订单创建、支付超时、库存扣减、Feed 推送、审核投递、ES / Milvus 同步都依赖 RabbitMQ 串联。只要发送确认、路由投递、消费重试任一环节不稳,就容易出现“业务已经提交但消息没到”“同一条消息被重复消费”“异常消息堆积后无人兜底”三类典型问题。
  • 解决方案:本项目没有走本地消息表 / Outbox,而是采用 "发送端 Confirm + Return 兜底 + 消费端 Redis 幂等 + 手动 ACK/NACK + 延迟 / 死信队列" 的最终一致性方案。
    1. 发送端统一通过 MqMessageSendUtils 注入 messageId,绑定 RetryCorrelationData 的 Confirm 回调;未 ACK 则本地最多重试 3 次。
    2. 消费端统一要求消息必须带 messageId,再通过 RedisService.tryConsumeOnce()SETNX + TTL 幂等占坑。
    3. 消费成功后手动 basicAck;消费异常时先清理幂等键,再交给 MQ 重试;非法消息直接拒绝,不让毒消息无限回圈。
    4. 延迟与死信队列负责最后一道兜底。订单超时未支付、支付记录过期这类链路已经形成自动处理闭环。

3.2 MySQL、Elasticsearch 与 Milvus 多端数据收敛问题

  • 挑战:店铺、商品、博客这些对象既要满足事务写入,又要支持搜索、推荐和向量检索。真正的业务状态在 MySQL,但前台查询又大量依赖 Elasticsearch 和 Milvus。如果在业务代码里直接同步双写,就很容易因为审核状态、删除下架、重试失败等问题,出现“数据库已更新,但索引或向量库还是旧数据”的分叉。
  • 解决方案:采用 "MySQL 作为真实源 + 审核通过后异步投递 + ES / Milvus 分别消费收敛" 的方案。
    1. 店铺、商品、博客的创建与修改先以 MySQL 为准;提交后通常先进入审核链路,只有审核状态变成 PASS 后才发送 ES 与 Milvus 的同步消息。
    2. 驳回、删除、下架时继续发送删除同步消息,把 ES 文档和 Milvus 向量一并清掉。
    3. EsSyncListenerMilvusSyncListener 都采用了同样的消费模型:必须携带 messageId、先用 Redis 做幂等占坑、按业务类型路由到对应策略、成功后手动 ACK,失败则清理幂等键并触发本地重试。

3.3 外部向量库不稳定时,AI 链路被整体拖垮的问题

  • 挑战:AI 这块不只是简单聊天,还涉及 店铺 / 商品 / 博客 / 评价 四类向量检索。如果把 Milvus 当成“必定可用”的强依赖,那么本地联调、开源接入、服务器冷启动或网络抖动时,整个 AI 能力都会跟着瘫掉。
  • 解决方案:在向量层接受“可降级”,而不是要求“永远在线”。
    1. milvusConfig 里先判断 enabledhostport 是否可用,再决定是否初始化 Milvus 客户端。
    2. 如果 Milvus 初始化失败或未配置,直接回退到 SimpleVectorStore,保证 AI 主链路还能运行。
    3. 这样虽然会牺牲一部分向量检索能力,但不会把博客生成、评价生成、商家助手这些功能整体拖死。

4. 推荐与社交

4.1 社交动态 Feed 流中的传统分页“数据偏移”问题

  • 挑战:在用户浏览主页粉丝动态或热榜 Feed 流时,由于系统无时无刻不在产生新内容,传统基于数据库 LIMIT offset, size 的翻页体验极差,常常会看见重复数据或内容错位。
  • 解决方案:摒弃传统分页机制,自研实现 "基于 Redis ZSet 的滚动分页(Scroll Pagination)"
    1. 动态发布时,将业务数据 ID 与时间戳(作为 Score)写入用户聚合流(Redis ZSet)。
    2. 前端请求携带 pageSizemaxScoreoffset
    3. 后端利用 ZREVRANGEBYSCORE key maxScore 0 LIMIT offset pageSize,以用户屏幕底部最后一条记录的时间戳作为绝对锚点进行查阅。

4.2 首页热门面板的冷启动与长期霸榜问题

  • 挑战:首页热门面板同时承接本地必吃榜、抢手好券榜、本地团购榜,热门博客内容流又是另一条独立读链。如果只按单一字段排序,新内容几乎没有曝光机会;如果完全依赖实时互动,又会出现老内容长期霸榜。
  • 解决方案:引入 "增量算分 + Top N 合并 + 凌晨全量重建" 的双层热榜维护方案。
    1. 各业务线的上架、销量、点赞、评论、评分等变化先进入各自的 calcQueue,由 XXL-JOB 周期性触发增量计算。
    2. 计算时先通过 RENAME 将当轮待处理数据切到 TEMP 快照,再把新候选集与历史榜单 Top N 合并。
    3. 不同业务类型采用不同热度公式,并加入时间衰减、销量、评分、互动等权重;每天凌晨再执行全量重建任务兜底修正。

4.3 实时聊天推送与消息持久化放在一起,导致链路相互拖累的问题

  • 挑战:如果把“WebSocket 在线推送”和“消息持久化入库”硬绑在一个通道里,那么聊天消息一旦遇到 DB 抖动、会话同步异常或业务服务短暂不可用,实时聊天体验就会被整体拖慢。
  • 解决方案:把 IM 链路拆成 "Netty 实时连接层 + Chat 持久化层 + MQ 广播层" 三段式结构。
    1. smartLive-im 模块只负责 WebSocket 长连接、在线状态、活跃会话和实时分发。
    2. 消息保存与会话同步由 RemoteChatService 交给 chat 模块落库。
    3. 实时广播再通过 MqMessageSendUtils 发 MQ 事件,把“实时推送”和“业务持久化”从线程与职责上拆开。

5. 内容治理

5.1 异构 UGC 内容的审核堆积与链路阻塞(责任链模式)

  • 挑战:平台有 用户 / 店铺 / 博客 / 商品 / 评论 / 评价 六条核心治理链路,需要经过文本违规过滤、AI 情绪倾向判定、人工抽检等多个审核流程。如果全部同步调用或在各业务代码里手写复杂校验逻辑,会导致接口耗时秒级上升,且任何审核节点变更都会引发所有业务线回归测试。
  • 解决方案:构建 "策略工厂化路由 + 异步审核责任链(Chain of Responsibility)" 安全防线平台。
    1. 所有业务线内容提交后,先存为“待审核”状态,再投递标准 AuditMessage 到 RabbitMQ 队列,让用户侧接口快速返回。
    2. 独立的 smartLive-audit 服务统一消费消息,通过 AuditStrategyFactory 动态路由不同业务策略。
    3. 审核处理链采用 AuditProcessChain 责任链,依据 Spring @Order 将具体校验节点串联。
    4. 一旦违规则中止链路、记录异常并通过站内信通知;审核通过则回调改变源数据状态为“已发布”。

6. 系统入口与安全

6.1 同一个网关同时承接后台和 App,两套登录态容易打架的问题

  • 挑战:项目入口不是单一系统,而是同一个 Gateway 同时承接 /admin/app 两条流量。如果把两端鉴权逻辑混在一起,就容易出现后台 JWT 误判 App 请求、App Token 逻辑误伤后台接口、头信息传递不统一等问题。
  • 解决方案:在 Gateway 前置层先做 "路径前缀分流 + 双端鉴权分治"
    1. AuthFilter 先根据 /admin/app 提取路径前缀,再走不同校验逻辑。
    2. 后台端走 JWT + Redis 登录态校验;App 端走 Redis Token + 用户信息 Map 恢复,并在网关层刷新 TTL。
    3. 两端都统一在 Gateway 里补齐透传头信息,把下游服务看到的用户上下文标准化。
    4. 再叠加 XssFilterValidateCodeFilterBlackListUrlFilter 这些前置过滤器,把输入清洗、验证码校验、黑名单校验收敛到入口层。

7. 这页最值得面试主动讲的 5 个坑

如果面试官只愿意听 1 分钟,这页建议优先讲这 5 个:

  1. 秒杀库存不是只有“超卖”,还有“少卖”和回补闭环问题
  2. RabbitMQ 不是“接入就完事”,真正难的是 Confirm / Return / 幂等 / 死信这一整套组合
  3. ES / Milvus 这种副本系统必须接受短暂不一致,但要保证最终收敛
  4. Feed 流分页在高频写入场景下,不能再用传统 offset 分页
  5. IM 不是单纯连上 WebSocket 就结束,必须把实时推送和持久化拆开治理