2131 字
约 7 分钟
2
手写一个企业级重试组件:从编码式 Retry 到 Spring Boot Sta

手写一个企业级重试组件:从编码式 Retry 到 Spring Boot Starter

一、为什么需要重试组件?

在分布式系统中,服务之间调用非常频繁。

例如:

订单服务调用库存服务:

订单服务                库存服务

   |                       |
   | ---- 扣库存请求 ----> |
   |                       |
   | <---- timeout ------- |

这次失败并不一定代表库存服务不可用。

可能原因:

  • 网络瞬时抖动
  • 服务 GC
  • 数据库连接池暂时不足
  • 下游服务负载过高

如果直接返回失败:

用户支付失败
订单创建失败
业务异常

但是如果等待一段时间再次请求:

第一次失败

等待

第二次成功

用户体验会更好。

因此需要:

当接口调用失败时,根据一定策略重新执行。

这就是:

重试机制(Retry)


二、最简单的重试方式有什么问题?

很多人第一反应:

for(int i=0;i<3;i++){

    try{

        remoteCall();

    }catch(Exception e){

    }

}

虽然可以工作,但是存在大量问题。

1. 重试策略无法扩展

比如:

固定等待:

失败

等待1秒

重试

但是有些场景需要:

指数退避:

失败

等待1秒

失败

等待2秒

失败

等待4秒

不同业务需要不同策略。


2. 不知道什么情况下需要重试

例如:

接口返回:

{
 "code":500
}

可能需要重试。

但是:

{
 "code":401
}

代表权限错误。

继续重试没有意义。

异常也是一样:

需要:

TimeoutException 重试

IllegalArgumentException 不重试

所以:

重试条件也应该抽象。


3. 同步异步无法统一

同步:

调用线程

失败

等待

重新调用

异步:

调用线程

失败

提交线程池

后台继续重试

应该由组件控制。


三、整体设计

最终设计如下:

                 Retryer

                    |
        -------------------------
        |                       |

    重试条件判断            重试执行策略

    Predicate              RetryStrategy


        |
        |
     Listener

        |
        |
    Executor

核心组成:

模块 作用
Retryer 负责整体流程
RetryerStrategy 控制等待策略
Predicate 判断是否需要重试
Listener 监听重试过程
Executor 支持异步
Builder 组装对象
AOP 实现注解调用

四、重试策略设计

1. 抽象父类

所有重试策略都有:

  • 最大次数
  • 等待时间
  • 时间单位

因此抽象:

public abstract class RetryerStrategy {


    protected int maxAttempts;


    protected long time;


    protected TimeUnit timeUnit;


    abstract boolean exec();

}

这里使用:

模板模式

父类负责公共能力:

参数保存
监听执行

子类负责:

具体等待算法

五、固定时间重试策略

例如:

配置:

最大次数 3

等待 1秒

执行:

第一次失败

sleep 1秒

第二次

sleep 1秒

第三次

sleep 1秒

代码:

public class TimeRetryerStrategy 
        extends RetryerStrategy{


@Override
boolean exec(){

    while(maxAttempts-->0){

        timeUnit.sleep(time);

        return true;

    }

    return false;
}

}

六、指数退避策略

在真实生产环境更加常见。

例如:

配置:

baseTime=1秒

执行:

第一次失败

等待1秒


第二次失败

等待2秒


第三次失败

等待4秒

公式:

等待时间 =
基础时间 × 2^(当前次数-1)

代码:

long waitTime =
(long)(time*Math.pow(2,count-1));

为什么不用固定时间?

因为:

如果下游服务挂了:

大量请求同时重试。

例如:

10000个请求:

0秒失败

1秒后全部重试

再次失败

2秒后全部重试

形成:

惊群效应

指数退避可以降低压力。


七、为什么需要 RetryListener?

重试过程中可能需要:

记录日志:

订单接口第3次重试

监控:

retry_count++

报警:

接口连续失败

如果直接写死:

Retryer越来越臃肿。

因此:

定义监听器:

@FunctionalInterface
public interface RetryerLister {


    void exec(RetryerStrategy strategy);


}

使用:

.retryerLister(strategy -> {

 System.out.println("发生重试");

})

这就是:

观察者模式


八、重试条件设计 Predicate

核心问题:

什么时候重试?

例如:

返回值:

false
500
error

异常:

TimeoutException
IOException

因此:

抽象:

Predicate

