架构思维:预约抢茅子架构设计

文章目录

  • 案例:预约抢茅子
  • 复杂度分析
    • 商品预约阶段
    • 等待抢购阶段
    • 商品抢购阶段
    • 订单支付阶段
  • 技术方案
    • 商品预约阶段
      • 一、基于 Redis 单节点的分布式锁方案
        • 1. 核心流程
        • 2. 关键设计点
      • 二、Redis 单节点方案的局限性
        • 1. 单点故障风险
        • 2. 主从切换问题
      • 三、多节点 Redis 实现高可靠分布式锁(RedLock)
        • 1. RedLock 核心流程
        • 2. RedLock 关键设计
        • 3. RedLock 的争议与改进
      • 四、不同场景下的技术选型
      • 五、补充优化措施
      • 六、小结
    • 等待抢购阶段
      • 一、页面静态化:降低服务端负载
        • 1. 静态资源拆分与缓存
        • 2. CDN缓存策略优化
        • 3. 动态内容降级方案
      • 二、服务端限流:保护核心系统
        • 1. 多层级限流设计
        • 2. Nginx限流配置示例
        • 3. 用户体验优化
      • 三、动态内容优化:降低API压力
        • 1. 客户端长轮询替代短轮询
        • 2. 边缘计算缓存动态数据
      • 四、方案效果与权衡
      • 五、小结
    • 商品抢购阶段
      • 一、流量削峰:异步化处理与消息队列优化
        • 1. 核心流程设计
        • 2. 消息队列
      • 二、扣减库存:高并发下的数据一致性保障
        • 1. Redis集群方案
        • 2. 可靠性增强
      • 三、分库分表:订单存储的高并发支持
        • 1. 分片策略
        • 2. 中间件选型
      • 四、带宽与网络优化
        • 1. 独立域名与DNS
        • 2. 协议优化
        • 3. 边缘计算
      • 五、小结
    • 订单支付阶段
      • 一、支付回调阶段的可靠消息投递设计
        • 1. 整体流程
        • 2. 关键设计
        • 3. 消息消费的幂等性
      • 二、容错与异常处理
        • 1. 支付回调接口的幂等性
        • 2. 消息补偿机制
        • 3. 数据库与MQ的一致性
      • 三、性能优化
        • 1. 本地消息表设计优化
        • 2. 批量处理
        • 3. 异步化处理
      • 四、高可用与监控
        • 1. 消息队列高可用
        • 2. 监控告警
        • 3. 熔断降级
      • 五、方案对比与选型
      • 小结
    • 抢购后不支付导致的库存占用问题
      • 一、预占库存与支付超时释放机制
        • 1. 库存状态分层设计
        • 2. 预占库存实现流程
        • 3. 关键技术实现
      • 二、恶意用户识别与拦截
        • 1. 用户行为风控模型
        • 2. 实时风控系统架构
      • 三、支付倒计时与用户提醒
        • 1. 前端体验优化
        • 2. 支付链路优化
      • 四、数据一致性保障
        • 1. 最终一致性方案
        • 2. 分布式事务
      • 五、监控与容灾
        • 1. 核心监控指标
        • 2. 熔断降级策略
      • 小结

在这里插入图片描述


案例:预约抢茅子

架构思维:预约抢茅子架构设计_第1张图片
业务流程如下:

  • 商品预约:用户进入商品详情页面,获取购买资格,并等待商品抢购倒计时。

  • 等待抢购:等待商品抢购倒计时,直到商品开放抢购。

  • 商品抢购:商品抢购倒计时结束,用户提交抢购订单,排队等待抢购结果,抢购成功后,扣减系统库存,生成抢购订单。

  • 订单支付:等待用户支付成功后,系统更新订单状态,通知用户购买成功。


复杂度分析

根据不同的业务流程阶段,逐一分析一下每个环节可能存在的技术挑战

商品预约阶段

在高并发量的情况下,让每个用户都能得到抢购资格 ?


等待抢购阶段

