Keep it simple.

本文介绍了日期时间类型如何在代码中进行计算,以及在数据库中如何保存的问题。尤其是在跨时区的应用中,需要注意的事项。

java.util.Date对象提供了before()和after()方法,可以很方便的比较两个Date对象的大小。如下代码所示:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date1 = sdf.parse("1970-01-01 00:00:00");
Date date2 = sdf.parse("1970-01-01 00:00:01");
System.out.println(date1.before(date2));

Date对象的内部是用long型表示的时间戳,单位是毫秒,其基准时间为1970-01-01 00:00:00.000。Date对象的getTime()方法可以直接获取到该时间对应的毫秒数。需要注意的是,getTime()方法返回的毫秒数是以UTC 0时区为基准的,也就是UTC标准时间。在上面的代码中,我们使用了SimpleDateFormat对象的parse()方法,将字符串转化成了Date对象。而parse()方法是会考虑你当前系统时区的。由于我的系统时区为东8区,即UTC+8,那么1970-01-01 00:00:00对应的UTC标准时间其实是1969-12-31 16:00:00,getTime()方法获取的时间戳是-28800000,而不是0。也就是说,东8区的0时对应的是UTC标准时间的前一天的16时。

一般情况下,我们不太关心时区问题。主要是因为我们的服务器和我们主要服务的用户都在中国,而中国统一使用东8区区时,因此时区的差异可以忽略。但如果业务扩展到全球,就一定要考虑时区问题了。比如某外国用户提交到服务器的参数中有一个字符串15:00:00,此时就要考虑该用户所属的时区:
当服务器时区与用户的时区一致时,比如都在中国,则该字符串表示15时整;
若用户在东9区的日本,显然用户的意思是日本的15时整,对于东8区的中国来说,对应的其实是14时整。

因此,为了降低时区差异而造成的复杂度,服务器在保存时间时应该统一存储成UTC标准时间。如果用户提交的参数里有时间类型,那么最好也使用long型的UTC标准时间戳来传递,尽量避免使用字符串。如果必须要用字符串,那么就要知道用户当前的时区设置,此时可以额外设计一个时区参数,或者在用户会话中获取时区参数(前提是会话中已经保存了用户所属时区)。对于输出给用户的时间,在传输时也最使用UTC标准时间戳,然后由浏览器(或客户端)来格式化成符合其时区设置的时间字符串(格式化时不仅要考虑时区偏移量,有些国家可能还要考虑夏令时偏移量。另外还要考虑年/月/日的形式还是月/日/年的形式,分隔符用什么表示,是12小时形式还是24小时形式等等细节问题)。也许某天,我们的代码和数据有可能会迁移部署到其它国家,只有当代码和数据都是时区无关的,即都以UTC标准时间,迁移成本才能最低。

具体的,Java中的java.util.Date对象的getTime()返回的时间戳是时区无关的。MySQL数据库的timestamp类型也是时区无关的,它在保存时间时会自动转化为UTC标准时间,查询时会根据当前服务器的时区重新转化为该时区的时间,当你的系统时区变化时,输出的时间也会相应变化,这样跨时区迁库就变得容易了。另外MySQL提供datetime类型,其特点是存储的时间会被原样输出,即使时区变化了也不会改变,此时跨时区迁库就要注意,因为datetime里存储的时间已经和新服务器的时区不匹配了,必须要谨慎处理。PostgreSQL数据库的timestamp类型也是时区无关的,跟MySQL一样。另外PostgreSQL提供有timestamp with time zone类型,官方并不推荐使用。

下面是一个具体的实例:由于银行系统在每天北京时间22:30:00到23:30:00处于结算维护期,不能进行交易。我们依赖银行系统的业务需要进行判断,对于处于该时段的交易请求,一律返回错误提示。

面对上面的需求,如果不考虑时区的话,我们有简单的处理变法。将开始时间22:30:00表示成6位数字223000,结束时间23:30:00也表示成6位数字233000,然后将当前时间的时、分、秒也表示成6位数字,最后转化成了数字的大小比较,很容易实现。但如果要考虑时区呢?

定义2个常量,开始时间和结束时间的毫秒数(以当天0时整为基准):

private static final long TIME_DENY_START = (long) (3600000 * (22.5 - 8));
private static final long TIME_DENY_END = (long) (3600000 * (23.5 - 8));

上面的代码,1小时有3600000毫秒,22.5即22:30,23.5即23:30,后面减8表示将北京时间转化为UTC标准时间。

接着定义一个方法,该方法可以判断是否不在银行的维护时间内,即当前可以进行交易:

private boolean checkTimeAllowed() {
    long time = new Date().getTime() % 86400000;
    return !(TIME_DENY_START <= time && time <= TIME_DENY_END);
}

上面的代码,将当前UTC标准时间的时间戳 % 一天的总毫秒数(即86400000毫秒),可以直接获取到以当天0时整为基准的毫秒数,接着判断该毫秒数是否处于前面定义的开始和结束时间段内,最后取反返回,结束。

  • zhengqiang
  • 技术
  • 2017-06-25 05:37:24.0
  • 852