0%

Cron 表达式详解——语法、规则与实际应用案例

今天定了早上8点的闹钟,想起 cron 的8点配置,真是生活与工作相结合…

介绍

  Cron 表达式是一种用于配置定时任务的标准格式,在 Unix、Linux 以及各种编程语言中广泛使用。通过 Cron 表达式,我们可以精确地指定任务的执行时间,实现自动化运维、数据备份、报告生成等功能。本文将详细介绍 Cron 表达式的语法规则、常见用法和实际应用场景。

Cron 表达式基础语法

Cron 表达式结构

  标准的 Cron 表达式由5个或6个字段组成(取决于是否包含秒字段),每个字段代表不同的时间单位。最常见的格式是5个字段:

1
2
3
4
5
6
7
* * * * *
│ │ │ │ │
│ │ │ │ └─── 星期几 (0 - 6) (周日=0)
│ │ │ └───── 月份 (1 - 12)
│ │ └─────── 日期 (1 - 31)
│ └───────── 小时 (0 - 23)
└─────────── 分钟 (0 - 59)

  有些 Cron 实现(如 Quartz Scheduler)还支持6个字段的格式,增加了秒字段:

1
2
3
4
5
6
7
8
* * * * * *
│ │ │ │ │ │
│ │ │ │ │ └─── 星期几 (0 - 6) (周日=0)
│ │ │ │ └───── 月份 (1 - 12)
│ │ │ └─────── 日期 (1 - 31)
│ │ └───────── 小时 (0 - 23)
│ └─────────── 分钟 (0 - 59)
└───────────── 秒 (0 - 59)

字段取值范围

字段取值范围可用字符
0-59, - * /
分钟0-59, - * /
小时0-23, - * /
日期1-31, - * / L W
月份1-12 或 JAN-DEC, - * /
星期0-7 或 SUN-SAT, - * ? L #

特殊字符含义

1
2
3
4
5
6
7
8
9
10
11
12
# 星号(*) - 表示任意值
* * * * * # 每分钟执行一次

# 逗号(,) - 表示多个值
30 8,12,16 * * * # 每天的8:30、12:30、16:30执行

# 连字号(-) - 表示范围
0 9-17 * * * # 工作时间(9点到17点)每小时执行

# 斜杠(/) - 表示间隔
*/5 * * * * # 每5分钟执行一次
0 */2 * * * # 每2小时执行一次

Cron 表达式详解

秒字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 Node.JS 中使用 Node-cron 库 const cron = require('Node-cron');

// 每5秒执行一次 cron.schedule('*/5 * * * * *', () => {
console.log('每5秒执行一次:', new Date());
});

// 每分钟的第10秒执行 cron.schedule('10 * * * * *', () => {
console.log('每分钟的第10秒:', new Date());
});

// 10-20秒之间每秒执行 cron.schedule('10-20 * * * * *', () => {
console.log('10-20秒之间每秒执行:', new Date());
});

分钟字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 每分钟执行 cron.schedule('* * * * *', () => {
console.log('每分钟执行:', new Date());
});

// 每30分钟执行一次 cron.schedule('*/30 * * * *', () => {
console.log('每30分钟执行:', new Date());
});

// 每小时的第0分和第30分执行 cron.schedule('0,30 * * * *', () => {
console.log('每小时的0分和30分执行:', new Date());
});

// 每天的9点到17点,每15分钟执行一次 cron.schedule('*/15 9-17 * * *', () => {
console.log('工作时间内每15分钟执行:', new Date());
});

小时字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 每小时执行一次 cron.schedule('0 * * * *', () => {
console.log('每小时执行:', new Date());
});

// 每天的8点和20点执行 cron.schedule('0 8,20 * * *', () => {
console.log('每天8点和20点执行:', new Date());
});

// 每个工作日的上午9点执行 cron.schedule('0 9 * * 1-5', () => {
console.log('工作日上午9点执行:', new Date());
});