用户预约成功之后,在商品详情页面中,会存在一个抢购倒计时,这个倒计时的初始时间是从服务端获取的,用户点击购买按钮时,系统还会去服务端验证是否已经到了抢购时间。

在等待抢购阶段,流量突增,因为在抢购商品之前(尤其是临近开始抢购之前的一分钟内),大部分用户会频繁刷新商品详情页,商品详情页面的读请求量剧增, 如果商品详情页面没有做好流量控制,就容易成为整个预约抢购系统中的性能瓶颈点


商品抢购阶段

在商品抢购阶段,用户会点击提交订单,这时,抢购系统会先校验库存,当库存足够时,系统会先扣减库存,然后再生成订单。在这个过程中,短时间之内提交订单的写流量非常高


订单支付阶段

在用户支付订单完成之后,一般会由支付平台回调系统接口,更新订单状态。在支付回调成功之后,抢购系统还会通过异步通知的方式,实现订单更新之外的非核心业务处理,比如积分累计、短信通知等


技术方案

商品预约阶段

在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。

在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。以下是基于分布式锁技术(参考第06讲内容)的完整技术分析,以及针对 Redis 单点故障问题的补充解决方案:


一、基于 Redis 单节点的分布式锁方案

1. 核心流程
// 伪代码示例:用户预约资格发放
public boolean reserveCommodity(String userId, String itemId) {
    // 生成唯一锁标识
    String lockKey = "reserve_lock:" + itemId;
    String clientId = UUID.randomUUID().toString();
    
    try {
        // 尝试获取分布式锁(设置超时防止死锁)
        boolean locked = redis.set(lockKey, clientId, "NX", "PX", 10000);
        if (!locked) return false;

        // ------ 临界区操作(原子性保障) ------
        // 1. 检查是否已预约(防重复)
        if (redis.sismember("reserved_users:" + itemId, userId)) {
            return false;
        }
        
        // 2. 发放预约资格(允许超库存预约)
        redis.sadd("reserved_users:" + itemId, userId); // 记录预约用户
        redis.incr("reserve_count:" + itemId);          // 统计预约总数
        
        return true;
        // -----------------------------------
        
    } finally {
        // 释放锁(Lua脚本保证原子性)
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientId));
    }
}
2. 关键设计点
  • 锁粒度:按商品ID分片锁(reserve_lock:itemId),避免全局锁竞争。
  • 防重复预约:使用 Redis 集合(sadd/sismember)记录已预约用户。
  • 超量预约支持:通过 incr 统计预约量,不依赖实际库存限制。
  • 锁超时机制:设置 10s 锁超时(PX 10000),避免客户端异常导致死锁。

二、Redis 单节点方案的局限性

1. 单点故障风险
  • 问题:若 Redis 主节点宕机,锁数据丢失,导致业务中断。
  • 表现
    • 新客户端无法获取锁(锁变量丢失)。
    • 已持有锁的客户端误认为锁仍有效(但锁实际已丢失)。
2. 主从切换问题
  • 若使用 Redis 主从集群,在主节点宕机后,由于 Redis 主从复制是异步的:
    • 客户端A在主节点获取锁。
    • 主节点宕机,从节点升级为新主节点,但未同步锁数据。
    • 客户端B在新主节点也能获取同一把锁,导致锁互斥性失效。

三、多节点 Redis 实现高可靠分布式锁(RedLock)

针对单点故障问题,需引入 RedLock 算法(参考第06讲),基于多个独立 Redis 节点实现分布式锁。

