记Date类型的一次踩坑

一. 背景


1. 线上问题

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


2. 排查过程

  1. http抓数据包: activityEndTime为1654761599000, 转化为Date为2022-06-09 15:59:59

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

  1. 查询DB数据: 活动结束时间是分为两个字段储存的:

    endDate (dateTime类型): 2022-06-09

    endTime(time类型): 23:59:59

java类型都是用Date接收的. 暂时也没发现问题, 难道是time类型转Date类型导致的?

  1. 查看缓存: endDate=1654704000000, endDate=57599000

换算了下没问题, 说明数据从Db -> java中Date类型 -> Json序列化, 链路是没问题的

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

  1. 查看拼接逻辑: 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时区


3. 问题的原因

那这个地方为什么会少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);

会发现date4少了8小时(多偏移了1次), date5少了16小时(多偏移了两次)

解决方法: 不要用时间戳相+的方式, 可以借助于dateFormat, 或者转化为Calendar后偏移时间

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


  • 下面是所有时区的简写, toString方法时候对应Thu Jan 01 23:59:59 CST中的CST
标准时间代码与GMT的偏移量描述
NZDT+13:00新西兰夏令时
IDLE+12:00国际日期变更线,东边
NZST+12:00新西兰标准时间
NZT+12:00新西兰时间
AESST+11:00澳大利亚东部夏时制
CST(ACSST)+10:30中澳大利亚标准时间
CADT+10:30中澳大利亚夏时制
SADT+10:30南澳大利亚夏时制
EST(EAST)+10:00东澳大利亚标准时间
GST+10:00关岛标准时间
LIGT+10:00澳大利亚墨尔本时间
CAST+9:30中澳大利亚标准时间
SAT(SAST)+9:30南澳大利亚标准时间
WDT(AWSST)+9:00澳大利亚西部标准夏令时
JST+9:00日本标准时间,(USSR Zone 8)
KST+9:00韩国标准时间
MT+8:30毛里求斯时间
WST(AWST)+8:00澳大利亚西部标准时间
CCT+8:00中国沿海时间(北京时间)
JT+7:30爪哇时间
IT+3:30伊朗时间
BT+3:00巴格达时间
EETDST+3:00东欧夏时制
CETDST+2:00中欧夏时制
EET+2:00东欧,(USSR Zone 1)
FWT+2:00法国冬时制
IST+2:00以色列标准时间
MEST+2:00中欧夏时制
METDST+2:00中欧白昼时间
SST+2:00瑞典夏时制
BST+1:00英国夏时制
CET+1:00中欧时间
DNT+1:00Dansk Normal Tid
FST+1:00法国夏时制
MET+1:00中欧时间
MEWT+1:00中欧冬时制
MEZ+1:00中欧时区
NOR+1:00挪威标准时间
SET+1:00Seychelles Time
SWT+1:00瑞典冬时制
WETDST+1:00西欧光照利用时间(夏时制)
GMT0:00格林威治标准时间
WET0:00西欧
WAT-1:00西非时间
NDT-2:30纽芬兰(新大陆)白昼时间
ADT-03:00大西洋白昼时间
NFT-3:30纽芬兰(新大陆)标准时间
NST-3:30纽芬兰(新大陆)标准时间
AST-4:00大西洋标准时间(加拿大)
EDT-4:00(美国)东部夏令时
CDT-5:00(美国)中部夏令时
EST-5:00(美国)东部标准时间
CST-6:00(美国)中部标准时间
MDT-6:00(美国)山地夏令时
MST-7:00(美国)山地标准时间
PDT-7:00(美国)太平洋夏令时
PST-8:00(美国)太平洋标准时间
YDT-8:00Yukon夏令时
HDT-9:00夏威仪/阿拉斯加白昼时间
YST-9:00Yukon标准时
AHST-10:00夏威仪-阿拉斯加标准时间
CAT-10:00中阿拉斯加时间
NT-11:00州时间(Nome Time)
IDLW-12:00国际日期变更线,西边
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值