Golang分布式应用定时任务示例详解

目录
  • 正文
  • 最小堆
  • 时间轮
  • 总结

正文

在系统开发中,有一类任务不是立即执行,而是在未来某个时间点或者按照一定间隔去执行,比如日志定期压缩、报表制作、过期数据清理等,这就是定时任务。

在单机中,定时任务通常需要实现一个类似crontab的系统,一般有两种方式:

  • 最小堆,按照任务执行时间建堆,每次取最近的任务执行
  • 时间轮,将任务放到时间轮列表中,每次转动取对应的任务列表执行

最小堆

最小堆是一种特殊的完全二叉树,任意非叶子节点的值不大于其子节点,如图

通过最小堆,根据任务最近执行时间键堆,每次取堆顶元素即最近需要执行的任务,设置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,依次执行

总结

本文主要总结了定时任务的两种实现方式,最小堆与时间轮,并分析其核心实现逻辑。

对于执行分布式定时任务,可以借助延时消息队列或者直接使用Kubernetes的CronJob。

自己开发的话可以借助Etcd:

  • 中心节点Coordinator将任务按照一定算法(Hash、轮询、或者更复杂的分配算法)将任务与工作节点Worker绑定
  • 每个Worker添加到有绑定到自己的任务则取出放到本地的Cron中
  • 如果Worker挂掉,执行将其上任务重新绑定即可

本文所有代码见github.com/qingwave/go…

