第四 时间
时间是什么?不同的人就会有不同的回答。古人根据日月星辰的运行规律,制定了历法。中国的干支纪年,西方的公元纪年。再到近代的格林尼治标准时间(Greenwich Mean Time,GMT),直到现在基于原子钟的世界标准时间(世界协调时 UTC),以及计算机领域著名的UNIX时间。
在计算机系统中,时间是程序运行、数据记录、网络通信的基础。从日志时间戳到定时任务,从缓存过期到分布式一致性,时间的准确性和一致性至关重要。本章将从计算机时间的本质出发,深入剖析时间戳、日期格式化、时区、时间间隔等核心概念,并通过 Rust 代码实现时间的获取、转换和运算。
4.1 计算机时间概述
4.1.1 时间的历史
人类对时间的计量经历了漫长的演变。古代人们使用圭表、日晷、滴漏等工具计时;近代发展出机械钟表;现代则使用原子钟计时,并可以通过无线电波、卫星、互联网等手段进行授时。
4.1.2 原子钟与 UTC
1967 年,国际计量大会将秒的定义改为:铯-133 原子基态的两个超精细能级之间跃迁所对应的辐射的 9,192,631,770 个周期的持续时间。这一定义基于原子钟,其精度可达每数百万年误差不超过 1 秒。
UTC(Coordinated Universal Time,协调世界时)是目前全球最通用的时间标准。它结合了:
- TAI(International Atomic Time,国际原子时):基于全球约 400 台原子钟的加权平均,是连续均匀的时间尺度。
- UT1(Universal Time 1):基于地球自转,与天文观测相关。
UTC 通过在 TAI 基础上插入闰秒来保持与 UT1 的偏差不超过 0.9 秒。
4.1.3 UNIX 时间戳原理
UNIX 时间戳(Unix Timestamp)是计算机系统中最常用的时间表示方式,定义为自 1970-01-01 00:00:00 UTC 起经过的秒数(或毫秒数、微秒数、纳秒数)。
选择 1970 年作为纪元(Epoch)的原因是 UNIX 操作系统诞生于 1969 年,1970-01-01 是 UNIX 的“生日“。
时间戳的核心优势在于:
- 无歧义:不受时区、夏令时、日期格式影响
- 便于计算:两个时间戳的差值即为时间间隔
- 存储高效:一个整数即可表示任意时刻
4.1.4 闰秒问题
由于地球自转速度不均匀(潮汐摩擦等因素导致逐渐变慢),UTC 需要通过插入闰秒来与地球自转保持一致。闰秒的插入规则是:
- 当 UTC 与 UT1 的偏差接近 0.9 秒时,在 6 月 30 日或 12 月 31 日的最后一分钟插入一个额外的第 61 秒
- 闰秒可以正(增加一秒)也可以负(减少一秒),但历史上只出现过正闰秒
闰秒给计算机系统带来了挑战:
- 一分钟可能有 61 秒或 59 秒
- 时间戳在闰秒期间可能出现倒退或重复
- 部分系统采用“闰秒抹平“(Leap Second Smearing)策略,将闰秒分散到多个小时中
4.2 时间戳
4.2.1 时间戳的精度
根据精度不同,时间戳可分为多个级别:
| 精度级别 | 单位 | 示例(2024-01-01 00:00:00 UTC) |
|---|---|---|
| 秒级 | 秒 | 1704067200 |
| 毫秒级 | 毫秒 | 1704067200000 |
| 微秒级 | 微秒 | 1704067200000000 |
| 纳秒级 | 纳秒 | 1704067200000000000 |
秒级时间戳在 32 位有符号整数中的最大值为 $2^{31} - 1 = 2147483647$,对应 2038-01-19 03:14:07 UTC,这就是著名的 Y2K38 问题(2038 年问题)。使用 64 位整数可以表示到约 2920 亿年后,彻底解决这个问题。
4.2.2 Rust 获取时间戳
use chrono::Utc;
fn main() {
// 秒级时间戳
println!("Utc timestamp: {}", Utc::now().timestamp());
// 毫秒级时间戳
println!("Utc timestamp_millis: {}", Utc::now().timestamp_millis());
// 微秒级时间戳
println!("Utc timestamp_micros: {}", Utc::now().timestamp_micros());
// 纳秒级时间戳
println!("Utc timestamp_nanos: {}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
}
4.3 日期格式化
4.3.1 ISO 8601 标准
ISO 8601 是国际标准化组织制定的日期和时间表示标准,旨在消除不同国家和文化之间的日期表示歧义。
基本格式:
$$\text{YYYY-MM-DDTHH:MM:SS±HH:MM}$$
其中:
YYYY:四位年份MM:两位月份(01-12)DD:两位日期(01-31)T:日期和时间的分隔符HH:MM:SS:时:分:秒±HH:MM:与 UTC 的时区偏移
示例:
2024-01-15T09:30:00+08:00(北京时间)2024-01-15T01:30:00Z(UTC 时间,Z表示零时区)
4.3.2 RFC 2822 与 RFC 3339
RFC 2822 是电子邮件中使用的日期格式:
Fri, 28 Nov 2014 12:00:09 +0000
RFC 3339 是互联网协议中广泛使用的日期格式,基于 ISO 8601 但做了一些限制:
2014-11-28T12:00:09+00:00
两者的主要区别:
| 特性 | RFC 2822 | RFC 3339 |
|---|---|---|
| 来源 | 电子邮件标准 | 互联网标准 |
| 星期 | 必须包含 | 不包含 |
| 时区格式 | +0000 或 GMT | +00:00 或 Z |
| 使用场景 | 邮件头、HTTP Date | JSON、API、配置文件 |
4.3.3 Rust chrono 格式化字符串
chrono crate 提供了丰富的格式化选项:
| 占位符 | 含义 | 示例 |
|---|---|---|
%Y | 四位年份 | 2024 |
%m | 两位月份 | 01 |
%d | 两位日期 | 15 |
%H | 24小时制小时 | 09 |
%M | 分钟 | 30 |
%S | 秒 | 00 |
%f | 微秒(6位) | 000123 |
%z | 时区偏移 | +0800 |
%Z | 时区名称 | CST |
4.3.4 日期与字符串相互转换
use chrono::{Utc, NaiveDateTime, TimeZone, NaiveDate};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. DateTime -> &str (格式化输出日期)
let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
assert_eq!(
dt.format("%Y-%m-%d %H:%M:%S").to_string(),
"2014-11-28 12:00:09"
);
assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC");
assert_eq!(dt.to_rfc2822(), "Fri, 28 Nov 2014 12:00:09 +0000");
assert_eq!(dt.to_rfc3339(), "2014-11-28T12:00:09+00:00");
assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z");
// 2. &str -> DateTime
let no_timezone = NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S");
println!("{:?}", no_timezone);
assert_eq!(
NaiveDateTime::parse_from_str("2014-5-17T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
Ok(NaiveDate::from_ymd_opt(2014, 5, 17).unwrap().and_hms_opt(12, 34, 56).unwrap())
);
Ok(())
}
4.4 时区
4.4.1 时区概念
地球自转导致不同经度地区看到太阳的位置不同,因此将地球划分为 24 个时区,每个时区跨 15 度经度。时区用 UTC 偏移量表示:
$$\text{本地时间} = \text{UTC} + \text{时区偏移}$$
常见时区:
UTC+5:30 印度新德里时间 (东5.5区时间)
UTC+8 北京时间 (东八区时间)
UTC+9 东京时间 (东九区时间)
UTC-5 东部时间(EST) (西五区时间)
UTC-8 太平洋标准时区(PST)(西八区时间)
4.4.2 夏令时
夏令时(Daylight Saving Time,DST)是一种为节约能源而人为调整时间的制度。在夏季将时钟拨快一小时,以充分利用日光。
- 并非所有国家/地区都使用夏令时
- 中国自 1991 年起不再实行夏令时
- 美国、欧洲大部分国家仍在使用
- 夏令时的开始和结束日期每年可能不同,给程序处理带来复杂性
4.4.3 Rust 时区处理
use chrono::{Local, DateTime, Utc, FixedOffset, NaiveDate, TimeZone};
use chrono::{Datelike, Timelike};
fn main() {
// ❌ 旧方法(已弃用)
// let utc_time = DateTime::<Utc>::from_utc(local_time.naive_utc(), Utc);
// ✅ 新方法:使用 Utc.from_utc_datetime()
let local_time = Local::now();
let utc_time = Utc.from_utc_datetime(&local_time.naive_utc());
// 定义时区(注意:FixedOffset::east() 已弃用,使用 east_opt() 或 from_hours())
let new_delhi_timezone = FixedOffset::east_opt(5 * 3600 + 1800).unwrap();
let china_timezone = FixedOffset::east_opt(8 * 3600).unwrap();
let japan_timezone = FixedOffset::east_opt(9 * 3600).unwrap();
let rio_timezone = FixedOffset::west_opt(2 * 3600).unwrap();
let est_timezone = FixedOffset::west_opt(5 * 3600).unwrap();
let pst_timezone = FixedOffset::west_opt(8 * 3600).unwrap();
println!("Local time now is {}", local_time);
println!("UTC time now is {}", utc_time);
// UTC --> 各时区时间
println!("Time in Beijing now is {}", utc_time.with_timezone(&china_timezone));
println!("Time in Tokyo now is {}", utc_time.with_timezone(&japan_timezone));
println!("Time in Rio de Janeiro now is {}", utc_time.with_timezone(&rio_timezone));
println!("Time in New Delhi now is {}", utc_time.with_timezone(&new_delhi_timezone));
println!("Time in EST now is {}", utc_time.with_timezone(&est_timezone));
println!("Time in PST now is {}", utc_time.with_timezone(&pst_timezone));
// 格式化输出
let local: DateTime<Local> = Local::now();
println!("{}", local.format("%Y-%m-%d %H:%M:%S"));
println!("当前本地时间: {}", local);
// NaiveDateTime 构建(需要 NaiveDate 类型)
let local_time_now = Local::now();
let dt = NaiveDate::from_ymd_opt(
local_time_now.year(),
local_time_now.month(),
local_time_now.day()
)
.unwrap()
.and_hms_opt(
local_time_now.hour(),
local_time_now.minute(),
local_time_now.second()
)
.unwrap();
println!("NaiveDateTime: {}", dt);
}
4.5 时间间隔 Duration
4.5.1 Duration 概念
Duration 表示两个时间点之间的间隔。在 Rust 的 chrono 库中,Duration 可以表示正负的时间长度,支持多种时间单位:
| 方法 | 说明 |
|---|---|
Duration::days(n) | n 天 |
Duration::hours(n) | n 小时 |
Duration::minutes(n) | n 分钟 |
Duration::seconds(n) | n 秒 |
Duration::milliseconds(n) | n 毫秒 |
Duration::microseconds(n) | n 微秒 |
Duration::nanoseconds(n) | n 纳秒 |
Duration::weeks(n) | n 周 |
4.5.2 时间运算
use chrono::{Duration, DateTime, Utc, TimeZone, Local};
fn day_earlier(date_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
date_time.checked_sub_signed(Duration::days(1))
}
fn main() {
let now = Utc::now();
let almost_three_weeks_from_now = now
.checked_add_signed(Duration::weeks(2))
.and_then(|in_2weeks| in_2weeks.checked_add_signed(Duration::weeks(1)))
.and_then(day_earlier);
match almost_three_weeks_from_now {
Some(x) => println!("三周前一天: {}", x),
None => eprintln!("Almost three weeks from now overflows!"),
}
// ✅ 新方法:使用 with_ymd_and_hms()
let today = Utc::now();
let now = Local::now();
let founding_date = Utc.with_ymd_and_hms(1949, 10, 1, 0, 0, 0).unwrap();
let years_passed = now.signed_duration_since(founding_date).num_days() / 365;
println!("今天{},建国{}年", now, years_passed);
// 时间运算示例
let tomorrow = now.checked_add_signed(Duration::days(1)).unwrap();
let last_week = now.checked_sub_signed(Duration::weeks(1)).unwrap();
let ten_minutes_later = now.checked_add_signed(Duration::minutes(10)).unwrap();
println!("明天: {}", tomorrow);
println!("上周: {}", last_week);
println!("十分钟后: {}", ten_minutes_later);
// 计算两个时间点的间隔
let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(2024, 6, 20, 12, 0, 0).unwrap();
let diff = end.signed_duration_since(start);
println!("从年初到今天的间隔: {} 天", diff.num_days());
println!("从年初到今天的间隔: {} 小时", diff.num_hours());
println!("从年初到今天的间隔: {} 秒", diff.num_seconds());
}
4.6 时间标准
4.6.1 NTP 协议
NTP(Network Time Protocol,网络时间协议)是用于同步计算机系统时钟的协议。它通过分层的时间服务器架构(Stratum 0-15)将时间从原子钟传播到互联网上的各种设备。
- Stratum 0:原子钟、GPS 等高精度时间源
- Stratum 1:直接与 Stratum 0 连接的时间服务器
- Stratum 2:从 Stratum 1 获取时间的服务器
- …
NTP 使用 UDP 端口 123,精度可达毫秒级。SNTP(Simple NTP)是 NTP 的简化版本,适用于对精度要求不高的场景。
4.6.2 日期与时间标准
4.7 总结
时间相关概念对比
| 概念 | 说明 | 示例 |
|---|---|---|
| 时间戳 | 自 1970-01-01 00:00:00 UTC 起的秒数 | 1704067200 |
| UTC | 协调世界时,基于原子钟的全球标准时间 | 2024-01-01T00:00:00Z |
| GMT | 格林尼治标准时间,基于地球自转(已逐渐被 UTC 取代) | Sun, 01 Jan 2024 00:00:00 GMT |
| 本地时间 | UTC + 时区偏移 | 2024-01-01T08:00:00+08:00 |
| ISO 8601 | 国际日期时间表示标准 | 2024-01-01T00:00:00+00:00 |
| RFC 3339 | 互联网日期时间格式(基于 ISO 8601) | 2024-01-01T00:00:00Z |
| RFC 2822 | 电子邮件日期时间格式 | Mon, 01 Jan 2024 00:00:00 +0000 |
| Duration | 两个时间点之间的间隔 | Duration::days(7) |
| 闰秒 | 为保持 UTC 与地球自转同步而插入的额外秒 | 2016-12-31 23:59:60 |
| 夏令时 | 夏季将时钟拨快一小时的制度 | UTC-5 → UTC-4 |
关键公式汇总
| 公式 | 说明 |
|---|---|
| $\text{本地时间} = \text{UTC} + \text{时区偏移}$ | 时区转换 |
| $\text{时间间隔} = t_2 - t_1$ | Duration 计算 |
| $\text{Y2K38 临界点} = 2^{31} - 1 = 2147483647$ | 32 位时间戳最大值 |
4.8 练习题
-
基础题:使用
chrono库获取当前时间的秒级、毫秒级和纳秒级时间戳,并输出到控制台。 -
格式转换:编写一个 Rust 程序,将当前 UTC 时间分别格式化为 ISO 8601、RFC 2822 和 RFC 3339 格式,并输出对比。
-
时区转换:给定一个 UTC 时间字符串
"2024-06-20T12:00:00Z",将其转换为北京时间(UTC+8)、东京时间(UTC+9)和纽约时间(UTC-5)的本地时间。 -
时间运算:计算从 1949 年 10 月 1 日 00:00:00 UTC 到当前时间经过了多少天、多少小时、多少分钟。
-
闰年判断:编写一个函数判断给定年份是否为闰年。闰年规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。
-
倒计时程序:编写一个 Rust 程序,计算距离 2038-01-19 03:14:07 UTC(Y2K38 问题发生时刻)还剩多少天、小时、分钟和秒。
-
时间解析:编写一个函数,解析各种格式的日期字符串(如
"2024-01-15"、"15/01/2024"、"Jan 15, 2024"),统一返回DateTime<Utc>类型。 -
思考题:为什么计算机系统使用时间戳而不是人类可读的日期字符串来存储时间?时间戳有哪些优势和局限性?