后端面试题 - 系统设计与架构篇

3 阅读24分钟

一、系统设计基础

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个订单)
   ↓
消费者批量写数据库
   ↓
订单创建成功

性能指标:

指标优化前优化后提升
承载QPS10010万1000倍
响应时间5秒50ms100倍
数据库压力10万QPS100 QPS减少1000倍
超卖风险存在-

总结: 秒杀系统设计的核心思想:

  1. 能拦截就拦截: 前端、网关、应用层层拦截无效请求
  2. 能缓存就缓存: 静态化、Redis,减少数据库访问
  3. 能异步就异步: 扣库存同步,创建订单异步,提升响应速度
  4. 能限流就限流: Nginx、网关、应用多层限流,保护后端
  5. 能批量就批量: 批量写数据库,减少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,过期时间10Server1: 执行业务逻辑,耗时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 (半开): 尝试恢复,允许少量请求通过

状态转换:
ClosedOpen:
- 统计最近N秒的请求
- 如果失败率 > 50%,打开熔断器
- 直接拒绝请求,不调用下游

OpenHalf-Open:
- 熔断器打开后,等待一段时间 (如10秒)
- 自动转为Half-Open状态
- 允许少量请求尝试调用下游

Half-OpenClosed:
- 如果请求成功,说明下游恢复
- 关闭熔断器,恢复正常

Half-OpenOpen:
- 如果请求失败,说明下游仍故障
- 重新打开熔断器

代码示例 (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%打开)
   ↓ (降级: 返回默认库存)
[库存服务][数据库]

效果:
- 库存服务故障 → 熔断器打开 → 订单服务不再调用库存服务
- 订单服务返回降级响应 (库存不可用,请稍后查看)
- 用户服务正常运行,不受影响
- 避免雪崩!

总结:

措施作用适用场景
超时快速失败,释放资源所有场景 (必须)
熔断故障隔离,避免传播调用第三方服务
限流保护系统,避免过载高并发场景
舱壁隔离资源隔离,避免相互影响多个下游服务
降级保证核心功能可用系统压力大时
异步化解耦,提升性能非实时场景

核心原则:

  1. 快速失败 - 不要一直等待,超时立即返回
  2. 优雅降级 - 部分功能不可用,但不是完全不可用
  3. 资源隔离 - 一个服务的故障,不影响其他服务
  4. 主动预防 - 限流、熔断等手段,提前防止雪崩

(未完待续...本文档包含系统设计基础部分,后续还有分布式事务、数据一致性、监控告警等内容)