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 
目录
相关文章
|
JSON API 数据格式
LangChain Agent:赋予 LLM 行动力的神秘力量
LangChain Agent 是什么?有什么用?基本原理是什么?那么多 Agent 类型在实际开发中又该如何选择?
1318 8
LangChain Agent:赋予 LLM 行动力的神秘力量
|
XML Java 测试技术
springboot 整合 apache camel实现企业级数据集成和处理
springboot 整合 apache camel实现企业级数据集成和处理
5402 0
springboot 整合 apache camel实现企业级数据集成和处理
|
传感器 人工智能 自然语言处理
火热邀测!DataWorks数据集成支持大模型AI处理
阿里云DataWorks数据集成新增大模型AI处理功能,支持在数据同步中无缝调用通义千问等AI模型,实现文本翻译、情感分析、摘要生成等功能。适用于电商客服、智能汽车、供应链、医疗、金融、法律及教育等多个场景,大幅提升数据处理效率与洞察深度。用户可通过自然语言配置,快速完成高级数据分析与处理,无需额外部署调试。立即申请测试资格,体验智能化数据处理!
1730 4
火热邀测!DataWorks数据集成支持大模型AI处理
|
9月前
|
数据采集 人工智能 自然语言处理
121_训练评估:困惑度分析 - 分析指标与下游任务关系
在大规模语言模型(LLM)的训练过程中,评估模型性能是一个至关重要但常被简化处理的环节。2025年的研究表明,仅依赖单一指标(如困惑度)来判断模型质量已经无法满足复杂应用场景的需求。困惑度作为语言模型训练中最核心的评估指标,其与下游任务表现之间的关系远比直觉更复杂。本文将深入剖析困惑度的数学原理、计算方法、优化策略,以及其与各类下游任务表现的相关性分析,为大规模语言模型的训练优化提供全面的技术指导。
891 1
|
11月前
|
监控 NoSQL 关系型数据库
保障Redis与MySQL数据一致性的强化方案
在设计时,需要充分考虑到业务场景和系统复杂度,避免为了追求一致性而过度牺牲系统性能。保持简洁但有效的策略往往比采取过于复杂的方案更加实际。同时,各种方案都需要在实际业务场景中经过慎重评估和充分测试才可以投入生产环境。
534 0
|
存储 Unix Linux
手写操作系统(4)——计算机是如何启动的?BIOS、GRUB、文件系统......
手写操作系统(4)——计算机是如何启动的?BIOS、GRUB、文件系统......
680 1
|
边缘计算 Prometheus 监控
边缘GPU节点的可观测原理和最佳实践
介绍ACK Edge接入的边缘GPU节点的可观测原理和最佳实践。
|
存储 分布式计算 OLAP
Apache Paimon统一大数据湖存储底座
Apache Paimon,始于Flink Table Store,发展为独立的Apache顶级项目,专注流式数据湖存储。它提供统一存储底座,支持流、批、OLAP,优化了CDC入湖、流式链路构建和极速OLAP查询。Paimon社区快速增长,集成Flink、Spark等计算引擎,阿里巴巴在内部广泛应用,旨在打造统一湖存储,打通Serverless Flink、MaxCompute等,欢迎大家扫码参与体验阿里云上的 Flink+Paimon 的流批一体服务。
21619 8
Apache Paimon统一大数据湖存储底座
|
Shell
常用 adb 命令
常用 adb 命令
1175 2
|
消息中间件 存储 资源调度
实时计算 Flink版产品使用问题之在消费Kafka的Avro消息,如何配置FlinkKafka消费者的相关参数
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。