当前位置: 首页 > Web前端 > JavaScript

go-sql-driver的一个奇葩bug

时间:2023-03-27 12:56:52 JavaScript

文|郝红帆京东技术专家Seata-go项目共同发起人微服务底层技术探索与研究。本文3482字,阅读7分钟。对于GoCURDBoy,相信大家对github.com/go-sql-driver/mysql这个库并不陌生。基本上,Go的CURD离不开这个特别重要的库。我们在开发Seata-go时也使用了这个库。不过最近在使用go-sql-driver/mysql查询MySQL时,出现了一个有趣的bug,觉得有必要分享一下,防止后来者再次踩坑。部分。1问题详细描述为了说明问题,这里不再详述Seata-go的相关代码,单独用一个demo对问题进行详细描述。1.1环境准备在MySQL实例上准备如下环境:CREATETABLE`Test1`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`create_time`timestampNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,-PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=101默认字符集=utf8mb4COLLATE=utf8mb4_unicode_ci;从这条SQL语句可以看出create_time是timestamp类型,所以这里要特别注意timestamp的类型。现在插入一条数据,然后查看刚刚插入的数据的值。insertintoTest1values(1,'2022-01-0100:00:00')查看MySQL当前时区。请记住相关数值,草蛇灰线,在这里埋下伏笔。显示像“%time_zone%”这样的变量;查询结果:接下来使用MySQLunix_timestamp查看create_time的时间戳:SELECTunix_timestamp(create_time)fromTest1whereid=1;查询结果:1.2测试程序有如下demo程序,例子使用go-sql-driver读取create_time的值:packagemainimport("database/sql""fmt""time"_"github.com/go-sql-driver/mysql")funcmain(){varuser="user"varpwd="password"vardbName="dbname"dsn:=fmt.Sprintf("%s:%s@tcp(localhost:3306)/%stimeout=100s&parseTime=true&interpolateParams=true",user,pwd,dbName)db,err:=sql.Open("mysql",dsn)iferr!=nil{panic(err)}延迟db.Close()行,err:=db.Query("selectcreate_timefromTest1limit1")iferr!=nil{panic(err)}forrows.Next(){t:=time.Time{}rows.Scan(&t)fmt.Println(t)fmt.Println(t.Unix())}}我们运行这个程序会输出如下结果:2022-01-0100:00:00+0000UTC16409952001.3问题详情找到问题了吗?图片如下。把结果放在一起,就可以详细解释问题图中红色箭头所指的两个结果了。go-sql-driver读取的结果和MySQL中unix_timestamp得到的结果明显不同。部分。2问题检测1.3小节最后一张图可以看出,数据库中create_time的值2022-01-0100:00:00是东八区的时间,也就是北京时间,对应的时间戳to这个时候是1640966400。但是go-sql-driver示例程序读取1640995200,这个值是多少?这是0时区的2022-01-0100:00:00,问题的直白描述是:MySQL的create_time是2022-01-0100:00:00+008,read是2022-01-0100:00:00+000,它们根本不一样。您基本上可以看到错误是如何发生的。那么就需要分析go-sql-driver的源码,追根溯源。2.1go-sq-driver源码分析“github.com/go-sql-driver/mysql”的详细源码这里就不贴了,只贴关键路径。调试时要密切注意调用路径中两个红色方块在内存中的值。//https://github.com/go-sql-driver/mysql/blob/master/packets.go#L788-L798func(rows*textRows)readRow(dest[]driver.Value)error{//...//解析时间字段切换rows.rs.columns[i].fieldType{casefieldTypeTimestamp,fieldTypeDateTime,fieldTypeDate,fieldTypeNewDate:ifdest[i],err=parseDateTime(dest[i].([]byte),mc.cfg.Loc);err!=nil{returnerr}}}funcparseDateTime(b[]byte,loc*time.Location)(time.Time,error){constbase="0000-00-0000:00:00.000000"switchlen(b){case10,19,21,22,23,24,25,26://直到"YYYY-MM-DDHH:MM:SS.MMMMMM"year,err:=parseByteYear(b)month:=time.Month(m)日,err:=parseByte2Digits(b[8],b[9])小时,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如果len(b)==19{returntime.Date(year,month,day,hour,min,sec,0,loc),nil}}}基本上从这里可以理解go-sql-driver取的是从databaseasAstring,然后按照MySQL时间戳“0000-00-0000:00:00.000000”的标准格式解析,分别得到年、月、日、时、分、秒,最后依赖时间.Location值传入,调用Go系统库time.Date()生成对应值。这里看似没有问题,但实际上非常依赖传入的time.Location,这个time.Location是如何获取的呢?进一步阅读源码,可以清楚的看到,它是通过解析传入的DSN的Loc得到的。关键代码是:https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L474。如果传入的DSN字符串不包含Loc,则Loc是默认的UTC时区。2.2回头看一开始的程序,初始化go-sql-driver的DSN为user:password@tcp(localhost:3306)/dbname?timeout=100s&parseTime=true&interpolateParams=true,不包含Loc信息,go-sql-driver使用默认的UTC时区。然后解析从MySQL获取的timestamp字段,使用默认的UTC时区生成Date,结果是错误的。因此,问题的主要原因是:go-sql-driver没有根据数据库的时区解析timestamp字段,依赖开发者生成的DSN传入的Loc。当开发者传入的Loc与数据库的time_Zone不匹配时,所有时间戳字段都会被解析错误。可能有人会有疑问,如果go-sql-driver不直接使用MySQL的时区来解析时间戳呢?我们提出了一个问题来讨论更好的解决方案:https://github.com/go-sql-dri...。部分。3最后的结论在MySQL中读写时间戳类型数据时,有以下注意事项:默认约定:在写入MySQL时间时,将当前时区的时间转换为UTC+00:00(世界标准时区)的值,并读取后在前端显示时再次转换;如果不想使用默认的约定,在这个阶段使用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之星?:https://github.com/seata/seata-go本周推荐阅读SeataAT模式代码级详解蚂蚁集团海外站Seata实践与探索Seata多语言系统搭建Seata-php半年计划