Skip to content

java8 time problems

landon edited this page Sep 14, 2018 · 1 revision

Java8时间API使用不当引起的问题

背景

  1. 2018.3.8 发现200服务器时间出错 显示的时间比正常时间小8个小时
  2. 发现后 直接利用date命令就时间修改为了正常时间
  3. 修改完毕后发现客户端时间显示不正确
  4. 2018.3.20 重启了200服务器 发现客户端时间显示正确 但是服务器时间却还是比正常时间小8个小时 不过这次仔细看了一下发现是UTC时间
  5. 另外出现玩家登录即重置次数的bug
  6. 2018.3.21 运维将UTC时区调整为CST时区 一些正常

概念

  1. UTC是世界协调时间 大家可以理解为是0时区
  2. 而我们的正常时间 即北京时间 其实是CST,即UTC + 8
  3. 即表示北京时间比UTC时间早8个小时
  4. 如北京时间 CST是 20:00,而UTC时间则是12:00
  5. 时区不同只表示两个地方的时间显示不同 但绝对时间戳是一样的 即System.currentTimeMillis在两个时区的值输出都是一样的

复盘1

  1. 200服务器不清楚什么原因被修改了UTC时间 即3.8号发现的
  2. 注意此时的UTC时间是正确的 只不过是UTC时区 如果此时切换为CST时区的话 时间就是正确的北京时间
  3. 但当时我发现的时候 没有仔细看 即没有看到UTC 只看到了时间比正常时间小于8个小时 所以我直接用date命令修改了正确的时间
  4. 但是这里注意 还是UTC时间
    • 如之前是UTC 07:00 此时北京时间应该是15:00 这个是正确的
    • 而我修改为了UTC 15:00 但此时对应的北京时间却应该是23:00
    • 所以我这里错误的相当于修改了服务器时间 相当于往后调了8个小时
  5. 所以出现了背景3提到的
  • 客户端显示不正确 因为客户端显示的时间是比正常时间 + 8小时
  • 而服务器时间显示''正确'' 这个只是看着正确 时区确实UTC
  1. 所以复盘1总结
    • 是我没有仔细看清楚到底是哪个时区 就随便调整了服务器时间

复盘2

  1. 2018.3.20号 200服务器重启了
  2. 因为date命令只是改了如终端时间 重启后时间会被重置
  3. 所以200服务器上输入date显示的时间是UTC时间
  4. 注意这个UTC时间是正确的 转为CST(+8 -> 北京时间后)就是正确的时间了
  5. 所以给客户端推送的系统时间是正确的
  6. 所以出现客户端时间正确 但是服务器却显示的是"不正确的"

复盘3

  1. 为什么在UTC时区的情况下 玩家登录都会被重置次数?

  2. 简单说一下重置次数原理

    • 玩家下线的时候会记一个resetTime
    • 这个resetTime是下一天的0点
    • 玩家上线的时候 会比较当前是否这个resetTime 如果大于就重置
  3. 200服务器当前是正确的UTC时间

  4. 玩家下线resetTime是用了

    LocalDateTime#toInstant(ZoneOffset.of("+8")).toEpochMilli()
    
    • 这个方法是比较ZoneOffset(比较当前时区和参数时区) 因为是当前UTC和参数ZoneOffset.of("+8")差8个小时
    • 所以要比正常的返回时间戳要小于8小时
    • 比如玩家2018.3.20 20点下线 那么这个方法返回的resetTime是2018.3.20 16点(24 - 8)
    • 玩家再次登录发现loginTime > resetTime则执行了重置
  5. 所以根本原因是获取resetTime的方法问题 手动指定了ZoneOffset为北京时区 应该用系统默认时区

  6. 那么为什么CST时区下(北京时区) 上面这点代码就工作正常呢

    • 因为当前时区就是北京时区 所以和参数ZoneOffset.of("+8")对比 发现不差
    • 所以返回的时间戳是正确的时间戳
  7. 所以那段代码在北京时区的服务器下是没有问题的 如果是其他时区的服务器下是一定出问题的

