一、系统设计基础
1.1 高并发系统设计
Q1: 如何设计一个高并发的秒杀系统?
答案:
秒杀系统的核心特点:
业务特点:
- 瞬时流量巨大: 平时100 QPS,秒杀时10万+ QPS
- 库存有限: 只有100件商品,但10万人抢购
- 时间集中: 集中在开始的几秒内
- 读多写少: 大部分人只是看,真正下单的很少
技术挑战:
1. 如何承载10万QPS?
2. 如何防止超卖?
3. 如何防止黄牛刷单?
4. 如何保证用户体验?
架构设计:
第一层: 前端优化
1. 页面静态化
- 商品详情页提前生成HTML,部署到CDN
- 用户访问直接从CDN返回,不请求服务器
- 10万QPS全部由CDN承载,服务器压力为0
2. 按钮控制
- 秒杀开始前,按钮置灰
- 前端倒计时,精确到秒杀开始时间
- 开始后才允许点击
3. 限制请求频率
- 前端防抖: 用户连续点击,只发送一次请求
- 本地标记: 已提交订单的用户,不允许重复提交
代码示例 (前端):
<script>
let submitted = false;
function seckill() {
if (submitted) {
alert("您已提交订单");
return;
}
// 防抖: 禁用按钮
document.getElementById("btn").disabled = true;
axios.post("/api/seckill", { product_id: 123 })
.then(response => {
submitted = true;
alert("抢购成功");
})
.catch(error => {
document.getElementById("btn").disabled = false;
});
}
</script>
第二层: 网关层限流
目标: 10万请求只放行1万个到后端,其余直接拒绝
1. Nginx限流
- 限制单IP的请求速率
- 限制全局请求速率
配置示例 (Nginx):
http {
# 定义限流规则: 每个IP每秒最多10个请求
limit_req_zone $binary_remote_addr zone=user_limit:10m rate=10r/s;
# 定义全局限流: 所有IP每秒最多1万个请求
limit_req_zone $server_name zone=global_limit:10m rate=10000r/s;
server {
location /api/seckill {
# 应用限流规则
limit_req zone=user_limit burst=5 nodelay;
limit_req zone=global_limit burst=100;
proxy_pass http://backend;
}
}
}
效果:
- 单个用户每秒只能发10个请求
- 全局每秒最多1万个请求
- 超过限制的请求直接返回429 (Too Many Requests)
2. 网关层令牌桶限流
- 每秒生成10000个令牌
- 请求到来时尝试获取令牌
- 无令牌则拒绝请求
代码示例 (Spring Cloud Gateway):
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("seckill", r -> r.path("/api/seckill")
.filters(f -> f.requestRateLimiter(c -> c
.setRateLimiter(redisRateLimiter())
.setKeyResolver(new IpKeyResolver())))
.uri("lb://seckill-service"))
.build();
}
@Bean
RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(10000, 20000); // 每秒10000个请求
}
第三层: 应用层优化
1. 用户验证前置
- 未登录用户直接拒绝
- 黑名单用户直接拒绝
- 减少后续无效请求
def seckill(user_id, product_id):
# 1. 校验用户登录状态 (从Redis读取,极快)
if not is_user_logged_in(user_id):
return {"error": "请先登录"}
# 2. 校验黑名单 (Redis Set)
if is_blacklist_user(user_id):
return {"error": "您已被限制购买"}
# 3. 校验是否已购买 (Redis)
if has_bought(user_id, product_id):
return {"error": "您已购买过该商品"}
# 4. 继续后续流程
...
优势:
- 非法请求在应用层就被拦截
- Redis查询极快 (微秒级)
- 减少数据库压力
2. 分段限流
- 将10000个库存分成10段
- 每段1000个,分配给10台服务器
- 每台服务器只需承载1000 QPS
架构:
Nginx (负载均衡)
|
+-----+-----+-----+
| | | |
Server1 Server2 ... Server10
库存0-999 库存1000-1999 ... 库存9000-9999
优势:
- 分散压力,避免单点瓶颈
- 提高并发能力
第四层: 缓存层优化
1. 库存预热
- 秒杀开始前,将库存加载到Redis
- 所有库存操作都在Redis进行
- 避免数据库访问
代码示例:
# 秒杀开始前预热
redis.set("stock:123", 10000) # 初始库存10000
def seckill(user_id, product_id):
# 1. Redis原子减库存
stock = redis.decr(f"stock:{product_id}")
if stock < 0:
# 库存不足,回滚
redis.incr(f"stock:{product_id}")
return {"error": "库存不足"}
# 2. 库存扣减成功,发送消息到MQ
mq.send("order.create", {
"user_id": user_id,
"product_id": product_id
})
return {"success": "抢购成功"}
# 异步创建订单
def create_order_async(message):
db.execute("INSERT INTO orders ...", message)
# 如果失败,补偿库存
if failed:
redis.incr(f"stock:{message['product_id']}")
优势:
- Redis QPS可达10万+,性能极高
- DECR原子操作,避免超卖
- 异步创建订单,不阻塞用户
2. Lua脚本保证原子性
- Redis DECR只能减1
- 复杂逻辑(扣库存+记录购买)需要Lua脚本
Lua脚本示例:
local stock_key = KEYS[1] -- stock:123
local user_key = KEYS[2] -- bought:123 (Set)
local user_id = ARGV[1]
-- 检查用户是否已购买
if redis.call('SISMEMBER', user_key, user_id) == 1 then
return -1 -- 已购买
end
-- 检查库存
local stock = redis.call('GET', stock_key)
if tonumber(stock) <= 0 then
return -2 -- 库存不足
end
-- 扣库存
redis.call('DECR', stock_key)
-- 记录用户购买
redis.call('SADD', user_key, user_id)
return 1 -- 成功
调用:
result = redis.eval(lua_script, 2, "stock:123", "bought:123", user_id)
if result == 1:
# 成功
elif result == -1:
# 已购买
elif result == -2:
# 库存不足
第五层: 数据库层优化
1. 异步下单
- 前端扣库存,后台异步创建订单
- 用户立即看到"抢购成功",不等待订单创建
- 即使订单创建失败,也能补偿库存
流程:
用户请求 → Redis扣库存 → 返回成功 → MQ发消息 → 消费者创建订单
2. 批量写入
- 消费者不是来一条消息就写一次数据库
- 积攒100条消息,批量INSERT
- 减少数据库IO
代码示例:
orders_batch = []
def consume_message(message):
orders_batch.append(message)
# 积攒100条或超过1秒,批量写入
if len(orders_batch) >= 100 or time_elapsed > 1:
db.executemany("INSERT INTO orders ...", orders_batch)
orders_batch.clear()
性能提升:
- 单条INSERT: 10ms,QPS=100
- 批量INSERT 100条: 50ms,QPS=2000
- 提升20倍!
3. 数据库读写分离
- 主库只写订单
- 从库查询订单
- 减轻主库压力
架构:
写请求 → 主库
读请求 → 从库1, 从库2, 从库3
4. 分库分表
- 订单量巨大(千万级),单表性能下降
- 按用户ID分表: user_id % 10
- 10张表,每张表只有10%的数据
分表规则:
orders_0: user_id % 10 = 0
orders_1: user_id % 10 = 1
...
orders_9: user_id % 10 = 9
查询路由:
user_id = 12345
table_index = 12345 % 10 = 5
query = f"SELECT * FROM orders_{table_index} WHERE user_id = 12345"
第六层: 防刷策略
1. 验证码
- 秒杀按钮点击后,弹出验证码
- 人工识别,防止机器人
- 延缓请求速度
2. 限制购买次数
- 每个用户只能购买1件
- 每个手机号只能购买1件
- 每个IP只能购买10件
代码示例:
# Redis记录购买次数
bought_count = redis.incr(f"buy_count:user:{user_id}")
if bought_count > 1:
redis.decr(f"buy_count:user:{user_id}") # 回滚
return {"error": "每人限购1件"}
# 设置过期时间 (秒杀结束后清空)
redis.expire(f"buy_count:user:{user_id}", 3600)
3. 风控系统
- 识别异常用户: 新注册账号、批量注册、短时间大量请求
- 实时计算风险分数
- 高风险用户直接拒绝或人工审核
风控规则:
IF 账号注册时间 < 7天 AND 请求次数 > 100 THEN 风险分数 += 50
IF IP请求次数 > 1000 THEN 风险分数 += 30
IF 设备指纹重复 > 10 THEN 风险分数 += 40
IF 风险分数 > 80 THEN 拒绝请求
4. 排队机制
- 10万人抢购,只有1万人能进入秒杀页面
- 其余9万人进入排队页面
- 避免无效请求
实现:
# Nginx限流,超过限制的请求返回排队页面
if rate_limited:
return redirect("/queue.html")
完整流程图:
用户请求 (10万QPS)
↓
CDN (静态资源)
↓
Nginx限流 (限制到1万QPS)
↓
网关验证 (登录、黑名单、风控) → 拒绝5000个
↓
应用服务 (5000 QPS)
↓
Redis扣库存 (Lua脚本原子操作) → 库存不足4900个
↓
MQ异步下单 (100个订单)
↓
消费者批量写数据库
↓
订单创建成功
性能指标:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 承载QPS | 100 | 10万 | 1000倍 |
| 响应时间 | 5秒 | 50ms | 100倍 |
| 数据库压力 | 10万QPS | 100 QPS | 减少1000倍 |
| 超卖风险 | 存在 | 无 | - |
总结: 秒杀系统设计的核心思想:
- 能拦截就拦截: 前端、网关、应用层层拦截无效请求
- 能缓存就缓存: 静态化、Redis,减少数据库访问
- 能异步就异步: 扣库存同步,创建订单异步,提升响应速度
- 能限流就限流: Nginx、网关、应用多层限流,保护后端
- 能批量就批量: 批量写数据库,减少IO
1.2 分布式系统设计
Q2: 什么是分布式锁?如何实现?有哪些问题?
答案:
分布式锁的定义:
在分布式系统中,多个进程(可能在不同机器上)需要互斥地访问共享资源,分布式锁提供了这种互斥机制。
为什么需要分布式锁?
场景1: 库存扣减
问题: 单机锁无法解决分布式场景
单机场景 (无问题):
Server1:
lock.acquire()
stock = db.query("SELECT stock FROM products WHERE id = 1") -- 10
stock -= 1
db.execute("UPDATE products SET stock = ? WHERE id = 1", stock)
lock.release()
分布式场景 (有问题):
Server1:
lock.acquire() -- Server1的本地锁
stock = db.query("SELECT stock FROM products WHERE id = 1") -- 10
Server2 (同时执行):
lock.acquire() -- Server2的本地锁
stock = db.query("SELECT stock FROM products WHERE id = 1") -- 10
Server1:
stock -= 1 -- 9
db.execute("UPDATE products SET stock = 9 WHERE id = 1")
Server2:
stock -= 1 -- 9
db.execute("UPDATE products SET stock = 9 WHERE id = 1")
结果: 库存应该是8,实际是9,超卖了!
原因: Server1和Server2的锁是独立的,无法互斥
分布式锁的实现方案:
方案1: 基于数据库
-- 创建锁表
CREATE TABLE distributed_locks (
lock_key VARCHAR(100) PRIMARY KEY,
owner VARCHAR(100),
expire_time BIGINT,
INDEX(expire_time)
);
-- 获取锁 (利用主键唯一性)
INSERT INTO distributed_locks (lock_key, owner, expire_time)
VALUES ('product:1', 'server1:thread1', UNIX_TIMESTAMP() + 10)
ON DUPLICATE KEY UPDATE
owner = IF(expire_time < UNIX_TIMESTAMP(), VALUES(owner), owner),
expire_time = IF(expire_time < UNIX_TIMESTAMP(), VALUES(expire_time), expire_time);
-- 判断是否获取锁成功
SELECT owner FROM distributed_locks WHERE lock_key = 'product:1';
-- 如果owner是当前进程,则获取锁成功
-- 释放锁
DELETE FROM distributed_locks WHERE lock_key = 'product:1' AND owner = 'server1:thread1';
优点:
- 实现简单,不需要引入新组件
- 强一致性,利用数据库事务保证
缺点:
- 性能差,每次加锁都需要数据库IO
- 无法自动过期,需要定时清理
- 数据库宕机,锁服务不可用
方案2: 基于Redis (推荐)
import redis
import uuid
import time
class RedisLock:
def __init__(self, redis_client, lock_key, ttl=10):
self.redis = redis_client
self.lock_key = lock_key
self.ttl = ttl
self.lock_value = str(uuid.uuid4()) # 唯一标识,防止误删
def acquire(self, timeout=5):
"""
获取锁
timeout: 等待锁的超时时间(秒)
"""
start_time = time.time()
while time.time() - start_time < timeout:
# SET NX: key不存在才设置
# EX: 设置过期时间
# 两个操作原子执行,避免死锁
result = self.redis.set(
self.lock_key,
self.lock_value,
nx=True, # Not eXists
ex=self.ttl # EXpire
)
if result:
return True # 获取锁成功
# 获取锁失败,等待一段时间后重试
time.sleep(0.01)
return False # 超时,获取锁失败
def release(self):
"""
释放锁 (使用Lua脚本保证原子性)
"""
lua_script = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
"""
self.redis.eval(lua_script, 1, self.lock_key, self.lock_value)
def __enter__(self):
"""支持with语句"""
if not self.acquire():
raise Exception("获取锁失败")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
# 使用示例
redis_client = redis.Redis(host='localhost', port=6379)
# 方式1: 手动加锁释放锁
lock = RedisLock(redis_client, "lock:product:1", ttl=10)
if lock.acquire(timeout=5):
try:
# 业务逻辑
stock = db.query("SELECT stock FROM products WHERE id = 1")
stock -= 1
db.execute("UPDATE products SET stock = ? WHERE id = 1", stock)
finally:
lock.release()
else:
print("获取锁失败")
# 方式2: 使用with语句 (推荐)
try:
with RedisLock(redis_client, "lock:product:1", ttl=10):
# 业务逻辑
stock = db.query("SELECT stock FROM products WHERE id = 1")
stock -= 1
db.execute("UPDATE products SET stock = ? WHERE id = 1", stock)
except Exception as e:
print(f"获取锁失败: {e}")
为什么需要lock_value?
场景: 防止误删其他进程的锁
时间线:
Server1: 获取锁,lock_value=uuid1,过期时间10秒
Server1: 执行业务逻辑,耗时12秒 (超过锁过期时间)
10秒后: Redis自动删除锁
Server2: 获取锁成功,lock_value=uuid2
Server1: 业务逻辑执行完,调用release()
如果没有校验lock_value,会删除Server2的锁!
导致Server3也能获取锁,破坏互斥性
解决方案:
释放锁时校验lock_value,只有匹配才删除:
if redis.get(lock_key) == lock_value:
redis.del(lock_key)
问题:
GET和DEL不是原子操作,可能在GET和DEL之间锁过期
最终方案:
使用Lua脚本,保证原子性:
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
Redis分布式锁的问题:
问题1: 锁过期时间不好设置
场景: 业务逻辑执行时间不确定
# 设置10秒过期
lock.acquire(ttl=10)
# 业务逻辑可能执行1秒,也可能执行20秒
process_business_logic() # 如果执行了20秒,锁自动过期,其他进程获取锁
lock.release() # 可能删除其他进程的锁
解决方案1: 锁续期 (Watchdog机制)
启动后台线程,定期延长锁的过期时间:
def watchdog(lock):
while lock.is_locked():
time.sleep(lock.ttl / 3) # 每隔1/3的TTL续期
redis.expire(lock.lock_key, lock.ttl)
threading.Thread(target=watchdog, args=(lock,)).start()
解决方案2: Redisson (Java)
Redisson自动实现Watchdog机制:
RLock lock = redisson.getLock("lock:product:1");
lock.lock(); // 自动续期,直到手动unlock
try {
// 业务逻辑
} finally {
lock.unlock();
}
问题2: Redis主从切换导致锁失效
场景: Redis主从架构,主节点宕机
时间线:
Server1: 在主节点Master获取锁 (SET lock:product:1 uuid1)
Master: 锁还未同步到从节点Slave,Master宕机
Redis Sentinel: 将Slave提升为新Master
Server2: 在新Master获取锁成功 (因为新Master没有锁数据)
结果: Server1和Server2同时持有锁,互斥失效!
解决方案: Redlock算法 (Redis官方推荐)
部署5个独立的Redis节点 (不是主从关系)
获取锁流程:
1. 获取当前时间戳t1
2. 依次向5个Redis节点请求锁,设置超时时间
3. 如果在3个或以上节点获取锁成功,且总耗时<锁的有效时间,则认为获取锁成功
4. 否则,向所有节点释放锁
释放锁流程:
向所有5个节点发送释放锁命令
代码示例 (Python):
from redlock import Redlock
# 5个独立的Redis节点
nodes = [
{'host': 'redis1', 'port': 6379, 'db': 0},
{'host': 'redis2', 'port': 6379, 'db': 0},
{'host': 'redis3', 'port': 6379, 'db': 0},
{'host': 'redis4', 'port': 6379, 'db': 0},
{'host': 'redis5', 'port': 6379, 'db': 0},
]
redlock = Redlock(nodes)
lock = redlock.lock("lock:product:1", ttl=10000) # 10秒
if lock:
try:
# 业务逻辑
...
finally:
redlock.unlock(lock)
else:
print("获取锁失败")
优点:
- 即使2个节点宕机,仍能正常工作
- 避免主从切换导致的锁失效
缺点:
- 需要部署5个Redis节点,成本高
- 性能下降 (需要向5个节点请求)
- 网络分区可能导致锁失效 (CAP理论中选择了AP,牺牲C)
方案3: 基于ZooKeeper
from kazoo.client import KazooClient
class ZookeeperLock:
def __init__(self, zk_hosts, lock_path):
self.zk = KazooClient(hosts=zk_hosts)
self.zk.start()
self.lock_path = lock_path
self.lock = self.zk.Lock(lock_path)
def acquire(self, timeout=None):
return self.lock.acquire(timeout=timeout)
def release(self):
self.lock.release()
# 使用示例
zk_lock = ZookeeperLock("localhost:2181", "/locks/product/1")
if zk_lock.acquire(timeout=5):
try:
# 业务逻辑
stock = db.query("SELECT stock FROM products WHERE id = 1")
stock -= 1
db.execute("UPDATE products SET stock = ? WHERE id = 1", stock)
finally:
zk_lock.release()
else:
print("获取锁失败")
优点:
- 强一致性: ZooKeeper基于Paxos/Raft协议,保证一致性
- 自动过期: 客户端断开连接,临时节点自动删除,锁自动释放
- 高可用: 集群模式,部分节点宕机不影响服务
缺点:
- 性能较低: 需要写入ZooKeeper,涉及多节点共识,延迟较高
- 依赖ZooKeeper: 需要额外部署和维护ZooKeeper集群
- 复杂度高: 比Redis方案复杂
ZooKeeper锁的实现原理:
1. 客户端在/locks/product/1下创建临时顺序节点 (EPHEMERAL_SEQUENTIAL)
例如: /locks/product/1/lock_0000000001
2. 客户端获取/locks/product/1下的所有子节点,按序号排序
3. 如果当前节点是序号最小的,则获取锁成功
4. 否则,监听前一个节点的删除事件,等待前一个节点释放锁
5. 客户端释放锁时,删除自己的节点
6. 客户端断开连接,临时节点自动删除,锁自动释放
优势:
- 公平锁: 按创建顺序获取锁,避免饥饿
- 避免惊群: 每个节点只监听前一个节点,不是所有节点都监听同一个节点
方案4: 基于etcd
import etcd3
class EtcdLock:
def __init__(self, etcd_client, lock_key, ttl=10):
self.etcd = etcd_client
self.lock_key = lock_key
self.ttl = ttl
self.lease = None
def acquire(self):
# 创建租约
self.lease = self.etcd.lease(ttl=self.ttl)
# 尝试获取锁 (利用事务的原子性)
success, _ = self.etcd.transaction(
compare=[self.etcd.transactions.create(self.lock_key) == 0], # key不存在
success=[self.etcd.transactions.put(self.lock_key, "1", lease=self.lease)], # 创建key
failure=[]
)
return success
def release(self):
# 删除key
self.etcd.delete(self.lock_key)
# 撤销租约
if self.lease:
self.etcd.revoke_lease(self.lease.id)
# 使用示例
etcd_client = etcd3.client(host='localhost', port=2379)
lock = EtcdLock(etcd_client, "/locks/product/1", ttl=10)
if lock.acquire():
try:
# 业务逻辑
...
finally:
lock.release()
else:
print("获取锁失败")
优点:
- 强一致性: etcd基于Raft协议
- 自动过期: 租约到期自动删除
- 性能较高: 比ZooKeeper快
- 云原生: Kubernetes使用etcd作为配置中心
缺点:
- 需要部署etcd集群
- 相对小众,社区不如Redis活跃
分布式锁方案对比:
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 数据库 | 强 | 低 | 低 | 小规模系统 |
| Redis单机 | 弱 | 高 | 低 | 一般业务 |
| Redis Redlock | 中 | 中 | 中 | 高可用要求 |
| ZooKeeper | 强 | 中 | 高 | 强一致性要求 |
| etcd | 强 | 中 | 高 | 云原生环境 |
如何选择?
一般业务 (库存扣减、防重复提交)
→ Redis单机 (简单、高性能)
高可用要求 (核心业务)
→ Redis Redlock 或 ZooKeeper
强一致性要求 (金融、支付)
→ ZooKeeper 或 etcd
云原生环境 (Kubernetes)
→ etcd
性能要求极高
→ Redis单机 + 业务层保证幂等性
分布式锁的最佳实践:
1. 设置合理的过期时间
- 太短: 业务未完成,锁就过期
- 太长: 进程崩溃,锁长时间无法释放
- 推荐: 业务耗时的2-3倍
2. 使用try-finally保证释放锁
try:
lock.acquire()
business_logic()
finally:
lock.release()
3. 设置锁的唯一标识,防止误删
lock_value = uuid.uuid4()
redis.set(lock_key, lock_value, ex=10)
# 释放时校验
if redis.get(lock_key) == lock_value:
redis.del(lock_key)
4. 处理获取锁失败的情况
- 重试: 等待一段时间后重试
- 降级: 返回友好提示,或使用其他方案
- 限流: 避免大量请求阻塞等待锁
5. 监控锁的持有时间
- 记录获取锁和释放锁的时间
- 告警持有时间过长的锁
- 排查业务逻辑瓶颈
6. 避免嵌套锁
- 容易死锁
- 难以排查和维护
1.3 服务治理
Q3: 什么是服务雪崩?如何预防和应对?
答案:
服务雪崩的定义:
在微服务架构中,一个服务故障导致依赖它的上游服务也故障,故障逐级传播,最终导致整个系统不可用,这种现象称为服务雪崩 (Cascading Failure)。
服务雪崩的场景:
架构:
用户服务 → 订单服务 → 库存服务 → 数据库
正常情况:
用户请求 → 用户服务 (10ms) → 订单服务 (20ms) → 库存服务 (30ms) → 返回
总耗时: 60ms,QPS=16
故障场景:
时间线:
12:00:00 - 库存服务数据库连接池满,响应变慢 (从30ms → 3000ms)
12:00:01 - 订单服务调用库存服务超时,线程阻塞
- 订单服务线程池 (200线程) 被耗尽
- 订单服务无法响应新请求
12:00:02 - 用户服务调用订单服务超时,线程阻塞
- 用户服务线程池被耗尽
- 用户服务无法响应新请求
12:00:03 - 所有用户请求失败,系统雪崩!
雪崩过程:
库存服务慢 (3s)
↓
订单服务阻塞 (线程池满)
↓
用户服务阻塞 (线程池满)
↓
整个系统不可用!
关键问题:
- 1个服务的故障,导致3个服务都不可用
- 故障放大: 库存服务影响有限,但传播到用户服务,影响所有用户
服务雪崩的原因:
1. 资源耗尽
- 线程池耗尽: 请求堆积,线程全部阻塞
- 内存溢出: 请求对象堆积,导致OOM
- 连接池耗尽: 数据库连接被占满
2. 超时未设置
- 调用下游服务没有超时时间
- 下游慢,上游一直等待
- 线程阻塞,无法处理新请求
3. 重试风暴
- 请求失败自动重试
- 下游故障,重试加剧压力
- 雪崩加速
4. 同步调用
- 同步调用阻塞线程
- 异步调用不阻塞
预防和应对措施:
措施1: 超时设置
import requests
from requests.exceptions import Timeout
def call_downstream_service():
try:
# 设置超时时间: 连接超时1秒,读取超时3秒
response = requests.get(
"http://inventory-service/api/stock/123",
timeout=(1, 3)
)
return response.json()
except Timeout:
# 超时后快速失败,不阻塞线程
return {"error": "服务超时"}
优点:
- 下游慢,上游快速失败
- 释放线程,处理其他请求
- 避免线程阻塞导致雪崩
注意:
- 超时时间不能太短,否则正常请求也会超时
- 不能太长,否则失去意义
- 推荐: P99响应时间的1.5-2倍
措施2: 熔断器 (Circuit Breaker)
原理: 类似家里的电闸,电流过大时自动断开,保护电路
熔断器状态:
1. Closed (关闭): 正常状态,请求正常通过
2. Open (打开): 故障状态,直接拒绝请求,不调用下游
3. Half-Open (半开): 尝试恢复,允许少量请求通过
状态转换:
Closed → Open:
- 统计最近N秒的请求
- 如果失败率 > 50%,打开熔断器
- 直接拒绝请求,不调用下游
Open → Half-Open:
- 熔断器打开后,等待一段时间 (如10秒)
- 自动转为Half-Open状态
- 允许少量请求尝试调用下游
Half-Open → Closed:
- 如果请求成功,说明下游恢复
- 关闭熔断器,恢复正常
Half-Open → Open:
- 如果请求失败,说明下游仍故障
- 重新打开熔断器
代码示例 (Hystrix):
@HystrixCommand(
fallbackMethod = "getStockFallback", // 降级方法
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 10个请求后开始统计
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 失败率50%
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000") // 10秒后尝试恢复
}
)
public Stock getStock(Long productId) {
// 调用下游服务
return restTemplate.getForObject("http://inventory-service/api/stock/" + productId, Stock.class);
}
// 降级方法: 熔断时返回默认值
public Stock getStockFallback(Long productId) {
return new Stock(productId, 0, "库存服务暂时不可用");
}
效果:
- 库存服务故障,熔断器打开
- 订单服务不再调用库存服务,直接返回降级响应
- 订单服务线程池不被占用,可以继续处理其他请求
- 避免雪崩!
措施3: 限流 (Rate Limiting)
from ratelimit import limits
# 限制每秒最多100个请求
@limits(calls=100, period=1)
def create_order(order_data):
# 调用下游服务
stock = call_inventory_service(order_data["product_id"])
# 创建订单
...
# 超过限流的请求直接拒绝
def create_order_with_limit(order_data):
try:
return create_order(order_data)
except RateLimitException:
return {"error": "请求过于频繁,请稍后再试"}
优点:
- 保护下游服务,避免过载
- 保护自身,避免资源耗尽
限流算法:
1. 固定窗口: 每秒固定100个请求
2. 滑动窗口: 任意1秒内最多100个请求
3. 令牌桶: 每秒生成100个令牌,请求消耗令牌
4. 漏桶: 请求进入桶,桶以固定速率漏出
推荐: 令牌桶 (允许短时突发,平滑限流)
措施4: 舱壁隔离 (Bulkhead)
原理: 轮船的舱壁,一个船舱进水,不影响其他船舱
应用: 隔离不同的依赖服务,避免相互影响
方案1: 线程池隔离
# 为每个下游服务分配独立的线程池
inventory_thread_pool = ThreadPoolExecutor(max_workers=10)
payment_thread_pool = ThreadPoolExecutor(max_workers=20)
logistics_thread_pool = ThreadPoolExecutor(max_workers=5)
def create_order(order_data):
# 调用库存服务 (使用独立线程池)
stock_future = inventory_thread_pool.submit(call_inventory_service, order_data["product_id"])
# 调用支付服务 (使用独立线程池)
payment_future = payment_thread_pool.submit(call_payment_service, order_data)
# 调用物流服务 (使用独立线程池)
logistics_future = logistics_thread_pool.submit(call_logistics_service, order_data)
# 等待所有服务返回
stock = stock_future.result(timeout=3)
payment = payment_future.result(timeout=5)
logistics = logistics_future.result(timeout=2)
效果:
- 库存服务慢,只占用inventory_thread_pool的10个线程
- 支付服务和物流服务不受影响,仍可正常处理
- 避免一个服务的故障影响其他服务
方案2: 信号量隔离
# 限制并发请求数
from threading import Semaphore
inventory_semaphore = Semaphore(10) # 最多10个并发请求
def call_inventory_service(product_id):
if inventory_semaphore.acquire(timeout=1):
try:
# 调用库存服务
...
finally:
inventory_semaphore.release()
else:
# 超过并发限制,快速失败
return {"error": "库存服务繁忙"}
优点:
- 轻量级,无需额外线程池
- 适合异步调用场景
措施5: 降级 (Degradation)
定义: 在系统压力过大或部分服务故障时,关闭非核心功能,保证核心功能可用
降级策略:
1. 读降级: 返回缓存数据或默认值
2. 写降级: 异步处理或丢弃非核心数据
3. 功能降级: 关闭推荐、评论等非核心功能
示例: 电商系统降级
核心功能: 浏览商品、下单、支付
非核心功能: 评论、推荐、优惠券
降级方案:
# 压力过大时,关闭推荐功能
def get_product_detail(product_id):
product = db.query("SELECT * FROM products WHERE id = ?", product_id)
# 检查系统负载
if system_load < 80%:
# 系统正常,返回推荐
recommendations = call_recommendation_service(product_id)
else:
# 系统压力大,降级返回空推荐
recommendations = []
return {
"product": product,
"recommendations": recommendations
}
# 极端情况,关闭评论功能
def get_product_reviews(product_id):
if system_load > 90%:
# 降级: 直接返回空
return []
else:
# 正常: 查询数据库
return db.query("SELECT * FROM reviews WHERE product_id = ?", product_id)
效果:
- 保证核心功能 (下单、支付) 可用
- 牺牲非核心功能,降低系统压力
- 用户体验略有下降,但不是完全不可用
措施6: 异步化
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379/0')
# 同步调用 (问题)
def create_order(order_data):
# 1. 创建订单
order_id = db.execute("INSERT INTO orders ...")
# 2. 发送邮件 (同步,3秒)
send_email(order_data["user_email"], "订单创建成功")
# 3. 发送短信 (同步,2秒)
send_sms(order_data["user_phone"], "订单创建成功")
# 4. 更新推荐 (同步,1秒)
update_recommendation(order_data["user_id"], order_data["product_id"])
return order_id
# 总耗时: 6秒
# 邮件服务故障,整个下单流程失败!
# 异步调用 (解决方案)
def create_order(order_data):
# 1. 创建订单
order_id = db.execute("INSERT INTO orders ...")
# 2. 异步发送邮件
send_email_async.delay(order_data["user_email"], "订单创建成功")
# 3. 异步发送短信
send_sms_async.delay(order_data["user_phone"], "订单创建成功")
# 4. 异步更新推荐
update_recommendation_async.delay(order_data["user_id"], order_data["product_id"])
return order_id
@app.task
def send_email_async(email, content):
send_email(email, content)
@app.task
def send_sms_async(phone, content):
send_sms(phone, content)
@app.task
def update_recommendation_async(user_id, product_id):
update_recommendation(user_id, product_id)
# 总耗时: <100ms (只写数据库)
# 邮件服务故障,不影响下单!
完整的防雪崩架构:
[用户请求]
↓
[API网关: 限流 + 黑名单]
↓
[用户服务]
↓ (设置超时3秒)
↓ (线程池隔离: 订单线程池20线程)
↓ (熔断器: 失败率>50%打开)
[订单服务]
↓ (设置超时2秒)
↓ (线程池隔离: 库存线程池10线程)
↓ (熔断器: 失败率>50%打开)
↓ (降级: 返回默认库存)
[库存服务]
↓
[数据库]
效果:
- 库存服务故障 → 熔断器打开 → 订单服务不再调用库存服务
- 订单服务返回降级响应 (库存不可用,请稍后查看)
- 用户服务正常运行,不受影响
- 避免雪崩!
总结:
| 措施 | 作用 | 适用场景 |
|---|---|---|
| 超时 | 快速失败,释放资源 | 所有场景 (必须) |
| 熔断 | 故障隔离,避免传播 | 调用第三方服务 |
| 限流 | 保护系统,避免过载 | 高并发场景 |
| 舱壁隔离 | 资源隔离,避免相互影响 | 多个下游服务 |
| 降级 | 保证核心功能可用 | 系统压力大时 |
| 异步化 | 解耦,提升性能 | 非实时场景 |
核心原则:
- 快速失败 - 不要一直等待,超时立即返回
- 优雅降级 - 部分功能不可用,但不是完全不可用
- 资源隔离 - 一个服务的故障,不影响其他服务
- 主动预防 - 限流、熔断等手段,提前防止雪崩
(未完待续...本文档包含系统设计基础部分,后续还有分布式事务、数据一致性、监控告警等内容)