一个 go-sql-driver 的离奇 bug

news2024/11/22 17:34:55

d4a0a6983bf08a0b40e162bb9656bac2.gif

文|郝洪范

京东技术专家

Seata-go 项目共同发起人

微服务底层技术的探索与研究。

本文 3482 字 阅读 7 分钟

对于 Go CURD Boy 来说,相信 github.com/go-sql-driver/mysql 这个库都不会陌生。基本上 Go 的 CURD 都离不开这个特别重要的库。我们在开发 Seata-go 时也使用了这个库。不过最近在使用 go-sql-driver/mysql 查询 MySQL 的时候,就出现一个很有意思的 bug, 觉得有必要分享出来,以防止后来者再次踩坑。

PART. 1

问题详述

为了说明问题,这里不详述 Seata-go 的相关代码,用一个单独的 demo 把问题详细描述清楚。

1.1 环境准备

在一个 MySQL 实例上准备如下环境:

CREATE TABLE `Test1` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

从这个 SQL 语句中可以看出来, create_time 是 timestamp 类型,这里要特别留意 timestamp 这个类型。

现在插入一条数据,然后查看刚插入的数据的值。

insert into Test1 values (1, '2022-01-01 00:00:00')

查看下 MySQL 当前的时区。请记好相关值,草蛇灰线,伏笔于此。

show VARIABLES like '%time_zone%';

查询结果:

bd2ea6ca8f199c19458626e31dd60f4d.png

接下来使用 MySQL unix_timestamp 查看 create_time 的时间戳:

SELECT unix_timestamp(create_time) from Test1 where id = 1;

查询结果:

17b3c7f784f2da1aeee49ca4e8484e31.png

1.2 测试程序

有如下 demo 程序,示例使用 go-sql-driver 读取 create_time 的值:

package main


import (
  "database/sql"
  "fmt"
  "time"


    _ "github.com/go-sql-driver/mysql"
)


func main() {
  var user = "user"
  var pwd = "password"
  var dbName = "dbname"


  dsn := fmt.Sprintf("%s:%s@tcp(localhost:3306)/%s?timeout=100s&parseTime=true&interpolateParams=true", user, pwd, dbName)
  db, err := sql.Open("mysql", dsn)
  if err != nil {
    panic(err)
  }
  defer db.Close()


  rows, err := db.Query("select create_time from Test1 limit 1")
  if err != nil {
    panic(err)
  }
  for rows.Next() {
    t := time.Time{}
    rows.Scan(&t)
    fmt.Println(t)
    fmt.Println(t.Unix())
  }
}

我们运行个程序会输出下面的结果:

2022-01-01 00:00:00 +0000 UTC
1640995200

1.3 问题详述

发现问题所在了吗?有图如下,把结果放在一块,可以详细说明问题。

07569ed67ca34b90b5c25db8ddf9c20d.png

图中红色箭头指向的两个结果,用 go-sql-driver 读取的结果和在 MySQL 中用 unix_timestamp 获取的结果明显是不一样的。

PART. 2

问题探案

1.3 小节中最后示图可以看出,数据库中 create_time  的值 2022-01-01 00:00:00 是东八区的时间,也就是北京时间,这个时间对应的时间戳就是 1640966400 。但是 go-sql-driver 示例程序读出来的却是 1640995200 , 这是什么值?这是 0 时区的 2022-01-01 00:00:00

对问题的直白描述就是:MySQL 的 create_time 是 2022-01-01 00:00:00 +008 ,而读取到的是 2022-01-01 00:00:00 +000 ,他俩压根就不是一个值。

基本能看出来 bug 是如何发生的了。那就需要剖析下 go-sql-driver 源码,追查问题的根源。

2.1 go-sq-driver 源码分析

这里就不粘贴 "github.com/go-sql-driver/mysql" 的详细源码了,只贴关键的路径。

ad5d7c00fdc79d290ba7dff7dd2de2b0.png