总结

  1. LocalDateTime如何返回正确的时间戳,我这边测试了两种方式,原理就是参数ZoneOffset一定要和本地时区一致

    long time5 = ldt.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli();
    
    ZoneId zoneId = ZoneId.systemDefault();
            ZoneOffset offset = zoneId.getRules().getOffset(ldt);
            long time4 = ldt.toInstant(offset).toEpochMilli();
    
  2. 时间相关代码一定不要使用和具体时区相关的代码 一定会出问题的

    • 我们的代码是一定要兼容不同时区服务器的
  3. 看服务器时间的时候一定要注意确认到底是UTC还是CST

    • 如果CST 那么就是北京时间很好确认
    • 如果是UTC 我们只需要确认比北京时间小8小时 就正确了

附参考代码

public class Java8TimeTest {

    @Test
    public void testNow() {
        // 绝对时间 和时区无关系
        long now1 = System.currentTimeMillis();
        long now2 = new Date().getTime();
        // 这个数值输出的long跟时区无关 但是LocalDateTime#toEpochMilli是和时区偏移有关系
        long now3 = Instant.now().toEpochMilli();

        System.out.println(now1);
        System.out.println(now2);
        System.out.println(now3);

        // 1521645418308
        // Wed Mar 21 15:16:58 UTC 2018
        // 1521645498317 Wed Mar 21 23:18:18 CST 2018
        // date显示和时区有关系
        System.out.println(new Date(now3));

        // 2018.3.22 10:49 cst 1521686999245 1521686999246 1521686999247
        // 2018.3.22 02:53 utc 1521687230061 1521687230062 1521687230062
        // 2018.3.22 cst 15:09:52 1521702592007 1521702592007 1521702592008

        // 总结A
        // 1. System.currentTimeMillis() | Instant.now().toEpochMilli()
        // 返回的都是当前系统时间的时间戳 是一个绝对时间
        // 2. 这个绝对时间和时区没有关系 即使切换时区如从cst(utc+8)切换到utc 这个绝对时间的输出也不会变
        // 3. 因为切换时区不同 所以世界现实的时间会不同

        // 总结B
        // issue中问题 最后一次是2018.3.20 200上显示的时间是utc 2:30 此时客户端时间10:30 这个解释因为utc
        // 2:30就是cst 10:30 所以从网络层给
        // 客户端发的绝对时间戳就是10:30 所以显示时间是正确的

        // 总结C
        // 第一次出时间的问题复盘 200上显示的时间就是utc时间是正确的如10点 而对应的cst时间是18点
        // 但是当我看到的时候没有看utc 直接看10点不对 所以直接通过date命令修改了时间手动加了8个小时 变为了utc 18点
        // 那么此时实际的时间cst是18+8 为第二天的2点 而客户端不显示天 只显示02
        // 而date修改过的时间再服务器重启以后又被重置为了utc正确的时间 即回到了总结B

        // 总结D
        // 为什么服务器变为utc时间之后 登录之后就会被重置次数
        // 1.服务器现在是正确的utc时间
        // 2.玩家下线resetTime是用了LocalDateTime#toInstant(ZoneOffset.of("+8")).toEpochMilli()
        // 3.这个方法是比较ZoneOffset 因为是utc和参数ZoneOffset.of("+8")差8个小时
        // 所以要比正常的返回时间戳要小于8小时
        // 4.比如玩家2018.3.20 20点下线 那么这个方法返回的resetTime是2018.3.20 16点(24 - 8)
        // 5.玩家再次登录发现loginTime > resetTime则执行了重置
        // 6.所以根本原因是获取resetTime的方法问题 指定了北京时区 应该用系统默认时区 即下面单元测试用到的两种方式
    }

