使用 golang 的定时任务(采用 robfig/cron 包),每 5s 调用一个函数进行处理,但是这个函数的处理可能会耗时,在没有执行完之前如果再次调用,就会导致计算结果不对。
如定时任务:
// 5s 定时任务
func run5Second() {
spec := "*/5, *, *, *, *, *"
c := cron.New()
//npc刷新
c.AddFunc(spec, modelFish.NpcRefresh)
c.Start()
}
NpcRefresh 方法定义:
// NpcRefresh 返回需要刷新的NPC
func NpcRefresh() {
online := getOnlineRoomIds()
//online := []string{"5"}
fmt.Println("online room:", online)
if len(online) > 0 {
for _, roomID := range online {
roomID, _ := strconv.Atoi(roomID)
go doNpcRefresh(roomID)
}
}
}
这里 doNpcRefresh 方法起了多个 goroutine 去并发执行,里面有大量的计算,发现如果重复调用就会导致计算结果不正确,那么在每次的执行结束之前,不应该再次执行。
一开始想用 sync.waitGroup 的方式去同步,它的原理是把要执行的 goroutine 放队列,执行完出队列,队列没有完成前阻塞。这是一个方法,这样确实可以达到不重复执行一个未完成函数的目的,但是不适用当前的场景。
sync.waitGroup 用法示例:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
}
for i := 0; i < 100; i++ {
go wg.Done()
}
fmt.Println("exit")
wg.Wait()
}
func add(wg sync.WaitGroup) {
wg.Add(1)
}
func done(wg sync.WaitGroup) {
wg.Done()
}
这个场景要解决的问题是:上一轮定时任务调用的函数没有执行完,就不执行,否则再次执行,而 sync.waitGroup 方式只解决了不重复执行,但是无法“丢弃”这次调用,而是放到队列等后面执行,这样也许会累积很多队列,这不是期望的。
因此,解决办法就是,在函数开始执行的时候,在 Redis 设置一个值表示未完成,执行结束的地方更新这个值为已完成,在每次执行最前面判断这个值的状态,如果是未完成,就直接返回 false.
大概流程:
// 执行具体游戏房间的npc刷新
func doNpcRefresh(roomid int) bool {
status := getRefreshStatus(roomid)
if status == "0" {
fmt.Printf("房间%d刷新未完成,不进行新一轮刷新\n", roomid)
return false
}
start := time.Now()
fmt.Println("====================检测NPC刷新====================")
setRefreshStart(roomid)
//此处省略800子
setRefreshDone(roomid)
return true
}
如此一来,就解决了问题。
注意:如果进程被意外 kill 掉,会导致 Redis 中这个值的状态永远都是未完成,那么重启服务后也就无法重新调用同一个函数了。这是个需要解决的问题。不过本场景暂时不需要考虑这种情况。