🏗️ 高并发企业自营电商小程序系统架构设计:从需求分析到部署监控的全链路方案

从一个真实的架构评审说起

某团队接到一个需求:为公司开发一款自营电商小程序,C 端消费者可以在小程序上购买公司产品。预估用户量 5 万 ~ 10 万,产品部门希望赶在活动上线前交付。

架构师在评审会上画出了第一版方案:Spring Boot 单体应用 + MySQL + Redis 缓存。评审进行到一半,有人提出了一个问题:“如果 1 万个用户同时抢一个秒杀商品,这套架构能撑住吗?”

这个问题让团队陷入了沉默。单体应用的库存扣减在数据库层面是一条 UPDATE ... SET stock = stock - 1 WHERE stock > 0,在高并发下这条 SQL 会成为瓶颈——数据库行锁(InnoDB 行级锁,对某一行数据的排他锁定)会让所有请求串行化,QPS(Queries Per Second,每秒请求数)直接降到数据库单行更新的极限:约 500 ~ 1000。

⚠️ 新手提示:数据库行锁串行化的意思是,当 1000 个请求同时更新同一行数据(比如同一商品的库存),InnoDB 会让它们排队执行,每个请求必须等前一个提交后才能继续。这不是"慢",而是"一个一个来"——1000 个请求就是 1000 次串行操作。

这就是本文要解决的核心问题: 如何设计一套能支撑 2 万 QPS 的企业自营电商系统,同时保证库存不超卖、订单不丢失、支付不重复。

📌 前置知识:阅读本文需要了解 Spring Boot 基础、Redis 基本操作、MySQL 基本用法、消息队列(RocketMQ/Kafka)的基本概念。如果对微服务架构不熟悉,建议先了解服务注册与发现(Nacos)的基本概念。


🏗️ 一、需求分析与业务建模

🎯 1.1 业务范围界定

在设计任何系统之前,第一步是明确边界——知道什么要做什么不做。

维度内容
业务形态企业自营 B2C 电商小程序,企业是唯一商家,面向 C 端消费者
用户规模预估 1 万 ~ 10 万注册用户
峰值 QPS预估 1000 ~ 5000(设计目标:支撑 2 万 QPS)
核心功能商品浏览、下单、支付、退款、物流查询

这里有一个关键决策: 设计目标(2 万 QPS)远大于预估峰值(5000 QPS) 。这不是过度设计,而是为以下场景预留缓冲:

  • 秒杀类营销活动带来的瞬时流量尖刺
  • 企业规模增长带来的用户量膨胀
  • 缓存失效时的"惊群效应"(大量请求同时穿透缓存直达数据库)

🎯 1.2 核心业务流程

整个系统的核心链路可以抽象为一条主流程 + 一条回退流程:

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;;

    %% ==========================================
    %% 正向流程
    %% ==========================================
    subgraph MAIN_FLOW ["正向主流程"]
        START([用户进入小程序]) --> BROWSE[🔍 浏览商品]
        BROWSE --> CART[🛒 加入购物车]
        CART --> SUBMIT[📝 提交订单]
        SUBMIT --> DEDUCT[📉 扣减库存]
        DEDUCT --> CREATE[📋 创建订单]
        CREATE --> WAIT_PAY[⏳ 等待支付]
        WAIT_PAY --> CALLBACK[📩 支付回调]
        CALLBACK --> UPDATE[✅ 修改订单状态]
        UPDATE --> NOTIFY[📦 通知物流]
        NOTIFY --> CONFIRM[📬 用户确认收货]
        CONFIRM --> DONE([订单完成])
    end

    %% ==========================================
    %% 回退流程
    %% ==========================================
    subgraph ROLLBACK_FLOW ["回退流程"]
        WAIT_PAY --> CANCEL[❌ 取消订单/退款]
        CANCEL --> ROLLBACK[🔄 回滚库存]
        ROLLBACK --> CLOSE([订单关闭])
    end

    class START,DONE,CLOSE startEnd;
    class BROWSE,CART,SUBMIT,DEDUCT,CREATE,WAIT_PAY,CALLBACK,UPDATE,NOTIFY,CONFIRM process;
    class CANCEL,ROLLBACK reject;

⚠️ 新手提示:这张图展示的是"业务视角"的流程,每个节点在系统内部可能会拆成多个子步骤。比如"扣减库存"在实际系统中包含:生成幂等键 → Redis Lua 脚本原子扣减 → 记录库存流水 → 发送 MQ 消息异步同步到数据库。后面会逐一展开。

🔢 1.3 领域模型设计

领域驱动设计(DDD,Domain-Driven Design,通过将业务划分为独立的"领域"来组织代码和数据的架构方法)的核心是划定边界。本系统划分为六大领域:

flowchart LR
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;;
classDef branch fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa,font-weight:bold;;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;

    ROOT[企业自营电商 领域划分]

    ROOT --> D1(商品域)
    D1 --> D1_sub["📋 SPU / SKU / 类目 / 品牌 / 属性"]

    ROOT --> D2(库存域)
    D2 --> D2_sub["📊 库存分片 / 库存流水 / 库存快照"]

    ROOT --> D3(订单域)
    D3 --> D3_sub["📝 订单主表 / 订单明细 / 订单状态 / 订单日志"]

    ROOT --> D4(支付域)
    D4 --> D4_sub["💳 支付单 / 支付流水 / 退款单"]

    ROOT --> D5(用户域)
    D5 --> D5_sub["👤 用户信息 / 收货地址 / 购物车"]

    ROOT --> D6(物流域)
    D6 --> D6_sub["🚚 发货单 / 物流轨迹 / 签收记录"]

    class ROOT root;
    class D1,D2,D3,D4,D5,D6 branch;
    class D1_sub,D2_sub,D3_sub,D4_sub,D5_sub,D6_sub leaf;
