Как вы делите время на равные интервалы и найти сейчас?

мне нужно запланировать периодическую работу для многих пользователей. Это задание будет выполняться с фиксированной скоростью, интервалом. Я хочу равномерно распределить выполнение задания для каждого пользователя за этот интервал. Например, если интервал составляет 4 дня, я бы использовал последовательная функция хэширования с идентификатором для каждого пользователя, чтобы запланировать задание в то же время, например. каждые 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 относительно начала текущего интервала.

здесь много математики и я не уверен, что преобразование в миллисекунды везде будет отстаивать фактические даты. Есть ли более точный способ разделить время между два момента и поиск одного (подразделения), в котором мы находимся в настоящее время?

5 ответов


Если вы хотите только уменьшить математику здесь, вы можете использовать остаток вместо деления и умножения.

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

здесь вам не нужно вычислять количество интервалов, прошедших с начала координат. Просто получить время прошло с intervalStart и вычесть его из текущего времени.

кроме того,startNext указывает на начало текущего интервала, а не следующего интервала. Правильно?


если ты на самом деле интересуется мгновениями и длительностью (т. е. ничего общего с периодами, датами, часовыми поясами и т. д.), Тогда ваш код должен быть в порядке. Я бы на самом деле пошел в миллисекунды ранее в этом случае... математика здесь простая.

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).)


Я думаю, что ты все усложняешь. Вам не нужно знать почти столько, сколько предлагает ваш код.

вам нужно только ответить "когда этот объект должен следующий запуск?", так что ответ статистически равномерно распределен по интервалу и последователен (не зависит от "Сейчас", за исключением того, что следующий запуск всегда после "сейчас").

этот метод это:

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


Если вам это нужно java.time классы будут использоваться в вашем API, вот тот же код, но с 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);
}

Я бы попытался определить каждый период времени как объект с датой начала и окончания. Затем используйте дерево RB для хранения объектов периода. Затем вы можете перемещаться по дереву для определенной даты:

Если дата находится в пределах первого периода, вы ее нашли. если дата предшествует дате начала периода, перейдите к левому узлу и проверьте этот период если дата находится после даты окончания периода, перейдите к правому узлу и проверьте этот период


Ну, как уже было сказано, найти содержащей Duration интервал, как вы уже делаете или используете millis напрямую, достаточен для этого случая использования, и математика проста. Однако, если у вас был прецедент, который гарантировал Period интервал с участием часов, вот как это может быть обработано:

  1. перевести Period в приблизительную продолжительность часов и используйте это, чтобы оценить, сколько интервалов от цели происхождение.
  2. масштабирование Period по вашей оценке. Рассматривайте агрегации 24 часов как дополнительные дни.
  3. переместите исходный интервал на масштабируемый период. Если перемещенный интервал содержит вашу цель, вы закончили. Если нет, пересчитайте на основе суммы, которую вы пропустили.

важно отметить, что при поиске содержащего интервала все Period добавление должно происходить непосредственно из источника, а не из промежуточных интервалов для консистенция. Например, если у вас есть Period одного месяца с началом 31 января интервалы сразу после начала интервала должны начинаться 28 февраля и 31 марта. Добавление двух месяцев к 31 января правильно приведет к 31 марта, но добавление одного месяца к 28 февраля неправильно приведет к 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);
}