手写 xxl-job 02

321 阅读3分钟

1.jpg 先上架构图

本次实现 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()
}

运行

  1. xin-job/admin 目录下执行 go run main.go
  2. xin-jon/example 目录下执行 go run main.go

存在问题

  1. 假设一个定时任务非常耗时,第一次执行还没结束,第二次就开始了。这种情况可能会导致数据错乱。
  2. 基于第一种假设,如果这类任务比较多,服务运行时间一段时间后,无法退出的 goroutine 越来越多,资源占用越来越大。最终可能会 goroutine 溢出

解决方案

首先,xxl-job 是 Java 实现的 go 语言与 Java 存在语言差异。本文暂时只讨论 xxl-job 解决方案。

  1. 每个任务与一个线程绑定,相同的任务只有一个线程执行。即使一个线程阻塞了,也只阻塞一个。
  2. 防止线程长时间不销毁占用资源问题,空轮询30次后销毁。

本文所有代码

xin-job: golang 实现 xin-job 垃圾版 (gitee.com) 下的 xin-job-02.tar.gz