一种退避序列实现

介绍功能和需求背景
介绍代码上下文,思路,方案。 
讲解代码,评委穿插提问。参会的其他同学有问题也可以提。 
评委对代码给建议和反馈

写在前面

         你有一个苹果,我有一个苹果,交换一下我们还是只有一个苹果;你有一本书,我有一本书,交换一下我们就有两本书了。每个程序员都有自己的工具集和代码私库,如果大家一起贡献出来,我们就会有一套较完整的工具集,相信可以解决大部分问题。

         作为一个程序员,一个工科生,现实中的理想就是有一套趁手的完整的工具套件,家里有点什么事情,只要拿出我的工具套件就能自己轻松搞定。跟现实中一样,我们中间件团队也在默默的打造自己的软件工具套件,也希望更多的同学一起来打造这一套称心的软件工具套件。

一、需求背景

1.1 业务需求

1.1.1 Nebulax流程实例保存失败重试和告警

流程执行完后会将流程实例调用http接口上报到星云服务端,若星云服务端不可用,不会发起重试,导致流程虽然执行完成,但是在控制台查看不到对应流程实例信息。

- 改造成上报失败按(指数退避算法)策略,在一段时间后重试,确保流程实例能够上传到服务端
- 若上传失败,在尝试若干次后记录日志并发送告警给对应业务负责人

1.1.2 模拟MetaQ失败退避算法

网关应用原先依赖MetaQ,改造升级后内部的重试策略也是继承一致的重试策略,按固定级别(1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h)退避。

1.1.3 延迟调度平台重试策略优化

  • 发送消息时设置重试策略,目前支持两种重试策略

1、【普通模式】:默认模式,强烈建议用该模式,该模式间隔会逐步递增(5s,10s,30s,60s... 逐渐递增,总周期为7天,总共20次)。

2、【表达式模式】:

距离上次重试的时间间隔 = 首次重试间隔 + 已重试次数 * 重试时间间隔步长 * 首次重试间隔(秒)

在表达式模式的策略下,最大重试、首次重试间隔和步长才会生效。注意普通模式下,以下参数不生效。

  • 调度限流重试策略

  1. 主动限流使用随机数,5~15分钟

jobDetail.setRedoIntervalMode(RedoIntervalModeConstant.EXPRESSION);
// 最大重试次数,默认99次 
jobDetail.setFailRedoCountMax(10);
// 首次重试间隔(秒)
jobDetail.setFailFirstRedoInterval(5);
// 重试时间间隔步长,如设置成0,则每次重试间隔相同,如设置成1,则间隔是5、10、15...
jobDetail.setFailRedoIntervalStep(0);

1.2 需求整理

  • 根据策略获取下一次重试间隔

  • 根据策略获取指定次数的重试间隔

  • 多种退避算法,如指数退避、固定步长、等差数列、自定义固定级别退避,以及不退避。

  • 可以设置最大重试间隔,如最大重试间隔不能超过1小时

  • 可以设置最大重试次数,如最大重试20次

  • 可以获取当前重试次数,即已经执行了几次

  • 可以获取从开始到当前流逝的全部时间

  • 可以获取从开始到指定次数流逝的全部时间

  • 标准化重试策略和接口,方便延迟调度平台使用

二 方案调研

2.1 Guava实现


    com.github.rholder
    guava-retrying
    1.0.6

详见: guava-retrying by rholder

2.2 Spring实现

 
      org.springframework  
      spring-core 
      4.3.17.RELEASE
 

public interface BackOff {

	/**
	 * Start a new back off execution.
	 * @return a fresh {@link BackOffExecution} ready to be used
	 */
	BackOffExecution start();

}
package org.springframework.util.backoff;

/**
 * Represent a particular back-off execution.
 *
 * 

Implementations do not need to be thread safe. * * @author Stephane Nicoll * @since 4.1 * @see BackOff */ public interface BackOffExecution { /** * Return value of {@link #nextBackOff()} that indicates that the operation * should not be retried. */ long STOP = -1; /** * Return the number of milliseconds to wait before retrying the operation * or {@link #STOP} ({@value #STOP}) to indicate that no further attempt * should be made for the operation. */ long nextBackOff(); }

三、我们的方案

2.2.1 基于Spring实现扩展

package com.xxx.commons.retry.backoff;

/**
 * @created 2021-12-20 4:26 PM
 * @description:
 */
public interface Backoff {

    /**
     * 开始
     *
     * @return
     */
    BackoffExecution start();

}
package com.xxx.commons.retry.backoff;

import java.util.concurrent.TimeUnit;

/**
 * @created 2021-12-20 4:26 PM
 * @description:
 */
public interface BackoffExecution {
    /**
     * 不再重试时返回的值
     */
    long STOP = -1;

    /**
     * The default time unit
     */
    TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS;

    /**
     * 下一次退避时间
     *
     * @return nextBackoff 下一次重试延迟时间(毫秒),不再重试时返回{@link #STOP} ({@value #STOP})
     */
    long nextBackoff();

    /**
     * 根据重试次数获取指定次数的退避时间
     *
     * @param attempt
     * @return
     */
    long getBackoff(int attempt);

    /**
     * 从开始到现在一共经过的时间
     *
     * @return
     */
    long getElapsedTime();

    /**
     * 从开始到指定次数一共经过的时间
     *
     * @param attempt
     * @return
     */
    long getElapsedTime(int attempt);

    /**
     * 当前尝试次数
     *
     * @return currentAttempt
     */
    int currentAttempt();

}

2.2.2 代码清单

接口与类

功能说明

备注说明

com.xxx.commons.retry.RetryService

重试服务工具类

com.xxx.commons.retry.backoff.Backoff

退避算法定义接口

com.xxx.commons.retry.backoff.BackoffExecution

退避算法执行接口

com.xxx.commons.retry.backoff.BackoffType

