首页 > 解决方案 > 如何处理夏令时时钟回滚引起的时间模糊

问题描述

我有这种情况,我想看看是否有更好的解决方案:

我从系统 A 收到时间戳,格式为“yyyy-MM-dd-HH-mm-ss”,没有时区,但他们可以修复它为UTC。

然后我需要通过 SSH 连接到系统 B 以获得“上次同步时间”,这可能是过去的时间,并且它不返回任何时区,甚至不返回年份(来自备份产品),这是一个示例:

“同步时间:11 月 3 日星期日 01:13”

系统 B 是运行一些专有软件的备份设备,并且有一个内部调度程序,它每 15 分钟唤醒一次,以检查是否有任何数据需要从源同步。如果上一个同步仍在运行,它将让当前同步完成而不采取进一步的行动。此“最后同步时间”将在每次同步后更新,因此在最坏的情况下,它不应落后于当前系统时间超过几个小时。

我需要比较这两个时间戳以确保 B 上的“上次同步时间”晚于 A。如果不是,我需要等待并重试查询,直到 B 晚于 A 才能继续下一步。

我有一个位于 Linux 机器上的 java 程序,当请求来自 A 时,它会向 B 查询“上次同步时间”,然后立即向 B 发出“日期”命令以获取当前系统时间,然后返回当前系统时间,格式为“EEE MMM dd kk:mm:ss z yyyy”,例如

“Fri Jun 14 21:07:07 EDT 2019”,

因此程序可以应用年份和此输出的时区完成“上次同步时间”的时间戳。该程序确实处理了“最后同步时间”和“日期”输出跨越一年边界的情况。

它工作正常,除非夏季夏令时结束,并且时钟回滚(凌晨 2 点回到凌晨 1 点),有一个一小时的窗口时间会模棱两可:

例如,我从系统 A 获得凌晨 1:20 的时间图(已从 UTC 转换为本地),然后当我从系统 B 获得凌晨 1:30 时,我无法判断 B 是否晚于 A。它可能是 1时钟更改前:30。系统B的当前时区,并没有明确“上次同步时间”在哪个时区。

我们要求用户将所有系统切换为UTC时间,因此我们无需担心时钟调整。但这对环境的影响太大了。

一种选择是我专门处理一个小时,然后等到“最后同步时间”时间戳超过凌晨 2 点。为此,我需要在代码中检查这是否是每次进行时间戳比较时的特定时间,我认为这不是最佳的。但我想不出更好的方法。

非常感谢任何建议。如果这是要走的路,是否有一个好的 Java 库来计算夏令时切换日期?太感谢了!

标签: javadatetimedst

解决方案


我看不出你可以完全确定。“Sun Nov 3”可能在 2002 年、2013 年或 2019 年,甚至更远的历史。一种方法是,如果您可以假设上次同步时间不超过您获得的当前系统 B 系统时间的几年前(也不是那之后),如果您可以检测到,则报告错误不是在这几年内。

夏季时间(夏令时)结束时的诀窍是如果有任何歧义,总是假设较早的时间。这样,当您检测到同步时间晚于来自 A 的时间戳时,无论如何解释不明确的同步时间,都是如此。这也意味着您将按照您的建议等到最后一次同步时间在凌晨 2 点之后。就比较而言,我不认为这会非常昂贵,但是如果您需要确定,您需要自己进行测量和判断。

    final DateTimeFormatter timestampFormatter
            = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH-mm-ss");
    final ZoneId systemBTimeZone = ZoneId.of("America/New_York");
    final String syncFormatPattern = "'Sync''ed-as-of time:' EEE MMM d HH:mm";
    final DateTimeFormatter currentSystemBTiemFormatter = new DateTimeFormatterBuilder()
            .appendPattern("EEE MMM dd HH:mm:ss ")
            .appendZoneText(TextStyle.SHORT, Collections.singleton(systemBTimeZone))
            .appendPattern(" yyyy")
            .toFormatter(Locale.US);
    final Period maxSyncAge = Period.ofYears(2);

    String systemATimestampString = "2019-11-03-06-05-55";
    String lastSyncMsg = "Sync'ed-as-of time: Sun Nov 3 01:13";
    String currentSystemBTime = "Sun Nov 03 01:13:07 EDT 2019";
    OffsetDateTime systemATimestamp = LocalDateTime.parse(systemATimestampString, timestampFormatter)
            .atOffset(ZoneOffset.UTC);
    ZonedDateTime maxLastSyncTime
            = ZonedDateTime.parse(currentSystemBTime, currentSystemBTiemFormatter);
    ZonedDateTime minLatSyncTime = maxLastSyncTime.minus(maxSyncAge);
    int candidateYear = maxLastSyncTime.getYear();
    ZonedDateTime syncTime;
    while (true) {
        DateTimeFormatter syncFormatter = new DateTimeFormatterBuilder()
                .appendPattern(syncFormatPattern)
                .parseDefaulting(ChronoField.YEAR, candidateYear)
                .toFormatter(Locale.US);
        try {
            syncTime = LocalDateTime.parse(lastSyncMsg, syncFormatter)
                    .atZone(systemBTimeZone)
                    .withEarlierOffsetAtOverlap();
            if (syncTime.isBefore(minLatSyncTime) || syncTime.isAfter(maxLastSyncTime)) {
                throw new IllegalStateException("Last sync time is out of range");
            }
            break;
        } catch (DateTimeParseException dtpe) {
            // Ignore; try next earlier year
        }
        candidateYear--;
    }
    System.out.println("Last sync time: " + syncTime);
    System.out.println("Timestamp from system A: " + systemATimestamp);
    System.out.println("Is OK? " + syncTime.toOffsetDateTime().isAfter(systemATimestamp));

正如代码片段所代表的那样,它的输出是:

Last sync time: 2019-11-03T01:13-04:00[America/New_York]
Timestamp from system A: 2019-11-03T06:05:55Z
Is OK? false

可以看到它选择了将上次同步时间 01:13 解释为夏令时,偏移量为 -04:00,导致检查失败。时间也可能是标准时间,偏移量 -05:00,在这种情况下,UTC 等效时间是 06:13,并且检查会成功。但如前所述,为了安全起见,我们更愿意等到同步时间在 02:00 之后再次变得明确。

while 循环假设不同的年份不断解析字符串,直到它遇到星期几匹配的年份。


推荐阅读