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

线上问题

新功能的营销活动时间 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学习资料

原文地址:https://cloud.tencent.com/developer/article/2117799

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340