Debug 的时候详细关注调用路径中红色的两个方块的内存中的值。

55b3330c10e95a2d0f64533926df5fb7.png

// https://github.com/go-sql-driver/mysql/blob/master/packets.go#L788-L798


func (rows *textRows) readRow(dest []driver.Value) error {


  // ... 


  // Parse time field
  switch rows.rs.columns[i].fieldType {
  case fieldTypeTimestamp,
    fieldTypeDateTime,
    fieldTypeDate,
    fieldTypeNewDate:
    if dest[i], err = parseDateTime(dest[i].([]byte), mc.cfg.Loc); err != nil {
      return err
    }
  }
}
func parseDateTime(b []byte, loc *time.Location) (time.Time, error) {
  const base = "0000-00-00 00:00:00.000000"
  switch len(b) {
  case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"


    year, err := parseByteYear(b)


    month := time.Month(m)


    day, err := parseByte2Digits(b[8], b[9])


    hour, err := parseByte2Digits(b[11], b[12])


    min, err := parseByte2Digits(b[14], b[15])


    sec, err := parseByte2Digits(b[17], b[18])


    // https://github.com/go-sql-driver/mysql/blob/master/utils.go#L166-L168
    if len(b) == 19 {
      return time.Date(year, month, day, hour, min, sec, 0, loc), nil
    }
  }
}

从这里基本上就能明白,go-sql-driver 把数据库读出来的 create_time timestamp 值当做一个字符串,然后按照 MySQL timestamp 的标准格式 "0000-00-00 00:00:00.000000" 去解析,分别得到 year, month, day, hour, min, sec。最后依赖传入 time.Location 值,调用 Go 系统库 time.Date() 再去生成对应的值。

这里表面看起来没有问题,其实这里严重依赖了传入的 time.Location。这个 time.Location 是如何得到的呢?进一步阅读源码,可以明显的看出来,是通过解析传入的 DSN 的 Loc 获取。

其中关键代码是:https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L474

1847a4b1de2d963bfbeab60d71ae909a.png

如果传入的 DSN 串不带 Loc 时,Loc 就是默认的 UTC 时区。

a0d9dcf0978401663209514192d6f54d.png

2.2 抽丝剥茧

回头看开头的程序,初始化 go-sql-driver 的 DSN 是 user:password@tcp(localhost:3306)/dbname?timeout=100s&parseTime=true&interpolateParams=true,该 DSN 里面并不包含 Loc 信息,go-sql-driver 用使用了默认的 UTC 时区。然后解析从 MySQL 中获取的 timestamp 字段了,也就用默认的 UTC 时区去生成 Date,结果也就错了。

因此,问题的主要原因是:go-sql-driver 并没有按照数据库的时区去解析 timestamp 字段,而且依赖了开发者生成的 DSN 传入的 Loc。当开发者传入的 Loc 和数据库的 time_Zone 不匹配的时候,所有的 timestamp 字段都会解析错误。

有些人可能有疑问,如果 go-sql-driver 为什么不直接使用 MySQL 的时区去解析 timestamp 呢?


我们已经提了一个 issue,商讨更好的解决方案:https://github.com/go-sql-driver/mysql/issues/1379

PART. 3

最后结论

在 MySQL 中读写 timestamp 类型数据时,有如下注意事项:

  1. 默认约定:写入 MySQL 时间时,把当前时区的时间转换为 UTC + 00:00(世界标准时区)的值,读取后在前端展示时再次进行转换;

  2. 如果不愿意使用默认约定,在现阶段使用 go-sql-driver 的时候,一定要特别注意,需要在 DSN 字符串加上 "loc=true&time_zone=*" , 和数据的时区保持一致,不然的话就会导致 timestamp 字段解析错误。

| 参考文档 | 

《The date, datetime, and timestamp Types》 

https://dev.mysql.com/doc/refman/8.0/en/datetime.html

《MySQL 的 timestamp 会存在时区问题?》

