微服务通信:比单体应用多出来的那些坑
单体应用里调用一个函数,要么成功要么失败。但在微服务里,服务A调用服务B,网络可能超时、服务B可能宕机、请求可能被执行了但响应丢了……这些不确定性是微服务最核心的挑战。
一、幂等性:重试安全的基础
幂等性指同一请求执行一次和执行多次,结果相同。在网络不可靠的场景下,客户端必须能安全重试,这要求服务端接口必须幂等。
| 操作类型 | 天然幂等 | 需要处理 |
|---|---|---|
| GET查询 | ✅ 是 | 无需额外处理 |
| PUT更新(替换) | ✅ 是 | PUT /users/1 {name:’张三’} 多次相同 |
| DELETE删除 | 基本是 | 已删返回成功即可 |
| POST创建 | ❌ 否 | 需要去重机制 |
| 扣款/加积分 | ❌ 否 | 必须防重复 |
二、实现幂等性的核心方案
对于非天然幂等的操作,标准方案是唯一请求ID + 去重表:
# 方案1:数据库唯一索引
CREATE TABLE order_dedup (
request_id VARCHAR(64) UNIQUE, -- 唯一请求ID(客户端生成)
result TEXT, -- 执行结果(供重试返回)
created_at TIMESTAMP DEFAULT NOW()
);
# 方案2:Redis SETNX
# 先检查是否处理过,SET request_id result NX EX 86400
# 如已存在,直接返回缓存的result
# 客户端实现:每次请求携带唯一ID
POST /api/orders
Headers: X-Request-ID: uuid-550e8400-e29b-41d4
Body: {product_id: 101, quantity: 2}
三、超时重试:指数退避+Jitter
重试策略设计不当会引发重试风暴——服务刚恢复,所有客户端同时重试,直接把服务再次打垮。指数退避加随机抖动(Jitter)是防止这种情况的标准方案:
import random, time
def retry_with_backoff(func, max_retries=3):
for attempt in range(max_retries + 1):
try:
return func()
except TimeoutError as e:
if attempt == max_retries:
raise
# 指数退避: 1s, 2s, 4s
base_delay = (2 ** attempt)
# 加30%随机抖动,防止惊群
jitter = base_delay * 0.3 * random.random()
time.sleep(base_delay + jitter)
print(f'第{attempt+1}次重试,等待{base_delay+jitter:.2f}s')
四、熔断器:快速失败保护整个系统
熔断器模式(Circuit Breaker)借鉴电路断路器:当下游服务故障率超过阈值,自动切断调用,避免级联崩溃:
| 状态 | 行为 | 转换条件 |
|---|---|---|
| 关闭(Closed) | 正常调用,统计失败率 | 失败率>50% → 打开 |
| 打开(Open) | 直接返回错误,不调下游 | 等待30s → 半开 |
| 半开(Half-Open) | 放行少量请求测试 | 成功 → 关闭;失败 → 打开 |
主流实现:Hystrix(Java)、Resilience4j(Java)、Sentinel(阿里)。Go语言推荐gobreaker库,Python推荐pybreaker。
五、接口设计的其他最佳实践
- 超时必须设置:默认不设超时会让线程池被慢服务耗尽,建议API调用超时设3-5秒
- 降级兜底:熔断后返回缓存数据或默认值,而非直接报错
- 版本化API:/api/v1/、/api/v2/,新旧版本并行运行,平滑升级
- 异步化长操作:超过3秒的操作改为异步,立即返回任务ID,客户端轮询或用WebSocket推送结果
总结:微服务可靠性设计的核心是拥抱失败——假设任何调用都可能失败,用幂等性保证重试安全,用指数退避防止重试风暴,用熔断器防止级联崩溃。这三剑客搭配使用,能让你的微服务在混乱的网络环境中稳定运行。
