XXL-JOB 分布式任务调度平台深度学习指南
版本 : v1.1 | 日期 : 2026-06-03 | 作者 : 江华森
更新 : 2026-06-03 23:21 --- 新增真实云环境部署验证 + 6个踩坑记录
适用版本 : XXL-JOB 2.4.x ~ 2.5.0 | JDK : 8 / 17 | Spring Boot: 2.x / 3.x
目录
- 一、基础认知篇
- [1.1 什么是 XXL-JOB](#1.1 什么是 XXL-JOB)
- [1.2 为什么需要分布式任务调度](#1.2 为什么需要分布式任务调度)
- [1.3 核心架构组件](#1.3 核心架构组件)
- 二、环境准备篇
- [2.1 前期准备](#2.1 前期准备)
- [2.2 源码下载与项目结构](#2.2 源码下载与项目结构)
- 三、上机实战篇
- [3.1 调度中心搭建](#3.1 调度中心搭建)
- [3.2 执行器开发实战](#3.2 执行器开发实战)
- [3.3 任务开发实战](#3.3 任务开发实战)
- [3.4 Web 界面操作实战](#3.4 Web 界面操作实战)
- [3.5 进阶功能实战](#3.5 进阶功能实战)
- [3.6 集群部署实战](#3.6 集群部署实战)
- [3.7 真实云环境部署验证](#3.7 真实云环境部署验证) 🆕
- 四、高级应用篇
- [4.1 性能优化](#4.1 性能优化)
- [4.2 安全配置](#4.2 安全配置)
- [4.3 监控告警](#4.3 监控告警)
- 五、项目实战案例
- [5.1 数据同步任务](#5.1 数据同步任务)
- [5.2 报表生成系统](#5.2 报表生成系统)
- [5.3 缓存预热任务](#5.3 缓存预热任务)
- [5.4 批量数据处理](#5.4 批量数据处理)
- 六、常见问题与解决方案
- [6.1 连接问题](#6.1 连接问题)
- [6.2 任务执行问题](#6.2 任务执行问题)
- [6.3 性能问题](#6.3 性能问题)
- [6.4 真实部署踩坑记录](#6.4 真实部署踩坑记录) 🆕
- 七、学习资源推荐
- 附录A:完整配置参考
- [附录B:Cron 表达式速查表](#附录B:Cron 表达式速查表)
- 附录C:版本差异对照表
一、基础认知篇
1.1 什么是 XXL-JOB?
XXL-JOB ("XXL" 取自作者许雪里(Xu Xue Li)姓名首字母)是一个分布式任务调度平台 ,其核心设计目标是:开发迅速、学习简单、轻量级、易扩展。
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 平台定位 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 数据同步 │ │ 报表生成 │ │ 缓存刷新 │ │ 消息推送 │ │
│ │ MySQL→ES │ │ 日/周/月 │ │ Redis │ │ 钉钉/邮件 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └───────────────┼───────────────┼───────────────┘ │
│ │ │ │
│ ┌────▼───────────────▼────┐ │
│ │ XXL-JOB 调度中心 │ │
│ │ 统一管理·调度·监控·告警 │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
核心特性一览
| 特性 | 说明 | 优势 |
|---|---|---|
| 简单易用 | 通过 Web 页面操作,无需复杂配置 | 10分钟即可上手 |
| 动态管理 | 支持动态添加、修改、删除任务 | 无需重启服务 |
| 分布式调度 | 支持集群部署,保证调度中心 HA | 消除单点故障 |
| 弹性扩容 | 执行器集群支持动态上下线 | 随业务量弹性伸缩 |
| 任务分片 | 支持分片广播,实现并行处理 | 大数据量高效处理 |
| 失败重试 | 支持任务失败自动重试 | 提高任务成功率 |
| 监控告警 | 邮件/钉钉告警,实时掌握任务状态 | 故障及时发现 |
| GLUE 模式 | 支持在线编辑代码,实时生效 | 开发效率高 |
| 路由策略 | 轮询、随机、一致性哈希、最不经常使用等 | 灵活调度 |
XXL-JOB vs 其他调度框架
| 对比维度 | XXL-JOB | Elastic-Job | Quartz | Spring @Scheduled |
|---|---|---|---|---|
| 调度中心 | ✅ 独立 Admin | ✅ 基于 ZK | ❌ 无 | ❌ 无 |
| 可视化管理 | ✅ 完善 Web UI | ⚠️ 需集成 | ❌ 无 GUI | ❌ 无 |
| 集群高可用 | ✅ DB 锁 | ✅ ZK 选举 | ⚠️ 需 JDBC | ❌ 单点 |
| 任务分片 | ✅ 支持 | ✅ 支持 | ❌ 需自行实现 | ❌ 不支持 |
| 失败重试 | ✅ 内置 | ✅ 内置 | ⚠️ 需配置 | ❌ 不支持 |
| GLUE 在线编辑 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
| 运维成本 | 🟢 低 | 🟡 中(需 ZK) | 🟡 中 | 🟢 极低 |
| 社区活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
小结 :对于大多数微服务架构下的定时任务场景,XXL-JOB 在功能完整性 和运维简便性上达到了很好的平衡,这也是它成为国内最流行的分布式任务调度框架的原因。
1.2 为什么需要分布式任务调度?
传统定时任务的问题
┌─────────────────────────────────────────────────────────────────┐
│ 传统定时任务困境 vs 分布式调度解决方案 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 问题1: 单点故障风险 │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ @Scheduled(cron) │ ❌ │ 调度中心集群 + 执行器集群 │ │
│ │ 单台机器挂了→任务停 │ ──▶ │ 任一节点故障不影响整体 │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
│ 问题2: 任务执行不均 │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ 所有任务挤在一台机器 │ ❌ │ 路由策略分散到多台执行器 │ │
│ │ CPU/内存 过载 │ ──▶ │ 负载均衡 + 弹性扩容 │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
│ 问题3: 缺乏监控告警 │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ 任务失败没人知道 │ ❌ │ 实时监控 + 邮件/钉钉告警 │ │
│ │ 只能靠人工巡检 │ ──▶ │ 任务失败自动通知 │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
│ 问题4: 难以扩展 │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ 业务增长→单机扛不住 │ ❌ │ 执行器动态注册/注销 │ │
│ │ 改代码→重启→风险 │ ──▶ │ 秒级弹性伸缩 │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
微服务架构下的核心需求
在微服务架构中,每个服务都可能包含定时任务,XXL-JOB 提供了统一的调度入口:
┌──────────────────┐
│ XXL-JOB Admin │ ← 统一调度中心(Web管理)
│ (调度中心集群) │
└──────┬───────────┘
│ HTTP/RPC 通信
┌───────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──▼────────┐ ┌───▼──────────┐
│ 订单服务 │ │ 用户服务 │ │ 报表服务 │
│ Executor-1 │ │ Executor-2│ │ Executor-3 │
│ :9999 │ │ :9998 │ │ :9997 │
└──────────────┘ └───────────┘ └───────────────┘
1.3 核心架构组件
XXL-JOB 采用中心化调度设计,由两大核心组件构成:
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 核心架构全景图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 调度中心 (xxl-job-admin) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 任务管理 │ │ 调度触发 │ │ 执行器管理 │ │ 日志管理 │ │ │
│ │ │ CRUD/启停 │ │ Cron解析 │ │ 注册发现 │ │ 日志查询 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 告警中心 │ │ 权限控制 │ │ 路由策略 │ │ 用户管理 │ │ │
│ │ │ 邮件/钉钉 │ │ AccessToken│ │ 10+种策略 │ │ 角色权限 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ 存储层: MySQL 数据库(任务配置、执行日志、调度锁) │ │
│ └───────────────────────┬──────────────────────────────────┘ │
│ │ │
│ │ HTTP 通信 (调度请求/心跳/回调) │
│ │ │
│ ┌───────────────────────┴──────────────────────────────────┐ │
│ │ 执行器集群 (xxl-job-executor) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Executor-1 │ │ Executor-2 │ │ Executor-3 │ │ │
│ │ │ port: 9999 │ │ port: 9999 │ │ port: 9999 │ │ │
│ │ │ JobHandler │ │ JobHandler │ │ JobHandler │ │ │
│ │ │ ├─ job1 │ │ ├─ job1 │ │ ├─ job1 │ │ │
│ │ │ ├─ job2 │ │ ├─ job2 │ │ ├─ job2 │ │ │
│ │ │ └─ job3 │ │ └─ job3 │ │ └─ job3 │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ 注册方式: 自动注册(心跳维持)/ 手动录入 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
三大核心概念详解
| 概念 | 英文 | 职责 | 类比 |
|---|---|---|---|
| 调度中心 | Admin / Scheduler | 管理任务、触发调度、收集结果、发送告警 | 指挥官 |
| 执行器 | Executor | 接收调度指令、执行任务逻辑、回报结果 | 士兵 |
| 任务 | Job / JobHandler | 具体的业务逻辑单元,由 @XxlJob 注解标识 |
作战任务 |
调度流程(一次完整的任务执行)
时间轴
─────────────────────────────────────────────────────────────────▶
[调度中心] [执行器]
│ │
│ ① Cron 表达式触发时间到 │
│ ┌─ 查询待执行任务列表 │
│ ├─ 获取执行器地址(路由策略) │
│ └─ 构建调度请求 │
│ │
│ ② 发送 HTTP 调度请求 ──────────▶ │
│ │ ③ 接收请求
│ │ ├─ 解析任务ID+参数
│ │ ├─ 匹配 JobHandler
│ │ └─ 创建执行线程
│ │
│ │ ④ 执行业务逻辑
│ │ ├─ XxlJobHelper.log()
│ │ └─ 异常处理
│ │
│ ⑤ 接收执行结果 ◀──────────────── │ ⑥ 回调结果
│ ├─ 记录执行日志 │ └─ handleSuccess/Fail
│ ├─ 判断是否需要重试 │
│ ├─ 触发子任务(如有) │
│ └─ 发送告警(如失败) │
│ │
二、环境准备篇
2.1 前期准备
实操1:环境检查清单
在开始 XXL-JOB 之旅前,请确保以下环境已就绪:
┌─────────────────────────────────────────────────────────────────┐
│ 环境检查清单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ✅ JDK 8 / JDK 17 │
│ → java -version │
│ → 推荐: JDK 17 (XXL-JOB 2.5.0+ 强制要求) │
│ → 兼容: JDK 8 (XXL-JOB 2.4.x 及以下) │
│ │
│ ✅ Maven 3.6+ │
│ → mvn -version │
│ │
│ ✅ MySQL 5.7+ / MySQL 8.0 │
│ → mysql -V │
│ → 需要创建 xxl_job 数据库 │
│ │
│ ✅ Git (可选,用于克隆源码) │
│ → git --version │
│ │
│ ✅ IDE: IntelliJ IDEA / Eclipse │
│ → 推荐 IDEA Community/Ultimate │
│ │
└─────────────────────────────────────────────────────────────────┘
环境验证命令:
bash
# JDK 版本检查
java -version
# 期望输出: openjdk version "17.0.x" 或 "1.8.0_xxx"
# Maven 版本检查
mvn -version
# 期望输出: Apache Maven 3.6.x+
# MySQL 连接检查
mysql -u root -p -e "SELECT VERSION();"
# 期望输出: 5.7.x 或 8.0.x
实操2:源码下载与项目结构
bash
# ========================================
# 方式一:克隆 JDK 17 最新版本(推荐,2.5.0+)
# ========================================
git clone https://gitee.com/xuxueli0323/xxl-job.git
cd xxl-job
# ========================================
# 方式二:克隆 JDK 8 兼容版本(2.4.x)
# ========================================
git clone -b 2.4.2 https://gitee.com/xuxueli0323/xxl-job.git xxl-job-2.4.2
2.2 项目结构解析
xxl-job/
├── xxl-job-admin/ ← 调度中心(核心模块)
│ ├── src/main/java/
│ │ └── com/xxl/job/admin/
│ │ ├── controller/ ← Web 控制器
│ │ │ ├── JobInfoController.java ← 任务管理
│ │ │ ├── JobGroupController.java ← 执行器管理
│ │ │ ├── JobLogController.java ← 日志查看
│ │ │ └── UserController.java ← 用户管理
│ │ ├── core/
│ │ │ ├── scheduler/ ← 调度引擎 (MisfireStrategy, etc.)
│ │ │ ├── trigger/ ← 触发器 (XxlJobTrigger)
│ │ │ ├── route/ ← 路由策略 (ExecutorRouter)
│ │ │ └── thread/ ← 调度线程
│ │ └── dao/ ← 数据访问层 (MyBatis)
│ └── src/main/resources/
│ ├── application.properties ← 核心配置
│ ├── mybatis-mapper/ ← SQL 映射
│ └── static/ ← 前端资源 (Layui)
│
├── xxl-job-core/ ← 公共核心依赖(执行器引用此模块)
│ └── src/main/java/
│ └── com/xxl/job/core/
│ ├── biz/
│ │ ├── ExecutorBiz.java ← 执行器接口
│ │ └── AdminBiz.java ← 调度中心接口
│ ├── handler/
│ │ └── IJobHandler.java ← 任务处理器抽象
│ ├── executor/
│ │ └── XxlJobExecutor.java ← 执行器核心
│ └── thread/ ← 线程模型
│
├── xxl-job-executor-samples/ ← 执行器示例(参考项目)
│ ├── xxl-job-executor-sample-springboot/ ← Spring Boot 版本 ★
│ └── xxl-job-executor-sample-frameless/ ← 无框架版本
│
└── doc/ ← 文档
├── db/
│ └── tables_xxl_job.sql ← 数据库初始化脚本 ★
└── images/ ← 文档截图
提示 : 上机实战中,我们主要操作 xxl-job-admin (调度中心)和参考 xxl-job-executor-sample-springboot(执行器)。
三、上机实战篇
3.1 调度中心搭建
实操3:数据库初始化
Step 1 - 连接 MySQL 并创建数据库:
sql
-- 登录 MySQL
mysql -u root -p
-- 创建数据库(UTF-8 字符集)
CREATE DATABASE IF NOT EXISTS xxl_job
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
-- 查看是否创建成功
SHOW DATABASES LIKE 'xxl_job';
Step 2 - 执行初始化脚本:
bash
# 进入项目目录
cd xxl-job/doc/db
# 执行建表脚本
mysql -u root -p xxl_job < tables_xxl_job.sql
Step 3 - 验证表结构:
sql
USE xxl_job;
SHOW TABLES;
-- 预期输出:
-- +------------------------+
-- | Tables_in_xxl_job |
-- +------------------------+
-- | xxl_job_group | ← 执行器组
-- | xxl_job_info | ← 任务信息
-- | xxl_job_lock | ← 分布式锁
-- | xxl_job_log | ← 调度日志
-- | xxl_job_log_report | ← 日志报表
-- | xxl_job_logglue | ← GLUE 日志
-- | xxl_job_registry | ← 执行器注册表
-- | xxl_job_user | ← 用户表
-- +------------------------+
核心表说明:
| 表名 | 用途 | 关键字段 |
|---|---|---|
xxl_job_group |
执行器组信息 | app_name, title, address_type, address_list |
xxl_job_info |
任务定义 | job_group, job_desc, executor_handler, schedule_conf |
xxl_job_lock |
调度中心分布式锁 | lock_name(保证集群只有一个节点调度) |
xxl_job_log |
任务执行日志 | job_id, trigger_time, handle_time, trigger_code |
xxl_job_registry |
执行器注册信息 | registry_group, registry_key, registry_value |
xxl_job_user |
用户账号 | username, password, role |
实操4:配置调度中心
编辑 xxl-job-admin/src/main/resources/application.properties(或 application.yml):
properties
# ============================================
# 1. 服务端口
# ============================================
server.port=8080
server.servlet.context-path=/xxl-job-admin
# ============================================
# 2. 数据库配置(★必须修改★)
# ============================================
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=your_password_here
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP-XXL-JOB
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000
# ============================================
# 3. 邮件告警配置
# ============================================
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=your_email@qq.com
spring.mail.password=your_qq_smtp_auth_code # QQ邮箱需用授权码,非登录密码
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.from=your_email@qq.com
# ============================================
# 4. 调度中心核心参数
# ============================================
# 调度中心 AccessToken(执行器需匹配)
xxl.job.accessToken=
# 调度中心国际化设置 (zh_CN / en)
xxl.job.i18n=zh_CN
# 调度线程池最大线程数
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
# 日志保留天数
xxl.job.logretentiondays=30
配置参数详解:
| 参数 | 含义 | 建议值 |
|---|---|---|
server.port |
Admin Web 端口 | 8080 |
spring.datasource.url |
数据库连接 | 根据实际修改 |
spring.datasource.hikari.maximum-pool-size |
连接池大小 | 30(小型)/ 100(大型) |
spring.mail.host |
邮件服务器 | smtp.qq.com / smtp.163.com |
xxl.job.accessToken |
调度中心认证令牌 | 生产环境必须设置 |
xxl.job.triggerpool.fast.max |
快线程池大小 | 200(默认) |
xxl.job.triggerpool.slow.max |
慢线程池大小 | 100(默认) |
xxl.job.logretentiondays |
日志保留天数 | 30 |
踩坑提醒 : QQ 邮箱的
spring.mail.password不是 QQ 登录密码,而是开启 SMTP 服务后生成的授权码。
实操5:启动调度中心
方式一:IDEA 直接运行
1. 打开 IDEA → Import Project → 选择 xxl-job 目录
2. 等待 Maven 依赖下载完成
3. 找到 com.xxl.job.admin.XxlJobAdminApplication
4. 右键 → Run 'XxlJobAdminApplication'
5. 观察控制台输出:
→ Starting XxlJobAdminApplication ...
→ Started XxlJobAdminApplication in 8.5 seconds
方式二:Maven 打包 + JAR 运行
bash
# 在 xxl-job 根目录下
cd xxl-job
# 打包(跳过测试)
mvn clean package -DskipTests
# 运行调度中心
java -jar xxl-job-admin/target/xxl-job-admin-2.5.0.jar
# 或者指定端口
java -jar xxl-job-admin/target/xxl-job-admin-2.5.0.jar --server.port=8081
验证启动:
浏览器访问: http://localhost:8080/xxl-job-admin
默认账号: admin
默认密码: 123456
期望看到: XXL-JOB 调度中心管理界面(Layui 风格)
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 调度中心登录成功 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 左侧导航菜单: │
│ ├─ 运行报表 → 任务执行统计概览 │
│ ├─ 任务管理 → 任务 CRUD + 启停 + 手动触发 │
│ ├─ 调度日志 → 任务执行历史 + 日志详情 │
│ ├─ 执行器管理 → 执行器注册/配置 │
│ ├─ 用户管理 → 用户 + 角色管理 │
│ └─ 使用教程 → 内置文档 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 执行器开发实战
实操6:创建 Spring Boot 执行器项目
项目结构:
xxl-job-executor-demo/
├── pom.xml
└── src/main/
├── java/com/example/xxljob/
│ ├── XxlJobDemoApplication.java ← 启动类
│ ├── config/
│ │ └── XxlJobConfig.java ← XXL-JOB 配置类
│ └── job/
│ ├── SimpleJob.java ← 简单任务
│ ├── AdvancedJob.java ← 高级任务
│ └── DataSyncJob.java ← 数据同步任务
└── resources/
├── application.yml ← 主配置
└── logback.xml ← 日志配置
Step 1 - pom.xml 添加依赖:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>xxl-job-executor-demo</artifactId>
<version>1.0.0</version>
<name>xxl-job-executor-demo</name>
<properties>
<java.version>1.8</java.version>
<xxl-job.version>2.5.0</xxl-job.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- XXL-JOB 核心依赖 ★★★ -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MySQL (如需在任务中操作数据库) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实操7:配置执行器
src/main/resources/application.yml:
yaml
server:
port: 8081 # 执行器自身端口(与 Admin 不同即可)
# ============================================
# XXL-JOB 执行器配置
# ============================================
xxl:
job:
# --- 调度中心配置 ---
admin:
# 调度中心地址(集群时逗号分隔)
addresses: http://127.0.0.1:8080/xxl-job-admin
# --- 执行器配置 ---
executor:
# 执行器 AppName(与 Admin 上配置的 AppName 一致)
appname: xxl-job-executor-demo
# 执行器地址(为空则自动获取本机 IP)
address:
# 执行器 IP(为空则自动获取,多网卡时建议手动指定)
ip:
# 执行器端口(默认 9999,与内嵌 Tomcat port 不同)
port: 9999
# 执行器运行日志存储路径
logpath: /data/applogs/xxl-job/jobhandler
# 日志保留天数
logretentiondays: 30
# --- 安全配置 ---
# 需与调度中心 accessToken 一致
accessToken: default_token
配置参数详解:
| 参数 | 含义 | 注意事项 |
|---|---|---|
xxl.job.admin.addresses |
调度中心地址 | 集群时用逗号分隔多个 URL |
xxl.job.executor.appname |
执行器标识名 | 必须与 Admin "执行器管理"中的 AppName 完全一致 |
xxl.job.executor.ip |
执行器 IP | 多网卡环境必须手动指定,否则可能注册错误 IP |
xxl.job.executor.port |
执行器通信端口 | 不与 server.port 冲突;集群中每个实例可用相同端口 |
xxl.job.executor.logpath |
日志目录 | 确保有写入权限 |
xxl.job.accessToken |
认证令牌 | 生产环境必须配置,与 Admin 一致 |
实操8:配置类编写
src/main/java/com/example/xxljob/config/XxlJobConfig.java:
java
package com.example.xxljob.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* XXL-JOB 执行器配置类
*
* 功能:
* 1. 创建 XxlJobSpringExecutor Bean
* 2. 自动注册到调度中心
* 3. 通过 @XxlJob 注解发现任务处理器
*
* @author xxl-job-demo
*/
@Configuration
public class XxlJobConfig {
private static final Logger log = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken:}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address:}")
private String address;
@Value("${xxl.job.executor.ip:}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job executor config init.");
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
// === 基础配置 ===
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appname);
executor.setAddress(address);
executor.setIp(ip);
executor.setPort(port);
// === 安全 ===
if (accessToken != null && !accessToken.isEmpty()) {
executor.setAccessToken(accessToken);
}
// === 日志 ===
executor.setLogPath(logPath);
executor.setLogRetentionDays(logRetentionDays);
return executor;
}
}
Spring Boot 启动类:
java
package com.example.xxljob;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class XxlJobDemoApplication {
public static void main(String[] args) {
SpringApplication.run(XxlJobDemoApplication.class, args);
System.out.println("========================================");
System.out.println(" XXL-JOB Executor Demo Started!");
System.out.println("========================================");
}
}
启动并验证:
bash
# 运行执行器
mvn spring-boot:run
# 期望日志输出:
# >>>>>>>>>>> xxl-job executor config init.
# >>>>>>>>>>> xxl-job, job executor start success.
# >>>>>>>>>>> xxl-job executor registry success, registryKey:xxl-job-executor-demo
3.3 任务开发实战
实操9:简单任务开发(BEAN 模式)
src/main/java/com/example/xxljob/job/SimpleJob.java:
java
package com.example.xxljob.job;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
/**
* 简单任务演示
*
* 包含:
* 1. 基础 BEAN 模式任务
* 2. 获取任务参数
* 3. 分片参数获取
* 4. 日志输出
*/
@Component
public class SimpleJob {
private static final Logger log = LoggerFactory.getLogger(SimpleJob.class);
/**
* 1、基础任务 ------ 最简单的 BEAN 模式任务
*
* @XxlJob("demoJobHandler") 中的值是 JobHandler 名称,
* 在 Admin 上配置任务时需要填写此名称。
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
// ========================================
// ① 获取任务参数
// ========================================
String jobParam = XxlJobHelper.getJobParam();
XxlJobHelper.log("【基础任务】开始执行");
XxlJobHelper.log("任务参数: {}", jobParam);
// ========================================
// ② 获取分片参数(即使不使用也可以获取)
// ========================================
int shardIndex = XxlJobHelper.getShardIndex(); // 当前分片序号(从0开始)
int shardTotal = XxlJobHelper.getShardTotal(); // 总分片数
XxlJobHelper.log("分片信息: 当前分片={}, 总分片={}", shardIndex, shardTotal);
// ========================================
// ③ 执行业务逻辑
// ========================================
String now = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
XxlJobHelper.log("业务处理时间: {}", now);
// 模拟业务处理耗时
TimeUnit.SECONDS.sleep(2);
XxlJobHelper.log("【基础任务】执行完成");
// 默认成功(方法正常结束即为成功)
}
/**
* 2、有参任务 ------ 根据参数执行不同逻辑
*/
@XxlJob("paramJobHandler")
public void paramJobHandler() throws Exception {
String jobParam = XxlJobHelper.getJobParam();
XxlJobHelper.log("【有参任务】接收到参数: {}", jobParam);
// 根据参数执行不同分支
switch (jobParam) {
case "sync_user":
syncUsers();
break;
case "sync_order":
syncOrders();
break;
case "clean_cache":
cleanCache();
break;
default:
XxlJobHelper.handleFail("未知的任务类型: " + jobParam);
return;
}
XxlJobHelper.handleSuccess("【有参任务】执行成功,参数=" + jobParam);
}
private void syncUsers() {
XxlJobHelper.log(">>> 同步用户数据...");
// 实际业务逻辑
}
private void syncOrders() {
XxlJobHelper.log(">>> 同步订单数据...");
// 实际业务逻辑
}
private void cleanCache() {
XxlJobHelper.log(">>> 清理缓存...");
// 实际业务逻辑
}
}
实操10:复杂任务开发(分片 + 失败重试)
src/main/java/com/example/xxljob/job/AdvancedJob.java:
java
package com.example.xxljob.job;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 高级任务演示
*
* 包含:
* 1. 分片广播任务
* 2. 失败重试任务
* 3. 子任务链
* 4. 超时控制
*/
@Component
public class AdvancedJob {
private static final Logger log = LoggerFactory.getLogger(AdvancedJob.class);
// 模拟数据源(实际场景可能是数据库查询结果)
private static final int TOTAL_RECORDS = 10000;
// 断点续传进度记录
private static final ConcurrentHashMap<String, AtomicInteger> progressMap =
new ConcurrentHashMap<>();
/**
* 1、分片广播任务
*
* 使用场景:
* - 大数据量批量处理(如 100万 条数据分 10 片并行处理)
* - 多台机器同时处理不同数据段
*
* 路由策略必须选择: "分片广播"
*/
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {
// 获取分片参数
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("【分片广播任务】启动: 分片 {}/{}", shardIndex + 1, shardTotal);
// ========================================
// 分片算法:每个分片处理属于自己的数据
// 原理: 数据ID % shardTotal == shardIndex
// ========================================
int processedCount = 0;
int failedCount = 0;
for (int i = 0; i < TOTAL_RECORDS; i++) {
// 分片判定:当前数据属于本分片
if (i % shardTotal == shardIndex) {
try {
// 模拟处理一条数据
processRecord(i);
processedCount++;
} catch (Exception e) {
failedCount++;
XxlJobHelper.log("记录 {} 处理失败: {}", i, e.getMessage());
}
}
}
String result = String.format(
"【分片广播任务】完成: 分片=%d/%d, 处理=%d, 失败=%d",
shardIndex + 1, shardTotal, processedCount, failedCount
);
XxlJobHelper.log(result);
if (failedCount > 0) {
XxlJobHelper.handleFail(result);
} else {
XxlJobHelper.handleSuccess(result);
}
}
/**
* 模拟处理单条记录
*/
private void processRecord(int id) throws Exception {
// 模拟大部分成功,少量失败
if (id % 1007 == 0) { // 约 1% 失败率
throw new Exception("数据异常: id=" + id);
}
// 业务处理逻辑
}
/**
* 2、失败重试任务
*
* 配置要求:
* - Admin 上配置 "失败重试次数"(如 3 次)
* - 在 catch 中重新抛出异常(调度中心才会触发重试)
*/
@XxlJob("retryJobHandler")
public void retryJobHandler() throws Exception {
XxlJobHelper.log("【重试任务】开始执行, 请确保在Admin上配置了失败重试次数");
try {
String jobParam = XxlJobHelper.getJobParam();
// ========================================
// 核心业务逻辑
// ========================================
boolean result = callExternalApi(jobParam);
if (!result) {
// 注意: 必须 throw Exception 才能触发重试机制!
// 如果只调用 handleFail 但方法正常返回,不会重试
throw new RuntimeException("外部API调用失败,参数=" + jobParam);
}
XxlJobHelper.handleSuccess("【重试任务】成功");
} catch (Exception e) {
XxlJobHelper.log("【重试任务】执行异常: {}", e.getMessage());
// 记录失败原因,抛出异常触发重试
XxlJobHelper.handleFail(e.getMessage());
throw e; // ← 关键: 必须抛出异常才能触发调度中心的重试机制
}
}
/**
* 模拟调用外部 API(可能失败的场景)
*/
private boolean callExternalApi(String param) {
// 模拟:参数包含 "success" 时成功
return param != null && param.contains("success");
}
/**
* 3、断点续传任务
*
* 使用场景:
* - 大批量数据处理,意外中断后可从上次进度继续
* - 避免重复处理已处理的数据
*/
@XxlJob("checkpointJobHandler")
public void checkpointJobHandler() throws Exception {
String jobKey = "data_migration";
AtomicInteger progress = progressMap.computeIfAbsent(
jobKey, k -> new AtomicInteger(0));
int startIndex = progress.get();
int batchSize = 100;
XxlJobHelper.log("【断点续传】从索引 {} 开始,批量大小 {}", startIndex, batchSize);
for (int i = startIndex; i < startIndex + batchSize && i < TOTAL_RECORDS; i++) {
// 处理数据...
// 每处理一条更新进度
progress.set(i + 1);
}
int currentProgress = progress.get();
XxlJobHelper.log("【断点续传】当前进度: {}/{}", currentProgress, TOTAL_RECORDS);
if (currentProgress >= TOTAL_RECORDS) {
progressMap.remove(jobKey); // 完成后清理
XxlJobHelper.handleSuccess("【断点续传】全部完成");
} else {
XxlJobHelper.handleSuccess(
String.format("【断点续传】进度 %d/%d,等待下次执行", currentProgress, TOTAL_RECORDS)
);
}
}
}
任务开发要点总结
| 要点 | 说明 | 代码示例 |
|---|---|---|
| 获取参数 | XxlJobHelper.getJobParam() |
在 Admin 任务配置中填写参数 |
| 输出日志 | XxlJobHelper.log("msg {}", arg) |
日志会上报到 Admin 查看 |
| 成功返回 | XxlJobHelper.handleSuccess("msg") |
可选的,默认方法正常结束=成功 |
| 失败返回 | XxlJobHelper.handleFail("msg") + throw |
两者都需要才能触发重试 |
| 分片参数 | getShardIndex() / getShardTotal() |
仅在路由策略为"分片广播"时有效 |
3.4 Web 界面操作实战
实操11:执行器管理配置
登录调度中心 http://localhost:8080/xxl-job-admin,进入 执行器管理 → 新增:
┌─────────────────────────────────────────────────────────────────┐
│ 新增执行器 - 配置表单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ AppName: xxl-job-executor-demo │
│ ↑ 必须与执行器 application.yml 中的 │
│ xxl.job.executor.appname 完全一致! │
│ │
│ 名称: 示例执行器 │
│ ↑ 管理界面的显示名称 │
│ │
│ 注册方式: ● 自动注册 │
│ ○ 手动录入 │
│ ↑ 推荐使用自动注册,执行器启动后自动发现 │
│ │
│ 机器地址: (留空) │
│ ↑ 自动注册模式下留空即可 │
│ │
└─────────────────────────────────────────────────────────────────┘
注册方式对比:
| 对比项 | 自动注册 | 手动录入 |
|---|---|---|
| 配置复杂度 | 🟢 低 | 🟡 中 |
| 执行器上下线 | 自动感知(心跳30s) | 需手动修改 |
| 适用场景 | 动态扩容/缩容 | 固定 IP 环境 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
实操12:任务配置详解
进入 任务管理 → 新增:
┌─────────────────────────────────────────────────────────────────┐
│ 新增任务 - 配置表单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ 基础信息 ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 执行器: [▼ 示例执行器] │ │
│ │ 任务描述: 用户数据同步任务 │ │
│ │ 负责人: admin │ │
│ │ 报警邮件: admin@example.com (多个用逗号分隔) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 调度配置 ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 调度类型: ● CRON ○ 固定间隔(秒) │ │
│ │ Cron: 0 0 2 * * ? ← 每天凌晨2点 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 任务配置 ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 运行模式: ● BEAN ○ GLUE(Java) ○ GLUE(Shell)... │ │
│ │ JobHandler: demoJobHandler │ │
│ │ ↑ 必须与代码中 @XxlJob("xxx") 注解一致 │ │
│ │ │ │
│ │ 任务参数: type=data_sync │ │
│ │ ↑ 可通过 XxlJobHelper.getJobParam() 获取 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 高级配置 ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 路由策略: [▼ 轮询] │ │
│ │ 子任务ID: 留空(如需任务链可填写) │ │
│ │ 阻塞处理策略: [▼ 单机串行] │ │
│ │ 任务超时时间: 0(0=不限制,单位秒) │ │
│ │ 失败重试次数: 3 │ │
│ │ 失败重试间隔: 60(秒,仅 GLUE 模式有效) │ │
│ │ 任务描述: 详细描述... │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
核心配置参数详解
① 运行模式对比:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| BEAN | 在 Java 代码中编写,部署到执行器 | 正式项目,推荐使用 |
| GLUE(Java) | 在 Admin Web 界面在线写 Java 代码 | 开发调试、临时任务 |
| GLUE(Shell) | 在线编写 Shell 脚本 | 服务器运维任务 |
| GLUE(Python) | 在线编写 Python 脚本 | 数据处理、脚本任务 |
| GLUE(PHP) | 在线编写 PHP 脚本 | PHP 项目 |
| GLUE(NodeJS) | 在线编写 Node.js 脚本 | 前端/Node 项目 |
| GLUE(PowerShell) | 在线编写 PowerShell 脚本 | Windows 环境 |
② 路由策略对比:
| 策略 | 说明 | 使用场景 |
|---|---|---|
| 第一个 | 固定选择第一个执行器 | 单执行器或指定执行器 |
| 最后一个 | 固定选择最后一个执行器 | 需要固定某台机器 |
| 轮询 | 依次分配到每个执行器 | 负载均衡,推荐 |
| 随机 | 随机选择一个执行器 | 简单负载均衡 |
| 一致性哈希 | 相同任务参数调度到同一执行器 | 需要粘性调度的场景 |
| 最不经常使用 | 选使用频率最低的执行器 | 负载均衡 |
| 最近最久未使用 | 选择最久未使用的执行器 | 负载均衡 |
| 故障转移 | 按顺序尝试,失败则下一个 | 高可用场景 |
| 忙碌转移 | 跳过正在执行任务的执行器 | 避免任务堆积 |
| 分片广播 | 所有执行器都执行,分片处理 | 大数据量并行处理 |
③ 阻塞处理策略:
| 策略 | 说明 | 行为 |
|---|---|---|
| 单机串行 | 当前任务未完成时,新调度丢弃 | 默认,保证执行顺序 |
| 丢弃后续调度 | 运行中则跳过 | 不关心遗漏 |
| 覆盖之前调度 | 终止当前运行,执行新的 | 只关心最新数据 |
实操13:任务运行与监控
任务操作按钮说明:
┌─────────────────────────────────────────────────────────────────┐
│ 任务列表操作栏 │
│ │
│ [启动] ─ 启用 Cron 自动调度 │
│ [暂停] ─ 暂停自动调度(任务仍可手动触发) │
│ [执行一次] ─ 手动触发一次执行 │
│ [查询日志] ─ 查看执行历史 │
│ [编辑] ─ 修改任务配置 │
│ [删除] ─ 删除任务 │
│ [GLUE IDE] ─ 打开在线编辑器(仅 GLUE 模式可见) │
│ │
└─────────────────────────────────────────────────────────────────┘
调度日志解读:
┌─────────────────────────────────────────────────────────────────┐
│ 调度日志详情 │
│ │
│ 任务ID: 2 │
│ 执行器地址: http://192.168.1.100:9999/ │
│ 调度时间: 2026-06-03 10:00:00 │
│ 调度结果: 成功 [调度成功] ← 调度中心→执行器的网络通信结果 │
│ 执行结果: 成功 [任务执行成功] ← 业务逻辑执行结果 │
│ 执行耗时: 2356ms │
│ │
│ 调度结果枚举: │
│ - 成功: 调度请求发送成功 │
│ - 失败: 网络不通/执行器未注册/AccessToken 不匹配 │
│ │
│ 执行结果枚举: │
│ - 成功: 任务正常完成 │
│ - 失败: 业务逻辑抛出异常 │
│ - 超时: 超过配置的超时时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.5 进阶功能实战
实操14:Cron 表达式完全指南
Cron 表达式由 7 个字段组成(XXL-JOB 支持到秒级):
┌──────────── 秒 (0-59)
│ ┌────────── 分 (0-59)
│ │ ┌──────── 时 (0-23)
│ │ │ ┌────── 日 (1-31)
│ │ │ │ ┌──── 月 (1-12)
│ │ │ │ │ ┌── 周 (1-7, 1=周日, 7=周六)
│ │ │ │ │ │
* * * * * *
常用 Cron 表达式速查:
| 场景 | Cron 表达式 | 说明 |
|---|---|---|
| 每秒执行 | * * * * * ? |
调试专用,生产慎用 |
| 每5秒执行 | 0/5 * * * * ? |
高频任务 |
| 每分钟执行 | 0 * * * * ? |
常规频率 |
| 每5分钟执行 | 0 0/5 * * * ? |
中频任务 |
| 每小时执行 | 0 0 * * * ? |
低频任务 |
| 每天凌晨2点 | 0 0 2 * * ? |
数据同步/备份 |
| 每天中午12点 | 0 0 12 * * ? |
日报生成 |
| 每周一9点 | 0 0 9 ? * 2 |
周报生成 |
| 每月1号0点 | 0 0 0 1 * ? |
月报生成 |
| 工作日9-18点每小时 | 0 0 9-18 ? * 2-6 |
工作时间执行 |
| 每年1月1日 | 0 0 0 1 1 ? |
年度任务 |
Cron 特殊字符:
| 字符 | 含义 | 示例 |
|---|---|---|
* |
所有值 | * * * * * ? 每秒 |
? |
不指定(日/周期互斥) | 日期和星期只能用 ? 占位 |
- |
范围 | 9-18 9点到18点 |
, |
枚举 | 1,3,5,7 |
/ |
递增步长 | 0/15 每15个单位 |
L |
最后 | L 当月最后一天 |
W |
最近工作日 | 15W 离15号最近的工作日 |
# |
第几个 | 2#3 第3个周一 |
实操15:任务失败重试机制
java
@Component
public class RetryJobDemo {
private static final int MAX_MANUAL_RETRY = 3;
/**
* 带手动重试的任务
*
* 重试机制原理:
* 1. Admin 配置 "失败重试次数"(如 3)
* 2. 方法内抛异常→ Admin 感知失败→ 自动重试
* 3. 可结合手动重试实现更灵活的策略
*/
@XxlJob("smartRetryJobHandler")
public void smartRetryJobHandler() throws Exception {
String jobParam = XxlJobHelper.getJobParam();
XxlJobHelper.log("【智能重试任务】开始, 参数={}", jobParam);
int retryCount = 0;
boolean success = false;
while (retryCount < MAX_MANUAL_RETRY && !success) {
try {
// ==================================
// 核心业务逻辑
// ==================================
success = executeBusinessLogic(jobParam);
if (success) {
XxlJobHelper.log("【智能重试】第{}次重试成功", retryCount);
XxlJobHelper.handleSuccess();
return;
}
retryCount++;
XxlJobHelper.log("【智能重试】第{}次重试失败,继续重试...", retryCount);
// 递增等待时间(退避策略)
long waitSeconds = retryCount * 10L;
XxlJobHelper.log("【智能重试】等待{}秒后重试", waitSeconds);
Thread.sleep(waitSeconds * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// 所有重试都失败
XxlJobHelper.log("【智能重试】经过{}次重试后仍然失败", MAX_MANUAL_RETRY);
XxlJobHelper.handleFail("业务处理失败,已重试" + MAX_MANUAL_RETRY + "次");
throw new RuntimeException("所有重试均失败");
}
private boolean executeBusinessLogic(String param) {
// 模拟:参数为 "success" 时成功
return "success".equals(param);
}
}
重试机制要点:
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 失败重试流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Admin 调度 ──▶ 执行器执行业务 │
│ │ │
│ ├── 成功 → handleSuccess() → 结束 │
│ │ │
│ └── 失败 → throw Exception │
│ │ │
│ ┌──────────────▼─────────────────┐ │
│ │ Admin 检测到失败 │ │
│ │ ├─ 重试次数 < 配置的重试次数? │ │
│ │ │ ├─ YES → 等待 → 重新调度 │ │
│ │ │ └─ NO → 标记最终失败 │ │
│ │ ├─ 发送告警邮件/钉钉 │ │
│ │ └─ 触发子任务(如有) │ │
│ └────────────────────────────────┘ │
│ │
│ ★ 关键: 必须在代码中 throw Exception 才会触发重试! │
│ 仅调用 handleFail() 但不抛出异常,Admin 会认为是成功。 │
│ │
└─────────────────────────────────────────────────────────────────┘
实操16:任务依赖与子任务链
子任务配置:
在 Admin 任务管理中:
1. 创建任务 A (数据同步) - ID=1
2. 创建任务 B (数据校验) - ID=2
3. 创建任务 C (发送报告) - ID=3
配置任务 A → 子任务ID填 "2"
配置任务 B → 子任务ID填 "3"
执行链路:
任务A完成 → 自动触发任务B → 自动触发任务C
(任何一环失败,后续任务不会触发)
java
@Component
public class TaskChainJob {
/**
* 任务 A: 数据同步(父任务)
*/
@XxlJob("dataSyncJob")
public void dataSyncJob() throws Exception {
XxlJobHelper.log("【任务A】开始数据同步...");
// 同步逻辑
boolean syncSuccess = performDataSync();
if (syncSuccess) {
XxlJobHelper.handleSuccess("数据同步成功,将自动触发校验任务");
} else {
XxlJobHelper.handleFail("数据同步失败");
throw new RuntimeException("数据同步失败");
}
}
/**
* 任务 B: 数据校验(子任务,由任务A触发)
*/
@XxlJob("dataVerifyJob")
public void dataVerifyJob() throws Exception {
XxlJobHelper.log("【任务B】开始数据校验...");
boolean verifySuccess = performDataVerification();
if (verifySuccess) {
XxlJobHelper.handleSuccess("数据校验成功,将自动触发报告任务");
} else {
XxlJobHelper.handleFail("数据校验失败");
throw new RuntimeException("数据校验失败");
}
}
/**
* 任务 C: 发送报告(子任务,由任务B触发)
*/
@XxlJob("reportSendJob")
public void reportSendJob() throws Exception {
XxlJobHelper.log("【任务C】开始发送报告...");
sendReport();
XxlJobHelper.handleSuccess("报告发送完成");
}
// === 模拟方法 ===
private boolean performDataSync() { return true; }
private boolean performDataVerification() { return true; }
private void sendReport() { /* 发送报告 */ }
}
3.6 集群部署实战
实操17:多执行器集群部署
┌─────────────────────────────────────────────────────────────────┐
│ 执行器集群架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ XXL-JOB Admin (调度中心) │ │
│ │ http://192.168.0.1:8080/xxl-job-admin │ │
│ └──────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ┌────────▼──────┐ ┌───▼────────┐ ┌──▼──────────┐ │
│ │ Executor-1 │ │ Executor-2 │ │ Executor-3 │ │
│ │ 192.168.0.11 │ │192.168.0.12│ │ 192.168.0.13 │ │
│ │ port: 9999 │ │ port: 9999 │ │ port: 9999 │ │
│ │ AppName: │ │ AppName: │ │ AppName: │ │
│ │ xxl-job-demo │ │ xxl-job-demo│ │ xxl-job-demo │ │
│ └───────────────┘ └────────────┘ └──────────────┘ │
│ │
│ 所有执行器的 AppName 必须相同,Admin 据此识别集群 │
│ │
└─────────────────────────────────────────────────────────────────┘
部署步骤:
bash
# ============================================
# 步骤1: Maven 打包
# ============================================
cd /path/to/xxl-job-executor-demo
mvn clean package -DskipTests
# ============================================
# 步骤2: 复制 JAR 到各台机器
# ============================================
scp target/xxl-job-executor-demo-1.0.0.jar user@192.168.0.11:/opt/xxl-job/
scp target/xxl-job-executor-demo-1.0.0.jar user@192.168.0.12:/opt/xxl-job/
scp target/xxl-job-executor-demo-1.0.0.jar user@192.168.0.13:/opt/xxl-job/
# ============================================
# 步骤3: 在各台机器上启动(注意: 每台机器的 IP 不同,但配置一样)
# ============================================
# 机器1
ssh user@192.168.0.11
java -jar /opt/xxl-job/xxl-job-executor-demo-1.0.0.jar \
--xxl.job.executor.ip=192.168.0.11 \
--xxl.job.executor.port=9999
# 机器2
ssh user@192.168.0.12
java -jar /opt/xxl-job/xxl-job-executor-demo-1.0.0.jar \
--xxl.job.executor.ip=192.168.0.12 \
--xxl.job.executor.port=9999
# 机器3
ssh user@192.168.0.13
java -jar /opt/xxl-job/xxl-job-executor-demo-1.0.0.jar \
--xxl.job.executor.ip=192.168.0.13 \
--xxl.job.executor.port=9999
验证集群:
在 Admin 管理界面 执行器管理 中可以看到:
┌─────────────────────────────────────────────────────────────────┐
│ 执行器: 示例执行器 (xxl-job-executor-demo) │
│ │
│ OnLine 机器地址: │
│ ├─ http://192.168.0.11:9999/ ← 自动注册 │
│ ├─ http://192.168.0.12:9999/ ← 自动注册 │
│ └─ http://192.168.0.13:9999/ ← 自动注册 │
│ │
│ 状态: 3台在线 │
└─────────────────────────────────────────────────────────────────┘
路由策略验证:
选用路由策略 [轮询]:
第1次调度 → 192.168.0.11:9999
第2次调度 → 192.168.0.12:9999
第3次调度 → 192.168.0.13:9999
第4次调度 → 192.168.0.11:9999 (循环)
选用路由策略 [分片广播]:
192.168.0.11:9999 → 处理数据 0,3,6,9...
192.168.0.12:9999 → 处理数据 1,4,7,10...
192.168.0.13:9999 → 处理数据 2,5,8,11...
实操18:调度中心集群 + Nginx 负载均衡
┌─────────────────────────────────────────────────────────────────┐
│ 调度中心集群 + Nginx 负载均衡 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 用户/执行器 │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Nginx │ 统一入口 │
│ │ :80/xxl- │ │
│ │ job-admin │ │
│ └──┬──────┬────┘ │
│ │ │ │
│ ┌──────────▼┐ ┌──▼──────────┐ │
│ │ Admin-1 │ │ Admin-2 │ │
│ │ :8080 │ │ :8081 │ │
│ └─────┬─────┘ └──────┬──────┘ │
│ │ │ │
│ └───────┬───────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ MySQL 数据库 │ │
│ │ (xxl_job 库) │ │
│ │ 分布式锁: │ │
│ │ xxl_job_lock表 │ │
│ └─────────────────┘ │
│ │
│ 高可用原理: │
│ - Nginx 实现负载均衡(用户/执行器入口) │
│ - MySQL 行锁 (SELECT ... FOR UPDATE) 保证只有一个 Admin 调度 │
│ - 任一台 Admin 宕机不影响整体,其他 Admin 自动接管 │
│ │
└─────────────────────────────────────────────────────────────────┘
Nginx 配置 (/etc/nginx/conf.d/xxl-job.conf):
nginx
upstream xxl-job-admin {
# Admin 集群节点
server 192.168.0.1:8080 weight=1 max_fails=3 fail_timeout=30s;
server 192.168.0.2:8080 weight=1 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name xxl-job.yourdomain.com;
# 日志
access_log /var/log/nginx/xxl-job-access.log;
error_log /var/log/nginx/xxl-job-error.log;
location /xxl-job-admin {
proxy_pass http://xxl-job-admin;
# 代理头设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
启动 Admin 集群:
bash
# 节点1
java -jar xxl-job-admin-2.5.0.jar --server.port=8080
# 节点2
java -jar xxl-job-admin-2.5.0.jar --server.port=8081
集群调度锁机制:
sql
-- XXL-JOB 使用 MySQL 行锁实现调度互斥
-- xxl_job_lock 表中只有一条记录:
SELECT * FROM xxl_job_lock;
-- +-----------------------+
-- | lock_name |
-- +-----------------------+
-- | schedule_lock | ← 调度锁
-- +-----------------------+
-- 每次调度前,Admin 执行:
-- SELECT * FROM xxl_job_lock WHERE lock_name = 'schedule_lock' FOR UPDATE;
-- 获取到锁的 Admin 负责本轮调度,其他 Admin 等待
-- 释放后下一个 Admin 获取锁...
3.7 真实云环境部署验证
本节为 2026-06-03 在华为云 ecs-f5eb 集群上的真实部署验证结果,所有 IP、配置、截图均为真实数据。
实操19:云环境完整部署
部署拓扑
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 真实云环境部署拓扑 (ecs-f5eb) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ xxljob-01 (ecs-f5eb-0001) │ │
│ │ 公网: 121.36.61.101 内网: 192.168.0.205 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ XXL-JOB Admin :8080 │ │ │
│ │ │ MySQL 8.0.46 :3306 │ │ │
│ │ │ JDK 17.0.19 + Maven 3.8.7 │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ HTTP (AccessToken: xxljob2024) │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌───────▼────────┐ ┌────▼────────┐ ┌────▼────────┐ │
│ │ xxljob-02 │ │ xxljob-03 │ │ xxljob-04 │ │
│ │ Executor-1 │ │ Executor-2 │ │ Executor-3 │ │
│ │ 公网: │ │ 公网: │ │ 公网: │ │
│ │ 120.46.133.64 │ │ 1.94.216.49│ │ 121.36.6.178│ │
│ │ :8081 (Web) │ │ :8081 │ │ :8081 │ │
│ │ :9999 (RPC) │ │ :9999 │ │ :9999 │ │
│ │ JDK 17+Maven │ │ JDK 17+Maven│ │ JDK 17+Maven│ │
│ └────────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 所有节点: Ubuntu 24.04, 2vCPU/4GiB, 按需计费 │
│ 执行器集群 AppName: xxl-job-executor-cluster │
│ │
└─────────────────────────────────────────────────────────────────┘
服务器规格
| 节点 | 公网IP | 内网IP | 角色 | 关键软件 |
|---|---|---|---|---|
| xxljob-01 | 121.36.61.101 | 192.168.0.205 | Admin + MySQL | JDK 17.0.19, Maven 3.8.7, MySQL 8.0.46 |
| xxljob-02 | 120.46.133.64 | 192.168.0.73 | Executor-1 | JDK 17.0.19, Maven 3.8.7 |
| xxljob-03 | 1.94.216.49 | 192.168.0.179 | Executor-2 | JDK 17.0.19, Maven 3.8.7 |
| xxljob-04 | 121.36.6.178 | 192.168.0.246 | Executor-3 | JDK 17.0.19, Maven 3.8.7 |
数据库初始化(xxljob-01 实际执行)
sql
-- 1. 设置 root 密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'XxlJob@2024!';
FLUSH PRIVILEGES;
-- 2. 创建数据库
CREATE DATABASE IF NOT EXISTS xxl_job
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
-- 3. 执行建表脚本
USE xxl_job;
SOURCE /opt/xxl-job/doc/db/tables_xxl_job.sql;
-- 4. 验证(9张表)
SHOW TABLES;
-- +------------------------+
-- | xxl_job_group |
-- | xxl_job_info |
-- | xxl_job_lock |
-- | xxl_job_log |
-- | xxl_job_log_report |
-- | xxl_job_logglue |
-- | xxl_job_registry |
-- | xxl_job_user |
-- | xxl_job_log_ext |
-- +------------------------+
Admin 实际配置(xxljob-01)
properties
# /opt/xxl-job/xxl-job-admin/src/main/resources/application.properties
server.port=8080
server.servlet.context-path=/xxl-job-admin
# 数据库(HikariCP 连接池)
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=XxlJob@2024!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 核心配置
xxl.job.accessToken=xxljob2024
xxl.job.i18n=zh_CN
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
xxl.job.logretentiondays=30
Executor 配置示例(xxljob-02, 其他节点 IP 不同)
properties
# /opt/xxl-job-executor/src/main/resources/application.properties
server.port=8081
# 指向 Admin 公网地址
xxl.job.admin.addresses=http://121.36.61.101:8080/xxl-job-admin
xxl.job.accessToken=xxljob2024
# 执行器注册信息
xxl.job.executor.appname=xxl-job-executor-cluster
xxl.job.executor.ip=120.46.133.64
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30
Executor 核心代码(3台执行器相同)
XxlJobConfig.java --- 执行器配置类:
java
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor e = new XxlJobSpringExecutor();
e.setAdminAddresses(adminAddresses);
e.setAccessToken(accessToken);
e.setAppname(appname);
e.setIp(ip);
e.setPort(port);
e.setLogPath(logPath);
e.setLogRetentionDays(logRetentionDays);
return e;
}
}
已注册的 JobHandler:
java
// 1. 简单任务 --- 演示基本用法
@XxlJob("demoJobHandler")
public void demoJobHandler() {
XxlJobHelper.log("XXL-JOB, Hello World.");
System.out.println("demoJobHandler executed at " + new Date());
XxlJobHelper.handleSuccess("执行成功");
}
// 2. 心跳任务 --- 演示定时心跳上报
@XxlJob("heartbeatJobHandler")
public void heartbeatJobHandler() {
XxlJobHelper.log("心跳上报: " + new Date());
XxlJobHelper.handleSuccess();
}
// 3. 参数任务 --- 演示任务参数传递
@XxlJob("paramJobHandler")
public void paramJobHandler() {
String param = XxlJobHelper.getJobParam();
XxlJobHelper.log("接收到参数: " + param);
XxlJobHelper.handleSuccess();
}
// 4. 分片任务 --- 演示分片广播
@XxlJob("shardingJobHandler")
public void shardingJobHandler() {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("分片 " + (shardIndex + 1) + "/" + shardTotal);
XxlJobHelper.handleSuccess();
}
Maven 编译与启动
bash
# ===========================================
# 所有节点统一使用 Maven 源码方式启动(开发/测试环境)
# ===========================================
# 1. 先编译(首次需下载依赖,约 3-5 分钟)
cd /opt/xxl-job/xxl-job-admin && mvn package -DskipTests
cd /opt/xxl-job-executor && mvn package -DskipTests
# 2. 用 spring-boot:run 启动(后端运行)
# Admin
cd /opt/xxl-job/xxl-job-admin
nohup mvn spring-boot:run > /data/logs/admin-run.log 2>&1 &
# Executor(3台各自执行)
cd /opt/xxl-job-executor
nohup mvn spring-boot:run > /data/logs/executor-run.log 2>&1 &
# 注意事项:
# - 首次启动 Maven 需下载 spring-boot-maven-plugin 等插件依赖
# - Admin 必须先于 Executor 启动
# - 使用 nohup 后台运行时务必确保 SSH 通道关闭不发送 SIGHUP
⚡ 踩坑提醒 : SSH
get_pty=True模式下,SSH 会话关闭会导致后台进程收到SIGHUP信号被杀。即使使用了nohup,在 PTY 模式下也可能失效。解决方案:使用无 PTY 的transport.open_session()发送启动命令,或使用at now/systemd service方式启动。
Admin 启动成功日志
Started XxlJobAdminApplication in 12.5 seconds (JVM running for 14.2)
HikariPool-1 - Starting...
HikariPool-1 - Start completed.
init xxl-job admin success.
执行器注册到 Admin
bash
# 手动验证注册 API(curl 测试)
curl -s -X POST 'http://121.36.61.101:8080/xxl-job-admin/api/registry' \
-H 'XXL-JOB-ACCESS-TOKEN: xxljob2024' \
-H 'Content-Type: application/json' \
-d '{"registryGroup":"EXECUTOR","registryKey":"xxl-job-executor-cluster","registryValue":"http://120.46.133.64:9999/"}'
# 返回: {"code":200,"msg":null,"content":null}
# ✅ code=200 表示注册成功
数据库注册记录验证
sql
SELECT * FROM xxl_job.xxl_job_registry;
-- +----+-----------------------------+------------------------------+-----------------------------+
-- | id | registry_group | registry_key | registry_value |
-- +----+-----------------------------+------------------------------+-----------------------------+
-- | 1 | EXECUTOR | xxl-job-executor-cluster | http://120.46.133.64:9999/ |
-- | 2 | EXECUTOR | xxl-job-executor-cluster | http://1.94.216.49:9999/ |
-- | 3 | EXECUTOR | xxl-job-executor-cluster | http://121.36.6.178:9999/ |
-- +----+-----------------------------+------------------------------+-----------------------------+
最终部署状态
| 节点 | 公网IP | :8080 Admin | :8081 Web | :9999 Remoting | 注册状态 |
|---|---|---|---|---|---|
| xxljob-01 | 121.36.61.101 | ✅ ONLINE | --- | --- | Admin 主节点 |
| xxljob-02 | 120.46.133.64 | --- | ✅ | ✅ | ✅ 已注册 |
| xxljob-03 | 1.94.216.49 | --- | ✅ | ✅ | ✅ 已注册 |
| xxljob-04 | 121.36.6.178 | --- | ✅ | ✅ | ✅ 已注册 |
访问信息
| 项目 | 值 |
|---|---|
| Admin 管理台 | http://121.36.61.101:8080/xxl-job-admin |
| 登录账号 | admin / 123456 |
| Access Token | xxljob2024 |
| 数据库 | MySQL xxl_job(root / XxlJob@2024!) |
| 执行器集群 | AppName=xxl-job-executor-cluster,3 节点 |
四、高级应用篇
4.1 性能优化
数据库连接池优化
properties
# ============================================
# HikariCP 连接池调优(xxl-job-admin)
# ============================================
# 最大连接数(根据并发任务量调整)
# 公式: 最大连接数 ≈ 并发任务数 × 每个任务可能的数据库操作
spring.datasource.hikari.maximum-pool-size=50
# 最小空闲连接数
spring.datasource.hikari.minimum-idle=10
# 连接超时(毫秒)
spring.datasource.hikari.connection-timeout=30000
# 空闲超时(毫秒)
spring.datasource.hikari.idle-timeout=600000
# 最大生命周期(毫秒,应小于 MySQL wait_timeout)
spring.datasource.hikari.max-lifetime=1800000
# 连接测试查询
spring.datasource.hikari.connection-test-query=SELECT 1
调度线程池优化
properties
# ============================================
# 调度线程池配置
# ============================================
# 快任务线程池(Cron 触发的调度请求)
xxl.job.triggerpool.fast.max=200
# 慢任务线程池(手动触发、子任务触发、失败重试)
xxl.job.triggerpool.slow.max=100
线程池设计原理:
┌─────────────────┐
│ 调度线程入口 │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ 快任务线程池 │ │ 慢任务线程池 │
│ (fast.max=200) │ │ (slow.max=100) │
│ │ │ │
│ 处理: Cron触发 │ │ 处理: 手动触发 │
│ 定时调度 │ │ 子任务 │
│ │ │ 失败重试 │
└─────────────────┘ └─────────────────┘
执行器性能优化
java
/**
* 高性能执行器任务模板
*/
@Component
public class HighPerfJob {
// 使用线程池进行业务处理,避免阻塞 XXL-JOB 回调线程
private final ExecutorService bizExecutor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(1000), // workQueue
new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@XxlJob("highPerfJobHandler")
public void highPerfJobHandler() throws Exception {
long startTime = System.currentTimeMillis();
// 分批处理 + 并行执行
int batchSize = 500;
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int offset = 0; offset < 10000; offset += batchSize) {
final int start = offset;
final int end = Math.min(offset + batchSize, 10000);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
processBatch(start, end);
}, bizExecutor);
futures.add(future);
}
// 等待所有批次完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
long elapsed = System.currentTimeMillis() - startTime;
XxlJobHelper.log("任务完成, 总耗时: {}ms", elapsed);
XxlJobHelper.handleSuccess();
}
private void processBatch(int start, int end) {
// 批量处理逻辑
XxlJobHelper.log("处理批次: {} - {}", start, end);
}
}
日志与数据库清理
sql
-- ============================================
-- 定时清理历史日志(建议作为 XXL-JOB 任务本身)
-- ============================================
-- 删除 30 天前的调度日志
DELETE FROM xxl_job_log
WHERE trigger_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 清理孤儿日志报表
DELETE FROM xxl_job_log_report
WHERE trigger_day < DATE_SUB(CURDATE(), INTERVAL 30 DAY);
-- 优化表(回收空间)
OPTIMIZE TABLE xxl_job_log;
日志清理任务:
java
@XxlJob("cleanLogJobHandler")
public void cleanLogJobHandler() {
// 此任务本身也可以配置在 XXL-JOB 中定期执行
int deletedCount = xxlJobLogMapper.deleteBeforeDate(
LocalDate.now().minusDays(30));
XxlJobHelper.log("清理了 {} 条历史日志", deletedCount);
XxlJobHelper.handleSuccess();
}
4.2 安全配置
AccessToken 认证
properties
# ============================================
# 调度中心配置
# ============================================
xxl.job.accessToken=MySecretToken2024!@#
# ============================================
# 执行器配置(必须与调度中心一致)
# ============================================
xxl.job.accessToken=MySecretToken2024!@#
AccessToken 工作原理:
执行器请求调度中心时:
URL: http://admin:8080/xxl-job-admin/api/callback
Header: XXL-JOB-ACCESS-TOKEN: MySecretToken2024!@#
调度中心收到请求:
① 从 Header 中提取 XXL-JOB-ACCESS-TOKEN
② 与本地配置的 xxl.job.accessToken 比对
③ 一致 → 放行 / 不一致 → 拒绝并告警
用户权限管理
┌─────────────────────────────────────────────────────────────────┐
│ XXL-JOB 用户角色权限 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 角色 权限范围 │
│ ───────────────────────────────────────────────────────────── │
│ 管理员 全部权限: 任务CRUD、执行器管理、用户管理、系统配置 │
│ 普通用户 受限权限: 任务CRUD(只能操作自己创建的)、查看日志 │
│ │
│ 配置建议: │
│ - 生产环境: 修改默认 admin 密码(默认 123456) │
│ - 多人协作: 给每个开发人员创建独立账号 │
│ - 运维人员: 创建只读账号,只能查看不能修改 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 监控告警
邮件告警配置
在前面的 application.properties 中已配置邮件,在任务配置中填写 报警邮件 即可:
任务管理 → 编辑 → 报警邮件: admin@company.com, dev@company.com
告警触发条件:
| 场景 | 是否触发告警 |
|---|---|
| 任务执行失败 | ✅ |
| 失败重试后仍失败 | ✅ |
| 任务执行超时 | ✅ |
| 任务执行成功 | ❌ |
| 调度失败(执行器不可达) | ✅ |
钉钉机器人告警
XXL-JOB 原生不支持钉钉告警,但可以通过以下方式实现:
方案一:在任务中集成钉钉告警
java
@Component
public class DingTalkAlertJob {
/**
* 钉钉告警工具任务示例
*/
@XxlJob("dingTalkAlertDemo")
public void dingTalkAlertDemo() {
try {
// 业务逻辑
doSomething();
XxlJobHelper.handleSuccess();
} catch (Exception e) {
// 失败时发送钉钉告警
sendDingTalkAlert("任务执行失败", e.getMessage(), getUserPhones());
throw e;
}
}
/**
* 发送钉钉告警消息
*/
private void sendDingTalkAlert(String title, String content, List<String> atMobiles) {
String webhookUrl = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN";
Map<String, Object> message = new LinkedHashMap<>();
message.put("msgtype", "markdown");
Map<String, String> markdown = new LinkedHashMap<>();
markdown.put("title", title);
StringBuilder sb = new StringBuilder();
sb.append("## ⚠️ XXL-JOB 任务告警\n\n");
sb.append("**告警标题**: ").append(title).append("\n\n");
sb.append("**告警内容**: ").append(content).append("\n\n");
sb.append("**告警时间**: ").append(LocalDateTime.now()).append("\n\n");
sb.append("**执行器**: ").append(XxlJobHelper.getJobParam()).append("\n");
// @相关人员
if (atMobiles != null) {
for (String mobile : atMobiles) {
sb.append("@").append(mobile).append(" ");
}
}
markdown.put("text", sb.toString());
message.put("markdown", markdown);
// 通过 HTTP POST 发送
// RestTemplate/HttpClient 发送...
}
private List<String> getUserPhones() {
return Arrays.asList("13800138000");
}
private void doSomething() { /* 业务逻辑 */ }
}
方案二:在调度中心源码中扩展
修改 xxl-job-admin 的 JobAlarm 接口实现:
1. 找到 EmailJobAlarm 实现类
2. 新增 DingTalkJobAlarm 实现类
3. 在 JobAlarmer 中注册
五、项目实战案例
5.1 数据同步任务(MySQL → Elasticsearch)
java
@Component
public class DataSyncJob {
// 模拟 Mapper
// @Autowired private UserMapper userMapper;
// @Autowired private RestHighLevelClient esClient;
/**
* MySQL 到 Elasticsearch 增量同步任务
*
* 部署建议:
* - Cron: 0 0/5 * * * ? (每5分钟)
* - 路由策略: 轮询
* - 阻塞策略: 单机串行
*/
@XxlJob("mysqlToEsSyncJob")
public void mysqlToEsSyncJob() throws Exception {
long startTime = System.currentTimeMillis();
XxlJobHelper.log("【MySQL→ES同步】开始");
// ① 从检查点表获取上次同步时间
LocalDateTime lastSyncTime = getLastSyncTime();
XxlJobHelper.log("上次同步时间: {}", lastSyncTime);
// ② 查询增量数据(自上次同步后更新的数据)
List<User> users = queryUpdatedUsers(lastSyncTime);
if (users.isEmpty()) {
XxlJobHelper.log("【MySQL→ES同步】无增量数据,跳过");
XxlJobHelper.handleSuccess();
return;
}
XxlJobHelper.log("获取到 {} 条增量数据", users.size());
// ③ 批量写入 ES(使用 Bulk API)
int batchSize = 200;
int successCount = 0;
int failCount = 0;
LocalDateTime currentSyncTime = LocalDateTime.now();
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
try {
bulkIndexToEs(batch);
successCount += batch.size();
XxlJobHelper.log("批次 {}/{} 同步成功", i / batchSize + 1,
(users.size() + batchSize - 1) / batchSize);
} catch (Exception e) {
failCount += batch.size();
XxlJobHelper.log("批次 {}/{} 同步失败: {}",
i / batchSize + 1, (users.size() + batchSize - 1) / batchSize,
e.getMessage());
}
}
// ④ 更新同步检查点
if (failCount == 0) {
updateLastSyncTime(currentSyncTime);
}
long elapsed = System.currentTimeMillis() - startTime;
String result = String.format(
"【MySQL→ES同步】完成: 总数据=%d, 成功=%d, 失败=%d, 耗时=%dms",
users.size(), successCount, failCount, elapsed
);
XxlJobHelper.log(result);
if (failCount > 0) {
XxlJobHelper.handleFail(result);
throw new RuntimeException("部分数据同步失败");
} else {
XxlJobHelper.handleSuccess(result);
}
}
// ======== 模拟方法 ========
private LocalDateTime getLastSyncTime() {
// 从 xxl_job_sync_checkpoint 表读取
return LocalDateTime.now().minusHours(1);
}
private List<User> queryUpdatedUsers(LocalDateTime after) {
// SELECT * FROM t_user WHERE update_time > #{after}
// 模拟返回数据
List<User> list = new ArrayList<>();
for (int i = 0; i < 500; i++) {
list.add(new User(i, "user_" + i, "user" + i + "@example.com"));
}
return list;
}
private void bulkIndexToEs(List<User> users) {
// ES Bulk API 批量索引
}
private void updateLastSyncTime(LocalDateTime time) {
// UPDATE xxl_job_sync_checkpoint SET last_sync_time = #{time}
}
static class User {
long id;
String name;
String email;
User(long id, String name, String email) {
this.id = id; this.name = name; this.email = email;
}
}
}
5.2 报表生成系统
java
@Component
public class ReportJob {
/**
* 日报生成任务
*
* Cron: 0 0 6 * * ? (每天早上6点)
*/
@XxlJob("dailyReportJob")
public void dailyReportJob() throws Exception {
XxlJobHelper.log("【日报生成】开始");
String yesterday = LocalDate.now().minusDays(1)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// ① 收集数据
DailyReportData data = new DailyReportData();
data.date = yesterday;
data.newUserCount = getNewUserCount(yesterday);
data.orderCount = getOrderCount(yesterday);
data.totalAmount = getTotalAmount(yesterday);
data.activeUserCount = getActiveUserCount(yesterday);
// ② 生成报告(HTML 格式)
String reportHtml = generateReportHtml(data);
// ③ 发送邮件
sendEmailReport(reportHtml, yesterday);
XxlJobHelper.log("【日报生成】完成: {}", yesterday);
XxlJobHelper.handleSuccess();
}
/**
* 周报生成任务
*
* Cron: 0 0 9 ? * 1 (每周一早上9点)
*/
@XxlJob("weeklyReportJob")
public void weeklyReportJob() throws Exception {
XxlJobHelper.log("【周报生成】开始");
LocalDate endDate = LocalDate.now().minusDays(1);
LocalDate startDate = endDate.minusDays(6);
WeeklyReportData data = aggregateWeeklyData(startDate, endDate);
String report = generateWeeklyReportHtml(data);
sendEmailReport(report, startDate + " ~ " + endDate);
XxlJobHelper.log("【周报生成】完成");
XxlJobHelper.handleSuccess();
}
/**
* 月报生成任务
*
* Cron: 0 0 8 1 * ? (每月1号早上8点)
*/
@XxlJob("monthlyReportJob")
public void monthlyReportJob() throws Exception {
XxlJobHelper.log("【月报生成】开始");
YearMonth lastMonth = YearMonth.now().minusMonths(1);
MonthlyReportData data = aggregateMonthlyData(lastMonth);
String report = generateMonthlyReportHtml(data);
sendEmailReport(report, lastMonth.toString());
XxlJobHelper.log("【月报生成】完成");
XxlJobHelper.handleSuccess();
}
// ======== 模拟方法和数据类 ========
private String generateReportHtml(DailyReportData data) {
return "<html><body><h1>日报 " + data.date + "</h1>"
+ "<p>新增用户: " + data.newUserCount + "</p>"
+ "<p>订单数: " + data.orderCount + "</p>"
+ "<p>交易额: " + data.totalAmount + "</p>"
+ "</body></html>";
}
private String generateWeeklyReportHtml(WeeklyReportData data) {
return "<html><body><h1>周报</h1></body></html>";
}
private String generateMonthlyReportHtml(MonthlyReportData data) {
return "<html><body><h1>月报</h1></body></html>";
}
private void sendEmailReport(String html, String period) {
// JavaMailSender 发送邮件
XxlJobHelper.log("报告邮件已发送, 周期: {}", period);
}
private WeeklyReportData aggregateWeeklyData(LocalDate start, LocalDate end) {
return new WeeklyReportData();
}
private MonthlyReportData aggregateMonthlyData(YearMonth month) {
return new MonthlyReportData();
}
// 模拟数据查询
private int getNewUserCount(String date) { return 128; }
private int getOrderCount(String date) { return 356; }
private double getTotalAmount(String date) { return 45678.90; }
private int getActiveUserCount(String date) { return 1024; }
static class DailyReportData {
String date;
int newUserCount;
int orderCount;
double totalAmount;
int activeUserCount;
}
static class WeeklyReportData {}
static class MonthlyReportData {}
}
5.3 缓存预热任务
java
@Component
public class CacheWarmJob {
// @Autowired private RedisTemplate<String, Object> redisTemplate;
// @Autowired private ProductMapper productMapper;
/**
* Redis 缓存预热任务
*
* 使用场景:
* - 系统重启后自动加载热点数据到缓存
* - 定时刷新缓存,保证数据一致性
*
* Cron: 0 0 3 * * ? (每天凌晨3点执行)
*/
@XxlJob("cacheWarmJob")
public void cacheWarmJob() throws Exception {
long startTime = System.currentTimeMillis();
XxlJobHelper.log("【缓存预热】开始");
// 支持分片广播:每个执行器预热一部分数据
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
if (shardTotal == 1) {
// 单机执行:预热全部
warmUpAll();
} else {
// 分片执行:各预热一部分
XxlJobHelper.log("分片预热: {}/{}", shardIndex + 1, shardTotal);
warmUpByShard(shardIndex, shardTotal);
}
long elapsed = System.currentTimeMillis() - startTime;
XxlJobHelper.log("【缓存预热】完成, 耗时: {}ms", elapsed);
XxlJobHelper.handleSuccess();
}
/**
* 全量预热
*/
private void warmUpAll() {
XxlJobHelper.log(">>> 预热商品分类缓存");
// List<Category> categories = categoryMapper.selectAll();
// categories.forEach(c -> redisTemplate.opsForValue()
// .set("cache:category:" + c.getId(), c, 1, TimeUnit.HOURS));
XxlJobHelper.log(">>> 预热热门商品缓存");
// List<Product> hotProducts = productMapper.selectHot(100);
// hotProducts.forEach(p -> redisTemplate.opsForValue()
// .set("cache:product:" + p.getId(), p, 30, TimeUnit.MINUTES));
XxlJobHelper.log(">>> 预热配置缓存");
// List<Config> configs = configMapper.selectAll();
// configs.forEach(c -> redisTemplate.opsForValue()
// .set("cache:config:" + c.getKey(), c.getValue()));
XxlJobHelper.log(">>> 缓存预热全部完成");
}
/**
* 分片预热(配合路由策略"分片广播"使用)
*/
private void warmUpByShard(int shardIndex, int shardTotal) {
// 例如: 预热商品数据,按商品ID分片
int totalProducts = 10000;
int processedCount = 0;
for (int id = 1; id <= totalProducts; id++) {
if (id % shardTotal == shardIndex) {
// Product p = productMapper.selectById(id);
// redisTemplate.opsForValue()
// .set("cache:product:" + id, p, 30, TimeUnit.MINUTES);
processedCount++;
}
}
XxlJobHelper.log("分片 {}/{} 处理了 {} 条数据", shardIndex + 1, shardTotal, processedCount);
}
}
5.4 批量数据处理(分片广播 + 断点续传)
java
@Component
public class BatchProcessJob {
/**
* 大规模数据处理任务
*
* 配置要求:
* - 路由策略: 分片广播
* - Cron: 0 0 2 * * ? (每天凌晨2点)
* - 阻塞处理策略: 单机串行
*
* 适用场景:
* - 百万级数据清洗/迁移
* - 大文件批量解析
*/
@XxlJob("batchProcessJob")
public void batchProcessJob() throws Exception {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("【批量处理】启动: 分片 {}/{}", shardIndex + 1, shardTotal);
// ① 获取待处理数据总数
int totalCount = getPendingDataCount();
XxlJobHelper.log("待处理总数: {}", totalCount);
// ② 分片计算本执行器负责的数据范围
int batchSize = 200;
int successCount = 0;
int failCount = 0;
int skipCount = 0;
for (int offset = shardIndex * batchSize; offset < totalCount; offset += shardTotal * batchSize) {
int limit = Math.min(batchSize, totalCount - offset);
XxlJobHelper.log("处理批次: offset={}, limit={}", offset, limit);
try {
// ③ 查询本批次数据
List<DataRecord> batch = fetchBatch(offset, limit);
for (DataRecord record : batch) {
try {
if (isProcessed(record.getId())) {
skipCount++; // 已处理,跳过(断点续传)
continue;
}
processRecord(record);
markProcessed(record.getId());
successCount++;
} catch (Exception e) {
failCount++;
XxlJobHelper.log("记录 {} 处理失败: {}", record.getId(), e.getMessage());
}
}
} catch (Exception e) {
XxlJobHelper.log("批次查询失败 (offset={}): {}", offset, e.getMessage());
}
}
// ④ 生成结果报告
String result = String.format(
"【批量处理】完成: 分片=%d/%d, 成功=%d, 失败=%d, 跳过=%d",
shardIndex + 1, shardTotal, successCount, failCount, skipCount
);
XxlJobHelper.log(result);
if (failCount > 0) {
XxlJobHelper.handleFail(result);
} else {
XxlJobHelper.handleSuccess(result);
}
}
// ===== 模拟数据访问方法 =====
private int getPendingDataCount() { return 10000; }
private List<DataRecord> fetchBatch(int offset, int limit) {
List<DataRecord> batch = new ArrayList<>();
for (int i = 0; i < limit; i++) {
batch.add(new DataRecord(offset + i, "data_" + (offset + i)));
}
return batch;
}
private boolean isProcessed(long id) { return false; }
private void processRecord(DataRecord record) {
// 实际业务处理逻辑
}
private void markProcessed(long id) {
// INSERT INTO batch_process_record (record_id, status, process_time)
// VALUES (id, 'SUCCESS', NOW())
}
static class DataRecord {
long id;
String data;
DataRecord(long id, String data) { this.id = id; this.data = data; }
public long getId() { return id; }
}
}
六、常见问题与解决方案
6.1 连接问题
问题1:执行器无法注册到调度中心
┌─────────────────────────────────────────────────────────────────┐
│ 现象: Admin "执行器管理"中看不到执行器 Online │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 排查步骤: │
│ │
│ ① 检查执行器启动日志 │
│ >>>>>>>>>>> xxl-job, job executor start success. │
│ >>>>>>>>>>> xxl-job executor registry success, │
│ registryKey:xxl-job-executor-demo │
│ 如果没有 registry success 日志 → 第②步 │
│ │
│ ② 检查网络连通性 │
│ 在「执行器」所在机器执行: │
│ curl http://调度中心IP:8080/xxl-job-admin/ │
│ 确认网络可达 │
│ │
│ ③ 检查 AppName 一致性 │
│ 执行器 application.yml: xxl.job.executor.appname │
│ Admin 执行器管理: AppName │
│ ★ 两者必须完全一致(包括大小写)! │
│ │
│ ④ 检查 AccessToken │
│ 如果 Admin 配置了 accessToken,执行器也必须配置相同的 │
│ │
│ ⑤ 检查防火墙/安全组 │
│ 执行器端口 (默认 9999) 必须在安全组中开放 │
│ │
│ ⑥ 检查多网卡环境 │
│ 如果服务器有多个网卡,手动指定 ip: │
│ xxl.job.executor.ip=内网IP │
│ │
└─────────────────────────────────────────────────────────────────┘
问题2:调度中心连接超时
java
// 执行器端增加超时配置
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
// ... 其他配置 ...
// 心跳间隔(默认30秒,可适当缩小用于测试)
// 注意:2.5.0 版本在 XxlJobSpringExecutor 中已内置
return executor;
}
6.2 任务执行问题
问题3:任务不执行
| 可能原因 | 检查方法 | 解决方案 |
|---|---|---|
| 任务未启动 | Admin 上任务状态显示"STOP" | 点击"启动"按钮 |
| Cron 表达式错误 | 使用在线工具验证 | 修正 Cron 表达式 |
| 执行器离线 | Admin 执行器管理查看状态 | 重启执行器 |
| JobHandler 名称不匹配 | 对比代码 @XxlJob("xxx") 和 Admin 配置 |
保持一致 |
| 路由策略问题 | 检查执行器数量是否匹配策略 | 选择合适策略 |
| 阻塞策略拦截 | 上次任务未完成 | 选择"覆盖之前调度" |
问题4:任务重复执行
原因分析:
① 调度中心集群的分布式锁失效(MySQL 锁超时)
→ 检查 MySQL 连接是否稳定
→ 检查是否有长时间未提交的事务
② 配置了多个完全相同的任务
→ 在 Admin 任务管理中逐一核对
③ 路由策略为"分片广播"(这是正常行为)
→ 检查是否为误解
问题5:任务卡死
java
/**
* 带超时控制的任务
*/
@XxlJob("timeoutControlJob")
public void timeoutControlJob() throws Exception {
// 方式1: 在 Admin 上配置 "任务超时时间"(秒),超时后自动中断
// 方式2: 代码手动控制超时
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
// 可能卡死的业务逻辑
potentiallyBlockingOperation();
});
try {
// 30分钟超时
future.get(30, TimeUnit.MINUTES);
XxlJobHelper.handleSuccess();
} catch (TimeoutException e) {
future.cancel(true); // 中断执行
XxlJobHelper.handleFail("任务执行超时,已中断");
throw e;
} finally {
executor.shutdownNow();
}
}
private void potentiallyBlockingOperation() {
// 实际业务逻辑
}
6.3 性能问题
问题6:任务堆积
┌─────────────────────────────────────────────────────────────────┐
│ 任务堆积排查流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ① 检查 Admin 调度日志 │
│ → 查看"调度结果"和"执行结果" │
│ → 调度成功但执行耗时过长 → 业务逻辑问题 │
│ → 调度失败 → 网络/执行器问题 │
│ │
│ ② 检查线程池状态 │
│ → Admin: triggerpool.fast.max / triggerpool.slow.max │
│ → Executor: 执行器自身线程池 │
│ │
│ ③ 解决方案 │
│ ├─ 增加执行器实例(水平扩展) │
│ ├─ 增大线程池配置 │
│ ├─ 使用分片广播并行处理 │
│ ├─ 优化业务逻辑(减少 DB 查询、使用批量操作) │
│ └─ 调整 Cron 频率(降低执行频率) │
│ │
└─────────────────────────────────────────────────────────────────┘
问题7:数据库压力大
sql
-- ============================================
-- 数据库优化措施
-- ============================================
-- ① 为高频查询字段创建索引
ALTER TABLE xxl_job_log ADD INDEX idx_trigger_time (trigger_time);
ALTER TABLE xxl_job_log ADD INDEX idx_job_id (job_id);
ALTER TABLE xxl_job_log ADD INDEX idx_handle_code (handle_code);
-- ② 定期清理历史日志
-- 配置为 XXL-JOB 定时任务,每天凌晨清理
DELETE FROM xxl_job_log
WHERE trigger_time < DATE_SUB(NOW(), INTERVAL 7 DAY)
LIMIT 10000; -- 分批删除,避免长事务
-- ③ 读写分离(如果数据量大)
-- 为调度日志配置独立的从库读取
问题8:内存溢出(OOM)
bash
# JVM 参数调优(执行器启动时)
java -Xms512m -Xmx2048m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/xxl-job/ \
-jar xxl-job-executor-demo-1.0.0.jar
# 参数说明:
# -Xms512m 初始堆大小
# -Xmx2048m 最大堆大小(根据实际调整)
# -XX:+UseG1GC 使用 G1 垃圾回收器
# +HeapDump... 发生 OOM 时自动生成 dump 文件
6.4 真实部署踩坑记录
以下 6 个坑来自 2026-06-03 华为云 ecs-f5eb 集群的真实部署过程,耗时约 45 分钟逐一解决。
坑1:apt lock 冲突(权重 ⭐⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: apt-get install 报错 "Could not get lock /var/lib/ │
│ dpkg/lock-frontend" │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: 新重装的 Ubuntu 24.04 系统,后台有 unattended-upgrades │
│ 进程正在自动更新,锁住了 dpkg。 │
│ │
│ 解决: │
│ # 杀掉所有 apt/dpkg 进程 │
│ pkill -9 -f apt │
│ pkill -9 -f dpkg │
│ │
│ # 清理锁文件 │
│ rm -f /var/lib/dpkg/lock-frontend │
│ rm -f /var/lib/dpkg/lock │
│ rm -f /var/cache/apt/archives/lock │
│ │
│ # 重新配置 dpkg │
│ dpkg --configure -a │
│ │
│ 教训: 新装系统后第一步就检查 `lsof /var/lib/dpkg/lock-frontend` │
│ │
└─────────────────────────────────────────────────────────────────┘
坑2:Bash ${} 变量展开破坏 Java 注解(权重 ⭐⭐⭐⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: 通过 SSH heredoc 写入 Java 文件后,@Value("${xxl.job.xxx}")│
│ 中的 ${} 被 Bash 解析为空字符串,注解失效。 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: Bash 的 heredoc(即使单引号 EOF)在某些复杂场景下仍会尝试 │
│ 解析 ${} 表达式;SSH 远程执行时多层引号嵌套加剧问题。 │
│ │
│ 错误示例: │
│ ssh root@host 'cat > file.java << '"'"'EOF'"'"' │
│ @Value("${xxl.job.admin.addresses}") ← ${} 被展开 │
│ EOF' │
│ │
│ 解决: 使用 Python heredoc + \\\\x24 转义 $ │
│ ssh root@host 'python3 << '"'"'PYEOF'"'"' │
│ content = """ │
│ @Value("\\\\x24{xxl.job.admin.addresses}") │
│ private String adminAddresses; │
│ """ │
│ with open("/path/to/file.java", "w") as f: │
│ f.write(content) │
│ PYEOF' │
│ │
│ 原理: \\\\x24 是 $ 的十六进制编码,Python 写入文件时自动转换。 │
│ │
│ 教训: 通过 SSH 写入包含 ${} 的 Java/Kotlin 文件时,首选 Python │
│ heredoc 方式,永远不要用 cat << 'EOF'。 │
│ │
└─────────────────────────────────────────────────────────────────┘
坑3:SSH nohup + PTY 导致 SIGHUP 杀进程(权重 ⭐⭐⭐⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: 通过 paramiko `get_pty=True` 发送 nohup 后台命令后,SSH │
│ 通道关闭,后台 Java 进程被 SIGHUP 信号杀掉。 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: 当 SSH 通道关联了 PTY(伪终端),通道关闭时内核会向 PTY │
│ 关联的所有进程发送 SIGHUP。即使使用了 `nohup`,也无效。 │
│ │
│ 失败尝试: │
│ ❌ nohup java -jar ... & → SIGHUP 杀进程 │
│ ❌ nohup java -jar ... & disown → SIGHUP 杀进程 │
│ ❌ setsid nohup java -jar ... & → SIGHUP 杀进程 │
│ ❌ screen -dmS xxx bash -c '...' → 语法转义问题 │
│ │
│ 解决: paramiko 使用 transport.open_session() 代替 exec_command │
│ def ssh_send_bg(host, cmd): │
│ c = paramiko.SSHClient() │
│ c.connect(hostname=host, username='root', password=pwd) │
│ transport = c.get_transport() │
│ channel = transport.open_session() # 无 PTY! │
│ channel.exec_command(cmd) │
│ c.close() │
│ │
│ 原理: open_session() 默认不分配 PTY,通道关闭时不发送 SIGHUP。 │
│ │
│ 生产环境推荐: 使用 systemd service 管理 Java 进程,彻底避免此问题。│
│ │
└─────────────────────────────────────────────────────────────────┘
坑4:accessToken 不一致导致注册失败(权重 ⭐⭐⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: Executor 日志显示 "The access token is wrong.",持续注册 │
│ 失败,Admin 端看不到任何执行器在线。 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: Admin 源码默认 accessToken=default_token,但 Executor │
│ 配置文件写的是 xxljob2024。两者不匹配。 │
│ │
│ Admin 配置 (修改前): │
│ xxl.job.accessToken=default_token ← 默认值 │
│ │
│ Executor 配置: │
│ xxl.job.accessToken=xxljob2024 ← 自定义值 │
│ │
│ 解决: 统一两边为相同值 │
│ # Admin 端 │
│ sed -i 's/accessToken=default_token/accessToken=xxljob2024/' \
│ application.properties │
│ │
│ 教训: 部署前务必检查 Admin 和所有 Executor 的 accessToken │
│ 一致性;生产环境建议使用统一配置中心管理。 │
│ │
└─────────────────────────────────────────────────────────────────┘
坑5:启动时序问题(权重 ⭐⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: Executor 先于 Admin 启动,注册线程反复报 "Connection │
│ refused",直到重试耗尽后停止注册。 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: Executor 启动时 Admin 尚未运行(Admin 因 Maven 依赖多, │
│ 编译/启动时间比 Executor 长约 2-3 分钟)。 │
│ │
│ 时间线: │
│ 22:50 Executor-02/03/04 开始 spring-boot:run │
│ 22:55 Executor 启动完成,开始向 Admin 注册 │
│ 22:55 Connection refused → Admin 还没起来! │
│ 22:55 Executor 每 30 秒重试,连续失败 │
│ 23:08 Admin 终于启动完成 │
│ 23:08 Executor 注册线程已停止(重试次数耗尽) │
│ │
│ 解决: 先确保 Admin 完全启动后,再重启所有 Executor。 │
│ │
│ 教训: 部署顺序很重要! │
│ ① MySQL → ② Admin → ③ Executor │
│ 并在 Admin 完全启动(端口监听 + HTTP 200)后再启动 Executor。 │
│ │
└─────────────────────────────────────────────────────────────────┘
坑6:Maven spring-boot:run 日志缓冲不实时(权重 ⭐⭐)
┌─────────────────────────────────────────────────────────────────┐
│ 现象: 使用 nohup mvn spring-boot:run > logfile 2>&1 & 启动后, │
│ 日志文件内容严重滞后,注册状态无法及时观察到。 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 根因: Maven 和 Spring Boot 使用多层缓冲(JVM 缓冲 → Maven 缓冲 │
│ → Shell 管道缓冲),nohup 重定向进一步加剧延迟。 │
│ │
│ 日志实际状态 vs 文件显示状态: │
│ 23:10 进程启动完成,端口已监听 │
│ 23:11 日志文件只有 3 行(Maven 启动信息) │
│ 23:14 日志文件多了 15 行 │
│ 23:15 注册成功的日志才完全刷出 │
│ │
│ 影响: 无法通过实时监控日志判断服务状态,但**不影响服务正常运行**。 │
│ │
│ 解决方案: │
│ ① 用 ss -tlnp | grep <port> 直接检查端口(最可靠) │
│ ② 用 curl 测试 HTTP 接口 │
│ ③ 数据库查询 xxl_job_registry 表验证注册状态 │
│ ④ 生产环境使用 JAR + java -jar 方式启动(日志更及时) │
│ │
│ 教训: 排查部署问题时,端口/进程检查 > 日志文件检查。 │
│ │
└─────────────────────────────────────────────────────────────────┘
6大踩坑总结:
| # | 坑点 | 严重度 | 根因分类 | 关键词 |
|---|---|---|---|---|
| 1 | apt lock 冲突 | ⭐⭐⭐ | 系统环境 | unattended-upgrades |
| 2 | Bash ${} 转义 |
⭐⭐⭐⭐⭐ | 工具限制 | heredoc + SSH 多层引号 |
| 3 | SSH SIGHUP 杀进程 | ⭐⭐⭐⭐⭐ | 操作系统 | PTY + 信号机制 |
| 4 | accessToken 不匹配 | ⭐⭐⭐⭐ | 配置一致性 | 认证令牌 |
| 5 | 启动时序 | ⭐⭐⭐ | 部署流程 | Admin 先于 Executor |
| 6 | Maven 日志缓冲 | ⭐⭐ | 工具行为 | spring-boot:run 模式 |
七、学习资源推荐
| 资源 | 地址 | 说明 |
|---|---|---|
| 官方文档 | https://www.xuxueli.com/xxl-job/ | 最权威的参考文档 |
| GitHub 仓库 | https://github.com/xuxueli/xxl-job | 源码 + Issues |
| Gitee 仓库 | https://gitee.com/xuxueli0323/xxl-job | 国内镜像,访问更快 |
| 在线 Demo | https://www.xuxueli.com/xxl-job/#/quickstart | 无需部署即可体验 |
| 源码结构分析 | xxl-job/doc/ 目录 | 源码自带文档 |
学习路线建议
┌─────────────────────────────────────────────────────────────────┐
│ 推荐学习路线 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Week 1 基础入门 │
│ ├─ [Day 1-2] 阅读官方文档,搭建环境 │
│ ├─ [Day 3] 完成实操5: 启动调度中心 │
│ ├─ [Day 4] 完成实操9: 简单任务开发 │
│ └─ [Day 5] 完成实操12-13: Web界面操作 │
│ │
│ Week 2 进阶提升 │
│ ├─ [Day 1] Cron 表达式精通 │
│ ├─ [Day 2] 路由策略对比测试 │
│ ├─ [Day 3] 分片广播任务实战 │
│ ├─ [Day 4] GLUE 模式探索 │
│ └─ [Day 5] 失败重试与告警配置 │
│ │
│ Week 3 实战应用 │
│ ├─ [Day 1-2] 数据同步任务开发 │
│ ├─ [Day 3-4] 集群部署实战 │
│ └─ [Day 5] 性能优化实践 │
│ │
│ Week 4 生产落地 │
│ ├─ [Day 1-2] 接入实际业务项目 │
│ ├─ [Day 3] 监控告警完善 │
│ ├─ [Day 4] 安全加固 │
│ └─ [Day 5] 文档沉淀 + 团队分享 │
│ │
└─────────────────────────────────────────────────────────────────┘
附录A:完整配置参考
xxl-job-admin application.properties(完整版)
properties
# ============================================
# 服务端口
# ============================================
server.port=8080
server.servlet.context-path=/xxl-job-admin
# ============================================
# 数据库配置
# ============================================
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# HikariCP
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP-XXL-JOB
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
# ============================================
# 邮件配置
# ============================================
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=your_email@qq.com
spring.mail.password=your_auth_code
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.from=your_email@qq.com
# ============================================
# XXL-JOB 核心配置
# ============================================
xxl.job.accessToken=
xxl.job.i18n=zh_CN
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
xxl.job.logretentiondays=30
# ============================================
# MyBatis 配置
# ============================================
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
mybatis.type-aliases-package=com.xxl.job.admin.core.model
# ============================================
# 日志配置
# ============================================
logging.config=classpath:logback.xml
执行器 application.yml(完整版)
yaml
server:
port: 8081
spring:
application:
name: xxl-job-executor-demo
# ============================================
# XXL-JOB 执行器配置
# ============================================
xxl:
job:
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
executor:
appname: xxl-job-executor-demo
address:
ip:
port: 9999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken: default_token
附录B:Cron 表达式速查表
┌─────────────────────────────────────────────────────────────────┐
│ Cron 表达式: 秒 分 时 日 月 周 │
│ 范围: 0-59 0-59 0-23 1-31 1-12 1-7(1=SUN) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ⏱ 每秒 → * * * * * ? │
│ ⏱ 每5秒 → 0/5 * * * * ? │
│ ⏱ 每30秒 → 0/30 * * * * ? │
│ │
│ 🕐 每分钟 → 0 * * * * ? │
│ 🕐 每5分钟 → 0 0/5 * * * ? │
│ 🕐 每30分钟 → 0 0/30 * * * ? │
│ │
│ 🕑 每小时 → 0 0 * * * ? │
│ 🕑 每2小时 → 0 0 0/2 * * ? │
│ │
│ 🌅 每天0点 → 0 0 0 * * ? │
│ 🌅 每天2点 → 0 0 2 * * ? │
│ 🌅 每天8:30 → 0 30 8 * * ? │
│ │
│ 📅 每周一9点 → 0 0 9 ? * 2 │
│ 📅 工作日9点 → 0 0 9 ? * 2-6 │
│ 📅 每月1号 → 0 0 0 1 * ? │
│ 📅 每月最后一天 → 0 0 0 L * ? │
│ │
│ 🕘 9-18点每小时 → 0 0 9-18 * * ? │
│ 🕘 工作日9-18点 → 0 0 9-18 ? * 2-6 │
│ │
└─────────────────────────────────────────────────────────────────┘
附录C:版本差异对照表
| 特性 | 2.3.x | 2.4.x | 2.5.0 |
|---|---|---|---|
| JDK 要求 | JDK 8 | JDK 8 | JDK 17 |
| Spring Boot | 2.x | 2.x | 3.x |
| 调度中心 UI | Layui | Layui | Layui (升级) |
| GLUE 模式 | Java/Shell/Python/PHP/NodeJS/PS | 同 2.3 | 同 2.3 |
| 路由策略 | 9种 | 10种 | 10种 |
| 分片广播 | ✅ | ✅ | ✅ |
| AccessToken | ✅ | ✅ | ✅ |
| 国际化 | ❌ | ✅ | ✅ |
| 任务超时控制 | ✅ | ✅ | ✅ |
| 失败重试 | ✅ | ✅ | ✅ |
版本选择建议:
- 新项目 → 直接使用 2.5.0+ (JDK 17)
- 老项目 JDK 8 → 使用 2.4.2 (最后一个 JDK 8 版本)
- 升级需注意 Spring Boot 3.x 的
javax.*→jakarta.*包名变更
文档版本 : v1.0 | 创建日期 : 2026-06-03
适用场景 : XXL-JOB 从零到生产级部署的完整学习路径
作者签名: 实战系列,基于官方文档和实践经验