前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从一次pr经历谈谈golang和mysql的时区问题

从一次pr经历谈谈golang和mysql的时区问题

作者头像
golangLeetcode
发布2023-09-06 19:26:48
3890
发布2023-09-06 19:26:48
举报
文章被收录于专栏:golang算法架构leetcode技术php

前一段时间,引入了第三方库https://github.com/dolthub/go-mysql-server来进行mysql的单测,它是一个纯go实现的mysql server端,使用它可以去除fake test对mysql环境/docker环境的依赖,实测可以提升运行速度50%以上。实际测试的过程中,发现它会改变datetime类型字段的时区值,导致时区被改的诡异现象。当我们用mysql-cli连上go-mysql-server后,设置当前时区为东八区,就会出现下面的诡异现象。

代码语言:javascript
复制
mysql> create table test (  `sale_end` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '停售时间');
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test (sale_end) values('2023-05-09 09:00:00 +0800 CST');
Query OK, 1 row affected (0.00 sec)

mysql> select * from test;
+---------------------+
| sale_end            |
+---------------------+
| 2023-05-09 01:00:00 |
+---------------------+
1 row in set (0.00 sec)

分析了下https://github.com/dolthub/go-mysql-server的源码后发现,go-mysql-server会解析datetime类型的字符串转换为time.Time, 但是它解析的时候用的时区是UTC,于是就导致了上述问题。所以我想到的办法是在go-mysql-server启动的时候设置TZ环境变量,也就是服务器时区为东八区,解析的时候使用time.ParseInLocation来解析,因为我们单测和go-mysql-server,运行在同一一个进程中,就能解决上述时区问题。

但是提交单测的时候发现golang是不允许修改时区的,比如下面的例子:

代码语言:javascript
复制
func TestTimeZone(t *testing.T) {
  os.Setenv("TZ", "Asia/Shanghai")
  spew.Dump(time.Parse("2006-01-02 15:04:05", "1970-01-01 00:00:00")) //1970-01-01 00:00:00 +0000 UTC
  spew.Dump(time.Local)
  spew.Dump(time.ParseInLocation("2006-01-02 15:04:05", "1970-01-01 00:00:00", time.Local)) //1970-01-01 00:00:00 +0800 CST
  loc, _ := time.LoadLocation("UTC")
  spew.Dump(time.Date(1970, 1, 1, 0, 0, 1, 0, loc))        //(time.Time) 1970-01-01 00:00:01 +0000 UTC
  spew.Dump(time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC))   //(time.Time) 1970-01-01 00:00:01 +0000 UTC
  spew.Dump(time.Date(1970, 1, 1, 0, 0, 1, 0, time.Local)) //(time.Time) 1970-01-01 00:00:01 +0800 CST
  os.Setenv("TZ", "UTC")                                   //修改tz没有用
  spew.Dump(time.Date(1970, 1, 1, 0, 0, 1, 0, time.Local)) //(time.Time) 1970-01-01 00:00:01 +0800 CST
  os.Setenv("TZ", "Asia/Shanghai")
  spew.Dump(time.Date(1970, 1, 1, 0, 0, 1, 0, time.Local)) //(time.Time) 1970-01-01 00:00:01 +0800 CST
}

可以看到 os.Setenv("TZ", "UTC") ,其实是不生效的,为什么呢?我们看下源码

代码语言:javascript
复制
var localLoc Location
var localOnce sync.Once

func (l *Location) get() *Location {
  if l == nil {
    return &utcLoc
  }
  if l == &localLoc {
    localOnce.Do(initLocal)
  }
  return l
}

