实践-大项目-时间戳的那些事儿 | 青训营

244 阅读4分钟

今天完抖音大项目时,折腾了一个晚上才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() 函数转化的对象精度是秒,因此如果前端传递的整型数据类型为毫秒,查询自然会报错,具体举例来说,两个时间戳 16930867471693058020405 分别表示不同的时间单位精度。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 时区文件,以确保正确的时区转换和处理。同时在时间列上进行索引和查询时,要注意时区和数据的匹配。跨时区的应用程序中,可能会涉及到不同时区的时间数据,这时需要确保正确的时区转换和一致性,避免数据错误。