尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

秒杀系统架构设计08:全链路架构

秒杀系统架构设计08:全链路架构 全链路异常处理实现订单不超卖、不掉单、不重复本文是「10Wqps 秒杀架构」系列的终章。前面的七篇文章分别拆解了分层架构、异步下单、动静分离、缓存、库存、MQ 和高可用。本篇将这些技术方案串联起来构建端到端的全链路异常处理体系最终实现不超卖、不掉单、不重复下单三大目标。一、开篇假设你的商城线上注册会员突破 6000 万促销活动期间下单 QPS 峰值 20000。面对这个量级你的订单全链路设计方案需要回答三个致命问题如何防止超卖——库存扣成负数平台赔到破产如何防止掉单——库存扣了但订单丢了用户投诉如潮如何防止重复下单——用户付了两遍钱客服工单爆炸这三个问题的共同解法不在某一个组件中而在整个链路的全环节设计中。每个节点都有正常路径和异常路径异常路径的覆盖率决定了系统的可靠性。二、全链路架构总览创建订单的全链路划分为五大层级。每一层都有明确的职责、依赖和异常处理策略第五层通知回调层第四层数据落地层第三层核心执行层第二层业务校验层第一层请求接入层负载均衡限流/认证/路由否是快速失败否是成功失败成功消费失败成功兜底定时对账每 5 分钟: Redis vs MySQL 库存对账每日: 预扣记录 vs MQ vs 订单表 全量对账Nginx 集群SpringCloud Gateway 集群校验通过?返回 4xx/429抢购服务校验通过?返回业务错误码Redis Lua 原子预扣RocketMQ 事务消息库存回滚MQ 消费者Spring 事务: 订单入库 库存确认重试 3 次 / 死信队列通知用户 / 触发支付流程三、第一层请求接入层异常处理核心职责负载均衡、流量管控、路由分发、前置过滤异常矩阵异常场景检测方式处理策略兜底方案限流触发Sentinel 计数器返回 HTTP 429 “活动太火爆请稍后再试”动态调整阈值Nacos 实时下发熔断触发Sentinel 熔断器状态 OPEN返回降级响应 记录熔断日志HALF_OPEN 自动探测恢复Gateway 节点故障健康检查失败K8s 自动摘除故障 Pod 重启多实例部署故障转移下游服务下线服务发现Nacos心跳超时路由表摘除故障实例降级到兜底接口Token 无效/过期Gateway GlobalFilter 解析返回 401不向下游传递记录异常请求日志Nginx 节点故障Keepalived 检测VIP 漂移到备用 Nginx—// 统一异常捕获: Gateway GlobalFilterComponentpublicclassGlobalExceptionFilterimplementsGlobalFilter{OverridepublicMonoVoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){returnchain.filter(exchange).onErrorResume(e-{// 统一封装错误响应ErrorResponseerrornewErrorResponse();error.setRequestId(exchange.getRequest().getId());error.setTimestamp(System.currentTimeMillis());if(einstanceofBlockException){error.setCode(429);error.setMessage(活动太火爆请稍后再试);}elseif(einstanceofConnectTimeoutException){error.setCode(504);error.setMessage(服务繁忙请稍后重试);}else{error.setCode(500);error.setMessage(系统异常);}// 上报监控metricsCollector.recordException(e);returnwriteResponse(exchange,error);});}}四、第二层业务校验层异常处理核心职责在核心业务执行前快速拦截所有非法请求校验链路任一失败即返回验证码校验 → 活动状态校验 → 黑名单检查 → 用户状态检查 → 商品状态检查 → 库存预检查异常矩阵异常场景检测方式处理策略验证码错误/过期验证码服务返回返回具体错误提示不消耗 Redis/DB 资源活动未开始/已结束Redis 查询活动时间窗口返回活动未开始或活动已结束用户在黑名单Redis Set 查询返回操作受限记录风控日志用户状态异常冻结Feign 调用用户中心熔断降级用户中心不可用时放行依赖下游订单服务二次校验商品不存在/下架Feign 调用商品服务熔断降级返回商品不存在远程调用超时Feign Sentinel熔断触发 → 返回降级提示快速失败原则所有校验都在业务校验层完成一旦失败立即终止请求处理释放线程、数据库连接等资源。不把无效请求带到核心执行层。RestControllerAdvicepublicclassSeckillExceptionHandler{ExceptionHandler(BusinessException.class)publicResulthandleBusinessException(BusinessExceptione){// 业务异常返回友好提示returnResult.fail(e.getCode(),e.getMessage());}ExceptionHandler(Exception.class)publicResulthandleSystemException(Exceptione){// 系统异常隐藏异常栈返回统一提示 记录完整日志log.error(系统异常: userId{}, seckillId{}, params{},RequestContext.getUserId(),RequestContext.getSeckillId(),RequestContext.getParams(),e);returnResult.fail(500,系统异常请稍后再试);}}五、第三层核心执行层异常处理这是全链路最关键的一层。两个操作——Redis 库存预扣 和 MQ 消息发送——必须保持原子性。异常矩阵异常场景处理策略兜底方案Redis 连接超时 300ms重试 1 次仍失败则返回系统繁忙记录超时日志监控告警Lua 脚本执行失败返回下单失败记录完整异常日志定期检查 Lua 脚本语法与逻辑库存预扣成功MQ 发送失败事务消息确保原子性回查接口兜底立即回滚 Redis 库存库存预扣成功MQ COMMIT 超时Broker 回查 → checkLocalTransaction 返回真实状态回查间隔缩短至 10sRedis 主从切换数据同步延迟预扣操作走主节点查询走从节点允许短暂不一致定时对账修复一致库存回滚的双重保障// 机制一即时回滚MQ 发送失败时publicvoidhandleMqSendFailure(SeckillOrderorder){// 通过 Lua 脚本原子回滚StringrollbackScriptredis.call(incrby, KEYS[1], 1) // 恢复库存redis.call(del, KEYS[2]);// 删除预扣记录redis.eval(rollbackScript,Arrays.asList(stock:seckill:order.getSeckillId(),pre:order:order.getOrderId()),Collections.emptyList());}// 机制二定时回滚每 1 分钟扫描超时预扣记录Scheduled(fixedDelay60000)publicvoidscanAndRollbackStalePreDeductions(){SetStringstaleKeysredis.keys(pre:order:*);longnowSystem.currentTimeMillis();for(Stringkey:staleKeys){longcreateTimeLong.parseLong(redis.hget(key,createTime));if(now-createTime600000){// 超过 10 分钟未确认StringseckillIdredis.hget(key,seckillId);rollbackStock(seckillId,key);}}}六、第四层数据落地层异常处理这是订单数据的终点站。MQ 消费者将订单写入 MySQL并确认库存扣减。消费成功后订单才算真正完成。消息处理流程含所有异常分支RocketMQMessageListener(topicSECKILL_ORDER_TOPIC,consumerGrouporder-create)publicclassOrderCreateConsumerimplementsRocketMQListenerMessageExt{OverridepublicvoidonMessage(MessageExtmessage){StringorderIdmessage.getKeys();// Step 1: 消息校验if(StringUtils.isBlank(orderId)){log.error(消息格式错误: msgId{},message.getMsgId());return;// 无效消息直接 ACK不重试}// Step 2: 幂等校验if(orderMapper.existsByOrderId(orderId)){return;// 已处理幂等跳过}// Step 3: 订单入库事务try{orderService.createOrderInTransaction(parseOrder(message));}catch(DuplicateKeyExceptione){// 数据库唯一索引兜底幂等log.warn(重复订单被唯一索引拦截: orderId{},orderId);return;}catch(Exceptione){log.error(订单入库失败: orderId{},orderId,e);thrownewRuntimeException(消费失败触发重试,e);}}}消费者事务内部细节Transactional(rollbackForException.class)publicvoidcreateOrderInTransaction(SeckillOrderorder){// 1. 订单入库orderMapper.insert(order);// 2. 库存确认UPDATE stock SET stock stock - 1 WHERE ...intaffectedstockMapper.confirmDeduction(order.getProductId(),order.getQuantity());if(affected0){thrownewBusinessException(库存确认失败可能库存不足);}// 3. 删除 Redis 预扣记录防止回滚任务误触发redis.del(pre:order:order.getOrderId());// 4. 优惠券核销积分更新同理if(order.getCouponId()!null){couponService.useCoupon(order.getCouponId(),order.getUserId());}// 任一步骤失败 → 整个事务回滚 → 消息重试}异常矩阵异常场景处理策略兜底方案消息重复消费订单号唯一索引拦截分布式锁 查表 双重幂等事务中某步骤失败Spring 事务回滚 消息重试3 次3 次失败入死信队列MySQL 死锁捕获 DeadlockLoserDataAccessException重试 2 次重试也失败则抛异常触发消息重试主从延迟订单写入后查不到1 分钟内用户查询强制走主库1 分钟后走从库 Redis 缓存兜底数据库连接池耗尽Druid 连接池监控告警临时扩容连接数 限流降级七、三大核心问题的兜底方案7.1 防止超卖三重校验第一重: Redis Lua 原子扣减 ↓ 通过但后续失败 第二重: MySQL FOR UPDATE 行锁再次校验库存充足性 ↓ 极端情况下绕过 第三重: 定时对账每 5 分钟对比 Redis vs MySQL 库存 → 不一致时以 MySQL 为准修复 Redis对账逻辑Scheduled(fixedDelay300000)publicvoidreconcileStock(){for(Seckillseckill:activeSeckills){intredisStockredis.get(stock:seckill:seckill.getId());intpreDeductedredis.keys(pre:order:seckill:seckill.getId():*).size();intmysqlStockmysql.query(SELECT stock FROM product WHERE id ?,seckill.getProductId());if(redisStockpreDeducted!mysqlStock){log.error(库存不一致: seckill{}, redis{}, pre{}, mysql{},seckill.getId(),redisStock,preDeducted,mysqlStock);// 以 MySQL 为准修复redis.set(stock:seckill:seckill.getId(),mysqlStock-preDeducted);}}}7.2 防止掉单四重保障第一重: RocketMQ 事务消息预扣 消息原子性 ↓ 极端情况下事务消息也失败 第二重: 消息重试消费失败自动重试 3 次 ↓ 3 次都失败 第三重: 死信队列 人工处理 ↓ 消息根本没到 MQ生产者本地故障 第四重: 每日全量对账补单 → 对比 Redis 预扣记录 vs MQ 消息记录 vs 订单表 → 发现缺失自动触发补单逻辑掉单的可能环节和排查路径用户说我没收到订单 → 排查路径: 1. Redis 有无预扣记录 无 → 预扣就失败了正常 2. MQ 有无该消息 无 → 事务消息发送失败需检查回查逻辑 3. 消费者有无消费日志 无 → 消息在 MQ 但未被消费Group 配置错误 4. 订单表有无记录 无 → 消费者执行失败查错误日志和死信队列 5. 什么都有 → 前端展示 Bug系统正常7.3 防止重复下单双重幂等第一重: Redis 分布式锁用户 ID 商品 ID 请求唯一标识 → 同一用户 3 秒内无法发起对同一商品的第二次秒杀 ↓ 极端情况下绕过如用户切换设备 第二重: 订单号唯一索引uk_order_id → 数据库层面拒绝重复插入 → 同时订单号以用户 ID SKU ID 时间戳通过雪花算法生成八、架构思维回顾贯穿整个秒杀架构设计的是六个核心架构思维思维在秒杀架构中的体现分层架构五层全链路接入 → 校验 → 执行 → 落地 → 通知。每层独立扩容各司其职微服务订单/抢购/库存/用户/商品/支付 独立部署。抢购服务单独承载高并发不影响其他异步解耦同步校验 MQ 异步下单。同步部分 30ms异步部分保障可靠性数据一致性事务消息 幂等设计 定时对账 库存回滚。不强依赖分布式事务走最终一致高可用限流/熔断/降级 无状态设计 弹性伸缩 多 IDC 双活容错兜底每层都有预防 → 处理 → 兜底三段式异常处理不信任任何单一组件九、总结不超卖靠三重校验Redis Lua 原子操作第一重 MySQL FOR UPDATE 行锁第二重 定时对账修复第三重。不掉单靠四重保障事务消息第一重 消费重试 死信队列第二重 本地消息表第三重 每日全量对账补单第四重。不重复下单靠双重幂等Redis 分布式锁应用层 订单号唯一索引数据库层。全链路异常处理的核心原则每一层都独立拦截自己职责范围内的异常不让异常向下渗透每层都有兜底机制不信任上游拦截是完美的。架构的本质是在可靠性、性能和复杂度之间做权衡。本文的方案选择了最终一致性而非强一致性用异步和定时对账换取高并发能力——这是一个有意识、可解释、可验证的权衡。
返回列表