func initLocal() {
  // consult $TZ to find the time zone to use.
  // no $TZ means use the system default /etc/localtime.
  // $TZ="" means use UTC.
  // $TZ="foo" or $TZ=":foo" if foo is an absolute path, then the file pointed
  // by foo will be used to initialize timezone; otherwise, file
  // /usr/share/zoneinfo/foo will be used.

  tz, ok := syscall.Getenv("TZ")

可以看到通过环境变量TZ,来更改当前时区信息是个单例,也就意味着,一个程序在运行期间,只有最早的那一次才生效。

代码语言:javascript
复制
var utcLoc = Location{name: "UTC"}

// Local represents the system's local time zone.
// On Unix systems, Local consults the TZ environment
// variable to find the time zone to use. No TZ means
// use the system default /etc/localtime.
// TZ="" means use UTC.
// TZ="foo" means use file foo in the system timezone directory.
var Local *Location = &localLoc

并且,我们的time.Local变量也默认是赋值为UTC的。怎么解决这个问题呢,我们可以单独声明一个包,初始化时区信息,并且在程序启动的最开始引入

代码语言:javascript
复制
package tzinit

import (
  "os"
  "time"
)

func init() {
  os.Setenv("TZ", "UTC")
  time.Local = time.UTC
}
代码语言:javascript
复制
import (
  _ "learn/time/time_zone/tz"

这样就能修改时区了。于是我就可以愉快地改单测了。然后顺利提交了我的pr:https://github.com/dolthub/go-mysql-server/pull/1733。

但是提交后go-mysql-server的作者和我交流了下mysql时区的问题。这里我们也可以复习下mysql的关于时间的处理标准,以及golang mysql client的处理逻辑。

从mysql的的官方文档https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html,我们可以知道mysql是这样处理时间相关字段的

代码语言:javascript
复制
The session time zone setting does not affect values 
displayed by functions such as UTC_TIMESTAMP() or values 
in DATE, TIME, or DATETIME columns. Nor are values in 
those data types stored in UTC; the time zone applies for
 them only when converting from TIMESTAMP values. If you 
 want locale-specific arithmetic for DATE, TIME, or 
 DATETIME values, convert them to UTC, perform the 
 arithmetic, and then convert back.

简单翻译下:mysql server 在接收到sql语句的时候除了TIMESTAMP类型的列会按照服务器时区进行解析,然后转换成UTC时间戳存储外,其它类型的列,比如DATE, TIME, or DATETIME会原样存储,UTC_TIMESTAMP() 函数的执行也不受服务器时区的影响,这些字段的时区都是client的具体逻辑决定的,对于mysql-server来说,这些字段是黑盒,原样按照字符串存储,并不会解析。

但是go-mysql-server在实现的时候,用UTC时区解析了上述字段,也就出现了前面奇怪的问题,更明确的对比可以看下下面的例子

mysql-server:

代码语言:javascript
复制
mysql> set time_zone='+08:00';
Query OK, 1 row affected (0.00 sec)

mysql> insert into test (sale_end) values('2023-05-09 09:00:00+08:00');
Query OK, 1 row affected (0.01 sec)

mysql> select * from test;
+---------------------+
| sale_end            |
+---------------------+
| 2023-05-09 09:00:00 |
+---------------------+
1 row in set (0.00 sec)


set time_zone='+00:00';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test (sale_end) values('2023-05-09 09:00:00+08:00');
Query OK, 1 row affected (0.00 sec)
select * from test;
+---------------------+
| sale_end            |
+---------------------+
| 2023-05-09 01:00:00 |
+---------------------+
1 row in set (0.00 sec)

go-mysql-server:

代码语言:javascript
复制
mysql> set time_zone
=
'+08:00'
;
Query OK, 1 row affected (0.00 sec)

mysql> insert into test (sale_end) values('2023-05-09 09:00:00 +0800 CST')
;
Query OK, 1 row affected (0.01 sec)

mysql> select * from test
;
+---------------------+
| sale_end            |
+---------------------+
| 2023-05-09 01:00:00 |
+---------------------+
1 row in set (0.00 sec)

 set time_zone
=
'+00:00'
;
Query OK, 1 row affected (0.00 sec)

mysql> insert into test (sale_end) values('2023-05-09 09:00:00 +0800 CST')
;
Query OK, 1 row affected (0.00 sec)

mysql> select * from test
;
+---------------------+
| sale_end            |
+---------------------+
| 2023-05-09 01:00:00 |
+---------------------+
1 row in set (0.00 sec)

所以我给的建议是服务器端用time.ParseInLocation解析时间,这样服务器和client的解析规则一样,就不会有问题。

说完服务器时区问题,我们讨论下go-mysql-client的时区是如何处理的。

mysql有两个时区概念全局时区和会话时区,对应变量如下:

代码语言:javascript
复制
global.time_zone: mysql服务设置的时区
session.time_zone: 此次连接的设置时区,
一般就是global.time_zone,上面返回的SYSTEM,
代表取系统时区,也就是东八区,默认会从TZ变量来取。

客户端在DSN参数上可以加两个变量parseTime和loc[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

代码语言:javascript
复制
parseTime默认为false,把mysql中的 DATE、DATETIME、TIMESTAMP 
转为golang中的[]byte类型
设置为true,将会转为golang中的 time.Time 类型
代码语言:javascript
复制
loc默认为UTC,表示转换DATE、DATETIME、TIMESTAMP 为 time.Time
 时所使用的时区,
设置成Local,则与系统设置的时区一致
如果想要设置成中国时区可以设置成 Asia/Shanghai

更多的时区可以参考 /usr/share/zoneinfo/ 或者$GOROOT/lib/time/zoneinfo.zip。在实际的使用中,我们往往会配置成 parseTime=true 和 loc=Local,这样避免了手动转换DATE、DATETIME、TIMESTAMP。

因为我们一般会把loc设置成系统的东八区,所以会有前文的问题。总结下:很多细节问题,虽然看上去没啥技术难度,并且很反人类,比如golang中途改TZ环境变量不生效,比如mysql-server的时间处理方式如此复杂。但是从软件的可维护性上来思考,这样做确实可以将整个系统复杂度降低,提升可维护性。假如golang任何地方改TZ环境变量马上生效,一个初学者,在一个进程中,多次设置了TZ,程序运行起来,到底是哪个时区,谁能弄清楚?是不是增加了维护成本?所以要辩证性看源码。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档