1. RedLock 核心流程
// 伪代码:RedLock 实现
public boolean tryRedLock(String itemId, String clientId, int ttlMs) {
    List<RedisNode> redisNodes = getRedisClusterNodes(); // 获取所有Redis节点
    
    long startTime = System.currentTimeMillis();
    int successCount = 0;
    
    // 向所有节点发起加锁请求
    for (RedisNode node : redisNodes) {
        if (node.setLock(lockKey, clientId, ttlMs)) {
            successCount++;
        }
    }
    
    // 计算加锁耗时
    long elapsedTime = System.currentTimeMillis() - startTime;
    
    // 判定条件:多数节点加锁成功,且耗时小于锁有效期
    boolean locked = (successCount >= redisNodes.size() / 2 + 1) 
                     && (elapsedTime < ttlMs);
    
    if (locked) {
        // 锁有效期需补偿网络耗时:ttlMs - elapsedTime
        scheduleLockExpiration(clientId, ttlMs - elapsedTime); 
        return true;
    } else {
        // 加锁失败,释放已获得的锁
        releasePartialLocks(redisNodes, lockKey, clientId);
        return false;
    }
}
2. RedLock 关键设计
设计点 说明
独立节点部署 使用至少5个独立的 Redis 主节点(非集群模式),降低同时故障概率
时钟同步要求 各节点需使用 NTP 服务保证时钟同步,避免锁过期时间计算偏差
锁有效期补偿 锁实际有效时间 = 初始 TTL - 加锁耗时,防止因网络延迟导致锁提前失效
失败回滚机制 加锁失败时需异步释放已获得的锁,避免残留锁数据
3. RedLock 的争议与改进
  • 争议点(Martin Kleppmann):
    • 依赖系统时钟同步,时钟跳跃可能导致锁失效。
    • 锁的有效期难以精确计算,存在理论上的竞态条件。
  • 改进方案
    • 使用 fencing token(递增令牌)机制,确保锁释放后旧请求不生效。
    • 结合业务层幂等性设计,容忍极低概率的锁失效问题。

四、不同场景下的技术选型

场景 技术方案 优缺点
预约量较小(QPS < 1万) 单 Redis 节点 + 哨兵模式 实现简单,但主从切换时存在短暂不可用
高可靠性要求(金融场景) RedLock + 5个独立 Redis 节点 高可用,但实现复杂、性能较低(需多节点交互)
超高性能要求(QPS > 10万) Redis 集群 + 细粒度锁(按用户ID分片) 通过分片提升并发能力,但需解决数据倾斜问题

五、补充优化措施

  1. 无锁化设计尝试

    • 使用 Redis INCR 原子操作统计预约总数,替代分布式锁。
    • 通过 SETNX user_reserved:userId:itemId 1 实现用户级防重复预约。
    • 优点:性能更高;缺点:无法处理跨多键的原子操作。
  2. 熔断降级策略

    • 当 Redis 不可用时,降级到本地限流(如令牌桶算法),保障核心流程可用。
    • 记录预约请求到 Kafka 队列,后台异步补偿处理。
  3. 监控与告警

    • 监控 Redis 锁等待时间、锁竞争频率。
    • 设置锁持有超时告警(如锁持有时间 > 8s 时触发)。

六、小结

在商品预约阶段,通过分布式锁控制资格发放时需综合考虑:

  1. 锁的可靠性:单节点方案简单但存在单点故障,RedLock 更可靠但实现复杂。
  2. 性能与一致性平衡:根据业务容忍度选择最终一致性或强一致性方案。
  3. 容灾设计:结合熔断降级、异步补偿机制应对极端场景。

实际工程中,若允许极低概率的重复预约,可优先使用单 Redis 节点 + 哨兵模式;若需强一致,则选择 RedLock + fencing token 组合方案。


等待抢购阶段

在等待抢购阶段,应对流量突增问题需综合运用页面静态化服务端限流策略,并结合动态内容优化与用户体验设计。


一、页面静态化:降低服务端负载

1. 静态资源拆分与缓存
  • 核心原则:将商品详情页中不变的内容(如商品图片、描述、规格参数)与动态内容(倒计时、库存状态)分离。
  • 实现步骤
    • 生成静态HTML:使用模板引擎(如Thymeleaf、Velocity)在抢购开始前预生成静态页面,存储至CDN。
    • 动态内容异步加载:在静态页面中通过JavaScript调用API获取倒计时、抢购状态等动态数据。
    
    <html>
      <body>
        <div id="product-image">div>
        <div id="countdown-timer">div>
        <script>
          // 异步获取倒计时
          fetch('/api/countdown?itemId=1001')
            .then(response => response.json())
            .then(data => updateCountdown(data));
        script>
      body>
    html>
    
