前言
从事开发工作的同学,应该对定时任务的概念并不陌生,就是我们的系统在运行过程中能够自动执行的一些任务、工作流程,无需人工干预。常见的使用场景包括:数据库的定时备份、文件系统的定时上传云端服务、每天早上的业务报表数据生成等等,实现方式也是层出不穷,对于简单的任务,我们可能在代码里写个死循环一直判断是否触发执行即可,对于复杂的、或者定时任务的种类数量较多,需要严密监控管理的业务场景,我们需要一些专门的定时任务工具去处理。笔者用过的定时任务有三个,分别是QuartZ,XXL-JOB,Snail-Job,关于这三个工具,我做了一个表格用来区分其使用场景和特点。
对比维度 | Quartz | XXL-JOB | |
---|---|---|---|
项目定位 | 经典的 Java 定时任务框架,专注于任务调度核心能力 | 分布式任务调度平台,强调易用性和企业级特性 | 轻量级分布式任务调度框架,注重性能和简洁性 |
核心架构 | 单机 / 集群模式(基于数据库锁实现集群) | 中心化架构(调度中心 + 执行器) | 去中心化架构(无单独调度中心,节点对等) |
任务触发方式 | 基于 Cron 表达式、固定间隔、日历等 | 支持 Cron 表达式、固定间隔、任务依赖、API 触发 | 支持 Cron 表达式、固定间隔、事件触发 |
分布式能力 | 需手动配置集群(依赖数据库同步状态) | 原生支持分布式部署,自动负载均衡 | 原生分布式,节点动态发现,自动容错 |
任务管理 | 需通过代码配置,无可视化界面 | 提供 Web 控制台,支持任务 CRUD、启停、日志查看 | 轻量控制台,支持基础任务管理和监控 |
失败重试 | 支持简单重试策略(需手动配置) | 内置失败重试机制,可配置重试次数和间隔 | 支持自定义重试策略,默认失败告警 |
任务依赖 | 不直接支持,需手动实现依赖逻辑 | 支持任务链式依赖、父子任务关系 | 支持简单任务依赖,基于事件驱动 |
监控告警 | 无内置监控,需集成第三方工具(如 Prometheus) | 内置监控面板,支持邮件、钉钉等告警方式 | 支持指标暴露(适配 Prometheus),告警集成 |
性能表现 | 单机性能稳定,集群模式下受数据库性能限制 | 调度中心性能优异,支持高并发任务触发 | 去中心化设计,减少网络开销,性能高效 |
学习成本 | 中等(需理解 Job、Trigger、Scheduler 等核心概念) | 低(文档丰富,开箱即用,API 简洁) | 低(设计简洁,源码轻量,易于上手) |
适用场景 | 中小型应用、非分布式环境,或作为底层调度组件 | 中大型分布式系统、企业级应用,需可视化管理 | 微服务架构、轻量级应用、对性能敏感的场景 |
生态集成 | 可集成 Spring、Spring Boot 等框架 | 深度集成 Spring 生态,支持 Docker、K8s 部署 | 支持 Spring Boot 自动配置,适配主流框架 |
社区活跃度 | 老牌项目,社区成熟但更新较慢 | 社区活跃,更新频繁,问题响应及时 | 新兴项目,社区正在成长,维护积极 |
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)中也较为常见,是分布式系统架构的典型优化思路。
三、服务端集成部署
代码拉取地址如下
# Gitee
git clone https://gitee.com/aizuda/snail-job.git
# GitHub
git clone https://github.com/aizuda/snail-job.git
国内用户直接使用gitee地址拉取就行,项目结构如下
项目拉下来后,笔者因为要和自己的微服务集成,所以对源码做了一些简单的修改,比如docker文件夹,就是我新加的,docker相关配置内容,dockerfile等。
还有就是,我把这个后端的服务也集成了nacos,使得能成功注册到nacos中去,以便能和客户端通信。下面是具体操作步骤。
引入nacos
如上图所示,在源码的相关路径下引入nacos依赖,一个是注册中心配置,一个是配置中心,这里的配置中心我其实是没有使用了,即只是把服务注册到了nacos上
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2023.0.3.2</version>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<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-job
spring:
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: true
logging:
config: classpath:logback-boot.xml
# level:
# ## 方便调试 SQL
# com.aizuda.snailjob.template.datasource.persistence.mapper: debug
snail-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
# author
MAINTAINER usteu
ENV 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文件主要内容如下
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--snail-job相关组件-->
<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: 9081
spring:
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,否则会执行报错,我估计是反射时会寻找这个固定的执行方法吧。具体的定时任务执行业务逻辑,在方法内部写即可。
附上一张测试结果图
五、总结
以上内容简单介绍了snail-job的使用方法和集成步骤,具体细节,如有不明白的,可参考官方文档,学习测试。希望本文可以给各位带来帮助。