// 每天凌晨2点执行 cron.schedule('0 2 * * *', () => {
console.log('每天凌晨2点执行:', new Date());
});

日期字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 每月1号执行 cron.schedule('0 0 1 * *', () => {
console.log('每月1号执行:', new Date());
});

// 每月1号和15号执行 cron.schedule('0 0 1,15 * *', () => {
console.log('每月1号和15号执行:', new Date());
});

// 每月1-7号每小时执行 cron.schedule('0 0-23 1-7 * *', () => {
console.log('每月前7天每小时执行:', new Date());
});

// 每月最后一天执行 (L 表示 Last)
// 注意:并非所有 Cron 实现都支持 L 字符 cron.schedule('0 0 L * *', () => {
console.log('每月最后一天执行:', new Date());
});

月份字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 每月执行 cron.schedule('0 0 1 * *', () => {
console.log('每月1号执行:', new Date());
});

// 每季度初执行 (1月、4月、7月、10月)
cron.schedule('0 0 1 1,4,7,10 *', () => {
console.log('每季度初执行:', new Date());
});

// 每年的工作月份执行 cron.schedule('0 0 1 1-12 *', () => {
console.log('每年每月1号执行:', new Date());
});

// 指定月份的多个日期执行 cron.schedule('0 0 1,15 3,6,9,12 *', () => {
console.log('每年3、6、9、12月的1号和15号执行:', new Date());
});

星期字段详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 每周日执行 (0表示星期日)
cron.schedule('0 0 * * 0', () => {
console.log('每周日凌晨执行:', new Date());
});

// 工作日执行 (1-5表示周一到周五)
cron.schedule('0 0 * * 1-5', () => {
console.log('每周工作日凌晨执行:', new Date());
});

// 周六和周日执行 cron.schedule('0 0 * * 6,0', () => {
console.log('周末凌晨执行:', new Date());
});

// 每周三的上午9点执行 cron.schedule('0 9 * * 3', () => {
console.log('每周三上午9点执行:', new Date());
});

实际应用案例

系统维护任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 数据库备份任务 const { exec } = require('child_process');

// 每天凌晨2点执行数据库备份 cron.schedule('0 2 * * *', async () => {
console.log('开始数据库备份:', new Date());

try {
// 执行备份命令 const backupCmd = `mysqldump -u username -p password database_name > backup_$(date +%Y%m%d_%H%M%S).sql`;

exec(backupCmd, (error, stdout, stderr) => {
if (error) {
console.error('备份失败:', error);
return;
}

console.log('备份完成:', stdout);
});
} catch (error) {
console.error('备份任务执行出错:', error);
}
});

// 日志清理任务
// 每天凌晨3点清理30天前的日志 cron.schedule('0 3 * * *', () => {
const fs = require('fs');
const path = require('path');

const logDir = './logs';
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

fs.readdir(logDir, (err, files) => {
if (err) return;

files.forEach(file => {
const filePath = path.join(logDir, file);
fs.stat(filePath, (err, stats) => {
if (err) return;

if (stats.mtime < thirtyDaysAgo) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('删除日志文件失败:', err);
} else {
console.log('已删除旧日志文件:', filePath);
}
});
}
});
});
});
});

数据处理任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 每小时处理一次数据 cron.schedule('0 * * * *', async () => {
console.log('开始处理数据:', new Date());

try {
// 模拟数据处理逻辑 await processData();
console.log('数据处理完成');
} catch (error) {
console.error('数据处理失败:', error);
}
});

// 每天上午9点生成报告 cron.schedule('0 9 * * *', async () => {
console.log('开始生成日报:', new Date());

try {
const report = await generateDailyReport();
await saveReport(report, new Date());
console.log('日报生成完成');
} catch (error) {
console.error('报告生成失败:', error);
}
});

