简介:源自京东商城的超大型电商系统架构设计方案,面向电商平台架构师、技术负责人及中大型系统设计者,为构建高效、可靠、可扩展的电商平台提供系统化架构参考。资料完整覆盖架构目标、业务架构设计原则、应用架构设计原则、数据架构设计原则、技术架构总览与系统运维原则,并深入拆解了京东应用架构分层、水平/垂直拆分、服务依赖设计等关键实践。方案对高可用指标(整体系统99.99%、单系统99.999%)、主流程与辅流程分离、核心与非核心业务隔离、异步解耦等策略均有具体说明,同时涉及订单表按ID取模分库分表、接口幂等性、无状态设计等落地细节,可帮助读者理解超大型电商平台如何在复杂业务场景下维持稳定与弹性。资源为单个PDF文件,大小2.51MB,已有186人学习,适合用于系统设计评审、技术方案预研及架构能力进阶参考。
1. 超大型电商系统架构设计方案:先别急着画拓扑图,把账算清楚
拿到“超大型电商系统架构设计方案”这个标题,大多数人第一反应是打开绘图工具开始画网关、服务、数据库的拓扑图。我见过不少方案文档,几十页 PPT 画得很漂亮,评审时被问三个问题就卡壳:预估峰值 QPS 是多少?核心链路的可用性能到几个 9?分库分表之后商家后台怎么查单?这三个问题答不上来,架构图就只是图,落不了地。超大型电商系统的架构设计,本质上是把流量、数据、一致性、成本这几个约束条件量化,再在约束条件下做技术选型。本文不打算给一份万能模板,而是按我自己做电商方案设计的顺序,把从容量估算、链路选型到峰值应对、踩坑复盘的全过程拆开讲,适合正在写方案或要评审方案的技术负责人、架构师和资深后端开发。
2. 先做容量估算:QPS、数据量、可用性目标是架构的地基
2.1 从业务指标推算峰值 QPS:三种口径的换算方法
超大型电商系统的架构设计,第一步永远是算流量。没有 QPS 预估,后面选什么缓存、分几张表、买多少台机器都是拍脑袋。常见做法是先定业务口径:日活用户数、人均浏览商品数、转化率、活动峰值倍数。我一般先按 PV 倒推 QPS,再按核心交易链路单独估算下单 QPS,两个口径差距很大但都有参考价值。
下面的 Python 脚本是我做设计文档时用来快速估算的,直接改参数就能用。
# 按 DAU 和人均访问次数估算整体入口 QPS DAU = 5000_0000 # 日活 5000 万 pages_per_user = 30 # 人均每天浏览 30 次页面 peak_to_avg_ratio = 5 # 峰值系数:晚高峰一般是均值 5 倍 seconds_per_day = 86400 avg_qps = DAU * pages_per_user / seconds_per_day peak_qps = avg_qps * peak_to_avg_ratio print(f"平均 QPS: {avg_qps:.0f}, 峰值 QPS: {peak_qps:.0f}") # 按转化漏斗估算下单 QPS transaction_rate = 0.03 # 浏览到下单转化率 3% checkout_seconds = 3600 # 下单集中在 1 小时高峰内 peak_order_qps = peak_qps * transaction_rate print(f"下单页面峰值 QPS: {peak_order_qps:.0f}") # 下单后触发的写操作放大倍数 write_multiplication = 8 # 下单、扣库存、生成支付单、发消息等 print(f"写链路总 QPS: {peak_order_qps * write_multiplication:.0f}")逻辑说明:第一个公式把日活和人均页数换算成全天平均 QPS,再乘以峰值系数得到入口峰值;第二个公式用转化率算订单 QPS,最后按照下单链路上的写放大倍数估算数据库写入压力。这里最容易被低估的是写放大倍数——一次用户下单往往触发订单、库存、支付单、物流单、消息、积分等多个写入点,一个下单接口落到存储上的写操作可能是它的 5 到 10 倍。
参数说明:峰值系数取 5 还是 8,取决于业务形态。典型电商晚高峰系数在 3 到 6,但如果文档里确定了要做“整点秒杀”类活动,这一项必须单独按秒杀场景建模,不能混在日常口径里计算。日活 5000 万的规模,入口峰值超过 8 万 QPS,意味着网关和商品详情页都不能只靠数据库撑,缓存和 CDN 承担了绝大部分读流量。
2.2 三年数据量估算与存储分层:什么样的表该进分库分表
流量算完接着算数据量。超大型电商系统的数据规模有三个显著特征:订单数据增长不可控、库存流水远超订单量、用户数据需要永久保留。我一般按三年规划来建一张容量表,把每类数据的来源、年增长量和总量写清楚。
| 数据域 | 日增条数 | 单条大小(KB) | 年增容量(GB) | 三年总量(GB) |
|---|---|---|---|---|
| 订单主表 | 5000 万 × 0.3 有效 ≈ 1500 万 | 1.5 | ≈ 8.2 TB(按行数估算,非纯容量) | 24.6 TB |
| 订单明细 | 1500 万 × 5 | 1 | 27.4 TB(行数口径) | 82 TB |
| 库存流水 | 峰值每秒 2 万 × 86400 × 活动天数 | 0.5 | 数百亿行 | 千亿行级 |
| 商品信息 | 10 万新增 + 修改 | 5 | 相对可控 | 数十亿行 |
| 用户与地址 | 新增 500 万/月 | 2 | 可控 | 数亿行 |
注意上表的口径:订单类数据是按行数估算而不是纯容量,因为在 MySQL 里一张表能支撑的瓶颈通常来自行数和索引大小,几张 TB 级的大表在单实例上基本无解。按这个规模,订单表必须按用户维度分库分片,库存流水单独走归档或分表,商品表可以保持单库但要做读写分离。
存储分层的选型逻辑很直接:热点读数据放 Redis,结构化核心数据放 MySQL,非结构化图片和静态资源放对象存储,检索类需求放 Elasticsearch。超大型电商系统架构设计里最忌讳的是把“缓存、数据库、搜索引擎”当成可选项,实际上它们各自解决一类问题,缺一个就要用另一个的过度设计来补。
2.3 可用性目标拆解:几个 9 说了算,也要算得清
可用性目标不是越高越好,而是成本和业务的平衡。四个九(99.99%)意味着全年停机不超过 53 分钟,这要求链路里每个关键组件都要冗余部署且有自动故障转移能力。超大型电商的架构方案如果只写“系统高可用”,评审基本不可能通过,必须拆到每个环节。
常见的拆法是这样:网关 99.999%、商品服务 99.99%、订单服务 99.99%、库存服务 99.99%、支付网关(外部依赖)99.95%。一个用户请求要经过网关、商品、订单、库存、支付五跳,整体可用性 = 0.99999 × 0.9999 × 0.9999 × 0.9999 × 0.9995 ≈ 0.9967,也就是三个 9 都不到。这个计算结果常让团队意识到:外部支付依赖直接决定了整体可用性上限,必须做异步化对账和超时重试,而不是在自研组件上盲目堆机器。可用性目标写进方案时,要带计算过程和降级预案,否则就是一句口号。
3. 核心链路选型逻辑:网关、商品、库存、订单的状态与数据流
3.1 接入层设计:限流、熔断与动态路由的最小配置
接入层是整个系统的第一道闸门,承担认证、鉴权、限流、路由、灰度等功能。超大型电商系统里,接入层最常见的形态是 LVS/Nginx 做流量入口,后面挂 API 网关做业务路由。网关节点本身必须无状态,所有配置下沉到配置中心,这样才能做到水平扩缩容。
限流算法选型上,我优先推滑动窗口或令牌桶,不推荐纯计数器。计数器方案在跨秒边界时容易放过两倍于阈值的突发流量,秒杀场景下会直接打到后端。下面是一段网关限流配置示例(以 Nginx 的 lua-resty-limit-traffic 为例):
# 网关限流:按客户端 IP + 用户 ID 双层维度限制 lua_shared_dict my_limit_store 100m; init_by_lua_block { -- 流量限制配置 ratelimit = { -- 单 IP 每秒 20 个请求,超出直接拒绝 ip = { rate = 20, burst = 40 }, -- 单用户每秒 50 个请求,超出排队或拒绝 user = { rate = 50, burst = 100 } } } access_by_lua_block { -- 取客户端 IP 和用户维度标识 local ngx = ngx local key_ip = ngx.var.remote_addr local key_user = ngx.var.http_x_user_id -- 由网关从 token 解析注入 if key_user then -- 用户维度限流:先查共享字典里当前窗口计数,超出则返回 429 local ok, err = ngx.shared.my_limit_store:incr(key_user, 1, 0, 60) if ok and ok > ratelimit.user.rate + ratelimit.user.burst then ngx.exit(429) end end -- IP 维度的限流逻辑同理 local ok_ip, err_ip = ngx.shared.my_limit_store:incr(key_ip, 1, 0, 60) if ok_ip and ok_ip > ratelimit.ip.rate + ratelimit.ip.burst then ngx.exit(429) end }逻辑说明:双维度限流的意义在于防止单 IP 绕过用户维度的限制(比如脚本批量注册小号),也防止用户维度缺失时用 IP 兜底。这里的计数窗口用 60 秒固定窗口,实际生产更稳妥的是滑动窗口;这段配置为了可读性做了一些简化。
参数说明:rate 和 burst 需要按业务压测结果调,不能拍脑袋。常见做法是先放量压测网关和后端服务的真实容量,得到单机 QPS 上限,再按入口总流量的 60% 到 70% 设定网关限流阈值,留 30% 余量给重试和突发。返回 429 之后客户端应当有退避策略,否则用户疯狂刷新会把限流本身变成故障源。
3.2 商品与库存拆分:读多写少和写热点不能混在一个服务里
商品详情是超大型电商系统里读流量最大的接口,读 QPS 可能是下单接口的几十倍。而库存是写热点最集中、数据一致性要求最高的模块。这两个模块放在同一个服务里,会带来两个问题:一是商品接口的流量波动会拖垮库存的写链路,二是库存的数据库行锁竞争会影响商品读取的稳定性。所以架构方案里一般把商品服务与库存服务完全拆开,商品走缓存优先,库存走预扣和异步释放。
商品读链路的标准做法是三级缓存:客户端缓存(HTTP 缓存头)、CDN 缓存、Redis 缓存。CDN 层一般只放图片和静态描述,商品价格、库存数量这类动态字段不能进 CDN,否则会出现价格不一致。Redis 里的商品详情缓存,更新时机靠 MQ 消息异步失效,而不是在商品修改接口里同步删缓存,避免缓存和数据库操作之间出现时间窗口。
库存模块的设计则围绕预扣展开。用户在提交订单时先预扣库存,支付成功后确认扣减,超时未支付自动释放。这个流程避免了“先下单再扣库存”导致超卖,也比“下单即锁库存”的并发能力高。预扣的库存放在 Redis 里加自减,数据库库存表通过异步任务落账,两边的数值一致性靠对账任务兜底。
3.3 订单与支付状态机:最终一致性靠什么保证
订单系统是交易链路里状态最多的模块:待支付、已支付、已发货、已完成、已取消、退款中。状态多意味着并发修改的冲突多。订单状态机设计的核心是明确定义每个状态允许的合法迁移,迁移必须是在单事务内完成的行级更新,不能靠应用层先查后改。
-- 订单状态迁移:只允许待支付->已支付,且带条件更新防止重复支付成功 UPDATE order_main SET status = 'PAID', pay_time = NOW(), pay_channel = #{channel}, update_time = NOW() WHERE order_id = #{orderId} AND status = 'WAIT_PAY' AND user_id = #{userId} AND deleted = 0;逻辑说明:条件更新是防重入的根基。无论支付回调来了多少次、消息队列重投了多少次,只有第一次能把状态从待支付改成已支付,后面的更新影响行数为 0,应用层据此判断是重复回调。这里的关键在于 WHERE 条件里带上原状态,而不是先 SELECT 再 UPDATE。
支付回调与订单状态同步的问题则要靠消息队列异步化。常见做法是支付网关回调 -> 收到后先落一张支付回调消息表 -> 返回“收到”给网关 -> 异步消费消息更新订单状态、通知发货系统、记录财务流水。如果直接同步调用订单服务更新状态,支付网关的超时重试会导致订单更新重复执行,而且支付回调高峰期会把订单数据库连接池打满。
在超大型电商系统架构设计方案里,订单支付这一块最容易引发争议的是到底要不要引入分布式事务框架。我的建议是:核心交易链路尽量避开强分布式事务,用本地消息表或事务消息做最终一致性,事务消息方案只有在业务量可控、对延时不敏感的场景下才值得用。引入 Seata 之类的框架意味着所有接口都要增加事务协调开销,大促高峰期会成为一个新的瓶颈和故障点。
4. 峰值场景设计:秒杀和大促不是“加大机器”这么简单
4.1 流量漏斗与限流降级:请求在到达数据库前被拒绝多少层
超大型电商系统架构设计和普通业务系统最大的区别在于峰值流量的应对。日常流量下的架构可以规规矩矩,但秒杀或大促场景会把流量放大十倍以上,这时候如果没有流量漏斗设计,数据库会在第一波流量里被打挂。
我一般把流量拦截分成四层。第一层是网关限流,按用户、IP、设备指纹等多维度拦截明显异常流量。第二层是应用层的读缓存,秒杀商品页、库存余量这些高访问字段全部走 Redis,不允许落库查询。第三层是业务层的信号量隔离,比如库存扣减接口每个实例最多允许同时 100 个请求进入,超出的直接返回繁忙。第四层是队列削峰,真正的扣库存操作异步化,用户提交秒杀请求后立刻返回“排队中”,后端消费者按顺序执行库存扣减。
// 服务层信号量隔离示例:限制单个商品秒杀接口的并发进入数 Semaphore seckillSemaphore = new Semaphore(50); public Result seckill(Long skuId, Long userId) { // 非公平模式保证吞吐量,但要注意个别线程长时间占用 boolean acquired = seckillSemaphore.tryAcquire(1, TimeUnit.MILLISECONDS); if (!acquired) { return Result.busy("当前参与人数过多,请稍后再试"); } try { return doSeckill(skuId, userId); } finally { seckillSemaphore.release(); } }逻辑说明:这里的信号量不是用来做业务判断,而是保护后端资源。就算 Redis 能扛住十万 QPS,数据库的落账能力也可能只有每秒几千,信号量把进入扣减逻辑的并发数限制在可控范围。
参数说明:50 这个值要配合下游数据库连接池大小来定。如果数据库连接池最大值是 50,那么信号量阈值设置成 50 到 80 是合理的,超过这个值意味着请求会在数据库等待上排队,结果就是超时率上升。这个值必须经过压测确定,不能拍脑袋。队列削峰同样要注意队列积压的问题:如果秒杀量超过消费能力,消费者来不及处理积压消息,用户长时间等不到结果,体验会比直接失败更糟。设计时需要设置队列最大长度,超出长度直接拒绝新请求。
4.2 热点 key 探测与缓存击穿防护
大促场景下的一个经典问题是热点 key 集中在少数几个爆款商品上。假设某个商品详情页的 QPS 有 10 万,如果 Redis 里只用单个 key 存储这个商品的详情,那么所有请求都会落到同一个 Redis 分片的同一个 key 上,单个分片的 CPU 会先被打满。
方案里常见的处理手段是把热点数据做副本分散。比如把同一个商品 key 复制成多个带后缀的 key 分布在不同的分片上,请求进来先对用户 ID 做哈希散列再选 key。另外一个关键操作是从缓存读取到数据库回源之间加互斥锁,避免缓存过期瞬间所有请求同时打到数据库。
# 用 Redis SETNX 实现缓存重建互斥锁,避免击穿 import redis import json r = redis.Redis(host='redis-cache', port=6379, decode_responses=True) def get_product_detail(product_id: str) -> dict: cache_key = f"product:detail:{product_id}" data = r.get(cache_key) if data: return json.loads(data) lock_key = f"product:lock:{product_id}" # 和 3 秒后过期,防止持有锁的线程异常退出导致死锁 locked = r.set(lock_key, "1", nx=True, ex=3) if not locked: # 没拿到锁,说明其他线程正在重建缓存,短暂等待后重读 import time time.sleep(0.05) data = r.get(cache_key) if data: return json.loads(data) return None try: # 直接读数据库,重建缓存(此处略去 SQL 细节) detail = query_db(product_id) r.setex(cache_key, 300, json.dumps(detail)) return detail finally: r.delete(lock_key)逻辑说明:互斥锁的粒度是单个商品而不是全局锁,防止所有商品的缓存重建互相阻塞。拿到锁的请求承担回源数据库并重建缓存的责任,其他请求短暂等待后重读缓存,而不是也回源。
参数说明:锁过期时间 3 秒是一个保守值,考虑的是数据库查询和缓存写入最坏情况在大型系统里不应该超过 1 秒,留出 2 倍余量。重建缓存设置的 TTL 是 300 秒,这个值需要结合实际商品更新频率来确认,太短会频繁回源,太长会导致价格和库存等信息更新不及时。这里需要特别注意锁提前过期的问题,如果重建缓存超过 3 秒,锁被自动释放,其他线程又会进来重复回源。所以查询数据库后写缓存的动作要尽量快,或者把锁过期时间放宽到 5 秒并配合监控报警。
4.3 大促预案:从系统架构到组织流程的 Checklist
架构设计文档不止写技术组件,还要写预案。超大型电商系统的大促预案一般包含三类:容量预案、降级预案、故障止损预案。
容量预案最容易做到:提前扩容、压测、巡检。难点在降级预案,因为降级意味着要主动砍掉一些功能来保核心链路。常见的降级顺序是先降级非核心功能(如推荐、评论、优惠券计算),再降级读链路(商品详情走缓存副本),最后才降级写链路(比如限制部分支付渠道)。这个顺序要提前和运营对齐,因为“砍功能”是业务决策,不能技术团队单方面决定。
故障止损预案要写明每个故障的触发条件、影响范围、执行动作和回滚方案。比如 Redis 集群彻底不可用时,业务是降级为纯数据库查询还是直接返回售罄?这个决定不能等故障发生了再开会讨论。预案需要用表格形式写进方案文档里,并且在每次大促前做一次演练,不演练的预案都是纸面的。
5. 避坑记录:超大型电商系统架构设计里常见的翻车点
5.1 现象:库存扣减超卖、订单状态错乱
大促时库存数据出现负数,或者同一个订单被支付系统当成两个订单处理。这类问题几乎都指向同一个原因:并发更新缺少条件约束,或者状态更新没有带上前置状态。
解决的办法在 3.3 节已经讲过了,核心是两条:库存扣减的 UPDATE 条件里必须带库存余量大于等于扣减数的条件;订单状态迁移必须带上当前状态作为 WHERE 条件。另外,扣减库存和创建订单这两件事要在一个事务里完成,不要拆成两个事务中间再加消息,否则事务中间的消息延迟会带来超卖窗口。说到底,超卖问题的本质不是缓存写丢了,而是数据库层缺失了原子性约束。
5.2 现象:缓存失效瞬间数据库被打挂
Redis 正常运作,大促一开始数据库 CPU 直接 100%。查看慢查询发现全是商品详情的 SELECT,而且发生在同一个时间段。原因几乎都是缓存 key 的过期时间设置成了同一个值,导致同一批热点 key 同时失效,流量全部穿透到数据库。
解决的办法有三条:一是过期时间加随机偏移量,避免大量 key 同时到期;二是热点数据不设置过期时间,改为后台任务主动更新缓存;三是用互斥锁控制回源数量。我一般会同时用第一种和第二种,随机偏移量太容易在有大量相同写入时间的数据上失效,主动更新的代价是要维护一份热点清单。
5.3 现象:分库分表后,商家后台查单和运营分析全部卡死
这是超大型电商系统架构设计里最容易被忽视的坑。订单表按 user_id 分片,用户查询自己的订单非常快,但商家要查“本店今天所有订单”,需要扫描所有分片。运营要做数据分析、财务要对账,每个查询都是全分片扫描,数据库的资源被这些后台查询消耗殆尽。
常见的解决方案是分库分表之后为查询场景构建独立的索引数据:商家订单查询走 Elasticsearch,运营和财务走离线数仓或 OLAP 引擎,用 Binlog 订阅同步数据。这个方案设计选型本身不复杂,难点在于同步链路的数据一致性保障:Binlog 消费失败导致查不到数据怎么办?同步延迟导致订单状态不一致怎么办?我通常的做法是设置数据延迟报警,超过 5 分钟就报警,同时在商家后台页面显示“数据延迟约 X 分钟”来降低用户预期。
5.4 现象:加了服务拆分,数据库连接数不够用了
微服务拆分之后,每个服务都要连数据库和 Redis,数据库连接数被十倍放大。一个 30 节点的订单服务,每节点 20 个连接,光订单库就要占掉 600 个连接,MySQL 默认连接数上限很快被打满。
解决这个问题靠两条:一是数据库连接池从默认配置往下调,单服务并发不需要动辄 50 个连接,二是按服务拆分数据库账号和数据源,限制每个账号的连接数上限。更重要的一条设计原则是:缓存能挡住大部分读流量,真正打到数据库的并发连接数并不大,如果每个服务的连接池都按峰值配置,叠加起来必然超限。这个坑在架构设计阶段就要做连接数总量核算,把这个计算放进文档里,否则上线之后再来调就手忙脚乱了。
5.5 现象:分布式事务框架失效,锁等待飙升
架构评审时团队决定引入分布式事务框架解决跨库一致性问题,结果大促压测发现数据库锁等待时间飙升。原因是分布式事务框架在数据提交阶段会对涉及的所有资源加锁,锁持有时间随参与的服务数量增加而增加,并发一高就大量堆积。
超大型电商系统的核心交易链路,我的建议是不要做跨服务的强一致事务,而是把“扣库存”和“创建订单”放进同一个服务同一个数据库事务里,支付再异步确认。这样做的前提是库存服务与订单服务之间不跨库,把两个模块合并成一个领域服务,只是内部拆分模块。如果业务上确实要跨库,那就接受最终一致性,用本地消息表加对账来兜底,而不是用一个反性能的分布式事务方案制造更大的问题。
6. 写好架构方案文档的五条落地技巧
架构设计方案的价值不在画图,而在让看到文档的人做出正确决策。我最近一年评审了不少方案,发现写得好的方案有共性。
第一,容量估算表放在最前面两页。日活、峰值 QPS、数据量表要先用,后面所有技术选型都能对着这张表解释“为什么选这个”,评审时不用翻到最后才知道前置假设。第二,每一个关键技术选型要写“放弃什么”。选了 Redis 做缓存,要写为什么不选本地缓存;选了消息队列异步,要写为什么不同步调用。这个“放弃理由”比“选择理由”更能暴露设计者对系统的理解深度。第三,接口时序图不画全链路,只画关键写路径的时序,并在每个环节标注量级:这里 5 万 QPS、这里三分支并发、这里允许 100ms 超时。第四,预案表比架构大图更重要,表格每一行都要有可执行的动作和负责的团队,不能只有“降级”两个字。第五,写清容量余量与扩展边界:当前方案在什么样的流量下需要扩哪些机器、改哪些配置,提前把触发条件和动作写下来,故障发生时能省掉半小时的讨论时间。
这套写作习惯是我在经历了两次大促故障复盘之后总结出来的,故障本身都源于设计文档里没有写清楚边界和触发条件。写方案时多花十分钟把边界写清楚,评审和实施阶段能省下的时间是以天为单位的。希望这些经验能帮到你。
本文还有配套的精品资源,点击获取