微服务集成snail-job分布式定时任务系统实践
前言
从事开发工作的同学,应该对定时任务的概念并不陌生,就是我们的系统在运行过程中能够自动执行的一些任务、工作流程,无需人工干预。常见的使用场景包括:数据库的定时备份、文件系统的定时上传云端服务、每天早上的业务报表数据生成等等,实现方式也是层出不穷,对于简单的任务,我们可能在代码里写个死循环一直判断是否触发执行即可,对于复杂的、或者定时任务的种类数量较多,需要严密监控管理的业务场景,我们需要一些专门的定时任务工具去处理。笔者用过的定时任务有三个,分别是QuartZ,XXL-JOB,Snail-Job,关于这三个工具,我做了一个表格用来区分其使用场景和特点。
- 
Quartz:作为定时任务领域的 “老前辈”,其核心优势在于稳定性和灵活性,适合作为底层调度引擎,但在分布式场景下需要额外开发适配逻辑,更适合传统单体应用或对定制化要求高的场景。
 - 
XXL-JOB:目前国内使用最广泛的分布式任务调度平台之一,凭借完善的功能和易用性成为企业级首选,尤其适合需要可视化管理、复杂任务依赖和高可靠性的场景(如电商定时任务、数据同步等)。
 - 
Snail-Job:作为新兴框架,主打轻量和性能,去中心化设计减少了单点故障风险,适合微服务、云原生环境,或对部署复杂度和资源占用有严格要求的场景。
 
前两种笔者都在实际开发中使用过,相比Quartz,XXL-JOB功能更为强大,但是也是比较重了,所以我在网上寻找有没有类似的替代工具。哎还真让我找到了,就是snail-job,轻量级的分布式任务重试、任务调度平台,特别适合微服务项目,而且界面清爽颜值不错,简单好用,没有复杂配置,接下来的内容我会逐步介绍如何在我们的微服务项目种集成它。不过我在这里需要声明的一点是,企业级的大型项目,还是使用XXL-JOB,毕竟社区活跃,有很多技术支持,snail-job的很多技术问题,是需要付费才能获得解答,这一点让笔者不太舒服,而且开源也只开了一半,前端的代码给的是打包编译后的文件,不过一般的项目用着还是可以的。话不多说,直接上集成教程。
一、snail-job简介
snail-job的官网地址:官网

上面的是它的官网界面截图,snail-job分为服务端、客户端两组概念,这里简单介绍下
- 服务端:用于管理和配置定时任务、监控定时任务的执行、告警配置等,总的来说就是定时任务的统筹调度中心
 - 客户端:这个是需要我们自己开发的部分,开发语言目前支持Python、Java,后续会支持其他语言,比如Go语言,主要是我们用来写定时任务的执行逻辑的。
 
二、端口概念介绍
先介绍下snail-job集成涉及到的四个端口的概念,防止混淆不清
1.服务端应用端口:这个端口是后台服务端的管理web页面的入口端口,这个是可以自由配置修改的,snail-job的服务端和XXL-JOB一样有一个管理页面。
 2. 服务端通讯端口:服务端对客户端暴露的用来通信的端口,默认是17888,可以修改。
 3. 客户端应用端口:这是我们自己开发的客户端服务的应用端口,也是自定义配置的。
 4. 客户端通讯端口:这个是我们开发的客户端对服务端暴露的进行通信的端口,这个官网给的配置默认值是1789,当然支持修改。
之所以是高性能通信,因为其内部使用了netty框架,除了应用端口,单独再搞两个通信端口,一是为了通信不被干扰,二是为了通信数据的安全考虑,可以使用各种加密技术。本质是将 “用户交互层” 和 “核心调度层” 解耦,在功能、协议、性能、安全等层面实现精细化管控,最终保障分布式任务调度的高效、稳定和可维护性。这一设计在主流分布式框架(如 XXL-Job、Dubbo)中也较为常见,是分布式系统架构的典型优化思路。
三、服务端集成部署
代码拉取地址如下
# Giteegit clone https://gitee.com/aizuda/snail-job.git# GitHubgit clone https://github.com/aizuda/snail-job.git
国内用户直接使用gitee地址拉取就行,项目结构如下

 项目拉下来后,笔者因为要和自己的微服务集成,所以对源码做了一些简单的修改,比如docker文件夹,就是我新加的,docker相关配置内容,dockerfile等。
还有就是,我把这个后端的服务也集成了nacos,使得能成功注册到nacos中去,以便能和客户端通信。下面是具体操作步骤。
引入nacos

如上图所示,在源码的相关路径下引入nacos依赖,一个是注册中心配置,一个是配置中心,这里的配置中心我其实是没有使用了,即只是把服务注册到了nacos上
  <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2023.0.3.2</version> </dependency>  <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2023.0.3.2</version> </dependency>
