练习时长两年半的个人练习生写的第一个包

89 阅读4分钟

背景

    在上一家公司做游戏服务端开发,为了丰富游戏内容留住玩家新增了许多的小游戏玩法,而小游戏里面涉及到计时器相
关的功能,一两个小功能还能继续用golang自带的timer去做计时器。随着小游戏种类的增多,计时业务的繁多,每一个计
时功能都用一个timer去实现,本身计时逻辑就有一定的复杂度,融合复杂的业务逻辑后。。。。
    再一个计时器管理起来也不方便。后来准备开始做一些计时器上的优化,结果公司创业未半而中道崩殂。后面基于我对
这方面的兴趣还是想继续完成这件事,设计一个和业务逻辑脱离的计时器系统,方便计时器的统一管理当然之前第一时间也
是想去看看有没有哪些开源项目对计时器业务有一个比较好的支持的,有考虑过go-cron,个人感觉它还是更适合做定时任
务相关的业务,不太适合用来做计时业务,
项目主要实现功能如下:
    1.要能够实时的由业务侧控制计时器的开关
    2.业务侧能够触发停止计时并执行相应的停止业务逻辑;
    3.如果计时器到了时间能够触发和执行相应的过期业务逻辑
    4.再重启项目时能看到是否还有计时器在运行

实现方案

1.核心计时操作

计时操作还是基于golang的timer去实现,利用channel接收结束信号并控制计时器的关闭

func (p *BaseTick) doTick() {
    timer := p.TickPool.GetFreeTimer()
    if timer == nil {
       blockBaseTickMutex.Lock()
       blockBaseTick = append(blockBaseTick, p)
       blockBaseTickMutex.Unlock()
       return
    }
    timer.Reset(p.TrickTime)
    timeTickMap.Store(p.Id, make(chan struct{}))
    defer p.TickPool.PushToFreeTimerList(timer)
    for {
       stopTick, ok := timeTickMap.Load(p.Id)
       if !ok {
          fmt.Println("计时器异常退出")
       }
       select {
       case <-timer.C:
          p.ServiceData.OverDoFunc()
          timeTickMap.Delete(p.Id)
          return
       case <-stopTick.(chan struct{}):
          p.ServiceData.StopDoFunc()
          timeTickMap.Delete(p.Id)
          return
       }
    }
}

利用for、select来实现阻塞等待,timer.C和stopTick.(chan struct{})分别对应计时器的到期和中途停止信号

timeTickMap用来存储该计时器的的唯一id和控制结束信号的管道,利用timeTickMap向外提供计时结束的功能

TickPool主要是实现timer的复用

2.timer池的实现

type TickWithConf struct {
    freeTimerList []*time.Timer //空闲的计时器存储列表
    effectiveNum  int32         //计时器数量超出改值将空闲的进行释放
    capacity      int32         //timer 最大容量
    runningNum    int32         //运行的计时器数量
    sync.Mutex
}

主要实现的timer池的创建、扩容、回收、释放容量,freeTimerList存放未被使用的timer,effectiveNum 将空闲的timer数量控制在这个数量下(初始的timer数量),capacity timer最大可扩容范围

1.获取timer:

func (c *TickWithConf) GetFreeTimer() *time.Timer {
    var tempTimer *time.Timer
    c.Lock()
    if len(c.freeTimerList) <= 0 {
       if c.runningNum > c.capacity {
          fmt.Println("超出规定最大可开启的计时器数量")
          return nil
       }
       tempTimer = c.newTimer()
    } else {
       tempTimer = c.freeTimerList[0]
       c.freeTimerList[0] = nil
       if len(c.freeTimerList) > 1 {
          c.freeTimerList = c.freeTimerList[1:]
       } else {
          c.freeTimerList = c.freeTimerList[0:0]
       }

    }
    c.Unlock()
    atomic.AddInt32(&c.runningNum, 1)
    if !tempTimer.Stop() {
       select {
       case <-tempTimer.C:
       default:
       }
    }
    return tempTimer
}

如果freeTimerList里面有timer会直接在里面取,如果freeTimerList没有的话看目前运行的timer是否超过最大限制,超过的话目前是获取timer失败将计时任务信息存储到blockBaseTick

2.计时完成的timer放入

func (c *TickWithConf) PushToFreeTimerList(timer *time.Timer) {
    timer.Stop()
    c.Lock()
    atomic.AddInt32(&c.runningNum, -1)
    c.freeTimerList = append(c.freeTimerList, timer)
    c.Unlock()
}

3.检测timer是否需要释放

func (c *TickWithConf) releaseTimer() {
    ticker := time.NewTicker(time.Second * 5)
    defer ticker.Stop()
    for {
       select {
       case <-ticker.C:
          c.Lock()
          if len(c.freeTimerList) > int(c.effectiveNum) {
             for i := int(c.effectiveNum); i < len(c.freeTimerList); i++ {
                c.freeTimerList[i].Stop()
                c.freeTimerList[i] = nil
             }
             c.freeTimerList = c.freeTimerList[:int(c.effectiveNum)]
          }
          c.Unlock()
       }

    }
}

在每创建timer池的时候就会开启一个清理协程

3.项目架构图

image.png

4.项目整体流程图

项目流程图-2024-07-03-1614.png

使用说明

go get github.com/csgZM/tick_server
package main

import (
    "fmt"
    "github.com/csgZM/tick_server/tick_core"
    "time"
)

type TestTickServiceModel struct {
    tick_core.BaseTickServiceModel
    TestServiceData string
}

// GetUniqueId 业务侧控制全局唯一计时器id
func (t TestTickServiceModel) GetUniqueId() string {
    return fmt.Sprintf("test_unique_%s", t.TestServiceData)
}

// OverDoFunc 计时器计时完成后的业务逻辑
func (t TestTickServiceModel) OverDoFunc() {
    fmt.Println("TestTickServiceModel over do function", t.TestServiceData)
}

// StopDoFunc 业务侧终止计时器后的业务逻辑
func (t TestTickServiceModel) StopDoFunc() {
    fmt.Println("TestTickServiceModel stop do function", t.TestServiceData)
}

func main() {
    tickPool := tick_core.NewTickWithConf(1000, 10000) //timer池配置
    test := &TestTickServiceModel{TestServiceData: fmt.Sprintf("test_%d", 1)}
    baseTick := tick_core.NewBaseTick(test.GetUniqueId(), time.Second*5, tick_core.InitTickTime, test, tickPool)
    baseTick.StartTick()        //开启计时
    time.Sleep(time.Second * 7) //等待计时完成 并执行计时完成逻辑
    baseTick.StartTick()        //开启计时
    baseTick.StopTick()         //业务关闭计时器并执行关闭后的业务逻辑
    time.Sleep(time.Second * 2)
}

后续优化

1. 项目重启会导致计时器失效
2. 日志优化
3. 阻塞任务的处理
4. timeTickMap性能需要优化

结尾

之前大多也会写一些工具函数但是都没有像这次一样完全分装成独立的包,这也算是第一次尝试除curd之外的第一次写包经验,既是分享也是请教后续的优化方向和项目中存在的问题。本人才学浅陋小菜鸡一枚,加上也是后面脱离实际生产出来的东西,后面的优化就没有太好的思路,还希望各位不惜吝啬发表一下自己看法!