领域核心聚合根职责边界
商品域SPU(Standard Product Unit,标准产品单元)商品信息管理、类目组织、属性定义
库存域库存分片库存的预扣、回滚、同步,所有库存变更必须经过此域
订单域订单订单生命周期管理,从创建到完成的所有状态流转
支付域支付单支付请求、回调处理、退款申请
用户域用户账号信息、收货地址维护
物流域发货单发货、物流轨迹追踪、签收确认

⚠️ 新手提示:SPU 和 SKU 是电商中最基础的两个概念。SPU 是"标准化产品单元",比如 iPhone 15 是一个 SPU;SKU 是"库存量单位",比如"iPhone 15 / 黑色 / 256G"是一个 SKU。一个 SPU 下可以有多个 SKU。库存扣减发生在 SKU 级别。


🏗️ 二、系统架构设计

🏗️ 2.1 整体架构分层

系统采用经典的微服务四层架构:客户端层 → 接入层 → 业务层 → 数据层,中间件层横向贯穿业务层与数据层。

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef client fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;;
classDef gateway fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;;
classDef service fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef middleware fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;;
classDef data fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;;

    %% ==========================================
    %% 四层架构
    %% ==========================================
    subgraph L1 ["客户端层"]
        MINI[微信小程序 / H5 / APP]
    end

    subgraph L2 ["接入层"]
        NGINX[Nginx + Lua 限流/黑白名单]
        GATEWAY[Spring Cloud Gateway 鉴权/路由/限流]
        NGINX --> GATEWAY
    end

    subgraph L3 ["业务层(微服务集群)"]
        direction LR
        USER[👤 用户服务]
        PRODUCT[📋 商品服务]
        ORDER[📝 订单服务]
        STOCK[📊 库存服务]
        PAY[💳 支付服务]
        CART_SVC[🛒 购物车服务]
        MARKET[📢 营销服务]
        LOGISTICS[🚚 物流服务]
        REVIEW[⭐ 评价服务]
    end

    subgraph L4 ["中间件层"]
        direction LR
        REDIS[Redis Cluster 缓存]
        MQ[RocketMQ 消息队列]
        ES[ElasticSearch 搜索引擎]
        JOB[XXL-Job 定时任务]
    end

    subgraph L5 ["数据层"]
        direction LR
        MYSQL[MySQL 8.0 主从 + 分库分表]
        CK[ClickHouse 实时分析]
        COS[对象存储 COS]
    end

    L1 --> L2
    GATEWAY --> L3
    L3 --> L4
    L4 --> L5
    L3 --> L5

    class MINI client;
    class NGINX,GATEWAY gateway;
    class USER,PRODUCT,ORDER,STOCK,PAY,CART_SVC,MARKET,LOGISTICS,REVIEW service;
    class REDIS,MQ,ES,JOB middleware;
    class MYSQL,CK,COS data;

每层的职责划分:

层次职责关键组件
客户端层用户交互界面,发起 HTTP 请求微信小程序 SDK
接入层限流、黑白名单、鉴权、路由转发Nginx + Lua、Spring Cloud Gateway
业务层核心业务逻辑处理,服务间通过 RPC 调用9 个独立微服务
中间件层缓存加速、异步解耦、全文搜索、定时调度Redis、RocketMQ、ES、XXL-Job
数据层持久化存储、分析查询、文件存储MySQL、ClickHouse、COS

🔢 2.2 技术选型清单

层次技术栈用途选型理由
开发框架Spring Boot 2.7+ / Spring Cloud 2021.x微服务基础框架生态成熟,团队熟悉
RPC 框架OpenFeign + Dubbo(可选)服务间远程调用Feign 声明式调用简洁;Dubbo 在高并发场景性能更优
注册中心Nacos服务发现、配置管理同时支持 AP 和 CP 模式,自带配置中心
网关Spring Cloud Gateway路由、鉴权、限流基于 WebFlux,异步非阻塞,性能优于 Zuul
缓存Redis Cluster / Caffeine分布式缓存 + 本地缓存Redis 集群保证高可用,Caffeine 减少网络开销
消息队列RocketMQ异步削峰、最终一致性支持事务消息、延迟消息,阿里系电商验证
数据库MySQL 8.0 + ShardingSphere-JDBC分库分表兼容 JDBC 标准,分片策略灵活
搜索引擎ElasticSearch商品搜索、订单搜索倒排索引,全文检索性能优秀
定时任务XXL-Job分布式任务调度可视化管控,支持分片广播
链路追踪SkyWalking调用链监控无代码侵入,Java Agent 自动探针
日志ELK日志收集分析ElasticSearch + Logstash + Kibana 黄金组合
监控Prometheus + Grafana指标监控告警云原生标准,生态丰富

🏗️ 三、核心业务流程详解

🔢 3.1 下单核心流程(时序图)

下单是整个系统中调用链最长、涉及服务最多的流程。下面是完整的时序交互:

sequenceDiagram
    %% ==========================================
    %% 参与者定义
    %% ==========================================
    participant U as 用户
    participant GW as 网关
    participant OS as 订单服务
    participant SS as 库存服务
    participant R as Redis
    participant DB as 数据库
    participant MQ as RocketMQ
    participant PS as 支付服务

    U->>GW: 提交订单请求
    GW->>GW: JWT 鉴权 + 限流检查
    GW->>OS: 转发订单请求

    OS->>OS: 生成订单号(雪花算法)
    OS->>SS: 请求预扣库存(productId,quantity,orderId)

    SS->>SS: 生成幂等键 deduct:{orderId}:{productId}
    SS->>R: SETNX 幂等键(60s过期)
    alt 幂等键已存在
        R-->>SS: 返回0(重复请求)
        SS-->>OS: 返回重复请求错误
    end

    SS->>R: 执行Lua脚本原子扣减
    alt 库存充足
        R-->>SS: 扣减成功
        SS->>MQ: 发送库存预扣成功消息
        SS-->>OS: 返回扣减成功
    else 库存不足
        R-->>SS: 扣减失败
        SS-->>OS: 返回库存不足
    end

    OS->>DB: 创建订单(状态:待支付)
    OS->>R: 设置订单过期时间(30分钟)
    OS->>PS: 请求创建支付单
    PS-->>OS: 返回支付链接/支付单号
    OS-->>GW: 返回订单ID + 支付信息
    GW-->>U: 返回下单结果