本质:

输入:

结果

输出:

是否重试

例如:

result -> result.equals(false)

九、多个重试条件

实际业务:

可能:

返回false

或者

返回500

或者

TimeoutException

所以:

需要组合。

设计:

Predicates.or(
 p1,
 p2,
 p3
)

内部:

for(Predicate p:list){

    if(p.apply(input)){
        return true;
    }

}

return false;

只要一个条件满足:

执行重试。


十、Retryer核心流程

用户:

retryer.exec(()->{

    return api.call();

});

内部流程:

执行用户代码

       |
       |

获取结果

       |
       |

Predicate判断

       |

是否需要重试?

       |

Strategy控制等待

       |

再次执行

代码:

public <T>T exec(RetryerSupplier supplier){

    try{

        T result=supplier.exec();


        if(shouldRetry(result)){

            retry();

        }


        return result;


    }catch(Exception e){

        if(shouldRetry(e)){

            retry();

        }

    }

}

十一、为什么使用 Builder?

Retryer需要:

策略

结果判断

异常判断

监听器

线程池

如果构造:

new Retryer(
 strategy,
 predicate,
 exception,
 listener,
 executor
)

参数越来越多。

所以:

使用建造者模式。

调用:

RetryerBuilder
.newBuilder()

.withRetryerStrategy(strategy)

.retryerIfResult(predicate)

.retryerLister(listener)

.build();

优点:

  • 可读性高
  • 扩展方便
  • 避免构造函数爆炸

十二、实现注解方式 Retry

前面是编码方式:

retryer.exec()

但是:

Spring项目更希望:

@Retryer
public Boolean createOrder(){

}

自动拥有重试能力。

怎么实现?

核心:

Spring Bean 生命周期。


十三、BeanPostProcessor实现自动增强

Spring创建Bean:

流程:

实例化

↓

属性注入

↓

初始化

↓

BeanPostProcessor

↓

放入IOC

我们插入:

postProcessAfterInitialization()

扫描:

method.isAnnotationPresent(Retryer.class)

找到:

@Retryer
public void test()

然后:

生成代理对象。


十四、代理模式实现增强

原本:

用户

 |

真实对象

 |

test()

变成:

用户

 |

代理对象

 |

Retryer

 |

真实对象.test()

代理里面:

自动执行:

重试逻辑

用户完全无感。

这就是:

Spring AOP核心思想。


十五、自动装配 Starter

最终希望:

用户只需要:

@EnableRetryer

即可。

实现:

resources:

META-INF/spring.factories

配置:

EnableAutoConfiguration=
RetryerAutoConfiguration

启动:

Spring自动加载。

这就是:

Spring Boot Starter开发流程。


十六、设计过程中遇到的问题

问题1:RetryStrategy状态污染

错误设计:

RetryerStrategy
{
 maxAttempts
}

执行:

3

2

1

0

第二次调用:

直接:

0

为什么?

因为:

策略对象保存了状态。

正确:

配置和状态分离。

例如:

配置:

RetryConfig{

maxAttempts=3

}

执行:

每次创建:

new RetryStrategy(config)

避免污染。


十七、最终架构总结

完整结构:

                 Spring Bean


                    |

            BeanPostProcessor


                    |

               Proxy对象


                    |

                Retryer


        ---------------------

        |                   |

    Predicate          Strategy


        |                   |

 判断是否重试          等待多久


                    |

               Listener


                    |

               Executor


十八、从这个组件可以学到什么?

这个项目真正价值不是重试。

而是学习:

1. 模板模式

抽象公共流程。

2. 策略模式

不同重试算法自由替换。

3. 建造者模式

解决复杂对象创建。

4. 代理模式

实现无侵入增强。

5. 观察者模式

监听扩展。

6. Spring Bean 生命周期

实现 Starter。


总结

一个成熟的重试组件,并不是:

while循环 + sleep

而应该考虑:

  • 什么情况下重试?
  • 重试多久?
  • 重试几次?
  • 是否异步?
  • 如何监控?
  • 如何无侵入接入业务?
  • 如何扩展?

通过这个组件,我们实际上完成了一次:

从工具类 → 框架组件 → Spring Boot Starter 的设计演进。

这也是很多开源框架的核心思想。

手写一个企业级重试组件:从编码式 Retry 到 Spring Boot Sta
http://www.clxhxhhr.top/posts/382/
作者
clxstart
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。