golang 的可重构弹性模式。 创建重试器需要两个参数: 重试间隔的时间(隐含重试次数) 决定重试哪些错误的分类器
仓库给的例子:
代码语言:javascript复制go 代码解读复制代码r := retrier.New(retrier.ConstantBackoff(3, 100*time.Millisecond), nil)
err := r.Run(func() error {
// do some work
return nil
})
if err != nil {
// handle the case where the work failed three times
}
创建重试器时,传入了两个参数,一个是重试时间的间隔(它是一个 time.Duration 类型的数组,数组的长度就是它隐含的重试次数),另一个是分类器,可以决定哪些错误需要重试,哪些错误不需要重试。
重试器的结构体
代码语言:javascript复制go 代码解读复制代码// Retrier implements the "retriable" resiliency pattern, abstracting out the process of retrying a failed action
// a certain number of times with an optional back-off between each retry.
// Retrier 实现了 "可重试 "弹性模式,将重试失败操作的过程抽象为
// 重试一定次数,每次重试之间可选择后退。
type Retrier struct {
// 重试时间的间隔
backoff []time.Duration
// 分类器
class Classifier
// 基数
jitter float64
// 随机数种子
rand *rand.Rand
// 计算休眠时间的锁
randMu sync.Mutex
}
新建一个重试器的函数
代码语言:javascript复制go 代码解读复制代码// New constructs a Retrier with the given backoff pattern and classifier. The length of the backoff pattern
// indicates how many times an action will be retried, and the value at each index indicates the amount of time
// waited before each subsequent retry. The classifier is used to determine which errors should be retried and
// which should cause the retrier to fail fast. The DefaultClassifier is used if nil is passed.
// New 使用给定的后退模式和分类器构建一个 Retrier。后退模式的长度
// 每个索引的值表示每次重试前等待的时间。
// 每次重试前等待的时间。分类器用于确定哪些错误应重试,哪些错误应导致重试。
// 哪些错误会导致重试快速失败。如果传入的是 nil,则使用 DefaultClassifier。
func New(backoff []time.Duration, class Classifier) *Retrier {
// 如果分类器为 nil,则使用默认的分类器
if class == nil {
class = DefaultClassifier{}
}
return &Retrier{
backoff: backoff,
class: class,
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
如果传入的分类器为 nil,则使用默认的分类器
有三种不同的分类器
- 默认分类器
- 白名单分类器
- 黑名单分类器
默认分类器以最简单的方式对错误进行分类。如果错误为 nil,则返回 Succeed,否则返回 Retry
WhitelistClassifier 根据白名单对错误进行分类。如果错误为 nil,则返回 Succeed;如果错误在白名单中,则返回 Retry;否则,它将返回 Fail。
BlacklistClassifier 根据黑名单对错误进行分类。如果错误为 nil,则返回 Succeed;如果错误在黑名单中,则返回 Fail;否则,它将返回 Retry。
重试器的执行有两个函数
一个是执行时,不用传入上下文字段的,实际执行还是调用了需要传入上下文字段的 RunCtx函数,只是传了个非 nil 的空 Context
代码语言:javascript复制go 代码解读复制代码// Run executes the given work function by executing RunCtx without context.Context.
func (r *Retrier) Run(work func() error) error {
return r.RunCtx(context.Background(), func(ctx context.Context) error {
// never use ctx
return work()
})
}
一个是执行时,需要传入上下文字段的
代码语言:javascript复制go 代码解读复制代码// RunCtx executes the given work function, then classifies its return value based on the classifier used
// to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is
// returned to the caller. If the result is Retry, then Run sleeps according to the its backoff policy
// before retrying. If the total number of retries is exceeded then the return value of the work function
// is returned to the caller regardless.
// 分类器对其返回值进行分类。
// 构造 Retrier 所使用的分类器对其返回值进行分类。如果结果是 "成功 "或 "失败",工作函数的返回值将
// 返回给调用者。如果结果是重试,运行将根据其后退策略休眠,然后再重试。
// 在重试之前休眠。如果超过了重试的总次数,则工作函数的返回值
// 返回给调用者。
func (r *Retrier) RunCtx(ctx context.Context, work func(ctx context.Context) error) error {
// 刚开始重试次数为 0
retries := 0
for {
// 执行工作函数(即我们想要进行处理的逻辑)
ret := work(ctx)
// 分类器根据返回值,判断是否需要重试
switch r.class.Classify(ret) {
case Succeed, Fail:
return ret
case Retry:
// 如果重试次数大于等于隐含的重试次数,返回工作函数的返回值
if retries >= len(r.backoff) {
return ret
}
// 如果重试次数小于隐含的重试次数,根据当前已重试的次数,计算休眠的时间
timeout := time.After(r.calcSleep(retries))
// 执行休眠函数
if err := r.sleep(ctx, timeout); err != nil {
return err
}
retries
}
}
}
计算休眠时间的函数
这里不理解的是为什么要加锁,看了测试用例,有可能会并发执行 Run 函��,但实际有场景会用得上吗?
这里还有一个基数的作为休息时间的随机性种子,可以通过 SetJitter
函数设置,jitter 的范围在 [0,1],否则设置无效,设置了基数后,回退时间在一定的范围内,比如你设置了基数为 0.25, backoff[i] 为 10 * time.Millisecond,那么这时的回退时间在 (7500 * time.Microsecond,12500*time.Microsecond)的范围内
go 代码解读复制代码func (r *Retrier) calcSleep(i int) time.Duration {
// lock unsafe rand prng
r.randMu.Lock()
defer r.randMu.Unlock()
// take a random float in the range (-r.jitter, r.jitter) and multiply it by the base amount
return r.backoff[i] time.Duration(((r.rand.Float64()*2)-1)*r.jitter*float64(r.backoff[i]))
}
休眠函数
从代码可以看出,如果到达时间范围了会返回 nil,然后 RunCtx 函数增加重试次数,继续重试,如果传入的上下文有带超时时长,这时候超时时间到了,返回错误,RunCtx 直接退出,这点也就是使用 Run 和 RunCtx 函数的唯一区别
代码语言:javascript复制go 代码解读复制代码func (r *Retrier) sleep(ctx context.Context, t <-chan time.Time) error {
select {
case <-t:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
创建重试器,传递超时时长时,有三个辅助函数
- ConstantBackoff
- ConstantBackoff 生成一个简单的回退策略,即重试“n”次,并在每次重试后等待相同的时间。
- ExponentialBackoff
- ExponentialBackoff 生成一个简单的回退策略,即重试 'n' 次,并将每次重试后等待的时间加倍。
- LimitedExponentialBackoff
- LimitedExponentialBackoff 生成一个简单的回退策略,即重试 'n' 次,并将每次重试后等待的时间加倍。如果回退达到 'limitAmount' ,则此后回退将填充 'limitAmount' 。