1. 秒杀 / 高并发下单
核心四板斧:限流 → 缓存扣减 → 异步落库 → 最终一致。
1. 前端/网关限流:按钮置灰防重复、Nginx/网关限流、答题验证码削峰。
2. 库存预热到 Redis:下单先在 Redis 用原子操作扣减,扛住高并发,DB 不直接顶流量。
3. 原子扣减防超卖:DECR 或 Lua 脚本(判断+扣减一个原子操作),扣成功才算抢到。
4. 异步下单:抢到资格丢 MQ,后端消费者慢慢写 DB 真正生成订单(削峰填谷)。
5. 兜底:Redis 扣到 0 直接拒绝;DB 层加乐观锁/stock>0 兜底防极端穿透。
❓ 常见追问 · 怎么避免超卖?→ 三道防线:① Redis 原子扣减是第一道(Lua 保证判断+扣减不被打断)② MQ 串行化消费下单 ③ DB 更新用 UPDATE ... SET stock=stock-1 WHERE id=? AND stock>0,影响行数=0 就是没库存了(乐观锁思想)。
❓ 常见追问 · 不用 Redis 只用 MySQL 怎么保证安全?→ 靠 DB 本身:① UPDATE stock=stock-1 WHERE id=? AND stock>=1,用行锁 + 影响行数判断,扣减和判断在一条 SQL 里原子完成 ② 或悲观锁 SELECT ... FOR UPDATE 锁住库存行。代价是并发上限受 DB 行锁性能限制,所以高并发才引 Redis。
2. 缓存与数据库一致性
主流方案 Cache Aside(旁路缓存)+ 延迟双删:
- 读:先读缓存,miss 再读 DB 并回写缓存。
- 写:先更新 DB,再删除缓存(不是更新缓存)。
- 延迟双删:更新 DB 前删一次缓存 → 更新 DB → 延迟几百 ms 再删一次(清掉这期间被旧数据回填的脏缓存)。
💡 深入理解 · 为什么删缓存而不更新缓存? ① 更新缓存可能算一堆最后没人读,浪费;② 并发下"更新缓存"顺序难保证易脏。删除是惰性——下次读再回填最新值。为什么先更DB后删缓存? 先删缓存再更DB,若更DB期间有读请求会把旧值回填缓存,产生长期脏数据。
❓ 常见追问 · 删缓存失败怎么办? → 引入重试(删除失败丢 MQ 重试)或订阅 binlog(Canal)异步删,保证最终一致。强一致场景才上分布式锁串行化(代价高)。
3. 分布式锁
Redis 分布式锁:SET key value NX EX 30——NX 保证只有一个客户端拿到锁,EX 设过期防死锁,value 存唯一标识(如 UUID)。释放锁用 Lua 脚本(判断是不是自己的锁 + 删除,两步原子),防误删别人的锁。
进阶问题:① 锁过期但业务没执行完 → 看门狗(Redisson watchdog)自动续期 ② 主从切换锁丢失 → RedLock(多数节点加锁成功才算)。
❓ 常见追问 · 为什么释放锁要用 Lua? → "判断锁是自己的"和"删除"是两步,中间锁可能到期被别人拿走,非原子会误删。Lua 在 Redis 单线程里原子执行。
4. 消息队列可靠性
消息不丢三个环节都要保:
- 生产端:开启 confirm 确认机制,发送失败重发。
- Broker:消息持久化到磁盘 + 队列/Exchange 持久化 + 副本(Kafka 多副本 ISR)。
- 消费端:手动 ACK——处理成功才确认,处理中崩了消息重回队列(不丢)。
❓ 常见追问 · 进程崩溃/K8s 重启后任务会不会丢?→ 不会,只要消息持久化 + 手动 ACK。崩溃时未 ACK 的消息重新入队,换个 worker 继续消费。前提是任务状态外化到 DB/Redis(哪些做完了),别只在内存里。
❓ 常见追问 · 断点续跑怎么实现?→ 把长任务拆成可记录的子步骤,每完成一步把进度写 DB/Redis(如"已处理到第 500 条")。重启后读进度从断点继续,而不是从头。这和 Harness 状态外化是同一思路。
❓ 常见追问 · 任务重试导致重复执行/重复调大模型,怎么降本?→ 幂等 + 结果缓存:① 每个任务给唯一 idempotency key,执行前查是否已处理过(Redis/DB 去重表)② 大模型推理结果按输入 hash 缓存,重试直接命中缓存不再调模型 ③ 语义缓存对相似输入复用。
5. 幂等性
同一请求执行一次和多次结果相同。实现方式:
- 唯一约束:DB 唯一索引(订单号),重复插入直接失败。
- Token 机制:请求前先领一次性 token,处理后作废,重复请求 token 失效被拒。
- 状态机:订单"待支付→已支付",只允许合法流转,重复支付回调不重复加积分。
- 去重表/Redis:记录已处理的请求 ID,处理前先查。
6. 支付流程
下单→调支付→异步回调→更新状态→发货:
1. 后端生成订单(状态"待支付")→ 调微信统一下单拿 prepay_id → 返回前端拉起支付。
2. 用户支付 → 微信异步回调通知后端。
3. 回调里先验签(防伪造)→ 校验金额/订单号 → 幂等判断(这单处理过没)→ 更新状态"已支付" → 发放积分/虚拟商品。
4. 返回微信成功响应,否则微信会重复回调。
❓ 常见追问 · 回调验签流程? → 用商户密钥对回调参数按规则重新签名,和微信传来的 sign 比对,一致才可信。防止别人伪造"支付成功"回调白嫖。
❓ 常见追问 · 支付为什么用异步队列? → 发积分/发货/通知等后续动作耗时且可能失败,回调里只做"标记已支付"这一核心事,其余丢 MQ 异步做,保证回调快速返回成功(否则微信超时重试)。
❓ 常见追问 · 队列挂了怎么保证支付状态一致? → 支付状态已落 DB(核心事务同步完成),队列只管后续动作;队列挂了靠对账补偿——定时任务扫"已支付但未发货"的单重新投递,最终一致。
7. 数据库事务与场景
事务 = ACID(原子性/一致性/隔离性/持久化),一组操作要么全成功要么全回滚。隔离级别与 MVCC 见 → 数据库与并发。
❓ 常见追问 · 举个项目里必须用事务的场景? → ① 支付成功后"扣款 + 加积分 + 生成订单"必须同成同败,中途失败要回滚防止扣了钱没积分 ② 数仓/业务里"扣库存 + 建订单"两表操作 ③ 账户转账 A 减 B 加。
8. 服务治理:熔断 / 限流 / 降级
高并发四件套,也是大模型服务健壮性的核心:
- 限流:令牌桶/漏桶,Redis + Lua 或网关层,保护后端不被打垮。
- 熔断:下游(大模型 API/第三方)连续失败到阈值就跳闸,一段时间内直接快速失败不再打请求,给下游恢复时间(Hystrix/Sentinel 思想)。半开状态试探性放少量请求探活。
- 降级:非核心功能故障时返回兜底(缓存旧值/默认回复/备用模型),保主链路可用。
- 异步/队列削峰:突发流量丢队列缓冲,后端匀速消费。
💡 深入理解 · 落到大模型场景:第三方模型超时 → 熔断切备用模型(豆包→千问)→ 非实时任务入队重试 → 高频结果缓存兜底。详见 → API 健壮性与并发。
9. 分库分表 / 读写分离
- 读写分离:主库写、从库读,主从复制同步。读多写少场景分摊读压力。注意主从延迟(刚写完读从库可能读不到,强一致读走主库)。
- 分库分表:单表数据量太大(千万/亿级)拆分。垂直拆(按业务拆库/按字段拆表)、水平拆(按 user_id 取模/按时间范围/一致性哈希分片)。带来的问题:跨分片 join、分布式事务、全局唯一 ID(雪花算法 Snowflake)。
❓ 常见追问 · MySQL 大表百万级慢插入怎么优化?→ ① 批量插入(Bulk Insert 代替逐条)② 插入时先禁用/延迟索引更新,导完再建索引 ③ 减少索引数量(每个索引都要维护)④ 分库分表/分区表 ⑤ 顺序主键(自增/雪花)避免页分裂。
❓ 常见追问 · 海豚调度同步数据 MySQL IO 爆红怎么排查?→ ① 看慢查询日志定位是查慢还是写慢 ② 三连表 join 无索引→加索引/拆分批查 ③ 插入端 IO 高→批量提交代替单条、调大事务批次、临时关唯一检查 ④ 错峰/限流同步频率 ⑤ 查写分离到不同实例。
10. 从 0 到 1 架构设计
AI 业务系统典型分层(被问"你的 AI 系统分几层"时的标准答法):
- 接入层:网关(鉴权/限流/路由)、SSE 长连接。
- 应用层:意图路由、各业务 Agent(对话/RAG/判题)、编排(LangGraph)。
- 能力层:大模型调用封装、RAG 检索、Function Calling / MCP 工具、记忆管理。
- 数据层:MySQL(业务/短期记忆)、向量库(长期记忆/知识)、Redis(缓存/锁)、对象存储(文件)。
- 支撑层:异步任务(Celery/MQ)、监控日志(Graylog/Trace)、成本 token 统计。
❓ 常见追问 · 从 0 到 1 怎么设计架构? → 先理业务边界和核心链路 → 定技术选型(模型/向量库/框架)→ 分层拆模块(高内聚低耦合)→ 定数据流转和接口契约 → 非功能性(并发/容错/成本/可观测)→ 先跑通 MVP 再迭代优化。
11. 高频追问速答
❓ 常见追问 · 单例模式优点?→ 全局唯一实例节省资源、统一访问点、避免重复创建开销(如 DB 连接池、配置对象)。
❓ 常见追问 · 怎么给上百个接口统一加日志/监控?→ 中间件/AOP 切面统一拦截,不侵入业务代码。FastAPI 用 middleware 或依赖注入,Nest 用拦截器/中间件,记录请求/响应/耗时/异常。
❓ 常见追问 · SSE 断线续传怎么做?→ 服务端给每个 chunk 带序号(id),客户端断开重连时通过 Last-Event-ID 头带上已收到的位置,服务端从断点续发。前提是服务端缓存了这次生成的内容(Redis 存流式中间结果)。
评论