关键步骤详解

步骤 1:生成订单号。使用雪花算法(Snowflake,Twitter 开源的分布式 ID 生成算法,基于时间戳 + 机器 ID + 序列号组成 64 位唯一 ID),保证全局唯一且趋势递增。订单号的格式通常为:时间戳(41bit)+ 机器 ID(10bit)+ 序列号(12bit)。

步骤 2:幂等键设计。幂等键的格式为 deduct:{orderId}:{productId},使用 Redis 的 SETNX(SET if Not eXists,仅当 key 不存在时才设置)命令,保证同一个订单的同一个商品只会被扣减一次。60 秒过期是为了防止极端情况下 key 残留。

⚠️ 新手提示:幂等(Idempotent)是指同一个操作执行一次和执行多次的结果完全一样。比如"扣减库存"这个操作,如果因为网络超时用户重试了 3 次,但库存只应该被扣 1 次——这就是幂等的价值。

步骤 3:Redis Lua 脚本原子扣减。Lua 脚本在 Redis 中执行时是原子的(Atomic,不可分割的,要么全部执行要么全部不执行),不会有其他命令插入。这保证了"判断库存是否充足 + 执行扣减"两个操作之间不会出现竞态条件(Race Condition,多个线程/进程的执行顺序不确定导致的错误)。

步骤 4:订单过期时间。订单创建后设置 30 分钟过期时间(Redis key 的 TTL),如果 30 分钟内未支付,订单自动取消并回滚库存。

🔢 3.2 库存扣减详细设计

库存扣减是整个系统中最容易出问题的环节。核心矛盾是: 高并发下如何保证不超卖,同时不让性能成为瓶颈。

🔢 3.2.1 Redis 库存分片

如果所有请求都去更新同一个 Redis key(如 stock:product:12345),这个 key 会成为热点。Redis 虽然是单线程模型(6.0 之后引入了 IO 多线程,但命令执行仍然是单线程),但热点 key 会导致单分片压力过大。

解决方案是 库存分片 :将同一个 SKU 的总库存拆成 N 个分片,请求随机或轮询选择一个分片扣减。

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;;

    START([接收扣减请求\nproductId, quantity, orderId])

    START --> GENKEY[生成幂等键\ndeduct:orderId:productId]

    GENKEY --> SETNX{SETNX 幂等键\n60s过期}

    SETNX -- 失败(已存在) --> DUP([返回: 重复请求])

    SETNX -- 成功 --> RAND["随机选择分片索引\nshardIndex = random(1,N)"]

    RAND --> LUA["执行 Lua 脚本\n扣减 stock:product:shardIndex"]

    LUA --> CHECK{扣减结果?}

    CHECK -- 成功 --> MQ[发送库存预扣成功消息到MQ]
    MQ --> MARK[标记分片已变更\nSADD changed_shards]
    MARK --> SUCCESS([返回: 扣减成功])

    CHECK -- 库存不足 --> RETRY{已尝试次数\n< 3 ?}

    RETRY -- 是 --> NEXT[选择下一个分片]
    NEXT --> LUA

    RETRY -- 否 --> DELKEY[删除幂等键]
    DELKEY --> FAIL([返回: 库存不足])

    class START,SUCCESS,FAIL,DUP startEnd;
    class SETNX,CHECK,RETRY condition;
    class GENKEY,RAND,MARK,NEXT,DELKEY process;
    class LUA,MQ data;

🔢 3.2.2 核心代码实现

Lua 脚本(Redis 原子扣减)

-- KEYS[1]: 库存分片的 key,如 stock:product:12345:3
-- ARGV[1]: 扣减数量
-- ARGV[2]: 变更分片集合的 key,如 changed_shards
-- 返回值: 1=扣减成功, 0=库存不足

local stock = redis.call('GET', KEYS[1])
if not stock then
    return 0
end

stock = tonumber(stock)
local quantity = tonumber(ARGV[1])

if stock >= quantity then
    redis.call('DECRBY', KEYS[1], quantity)
    redis.call('SADD', ARGV[2], KEYS[1])
    return 1
else
    return 0
end

Java 端库存扣减服务