https://juejin.cn/post/7007044908250824741

《Feature request: Fetch connection time_zone automatically》

https://github.com/go-sql-driver/mysql/issues/1379

社区讨论群

细节处见真章,

Seata-go 社区认认真真做开源,

做对用户负责任的高质量的项目。

扫扫加入钉钉群:

Seata-go 社区群:33069364

2b9289dbe311fa36bd682033872e0381.png

Seata-go 开发群:44816898

cbfccc3510162d547b5f5c16fe2289b5.png

 了解更多...

Seata Star 一下✨:
https://github.com/seata/seata-go

  本周推荐阅读  

345cc0a8c58c2b3c95ee1eeb1694cc52.png

Seata AT 模式代码级详解

fa5456fb8fb19a79c75397a2e40f925e.png

蚂蚁集团境外站点 Seata 实践与探索

3f2757b56a404c4da070744396d4a50a.png

Seata 多语言体系建设

4dc74ab7ee673cf37ec7899a67209f43.png

Seata-php 半年规划

e9b7c7834dc996187d76437d69841b0d.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/170055.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LabVIEW将现有数据文件映射至TDMS数据文件格式

LabVIEW将现有数据文件映射至TDMS数据文件格式在某些情况下,可能无法使用TDMS文件格式,例如客户或供应商指定必须使用某种格式存储数据。有些传统仪器可能会自动使用某种自定义格式提供数据输出文件。此外,已经用某种方式收集的传统测量数据无…

PyQt6快速入门-自定义Widget

自定义Widget 文章目录 自定义Widget1、准备工作2、重写paintEvent事件3、Position策略4、更新显示5、绘制条形框5.1 绘制计算5.2 绘制条形框6、自定义样式7、添加鼠标交互能力8、完整代码QPainter是Qt中所有小部件绘制的基础。在本文中,详细介绍如何构建一个全新的自定义 GUI…

vue文本点击样式设置

vue文本点击样式设置嘚吧嘚干就完了光标边小手文本域样式修改hover语法语法一语法二语法三语法四学以致用,效果实现嘚吧嘚 相信当家在写代码的过程中,文本的点击事件是常有的吧,如历史搜索记录、页面跳转等。本次就就分享一下文本点击样式设…

从CES的亚马逊云科技展台,看云计算如何改变汽车行业

当云计算技术被广泛运用于智能汽车的制造,会给整个汽车行业带来怎样的变革?CES 2023汽车展区:亚马逊云科技展台成为焦点作为全球规模最大、影响力最为广泛的国际消费电子展,CES 2023于近日在美国拉斯维加斯圆满落下帷幕。在这场汇…

数据结构和算法的基本概念和基本术语(数据,数据元素,数据项,数据对象)

目录 一、数据结构的研究内容 1.1学生信息管理系统 1. 2人机对弈问题 1. 3最短路径问题 二、基本概念和术语 2.1数据,数据元素,数据项,数据对象 2.1.1 数据(Data): 2.1.2 数据元素(Data Element)&a…

关于elasticsearch一些基本操作

哈喽~大家好,这篇来看看关于elasticsearch一些基本操作。 🥇个人主页:个人主页​​​​​ 🥈 系列专栏: 【微服务】 🥉与这篇相关的文章: SpringCloud Se…

Import语句基础

1 问题 在 Java 中,如果给出一个完整的限定名,包括包名、类名,那么 Java 编译器就可以很容易地定位到源代码或者类。import 语句就是用来提供一个合理的路径,使得编译器可以找到某个类。 2 方法 1.import导入声明可分为两种: 1&a…

【每日一道智力题】之 轮流取石子(简单的尼姆博弈)

题目:一共有N颗石子(或者其他乱七八糟的东西),每次最多取M颗最少取1颗,A,B轮流取,谁最后会获胜?(假设他们每次都取最优解)。解答:结论&#xff1a…

告诉大家几个好用的功能

