这篇“Golang分布式应用定时任务如何实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Golang分布式应用定时任务如何实现”文章吧。
最小堆是一种特殊的完全二叉树,任意非叶子节点的值不大于其子节点,如图
通过最小堆,根据任务最近执行时间键堆,每次取堆顶元素即最近需要执行的任务,设置timer定时器,到期后触发任务执行。由于堆的特性每次调整的时间复杂度为O(lgN),相较于普通队列性能更快。
在container/heap
中已经实现操作堆的相关函数,我们只需要实现定期任务核心逻辑即可。
// 运行 func (c *Cron) Run() error { // 设置cron已启动,atomic.Bool来保证并发安全 c.started.Store(true) // 主循环 for { // 如果停止则退出 if !c.started.Load() { break } c.runTask() } return nil } // 核心逻辑 func (c *Cron) runTask() { now := time.Now() duration := infTime // 获取堆顶元素 task, ok := c.tasks.Peek() if ok { // 如果已删除则弹出 if !c.set.Has(task.Name()) { c.tasks.Pop() return } // 计算于当前时间查找,设置定时器 if task.next.After(now) { duration = task.next.Sub(now) } else { duration = 0 } } timer := time.NewTimer(duration) defer timer.Stop() // 当有新元素插入直接返回,防止新元素执行时间小于当前堆顶元素 select { case <-c.new: return case <-timer.C: } // 弹出任务,执行 go task.Exec() // 计算下次执行时间,如果为0说明任务已结束,否则重新入堆 task.next = task.Next(time.Now()) if task.next.IsZero() { c.set.Delete(task.Name()) } else { c.tasks.Push(task) } }
主要逻辑可总结为:
将任务按照下次执行时间建最小堆
每次取堆顶任务,设置定时器
如果中间有新加入任务,转入步骤2
定时器到期后执行任务
再次取下个任务,转入步骤2,依次执行
另一种实现Cron的方式是时间轮,时间轮通过一个环形队列,每个插槽放入需要到期执行的任务,按照固定间隔转动时间轮,取插槽中任务列表执行,如图所示:
时间轮可看作一个表盘,如图中时间间隔为1秒,总共60个格子,如果任务在3秒后执行则放为插槽3,每秒转动次取插槽上所有任务执行。
如果执行时间超过最大插槽,比如有个任务需要63秒后执行(超过了最大格子刻度),一般可以通过多层时间轮,或者设置一个额外变量圈数,只执行圈数为0的任务。
时间轮插入的时间复杂度为O(1),获取任务列表复杂度为O(1),执行列表最差为O(n)。对比最小堆,时间轮插入删除元素更快。
核心代码如下:
// 定义 type TimeWheel struct { interval time.Duration // 触发间隔 slots int // 总插槽数 currentSlot int // 当前插槽数 tasks []*list.List // 环形列表,每个元素为对应插槽的任务列表 set containerx.Set[string] // 记录所有任务key值,用来检查任务是否被删除 tricker *time.Ticker // 定时触发器 logger logr.Logger } func (tw *TimeWheel) Run() error { tw.tricker = time.NewTicker(tw.interval) for { // 通过定时器模拟时间轮转动 now, ok := <-tw.tricker.C if !ok { break } // 转动一次,执行任务列表 tw.RunTask(now, tw.currentSlot) tw.currentSlot = (tw.currentSlot + 1) % tw.slots } return nil } func (tw *TimeWheel) RunTask(now time.Time, slot int) { // 一次执行任务列表 for item := taskList.Front(); item != nil; { task, ok := item.Value.(*TimeWheelTask) // 任务圈数大于0,不需要执行,将圈数减一 if task.circle > 0 { task.circle-- item = item.Next() continue } // 运行任务 go task.Exec() // 计算任务下次运行时间 next := item.Next() taskList.Remove(item) item = next task.next = task.Next(now) if !task.next.IsZero() { tw.add(now, task) } else { tw.Remove(task.Name()) } } } // 添加任务,计算下一次任务执行的插槽与圈数 func (tw *TimeWheel) add(now time.Time, task *TimeWheelTask) { if !task.initialized { task.next = task.Next(now) task.initialized = true } duration := task.next.Sub(now) if duration <= 0 { task.slot = tw.currentSlot + 1 task.circle = 0 } else { mult := int(duration / tw.interval) task.slot = (tw.currentSlot + mult) % tw.slots task.circle = mult / tw.slots } tw.tasks[task.slot].PushBack(task) tw.set.Insert(task.Name()) }
时间轮的主要逻辑如下:
将任务存在对应插槽的时间
通过定时间模拟时间轮转动
每次到期后遍历当前插槽的任务列表,若任务圈数为0则执行
如果任务未结束,计算下次执行的插槽与圈数
转入步骤2,依次执行
以上就是关于“Golang分布式应用定时任务如何实现”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。