Guava Retry を使用して再試行メカニズムをエレガントに実装する

ビジネスの背景

私たちのシステムでは、顧客が支払いを完了すると、保険契約管理システムが、複数のシステムによって購読されている MQ を通じて保険契約情報を含むメッセージをプッシュします。

メッセージプッシュプラットフォームはポリシー情報に基づいてさまざまな通知(SMSメッセージ、WeChat通知など)を送信し、会員センターはポリシー情報に基づいて会員のポイント蓄積と会員レベルの変更を完了します。初期のプッシュ通知では会員のレベル情報は含まれていませんでしたが、現在は顧客のアイデンティティを強調するために、通知に顧客のレベルを表示する必要があります。

理想的な状況は、メッセージ プッシュ プラットフォームが顧客情報を照会する前に、会員センターが顧客レベルの変更を完了していることですが、実際には、さまざまな理由により、カスタマー センターが会員レベルの変更を時間内に処理できない場合があります。その結果、メッセージ プッシュ プラットフォームは最新の情報をクエリできなくなります。この状況を「回避」するために、顧客情報の照会時に再試行メカニズムを導入しました。

ビジネス要件では、通知内の顧客のメンバーシップ レベルは多少の逸脱が許可されていますが、通知は時間内に送信する必要があるため、再試行ポリシーは比較的緩やかです。今回の注文またはその後の注文によって会員情報が変更されたかどうかを確認します。変更された場合は、いったん終了して再試行してください。そうでない場合は、1 秒間隔で 3 回再試行してください。それでも期待した結果が得られない場合は、現在の結果を使用してください結果を送信して通知を送信します

テクノロジーの選択

考えられる最も簡単な解決策は、while ループを通じて再試行し、クエリ結果と再試行回数を制限し、再試行をいつ終了するかを決定することです。次に例を示します。

CustomerInfo customerInfo;
int count = 0;
 
while(count < 3) {
  customerInfo = CustomerCenter.queryCustomerInfo(customerid);
  if(判断条件) {
    break;
  }
  count ++;
  if(count < 3) {
    TimeUnit.SECONDS.sleep(1);
  }
}

このように記述されていますが、ビジネス ニーズを満たすことができますが、再試行条件、再試行回数、スリープ時間、および再試行メカニズムが組み合わされています。どの時点での変更も、再試行メカニズム全体を変更するのと同じであり、再試行メカニズム全体を変更するのと同じではありません。十分にエレガントですが、非常に粗雑でもあるため、市場でさらに人気のある 2 つの再試行フレームワーク、Spring Retry と Guava Retry を考えました。

春のリトライ

Spring Retry はアノテーションとコードを通じて再試行メカニズムをサポートしていますが、問題は、Spring Retry が例外がスローされた場合の再試行のみをサポートしていることです。たとえば、コードを通じて ReteyTemplate を構築します。

RetryTemplate retryTemplate = RetryTemplate.builder().retryOn(Exception.class).build();

再試行条件はRetryTemplateBuilder#retryOnによって設定されます。このメソッドの宣言を見てみましょう。

public class RetryTemplateBuilder {
  public RetryTemplateBuilder retryOn(Class<? extends Throwable> throwable) {
    // 省略
  }
 
  public RetryTemplateBuilder retryOn(List<Class<? extends Throwable>> throwables) {
    // 省略
  }
}

ご覧のとおりRetryTemplateBuilder#retryOnメソッドの入力パラメータは Throwable とそのサブクラスのみをサポートしているため、Spring Retry はビジネス ニーズを満たすことができず、拒否されました。

グアバ リトライ

Guava Retry は、より柔軟な再試行条件を提供し、例外がスローされた場合、または結果が期待を満たさない場合に再試行できるようにします。

public class RetryerBuilder<V> {
  public RetryerBuilder<V> retryIfException() {
    // 省略
  }
 
  public RetryerBuilder<V> retryIfRuntimeException() {
    // 省略
  }
 
  public RetryerBuilder<V> retryIfExceptionOfType(@Nonnull Class<? extends Throwable> exceptionClass) {
    // 省略
  }
 
  public RetryerBuilder<V> retryIfException(@Nonnull Predicate<Throwable> exceptionPredicate) {
    // 省略
  }
 
  public RetryerBuilder<V> retryIfResult(@Nonnull Predicate<V> resultPredicate) {
    // 省略
  }
}

実際のビジネス ニーズと組み合わせることで、Guava Retry はビジネス ニーズを満たすことができます。

Guava の再試行の使用

まず、Guava Retry の依存関係を紹介します。

<dependency>
  <groupId>com.github.rholder</groupId>
  <artifactId>guava-retrying</artifactId>
  <version>2.0.0</version>
</dependency>

依存関係を導入した後、リトライアー Retryer をビルドして使用できます。次に、リトライアーをビルドする 2 つの方法を見ていきます。コンストラクターを使用して作成する方法と、ビルダーを使用して作成する方法

ヒント: 以下のソース コードに関係する部分では、パラメーター チェック部分は省略されます。

リトライアの構築者

まず Retryer のコンストラクターを見てみましょう。

public final class Retryer<V> {
  public Retryer(@Nonnull StopStrategy stopStrategy,
                 @Nonnull WaitStrategy waitStrategy,
                 @Nonnull Predicate<Attempt<V>> rejectionPredicate) {
    this(AttemptTimeLimiters.<V>noTimeLimit(), stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
  }
 
  public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
                 @Nonnull StopStrategy stopStrategy,
                 @Nonnull WaitStrategy waitStrategy,
                 @Nonnull Predicate<Attempt<V>> rejectionPredicate) {
    this(attemptTimeLimiter, stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate);
  }
  