2. CDN缓存策略优化
  • 缓存规则
    • 静态资源(HTML、CSS、JS、图片):设置长期缓存(如1年),通过文件名哈希(main.[hash].css)实现版本更新。
    • 动态API响应:在CDN边缘节点缓存倒计时接口(/api/countdown),设置短时间缓存(如1秒),确保用户看到准实时数据。
  • 预热与刷新
    • 预缓存:在抢购开始前1小时,通过CDN预热工具(如阿里云CDN Prefetch)主动加载静态资源至所有节点。
    • 强制刷新:抢购开始时,通过CDN API刷新关键页面(如商品详情页),确保用户获取最新版本。
3. 动态内容降级方案
  • 本地时钟兜底:若倒计时API不可用,前端使用本地时钟计算剩余时间,并在倒计时结束后提示用户刷新页面。
    let serverTime = 1664000000; // 服务端返回的抢购开始时间戳
    let localOffset = Date.now() / 1000 - serverTime;
    
    function updateCountdown() {
      let remaining = serverTime - Math.floor(Date.now() / 1000 - localOffset);
      if (remaining <= 0) {
        showBuyButton(); // 显示购买按钮
      } else {
        displayTimer(remaining); // 显示倒计时
        setTimeout(updateCountdown, 1000);
      }
    }
    

二、服务端限流:保护核心系统

1. 多层级限流设计
层级 限流策略 工具/实现
CDN/边缘节点 限制同一IP的请求频率(如10次/秒) 阿里云CDN频率控制、Cloudflare Rate Limiting
网关层 按API维度限制QPS(如商品详情页动态接口10万QPS) Nginx limit_req(漏桶算法)、Spring Cloud Gateway RequestRateLimiter
应用层 基于用户ID或设备指纹的细粒度限流(如单个用户每秒最多5次请求) Redis + Lua脚本(计数器算法)
服务层 熔断非核心服务(如推荐系统、用户画像),保障抢购链路资源 Hystrix、Sentinel
2. Nginx限流配置示例
http {
  limit_req_zone $binary_remote_addr zone=product_detail:10m rate=100r/s;

  server {
    location /api/countdown {
      limit_req zone=product_detail burst=20 nodelay;
      proxy_pass http://backend_servers;
    }
  }
}
  • 解释
    • limit_req_zone:定义限流区域(按IP),10MB内存存储状态,允许100请求/秒。
    • burst=20:允许突发20个请求进入队列。
    • nodelay:突发请求不延迟处理,直接拒绝超限请求。
3. 用户体验优化
  • 排队机制:前端在限流响应(HTTP 429)时展示排队等待页,并自动重试。
    // 前端处理限流响应
    fetch('/api/countdown')
      .catch(error => {
        if (error.status === 429) {
          showQueuePage(); // 显示排队页,倒计时后重试
        }
      });
    
  • 请求退避策略:前端在重试时采用指数退避(Exponential Backoff),减少服务端压力。
    let retries = 0;
    function fetchWithRetry() {
      fetch('/api/countdown')
        .catch(() => {
          setTimeout(() => {
            retries++;
            fetchWithRetry();
          }, Math.min(1000 * 2 ** retries, 30000));
        });
    }
    

三、动态内容优化:降低API压力

1. 客户端长轮询替代短轮询
  • 方案:使用长轮询(Long Polling)或WebSocket减少无效请求。
    // 长轮询示例
    function longPollCountdown() {
      fetch('/api/countdown?longPoll=true')
        .then(response => {
          updateCountdown(response.data);
          longPollCountdown(); // 递归调用
        })
        .catch(() => setTimeout(longPollCountdown, 5000));
    }
    
  • 服务端实现:当倒计时未结束时,服务端挂起请求,直到时间临近(如剩余10秒)再响应。
