# 场景设计题 ## 说明 场景设计题是考察系统设计能力和问题解决能力的常见题型。本文档针对你的简历背景,设计相关的场景题。 --- ## 1. 电商促销系统设计 ### 题目 **面试官问**: "假设你要设计一个电商秒杀系统,支持百万用户同时抢购,你会如何设计?" --- ### 参考回答 **【需求分析】** ``` 核心需求: 1. 高并发:百万用户同时抢购 2. 防超卖:库存不能卖超 3. 高可用:服务不能挂 4. 公平性:先到先得 5. 用户体验:快速响应 非功能需求: - QPS: 100万+ - 延迟: <100ms - 可用性: 99.99% - 数据一致性: 强一致 ``` --- **【架构设计】** ``` ┌─────────────────────────────────────────────┐ │ 客户端层 │ │ 移动App、Web、H5 │ └──────────────┬──────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ CDN层 │ │ - 静态资源(图片、CSS、JS) │ │ - 动态接口(边缘计算) │ └──────────────┬──────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ API网关层 │ │ - 限流:100万QPS │ │ - 熔断:保护后端 │ │ - 负载均衡 │ └──────────────┬──────────────────────────────┘ │ ┌────────┴────────┐ ↓ ↓ ┌──────────┐ ┌──────────┐ │ 缓存层 │ │ 服务层 │ └──────────┘ └──────────┘ │ │ ↓ ↓ ┌─────────────────────────────────┐ │ 数据库层 │ │ MySQL主从 + 分库分表 │ └─────────────────────────────────┘ ``` --- **【详细设计】** ### 1. 缓存层设计(关键) ```java /** * Redis预减库存(核心) */ @Service public class SeckillService { @Autowired private RedisTemplate redisTemplate; /** * 预减库存(原子操作) */ public boolean decrementStock(Long productId, Long userId) { String key = "stock:seckill:" + productId; // 原子减库存 Long stock = redisTemplate.opsForValue().decrement(key); if (stock < 0) { // 库存不足,回滚 redisTemplate.opsForValue().increment(key); return false; } // 记录用户抢购记录 String userKey = "user:seckill:" + productId + ":" + userId; redisTemplate.opsForValue().set(userKey, "1", 24, TimeUnit.HOURS); return true; } /** * 异步下单(消息队列) */ public void asyncOrder(Order order) { // 发送到消息队列 rabbitTemplate.convertAndSend("order.queue", order); } } /** * 消息队列消费者 */ @Component public class OrderConsumer { @RabbitListener(queues = "order.queue") public void createOrder(Order order) { // 1. 校验库存(数据库) int stock = productMapper.getStock(order.getProductId()); if (stock <= 0) { throw new InsufficientStockException(); } // 2. 扣减库存(数据库) productMapper.decrementStock(order.getProductId()); // 3. 创建订单 orderMapper.insert(order); // 4. 返回成功 log.info("订单创建成功: {}", order.getId()); } } ``` --- ### 2. 限流设计 ```java /** * 限流策略(多级) */ @Configuration public class RateLimiterConfig { /** * 接口级限流(令牌桶) */ @Bean public RateLimiter apiRateLimiter() { // 100万QPS return RateLimiter.create(1000000.0); } /** * 用户级限流(防刷) */ public boolean checkUserRateLimit(Long userId) { String key = "rate_limit:user:" + userId; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } // 每分钟最多10次 return count <= 10; } } /** * 限流拦截器 */ @Component public class RateLimitInterceptor implements HandlerInterceptor { @Autowired private RateLimiter apiRateLimiter; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 接口级限流 if (!apiRateLimiter.tryAcquire()) { response.setStatus(429); // Too Many Requests return false; } // 用户级限流 Long userId = getUserId(request); if (!checkUserRateLimit(userId)) { response.setStatus(429); return false; } return true; } } ``` --- ### 3. 防刷设计 ```java /** * 防刷策略 */ @Service public class AntiSpamService { /** * 验证码(图形验证码 + 滑动验证) */ public boolean verifyCaptcha(String token, String answer) { // 调用验证码服务 return captchaService.verify(token, answer); } /** * 风控规则 */ public RiskLevel checkRisk(Long userId, SeckillRequest request) { RiskScore score = new RiskScore(); // 1. IP风险检测 String ip = getIp(request); if (ipRiskService.isHighRisk(ip)) { score.add(50); } // 2. 设备指纹检测 String deviceId = getDeviceId(request); if (deviceRiskService.isEmulator(deviceId)) { score.add(80); // 模拟器 } // 3. 行为检测 if (userBehaviorService.isAbnormal(userId)) { score.add(30); } // 4. 频率检测 int requestCount = getRequestCount(userId, 1, TimeUnit.MINUTES); if (requestCount > 100) { score.add(40); } // 判定风险等级 if (score.getScore() >= 80) { return RiskLevel.HIGH; // 拒绝 } else if (score.getScore() >= 50) { return RiskLevel.MEDIUM; // 需要验证 } else { return RiskLevel.LOW; // 通过 } } } ``` --- ### 4. 降级策略 ```java /** * 降级策略 */ @Component public class DegradationService { /** * 降级开关(配置中心) */ @Value("${seckill.degradation.enabled:false}") private boolean degradationEnabled; /** * 降级方案 */ public SeckillResult handleSeckill(SeckillRequest request) { if (degradationEnabled) { // 降级:返回静态页面 return SeckillResult.degraded(); } // 正常流程 return seckillService.seckill(request); } /** * 自动降级(基于错误率) */ @Scheduled(fixedRate = 1000) // 每秒检查 public void autoDegradation() { double errorRate = getErrorRate(); if (errorRate > 0.5) { // 错误率超过50% // 开启降级 enableDegradation(); // 发送告警 alertService.send("错误率过高,已自动降级"); } } } ``` --- ### 5. 库存回滚(重要) ```java /** * 库存回滚机制 */ @Service public class StockRollbackService { /** * 定时任务:回滚未支付的订单库存 */ @Scheduled(cron = "0 */5 * * * ?") // 每5分钟 public void rollbackUnpaidStock() { // 查询15分钟前未支付的订单 List unpaidOrders = orderMapper.getUnpaidOrders(15); unpaidOrders.forEach(order -> { // 回滚库存(Redis) String key = "stock:seckill:" + order.getProductId(); redisTemplate.opsForValue().increment(key); // 回滚库存(数据库) productMapper.incrementStock(order.getProductId()); // 取消订单 orderMapper.updateStatus(order.getId(), OrderStatus.CANCELLED); log.info("库存已回滚: orderId={}", order.getId()); }); } } ``` --- **【容量预估】** ``` 1. QPS预估 - 用户数:100万 - 峰值QPS:100万/秒 - 考虑10倍冗余:1000万QPS 2. 缓存容量 - Redis实例:10个 - 每个实例:10万QPS - 总计:100万QPS 3. 消息队列 - Kafka分区:100个 - 消费者:200个 - 吞吐量:100万/秒 4. 数据库 - 主库:1个(写入) - 从库:10个(读取) - 分库分表:100个库,1000个表 ``` --- **【压测方案】** ```java /** * 压测方案 */ @Test public class LoadTest { /** * 压测脚本 */ @Test public void testSeckill() { // 1. 预热 warmUp(); // 2. 施加压力 int threadCount = 10000; // 1万个线程 int requestsPerThread = 100; // 每个线程100次请求 ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); long startTime = System.currentTimeMillis(); for (int i = 0; i < threadCount; i++) { executor.submit(() -> { try { for (int j = 0; j < requestsPerThread; j++) { // 发起请求 seckillService.seckill(request); } } finally { latch.countDown(); } }); } latch.await(); long endTime = System.currentTimeMillis(); long duration = endTime - startTime; int totalRequests = threadCount * requestsPerThread; double qps = (double) totalRequests / (duration / 1000.0); System.out.println("QPS: " + qps); System.out.println("平均耗时: " + (duration / totalRequests) + "ms"); } } ``` --- **【监控告警】** ``` 监控指标: 1. QPS 2. 响应时间(P50、P95、P99) 3. 错误率 4. 库存消耗速度 5. 订单创建速度 告警策略: - QPS < 100万:警告 - P99 > 500ms:警告 - 错误率 > 1%:严重告警 - 库存耗尽:通知 ``` --- ## 2. 优惠券系统设计 ### 题目 **面试官问**: "设计一个优惠券系统,支持满减、折扣、立减等多种类型,支持叠加使用,如何设计?" --- ### 参考回答 **【需求分析】** ``` 功能需求: 1. 优惠券类型 - 满减券(满100减20) - 折扣券(打8折) - 立减券(立减10元) - 兑换券(兑换商品) 2. 使用规则 - 叠加使用(可以同时使用多张) - 互斥使用(不能同时使用) - 优先级(先使用优先级高的) 3. 限制条件 - 品类限制(仅限某品类) - 商品限制(仅限某商品) - 用户限制(新用户专享) - 时间限制(有效期) 非功能需求: - 性能:计算优惠 < 10ms - 准确:不能算错 - 灵活:方便配置新规则 ``` --- **【数据模型设计】** ```sql -- 优惠券模板 CREATE TABLE coupon_template ( id BIGINT PRIMARY KEY, name VARCHAR(128), -- 名称 type TINYINT, -- 类型:1满减 2折扣 3立减 discount_value DECIMAL(10,2), -- 优惠值 min_amount DECIMAL(10,2), -- 最低消费金额 max_discount DECIMAL(10,2), -- 最大优惠金额 category_id BIGINT, -- 适用品类 product_id BIGINT, -- 适用商品 user_limit INT, -- 每人限领数量 total_count INT, -- 总发行量 issued_count INT DEFAULT 0, -- 已发行量 used_count INT DEFAULT 0, -- 已使用量 valid_days INT, -- 有效天数 status TINYINT, -- 状态:1草稿 2进行中 3已结束 priority INT DEFAULT 0, -- 优先级 stackable TINYINT DEFAULT 0, -- 是否可叠加 create_time DATETIME, update_time DATETIME ); -- 用户优惠券 CREATE TABLE user_coupon ( id BIGINT PRIMARY KEY, user_id BIGINT, template_id BIGINT, status TINYINT, -- 状态:1未使用 2已使用 3已过期 order_id BIGINT, -- 关联订单 receive_time DATETIME, use_time DATETIME, expire_time DATETIME, INDEX idx_user_id_status (user_id, status) ); -- 优惠券使用记录 CREATE TABLE coupon_usage_log ( id BIGINT PRIMARY KEY, user_id BIGINT, coupon_id BIGINT, order_id BIGINT, discount_amount DECIMAL(10,2), create_time DATETIME ); ``` --- **【核心代码设计】** ```java /** * 优惠计算引擎 */ @Service public class DiscountCalculator { /** * 计算最优优惠 */ public DiscountResult calculateBestDiscount( Long userId, List items, List availableCouponIds ) { // 1. 获取可用优惠券 List availableCoupons = getAvailableCoupons(userId, availableCouponIds); if (availableCoupons.isEmpty()) { return DiscountResult.empty(); } // 2. 计算订单金额 BigDecimal originalAmount = calculateOriginalAmount(items); // 3. 尝试所有优惠券组合 DiscountResult bestResult = null; BigDecimal maxDiscount = BigDecimal.ZERO; // 3.1 单张优惠券 for (UserCoupon coupon : availableCoupons) { DiscountResult result = calculateDiscount(coupon, items); if (result.getDiscountAmount().compareTo(maxDiscount) > 0) { bestResult = result; maxDiscount = result.getDiscountAmount(); } } // 3.2 多张优惠券组合(如果可叠加) List stackableCoupons = availableCoupons.stream() .filter(c -> c.getTemplate().isStackable()) .collect(Collectors.toList()); if (stackableCoupons.size() > 1) { // 递归计算所有组合 List combinations = calculateCombinations(stackableCoupons, items); for (DiscountResult result : combinations) { if (result.getDiscountAmount().compareTo(maxDiscount) > 0) { bestResult = result; maxDiscount = result.getDiscountAmount(); } } } return bestResult; } /** * 计算单张优惠券的优惠 */ private DiscountResult calculateDiscount(UserCoupon coupon, List items) { CouponTemplate template = coupon.getTemplate(); BigDecimal originalAmount = calculateOriginalAmount(items); BigDecimal discountAmount = BigDecimal.ZERO; switch (template.getType()) { case 1: // 满减券 if (originalAmount.compareTo(template.getMinAmount()) >= 0) { discountAmount = template.getDiscountValue(); // 不超过最大优惠 if (discountAmount.compareTo(template.getMaxDiscount()) > 0) { discountAmount = template.getMaxDiscount(); } } break; case 2: // 折扣券 discountAmount = originalAmount.multiply( BigDecimal.ONE.subtract(template.getDiscountValue().divide(BigDecimal.valueOf(100))) ); break; case 3: // 立减券 discountAmount = template.getDiscountValue(); break; } return DiscountResult.builder() .couponId(coupon.getId()) .discountAmount(discountAmount) .finalAmount(originalAmount.subtract(discountAmount)) .build(); } /** * 获取可用优惠券 */ private List getAvailableCoupons(Long userId, List couponIds) { // 查询未使用且未过期的优惠券 return userCouponMapper.selectByUserIdAndStatusAndIds( userId, UserCouponStatus.UNUSED.getValue(), couponIds ); } /** * 计算原始金额 */ private BigDecimal calculateOriginalAmount(List items) { return items.stream() .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } } /** * 优惠券使用服务 */ @Service public class CouponUsageService { @Autowired private DiscountCalculator discountCalculator; @Autowired private RedisTemplate redisTemplate; /** * 使用优惠券(分布式锁保证幂等) */ @Transactional public void useCoupon(Long userId, Long couponId, Long orderId) { String lockKey = "coupon:lock:" + couponId; // 分布式锁 Boolean locked = redisTemplate.opsForValue().setIfAbsent( lockKey, "1", 10, TimeUnit.SECONDS ); if (!locked) { throw new BusinessException("系统繁忙,请稍后重试"); } try { // 1. 查询优惠券 UserCoupon coupon = userCouponMapper.selectById(couponId); // 2. 校验状态 if (coupon.getStatus() != UserCouponStatus.UNUSED.getValue()) { throw new BusinessException("优惠券已使用或已过期"); } // 3. 校验有效期 if (coupon.getExpireTime().before(new Date())) { throw new BusinessException("优惠券已过期"); } // 4. 更新状态 userCouponMapper.updateStatus(couponId, UserCouponStatus.USED.getValue()); // 5. 记录使用日志 couponUsageLogMapper.insert(CouponUsageLog.builder() .userId(userId) .couponId(couponId) .orderId(orderId) .discountAmount(coupon.getTemplate().getDiscountValue()) .build()); } finally { // 释放锁 redisTemplate.delete(lockKey); } } } ``` --- ## 3. 数据一致性设计 ### 题目 **面试官问**: "在大促活动中,如何保证Redis缓存和数据库的数据一致性?" --- ### 参考回答 **【问题分析】** ``` 不一致的场景: 1. 先更新数据库,后删除缓存 - A更新数据库 - B读取缓存(旧数据) - A删除缓存 - B写入缓存(旧数据) → 不一致 2. 先删除缓存,后更新数据库 - A删除缓存 - B读取数据库(旧数据) - B写入缓存(旧数据) - A更新数据库 → 不一致 3. 缓存过期 - 缓存过期 - A读取数据库(旧数据) - B更新数据库 - A写入缓存(旧数据) → 不一致 ``` --- **【解决方案】** ### 方案1:延迟双删(推荐) ```java /** * 延迟双删 */ @Service public class DataUpdateService { @Autowired private RedisTemplate redisTemplate; @Autowired private ProductMapper productMapper; /** * 更新商品(延迟双删) */ @Transactional public void updateProduct(Product product) { // 1. 删除缓存 String cacheKey = "product:" + product.getId(); redisTemplate.delete(cacheKey); // 2. 更新数据库 productMapper.updateById(product); // 3. 延迟删除缓存(异步) asyncDeleteCache(cacheKey); } /** * 异步延迟删除缓存 */ @Async public void asyncDeleteCache(String cacheKey) { try { // 延迟1秒(保证数据库主从同步完成) Thread.sleep(1000); // 再次删除缓存 redisTemplate.delete(cacheKey); log.info("延迟删除缓存成功: {}", cacheKey); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } ``` --- ### 方案2:订阅Binlog(最终一致) ```java /** * Binlog监听器 */ @Component public class BinlogListener { @Autowired private RedisTemplate redisTemplate; /** * 监听数据库更新 */ @CanalEventListener( destination = "product", filter = ".*\\..*" ) public void onUpdate(CanalEntry.Entry entry) { // 解析Binlog CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { // 获取主键ID Long id = rowData.getAfterColumnsList().stream() .filter(c -> c.getIsKey()) .findFirst() .map(c -> Long.valueOf(c.getValue())) .orElse(null); if (id != null) { // 删除缓存 String cacheKey = "product:" + id; redisTemplate.delete(cacheKey); log.info("Binlog监听删除缓存: {}", cacheKey); } } } } ``` --- ### 方案3:分布式锁(强一致) ```java /** * 分布式锁保证一致性 */ @Service public class ConsistentService { @Autowired private RedissonClient redissonClient; /** * 更新数据(强一致) */ public void updateData(String key, Object value) { String lockKey = "lock:data:" + key; RLock lock = redissonClient.getLock(lockKey); try { // 加锁 lock.lock(10, TimeUnit.SECONDS); // 1. 删除缓存 redisTemplate.delete(key); // 2. 更新数据库 databaseMapper.update(key, value); // 3. 查询最新值 Object newValue = databaseMapper.select(key); // 4. 写入缓存 redisTemplate.opsForValue().set(key, newValue, 1, TimeUnit.HOURS); } finally { // 释放锁 lock.unlock(); } } } ``` --- ## 4. 限流降级设计 ### 题目 **面试官问**: "在大促活动中,如何设计限流和降级策略?" --- ### 参考回答 **【限流策略】** ```java /** * 多级限流 */ @Component public class MultiLevelRateLimiter { /** * Level 1: 接口级限流(保护服务) */ @Bean public RateLimiter interfaceRateLimiter() { // 令牌桶:100万QPS return RateLimiter.create(1000000.0); } /** * Level 2: 用户级限流(防刷) */ public boolean checkUserLimit(Long userId) { String key = "limit:user:" + userId; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } // 每分钟10次 return count <= 10; } /** * Level 3: IP级限流(防DDoS) */ public boolean checkIPLimit(String ip) { String key = "limit:ip:" + ip; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } // 每分钟100次 return count <= 100; } } ``` --- **【降级策略】** ```java /** * 降级策略 */ @Service public class DegradationService { /** * 自动降级(基于错误率) */ @Scheduled(fixedRate = 5000) // 每5秒检查 public void autoDegradation() { // 1. 统计错误率 double errorRate = getErrorRate(); // 2. 判断是否需要降级 if (errorRate > 0.5) { // 错误率超过50% enableDegradation(); sendAlert("错误率过高,已自动降级"); } // 3. 统计RT double rt = getAverageResponseTime(); if (rt > 1000) { // RT超过1秒 enableDegradation(); sendAlert("响应时间过长,已自动降级"); } } /** * 降级执行 */ public Result executeWithDegradation(String serviceName) { if (isDegradationEnabled()) { // 返回默认值或静态数据 return Result.degraded(); } // 正常调用 return serviceClient.call(serviceName); } /** * 手动降级(配置中心) */ @EventListener(condition = "#event.type == 'MANUAL_DEGRADATION'") public void onManualDegradation(ConfigChangeEvent event) { boolean enabled = Boolean.parseBoolean(event.getValue()); setDegradationEnabled(enabled); } } ``` --- ## 💡 设计题答题技巧 ### 1. 答题框架 ``` 【需求分析】 - 功能需求:XX - 非功能需求:XX - 约束条件:XX 【架构设计】 - 整体架构图 - 技术选型 - 数据模型 【详细设计】 - 核心代码 - 关键流程 - 边界处理 【容量预估】 - QPS预估 - 存储预估 - 带宽预估 【压测方案】 - 压测工具 - 压测场景 - 优化方案 【监控告警】 - 监控指标 - 告警策略 ``` --- ### 2. 常见追问 **追问1:如果QPS扩大10倍,怎么办?** ``` 答: 1. 垂直扩展:升级服务器配置 2. 水平扩展:增加服务器数量 3. 缓存优化:增加缓存命中率 4. 读写分离:减轻主库压力 5. 分库分表:分散压力 6. CDN加速:减轻源站压力 7. 异步化:消息队列削峰 ``` **追问2:如何保证数据一致性?** ``` 答: 1. 强一致:分布式锁 2. 最终一致:延迟双删、订阅Binlog 3. 补偿机制:定时任务对账 ``` **追问3:如何应对突发流量?** ``` 答: 1. 弹性扩容:Serverless、K8s HPA 2. 限流:保护系统 3. 降级:保证核心功能 4. 预热:提前加载缓存 ```