  public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
                 @Nonnull StopStrategy stopStrategy,
                 @Nonnull WaitStrategy waitStrategy,
                 @Nonnull BlockStrategy blockStrategy,
                 @Nonnull Predicate<Attempt<V>> rejectionPredicate) {
    this(attemptTimeLimiter, stopStrategy, waitStrategy, blockStrategy, rejectionPredicate, new ArrayList<RetryListener>());
  }
 
  @Beta
  public Retryer(@Nonnull AttemptTimeLimiter<V> attemptTimeLimiter,
                 @Nonnull StopStrategy stopStrategy,
                 @Nonnull WaitStrategy waitStrategy,
                 @Nonnull BlockStrategy blockStrategy,
                 @Nonnull Predicate<Attempt<V>> rejectionPredicate,
                 @Nonnull Collection<RetryListener> listeners) {
    this.attemptTimeLimiter = attemptTimeLimiter;
    this.stopStrategy = stopStrategy;
    this.waitStrategy = waitStrategy;
    this.blockStrategy = blockStrategy;
    this.rejectionPredicate = rejectionPredicate;
    this.listeners = listeners;
  }
}

Retryer は 4 つのコンストラクターを提供します。最初の 3 つのコンストラクターは、6 つのパラメーターを含むコンストラクターに戻ります。これらの 6 つのパラメーターの機能は以下で説明されます。

  • AttemptTimeLimiter<V> を使用すると、リクエストのタイムアウトを設定できます。この時間を超えると、Retryer は中断されます。

  • StopStrategy、再試行戦略。最大再試行回数の設定に使用されます。最大再試行回数に達すると、Retryer は中断します。

  • WaitStrategy、スリープ時間戦略。各リクエスト後のスリープ時間を計算します。

  • BlockStrategy (ブロック戦略) もリクエストの後に機能し、Retry がどのようにブロックするかを決定します (WaitStrategy によって計算された時間が必要です)。

  • Predicate、条件述語。再試行するかどうかを決定します。

  • Collection<RetryListener> リスナーは、リクエスト後のコールバックを許可します。

上記 6 つのパラメータはすべてインターフェイスであり、RetryListener を除く Guava Retry はデフォルトの実装を提供しますが、同時にビジネス ニーズに応じてカスタマイズされた戦略を実装することもできます。

リトライアーのビルダー

コンストラクターを使用して Retryer オブジェクトを作成することに加えて、Guava Retry はビルダー パターン RetryerBuilder も提供します。

public class RetryerBuilder<V> {
 
  public static <V> RetryerBuilder<V> newBuilder() {
    return new RetryerBuilder<V>();
  }
 
  // 省略设置策略的部分
 
  public Retryer<V> build() {
    AttemptTimeLimiter<V> theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.<V>noTimeLimit() : attemptTimeLimiter;
    StopStrategy theStopStrategy = stopStrategy == null ? StopStrategies.neverStop() : stopStrategy;
    WaitStrategy theWaitStrategy = waitStrategy == null ? WaitStrategies.noWait() : waitStrategy;
    BlockStrategy theBlockStrategy = blockStrategy == null ? BlockStrategies.threadSleepStrategy() : blockStrategy;
 
    return new Retryer<V>(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners);
  }
}

RetryerBuilder#buildこのメソッドは最終的に Retryer のコンストラクターを呼び出します。ビルダーを通じて Retryer を作成する例を見てみましょう。

Retryer<Long> retryer = RetryerBuilder.<Long>newBuilder()
.retryIfException() // 抛出异常时重试
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 最大重试次数 3 次
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) // 每次重试间隔 1 秒
.build();

ここでは、さまざまな戦略に対応するツール クラスを使用して、Guava で提供されるデフォルトの戦略を取得します。

コンストラクターまたはビルダーを通じてリトライアー Retryer を作成した後、Retryer#call次のような再試行メカニズムを含む呼び出しを直接行うことができます。

Long time = retryer.call(new Callable<Long>() {
	@Override
	public Long call() throws Exception {
		return System.currentTimeMillis();
	}
});

次に、Retryer のリトライ メカニズムと、Guava Retry が提供する戦略をソース コードを通じて分析します。

ソースコード分析

リトライアー リトライアー

Retryer は、再試行メカニズムを提供する Guava Retry のコア クラスです。Retryer が提供するメソッドは、構築メソッドのほかに、Retryer#callRetryer#warp

その内Retryer#warpは、Retryer と Callable のパッケージ化を提供します。ソース コードは非常に単純なので、ここでは詳しく説明しません。Retryer#call メソッド: a>

public V call(Callable<V> callable) throws ExecutionException, RetryException {
  long startTime = System.nanoTime();
  // 创建计数器
  for (int attemptNumber = 1; ; attemptNumber++) {
    Attempt<V> attempt;
    try {
      // 调用 Callable 接口
      V result = attemptTimeLimiter.call(callable);
      // 封装结果未 ResultAttempt 对象
      attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
    } catch (Throwable t) {
      // 封装异常为 ExceptionAttempt 对象
      attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
    }
 
    // 调用监听器
    for (RetryListener listener : listeners) {
      listener.onRetry(attempt);
    }
 
    // 判断是否满足重试条件
    if (!rejectionPredicate.apply(attempt)) {
      return attempt.get();
    }
 
    // 判断是否达到最大重试次数
    if (stopStrategy.shouldStop(attempt)) {
      throw new RetryException(attemptNumber, attempt);
    } else {
      // 计算休眠时间
      long sleepTime = waitStrategy.computeSleepTime(attempt);
      try {
        // 调用阻塞策略
        blockStrategy.block(sleepTime);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RetryException(attemptNumber, attempt);
      }
    }
  }
}