2. 边缘计算缓存动态数据
  • 方案:将倒计时数据缓存在CDN边缘节点。
    // Cloudflare Worker脚本
    addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request));
    });
    
    async function handleRequest(request) {
      const cache = caches.default;
      let response = await cache.match(request);
      if (!response) {
        response = await fetch(request);
        response = new Response(response.body, response);
        // 缓存1秒,确保各节点数据准实时
        response.headers.append('Cache-Control', 'max-age=1');
        event.waitUntil(cache.put(request, response.clone()));
      }
      return response;
    }
    

四、方案效果与权衡

指标 静态化+限流方案 传统动态渲染方案
服务端负载 降低80%以上动态请求(仅处理倒计时API) 所有请求需动态生成页面,压力大
用户体验 页面加载快,但倒计时依赖API(需处理失败场景) 页面加载慢,但数据实时性高
开发复杂度 需维护静态生成工具与动态API协同 开发简单,直接渲染动态页面
成本 CDN费用增加,服务器成本降低 服务器成本高,CDN费用低

五、小结

等待抢购阶段的流量突增问题需通过动静分离分层限流综合解决:

  1. 静态化:将页面主体内容缓存在CDN,减少回源请求。
  2. 限流:在网关层拦截超量请求,保护后端服务。
  3. 动态内容优化:使用长轮询、边缘缓存降低API压力。
  4. 容灾与监控:通过自动扩缩容和混沌工程保障系统韧性。

商品抢购阶段

在商品抢购阶段,面对瞬时高并发流量,需通过多层次架构设计保障系统的高可用性与数据一致性。主要依赖流量削峰、扣减库存、分库分表三大核心方案。


一、流量削峰:异步化处理与消息队列优化

1. 核心流程设计
用户 网关 MQ 订单服务 Redis DB 提交抢购请求 发送订单请求(含用户ID、商品ID) 返回“排队中”提示 消费者拉取消息 原子扣减库存(Lua脚本) 扣减成功 插入订单(分库分表) 插入成功 通知抢购成功 扣减失败 通知抢购失败 alt [库存充足] [库存不足] 用户 网关 MQ 订单服务 Redis DB
2. 消息队列
  • 防消息丢失

    • 生产者确认:启用RabbitMQ的publisher confirms或Kafka的acks=all
    • 持久化存储:消息持久化到磁盘,副本数≥3(Kafka推荐配置)。
    • 消费者手动ACK:业务处理成功后再提交偏移量。
  • 消息积压处理

    • 动态扩缩容:基于队列长度自动增加消费者实例(如Kubernetes HPA)。
    • 批量消费:单次拉取多条消息(Kafka的max.poll.records=500)。
    • 死信队列:处理失败消息,避免阻塞正常流程。
  • 消息去重

    • 唯一业务ID:订单ID使用雪花算法生成,Redis记录已处理ID。
    String orderId = "ORDER_" + snowflake.nextId();
    if (redis.setnx("order:dedup:" + orderId, "1") == 1) {
        processOrder(orderId);
    }
    

二、扣减库存:高并发下的数据一致性保障

1. Redis集群方案
  • 架构选择

    • Redis Cluster:自动分片(16384 slots),支持水平扩展。
    • Codis:Proxy-based分片方案,适合大规模集群。
  • 库存扣减Lua脚本

-- KEYS[1]: 库存Key(stock:item_1001)
-- ARGV[1]: 扣减数量(通常为1)
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
    return redis.call('DECRBY', KEYS[1], ARGV[1])
else
    return -1
end
2. 可靠性增强
  • 多级缓存兜底

    • 本地缓存:Guava Cache记录热点商品库存(短时间缓存,如1秒)。
    • 数据库异步校对:定时任务对比Redis与数据库库存,修复差异。
  • 降级策略

    • 限流降级:库存不足时直接返回失败,避免无效请求穿透。
    • 预扣库存:提前将库存从数据库加载至Redis,避免击穿。

三、分库分表:订单存储的高并发支持

1. 分片策略
  • 分片键选择:以用户ID后4位取模(user_id % 1024),分为16库×64表。
  • 基因法分片:将分片信息嵌入订单ID,避免跨库查询。
    // 订单ID结构: 时间戳(41bit) + 分库编号(10bit) + 分表编号(6bit) + 序列号(7bit)
    long orderId = (timestamp << 23) | (dbNo << 16) | (tableNo << 10) | sequence;
    
