
日期2026-07-02共 6 道高频后端/系统设计场景题目录500 万数据做导出库存防止超卖多张表关联优化–这里采用5张接口响应从 2 秒优化到 50ms分布式事务——下单扣库存扣余额生成积分热点数据——1000 万人刷同一个详情页附录幂等实现方式汇总题1500 万数据做导出痛点一次性加载全量数据到内存 →OutOfMemoryError同步阻塞用户请求 → 用户等 3 分钟体验极差核心矛盾数据量 内存容量且同步阻塞用户方案方案原理适用场景游标分页 流式写入WHERE id lastId LIMIT 5000逐批查询逐批写文件流通用方案异步任务 消息队列用户提交任务 → MQ → Worker 处理 → 通知下载用户触发导出MySQLSELECT INTO OUTFILE数据库直接写文件不走应用层DBA 有权限、格式简单数据异构到 ES/Hive大数据引擎处理海量数据高频导出、千万级实现推荐游标分页 流式写入 异步化用户点导出 → 提交任务(秒返回taskId) → MQ → Worker流式导出 → 上传OSS → 通知下载// 1. Controller秒返回PostMapping(/export)publicResultexport(){StringtaskIdexportService.submitTask();returnResult.ok(taskId);// 前端轮询 /task/{taskId}/status}// 2. Worker流式分批内存恒定publicvoiddoExport(StringtaskId){try(GZIPOutputStreamgznewGZIPOutputStream(newFileOutputStream(tmpFile));BufferedWriterwriternewBufferedWriter(newOutputStreamWriter(gz))){longcursor0;while(true){ListOrderbatchorderMapper.nextBatch(cursor,5000);// SQL: SELECT * FROM orders WHERE id #{cursor} LIMIT 5000if(batch.isEmpty())break;for(Ordero:batch){writer.write(o.toCsvLine()\n);}cursorbatch.get(batch.size()-1).getId();taskMapper.updateProgress(taskId,processedCount);}}StringurlossService.upload(tmpFile,exports/taskId.csv.gz);taskMapper.markDone(taskId,url);notifyService.push(taskId,url);}关键细节游标分页 OFFSET 分页WHERE id lastId每次定位是 O(1)LIMIT offset, N翻到后面越来越慢CSV Gzip 压缩500 万行 CSV 约几百 MB → gzip 后几十 MB不用 Excelxlsx 最大 1048576 行根本装不下RR 隔离级别保证导出数据的事务一致性扩展追问导出期间数据变更RR 快照读 文件标注数据截至 xxx下载安全OSS 预签名 URL 24h 过期文件太大分片方案每 100 万行一个文件打包 zip进度推送SSE/WebSocket 实时推送进度百分比题2防止超卖痛点并发下多个请求同时读到库存 0同时执行扣减❌ 错误写法先 SELECT 读库存 → 判断够 → UPDATE 扣减两步不原子核心矛盾读和写之间有时间窗口没有原子性保证方案方案原理并发能力适用量级数据库原子更新UPDATE ... WHERE stock ?利用行锁原子中等~1000/s中低并发Redis Lua 脚本Redis 单线程执行 Lua原子判断扣减高10w QPS秒杀/高并发乐观锁版本号UPDATE ... WHERE version ?冲突重试低冲突重试风暴并发冲突概率低悲观锁SELECT FOR UPDATE显式锁定行串行化最低几乎不用实现方案一数据库原子更新最简洁可靠Transactionalpublicbooleandeduct(LongproductId,intcount){introwsmapper.deduct(productId,count);// SQL: UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}if(rows0)thrownewBizException(库存不足);}原理InnoDB 行锁 WHERE stock count是最后防线一条 SQL 搞定没有 SELECT→UPDATE 的间隙方案二Redis Lua扛高并发localstockredis.call(GET,KEYS[1])localcounttonumber(ARGV[1])ifstockandtonumber(stock)countthenredis.call(DECRBY,KEYS[1],count)return1elsereturn0end方案三完整秒杀漏斗前端层 验证码/答题 → 挡掉脚本 ↓ 网关层 Nginx 限流 → 只放 10w/s ↓ 服务层 Redis Lua 扣库存 → 核心 ↓ 消息层 MQ 异步创建订单 → 削峰 ↓ 数据库 UPDATE WHERE stock ? → 兜底校验选型决策并发量 1000/s → 数据库原子 UPDATE最简单可靠 并发量 1000~1w/s → Redis Lua DB 异步刷盘 并发量 10w/s → Redis Lua 前端限流/验证码 排队机制扩展追问为什么不用SELECT FOR UPDATE→ 悲观锁串行化吞吐低。原子 UPDATE 只锁一瞬间。Redis 挂了怎么办→ 降级到 DB 原子更新 限流保护定时对账修正差异下单不支付→ 预扣库存锁定超时 15 分钟释放。这是真实电商和面试 demo 的本质区别。乐观锁为什么不行→ 高并发 1000 人抢 1 个999 人重试999 次白读。乐观锁适合并发冲突率低的场景。题35 张表关联优化痛点5 张表 JOIN中间结果笛卡尔积放大缺索引 → 全表扫描 → 执行 8 秒甚至超时核心矛盾参与 JOIN 的数据量 × 关联次数 执行成本爆炸优化优先级顺序手段预期收益改动量1SELECT *→ 只取需要的列传输减少 90%改 SQL2加覆盖索引消灭回表JOIN 不再回表DDL SQL3子查询 LIMIT 缩小驱动表驱动表 50w 行 → 20 行改 SQL4Redis 缓存命中时 5ms加缓存层5拆分查询1 接口 N 次简查避免大 JOIN改代码6宽表/快照表2s → 5ms根治改表结构实现第一步EXPLAIN 定位瓶颈EXPLAINSELECT...FROMaJOINbONa.idb.a_idJOINcONb.idc.b_id...关注三个信号typeALL→ 全表扫描必须消灭rows最大 → 瓶颈表Extra: Using filesort / Using temporary→ 排序/分组没走索引第二步加覆盖索引-- ❌ 单列索引走了索引但还要回表ALTERTABLEordersADDINDEXidx_user_id(user_id);-- ✅ 覆盖索引包含查询所需全部列不回表ALTERTABLEordersADDINDEXidx_user_id_cover(user_id,order_no,amount,status,create_time);EXPLAIN Extra 字段出现Using index不是Using index condition才说明覆盖成功。第三步缩小驱动表收益最大-- ✅ 优化后先 LIMIT 再 JOINSELECTo.*,i.*,p.name,u.nickname,t.tag_nameFROM(SELECTid,user_id,create_timeFROMordersWHEREcreate_time2025-01-01ORDERBYcreate_timeDESCLIMIT20)oJOINorder_items iONo.idi.order_idJOINproducts pONi.product_idp.idJOINusers uONo.user_idu.idLEFTJOINuser_tags tONu.idt.user_id;第四步业务拆分查询// 与其一次 5 表 JOIN不如分步拿、应用层组装3 次内ListOrderordersorderMapper.listByTime(2025-01-01,20);// 20 条ListOrderItemitemsitemMapper.listByOrderIds(orderIds);// IN 查询ListProductproductsproductMapper.listByIds(productIds);// IN 查询第五步架构层面CQRS 宽表写入保持规范化查询直接走宽表单表查询Canal → ESbinlog 实时同步查询走 ES排查口诀EXPLAIN → 加索引 → 减数据 → 调顺序 → 改架构扩展追问加索引反而更慢→ ①优化器选错FORCE INDEX对比②索引区分度太低SHOW INDEX看 Cardinality统计信息过期→ Cardinality 不准确导致选错索引ANALYZE TABLE重算覆盖索引反而更慢→ 索引尺寸过大包含 VARCHAR(1000)扫描成本超全表真的必须 JOIN 5 张表吗→ 反问场景OLTP 还是 OLAP报表走 ES/ClickHouse/宽表题4接口响应从 2 秒优化到 50ms痛点订单详情接口查 8 张表、返回 200 个字段P99 2 秒核心矛盾大量 JOIN 冗余字段 无缓存方案顺序手段预期收益改动量1字段裁剪只取需要的 15 列传输减少 90%改 SQL2加覆盖索引消灭 JOIN 回表JOIN 加速DDL SQL3子查询 LIMIT 缩小驱动表驱动表 50万→20 行改 SQL4Redis 缓存命中 5ms加缓存层5拆查询1 接口 N 次简查避免大 JOIN改代码6宽表/快照表2s → 5ms改表结构7读写分离重查询隔离到从库架构调整缓存一致性策略三种策略原理适用场景Cache Aside最常用读缓存 → DB → 回填写先更新 DB → 删缓存大部分业务Read/Write Through缓存层代理读写应用不感知一致性要求高写少Write Behind只写缓存 → 异步批量刷 DB写 QPS 极高可容忍少量丢Cache Aside 细节顺序先更新 DB再删缓存反过来会导致缓存脏数据兜底延迟双删删完等一小段时间再删一次终极方案Canal 监听 binlog → 异步删除缓存扩展追问3 个接口拼装 vs 1 个接口→ 1 个接口内多次简单查询应用层组装而不是让前端发 3 次请求订单详情缓存物流状态频繁变更怎么办→ Cache Aside物流变更时删缓存下次读重建题5分布式事务痛点下单链路订单服务扣库存→ 账户服务扣余额→ 积分服务发积分三个服务三个数据库各自独立事务扣库存成功扣余额失败 → 库存已 commit跨服务无法回滚核心矛盾本地事务管不了别人的数据库方案方案核心思路一致性一句话可靠消息最终一致MQ 本地消息表消息不丢 重试最终一致“慢慢追上最终对的”TCCTry 预留 → Confirm 确认 / Cancel 释放强一致业务层“先占座确认后一起吃”Saga正向操作 反向补偿失败逆序补偿最终一致“错了就倒着退”Seata AT自动生成 undo_log失败自动反向 SQL最终一致接近强“自动化的 Saga”实现方案一可靠消息最终一致性最常用TransactionalpublicvoidcreateOrder(Orderorder){// ① 扣库存introwsstockMapper.deduct(order.getProductId(),order.getCount());if(rows0)thrownewBizException(库存不足);// ② 写本地消息表 —— 和①在同一个事务MsgmsgnewMsg();msg.setTopic(ORDER_CREATED);msg.setBody(JSON.toJSONString(order));msg.setStatus(PENDING);msgMapper.insert(msg);// ③ 创建订单orderMapper.insert(order);}// 事务提交 → 定时任务扫 PENDING 消息发 MQ → 下游消费关键点扣库存 写消息在一个本地事务里。扣库存失败 → 消息也不存在 → 下游不执行。方案二TCCTry-Confirm-CancelTry: 冻结资源真的写DB语义是锁定而不是消费 └─ 库存: stock-5, frozen_stock5 └─ 余额: balance-100, frozen_balance100 全部 Try 成功 → Confirm: 冻结→已消费 └─ frozen_stock-5 └─ frozen_balance-100 任一 Try 失败 → Cancel: 冻结→释放 └─ frozen_stock-5, stock5 └─ frozen_balance-100, balance100TCC 防并发幂等的核心预留明细表-- 预留明细表不是聚合字段CREATETABLEstock_frozen(idBIGINTPRIMARYKEY,txn_idVARCHAR(64),-- 全局事务IDcountINT,statusVARCHAR(20),-- FROZEN / CONFIRMED / CANCELLEDUNIQUEKEYidx_txn_id(txn_id));-- Cancel 幂等WHERE status FROZEN 保证只执行一次UPDATEstock_frozenSETstatusCANCELLEDWHEREtxn_id?ANDstatusFROZEN;-- 第二次执行status 已经是 CANCELLED → WHERE 不满足 → rows0 → 跳过TCC 的硬约束Confirm 不得依赖外部服务不能失败只能重试直到成功Cancel 必须幂等WHERE 状态条件保证必须有超时机制N 秒未 Confirm/Cancel → 自动 Cancel空回滚Cancel 先于 Try 到达 → 预留不存在 → 直接返回方案三Seata ATGlobalTransactionalpublicvoidcreateOrder(Orderorder){orderService.create(order);// RM 自动写 undo_logaccountService.deduct(order);// RM 自动写 undo_logpointService.addPoints(order);}// 异常 → TC 协调 → 逆序执行 undo_log 反向 SQL 回滚选型问自己三个问题 1. 能接受短暂不一致吗库存扣了但积分几秒后才加上 → 能 → 可靠消息最终一致 → 不能 → TCC 或 Seata AT 2. 业务愿意改代码实现 Try/Confirm/Cancel 吗 → 愿意 → TCC性能好 → 不愿意 → Seata AT无侵入 3. 并发量多大 → 高并发 → TCCTry 只预留不加长时间锁 → 一般并发 → 可靠消息 或 Seata AT扩展追问消息丢失→ 本地消息表 定时任务重扫 PENDING 消息补发TCC 的 Try 阶段已经写了 DBConfirm 没来怎么办→ 超时自动 CancelSeata AT 的 undo_log 是谁写的→ RM 代理 DataSource 拦截 SQL执行前快照写入所有方案的共同前提→幂等每个操作必须可重复执行题61000万人刷热点数据痛点微博热搜、秒杀商品详情页1000 万人同时刷数据库连接池打满SQL 排队RT 爆炸一个热点拖垮整个系统雪崩核心矛盾所有请求穿透到同一个数据源单点被打穿方案请求量衰减漏斗1000 万 ──→ CDN / 浏览器缓存 剩余 100 万 ──→ Nginx 本地缓存 剩余 10 万 ──→ Redis 剩余 1 万 ──→ Caffeine 本地缓存 剩余 1000 ──→ 限流 / 熔断 剩余 100 ──→ MySQL只读从库 最终到达层级手段命中后 RTCDN / 浏览器静态化/长缓存 1msNginxproxy_cache/proxy_cache_lock 1ms应用层Caffeine 本地缓存 0.1msRedis集中缓存 逻辑过期 1msDB读写分离 限流兜底5~50ms实现第一层CDN效果最大add_header Cache-Control public, max-age60; # CDN 缓存 60 秒第二层Nginx 本地缓存proxy_cache hot_cache; proxy_cache_valid 200 30s; proxy_cache_lock on; # ★ 并发只放 1 个到后端 proxy_cache_use_stale updating; # 过期先返旧数据后台异步更新第三层Caffeine 本地缓存CacheLong,ProductcacheCaffeine.newBuilder().maximumSize(1000).expireAfterWrite(5,TimeUnit.SECONDS).build();Productpcache.get(productId,id-{// 未命中 → Redis → DBProductredisValredisTemplate.opsForValue().get(product:id);if(redisVal!null)returnredisVal;returnproductMapper.selectById(id);});第四层Redis 逻辑过期防缓存击穿// key 永不过期value 里带 expireTime// 逻辑过期 → 抢锁抢到的异步更新没抢到的返回旧数据if(cacheObj.isLogicallyExpired()){booleanlockedredisTemplate.opsForValue().setIfAbsent(lock:product:id,1,Duration.ofSeconds(3));if(locked){threadPool.submit(()-refreshCache(id));// 异步更新}returncacheObj.getData();// 不管怎样先返回旧数据}缓存三大问题类型现象原因解法击穿单个热点 key 过期大量请求打 DBkey 刚好过期 高并发互斥锁 / 逻辑过期雪崩大量 key 同时过期DB 被打挂TTL 设了相同值TTL 加随机偏移3600 random(0,300)穿透查不存在的数据每次穿透到 DB恶意攻击 / bug布隆过滤器 / 缓存空值60s 过期扩展追问实时库存怎么缓存→ 页面主体走 CDN库存数字 ajax 异步单独请求 Redis多级缓存一致性→ 热点场景不强求实时一致MQ 通知主动删除各级缓存写热点如大量评论涌入同一条微博→ 异步 MQ 分片写入comment_0 ~ comment_N附录幂等实现方式汇总五种方式方式核心适用场景唯一索引/唯一约束数据库保证重复插入报 DuplicateKeyException创建类操作状态机 WHERE 条件UPDATE ... WHERE status OLD_STATUS原子状态转换状态流转、支付回调Token / 唯一请求号客户端生成 requestId服务端先查是否处理过最通用方案乐观锁版本号UPDATE ... WHERE version ?冲突重试编辑类、冲突概率低Redis 分布式锁SETNX锁一个操作窗口无法建唯一索引时三个易忽略细节幂等不只是不重复执行还要返回相同结果幂等 key 的粒度要选对太粗一天只能操作一次太细防不住重试过期清理幂等记录定时清理防止表无限膨胀在场景题中的映射题2 防超卖 状态机 WHEREUPDATE WHERE stock ? 唯一请求号 题5 分布式事务 状态机 WHERETCC Cancel/Confirm 唯一索引txn_id 题4 接口优化 唯一索引缓存重建的并发控制知识体系速查数据库优化 ├── 索引EXPLAIN → type/rows/Extra → 覆盖索引 单列索引 ├── JOIN小驱动表 → 子查询 LIMIT → 拆查询 → 宽表 ├── 大数据量游标分页 OFFSET → 流式写入 → 异步化 └── 统计信息Cardinality → ANALYZE TABLE → innodb_stats_persistent_sample_pages 并发控制 ├── 防超卖原子 UPDATE Redis Lua 乐观锁 悲观锁 ├── 幂等唯一索引 / 状态机 WHERE / Token / 版本号 / 分布式锁 └── 缓存击穿互斥锁 / 逻辑过期 分布式 ├── 分布式事务可靠消息 TCC Seata AT Saga ├── TCC 三异常空回滚 / 幂等 / 悬挂 └── 热点防护CDN → Nginx → Caffeine → Redis → 限流 → DB 缓存策略 ├── Cache Aside更新 DB → 删缓存最常用 ├── Read/Write Through缓存层代理 ├── Write Behind写缓存 → 异步刷 DB └── 三大问题击穿逻辑过期/ 雪崩TTL 随机/ 穿透布隆/空值