一勞永逸,解決.NET釋出雲伺服器的時區問題

波多爾斯基發表於2022-01-27

國內大多數開發者使用的電腦,都是使用的北京時間,日常開發的過程中其實並沒有什麼不便;不過,等遇到了阿里雲等雲伺服器,系統預設使用的時間大多為UTC時間,這個時候,時區和時間的問題,就是不容忽視的大問題。

概念

首先明確一點,對於一個時刻,不管你用UTC時間還是UTC+8的時間來表示,本質上是一個時刻,就是一樣的。我們處理日期和時間的目標,也是為了保證這個時刻不會因為時區的不同出現對不上的情況。

DateTime與DateTimeOffset

.NET中表示時刻的資料型別有這兩個(新出的Date和Time不作討論),關於這兩個資料型別,已經有同學寫的很清楚了,阿里雲很多伺服器使用的時間為UTC時間,這個時候,如果使用DateTime,是很難說清楚時區(Kind只有UTC、Local還有未指定,不支援特定的某個時區),因此我們應當優先使用DateTimeOffset。

TimeZoneInfo

用於跨時區的情況下,時區的資訊是很重要的,.NET中使用TimeZoneInfo這個類表示時區的資訊。該類提供了一些靜態方法,可以用於查詢時區和建立時區等等。最早我是傾向於使用這些方法找到東八區的資訊的,但是我發現諸如ConvertTimeBySystemTimeZoneIdFindSystemTimeZoneById的方法,都依賴於系統中的定義,不同的系統可能還不一樣,自己定義是比較保險的,於是,我使用了CreateCustomTimeZone來新建一個時區。

Unix時間戳是比較於1970年的UTC標準時間,因此在處理的過程中,DateTime的時間表示應當將它轉換為UTC時間,以下的程式碼,是使用TimeZoneInfo實現時間轉換的,使用的是DateTime資料型別。如果改用DateTimeOffset,這個型別對轉換為Unix時間戳更加友好。

internal static class DateTimeExtension
{
    private static readonly TimeZoneInfo gmt8 = TimeZoneInfo.CreateCustomTimeZone("GMT+8", TimeSpan.FromHours(8), "China Standard Time", "(UTC+8)China Standard Time");
    
    public static long ToUnixTime(this DateTime datetime)
    {
        DateTime dateTimeUtc = datetime;
        if (datetime.Kind != DateTimeKind.Utc)
        {
            dateTimeUtc = datetime.ToUniversalTime();
        }

        if (dateTimeUtc.ToUniversalTime() <= DateTime.UnixEpoch)
        {
            return 0;
        }

        return (long)(dateTimeUtc - DateTime.UnixEpoch).TotalMilliseconds;
    }

    public static DateTime ToDateTime(this long unixTimestamp)
    {
        DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
        return TimeZoneInfo.ConvertTimeFromUtc(time, gmt8);
    }

    public static DateTime ToDateTime(this long unixTimestamp, int timezone)
    {
        DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
        return time.AddHours(timezone);
    }
}

其實,只要時區是正確的,那麼可以也可以使用網友提供的方法進行轉換。

// Code from https://stackoverflow.com/questions/5615538/parse-a-date-string-into-a-certain-timezone-supporting-daylight-saving-time
public DateTimeOffset ParseDateExactForTimeZone(string dateTime, TimeZoneInfo timezone)
{
    var parsedDateLocal = DateTimeOffset.ParseExact(dateTime, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
    var tzOffset = timezone.GetUtcOffset(parsedDateLocal.DateTime);
    var parsedDateTimeZone = new DateTimeOffset(parsedDateLocal.DateTime, tzOffset);
    return parsedDateTimeZone;
}

實踐指南

處理日期與時間的過程中,如果加入了TimeZoneInfo的情況下會使得程式變得非常麻煩,特別是各種TimeZone的Id和名稱,不同系統也不統一的情況下,容易出現各種各樣的問題。我想的就是避免用它,說說我的處理原則吧。

  1. 日期時間不使用DateTime類,全部使用DateTimeOffset型別
  2. 系統的內部處理,全部使用UTC標準時間進行資料表示
  3. 對於字串的轉換為DataTimeOffset的情況,顯式指定時區的小時偏移量
  4. 直接使用時間的加減,避免使用時區的資訊轉換導致的程式碼複雜度增加
  5. 【可選】如果不用考慮2038年的情況下,可以考慮Unix時間戳簡化時間表示

直接貼上我現在使用的程式碼段,思路就是在強制給字串表示的時間,加上UTC標準時區資訊,然後再修正時差。

public static class DateTimeExtension
{
    public static long? ParseUnixTimeMillisecondsWithTimeZone(string datetimeString, string format = "yyyyMMddHHmmss", int timezoneOffset = 8)
    {
    	//注意這裡非常關鍵的引數DateTimeStyles.AssumeUniversal,就是設定資料都是UTC的,不管是不是,都強行指定為UTC,然後再按照時區的資訊調整為正確的時間。
    	//給定的資料是東八區時間,但是加上這個引數,實際上的時間就會提前了8個小時,因此需要在後面的資料中直接減去8個小時,如果是其他地區的時間,那麼也是一樣操作。
        if (!DateTimeOffset.TryParseExact(datetimeString, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset time)) return null;
        DateTimeOffset dateTimeUtcOffset = time.AddHours(-timezoneOffset);
        return dateTimeUtcOffset.ToUnixTimeMilliseconds();
    }

    public static DateTimeOffset ToDateTime(this long unixTimestamp) => DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp);
}

對於ASP.NET CORE,JSON.NET會自動處理符合ISO8601規範的日期格式,只要指定資料型別為DateTimeOffset,就能夠準確轉換了。

參考

相關文章