Cron表达式实战宝典:定时任务从入门到精通
我第一次被cron坑,是把一个"每天发摘要邮件"的任务写成了 */5 * * * *。当时脑子里想的是"5"代表5点,手一抖写进了分钟那一列。结果它每5分钟跑一次,一晚上往用户邮箱里塞了快一万封邮件,第二天早上是被客服的电话叫醒的。从那以后我养成了一个习惯:任何cron表达式上线前,先拿生成器跑一遍,看清楚它到底在哪几列。
cron这东西就是这样,语法看着简单——五个数字、几个符号——但坑全在细节里。日期那列和星期那列同时写会怎么样?服务器跑在UTC时区,你以为的"凌晨3点备份"其实是上午11点。这些不踩一遍很难记住。这篇就把我这些年攒下来的经验和踩过的坑都摊开讲,从语法到实战,希望能帮你少挨几个凌晨的电话。
目录
Cron基础概念
什么是Cron?
Cron是一个Linux/Unix的定时任务守护进程(daemon),它会在后台运行,并根据配置的时间表(crontab)自动执行任务。
Cron的应用场景
- 📊 数据备份:每天凌晨自动备份数据库
- 📧 发送邮件:定期发送报表邮件
- 🔄 数据同步:定时同步远程数据
- 🧹 清理任务:定期清理临时文件
- 📈 数据分析:每小时统计业务数据
- 🔔 监控告警:定时检查系统状态
Cron表达式语法
标准Cron表达式(5位)
┌───────────── 分钟 (0 - 59)
│ ┌───────────── 小时 (0 - 23)
│ │ ┌───────────── 日期 (1 - 31)
│ │ │ ┌───────────── 月份 (1 - 12)
│ │ │ │ ┌───────────── 星期 (0 - 6,0代表星期日)
│ │ │ │ │
* * * * *
扩展Cron表达式(6或7位)
┌───────────── 秒(0 - 59)【可选】
│ ┌───────────── 分钟 (0 - 59)
│ │ ┌───────────── 小时 (0 - 23)
│ │ │ ┌───────────── 日期 (1 - 31)
│ │ │ │ ┌───────────── 月份 (1 - 12)
│ │ │ │ │ ┌───────────── 星期 (0 - 6)
│ │ │ │ │ │ ┌───────────── 年份【可选】
│ │ │ │ │ │ │
* * * * * * *
特殊字符说明
| 字符 | 含义 | 示例 |
|---|---|---|
* | 任意值 | * * * * * = 每分钟 |
, | 列表分隔 | 1,15,30 * * * * = 每小时的1、15、30分 |
- | 范围 | 1-5 * * * * = 每小时的1到5分 |
/ | 步长 | */5 * * * * = 每5分钟 |
? | 不指定值(仅日期和星期) | 0 0 ? * 1 = 每周一零点 |
L | 最后(Last) | 0 0 L * * = 每月最后一天 |
W | 工作日(Weekday) | 0 0 15W * * = 最接近15号的工作日 |
# | 第几个星期几 | 0 0 ? * 6#3 = 每月第3个星期五 |
五个字段的踩坑点
下面这张表是我贴在工位上的,写每个字段前都瞄一眼,专治记混:
| 位置 | 字段 | 范围 | 最容易踩的坑 |
|---|---|---|---|
| 第1列 | 分钟 | 0-59 | 把小时数字写进这列,5变成"每小时第5分钟"而不是"5点" |
| 第2列 | 小时 | 0-23 | 24小时制,没有 AM/PM;下午5点是 17 不是 5 |
| 第3列 | 日期 | 1-31 | 和星期列同时指定时是"或"关系,不是"且",下面会专门讲 |
| 第4列 | 月份 | 1-12 | 这列基本没坑,但别忘了它存在,少写一列整个表达式就错位了 |
| 第5列 | 星期 | 0-6 | 0 和 7 一般都代表周日;范围 1-5 才是工作日,别写成 0-4 |
记不住顺序就背一句"分时日月周",从左到右,缺一不可。
常用Cron表达式
基础时间表达式
# 每分钟执行
* * * * *
# 每小时执行(整点)
0 * * * *
# 每天执行(午夜)
0 0 * * *
# 每周执行(周日午夜)
0 0 * * 0
# 每月执行(1号午夜)
0 0 1 * *
# 每年执行(1月1日午夜)
0 0 1 1 *
固定时间执行
# 每天早上8点
0 8 * * *
# 每天下午5点30分
30 17 * * *
# 每周一早上9点
0 9 * * 1
# 每月15号中午12点
0 12 15 * *
# 每年12月31日晚上11点59分
59 23 31 12 *
间隔时间执行
# 每5分钟执行
*/5 * * * *
# 每15分钟执行
*/15 * * * *
# 每30分钟执行
*/30 * * * *
# 每2小时执行
0 */2 * * *
# 每6小时执行
0 */6 * * *
# 每3天执行(午夜)
0 0 */3 * *
工作日vs周末
# 工作日(周一到周五)早上9点
0 9 * * 1-5
# 周末(周六和周日)早上10点
0 10 * * 6,0
# 工作日每小时
0 * * * 1-5
# 只在工作日的工作时间(9-18点)
0 9-18 * * 1-5
特定时间段
# 早上9点到下午6点,每小时
0 9-18 * * *
# 凌晨1点到5点,每2小时
0 1-5/2 * * *
# 每个工作日的下午1点到5点,每30分钟
*/30 13-17 * * 1-5
# 每周一、三、五的早上8点和晚上8点
0 8,20 * * 1,3,5
高级技巧
组合条件
# 每月1号和15号的早上9点
0 9 1,15 * *
# 每周一和周五的早上9点、中午12点、下午6点
0 9,12,18 * * 1,5
# 每季度第一天(1、4、7、10月的1号)早上9点
0 9 1 1,4,7,10 *
# 工作日的每个整点(周一到周五)
0 * * * 1-5
使用特殊字符
# 每月最后一天
0 0 L * *
# 每月最后一个工作日
0 0 LW * *
# 每月最后一个周五
0 0 ? * 5L
# 每月第一个周一
0 0 ? * 1#1
# 每月第三个周三
0 0 ? * 3#3
# 最接近15号的工作日
0 0 15W * *
避开特定时间
# 不在整点执行(每小时的第1分钟)
1 * * * *
# 避开午夜(凌晨1点)
0 1 * * *
# 工作时间外(晚上7点到早上7点)
0 19-7 * * *
# 非工作日(周末)
0 0 * * 0,6
不同平台的差异
Linux Crontab
# 查看当前用户的crontab
crontab -l
# 编辑crontab
crontab -e
# 删除crontab
crontab -r
# crontab格式(5位)
# 分 时 日 月 周 命令
30 2 * * * /path/to/backup.sh
JavaScript (node-cron)
const cron = require('node-cron');
// 6位表达式(包含秒)
// 秒 分 时 日 月 周
cron.schedule('0 */5 * * * *', () => {
console.log('每5分钟执行一次');
});
// 支持的特殊字符
cron.schedule('*/10 * * * * *', () => {
console.log('每10秒执行');
});
// 任务控制
const task = cron.schedule('* * * * *', () => {
console.log('运行中...');
});
task.stop(); // 停止任务
task.start(); // 启动任务
Spring (Java)
import org.springframework.scheduling.annotation.Scheduled;
// 6位表达式
@Scheduled(cron = "0 0 2 * * ?")
public void dailyBackup() {
// 每天凌晨2点执行
}
// 7位表达式(包含年份)
@Scheduled(cron = "0 0 0 1 1 ? 2024")
public void newYearTask() {
// 2024年1月1日零点执行
}
Quartz (Java/C#)
// Quartz使用7位表达式
// 秒 分 时 日 月 周 年
"0 0 12 ? * WED" // 每周三中午12点
"0 15 10 * * ? *" // 每天上午10:15
"0 0/5 14 * * ?" // 每天下午2点到2:55,每5分钟
实战案例
案例1:数据库备份
# 每天凌晨3点备份数据库
0 3 * * * /usr/local/bin/backup-database.sh
# backup-database.sh脚本内容
#!/bin/bash
DATE=$(date +%Y%m%d)
BACKUP_DIR="/backups"
DB_NAME="myapp"
# 创建备份
mysqldump -u root -p$DB_PASSWORD $DB_NAME > $BACKUP_DIR/db_$DATE.sql
# 压缩备份文件
gzip $BACKUP_DIR/db_$DATE.sql
# 删除30天前的备份
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete
echo "Database backup completed at $(date)"
案例2:日志清理
# 每周日凌晨4点清理旧日志
0 4 * * 0 /usr/local/bin/cleanup-logs.sh
# cleanup-logs.sh
#!/bin/bash
LOG_DIR="/var/log/myapp"
# 删除7天前的日志
find $LOG_DIR -name "*.log" -mtime +7 -delete
# 压缩3天前的日志
find $LOG_DIR -name "*.log" -mtime +3 -exec gzip {} \;
echo "Log cleanup completed"
案例3:健康检查
# 每5分钟检查服务状态
*/5 * * * * /usr/local/bin/health-check.sh
# health-check.sh
#!/bin/bash
SERVICE_URL="https://myapp.com/health"
ALERT_EMAIL="admin@example.com"
# 检查服务
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $SERVICE_URL)
if [ $STATUS -ne 200 ]; then
echo "Service is down! Status: $STATUS" | \
mail -s "ALERT: Service Down" $ALERT_EMAIL
# 尝试重启服务
systemctl restart myapp
fi
案例4:定时报表
# 每天早上9点生成并发送报表
0 9 * * * /usr/local/bin/daily-report.sh
# daily-report.sh
#!/bin/bash
REPORT_DATE=$(date +%Y-%m-%d)
REPORT_FILE="/tmp/report_$REPORT_DATE.csv"
# 生成报表
mysql -u报表用户 -p密码 -e "
SELECT
DATE(created_at) as date,
COUNT(*) as total_orders,
SUM(amount) as total_revenue
FROM orders
WHERE DATE(created_at) = CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(created_at)
" mydb > $REPORT_FILE
# 发送邮件
echo "Daily Report" | mail -s "报表 $REPORT_DATE" \
-a $REPORT_FILE manager@example.com
# 清理临时文件
rm $REPORT_FILE
案例5:缓存预热
# 每天凌晨5点预热缓存
0 5 * * * /usr/local/bin/warm-cache.sh
# warm-cache.sh
#!/bin/bash
API_URL="https://myapp.com/api"
# 预热热门页面
curl -s "$API_URL/home" > /dev/null
curl -s "$API_URL/products" > /dev/null
curl -s "$API_URL/categories" > /dev/null
echo "Cache warming completed at $(date)"
常见问题
Q1: Cron任务没有执行怎么办?
检查清单:
# 1. 检查cron服务是否运行
systemctl status cron
# 2. 检查crontab语法
crontab -l
# 3. 查看cron日志
tail -f /var/log/cron
# 或
grep CRON /var/log/syslog
# 4. 检查脚本权限
ls -l /path/to/script.sh
chmod +x /path/to/script.sh
# 5. 使用绝对路径
# ❌ 错误
0 0 * * * backup.sh
# ✅ 正确
0 0 * * * /usr/local/bin/backup.sh
# 6. 设置环境变量
0 0 * * * source /home/user/.bashrc && /path/to/script.sh
Q2: 如何调试Cron任务?
# 方法1:输出到日志文件
0 0 * * * /path/to/script.sh >> /var/log/myscript.log 2>&1
# 方法2:发送邮件
# cron会自动将输出发送到用户邮箱
0 0 * * * /path/to/script.sh
# 方法3:临时增加频率测试
*/1 * * * * /path/to/script.sh # 改为每分钟,测试完改回
# 方法4:手动运行脚本
/path/to/script.sh
Q3: Cron表达式生成器
// JavaScript Cron表达式生成器
class CronBuilder {
constructor() {
this.minute = '*';
this.hour = '*';
this.day = '*';
this.month = '*';
this.weekday = '*';
}
everyMinute() {
this.minute = '*';
return this;
}
atMinute(minute) {
this.minute = minute.toString();
return this;
}
everyNMinutes(n) {
this.minute = `*/${n}`;
return this;
}
atHour(hour) {
this.hour = hour.toString();
return this;
}
everyNHours(n) {
this.hour = `*/${n}`;
return this;
}
onDay(day) {
this.day = day.toString();
return this;
}
onWeekday(weekday) {
this.weekday = weekday.toString();
return this;
}
build() {
return `${this.minute} ${this.hour} ${this.day} ${this.month} ${this.weekday}`;
}
}
// 使用示例
const cron = new CronBuilder()
.everyNMinutes(30)
.atHour(9)
.onWeekday(1) // 周一
.build();
console.log(cron); // "*/30 9 * * 1"
Q4: 如何表达"每个工作日的每小时"?
# 方法1:使用星期范围
0 * * * 1-5
# 方法2:使用列表
0 * * * 1,2,3,4,5
# 解释:
# 0 - 每小时的第0分钟(整点)
# * - 每小时
# * - 每天
# * - 每月
# 1-5 - 周一到周五
Q5: 时区问题如何处理?
# 方法1:在crontab中设置时区
CRON_TZ=Asia/Shanghai
0 9 * * * /path/to/script.sh
# 方法2:在脚本中转换时区
#!/bin/bash
export TZ='Asia/Shanghai'
echo "Current time: $(date)"
# 方法3:使用系统时区
timedatectl set-timezone Asia/Shanghai
写在最后
说句实在话,cron适合的是规整的周期任务——每天几点、每隔几分钟。一旦调度逻辑复杂起来,比如"每月最后一个工作日但跳过节假日",硬抠cron表达式只会让自己和接手的同事都头疼,那种场景该上专门的调度系统(Airflow、Quartz 之类)就别犹豫。
我的铁律就一条:任何表达式上线前一定先验证。别凭感觉,更别直接 crontab -e 保存了事。我现在的习惯是写完先丢进生成器里,看清楚它解析出来的"下次执行时间"对不对,确认是凌晨3点而不是下午3点,再粘到服务器上。这一步花不了十秒,能省掉一万封邮件那种半夜惊魂。我们站上那个 Cron生成器 就是纯浏览器里跑的,表达式不会发到任何服务器,边改边看下次触发时间,挺顺手。
几个常用符号,混了就回来翻
- 从左到右:分→时→日→月→周
- 星号代表全部:
*表示任意值 - 逗号表示或:
1,15表示1或15 - 横线表示范围:
1-5表示1到5 - 斜杠表示间隔:
*/5表示每5个单位
最佳实践
- ✅ 使用注释说明cron任务的用途
- ✅ 记录输出到日志文件
- ✅ 使用绝对路径
- ✅ 测试脚本权限和执行权限
- ✅ 避开高峰时段执行重任务
相关工具
- ⏰ 在线Cron表达式生成器 - 可视化生成Cron表达式
- 🕐 时间戳转换工具 - 时间格式转换
- 📅 日期计算器 - 计算日期差
关键词: Cron表达式, 定时任务, Linux, Crontab, 任务调度
更新时间: 2026-01-05
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们相关阅读
一张照片能做多少事:AI 改图工具实战指南
换发型、换发色、换装、转卡通、风格迁移、室内设计、老照片修复——用一张照片走完这些 AI 改图流程,以及怎么让结果更稳、更像本人。
把脸保住:AI 改图提示词怎么写才不"换人"
换发型、换装、换风格时,AI 老把脸也改了?这篇讲清楚改图提示词的写法:保人约束、双通道(gpt-image-2 / Nano Banana)各自的脾气,以及一套能直接抄的模板。
TypeScript 类型体操:什么时候值,什么时候在炫技
务实的 TypeScript 高级类型指南 — mapped types、conditional types、template literal types 真正能给你什么,什么时候用,什么时候应该退回到朴素代码。