以上就是Golang分布式应用定时任务示例详解的详细内容,更多关于Golang分布式定时的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go 语言下基于Redis分布式锁的实现方式

    分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 项目地址: https://github.com/Spongecaptain/redisLock 1. Go 原生的互斥锁 Go 原生的互斥锁即 sync 包下的 M

  • Golang分布式应用之Redis示例详解

    目录 正文 分布式锁 运行测试 分布式过滤器 运行测试 分布式限流器 运行测试 其他 正文 Redis作是一个高性能的内存数据库,常被应用于分布式系统中,除了作为分布式缓存或简单的内存数据库还有一些特殊的应用场景,本文结合Golang来编写对应的中间件. 本文所有代码见github.com/qingwave/go… 分布式锁 单机系统中我们可以使用sync.Mutex来保护临界资源,在分布式系统中同样有这样的需求,当多个主机抢占同一个资源,需要加对应的“分布式锁”. 在Redis中我们可以通过s

  • redis分布式锁的go-redis实现方法详解

    在分布式的业务中 , 如果有的共享资源需要安全的被访问和处理 , 那就需要分布式锁 分布式锁的几个原则; 1.「锁的互斥性」:在分布式集群应用中,共享资源的锁在同一时间只能被一个对象获取. 2. 「可重入」:为了避免死锁,这把锁是可以重入的,并且可以设置超时. 3. 「高效的加锁和解锁」:能够高效的加锁和解锁,获取锁和释放锁的性能也好. 4. 「阻塞.公平」:可以根据业务的需要,考虑是使用阻塞.还是非阻塞,公平还是非公平的锁. redis实现分布式锁主要靠setnx命令 1. 当key存在时失败

  • 用Go+Redis实现分布式锁的示例代码

    目录 为什么需要分布式锁 分布式锁需要具备特性 实现 Redis 锁应先掌握哪些知识点 set 命令 Redis.lua 脚本 go-zero 分布式锁 RedisLock 源码分析 关于分布式锁还有哪些实现方案 项目地址 为什么需要分布式锁 用户下单 锁住 uid,防止重复下单. 库存扣减 锁住库存,防止超卖. 余额扣减 锁住账户,防止并发操作. 分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性. 分布式锁需要具备特性 排他性 锁的基本特性,并且只能被第一个持有者持有. 防死锁

  • Go结合Redis用最简单的方式实现分布式锁

    目录 前言 单Redis实例场景 加解锁示例 小结 多Redis实例场景 加解锁示例 小结 总结 前言 在项目中我们经常有需要使用分布式锁的场景,而Redis是实现分布式锁最常见的一种方式,并且我们也都希望能够把代码写得简单一点,所以今天我们尽量用最简单的方式来实现. 下面的代码使用go-redis客户端和gofakeit,参考和引用了Redis官方文章 单Redis实例场景 如果熟悉Redis的命令,可能会马上想到使用Redis的set if not exists操作来实现,并且现在标准的实现

  • Golang分布式应用定时任务示例详解

    目录 正文 最小堆 时间轮 总结 正文 在系统开发中,有一类任务不是立即执行,而是在未来某个时间点或者按照一定间隔去执行,比如日志定期压缩.报表制作.过期数据清理等,这就是定时任务. 在单机中,定时任务通常需要实现一个类似crontab的系统,一般有两种方式: 最小堆,按照任务执行时间建堆,每次取最近的任务执行 时间轮,将任务放到时间轮列表中,每次转动取对应的任务列表执行 最小堆 最小堆是一种特殊的完全二叉树,任意非叶子节点的值不大于其子节点,如图 通过最小堆,根据任务最近执行时间键堆,每次取堆

  • Java使用quartz实现定时任务示例详解

    目录 正文 配置文件 pom 定时任务和触发器 定时任务的业务代码 正文 最近新到了一个项目,用到定时任务的地方是真滴多. 就稍微研究了一下,来做个demo. 其实定时任务使用很广泛也很方便,之前做的人事管理项目,就会定期执行定时任务计算工资,对于一个saas服务来说,即时的计算所有员工的工资有点奢侈,所以在每周末计算一次就ok了. 国外有的公司是一周发一次工资,所以当时的逻辑是一周算一次.在国内就一月一次很ok了.在当时的report服务中,也是定时任务同步数据到Birt服务,然后展现数据.

  • SpringBoot如何实现定时任务示例详解

    目录 写在前面 一.基于注解(@Scheduled) 二.数据库动态配置 1.表数据添加,资源配置 1.1 添加表 1.2 插入两条数据,job_key根据是完整的类名 1.3 引入依赖 1.4 配置application.yml 2.疯狂贴代码 2.1 创建定时任务线程池 2.2 项目启动时初始化定时任务 2.3 定时任务公共接口 2.4 创建两个定时任务实现类 2.5 定时任务管理接口 2.6 定时任务管理实现类 2.8 上面用到的获取Bean的工具类SpringContextUtil 2.

  • 基于gin的golang web开发:路由示例详解

    Gin是一个用Golang编写的HTTP网络框架.它的特点是类似于Martini的API,性能更好.在golang web开发领域是一个非常热门的web框架. 启动一个Gin web服务器 使用下面的命令安装Gin go get -u github.com/gin-gonic/gin 在代码里添加依赖 import "github.com/gin-gonic/gin" 快速启动一个Gin服务器的代码如下 package main import "github.com/gin-

  • golang中的三个点 '...'的用法示例详解

    '-' 其实是go的一种语法糖. 它的第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数. 第二个用法是slice可以被打散进行传递. 下面直接上例子: func test1(args ...string) { //可以接受任意个string参数 for _, v:= range args{ fmt.Println(v) } } func main(){ var strss= []string{ "qwr", "234", "yui

  • golang编程开发使用sort排序示例详解

    golang sort package: https://studygolang.com/articles/3360 sort 操作的对象通常是一个 slice,需要满足三个基本的接口,并且能够使用整数来索引 // A type, typically a collection, that satisfies sort.Interface can be // sorted by the routines in this package. The methods require that the /

  • golang gorm更新日志执行SQL示例详解

    目录 1. 更新日志 1.1. v1.0 1.1.1. 破坏性变更 gorm执行sql 1. 更新日志 1.1. v1.0 1.1.1. 破坏性变更 gorm.Open返回类型为*gorm.DB而不是gorm.DB 更新只会更新更改的字段 大多数应用程序不会受到影响,只有当您更改回调中的更新值(如BeforeSave,BeforeUpdate)时,应该使用scope.SetColumn,例如: func (user *User) BeforeUpdate(scope *gorm.Scope) {

  • Golang 实现 RTP音视频传输示例详解

    目录 引言 RTP 数据包头部字段 Golang 的相关实现 结尾 引言 在 Coding 之前我们先来简单介绍一下 RTP(Real-time Transport Protocol), 正如它的名字所说,用于互联网的实时传输协议,通过 IP 网络传输音频和视频的网络协议. 由音视频传输工作小组开发,1996 年首次发布,并提出了以下使用设想. 简单的多播音频会议 使用 IP 的多播服务进行语音通信.通过某种分配机制,获取多播组地址和端口对.一个端口用于音频数据的,另一个用于控制(RTCP)包,

  • Makefile构建Golang项目示例详解

    目录 背景 创建项目并运行 添加 Makefile 文件 Makefile 概念 变量 使用 Makefile 自动化任务 背景 构建和测试大型项目时都会很耗时,且容易出错.开发者在开发过程中需要不断执行go build.go run .go test等相关命令.还可能需要多个命令来构建不同平台的二进制文件.在正式部署时候,我们可能还需要安装一些依赖项,或者在发布之前进行代码覆盖率测试等相关前置工作. 整个过程需要很多步骤,但我们有一种简单的方法可以解决这些复杂琐碎的步骤.使用 Make 进行自

随机推荐