退避算法类型接口

com.xxx.commons.retry.backoff.BackoffTypeEnum

退避算法类型枚举类

com.xxx.commons.retry.backoff.CustomBackoff

自定义序列退避算法实现

com.xxx.commons.retry.backoff.ExponentialBackoff

指数退避算法实现

com.xxx.commons.retry.backoff.FixedBackoff

固定步长退避算法实现

com.xxx.commons.retry.backoff.GradualBackoff

等差数列退避算法实现

com.xxx.commons.retry.backoff.NoneBackoff

不退避算法实现

com.xxx.commons.retry.backoff.FibonacciBackoff

裴波那契退避算法实现

暂未实现

package com.xxx.commons.retry.backoff;

/**
 * @created 2021-12-31 3:24 PM
 * @description:
 */
public interface BackoffType {

    /**
     * 类型编码
     *
     * @return
     */
    int getCode();

    /**
     * 类型名字
     *
     * @return
     */
    String getName();

    /**
     * 类型说明
     *
     * @return
     */
    String getDesc();
}
package com.xxx.commons.retry.backoff;

/**
 * 重试补偿步长类型
 *
 * @created 2021-12-20 4:29 PM
 * @description:
 */
public enum BackoffTypeEnum implements BackoffType {
    /**
     * 无补偿
     **/
    NONE(0, "none", "无补偿"),

    /**
     * 固定步长
     **/
    FIXED(1, "fixed", "固定步长"),

    /**
     * 等差步长
     */
    GRADUAL(2, "gradual", "等差步长"),

    /**
     * 指数退避
     **/
    EXPONENTIAL(3, "exponential", "指数退避"),

    /**
     * 自定义步长
     */
    CUSTOM(4, "custom", "自定义步长"),

    ;

    private int code;
    private String name;
    private String desc;

    BackoffTypeEnum(int code, String name, String desc) {
        this.code = code;
        this.name = name;
        this.desc = desc;
    }

    public static BackoffTypeEnum typeOf(int code) {
        for (BackoffTypeEnum type : BackoffTypeEnum.values()) {
            if (type.code == code) {
                return type;
            }
        }
        return null;
    }

    @Override
    public int getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDesc() {
        return desc;
    }
}
package com.xxx.commons.retry;

/**
 * @created 2021-12-31 3:21 PM
 * @description:
 */

import com.xxx.commons.retry.backoff.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;

public class RetryService {

    static {
        Runtime.getRuntime().addShutdownHook(RetryServiceShutdownHook.instance());
    }

    /**
     * 重试线程池大小
     */
    public static final int RETRY_CORE_POOL_SIZE = Integer.getInteger(
            RetryConstants.DEFAULT_RETRY_SERVICE_SCHEDULED_THREAD_POOL_SIZE_KEY,
            RetryConstants.DEFAULT_RETRY_SERVICE_SCHEDULED_THREAD_POOL_DEFAULT_SIZE);

    /**
     * 重试服务线程池
     */
    final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(RETRY_CORE_POOL_SIZE);

    private static final RetryService instance = new RetryService();

    public static RetryService getInstance() {
        return instance;
    }

    private RetryService() {
        RetryServiceShutdownHook.instance().addExecutor(executor);
    }

    public ScheduledFuture retry(Callable callable, long delay) {
        return executor.schedule(callable, delay, BackoffExecution.DEFAULT_TIME_UNIT);
    }

    public ScheduledFuture retry(Runnable runnable, long delay) {
        return executor.schedule(runnable, delay, BackoffExecution.DEFAULT_TIME_UNIT);
    }

    public ScheduledFuture retry(Callable callable, long delay, TimeUnit unit) {
        return executor.schedule(callable, delay, unit);
    }

    public ScheduledFuture retry(Runnable runnable, long delay, TimeUnit unit) {
        return executor.schedule(runnable, delay, unit);
    }

    public interface RetryConstants {
        String DEFAULT_RETRY_SERVICE_SCHEDULED_THREAD_POOL_SIZE_KEY = "default.retry.service.scheduled.thread.pool.size";
        int DEFAULT_RETRY_SERVICE_SCHEDULED_THREAD_POOL_DEFAULT_SIZE = 8;
    }

    static class RetryServiceShutdownHook extends Thread {

        private final Logger logger = LoggerFactory.getLogger(this.getClass());

        private List executors = Collections.synchronizedList(new ArrayList());
        private final static RetryServiceShutdownHook instance = new RetryServiceShutdownHook();

        public static RetryServiceShutdownHook instance() {
            return instance;
        }

        private RetryServiceShutdownHook() {
        }

        public RetryServiceShutdownHook addExecutor(ExecutorService executor) {
            executors.add(executor);
            return this;
        }

        @Override
        public void run() {
            logger.info("retry service shutdown");

            for (int i = 0; i < executors.size(); i++) {
                ExecutorService executorService = executors.get(i);
                try {
                    if (!executorService.isShutdown()) {
                        executorService.shutdown();
                    }
                } catch (Exception e) {
                    logger.error("retry service shutdown executor service error", e.getMessage());
                }
            }
        }
    }


}

package com.xxx.commons.retry.backoff;


import com.xxx.commons.data.constant.CommonConstants;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * @created 2021-12-27 9:47 AM
 * @description: 自定义序列退避算法实现
 */
public class CustomBackoff implements Backoff, Serializable {

    private static final long serialVersionUID = 5677908306014056187L;

    public static final String DEFAULT_SEPARATOR = CommonConstants.SPACE_SEPARATOR;
    public static final String LEFT_BRACKET = "{";
    public static final String RIGHT_BRACKET = "}";

    /**
     * Constant value indicating an unlimited number of attempt.
     */
    public static final int UNLIMITED_ATTEMPTS = Integer.MAX_VALUE;

