时间怎么算?cron 五个字段、时区与日期校验
时间处理分三个层面:绝对时刻(时间戳)、未来规则(cron)、以及被写进字符串里的日期(身份证号里的生日)。三者的坑不一样,但都会导致「差几个小时」或「日期不存在」。
- cron 的日与周同时被限定时按「或」处理,
0 0 1 * 1表示每月 1 号或每周一,而各家 cron 实现对此有分歧 - 时间戳是距 1970-01-01 00:00:00 UTC 的秒数,本身没有时区,只有换算成日期时才涉及时区
- 秒级时间戳塞进 32 位有符号整数会在 2038-01-19 03:14:07 之后溢出成负数
- 10 位数值是秒、13 位是毫秒,11 位才是真正的歧义区间,会被当成秒也就是约公元 2286 年
- 身份证号里的生日要排除 2 月 30 日这类不存在的日期,而 15 位老号码没有校验位、只能核格式
#三个层面,三种坑
时间问题之所以难查,是因为「时间」在系统里以三种完全不同的形态存在:一个数字、一条规则、一段字符串。 三者的错误表现完全不同。
| 形态 | 例子 | 典型错误 |
|---|---|---|
| 绝对时刻 | 1780000000 | 秒当毫秒、时区换算差 8 小时 |
| 未来规则 | 0 9 * * 1-5 | 日与周的语义、时区与展示端不同 |
| 字符串日期 | 19900230 | 日期根本不存在,但格式合法 |
#cron:五个字段与「或」的语义
cron 表达式是五段:分、时、日、月、周。日与周同时被限定时,标准 Vixie cron 按「或」处理。 这条最容易记反。
0 0 1 * 1 的意思是「每月 1 号或每周一」,而不是「1 号且周一」。写成「且」的话,一年只会有几次同时满足,看起来像定时任务没生效。
用 Cron 表达式生成 时,几条边界要知道:
- 只认数字和
*(任意)、/(步进)、-(范围)、,(枚举)四种语法 - 带字母的写法(
MON-FRI)、Quartz 的?LW、@daily这类宏都不被接受 —— 它们本来就不是同一种方言 - 「下次执行时间」用浏览器本地时区搜索,不是服务器时区
排程跑偏最常见的原因就是时区。工具按你浏览器的时区算,服务器按它自己的时区执行。两边不一致时,这里显示的时间要按差值换算 —— 差 8 小时的话,早上 9 点的任务会在你下午 5 点跑。
另外,0 0 29 2 *(闰日)这类表达式在两年内可能一次都不触发,工具会显示找不到下次执行时间,这不是 bug。
#时间戳:它没有时区
Unix 时间戳是「距 1970-01-01 00:00:00 UTC 的秒数」,它本身不含时区信息。 所以同一串数字在任何国家代表同一个瞬间 —— 时区只在换算成日期时才介入。
用 Unix 时间戳 时,输出会同时给出本地时间与 UTC、北京、东京、伦敦、纽约五地对照。伦敦和纽约会随夏令时自动偏移,所以它们的偏移量标注成 UTC+0/+1 与 UTC-5/-4;北京时间不实行夏令时,恒定 UTC+8。
反向换算取的是浏览器所在时区的时间,没有时区选择器。想算某个 UTC 时刻对应的秒数,需要先把时差折进去:比如把 UTC 12:00 按本地 UTC+8 填成 20:00,否则会差几个小时。
复制按钮复制的是 13 位毫秒值。需要 10 位秒级时去掉后三位即可 —— 反过来把 10 位当毫秒用,会得到一个 1970 年的日期,这是「时间显示成 1970 年」的唯一原因。
#2038 与 11 位的歧义
把秒级时间戳塞进 32 位有符号整数,上限是 2147483647,对应 2038-01-19 03:14:07 UTC,之后会溢出成负数。 这就是「2038 问题」。
现代实现大多已经改用 64 位或浮点秒,但老系统、嵌入式设备、部分数据库字段仍有这个隐患。检查办法:把秒级值手动加到一个远期日期上,看会不会变负。
秒与毫秒的判定规则也值得记住:
- 10 位:按秒解释(约 2001 到 2286 年)
- 13 位:按毫秒解释
- 11 位:真正的歧义区间,工具按秒处理,也就是约公元 2286 年 —— 遇到 11 位请自己补三位或去掉三位
早期实现用 1e12 做阈值,会把 2000-01-01 的毫秒值 946684800000 解析成公元 31969 年。现在阈值是 1e11(12 位及以上算毫秒),这类误判就消失了。
#日期字符串:为什么会「不存在」
形如 19900230 的日期格式完全合法,但 2 月没有 30 号 —— 这类「合法但不存在」的日期是字符串时间最隐蔽的坑。 范围检查(月 1-12、日 1-31)挡不住它。
用 身份证信息查询 时,生日部分除了范围检查,还会用「按年月日构造日期后回读比对」的方式排除 2 月 30 日这类情况;如果生日晚于今天,结果里也会标注出来。
身份证的时间信息还有两个容易误判的点:
- 15 位老号码只出现在 1999 年之前,没有校验位、年份只有两位(按 19xx 补全)。所以对它只能核格式与生日是否合理,页面会明确写「无法核验」,不会给出通过结论
- 生肖按公历年算,而农历春节前出生的人属上一年的生肖,1 月到 2 月中旬的生日需要自行核对
18 位身份证的校验位有确定算法:前 17 位按固定权重加权求和、对 11 取模、再用 10X98765432 映射。但「校验通过」只代表最后一位对得上,不代表号码真实存在 —— 任何人都能按同一套算法造出合法号码。真实核验只有公安或运营商渠道能做。
#什么时候该用哪个工具
| 你想做的事 | 用哪个 | 关键约束 |
|---|---|---|
| 把秒或毫秒换成日期 | Unix 时间戳 | 11 位是歧义区间;输出五地时区对照 |
| 写定时任务表达式 | Cron 表达式生成 | 只支持 5 段与四种语法;日周为「或」 |
| 核对身份证里的生日 | 身份证信息查询 | 15 位无法核验校验位 |
| 验证两个时间字符串是否相同 | 文本差异对比 | 逐字符比较,2026-11-05 与 2026-11-5 不同 |
| 生成不重复的事件 ID | UUID / GUID 生成 | v1 含时间戳,v4 纯随机 |
处理顺序建议:先确定这个时间的语义(绝对时刻 / 规则 / 字符串),再选工具。 三者混着看,就会在时区、闰日、2 月 30 日之间反复打转。
#常见问题
#cron 写的「1 号和周一」为什么不生效?
因为标准语义是「或」:0 0 1 * 1 会在每个 1 号以及每个周一都执行。想表达「且」(既是 1 号又是周一),标准 cron 做不到,需要在脚本里自己判断日期。注意各家实现有分歧,写运维脚本前先确认目标机的语义。
#时间戳转出来的时间差 8 小时?
因为反向换算用的是浏览器本地时区。要得到某个 UTC 时刻对应的秒数,先把时差折进去;要显示某个服务器时区的时间,先在脑子里做一次偏移。工具没有时区选择器,这是有意的简化。
#时间显示成 1970 年?
说明把秒当毫秒了。10 位秒级值乘 1000 才是毫秒;直接当毫秒用就是 1970 年 1 月。反过来 13 位当秒用会得到一个几十万年后的日期。
#2038 年之后的时间戳还能用吗?
64 位系统上没问题。有风险的是把时间戳存进 32 位有符号整数的场景:数据库 INT 字段、嵌入式设备、老的语言运行时。检查办法是看那个字段的类型是不是 INT(而不是 BIGINT)。
#身份证校验通过就说明号码是真的吗?
不是。校验位只是一次算术校验,任何人都能按同样的算法造出通过校验的号码。它只能发现「输错一位」这类错误,不能证明号码存在。真实核验需要通过公安或运营商渠道。