功能说明
下游服务持续超时或报错时,继续重试只会拖垮调用方线程池和连接池。熔断器在连续失败达到阈值后进入打开状态,所有请求立即被拒绝,不再访问故障依赖;冷却时间结束后进入半开状态,只放行一次试探请求,成功则恢复正常,失败则重新打开并延长等待。
本例用枚举表示三种状态,用失败计数、阈值和冷却时长驱动转换。allow 决定当前请求能否执行,record 在请求结束后记录成功或失败。演示连续四次请求中前两次被允许且失败,随后两次被快速拒绝;等待冷却后再放行一次,最后记录成功使熔断器复位。
关键代码
State 派生 Copy 和 PartialEq,便于在 match 与条件中直接使用。allow 在打开状态下检查 opened_at 的经过时间,达到冷却就切换到半开并允许一次。半开状态再次调用 allow 会拒绝,防止并发涌入,真实实现应为半开设置单飞锁。record 在成功时清零失败并关闭;失败时累加计数,达到阈值或在半开中失败就打开并刷新计时。主程序只在请求被允许时调用 record,避免被拒绝的请求延长冷却。
使用注意
熔断器应按依赖和操作分别创建,不能用一个实例覆盖所有下游。阈值和冷却时间需要根据错误率与恢复速度调优,固定阈值无法适应流量波动。应区分可熔断错误与业务错误,参数校验失败不应计入故障。半开状态必须限制并发试探数,否则恢复瞬间仍可能压垮依赖。熔断只是保护手段,还应配合超时、重试预算和降级响应。被拒绝的请求不要调用 record,否则会不断重置冷却。
验证方式
运行脚本,前两次 allowed 为 true,随后两次为 false,说明阈值二触发打开。等待六十毫秒后输出 after cooldown allowed=true,进入半开试探;若把最后一行改为记录失败,再次调用 allow 应立即返回 false。将阈值改为一,可看到第一次失败后就直接打开。
代码示例
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Closed,
Open,
HalfOpen,
}
struct CircuitBreaker {
state: State,
failures: u32,
threshold: u32,
cooldown: Duration,
opened_at: Instant,
}
enum Decision {
Allow,
Reject,
}
impl CircuitBreaker {
fn new(threshold: u32, cooldown: Duration) -> Self {
Self {
state: State::Closed,
failures: 0,
threshold,
cooldown,
opened_at: Instant::now(),
}
}
fn allow(&mut self) -> Decision {
match self.state {
State::Closed => Decision::Allow,
State::Open if self.opened_at.elapsed() >= self.cooldown => {
self.state = State::HalfOpen;
Decision::Allow
}
State::Open => Decision::Reject,
State::HalfOpen => Decision::Reject,
}
}
fn record(&mut self, success: bool) {
if success {
self.state = State::Closed;
self.failures = 0;
return;
}
self.failures += 1;
if self.failures >= self.threshold || self.state == State::HalfOpen {
self.state = State::Open;
self.opened_at = Instant::now();
}
}
}
fn main() {
let mut breaker = CircuitBreaker::new(2, Duration::from_millis(50));
for attempt in 0..4 {
let allowed = matches!(breaker.allow(), Decision::Allow);
println!("attempt {attempt}: allowed={allowed}");
if allowed {
breaker.record(false);
}
}
std::thread::sleep(Duration::from_millis(60));
let trial = matches!(breaker.allow(), Decision::Allow);
println!("after cooldown allowed={trial}");
breaker.record(true);
}