Retryer#callソース コードは複雑ではなく、最初に考えた while ループによるリトライの原理と同じですが、Guava Retry では、コードに結合したリトライ回数やリトライ条件をさまざまなストラテジ インターフェイスを使用して置き換えます。時間など、結果と例外はカプセル化されます。

Guava Retry は、インターフェイスを使用して、さまざまな戦略を再試行メカニズムから切り離します。戦略のいずれかを変更するか置き換える必要がある場合は、対応する戦略の実装を変更するか、新しい戦略を追加して置き換えるだけで済みます。再試行メカニズムのコードを変更する必要はなく、これが再試行メカニズムを適切に使用するための鍵です

次に、Retryer#call メソッドでのさまざまなストラテジ呼び出しの順序に従って、各ストラテジ インターフェイスによって提供される関数を 1 つずつ分析します。

ヒント: Retryer#call メソッドに関係する次の行は、この章のソース コードを示す行数です。

AttemptTimeLimiter インターフェース

Retryer#call メソッドの 8 行目を最初に見てください

V result = attemptTimeLimiter.call(callable);

このコード行では AttemptTimeLimiter インターフェイスを使用していますが、このインターフェイスではメソッドが 1 つだけ提供されています。

public interface AttemptTimeLimiter<V> {
  V call(Callable<V> callable) throws Exception;
}

このメソッドは、Callable インターフェイスの実装を呼び出すために使用されます。Guava Retry には、NoAttemptTimeLimit とFixedAttemptTimeLimit という 2 つの AttemptTimeLimiter 実装が用意されています。これらはすべて、ユーティリティ クラス AttemptTimeLimiters の内部クラスです。

public class AttemptTimeLimiters {
  @Immutable
  private static final class NoAttemptTimeLimit<V> implements AttemptTimeLimiter<V> {
    @Override
    public V call(Callable<V> callable) throws Exception {
      return callable.call();
    }
  }
 
  @Immutable
  private static final class FixedAttemptTimeLimit<V> implements AttemptTimeLimiter<V> {
 
    private final TimeLimiter timeLimiter;
    private final long duration;
    private final TimeUnit timeUnit;
 
    // 省略构造方法
 
    @Override
    public V call(Callable<V> callable) throws Exception {
      return timeLimiter.callWithTimeout(callable, duration, timeUnit, true);
    }
  }
}

ソース コードから明らかなように、NoAttemptTimeLimit#call は通話のタイムアウト期間を制限しませんが、FixedAttemptTimeLimit#call タイムアウト期間を追加します。 a>。タイムアウトのある呼び出しは、Guava の TimeLimiter を通じて実装されます。

NoAttemptTimeLimit およびFixedAttemptTimeLimit はツール クラス AttemptTimeLimiters のプライベート内部クラスであるため、外部クラスで直接使用することはできません。ただし、ツール クラス AttemptTimeLimiters を通じて NoAttemptTimeLimit およびFixedAttemptTimeLimit を取得できます。ソース コードは次のとおりです。

public class AttemptTimeLimiters {
  public static <V> AttemptTimeLimiter<V> noTimeLimit() {
    return new NoAttemptTimeLimit<V>();
  }
  
  public static <V> AttemptTimeLimiter<V> fixedTimeLimit(long duration, @Nonnull TimeUnit timeUnit) {
    return new FixedAttemptTimeLimit<V>(duration, timeUnit);
  }
  
  public static <V> AttemptTimeLimiter<V> fixedTimeLimit(long duration, @Nonnull TimeUnit timeUnit, @Nonnull ExecutorService executorService) {
    return new FixedAttemptTimeLimit<V>(duration, timeUnit, executorService);
  }
}

インターフェースの試行

Retryer#call メソッドの 5 行目で宣言されている Attempt を見てみましょう

public interface Attempt<V> {
 
  public V get() throws ExecutionException;
 
  public boolean hasResult();
 
  public boolean hasException();
 
  public V getResult() throws IllegalStateException;
 
  public Throwable getExceptionCause() throws IllegalStateException;
 
  public long getAttemptNumber();
 
  public long getDelaySinceFirstAttempt();
}

Attempt インターフェイスは、再試行メカニズムの結果 (正しい呼び出しの結果または発生した例外) のカプセル化を提供します。。インターフェイスには 7 つのメソッドが用意されています。メソッド名は使用できると思います。各メソッドの役割を知ることができます(各メソッドの役割については実装クラスを通じて後述します)。

Attempt インターフェイスの 2 つの実装クラス、ResultAttempt と ExceptionAttempt を見てみましょう。これら 2 つのクラスは、Retryer の静的な内部クラスです。まず、ResultAttempt が Retryer#call でどのように使用されるかを見てみましょう。メソッド:

attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));

ResultAttempt オブジェクトを作成するとき、Callacle の呼び出し結果、再試行回数、最初の呼び出しからの経過時間という 3 つのパラメーターが渡されます。 ResultAttempt のソース コードを見てみましょう。

