使用 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 中这个值的状态永远都是未完成,那么重启服务后也就无法重新调用同一个函数了。这是个需要解决的问题。不过本场景暂时不需要考虑这种情况。