写了9年代码,我靠这8道架构题拿下了P7 offer
写了9年代码我靠这8道架构题拿下了P7 offer面试官问“你的系统日订单量从10万涨到1000万架构怎么演进” 这道题我准备了3个月最终帮我拿下了P7。大家好我是卷毛。去年年底我面试了几家大厂最终拿到了P7的offer。回头看整个面试过程架构设计题是最拉分的环节。我把面试中被问到的8道核心架构题整理出来每道题都附上我的回答思路和面试官追问的方向。这篇文章建议收藏面试前拿出来复习。题目一高并发下单如何防止超卖场景描述秒杀场景1000件商品10万人同时抢购如何保证不超卖我的回答三层防护前端限流 → 分布式锁 → 数据库兜底// 第一层Redis预扣库存原子操作publicbooleandeductStock(StringitemId,StringuserId){Stringkeystock:itemId;// Lua脚本保证原子性StringluaScript if redis.call(get, KEYS[1]) then if tonumber(redis.call(get, KEYS[1])) 0 then redis.call(decr, KEYS[1]) return 1 end end return 0 ;LongresultredisTemplate.execute(newDefaultRedisScript(luaScript,Long.class),Collections.singletonList(key));if(resultnull||result0){returnfalse;// 库存不足}// 第二层发送MQ异步创建订单orderMessageProducer.send(newOrderMessage(itemId,userId));returntrue;}// 第三层数据库乐观锁兜底TransactionalpublicvoidcreateOrder(StringitemId,StringuserId){// 乐观锁WHERE stock 0intaffectedjdbcTemplate.update(UPDATE item SET stock stock - 1 WHERE id ? AND stock 0,itemId);if(affected0){// Redis预扣了但DB没有了补偿redisTemplate.opsForValue().increment(stock:itemId);thrownewSoldOutException(商品已售罄);}orderDao.insert(newOrder(itemId,userId));}面试官追问QRedis挂了怎么办ARedis Cluster高可用 本地缓存降级 数据库直接扛限流降级Q如果用户扣了库存但不下单怎么办AMQ设置延迟消息15分钟未支付自动回滚库存Q分布式锁用Redis还是ZooKeeperA秒杀场景用Redis性能优先金融场景用ZooKeeper一致性优先题目二如何设计一个分布式锁场景描述系统有多个节点某个定时任务只能在一个节点执行怎么实现我的回答// 方案一Redis分布式锁RedissonpublicvoidexecuteTask(){RLocklockredissonClient.getLock(scheduled:task:lock);try{// 尝试加锁等待0秒非阻塞租约30秒if(lock.tryLock(0,30,TimeUnit.SECONDS)){doTask();}else{log.info(其他节点正在执行跳过);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}// 方案二数据库唯一索引最简单可靠TransactionalpublicvoidexecuteTask(){try{// 唯一索引(task_name, execute_date)taskLockDao.insert(newTaskLock(scheduled_task,LocalDate.now()));doTask();}catch(DuplicateKeyExceptione){log.info(其他节点已获取锁);}}三种方案对比方案性能可靠性复杂度适用场景Redis(Redisson)高中需看门狗中高并发、允许偶尔失败ZooKeeper中高高金融、强一致性要求数据库唯一索引低高低低频任务、最可靠面试官追问QRedis锁的看门狗机制是什么ARedisson的lock会启动一个后台线程每隔1/3租约时间续期防止业务没执行完锁就过期了QRedLock算法了解吗A向N个独立Redis节点同时加锁超过半数成功才算加锁成功。但Martin Kleppmann指出有安全问题生产中我更倾向于用Redis Cluster Redisson题目三百万级消息堆积怎么处理场景描述消费者挂了一段时间MQ堆积了百万条消息重启后如何快速消费我的回答// 核心思路临时扩容消费者 批量消费 跳过堆积// 1. 临时增加消费者实例K8s快速扩容// 从3个Pod扩到20个Pod// 2. 批量消费模式KafkaListener(topicsorder-events,groupIdorder-consumer,containerFactorybatchFactory)publicvoidbatchConsume(ListConsumerRecordString,Stringrecords){// 批量处理减少网络往返ListOrderordersrecords.stream().map(r-JSON.parseObject(r.value(),Order.class)).toList();orderService.batchInsert(orders);}// 3. 配置批量消费BeanpublicConcurrentKafkaListenerContainerFactoryString,StringbatchFactory(ConsumerFactoryString,StringconsumerFactory){ConcurrentKafkaListenerContainerFactoryString,StringfactorynewConcurrentKafkaListenerContainerFactory();factory.setConsumerFactory(consumerFactory);factory.setBatchListener(true);factory.getContainerProperties().setPollTimeout(3000);returnfactory;}面试官追问Q如果消息有时效性过期消息怎么处理A消费者端判断时间戳过期消息直接丢弃 记录日志Q如何避免堆积再次发生A监控消费延迟、设置告警阈值、消费者优雅下线处理完当前消息再退出题目四如何设计接口幂等性场景描述支付接口可能被重复调用网络重试、用户双击如何保证幂等我的回答// 核心方案唯一请求ID 状态机ServicepublicclassPaymentService{AutowiredprivateRedisTemplateString,StringredisTemplate;AutowiredprivatePaymentDaopaymentDao;TransactionalpublicPaymentResultpay(PaymentRequestrequest){StringidempotentKeypay:request.getRequestId();// Step 1: Redis标记处理中BooleanfirstTimeredisTemplate.opsForValue().setIfAbsent(idempotentKey,PROCESSING,30,TimeUnit.MINUTES);if(Boolean.FALSE.equals(firstTime)){// 不是第一次请求查之前的结果StringstatusredisTemplate.opsForValue().get(idempotentKey);if(SUCCESS.equals(status)){// 返回之前的结果returnpaymentDao.findByRequestId(request.getRequestId()).toResult();}if(PROCESSING.equals(status)){thrownewConcurrentRequestException(请求处理中请勿重复提交);}}// Step 2: 执行业务try{PaymentResultresultdoPayment(request);// Step 3: 更新状态为成功redisTemplate.opsForValue().set(idempotentKey,SUCCESS,30,TimeUnit.MINUTES);returnresult;}catch(Exceptione){// 失败了删除标记允许重试redisTemplate.delete(idempotentKey);throwe;}}}4种幂等方案方案实现方式适用场景唯一索引DB唯一约束插入操作乐观锁version字段更新更新操作状态机状态流转校验有状态的业务Token机制预获取token使用后失效表单提交题目五系统从单机到千万级并发架构怎么演进我的回答阶段1单体应用日活1万 → Spring Boot单体 MySQL单机 Redis单机 阶段2垂直拆分日活10万 → 按业务拆分服务用户、订单、商品 → 数据库读写分离 阶段3微服务化日活100万 → Spring Cloud微服务 → 分库分表订单表按用户ID分16库×64表 → 消息队列异步解耦 → CDN 分布式缓存 阶段4中台化日活1000万 → 业务中台 数据中台 → 异地多活单元化部署 → 全链路压测 监控告警 每个阶段的核心原则 → 不要过度设计当前阶段能撑住就行 → 但要预留扩展点接口抽象、数据分片键设计 → 架构演进是连续的不是一步到位的面试官追问Q分库分表后跨库join怎么做A1应用层组装 2宽表冗余 3ElasticSearch做查询侧 4避免跨库join的设计Q分库分表键怎么选A选择查询频率最高的维度。订单系统选user_id因为90%的查询是查某用户的订单题目六如何保证分布式事务的最终一致性我的回答// 核心方案本地消息表 MQ可靠投递// Step 1: 业务操作 记录本地消息同一个事务TransactionalpublicvoidcreateOrder(Orderorder){orderDao.insert(order);// 业务操作// 本地消息表和业务表在同一个DBmessageDao.insert(newLocalMessage(UUID.randomUUID().toString(),order-created,JSON.toJSONString(order),PENDING));}// Step 2: 定时扫描本地消息表投递到MQScheduled(fixedDelay5000)publicvoidsendPendingMessages(){ListLocalMessagemessagesmessageDao.findPending(100);for(LocalMessagemsg:messages){try{kafkaTemplate.send(order-events,msg.getContent());messageDao.markSent(msg.getId());}catch(Exceptione){// 下次继续重试log.error(发送失败,e);}}}// Step 3: 消费者幂等消费 回调确认KafkaListener(topicsorder-events)publicvoidconsume(Stringmessage){OrderorderJSON.parseObject(message,Order.class);// 幂等检查if(inventoryService.isProcessed(order.getId())){return;}inventoryService.deduct(order);}面试官追问Q为什么不直接用SeataASeata AT模式有全局锁性能开销大。最终一致性方案吞吐量高10倍以上适合大多数互联网场景Q本地消息表数据越来越大怎么办A1已发送的消息定期归档 2分表存储 3使用RocketMQ事务消息替代本地消息表题目七如何设计一个限流方案我的回答// 四种限流算法各有适用场景// 1. 令牌桶最常用—— Guava RateLimiterprivatefinalRateLimiterlimiterRateLimiter.create(100);// 100 QPSpublicResponsehandle(Requestrequest){if(!limiter.tryAcquire(1,500,TimeUnit.MILLISECONDS)){returnResponse.tooManyRequests(系统繁忙请稍后重试);}returndoHandle(request);}// 2. 滑动窗口 —— Redis Lua分布式限流publicbooleanisAllowed(Stringkey,intmaxRequests,intwindowSeconds){StringluaScript local current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[2]) end if tonumber(current) tonumber(ARGV[1]) then return 0 end return 1 ;LongresultredisTemplate.execute(newDefaultRedisScript(luaScript,Long.class),Collections.singletonList(rate:key),String.valueOf(maxRequests),String.valueOf(windowSeconds));returnresult!nullresult1;}// 3. Sentinel阿里开源—— 支持多种限流策略SentinelResource(valuequeryOrder,blockHandlerqueryOrderBlocked)publicOrderqueryOrder(LongorderId){returnorderService.findById(orderId);}publicOrderqueryOrderBlocked(LongorderId,BlockExceptionex){returnOrder.degraded();// 降级返回}题目八线上接口突然变慢怎么排查我的回答排查SOP1分钟内看监控 → CPU/内存/GC是否异常 → 是单个接口慢还是所有接口慢 → DB慢查询日志有没有新增 5分钟内定位瓶颈 → 链路追踪SkyWalking/Zipkin看耗时分布 → 是DB慢Redis慢外部API慢GC停顿 15分钟内紧急处理 → 单接口慢限流降级 → DB慢查执行计划是否索引失效 → GC频繁dump内存分析是否有内存泄漏 事后复盘 → 为什么没有提前发现 → 监控告警是否覆盖了这个场景 → 如何避免再次发生// 常见性能问题代码示例// 问题1N1查询// ❌ListOrderordersorderDao.findAll();for(Orderorder:orders){UseruseruserDao.findById(order.getUserId());// N1次查询}// ✅ListOrderordersorderDao.findAll();SetLonguserIdsorders.stream().map(Order::getUserId).collect(toSet());MapLong,UseruserMapuserDao.findByIdIn(userIds).stream().collect(toMap(User::getId,Function.identity()));// 问题2大对象序列化// ❌ 返回全部字段returnResponseEntity.ok(order);// ✅ 只返回需要的字段returnResponseEntity.ok(OrderVO.from(order));// 问题3锁粒度过大// ❌publicsynchronizedvoidupdateUser(Useruser){// 整个方法加锁}// ✅publicvoidupdateUser(Useruser){synchronized((user:user.getId()).intern()){// 只锁当前用户}}面试心得架构题没有标准答案但有回答框架先说方案 → 再说取舍 → 最后说追问用真实项目经历背书每个方案都结合自己的项目说我们在xxx场景下是这样做的主动说出方案的缺点面试官最怕听到完美方案说出trade-off反而加分准备3个深度案例一个高并发、一个分布式、一个性能优化覆盖80%的架构题写在最后架构面试考的不是背八股文而是解决实际问题的思维能力。9年开发经验告诉我真正好的架构不是设计出来的是在解决实际问题中演进出来的。准备面试的过程也是梳理自己知识体系的过程。与其焦虑不如把每个问题想透。我是卷毛9年Java开发专注技术成长和面试经验分享。这8道题如果对你有帮助收藏关注面试前翻出来复习。后续会持续分享架构设计实战和大厂面试系列。《卷毛的技术笔记》—— 一起卷出技术力。你面试遇到过什么架构题评论区交流