@Immutable
static final class ResultAttempt<R> implements Attempt<R> {
  private final R result;
  private final long attemptNumber;
  private final long delaySinceFirstAttempt;
 
  // 省略构造方法
 
  // 获取调用结果
  @Override
  public R get() throws ExecutionException {
    return result;
  }
 
  // 是否包含结果,ResultAttempt 的实现中只返回 true
  @Override
  public boolean hasResult() {
    return true;
  }
 
  // 是否包含异常,ResultAttempt 的实现中只返回 false
  @Override
  public boolean hasException() {
    return false;
  }
 
  // 获取调用结果
  @Override
  public R getResult() throws IllegalStateException {
    return result;
  }
 
  // 获取异常原因,因为 ResultAttempt 是成功调用,因此无异常
  @Override
  public Throwable getExceptionCause() throws IllegalStateException {
    throw new IllegalStateException("The attempt resulted in a result, not in an exception");
  }
 
  // 获取重试次数
  @Override
  public long getAttemptNumber() {
    return attemptNumber;
  }
 
  // 获取自首次调用后的耗时
  @Override
  public long getDelaySinceFirstAttempt() {
    return delaySinceFirstAttempt;
  }
}

は実装が非常に簡単なので、ここでは詳しく説明しません。 ExceptionAttempt がRetryer#call メソッドでどのように使用されるかを見てみましょう。

try {
  // 此处是使用 ResultAttempt的逻辑
} catch (Throwable t) {
  attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}

ResultAttempt の結果が例外情報に置き換えられることを除いて、3 つのパラメーターは同じです。

ExceptionAttempt のソース コード実装に関しては、Attempt インターフェイスも実装されているため、ExceptionAttempt は ResultAttempt の単なる「逆」であると簡単に考えることができます。

RetryListenerインターフェース

Retryer#callRetryListener はメソッドの 17 ~ 19 行目で呼び出されます。

for (RetryListener listener : listeners) {
  listener.onRetry(attempt);
}

RetryListener インターフェースが提供するメソッドは 1 つだけです。

@Beta
public interface RetryListener {
  <V> void onRetry(Attempt<V> attempt);
}

RetryListener は再試行プロセスのリスナーとして表示され、拡張処理のためのコールバック メカニズムを提供します。 Guava Retry はデフォルトの実装を提供しません。また、RetryListener はベータとしてマークされています。Guava の解釈では、注釈ベータが付けられており、将来的に大きな変更が加えられるか、削除される可能性があります。

述語インターフェース

Retryer#callメソッド呼び出し Predicate の行 22 ~ 24:

if (!rejectionPredicate.apply(attempt)) {
  return attempt.get();
}

Predicate は、Guava の述語インターフェイスです。。Predicate インターフェイスで提供されるメソッドを見てみましょう。

public interface Predicate<T extends @Nullable Object> extends java.util.function.Predicate<T> {
 
  // Guava Retry 中定义的方法
  boolean apply(@ParametricNullness T input);
 
  // 继承自 Java 中的 Object 类
  @Override
  boolean equals(@CheckForNull Object object);
}

上記の 2 つのメソッドに加えて、Guava の Predicate インターフェイスも Java の Predicate インターフェイスを継承していますが、これらは今日の焦点では​​ありません。

Predicate インターフェイスには、Guava Retry に ResultPredicate、ExceptionClassPredicate、ExceptionPredicate の 3 つの実装クラスがあります。これらはすべて RetryerBuilder の内部クラスとして表示されます。

private static final class ResultPredicate<V> implements Predicate<Attempt<V>> {
 
  private Predicate<V> delegate;
 
  public ResultPredicate(Predicate<V> delegate) {
    this.delegate = delegate;
  }
 
  @Override
  public boolean apply(Attempt<V> attempt) {
    // 判断 Attempt 中是否包含结果
    if (!attempt.hasResult()) {
      return false;
    }
    // 获取结果并调用条件谓词的 apply 方法
    V result = attempt.getResult();
    return delegate.apply(result);
  }
}
 
private static final class ExceptionClassPredicate<V> implements Predicate<Attempt<V>> {
 
  private Class<? extends Throwable> exceptionClass;
 
  public ExceptionClassPredicate(Class<? extends Throwable> exceptionClass) {
    this.exceptionClass = exceptionClass;
  }
 
  @Override
  public boolean apply(Attempt<V> attempt) {
    if (!attempt.hasException()) {
      return false;
    }
    return exceptionClass.isAssignableFrom(attempt.getExceptionCause().getClass());
  }
}
 
private static final class ExceptionPredicate<V> implements Predicate<Attempt<V>> {
 
  private Predicate<Throwable> delegate;
 
  public ExceptionPredicate(Predicate<Throwable> delegate) {
    this.delegate = delegate;
  }
 
  @Override
  public boolean apply(Attempt<V> attempt) {
    if (!attempt.hasException()) {
      return false;
    }
    return delegate.apply(attempt.getExceptionCause());
  }
}

コードの一部を使用して、ResultPredicate がどのように機能するかを説明することに焦点を当てましょう。まず、ビルダー パターンを使用して Retryer オブジェクトを作成します。

Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
.retryIfResult(new Predicate<Integer>() {
  @Override
  public boolean apply(Integer result) {
    return result > 0;
  }
}).withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();

RetryerBuilder#retryIfResult メソッドのソース コードを見てみましょう

public class RetryerBuilder<V> {
  public RetryerBuilder<V> retryIfResult(@Nonnull Predicate<V> resultPredicate) {
    rejectionPredicate = Predicates.or(rejectionPredicate, new ResultPredicate<V>(resultPredicate));
    return this;
  }
}

