高并发系统设计与演进实战指南
极客时间《高并发系统40讲》总结
第一章:高并发系统的设计理念与目标
在互联网业务飞速发展的今天,高并发系统设计已成为架构设计的核心挑战。设计一个优秀的系统,必须围绕三个核心目标展开:高性能、高可用、可扩展。
1.1 系统设计的三大目标
- 高性能 (High Performance) 性能是系统设计的基石。它是指系统能够以更短的响应时间处理更多的并发请求。
- 度量指标:主要包括吞吐量(Throughput)*和*响应时间(Response Time)。这两个指标呈倒数关系。通常使用平均值、最大值以及分位值(如 99 分位值,即 99% 的请求响应时间在多少毫秒以内)来衡量。
- 优化原则:性能优化应遵循“问题导向”(不盲目优化)、“八二原则”(20% 的优化解决 80% 的瓶颈)、“数据支撑”以及“持续优化”。
- 阿姆达尔定律:该定律描述了通过增加处理核心数来提升性能的极限,提示我们在并行计算中,串行部分将决定系统的加速上限。
- 高可用 (High Availability, HA) 高可用指系统具备较高的无故障运行能力,通常用“N 个 9”来衡量。
- 度量:Availability = MTBF / (MTBF + MTTR)。MTBF 为平均故障间隔时间,MTTR 为平均恢复时间。核心系统通常要求达到四个 9(99.99%),即年故障时间不超过 1 小时。
- 设计原则:Design for failure(为失败而设计)。必须预设故障必然发生,从而设计自动发现和恢复机制。
- 可扩展性 (Scalability) 指系统可以通过增加机器资源的方式线性提高处理能力。
- 瓶颈:无状态服务(如 Web 层)易于扩展,而有状态组件(如数据库)扩展难度大。
- 拆分思路:通过业务维度(垂直拆分)和数据维度(水平拆分)将复杂系统解耦。
1.2 高并发设计的通用方法
应对高并发大流量的冲击,业界通用的三种方法论包括:
- Scale-out(横向扩展):分而治之,通过分布式部署将流量分流到多台服务器上。
- 缓存:利用内存等高速介质提升读取性能,降低数据库压力,遵循“空间换时间”思想。
- 异步:在未处理完成前先返回,后续通过回调或消息通知,减少调用方阻塞,削峰填谷。
第二章:数据库架构演进与优化
随着流量增加,数据库往往是最先遇到的瓶颈。演进过程通常从单机优化开始,逐步走向分布式架构。
2.1 数据库连接池化
在系统初期,频繁创建和销毁数据库连接会消耗大量资源(TCP 握手、认证)。
- 解决方案:引入数据库连接池,预先建立连接并复用。
- 关键配置:需合理设置最小连接数和最大连接数。如果连接长时间空闲,数据库可能会主动关闭,因此连接池需具备定期检测连接可用性(如发送
select 1)的机制。
2.2 主从读写分离
大部分互联网业务呈现“读多写少”的特征。
- 架构:将数据库分为主库(Master,负责写)和从库(Slave,负责读)。主库将 Binlog 同步给从库,从库重放实现数据复制。
- 挑战:主从延迟。数据写入主库后,从库可能尚未同步,导致业务读不到最新数据。
- 应对策略:
- 数据冗余:关键数据在消息队列中携带,避免回查数据库。
- 缓存:写主库同时写缓存,读请求优先读缓存。
- 强制读主:对于一致性要求极高的场景(如注册后立即登录),强制查询主库,但需谨慎使用以免压垮主库。
2.3 分库分表(Sharding)
当单表数据量达到千万级或亿级,或者单库写入成为瓶颈时,必须进行拆分。
- 垂直拆分:按业务模块将表拆分到不同的数据库(如用户库、订单库),实现专库专用和故障隔离。
- 水平拆分:将单表按规则拆分到多个库表中。
- 哈希拆分:按 ID 哈希取模,数据分布均匀,但扩容迁移复杂。
- 区间拆分:按时间或 ID 区间拆分,扩容简单,但容易产生热点数据。
- 引入的问题:
- 分区键限制:查询必须带上分区键,否则会导致全库扫描。
- Join 与聚合限制:跨库 Join 性能极差,Count() 需要单独计数表支持。
- ID 全局唯一性:自增 ID 失效,需引入分布式 ID 生成器。
2.4 分布式 ID 生成:Snowflake 算法
为了解决分库分表后的 ID 问题,推荐使用 Snowflake(雪花)算法。
- 结构:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。
- 优势:全局唯一、单调递增(有利于 B+ 树索引性能)、包含业务含义(时间、机房信息)。
- 注意事项:严重依赖系统时间,需处理时钟回拨问题。低并发下可能导致 ID 末位总是偶数,造成分片不均,可随机起始序列号解决。
2.5 NoSQL 的互补应用
关系型数据库在某些场景下存在短板,需引入 NoSQL 作为补充。
- 写入性能:利用 LSM 树结构(如 HBase)将随机写转换为顺序写,提升写入吞吐。
- 全文搜索:使用 Elasticsearch 利用倒排索引解决模糊查询(如
LIKE %keyword%)无法走索引的问题。 - 扩展性:MongoDB 等原生支持 Replica Set 和 Sharding,扩容更加自动化。
2.6 数据库迁移方案
在架构升级(如上云、分库分表)时,数据迁移必须保证平滑和数据一致性。
- 双写方案:
- 新库配置为旧库从库同步历史数据。
- 业务改造,开启双写(异步写新库),并记录写入新库失败的日志用于补偿。
- 数据校验,抽取数据比对。
- 灰度切换读流量,最终切断旧库写入。
- 级联同步:适合全量迁移上云。旧库 -> 新库 -> 备库,通过主从复制层层同步,切换时只需短暂亦可。
第三章:缓存架构设计
缓存是提升系统性能的“银弹”,通过将热点数据存储在内存中,极大降低数据库压力。
3.1 缓存分类
- 静态缓存:CDN、Nginx 静态页面,用于加速静态资源(图片、视频、JS/CSS)访问。
- 分布式缓存:Redis、Memcached,用于加速动态请求,通过集群突破单机内存限制。
- 本地缓存:Guava Cache 等,部署在应用进程内,无需跨网络,用于阻挡极端热点查询(如秒杀前几秒的配置读取)。
3.2 缓存读写策略
- Cache Aside(旁路缓存):业界最常用策略。
- 读:先读缓存,命中返回;未命中读 DB,然后回种缓存。
- 写:先更新 DB,后删除缓存。
- 注意:先删缓存后写库会导致并发下的脏数据;先更新 DB 后更新缓存也可能因并发导致不一致。推荐配合分布式锁或较短的过期时间使用。
- Read/Write Through:应用只与缓存交互,缓存负责同步 DB。
- Write Back:只写缓存,异步刷新到 DB(如 Linux Page Cache)。性能最好,但在掉电等故障下会丢失数据。
3.3 缓存高可用
- 客户端分片:一致性 Hash 算法(带虚拟节点),减少节点变动时的数据漂移。
- 中间代理:Codis、Twemproxy,代理层负责路由和分片。
- 服务端方案:Redis Sentinel(哨兵)实现主从自动切换;Redis Cluster 实现去中心化的分片和高可用。
3.4 常见痛点与解决方案
- 缓存穿透:查询不存在的数据,请求直击数据库。
- 解决:回种空值(设短过期)或使用布隆过滤器(Bloom Filter)拦截。
- 缓存击穿 (Dog-pile effect):热点 Key 失效,大量并发请求穿透。
- 解决:分布式锁(只允许一个线程回源加载),或后台线程定时更新。
- 缓存雪崩:大量缓存同时失效。
- 解决:过期时间加随机值,多级缓存架构。
3.5 CDN 加速
CDN 利用 DNS 的 CNAME 解析和 GSLB(全局负载均衡),将用户请求调度到最近的边缘节点,解决跨地域网络延迟问题。对于静态资源(图片、视频),CDN 是必选项。
第四章:消息队列 (MQ)
消息队列是分布式系统中的“缓冲剂”和“解耦剂”。
4.1 核心作用
- 削峰填谷:在秒杀等场景下,暂存瞬时流量,避免数据库崩溃。通过控制消费速度保护后端。
- 异步处理:将非核心流程(如发货后发送通知、积分处理)移出主流程,降低响应时间。
- 解耦合:生产者和消费者仅依赖队列,互不感知,便于独立扩展。
4.2 可靠性保证
- 防止消息丢失:
- 生产端:失败重试。
- 存储端:配置同步刷盘或 ISR 机制(如 Kafka
acks=all)保证多副本同步。 - 消费端:确保业务逻辑处理完成后再提交 Offset(消费进度)。
- 防止重复消费(幂等性):
- 网络抖动会导致 ACK 丢失从而重发消息。
- 解决:业务层必须实现幂等。例如使用唯一业务 ID 在数据库检查是否已处理,或利用数据库乐观锁(版本号)更新。
4.3 性能与延迟监控
- 零拷贝:利用
Sendfile技术,直接从内核缓冲区发送到网卡,减少 CPU 上下文切换和拷贝次数。 - 监控:利用 JMX 或生成“监控消息”定期写入并检测消费延迟,对消息堆积进行报警。
- 优化:增加 Partition(分区)以增加并发度,或在 Consumer 内部使用线程池并行处理。
第五章:分布式服务架构 (微服务)
当单体架构面临研发效率低、部署困难、连接数瓶颈时,微服务化是必然选择。
5.1 服务拆分原则
- 高内聚低耦合:每个服务职责单一,关注点分离。
- 先粗后细:逐步拆分,优先剥离独立性强的边界服务(如短信服务)。
- 接口兼容:参数封装,避免破坏性变更。
5.2 核心组件
- RPC 框架:解决跨进程通信。
- IO 模型:推荐同步多路 IO 复用(如 Netty, Epoll),避免阻塞。
- 序列化:Protobuf/Thrift(性能好、跨语言、字节数小),JSON(通用但性能稍差)。
- 注册中心:服务自动注册与发现(Service Discovery)。
- 机制:心跳检测服务健康状态。需防范“通知风暴”和网络抖动导致的节点过度摘除(设置自我保护阈值)。
- API 网关:系统的统一入口。
- 功能:路由转发、协议转换、认证鉴权、限流熔断、黑白名单。
- 实现:Zuul, Kong。采用责任链模式扩展功能,利用 IO 多路复用提升性能。
5.3 Service Mesh
为了解决跨语言服务治理复用难的问题,Service Mesh 将治理逻辑下沉到 Sidecar 代理(如 Envoy),业务代码只需与 Localhost 通信。
- 实现方式:iptables 流量劫持或轻量级客户端。
- 代表:Istio, Linkerd。
第六章:系统治理与维护
高可用系统不仅在于设计,更在于运行期的治理。
6.1 服务保护机制
- 熔断 (Circuit Breaker):当调用下游服务失败率达到阈值时(如 50%),暂时断开调用,快速返回错误,防止级联故障(雪崩)。状态机:关闭 -> 打开 -> 半打开。
- 降级:主动关闭非核心服务(如评论、推荐),释放资源保护核心业务(如下单)。可通过配置中心动态开关实现。
- 限流:限制并发请求数,防止系统过载。
- 算法:令牌桶(Token Bucket,允许突发流量)、漏桶(Leaky Bucket,平滑流量)、滑动窗口。
- 部署:网关层(全局限流)或 RPC 客户端层。
6.2 负载均衡
- 分类:
- 四层负载:LVS,性能高,工作在传输层。
- 七层负载:Nginx,灵活,工作在应用层。
- 客户端负载:Ribbon/Dubbo 内置。
- 策略:轮询、加权轮询、最小活跃数、一致性 Hash。推荐使用动态策略(如最小活跃数)以感知后端负载。
6.3 监控与配置
- 监控体系:
- 指标:Google 四个黄金信号(延迟、通信量、错误、饱和度)。
- APM:端到端监控,采集客户端网络、卡顿数据。
- 分布式追踪:利用
TraceId和SpanId串联跨服务调用,解决微服务下故障定位难的问题。
- 配置中心:如 Apollo、Nacos。实现配置的动态热更新,无需重启服务。需设计多级缓存(内存+文件)保证配置中心宕机不影响应用启动。
6.4 全链路压测
- 目的:在双 11 等大促前发现瓶颈。
- 核心技术:
- 流量隔离:使用“影子库”(Shadow DB),压测数据带标记,路由到影子库,不污染线上数据。
- 数据构造:基于线上真实流量回放或脱敏拷贝。
第七章:高可用架构演进
7.1 多机房部署
- 同城双活:机房延迟低(<3ms)。DB 主库在一个机房,备机房读从库。容灾级别:机房级。
- 异地多活:机房延迟大(>30ms)。
- 核心原则:数据闭环。用户按 ID 分片到不同“单元”,所有读写在单元内完成。
- 数据同步:跨机房异步复制,容忍数据短时间不一致。
第八章:典型案例实战
8.1 计数系统 (微博场景)
- 挑战:万亿级数据,高并发写。
- 演进:
- MySQL:性能差,分库分表也难抗。
- Redis:
Incr原子操作。优化存储结构,减少指针开销;使用 Hash 结构存储多条计数。 - 冷热分离:SSD + 内存。热数据在内存,冷数据 Dump 到磁盘(如 Pika 方案),降低成本。
8.2 信息流系统 (Feed)
- 推模式 (Push/写扩散):发微博时写入所有粉丝的“收件箱”。读性能好,但大 V 发博写压力巨大,不仅延迟高且存储成本高。
- 拉模式 (Pull/读扩散):只写“发件箱”。用户读时聚合关注人的发件箱。写性能好,但读聚合成本高,缓存带宽压力大。
- 推拉结合:
- 大 V 发微博:只推送给在线/活跃粉丝,或不推送。
- 普通用户发微博:推给所有粉丝。
- 读取时:合并拉取的大 V 内容和推送的普通内容。
8.3 未读数系统
- 全量通知(如系统公告):不给每个人存未读数。只记录用户最后读取的全局消息 ID,未读数 = 最新全局 ID - 用户 Last Read ID。
- 信息流未读:记录用户关注关系中所有人的博文总数快照。未读数 = (当前关注人总博文数) - (快照中的总数)。
总结
高并发系统的演进是一个持续的过程,遵循“从简单到复杂”、“问题驱动”的原则。
- 初期:通过垂直扩展 (Scale-up) 和简单的缓存、连接池解决问题。
- 中期:引入读写分离、分库分表解决存储瓶颈,利用消息队列削峰解耦,将单体应用拆分为微服务。
- 后期:建设完善的监控、治理(限流熔断)、压测体系,并向Service Mesh、异地多活等高级架构演进。
每一个技术组件(Redis、Kafka、MySQL、Nginx)都有其适用场景和副作用(如一致性问题),架构师的核心职责就是在业务需求、成本、性能和复杂度之间做权衡(Trade-off)。