当你开发的系统从几百个用户增长到几百万、几千万,单台服务器已经无法承载巨大的流量和数据量。这时候,系统架构必须从单体应用演进为分布式系统,并从低并发走向高并发。分布式与高并发是软件开发进阶技能中最具挑战性也最有价值的部分,它融合了网络通信、数据一致性、容错、性能优化、架构设计等众多领域的知识。
什么是分布式系统? 分布式系统是由多个通过网络通信的计算机节点组成的系统,对外表现为一个统一的整体。它的目标是:可扩展性(Scalability)、高可用性(Availability)、容错性(Fault Tolerance)。
什么是高并发? 高并发是指在极短的时间内,系统需要处理大量请求(例如双十一的秒杀、春运抢票)。高并发系统需要充分利用多核、多机并行处理,并通过多种技术手段(缓存、异步、限流、降级)保证系统稳定。
本文将从基础理论(CAP、BASE)、负载均衡、缓存设计、消息队列、分布式事务、分布式锁、服务治理与微服务、限流熔断、一致性哈希、分布式 ID 等方面,结合大量代码示例和真实场景,深入讲解分布式与高并发的核心技能。
预备知识:你已经熟悉后端开发(如 Java/Go/Python),了解数据库和网络基础,写过 Web 服务。
第一部分:分布式系统基础理论 —— CAP 与 BASE
1.1 CAP 定理
CAP 定理指出,一个分布式系统最多只能同时满足以下三个特性中的两个:
一致性(Consistency):所有节点在同一时刻看到相同的数据。即写操作完成后,读操作必须返回最新的写入值。
可用性(Availability):每个请求在有限时间内都能收到非错误的响应(但不保证是最新数据)。
分区容错性(Partition Tolerance):当网络分区(节点之间通信中断)发生时,系统依然能继续运行。
在实际分布式系统中,网络分区是不可避免的(交换机故障、光缆断裂),所以 P 必须满足。因此,系统需要在 CP(一致+分区容忍)和 AP(可用+分区容忍)之间做出权衡。
CP 系统:保证强一致性,但可能在网络分区时拒绝服务(例如 ZooKeeper、HBase、MongoDB 在某些配置下)。典型实现:多数派写入(Paxos/Raft)。
AP 系统:保证可用性,但可能读到不一致的数据(最终一致性,例如 Cassandra、DynamoDB、CouchDB)。典型实现:冲突解决(Last Write Wins, Merkle Tree 反熵)。
1.2 BASE 理论 —— 对 CAP 中一致性的弱化
BASE 是 Basically Available(基本可用)、Soft state(软状态) 和 Eventually consistent(最终一致性) 的缩写。它是对大型分布式系统实践经验的总结,强调用最终一致性换取高可用。
基本可用:系统出现故障时,允许部分功能降级(如响应时间变长、返回旧缓存数据)。
软状态:允许系统中存在中间状态,该状态不影响系统整体可用性(例如数据复制延迟)。
最终一致性:经过一段时间的异步修复,数据最终会达到一致。
最终一致性的变种:
因果一致性:有因果关系的操作需按顺序被所有节点看到。
会话一致性:同一个会话中读操作保证读到自己的写操作。
单调读一致性:一旦读到新值,不会回滚到旧值。
很多实际系统(如 DNS、Gossip 协议)都基于最终一致性设计。
第二部分:高并发架构概述 —— 纵向扩展与横向扩展
要支撑高并发,架构上要么向上(纵向扩展),换更强的服务器(CPU 更多、内存更大),要么向外(横向扩展),增加更多普通服务器。纵向扩展有物理上限且成本非线性增长,因此现代高并发系统主要依赖横向扩展。
横向扩展会带来以下挑战:
负载如何均匀分发到多台机器? → 负载均衡
用户会话(session)如何共享? → 分布式会话
数据如何分布到多个数据库? → 分库分表 / 分片
频繁读取的数据如何减轻数据库压力? → 多级缓存
大量写请求如何削峰填谷? → 消息队列
服务之间如何通信和治理? → 微服务 + 服务注册发现
下面将逐一深入讲解。
第三部分:负载均衡 —— 流量的交通警察
负载均衡器(Load Balancer)是分布式系统的门面,它接收所有客户端请求,然后按照一定策略分发到后端服务器。
3.1 负载均衡层次
DNS 负载均衡:最简单的全局负载,将域名解析到不同 IP。缺点:缓存生效慢,无法感知服务健康状态。
硬件负载均衡(F5、A10):性能极高,但昂贵,通常用于大型入口。
软件负载均衡(Nginx、HAProxy、LVS):开源、灵活,是主流选择。
客户端负载均衡(Ribbon、Spring Cloud LoadBalancer):内嵌在应用内,通过服务注册中心获取可用列表,自行选择节点。
3.2 常见负载均衡算法
3.3 Nginx 配置示例
upstream backend {
# 加权轮询
server 192.168.1.10 weight=3;
server 192.168.1.11 weight=1;
# 最少连接
least_conn;
# IP Hash
ip_hash;
# 健康检查(需 nginx plus 或第三方模块)
server 192.168.1.12 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
3.4 分布式会话问题与解决方案
当负载均衡使用轮询时,用户请求可能落在不同服务器,导致 session 丢失。解决方案:
粘性会话(Sticky Session):通过 IP Hash 或 cookie 绑定,但某台机器宕机时会话仍会丢失。
会话复制(Session Replication):在 Tomcat 等容器中同步 session 到所有节点,网络开销大,不推荐。
集中存储(推荐):将 session 存入 Redis 或数据库,所有服务器共享。Spring Session + Redis 是典型方案。
Spring Session + Redis 示例:
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("redis-host", 6379);
}
}
第四部分:缓存 —— 性能加速的利器
缓存是扛高并发最有效的手段之一。正确使用缓存可以让读请求在内存中直接返回,避免穿透到数据库。
4.1 缓存层次
客户端缓存(浏览器缓存、App 本地缓存)
CDN 缓存:分发静态资源,减轻源站压力。
反向代理缓存(Nginx、Varnish):缓存动态页面片段。
应用本地缓存(Guava Cache、Caffeine):进程内缓存,速度极快,但无法跨服务共享。
分布式缓存(Redis、Memcached):独立于应用,多节点共享。
4.2 缓存模式
4.2.1 Cache Aside(旁路缓存)
这是最常用的模式:
读:先读缓存,命中则返回;未命中则读数据库,写回缓存。
写:先更新数据库,然后删除缓存(或更新缓存,但删除更简单,避免并发脏数据)。
public User getUser(Long id) {
// 读缓存
User user = redis.get("user:" + id);
if (user != null) return user;
// 读数据库
user = db.query("SELECT * FROM user WHERE id = ?", id);
if (user != null) {
redis.setex("user:" + id, 3600, user);
}
return user;
}
public void updateUser(User user) {
db.update("UPDATE user SET name=? WHERE id=?", user.getName(), user.getId());
redis.del("user:" + user.getId()); // 删除缓存
}
为什么是删除而不是更新? 因为更新缓存可能有并发问题:两个写线程,一个更新了数据库,另一个用旧值覆盖了缓存。删除后,下次读会重新加载新值。
4.2.2 Read/Write Through(读写穿透)
应用只和缓存交互,缓存作为代理负责读写数据库。适合对应用透明的场景,但缓存层实现复杂。
4.2.3 Write Behind Caching(写回)
先写缓存,异步批量写数据库。写入性能极高,但可能丢失数据(缓存宕机)。适用对数据丢失不敏感的日志、计数等。
4.3 缓存三大痛点:穿透、击穿、雪崩
布隆过滤器示例(Guava 版)
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
BloomFilter<Long> bloom = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
// 初始化时将所有存在的 id 加入
for (Long id : allUserIds) {
bloom.put(id);
}
// 查询前先判断
if (!bloom.mightContain(userId)) {
return null; // 一定不存在,直接返回
}
// 否则走缓存/DB
互斥锁防止击穿(Redis 分布式锁)
public User getWithMutex(Long id) {
String key = "user:" + id;
User user = redis.get(key);
if (user != null) return user;
// 尝试获取锁
String lockKey = "lock:user:" + id;
Boolean locked = redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
user = db.query(...);
redis.setex(key, 3600, user);
} finally {
redis.del(lockKey);
}
} else {
// 等待一小段时间后重试
Thread.sleep(50);
return getWithMutex(id);
}
return user;
}
4.4 缓存一致性(双写一致性)
当数据库和缓存同时写时,保证一致性是难点。除了 Cache Aside 模式(先更新 DB 再删缓存),还可以:
订阅数据库变更日志(Canal):解析 MySQL binlog,异步刷新缓存。
延时双删:先删缓存,再更新 DB,然后延时(如 1 秒)再删一次,解决并发读导致的脏数据问题。
来源:
https://yyvgt.cn/