条件付き述語を構築するために RetryerBuilder#retryIfResultPredicates#or が使用されていることがわかります。最初のパラメータは、前のパラメータを介して RetryerBuilder のメンバー変数拒否Predicate です。 RetryerBuilder のソース コードを見ると、 RetryerBuilder の拒否Predicate が最終的に Retryer のメンバー変数になり、2 番目のパラメーターが、渡した Predicate オブジェクトによって構築された ResultPredicate オブジェクトであることがわかります。

Predicates#orこのメソッドの機能は、受信パラメータを新しい Predicate オブジェクトにマージすることです。

public static <T extends @Nullable Object> Predicate<T> or(Predicate<? super T> first, Predicate<? super T> second) {
  return new OrPredicate<>(Predicates.<T>asList(checkNotNull(first), checkNotNull(second)));
}

マージされた Predicate オブジェクトはその実装クラス OrPredicate であることに注意してください。このクラスのメンバー変数private final List<? extends Predicate<? super T>> componentsには、RetryerBuilder を通じて追加されたすべての条件付き述語が含まれます。

OrPredicate#applyこのメソッドは、ループを通じてさまざまな Predicate オブジェクトを呼び出します。

private static class OrPredicate<T extends @Nullable Object> implements Predicate<T>, Serializable {
 
  private final List<? extends Predicate<? super T>> components;
 
  @Override
  public boolean apply(@ParametricNullness T t) {
    for (int i = 0; i < components.size(); i++) {
      if (components.get(i).apply(t)) {
        return true;
      }
    }
    return false;
  }
}

OrPredicate#apply メソッドは条件述語をループして Predicate#apply メソッドを呼び出し、ResultPredicate#apply メソッドに戻ります。

ResultPredicate オブジェクトを構築するときRetryerBuilder#retryIfResult、渡した内部クラス Predicate オブジェクトが ResultPredicate のメンバー変数デリゲートとして使用され、それが ResultPredicate かどうかの最終的な判断者であることに気付きました。結果は条件を満たします。メンバー変数デリゲートを通じて達成されます:

private static final class ResultPredicate<V> implements Predicate<Attempt<V>> {
  @Override
  public boolean apply(Attempt<V> attempt) {
    if (!attempt.hasResult()) {
      return false;
    }
    V result = attempt.getResult();
    return delegate.apply(result);
  }
}

ここまでで、Retryer#call メソッドで ResultPredicate がどのように動作するかがわかりました。ExceptionClassPredicate と ExceptionPredicate については、原理が ResultPredicate と同様であるため、説明は省略します。

StopStrategy インターフェイス

Retryer#callStopStrategy はメソッドの 27 行目で呼び出されます。

if (stopStrategy.shouldStop(attempt)) {
  throw new RetryException(attemptNumber, attempt);
} else {
  // 省略休眠策略 
}

StopStrategy インターフェイスは、再試行が必要かどうかを判断するメソッドを 1 つだけ提供します。インターフェイスの宣言は次のとおりです:

public interface StopStrategy {
  boolean shouldStop(Attempt failedAttempt);
}

Guava Retry は、NeverStopStrategy、StopAfterAttemptStrategy、および StopAfterDelayStrategy の 3 つの実装クラスを提供します。これら 3 つの実装クラスはすべて、ツール クラス StopStrategys の内部クラスです。

public final class StopStrategies {
  @Immutable
  private static final class NeverStopStrategy implements StopStrategy {
    @Override
    public boolean shouldStop(Attempt failedAttempt) {
      return false;
    }
  }
  
  @Immutable
  private static final class StopAfterAttemptStrategy implements StopStrategy {
    private final int maxAttemptNumber;
 
    // 省略构造方法
 
    @Override
    public boolean shouldStop(Attempt failedAttempt) {
      return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
    }
  }
 
  @Immutable
  private static final class StopAfterDelayStrategy implements StopStrategy {
    private final long maxDelay;
 
    // 省略构造方法
 
    @Override
    public boolean shouldStop(Attempt failedAttempt) {
      return failedAttempt.getDelaySinceFirstAttempt() >= maxDelay;
    }
  }
}

これら 3 つの戦略の機能を説明しましょう。

  • NeverStopStrategy、条件述語が満たされない限り再試行を停止しない

  • StopAfterAttemptStrategy、指定された回数の後に再試行を停止します。

  • StopAfterDelayStrategy は、指定された時間の後に再試行を停止します。

通常は StopAfterAttemptStrategy を選択しますが、時間要件のあるシナリオでは StopAfterDelayStrategy を選択することもできます。

Retryer#call のメソッドでは、StopStrategy のトリガーにより再試行が停止された場合、例外情報をカプセル化した例外 RetryException がスローされることに注意してください。最後のリクエスト。これには、Retryer を使用する場合の例外処理が必要です。

WaitStrategy インターフェイスと BlockStrategy インターフェイス

これら 2 つのインターフェイスは、Retryer#call メソッドの 31 行目と 34 行目でそれぞれ呼び出されます。

if (stopStrategy.shouldStop(attempt)) {
  throw new RetryException(attemptNumber, attempt);
} else {
  // 调用计算休眠时间策略
  long sleepTime = waitStrategy.computeSleepTime(attempt);
  try {
    // 调用阻塞策略
    blockStrategy.block(sleepTime);
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new RetryException(attemptNumber, attempt);
  }
}

