一次因时区引发的血案

前言

曾经以为时区这种东西一辈子都不会用到,又不出国,也不会接触到国外的项目。怎奈公司项目涉及海外版本,并且架构本身并没有考虑到时区问题,数据库中存储的相关时间部分不是时间戳,而是timestamp类型,当他来的时候总是那么的措不及防,以下是血泪史的记录,谨以此文告慰一周死去的脑细胞。

时区基础知识

  • 世界时区总共分为24个,从-12到+12,分别被称作是东一到东十二,西一到西十二。
    以北京时间为例: 属于东八区 +8 ,美国 西六区 -6

  • UTC :Coordinated Universal Time 协调世界时。因为地球自转越来越慢,每年都会比前一年多出零点几秒,每隔几年协调世界时组织都会给世界时+1秒,让基于原子钟的世界时和基于天文学(人类感知)的格林尼治标准时间相差不至于太大。并将得到的时间称为UTC,这是现在使用的世界标准时间。不与任何地区位置相关,也不代表此刻某地的时间

  • GMT: 根据地球的自转和公转来计算时间 ,由于现在世界上最精确的原子钟50亿年才会误差1秒,格林尼治刚好在0时区上 因此也近似等于 UTC+0

  • CAT: 同时可以代表如下 4 个不同的时区,常见于数据库中的时区
    Central Standard Time (USA) UT-6:00
    Central Standard Time (Australia) UT+9:30
    China Standard Time UT+8:00
    Cuba Standard Time UT-4:00

  • 时间戳:从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。是一个差值,和时区无关。同一个时间,不管哪个时区获取到时间戳都是一样的,但是代表的日期却因为时区不同而不同。

时区函数选择

因为项目使用的是php进行开发的,查阅相关文档,并没有发现php并不能够直接设置 UTC+(-)时区,而GMT可以,因此选择GMT来设置时区。
格式如下:
/Etc/GMT+8

php的坑

参考其他语言中设置时区,东八区就设置+8,但是php就是这么个性,需要取反,设置为-8,如果要设置为东八区需要:

date_default_timezone_set("Etc/GMT-8");

MySQL中的时区

查看当前时区,因为我设置过了所以这里查询出来是-6

show variables like '%time_zone%';

我们可以看到有一个系统的时区,同时也有一个当前会话对应的时区time_zone。
在这里插入图片描述
数据库中timestamp类型:
存储的时候,按照当前数据库的时区设置时间,比如再写入数据库的时候是+8区的时间,
在这里插入图片描述
我们设置为+7区,在查看时间,就晚了一个小时。
在这里插入图片描述

填坑

框架底层

项目使用yii2,重写数据库刚刚连接的方法,立即重置时区,保证一次会话中php和mysql时区都是一致的。
在这里插入图片描述

缓存

因为项目中在最底层重写了数据查询结果集,全部存储在redis缓存中,每当更新某个表的时候,刷新相关缓存,key的取值又是这种获取的类名和函数名,对于这种,在刷新相关缓存的key的时候,需要把时区的概念带进去。
在这里插入图片描述

对外api接口

因为游戏中心是所有数据的"心脏",对外提供不同的访问者,至少有5-6方,各个系统要求的格式也不一样,如果说所有的都由接口提供方转换,那么对于全列表的大数据量的json来说,那是致命的伤害。
因为在接口中不同的版本对应不同的时区,循环查询的时候频繁的设置数据库时区,并且写入缓存中,效率可想而知。

那就让需求方自己传一个时区过来,还是在连接数据库完成的时候,直接设置时区或者传和0区的秒数差,全部按照指定时区返回,由使用方自行调整。

配置文件中日期

有些数据是以字段的形式存储在数据库某个json字段中,对于这种纯字符串,没有timestamp的概念,只有存储的时候,以目标时区为准,在取出来的时候设置指定时区,后面通过php转为指定时区。

附录时区转换封装类

class DateHelper
{
    
    

    public static $cacheTime = 600;

    /**
     * 根据serverType获取时区
     * @param $serverType int|string
     * @param $gid int|string 用于缓存中使用
     * @return string 格式:+08:00
     * @throws \Exception
     */
    public static function getTimeZoneByServerType($serverType,$gid)
    {
    
    
        $cacheKey = __CLASS__ . __FUNCTION__ . $serverType;
        $value = \Yii::$app->ac->get($cacheKey, GameServerType::tableName().'::'.$gid, function () use ($serverType) {
    
    
            //根据serverType获取时区
            $server_type_info = GameServerType::findOne($serverType);
            if (empty($server_type_info) || is_null($server_type_info)) {
    
    
                \Yii::info("date_helper:获取{
      
      $serverType}信息失败");
                $time_zone = "+08:00";
            } else {
    
    
                $data = $server_type_info->data;
                AppCommon::decodeJsonToArray($data);//配置中配置的json转为数组
                //配置了时区
                if (!empty($data) && isset($data['time_zone'])) {
    
    
                    if (self::isTimeZoneAvaiable($data['time_zone'])) {
    
    
                        $time_zone = $data['time_zone'];
                    } else {
    
    
                        throw new \Exception("date_helper---serverType:{
      
      $serverType},time_zone:{
      
      $data['time_zone']}格式非法");
                    }
                } else {
    
    
                    $time_zone = "+08:00";
                }
            }
            return $time_zone;
        });
        return $value;
    }


