今天完抖音大项目时,折腾了一个晚上才Debug成功,原因竟然在于时间戳的精度问题。
1 项目背景
1.1 接口需求
青训营抖音大项目需要支持用户聊天记录和更新聊天记录的功能,需要从数据库中查询比给定时间戳更大的message记录。
1.2 数据库schema
CREATE TABLE messages
(
id SERIAL PRIMARY KEY, -- 自增主键
from_id bigint, -- 发送者id,外键
to_id bigint, -- 接受者id,外键
content VARCHAR(255) NOT NULL, -- 内容
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, -- 日期
deleted date DEFAULT null, -- 软删除
FOREIGN KEY (from_id) REFERENCES users (id),
FOREIGN KEY (to_id) REFERENCES users (id)
);
使用的数据库为Postgresql,Postgresql中,常见的表示日期的数据类型如下
DATE类型:这个数据类型只存储日期,没有时间信息。精确到天。TIMESTAMP类型:存储日期和时间,精确到秒。可以包含小数秒部分,但精度是微秒(microsecond)级别,即精确到百万分之一秒。TIMESTAMP WITH TIME ZONE类型:与TIMESTAMP类型类似,但包括时区信息。TIMESTAMP WITHOUT TIME ZONE类型:与TIMESTAMP类型类似,但不包含时区信息。
因为一天有多条记录需要更新,因此选择日期类型为timestamp without time zone。
2 问题描述和解决方案
2.1 时间戳格式编码错误
本项目使用gorm作为数据库的ORM框架,定义messs表的结构体如下:
type DBMessage struct {
ID int64 `gorm:"primaryKey"`
FromID int64
ToID int64
Content string
CreatedAt time.Time
Deleted gorm.DeletedAt `gorm:"default:NULL"`
}
进行下属查询时出现了错误:
err := DB.Where("((from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?)) AND created_at > ?", request.ToUserID, clientuser.ID, clientuser.ID, request.ToUserID, cmp).Order("ID").Find(&msgList)
错误信息为:
unable to encode 0 into binary format for timestamp (OID 1114): cannot find encode plan
此查询的目的是查询客户端用户与指定的客户间的消息记录,并且要比指定的时间戳(此处的cmp)更新,前端传递的时间戳格式为int64类型,此时需要将int64转化为time.Time类型:
timeObj := time.Unix(tmp, 0)
更改数据类型后,该报错信息解决。
2.2 时间戳精度不统一
解决已上报错后,发现gorm进行查询时,数据格式转化不正确。原因在于time.Unix() 函数转化的对象精度是秒,因此如果前端传递的整型数据类型为毫秒,查询自然会报错,具体举例来说,两个时间戳 1693086747 和 1693058020405 分别表示不同的时间单位精度。1693086747 是一个以秒为单位的时间戳,它表示从 1970 年 1 月 1 日 UTC 到 2023-09-25 05:25:47 UTC 的秒数。1693058020405 是一个以毫秒为单位的时间戳,它表示从 1970 年 1 月 1 日 UTC 到 2023-09-25 05:25:20.405 UTC 的毫秒数。但如果强行将这两个整数放入time.Unix中就会出现错误的答案。
因此,需要端前端传入的整数进行位数判断以决定其时间戳精度。
2.3 时间戳精度不匹配ORM框架
如果前端传递的时间戳格式不匹配,需要进行转化,以下一个从以毫秒为单位的整数时间戳(int64 类型)转换为一个 time.Time 类型的时间对象的示例:
func I64ToTime(num int64) time.Time {
// 提取时间戳的秒和微秒部分
seconds := num / 1000
microseconds := (num % 1000) * 1000
// 将 Unix 时间戳转换为 time.Time 类型
timeObj := time.Unix(seconds, microseconds)
// 格式化为指定的日期时间格式
//formattedTime := timeObj.Format("2006-01-02 15:04:05.000000")
return timeObj
}
2.4 时区匹配问题
最后还存在这样的问题,数据库中存储的时间戳包含了时区信息,但传递给前端时丢失了时区信息,当前端返回时同样的时间戳将被go语言的time库解析成错误的时区信息,因此查询逻辑错误。 最根本的解决方法是根据当前时区规范化传出的时间戳,也可以对传入的时区进行手动调整如下:
cmp = cmp.Add(-time.Hour * 8)
因为0时区和东八区差距为8个小时,所以需要减去8h时间。
3 更多注意事项
使用 PostgreSQL 时间数据类型需要考虑时区、精度、存储格式和与应用程序交互等方面的问题。合理选择数据类型、正确处理时区和使用内置函数都是确保正确处理时间数据的关键。PostgreSQL 使用 IANA 时区数据库来处理时区信息。在服务器上运行的 PostgreSQL 版本需要正确配置和更新 IANA 时区文件,以确保正确的时区转换和处理。同时在时间列上进行索引和查询时,要注意时区和数据的匹配。跨时区的应用程序中,可能会涉及到不同时区的时间数据,这时需要确保正确的时区转换和一致性,避免数据错误。