前言

日常运维中难免会遇到主从切换的场景,比如机房迁移、故障机替换等待,为了平滑迁移需要先将待下线主机上的主全部切走,主从切换动作有损所以需要低峰期操作,一般都是凌晨以后,如果联动业务核心指标做好前后校验,其实是可以将该动作自动化的,如果自动化就涉及到定时执行,这里记录下在平台上通过go语言实现定时任务的简单思路

通过go语言实现定时任务有两种方法,一种是采用主机自带的crontab机制,go语言有对应的包实现,另一种是采用计时器的方式,如果采用前者,前端需要将通过时间选择器获取的时间转化为定时任务的时间格式,传参方式没有后者简单,所以选择了通过计时器实现定时任务
创建定时器

f:=func(){
	fmt.Println("Timer expired,begin to do next task")
}
t := time.AfterFunc(time.Duration(10)*time.Second, f)//等到10s后会启动一个协程执行函数f,t是创建的计时器,可以调用t.stop来取消该计时器

创建万定时器后,如何取消已设置的定时器?
当接受到任务取消的请求时需要知道待取消的定时器时哪个才可以执行取消操作,这就涉及到线程间的通信,一种解法是通过channel
在创建完定时器后将定时器写入到管道中,当接收到取消定时任务的请求是从管道中读取定时器然后调用其stop方法来关闭定时器

//将定时器写入到管道
taskTimer := TaskTimer{
			ClusterName: para.ClusterName,
			TimerName:   t,
			TaskType:    pb.TaskType_CLUSTER_MASTER_SWITCH,
			TimerOpt:    TIMER_OPT_TYPE_CANCEL,
			LogId:       logRes.LogId,
		}
TaskTimerChan <- &taskTimer
//当接收到取消定时任务的请求是取消定时任务
func CancelTimer() (logId int64, err error) {
	timerMsg, ok := <-TaskTimerChan
	logId = timerMsg.LogId
	if !ok {
		common.Log.Warn("The TaskTimerChan is closed,The routine exits normally")
	} else {
		if !timerMsg.TimerName.Stop() {
			err = fmt.Errorf("fail to stop timer,timer=[%v]", timerMsg)
			return
		}
		common.Log.Notice("has canceled the timer task,timerMsg=[%v]", timerMsg)
	}
	return
}

那么如何保证不会误操作其他的计时器?
比如创建了两个计时器t1和t2,然后点击取消t2的任务,由于消费的时候顺序读取channel中的消息,无法直接获取指定定时器名,所以会误消费消息造成其他消息丢失而无法再对其他定时器执行取消操作,将channel的长度设置为1是一种解法,不过会带来其他的问题:只允许同时存在一个计时器
如何实现同时支持多个计时器?
可以初始化多个管道,管道名和计时器名相关联,取消计时器时根据计时器名判断要消费目标管道
对于分布式系统需要考虑几个问题
如何保证定时任务不会重入?
1、通过检查数据库中是否已经存在正在执行此任务
2、写管道前先判断管道是否已满
如何保证消费的是已经写入消息的管道?
考虑一个场景,比如有两个服务,创建定时任务的请求被路由到了服务a上,而取消该定时任务的请求被路由到了服务b上,这时b上的管道没有消息,读取被阻塞,所以
1、读取前判断是否已有消息来避免读阻塞
2、引入三方组件实现分布式一致性,比如写入消息时将服务id和计时器名字存入到redis,消费时如何管道为空则从redis中获取服务id并将请求转发给他
如果要求较高,这样实现起来就复杂化了,不如采取crontab实现,要求不高,比如取消时发现管道为空报错,那就多重试几次,则可以选择计时器