@Service
public class StockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // Lua 脚本的 SHA1 摘要,避免每次传输完整脚本
    private String luaSha;

    @PostConstruct
    public void init() {
        // 脚本加载到 Redis,获取 SHA1 摘要
        String script = "local stock = redis.call('GET', KEYS[1])\n" +
            "if not stock then return 0 end\n" +
            "stock = tonumber(stock)\n" +
            "local quantity = tonumber(ARGV[1])\n" +
            "if stock >= quantity then\n" +
            "    redis.call('DECRBY', KEYS[1], quantity)\n" +
            "    redis.call('SADD', ARGV[2], KEYS[1])\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        luaSha = redisTemplate.getRequiredConnectionFactory()
            .getConnection().scriptLoad(script.getBytes());
    }

    public boolean deductStock(Long productId, int quantity, String orderId) {
        // 1. 幂等检查
        String idempotentKey = "deduct:" + orderId + ":" + productId;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(idempotentKey, "1", Duration.ofSeconds(60));
        if (Boolean.FALSE.equals(locked)) {
            throw new BizException("重复的扣减请求");
        }

        // 2. 随机选择分片,最多重试 3 次
        int shardCount = 10; // 每个 SKU 分 10 片
        int maxRetry = 3;
        for (int i = 0; i < maxRetry; i++) {
            int shardIdx = ThreadLocalRandom.current().nextInt(shardCount);
            String stockKey = "stock:product:" + productId + ":" + shardIdx;

            Long result = redisTemplate.execute(
                new DefaultRedisScript<>(luaSha, Long.class),
                Collections.singletonList(stockKey),
                String.valueOf(quantity),
                "changed_shards"
            );

            if (result != null && result == 1) {
                // 3. 发送 MQ 消息,异步同步到数据库
                sendStockDeductedMsg(productId, quantity, orderId, shardIdx);
                return true;
            }
        }

        // 扣减失败,清理幂等键
        redisTemplate.delete(idempotentKey);
        return false;
    }
}

设计要点

  1. 分片数如何确定:分片数 N 建议等于 Redis Cluster 的主节点数,这样每个节点均匀承载一个分片。如果预计热点严重,可以将分片数设为节点数的 2 ~ 3 倍。
  2. 分片间的库存分配:初始化时,将 SKU 总库存尽可能均匀分配到各分片。如果某个分片库存用完,请求会尝试下一个分片(最多 3 次)。
  3. Lua 脚本的 SHA 优化:使用 SCRIPT LOAD 提前将脚本加载到 Redis,后续使用 SHA1 摘要调用 EVALSHA,避免每次传输完整脚本内容。

⚠️ 新手提示:为什么是"最多尝试 3 次"而不是把所有分片都试一遍?因为如果大部分分片库存都已耗尽,继续遍历只是在浪费 Redis 的 CPU 时间。3 次是工程上的经验值——既能处理分片库存不均的问题,又不会引入过多延迟。

🔢 3.3 订单状态机设计

订单是系统的核心实体,其状态变迁必须用状态机来严格约束。每次状态变更都通过 CAS(Compare And Swap,比较并交换,先比较当前值是否符合预期,符合才更新)保证并发安全。

stateDiagram-v2
    %% ==========================================
    %% 订单状态机(10 个状态)
    %% ==========================================

    [*] --> 待支付: 创建订单 + 库存预扣成功

    待支付 --> 已取消: 超时未支付(30分钟)\n或用户主动取消
    待支付 --> 支付中: 用户发起支付
    待支付 --> 已过期: 系统取消(活动结束等)

    支付中 --> 已支付: 支付回调成功
    支付中 --> 待支付: 支付超时/失败

    已支付 --> 待发货: 支付确认完成

    待发货 --> 已发货: 仓库发货

    已发货 --> 待收货: 物流发出

    待收货 --> 已完成: 用户确认收货\n或自动确认(15天)
    待收货 --> 退款中: 用户申请退款

    已完成 --> 退款中: 用户申请售后

    退款中 --> 已退款: 退款成功
    退款中 --> 已完成: 退款审核不通过

    已退款 --> [*]
    已取消 --> [*]
    已完成 --> [*]
    已过期 --> [*]

订单状态表

状态码状态名说明可流转到的状态
0待支付订单创建成功,等待用户支付已取消、支付中、已过期
1支付中用户已发起支付,等待回调已支付、待支付
2已支付支付确认成功待发货
3待发货支付完成,等待仓库发货已发货、退款中
4已发货仓库已出库待收货、退款中
5待收货物流运输中已完成、退款中
6已完成用户确认收货退款中
7已取消超时或用户主动取消终态
8已过期系统取消(活动结束等)终态
9退款中退款流程进行中已退款、已完成
10已退款退款完成终态

CAS 状态更新示例

// 支付回调时更新订单状态:只有当前状态为"支付中"才能更新为"已支付"
String updateSql = "UPDATE t_order SET status = ?, pay_status = ?, pay_time = NOW() "
    + "WHERE order_no = ? AND status = ?";

int affected = jdbcTemplate.update(updateSql,
    OrderStatus.PAID.getCode(),       // 新状态:已支付
    PayStatus.PAID.getCode(),         // 新支付状态:已支付
    orderNo,
    OrderStatus.PAYING.getCode()      // 期望的当前状态:支付中
);

if (affected == 0) {
    // 更新失败:说明订单状态已被其他操作改变
    // 可能的情况:订单已过期取消、并发退款已触发
    // 需要查询当前状态,决定下一步动作
}

CAS 更新的关键在于 WHERE status = ?,这个条件保证了只有当前状态符合预期时才会执行更新。如果 affected = 0,说明在读取和写入之间,状态已经被其他操作改变了,需要走异常处理分支。


🏗️ 四、支付回调处理详解

🔢 4.1 支付回调的五大挑战

支付回调是系统中最复杂的环节之一,因为它涉及外部系统(微信支付/支付宝)的交互,且直接与资金相关。设计时必须同时考虑以下五个问题:

挑战触发场景核心矛盾
重复回调支付网关因网络超时重发回调同一笔支付可能收到多次通知
订单过期支付用户在订单过期前最后一秒完成支付订单已取消但支付已成功
并发冲突支付回调与退款同时到达两个操作都想修改同一个订单状态
回调失败重试系统内部处理失败必须保证最终一致性
后续流程编排支付成功后要触发库存确认、物流通知等多个下游服务需要协调

