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机器学习最佳实战:根因分析(一)
通过算法,快速定位到某个宏观异常在微观粒度的具体表现形式,能够更好的帮助运营同学和运维同学分析大量异常,降低问题定位的时间。
13565 0
|
SQL Java 数据库连接
Phoenix客户端进化之由重到轻
Phoenix重客户端 Phoenix是HBase之上的SQL层,它为HBase赋予了NEWSQL的特性,支持了大多数的标准SQL特性,并提供了JDBC的访问接口,使得我们在应用程序中能够方便的集成使用。
7872 2
|
存储 监控 分布式数据库
HBase在新能源汽车监控系统中的应用
重庆博尼施科技有限公司是一家商用车全周期方案服务商,利用车联网、云计算、移动互联网技术,在物流领域 为商用车的生产、销售、使用、售后、回收各个环节提供一站式解决方案,其中的新能源车辆监控系统就是由该公司提供的,本文是阿里云客户重庆博尼施科技有限公司介绍如何使用阿里云 HBase 来实现新能源车辆监控系统。
7313 0
|
SQL Java 分布式数据库
阿里云HBase SQL(Phoenix)服务深度解读
阿里云HBase SQL基于Phoenix 5.0版本,为云HBase2.0赋予NewSQL特性,降低kv接口使用复杂性,并提供Schema、Secondary Indexes、View 、Bulk Loading(离线大规模load数据)、Atomic Upsert、Salted Tables、Dynamic Columns、Skip Scan等特性的能力,大大降低了用户的使用门槛。
11758 0
阿里云HBase SQL(Phoenix)服务深度解读
|
分布式计算 大数据 MaxCompute
【大数据技巧】MaxCompute中实现IP地址归属地转换
大数据平台的成熟使得更多种类的非结构化、半结构化的数据分析成为可能其中应用非常广泛的一种场景就是日志分析。在日志类型数据的清洗转换过程中把IP地址转换为归属地又是极为常见的一种场景。
12176 1
|
Java AndFix Android开发
Android热修复升级探索——追寻极致的代码热替换
阿里云移动热修复Sophix技术实现 。手机淘宝开发团队对代码的native替换原理重新进行了深入思考,从克服其限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复。
26830 1
|
SQL 分布式数据库 索引
Phoenix入门到精通
此Phoenix系列文章将会从Phoenix的语法和功能特性、相关工具、实践经验以及应用案例多方面从浅入深的阐述。希望对Phoenix入门、在做架构设计和技术选型的同学能有一些帮助。
33715 0
|
SQL 存储 数据库
零距离接触阿里云时序时空数据库TSDB
最近,Amazon新推出了完全托管的时间序列数据库Timestream,可见,各大厂商对未来时间序列数据库的重视与日俱增。阿里云TSDB是阿里巴巴集团数据库事业部研发的一款高性能分布式时序时空数据库(面向智联网领域),在即将过去的2018年,我们对TSDB进行了多次的系统架构改进,引入了倒排索引、无限时间线支持、时序数据高压缩比算法、内存缓存、数据预处理、分布式并行聚合、GPU加速等多项核心技术,并且引入了新的计算引擎层和分布式SQL层,使得引擎核心能力有了质的提升,也基本上统一了集团内部的监控存储业务。
13245 0
|
存储 分布式计算 NoSQL
结构化大数据分析平台设计
前言  任何线上系统都离不开数据,有些数据是业务系统自身需要的,例如系统的账号,密码,页面展示的内容等。有些数据是业务系统或者用户实时产生的,例如业务系统的日志,用户浏览访问的记录,系统的购买订单,支付信息,会员的个人资料等。
17053 0
|
自然语言处理 分布式计算 Java
Schedulerx2.0支持多语言版本的分片模型
1. 简介 任务调度系统可以对多种任务进行调度(定时、编排、重刷历史数据等),有些任务调度系统还提供了分布式任务,帮助用户解决大数据处理的难题。分布式任务主要分为静态分片和动态分片。 1.1 静态分片 主要场景是处理固定的分片数,比如分库分表固定1024张表,需要若干台机器分布式去处理。
4243 0
Schedulerx2.0支持多语言版本的分片模型

热门文章

最新文章