ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

电商平台软件架构设计:微服务拆分、分布式事务与高并发落地指南

电商平台软件架构设计:微服务拆分、分布式事务与高并发落地指南 简介面向系统设计与开发人员电商平台软件架构是绕不开的核心课题。这份文档以黄金超市系统为实例清晰梳理电商后台的完整技术栈涵盖中台服务、分布式缓存、消息队列、数据存储与统一接口层并延伸至订单全流程、数据库读写分离、安全防护及第三方对接等关键环节。资源共1个PDF文件整体大小342KB体量虽小但内容密度较高适合想快速建立电商架构全局观的初中级工程师阅读。目前已有103人学习下载这一主题关注度较高。通过阅读能获得一套可复用的架构分层方法包括订单、库存、支付、积分等业务域的拆分思路以及监控、同步、负载均衡等运维侧经验这些内容有助于理解高并发场景下的服务协作与数据一致性为实际系统设计提供参考。1. 电商平台软件架构为什么值得花力气去读、去落地订单量从一天几百涨到几万之后最难受的不是代码写不完而是没人说得清整个电商平台软件架构长什么样商品、订单、库存、支付全搅在一个进程里改一个下单流程要拉上五个团队互相确认线上出问题只能靠老员工翻日志猜。这个时候读一份能落地的架构文档比写一百行代码更值钱。它解决的是边界问题——每个服务管什么数据、对外提供什么能力、调用链路里哪些必须同步、哪些可以异步这些定死了扩容、拆服务、加人都有依据。这篇就是按这类架构文档的常见骨架从分层选型、核心服务划分、数据一致性到落地避坑给你一套能直接抄作业的方案。2. 电商平台软件架构的总体分层业务边界、调用链路与软件架构风格选型2.1 业务域划分把电商拆成四个核心域拿到一份电商平台软件架构文档先不要看技术栈先看业务域怎么切。切得不好后面所有微服务、消息队列都是给自己挖坑。常见的切法不是按页面拆也不是按团队组织拆而是按“数据归属”和“变化频率”拆。我一般会把电商业务分成四个核心域商品域管“卖什么”库存域管“有多少”交易域管“怎么卖”支付与履约域管“钱和货怎么到”。每个域自带一份数据、一组服务、一套对外接口。商品域的类目、属性、价格交易域的购物车、订单、结算库存域的实物库存与预占支付域的支付单、退款单、对账单这些都是高内聚的数据集合拆开之后互相不直接碰对方数据库。支撑域则包括用户、营销、售后。这些域不直接参与核心交易链路但会通过事件或接口被核心域调用比如下单成功后发一张优惠券就是消费订单创建事件。下表是常见电商平台软件架构的域划分参考小平台可以合并但边界要清晰核心域主要服务典型数据主要对外能力商品域商品服务、类目服务、价格服务SPU/SKU、类目树、价格快照商品详情、列表搜索、价格查询交易域购物车服务、订单服务、结算服务购物车、订单主表、子订单、支付单加购、下单、拆单、取消、售后单库存域库存服务实物库存、预占库存、库存流水预占、释放、扣减、回补、库存查询支付与履约域支付服务、退款服务、物流服务支付单、退款单、物流轨迹支付下单、回调处理、退款、物流查询划分完之后还要做一道“中台与前台”的隔离。前台是可组合的应用层小程序、App、H5、管理后台。中台是能力层上面这四个域提供的标准服务。前台可以组合中台的能力但不能反向入侵中台的数据库。这条红线守住后面做多端适配、做促销活动都不会把核心链路搞乱。2.2 软件架构风格怎么选单体、微服务还是事件驱动软件架构风格直接决定了这套电商平台后续的扩展性和运维成本。常见的三种风格——分层单体、微服务、事件驱动——不是越新越好要看团队规模和流量形态。我见过太多团队一上来就上微服务结果五个人维护二十个服务RPC 超时调一堆一个下单链路串了八个服务排查问题要用半天。反过来也有团队死守单体订单量涨到几十万日单后扛不住频繁做灰度又互相影响。架构风格的选型要看四个参数团队规模、发布频率、数据一致性要求、流量波峰形态。维度分层单体微服务事件驱动开发效率初期最高后期下降初期低中期高异步化后核心链路更快扩展性整体扩展浪费资源按服务扩展削峰填谷抗突发最好一致性强一致本地事务分布式事务成本高最终一致需补偿设计运维成本最低最高注册中心、链路追踪、配置中心中高MQ 集群、消费监控排障难度低日志集中高跨服务追踪中消息积压和顺序问题多推荐场景日订单万级以下日订单十万级以上、多团队并行大促秒杀、异步通知类场景真实落地的电商平台绝大多数是混合架构核心交易用领域服务跨域通知用事件驱动报表和搜索用独立的读模型。也就是说不是选一个风格用到死而是在“订单创建必须强一致扣预占”这类关键路径上用同步调用在“发短信、送积分、更新搜索索引”这类非关键路径上用异步事件。从单体演进到微服务我的习惯是先把单体内部按域画好模块边界用接口隔离再按域逐步拆出服务而不是一次到位。架构文档里要写清楚每个服务的拆出顺序和依赖方向否则拆到一半依赖变成循环代码比单体还难改。2.3 同步、异步与补偿调用链路的三个约定域划分好之后下一个要定的是调用链路的分界线。架构文档里最常见的坑是“所有接口都同步调”结果一个下单接口串了商品、库存、优惠、订单、支付五个服务P99 直接飙到三秒。反过来全部异步也不行用户点了下单按钮等半天不知道成没成功。我在架构文档里会定三条硬性约定。第一条核心实时链路走同步加购物车、提交订单、支付下单这些用户直接等待的操作同步链路总耗时控制在 500ms 以内超过就要并行化或异步化。第二条旁路操作走异步订单创建成功后发短信、送积分、更新搜索索引、推送消息全部走 MQ不占主线程。第三条失败必须有补偿支付成功但库存扣减失败要回滚支付单而不是丢一个异常在日志里。异步消息体要设计得能自解释常见做法是这样定义事件{ eventId: uuid, eventType: order.created.v3, timestamp: 1713427200000, source: order-service, data: { orderId: 202404180001, buyerId: 888888, amount: 29900 } }这块的逻辑很简单eventId 是幂等键消费端拿到同一个 eventId 只处理一次eventType 带版本号后续字段有变更就升级为 v4下游可以同时兼容新旧版本data 里只放关键业务数据完整数据由消费端去查服务端获取避免消息体过大。timestamp 用于延迟监控。参数上要注意的两点是eventId 必须由生产者生成不能用订单 ID 代替事件版本字段一定要留在约定里否则上线兼容就是黑匣子。这三条约定写进架构文档后每次评审接口都要对照检查比任何技术选型都管用。3. 核心服务划分与接口约定商品、订单、库存、支付的服务边界怎么定3.1 商品服务SPU/SKU 模型、详情查询与上架状态机商品服务是电商平台软件架构里最基础也最容易被低估的域。很多团队把商品详情页做成直接查 MySQL一张大表把标题、图片、详情 HTML 全塞进去结果列表页和详情页互相拖垮。商品域先要分清 SPU 和 SKU 两层。SPU 是抽象商品比如“iPhone 15 Pro”SKU 是具体可卖的单元比如“iPhone 15 Pro 256G 原色”。商品服务对外暴露的接口以 SKU 为核心SKU 价格、SKU 库存、SKU 状态。常见的接口约定如下GET /api/v1/skus/{skuId}?fieldsbase,price,stock返回体中base 是 SPU 层的基础信息标题、类目、图片price 是 SKU 价格stock 是实时库存快照。fields 参数用来控制返回字段避免详情页接口一次返回 200KB 数据拖垮网关。这个接口的参数有三个必须定清楚第一fields 由调用方声明服务端不默认全量返回第二stock 只作展示不作为下单扣减依据扣减走库存服务专用接口第三价格不是直接从价格表读而是经过价格服务计算后的最终售价要含促销分摊。上架流程用状态机管理草稿 → 审核中 → 已上架 → 已下架。状态流转只能由商品服务内部方法触发不允许调用方直接改状态。每个 SKU 变更要落一条变更流水记录下来谁在什么时间改了价格还是库存财务对账和问题追踪都靠它。缓存设计上商品详情是典型的读多写少。我会把 SKU 基础信息和价格放在 Rediskey 设计为sku:{skuId}:base和sku:{skuId}:priceTTL 设 30 到 60 分钟后台变更时主动删除缓存让请求回源。库存快照不缓存或者只缓存极短时间因为库存的准确性比性能重要。3.2 订单与支付状态机、幂等键与拆单结算订单服务是电商平台软件架构里状态最复杂的服务没有之一。它的核心是一张状态机表所有状态迁移必须走统一的状态流转方法不允许直接 UPDATE 状态字段。当前状态可迁移状态触发动作待付款已取消 / 待发货用户取消 / 支付成功回调待发货已发货 / 已取消仅售后场景商家发货 / 超时未发货自动取消已发货待收货物流轨迹更新待收货已完成 / 售后中用户确认收货 / 申请售后已完成售后中售后申请售后中已完成退款成功退款完成状态机表里两个容易被忽略的点待发货不能直接跳到已完成必须经过物流发货售后中是一个“挂起”状态退款成功才能回到已完成。每个迁移动作都要记录状态变更日志字段包括 orderId、fromStatus、toStatus、operator、timestamp、reason。这条日志是之后排查“订单怎么变成这样”的唯一依据。支付部分的关键是幂等。用户连续点击两次“立即支付”不能生成两笔支付单。常见实现是下单接口要求调用方传一个 clientRequestId订单服务用它做唯一约束支付回调则以支付渠道返回的 transactionId 为幂等键同一笔支付回调重复到达只处理一次。接口约定如下POST /api/v1/orders { buyerId: 888888, skuList: [{skuId: 1001, num: 1}], addressId: 233, clientRequestId: uuid }这个接口背后的逻辑是订单服务先查 clientRequestId 是否已存在订单存在就直接返回原订单不存在才创建。参数上要注意 clientRequestId 由前端生成不依赖后端接口响应里必须带 orderId 和 status方便前端轮询。拆单逻辑要单独说。一个订单里如果包含不同店铺的商品或者一个仓库放不下的商品就要拆成多个子订单每个子订单各自结算、各自发货。分摊规则是拆单里最容易翻车的地方满减优惠券金额要按子订单商品金额比例分摊退款也只能退到对应子订单。我一般会在结算服务里先算分摊再落订单表分摊比例保留四位小数避免出现“退完一个子订单主订单优惠金额对不上”的财务事故。3.3 库存服务预占、扣减与防超卖库存服务是电商平台软件架构里保证“超卖不超卖”的胜负手。先说三种主流方案的对比方案预占时机扣减时机超卖风险用户体验下单预占创建订单时支付成功时低下单快但占用库存未支付订单积压会“锁死”库存支付扣减不预占支付成功时中高下单快但支付高峰可能无库存可扣预占 超时释放创建订单时支付成功时超时自动释放低下单快需定时任务释放超时预占“超时释放”是绝大多数电商平台的标配下单预占库存支付超时比如 15 或 30 分钟未支付系统自动取消订单并释放预占库存。这个时间窗不是拍脑袋定的要看支付成功率曲线和用户犹豫时长一般设置在 15 到 30 分钟之间太短伤转化率太长压库存。扣减动作必须原子化。用 Redis 做库存预热的场景常见写法是 Lua 脚本if redis.call(exists, KEYS[1]) 0 then return -1 end local stock tonumber(redis.call(get, KEYS[1])) local num tonumber(ARGV[1]) if stock num then redis.call(decrby, KEYS[1], num) return 1 end return 0这段脚本的逻辑是先判断库存 key 是否存在不存在说明未预热直接返回 -1 让请求回源 MySQL再判断剩余库存是否足够足够才扣减。因为 Lua 脚本在 Redis 单线程模型下是原子执行的两个并发请求不会都扣成功。参数上要注意三点第一个库存 key 的初始值预热时要用SET sku:1001:stock 100不要用 INCR 累加避免重复预热翻倍第二个扣减后要异步写库存流水表记录 skuId、扣减量、orderId、时间这是后续对账的唯一证据第三个Redis 库存是“预扣水位”实际库存以 MySQL 库存表为准Redis 与 MySQL 的一致性问题放到第 4 章讲。MySQL 侧要加一道兜底库存流水表对 (skuId, orderId) 建唯一索引同一订单重复扣减会被数据库挡掉。Redis 挂了或者没预热回源到 MySQL 时扣减 SQL 要带条件where stock num再配合行锁双重保险。4. 数据存储与一致性设计分库分表、缓存回源与最终一致4.1 存储选型表和分库分表策略电商平台软件架构里的数据不是一张 MySQL 表打天下而是按访问特征分散到不同的存储里。我通常按这张表来选型数据类型存储理由商品主数据、订单、库存流水MySQL强事务、需回滚商品搜索、列表筛选Elasticsearch类目 属性的组合查询热点缓存、库存预占Redis高并发、原子操作异步消息、事件MQRocketMQ / Kafka削峰、解耦、事件驱动图片、详情 HTML、物流面单对象存储 OSS大文件、低频变更、CDN 加速分库分表策略要提前写在架构文档里不然业务跑到三千万订单再改就是伤筋动骨。订单表是最先需要分片的表。常见路由方式有两种按订单 ID 哈希取模和按买家 ID 取模。按订单 ID 哈希的好处是订单数据天然按 ID 散列按订单号查询效率高但买家维度的“查我的订单列表”会跨库需要汇总按买家 ID 取模的好处是同一个买家的订单都在同一分片列表查询快但订单号查询需要带上 buyerId 做路由。电商平台的订单查询绝大多数是买家维度所以我一般建议主分片键用 buyerId。一个常用的分片配置是 32 库 × 128 表即每个库 128 张订单表路由规则可以描述为库序号 hash(buyerId) % 32 表序号 hash(buyerId) % 128这个规则的参数含义是32 和 128 要按预估数据量反推单表行数控制在 2000 万以内路由键必须带在 SQL 里不允许全表扫描跨库 join扩容时要么翻倍库数并用一致性哈希要么提前按“年 买家哈希”做两级路由。分库分表之后原来的多表 join 要改造成宽表或 ES商品维度和订单维度的数据分开存储。4.2 缓存与数据库一致性回源、热 key 与延迟双删缓存是电商平台软件架构里性能提升最快也最容易出脏数据的一环。商品详情页典型的“先读缓存没有再读库再回填”是 Cache Aside 模式这个没问题问题多出在缓存更新策略。写操作来了之后我习惯用延迟双删代替直接更新缓存先更新数据库删除缓存隔几百毫秒再删一次缓存。为什么要删两次因为第一次删除后并发请求可能把旧数据回填进缓存第二次删就是为了清掉这个脏数据。延迟时间设 500ms 到 1s 之间超过通常没意义。热 key 是每到大促必出的问题。某个爆款 SKU 的缓存 key 被集中访问单个 Redis 实例被打满会拖垮整个 Redis 集群。常见做法是把热 key 打散成多个子 keysku:1001:hot:0 sku:1001:hot:1 sku:1001:hot:2读取时对 key 序号做随机或轮询写入时同步更新所有子 key。参数上要注意打散的粒度一般 10 到 20 个子 key 足够太多反而增加管理成本。另外要有热 key 探测机制流量上来时自动发现并打散而不是等 Redis 报警再去救火。缓存穿透、击穿、雪崩三个坑架构文档里要写明对应处理穿透用布隆过滤器先挡一层拦截不存在的商品 ID击穿用互斥锁缓存失效时只放一个线程回源雪崩在 TTL 上做随机化围绕基础 TTL 上下浮动 10% 到 20%防止同一时刻集体过期。需要注意订单和库存数据不建议用传统缓存。库存预占是强实时操作缓存只做“预占水位”最终性以 MySQL 为准订单状态更是不能靠缓存推断因为状态迁移必须落库。4.3 分布式事务本地消息表、事务消息与补偿服务拆分之后最头疼的就是跨服务的强一致需求。以“创建订单 扣减库存”为例订单服务和库存服务是两套数据库不能共用一个本地事务必须有跨服务的方案。三种主流的落地方式对比方案优点缺点适用场景本地消息表实现简单不依赖 MQ 特殊功能多写一张消息表定时任务扫表有延迟团队小、不想引入事务消息的初期MQ 事务消息半消息机制生产端不落库需要 MQ 支持事务消息RocketMQ、Kafka 系的标准做法TCC / Saga能处理复杂回滚实现成本高空回滚、悬挂难处理资金类强对账场景本地消息表的流程是订单服务开启本地事务写订单表 写一条“待发送”的库存扣减消息事务提交后由定时任务扫描消息表把消息发到 MQ库存服务消费后扣库存成功后回写消息状态为“已完成”。如果扣库存失败消息保留重试超过重试次数进入死信队列人工处理。事务消息则省掉了消息表MQ 拿到半消息后先确认生产端本地事务成功才把消息投递给消费端。我个人的经验是事务消息能解决 80% 的跨服务场景但“消费成功后的业务失败”它管不了还是需要消费端的本地事务和重试机制。能不进分布式事务的尽量不进。下单时可以只写订单表不扣库存库存放到支付回调后再扣这个场景就从强一致变成了最终一致反而更简单。架构文档里该写清楚的是哪些场景必须强一致支付、退款、改价哪些最终一致就够了送积分、发券、更新搜索索引不要一刀切。5. 电商平台架构落地避坑高并发、分布式事务和演进中的五个常见问题5.1 现象一大促秒杀把订单服务打满数据库连接池先挂现象大促开始时订单服务 CPU 没到 50%数据库先报连接数超限紧接着订单接口超时率飙升。原因下单链路是同步串行的每个请求都占用数据库连接等待库存查询、优惠计算秒杀流量一来连接池被占满后续请求全部排队。数据库连接池满不是数据库性能问题是上游并发挤进来的请求太多。解决库存先预热到 Redis下单时库存相关查询全部走 Redis读不到才回源 MySQL秒杀入口加限流下单接口按用户维度限流比如 1 秒 1 个请求超出直接返回“排队中”订单创建后走 MQ 削峰异步完成后续的优惠计算和库存扣减。数据库连接池要设硬上限并且给慢查询加超时避免一条 2 秒的 SQL 把连接占死。5.2 现象二支付回调丢失订单状态永远停在待付款现象用户支付扣款成功了但订单状态还是待付款反复收到催付短信。原因支付网关回调接口超时或响应格式不对网关重试几次后放弃或者回调消息到达后端后消费逻辑抛异常被误吞消息被当成已处理。本质上是没有“回调落库 主动对账”的兜底。解决支付回调到达后第一件事是落库写一条支付回调流水然后再更新订单状态更新订单状态失败要抛出异常让 MQ 重试不能 catch 吞掉同时每天跑一次对账任务拉取支付渠道的账单与本地支付单逐个比对超过 30 分钟未一致的标记异常触发补单。5.3 现象三库存扣减超卖财务对不上账现象商家后台显示卖出 120 件库存表只剩 80 件但订单里确实有 120 个成功订单。原因代码是先 SELECT 查库存再 UPDATE 扣减两个操作之间有并发窗口或者扣减 SQL 没带stock num条件直接减到负数。超卖在架构层面不是 bug是并发控制没做对。解决核心扣减走 Redis Lua 脚本Lua 步骤做原子判断和扣减MySQL 侧兜底加update stock set stock stock - num where sku_id ? and stock num影响行数为 0 说明库存不足事务回滚。另外给库存流水表加 (skuId, orderId) 唯一索引重复扣减直接报错。对账脚本每天跑一遍库存流水总和加上当前库存等于初始库存不等就报警。5.4 现象四拆分后一次查询跨五个服务响应超过 2 秒现象订单列表页要同时展示商品标题、价格、物流状态、优惠明细前端一次请求后端要调订单、商品、物流、优惠四个服务串行执行总耗时超过 2 秒。原因服务拆了但没有对应的聚合层每个前端页面都在自行编排后端服务调用还是串行的。更糟的是直接在服务内查多个库做 join跨库 join 在分库分表后根本走不通。解决加一个 BFF 聚合层Backend for Frontend由它并行调用四个服务设置 200ms 超时单个服务失败返回降级数据而不是整体失败订单列表所需的商品标题、图片、价格等冗余字段在订单创建时异步写入订单宽表或 ES查询走读模型不再回源商品服务。查询链路从串行变成并行、从 4 次跨服务调用变成 1 次读宽表。5.5 现象五一次上线把消息顺序搞乱下游消费错乱现象同一个订单的状态变更事件先发了待发货后发了已发货下游却先消费到已发货又消费到待发货订单状态被覆盖回旧状态。原因消费者开了多线程处理或者同一个订单的消息被路由到不同分区并发消费导致顺序颠倒。MQ 默认只保证分区内有序不保证全局有序。解决订单类消息按 orderId 哈希路由到同一个分区或同一个队列消费者设置单线程顺序消费由于顺序消息会降低吞吐非核心场景比如优惠券发放不要求顺序可以走普通消息并行消费。对刷新状态类的消费加一个简单的版本校验消息体里带 eventId 或时间戳旧消息来了直接丢弃。6. 把架构文档变成可演进的基线评审清单、压测验证与灰度发布6.1 架构评审的十个检查项架构文档画完不等于落地落地前要过一遍评审。我一般用这份十项清单逐项打勾序号检查项通过标准1服务边界每个服务数据归属清晰无跨库直连2同步链路耗时关键业务链路 P99 小于 500ms3幂等设计所有写接口都有幂等键4超时与重试所有 RPC 调用都设超时重试有上限5异步消息规范事件带 eventId、版本号消费端幂等6状态机完整订单、支付、售后状态迁移全覆盖7库存防超卖扣减原子化流水表唯一索引8分库分表路由路由键明确SQL 必带路由条件9缓存淘汰策略关键 key 有明确 TTL 和更新方案10对账任务支付、库存、优惠至少有一套对账脚本这十项里最容易漏的是第 4 和第 9。很多团队线上故障都出在“没设超时重试无上限”一个接口慢下游全部线程被拖住缓存不设 TTL数据永远不更新上架新价格用户看不到。6.2 链路压测的四个指标与止损线架构评审过了下一步是压测。不需要一上来就追求压满而是用小流量验证链路和放大流量观察指标。我一般盯四个指标TPS每秒请求数、P99 延迟、错误率、数据库连接池占用率。止损线可以这样定错误率超过 0.1% 或 P99 超过 1 秒就停止加压先修问题再继续不要硬压到系统崩溃再去复盘。压测时有个容易被忽略的点压测数据和线上数据要同构否则分库分表路由偏斜压出来的结果没有参考价值。压完要清掉压测产生的脏订单和流水不然干扰财务对账。6.3 灰度发布与架构文档的持续维护新架构灰度发布我喜欢按“内部用户 → 白名单用户 → 百分比放量 → 全量”的顺序推进。每一次放量要回看监控订单失败率、支付回调延时、库存扣减失败量、消息积压数量全部稳定才进下一档。灰度期间要预留快速回滚方案不能指望“改配置再发布”要有一键回滚前一版本的能力。架构文档本身也要随代码一起维护。改一个接口、加一个新服务、调整分库参数都要同步更新到文档里否则半年后文档和线上对不上它就只是一份没用的 PDF 文件还不如不写。我吃过这个亏线上服务都拆到第三期了架构文档还停在第一期的单体图新同事照着文档排查问题越查越偏。现在我的习惯是把架构文档纳入代码评审的一部分谁动接口谁更新文档评审不通过不许合代码。这套做法听上去不性感但能让架构长时间保持可演进的状态希望帮到你。本文还有配套的精品资源点击获取
返回列表