🔢 4.2 支付回调完整流程

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;;

    START([支付网关回调]) --> VERIFY[验证签名\nRSA/SM2 验签]

    VERIFY --> CHECK_SIGN{签名\n正确?}
    CHECK_SIGN -- 否 --> BAD_SIGN([返回失败\n让网关重试])

    CHECK_SIGN -- 是 --> QUERY_ORDER[查询本地支付单]

    QUERY_ORDER --> CHECK_PAY{支付单\n状态?}

    CHECK_PAY -- 已处理 --> DUP_RESP([返回成功\n幂等:不重复处理])

    CHECK_PAY -- 未处理 --> IDEM_KEY[获取支付单幂等锁\nSETNX pay:lock:payNo]

    IDEM_KEY --> CHECK_LOCK{获取\n成功?}
    CHECK_LOCK -- 否(并发冲突) --> RETRY_LATER([返回失败\n稍后重试])

    CHECK_LOCK -- 是 --> CAS_ORDER[CAS更新订单状态\nWHERE status=支付中]

    CAS_ORDER --> CHECK_CAS{更新\n行数?}

    CHECK_CAS -- affected=0 --> CHECK_EXP{查询订单\n当前状态?}

    CHECK_EXP -- 已取消/已过期 --> REFUND[发起自动退款\n通知支付网关退款]
    REFUND --> RELEASE[释放幂等锁]
    RELEASE --> EXP_END([返回成功\n已发起退款])

    CHECK_EXP -- 其他异常 --> ALERT[人工介入告警]
    ALERT --> RELEASE2[释放幂等锁]
    RELEASE2 --> ERR_END([返回成功\n已告警])

    CHECK_CAS -- affected=1(成功) --> UPDATE_PAY[更新支付单状态=已支付]

    UPDATE_PAY --> SEND_MQ[发送支付成功MQ消息\ntopic: order_paid]

    SEND_MQ --> NOTIFY_STOCK[通知库存服务:\n确认库存预扣→实际扣减]

    NOTIFY_STOCK --> NOTIFY_LOG[通知物流服务:\n创建发货单]

    NOTIFY_LOG --> RELEASE3[释放幂等锁]

    RELEASE3 --> SUCCESS([返回成功])

    class START,SUCCESS,DUP_RESP,BAD_SIGN,RETRY_LATER,EXP_END,ERR_END startEnd;
    class CHECK_SIGN,CHECK_PAY,CHECK_LOCK,CHECK_CAS,CHECK_EXP condition;
    class VERIFY,QUERY_ORDER,REFUND,ALERT,UPDATE_PAY,NOTIFY_STOCK,NOTIFY_LOG,RELEASE,RELEASE2,RELEASE3 process;
    class IDEM_KEY,CAS_ORDER,SEND_MQ data;
    class RELEASE,RELEASE2,RELEASE3 reject;

五个挑战的解决思路

(1)防止重复支付:三层防护——① 支付网关侧的去重(基于支付单号);② 本地支付单状态判断(CHECK_PAY);③ Redis 幂等锁(SETNX pay:lock:payNo),保证同一个支付单的回调在同一时刻只有一个线程在处理。

(2)订单过期支付(用户卡点支付):CAS 更新 WHERE status = 支付中 。如果订单已超时取消(状态变为"已取消"),affected = 0 ,触发自动退款。这一步的逻辑是: 钱已经收到了,但订单已经不存在了,所以必须把钱退回去

(3)并发下支付和退款冲突:利用数据库行锁 + CAS 更新。无论是支付回调还是退款请求,都通过 WHERE status = ? 进行乐观锁竞争,谁先更新成功谁就获得了处理权。

(4)回调失败重试:支付网关侧通常有指数退避重试策略(如 1min → 5min → 30min → 1h)。本地侧通过 返回失败 → 网关重试 的机制实现最终一致性。

(5)支付成功后后续流程:通过 MQ 消息异步触发,保证支付回调接口快速返回(避免超时导致网关重试),同时解耦支付服务与库存、物流等下游服务。

⚠️ 新手提示:为什么支付回调要通过 MQ 来触发后续流程,而不是在回调处理中直接调用库存服务和物流服务?原因有二:① 如果直接同步调用,库存服务或物流服务响应慢会导致支付回调超时,支付网关会认为通知失败并重试,造成重复处理;② MQ 天然具备重试能力,如果消费失败可以自动重试,不需要在支付回调处理逻辑中自己写重试代码。


🏗️ 五、极端情况处理与数据一致性

🎯 5.1 异常场景全景

高并发分布式系统中,异常是常态而非意外。下面清单覆盖了从基础设施故障到业务边界的全部异常场景:

异常场景触发条件影响范围解决方案
网络超时Redis 扣减成功但 TCP 响应丢失库存已扣但订单未创建幂等键防重复 + 定时对账补偿
Redis 宕机缓存集群主节点故障库存扣减不可用降级到数据库扣减 + 本地缓存缓冲
订单过期支付用户在过期前最后一秒完成支付订单已取消但钱已付CAS 状态检查 + 自动发起退款
重复支付用户多次点击支付按钮同一笔订单多次扣款支付单幂等 + 支付网关侧去重
发货后退款物流已发出后用户申请退款商品在路上但已退款拦截物流 + 退款流程与退货流程分离
MQ 消息丢失Broker 宕机或网络分区库存同步中断生产者确认机制 + 消费者手动 ACK + 定时对账兜底
数据库死锁高并发下多个事务互相等待订单创建失败重试机制(Spring Retry)+ 乐观锁替代悲观锁
服务雪崩下游服务响应变慢 → 上游级联超时整个系统不可用Sentinel 熔断降级 + 限流 + 线程池隔离

🗄️ 5.2 数据一致性保障方案