WaitStrategy インターフェイスはスリープ時間を計算するための戦略を提供し、BlockStrategy インターフェイスは再試行ブロック戦略を提供します。インターフェイスの宣言は次のとおりです:

public interface WaitStrategy {
  long computeSleepTime(Attempt failedAttempt);
}
 
public interface BlockStrategy {
  void block(long sleepTime) throws InterruptedException;
}

この 2 つの機能は相互に補完します。WaitStrategy#computeSleepTime は毎回スリープ時間を計算し、BlockStrategy#block はブロック戦略の実行を担当します。 a>

まず、Guava Retry で提供される BlockStrategy の実装である ThreadSleepStrategy を見てみましょう。この実装は、ツール クラス BlockStrategys の内部クラスとして表示されます。実装は非常に簡単です。

public final class BlockStrategies {
  @Immutable
  private static class ThreadSleepStrategy implements BlockStrategy {
 
    @Override
    public void block(long sleepTime) throws InterruptedException {
      // 休眠指定时间
      Thread.sleep(sleepTime);
    }
  }
}

次に、WaitStrategy の実装クラスを見てみましょう。Guava Retry は、WaitStrategy インターフェイスの 7 つの実装を提供します。

  • FixedWaitStrategy、固定スリープ時間戦略。

  • RandomWaitStrategy、ランダムなスリープ時間戦略。

  • IncrementingWaitStrategy、ステップ サイズごとに増加する睡眠時間戦略。

  • ExponentialWaitStrategy、指数関数的に増加する睡眠時間戦略。

  • FibonacciWaitStrategy、フィボナッチ数列を通じて睡眠時間を計算する戦略。

  • CompositeWaitStrategy、混合睡眠時間戦略。

  • ExceptionWaitStrategy、例外が発生したときのスリープ時間。

上記の 7 つの戦略はすべて、ツール クラス WaitStrategys の内部クラスであり、WaitStrategys を通じて直接使用できます。

public final class WaitStrategies {
 
	// 使用固定休眠时间策略
	public static WaitStrategy fixedWait(long sleepTime, @Nonnull TimeUnit timeUnit) throws IllegalStateException {
		return new FixedWaitStrategy(timeUnit.toMillis(sleepTime));
	}
 
	// 使用随机休眠时间策略
	public static WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit) {
		return new RandomWaitStrategy(0L, timeUnit.toMillis(maximumTime));
	}
 
	public static WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit) {
		return new RandomWaitStrategy(0L, timeUnit.toMillis(maximumTime));
	}
 
	// 使用按步长增长的休眠时间策略
	public static WaitStrategy incrementingWait(long initialSleepTime,  @Nonnull TimeUnit initialSleepTimeUnit, long increment, @Nonnull TimeUnit incrementTimeUnit) {
		return new IncrementingWaitStrategy(initialSleepTimeUnit.toMillis(initialSleepTime), incrementTimeUnit.toMillis(increment));
	}
 
	// 使用指数增长的休眠时间策略
	public static WaitStrategy exponentialWait() {
		return new ExponentialWaitStrategy(1, Long.MAX_VALUE);
	}
 
	public static WaitStrategy exponentialWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit) {
		return new ExponentialWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime));
	}
 
	public static WaitStrategy exponentialWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit) {
		return new ExponentialWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
	}
 
	// 使用通过斐波那契数列计算休眠时间的策略
	public static WaitStrategy fibonacciWait() {
		return new FibonacciWaitStrategy(1, Long.MAX_VALUE);
	}
 
	public static WaitStrategy fibonacciWait(long maximumTime, @Nonnull TimeUnit maximumTimeUnit) {
		return new FibonacciWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime));
	}
 
	public static WaitStrategy fibonacciWait(long multiplier, long maximumTime, @Nonnull TimeUnit maximumTimeUnit) {
		return new FibonacciWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
	}
 
	// 使用混合的休眠时间策略
	public static WaitStrategy join(WaitStrategy... waitStrategies) {
		List<WaitStrategy> waitStrategyList = Lists.newArrayList(waitStrategies);
		return new CompositeWaitStrategy(waitStrategyList);
	}
 
	// 使用发生异常时的休眠时间
	public static <T extends Throwable> WaitStrategy exceptionWait(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
		return new ExceptionWaitStrategy<T>(exceptionClass, function);
	}
}

最後に、各戦略がどのように実装されているかを 1 つずつ分析してみましょう。

固定待機戦略

最も一般的に使用される戦略は、各再試行後に一定時間スリープすることです。ソース コードは次のとおりです。

@Immutable
private static final class FixedWaitStrategy implements WaitStrategy {
  private final long sleepTime;
 
  public FixedWaitStrategy(long sleepTime) {
    this.sleepTime = sleepTime;
  }
 
  @Override
  public long computeSleepTime(Attempt failedAttempt) {
    return sleepTime;
  }
}

RandomWait戦略

毎回、最小スリープ時間と最大スリープ時間の間でスリープ時間がランダムに選択されます。ソース コードは次のとおりです。

@Immutable
private static final class RandomWaitStrategy implements WaitStrategy {
  private static final Random RANDOM = new Random();
  private final long minimum;
  private final long maximum;
 
  public RandomWaitStrategy(long minimum, long maximum) {
    this.minimum = minimum;
    this.maximum = maximum;
  }
 
  @Override
  public long computeSleepTime(Attempt failedAttempt) {
    long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum);
    return t + minimum;
  }
}

