ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

时间戳与时间格式化实战指南:从基础概念到日志排查

时间戳与时间格式化实战指南:从基础概念到日志排查 搞开发这些年我几乎每天都要跟时间打交道。日志里满屏的yyyy-MM-dd HH:mm:ss接口返回的一串13位或10位数字数据库里存的datetimeExcel 表格里那个看起来像乱码的 43000 多……这些都是时间只是呈现方式不一样。我见过太多同事在时间转换上栽跟头格式化字符串写错大小写导致时间差8小时、时间戳位数没搞清直接整除1000、前后端时区没对齐导致数据对不上。这篇文章我就把这套东西彻底捋一遍从基本概念到各语言实操再到日志排查中的实战一次性讲透保证你看完能直接上手少踩几个坑。yyyy-MM-dd HH:mm:ss和“时间戳”看似是两个东西其实是一体两面。搞懂它们之间的换算逻辑前端后端、脚本、Excel、日志分析都会顺手很多。不管你写 Java、JavaScript、Python还是日常要处理日志、Excel 数据这篇文章都适用。1. 时间格式与时间戳先搞清楚两个基础概念1.1 什么是时间戳时间戳Timestamp本质上是一个数字代表从某个固定起点到当前时刻经过的时间量。最常见的起点是 Unix Epoch也就是 1970年1月1日 00:00:00 UTC。我经常跟新人打比方时间戳就像你从出生到现在活了多少秒它是个单调递增的计数不依赖任何时区全球统一。而人类习惯看的年月日时分秒更像是把这个计数按照你所在的时区翻译成一个“本地显示字符串”。具体到位数常见有两种秒级时间戳10位数字比如1698825600。很多后端系统、数据库存储都用这个。毫秒级时间戳13位数字比如1698825600000。JavaScript 的Date.now()、Java 的System.currentTimeMillis()返回的都是这种。很多新手搞混 10 位和 13 位导致转换出来的时间差了几十年这其实是整篇文章里最不值得踩的坑但偏偏踩的人最多。记住一句话10 位是秒13 位是毫秒。看到 13 位先除以 1000 再转看到 10 位直接转就行。1.2 什么是时间格式时间格式是人类把时间戳翻译成可读字符串时使用的“模板”。yyyy-MM-dd HH:mm:ss是最常见的一种比如2023-11-01 14:30:50。这里每个字母都有固定含义字母含义示例yyyy四位年份2023MM两位月份01~1211dd两位日期01~3101HH24小时制小时00~2314mm分钟00~5930ss秒钟00~5950这里最容易翻车的是HH和hh的区别。大写HH是 24 小时制小写hh是 12 小时制。如果格式化用hh下午 2 点会显示成02虽然你心里想的是 14 点。再有就是MM和mm月份和分钟都带 M大小写一错整个时间全部错乱。这种问题在代码 review 时肉眼特别难看出来因为字符串模板一眼扫过去全是字母 M 和 m不抠细节根本发现不了。1.3 为什么需要相互转换实际开发中时间戳和时间格式的使用场景完全不同。数据库里为了比较大小、做索引、避免时区混乱经常直接存时间戳或者datetime类型但日志输出、用户展示、接口返回给前端的时候又必须用可读的字符串。前后端数据交互时如果后端返回一个可读字符串前端想计算时间差或者做排序还得先转回时间戳。数据从存储到展示往往要经历“时间戳 - 格式化字符串”的转换数据从用户输入到入库又要“字符串 - 时间戳”的解析。这个转换过程如果各端处理方式不一致就会出现时间错乱。所以我一般建议团队定一个统一约定内部存储用时间戳对外展示用格式化字符串跨端传输尽量用带时区信息的格式比如ISO 8601。2. 主流语言与工具中的时间戳转换实操2.1 Java 中的时间格式化与时间戳Java 8 之前大家用SimpleDateFormat它线程不安全在多线程环境下记得new一个局部变量否则会出现数据错乱。Java 8 之后推荐用java.time包线程安全API 也更顺手。从时间戳转格式long timestamp System.currentTimeMillis(); // timestamp 是 13 位毫秒值 Instant instant Instant.ofEpochMilli(timestamp); ZonedDateTime dateTime instant.atZone(ZoneId.of(Asia/Shanghai)); String formatted dateTime.format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); System.out.println(formatted);如果是 10 位秒级时间戳把Instant.ofEpochMilli换成Instant.ofEpochSecond即可。从格式字符串解析成时间戳String text 2023-11-01 14:30:50; DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss) .withZone(ZoneId.of(Asia/Shanghai)); long epochMilli Instant.from(formatter.parse(text)).toEpochMilli(); System.out.println(epochMilli);这里有一个特别重要的点解析时必须指定时区。如果不指定Instant.from会直接报错提示找不到时区。很多新人在这卡住就是因为只给了格式模板没给时区。再看老项目里常见的SimpleDateFormat写法SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); sdf.setTimeZone(TimeZone.getTimeZone(Asia/Shanghai)); String formatted sdf.format(new Date(1698825600000L));有个细节值得注意SimpleDateFormat默认使用 JVM 所在系统时区如果你的服务器时区是 UTC本地测试是东八区同一个时间戳格式化出来的字符串就会不一样。这也是很多环境问题排查到头大时的原因——代码没问题时区不一致。2.2 JavaScript 前端时间戳处理前端用 JS 处理时间戳最常用的场景是从服务端拿到时间戳然后渲染到页面上。JS 的Date对象内部就是毫秒时间戳所以直接new Date(timestamp)就行。const timestamp 1698825600000; const date new Date(timestamp); // 本地时间字符串 const formatted date.getFullYear() - String(date.getMonth() 1).padStart(2, 0) - String(date.getDate()).padStart(2, 0) String(date.getHours()).padStart(2, 0) : String(date.getMinutes()).padStart(2, 0) : String(date.getSeconds()).padStart(2, 0); console.log(formatted);看着很啰嗦所以现在大家基本都用第三方库。我常用的是dayjs或date-fns。老项目里还在用moment.js虽然库体积大一点但功能完整稳定。用dayjs的话import dayjs from dayjs; const formatted dayjs(1698825600000).format(YYYY-MM-DD HH:mm:ss); console.log(formatted);注意这里用YYYY不是yyyy。JS 库普遍遵循YYYY-MM-DD这种风格跟 Java 的yyyy略有区别。跨语言协作时模板要跟具体语言/库的文档对齐别想当然。还有一个前端经常遇到的问题后端返回 10 位秒级时间戳JS 直接new Date(1698825600)拿到的是 1970 年因为 JS 只认毫秒。解决方案很简单乘 1000const timestampSec 1698825600; const date new Date(timestampSec * 1000);2.3 Python 高效处理方法Python 里最常用的就是datetime模块和time模块。从时间戳转格式import time from datetime import datetime, timezone, timedelta # 秒级时间戳 timestamp 1698825600 dt datetime.fromtimestamp(timestamp, tztimezone(timedelta(hours8))) formatted dt.strftime(%Y-%m-%d %H:%M:%S) print(formatted)注意 Python 的fromtimestamp如果不传tz参数会使用系统本地时区。如果脚本部署在 UTC 时区的服务器上输出会差 8 小时。我一般显式指定时区省得被环境坑。从字符串解析text 2023-11-01 14:30:50 dt datetime.strptime(text, %Y-%m-%d %H:%M:%S) # 转为秒级时间戳需要先指定 UTC 时区再转 timestamp dt.replace(tzinfotimezone(timedelta(hours8))).timestamp() print(timestamp)这里有个 Python 特有的坑strptime返回的datetime没有时区信息直接.timestamp()会按本地时区去算在服务器时区不是东八区时结果就不对。所以先replace(tzinfo...)再转。另外处理日志文件里的时间戳时我经常直接写一个正则替换脚本import re log_line 2023-11-01 14:30:50 [INFO] 用户登录成功 match re.search(r(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}), log_line) if match: time_str match.group(1) print(日志中的时间:, time_str)这种方法在做日志清洗和时间字段抽取时非常实用。2.4 Excel 时间戳转时间Excel 里的时间存储方式比较特殊。它内部把日期存成序列号把时间存成小数。比如2023-11-01 14:30:50实际存的是45231.60474537左右。这个数是从 1900-01-00 开始按天计的所以直接看会一头雾水。如果手动转换我常用两个思路把时间戳秒或毫秒除以一天的秒数86400再加上 1970-01-01 对应的 Excel 序列号 25569得到 Excel 可识别的日期数字。用公式转换。Excel 公式处理 10 位秒级时间戳(A1/86400)DATE(1970,1,1)然后把单元格格式设为yyyy-mm-dd hh:mm:ss即可。如果是 13 位毫秒级先除以 1000((A1/1000)/86400)DATE(1970,1,1)如果是在 Python 里处理 Excel我会用pandas的pd.to_datetime它可以直接把 Excel 序列号转成标准时间import pandas as pd # 假设有一列 Excel 序列号 df[时间] pd.to_datetime(df[序列号], unitD, origin1899-12-30)这里origin填1899-12-30是 Excel 的 0 起点这是个冷知识。Excel 设计时把 1900-01-01 当成序列号 1实际上往前推了半天所以origin1899-12-30才对。2.5 数据库MySQL时间处理MySQL 里最常用的是FROM_UNIXTIME和UNIX_TIMESTAMP这一对函数。从时间戳转格式SELECT FROM_UNIXTIME(1698825600, %Y-%m-%d %H:%i:%s);从格式转时间戳SELECT UNIX_TIMESTAMP(2023-11-01 14:30:50);有几个细节容易踩坑UNIX_TIMESTAMP的返回值跟会话时区有关。MySQL 连接串里如果没指定特定期望服务器时区是 UTC 时转换结果会偏差。建议在 JDBC 连接串里加serverTimezoneAsia/Shanghai并保证数据库全局时区设置一致。如果表里存的是 13 位毫秒时间戳很多大厂表结构确实这么干FROM_UNIXTIME不能直接用需要除以 1000SELECT FROM_UNIXTIME(create_time / 1000, %Y-%m-%d %H:%i:%s) FROM orders;比较查询时如果列是datetime参数是时间戳最好统一先转换再比较让索引能用上。直接在列上套函数会放弃索引。比如-- 不好索引失效 SELECT * FROM orders WHERE UNIX_TIMESTAMP(create_time) 1698825600; -- 好 SELECT * FROM orders WHERE create_time FROM_UNIXTIME(1698825600);3. 时间格式化中的常见坑与避坑技巧3.1 大小写字母含义别搞混这块太容易出问题了单独拿出来说。很多人背了模板却不理解每个字母为什么这么写。我来个通俗版本的“字典”yyyy年。注意这是“日历年份”ISO 周年份是YYYY两者在跨年那几天可能不一样。用YYYY格式化 2023年12月31日可能得到2024因为那周属于 2024 年的 ISO 周。Java 的SimpleDateFormat和 Python 的%Y都有类似差异。所以统一用yyyy不要用YYYY。MM月。mm是分钟。dd日。DD在有些库里是一年中的第几天比如 2023-11-01 的DD是 305不是 01。HH24 小时制小时。hh是 12 小时制。mm分钟。ss秒。SS在 Java 里是毫秒在 Python 里面对应%f微秒。一句话记法大写 M 是 month小写 m 是 minute大写 H 是 24 小时制小写 h 是 12 小时制。日期小写ddDD另一套意思统一别用。3.2 时区问题时区问题是最隐蔽的坑。同一个时间戳在北京是2023-11-01 14:30:50在伦敦是2023-11-01 06:30:50。如果服务器时区是 UTC格式化出来的字符串就会与预期不同。我的建议是后端统一按 UTC 存储时间戳或datetime。对外接口返回 ISO 8601 格式如2023-11-01T14:30:5008:00这样前端拿到后可以转成任意时区。如果非要返回yyyy-MM-dd HH:mm:ss这种字符串必须明确时区最好在接口文档里写清楚是 Asia/Shanghai 还是 UTC。配置中心里放一个全局时区变量代码里读取这个变量来格式化不要硬编码在业务代码里。我自己处理过一个真实案例测试环境正常生产环境所有接口返回的时间都差了 8 小时。最后定位到生产服务器的/etc/localtime是 UTC而代码里用new Date()配合SimpleDateFormat默认时区格式化自然差 8 小时。后来我统一改成DateTimeFormatter并显式传入ZoneId问题彻底解决。3.3 闰秒、毫秒与纳秒多数业务场景用不到闰秒但你要知道它的存在。闰秒是指由于地球自转速度不均匀UTC 会在某些年份额外插入一秒。操作系统和编程语言一般会自动处理但对极个别对时间敏感的系统可能会造成 1 秒的误差。业界现在也在讨论取消闰秒但短时间内它还在。毫秒和纳秒的问题就常见多了。Java 17 的Instant支持纳秒精度但转成LocalDateTime格式化时默认会保留纳秒。如果你直接.toString()可能看到2023-11-01T14:30:50.123456789。如果不需要这么高精度格式化模板里就别写SS或SSSSSS否则输出一串数字很丑解析时也容易出错。Python 同理datetime.now()默认带有微秒%f。打印日志时要注意去掉微秒否则日志里全是.123456看着也不清爽。3.4 fastjson 时间转时间戳的坑热词里提到了fastjson 时间 转 时间戳这里也单独聊聊。Fastjson 在把 Java 对象序列化成 JSON 时默认会把Date转成时间戳毫秒。如果你在接口里返回了一个Date字段前端拿到的是类似1698825600000的数字不是字符串。如果用 Fastjson 且希望输出格式化字符串可以在字段上加注解public class UserVO { JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone Asia/Shanghai) private Date createTime; }这里必须写timezone否则默认可能按 UTC 格式化。Fastjson 还有一个全局配置SerializeConfig config new SerializeConfig(); config.put(Date.class, new SimpleDateFormatSerializer(yyyy-MM-dd HH:mm:ss));但如果你用的是 Fastjson 2要留意包名和 API 有变化。另外很多老项目踩过 Fastjson 序列化循环引用和 autoType 的坑这里不展开。总之只要涉及Date序列化一定先确认前端期望的是字符串还是时间戳再选择配不配注解。4. 日志时间戳与排查实践4.1 日志时间戳的作用日志里的时间戳是排查问题的第一线索。比如热词里提到“错误应用程序名称: explorer.exe, 版本: ..., 时间戳: 0x57c44efe”这类 Windows 错误日志里会给一个十六进制时间戳。0x57c44efe是 PE 文件头里的时间戳表示程序编译时间不是崩溃时间。排查看起来容易误导需要额外注意。在实际后端日志里我们经常从yyyy-MM-dd HH:mm:ss入手确定问题发生的起始时间、持续时间、频率。比如统计一次接口慢调用我会先查日志里所有ERROR的时间点再看同一秒附近有没有异常堆栈。如果没有统一时间格式日志里出现多种格式或者时区不一致排查效率会非常差。所以团队日志格式需要统一。我一般建议 log4j2 或 logback 的 pattern 这样配置pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/patternSSS表示毫秒用来追踪一次请求的先后顺序非常有用。强烈建议日志模板里带上毫秒。4.2 终端日志工具中的时间戳功能热词里出现crt日志时间戳和mobaxterm时间戳。这类终端工具在连接远程服务器、查看滚动日志时一般默认不显示本地时间。如果日志系统本身没有打时间戳你在终端看到的就是一屏没有时间的输出无法判断每行发生在什么时候。SecureCRT 和 MobaXterm 都支持“终端输出加时间戳”的功能但原理是客户端本地在收到每一行输出时打上客户端本地时间。这个时间不是服务器上的时间两者可能有偏差只能用来粗估先后顺序不能当成精确时刻。MobaXterm 的开启方式在Settings - Terminal里找到Timestamp相关选项把 “Display timestamps for each line” 打开。SecureCRT 则在Session Options - Terminal - Emulation里配置Logging或Timestamps。要真正拿到服务器日志的精确时间最靠谱的办法还是让应用/中间件自己在日志里输出时间戳。比如 nginx 默认$time_local、Java 应用通过 logback 配置、Linuxsyslog自带时间这些才是权威来源。终端加时间戳只能作为辅助手段。4.3 实战定位错误日志的时间戳有一次线上告警说订单服务有报错我拿到错误日志文件后先在本地用命令把时间字段提取出来grep ERROR app.log | tail -n 100然后统计出错的分钟分布grep ERROR app.log | awk {print $1, $2} | cut -d: -f1-2 | sort | uniq -c这个命令按“年-月-日 时:分”分组直接看出高峰期。如果我处理的是 Excel 导出的错误列表时间列是时间戳我用 Python 脚本批量转换并统计import pandas as pd df pd.read_excel(errors.xlsx) df[时间] pd.to_datetime(df[时间戳], units) # 按小时分组统计 df[小时] df[时间].dt.hour print(df.groupby(小时).size())有了时间分布就好跟应用发布记录、数据库慢查询日志做交叉比对快速圈定范围。这种实践是日志排查的基本功核心就是先统一时间格式再聚合分析。4.4 常见错误与排查速查表我把日常遇到最多的时间相关报错和解决办法整理成一张表方便你直接对号入座症状可能原因解决方案时间戳转出来是 1970 年10 位秒级当毫秒处理乘 1000或Instant.ofEpochSecond时间差了 8 小时时区设置不一致统一使用 UTC 存储显示时指定时区字符串解析报错Unparseable date模板与字符串不匹配比如用了yyyy但字符串是YY检查模板大小写和分隔符Invalid format: 2023-11-01 14:30:50未指定时区解析时withZone(ZoneId.of(Asia/Shanghai))Excel 时间戳转出来不对序列号起点算错用origin1899-12-30日志时间乱了多实例时区不一致启动命令加-Duser.timezoneAsia/Shanghai数据库日期差 8 小时JDBC 连接没指 serverTimezone加serverTimezoneAsia/Shanghai5. 一套可以直接抄的时间处理方案5.1 前后端统一时间格式约定我带的项目一般都会在开发规范里约定后端内部存储datetime使用 UTC时间戳统一为 13 位毫秒。接口对外传输时间字段一律使用yyyy-MM-dd HH:mm:ss字符串且明确标注时区为Asia/Shanghai。如果接口需要传递带毫秒的时间扩展为yyyy-MM-dd HH:mm:ss.SSS不用 ISO 8601减少前端解析成本。前端拿到时间字符串不直接操作字符串统一转成dayjs对象再处理。这个约定对中小团队来说够用也方便排查。大家不用再纠结接口返回的时间到底要不要加时区后缀因为规范里已经写死了。5.2 常用代码片段汇总这里贴一段我经常用的 Java 时间工具类核心代码支持 10 位/13 位时间戳转字符串public static String formatTimestamp(Long timestamp, String pattern) { if (timestamp null) { return ; } Instant instant; if (String.valueOf(timestamp).length() 10) { instant Instant.ofEpochSecond(timestamp); } else { instant Instant.ofEpochMilli(timestamp); } return DateTimeFormatter.ofPattern(pattern) .withZone(ZoneId.of(Asia/Shanghai)) .format(instant); }前端对应函数function formatTimestamp(timestamp, pattern) { const d new Date(timestamp); const pad (num) String(num).padStart(2, 0); return pattern .replace(yyyy, d.getFullYear()) .replace(MM, pad(d.getMonth() 1)) .replace(dd, pad(d.getDate())) .replace(HH, pad(d.getHours())) .replace(mm, pad(d.getMinutes())) .replace(ss, pad(d.getSeconds())); }这段代码没有依赖第三方库适合轻度场景。更复杂的需求建议直接上dayjs。5.3 时间处理工具类设计设计时间工具类时有几个原则是长期实践沉淀下来的所有方法必须接收时区参数或使用配置中心统一读取的时区不能直接ZoneId.systemDefault()。统一入口格式化、解析、转换都收敛到一个工具类里不要散落在业务代码中。对 null 值做好兜底尤其在接口返回时避免 NPE。转换逻辑要做位数判断兼容 10 位和 13 位时间戳。工具类要抛异常或返回默认值要有明确约定不要静默吞掉。我在 Java 里经常把LocalDateTime、Instant、Date、long之间的转换封装成几个重载方法这样业务代码里一行调用测试也容易覆盖。例如public static LocalDateTime toLocalDateTime(Long timestamp) { return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of(Asia/Shanghai)); }另外时间处理相关代码一定要写单元测试。因为日期时间边界条件太多比如月底、年底、夏令时切换虽然国内没有但全球化项目要有、闰年。我用 JUnit 参数化测试覆盖典型边界跑一遍心里才有底。最后再分享一个实操小心得我在解析日志或者处理 Excel 时间戳时从来不在脑子里口算时间戳换算而是现写一个小函数或者直接用在线工具验证一次再批量处理。不要高估自己心算的准确率时间戳动辄 13 位一个数字看错整批数据就废了。学会用脚本批量处理和抽样验证才是真正靠谱的工作方式。
返回列表