Phoenix关于时区的处理方式说明

简介: 开源版Phoenix对于时区的处理比较混乱,容易造成用户误解、误用。本文梳理了开源Phoenix对于时区的处理逻辑,以及介绍了阿里云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内部关于时区的实现逻辑,进一步理解文档中关于时区的表述。java.sql.Timestamp类型是带时区的,默认是本地时区,且不能通过函数参数设置。Phoenix在做String和Timestamp转换时使用的是GMT时区,也可以认为不带时区。比如对于"1970-01-01 08:00:00.000",Phoenix存储的数值是28800000,而Timestamp.valueOf("1970-01-01 08:00:00.000").getTime()得到的数值则是0,两者混用就会出现偏差。这个逻辑也是造成程序测试结果的根本原因。

此外,上面提到的是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的方式,会导致脏数据,并且是没有办法区分的。

不要混用两种方式!字符串拼SQL和对象设置PreparedStatement,只选一种,不管是读还是写。

二、阿里云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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
机器学习/深度学习 监控 Web App开发
SLS机器学习最佳实战:根因分析(一)
通过算法,快速定位到某个宏观异常在微观粒度的具体表现形式,能够更好的帮助运营同学和运维同学分析大量异常,降低问题定位的时间。
13549 0
|
SQL Java 数据库连接
Phoenix客户端进化之由重到轻
Phoenix重客户端 Phoenix是HBase之上的SQL层,它为HBase赋予了NEWSQL的特性,支持了大多数的标准SQL特性,并提供了JDBC的访问接口,使得我们在应用程序中能够方便的集成使用。
7861 2
|
存储 监控 分布式数据库
HBase在新能源汽车监控系统中的应用
重庆博尼施科技有限公司是一家商用车全周期方案服务商,利用车联网、云计算、移动互联网技术,在物流领域 为商用车的生产、销售、使用、售后、回收各个环节提供一站式解决方案,其中的新能源车辆监控系统就是由该公司提供的,本文是阿里云客户重庆博尼施科技有限公司介绍如何使用阿里云 HBase 来实现新能源车辆监控系统。
7298 0
|
Java AndFix Android开发
Android热修复升级探索——追寻极致的代码热替换
阿里云移动热修复Sophix技术实现 。手机淘宝开发团队对代码的native替换原理重新进行了深入思考,从克服其限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复。
26813 1
|
SQL 分布式数据库 索引
Phoenix入门到精通
此Phoenix系列文章将会从Phoenix的语法和功能特性、相关工具、实践经验以及应用案例多方面从浅入深的阐述。希望对Phoenix入门、在做架构设计和技术选型的同学能有一些帮助。
33673 0
|
存储 域名解析 缓存
HTTPDNS域名解析场景下如何使用Cookie?
1. Cookie 由于HTTP协议是无状态的,为了维护服务端和客户端的会话状态,客户端可存储服务端返回的Cookie,之后请求中可携带Cookie标识状态。 客户端根据服务端返回的携带Set-Cookie的HTTP Header来创建一个Cookie,Set-Cookie为字符串,主要字段如下
11099 4
|
存储 编解码 UED
用更少的钱看更清晰的视频——详谈阿里云窄带高清
在云栖社区在线技术培训上,阿里云高级视频专家江文斐为大家详细讲述了阿里窄带高产品的工作原理和使用用场景。通过使用窄带高清,能够让客户在成本和视觉体验上达到最佳平衡。
13082 0
|
JSON 分布式计算 MaxCompute
MaxCompute中使用OSS外部表读取JSON数据
本文介绍了MaxCompute中使用OSS外部表读取JSON文件的数据,以及需要设立的flag。
6073 0
MaxCompute中使用OSS外部表读取JSON数据
阿里云AI解决方案
阿里云AI整体解决方案概览
|
JSON 监控 数据格式
Logtail提升采集性能
为防止滥用消耗过多机器资源,我们对默认安装的Logtail进行了一系列的资源限制。默认安装的Logtail最多日志采集速度为20M/s,20个并发发送。如需提高采集性能,请参考本篇文章。
8850 0

热门文章

最新文章