// 每周一上午8点生成周报 cron.schedule('0 8 * * 1', async () => {
console.log('开始生成周报:', new Date());

try {
const weeklyReport = await generateWeeklyReport();
await saveReport(weeklyReport, new Date());
console.log('周报生成完成');
} catch (error) {
console.error('周报生成失败:', error);
}
});

// 每月1号生成月报 cron.schedule('0 0 1 * *', async () => {
console.log('开始生成月报:', new Date());

try {
const monthlyReport = await generateMonthlyReport();
await saveReport(monthlyReport, new Date());
console.log('月报生成完成');
} catch (error) {
console.error('月报生成失败:', error);
}
});

监控和健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 每5分钟检查系统状态 cron.schedule('*/5 * * * *', async () => {
try {
const healthCheck = await performHealthCheck();
console.log('系统健康检查结果:', healthCheck);

if (!healthCheck.healthy) {
// 发送警报 await sendAlert(healthCheck);
}
} catch (error) {
console.error('健康检查失败:', error);
}
});

// 每小时检查 API 可用性 cron.schedule('0 * * * *', async () => {
try {
const apis = ['https://api1.example.com', 'https://api2.example.com'];

for (const apiUrl of apis) {
const response = await fetch(apiUrl, { timeout: 5000 });
if (!response.ok) {
console.error(`API ${apiUrl} 不可用: ${response.status}`);
await sendAlert(`API ${apiUrl} 不可用`);
}
}
} catch (error) {
console.error('API 检查失败:', error);
}
});

// 每天检查磁盘空间 cron.schedule('0 6 * * *', async () => {
const os = require('os');
const diskSpace = await getDiskSpace();

if (diskSpace.usagePercentage > 80) {
console.warn('磁盘空间不足:', diskSpace);
await sendAlert(`磁盘使用率达到 ${diskSpace.usagePercentage}%`);
}
});

业务相关任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 订单超时处理 cron.schedule('*/10 * * * *', async () => {
// 每10分钟检查超时订单 const expiredOrders = await findExpiredOrders();

for (const order of expiredOrders) {
await cancelOrder(order.id);
console.log(`订单 ${order.id} 已取消`);
}
});

// 优惠券到期提醒 cron.schedule('0 9 * * *', async () => {
// 每天上午9点检查今天到期的优惠券 const expiringCoupons = await findExpiringCoupons();

for (const coupon of expiringCoupons) {
await sendExpirationNotice(coupon.userId, coupon);
console.log(`发送优惠券到期提醒: ${coupon.id}`);
}
});

// 会员到期提醒 cron.schedule('0 10 * * *', async () => {
// 每天上午10点检查会员到期情况 const expiringMemberships = await findExpiringMemberships();

for (const membership of expiringMemberships) {
await sendMembershipExpirationNotice(membership.userId, membership);
console.log(`发送会员到期提醒: ${membership.userId}`);
}
});

高级 Cron 技巧

复杂时间安排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 每月的第一个周一
// 实际上需要通过代码逻辑实现,因为 Cron 无法直接表达 cron.schedule('0 9 1-7 * *', async () => {
const now = new Date();
const dayOfWeek = now.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday

if (dayOfWeek === 1) { // 如果是周一 console.log('每月的第一个周一执行:', now);
// 执行特定任务
}
});

// 每月的最后一个工作日 cron.schedule('0 18 * * 1-5', async () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;

// 获取当月最后一天 const lastDay = new Date(year, month, 0).getDate();
const isLastWeekdayOfMonth = now.getDate() === lastDay ||
(now.getDate() === lastDay - 1 && now.getDay() === 5) || // Friday before weekend
(now.getDate() === lastDay - 2 && now.getDay() === 4); // Thursday before long weekend

if (isLastWeekdayOfMonth) {
console.log('本月最后一个工作日:', now);
// 执行月末任务
}
});

