简介这份《电商平台软件架构.pdf》面向电商后端开发、架构设计与技术面试人群系统梳理高并发电商系统的分层结构与核心组件帮助读者建立从业务到基础设施的完整认知框架。内容围绕中台服务展开涵盖订单、库存、支付、购物车、积分与促销等业务模块并延伸至分布式缓存、消息队列、数据存储、API网关、运维监控、第三方接口与安全体系同时给出中央主库、读写库、TMS/WMS/BI库的数据库架构与订单全流程链路配合架构图便于理解服务间调用关系。资源包为1个PDF文件大小约342KB轻量易读适合通勤或面试前快速翻阅。目前已有103人学习可作为电商架构入门梳理与面试复习的参考材料。1. 电商平台软件架构从一份 PDF 到能扛住大促的落地骨架很多人第一次拿到「电商平台软件架构.pdf」这类资料时翻完目录觉得什么都讲了真到自己动手搭一套能跑通下单、扣库存、支付回调的系统又发现每个模块都缺一根筋。问题不在资料在于架构文档天然是「结果快照」它不会告诉你为什么订单服务要拆出来、库存扣减为什么不能放在订单事务里、支付回调为什么要做幂等。电商平台软件架构的核心矛盾只有一个流量峰值和资金安全同时压在身上任何一处设计偷懒大促当天都会以订单丢失或超卖的形式还回来。这份内容适合两类人一是准备从单体拆到微服务的中级后端二是需要给团队定技术方案、但不想照抄大厂 PPT 的负责人。接下来我不复述 PDF 目录而是按「一个订单从进站到履约」的真实链路把架构拆成能落地的模块、参数和排错点。2. 电商平台软件架构的分层与选型为什么不能一上来就微服务2.1 分层不是画图是决定故障爆炸半径电商系统的经典分层是接入层、应用层、领域服务层、数据层、基础设施层。但分层真正的价值不在图上好看而在于故障隔离。接入层挂了用户看到 502应用层挂了某个业务线不可用领域服务层挂了可能只是优惠券算不出来下单还能继续数据层挂了全站完蛋。所以分层的第一个动作是给每层定 SLA 和降级策略而不是先选框架。我一般会按这个顺序定边界层级典型组件故障影响降级手段接入层Nginx / 网关全站不可访问静态页兜底、限流应用层商品、订单、支付 BFF单业务线不可用关闭非核心功能领域服务库存、价格、优惠部分计算错误走缓存旧值数据层MySQL、Redis、MQ全站不可用只读模式、队列缓冲这张表的意义是当你只有三个人维护系统时不要把所有逻辑塞进一个应用层服务否则一次发版就是全站故障。常见做法是先把「读多写少」的商品查询和「写重」的订单创建分开部署哪怕还在同一个代码仓库里部署单元也要拆。2.2 单体、SOA 还是微服务按团队规模倒推选型不看技术潮流看团队沟通成本。一个 5 人团队上微服务光是服务发现、链路追踪、分布式事务就能把迭代速度拖垮。我的判断标准很直接团队 10 人日订单 5 万模块化单体按包边界隔离数据库单库分表。团队 1030 人日订单 5 万50 万SOA按业务域拆 35 个服务共享数据库但分 schema。团队 30 人日订单 50 万微服务每服务独立库引入 MQ 和分布式事务框架。这里有个血泪经验不要为了拆而拆。我见过一个日订单三千的系统拆了十二个微服务结果一个下单请求跨了七个服务排查一次超时要翻七份日志。拆分的唯一理由是「不同模块的发布频率和伸缩需求差异大到无法在一个进程里协调」。2.3 最小可运行骨架的目录与依赖如果你现在要从零搭一个能跑通「浏览商品 → 加购 → 下单 → 扣库存 → 支付回调」的骨架我建议先用模块化单体起步。下面是一个可复现的 Maven 多模块结构不依赖任何特定云厂商ecommerce-root/ ├── pom.xml ├── ecommerce-common/ # 通用工具、常量、异常 ├── ecommerce-product/ # 商品域查询、缓存 ├── ecommerce-order/ # 订单域创建、状态机 ├── ecommerce-inventory/ # 库存域扣减、回滚 ├── ecommerce-payment/ # 支付域回调、对账 └── ecommerce-gateway/ # 接入层路由、限流每个模块独立pom.xml根pom.xml用modules聚合。这样做的原因是未来任何模块要拆成独立服务只需要把该模块的pom.xml改成可执行 jar加一个启动类数据库连接换成独立配置即可业务代码几乎不动。依赖版本上Spring Boot 3.x JDK 17 是目前稳定组合MySQL 8.0 做订单库Redis 7 做商品缓存和分布式锁RocketMQ 或 Kafka 做订单事件。不要在这个阶段引入 Service Mesh那是在解决你还没有的问题。提示模块间调用只允许上层依赖下层禁止 order 模块直接 import inventory 的 Mapper。跨域调用走接口或事件这是未来拆服务时唯一不用重写的地方。3. 订单与库存的一致性设计把超卖挡在事务之外3.1 为什么扣库存不能和创建订单放在同一个本地事务这是电商架构里最经典的翻车点。很多人第一版代码是这样写的Transactional public void createOrder(OrderDTO dto) { // 1. 扣库存 inventoryMapper.decrease(dto.getSkuId(), dto.getQuantity()); // 2. 创建订单 orderMapper.insert(buildOrder(dto)); }在单库单机、并发量低的时候它能跑。一旦库存表被多个订单服务实例同时更新decrease的update stock stock - n where stock n虽然能防负库存但订单创建失败回滚时库存扣减也会回滚——这看起来没问题问题在于支付超时未支付的场景订单创建成功库存扣了用户不付款库存被锁死需要定时任务释放。如果释放逻辑和订单状态不一致就出现「订单已取消但库存没回滚」或「库存回了但订单还能支付」。更合理的做法是库存扣减异步化 预占模型// 订单服务只写订单发事件 Transactional public void createOrder(OrderDTO dto) { Order order buildOrder(dto); orderMapper.insert(order); // 发事务消息确保订单写入和消息发送原子 mqProducer.sendInTransaction(order_created, order.getId()); } // 库存服务消费事件执行预占 MQListener(order_created) public void onOrderCreated(String orderId) { Order order orderMapper.selectById(orderId); boolean locked inventoryService.tryLock(order.getSkuId(), order.getQuantity()); if (!locked) { // 库存不足发取消事件 mqProducer.send(order_cancel, orderId); } }逻辑说明订单服务不再直接操作库存表而是通过事务消息把「订单已创建」这个事实广播出去。库存服务消费后尝试预占预占失败则发取消事件订单服务收到后把订单置为已取消。这样订单和库存各自维护自己的事务边界通过最终一致性对齐。参数说明tryLock内部用 Redis 的DECR或数据库的update ... where stock n实现关键是预占记录要带过期时间比如 15 分钟超时未支付由定时任务扫描预占表释放。预占表字段至少包含order_id、sku_id、quantity、status、expire_at并对order_id sku_id建唯一索引防止重复消费导致重复扣减。3.2 支付回调的幂等与对账别让用户付两次钱支付回调是另一个高频踩坑区。第三方支付平台可能重复回调、乱序回调甚至回调丢失。架构上必须做三件事第一回调入口幂等。用支付流水号做唯一键插入payment_record表重复插入直接返回成功。CREATE TABLE payment_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, trade_no VARCHAR(64) NOT NULL, order_id VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_trade_no (trade_no) );第二状态机驱动订单流转。订单状态只允许按待支付 → 已支付 → 已发货 → 已完成单向流转任何逆向操作走独立的取消/退款流程。支付回调只做一件事把订单从待支付推进到已支付如果当前状态不是待支付直接忽略。第三每日对账。定时任务拉取支付平台账单和本地payment_record逐笔比对差异记录进reconcile_diff表人工处理。这一步不能省它是资金安全的最后一道防线。3.3 库存预占的过期释放与防重预占释放的定时任务要处理两个边界一是释放时订单已经支付不能释放二是释放时订单已经取消不能重复释放。做法是在释放前查订单状态并用update inventory_lock set status released where order_id ? and status locked的受影响行数判断是否真的释放成功。Scheduled(fixedDelay 60000) public void releaseExpiredLocks() { ListInventoryLock expired lockMapper.selectExpired(now()); for (InventoryLock lock : expired) { Order order orderMapper.selectById(lock.getOrderId()); if (order.getStatus() ! OrderStatus.WAIT_PAY) { continue; // 已支付或已取消跳过 } int updated lockMapper.release(lock.getOrderId()); if (updated 0) { inventoryMapper.increase(lock.getSkuId(), lock.getQuantity()); } } }参数说明fixedDelay 60000表示上一轮执行完 60 秒后再执行避免任务堆积。selectExpired走expire_at索引每次限制 500 条防止一次拉太多导致 GC 压力。release的where status locked是防重的关键即使两个实例同时扫到同一条记录也只有一个能更新成功。注意预占释放和支付回调可能并发。如果支付回调先到订单状态变成已支付释放任务查到的状态就不是待支付会跳过如果释放任务先执行库存加回去了支付回调再进来订单状态从待支付变已支付但库存已经释放——这会导致少卖。解决办法是支付回调里检查预占记录是否还存在如果已被释放触发一次重新扣减或人工介入。这个边界没有银弹必须用对账兜底。4. 高并发下的缓存与限流把流量挡在数据库之前4.1 商品详情缓存的更新策略与穿透防护商品详情是读多写少的典型。缓存策略我一般用Cache-Aside读的时候先查 Redis没有就查数据库并回填写的时候先更新数据库再删除缓存。删除而不是更新缓存是为了避免并发写导致脏数据。public ProductVO getProduct(Long skuId) { String key product: skuId; String cached redis.get(key); if (cached ! null) { return JSON.parseObject(cached, ProductVO.class); } // 防穿透空值也缓存短过期 ProductVO product productMapper.selectById(skuId); if (product null) { redis.setex(key, 60, ); return null; } redis.setex(key, 300, JSON.toJSONString(product)); return product; }参数说明正常缓存 300 秒空值缓存 60 秒。空值缓存是为了防止恶意请求不存在的 skuId 反复打数据库。但空值缓存时间不能太长否则商品上架后要等一分钟才能被看到。缓存更新时先update数据库再deleteRedis。如果删除失败靠过期时间兜底。更严格的做法是订阅数据库 binlog用 Canal 之类的工具异步删缓存但那是团队有运维能力之后的事。4.2 网关限流的三种维度和参数设置限流要分维度按 IP、按用户、按接口。网关层用令牌桶算法我一般这样配维度阈值适用接口超限行为IP100 QPS商品查询返回 429用户10 QPS下单排队或拒绝接口5000 QPS秒杀拒绝并提示参数设置没有标准答案要看压测结果。我的经验是先按预估峰值的 1.5 倍设大促前压测再调。限流阈值太低会误伤正常用户太高等于没限。关键是限流要分级核心接口下单、支付的阈值要留足余量非核心接口评论、推荐可以限得狠一点超限直接返回降级内容。4.3 热点库存的 Redis 扣减与数据库最终一致秒杀场景下库存扣减不能直接打数据库。常见做法是把库存预热到 Redis用 Lua 脚本原子扣减-- KEYS[1]: 库存 key, ARGV[1]: 扣减数量 local stock redis.call(GET, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return -2 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 1返回值说明-1表示库存未预热-2表示库存不足1表示扣减成功。扣减成功后发 MQ 消息异步落数据库数据库扣减成功再发订单创建事件。如果数据库扣减失败比如库存真的不够需要回滚 Redis 库存并通知用户。这个方案的风险是 Redis 和数据库可能不一致。兜底手段是定时任务比对 Redis 库存和数据库库存差异超过阈值就告警。另外Redis 扣减成功后如果消息丢失会导致少卖所以 MQ 要用可靠投递生产端本地事务表 定时补偿。提示热点库存的 Redis key 不要用单个 key否则所有请求打到一个分片。可以按 skuId 哈希到多个 key每个 key 存一部分库存扣减时随机选一个降低单分片压力。5. 电商平台软件架构的避坑与排查五个真实翻车现场5.1 现象大促开始后订单量暴涨但库存扣减消息积压用户看到「下单成功」却迟迟不发货原因库存服务消费能力不足MQ 堆积。根因通常是库存服务的数据库连接池太小或者消费逻辑里有同步远程调用。解决先扩容库存服务实例同时把消费逻辑里的远程调用改成异步或批量。连接池大小按CPU 核数 * 2 磁盘数估算但要以压测为准。更根本的是给 MQ 设监控积压超过阈值自动告警。5.2 现象支付回调偶尔丢失用户付了钱订单还是待支付原因回调接口处理超时支付平台重试几次后放弃或者回调接口抛异常没有返回成功标识。解决回调接口第一件事是记录原始报文到payment_record然后立即返回成功后续业务逻辑异步处理。这样即使业务处理失败也能通过定时任务补偿。记住回调接口的唯一职责是收单不是处理业务。5.3 现象商品缓存和数据库不一致用户看到已下架商品还能下单原因缓存删除失败或者更新数据库后删除缓存前有并发读旧值被回填。解决用延迟双删——更新数据库后删一次缓存延迟 500ms 再删一次。更彻底的是订阅 binlog 异步删缓存。同时下单时校验商品状态不能只信缓存。5.4 现象分布式锁释放时误删别人的锁原因锁的 value 没有唯一标识A 线程超时释放了 B 线程的锁。解决加锁时 value 用UUID 线程 ID释放时用 Lua 脚本比对 value 再删除。锁过期时间要大于业务最大耗时但也不能太长否则故障时锁无法释放。5.5 现象数据库连接池耗尽所有接口超时原因慢 SQL 或长事务占住连接。常见的是订单查询没走索引或者事务里包含了远程调用。解决开启慢查询日志找出执行超过 1 秒的 SQL。事务里禁止远程调用远程调用放到事务外。连接池设最大等待时间超时快速失败而不是无限等待。6. 架构验证与压测用数据证明你的方案能扛住架构写完不是终点能证明它扛得住才是。我一般用三步验证单元测试覆盖核心状态机、集成测试跑通下单全链路、压测验证峰值吞吐。压测工具用 JMeter 或 wrk重点压三个接口商品查询、下单、支付回调。下单接口的压测要模拟真实用户行为加购、填地址、提交订单按比例分布不能只压一个接口。# wrk 压测商品查询12 线程400 连接持续 60 秒 wrk -t12 -c400 -d60s --latency http://localhost:8080/api/product/1001参数说明-t12是线程数一般设为 CPU 核数-c400是连接数模拟并发用户-d60s是持续时间。看结果重点看 P99 延迟和错误率P99 超过 500ms 就要优化。压测时观察三个指标CPU 使用率、数据库 QPS、Redis 命中率。CPU 到 80% 就是瓶颈信号数据库 QPS 到 5000 要考虑分库分表Redis 命中率低于 90% 要检查缓存策略。最后说个我自己的习惯每次架构改动前先写一份「故障假设清单」——如果这个模块挂了系统会怎样用户会看到什么我怎么在 5 分钟内恢复。这份清单比任何架构图都管用因为它逼你面对最坏情况。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?