2. 中间件选型
中间件 优势 适用场景
ShardingSphere 兼容性强,支持多种数据库 需要灵活路由规则的业务
MyCAT 成熟稳定,社区活跃 传统分库分表改造项目
Vitess Kubernetes原生,适合云环境 大规模MySQL集群管理

四、带宽与网络优化

1. 独立域名与DNS
  • 子域名隔离:将抢购接口(如buy.example.com)与常规业务分离,独立部署负载均衡。
  • DNS负载均衡:配置加权轮询(WRR)或基于地理位置的DNS解析。
2. 协议优化
  • HTTP/2多路复用:减少TCP连接数,提升传输效率。
  • QUIC协议:Google推出的基于UDP的低延迟协议,适用于高并发场景。
3. 边缘计算
  • CDN动态加速:通过边缘节点转发API请求,减少网络延迟。
  • WebSocket长连接:保持用户会话,减少重复握手开销。

五、小结

商品抢购阶段的架构设计需围绕三大核心展开:

  1. 流量削峰:通过异步队列与动态扩缩容应对瞬时高峰。
  2. 扣减库存:基于Redis集群与原子操作保障高并发下的数据一致性。
  3. 分库分表:结合基因分片与柔性事务实现海量订单存储。

订单支付阶段

在订单支付阶段,确保订单状态更新与异步通知的可靠性是核心挑战。要实现可靠消息投递机制的完整解决方案,可以结合本地消息表、幂等性设计与容错策略


一、支付回调阶段的可靠消息投递设计

1. 整体流程
支付平台 订单服务 本地消息表 MQ 积分服务 回调支付结果(含订单ID) 1. 开启事务 2. 更新订单状态为已支付 3. 插入消息记录(状态=待发送) 4. 返回成功响应 5. 异步发送消息(失败重试) 6. 消费消息,处理积分 7. 幂等性校验 8. 确认消费(ACK) 9. 更新消息状态=已处理 支付平台 订单服务 本地消息表 MQ 积分服务
2. 关键设计
  • 本地消息表与事务绑定
    将订单状态更新与消息记录插入放在同一数据库事务中,保证原子性。

    BEGIN TRANSACTION;
    UPDATE orders SET status = 'paid' WHERE order_id = '1001';
    INSERT INTO message_table (msg_id, order_id, status) 
    VALUES ('msg_001', '1001', 'pending');
    COMMIT;
    
  • 异步消息发送
    使用独立线程池或定时任务扫描本地消息表,将status=pending的消息发送到MQ。

    @Scheduled(fixedDelay = 5000)
    public void sendPendingMessages() {
        List<Message> messages = messageDao.selectPending();
        for (Message msg : messages) {
            mqProducer.send(msg);
            messageDao.updateStatus(msg.getId(), "sent");
        }
    }
    
  • 消息重试机制
    若MQ发送失败,通过指数退避策略重试(如首次1秒,第二次2秒,第三次4秒)。

3. 消息消费的幂等性
  • 唯一标识:为每条消息生成全局唯一ID(如msg_id),下游服务通过该ID判断是否已处理。

    public void handleMessage(Message msg) {
        if (redis.setnx("msg_dedup:" + msg.getId(), "1")) {
            addPoints(msg.getUserId(), msg.getPoints());
        }
    }
    
  • 业务状态校验
    处理消息前检查业务状态(如积分是否已到账)。

    SELECT * FROM user_points WHERE order_id = '1001';
    -- 若存在记录,则跳过处理
    

二、容错与异常处理

1. 支付回调接口的幂等性
  • 设计要点
    • 支付平台可能多次回调,需保证订单状态更新幂等。
    • 在更新订单状态前先查询当前状态。
    public void handlePaymentCallback(String orderId) {
        Order order = orderDao.select(orderId);
        if (order.getStatus().equals("paid")) {
            return; // 已处理,直接返回
        }
        // 处理支付逻辑
    }
    