在分布式系统中,强一致性(所有节点在同一时刻看到的数据完全相同)代价极高。本系统采用 最终一致性 (允许短暂不一致,但最终会达到一致状态)策略,通过三个典型场景说明:

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;;

    %% ==========================================
    %% 场景一
    %% ==========================================
    subgraph S1 ["场景一: Redis扣减成功 但订单创建失败"]
        S1A[Redis Lua 扣减成功] --> S1B[发送库存回滚延迟消息\ndelay: 10秒]
        S1B --> S1C{10秒内订单\n创建成功?}
        S1C -- 是 --> S1D[取消回滚消息\n不做任何操作]
        S1C -- 否 --> S1E[执行库存回滚\nRedis INCRBY 恢复库存]
    end

    %% ==========================================
    %% 场景二
    %% ==========================================
    subgraph S2 ["场景二: 支付成功 但订单状态更新失败"]
        S2A[支付回调到达] --> S2B[CAS 更新订单状态]
        S2B --> S2C{affected > 0?}
        S2C -- 否 --> S2D[支付回调重试\n指数退避: 1min/5min/30min]
        S2D --> S2B
        S2C -- 多次重试仍失败 --> S2E[人工对账介入\n查询支付网关确认款项]
    end

    %% ==========================================
    %% 场景三
    %% ==========================================
    subgraph S3 ["场景三: 库存同步到DB失败"]
        S3A[定时任务扫描\nchanged_shards集合] --> S3B[批量读取Redis分片库存]
        S3B --> S3C[UPDATE stock SET quantity=Redis值]
        S3C --> S3D{更新成功?}
        S3D -- 否 --> S3E[记录失败队列]
        S3E --> S3F[重试最多3次]
        S3F --> S3G{仍失败?}
        S3G -- 是 --> S3H[告警 + 人工处理]
        S3G -- 否 --> S3I[从changed_shards移除]
        S3D -- 是 --> S3I
    end

    class S1D,S3I startEnd;
    class S1C,S2C,S3D,S3G condition;
    class S1A,S1E,S2B,S2D,S3B,S3C,S3E,S3F process;
    class S2E,S3H reject;
    class S1B,S2A,S3A data;

场景一(库存补偿)的核心代码思路

// 下单服务:先扣Redis库存,再创建订单
public Order createOrder(CreateOrderRequest req) {
    // 1. Redis 扣减库存
    boolean deducted = stockService.deductStock(
        req.getProductId(), req.getQuantity(), req.getOrderNo());

    if (!deducted) {
        throw new BizException("库存不足");
    }

    try {
        // 2. 创建订单
        Order order = orderMapper.insert(buildOrder(req));

        // 3. 订单创建成功,发送"确认扣减"消息,取消之前的回滚延迟消息
        // (这里不做任何事即可,因为回滚消息收到后会检查订单是否存在)
        return order;

    } catch (Exception e) {
        // 4. 订单创建失败,发送延迟回滚消息(10秒后执行)
        StockRollbackMsg msg = new StockRollbackMsg();
        msg.setProductId(req.getProductId());
        msg.setQuantity(req.getQuantity());
        msg.setOrderNo(req.getOrderNo());

        rocketMQTemplate.syncSend(
            "stock-rollback-topic",
            MessageBuilder.withPayload(msg).build(),
            3000,  // 超时
            4      // 延迟级别: 10秒后
        );
        throw new BizException("订单创建失败,库存将在10秒后回滚");
    }
}

// 回滚消息消费者
@RocketMQMessageListener(topic = "stock-rollback-topic", consumerGroup = "stock-rollback")
public class StockRollbackConsumer implements RocketMQListener<StockRollbackMsg> {
    @Override
    public void onMessage(StockRollbackMsg msg) {
        // 先检查订单是否已创建(因为有可能订单创建比回滚消息晚)
        Order order = orderMapper.selectByOrderNo(msg.getOrderNo());
        if (order != null) {
            // 订单已存在,不需要回滚
            return;
        }
        // 订单不存在,执行库存回滚
        stockService.rollbackStock(msg.getProductId(), msg.getQuantity());
    }
}

⚠️ 新手提示:为什么延迟消息是 10 秒而不是立即回滚?因为订单创建可能在 Redis 扣减之后才成功(比如数据库连接池正在等待连接),给 10 秒的缓冲时间让订单有机会创建完成。这是一种"补偿"而非"回滚"的设计思路:先乐观地假设订单能创建成功,失败了再补偿。

🛡️ 5.3 降级策略

当 Redis 不可用时,系统必须有能力降级运行:

Redis 可用 → Redis Lua 脚本扣减(正常路径,QPS 最高)
    ↓ 不可用
Caffeine 本地缓存 → 本地缓存热点库存(降级一级,QPS 受限)
    ↓ 不可用/缓存未命中
MySQL 数据库 → 直接数据库扣减(降级二级,QPS 大幅降低)
    ↓ 数据库压力过大
Sentinel 限流 → 拒绝部分请求,保护系统(降级三级,部分用户不可用)

降级通过 Sentinel(流量防卫兵)自动触发。当 Redis 异常比例超过阈值时,自动切换降级逻辑:

@SentinelResource(
    value = "deductStock",
    fallback = "deductStockFallback",    // 降级后的兜底方法
    blockHandler = "deductStockBlockHandler"  // 限流后的处理方法
)
public boolean deductStock(Long productId, int quantity, String orderNo) {
    // 正常路径:Redis Lua 脚本扣减
    return redisStockDeduct(productId, quantity, orderNo);
}

// 降级方法:Redis 不可用时走数据库
public boolean deductStockFallback(Long productId, int quantity,
                                    String orderNo, Throwable t) {
    log.warn("Redis库存扣减降级,切换到数据库扣减. orderNo={}", orderNo, t);
    return dbStockDeduct(productId, quantity);
}

// 限流方法:系统过载时拒绝请求
public boolean deductStockBlockHandler(Long productId, int quantity,
                                        String orderNo, BlockException e) {
    throw new BizException("系统繁忙,请稍后重试");
}

