go編程中g(shù)o-sql-driver的離奇bug解決記錄分析
正文
對于 Go CURD Boy 來說,相信 github.com/go-sql-driver/mysql
這個庫都不會陌生?;旧?Go 的 CURD 都離不開這個特別重要的庫。我們在開發(fā) Seata-go 時也使用了這個庫。
不過最近在使用 go-sql-driver/mysql 查詢 MySQL 的時候,就出現(xiàn)一個很有意思的 bug, 覺得有必要分享出來,以防止后來者再次踩坑。
PART. 1 問題詳述
為了說明問題,這里不詳述 Seata-go 的相關(guān)代碼,用一個單獨(dú)的 demo 把問題詳細(xì)描述清楚。
1.1 環(huán)境準(zhǔn)備
在一個 MySQL 實(shí)例上準(zhǔn)備如下環(huán)境:
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 這個類型。
現(xiàn)在插入一條數(shù)據(jù),然后查看剛插入的數(shù)據(jù)的值。
insert?into?Test1?values?(1,?'2022-01-01?00:00:00')
查看下 MySQL 當(dāng)前的時區(qū)。請記好相關(guān)值,草蛇灰線,伏筆于此。
show?VARIABLES?like?'%time_zone%';
查詢結(jié)果:
接下來使用 MySQL unix_timestamp 查看 create_time 的時間戳:
SELECT?unix_timestamp(create_time)?from?Test1?where?id?=?1;
查詢結(jié)果:
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)/%stimeout=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()) }}
我們運(yùn)行個程序會輸出下面的結(jié)果:
2022-01-01 00:00:00 +0000 UTC1640995200
1.3 問題詳述
發(fā)現(xiàn)問題所在了嗎?有圖如下,把結(jié)果放在一塊,可以詳細(xì)說明問題。
圖中紅色箭頭指向的兩個結(jié)果,用 go-sql-driver 讀取的結(jié)果和在 MySQL 中用 unix_timestamp 獲取的結(jié)果明顯是不一樣的。
PART. 2 問題探案
1.3 小節(jié)中最后示圖可以看出,數(shù)據(jù)庫中 create_time 的值 2022-01-01 00:00:00
是東八區(qū)的時間,也就是北京時間,這個時間對應(yīng)的時間戳就是 1640966400
。但是 go-sql-driver 示例程序讀出來的卻是 1640995200
, 這是什么值?這是 0 時區(qū)的 2022-01-01 00:00:00
。
對問題的直白描述就是:MySQL 的 create_time 是 2022-01-01 00:00:00 +008
,而讀取到的是 2022-01-01 00:00:00 +000
,他倆壓根就不是一個值。
基本能看出來 bug 是如何發(fā)生的了。那就需要剖析下 go-sql-driver 源碼,追查問題的根源。
2.1 go-sq-driver 源碼分析
這里就不粘貼 "github.com/go-sql-driver/mysql"
的詳細(xì)源碼了,只貼關(guān)鍵的路徑。
Debug 的時候詳細(xì)關(guān)注調(diào)用路徑中紅色的兩個方塊的內(nèi)存中的值。
// 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 把數(shù)據(jù)庫讀出來的 create_time timestamp 值當(dāng)做一個字符串,然后按照 MySQL timestamp 的標(biāo)準(zhǔn)格式 "0000-00-00 00:00:00.000000" 去解析,分別得到 year, month, day, hour, min, sec
。最后依賴傳入 time.Location 值,調(diào)用 Go 系統(tǒng)庫 time.Date() 再去生成對應(yīng)的值。
這里表面看起來沒有問題,其實(shí)這里嚴(yán)重依賴了傳入的 time.Location。這個 time.Location 是如何得到的呢?進(jìn)一步閱讀源碼,可以明顯的看出來,是通過解析傳入的 DSN 的 Loc 獲取。
其中關(guān)鍵代碼是:https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L474 。
如果傳入的 DSN 串不帶 Loc 時,Loc 就是默認(rèn)的 UTC 時區(qū)。
2.2 抽絲剝繭
回頭看開頭的程序,初始化 go-sql-driver 的 DSN 是 user:password@tcp(localhost:3306)/dbname?timeout=100s&parseTime=true&interpolateParams=true
,該 DSN 里面并不包含 Loc 信息,go-sql-driver 用使用了默認(rèn)的 UTC 時區(qū)。然后解析從 MySQL 中獲取的 timestamp 字段了,也就用默認(rèn)的 UTC 時區(qū)去生成 Date,結(jié)果也就錯了。
因此,問題的主要原因是:go-sql-driver 并沒有按照數(shù)據(jù)庫的時區(qū)去解析 timestamp 字段,而且依賴了開發(fā)者生成的 DSN 傳入的 Loc。當(dāng)開發(fā)者傳入的 Loc 和數(shù)據(jù)庫的 time_Zone 不匹配的時候,所有的 timestamp 字段都會解析錯誤。
有些人可能有疑問,如果 go-sql-driver 為什么不直接使用 MySQL 的時區(qū)去解析 timestamp 呢?
我們已經(jīng)提了一個 issue,商討更好的解決方案:https://github.com/go-sql-dri...。
PART. 3 最后結(jié)論
在 MySQL 中讀寫 timestamp 類型數(shù)據(jù)時,有如下注意事項(xiàng):
- 默認(rèn)約定:寫入 MySQL 時間時,把當(dāng)前時區(qū)的時間轉(zhuǎn)換為 UTC + 00:00(世界標(biāo)準(zhǔn)時區(qū))的值,讀取后在前端展示時再次進(jìn)行轉(zhuǎn)換;
- 如果不愿意使用默認(rèn)約定,在現(xiàn)階段使用 go-sql-driver 的時候,一定要特別注意,需要在 DSN 字符串加上 "loc=true&time_zone=*" , 和數(shù)據(jù)的時區(qū)保持一致,不然的話就會導(dǎo)致 timestamp 字段解析錯誤。
| 參考文檔 |
《The date, datetime, and timestamp Types》
https://dev.mysql.com/doc/refman/8.0/en/datetime.html
《MySQL 的 timestamp 會存在時區(qū)問題?》
http://www.dbjr.com.cn/article/255355.htm
《Feature request: Fetch connection time_zone automatically》
https://github.com/go-sql-driver/mysql/issues/1379
以上就是go編程中g(shù)o-sql-driver的離奇bug解決記錄分析的詳細(xì)內(nèi)容,更多關(guān)于go-sql-driver bug的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解如何利用GORM實(shí)現(xiàn)MySQL事務(wù)
為了確保數(shù)據(jù)一致性,在項(xiàng)目中會經(jīng)常用到事務(wù)處理,對于MySQL事務(wù)相信大家應(yīng)該都不陌生。這篇文章主要總結(jié)一下在Go語言中Gorm是如何實(shí)現(xiàn)事務(wù)的;感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助2022-09-09基于Golang實(shí)現(xiàn)Redis協(xié)議解析器
這篇文章主要為大家詳細(xì)介紹了如何通過GO語言編寫簡單的Redis協(xié)議解析器,文中的示例代碼講解詳細(xì),對我們深入了解Go語言有一定的幫助,需要的可以參考一下2023-03-03基于Go實(shí)現(xiàn)TCP長連接上的請求數(shù)控制
在服務(wù)端開啟長連接的情況下,四層負(fù)載均衡轉(zhuǎn)發(fā)請求時,會出現(xiàn)服務(wù)端收到的請求qps不均勻的情況或是服務(wù)器無法接受到請求,因此需要服務(wù)端定期主動斷開一些長連接,所以本文給大家介紹了基于Go實(shí)現(xiàn)TCP長連接上的請求數(shù)控制,需要的朋友可以參考下2024-05-05如何使用Goland IDE go mod 方式構(gòu)建項(xiàng)目
這篇文章主要介紹了如何使用Goland IDE go mod 方式構(gòu)建項(xiàng)目,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10