处理闰秒
引言
闰秒是对协调世界时(UTC)偶尔添加的一秒调整,以保持其与地球自转同步。这是软件开发中时间处理最复杂的方面之一。
快速总结:UTC 添加闰秒以保持在 UT1(太阳时)的 0.9 秒以内。截至 2026 年 1 月,自 1972 年以来已添加 37 个闰秒,使 UTC 比国际原子时(TAI)慢 37 秒。
什么是闰秒?
定义
一个闰秒是对 UTC 应用的一秒调整,用于考虑:
地球自转减速:地球自转逐渐变慢
不规律的自转速率:地球自转速度不可预测地变化
UT1-UTC 偏移:保持 UTC 在太阳时(UT1)的 0.9 秒内
CopyTEXT1闰秒公式:
2 如果 UT1 - UTC > 0.9 秒 → 添加正闰秒
3 如果 UT1 - UTC < -0.9 秒 → 添加负闰秒
4
5结果:UTC 保持与地球自转同步
闰秒如何工作
当添加闰秒时,UTC 日的最后一分钟有 61 秒而不是 60 秒:
CopyTEXT1普通日(无闰秒):
2 23:59:58 UTC
3 23:59:59 UTC
4 00:00:00 UTC(第二天)
5
6闰秒日:
7 23:59:58 UTC
8 23:59:59 UTC
9 23:59:60 UTC ← 闰秒!
10 00:00:00 UTC(第二天)
注意:负闰秒在实践中从未出现过,尽管如果地球自转突然加速,理论上是可能的。
闰秒历史
时间线
年份事件UTC-TAI 偏移1972首次添加闰秒+10 秒1972-1984添加 12 个闰秒+22 秒1985-1995添加 8 个闰秒+29 秒1996-2005添加 3 个闰秒+32 秒2008-2016添加 3 个闰秒+35 秒2017最近一次闰秒+36 秒2025未来闰秒+37 秒
最近的闰秒
CopyTEXT1所有闰秒(1972 - 2025):
2 - 1972-06-30: +1 秒(UTC-TAI = +11s)
3 - 1972-12-31: +1 秒(UTC-TAI = +12s)
4 - 1973-12-31: +1 秒(UTC-TAI = +13s)
5 - 1974-12-31: +1 秒(UTC-TAI = +14s)
6 - 1975-12-31: +1 秒(UTC-TAI = +15s)
7 - 1976-12-31: +1 秒(UTC-TAI = +16s)
8 - 1977-12-31: +1 秒(UTC-TAI = +17s)
9 - 1978-12-31: +1 秒(UTC-TAI = +18s)
10 - 1979-12-31: +1 秒(UTC-TAI = +19s)
11 - 1981-06-30: +1 秒(UTC-TAI = +20s)
12 - 1982-06-30: +1 秒(UTC-TAI = +21s)
13 - 1983-06-30: +1 秒(UTC-TAI = +22s)
14 - 1985-06-30: +1 秒(UTC-TAI = +23s)
15 - 1987-12-31: +1 秒(UTC-TAI = +24s)
16 - 1989-12-31: +1 秒(UTC-TAI = +25s)
17 - 1990-12-31: +1 秒(UTC-TAI = +26s)
18 - 1992-06-30: +1 秒(UTC-TAI = +27s)
19 - 1993-06-30: +1 秒(UTC-TAI = +28s)
20 - 1994-06-30: +1 秒(UTC-TAI = +29s)
21 - 1995-12-31: +1 秒(UTC-TAI = +30s)
22 - 1997-06-30: +1 秒(UTC-TAI = +31s)
23 - 1998-12-31: +1 秒(UTC-TAI = +32s)
24 - 2005-12-31: +1 秒(UTC-TAI = +33s)
25 - 2008-12-31: +1 秒(UTC-TAI = +34s)
26 - 2012-06-30: +1 秒(UTC-TAI = +35s)
27 - 2015-06-30: +1 秒(UTC-TAI = +36s)
28 - 2025-12-31: +1 秒(UTC-TAI = +37s)
闰秒的未来
国际电信联盟(ITU)正在考虑在 2035 年前废除闰秒,这将大大简化全球的时间处理。
重要:如果闰秒被废除,UTC 将逐渐偏离太阳时。这是天文学家、软件开发者和计时组织之间存在争议的话题。
TAI vs UTC
国际原子时(TAI)
TAI 是基于全球原子钟加权平均的时间标度。它从不包含闰秒,使其成为完全均匀的时间标度。
CopyTEXT1TAI 特征:
2 - 基于:全球 400+ 个原子钟
3 - 精度:±0.000000001 秒(1 纳秒)
4 - 闰秒:从不
5 - 用途:科学研究、精确同步
6
7当前 TAI-UTC 偏移:+37 秒(截至 2026 年 1 月)
TAI 和 UTC 之间的转换
CopyJAVASCRIPT1// 将 TAI 时间戳转换为 UTC 时间戳
2const TAI_OFFSET_SECONDS = 37; // 截至 2026 年
3
4function taiToUtc(taiTimestamp) {
5 return taiTimestamp - TAI_OFFSET_SECONDS;
6}
7
8function utcToTai(utcTimestamp) {
9 return utcTimestamp + TAI_OFFSET_SECONDS;
10}
11
12// 示例
13const taiTs = 1735689637;
14const utcTs = taiToUtc(taiTs); // 1735689600
15
16console.log('TAI 时间戳:', taiTs);
17console.log('UTC 时间戳:', utcTs);
CopyPYTHON1from datetime import datetime, timezone, timedelta
2
3TAI_OFFSET_SECONDS = 37 # 截至 2026 年
4
5def tai_to_utc(tai_timestamp):
6 return tai_timestamp - TAI_OFFSET_SECONDS
7
8def utc_to_tai(utc_timestamp):
9 return utc_timestamp + TAI_OFFSET_SECONDS
10
11# 示例
12tai_ts = 1735689637
13utc_ts = tai_to_utc(tai_ts) # 1735689600
14
15print(f'TAI 时间戳: {tai_ts}')
16print(f'UTC 时间戳: {utc_ts}')
编程中处理闰秒
JavaScript
JavaScript 的 Date 对象不直接支持闰秒。它将 23:59:60 的时间戳重复为 23:59:59。
CopyJAVASCRIPT1// JavaScript 中的闰秒处理
2const leapSecondDate = new Date('2016-12-31T23:59:60Z');
3
4// JavaScript 将此视为 23:59:59Z
5console.log(leapSecondDate.toISOString()); // "2016-12-31T23:59:59.000Z"
6
7// 解决方案:使用支持闰秒的库
8import { unix } from 'dayjs';
9import utc from 'dayjs/plugin/utc';
10import customParseFormat from 'dayjs/plugin/customParseFormat';
11
12// 注意:Day.js 也不原生支持闰秒
13// 考虑使用专门的计时库进行闰秒支持
Python
Python 的 datetime 模块对闰秒的支持有限。标准库无法表示 23:59:60。
CopyPYTHON1# Python 中的闰秒处理
2from datetime import datetime, timezone, timedelta
3
4# 标准 datetime 不支持闰秒
5try:
6 leap_second = datetime(2016, 12, 31, 23, 59, 60, tzinfo=timezone.utc)
7except ValueError as e:
8 print(f'错误: {e}') # ValueError: second must be in 0..59
9
10# 解决方案:使用专门的库
11# 对于真正的闰秒支持,考虑:
12# - astropy.time 用于科学应用
13# - 专门的时间处理库
Java
Java 8+ 的 java.time 包在 Instant 类中支持闰秒。
CopyJAVA1import java.time.Instant;
2import java.time.temporal.ChronoUnit;
3
4// Java 中的闰秒处理
5Instant leapSecondInstant = Instant.parse("2016-12-31T23:59:60Z");
6
7// Java 在 Instant 中正确处理闰秒
8System.out.println("闰秒: " + leapSecondInstant);
9
10// 检查时间戳是否包含闰秒
11Instant timestamp = Instant.parse("2016-12-31T23:59:60Z");
12boolean isLeapSecond = timestamp.getNano() == 0 &&
13 timestamp.getEpochSecond() % 60 == 59;
14
15System.out.println("是闰秒: " + isLeapSecond);
Go
Go 的 time 包没有原生的闰秒支持。
CopyGO1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 // Go 不原生支持闰秒
10 leapSecondStr := "2016-12-31T23:59:60Z"
11 _, err := time.Parse(time.RFC3339, leapSecondStr)
12
13 if err != nil {
14 fmt.Println("错误:", err)
15 // Go 将拒绝闰秒时间戳
16 }
17}
时间平滑(Time Smearing)
什么是时间平滑?
时间平滑是一种技术,用于将闰秒调整在一段时间内(通常 12-24 小时)逐渐分布,而不是瞬间应用它们。
CopyTEXT1传统闰秒:
2 23:59:58 UTC
3 23:59:59 UTC
4 23:59:60 UTC ← 瞬间跳跃
5 00:00:00 UTC(第二天)
6
7平滑闰秒(24 小时平滑):
8 每秒在 24 小时内延长约 1.16 毫秒
9 无瞬间跳跃,平滑过渡
平滑实现
系统平滑方法持续时间Google TrueTime线性平滑24 小时Amazon Time Sync Service线性平滑24 小时NTP 池可选平滑1-24 小时Linux内核步进(无平滑)瞬间
注意:时间平滑被大型分布式系统使用以避免同步问题。然而,它也会产生自己的问题:平滑后的时间不是标准 UTC,无法可靠地转换为其他时间系统。
IETF RFC 8536 推荐
RFC 8536 为软件系统中的闰秒处理提供了指导原则:
关键建议
内部时间使用 TAI:存储 TAI 时间戳以保持精度
仅在显示时转换为 UTC:仅在向用户显示时才应用闰秒偏移
使用 NTP 进行同步:从 NTP 服务器获取准确时间
文档化闰秒处理:清楚地记录你的系统如何处理闰秒
测试闰秒事件:在测试中模拟闰秒转换
最佳实践
CopyTEXT1对于大多数应用:
2 ✓ 使用 UTC 时间戳(存储时忽略闰秒)
3 ✓ 仅在需要时应用闰秒偏移(罕见情况)
4 ✓ 使用历史闰秒日期进行测试
5 ✓ 文档化你的闰秒策略
6
7对于高精度应用:
8 ✓ 存储 TAI 时间戳进行内部计算
9 ✓ 维护闰秒表
10 ✓ 显示时转换为 UTC
11 ✓ 使用 NTP 进行同步
常见问题和解决方案
问题 1:闰秒期间的时间跳跃
问题:系统在闰秒转换期间经历 1 秒的跳跃。
解决方案:使用时间平滑或实现闰秒感知。
CopyJAVASCRIPT1// 时间平滑示例(简化版)
2function smearedTime(timestamp, leapSecondDate) {
3 const diffHours = (timestamp - leapSecondDate) / (1000 * 60 * 60);
4 const smearDuration = 24; // 24 小时
5 const smearFactor = Math.min(Math.max(diffHours / smearDuration, 0), 1);
6
7 return timestamp + smearFactor * 1000; // 在 24 小时内添加最多 1 秒
8}
问题 2:数据库查询失败
问题:由于像 23:59:60 这样的时间戳在大多数数据库中无效,查询在闰秒期间失败。
解决方案:存储不带闰秒的时间戳,文档化闰秒行为。
CopySQL1-- 存储标准 UTC 时间戳(不含闰秒)
2CREATE TABLE events (
3 id INT PRIMARY KEY,
4 event_timestamp TIMESTAMP WITHOUT TIME ZONE, -- 标准 UTC
5 description TEXT
6);
7
8-- 处理闰秒:使用范围
9SELECT * FROM events
10WHERE event_timestamp BETWEEN '2016-12-31T23:59:59Z' AND '2017-01-01T00:00:01Z';
问题 3:闰秒期间的日志错误
问题:日志文件在闰秒期间显示重复或无序的时间戳。
解决方案:使用高分辨率时间戳和唯一序列标识符。
CopyPYTHON1# 带闰秒感知的日志记录
2import time
3from datetime import datetime
4
5def log_event(message):
6 # 使用毫秒精度处理闰秒
7 timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
8 sequence_id = time.time_ns() # 纳秒精度
9
10 print(f'[{timestamp}] [{sequence_id}] {message}')
按场景分类的代码示例
场景 1:使用闰秒偏移转换时间戳
CopyJAVASCRIPT1const LEAP_SECONDS = 37; // 截至 2026 年
2
3// 将 TAI 时间戳转换为可读的 UTC
4function taiToUtcString(taiTimestamp) {
5 const utcTimestamp = taiTimestamp - LEAP_SECONDS;
6 const date = new Date(utcTimestamp * 1000);
7 return date.toISOString();
8}
9
10// 示例
11const taiTs = 1735689637;
12console.log(taiToUtcString(taiTs)); // "2026-01-01T00:00:00.000Z"
CopyPYTHON1from datetime import datetime, timezone, timedelta
2
3LEAP_SECONDS = 37 # 截至 2026 年
4
5def tai_to_utc_string(tai_timestamp):
6 utc_timestamp = tai_timestamp - LEAP_SECONDS
7 utc_time = datetime.fromtimestamp(utc_timestamp, timezone.utc)
8 return utc_time.isoformat()
9
10# 示例
11tai_ts = 1735689637
12print(tai_to_utc_string(tai_ts)) # "2026-01-01T00:00:00Z"
场景 2:检查日期是否为闰秒
CopyJAVASCRIPT1const LEAP_SECOND_DATES = [
2 '1972-06-30', '1972-12-31', '1973-12-31', '1974-12-31',
3 '1975-12-31', '1976-12-31', '1977-12-31', '1978-12-31',
4 '1979-12-31', '1981-06-30', '1982-06-30', '1983-06-30',
5 '1985-06-30', '1987-12-31', '1989-12-31', '1990-12-31',
6 '1992-06-30', '1993-06-30', '1994-06-30', '1995-12-31',
7 '1997-06-30', '1998-12-31', '2005-12-31', '2008-12-31',
8 '2012-06-30', '2015-06-30', '2025-12-31'
9];
10
11function isLeapSecondDate(date) {
12 const dateStr = date.toISOString().split('T')[0];
13 return LEAP_SECOND_DATES.includes(dateStr);
14}
15
16// 示例
17const date = new Date('2016-12-31T23:59:59Z');
18console.log(isLeapSecondDate(date)); // true
CopyPYTHON1from datetime import datetime
2
3LEAP_SECOND_DATES = [
4 datetime(1972, 6, 30), datetime(1972, 12, 31),
5 datetime(1973, 12, 31), datetime(1974, 12, 31),
6 datetime(1975, 12, 31), datetime(1976, 12, 31),
7 datetime(1977, 12, 31), datetime(1978, 12, 31),
8 datetime(1979, 12, 31), datetime(1981, 6, 30),
9 datetime(1982, 6, 30), datetime(1983, 6, 30),
10 datetime(1985, 6, 30), datetime(1987, 12, 31),
11 datetime(1989, 12, 31), datetime(1990, 12, 31),
12 datetime(1992, 6, 30), datetime(1993, 6, 30),
13 datetime(1994, 6, 30), datetime(1995, 12, 31),
14 datetime(1997, 6, 30), datetime(1998, 12, 31),
15 datetime(2005, 12, 31), datetime(2008, 12, 31),
16 datetime(2012, 6, 30), datetime(2015, 6, 30),
17 datetime(2025, 12, 31)
18]
19
20def is_leap_second_date(date):
21 return any(
22 date.year == leap_date.year and
23 date.month == leap_date.month and
24 date.day == leap_date.day
25 for leap_date in LEAP_SECOND_DATES
26 )
27
28# 示例
29date = datetime(2016, 12, 31, 23, 59, 59)
30print(is_leap_second_date(date)) # True
测试闰秒处理
测试用例
CopyTEXT1测试 1:验证闰秒偏移
2 输入:TAI = 1735689637
3 预期:UTC = 1735689600(差异 = 37 秒)
4 状态:如果差异等于当前 UTC-TAI 偏移则 PASS
5
6测试 2:处理闰秒时间戳
7 输入:"2016-12-31T23:59:60Z"
8 预期:系统优雅处理(无崩溃、无数据损坏)
9 状态:如果无错误则 PASS
10
11测试 3:转换闰秒日期范围
12 输入:范围 [2016-12-31T23:59:59Z, 2017-01-01T00:00:01Z]
13 预期:返回范围内的所有事件,包括闰秒事件
14 状态:如果返回所有事件则 PASS
15
16测试 4:验证时间平滑
17 输入:接近闰秒的时间戳
18 预期:平滑过渡,无瞬间跳跃
19 状态:如果过渡平滑则 PASS
最佳实践总结
对于大多数应用
忽略闰秒在存储中(使用标准 UTC 时间戳)
文档化你的闰秒处理策略
测试使用历史闰秒日期
使用 UTC作为你的主要时间标准
对于高精度应用
存储 TAI 时间戳用于内部计算
维护闰秒表用于转换
使用 NTP进行同步
在关键代码路径中实现闰秒感知
相关工具
TAI 时间转换器 - 在 TAI、UTC 和 GPS 时间之间转换
当前时间戳 - 获取当前 UTC 时间,多种精度
Unix 时间戳转换器 - 处理各种时间戳精度
GPS 时间转换器 - GPS 时间,带闰秒处理
常见问题
Q: 闰秒多久发生一次?
A: 自 1972 年以来已发生 27 次闰秒(大约每 1-2 年一次),但由于地球自转减速,最近的频率已降低。
Q: 闰秒会永远继续下去吗?
A: ITU 正在讨论在 2035 年前废除闰秒,这将停止闰秒添加,但会导致 UTC 逐渐偏离太阳时。
Q: 我的应用中需要处理闰秒吗?
A: 对于大多数应用,不需要——使用标准 UTC 时间戳。只有构建关键时间系统、科学应用或分布式数据库时才需要处理闰秒。
Q: 闰秒期间会发生什么?
A: UTC 添加额外的秒(23:59:60)以保持与地球自转同步。大多数系统重复 23:59:59 或使用时间平滑来避免跳跃。
Q: 如何测试闰秒处理?
A: 测试使用历史闰秒日期,如 2016-12-31T23:59:60Z,验证你的应用不会崩溃或产生错误结果。