    /**
     * 默认退避时间序列
     * 示例1:1  2  3  10  10  10  30  60 120 180 600 1800 3600  3600  3600  3600  3600  3600
     * 示例2:1s 2s 3s 10s 10s 10s 30s 1m 2m  3m  10m 30m  1h    1h    1h    1h    1h    1h
     * 示例3:1  2  3  10{3}       30  60 120 180 600 1800 3600{6}
     * 示例4:1s 2s 3s 10s{3}      30s 1m 2m  3m  10m 30m  1h{6}
     */
    public static final String DEFAULT_SEQUENCE = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h{64}";


    /**
     * 自定义退避序列
     */
    private String sequence = DEFAULT_SEQUENCE;

    /**
     * 最大尝试次数
     */
    private int maxAttempt = UNLIMITED_ATTEMPTS;

    /**
     * 自定义序列分隔符
     */
    private String separator = DEFAULT_SEPARATOR;


    /**
     * Create an instance with an sequence with DEFAULT.
     */
    public CustomBackoff() {
    }

    /**
     * Create an instance.
     *
     * @param sequence the origin sequence
     */
    public CustomBackoff(String sequence) {
        this.sequence = sequence;
    }

    public CustomBackoff(String sequence, String separator) {
        this.sequence = sequence;
        this.separator = separator;
    }