2. 消息补偿机制
  • 场景:消息发送失败或消费失败。
  • 方案
    • 定时任务扫描:定期检查本地消息表中status=sent但未ACK的消息,重新发送。
    • 死信队列(DLQ):MQ将多次重试失败的消息转入DLQ,触发人工干预。
3. 数据库与MQ的一致性
  • 最终一致性保障
    • 本地消息表:确保消息至少被发送一次。
    • 消费者ACK机制:MQ在消息被成功处理后提交确认(如Kafka的enable.auto.commit=false)。

三、性能优化

1. 本地消息表设计优化
  • 分库分表:按订单ID分片,避免单表过大。
  • 读写分离:将消息表的查询操作路由到从库。
2. 批量处理
  • 消息批量发送:合并多条消息为一批发送,减少MQ调用次数。

    List<Message> batch = messages.subList(0, 100);
    mqProducer.sendBatch(batch);
    
  • 批量更新状态
    发送成功后批量更新消息状态为sent

    UPDATE message_table SET status = 'sent' 
    WHERE msg_id IN ('msg_001', 'msg_002', ...);
    
3. 异步化处理
  • 非阻塞IO:使用Netty或异步Servlet处理支付回调,避免线程阻塞。
  • 线程池隔离:核心业务(订单状态更新)与非核心业务(消息发送)使用独立线程池。

四、高可用与监控

1. 消息队列高可用
  • 集群部署:使用Kafka多副本机制,确保Broker故障时自动切换。
  • 持久化配置
    Kafka设置replication.factor=3min.insync.replicas=2
2. 监控告警
  • 关键指标
    • 消息积压量:MQ消费者Lag(如Kafka的consumer_lag)。
    • 处理延迟:从消息生产到消费的时间差。
    • 错误率:消息发送/消费失败的比例。
3. 熔断降级
  • 规则配置:当积分服务故障时,暂停消息消费并降级。
    // 使用Sentinel熔断
    @SentinelResource(
        value = "addPoints",
        fallback = "addPointsFallback",
        blockHandler = "addPointsBlockHandler"
    )
    public void addPoints(String userId, int points) { ... }
    

五、方案对比与选型

方案 优点 缺点 适用场景
本地消息表 强一致性,无外部依赖 数据库压力大 中小规模业务
RocketMQ事务消息 无侵入,天然支持分布式事务 依赖特定MQ,成本高 高并发、强一致性场景
最大努力通知 实现简单,资源消耗低 可能丢失消息 容忍最终一致性的非核心业务

小结

在订单支付阶段,通过本地消息表 + 异步重试 + 幂等性设计的组合方案,可有效解决支付回调与异步通知的可靠性问题。核心要点包括:

  1. 事务绑定:确保订单状态更新与消息记录原子性。
  2. 消息可靠投递:通过重试、ACK、死信队列实现最终一致性。
  3. 下游幂等:防止重复处理导致数据错误。
  4. 监控与容错:实时感知异常并触发补偿机制。

抢购后不支付导致的库存占用问题

用户提交订单抢到商品后,此时系统的库存已经扣减掉了,但是订单中的状态还是未支付,如果此时用户是恶意的行为,只抢购不支付,那么怎么优化架构设计来应对这样的操作?

一、预占库存与支付超时释放机制

1. 库存状态分层设计
库存系统
可售库存
预占库存
已售库存
  • 可售库存:前端展示的剩余数量,用户可见。
  • 预占库存:用户下单后临时占用的库存(支付超时后释放)。
  • 已售库存:支付成功后永久扣除的库存。
