1. 问题描述
当使用 timestamp 类型存储时间字段时,经常发生时区错误,比如相差 8 小时这样的问题。
2. 为什么会发生
首先要介绍一个 timestamp 的存储结构与工作模式。
2.1. timestamp 的存储结构
当在 DB 表结构中使用 timestamp 字段时,DB 内部是以一个时间戳保存数据的,不存储具体时区。而展示 timestamp 字段时,是以年月日时分秒形式展示的。时间戳本身是不包含时区信息的,那么这要如何转换呢?
2.2. timestamp 如何展示
答案是根据 session 的时区来转换。而 session 的时区有两种设置方式,一种是在建立连接时由 client 设定,一种是使用 DB server 的全局默认时区。
2.3. timestamp 如何传输
是按照年月日时分秒形式传输的。比如:
- 当查询一个 timestamp 字段时,server 取到 字段的时间戳,再用 session 的时区,转换为 年月日时分秒 的字符串,然后把这个字符串返回给 client。
- 当update 或者 insert 一个 timestamp 字段时,client 传输给 server 的也是一个 年月日时分秒 的字符串,server 根据 session 的时区,转化为时间戳,再保存到字段中。
2.4. 转换发生的时机
查询过程:
server 取时间戳 -> server 使用 session 时区 转换为 字符串 -> client 接收字符串 -> client 可能会使用 client 系统时区转换成时间戳
写入过程:
client 获取时间戳 -> client 使用 client 系统时区换成字符串 -> server 使用 session 时区转换为时间戳
2.5. 问题原因
client 使用的时区与session时区不同。
2.6. 解决方案(python sqlalchemy)
client 本地时区与 session 时区保持一致。
import pytz
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
TZ = 'Asia/Shanghai'
TIME_ZONE = pytz.timezone(TZ)# 指定本地时区
now = datetime.now(TIME_ZONE) # 时区是 +08:00
dt_from_timestamp = datetime.fromtimestamp(1714289876, TIME_ZONE) # 时区是 +08:00, TIME_ZONE 参数不可省略,否则 datetime 对象缺失 时区属性# 指定 session 时区
db = SQLAlchemy(engine_options={"connect_args" : {"init_command": f'SET time_zone="{TZ}"'}},
)# 查询 DB 时 datetime 默认时区是空,需要手动补充时区后取用
user = db.session.query(User).first()
# create_timestamp = user.created.timestamp() # 错误,user.created 对象 时区属性为空,转换为 timestamp 时会使用系统默认时区,可能与 session 时区不一致
# create_timestamp = user.created.replace(tzinfo=TIME_ZONE).timestamp() # 错误,时区是 +08:06,最终时间戳相差了 360 秒
create_timestamp = TIME_ZONE.localize(user.created).timestamp() # 正确,时区是 +08:00
用这种方式,既不依赖 client 系统默认时区,也不依赖 DB 默认时区。保证了 client 使用的时区与 session 一致,都是 TZ 变量的取值。TZ 也可以从环境变量来取值。