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 
目录
相关文章
|
分布式计算 DataWorks API
一分钟“零代码”生成API,DataWorks数据服务快速上手指南
DataWorks数据服务提供了快速生成API的能力,可以一分钟“零代码”生成API,本文是一篇快速上手指南,帮助你快速认识数据服务。
12828 0
|
弹性计算 分布式计算 Hadoop
[大数据新手上路]“零基础”系列课程--如何将ECS上的Hadoop数据迁移到阿里云数加·MaxCompute
 想用阿里云数加·大数据计算服务(MaxCompute),但是现在数据还在hadoop上,怎么办?   别烦恼,跟着我们走,来一次MaxCompute零基础数据迁移之旅~Let’s Go!
19260 111
|
分布式计算 关系型数据库 大数据
[ETL实践指南]基于Kettle的MaxCompute插件实现数据上云
本文用到的 阿里云数加-大数据计算服务MaxCompute产品地址:https://www.aliyun.com/product/odps 简介 Kettle是一款开源的ETL工具,纯java实现,可以运行于Windows, Unix, Linux上运行,提供图形化的操作界面,可以通过拖拽控件的方式,方便地定义数据传输的拓扑。
21600 3
|
SQL Java 数据库连接
Phoenix客户端进化之由重到轻
Phoenix重客户端 Phoenix是HBase之上的SQL层,它为HBase赋予了NEWSQL的特性,支持了大多数的标准SQL特性,并提供了JDBC的访问接口,使得我们在应用程序中能够方便的集成使用。
7909 2
|
分布式计算 大数据 MaxCompute
【大数据技巧】MaxCompute中实现IP地址归属地转换
大数据平台的成熟使得更多种类的非结构化、半结构化的数据分析成为可能其中应用非常广泛的一种场景就是日志分析。在日志类型数据的清洗转换过程中把IP地址转换为归属地又是极为常见的一种场景。
12211 1
|
SQL 分布式数据库 索引
Phoenix入门到精通
此Phoenix系列文章将会从Phoenix的语法和功能特性、相关工具、实践经验以及应用案例多方面从浅入深的阐述。希望对Phoenix入门、在做架构设计和技术选型的同学能有一些帮助。
33802 0
|
运维 流计算
阿里云实时计算产品案例&解决方案汇总
阿里云实时计算产品案例&解决方案汇总页
32686 3
|
存储 域名解析 缓存
HTTPDNS域名解析场景下如何使用Cookie?
1. Cookie 由于HTTP协议是无状态的,为了维护服务端和客户端的会话状态,客户端可存储服务端返回的Cookie,之后请求中可携带Cookie标识状态。 客户端根据服务端返回的携带Set-Cookie的HTTP Header来创建一个Cookie,Set-Cookie为字符串,主要字段如下
11162 4
|
存储 物联网 数据库
TableStore发布多元索引功能,打造统一的在线数据平台
TableStore发布多元索引功能,提供多字段ad-hoc查询、模糊查询、全文检索、排序、范围查询、嵌套查询、空间查询等功能,打造统一的在线数据平台
8553 0
|
SQL 物联网 大数据
HBase进化之从NoSQL到NewSQL,凤凰涅槃成就Phoenix
数据爆炸使得传统单机数据库面临一系列挑战,HBase作为NoSQL数据库,解决了大规模数据实时读写的难题。Phoenix项目在HBase的基础上增加了SQL语言表达,算子原地执行,二级索引,加盐表等特性,进化成为了NewSQL数据库,更好用也更强大。
9996 0

热门文章

最新文章