跳转至

课程六:电商平台

课程六:大型电商平台架构演进案例

目标:电商平台是互联网架构复杂性的集大成者。本课程将带你亲历一个典型电商平台从零起步,逐步应对用户量激增、业务功能扩展、高并发挑战、数据一致性难题,最终演化为复杂分布式系统的全过程。你将深入理解微服务拆分、服务治理、分布式事务、缓存应用、数据库扩展、消息队列解耦、异地多活等核心架构设计。


阶段 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 数据库压力巨大,主从延迟增大,写入瓶颈明显。
  • 不同业务线的开发团队(如商品团队、订单团队、用户团队)之间相互影响,发布协调困难。

❓ 架构师的思考时刻:如何拆解这个庞然大物,提升整体容量和开发效率?

(垂直拆分还是水平拆分?数据库怎么拆?拆分后服务间如何通信?)

✅ 演进方向:垂直拆分应用 + 数据库主从分离/分库

  1. 应用垂直拆分(按业务线)
    • 将单体应用按照业务边界拆分成多个独立的子系统(或称为服务),如:用户服务、商品服务、订单服务、支付服务。
    • 每个服务有自己独立的代码库开发团队部署流程
    • 初期服务间可以通过 HTTP API (RESTful)RPC (如 Dubbo, gRPC, Thrift) 进行通信。
  2. 数据库主从分离
    • 配置 MySQL 主从复制,将读请求(占大部分)路由到从库 (Slave)写请求路由到主库 (Master)
    • 引入数据库中间件(如 MyCAT, ShardingSphere-Proxy)或在应用层实现读写分离逻辑,对应用层屏蔽主从细节。
  3. (可选)数据库垂直分库
    • 如果单一业务的数据量或 QPS 仍然过大,可以将该业务的数据库进一步垂直拆分,例如将用户相关表(用户基本信息、地址、积分)放到 user_db,商品相关表放到 product_db
    • 注意:垂直分库会引入跨库 Join 的问题,通常需要通过服务层聚合数据来解决,尽量避免数据库层面的直接 Join。

架构调整

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 等)散落在各处,管理混乱。

❓ 架构师的思考时刻:如何管理好这堆"小服务"?配置怎么集中管理?

(需要注册中心吗?客户端负载均衡还是服务端?熔断限流怎么做?配置变更如何实时生效?)

