Phoenix關於時區的處理方式說明

一、社區版Phoenix時間相關類型介紹

時間數據處理是數據開發者經常遇到的問題,衆所周知時間都是跟時區相關的,如果對於時區處理不當,會造成時間數據錯誤,進而引入一系列棘手的問題。Phoenix中跟時間相關的類型有TIMESTAMP,DATE和TIME,這些類型對於時區的處理邏輯是相同的,後面筆者就以TIMESTAMP類型爲例來說明Phoenix關於時區的處理方式。首先,我們先來看下Phoenix文檔中對於TIMESTAMP類型的描述:

The timestamp data type. The format is yyyy-MM-dd hh:mm:ss[.nnnnnnnnn]. Mapped to java.sql.Timestamp with an internal representation of the number of nanos from the epoch. The binary representation is 12 bytes: an 8 byte long for the epoch time plus a 4 byte integer for the nanos. Note that the internal representation is based on a number of milliseconds since the epoch (which is based on a time in GMT), while java.sql.Timestamp will format timestamps based on the client's local time zone.

這段描述中明確指出TIMESTAMP類型在處理時是基於GMT時區的毫秒值(默認的基準都是"1970-01-01 00:00:00.000"),而java.sql.Timestamp使用的是客戶端的本地時區。下面我們通過一個例子來說明這個設定在實際使用中,容易遇到的問題。

Statement stmt = con.createStatement();
stmt.execute("drop table test");
stmt.execute("create table test(mykey integer primary key, mytime timestamp)");
stmt.execute("upsert into test values(1, '2018-11-11 10:00:00.000')");
PreparedStatement pstmt = con.prepareStatement("upsert into test values(?, ?)");
pstmt.setInt(1, 2);
pstmt.setTimestamp(2, Timestamp.valueOf("2018-11-11 10:00:00.000"));
pstmt.executeUpdate();
con.commit();
stmt.execute("select * from test");
ResultSet rs = stmt.getResultSet();
System.out.println("select without filter results:");
while (rs.next()) {
    System.out.println(rs.getInt(1) + " : " + rs.getString(2) + " : " + rs.getTimestamp(2));
}
stmt.execute("select * from test where mytime = timestamp'2018-11-11 10:00:00.000'");
rs = stmt.getResultSet();
System.out.println("select with statement:");
while (rs.next()) {
    System.out.println(rs.getInt(1) + " : " + rs.getString(2) + " : " + rs.getTimestamp(2));
}
pstmt = con.prepareStatement("select * from test where mytime = ?");
pstmt.setTimestamp(1, Timestamp.valueOf("2018-11-11 10:00:00.000"));
pstmt.execute();
rs = pstmt.getResultSet();
System.out.println("select with preparedStatement:");
while (rs.next()) {
    System.out.println(rs.getInt(1) + " : " + rs.getString(2) + " : " + rs.getTimestamp(2));
}

結果輸出如下:

select without filter results:
1 : 2018-11-11 10:00:00.000 : 2018-11-11 18:00:00.0
2 : 2018-11-11 02:00:00.000 : 2018-11-11 10:00:00.0
select with statement:
1 : 2018-11-11 10:00:00.000 : 2018-11-11 18:00:00.0
select with preparedStatement:
2 : 2018-11-11 02:00:00.000 : 2018-11-11 10:00:00.0

我們可以發現以下規律:

  1. 用string寫入用getTimestamp讀取時時間戳多了8個小時;而用setTimestamp寫入,用getString讀出時間戳則少了8個小時。
  2. 當查詢時,按照字符串的方式拼where條件只能匹配到使用string寫入的數據,而用setTimestamp設置where條件中的字段只能匹配到用setTimestamp方式寫入的時間戳。

需要指出的是,當我們使用客戶端也就是sqlline.py時,只能是用字符串寫入,然後字符串讀出。用戶經常遇到的使用場景是,在線系統用 setTimestamp寫入,然後會用sqlline.py做查詢,或者用getString在頁面展示,這個時候就會出現多8個小時的情況;而做條件過濾時,用戶一定要注意使用方式,否則會出現匹配不到的情況,而當使用sqlline查詢時,必須使用convert_tz方法做時區轉換才能得到正確結果。

