课程六:电商平台
课程六:大型电商平台架构演进案例
目标:电商平台是互联网架构复杂性的集大成者。本课程将带你亲历一个典型电商平台从零起步,逐步应对用户量激增、业务功能扩展、高并发挑战、数据一致性难题,最终演化为复杂分布式系统的全过程。你将深入理解微服务拆分、服务治理、分布式事务、缓存应用、数据库扩展、消息队列解耦、异地多活等核心架构设计。
阶段 0:万事开头难(单体应用 + MySQL)
系统描述
- 一个初创电商网站,核心功能:用户注册登录、商品浏览、购物车、下单、简单后台管理。
- 技术栈:LAMP/LNMP(Linux + Apache/Nginx + MySQL + PHP/Python/Java)
- Web 服务器:Apache 或 Nginx
- 应用框架:如 PHP 的 Laravel/ThinkPHP,Java 的 Spring Boot
- 数据库:单个 MySQL 实例,所有业务数据都在里面。
- 部署:所有代码打包成一个 WAR/JAR 包或直接部署 PHP 文件到单台服务器。
当前架构图
graph TD
User[用户] --> Browser[浏览器];
Browser --> LB("负载均衡器, 可选");
LB --> AppServer("应用服务器: Nginx + PHP/Java App");
AppServer --> DBMaster("MySQL 主库");
Admin[管理员] --> AppServer;
此刻的痛点:
- 开发效率低:所有功能耦合在一个代码库,几个人改同一个文件容易冲突,代码合并困难。任何小改动都需要整个应用重新编译、测试、部署。
- 技术栈单一:无法为不同业务模块选择最合适的技术(比如搜索模块可能用 Elasticsearch 更好)。
- 可靠性差:一个不起眼的功能 Bug 可能导致整个网站瘫痪。
- 扩展性受限:无法针对性地扩展某个瓶颈模块(如商品查询),只能加机器部署整个应用,成本高。
阶段 1:顶不住了 → 应用与数据拆分
挑战浮现
- 用户量和订单量快速增长,单体应用不堪重负,响应变慢。
- 单个 MySQL 数据库压力巨大,主从延迟增大,写入瓶颈明显。
- 不同业务线的开发团队(如商品团队、订单团队、用户团队)之间相互影响,发布协调困难。
❓ 架构师的思考时刻:如何拆解这个庞然大物,提升整体容量和开发效率?
(垂直拆分还是水平拆分?数据库怎么拆?拆分后服务间如何通信?)
✅ 演进方向:垂直拆分应用 + 数据库主从分离/分库
- 应用垂直拆分(按业务线):
- 将单体应用按照业务边界拆分成多个独立的子系统(或称为服务),如:用户服务、商品服务、订单服务、支付服务。
- 每个服务有自己独立的代码库、开发团队、部署流程。
- 初期服务间可以通过 HTTP API (RESTful) 或 RPC (如 Dubbo, gRPC, Thrift) 进行通信。
- 数据库主从分离:
- 配置 MySQL 主从复制,将读请求(占大部分)路由到从库 (Slave),写请求路由到主库 (Master)。
- 引入数据库中间件(如 MyCAT, ShardingSphere-Proxy)或在应用层实现读写分离逻辑,对应用层屏蔽主从细节。
- (可选)数据库垂直分库:
- 如果单一业务的数据量或 QPS 仍然过大,可以将该业务的数据库进一步垂直拆分,例如将用户相关表(用户基本信息、地址、积分)放到
user_db
,商品相关表放到product_db
。 - 注意:垂直分库会引入跨库 Join 的问题,通常需要通过服务层聚合数据来解决,尽量避免数据库层面的直接 Join。
- 如果单一业务的数据量或 QPS 仍然过大,可以将该业务的数据库进一步垂直拆分,例如将用户相关表(用户基本信息、地址、积分)放到
架构调整:
graph TD
User --> LB("负载均衡器");
LB --> Gateway("API 网关 / BFF");
Gateway --> UserService("用户服务");
Gateway --> ProductService("商品服务");
Gateway --> OrderService("订单服务");
UserService --> UserDBMaster("用户库 Master");
UserService --> UserDBSlave("用户库 Slave");
ProductService --> ProductDBMaster("商品库 Master");
ProductService --> ProductDBSlave("商品库 Slave");
OrderService --> OrderDBMaster("订单库 Master");
OrderService --> OrderDBSlave("订单库 Slave");
%% 读写分离
UserService -- 写 --> UserDBMaster;
UserService -- 读 --> UserDBSlave;
ProductService -- 写 --> ProductDBMaster;
ProductService -- 读 --> ProductDBSlave;
OrderService -- 写 --> OrderDBMaster;
OrderService -- 读 --> OrderDBSlave;
阶段 2:服务多了管不过来 → 引入服务治理与分布式配置
新的挑战:微服务之殇初现
- 服务数量增多,服务间的依赖关系变得复杂。
- 如何有效地发现新上线的服务实例?
- 如何对服务调用进行负载均衡?
- 服务挂了怎么办?如何进行熔断、降级,防止雪崩效应?
- 各个服务的配置(数据库地址、第三方 Key 等)散落在各处,管理混乱。
❓ 架构师的思考时刻:如何管理好这堆"小服务"?配置怎么集中管理?
(需要注册中心吗?客户端负载均衡还是服务端?熔断限流怎么做?配置变更如何实时生效?)
✅ 演进方向:引入服务注册发现 + 配置中心 + 服务治理框架
- 服务注册与发现:
- 引入注册中心(如 Consul, Nacos, ZooKeeper, Eureka)。
- 服务提供者启动时将自己的地址注册到注册中心。
- 服务消费者从注册中心获取服务提供者的地址列表。
- 服务调用与负载均衡:
- 服务消费者获取到地址列表后,通过负载均衡算法(如轮询、随机、加权)选择一个实例进行调用。这通常由 RPC 框架(如 Dubbo)或专门的客户端库(如 Ribbon)实现。
- 熔断、降级、限流:
- 引入服务治理框架(如 Sentinel, Hystrix)或 RPC 框架自带的能力。
- 熔断:当某个服务调用持续失败达到阈值时,暂时"熔断"对其的调用,快速失败,避免资源耗尽。
- 降级:在高并发或非核心服务故障时,主动关闭或简化某些非核心功能,保证核心流程可用。
- 限流:控制单位时间内服务的请求量,防止系统被突发流量打垮。
- 分布式配置中心:
- 引入配置中心(如 Nacos, Apollo, Spring Cloud Config)。
- 将所有服务的配置集中存储在配置中心。
- 应用启动时从配置中心拉取配置,并能监听配置变更,实现配置的动态更新。
架构调整(增加治理与配置层):
graph TD
subgraph "服务治理与配置"
Registry("Nacos/Consul 注册中心");
ConfigCenter("Nacos/Apollo 配置中心");
end
subgraph "服务层"
UserService -- 注册/发现 --> Registry;
UserService -- 拉取配置 --> ConfigCenter;
ProductService -- 注册/发现 --> Registry;
ProductService -- 拉取配置 --> ConfigCenter;
OrderService -- 注册/发现 --> Registry;
OrderService -- 拉取配置 --> ConfigCenter;
%% 服务间调用
OrderService -- "RPC (负载均衡/熔断)" --> UserService;
OrderService -- "RPC (负载均衡/熔断)" --> ProductService;
end
subgraph "数据层"
UserDB("用户库");
ProductDB("商品库");
OrderDB("订单库");
end
UserService --> UserDB;
ProductService --> ProductDB;
OrderService --> OrderDB;
阶段 3:读压力山大 → 引入分布式缓存
挑战再升级:数据库读瓶颈
- 随着用户量进一步增长,"双十一"等大促活动来临,商品详情页、首页推荐等高频读取场景的 QPS 飙升。
- 即使做了主从分离,大量读请求仍然打垮数据库从库。
❓ 架构师的思考时刻:如何在不增加太多数据库成本的情况下,大幅提升读性能?
(缓存是王道。用哪种缓存?Redis 还是 Memcached?缓存雪崩、穿透、击穿怎么办?)
✅ 演进方向:引入分布式缓存集群 (Redis/Memcached)
- 缓存选型:
- 通常选择 Redis,因为它支持更丰富的数据结构(String, Hash, List, Set, Sorted Set),并且有持久化能力,可以做更多事情。
- Memcached 相对更简单,内存管理效率可能略高,适合纯粹的 KV 缓存。
- 缓存策略:
- Cache-Aside Pattern (旁路缓存):最常用。读:先读缓存,没有则读数据库,再写回缓存。写:先更新数据库,然后删除 (invalidate) 缓存(而不是更新缓存,以避免并发更新问题)。
- 为缓存设置合理的过期时间 (TTL)。
- 缓存位置:
- 本地缓存 (Local Cache):如 Guava Cache, Caffeine。在应用JVM内部缓存,速度最快,但容量有限,且数据不一致风险高。
- 分布式缓存 (Distributed Cache):如 Redis Cluster, Memcached Cluster。独立部署,容量大,所有服务实例共享,是主要使用的缓存层。
- 缓存问题应对:
- 缓存穿透(查不存在的数据):用布隆过滤器 (Bloom Filter) 预判,或缓存空对象。
- 缓存击穿(热点 Key 过期):用分布式锁(如 Redisson)或互斥锁,只让一个请求去加载数据并写回缓存。
- 缓存雪崩(大量 Key 同时过期):设置随机过期时间,或使用缓存预热,或做多级缓存(本地缓存 + 分布式缓存)。
架构调整(增加缓存层):
graph TD
subgraph "应用层"
UserService;
ProductService;
OrderService;
end
subgraph "缓存层"
RedisCluster("Redis 集群");
end
subgraph "数据层"
UserDB("用户库");
ProductDB("商品库");
OrderDB("订单库");
end
%% 读写路径
UserService -- 读 --> RedisCluster -- Cache Miss --> UserDB;
UserService -- "写DB & 失效缓存" --> UserDB;
ProductService -- 读 --> RedisCluster -- Cache Miss --> ProductDB;
ProductService -- "写DB & 失效缓存" --> ProductDB;
OrderService -- 读 --> RedisCluster -- Cache Miss --> OrderDB;
OrderService -- "写DB & 失效缓存" --> OrderDB;
阶段 4:写也扛不住了 → 数据库水平拆分 (分库分表)
挑战登顶:数据库写瓶颈与容量极限
- 订单量、用户量持续爆炸式增长,单一业务库(即使做了主从)的写 QPS 也达到瓶颈。
- 单表数据量过大(如订单表几百亿行),查询、索引维护、DDL 操作都变得极其缓慢甚至不可能。
- 数据库的存储容量也接近极限。
❓ 架构师的思考时刻:数据库的终极扩展方案是什么?
(垂直拆分已到头。如何进行水平拆分?按什么维度分?分片键怎么选?分布式 ID 怎么生成?如何平滑迁移?)
✅ 演进方向:数据库水平拆分 (Sharding) + 分布式 ID 生成器
- 水平拆分策略:
- 按范围 (Range):如按时间范围(每月一张订单表)或 ID 范围。优点是扩展简单,缺点是可能有数据热点(如最近一个月的数据访问最频繁)。
- 按哈希 (Hash):选择一个分片键 (Sharding Key)(如
user_id
或order_id
),计算哈希值,然后对分库/分表数量取模,决定数据落在哪个库/表。- 选择合适的分片键至关重要:通常选择查询时最常用的字段,如用户 ID、订单 ID、商品 ID。需要避免热点(如大卖家的所有订单落在一个分片)。
- 常用组合:先按
user_id
Hash 分库,库内再按order_id
Hash 或按时间 Range 分表。
- 分库分表中间件:
- 引入数据库分片中间件(如 ShardingSphere-JDBC/Proxy, TDDL, MyCAT)。
- 中间件负责解析 SQL,根据分片规则将请求路由到正确的物理库/表,并聚合结果,对应用层尽量透明。
- 分布式 ID 生成器:
- 分库分表后,数据库自增主键不再全局唯一。需要引入分布式 ID 生成服务。
- 常用方案:雪花算法 (Snowflake)、UUID(太长,不推荐作主键)、数据库号段模式、Redis Incr。
- 数据迁移与扩容:
- 需要制定详细的数据迁移方案(如双写、增量同步+全量校对)和平滑的扩容计划(如倍增扩容)。这是非常复杂且风险高的操作。
架构调整(数据库层深度改造):
graph TD
subgraph "应用层"
UserService --> ShardingMiddleware("分库分表中间件 ShardingSphere/TDDL");
OrderService --> ShardingMiddleware;
end
subgraph "分布式 ID"
IDGenerator("分布式 ID 生成服务");
UserService -- 获取 ID --> IDGenerator;
OrderService -- 获取 ID --> IDGenerator;
end
subgraph "数据库集群 (Sharded)"
UserDB_0("UserDB_0");
UserDB_1("UserDB_1");
...
UserDB_N("UserDB_N");
OrderDB_0("OrderDB_0");
OrderDB_1("OrderDB_1");
...
OrderDB_M("OrderDB_M");
end
ShardingMiddleware -- 路由读写 --> UserDB_0;
ShardingMiddleware -- 路由读写 --> UserDB_1;
ShardingMiddleware -- 路由读写 --> UserDB_N;
ShardingMiddleware -- 路由读写 --> OrderDB_0;
ShardingMiddleware -- 路由读写 --> OrderDB_1;
ShardingMiddleware -- 路由读写 --> OrderDB_M;
阶段 5:服务间依赖与事务难题 → 引入消息队列与分布式事务
棘手问题:服务强依赖与数据一致性
- 业务流程越来越长,跨多个服务的调用链变深。例如:下单操作需要依次调用订单服务、库存服务、积分服务、优惠券服务...
- 同步调用问题:
- 性能差:整个调用链耗时是所有服务耗时之和。
- 可靠性低:任何一个下游服务故障,都会导致整个下单流程失败。
- 分布式事务问题:
- 如何保证跨多个服务的操作要么都成功,要么都失败?(如下单成功了,库存扣减失败了怎么办?)
- 数据库层面的两阶段提交 (2PC/XA) 在互联网场景下性能太差,基本不使用。
❓ 架构师的思考时刻:如何解耦服务?如何保证最终一致性?
(同步改异步?用什么中间件?分布式事务有哪些成熟方案?各自优缺点是什么?)
✅ 演进方向:引入消息队列 (MQ) 实现异步解耦 + 基于 MQ 的最终一致性方案 / Seata 等
- 消息队列 (MQ) 异步解耦:
- 引入消息队列(如 Kafka, RocketMQ, RabbitMQ, Pulsar)。
- 将服务间的同步强依赖调用改为异步消息驱动。
- 例如:订单服务创建订单成功后,发送一条"订单已创建"的消息到 MQ。库存服务、积分服务等订阅该消息,各自完成后续操作。
- 优点:解耦(服务间不直接依赖),削峰填谷(应对流量洪峰),提升性能和可用性。
- 基于 MQ 的最终一致性(事务消息):
- 很多 MQ(如 RocketMQ)提供事务消息功能,可以实现类似 2PC 的效果,保证"消息发送"和"本地事务(如创建订单)"要么都成功,要么都失败。
- 下游服务需要保证幂等消费消息。
- 这是一种广泛使用的柔性事务、最终一致性方案。
- (可选) 分布式事务框架 Seata:
- 如果需要更强的事务协调能力或对一致性要求更高,可以考虑引入分布式事务框架 Seata。
- Seata 提供 AT(自动补偿)、TCC(Try-Confirm-Cancel)、SAGA(长事务编排)、XA 等多种模式,其中 AT 模式对业务侵入较小。
- 但引入 Seata 会增加系统的复杂度和依赖。
架构调整(引入 MQ 与事务协调):
graph TD
subgraph "服务层"
OrderService -- 1. 创建订单 --> OrderDB;
OrderService -- 2. 发送事务消息 --> MQ(Kafka/RocketMQ);
end
subgraph "消息队列"
MQ -- 订单创建消息 --> InventoryService(库存服务);
MQ -- 订单创建消息 --> PointsService(积分服务);
MQ -- 订单创建消息 --> CouponService(优惠券服务);
end
subgraph "下游服务"
InventoryService -- 消费消息 & 扣减库存 --> InventoryDB;
PointsService -- 消费消息 & 增加积分 --> PointsDB;
CouponService -- 消费消息 & 核销券 --> CouponDB;
end
%% (可选) 分布式事务协调器
%% SeataCoordinator(Seata TC);
%% OrderService -- 注册全局事务 --> SeataCoordinator;
%% InventoryService -- 注册分支事务 --> SeataCoordinator;
阶段 6:可用性挑战 → 异地多活与容灾
终极挑战:单地域故障与业务连续性
- 业务规模巨大,对可用性要求达到极致(如 99.99% 或更高)。
- 单个数据中心发生故障(如断电、网络中断、自然灾害)将导致整个业务中断,损失巨大。
- 需要实现跨地域的容灾能力,甚至做到异地多活。
❓ 架构师的思考时刻:如何让系统扛住城市级的灾难?
(双活还是多活?数据如何跨地域同步?流量如何调度?如何保证一致性?)
✅ 演进方向:构建异地多活架构
- 多数据中心部署:
- 在不同地理区域(如华北、华东、华南)建立多个独立的数据中心(IDC)。
- 每个数据中心部署完整的应用服务和数据库实例。
- 数据同步:
- 核心挑战:如何在多个活跃的数据中心之间实时同步数据并解决冲突?
- 数据库层面:使用支持多主写入和冲突解决的数据库(如某些 NewSQL 数据库)或通过数据同步中间件(如 Otter, Canal 配合业务逻辑)实现。
- 缓存层面:通常保证最终一致性,或只写本地缓存,跨地域失效。
- 流量调度:
- 使用全局流量管理器 (GTM) 或基于 DNS 的智能解析、HTTPDNS 等技术。
- 根据用户地理位置、网络延迟、数据中心负载、可用性状态等因素,将用户请求智能地路由到最近或最合适的数据中心。
- 异地多活单元化 (Unitization):
- 更高级的模式是将用户或数据按某种维度(如
user_id
哈希)分片,每个分片(称为一个 Unit 或 Cell)的数据和流量闭环在某个或某几个数据中心内处理。 - 可以实现更精细的流量调度和故障隔离。
- 更高级的模式是将用户或数据按某种维度(如
- 容灾切换:
- 需要有完善的监控体系和自动化切换预案,当某个数据中心故障时,能快速将流量切换到其他可用中心。
架构调整(多地域部署):
graph TD
subgraph "全局流量调度"
GTM(全局流量管理器 GSLB/HTTPDNS);
end
subgraph "RegionA (华北)"
LB_A(负载均衡器);
APIGW_A(API 网关);
Services_A(应用服务集群);
Cache_A(缓存集群);
DB_A(数据库集群);
MQ_A(消息队列);
DataSync_A(数据同步组件);
GTM -- 流量 --> LB_A;
LB_A --> APIGW_A --> Services_A;
Services_A --> Cache_A;
Services_A --> DB_A;
Services_A --> MQ_A;
DB_A <-.-> DataSync_A;
end
subgraph "RegionB (华东)"
LB_B(负载均衡器);
APIGW_B(API 网关);
Services_B(应用服务集群);
Cache_B(缓存集群);
DB_B(数据库集群);
MQ_B(消息队列);
DataSync_B(数据同步组件);
GTM -- 流量 --> LB_B;
LB_B --> APIGW_B --> Services_B;
Services_B --> Cache_B;
Services_B --> DB_B;
Services_B --> MQ_B;
DB_B <-.-> DataSync_B;
end
%% 数据同步链路
DataSync_A <-->|"跨地域同步"| DataSync_B;
User --> GTM;