2. 预占库存实现流程
用户 订单服务 Redis 支付服务 定时任务 提交订单 预占库存(DECR可售库存,INCR预占库存) 操作成功 生成待支付订单 返回支付页面(含倒计时) 完成支付 扣减预占库存(DECR预占库存,INCR已售库存) 更新订单状态为已支付 扫描超时订单 释放预占库存(DECR预占库存,INCR可售库存) 标记订单为已取消 alt [超时未支付] 用户 订单服务 Redis 支付服务 定时任务
3. 关键技术实现
  • 库存预占原子性
    使用Redis Lua脚本保证库存操作的原子性:

    -- KEYS[1]=可售库存, KEYS[2]=预占库存
    local available = tonumber(redis.call('GET', KEYS[1]))
    if available <= 0 then
        return 0
    end
    redis.call('DECR', KEYS[1])
    redis.call('INCR', KEYS[2])
    return 1
    
  • 支付超时管理

    • 延迟队列:使用RabbitMQ死信队列(DLX)或RocketMQ延迟消息触发超时检查。
    • 定时任务分片:按订单ID哈希分片,分布式调度(如ElasticJob)避免单点压力。

二、恶意用户识别与拦截

1. 用户行为风控模型
指标 检测规则 处置措施
未支付订单率 用户近1小时未支付订单数 > 5 限制参与抢购1小时
设备指纹关联 同一设备生成 > 3个未支付订单 封禁设备ID
IP异常请求 单个IP来源的抢购请求频率 > 100次/分钟 IP限流或临时封禁
2. 实时风控系统架构
允许
拦截
用户请求
网关层
风控引擎
规则库: 频率/IP/设备
Flink实时计算
风险决策
业务系统
返回错误码
  • 技术选型
    • 流计算:Apache Flink实时统计用户行为指标。
    • 特征存储:Redis存储用户行为计数(如INCR user:1001:unpaid_orders)。
    • 决策引擎:Drools规则引擎动态加载风控策略。

三、支付倒计时与用户提醒

1. 前端体验优化
  • 倒计时同步
    前端定时从服务端同步剩余时间(避免客户端时间篡改):

    function syncCountdown() {
        fetch('/api/payment/timeleft?orderId=1001')
            .then(res => res.json())
            .then(data => {
                updateUI(data.remainingSeconds);
            });
    }
    // 每10秒同步一次
    setInterval(syncCountdown, 10000);
    
  • 多通道提醒

    • 站内信:用户登录时提示待支付订单。
    • 短信/邮件:支付截止前15分钟发送提醒。
2. 支付链路优化
  • 预创建支付单:在生成订单时预创建支付流水(如支付宝的alipay.trade.precreate),缩短支付跳转时间。
  • 支付状态主动查询:前端轮询支付结果,避免依赖异步回调延迟。

四、数据一致性保障

1. 最终一致性方案
  • 库存释放补偿任务
    定时扫描预占库存与订单状态,修复异常数据:
    -- 补偿任务SQL(伪代码)
    UPDATE inventory 
    SET available = available + reserved, 
        reserved = 0 
    WHERE item_id IN (
        SELECT item_id 
        FROM orders 
        WHERE status = 'unpaid' 
          AND create_time < NOW() - INTERVAL 30 MINUTE
    );
    
2. 分布式事务
  • Saga模式
    订单服务 库存服务 支付服务 预占库存(Saga起点) 预占成功 发起支付 确认扣减(Saga Commit) 取消预占(Saga Compensate) alt [支付成功] [支付超时/失败] 订单服务 库存服务 支付服务

五、监控与容灾

1. 核心监控指标
指标 监控方式 告警阈值
预占库存释放延迟 Prometheus + Grafana > 5分钟未释放
未支付订单占比 ELK日志分析 超过10%触发预警
风控规则命中率 实时Dashboard 单规则命中率突增50%
2. 熔断降级策略
  • 库存服务不可用
    降级为同步扣减数据库库存,事后通过对账修复。
  • 风控服务超时
    跳过风控检查,记录日志事后审计。

小结

通过预占库存机制 + 支付超时释放 + 实时风控拦截的组合方案,可有效解决恶意占库存问题:

  1. 资源隔离:预占库存与实际可售库存分离,避免恶意占用影响正常销售。
  2. 及时释放:通过延迟队列和定时任务确保超时订单快速释放。
  3. 行为管控:实时风控识别恶意用户,降低资源浪费。
  4. 体验优化:倒计时提醒与支付链路优化,提升用户支付率。

架构思维:预约抢茅子架构设计_第2张图片

你可能感兴趣的:(【架构思维】,架构,预约抢购)