查看原文
其他

为啥活动提前结束了?记 Date 类型的一次踩坑!

程序猿DD 2022-10-16

线上问题

新功能的营销活动时间 23:59:59 才结束, 但是 16 点多发现页面显示"活动已结束"。

排查过程


HTTP 抓数据包

activityEndTime 为 1654761599000, 转化为 Date 为 2022-06-09 15:59:59

问题显而易见, 少了 8 个小时. 且不是前端的问题。

查询 DB 数据

活动结束时间是分为两个字段储存的:

endDate (dateTime类型): 2022-06-09endTime(time类型): 23:59:59
java 类型都是用 Date 接收的. 暂时也没发现问题, 难道是 time 类型转 Date 类型导致的?

查看缓存

endDate=1654704000000, endDate=57599000

换算了下没问题, 说明数据从 DB -> Java 中 Date 类型 -> JSON 序列化,链路是没问题的。

那只有一种可能:endDate 和 endTime 拼接成 activityEndTime 的时候出问题了

查看拼接逻辑

activityEndTime = endDate.getTime() + endTime.getTime();

这太简单了, 难道会有问题?Debug 跟了一下, 果然结果有问题!

带着疑问点开了 Date.getTime() 方法:

/** * Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT * represented by this <tt>Date</tt> object. * * @return the number of milliseconds since January 1, 1970, 00:00:00 GMT * represented by this date. */public long getTime() { return getTimeImpl();}
也就是说 getTime() 返回的时间戳是相对于 0 时区的 January 1, 1970, 00:00:00 而言。
此时对于东 8 区而言, 是

January 1, 1970, 08:00:00

验证了下: 

57599000 / 3600000 = 15:59:59


new Date(57599000L).toString()

结果是:

Thu Jan 01 23:59:59 CST 1970
总结一句话:Date 是带时区的(跟着当前坐标的时区走),long 类型的时间戳相对于 0 时区

问题的原因


那这个地方为什么会少 8 小时呢?

因为案例里的 2022-06-09 和 23:59:59, 都是当前时区的时间, 即东 8 区的时间

activityEndTime = endDate.getTime() + endTime.getTime()

调用 getTime() 会换算成 0 时区的时间戳时, 会向左偏移 8 小时(即减少),前端拿到相加的 activityEndTime 会向右偏移转化为东 8 区的 Date 类型。

但前面向左偏移了两次,后面只向右偏移了一次,所以不对等。导致多偏移了一次减少了 8 小时。

可以运行以下 Demo:

Date date1 = new Date(0L); // 0时区的0点,东8区的8点Date date2 = new Date(3600*1000L); //0时区的1点,东8区的9点Date date3 = new Date(7200L*1000); //0时区的3点,东8区的11点
Date date4 = new Date(date1.getTime() + date2.getTime());Date date5 = new Date(date1.getTime() + date2.getTime() + date3.getTime());System.out.println(date4);System.out.println(date5);


4. Date 的时区确定


那 Date 是怎么确定当前时区的呢? 带着疑问看了下 toString 方法:

public String toString() { // "EEE MMM dd HH:mm:ss zzz yyyy"; BaseCalendar.Date date = normalize(); //将时间戳换算成当前时区的时间 StringBuilder sb = new StringBuilder(28); int index = date.getDayOfWeek(); if (index == BaseCalendar.SUNDAY) { index = 8; } convertToAbbr(sb, wtb[index]).append(' '); // EEE convertToAbbr(sb, wtb[date.getMonth() - 1 + 2 + 7]).append(' '); // MMM CalendarUtils.sprintf0d(sb, date.getDayOfMonth(), 2).append(' '); // dd
CalendarUtils.sprintf0d(sb, date.getHours(), 2).append(':'); // HH CalendarUtils.sprintf0d(sb, date.getMinutes(), 2).append(':'); // mm CalendarUtils.sprintf0d(sb, date.getSeconds(), 2).append(' '); // ss TimeZone zi = date.getZone(); if (zi != null) { sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)); // zzz } else { sb.append("GMT"); } sb.append(' ').append(date.getYear()); // yyyy return sb.toString();}
private final BaseCalendar.Date normalize() { if (cdate == null) { BaseCalendar cal = getCalendarSystem(fastTime); cdate = (BaseCalendar.Date) cal.getCalendarDate(fastTime, TimeZone.getDefaultRef()); return cdate; } // Normalize cdate with the TimeZone in cdate first. This is // required for the compatible behavior. if (!cdate.isNormalized()) { cdate = normalize(cdate); }
// If the default TimeZone has changed, then recalculate the // fields with the new TimeZone. TimeZone tz = TimeZone.getDefaultRef(); if (tz != cdate.getZone()) { cdate.setZone(tz); CalendarSystem cal = getCalendarSystem(cdate); cal.getCalendarDate(fastTime, cdate); } return cdate;}
static TimeZone getDefaultRef() { TimeZone defaultZone = defaultTimeZone; if (defaultZone == null) { // Need to initialize the default time zone. defaultZone = setDefaultZone(); assert defaultZone != null; } // Don't clone here. return defaultZone;}
private static synchronized TimeZone setDefaultZone() { TimeZone tz; // get the time zone ID from the system properties String zoneID = AccessController.doPrivileged( new GetPropertyAction("user.timezone"));
// if the time zone ID is not set (yet), perform the // platform to Java time zone ID mapping. if (zoneID == null || zoneID.isEmpty()) { String javaHome = AccessController.doPrivileged( new GetPropertyAction("java.home")); try { zoneID = getSystemTimeZoneID(javaHome); if (zoneID == null) { zoneID = GMT_ID; } } catch (NullPointerException e) { zoneID = GMT_ID; } }
// Get the time zone for zoneID. But not fall back to // "GMT" here. tz = getTimeZone(zoneID, false);
if (tz == null) { // If the given zone ID is unknown in Java, try to // get the GMT-offset-based time zone ID, // a.k.a. custom time zone ID (e.g., "GMT-08:00"). String gmtOffsetID = getSystemGMTOffsetID(); if (gmtOffsetID != null) { zoneID = gmtOffsetID; } tz = getTimeZone(zoneID, true); } assert tz != null;
final String id = zoneID; AccessController.doPrivileged(new PrivilegedAction < Void > () { @Override public Void run() { System.setProperty("user.timezone", id); return null; } });
defaultTimeZone = tz; return tz;}
private static native String getSystemTimeZoneID(String javaHome);

总结下:就是先根据系统变量 user.timezone 获取,若未设置最后调用到本地方法根据 javaHome 获取。

一般当前时区配置在 /etc/localtime 里, 多有的地区对应的时区库在 /var/db/timezone/zoneinfo。

转自:2021不再有雨

链接:blog.csdn.net/w727655308/article/details/125211726


------
我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取

推荐阅读

··································

你好,我是程序猿DD,10年开发老司机、阿里云MVP、腾讯云TVP、出过书创过业、国企4年互联网6年。从普通开发到架构师、再到合伙人。一路过来,给我最深的感受就是一定要不断学习并关注前沿。只要你能坚持下来,多思考、少抱怨、勤动手,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你看好一个事情,一定是坚持了才能看到希望,而不是看到希望才去坚持。相信我,只要坚持下来,你一定比现在更好!如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯,帮你积累弯道超车的资本。

点击领取2022最新10000T学习资料

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存