    public CustomBackoff(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    public CustomBackoff(String sequence, int maxAttempt) {
        this.sequence = sequence;
        this.maxAttempt = maxAttempt;
    }

    /**
     * @param sequence   the origin sequence
     * @param maxAttempt the maximum number of attempt
     * @param separator  the sequence's separator
     */
    public CustomBackoff(String sequence, int maxAttempt, String separator) {
        this.sequence = sequence;
        this.maxAttempt = maxAttempt;
        this.separator = separator;
    }

    /**
     * Set the maximum number of attempt in milliseconds.
     */
    public void setMaxAttempt(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the maximum number of attempt in milliseconds.
     */
    public int getMaxAttempt() {
        return maxAttempt;
    }

    public String getSeparator() {
        return separator;
    }

    public void setSeparator(String separator) {
        this.separator = separator;
    }

    @Override
    public BackoffExecution start() {
        return new CustomBackoffExecution(this.sequence, this.maxAttempt, this.separator);
    }

    public static class CustomBackoffExecution implements BackoffExecution, Serializable {

        private static final long serialVersionUID = 2991013977130023823L;

        /**
         * 自定义退避序列
         */
        private String sequence;

        /**
         * 最大尝试次数
         */
        private int maxAttempt;

        /**
         * 自定义序列分隔符
         */
        private String separator;

        /**
         * 解析后的退避时间间隔列表
         */
        private List intervals;

        private long currentElapsedTime = 0;

        private int currentAttempt = 0;

        public CustomBackoffExecution() {
            this(DEFAULT_SEQUENCE, UNLIMITED_ATTEMPTS, DEFAULT_SEPARATOR);
        }

        public CustomBackoffExecution(String sequence, int maxAttempt, String separator) {
            this.sequence = sequence;
            this.maxAttempt = maxAttempt;
            this.separator = separator;
            this.parse();
        }

        private void parse() {
            intervals = new ArrayList<>();

            HashMap timeUnitTable = new HashMap();
            timeUnitTable.put("s", 1000L);
            timeUnitTable.put("m", 1000L * 60);
            timeUnitTable.put("h", 1000L * 60 * 60);
            timeUnitTable.put("d", 1000L * 60 * 60 * 24);

            String[] fields = sequence.split(separator);
            boolean completed = false;
            for (String field : fields) {
                field = field.trim();
                int count = 1;
                long multiple = 1;
                int leftBracket = field.indexOf(CustomBackoff.LEFT_BRACKET);
                int rightBracket = field.indexOf(CustomBackoff.RIGHT_BRACKET);
                if (leftBracket > 0 && rightBracket > 0) {
                    count = Integer.parseInt(field.substring(leftBracket + 1, rightBracket));
                    field = field.substring(0, leftBracket);
                }
                Long unit = timeUnitTable.get(String.valueOf(field.charAt(field.length() - 1)));
                if (unit != null) {
                    multiple = unit.longValue();
                    field = field.substring(0, field.length() - 1);
                }
                long timeInMills = Long.parseLong(field) * multiple;
                for (int i = 0; i < count; i++) {
                    if (this.intervals.size() < this.maxAttempt) {
                        this.intervals.add(timeInMills);
                    } else {
                        completed = true;
                    }
                }
                if (completed) {
                    break;
                }
            }
            // if maxAttempt greater than intervals's size, then set intervals's size as maxAttempt
            if (this.intervals.size() < this.maxAttempt) {
                this.maxAttempt = this.intervals.size();
            }
        }

        @Override
        public int currentAttempt() {
            return currentAttempt;
        }

        @Override
        public long getElapsedTime() {
            return this.currentElapsedTime;
        }

        @Override
        public long nextBackoff() {

            if (currentAttempt >= maxAttempt) {
                return STOP;
            }

            if (currentAttempt >= this.intervals.size()) {
                return STOP;
            }

            long nextInterval = this.intervals.get(this.currentAttempt);
            this.currentElapsedTime += nextInterval;
            this.currentAttempt += 1;
            return nextInterval;
        }

        @Override
        public void updateCurrentAttempt(int attempt) {
            if (attempt < 0) {
                this.currentAttempt = 0;
            } else if (attempt >= maxAttempt) {
                this.currentAttempt = maxAttempt;
            } else {
                this.currentAttempt = attempt;
            }
            this.currentElapsedTime = this.getElapsedTime(this.currentAttempt);
        }

        @Override
        public long getBackoff(int attempt) {

            if (attempt <= 0) {
                return 0L;
            }

            if (attempt - 1 >= maxAttempt) {
                return STOP;
            }

            if (attempt > this.intervals.size()) {
                return STOP;
            }

            return this.intervals.get(attempt - 1);
        }

        @Override
        public long getElapsedTime(int attempt) {
            if (attempt >= maxAttempt) {
                attempt = maxAttempt;
            }

            if (attempt >= this.intervals.size()) {
                attempt = this.intervals.size();
            }
            long elapsedTime = 0;
            for (int i = 0; i < attempt; i++) {
                elapsedTime += this.intervals.get(i);
            }

            return elapsedTime;
        }

        public String getSequence() {
            return sequence;
        }

        public void setSequence(String sequence) {
            this.sequence = sequence;
        }

        @Override
        public int getMaxAttempt() {
            return maxAttempt;
        }

        @Override
        public void setMaxAttempt(int maxAttempt) {
            this.maxAttempt = maxAttempt;
        }

        public String getSeparator() {
            return separator;
        }

        public void setSeparator(String separator) {
            this.separator = separator;
        }

        public List getIntervals() {
            return intervals;
        }

        public void setIntervals(List intervals) {
            this.intervals = intervals;
        }

        public long getCurrentElapsedTime() {
            return currentElapsedTime;
        }

        public void setCurrentElapsedTime(long currentElapsedTime) {
            this.currentElapsedTime = currentElapsedTime;
        }

        public int getCurrentAttempt() {
            return currentAttempt;
        }

        public void setCurrentAttempt(int currentAttempt) {
            this.currentAttempt = currentAttempt;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("CustomBackoff{");
            sb.append("intervals=").append(intervals);
            String attemptValue = (maxAttempt == Integer.MAX_VALUE ?
                    "unlimited" : String.valueOf(maxAttempt));
            sb.append(", maxAttempt=").append(attemptValue);
            sb.append(", currentElapsedTime=").append(currentElapsedTime);
            sb.append(", currentAttempt=").append(this.currentAttempt);
            sb.append('}');
            return sb.toString();
        }
    }

    public static void main(String[] args) {
        BackoffExecution backoffExecution = new CustomBackoff().start();
        System.out.println(backoffExecution.getBackoff(1));
        System.out.println(backoffExecution.getBackoff(2));
        System.out.println(backoffExecution.getBackoff(3));
        System.out.println(backoffExecution.getElapsedTime(1));
        System.out.println(backoffExecution.getElapsedTime(2));
        System.out.println(backoffExecution.getElapsedTime(3));
    }

}

package com.xxx.commons.retry.backoff;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @created 2021-12-20 4:31 PM
 * @description:
 */
public class ExponentialBackoff implements Backoff, Serializable {

    private static final long serialVersionUID = -3955367506662250971L;

    /**
     * The default initial interval.
     */
    public static final long DEFAULT_INITIAL_INTERVAL = 2000L;

    /**
     * The default multiplier (increases the interval by 50%).
     */
    public static final double DEFAULT_MULTIPLIER = 2;

    /**
     * The default maximum back off time.
     */
    public static final long DEFAULT_MAX_INTERVAL = 10 * 60 * 1000L;

    /**
     * Constant value indicating an unlimited number of attempt.
     */
    public static final int UNLIMITED_ATTEMPTS = Integer.MAX_VALUE;

    private long initialInterval = DEFAULT_INITIAL_INTERVAL;

    private double multiplier = DEFAULT_MULTIPLIER;

    private long maxInterval = DEFAULT_MAX_INTERVAL;

    private int maxAttempt = UNLIMITED_ATTEMPTS;

    /**
     * Create an instance with the default settings.
     *
     * @see #DEFAULT_INITIAL_INTERVAL
     * @see #DEFAULT_MULTIPLIER
     * @see #DEFAULT_MAX_INTERVAL
     */
    public ExponentialBackoff() {
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     */
    public ExponentialBackoff(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance with the supplied settings.
     *
     * @param initialInterval the initial interval in milliseconds
     */
    public ExponentialBackoff(long initialInterval) {
        this.initialInterval = initialInterval;
        this.checkInterval(this.initialInterval, this.maxInterval);
    }

    /**
     * Create an instance with the supplied settings.
     *
     * @param initialInterval the initial interval
     * @param timeUnit        the initial interval's time unit
     * @param multiplier      the multiplier (should be greater than or equal to 1)
     */
    public ExponentialBackoff(long initialInterval, TimeUnit timeUnit, double multiplier) {
        checkMultiplier(multiplier);
        this.multiplier = multiplier;
        this.initialInterval = BackoffExecution.DEFAULT_TIME_UNIT.convert(initialInterval, timeUnit);
        this.checkInterval(this.initialInterval, this.maxInterval);
    }

    public ExponentialBackoff(long initialInterval, long maxInterval, TimeUnit timeUnit, double multiplier) {
        checkMultiplier(multiplier);
        this.multiplier = multiplier;
        this.initialInterval = BackoffExecution.DEFAULT_TIME_UNIT.convert(initialInterval, timeUnit);
        this.maxInterval = BackoffExecution.DEFAULT_TIME_UNIT.convert(maxInterval, timeUnit);
        this.checkInterval(this.initialInterval, this.maxInterval);
    }

    /**
     * The initial interval in milliseconds.
     */
    public void setInitialInterval(long initialInterval) {
        this.initialInterval = initialInterval;
        this.checkInterval(this.initialInterval, this.maxInterval);
    }

    /**
     * Return the initial interval in milliseconds.
     */
    public long getInitialInterval() {
        return initialInterval;
    }

    /**
     * The value to multiply the current interval by for each retry attempt.
     */
    public void setMultiplier(double multiplier) {
        checkMultiplier(multiplier);
        this.multiplier = multiplier;
    }

    /**
     * Return the value to multiply the current interval by for each retry attempt.
     */
    public double getMultiplier() {
        return multiplier;
    }

    /**
     * The maximum back off time.
     */
    public void setMaxInterval(long maxInterval) {
        this.maxInterval = maxInterval;
    }

    /**
     * Return the maximum back off time.
     */
    public long getMaxInterval() {
        return maxInterval;
    }

    /**
     * Set the maximum number of attempt in milliseconds.
     */
    public void setMaxAttempt(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the maximum number of attempt in milliseconds.
     */
    public int getMaxAttempt() {
        return maxAttempt;
    }

    @Override
    public BackoffExecution start() {
        return new ExponentialBackoffExecution(initialInterval, maxInterval, multiplier, maxAttempt);
    }

    private void checkMultiplier(double multiplier) {
        if (multiplier < 1) {
            throw new IllegalArgumentException("Invalid multiplier '" + multiplier + "'. Should be equal" +
                    "or higher than 1. A multiplier of 1 is equivalent to a fixed interval");
        }
    }

    private void checkInterval(long initialInterval, long maxInterval) {
        if (initialInterval <= 0) {
            throw new IllegalArgumentException("Invalid initialInterval '" + initialInterval + "'. Should be higher" +
                    " than 0. A initialInterval of 0 is equivalent to no interval");
        }
        if (maxInterval <= initialInterval * getMultiplier()) {
            throw new IllegalArgumentException("Invalid maxInterval '" + maxInterval + "'. Should be higher" +
                    " than initialInterval * multiplier. A maxInterval equal or less than initialInterval * multiplier " +
                    "is equivalent to a fixed interval");
        }
    }

    public static class ExponentialBackoffExecution implements BackoffExecution, Serializable {

        private static final long serialVersionUID = 672748521748221970L;

        private long initialInterval;

        private double multiplier;

        private long maxInterval;

        private int maxAttempt;

        private long currentInterval = -1;

        private long currentElapsedTime = 0;

        private int currentAttempt = 0;

        public ExponentialBackoffExecution() {
            this(DEFAULT_INITIAL_INTERVAL, DEFAULT_MAX_INTERVAL, DEFAULT_MULTIPLIER, UNLIMITED_ATTEMPTS);
        }

        public ExponentialBackoffExecution(long initialInterval, long maxInterval, double multiplier, int maxAttempt) {
            this.initialInterval = initialInterval;
            this.maxInterval = maxInterval;
            this.multiplier = multiplier;
            this.maxAttempt = maxAttempt;
        }

        @Override
        public int currentAttempt() {
            return currentAttempt;
        }

        @Override
        public long getElapsedTime() {
            return this.currentElapsedTime;
        }

        @Override
        public long nextBackoff() {

            if (currentAttempt >= maxAttempt) {
                return STOP;
            }

            long nextInterval = computeNextInterval();
            this.currentElapsedTime += nextInterval;
            this.currentAttempt += 1;
            return nextInterval;
        }

        private long computeNextInterval() {
            long maxInterval = this.maxInterval;
            if (this.currentInterval >= maxInterval) {
                return maxInterval;
            } else if (this.currentInterval < 0) {
                this.currentInterval = this.initialInterval;
            } else {
                this.currentInterval = multiplyInterval(this.currentInterval, maxInterval);
            }
            return this.currentInterval;
        }

        private long multiplyInterval(long currentInterval, long maxInterval) {
            long interval = currentInterval;
            interval *= this.multiplier;
            return (interval > maxInterval ? maxInterval : interval);
        }

        @Override
        public void updateCurrentAttempt(int attempt) {
            if (attempt < 0) {
                this.currentAttempt = 0;
            } else if (attempt >= maxAttempt) {
                this.currentAttempt = maxAttempt;
            } else {
                this.currentAttempt = attempt;
            }
            this.currentElapsedTime = this.getElapsedTime(this.currentAttempt);
        }

        @Override
        public long getBackoff(int attempt) {
            if (attempt <= 0) {
                return 0L;
            }

            if (attempt - 1 >= maxAttempt) {
                return STOP;
            }

            long interval = this.initialInterval;
            long maxInterval = this.maxInterval;
            for (long i = 1; i < attempt; i++) {
                interval = multiplyInterval(interval, maxInterval);
                if (interval >= maxInterval) {
                    break;
                }
            }
            return interval;
        }

        @Override
        public long getElapsedTime(int attempt) {
            if (attempt >= maxAttempt) {
                attempt = maxAttempt;
            }
            long interval = this.initialInterval;
            long maxInterval = this.maxInterval;
            long elapsedTime = 0;
            if (attempt <= 0) {
                return elapsedTime;
            } else {
                elapsedTime = interval;
            }

            for (long i = 1; i < attempt; i++) {
                if (interval < maxInterval) {
                    interval = multiplyInterval(interval, maxInterval);
                }
                elapsedTime += interval;
            }
            return elapsedTime;
        }

        public long getInitialInterval() {
            return initialInterval;
        }

        public void setInitialInterval(long initialInterval) {
            this.initialInterval = initialInterval;
        }

        public double getMultiplier() {
            return multiplier;
        }

        public void setMultiplier(double multiplier) {
            this.multiplier = multiplier;
        }

        public long getMaxInterval() {
            return maxInterval;
        }

        public void setMaxInterval(long maxInterval) {
            this.maxInterval = maxInterval;
        }

        @Override
        public int getMaxAttempt() {
            return maxAttempt;
        }
        @Override
        public void setMaxAttempt(int maxAttempt) {
            this.maxAttempt = maxAttempt;
        }

        public long getCurrentInterval() {
            return currentInterval;
        }

        public void setCurrentInterval(long currentInterval) {
            this.currentInterval = currentInterval;
        }

        public long getCurrentElapsedTime() {
            return currentElapsedTime;
        }

        public void setCurrentElapsedTime(long currentElapsedTime) {
            this.currentElapsedTime = currentElapsedTime;
        }

        public int getCurrentAttempt() {
            return currentAttempt;
        }

        public void setCurrentAttempt(int currentAttempt) {
            this.currentAttempt = currentAttempt;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("ExponentialBackOff{");
            sb.append("currentInterval=").append(this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms");
            sb.append(", multiplier=").append(this.multiplier);
            sb.append('}');
            return sb.toString();
        }
    }

}
package com.xxx.commons.retry.backoff;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @created 2021-12-20 4:32 PM
 * @description:
 */
public class FixedBackoff implements Backoff, Serializable {

    private static final long serialVersionUID = 2511140811757045231L;

    /**
     * The default recovery interval: 5000 ms = 5 seconds.
     */
    public static final long DEFAULT_INTERVAL = 5000L;

    /**
     * Constant value indicating an unlimited number of attempt.
     */
    public static final int UNLIMITED_ATTEMPTS = Integer.MAX_VALUE;

    private long interval = DEFAULT_INTERVAL;

    private int maxAttempt = UNLIMITED_ATTEMPTS;


    /**
     * Create an instance with an interval of {@value #DEFAULT_INTERVAL}
     * ms and an unlimited number of attempt.
     */
    public FixedBackoff() {
    }


    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     */
    public FixedBackoff(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param interval the interval between two attempt
     * @param timeUnit the time unit of interval
     */
    public FixedBackoff(long interval, TimeUnit timeUnit) {
        this.interval = BackoffExecution.DEFAULT_TIME_UNIT.convert(interval, timeUnit);
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     * @param interval   the interval between two attempt
     */
    public FixedBackoff(int maxAttempt, long interval) {
        this.interval = interval;
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     * @param interval   the interval between two attempt
     * @param timeUnit   the time unit of interval
     */
    public FixedBackoff(int maxAttempt, long interval, TimeUnit timeUnit) {
        this.interval = BackoffExecution.DEFAULT_TIME_UNIT.convert(interval, timeUnit);
        this.maxAttempt = maxAttempt;
    }

    /**
     * Set the interval between two attempt in milliseconds.
     */
    public void setInterval(long interval) {
        this.interval = interval;
    }

    /**
     * Return the interval between two attempt in milliseconds.
     */
    public long getInterval() {
        return interval;
    }

    /**
     * Set the maximum number of attempt in milliseconds.
     */
    public void setMaxAttempt(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the maximum number of attempt in milliseconds.
     */
    public int getMaxAttempt() {
        return maxAttempt;
    }


    @Override
    public BackoffExecution start() {
        return new FixedBackoffExecution(interval, maxAttempt);
    }


    public static class FixedBackoffExecution implements BackoffExecution, Serializable {

        private static final long serialVersionUID = 1656175761226107038L;

        private long interval;

        private int maxAttempt;

        private int currentAttempt = 0;
        private long currentElapsedTime = 0;

        public FixedBackoffExecution() {
            this(DEFAULT_INTERVAL, UNLIMITED_ATTEMPTS);
        }

        public FixedBackoffExecution(long interval, int maxAttempt) {
            this.interval = interval;
            this.maxAttempt = maxAttempt;
        }

        public long getInterval() {
            return interval;
        }

        public void setInterval(long interval) {
            this.interval = interval;
        }

        @Override
        public int getMaxAttempt() {
            return maxAttempt;
        }
        @Override
        public void setMaxAttempt(int maxAttempt) {
            this.maxAttempt = maxAttempt;
        }

        public int getCurrentAttempt() {
            return currentAttempt;
        }

        public void setCurrentAttempt(int currentAttempt) {
            this.currentAttempt = currentAttempt;
        }

        public long getCurrentElapsedTime() {
            return currentElapsedTime;
        }

        public void setCurrentElapsedTime(long currentElapsedTime) {
            this.currentElapsedTime = currentElapsedTime;
        }

        @Override
        public int currentAttempt() {
            return currentAttempt;
        }

        @Override
        public long getElapsedTime() {
            return this.currentElapsedTime;
        }

        @Override
        public long nextBackoff() {
            if (this.currentAttempt < this.maxAttempt) {
                long nextInterval = this.interval;
                this.currentElapsedTime += nextInterval;
                this.currentAttempt++;
                return nextInterval;
            } else {
                return STOP;
            }
        }

        @Override
        public void updateCurrentAttempt(int attempt) {
            if (attempt < 0) {
                this.currentAttempt = 0;
            } else if (attempt >= maxAttempt) {
                this.currentAttempt = maxAttempt;
            } else {
                this.currentAttempt = attempt;
            }
            this.currentElapsedTime = this.getElapsedTime(this.currentAttempt);
        }

        @Override
        public long getBackoff(int attempt) {
            if (attempt <= 0) {
                return 0L;
            }

            if (attempt - 1 >= maxAttempt) {
                return STOP;
            }

            return this.interval;
        }

        @Override
        public long getElapsedTime(int attempt) {
            return this.interval * attempt;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("FixedBackoff{");
            sb.append("interval=").append(this.interval);
            String attemptValue = (this.maxAttempt == Integer.MAX_VALUE ?
                    "unlimited" : String.valueOf(this.maxAttempt));
            sb.append(", maxAttempt=").append(attemptValue);
            sb.append(", currentAttempt=").append(this.currentAttempt);
            sb.append('}');
            return sb.toString();
        }
    }

}

package com.xxx.commons.retry.backoff;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

import static com.xxx.commons.retry.backoff.BackoffExecution.DEFAULT_TIME_UNIT;


/**
 * @created 2021-12-20 4:33 PM
 * @description:
 */
public class GradualBackoff implements Backoff, Serializable {

    private static final long serialVersionUID = -4082336395861849848L;

    /**
     * The default initial interval.
     */
    public static final long DEFAULT_INITIAL_INTERVAL = 2000L;

    /**
     * The default recovery interval: 3000 ms = 3 seconds.
     */
    public static final long DEFAULT_INTERVAL = 3000L;

    /**
     * Constant value indicating an unlimited number of attempt.
     */
    public static final int UNLIMITED_ATTEMPTS = Integer.MAX_VALUE;

    private long initialInterval = DEFAULT_INITIAL_INTERVAL;

    private long interval = DEFAULT_INTERVAL;

    private int maxAttempt = UNLIMITED_ATTEMPTS;


    /**
     * Create an instance with an interval of {@value #DEFAULT_INTERVAL}
     * ms and an unlimited number of attempt.
     */
    public GradualBackoff() {
    }


    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     */
    public GradualBackoff(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param interval the interval between two attempt
     * @param timeUnit the time unit of interval
     */
    public GradualBackoff(long initialInterval, long interval, TimeUnit timeUnit) {
        this.initialInterval = DEFAULT_TIME_UNIT.convert(initialInterval, timeUnit);
        this.interval = DEFAULT_TIME_UNIT.convert(interval, timeUnit);
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     * @param interval   the interval between two attempt
     */
    public GradualBackoff(int maxAttempt, long initialInterval, long interval) {
        this.initialInterval = initialInterval;
        this.interval = interval;
        this.maxAttempt = maxAttempt;
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     * @param interval   the interval between two attempt
     * @param timeUnit   the time unit of interval
     */
    public GradualBackoff(int maxAttempt, long initialInterval, long interval, TimeUnit timeUnit) {
        this.initialInterval = DEFAULT_TIME_UNIT.convert(initialInterval, timeUnit);
        this.interval = DEFAULT_TIME_UNIT.convert(interval, timeUnit);
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the interval of first attempt in milliseconds.
     *
     * @return
     */
    public long getInitialInterval() {
        return initialInterval;
    }

    /**
     * Set the interval of first attempt in milliseconds.
     */
    public void setInitialInterval(long initialInterval) {
        this.initialInterval = initialInterval;
    }

    /**
     * Set the interval between two attempt in milliseconds.
     */
    public void setInterval(long interval) {
        this.interval = interval;
    }

    /**
     * Return the interval between two attempt in milliseconds.
     */
    public long getInterval() {
        return interval;
    }

    /**
     * Set the maximum number of attempt in milliseconds.
     */
    public void setMaxAttempt(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the maximum number of attempt in milliseconds.
     */
    public int getMaxAttempt() {
        return maxAttempt;
    }

    @Override
    public BackoffExecution start() {
        return new GradualBackoffExecution(initialInterval, interval, maxAttempt);
    }

    public static class GradualBackoffExecution implements BackoffExecution, Serializable {

        private static final long serialVersionUID = 6691539566098442985L;

        private long initialInterval;

        private long interval;

        private int maxAttempt;

        private int currentAttempt = 0;
        private long currentElapsedTime = 0;

        public GradualBackoffExecution() {
            this(DEFAULT_INITIAL_INTERVAL, DEFAULT_INTERVAL, UNLIMITED_ATTEMPTS);
        }

        public GradualBackoffExecution(long initialInterval, long interval, int maxAttempt) {
            this.initialInterval = initialInterval;
            this.interval = interval;
            this.maxAttempt = maxAttempt;
        }

        @Override
        public int currentAttempt() {
            return currentAttempt;
        }

        @Override
        public long getElapsedTime() {
            return this.currentElapsedTime;
        }

        @Override
        public long nextBackoff() {
            if (this.currentAttempt < this.maxAttempt) {
                long nextInterval = this.currentAttempt == 0 ? this.initialInterval : this.interval;
                this.currentElapsedTime += nextInterval;
                this.currentAttempt++;
                return nextInterval;
            } else {
                return STOP;
            }
        }

        @Override
        public void updateCurrentAttempt(int attempt) {
            if (attempt < 0) {
                this.currentAttempt = 0;
            } else if (attempt >= maxAttempt) {
                this.currentAttempt = maxAttempt;
            } else {
                this.currentAttempt = attempt;
            }
            this.currentElapsedTime = this.getElapsedTime(this.currentAttempt);
        }

        @Override
        public long getBackoff(int attempt) {
            if (attempt <= 0) {
                return 0L;
            }

            if (attempt - 1 >= maxAttempt) {
                return STOP;
            }

            return attempt == 1 ? this.initialInterval : this.interval;
        }

        @Override
        public long getElapsedTime(int attempt) {
            return attempt == 1 ? this.initialInterval : this.initialInterval + this.interval * (attempt - 1);
        }

        public long getInitialInterval() {
            return initialInterval;
        }

        public void setInitialInterval(long initialInterval) {
            this.initialInterval = initialInterval;
        }

        public long getInterval() {
            return interval;
        }

        public void setInterval(long interval) {
            this.interval = interval;
        }

        @Override
        public int getMaxAttempt() {
            return maxAttempt;
        }
        @Override
        public void setMaxAttempt(int maxAttempt) {
            this.maxAttempt = maxAttempt;
        }

        public int getCurrentAttempt() {
            return currentAttempt;
        }

        public void setCurrentAttempt(int currentAttempt) {
            this.currentAttempt = currentAttempt;
        }

        public long getCurrentElapsedTime() {
            return currentElapsedTime;
        }

        public void setCurrentElapsedTime(long currentElapsedTime) {
            this.currentElapsedTime = currentElapsedTime;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("GradualBackoff{");
            sb.append("initialInterval=").append(this.initialInterval);
            sb.append("interval=").append(this.interval);
            String attemptValue = (this.maxAttempt == Integer.MAX_VALUE ?
                    "unlimited" : String.valueOf(this.maxAttempt));
            sb.append(", maxAttempt=").append(attemptValue);
            sb.append(", currentAttempt=").append(this.currentAttempt);
            sb.append('}');
            return sb.toString();
        }
    }

}

package com.xxx.commons.retry.backoff;

import java.io.Serializable;

/**
 * @created 2021-12-20 4:34 PM
 * @description:
 */
public class NoneBackoff implements Backoff, Serializable {

    private static final long serialVersionUID = 8227959838869099892L;

    /**
     * The default recovery interval: 0 ms = 0 seconds.
     */
    public static final long DEFAULT_INTERVAL = 0L;

    /**
     * Constant value indicating an unlimited number of attempt.
     */
    public static final int UNLIMITED_ATTEMPTS = Integer.MAX_VALUE;

    private long interval = DEFAULT_INTERVAL;

    private int maxAttempt = UNLIMITED_ATTEMPTS;


    /**
     * Create an instance with an interval of {@value #DEFAULT_INTERVAL}
     * ms and an unlimited number of attempt.
     */
    public NoneBackoff() {
    }

    /**
     * Create an instance.
     *
     * @param maxAttempt the maximum number of attempt
     */
    public NoneBackoff(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    /**
     * Return the interval between two attempt in milliseconds.
     */
    public long getInterval() {
        return interval;
    }

    /**
     * Return the maximum number of attempt in milliseconds.
     */
    public int getMaxAttempt() {
        return maxAttempt;
    }

    /**
     * Set the maximum number of attempt in milliseconds.
     */
    public void setMaxAttempt(int maxAttempt) {
        this.maxAttempt = maxAttempt;
    }

    @Override
    public BackoffExecution start() {
        return new NoneBackoffExecution(interval, maxAttempt);
    }


    public static class NoneBackoffExecution implements BackoffExecution, Serializable {

        private static final long serialVersionUID = 2453613900451888865L;

        private long interval;

        private int maxAttempt;

        private int currentAttempt = 0;
        private long currentElapsedTime = 0;

        public NoneBackoffExecution() {
            this(DEFAULT_INTERVAL, UNLIMITED_ATTEMPTS);
        }

        public NoneBackoffExecution(long interval, int maxAttempt) {
            this.interval = interval;
            this.maxAttempt = maxAttempt;
        }

        @Override
        public int currentAttempt() {
            return currentAttempt;
        }

        @Override
        public long getElapsedTime() {
            return this.currentElapsedTime;
        }

        @Override
        public long nextBackoff() {
            if (this.currentAttempt < this.maxAttempt) {
                long nextInterval = this.interval;
                this.currentElapsedTime += nextInterval;
                this.currentAttempt++;
                return nextInterval;
            } else {
                return STOP;
            }
        }

        @Override
        public void updateCurrentAttempt(int attempt) {
            if (attempt < 0) {
                this.currentAttempt = 0;
            } else if (attempt >= maxAttempt) {
                this.currentAttempt = maxAttempt;
            } else {
                this.currentAttempt = attempt;
            }
            this.currentElapsedTime = this.getElapsedTime(this.currentAttempt);
        }

        @Override
        public long getBackoff(int attempt) {
            if (attempt <= 0) {
                return 0L;
            }

            if (attempt - 1 >= maxAttempt) {
                return STOP;
            }

            return this.interval;
        }

        @Override
        public long getElapsedTime(int attempt) {
            return this.interval * attempt;
        }

        public long getInterval() {
            return interval;
        }

        public void setInterval(long interval) {
            this.interval = interval;
        }

        @Override
        public int getMaxAttempt() {
            return maxAttempt;
        }
        @Override
        public void setMaxAttempt(int maxAttempt) {
            this.maxAttempt = maxAttempt;
        }

        public int getCurrentAttempt() {
            return currentAttempt;
        }

        public void setCurrentAttempt(int currentAttempt) {
            this.currentAttempt = currentAttempt;
        }

        public long getCurrentElapsedTime() {
            return currentElapsedTime;
        }

        public void setCurrentElapsedTime(long currentElapsedTime) {
            this.currentElapsedTime = currentElapsedTime;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("NoneBackoff{");
            sb.append("interval=").append(this.interval);
            sb.append(", maxAttempt=").append(this.maxAttempt);
            sb.append('}');
            return sb.toString();
        }
    }
}

2.2.3 业务使用示例

网关使用示例:

  • 添加依赖

    
    
        com.xxx.arch
        xxx-commons-recipes
        1.0.8 
        
    
  • 初始化

@Configuration
@AutoConfigureAfter(OnsConfig.class)
public class LinkBean {
    
    public static final String MESSAGE_DELAY_LEVEL = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
    
    @Bean
    public BackoffExecution backoffExecution() {
        return new CustomBackoff(MESSAGE_DELAY_LEVEL, MAX_RETRY_COUNT).start();
    }
    
    ...
}
  • 具体使用

@Slf4j
public class RetryFilterFactory implements FilterFactory {
    private Producer producer;
    private BackoffExecution backoffExecution;

    public RetryFilterFactory(Producer producer, BackoffExecution backoffExecution) {
        this.producer = producer;
        this.backoffExecution = backoffExecution;
    }

    @Override
    public String name() {
        return "retry";
    }

    @Override
    public Filter apply(Properties properties) {

        return (context, chain) -> {

            int retry = context.getRetryCount();
            retry++;

            long retryTime = context.getAttributeOrDefault(LinkContext.FAILOVER_RETRY_TIME_ATTRIBUTE, 0L);
            context.getAttributes().put(LinkContext.FAILOVER_RETRY_TIME_ATTRIBUTE,
                retryTime + backoffExecution.getBackoff(retry));

            try {
                producer.send(context);
            } catch (Exception exp) {
                log.error("master send context: {}, error:", context, exp);
                throw new LinkIOException("send master mq error");
            }
            chain.filter(context);
        };
    }
}

四、未来展望

希望大家一起来共建xxx-commons组件,也希望xxx-commons组件能够成为内部基础组件被广泛使用,提升系统和业务开发效率,并提升开发质量。

你可能感兴趣的:(java)