此外,上面提到的是Phoenix重客戶端的邏輯,而Phoenix輕客戶端對於時區的處理跟Phoenix重客戶端也有不一樣的地方。我們使用前面完全相同的邏輯,在實現中把jdbc url串換成輕客戶端的格式,打印結果如下:

select without filter results:
1 : 2018-11-11 10:00:00 : 2018-11-11 10:00:00.0
2 : 2018-11-11 02:00:00 : 2018-11-11 02:00:00.0
select with statement:
1 : 2018-11-11 10:00:00 : 2018-11-11 10:00:00.0
select with preparedStatement:
2 : 2018-11-11 02:00:00 : 2018-11-11 02:00:00.0

我們可以發現以下規律:

  1. 打印的時候輕客戶端的getString和getTimestamp的結果是一樣的,且和重客戶端的getString保持一致。
  2. 寫入和查詢的時候輕客戶端和重客戶端邏輯一樣。

這是由於社區版輕客戶端在實現getTimestamp的時候,在構造Timestamp對象之前先把得到的毫秒數值減去了時區,而其他操作都是直接透傳給重客戶端實現的。

通過以上描述,我們可以發現Phoenix對於時區的處理非常複雜,稍不留意就會出錯。更嚴重的,如果用戶在寫入的時候混用了拼SQL和setTimestamp的方式,會導致髒數據,並且是沒有辦法區分的。

二、阿里雲Phoenix對時區問題的解決

首先,我們先看下傳統開源數據庫中對於時區問題處理方法。

在ANSI SQL標準中,TIMESTAMP類型分兩種,分別是TIMESTAMP WITH TIMEZONE和TIMESTAMP,前一種是考慮時區的,後一種是不考慮時區的。在MYSQL中TIMESTAMP類型是默認帶時區的,用戶輸入的如果不指定時區,默認是本地時區,在實際存儲時會轉變爲GMT時區,當用戶讀取時再轉化爲本地時區;而不帶時區的類型在MYSQL中是DATETIME類型,用戶在調用getTimestamp接口時,會根據DATETIME的年月日時分秒構造出來Timestamp對象,這樣用戶通過getString和getTimestamp拿到的時間始終是一致的。

PostgresSQL對於時區的處理跟MYSQL不同,PG的TIMESTAMP類型是不帶時區的,而TIMESTAMPTZ是帶時區的。處理的邏輯同MYSQL類似,只是內部存儲和實現上會有不同,這裏不再贅述。文末附有MYSQL和PG對於時區的參考文檔,感興趣的讀者可以進一步研究。有一點相同的是,不管MYSQL和PG怎麼實現和表述,在用戶使用的過程中都不會像開源Phoenix那麼讓人困惑。

阿里雲團隊在Phoenix 5.x版本中對時區問題進行了統一解決,不管用戶使用輕客戶端和重客戶端,都不會再像以前那麼費解。實現邏輯跟MYSQL類似,也就是,TIMESTAMP類型在實際存儲時都是使用GMT時區,用戶使用客戶端讀寫時,會根據本地時區進行轉化。不管用戶使用輕客戶端還是重客戶端,在寫入時使用statement還是PreparedStatement,在讀取時使用getString還是getTimestamp,在查詢時使用拼字符串還是setTimestamp等,拿到的結果都是一致,容易理解且符合預期的。

我們同樣使用前文提到的測試程序,把Phoenix版本改成阿里雲版本的Phoenix 5.x,得到的結果如下:

select without filter results:
1 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0
2 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0
select with statement:
1 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0
2 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0
select with preparedStatement:
1 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0
2 : 2018-11-11 10:00:00.000 : 2018-11-11 10:00:00.0

三、參考文獻

http://phoenix.apache.org/language/datatypes.html#timestamp_type

https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html

https://www.postgresql.org/docs/current/datatype-datetime.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章