計算方法は複雑ではなく、最小時間から最大時間までの乱数を計算し、最小時間を加算するだけです。

増分待機戦略

再試行のたびにスリープ時間が固定的に増加する戦略:

@Immutable
private static final class IncrementingWaitStrategy implements WaitStrategy {
  private final long initialSleepTime;
  private final long increment;
 
  public IncrementingWaitStrategy(long initialSleepTime, long increment) {
    this.initialSleepTime = initialSleepTime;
    this.increment = increment;
  }
 
  @Override
  public long computeSleepTime(Attempt failedAttempt) {
    long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1));
    return result >= 0L ? result : 0L;
  }
}

パラメータは初期スリープ時間と各増加のステップ サイズで、各増加の時間は Retryer の再試行回数によって計算されます。

ExponentialWait戦略

再試行回数に応じてスリープ時間を指数関数的に増やす戦略:

@Immutable
private static final class ExponentialWaitStrategy implements WaitStrategy {
  private final long multiplier;
  private final long maximumWait;
 
  public ExponentialWaitStrategy(long multiplier, long maximumWait) {
    this.multiplier = multiplier;
    this.maximumWait = maximumWait;
  }
 
  @Override
  public long computeSleepTime(Attempt failedAttempt) {
    double exp = Math.pow(2, failedAttempt.getAttemptNumber());
    long result = Math.round(multiplier * exp);
    if (result > maximumWait) {
      result = maximumWait;
    }
    return result >= 0L ? result : 0L;
  }
}

入力パラメータは最大スリープ時間と係数です。スリープ時間の底は2を底とし、リトライ回数を指標として計算されます。渡された係数を乗じて実際のスリープ時間を求めます。計算結果が制限時間を超えた場合は、最大スリープ時間 、最大スリープ時間を使用します。

フィボナッチ待ち戦略

再試行回数に基づいて、対応するフィボナッチ数をスリープ時間として取得する戦略:

@Immutable
private static final class FibonacciWaitStrategy implements WaitStrategy {
	private final long multiplier;
	private final long maximumWait;
 
	public FibonacciWaitStrategy(long multiplier, long maximumWait) {
		this.multiplier = multiplier;
		this.maximumWait = maximumWait;
	}
 
	@Override
	public long computeSleepTime(Attempt failedAttempt) {
		long fib = fib(failedAttempt.getAttemptNumber());
		long result = multiplier * fib;
 
		if (result > maximumWait || result < 0L) {
			result = maximumWait;
		}
 
		return result >= 0L ? result : 0L;
	}
 
	private long fib(long n) {
		if (n == 0L) return 0L;
		if (n == 1L) return 1L;
 
		long prevPrev = 0L;
		long prev = 1L;
		long result = 0L;
 
		for (long i = 2L; i <= n; i++) {
			result = prev + prevPrev;
			prevPrev = prev;
			prev = result;
		}
 
		return result;
	}
}

これは ExponentialWaitStrategy の戦略に非常に似ています。入力パラメータは最大スリープ時間と係数です。再試行回数に対応するフィボナッチ数がスリープ時間のベースとして取得され、渡された係数を乗算して、実スリープ時間 計算結果が最大スリープ時間を超える場合、最大スリープ時間が使用されます。

複合待機戦略

複数の計算スリープ時間戦略を統合する戦略:

@Immutable
private static final class CompositeWaitStrategy implements WaitStrategy {
	private final List<WaitStrategy> waitStrategies;
 
	public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) {
		this.waitStrategies = waitStrategies;
	}
 
	@Override
	public long computeSleepTime(Attempt failedAttempt) {
		long waitTime = 0L;
		for (WaitStrategy waitStrategy : waitStrategies) {
			waitTime += waitStrategy.computeSleepTime(failedAttempt);
		}
		return waitTime;
	}
}

各睡眠時間ポリシーの睡眠時間を計算し、それらを合計して最終的な睡眠時間を取得します。

例外待機戦略

この戦略は、例外が発生したときにスリープ時間を計算するために使用されます。

@Immutable
private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
	private final Class<T> exceptionClass;
	private final Function<T, Long> function;
 
	public ExceptionWaitStrategy(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
		this.exceptionClass = exceptionClass;
		this.function = function;
	}
 
	@SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ConstantConditions", "unchecked"})
	@Override
	public long computeSleepTime(Attempt lastAttempt) {
		if (lastAttempt.hasException()) {
			Throwable cause = lastAttempt.getExceptionCause();
			if (exceptionClass.isAssignableFrom(cause.getClass())) {
				return function.apply((T) cause);
			}
		}
		return 0L;
	}
}

は、例外のタイプと Function の実装を渡す必要があります。対応するタイプの例外が発生した場合、 Function#apply メソッドを実行してスリープ時間を計算します。例外ごとに睡眠時間は異なります。

たとえば、最初に 3 つの例外とその親クラスを定義します。

public class BaseException extends Exception {
	public BaseException(String message) {
		super(message);
	}
}
 
public class OneException extends BaseException {
	public OneException(String message) {
		super(message);
	}
}
 
public class TwoException extends BaseException {
	public TwoException(String message) {
		super(message);
	}
}
 
public class ThreeException extends BaseException {
	public ThreeException(String message) {
		super(message);
	}
}

次に、Function インターフェイスを実装します。

public class ExceptionFunction implements Function<BaseException, Long> {
 
