第十三篇:系统设计——把前面学的所有技术串起来 别再背架构图了你只需要知道每个环节为什么选这个技术以及挂了怎么办写在前面系统设计是面试的终极boss。前面所有章节Java基础、MySQL、Redis、MQ、微服务……都是零件系统设计就是把这些零件组装成一台能跑的机器。面试官考系统设计本质上不考你知不知道这个技术而是考你在多个技术之间怎么权衡、怎么选型、怎么兜底。一道典型的系统设计面试题面试官会追问三件事“为什么选这个方案”→ 考你的选型能力。“这里挂了怎么办”→ 考你的容错意识。“QPS翻10倍怎么办”→ 考你的扩展性思考。这篇文章选了四个经典面试题秒杀系统、短链系统、排行榜、IM系统。你把这几道的权衡逻辑搞明白遇到新题也能用同样的思路去拆解。一、秒杀系统最经典的系统设计题场景双11秒杀100万用户抢1件商品不能超卖不能把数据库打崩。1.1 整体架构从上到下用户浏览器/App ↓ CDN静态资源 ↓ Nginx 网关限流 ↓ 商品详情页Redis缓存 ↓ 秒杀核心服务扣Redis库存 发送MQ ↓ 订单服务消费MQ异步落库 ↓ 数据库最终扣减1.2 逐层拆解① 前端层防刷按钮防抖点击后置灰防止用户疯狂点。验证码防止机器刷人机验证。静态资源走CDN减轻服务器压力。② 网关层限流Sentinel限流每秒只放行N个请求比如1000超过的直接返回秒杀已结束。限流算法令牌桶允许少量突发或漏桶完全平滑。③ 核心逻辑Redis预扣库存 消息队列异步这是秒杀系统的灵魂。不要把数据库当成库存的实时扣减点数据库太慢了。// 伪代码PostMapping(/seckill)publicResultseckill(LongproductId,LonguserId){// 1. Redis里扣库存Lua脚本保证原子性StringluaScriptlocal stock redis.call(get, KEYS[1]) if stock and tonumber(stock) 0 then redis.call(decr, KEYS[1]) return 1 else return 0 end;LongresultredisTemplate.execute(luaScript,Collections.singletonList(stock:productId));if(result0){returnResult.fail(库存不足);}// 2. 扣库存成功发送MQ消息异步创建订单mqProducer.send(newOrderMessage(productId,userId));returnResult.success(抢购成功正在生成订单);}为什么用Lua脚本Redis的getdecr如果是两条命令有并发问题A读取stock1B也读取stock1A减成0B也减成0超卖。Lua脚本在Redis里是原子执行的不会被其他命令打断。为什么要用MQ异步创建订单因为创建订单要写数据库磁盘IO如果同步等待订单写入完成再返回响应时间会变长而且数据库可能扛不住瞬间的写入压力。MQ削峰让数据库按自己的节奏慢慢消费。④ 数据库最终扣减兜底Redis扣库存成功了但订单创建失败了怎么办比如用户下单时余额不足、风控拦截等-- 订单表插入记录时扣减数据库库存乐观锁UPDATEproductSETstockstock-1,versionversion1WHEREid#{productId} AND stock 0 AND version #{version};如果UPDATE影响行数为0说明库存被其他事务改了超卖风险这个订单就作废同时要把Redis的库存加回去补偿。注意Redis扣库存和数据库扣库存永远不可能保证强一致性秒杀系统的设计哲学就是允许极少数情况下的数据补偿人工或自动对账修复换取极高的并发能力。1.3 “挂了怎么办”面试追问必考QRedis挂了怎么办ARedis挂了秒杀直接不可用这是可接受的比起让数据库挂了强。但要有兜底方案Redis不可用时降级到数据库扣库存性能会差很多但至少能保证核心流程跑通。Sentinel做自动故障切换。QMQ挂了怎么办A秒杀核心逻辑必须先扣Redis库存再发MQ。如果MQ挂了可以重试N次比如3次还是失败则记录到本地日志或数据库的异常表人工补偿。RocketMQ本身也是高可用集群单节点挂了不影响。Q用户重复点击怎么办A用用户ID 商品ID做幂等。Redis里存一份已购买标志SETNX userId:productId 1 EX 3600如果SETNX返回0说明已经买过了直接返回已抢购。1.4 面试话术“我们用了Redis预扣库存 RocketMQ异步下单的方案。Redis用Lua脚本保证扣库存的原子性不超卖。MQ削峰填谷让订单库按自己的节奏消费不会被打崩。最终一致性靠数据库乐观锁兜底。如果Redis挂了降级到数据库扣库存虽然慢但能用MQ挂了则重试日志记录人工补偿。”二、短链系统场景把长URL转成短URLt.cn/abc123访问短链时重定向到长链。2.1 核心设计① 发号器生成唯一ID短链的abc123怎么来的不是直接对长URL做hash而是用发号器一个自增ID生成器把ID转成62进制0-9a-zA-Z得到短码。发号器Snowflake/数据库自增→ ID: 10086 → 62进制: 2Fk → 短链: t.cn/2Fk为什么不用hash两个不同长URL可能hash冲突碰撞发号器保证全局唯一。② 存储MySQL RedisMySQL存短码, 长URL, 创建时间, 过期时间。Redis缓存热点短链短码 → 长URL设置过期时间比如24小时。③ 访问流程用户访问 t.cn/2Fk ↓ 网关路由到短链服务 ↓ 先查Redis缓存2Fk → 长URL ↓ 缓存没有 → 查MySQL ↓ 拿到长URL → 返回302重定向 ↓ 同时写回Redis缓存异步或同步2.2 细节优化① 发号器的瓶颈单机发号器可能成为性能瓶颈。用Snowflake雪花算法分布式发号器或美团Leaf号段模式每次取一批号段用完再取减少DB访问。② 缓存策略缓存穿透不存在的短码查Redis空缓存空值设置短过期时间。热点短链极热门的短链比如双11链接可以提前预热到Redis。③ 302重定向 vs 301永久重定向302临时重定向每次重定向短链服务可以统计访问量PV/UV。301永久重定向浏览器会缓存短链服务收不到后续请求无法统计。一般用302可以用CDN统计替代。三、排行榜Redis ZSet场景游戏排行榜、热门文章TOP100。3.1 核心实现Redis的ZSet有序集合天然适合排行榜。// 更新分数redisTemplate.opsForZSet().incrementScore(rank:2024-01,userId,score);// 获取TOP 10SetZSetOperations.TypedTupleObjecttop10redisTemplate.opsForZSet().reverseRangeWithScores(rank:2024-01,0,9);底层ZSet用跳表SkipList 哈希表实现添加/更新是O(logN)取TOP N是O(logN N)。3.2 分页查询// 第2页每页10条SetZSetOperations.TypedTupleObjectpage2redisTemplate.opsForZSet().reverseRangeWithScores(rank:2024-01,10,19);注意ZSet的rank是实时排序的Redis的ZSet可以轻松支撑百万级元素千万级开始性能下降明显。超过千万的排行榜考虑分层存储热数据存Redis冷数据存MySQL或HBase查询时合并。3.3 大促场景优化分桶大促期间多个商品的排行榜可能同时更新单个ZSet过大影响性能。解决方案按商品维度分桶。key: rank:product:1001 // 商品1001的排行榜 key: rank:product:1002 // 商品1002的排行榜每个商品的排行榜独立互不影响。查询时只查对应桶。四、IM系统即时通讯系统场景微信、钉钉、私信功能。用户A给用户B发消息。4.1 核心设计① 存储消息落库消息存MySQL或TiDB表结构CREATETABLEmessage(idBIGINTPRIMARYKEYAUTO_INCREMENT,from_userVARCHAR(32),to_userVARCHAR(32),contentTEXT,create_timeDATETIME,statusTINYINTDEFAULT0-- 0未读 1已读);② 消息投递推 vs 拉推Push消息到达服务器后服务器主动推送给接收方WebSocket/长连接。实时性好但连接数多时服务器压力大。拉Pull接收方定时轮询服务器有新消息吗服务器压力小但延迟高轮询间隔。通常方案在线用户用推WebSocket离线用户用拉下次上线时拉取未读消息。③ 消息ID生成IM消息要求全局有序聊天记录按时间排序。用雪花算法趋势递增或数据库自增单点瓶颈。④ 未读消息数SELECTCOUNT(*)FROMmessageWHEREto_userBANDstatus0;如果用户量巨大这个查询会变慢。优化Redis存每个用户的未读计数收到消息时INCR已读时DECR。读的时候直接GETO(1)。4.2 离线消息拉取分页-- 拉取最近50条未读消息SELECT*FROMmessageWHEREto_userBANDstatus0ORDERBYcreate_timeDESCLIMIT50;问题如果B离线一天积累了10000条未读一次性拉取太慢。让客户端分批拉取比如每次50条带上时间戳翻页。五、系统设计题的通用框架遇到一个没见过的系统设计题按这个模板答基本不会偏5.1 四步法① 明确需求先问清楚用户量多大日活、月活、峰值QPS数据量多大每天产生多少数据存多久延迟要求实时还是异步一致性要求强一致还是最终一致② 设计数据模型存什么核心表结构有哪些索引怎么建分库分表策略是什么③ 设计核心流程怎么查/怎么改用户请求来了经过哪些服务每个服务用什么技术缓存怎么用④ 考虑挂掉怎么办兜底缓存挂了怎么办数据库挂了怎么办MQ挂了怎么办第三方接口挂了怎么办六、面试官真正想听什么场景“设计一个秒杀系统”普通回答60分“用Redis扣库存MQ异步下单防止超卖。”加分回答90分“我们按四个层面来设计前端防刷按钮置灰验证码、网关限流Sentinel按秒级QPS限流、核心逻辑Redis Lua脚本原子扣库存 RocketMQ事务消息保证最终一致性、数据库兜底乐观锁版本号防止极端情况下的超卖。挂了怎么办Redis挂了降级到数据库扣库存MQ挂了重试落库人工补偿Sentinel限流防止秒杀服务本身被冲垮。扩展性如果未来QPS翻10倍可以把库存拆成多个分片比如按商品ID哈希分片每个分片独立扣库存提高并行度。”场景“短链系统设计”加分回答“发号器我们用了Snowflake支持分布式部署每秒能生成百万级ID。存储用了MySQL持久化 Redis热点缓存过期策略是LRU防止内存撑爆。访问统计用302临时重定向每次重定向都记录访问日志到Kafka再由Flume流式处理统计UV/PV。挂了怎么办Redis挂了走MySQL虽然慢但能用MySQL挂了走只读从库主从同步延迟在毫秒级。”小结面试题核心组件面试考点秒杀系统Redis原子扣库存 MQ削峰 DB乐观锁不超卖、挂了怎么办短链系统发号器 缓存 302重定向全局唯一ID、缓存策略排行榜Redis ZSet 分桶分页、冷热分离IM系统WebSocket 推拉结合 未读计数实时性、离线消息记住一句话系统设计没有标准答案只有权衡。面试官想听的是你为什么这么选和你考虑了哪些异常情况。下一篇我们讲AI面试——2026年的面试AI已经不是一个加分项而是默认项了。怎么把AI能力和你的技术栈结合怎么在面试里主动聊AI下期见。