    /**
     * 设置会话级php和MySQL的时区
     * @param int $serverType string serverType
     * @param int $gid string gid
     * @param bool $is_set_mysql 是否设置mysql数据库时区,有的地方只需要设置php的时区
     * @throws \Exception
     * @throws \yii\db\Exception
     */
    public static function setSessionTimeZone($serverType = 0, $gid=0,$is_set_mysql = true)
    {
    
    
        //如果是根据ServerType获取时区
        if ($serverType != 0) {
    
    
            $time_zone = self::getTimeZoneByServerType($serverType,$gid);
            //设置时区 mysql+php
            if ($is_set_mysql) {
    
    
                \Yii::$app->db->createCommand("set time_zone = '{
      
      $time_zone}'")->execute();
            }

            $php_time_zone = explode(':', $time_zone)[0];
        } else {
    
    
            //$time_zone='Etc/GMT-8'  //如果没有传入time_zone直接获取php默认的时区
            //设置时区 mysql+php
            if ($is_set_mysql) {
    
    
                \Yii::$app->db->createCommand("set time_zone = '+08:00'")->execute();
            }
            $php_time_zone = '+8';
        }

        if (strpos($php_time_zone, '-') !== false) {
    
    
            $diff = intval(trim($php_time_zone, '-'));
            date_default_timezone_set("Etc/GMT+{
      
      $diff}");
        } else {
    
    
            $diff = intval(trim($php_time_zone, '+'));
            date_default_timezone_set("Etc/GMT-{
      
      $diff}");
        }
    }


    /**
     * 判断设置的时区是否可用
     * @param $time_zone string 格式:+08:00
     * @param int $serverType string|int 兼容根据serverType!=0时根据serverType获取
     * @return bool
     */
    public static function isTimeZoneAvaiable($time_zone, $serverType = 0)
    {
    
    
        if (empty($time_zone)) {
    
    
            return false;
        }
        preg_match('/[+,-]{1}[0-2]{1}[0-9]{1}\:\w+/', $time_zone, $match);
        if (!empty($match)) {
    
    
            return true;
        } else {
    
    
            \Yii::info("serverType:{
      
      $serverType}配置时区格式错误,time_zone:{
      
      $time_zone}");
            return false;
        }
    }


    /**
     * 获取php默认时区,time_zone 格式:/Etc/GMT+8 simple_zone 格式:+08:00
     * @return array
     */
    public static function getPHPDefaultZone()
    {
    
    
        $time_zone = 'Etc/GMT-8';//默认东八区,对应服务器上php的时区,如果服务器上改了,这里需要改,配置文件里面那个和时区相关的字段也需要修改。
        $simple_zone = self::getSimpleZone($time_zone);
        return [
            'time_zone' => $time_zone,
            'simple_zone' => $simple_zone,
        ];
    }

    /**
     * 转换 /Etc/GMT+8 格式 ---> +08:00 格式
     * @param $time_zone
     * @return int|string
     */
    public static function getSimpleZone($time_zone)
    {
    
    
        $temp = substr($time_zone, 7);// +8 -10 这种
        $op = substr($temp, 0, 1);
        $num = intval(substr($temp, 1));
        $op = ($op == '+' ? '-' : '+');
        $num = $op . str_repeat('0', 2 - strlen($num)) . $num . ':00';
        return $num;
    }


    /**
     * 设置固定格式时区
     * @param $time_zone string 游戏中心配置的 +08:00这种
     * @throws \Exception
     * @throws \yii\db\Exception
     */
    public static function setFixedTimeZone($time_zone)
    {
    
    
        //判断传过来的时区格式 符合 +08:00这种 才设置
        if (self::isTimeZoneAvaiable($time_zone)) {
    
    
            \Yii::$app->db->createCommand("set time_zone = '{
      
      $time_zone}'")->execute();
            $php_time_zone = explode(':', $time_zone)[0];
            if (strpos($php_time_zone, '-') !== false) {
    
    
                $diff = intval(trim($php_time_zone, '-'));
                date_default_timezone_set("Etc/GMT+{
      
      $diff}");
            } else {
    
    
                $diff = intval(trim($php_time_zone, '+'));
                date_default_timezone_set("Etc/GMT-{
      
      $diff}");
            }
        } else {
    
    
            self::setSessionTimeZone();
        }
    }
}

php转换时区

/**
 * Created by PhpStorm.
 * User: 骆同超
 * Date: 2020/12/15 10:17
 * Mail:[email protected]
 */
