引言
闰秒是对协调世界时(UTC)偶尔添加的一秒调整,以保持其与地球自转同步。这是软件开发中时间处理最复杂的方面之一。
快速总结:UTC 添加闰秒以保持在 UT1(太阳时)的 0.9 秒以内。截至 2026 年 1 月,自 1972 年以来已添加 37 个闰秒,使 UTC 比国际原子时(TAI)慢 37 秒。
什么是闰秒?
定义
一个闰秒是对 UTC 应用的一秒调整,用于考虑:
- 地球自转减速:地球自转逐渐变慢
- 不规律的自转速率:地球自转速度不可预测地变化
- UT1-UTC 偏移:保持 UTC 在太阳时(UT1)的 0.9 秒内
TEXT1闰秒公式: 2 如果 UT1 - UTC > 0.9 秒 → 添加正闰秒 3 如果 UT1 - UTC < -0.9 秒 → 添加负闰秒 4 5结果:UTC 保持与地球自转同步
闰秒如何工作
当添加闰秒时,UTC 日的最后一分钟有 61 秒而不是 60 秒:
TEXT1普通日(无闰秒): 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 秒 |
最近的闰秒
TEXT1所有闰秒(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 是基于全球原子钟加权平均的时间标度。它从不包含闰秒,使其成为完全均匀的时间标度。
TEXT1TAI 特征: 2 - 基于:全球 400+ 个原子钟 3 - 精度:±0.000000001 秒(1 纳秒) 4 - 闰秒:从不 5 - 用途:科学研究、精确同步 6 7当前 TAI-UTC 偏移:+37 秒(截至 2026 年 1 月)
TAI 和 UTC 之间的转换
JAVASCRIPT1// 将 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);
PYTHON1from 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。
JAVASCRIPT1// 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。
PYTHON1# 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 类中支持闰秒。
JAVA1import 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 包没有原生的闰秒支持。
GO1package 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 小时)逐渐分布,而不是瞬间应用它们。
TEXT1传统闰秒: 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 服务器获取准确时间
- 文档化闰秒处理:清楚地记录你的系统如何处理闰秒
- 测试闰秒事件:在测试中模拟闰秒转换
最佳实践
TEXT1对于大多数应用: 2 ✓ 使用 UTC 时间戳(存储时忽略闰秒) 3 ✓ 仅在需要时应用闰秒偏移(罕见情况) 4 ✓ 使用历史闰秒日期进行测试 5 ✓ 文档化你的闰秒策略 6 7对于高精度应用: 8 ✓ 存储 TAI 时间戳进行内部计算 9 ✓ 维护闰秒表 10 ✓ 显示时转换为 UTC 11 ✓ 使用 NTP 进行同步
常见问题和解决方案
问题 1:闰秒期间的时间跳跃
问题:系统在闰秒转换期间经历 1 秒的跳跃。
解决方案:使用时间平滑或实现闰秒感知。
JAVASCRIPT1// 时间平滑示例(简化版) 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 这样的时间戳在大多数数据库中无效,查询在闰秒期间失败。
解决方案:存储不带闰秒的时间戳,文档化闰秒行为。
SQL1-- 存储标准 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:闰秒期间的日志错误
问题:日志文件在闰秒期间显示重复或无序的时间戳。
解决方案:使用高分辨率时间戳和唯一序列标识符。
PYTHON1# 带闰秒感知的日志记录 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:使用闰秒偏移转换时间戳
JAVASCRIPT1const 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"
PYTHON1from 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:检查日期是否为闰秒
JAVASCRIPT1const 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
PYTHON1from 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
测试闰秒处理
测试用例
TEXT1测试 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,验证你的应用不会崩溃或产生错误结果。