用golang實(shí)現(xiàn)一個(gè)定時(shí)器任務(wù)隊(duì)列實(shí)例
很有幸得到公司信任,采用新的語言進(jìn)行一些底層服務(wù)的開發(fā),在實(shí)現(xiàn)功能的同時(shí),也獲得了一些感悟,因此在這記錄一下,方便自己查看也可以共享給大家。
golang中定時(shí)器
golang中提供了2種定時(shí)器timer和ticker(如果JS很熟悉的話應(yīng)該會很了解),分別是一次性定時(shí)器和重復(fù)任務(wù)定時(shí)器。
一般用法:
func main() { input := make(chan interface{}) //producer - produce the messages go func() { for i := 0; i < 5; i++ { input <- i } input <- "hello, world" }() t1 := time.NewTimer(time.Second * 5) t2 := time.NewTimer(time.Second * 10) for { select { //consumer - consume the messages case msg := <-input: fmt.Println(msg) case <-t1.C: println("5s timer") t1.Reset(time.Second * 5) case <-t2.C: println("10s timer") t2.Reset(time.Second * 10) } } }
源碼觀察
這個(gè)C是啥,我們?nèi)ピ创a看看,以timer為例:
type Timer struct { C <-chan Time r runtimeTimer }
原來是一個(gè)channel,其實(shí)有GO基礎(chǔ)的都知道,GO的運(yùn)算符當(dāng)出現(xiàn)的->或者<-的時(shí)候,必然是有一端是指channel。按照上面的例子來看,就是阻塞在一個(gè)for循環(huán)內(nèi),等待到了定時(shí)器的C從channel出來,當(dāng)獲取到值的時(shí)候,進(jìn)行想要的操作。
設(shè)計(jì)我們的定時(shí)任務(wù)隊(duì)列
我的需求
當(dāng)時(shí)我的需求是這樣,我需要接收到客戶端的請求并產(chǎn)生一個(gè)定時(shí)任務(wù),會在固定時(shí)間執(zhí)行,可能是一次,也可能是多次,也可能到指定時(shí)間自動停止,可能當(dāng)任務(wù)終止的時(shí)候,我還要能停止掉。
具體我畫了個(gè)流程圖,差不多如下,畫圖水平有限,請見諒。
定義結(jié)構(gòu)
type OnceCron struct { tasks []*Task //任務(wù)的列隊(duì) add chan *Task //當(dāng)遭遇到新任務(wù)的時(shí)候 remove chan string //當(dāng)遭遇到刪除任務(wù)的時(shí)候 stop chan struct{} //當(dāng)遇到停止信號的時(shí)候 Logger *log.Logger //日志 } type Job interface { Run() //執(zhí)行接口 } type Task struct { Job Job //要執(zhí)行的任務(wù) Uuid string //任務(wù)標(biāo)識,刪除時(shí)用 RunTime int64 //執(zhí)行時(shí)間 Spacing int64 //間隔時(shí)間 EndTime int64 //結(jié)束時(shí)間 Number int //總共要次數(shù) }
隊(duì)列實(shí)現(xiàn)
首先,我們要獲得一個(gè)隊(duì)列任務(wù)
func NewCron() *OnceCron 常規(guī)操作,為了節(jié)省篇幅,我就不寫出來,具體可以看源碼,貼在了底部。
然后,開始定時(shí)器隊(duì)列的運(yùn)行,一般,都會命名為Start。那么就有一個(gè)問題,我們剛開始啟動程序的時(shí)候,這個(gè)時(shí)候是沒有任務(wù)隊(duì)列,那豈不是for{ select{}}在等待個(gè)毛毛球?所以,我們需要在Start的時(shí)候添加一個(gè)默認(rèn)的任務(wù), 我是這么做的,添加了一個(gè)一小時(shí)執(zhí)行一次的重復(fù)隊(duì)列,防止隊(duì)列退出。
func (one *OnceCron) Start() { //初始化的時(shí)候加入一個(gè)一年的長定時(shí)器,間隔1小時(shí)執(zhí)行一次 task := getTaskWithFuncSpacing(3600, time.Now().Add(time.Hour*24*365).Unix() , func() { log.Println("It's a Hour timer!") }) //為了代碼格式markdown 里面有個(gè)括號我改成全角了 one.tasks = append(one.tasks, task) go one.run() //協(xié)成執(zhí)行 防止主進(jìn)程被阻塞 }
執(zhí)行部分應(yīng)該是重點(diǎn)的,我的理解是,分成三部:
- 首先獲得一個(gè)最先執(zhí)行的任務(wù)
- 然后產(chǎn)生一個(gè)定時(shí)器,用于執(zhí)行任務(wù)
- 進(jìn)行阻塞判斷,獲取我們要進(jìn)行的操作
func (one *OnceCron) run() { for { //第一步 獲取任務(wù) now := time.Now() //獲取到當(dāng)前時(shí)間 task, key := one.GetTask() //獲取最近的一個(gè)任務(wù)的執(zhí)行時(shí)間 i64 := task.RunTime - now.Unix() //任務(wù)執(zhí)行和當(dāng)前時(shí)間的差 var d time.Duration if i64 < 0 { //如果任務(wù)時(shí)間已過期,將執(zhí)行時(shí)間改成現(xiàn)在并且利馬執(zhí)行 one.tasks[key].RunTime = now.Unix() one.doAndReset(key) continue } else { //否則,獲取距離執(zhí)行開始的間隔時(shí)間 d = time.Unix(task.RunTime, 0).Sub(now) } //第二步 產(chǎn)生定時(shí)器 timer := time.NewTimer(d) //第三步 捕獲定時(shí)器或者其他事件 for { select { //當(dāng)定時(shí)器到了執(zhí)行時(shí)間時(shí),執(zhí)行當(dāng)前任務(wù)并關(guān)閉定時(shí)器 case <-timer.C: one.doAndReset(key) if task != nil { go task.Job.Run() timer.Stop() } //當(dāng)外部添加了任務(wù)時(shí),關(guān)閉當(dāng)前定時(shí)器 case <-one.add: timer.Stop() //當(dāng)外部要刪除一個(gè)任務(wù)時(shí),刪除ID為uuidstr的任務(wù) case uuidstr := <-one.remove: one.removeTask(uuidstr) timer.Stop() //當(dāng)遇到要關(guān)閉整個(gè)定時(shí)器任務(wù)時(shí) case <-one.stop: timer.Stop() return } break } } }
后記
這個(gè)文章純粹為筆記分析類的文章,旨在分析我碰到一個(gè)需求是如何通過分析過程來產(chǎn)生我們需要的代碼的。
源碼地址:timing 一個(gè)任務(wù)隊(duì)列
應(yīng)用地址:一個(gè)應(yīng)用于谷歌消息推送的轉(zhuǎn)發(fā)中間件
參考源碼:GOLANG實(shí)現(xiàn)crontab功能
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
深入探究Golang中flag標(biāo)準(zhǔn)庫的使用
在本文中,我們將深入探討 flag 標(biāo)準(zhǔn)庫的實(shí)現(xiàn)原理和使用技巧,以幫助讀者更好地理解和掌握該庫的使用方法,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-04-04Go的gin參數(shù)校驗(yàn)中的validator庫詳解
這篇文章主要介紹了Go的gin參數(shù)校驗(yàn)之validator庫,使用 validator 以后,只需要在定義結(jié)構(gòu)體時(shí)使用 binding 或 validate tag標(biāo)識相關(guān)校驗(yàn)規(guī)則,就可以進(jìn)行參數(shù)校驗(yàn)了,而不用自己單獨(dú)去寫常見的校驗(yàn)規(guī)則,需要的朋友可以參考下2023-08-08GO 函數(shù)式選項(xiàng)模式(Functional Options Pattern)
Option模式支持傳遞多個(gè)參數(shù),并且在參數(shù)個(gè)數(shù)、類型發(fā)生變化時(shí)保持兼容性,任意順序傳遞參數(shù),下面給大家介紹GO 函數(shù)式選項(xiàng)模式(Functional Options Pattern)的相關(guān)知識,感興趣的朋友一起看看吧2021-10-10Go語言基礎(chǔ)函數(shù)包的使用學(xué)習(xí)
本文通過一個(gè)實(shí)現(xiàn)加減乘除運(yùn)算的小程序來介紹go函數(shù)的使用,以及使用函數(shù)的注意事項(xiàng),并引出了對包的了解和使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05go并發(fā)數(shù)據(jù)一致性事務(wù)的保障面試應(yīng)答
這篇文章主要為大家介紹了go并發(fā)數(shù)據(jù)一致性事務(wù)的保障面試應(yīng)答,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12golang中http請求的context傳遞到異步任務(wù)的坑及解決
這篇文章主要介紹了golang中http請求的context傳遞到異步任務(wù)的坑及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03Go并發(fā)編程結(jié)構(gòu)體多字段原子操作示例詳解
這篇文章主要為大家介紹了Go并發(fā)編程結(jié)構(gòu)體多字段原子操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12