先上架构图
本次实现 xin-job-client 中的一部分代码。
本次实现的内容为,xin-job-admin 调度完成后,将任务信息通过 http 发送到 xin-job-client,然后根据任务信息,找到任务开始执行。
要实现这个内容,主要有以下几个问题:
发送什么内容?
这个问题很简单,发送任务ID和任务名称即可。(暂时先这么着)
如何根据内容找到定时任务
这个问题就比较麻烦了。了解 Java 的都知道 Java 虚拟机是字节码执行的。所以 Java 虚拟机可以使用反射直接获取到类信息。从而找到任务。
但是 Golang 语言本身直接是机器码执行,无法通过上面的方式实现通过接口找到所有的实现。 虽然也可以通过一些办法实现,比如遍历包路径下所有定义的类型,然后判断每个类型是否是该接口。性能损失很大。
所以我使用的是手动设置任务。
代码实现
core
增加 model ,用于 admin,client 通信传输
package biz
type Return[T any] struct {
Code int `json:"code"`
Msg string `json:"msg"`
Content T `json:"content"`
}
type TriggerParam struct {
JobId int `json:"jobId"` // 任务ID
ExecutorHandler string `json:"executorHandler"` // JobHandler的名字
}
client
定义任务接口,所有任务都需要实现该接口
package handler
// 任务接口
type Handler interface {
GetName() string // 任务名称
Execute() // 执行任务
Init() // 任务初始化
Destroy() // 任务销毁
}
任务执行器
executor 结构体以及方法全部都是私有的。只有包内可以访问。
package server
import (
"sync"
"sync/atomic"
"xin-job/client/handler"
)
type executor struct {
handlerRepository sync.Map // 任务仓库,用户缓存项目内所有的定时任务
handlersCount atomic.Uint32 // 任务个数
}
func newExecutor() *executor {
return &executor{}
}
// addHandler 新增任务
func (e *executor) addHandler(h handler.Handler) {
e.handlerRepository.Store(h.GetName(), h)
e.handlersCount.Add(1)
}
// loadHandler 根据任务名称查询任务
func (e *executor) loadHandler(name string) handler.Handler {
h, ok := e.handlerRepository.Load(name)
if ok {
if v, ok := h.(handler.Handler); ok {
return v
}
}
return nil
}
// runHandler 执行任务
func (e *executor) runHandler(h handler.Handler) {
// 开启一个协程直接运行,不阻碍 http 结果返回到 admin
go func() {
h.Init()
h.Execute()
h.Destroy()
}()
}
server
部分代码,只展示了接收到 http 请求后的处理流程。f
func h(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != "POST" {
res := biz.Return[string]{Code: 500, Msg: "invalid request, HttpMethod not support."}
resData, err := json.Marshal(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(resData)
}
fmt.Println(r.URL)
// TODO token
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
p := biz.TriggerParam{}
err = json.Unmarshal(body, &p)
if err != nil {
http.Error(w, "Error parsing JSON", http.StatusBadRequest)
return
}
result := biz.Return[string]{
Code: 200,
Msg: "success",
Content: "success",
}
// 加载任务
h := s.e.loadHandler(p.ExecutorHandler)
if h == nil {
result.Code = 500
result.Msg = "No handlers found"
result.Content = "No handlers found"
} else {
// 执行任务
s.e.runHandler(h)
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("trigger err %v\n", err)
return
}
w.Write(data)
}
admin
trigger
修改上一篇文章中写死的 http.get
example
定义任务,并实现任务接口
package job
import "log"
type EchoJob struct {
Name string
}
func (t *EchoJob) GetName() string {
return t.Name
}
func (t *EchoJob) Execute() {
log.Println("run echo task")
}
func (t *EchoJob) Init() {
log.Println("run echo init")
}
func (t *EchoJob) Destroy() {
log.Println("run echo destroy")
}
package main
import (
"xin-job/client/server"
"xin-job/example/job"
)
func main() {
j := job.EchoJob{Name: "echo_job"} // 新建定时任务
es := server.GetServer()
es.AddHandler(&j) // 新增任务
es.Start()
}
运行
- xin-job/admin 目录下执行
go run main.go - xin-jon/example 目录下执行
go run main.go
存在问题
- 假设一个定时任务非常耗时,第一次执行还没结束,第二次就开始了。这种情况可能会导致数据错乱。
- 基于第一种假设,如果这类任务比较多,服务运行时间一段时间后,无法退出的
goroutine越来越多,资源占用越来越大。最终可能会 goroutine 溢出
解决方案
首先,xxl-job 是 Java 实现的 go 语言与 Java 存在语言差异。本文暂时只讨论 xxl-job 解决方案。
- 每个任务与一个线程绑定,相同的任务只有一个线程执行。即使一个线程阻塞了,也只阻塞一个。
- 防止线程长时间不销毁占用资源问题,空轮询30次后销毁。
本文所有代码
xin-job: golang 实现 xin-job 垃圾版 (gitee.com) 下的 xin-job-02.tar.gz