✅ 演进方向:引入服务注册发现 + 配置中心 + 服务治理框架

  1. 服务注册与发现
    • 引入注册中心(如 Consul, Nacos, ZooKeeper, Eureka)。
    • 服务提供者启动时将自己的地址注册到注册中心。
    • 服务消费者从注册中心获取服务提供者的地址列表。
  2. 服务调用与负载均衡
    • 服务消费者获取到地址列表后,通过负载均衡算法(如轮询、随机、加权)选择一个实例进行调用。这通常由 RPC 框架(如 Dubbo)或专门的客户端库(如 Ribbon)实现。
  3. 熔断、降级、限流
    • 引入服务治理框架(如 Sentinel, Hystrix)或 RPC 框架自带的能力。
    • 熔断:当某个服务调用持续失败达到阈值时,暂时"熔断"对其的调用,快速失败,避免资源耗尽。
    • 降级:在高并发或非核心服务故障时,主动关闭或简化某些非核心功能,保证核心流程可用。
    • 限流:控制单位时间内服务的请求量,防止系统被突发流量打垮。
  4. 分布式配置中心
    • 引入配置中心(如 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)

  1. 缓存选型
    • 通常选择 Redis,因为它支持更丰富的数据结构(String, Hash, List, Set, Sorted Set),并且有持久化能力,可以做更多事情。
    • Memcached 相对更简单,内存管理效率可能略高,适合纯粹的 KV 缓存。
  2. 缓存策略
    • Cache-Aside Pattern (旁路缓存):最常用。读:先读缓存,没有则读数据库,再写回缓存。写:先更新数据库,然后删除 (invalidate) 缓存(而不是更新缓存,以避免并发更新问题)。
    • 为缓存设置合理的过期时间 (TTL)
  3. 缓存位置
    • 本地缓存 (Local Cache):如 Guava Cache, Caffeine。在应用JVM内部缓存,速度最快,但容量有限,且数据不一致风险高。
    • 分布式缓存 (Distributed Cache):如 Redis Cluster, Memcached Cluster。独立部署,容量大,所有服务实例共享,是主要使用的缓存层。
  4. 缓存问题应对
    • 缓存穿透(查不存在的数据):用布隆过滤器 (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 生成器

  1. 水平拆分策略
    • 按范围 (Range):如按时间范围(每月一张订单表)或 ID 范围。优点是扩展简单,缺点是可能有数据热点(如最近一个月的数据访问最频繁)。
    • 按哈希 (Hash):选择一个分片键 (Sharding Key)(如 user_idorder_id),计算哈希值,然后对分库/分表数量取模,决定数据落在哪个库/表。
      • 选择合适的分片键至关重要:通常选择查询时最常用的字段,如用户 ID、订单 ID、商品 ID。需要避免热点(如大卖家的所有订单落在一个分片)。
    • 常用组合:先按 user_id Hash 分库,库内再按 order_id Hash 或按时间 Range 分表。
  2. 分库分表中间件
    • 引入数据库分片中间件(如 ShardingSphere-JDBC/Proxy, TDDL, MyCAT)。
    • 中间件负责解析 SQL,根据分片规则将请求路由到正确的物理库/表,并聚合结果,对应用层尽量透明。
  3. 分布式 ID 生成器
    • 分库分表后,数据库自增主键不再全局唯一。需要引入分布式 ID 生成服务
    • 常用方案:雪花算法 (Snowflake)UUID(太长,不推荐作主键)、数据库号段模式Redis Incr
  4. 数据迁移与扩容
    • 需要制定详细的数据迁移方案(如双写、增量同步+全量校对)和平滑的扩容计划(如倍增扩容)。这是非常复杂且风险高的操作。

架构调整(数据库层深度改造)

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 等

  1. 消息队列 (MQ) 异步解耦
    • 引入消息队列(如 Kafka, RocketMQ, RabbitMQ, Pulsar)。
    • 将服务间的同步强依赖调用改为异步消息驱动。
    • 例如:订单服务创建订单成功后,发送一条"订单已创建"的消息到 MQ。库存服务、积分服务等订阅该消息,各自完成后续操作。
    • 优点:解耦(服务间不直接依赖),削峰填谷(应对流量洪峰),提升性能和可用性
  2. 基于 MQ 的最终一致性(事务消息)
    • 很多 MQ(如 RocketMQ)提供事务消息功能,可以实现类似 2PC 的效果,保证"消息发送"和"本地事务(如创建订单)"要么都成功,要么都失败。
    • 下游服务需要保证幂等消费消息。
    • 这是一种广泛使用的柔性事务最终一致性方案。
  3. (可选) 分布式事务框架 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% 或更高)。
  • 单个数据中心发生故障(如断电、网络中断、自然灾害)将导致整个业务中断,损失巨大。
  • 需要实现跨地域的容灾能力,甚至做到异地多活

❓ 架构师的思考时刻:如何让系统扛住城市级的灾难?

(双活还是多活?数据如何跨地域同步?流量如何调度?如何保证一致性?)

✅ 演进方向:构建异地多活架构

  1. 多数据中心部署
    • 不同地理区域(如华北、华东、华南)建立多个独立的数据中心(IDC)。
    • 每个数据中心部署完整的应用服务和数据库实例。
  2. 数据同步
    • 核心挑战:如何在多个活跃的数据中心之间实时同步数据并解决冲突?
    • 数据库层面:使用支持多主写入和冲突解决的数据库(如某些 NewSQL 数据库)或通过数据同步中间件(如 Otter, Canal 配合业务逻辑)实现。
    • 缓存层面:通常保证最终一致性,或只写本地缓存,跨地域失效。
  3. 流量调度
    • 使用全局流量管理器 (GTM) 或基于 DNS 的智能解析、HTTPDNS 等技术。
    • 根据用户地理位置、网络延迟、数据中心负载、可用性状态等因素,将用户请求智能地路由到最近或最合适的数据中心。
  4. 异地多活单元化 (Unitization)
    • 更高级的模式是将用户或数据按某种维度(如 user_id 哈希)分片,每个分片(称为一个 UnitCell)的数据和流量闭环在某个或某几个数据中心内处理。
    • 可以实现更精细的流量调度和故障隔离。
  5. 容灾切换
    • 需要有完善的监控体系自动化切换预案,当某个数据中心故障时,能快速将流量切换到其他可用中心。

架构调整(多地域部署)

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;