功能一:打开通知面板/月历面板 WinN的作用是调出通知面板,由于Windows 11将月历与通知面板合在了一起,因此它的另一项功能,就是——打开月历。 功能二:WindowsW:启用小组件面板 如果我们需要用到系统自带的小组件&am…

WC2023游记

今年,我势必打破铜牌魔咒 Day -?~? 虽然已年及高二,但WC的讲课还是没有听懂多少,这段时间,北师大还有一名E队来我校训练,我只能感慨:“如果一个选手比你强,还比你小,那你就再也打…

51 种 AI 工具,生活、编程、内容创建都应该使用它

AI 正在席卷全球 🔥🔥🔥 它具有无限的潜力,并将改变我们的生活,让生活变得更美好。这项技术将迅速改进,您今天可以使用许多工具来提高您的工作效率,帮助您完成工作,为您提供有关许多…

单网口ubuntu主机配置virt-manager传统桥接bridge网络

单网口ubuntu主机配置virt-manager传统桥接bridge网络 虚拟机的网络桥接bridge模式往往需要物理宿主机有两个网口,一个网口1连接外网配置ip,另一个网口2空闲不配置ip,在virt-manager里配置虚拟机的网卡绑定网口2,从而实现虚拟机桥…

81.门控循环单元(GRU)以及代码实现

1. 关注一个序列 做RNN的时候,处理不了太长的序列,因为把整个序列信息全部放在隐藏状态中,所有东西都放进去,当时间步很长的话,隐藏状态就会累积太多东西,就可能对很前面的信息不那么容易抽取出来了。 所…

Aftermath:一款针对macOS的免费开源事件响应框架

关于Aftermath Aftermath是一款针对macOS的事件响应框架,该工具基于Swift语言开发,是一款完全免费且开源的网络安全事件响应框架。 在Aftermath的帮助下,广大研究人员可以轻松收集并分析受感染主机的数据。除此之外,在理想情况下…

“深度学习”学习日记。误差反向传播法--Affine/Softmax层的实现

2023.1.17 Affine层: 在神经网络的正向传播中,为了计算加权信号的总和,使用矩阵乘积运算。 比如: import numpy as npx np.arange(6).reshape(2, 3) # (2,3) w np.arange(6).reshape(3, 2) # (3,2) b np.arange(4).resha…

LeetCode083_83. 删除排序链表中的重复元素

LeetCode083_83. 删除排序链表中的重复元素 一、描述 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 示例 1: 输入:head [1,1,2] 输出:[1,2]示例 2&#xff1…

虚拟机或Linux安装Nginx及本地指定虚拟机域名

安装必要工具和依赖 yum -y install wget gcc gcc-c pcre pcre-devel zlib zlib-devel openssl openssl-devel -----------------------------------废话开始------------------------------------------------- 上面这句话的意思:以下当废话 yum ---在线安装命令 inst…

82.长短期记忆网络(LSTM)以及代码实现

1. 长短期记忆网络 忘记门:将值朝0减少输入门:决定不是忽略掉输入数据输出门:决定是不是使用隐状态 2. 门 3. 候选记忆单元 4. 记忆单元 5. 隐状态 6. 总结 7. 从零实现的代码 我们首先加载时光机器数据集。 import torch from torch imp…

基于python手撕实现BP 神经网络实现手写数字识别(不调库,附完整版本代码)

本项目使用python实现全连接网络和梯度优化 方向传播并且实现了 手写数字识别项目: 神经网络 model 先介绍个三层的神经网络,如下图所示输入层(input layer)有三个 units( 为补上的 bias,通常设为 1)

安卓影像飞升时刻:vivo X90 Pro+打通HDR任督二脉

在手机产业中,大多数人会有一种刻板印象:一项新技术/功能,苹果发布会上展示意味着已经成熟,具有很高的产品完成度,好用且有效;而安卓厂商在发布会上展示出的一些炫酷技术,往往还需要时间观望&am…