// 工作日的特殊时间
// 每个工作日的上午9点和下午5点 cron.schedule('0 9,17 * * 1-5', () => {
console.log('工作日上午9点和下午5点:', new Date());
});

// 每月15号或最后一天 cron.schedule('0 0 15,L * *', () => {
console.log('每月15号或最后一天:', new Date());
});

动态 Cron 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 根据配置动态创建定时任务 class DynamicCronManager {
constructor() {
this.jobs = new Map();
}

// 添加定时任务 addJob(name, cronExpression, task) {
if (this.jobs.has(name)) {
this.removeJob(name);
}

const job = cron.schedule(cronExpression, task);
this.jobs.set(name, job);

console.log(`定时任务 ${name} 已添加: ${cronExpression}`);
}

// 移除定时任务 removeJob(name) {
if (this.jobs.has(name)) {
const job = this.jobs.get(name);
job.stop();
this.jobs.delete(name);
console.log(`定时任务 ${name} 已移除`);
}
}

// 更新定时任务 updateJob(name, newCronExpression, newTask) {
this.removeJob(name);
this.addJob(name, newCronExpression, newTask);
}

// 获取所有任务状态 getJobsStatus() {
const status = {};
for (const [name, job] of this.jobs) {
status[name] = {
expression: job.options.cronTime,
running: job.running
};
}
return status;
}
}

// 使用示例 const cronManager = new DynamicCronManager();

// 从配置文件读取定时任务配置 const cronConfig = {
dailyBackup: '0 2 * * *',
hourlyCheck: '0 * * * *',
weeklyReport: '0 9 * * 1'
};

Object.entries(cronConfig).forEach(([name, expression]) => {
cronManager.addJob(name, expression, () => {
console.log(`${name} 任务执行:`, new Date());
});
});

任务调度管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 任务调度管理器 class CronScheduler {
constructor() {
this.scheduler = require('Node-cron');
this.jobs = new Map();
this.pausedJobs = new Set();
}

// 创建任务 createJob(name, cronExpression, task, options = {}) {
const job = this.scheduler.schedule(cronExpression, () => {
if (this.pausedJobs.has(name)) {
return; // 任务被暂停,跳过执行
}

// 任务执行前钩子 if (options.beforeExecute) {
options.beforeExecute();
}

// 执行任务 Promise.resolve(task())
.then(result => {
console.log(`任务 ${name} 执行成功:`, result);

// 任务成功后钩子 if (options.onSuccess) {
options.onSuccess(result);
}
})
.catch(error => {
console.error(`任务 ${name} 执行失败:`, error);

// 任务失败后钩子 if (options.onError) {
options.onError(error);
}
});
}, {
scheduled: !options.paused, // 是否立即启动 timezone: options.timezone || 'Asia/Shanghai'
});

this.jobs.set(name, {
job: job,
expression: cronExpression,
options: options
});

return job;
}

// 启动任务 startJob(name) {
const jobInfo = this.jobs.get(name);
if (jobInfo) {
jobInfo.job.start();
this.pausedJobs.delete(name);
console.log(`任务 ${name} 已启动`);
}
}

// 暂停任务 pauseJob(name) {
const jobInfo = this.jobs.get(name);
if (jobInfo) {
jobInfo.job.stop();
this.pausedJobs.add(name);
console.log(`任务 ${name} 已暂停`);
}
}

// 重启任务 restartJob(name) {
this.pauseJob(name);
this.startJob(name);
}

// 更新任务表达式 updateJob(name, newExpression) {
const jobInfo = this.jobs.get(name);
if (jobInfo) {
// 先停止旧任务 jobInfo.job.stop();

// 创建新任务 this.createJob(name, newExpression,
() => jobInfo.job.callback(),
jobInfo.options
);

console.log(`任务 ${name} 表达式已更新: ${newExpression}`);
}
}

