【问题标题】:How do you divide a time period into equal intervals and find the current one?您如何将时间段划分为相等的时间间隔并找到当前时间段?
【发布时间】:2015-10-27 15:06:23
【问题描述】:

我需要为大量用户安排定期作业。此作业将以固定速率运行,即间隔。我想在该时间间隔内均匀地分配每个用户的作业执行。例如,如果间隔是 4 天,我会使用 consistent hashing function 和每个用户的标识符来同时安排作业,例如。每 4 天,第 3 天。

间隔是相对于所有用户都相同的原始时刻。给定这样一个起始时刻,如Instant#EPOCH 或其他一些常数值,我如何找到当前间隔的开始日期?

我可以的

Instant now = Instant.now();
Instant origin = Instant.EPOCH;
Duration interval = Duration.ofDays(4);

Duration duration = Duration.between(origin, now);
long sinceOrigin = duration.toMillis();
long millisPerInterval = interval.toMillis();

long intervalsSince = sinceOrigin / millisPerInterval;
Instant startNext = origin.plus(interval.multipliedBy(intervalsSince));

int cursor = distributionStrategy.distribute(hashCode, millisPerInterval);

然后我可以使用cursor 将作业安排在相对于当前间隔开始的Instant

这里有很多数学问题,我不确定在任何地方都转换为毫秒是否会支持实际日期。有没有更精确的方法来划分两个瞬间之间的时间并找到我们当前所处的那个(细分)?

【问题讨论】:

  • 我对这个问题有点困惑。间隔内的工作分配实际上与问题无关,对吗?您只是在问如何找到正确的下一个间隔的确切开始时刻?
  • @Zwander 没错。一旦我有了起点,我就可以应用分布。我只是想表明我的最终目标。
  • 在您的示例中,间隔是 4 个逻辑天还是 345,600 秒?换句话说,是Period 还是Duration
  • @namshub 理想情况下两者都是,但假设是持续时间。
  • @Pillar PeriodDuration 代表不同的概念。如果您的语言环境采用夏令时,则四天可能少至 342,000 秒或多至 349,200 秒。

标签: java jodatime java-time


【解决方案1】:

如果你只想减少这里的数学,你可以使用余数来代替除法和乘法。

long millisSinceIntervalStart = sinceOrigin % millisPerInterval;
Instant startNext = now.minusMillis(millisSinceIntervalStart);

在这里,您不必计算自起源以来经过的间隔数。只需获取自 intervalStart 以来经过的时间,然后从当前时间中减去它。

此外,您的 startNext 似乎表示当前间隔的开始,而不是下一个间隔。对吗?

【讨论】:

  • 是的,startNext 应该是 startCurrent。这样可以节省一两个等式,这很好。
【解决方案2】:

假设您实际上对瞬间和持续时间(即与期间、日期、时区等无关)感兴趣,那么您的代码应该没问题。在这种情况下,我实际上会提前进入毫秒......这里的数学很简单。

Interval getInterval(Instant epoch, Duration duration, Instant now) {
    long epochMillis = epoch.getMillis();
    long durationMillis = duration.getMillis();

    long millisSinceEpoch = now.getMillis() - epochMillis;        
    long periodNumber = millisSinceEpoch / durationMillis;
    long start = epochMillis + periodNumber * durationMillis;
    return new Interval(start, start + durationMillis);
}

这假设您不需要担心 now 会在 epoch 之前 - 在这一点上,您需要做一些工作,因为您希望该部门的 地板运算,而不是向 0 截断。

(如果你只想开始,你可以直接返回new Instant(start)。)

【讨论】:

  • 有没有一种简单的方法来协调期间和持续时间?例如,如果我想做一天零三个小时,我想将这一天视为一天而不是 86400 秒,并将三个小时附加到末尾。
  • @Pillar:你可以很容易地创建一个“1 天 3 小时”的时间段 - 但是在你的用例中处理它的所有代码都变得更多那个时候很复杂。您还需要考虑有时有 28 小时,有时有 26 小时来分配工作,例如......以及指定当前无关紧要的时区。如果可以的话,我强烈建议你坚持固定的持续时间。
【解决方案3】:

我认为你把事情复杂化了。您不需要知道代码所暗示的那么多。

您只需要回答“这个对象下一次应该什么时候运行?”,使得答案在统计上均匀分布在区间上并且一致(不依赖于“现在”,除非下一次运行总是在“现在”之后)。

这个方法可以做到:

public static long nextRun(long origin, long interval, Object obj) {
    long nextRunTime = origin + (System.currentTimeMillis() - origin)
       / interval * interval + Math.abs(obj.hashCode() % interval);
    return nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + interval;
}

此方法返回对象下一次应该运行的时间,使用其hashCode() 来确定它应该在持续时间内安排的位置,然后返回下一个实际发生的时间。

小实现说明:使用Math.abs(obj.hashCode() % interval) 代替Math.abs(obj.hashCode()) % interval 来防范hashCode() 返回Integer.MIN_VALUE 并知道Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE


如果您需要在您的 API 中使用 java.time 类,下面是相同的代码,但带有 java.time 参数和返回类型:

public static Instant nextRun(Instant origin, Duration interval, Object target) {
    long start = origin.toEpochMilli();
    long width = interval.toMillis();
    long nextRunTime = start + (System.currentTimeMillis() - start)
       / width * width + Math.abs(target.hashCode() % width);
    nextRunTime = nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + width;
    return Instant.ofEpochMilli(nextRunTime);
}

为了帮助理解数学,这里有一个较长的版本,其中将组件计算分解并分配给有意义的变量名称:

public static Instant nextRun(Instant origin, Duration duration, Object target) {
    long now = System.currentTimeMillis();
    long start = origin.toEpochMilli();
    long intervalWidth = duration.toMillis();
    long ageSinceOrigin = now - start;
    long totalCompleteDurations = ageSinceOrigin / intervalWidth * intervalWidth;
    long mostRecentIntervalStart = start + totalCompleteDurations;
    long offsetInDuration = Math.abs(target.hashCode() % intervalWidth);
    long nextRun = mostRecentIntervalStart + offsetInDuration;
    // schedule for next duration if this duration's time has already passed
    if (nextRun < now) { 
        nextRun += intervalWidth;
    }
    return Instant.ofEpochMilli(nextRun);
}

【讨论】:

  • 只看你的java.time 代码(可以简化为long 值),你提出的和我已有的有什么不同吗? (除了删除中间变量。)
  • @Pillar 我的代码不知道startNextdistributionStrategy,只有originhashCode。此外,我的答案中的第一个版本是我能想到的完成工作的最少代码量(我个人更喜欢“更少代码”的解决方案)
  • 这就是他删除中间变量的意思。在您的上一个 sn-p 中,mostRecentIntervalStart 等效于 startNext(应该是 startCurrent),hashCode 等效于 distributionStrategy
【解决方案4】:

我会尝试将每个时间段定义为具有开始和结束日期的对象。然后使用 RB 树来存储 Period 对象。然后您可以在树中导航特定日期:

如果日期在第一个周期内,则您已找到它。 如果日期早于期间的开始日期,请导航到左侧节点并检查该期间 如果日期晚于期间的结束日期,请导航到正确的节点并检查该期间

【讨论】:

  • 我必须保留(或重新创建)多棵树(对于每个作业),其中可能包含数千个节点,具体取决于间隔大小和原始日期。另外,树的尽头在哪里?它是否以now 结尾?这真的比数学好吗?
  • 有可能构建出一个间隔流.. 只生成足以找到当前迭代的内容。也许只保留最后一个作业实例被触发的最后一次迭代?也许还将用户放入一个组,计算该组的触发时间并应用该组内的用户?
【解决方案5】:

好吧,正如已经说过的那样,找到包含Duration 的区间,就像你已经在做的那样或直接使用millis 就足够了,而且所涉及的数学运算也很简单。但是,如果您确实有一个用例保证 Period 间隔涉及数小时,那么可以按以下方式处理它:

  1. Period 转换为大约几小时的持续时间,并使用它来估计目标与原点的距离。
  2. 按您的估计缩放Period。将 24 小时的聚合视为额外的天数。
  3. 将原始间隔移动缩放周期。如果移动的间隔包含您的目标,那么您就完成了。如果没有,请根据您错过的金额重新估算。

在查找包含区间时要注意的重要一点是,所有Period 加法都必须直接从原点进行,而不是从中间区间进行以保持一致性。例如,如果您有一个月的Period,其原点为 1 月 31 日,则紧接在原点间隔之后的间隔应从 2 月 28 日和 3 月 31 日开始。向 1 月 31 日添加两个月将正确生成 3 月 31 日,但向 2 月 28 日添加一个月将错误地生成 3 月 28 日。

以下是上述方法的代码。请注意,这种事情有很多异常情况,我只测试了其中的几个,所以不要认为这段代码是经过严格测试的。

public static final int NUM_HOURS_IN_DAY = 24;
public static final int NUM_HOURS_IN_MONTH = 730;  // approximate

public ZonedDateTime startOfContainingInterval(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
    return intervalStart(origin, period, hours, containingIntervalNum(origin, period, hours, target));
}

public int containingIntervalNum(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
    int intervalNum = 0;
    ZonedDateTime intervalStart = origin, intervalFinish;
    long approximatePeriodHours = period.toTotalMonths() * NUM_HOURS_IN_MONTH + period.getDays() * NUM_HOURS_IN_DAY + hours;
    do {
        long gap = ChronoUnit.HOURS.between(intervalStart, target);
        long estimatedIntervalsAway = Math.floorDiv(gap, approximatePeriodHours);
        intervalNum += estimatedIntervalsAway;
        intervalStart = intervalStart(origin, period, hours, intervalNum);
        intervalFinish = intervalStart(origin, period, hours, intervalNum + 1);
    } while (!(target.isAfter(intervalStart) && target.isBefore(intervalFinish) || target.equals(intervalStart)));
    return intervalNum;
}

public ZonedDateTime intervalStart(ZonedDateTime origin, Period period, int hours, int intervalNum) {
    Period scaledPeriod = period.multipliedBy(intervalNum).plusDays(hours * intervalNum / NUM_HOURS_IN_DAY);
    long leftoverHours = hours * intervalNum % NUM_HOURS_IN_DAY;
    return origin.plus(scaledPeriod).plusHours(leftoverHours);
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-17
    • 2019-02-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多