class DateTools
{
    
    
    //根据配置的 +08:00 配置返回 php 设置时区需要的参数
    public static function getPhpFormatTimeZone($simple_time_zone)
    {
    
    
        $php_time_zone = explode(':', $simple_time_zone)[0];
        if (strpos($php_time_zone, '-') !== false) {
    
    
            $diff = intval(trim($php_time_zone, '-'));
            return "Etc/GMT+{
      
      $diff}";
        } else {
    
    
            $diff = intval(trim($php_time_zone, '+'));
            return "Etc/GMT-{
      
      $diff}";
        }
    }

    /**
     * 把日期从指定时区转到指定时区,
     * @param $src string 源时区
     * @param $target string 目标时区
     * @param $datetime array 需要转换的字段,同一张表可能多个字段都需要转时区,如['open_time'=>'具体时间','create_time'=>'具体时间']
     * @param string $format string 日期格式
     * @return array
     */
    public static function convertTimeZone($src, $target, $datetime, $format = 'Y-m-d H:i:s')
    {
    
    
        //目标时区和源时区一样,转个格式直接返回
        if ($src == $target) {
    
    
            $news = [];
            foreach ($datetime as $k => $v) {
    
    
                if (!empty($v)) {
    
    
                    $news[$k] = date($format, strtotime($v));
                } else {
    
    
                    $news[$k] = $v;
                }
            }
            return [
                'data' => $news,
                'today_datetime' => date('Y-m-d H:i:s'),
            ];

        }
        //当前php时区默认和源时区不一样才重置时区少一步操作
        if (date_default_timezone_get() != $src) {
    
    
            date_default_timezone_set($src);
        }

        //全部转为当前时区的时间戳,避免转为最小时间戳,导致转为日期是出现 1970-01-01这种
        $src_time_stamp = [];
        foreach ($datetime as $kk => $vv) {
    
    
            if(!empty($vv)){
    
    
                $src_time_stamp[$kk] = strtotime($vv);
            }else{
    
    
                $src_time_stamp[$kk] = $vv;
            }
        }

        //转为目标时区
        date_default_timezone_set($target);
        $target_time_arr = [];
        foreach ($src_time_stamp as $kkk => $vvv) {
    
    
            if(!empty($vvv)){
    
    
                $target_time_arr[$kkk] = date($format, $vvv);
            }else{
    
    
                $target_time_arr[$kkk] = $vvv;
            }
        }
        $today_datetime = date('Y-m-d H:i:s');//获取当前时区下
        date_default_timezone_set($src);//重置为源时区
        return [
            'data' => $target_time_arr,
            'today_datetime' => $today_datetime,
        ];
    }


    /**
     * 处理时间问题,重置指定时区的若干字段。
     * @param $list array 查询出来的结果集
     * @param $gid int|string game_id
     * @param array $key array 需要转的具体的字段
     * @return mixed
     * @throws \Exception
     */
    public static function dealTime($list, $gid, $key = ['datetime'])
    {
    
    
        $org_time_zone = DateHelper::getPHPDefaultZone();
        $src_time_zone = $org_time_zone['time_zone'];//当前时区,/Etc/GMT-8 格式
        $src_simple_time_zone = $org_time_zone['simple_zone'];//当前时区+08:00
        foreach ($list as &$item) {
    
    
            //根据sid获取server_type
            $server_type = DateHelper:: getServerTypeBySid($gid, $item['sid']);
            //根据server_type获取时区
            $dst_simple_time_zone = DateHelper::getTimeZoneByServerType($server_type,$gid);
            //两个时区不同才转换
            if (trim($src_simple_time_zone) != trim($dst_simple_time_zone)) {
    
    
                //根据配置时区转换为php格式时区
                $dst_time_zone = self::getPhpFormatTimeZone($dst_simple_time_zone);
                //设置目标时区
                $data = self::convertTimeZone($src_time_zone, $dst_time_zone, self::getChangeCondition($key, $item));
                foreach ($key as $v) {
    
    
                    $item[$v] = $data['data'][$v];
                }
            }
            $item['mergeSid'] = $item['sid'];
        }
        unset($item);
        return $list;
    }


    /**
     * 生成循环的条件
     * @param array $change_field_arr
     * @param $item
     * @return array
     */
    public static function getChangeCondition($change_field_arr = ['datetime'], $item)
    {
    
    
        $news = [];
        foreach ($change_field_arr as $k => $v) {
    
    
            $news[$v] = $item[$v];
        }
        return $news;
    }


    /**
     * 根据传过来的和0时区相差的秒数转换为时区格式
     * @param $second
     * @return string
     */
    public static function getTimeZoneBySecond($second)
    {
    
    
        $diff = floor($second / 3600);//计算相差的小时数
        $op = $diff > 0 ? '+' : '-';
        return $op . str_repeat(0, 2 - strlen(abs($diff))) . abs($diff) . ':00';
    }


}

猜你喜欢

转载自blog.csdn.net/abc8125/article/details/112253714
今日推荐