请求限速器RateLimiter
RateLimiter
是一种用于控制操作速率的工具,通常用于限制请求的频率,以防止系统过载。它常见于 API 限流、流量控制、保护后端服务等场景。
原理:
1. 令牌桶算法
-
工作原理:
-
令牌桶算法维护一个桶,其中存储着一定数量的令牌(token)。
-
每当请求到来时,必须从桶中获取一个令牌。如果桶中有令牌,请求就可以继续;如果没有,则请求会被拒绝或等待。
-
令牌以一定的速率生成,桶的容量限制了可以存储的最大令牌数。
-
-
优点:
-
适合突发流量控制,可以在短时间内允许一定量的请求。
-
易于实现,并且可以灵活调整。
-
2. 漏桶算法
-
工作原理:
-
漏桶算法使用一个固定容量的桶,桶内的水以固定速率漏出。
-
请求到达时,如果桶未满,则可以进入桶中;如果桶已满,请求会被拒绝。
-
请求的处理速率是固定的,与到达的请求数无关。
-
-
优点:
-
适合平滑流量控制,能够限制请求的最大处理速率。
-
用法:
1. 添加依赖
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0.1-jre</version> </dependency>
2.方法
-
tryAcquire()
: 尝试获取令牌,立即返回。 -
acquire() :获取1个令牌,等待
-
acquire(int permits)
: 获取指定数量的令牌。 -
setRate(double permitsPerSecond)
: 动态调整速率。
3.示例
import com.google.common.util.concurrent.RateLimiter; public class RateLimiterExample { public static void main(String[] args) { // 创建一个 RateLimiter,设置每秒放置 5 个令牌 RateLimiter rateLimiter = RateLimiter.create(5.0); for (int i = 0; i < 10; i++) { // 获取令牌,如果没有令牌,则阻塞直到获取到令牌 rateLimiter.acquire(); // 执行操作 System.out.println("Thread " + Thread.currentThread().getName() + " is executing task " + i); } } }
重试机制RetryTemplate
RetryTemplate
是 Spring 框架中用于实现重试机制的一个工具类。它提供了一种简洁且灵活的方式来处理因暂时性故障而导致的操作失败,允许开发者在执行某个操作时设置重试逻辑,以确保在遇到可恢复的异常时能够重新尝试。
使用场景:
-
网络请求:在进行 HTTP 请求时,如果遇到网络问题,可以重试请求。
-
数据库操作:在处理数据库操作时,如果出现暂时性错误(如连接超时),可以重试。
-
外部服务调用:在调用外部服务时,可能会因为服务短暂不可用而导致失败,重试可以增加成功的概率。
参数:
-
RetryCallback:该接口封装了业务代码,且failback后,会再次调用RetryCallback接口,直到达到重试次数/时间上限;
-
RecoveryCallback:当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为最终的返回结果。
注意:RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext上下文参数,通过它可以获取到尝试次数、异常,也可以通过其setAttribute()和getAttribute()来传递一些信息。
-
RetryPolicy:重试策略,描述什么条件下可以尝试重复调用RetryCallback接口;策略包括最大重试次数、指定异常集合/忽略异常集合、重试允许的最大超时时间;RetryTemplate内部默认时候用的是SimpleRetryPolicy,SimpleRetryPolicy默认将对所有异常进行尝试,最多尝试3次。还有其他多种更为复杂功能更多的重试策略;
-
BackOffPolicy:回退策略,用来定义在两次尝试之间需要间隔的时间,如固定时间间隔、递增间隔、随机间隔等;RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的;
-
RetryListener:RetryTemplate中可以注册一些RetryListener,可以理解为是对重试过程中的一个增强,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作;如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法;
1. 添加依赖
如果您使用 Maven,可以在 pom.xml
中添加 Spring Retry 依赖:
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.1</version> <!-- 请根据需要选择版本 --> </dependency>
2.示例
public class RetryTemplateTest {
private RetryTemplate retryTemplate;
static Long RETRY_TIME = 1000L;
@PostConstruct
public void init() {
rateLimiter = RateLimiter.create(Double.parseDouble(globalArkConfig.getRateLimiterRate()));
this.retryTemplate = createRetryTemplate();
}
/**
* 定义重试策略:可以定义重试的次数、每次重试之间的间隔时间,甚至可以设置不同的重试策略(如指数退避策略)
*/
private static RetryTemplate createRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// 设置重试策略,无线重试
AlwaysRetryPolicy alwaysRetryPolicy = new AlwaysRetryPolicy();
retryTemplate.setRetryPolicy(alwaysRetryPolicy);
//设置重试策略,最高重试100次
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
simpleRetryPolicy.setMaxAttempts(5);
retryTemplate.setRetryPolicy(simpleRetryPolicy);
// 设置回退策略-重试的间隔策略
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
//重试间隔ms
backOffPolicy.setBackOffPeriod(1000);
retryTemplate.setBackOffPolicy(backOffPolicy);
//设置监听器
RetryListener retryListener = new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
System.out.println("---open----在第一次重试时调用");
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("close----在最后一次重试后调用(无论成功与失败)。" + context.getRetryCount());
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("error----在每次调用异常时调用。" + context.getRetryCount());
}
};
retryTemplate.registerListener(retryListener);
return retryTemplate;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool(16);
try {
ArrayList<Integer> list = new ArrayList<>(1000);
for (int i = 0; i < 101; i++) {
list.add(i);
}
System.out.println("数据准备完成。" + Thread.currentThread().getName());
forkJoinPool.submit(() -> {
list.parallelStream().forEach(res -> {
/**
* 通过 RetryTemplate 的 execute 方法来执行需要重试的操作。如果操作成功,则返回结果;
* 如果失败,则根据定义的重试策略进行重试。
*/
retryTemplate.execute(
//每次重试的操作
retryContext -> {
if (res == 100) {
throw new RuntimeException("偶数异常 => " + res);
}
return 0;
},
//达到最大重试次数后执行,并以其返回结果作为最终的返回结果
retryContext -> {
throw new RuntimeException("达到最大重试次数后 => " + res);
}
);
});
}).get();
} catch (Exception e) {
throw new RuntimeException("捕获到线程内部异常 => " + e);
} finally {
forkJoinPool.shutdown();
}
}
}
3.使用 @Retryable
注解
开启重试策略机制
@EnableRetry
public class Demo17Application {
public static void main(String[] args) {
SpringApplication.run(Demo17Application.class, args);
}
}
固定间隔退避策略:在每次重试之间等待固定的时间间隔。例如:@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000)),表示在每次重试之间等待1秒的时间。 指数退避策略:在每次重试之间等待指数级增长的时间间隔。例如:@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2.0)),表示在第一次重试之后等待1秒的时间,在第二次重试之后等待2秒的时间,在第三次重试之后等待4秒的时间。 随机间隔退避策略:在每次重试之间等待随机的时间间隔。例如:@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, maxDelay = 2000)),表示在每次重试之间等待1到2秒之间的随机时间。 指数退避策略(带有最大退避时间):在每次重试之间等待指数级增长的时间间隔,但限制最大等待时间。例如:@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 5000)),表示在第一次重试之后等待1秒的时间,在第二次重试之后等待最多2秒的时间,在第三次重试之后等待最多5秒的时间。 自定义退避策略:可以实现org.springframework.retry.backoff.BackOffPolicy接口来定义自定义的退避策略。例如:@Retryable(maxAttempts = 3, backoff = @Backoff(value = MyBackoffPolicy.class)),表示使用自定义的退避策略类MyBackoffPolicy。
@Retryable(maxAttemptsExpression = "${retry.maxAttempts:10}",
backoff = @Backoff(delayExpression = "${retry.delay:2000}"
))
private void retryableTest(Integer counter) {
if (counter < 10) {
System.out.println("try ........");
throw new IllegalStateException("????");
}
}
4. 注意事项
-
异常过滤:在使用
@Retryable
注解时,可以通过value
属性指定需要重试的异常类型。 -
恢复方法:可以使用
@Recover
注解定义恢复逻辑,当所有重试都失败时会调用此方法。 -
线程安全:
RetryTemplate
是线程安全的,可以在多个线程中共享。
延迟器Timer
Timer
是 Java 中用于调度任务的类,它可以在指定的时间点或以固定的间隔执行任务。Timer
类在 java.util
包中定义,通常与 TimerTask
类一起使用,后者表示需要执行的任务。
原理
Timer
的工作原理基于一个线程和一个任务队列。当您安排一个任务时,Timer
会将该任务放入一个队列中,并在指定的时间点或间隔执行它。Timer
的实现使用了一个后台线程,它会定期检查任务队列,并执行到期的任务。
用法:
1. 创建 Timer 和 TimerTask
首先,您需要创建一个 Timer
实例和一个 TimerTask
实例。以下是一个基本的例子:
import java.util.Timer; import java.util.TimerTask; public class TimerExample { public static void main(String[] args) { Timer timer = new Timer(); // 创建 Timer 实例 // 创建一个 TimerTask TimerTask task = new TimerTask() { @Override public void run() { System.out.println("Task executed at: " + System.currentTimeMillis()); } }; // 安排任务在 2 秒后执行 timer.schedule(task, 2000); // 延时 2000 毫秒(2 秒) } }
2. 定期执行任务
您可以使用 scheduleAtFixedRate
方法安排任务的定期执行:
import java.util.Timer; import java.util.TimerTask; public class PeriodicTaskExample { public static void main(String[] args) { Timer timer = new Timer(); // 创建 Timer 实例 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("Periodic task executed at: " + System.currentTimeMillis()); } }; // 安排任务每 2 秒执行一次,延时 1 秒后开始 timer.scheduleAtFixedRate(task, 1000, 2000); // 延时 1000 毫秒(1 秒),每 2000 毫秒(2 秒)执行一次 } }
3. 取消任务
如果您希望在某个条件下取消任务,可以使用 cancel()
方法。您可以通过 TimerTask
的 cancel()
方法来取消当前任务,也可以通过 Timer
的 cancel()
方法来取消所有任务。
import java.util.Timer; import java.util.TimerTask; public class CancelTaskExample { public static void main(String[] args) { Timer timer = new Timer(); // 创建 Timer 实例 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("Task executed at: " + System.currentTimeMillis()); cancel(); // 取消自身任务 timer.cancel(); // 取消 Timer,停止所有任务 } }; // 安排任务在 2 秒后执行 timer.schedule(task, 2000); } }
4. 使用定时器的注意事项
-
线程安全:
Timer
是线程安全的,但如果您在任务中调用cancel()
,需要小心。取消任务或定时器时,确保不会在多个线程中同时访问它。 -
异常处理:如前所述,如果
TimerTask
中的run()
方法抛出未捕获的异常,将会导致Timer
停止执行所有任务,因此在run()
方法内处理异常是非常重要的。 -
精度:
Timer
的定时精度不是非常高,适合一些简单的定时任务。如果需要更高的精度或者更复杂的调度逻辑,可以考虑使用ScheduledExecutorService
。
5. 示例:复杂用法
下面是一个更复杂的示例,展示如何在指定时间点执行任务,并处理异常:
import java.util.Timer; import java.util.TimerTask; public class AdvancedTimerExample { public static void main(String[] args) { Timer timer = new Timer(); // 创建 Timer 实例 // 定义一个任务 TimerTask task = new TimerTask() { @Override public void run() { try { // 模拟执行任务 System.out.println("Task executed at: " + System.currentTimeMillis()); // 可能会抛出的异常 if (Math.random() > 0.7) { throw new RuntimeException("Random failure!"); } } catch (Exception e) { System.err.println("Error occurred: " + e.getMessage()); } } }; // 安排任务每 3 秒执行一次,延时 1 秒后开始 timer.scheduleAtFixedRate(task, 1000, 3000); } }