    /*
     * 1521655200000
     * 
     * Thu Mar 22 02:00:00 CST 2018
     * 
     * 1521626400033
     * 
     * Wed Mar 21 18:00:00 CST 2018
     * 
     * 1521626400000
     * 
     * Wed Mar 21 18:00:00 CST 2018
     */
    @Test
    public void testLocalDateTime() {
        // 默认现在系统时区是cst

        // UTC
        // 我当前时区是cst(utc+8) 传入(ZoneOffset.UTC) 表示我当时时区比参数时区的offset 明显是大8个小时
        // 所以输出是+8个小时
        LocalDateTime ldt = LocalDateTime.of(2018, 3, 21, 18, 0);
        long time1 = ldt.toInstant(ZoneOffset.UTC).toEpochMilli();
        System.out.println(time1);
        Date date1 = new Date(time1);
        System.out.println(date1);

        // 默认cst
        Calendar calendar = Calendar.getInstance();
        calendar.set(2018, 2, 21, 18, 0, 0);
        long time2 = calendar.getTimeInMillis();
        System.out.println(time2);
        Date date2 = new Date(time2);
        System.out.println(date2);

        // +8 cst
        // 为什么默认没有cst没有问题 因为当前就是utc+8 没有offset 所以正确
        long time3 = ldt.toInstant(ZoneOffset.of("+8")).toEpochMilli();
        System.out.println(time3);
        Date date3 = new Date(time3);
        System.out.println(date3);

        // 正确方式 测试无问题 测试发现ldt.toInstant传入的offset必须要本地时区一致才可以
        ZoneId zoneId = ZoneId.systemDefault();
        ZoneOffset offset = zoneId.getRules().getOffset(ldt);
        long time4 = ldt.toInstant(offset).toEpochMilli();
        System.out.println(time4);
        Date date4 = new Date(time4);
        System.out.println(date4);

        // 尝试另外一种方式
        long time5 = ldt.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli();
        System.out.println(time5);
        Date date5 = new Date(time5);
        System.out.println(date5);
    }

    // 修改zone为utc(win7)
    /*
     * 1521655200000
     * 
     * Wed Mar 21 18:00:00 UTC 2018
     * 
     * 1521655200401
     * 
     * Wed Mar 21 18:00:00 UTC 2018
     * 
     * 1521626400000
     * 
     * Wed Mar 21 10:00:00 UTC 2018
     */
    @Test
    public void testChangeZone() {
        // win7直接修改时区为UTC(协调世界时) 此时显示的时间比北京时间晚8个小时

        LocalDateTime ldt = LocalDateTime.of(2018, 3, 21, 18, 0);
        long time1 = ldt.toInstant(ZoneOffset.UTC).toEpochMilli();
        System.out.println(time1);
        Date date1 = new Date(time1);
        System.out.println(date1);

        Calendar calendar = Calendar.getInstance();
        calendar.set(2018, 2, 21, 18, 0, 0);
        long time2 = calendar.getTimeInMillis();
        System.out.println(time2);
        Date date2 = new Date(time2);
        System.out.println(date2);

        // 我当前时区是utc 传入(ZoneOffset.of("+8")) 表示我当时时区比参数时区的offset 明显是小于8个小时
        // 所以输出是-8个小时
        long time3 = ldt.toInstant(ZoneOffset.of("+8")).toEpochMilli();
        System.out.println(time3);
        Date date3 = new Date(time3);
        System.out.println(date3);

        // 正确方式 测试无问题 测试发现ldt.toInstant传入的offset必须要本地时区一致才可以
        ZoneId zoneId = ZoneId.systemDefault();
        ZoneOffset offset = zoneId.getRules().getOffset(ldt);
        long time4 = ldt.toInstant(offset).toEpochMilli();
        System.out.println(time4);
        Date date4 = new Date(time4);
        System.out.println(date4);

        // 尝试另外一种方式
        long time5 = ldt.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli();
        System.out.println(time5);
        Date date5 = new Date(time5);
        System.out.println(date5);
    }
}
Clone this wiki locally