🏗️ 六、定时任务设计

定时任务是保证最终一致性的关键机制。这些任务运行在 XXL-Job 分布式调度平台上,通过分片广播确保不会重复执行。

sequenceDiagram
    participant XXL as XXL-Job 调度中心
    participant W1 as 执行器节点1
    participant W2 as 执行器节点2
    participant R as Redis
    participant DB as 数据库

    Note over XXL,DB: 库存同步任务(每30秒执行一次)

    XXL->>W1: 分片广播: shard=0,total=2
    XXL->>W2: 分片广播: shard=1,total=2

    W1->>R: SMEMBERS changed_shards (取一半)
    W2->>R: SMEMBERS changed_shards (取另一半)

    W1->>R: GET 每个分片的库存值
    W2->>R: GET 每个分片的库存值

    W1->>DB: 批量 UPDATE stock 表
    W2->>DB: 批量 UPDATE stock 表

    W1->>R: SREM changed_shards (已同步的分片)
    W2->>R: SREM changed_shards (已同步的分片)

四个核心定时任务

任务名称执行频率功能描述关键逻辑
库存同步任务每 30 秒扫描 changed_shards 集合,批量更新数据库库存分片广播避免重复处理
订单超时取消每 1 分钟扫描待支付超时订单,取消 + 回滚库存游标分页避免深分页问题
对账任务每天凌晨 2 点核对 Redis 库存与数据库库存差异汇总各分片 Redis 值,与 DB SUM 对比
支付结果查询每 10 分钟查询 pending 状态支付单,主动向支付网关对账只查询超过一定时间(如 5 分钟)仍为 pending 的单

订单超时取消任务示例

@Component
@JobHandler("orderTimeoutCancelJob")
public class OrderTimeoutCancelJob extends IJobHandler {

    private static final int BATCH_SIZE = 200;
    private static final int ORDER_TIMEOUT_MINUTES = 30;

    @Override
    public ReturnT<String> execute(String param) {
        // 使用游标分页,避免 offset 深分页性能问题
        Long lastOrderId = 0L;
        while (true) {
            List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(
                OrderStatus.PENDING_PAY.getCode(),
                LocalDateTime.now().minusMinutes(ORDER_TIMEOUT_MINUTES),
                lastOrderId,
                BATCH_SIZE
            );

            if (CollectionUtils.isEmpty(timeoutOrders)) {
                break;
            }

            for (Order order : timeoutOrders) {
                // CAS 更新状态
                int affected = orderMapper.updateStatus(
                    order.getOrderNo(),
                    OrderStatus.CANCELLED.getCode(),
                    OrderStatus.PENDING_PAY.getCode() // 只有待支付才能取消
                );

                if (affected > 0) {
                    // 回滚库存
                    stockService.rollbackStock(
                        order.getProductId(), order.getQuantity());
                }
                lastOrderId = order.getId();
            }
        }
        return ReturnT.SUCCESS;
    }
}

⚠️ 新手提示:为什么定时任务要用游标分页而不是 LIMIT offset, size?因为当扫描到数据量很大时(比如有几百万条超时订单),LIMIT 100000, 200 需要数据库扫描并跳过前 10 万行,性能极差。游标分页(WHERE id > lastId ORDER BY id LIMIT 200)可以利用主键索引直接定位,无论数据量多大,每次扫描都是常量时间。


🏗️ 七、数据库设计

📐 7.1 核心表结构

订单主表(分片键:user_id):