// 获取任务信息 getJobInfo(name) {
const jobInfo = this.jobs.get(name);
if (jobInfo) {
return {
name: name,
expression: jobInfo.expression,
running: jobInfo.job.running,
paused: this.pausedJobs.has(name),
nextRuns: jobInfo.job.nextDates(5) // 获取接下来5次运行时间
};
}
return null;
}
}

// 使用示例 const scheduler = new CronScheduler();

// 创建一个数据同步任务 scheduler.createJob(
'dataSync',
'0 */2 * * *', // 每2小时执行 async () => {
console.log('开始数据同步:', new Date());
await syncData();
console.log('数据同步完成:', new Date());
},
{
beforeExecute: () => console.log('数据同步即将开始'),
onSuccess: (result) => console.log('数据同步成功', result),
onError: (error) => console.error('数据同步失败', error)
}
);

// 创建一个健康检查任务 scheduler.createJob(
'healthCheck',
'*/5 * * * *', // 每5分钟执行 async () => {
const health = await checkSystemHealth();
return health;
}
);

常见问题与解决方案

时区问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 不同时区的 Cron 任务 const cron = require('Node-cron');

// UTC 时区执行(与服务器时间可能不一致)
const utcJob = cron.schedule('0 0 * * *', () => {
console.log('UTC 时间每天午夜执行:', new Date());
}, {
timezone: 'UTC'
});

// 中国时区执行 const chinaJob = cron.schedule('0 8 * * *', () => {
console.log('中国时间每天上午8点执行:', new Date());
}, {
timezone: 'Asia/Shanghai'
});

// 美国东部时间执行 const easternJob = cron.schedule('0 8 * * *', () => {
console.log('美国东部时间每天上午8点执行:', new Date());
}, {
timezone: 'America/New_York'
});

并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 防止任务重复执行 class ConcurrencyControl {
constructor() {
this.runningTasks = new Set();
}

async executeTask(taskName, taskFunction) {
if (this.runningTasks.has(taskName)) {
console.log(`任务 ${taskName} 正在运行,跳过本次执行`);
return;
}

this.runningTasks.add(taskName);

try {
const result = await taskFunction();
console.log(`任务 ${taskName} 执行完成`);
return result;
} catch (error) {
console.error(`任务 ${taskName} 执行失败:`, error);
throw error;
} finally {
this.runningTasks.delete(taskName);
}
}
}

const concurrencyControl = new ConcurrencyControl();

// 使用并发控制的任务 cron.schedule('0 */3 * * *', () => {
concurrencyControl.executeTask('longRunningTask', async () => {
console.log('长时间运行任务开始:', new Date());

// 模拟长时间运行的任务 await new Promise(resolve => setTimeout(resolve, 60000)); // 1分钟 console.log('长时间运行任务结束:', new Date());
});
});

