V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
Nazz
V2EX  ›  Go 编程语言

代码分享: 延迟时间指数递增的重试机制实现

  •  
  •   Nazz · 104 天前 · 1399 次点击
    这是一个创建于 104 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在工作中, 我们经常会碰到重试的需求, 很多人会用定时任务来实现. 如果有更高的要求, 可以尝试下"延迟时间指数递增"的策略, 相比定时任务, 更加节省 CPU 和数据库资源.

    为简化逻辑, 代码中没有使用 RedisMySQL 维护失败次数.

    Quick Start

    package main
    
    import (
    	"errors"
    	"fmt"
    	"github.com/lxzan/memorycache"
    	"math"
    	"sync/atomic"
    	"time"
    )
    
    const (
    	MaxRetryTimes = 17              // 最大重试次数
    	FirstDelay    = 5 * time.Second // 初始延迟
    )
    
    func NewRetryer() *Retryer {
    	return &Retryer{
    		mc: memorycache.New[string, string](
    			memorycache.WithBucketNum(16),
    			memorycache.WithBucketSize(16, math.MaxInt64),
    			memorycache.WithInterval(time.Second, 5*time.Second),
    		),
    	}
    }
    
    type Retryer struct {
    	mc *memorycache.MemoryCache[string, string]
    }
    
    func (c *Retryer) Add(jobId string, jobFunc func() error) {
    	// 检查任务是否已存在
    	if _, exist := c.mc.GetOrCreate(jobId, jobId, -1); exist {
    		return
    	}
    
    	var callback = func(element *memorycache.Element[string, string], reason memorycache.Reason) {
    		// 只处理过期触发的回调
    		if reason != memorycache.ReasonExpired {
    			return
    		}
    
    		// 回调函数必须是非阻塞的; 酌情使用任务队列, 控制最大并发.
    		go func(id string) {
    			if err := jobFunc(); err == nil {
    				c.delete(id)
    			}
    		}(element.Value)
    	}
    
    	// 注册延迟及回调函数
    	var failedTimes = 1
    	var delay, delta = FirstDelay, FirstDelay
    	for i := 1; i <= MaxRetryTimes; i++ {
    		if i >= failedTimes {
    			var key = fmt.Sprintf("%s-%d", jobId, i)
    			c.mc.SetWithCallback(key, jobId, delay, callback)
    		}
    		delta *= 2
    		delay += delta
    	}
    }
    
    // 任务执行成功, 删除剩余的重试任务
    func (c *Retryer) delete(jobId string) {
    	for i := 1; i <= MaxRetryTimes; i++ {
    		var key = fmt.Sprintf("%s-%d", jobId, i)
    		c.mc.Delete(key)
    	}
    	c.mc.Delete(jobId)
    }
    
    // 模拟一个任务
    func newJob() (jobId string, jobFunc func() error) {
    	var counter = new(atomic.Int64)
    	return "1", func() error {
    		defer counter.Add(1)
    		serial := counter.Load()
    		if serial < 5 {
    			fmt.Printf("serial=%d, t=%d, success=false\n", serial, time.Now().Unix())
    			return errors.New("test")
    		}
    		fmt.Printf("serial=%d, t=%d, success=true\n", serial, time.Now().Unix())
    		return nil
    	}
    }
    
    func main() {
    	retryer := NewRetryer()
    	jobId, jobFunc := newJob()
    	if err := jobFunc(); err != nil {
    		retryer.Add(jobId, jobFunc)
    	}
    	select {}
    }
    

    Output

    serial=0, t=1705196714, success=false
    serial=1, t=1705196719, success=false
    serial=2, t=1705196729, success=false
    serial=3, t=1705196749, success=false
    serial=4, t=1705196789, success=false
    serial=5, t=1705196869, success=true
    
    8 条回复    2024-01-22 20:27:27 +08:00
    Guaderxx
        1
    Guaderxx  
       104 天前
    指数退避么
    Nazz
        2
    Nazz  
    OP
       104 天前 via Android
    @Guaderxx 单调递增
    diagnostics
        3
    diagnostics  
       104 天前   ❤️ 2
    这有啥好分享的,又不是大学生
    Opportunity
        4
    Opportunity  
       104 天前   ❤️ 1
    现在连重试都需要上 redis 了吗。。
    zhangzEric
        5
    zhangzEric  
       104 天前 via iPhone
    @Opportunity 本地重试不需要,分布式异步重试就得有地方存储了
    Nazz
        6
    Nazz  
    OP
       104 天前 via Android
    @Opportunity 我就用 redis 保存下失败次数,不想用 mysql
    shellcodecow
        7
    shellcodecow  
       96 天前
    设计上
    建议独立一个服务做调度功能
    比如延迟调度、循环调度、递增调度 通过 GRPC 进行 timeout 或者 callback
    Nazz
        8
    Nazz  
    OP
       96 天前 via Android
    @shellcodecow 分布式是该这样
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1147 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:04 · PVG 02:04 · LAX 11:04 · JFK 14:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.