CREATE TABLE `t_order` (
  `id` bigint NOT NULL COMMENT '订单ID(雪花算法生成)',
  `order_no` varchar(32) NOT NULL COMMENT '订单号(展示用)',
  `user_id` bigint NOT NULL COMMENT '用户ID(分片键)',
  `total_amount` decimal(10,2) NOT NULL COMMENT '商品总金额',
  `discount_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '优惠金额',
  `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
  `status` tinyint NOT NULL COMMENT '订单状态: 0-待支付 1-支付中 2-已支付 3-待发货 4-已发货 5-待收货 6-已完成 7-已取消 8-已过期 9-退款中 10-已退款',
  `pay_status` tinyint NOT NULL COMMENT '支付状态: 0-未支付 1-支付中 2-已支付 3-已退款',
  `delivery_status` tinyint NOT NULL COMMENT '物流状态: 0-未发货 1-已发货 2-已签收 3-已退回',
  `delivery_address_id` bigint DEFAULT NULL COMMENT '收货地址ID',
  `remark` varchar(255) DEFAULT NULL COMMENT '用户备注',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `pay_time` datetime DEFAULT NULL COMMENT '支付完成时间',
  `delivery_time` datetime DEFAULT NULL COMMENT '发货时间',
  `finish_time` datetime DEFAULT NULL COMMENT '完成时间',
  PRIMARY KEY (`id`, `user_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_status_created` (`status`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

库存流水表(分片键:product_id):

CREATE TABLE `t_stock_flow` (
  `id` bigint NOT NULL COMMENT '雪花算法ID',
  `product_id` bigint NOT NULL COMMENT '商品ID(分片键)',
  `sku_id` bigint NOT NULL COMMENT 'SKU ID',
  `change_type` tinyint NOT NULL COMMENT '变更类型: 1-预扣 2-确认扣减 3-回滚 4-初始化 5-调整',
  `quantity` int NOT NULL COMMENT '变更数量(正数为增,负数为减)',
  `before_stock` int NOT NULL COMMENT '变更前库存',
  `after_stock` int NOT NULL COMMENT '变更后库存',
  `order_no` varchar(32) DEFAULT NULL COMMENT '关联订单号',
  `source` varchar(32) NOT NULL COMMENT '变更来源: ORDER/DEDUCT/ROLLBACK/SYNC',
  `operator` varchar(64) DEFAULT NULL COMMENT '操作人',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`, `product_id`),
  KEY `idx_product_id` (`product_id`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

⚠️ 新手提示:t_stock_flow 是一张"流水表",不直接存储当前库存值,而是记录每一次库存变更。当前库存 = Redis 中的实时值,数据库中的库存表(t_stock)由定时任务从 Redis 同步。流水表的作用是审计和对账:如果 Redis 和数据库出现了不一致,可以通过流水表追溯每一笔变更。

🔢 7.2 分库分表策略

表名分片键分片策略分片数选键理由
t_orderuser_id哈希取模8 库 × 16 表用户维度查询最多(我的订单列表),按 user_id 分片让订单查询只访问一个分片
t_order_itemorder_id哈希取模8 库 × 16 表订单明细总是伴随订单查询,与 t_order 同分片策略保证关联查询在同一个库
t_stock_flowproduct_id哈希取模4 库 × 8 表商品维度的库存查询最频繁,按 product_id 分片减少跨分片扫描

关键设计决策: t_order 的 PRIMARY KEY 是 (id, user_id) 而非 (id)(user_id, id) 。这是因为 ShardingSphere 需要在主键中包含分片键,这样才能在不知道 id 属于哪个分片的情况下,通过 user_id 定位到正确的分片。


🏗️ 八、部署与监控

🏗️ 8.1 部署架构

生产环境采用 Kubernetes(容器编排平台)部署,所有服务以 Deployment 方式运行,通过 Service 暴露,Ingress 对外提供访问入口。

flowchart TD
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef infra fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;;
classDef svc fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa,font-weight:bold;;
classDef mid fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;;

    subgraph K8S ["K8s 集群 - Namespace: ecommerce-prod"]
        subgraph SVC_GROUP ["业务服务"]
            GW[网关服务\n3副本 × 2C4G]
            ORDER[订单服务\n5副本 × 4C8G]
            STOCK[库存服务\n3副本 × 4C8G]
            PAY[支付服务\n2副本 × 2C4G]
            PRODUCT[商品服务\n5副本 × 4C8G]
        end

        subgraph MID_GROUP ["中间件(独立集群/物理机)"]
            REDIS_CLUSTER[Redis Cluster\n3主3从 × 16G]
            MQ_CLUSTER[RocketMQ\n4主4从]
            MYSQL_CLUSTER[MySQL\n8分片 × 主从]
        end
    end

    class GW,ORDER,STOCK,PAY,PRODUCT svc;
    class REDIS_CLUSTER,MQ_CLUSTER,MYSQL_CLUSTER mid;

各服务资源配置说明

服务副本数资源理由
网关32C4G纯 IO 转发,CPU 需求低
订单服务54C8G核心业务,高并发下需要充足 CPU 处理业务逻辑
库存服务34C8GIO 密集型(Redis + MQ),但非核心路径,3 副本足够
支付服务22C4G低频调用,但必须保证高可用
商品服务54C8G查询量最大(首页、列表、详情),多副本应对读流量

📊 8.2 监控指标体系

指标类型具体指标告警阈值告警级别处理动作
业务下单成功率< 95%P1检查库存服务、订单服务日志
业务支付成功率< 98%P1检查支付网关连通性
业务平均响应时间> 200msP2检查慢查询、GC 日志
系统CPU 使用率> 80%P2扩容或限流
系统JVM 堆内存> 85%P2检查内存泄漏、调大堆
系统GC 停顿时间> 500msP3检查 GC 日志
中间件Redis 命中率< 90%P2检查缓存策略
中间件MQ 消息堆积量> 10 万P1检查消费者健康状态
中间件DB 连接池使用率> 80%P2检查慢查询、连接泄漏

🏗️ 九、架构设计精髓总结

设计点核心思路解决的问题
库存分片Redis 分片 + 随机选择分散热点请求单 key 热点导致单 Redis 节点过载
异步同步定时任务批量将 Redis 变更同步到 DB减少 DB 实时写压力,将 TPS 从"请求级"降到"定时批处理级"
状态机 + CAS订单状态流转全部通过 CAS 更新并发场景下状态变更的原子性保证
延迟消息补偿关键操作失败后发送延迟回滚消息跨服务的数据最终一致性
多级降级Redis → Caffeine → MySQL → 限流逐级保护,避免单点故障导致全链路崩溃
过期支付退款CAS 检查订单状态,已取消则自动退款处理"卡点支付"的边界场景

🏗️ 十、总结

本文从需求分析到部署监控,完整覆盖了企业自营 B2C 电商小程序的架构设计全链路。核心脉络可以用一句话概括: 以 Redis 分片库存扣减为性能引擎,以订单状态机 + CAS 为一致性保障,以 MQ 延迟消息为补偿手段,以多级降级为容错防线。

对于实际项目落地,建议按照以下优先级推进:

  1. 先跑通核心链路:下单 → 支付 → 发货,这个链路跑通了系统就有价值
  2. 再补异常处理:超时取消、退款、对账,这些是保证资金安全的底线
  3. 最后做性能优化:库存分片、多级缓存、读写分离,这些是锦上添花

架构设计没有银弹,这里给出的方案是基于特定约束(企业自营 B2C 电商、5 万 ~ 10 万用户、2 万 QPS 设计目标)下的最优解。如果约束条件变化(如用户量达到百万级、业务从自营扩展到多商家平台),架构也需要相应演进。


📌 如果读者对文中涉及的具体技术点想深入了解,建议按以下顺序阅读本博客系列的相关文章:Redis 缓存策略 → RocketMQ 消息可靠性 → ShardingSphere 分库分表实战 → Spring Cloud Gateway 限流熔断配置。