错误处理和重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 带重试机制的任务执行器 class RetryableTask {
constructor(maxRetries = 3, retryDelay = 1000) {
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
}

async executeWithRetry(taskName, taskFunction) {
let lastError;

for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
console.log(`${taskName} - 尝试 ${attempt}/${this.maxRetries}`);
const result = await taskFunction();
console.log(`${taskName} - 执行成功 (尝试: ${attempt})`);
return result;
} catch (error) {
lastError = error;
console.error(`${taskName} - 执行失败 (尝试: ${attempt}):`, error.message);

if (attempt < this.maxRetries) {
console.log(`等待 ${this.retryDelay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
this.retryDelay *= 2; // 指数退避
}
}
}

console.error(`${taskName} - 所有重试均已失败`);
throw lastError;
}
}

const retryableTask = new RetryableTask(3, 2000);

// 使用重试机制的任务 cron.schedule('0 1 * * *', () => {
retryableTask.executeWithRetry('dailyCleanup', async () => {
// 可能失败的任务 await performCleanup();
});
});

Cron 表达式工具和验证

在线 Cron 表达式生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 一个简单的 Cron 表达式验证工具 class CronValidator {
static validate(expression) {
const parts = expression.trim().split(/\s+/);

if (parts.length !== 5 && parts.length !== 6) {
return {
valid: false,
error: 'Cron 表达式必须包含5个或6个字段'
};
}

const fieldValidators = [
this.validateSeconds,
this.validateMinutes,
this.validateHours,
this.validateDayOfMonth,
this.validateMonth,
this.validateDayOfWeek
];

const startIndex = parts.length === 5 ? 1 : 0;

for (let i = startIndex; i < parts.length; i++) {
const validator = fieldValidators[i];
const validationResult = validator(parts[i]);

if (!validationResult.valid) {
return {
valid: false,
error: `字段 ${i - startIndex + 1} (${this.getFieldName(i - startIndex)}): ${validationResult.error}`
};
}
}

return { valid: true };
}

static validateField(field, min, max) {
if (field === '*') return { valid: true };
if (field === '?') return { valid: true }; // 可选字段

// 检查逗号分隔的多个值 const values = field.includes(',') ? field.split(',') : [field];

for (const value of values) {
if (value.includes('-')) {
// 范围验证 const rangeParts = value.split('-');
if (rangeParts.length !== 2) {
return { valid: false, error: `无效的范围格式: ${value}` };
}

const start = parseInt(rangeParts[0]);
const end = parseInt(rangeParts[1]);

if (isNaN(start) || isNaN(end) || start < min || end > max || start > end) {
return { valid: false, error: `范围超出界限: ${value} (范围: ${min}-${max})` };
}
} else if (value.includes('/')) {
// 间隔验证 const intervalParts = value.split('/');
if (intervalParts.length !== 2) {
return { valid: false, error: `无效的间隔格式: ${value}` };
}

const divisor = parseInt(intervalParts[1]);
if (isNaN(divisor) || divisor <= 0) {
return { valid: false, error: `无效的间隔值: ${value}` };
}
} else {
// 单个值验证 const num = parseInt(value);
if (isNaN(num) || num < min || num > max) {
return { valid: false, error: `值超出界限: ${num} (范围: ${min}-${max})` };
}
}
}

return { valid: true };
}

static validateSeconds(field) { return this.validateField(field, 0, 59); }
static validateMinutes(field) { return this.validateField(field, 0, 59); }
static validateHours(field) { return this.validateField(field, 0, 23); }
static validateDayOfMonth(field) { return this.validateField(field, 1, 31); }
static validateMonth(field) { return this.validateField(field, 1, 12); }
static validateDayOfWeek(field) { return this.validateField(field, 0, 7); }

static getFieldName(index) {
const names = ['秒', '分钟', '小时', '日期', '月份', '星期'];
return names[index] || '未知';
}
}

// 使用示例 console.log(CronValidator.validate('0 0 12 * * *')); // { valid: true }
console.log(CronValidator.validate('0 0 25 * *')); // { valid: false, error: '字段 3 (小时): 值超出界限: 25 (范围: 0-23)' }

Cron 表达式解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// Cron 表达式解析器 class CronParser {
static parse(expression) {
const parts = expression.trim().split(/\s+/);
if (parts.length !== 5 && parts.length !== 6) {
throw new Error('Invalid cron expression: must have 5 or 6 fields');
}

const isSixFields = parts.length === 6;

return {
seconds: isSixFields ? parts[0] : '0',
minutes: isSixFields ? parts[1] : parts[0],
hours: isSixFields ? parts[2] : parts[1],
dayOfMonth: isSixFields ? parts[3] : parts[2],
month: isSixFields ? parts[4] : parts[3],
dayOfWeek: isSixFields ? parts[5] : parts[4],
toString: () => expression,
isSixFields: isSixFields
};
}

static expand(field, max) {
const result = [];

if (field === '*') {
for (let i = 0; i <= max; i++) {
result.push(i);
}
} else if (field.includes(',')) {
const values = field.split(',');
for (const value of values) {
if (value.includes('-')) {
const [start, end] = value.split('-').map(Number);
for (let i = start; i <= end; i++) {
result.push(i);
}
} else if (value.includes('/')) {
const [range, step] = value.split('/');
if (range === '*') {
for (let i = 0; i <= max; i += Number(step)) {
result.push(i);
}
} else {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i += Number(step)) {
result.push(i);
}
}
} else {
result.push(Number(value));
}
}
} else if (field.includes('-')) {
const [start, end] = field.split('-').map(Number);
for (let i = start; i <= end; i++) {
result.push(i);
}
} else if (field.includes('/')) {
const [range, step] = field.split('/');
if (range === '*') {
for (let i = 0; i <= max; i += Number(step)) {
result.push(i);
}
} else {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i += Number(step)) {
result.push(i);
}
}
} else {
result.push(Number(field));
}

return [...new Set(result)].sort((a, b) => a - b);
}

static getNextRun(expression, startDate = new Date()) {
const parsed = this.parse(expression);
const now = new Date(startDate);
const targetDate = new Date(now);

// 由于 Cron 表达式复杂性,这里只提供一个简化的实现
// 实际应用中可能需要更复杂的算法 const minuteValues = this.expand(parsed.minutes, 59);
const hourValues = this.expand(parsed.hours, 23);
const dayValues = this.expand(parsed.dayOfMonth, 31);
const monthValues = this.expand(parsed.month, 12).map(m => m - 1); // 月份从0开始 const dayOfWeekValues = this.expand(parsed.dayOfWeek, 6);

// 这是一个简化的实现,实际应用中可能需要考虑更多边界情况 targetDate.setMinutes(minuteValues[0] || 0);
targetDate.setHours(hourValues[0] || 0);
targetDate.setDate(dayValues[0] || 1);
targetDate.setMonth(monthValues[0] || 0);

if (targetDate <= now) {
targetDate.setDate(targetDate.getDate() + 1);
}

return targetDate;
}
}

// 使用示例 const parser = CronParser.parse('0 30 9 * * *'); // 每天上午9:30
console.log(parser); // 解析结果 console.log(CronParser.getNextRun('0 30 9 * * *')); // 下次执行时间

最佳实践

1. 选择合适的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
// 避免系统繁忙时段
// 错误示例:在高峰期执行耗时任务
// cron.schedule('0 9 * * *', heavyTask); // 9点是工作高峰

// 正确示例:选择低峰时段 cron.schedule('0 2 * * *', heavyTask); // 凌晨2点,系统负载较低

// 随机化执行时间以分散负载 function getRandomizedSchedule(baseMinute, baseHour) {
const randomOffset = Math.floor(Math.random() * 10); // 随机偏移0-9分钟 return `${baseMinute + randomOffset} ${baseHour} * * *`;
}

// 多个相似任务使用随机时间避免同时执行 cron.schedule(getRandomizedSchedule(0, 2), task1);
cron.schedule(getRandomizedSchedule(0, 2), task2);
cron.schedule(getRandomizedSchedule(0, 2), task3);

2. 任务执行时间限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 设置任务超时时间 function createTimeoutTask(taskFunction, timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Task timeout'));
}, timeoutMs);

Promise.resolve(taskFunction())
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(error => {
clearTimeout(timeoutId);
reject(error);
});
});
}

// 使用示例 cron.schedule('0 */1 * * * *', async () => {
try {
await createTimeoutTask(async () => {
// 可能长时间运行的任务 await heavyComputation();
}, 60000); // 1分钟超时
} catch (error) {
console.error('任务执行超时或失败:', error);
}
});

3. 任务依赖管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 任务依赖链 class TaskDependencyManager {
constructor() {
this.tasks = new Map();
this.dependencies = new Map();
this.completedTasks = new Set();
}

addTask(name, taskFunction, dependencies = []) {
this.tasks.set(name, taskFunction);
this.dependencies.set(name, dependencies);
}

async executeTask(name) {
const deps = this.dependencies.get(name) || [];

// 等待依赖任务完成 for (const dep of deps) {
if (!this.completedTasks.has(dep)) {
await this.executeTask(dep);
}
}

// 执行当前任务 const task = this.tasks.get(name);
if (task) {
try {
await task();
this.completedTasks.add(name);
console.log(`任务 ${name} 执行完成`);
} catch (error) {
console.error(`任务 ${name} 执行失败:`, error);
throw error;
}
}
}

async executeAll() {
// 简化实现,实际应用中可能需要拓扑排序 for (const [name] of this.tasks) {
if (!this.completedTasks.has(name)) {
await this.executeTask(name);
}
}
}
}

// 使用示例 const taskManager = new TaskDependencyManager();

taskManager.addTask('databaseBackup', async () => {
console.log('执行数据库备份');
}, []);

taskManager.addTask('generateReport', async () => {
console.log('生成报告');
}, ['databaseBackup']); // 依赖数据库备份 taskManager.addTask('sendEmail', async () => {
console.log('发送邮件');
}, ['generateReport']); // 依赖报告生成

常用 Cron 表达式示例

基础示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 每分钟执行
* * * * *

# 每小时执行
0 * * * *

# 每天凌晨1点执行
0 1 * * *

# 每周一凌晨1点执行
0 1 * * 1

# 每月1号凌晨1点执行
0 1 1 * *

# 每季度初执行
0 0 1 1,4,7,10 *

# 每年1月1日12:00执行
0 0 1 1 *

复杂示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 每个工作日(周一到周五)上午9点
0 9 * * 1-5

# 每周六和周日上午9点
0 9 * * 6,0

# 每月的1号、15号和28号上午6点
0 6 1,15,28 * *

# 每小时的第30分钟执行
30 * * * *

# 每天的9点到17点,每30分钟执行
*/30 9-17 * * *

# 每月第一个周一的上午9点
0 9 1-7 * * [ $(date +\%w) -eq 1 ]

# 每月最后一个工作日的下午5点
0 17 * * 1-5 [ $(date -d "+1 day" +\%d) -eq 1 ]

# 每个工作日的上班时间(9-18点)每小时执行
0 9-18 * * 1-5

# 每月的前7天每天执行
0 0 1-7 * *

# 每小时的前10分钟每分钟执行
*/1 * * * * [ $(date +\%M) -lt 10 ]

# 每天的指定时间(8:30、12:30、16:30)
30 8,12,16 * * *

总结

  • Cron 表达式是配置定时任务的标准格式,由多个字段组成,每个字段代表不同的时间单位
  • 了解各个字段的取值范围和特殊字符含义是掌握 Cron 的关键
  • 在实际应用中需要考虑时区、并发控制、错误处理等实际问题
  • 合理选择执行时间,避免在系统繁忙时段执行耗时任务
  • 使用适当的工具和验证机制确保 Cron 表达式的正确性
  • 建立完善的日志和监控机制来跟踪定时任务的执行情况
  • 考虑任务间的依赖关系,确保正确的执行顺序

今天早上定了8点的闹钟,突然想起 cron 表达式的8点配置,真是生活与工作完美结合的体现!Cron 就像一位勤劳的时间管家,默默地在后台执行着各种定时任务,让我们的系统和服务能够自动化运行。

扩展阅读

  1. Cron 表达式详解
  2. Node-Cron 官方文档
  3. Linux Crontab 手册
  4. Quartz Scheduler Cron 表达式

练习建议

  1. 尝试编写各种复杂时间需求的 Cron 表达式
  2. 使用在线 Cron 表达式验证工具测试表达式
  3. 在项目中实现定时任务管理功能
  4. 研究不同 Cron 实现的差异和特性
bulb