application.yml文件修改
server: port: 9082 servlet: context-path: /snail-jobspring: application: name: snail-job-server profiles: active: prod cloud: nacos: # 注册中心 discovery: server-addr: ****:8848 namespace: hulei-dev group: DEFAULT_GROUP username: ****** password: ****** # 配置中心 config: import-check: enabled: false server-addr: ****:8848 namespace: hulei-dev group: DEFAULT_GROUP username: ****** password: ****** file-extension: yml datasource: name: snail_job ## mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/snail_job?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai username: root password: root# url: jdbc:mysql://usteu-it.rwlb.rds.aliyuncs.com:3306/snail_job?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai# username: usteu_dev# password: KDNT_dev999 ## postgres # driver-class-name: org.postgresql.Driver # url: jdbc:postgresql://localhost:5432/snail_job?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true # username: root # password: root ## Oracle # driver-class-name: oracle.jdbc.OracleDriver # url: jdbc:oracle:thin:@//localhost:1521/XEPDB1 # username: snail_job # password: SnailJob ## SQL Server # driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver # url: jdbc:sqlserver://localhost:1433;DatabaseName=snail_job;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true # username: SA # password: SnailJob@24 ## mariadb # driver-class-name: org.mariadb.jdbc.Driver # url: jdbc:mariadb://localhost:3308/snail_job?useSSL=false&characterEncoding=utf8&useUnicode=true # username: root # password: root ## dm8 # driver-class-name: dm.jdbc.driver.DmDriver # url: jdbc:dm://127.0.0.1:5236 # username: SYSDBA # password: SYSDBA001 # kingbase # driver-class-name: com.kingbase8.Driver # url: jdbc:kingbase8://localhost:54321/test # username: root # password: root type: com.zaxxer.hikari.HikariDataSource hikari: connection-timeout: 30000 minimum-idle: 5 maximum-pool-size: 100 auto-commit: true idle-timeout: 30000 pool-name: snail_job max-lifetime: 1800000 web: resources: static-locations: classpath:admin/mybatis-plus: typeAliasesPackage: com.aizuda.snailjob.template.datasource.persistence.po global-config: db-config: where-strategy: NOT_EMPTY capital-mode: false logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true cache-enabled: truelogging: config: classpath:logback-boot.xml# level:# ## 方便调试 SQL# com.aizuda.snailjob.template.datasource.persistence.mapper: debugsnail-job: retry-pull-page-size: 1000 # 拉取重试数据的每批次的大小 job-pull-page-size: 1000 # 拉取重试数据的每批次的大小 server-port: 17888 # 服务器端口 log-storage: 7 # 日志保存时间(单位: day) rpc-type: grpc
从上面的yml文件可以看到,我的服务端的应用端口配置的是9082这个端口,关于服务端的通信端口,在如上文件底部所示为17888,这个是默认的,一般没必要不去修改它。
服务端使用的数据库,我用的是mysql,所以我注释了其他数据库的连接配置。一些建表的sql脚本自然也就是mysql相关的,sql脚本位置如下

 把对应的sql脚本拷贝到,application.yml中,你配置的mysql数据库中执行即可,执行后的自动创建的数据库名称就叫snail-job
关于docker文件,我没有使用项目自带的doc/docker下的任何内容,而是我直接在项目根目录下创建了一个文件夹docker,里面配置了我自己的dockerfile,内容如下,给大家做个参考
# 基础镜像FROM openjdk:17-jdk# authorMAINTAINER usteuENV JAVA_OPTS=\"-Xms1g -Xmx2g -XX:+UseG1GC\"# 挂载目录VOLUME /home/snailjob-server# 创建目录RUN mkdir -p /home/snailjob-server# 指定路径WORKDIR /home/snailjob-server# 复制jar文件到路径COPY ./jar/snail-job-server-exec.jar /home/snailjob-server/snail-job-server-exec.jar# 启动系统服务ENTRYPOINT [\"sh\", \"-c\", \"java $JAVA_OPTS -jar snail-job-server-exec.jar\",\"-Duser.timezone=GMT+08\"]
docker-compose.yml中有关snail-job服务端的service配置内容如下,可以看到,我映射了宿主机和容器的两个端口
 #snail-job的服务端docker部署,使用的是本地拉取的源码打包部署,非镜像 snail-job-server: container_name: snail-job-server build: context: ./snailjobServer dockerfile: dockerfile ports: - \"9082:9082\" - \"17888:17888\" volumes: - /etc/localtime:/etc/localtime:ro # 添加日志限制,也可以全局限制,和services同级 logging: driver: \"json-file\" options: max-size: \"100m\" max-file: \"1\"
读者朋友也可以不用docker部署,这完全看个人需求,核心是snail-job-server-exec.jar这个jar包,至于怎么部署使用随便。
snail-job-server-exec.jar
这个就是我们package后的可执行jar包了,里面包含服务端的前端页面内容,具体位置如下

笔者使用的工具是IDEA,打包时操作如下图所示,注意一定要勾选skipFrontend,否则就会打包失败,前面说了前端并没有给源码,对此笔者也无力吐槽,毕竟人家搞出来这个东西就是为了挣钱的,免费给你用也还算可以了,要求不要太高哈。

服务端启动登录
对面上面的配置,使用IDEA启动后登录界面如下
浏览器访问地址:http://localhost:9082/snail-job

默认登录用户admin,密码也是admin,可以修改密码,也可以新增普通用户进行权限管控,这个后面再说,先登录进去

 可以看到一个在线机器,为服务端机器,同时nacos服务也多了一个snail-job-server的服务

