如何处理夏令时时钟回滚导致的时间歧义
How To Handle Time Ambiguity Caused by Daylight Saving Time Clock Rollback
我有这种情况,想看看有没有更好的解决方案:
我从系统A收到一个时间戳,格式为"yyyy-MM-dd-HH-mm-ss",没有时区,但他们可以将其修复为 UTC。
然后我需要通过 SSH 进入系统 B 以获得一个 "last sync time",它可能是过去的,它不 return 任何时区,甚至不年份(来自备份产品),这里是一个示例:
"Sync'ed-as-of time: Sun Nov 3 01:13"
系统 B 是一个备份设备,运行一些专有的软件,还有一个内部调度程序,每 15 分钟唤醒一次,以检查是否有任何数据需要从源同步。如果之前的同步仍然是 运行,它会让当前的同步完成而不采取进一步的操作。此 "last sync time" 将在每次同步后更新,因此在最坏的情况下,它不应比当前系统时间滞后超过几个小时。
我需要比较这两个时间戳以确保 B 上的 "last sync time" 晚于 A。如果不是,我需要等待并重试查询,直到 B 晚于 A 才能继续到下一步。
我有 java 程序位于 Linux 框上,当请求来自 A 时,它会向 B 查询 "last sync time",然后立即向B发出"date"命令获取当前系统时间,其中return是当前系统时间,格式为"EEE MMM dd kk:mm:ss z yyyy",如
"Fri Jun 14 21:07:07 EDT 2019",
所以程序可以应用年份,以及这个输出的时区来完成"last sync time"的时间戳。该程序确实处理了 "last snync time" 和 "date" 输出分布在一年边界的情况。
除了夏季夏令时结束,时钟回滚(凌晨 2 点回到凌晨 1 点),它工作正常,有一个小时 window 时间会模棱两可:
例如,我从系统 A 得到一个 1:20am 时间图(已经从 UTC 转换为本地),然后当我从系统 B 得到一个 1:30am 时,我不能判断 B 是否晚于 A。它可能是时钟更改之前的 1:30。系统B的当前时区,没有明确"last sync time"在哪个时区。
我们要求用户将所有系统切换为UTC时间,所以我们不需要担心时钟调整。但这对环境的影响太大了。
另一种选择是我专门处理那一个小时,然后等到 "last sync time" 时间戳超过凌晨 2 点。为此,我需要在每次进行时间戳比较时检查代码是否是特定时间,我认为这不是最佳做法。但我想不出更好的方法。
非常感谢任何建议。如果这是要走的路,是否有一个很好的 Java 库来计算夏令时切换日期?百万感谢!
在 Matt Johnson 的评论中得到了这个,我认为这是最好的解决方案:
现代 Java 代码应该使用 java.time 包,自 Java 8 以来一直是 built-in。ZoneRules.getValidOffsets 将告诉您在特定时区适用于给定 LocalDateTime 的偏移量。如果返回了多个偏移量,则说明您处在一个不明确的时期。
我看不出你能完全确定。 “11 月 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 循环假设不同的年份不断解析字符串,直到遇到星期几匹配的年份。
我有这种情况,想看看有没有更好的解决方案:
我从系统A收到一个时间戳,格式为"yyyy-MM-dd-HH-mm-ss",没有时区,但他们可以将其修复为 UTC。
然后我需要通过 SSH 进入系统 B 以获得一个 "last sync time",它可能是过去的,它不 return 任何时区,甚至不年份(来自备份产品),这里是一个示例:
"Sync'ed-as-of time: Sun Nov 3 01:13"
系统 B 是一个备份设备,运行一些专有的软件,还有一个内部调度程序,每 15 分钟唤醒一次,以检查是否有任何数据需要从源同步。如果之前的同步仍然是 运行,它会让当前的同步完成而不采取进一步的操作。此 "last sync time" 将在每次同步后更新,因此在最坏的情况下,它不应比当前系统时间滞后超过几个小时。
我需要比较这两个时间戳以确保 B 上的 "last sync time" 晚于 A。如果不是,我需要等待并重试查询,直到 B 晚于 A 才能继续到下一步。
我有 java 程序位于 Linux 框上,当请求来自 A 时,它会向 B 查询 "last sync time",然后立即向B发出"date"命令获取当前系统时间,其中return是当前系统时间,格式为"EEE MMM dd kk:mm:ss z yyyy",如
"Fri Jun 14 21:07:07 EDT 2019",
所以程序可以应用年份,以及这个输出的时区来完成"last sync time"的时间戳。该程序确实处理了 "last snync time" 和 "date" 输出分布在一年边界的情况。
除了夏季夏令时结束,时钟回滚(凌晨 2 点回到凌晨 1 点),它工作正常,有一个小时 window 时间会模棱两可:
例如,我从系统 A 得到一个 1:20am 时间图(已经从 UTC 转换为本地),然后当我从系统 B 得到一个 1:30am 时,我不能判断 B 是否晚于 A。它可能是时钟更改之前的 1:30。系统B的当前时区,没有明确"last sync time"在哪个时区。
我们要求用户将所有系统切换为UTC时间,所以我们不需要担心时钟调整。但这对环境的影响太大了。
另一种选择是我专门处理那一个小时,然后等到 "last sync time" 时间戳超过凌晨 2 点。为此,我需要在每次进行时间戳比较时检查代码是否是特定时间,我认为这不是最佳做法。但我想不出更好的方法。
非常感谢任何建议。如果这是要走的路,是否有一个很好的 Java 库来计算夏令时切换日期?百万感谢!
在 Matt Johnson 的评论中得到了这个,我认为这是最好的解决方案:
现代 Java 代码应该使用 java.time 包,自 Java 8 以来一直是 built-in。ZoneRules.getValidOffsets 将告诉您在特定时区适用于给定 LocalDateTime 的偏移量。如果返回了多个偏移量,则说明您处在一个不明确的时期。
我看不出你能完全确定。 “11 月 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 循环假设不同的年份不断解析字符串,直到遇到星期几匹配的年份。