	@Override
	public Long apply(BaseException input) {
		if (OneException.class.isAssignableFrom(input.getClass())) {
			System.out.println("触发OneException,休眠1秒!");
			return 1000L;
		}
		if (TwoException.class.isAssignableFrom(input.getClass())) {
			System.out.println("触发TwoException,休眠2秒!");
			return 2000L;
		}
		if (ThreeException.class.isAssignableFrom(input.getClass())) {
			System.out.println("触发ThreeException,休眠3秒!");
			return 3000L;
		}
		return 0L;
	}
}

このインターフェイスは、さまざまな例外に基づいてさまざまなスリープ時間を返します。

最後に、リトライアーを構築し、Retryer#call メソッドを呼び出します。

Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
.retryIfException()
.withWaitStrategy(WaitStrategies.exceptionWait(BaseException.class, new ExceptionFunction()))
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.build();
 
int number = retryer.call(new Callable<>() {
	private int count = 1;
	@Override
	public Integer call() throws Exception {
		if (count < 2) {
			count++;
			throw new OneException("One");
		}
		if (count < 3) {
			count++;
			throw new TwoException("Two");
		}
		if (count < 4) {
			count++;
			throw new ThreeException("Three");
		}
		return count;
	}
});
System.out.println(number);

Retryer Retryer は、インターフェイス例外を呼び出すときに再試行します。再試行の最大数は 4 です。スリープ時間戦略の観点からは、例外が発生すると、さまざまな例外に応じて異なる時間スリープします。 Retryer.call によって呼び出される Callable インターフェイスでは、最初の 3 つの呼び出しで OneException、TwoException、ThreeException がそれぞれスローされ、4 番目の呼び出しで数値 4 が返されます。コードを実行すると、次の出力が表示されます。

さまざまな例外が発生すると、さまざまなスリープ時間戦略がトリガーされることが証明されています。

実践的な演習

ここまで、Guava Retry の使い方と原理を理解しましたが、次に、冒頭で述べたシナリオを例にして実践的な演習を行っていきます。

当社のビジネス シナリオでは、通知におけるメンバーシップ レベルの変更がタイムリーでないことは許容されますが、金融規制上の要件により、顧客レベルの変更によって通知が遅れることは許容されません。したがって、1 秒間隔で 3 回再試行し、3 回実行しても最新のデータが取得できない場合は、前回のリクエストの結果が使用されるという非常にリラックスした再試行戦略を策定します。

まずクライアント クラスを作成します。

public class CustomerDTO {
 
  private Long customerId;
 
  private String customerName;
 
  private CustomerLevel customerLevel;
 
  private Long lastOrderId;
}

このうち、lastOrderIdは顧客レベルや顧客ポイントを変更した最後の注文IDを記録しており、該当する顧客情報が取得できているかどうかを判断するために使用します。

次に、カスタマー センターを模倣して顧客情報を取得するメソッドを作成します。

public class CustomerCenter {
 
  private static int count = 0;
 
  public static CustomerDTO getCustomerInfo(Long customerId) {
    if (count < 1) {
      count++;
      return createCustomerInfo(customerId, CustomerLevel.JUNIOR_MEMBER, 1234567L);
    } else if (count < 2) {
      count++;
      return createCustomerInfo(customerId, CustomerLevel.INTERMEDIATE_MEMBER, 12345678L);
    } else {
      count = 0;
      return createCustomerInfo(customerId, CustomerLevel.SENIOR_MEMBER, 123456789L);
    }
  }
 
  private static CustomerDTO createCustomerInfo(Long customerId, CustomerLevel customerLevel, Long lastOrdertId) {
    CustomerDTO customerDTO = new CustomerDTO();
    customerDTO.setCustomerId(customerId);
    customerDTO.setCustomerName("WYZ");
    customerDTO.setCustomerLevel(customerLevel);
    customerDTO.setLastOrderId(lastOrdertId);
 
    return customerDTO;
  }
}

その内CustomerCenter#getCustomerInfoでは、3 番目のクエリで最新の顧客情報の取得をシミュレートします。

最後に、再試行コードを書きましょう。

public static void main(String[] args) throws ExecutionException {
 
  Long lastOrderId = 123456789L;
 
  Retryer<CustomerDTO> retryer = RetryerBuilder.<CustomerDTO>newBuilder()
  .retryIfResult(customerDTO -> !lastOrderId.equals(customerDTO.getLastOrderId()))
  .withWaitStrategy(failedAttempt -> 1000)
  .withStopStrategy(attempt -> attempt.getAttemptNumber() > 2)
  .build();
 
  CustomerDTO customerDTO;
  try {
    customerDTO = retryer.call(() -> CustomerCenter.getCustomerInfo(1L));
  } catch (RetryException e) {
    Attempt<?> attempt = e.getLastFailedAttempt();
    customerDTO = (CustomerDTO) attempt.get();
  }
}

Retryer の作成手順についてはあまり説明しませんので、15 行目と 16 行目の部分を見てみましょう。先ほどのソースコードを見ると、Guava Retry がリトライ回数を超えても期待した結果が得られない場合がわかります。 , RetryException 例外がスローされます。この例外には、例外情報に加えて、最後の実行後の Attempt も含まれているため、Attempt を通じて最後の実行結果を取得できます。これはビジネス ニーズを満たしています。

さて、今日はここまでです。

この記事はWang Youzhi から転載されました。

元のリンク:https://www.cnblogs.com/wyz1994/p/17878413.html

おすすめ

転載: blog.csdn.net/sdgfafg_25/article/details/134831277