文章目录
- 写在前面
- 一、轮询类模式(拉取状态,和你的库存业务最相关)
- 二、重试类模式
- 三、流量控制模式
- 四、自适应采样模式(链路追踪领域)
- [五、事件驱动架构 EDA(替代轮询)](#五、事件驱动架构 EDA(替代轮询))
- 六、全部模式横向对比总结
- 七生产落地关键点
写在前面
自适应架构核心本质:不使用固定参数,依靠外部反馈指标动态调整行为 。
分为四大类:轮询模式、重试模式、流量控制模式、采样调度模式。每一项包含:定义、适用场景、问题、伪代码。
一、轮询类模式(拉取状态,和你的库存业务最相关)
1. 自适应轮询 Adaptive Polling
(1)定义
根据业务状态指标,动态调整拉取间隔;风险越高轮询越频繁,风险越低间隔越大,平衡及时性与资源消耗。
(2)业务场景
1.第三方平台商品库存监控(对方不支持MQ推送),库存大于10十分钟轮询,小于1010秒钟轮询;
2.设备在线状态监控:设备正常5分钟轮询,设备告警10s轮询;
3.订单状态监控:大额订单高频轮询,普通订单低频轮询。
(3)存在问题
1.阈值附近震荡:库存9↔11,不停切换10s/2min;需要滞回(回差)
2.分布式多实例会重复轮询,需要分布式锁;
3.依然存在最大间隔的延迟,不是真正实时。
(4)伪代码(增加滞回防抖动)
java
/**
* 滞回阈值:
* 当库存 <8 切换10s高频
* 当库存 >15 切换回2min
* >100 切换10min
*/
// 每个商品保存上一轮使用的轮询间隔,状态不能只靠本次库存计算
class ProductMonitor {
Long productId;
Long lastPollInterval; // ms,上一次生效间隔
Long stock;
}
// 获取下一次轮询间隔
long getNextInterval(ProductMonitor monitor){
long stock = monitor.stock;
long oldInterval = monitor.lastPollInterval;
// 当前已经是高频模式:需要升到15以上才退出高频
if(oldInterval == 10*1000){
if(stock > 15){
if(stock >100){
return 10*60*1000; //10min
}else{
return 2*60*1000; //2min
}
}else{
return 10*1000; //保持10s
}
}
// 当前中等间隔模式
else if(oldInterval == 2*60*1000){
if(stock < 8){
return 10*1000;
}else if(stock>100){
return 10*60*1000;
}else{
return 2*60*1000;
}
}
// 当前长间隔模式
else{
if(stock <8){
return 10*1000;
}else if(stock <=100){
return 2*60*1000;
}else{
return 10*60*1000;
}
}
}
// 调度主循环
void pollLoop(ProductMonitor product){
while(true){
//分布式锁,防止多实例同时执行
if(!tryLock(product.productId)){
sleep(500);
continue;
}
try{
//拉取最新库存
product.stock = remoteApi.getStock(product.productId);
if(product.stock <=0){
sendWarnAlert(product); //库存0告警
}
//计算下一次间隔
long nextInterval = getNextInterval(product);
product.lastPollInterval = nextInterval;
}finally {
unlock();
}
//增加随机抖动 ±10%,避免大量任务同一时刻执行(惊群)
long jitter = (long)(nextInterval * (0.9 + Math.random()*0.2));
sleep(jitter);
}
}
2. 指数退避轮询 Exponential Backoff Polling
(1)定义
轮询等待结果,长时间得不到结果,间隔不断放大,上限封顶。等待越久查询越慢。
和自适应轮询相反:自适应轮询是风险越高越快;指数退避轮询是等待越久越慢。
(2)业务场景
1.异步导出文件任务轮询;
2.第三方支付异步结果查询;
3.大文件异步转码任务状态查询。
(3)存在问题
初始间隔不能太大,设置最大间隔防止无限增大;需要加jitter抖动。
(4)伪代码
java
/**
* 规则:初始1s,失败后 2s,4s,8s,最大32s
*/
long getBackoffPollInterval(int pollCount){
long base = 1000;
long interval = base * (1L << pollCount); // 2^pollCount
long maxInterval = 32*1000;
interval = Math.min(interval, maxInterval);
// 随机抖动 ±30%
double jitterFactor = 0.7 + Math.random()*0.6;
return (long)(interval * jitterFactor);
}
void pollAsyncTask(Long taskId){
int pollCount =0;
while(true){
TaskStatus status = remoteApi.queryTask(taskId);
if(status == SUCCESS){
handleSuccess();
break;
}
if(status == FAIL){
handleFail();
break;
}
//还在处理中,继续轮询
long sleepMs = getBackoffPollInterval(pollCount);
pollCount++;
sleep(sleepMs);
}
}
3. 长轮询 Long Polling
(1)定义
客户端发送http请求,服务端hold连接挂起,有变更立刻返回;若无变更超时后客户端重新发起请求。介于轮询和推送中间。
(2)业务场景
1.Web端消息通知;
2.浏览器端实时库存展示;
3.小程序状态同步,不想维护websocket。
(3)缺点
占用连接,高并发场景服务端压力大。
java
//服务端伪代码
HttpResponse longPoll(HttpRequest req){
long timeout = 30000; //30s超时
long start = now();
while(now()-start < timeout){
StockChange event = getNewStockEvent(req.productId);
if(event != null){
return Response.ok(event);
}
sleep(100);
}
//超时返回空,客户端立刻重请求
return Response.noContent();
}
二、重试类模式
1. 指数退避重试 Exponential Backoff Retry
(1)定义
调用下游失败,每次重试等待时间指数放大,避免瞬间大量请求打崩故障服务。微服务最经典模式。
(2)业务场景
1.RPC调用第三方接口失败重试;
2.MQ消息消费失败本地重试;
3.调用数据库临时抖动重试。
注意:幂等!重试必须保证接口幂等,否则产生脏数据。
(3)伪代码
java
/**
* 最大重试5次,初始1000ms,最大等待16000ms
*/
<T> T callWithBack(Supplier<T> func,int maxRetry) throws Exception{
int retryTimes =0;
while(true){
try{
return func.get();
}catch(Exception e){
retryTimes++;
if(retryTimes >= maxRetry){
throw e;
}
long wait = 1000 * (1L << retryTimes);
long maxWait =16000;
wait = Math.min(wait,maxWait);
//加抖动,防止大量请求同一时刻重试(惊群效应)
wait = (long)(wait * (0.6 + Math.random()*0.8));
sleep(wait);
}
}
}
//调用示例
callWithBack(()-> remoteApi.updateStock(),5);
2. 自适应重试 Adaptive Retry
(1)定义
不是简单指数退避,根据错误类型动态选择策略。
(2)业务场景
对外网关,调用多个异构第三方接口。
- 参数错误(400):业务错误,直接不重试;
- 限流(429):拉长等待时间;
- 网络超时:快速重试一次。
(3)伪代码
java
long getAdaptiveWaitTime(Exception ex,int retryCount){
if(ex instanceof ParamInvalidException){
return -1; //不重试
}else if(ex instanceof RateLimitException){
return 5000 + retryCount*2000; //限流等待更久
}else if(ex instanceof TimeoutException){
return 500; //超时快速重试
}else{
//默认指数退避
return Math.min(1000*(1L<<retryCount),15000);
}
}
三、流量控制模式
1. 自适应限流 Adaptive Rate‑Limiting
(1)定义
区别固定QPS限流,根据下游RT、错误率动态调整允许通过流量。Sentinel的系统自适应限流就是这个模式。
(2)业务场景
1.API网关,保护下游服务;
2.调用第三方接口,对方能力浮动,无法设置固定QPS。
逻辑:下游平均RT上涨、错误率上升,自动降低放行QPS;恢复正常,逐步提升QPS。
(3)伪代码
java
class AdaptiveLimiter{
double maxAllowedQps =100;
double currentQps =100;
double errorThreshold =0.05; //错误率超过5%开始降速
double rtThreshold =200; //平均RT>200ms降速
//每一个统计窗口调用一次,统计指标
void adjust(double avgRt, double errorRate){
if(errorRate> errorThreshold || avgRt> rtThreshold){
//压力大,降低30%流量
currentQps = currentQps *0.7;
currentQps = Math.max(currentQps,5); //最低保护
}else{
//状态恢复,缓慢回升
currentQps = currentQps *1.1;
currentQps = Math.min(currentQps,maxAllowedQps);
}
}
boolean tryAcquire(){
return getCurrentPassQps() < currentQps;
}
}
2. 分级熔断(Hystrix/Sentinel)
(1)定义
根据错误率熔断,半开状态试探恢复,动态开关流量。
(2)场景
微服务故障隔离。
1.错误率超过阈值→熔断拒绝请求;
2.熔断冷却时间后进入半开,放行少量请求试探;
3.如果调用成功,关闭熔断;失败重新打开熔断。
(3)伪代码
java
class CircuitBreaker{
enum State{CLOSED,OPEN,HALF_OPEN}
State state = State.CLOSED;
int errorCount;
int windowTotal;
long openTime;
double errorRateTrigger =0.5; //错误率50%熔断
long coolDownMs =5000;
boolean allowRequest(){
if(state == CLOSED) return true;
if(state == OPEN){
if(now()-openTime > coolDownMs){
state = State.HALF_OPEN;
return true; //半开放行少量请求试探
}
return false;
}
//HALF_OPEN
return true;
}
void recordResult(boolean success){
windowTotal++;
if(!success) errorCount++;
double rate = 1.0*errorCount / windowTotal;
if(state == CLOSED && rate >= errorRateTrigger){
state = State.OPEN;
openTime = now();
}
if(state == HALF_OPEN){
if(success){
//恢复正常
state = State.CLOSED;
errorCount=0;windowTotal=0;
}else{
//试探失败重新熔断
state = State.OPEN;
openTime = now();
}
}
}
}
四、自适应采样模式(链路追踪领域)
1.自适应采样 Adaptive Sampling
(1)定义
系统正常时低采样率减少存储压力;出现异常(错误、慢调用)提高采样率,尽可能采集故障链路。
(2)业务场景
SkyWalking/Jaeger链路追踪;日志采集。
百度地图定位上报,静止(10秒上报一次),慢速(5秒上报一次),高速(2秒上报一次)
(3)伪代码
java
class AdaptiveSampler{
double normalRate =0.1; //正常只采样10%
double errorRate =1.0; //异常100%采样
double slowRt =500; //超过500ms视为慢调用
boolean isSample(Span span){
if(span.isError() || span.costMs>slowRt){
return Math.random() < errorRate;
}else{
return Math.random() < normalRate;
}
}
}
五、事件驱动架构 EDA(替代轮询)
(1)定义
数据变更时业务主动发出事件,通过MQ推送消费者,不需要轮询拉取。
(2)业务场景
内部系统库存变更,能够修改业务代码;库存变更、订单完成后触发告警、通知、下游业务。
生产最佳实践:事件驱动为主,自适应轮询做兜底,防止MQ丢消息漏告警。
(3)伪代码
java
//库存写业务,更新DB后发送事件
void updateStock(Long productId, Long newStock){
db.updateStock(productId,newStock);
//发送库存变更事件到MQ
mq.send(new StockChangeEvent(productId,newStock));
}
//消费者监听事件
void consume(StockChangeEvent event){
if(event.stock <=0){
sendAlert(event.productId);
}
}
//兜底:自适应轮询定时扫描,补偿MQ丢失消息
六、全部模式横向对比总结
| 模式 | 驱动条件 | 频率/流量变化方向 | 典型业务 |
|---|---|---|---|
| 自适应轮询 | 业务指标(库存、设备状态) | 风险越高,轮询越快 | 第三方库存监控、设备监控 |
| 指数退避轮询 | 轮询次数/等待时长 | 等待越久,轮询越慢 | 异步任务查询、导出任务 |
| 指数退避重试 | 失败次数 | 失败越多,重试间隔越大 | RPC接口调用失败重试 |
| 自适应重试 | 错误类型 | 不同错误不同策略 | 异构第三方网关调用 |
| 自适应限流 | 下游RT、错误率 | 压力越大,放行流量越小 | API网关保护下游服务 |
| 自适应采样 | 异常、慢请求 | 异常越多,采样率越高 | 链路追踪、日志采集 |
| 事件驱动EDA | 数据变更事件 | 变更立刻触发,无轮询 | 内部业务解耦,库存变更通知 |
七生产落地关键点
- 抖动Jitter:轮询、重试务必增加随机抖动,避免惊群效应,大量任务同一时刻请求;
- 滞回回差:阈值判断类逻辑,一定要避免临界值来回震荡;
- 保护边界:所有动态间隔、QPS,必须设置最大值、最小值,防止极端条件参数失控;
- 分布式场景:轮询任务需要分布式锁,防止多实例重复执行;
- 可靠性:轮询优先作为兜底,能事件推送优先事件驱动。