新建命名空间
这个只有admin账户或者拥有管理员权限的账户才能新建,有一个默认的default,但我不想用,所以自己建了,名字什么的更专业点。

新建执行器组
上面选择新建的命名空间,然后新增执行器组,这里的token后面客户端会用到,比较重要


新建用户

 这个比较简单了,就是新建用户,赋予对应执行器组的权限,也可以对admin用户密码进行修改,不再叙述。
告警通知配置
这个主要是用来针对定时任务执行异常时的通知配置,可以配置通知人,通知方式等,实际使用中一般是,针对某个组的某个定时任务去配置,设置对应的通知人。

定时任务配置
这个是核心了,支持按照固定时间间隔、cron表达式、指定时间点、工作流方式,具体请看官方文档介绍,十分详细了。
 
任务类型一般就是默认集群。集群模式下,一个任务会根据路由策略只打到一个节点上执行。
 执行器选择自定义执行器,里面的名称是我们在客户端代码些定时任务逻辑时对应的,不是乱填的。
还要注意的是超时时间的设置,如果你的任务执行时间比较长,那要注意把超时时间调的大点,否则会执行失败,超时

四、客户端开发部署
这部分内容需要我们自己在应用内开发了,也非常简单,就拿笔者的微服务项目来展示吧

创建一个snail-job的客户端微服务,也是要注册到nacos当中去的,pom文件主要内容如下
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency><groupId>com.aizuda</groupId><artifactId>snail-job-client-starter</artifactId><version>${snail-job.version}</version></dependency><dependency><groupId>com.aizuda</groupId><artifactId>snail-job-client-job-core</artifactId><version>${snail-job.version}</version></dependency><dependency><groupId>com.aizuda</groupId><artifactId>snail-job-client-retry-core</artifactId><version>${snail-job.version}</version></dependency>
上面只是列举的必要的依赖,可能还需要其他依赖,比如数据库驱动等依赖。这个服务就是客户端服务了,客户端的应用端口我设置成了9081,yml配置文件如下
server: port: 9081spring: application: name: usteu-snailjob profiles: active: @activatedProperties@ cloud: nacos: # 注册中心 discovery: server-addr: @nacosAddress@ namespace: @nacosNamespace@ group: DEFAULT_GROUP username: nacos password: KDNT_dev666 # 配置中心 config: server-addr: @nacosAddress@ namespace: @nacosNamespace@ group: DEFAULT_GROUP username: nacos password: KDNT_dev666 file-extension: yml shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
其中还有部分配置,我是放在nacos中了,关键配置如下:
snail-job: # 任务调度服务器信息 server: # 服务器IP地址(或域名);集群时建议通过 nginx 做负载均衡 host: 127.0.0.1 # 服务器通讯端口(不是后台管理页面服务端口) port: 17888 # 命名空间 【上面配置的空间的唯一标识】 namespace: vL4pfYMz8vFf5m0PXItAVK_aA9pGpH6L # 接入组名【上面配置的组名称】注意: 若通过注解配置了这里的配置不生效 group: usteu_group # 接入组 token 【上面配置的token信息】 token: SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj # 客户端绑定IP,必须服务器可以访问到;默认自动推断,在服务器无法调度客户端时需要手动配置 host: 127.0.0.1 # 指定客户端通讯端口,不配置默认 1789 port: 1789 # 是否开启 enabled: true
这里客户端配置也是需要写上服务端的ip地址和通信端口号的,之前说过是17888,剩下的命名空间,组名称,以及执行器组对应的token,都可以登录服务端管理界面去查找,客户端的通讯端口,笔者这里配置的是1789,默认值也是这个。
定时任务编写
我根据官方提供的案例,写了一个测试的定时任务
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;import com.aizuda.snailjob.client.job.core.dto.JobArgs;import com.aizuda.snailjob.client.model.ExecuteResult;import com.aizuda.snailjob.common.log.SnailJobLog;import org.springframework.stereotype.Component;/** * 测试定时任务 */@Component@JobExecutor(name = \"testJob\")public class TestJob { public ExecuteResult jobExecute(JobArgs jobArgs) { SnailJobLog.REMOTE.info(\"哈哈,测试成功了:\"+jobArgs.getJobId() + \" \" + jobArgs.getJobParams()); return ExecuteResult.success(); }}
这个@JobExecutor(name = “testJob”)就是核心注解了,testJob即为我们在服务端进行定时配置时设置的执行器名称,要对应上。
另外要注意的点是,再定义其他定时器时,方法名不要修改统一都是jobExecute,否则会执行报错,我估计是反射时会寻找这个固定的执行方法吧。具体的定时任务执行业务逻辑,在方法内部写即可。
如果你真想改的话,按照下面的方式改(我是觉得没有什么必要改):
@JobExecutor(name = \"testJob\",method=自定义方法名)
附上一张测试结果图

五、总结
以上内容简单介绍了snail-job的使用方法和集成步骤,具体细节,如有不明白的,可参考官方文档,学习测试。希望本文可以给各位带来帮助。


