Платежная система - Дизайн отложенных задач

Микросервисы
Платежная система - Дизайн отложенных задач

предисловие

Офлайн-задачи, которые не зависят от времени в общих задачах бизнес-использования, таких какQuartzСтруктура временных задач класса может справиться с этим. Это может быть сложно, если вы столкнетесь со следующими сценариями:

  • Заказы, не оплаченные в течение получаса, автоматически закрываются
  • Выполнение задачи через указанное время
  • Push Push/SMS через полчаса

Общность этого типа сценария заключается в том, что ему необходимо подождать определенный период времени или достичь определенного момента времени, чтобы запустить указанную бизнес-логику, и точность времени очень высока. Есть элегантное решение延迟消息Он создан специально для этого. Эта статья посвящена обсуждению этой технологии.

Общие реализации

Библиотека регулярного сканирования

Этот способ самый простой, но недостатком является сложность установки периода сканирования по времени. Настройка на два часа примерно такая же, как и на 20 минут, но есть неточности. Как обеспечить более точную реализацию? Уменьшение цикла сканирования увеличивает его частоту, что является более точным, но увеличивает нагрузку на базу данных. В любом случае есть неточности, некоторые задержки, и максимальное время может быть ожидание одного цикла сканирования.

Это может быть гарантировано в бизнесе, но есть две проблемы:

  • неточный
  • давление на базу данных

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

Есть ли более элегантное решение? Это запоздалые новости.

Отложенное сообщение

Обычно используемое промежуточное программное обеспечение сообщений, такое какRabbitMQа такжеRocketMQМожно реализовать функцию задержки сообщений.

RocketMQ

сначала сRocketMQПример. На момент написания этой статьиRocketMQПоддержка версии с открытым исходным кодом18level уровня задержки, значение по умолчанию1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h, производитель отправляет сообщение, устанавливаяdelayLevelЗначение уровня задержки, тем самым косвенно устанавливая время задержки.

Вот краткий обзор принципа его реализации, чтобы проанализировать, почему его так плохо использовать.

Для облегчения описания, описание здесь упрощеноRocketMQПроцесс доставки сообщения, на следующем рисунке показан обычный процесс доставки сообщения.

正常的消息流程

нормальный поток сообщений

Вот поток отложенных сообщений:

延迟消息的流程

Поток задержанных сообщений

Изменить после того, как сообщение будет доставлено в обычном режимеTopicВойдите в переименованную очередь, создайте запланированную задачу для очереди, отсканируйте время прибытия и доставьте ее в обычную очередь потребления. Идея проста, но есть проблема с этой схемой: если по логике есть только одна очередь, сообщения, доставленные в эту очередь, должны быть упорядочены. То есть сообщения должны переупорядочиваться для каждой доставки, что явно недопустимо.

Некоторые учащиеся могут спросить, что значит регулярные новости? привожу пример:

Допустим, мы доставляем три сообщения, как показано на рисунке:

FIFO投递

Доставка ФИФО

Он будет доставлен строго в порядке очереди, что, очевидно, не соответствует нашим ожиданиям.1S Cсообщение должно быть в5S Bраньше. Очередь обязательно должна взбивать всю очередь для сортировки, чтобы удовлетворить наши потребности.

Давайте посмотрим на оптимизированную реализацию:

优化后延迟消息的流程

Оптимизирован поток отложенных сообщений

просто добавьdelayLevelЗначение уровня задержки, в соответствии с различными уровнямиdispatchДля разных очередей каждая очередь имеет сообщения только с одинаковым уровнем задержки, что позволяет избежать упорядочения сообщений. Время задержки каждой очереди одинаково, и оно становится порядком очереди и порядка.

Я считаю, что читатели, которые видят здесь, должны понятьRocketMQИдеи оформления отложенных сообщений. Я лично не думаю, что это достаточно дружественно к бизнесу,18Уровня конфигурации явно недостаточно, хотяRocketMQПоддерживает настройку различных уровней правил времени задержки. Если вы хотите поддерживать различные времена задержки, вам придется создавать несколько кластеров, каждый со своим временем, что слишком глупо. Однако платная версия Alibaba Cloud поддерживает любое время, возможно, в этом сила денег.

в заключении,RocketMQКонфигурация отложенных сообщений одиночная, а доступность невысокая.

Итак, если есть компонент, поддерживающий задержку сообщений на любое время, вместо указанияdelayLevel, нам очень удобно заниматься развитием бизнеса, указывая время задержки или время синхронизации при передаче параметров. Но если говорить о реальной реализации, давайте сначала посмотримRabbitMQКак сделать шаг, чтобы реализовать отложенные сообщения.

RabbitMQ

строго говоря,RabbitMQОтложенные сообщения не поддерживаются. Но мы можем использовать его死信队列реализовать функцию задержки. Это может быть трудно понять напрямую, поэтому позвольте мне привести пример. Сообщения, накопленные в общей очереди, не имеют потребителей, и общая очередь будет передавать эти сообщения в другую нижнюю очередь, привязанную к ней. Если в этой нижней очереди есть потребители, эти автоматические сообщения могут быть обработаны.死信队列. Конечно, вRabbitMQПривязка двух очередей достигается совместным действием коммутатора и ключа маршрутизации и напрямую не привязывается к очереди. Эта штука раньше использовалась не по назначению в конструкции пуш-системы интернет-аукциона, и прочно наступила на яму.

Давайте поговорим о потребностях того времени. Разработанная мной аукционная система имеет два режима торгов: ограниченный по времени аукцион и специальный аукцион.

  • Аукцион с ограничением по времени: пользователь А назначает цену, пользователь Б также назначает цену, и аукционная платформа автоматически увеличивает цену в соответствии с назначенной ценой.По истечении времени закрытия аукциона побеждает участник, предложивший самую высокую ставку. Как правило, период аукциона в днях.
  • Специальный аукцион: Продавец публикует специальный аукцион, в котором перечислены только товары продавца, то есть персональный аукцион продавца. И в шоу есть开拍时间, это будущее确定时间点. Онлайн-реализация этой модели требует имитации офлайн-торгов на месте, и каждый лот появляется по очереди в соответствии с лотом. Обратный отсчет составляет 30 секунд, и кто-то предлагает продлить обратный отсчет на 5 секунд. В течение этого периода пользователи терминала могут свободно делать ставки, и тот, у кого самая высокая цена, должен делать ставки.

После разговора о бизнес-модели вы, естественно, можете подумать об использовании отложенных сообщений для достижения этой цели.定时消息функция. Как это делалось тогда? потому что предыдущийRocketMQЗнайте, что у него нет функции синхронизированных сообщений. Поэтому мы ориентируемся на огромную долю рынка.RabbitMQ. Конечно, ищитеRabbitMQ 延迟消息Появляется много нужной информации. Я забыл, какой из них я читал в то время, кажется, он называется30分钟关闭订单怎么实现?试试 RabbitMQтакие статьи. На первый взгляд это можно установитьtime-to-liveШаблон сообщения 's предназначен для моих потребностей во времени. Просто нужно найти разницу между временем начала специальной сессии и сейчас, установить его какttlПросто сделай это. Что касаетсяRabbtiMQиспользоватьttlНа самом деле есть два способа реализовать отложенные сообщения. В то время был выбран режим без плагина, потому что плагин используетerlangРазработка, если есть проблема, то она не считается исправленной. Давайте посмотрим на реализацию бизнеса в то время:

Привязка очереди и мертвого письма:

队列与死信的绑定

Связывание очередей и мертвых писем

Клиент отправляет задержанное сообщение:

@Component
public class DelayMessageProducer  {

	private static Logger logger = LoggerFactory.getLogger(DelayMessageProducer.class);

	@Autowired
	private RabbitTemplate rabbitTemplate;


	public void send(DelayMessage data) {

		logger.info("【延迟消息生产者】准备发送消息,data:{}", data.toJSON());

		LocalDateTime now = LocalDateTime.now();
		LocalDateTime beginTime = data.getBeginTime();

		long delayMillis  = Duration.between(now, beginTime).toMillis();

		long delaySeconds = Duration.ofMillis(delayMillis).getSeconds();

		if (delayMillis < 0) {
			logger.warn("【延迟消息生产者】 警告:延迟时间计算有误,开始时间:{}", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(beginTime));
			return;
		}

		logger.info("消息理论在{}秒后到达战场", delaySeconds);

		rabbitTemplate.convertAndSend(RabbitConst.Begin.EXCHANGE, RabbitConst.Begin.ROUTING_KEY, data, m -> {
				m.getMessageProperties().setExpiration(String.valueOf(delayMillis));
				return m;
		});
	}
}

Ну, я думаю, что использовалRabbitMQучащиеся могут понять смысл. Ключ в этой строкеm.getMessageProperties().setExpiration(String.valueOf(delayMillis))настраиватьttl. В то время я тестировал несколько специальных сессий, и не было никаких проблем, когда я собирался выйти в интернет.

До сих пор помню, как мне позвонил ПМ и сказал, что я выложил спецсессию через три дня, а потом выложил еще через 10 минут, почему она еще не началась. Я думал, что это должно бытьMQПовесив трубку, я посмотрел на это, и проблем не было, и я был озадачен. На самом деле это вышеRocketMQПроблемы с порядком сообщений, описанные в .10Сообщения, поступающие в течение нескольких минут, должны ждать3Большой Брат Тиан идет к команде, чтобы получить его. На тот момент это было немного удручающе, потому что о работе спецрежима должны были объявить на следующий день, а накануне днем ​​возникла проблема.

Затем я провел небольшое исследование,RabbitMQизJAVA APIОн поддерживает динамическое создание очередей. Естественно, когда я отправляю такого рода сообщения по времени, для каждого из них динамически создается очередь, так что в этих очередях всегда находится только одно сообщение, и, конечно, нет проблемы упорядочения сообщений. Следующий вопрос заключается в том, что эта очередь не может существовать всегда, а потом я обнаружил, что очередь действительно может задавать время выживания. просто начните я устанавливаю и сообщениеttlВремя то же.После тестирования выяснено,что иногда при автоматическом удалении очереди времени доставки сообщения выдается ошибка.Я серьезно подозреваю,что сообщение не обрабатывается вовремя. потому чтоdeadlineПо этой причине я просто поставил время удаления очереди наttlплюс несколько секунд. Таким образом проблема временно решается. Давайте посмотрим на реализацию в то время:

Для простоты использования я также инкапсулировал специальный класс, хотя знаю, что это лишь временная реализация. Ни в коем случае, даже если я втяну кусок дерьма в проект, я надеюсь, что это дерьмо будет стоять вертикально, а не быть тонким:

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

нужно зарегистрироваться наSpring, используйте в одноэлементном режиме:

@Bean(name = "rabbitAdmin")
public RabbitAdmin getRabbitAdmin(RabbitTemplate rabbitTemplate) {
 return new RabbitAdmin(rabbitTemplate);
}
 // 临时队列小助手
@Bean
public FixedTimeQueueHelper fixedTimeQueueHelper(RabbitAdmin rabbitAdmin) {
 return new FixedTimeQueueHelper(rabbitAdmin);
}

Его можно использовать, вводя его:

@Component
public class FixedTimeMessageProducer  {

	private static Logger logger = LoggerFactory.getLogger(FixedTimeMessageProducer.class);

	@Autowired
	private FixedTimeQueueHelper fixedTimeQueueHelper;

	public void send(FixedTimeMessage data) {

		logger.info("【定时消息生产者】准备发送消息,data:{}", data.toJSON());
		try {
			fixedTimeQueueHelper.declareAndSend(RabbitConst.FixedTime.EXCHANGE, RabbitConst.FixedTime.ROUTING_KEY,
					data.getId(),
					data.getExcutetime(),
					data.getPayload());
		} catch (FixedTimeDeclareException e) {
			logger.warn("定时队列创建失败,{}", e.getMessage());
		}
	}

}

Конечно, эти временные очереди также привязаны к очередям недоставленных сообщений, которые были объявлены заранее.

/**
 * 创建队列,该定时队列用来兜底多个临时队列中没有路由的消息
 */
private void createTimeFixedQueue() {
 Exchange exchangeFixedTime = ExchangeBuilder.directExchange(RabbitConst.FixedTime.EXCHANGE).durable(true).build();
 Queue  queueFixedTime  = QueueBuilder.durable(RabbitConst.FixedTime.QUEUE).build();
 Binding bindingFixedTime  = BindingBuilder.bind(queueFixedTime).to(exchangeFixedTime).with(RabbitConst.FixedTime.ROUTING_KEY).noargs();
 rabbitAdmin.declareExchange(exchangeFixedTime);
 rabbitAdmin.declareQueue(queueFixedTime);
 rabbitAdmin.declareBinding(bindingFixedTime);
}

Когда я приеду сюда, мой процесс майнинга будет завершен. Некоторые дети могут спросить: «Ах, ты слишком низкий. Что, если ты удалишь сообщение из очереди без ACK?». Хороший вопрос, мой ответ на него заключается в том, что в моем бизнес-сценарии пользователи будут видеть конкретные секунды обратного отсчета до начала сеанса, а система может достигать каждую минуту и ​​секунду. Другими словами, какое время установлено, в какое время оно начинается. Если он не запускается вовремя по системным причинам, подождитеRabbitMQ BrokerПовторная доставка до успешного ответа может занять от нескольких секунд до нескольких минут. Итак, сколько пользователей все еще остаются на этой странице после ожидания в течение нескольких минут? Если пользователь останется на странице, а других игроков, сделавших за него ставку, там нет, это нанесет ущерб честности аукциона. И специальный аукцион также включает в себя логику торгов с ограничением по времени. Поэтому этот сценарий требует не ручного ответа, а автоматического ответа. В бэкенде есть функция перевыпуска операции в случае сбоя, конечно при условии, что анонсируют пользователя.

Не по теме:

Фактически, в начале проектирования системы предполагалось использовать колесо времени для реализации этой функции. Поскольку мне нравится делать общие компоненты, когда я что-то делаю, на самом деле это занимает немного времени. Так что просто используйтеRabbitMQПриходите к выводу, простое красиво. Неожиданно путь небес перевоплощается, и, в конце концов, я должен вернуться, чтобы восполнить эту яму. К счастью, с этой проблемой справились своевременно, иначе другим решением было бы выполнение высокочастотного сканирования задач на время, вроде бы я временно наелся дерьма за это. При проектировании системы вы должны тщательно изучить план, иначе вам, возможно, придется делать все заново в конце, помните.

алгоритм колеса времени

Ну наконец-то дошел до сути этой статьи,时间轮算法, сначала дайте тезисссылка на скачивание. Я считаю, что лучше всего изучать технологию, глядя на источник и изучая лежащую в его основе логику. Старайтесь не смотреть на информацию из вторых рук, потому что легко увлечься создателями из вторых рук с неверными личными товарами и ценностями. Конечно, есть разные мнения по этому вопросу.Нам нужно только потратить немного своего мозга, чтобы увидеть, что жевали другие.Даже если некоторые из них ошибаются, обучение на 70% лучше, чем на 0%. Пока это может удовлетворить ваши потребности в обучении поэтапно, это не имеет значения.Когда однажды вы обнаружите, что человек, за которым вы следите, кажется, говорит что-то не так, это означает, что вы выросли, и вы можете избавиться от него. . Как и моя статья, я тут давно.Если вы не знакомы с этой технологией раньше, мне легко взять ритм. Если вы часто читаете статьи определенного человека, вам будет легко незаметно ввести значения. Я надеюсь, что читатели смогут сохранить способность мыслить независимо. Большинство людей просто кажутся очень могущественными. Настоящий рост зависит от вас.

Сразу к делу: алгоритм колеса времени можно использовать для эффективного выполнения большого количества задач синхронизации с простой реализацией и очень высокой точностью. Как этого добиться? Давайте посмотрим на это дальше.

Небесный класс

Начнем с самого простого варианта. Есть такое требование: задание нужно выполнить в час дня. Что бы вы сделали, если бы это произошло? Прежде всего, проще всего представить массив + связанный список. Для простоты понимания нарисовал картинку:

天级时间轮

Небесное колесо времени

Шкала времени относится к неделе24Час, стрелки двигаются один раз в час. Как показано на картинке в первый день дня5В час, то есть в 5:00, в связанном списке было найдено несколько задач. Просто возьмите его и выполните. Некоторые друзья могут спросить, что мне делать, когда время истекает? Что я хочу использовать завтра? Просто увеличьте временной интервал.

Недельный уровень

Непосредственно скорректируйте временной интервал до одной недели, чтобы выполнить задание на всю точку в течение недели.

周级时间轮

Еженедельное колесо времени

Шкала относится к неделе7*24Час, стрелки двигаются один раз в час. Как показано на картинке в первый день недели28В связанном списке найдена задача, соответствующая часу в 4:00 вторника. Просто возьмите его и выполните. Конечно, я нарисовал только одну задачу в каждый момент на этой картинке.

В этот момент некоторые друзья могут спросить, что мне делать, если я хочу поддерживать в течение года?

Быстрый ответ: это непросто. Если вы хотите поддерживать один год, увеличьте шкалу времени до365*24Вот и все.

Отлично, я ценю способность делать выводы о других вещах. Но что, если я хочу поддерживать год с точностью до секунды?365*24*60*60С таким количеством масштабов, не будет ли мне пустой тратой места использовать массив для его сохранения? Если у меня есть задача, которая является последней секундой недели, колесо будет работать много секунд, чтобы получить ее, и эффективность подбора задач не высока. Есть ли более элегантный способ? Ответ - да, то есть перебрать массив. Без дальнейших церемоний, пожалуйста, читайте дальше.

Неограниченное количество часов

Так как я тоже на уровне двух ножей в создании PPT, я просто нарисовал его невзначай и давайте посмотрим вместе.

无限小时级时间轮

Неограниченное часовое колесо времени

Короче говоря, временная шкала только24один, представляющий день24Часы, черная стрелка вернется на дно, когда черная стрелка достигнет дна1(Вы чувствуете, что вращение очень волшебно). Вот почему его называют колесом времени, которое на самом деле представляет собой круговую решетку. Простите меня за то, что я не нарисовал это как колесо, я постараюсь в следующий раз (если есть более простой инструмент для создания такой простой анимации, пожалуйста, дайте мне знать).

Следует также отметить, что в каждой задачеRoundотметка. Только когда указатель проходит мимо, обнаруживается, что онRound=0задачи будут выполняться. Благодаря такому дизайну, когда задача поставлена,Roundи соответствующий масштаб времени. каждый цикл,Round>0Задача уменьшается на единицу, и ее можно выполнить, когда она возвращается к нулю.

Сколько часов поддерживает это колесо? Ответ — бесконечные часы.Конечно, если учитывать доступность, нужен отказоустойчивый механизм, самый простой из них — персистентность.

Иерархическое колесо времени

Я полагаю, что читатели здесь имеют определенное представление об основном принципе колеса времени.Вышеприведенная конструкция может поддерживать только часовой уровень. Если вы хотите преобразовать его в секунды, вы можете просто и грубо увеличить масштаб, не считая памяти как24*60*60. Если вы хотите быть более элегантным, вы можете использовать层级时间轮. Что это значит?

Прежде чем обсуждать, давайте определим понятие профессионала в соответствии с приведенным выше описанием:

  • События (временные задачи, которые необходимо выполнить)
  • сегмент (связанный список)
  • Курсор (текущий указатель)
  • Точность времени (циферблат времени)
  • массив (цикл)

Вызвали ли два приведенных выше простых примера своего рода错觉, думаете шкала времени, на которой находится курсор, это реальное время? Не так! Почему возникает эта иллюзия? Потому что с самого начала мы анализировали в реальных единицах времени. На самом деле колеса в программе не гарантированно идут по часам, минутам и секундам. Это может быть 20/30/50 секунд на круге. Время является относительным понятием, будь то однослойная или многоуровневая реализация колеса времени. Внутри он должен иметь свои собственные встроенные часы и постоянно бить. Когда вам нужно добавить новую задачу, рассчитайте соответствующую позицию указателя в соответствии с текущими часами и временем выполнения задачи. привожу пример:

Если текущие часы currentTime равны 0 секундам, полная шкала циферблата составляет 30 секунд, а один скачок равен 1 секунде. Ваша задача должна быть выполнена через 18 секунд, добавьте ее сейчас, и она войдетindex=18позиция. Если время прошло восемнадцать секунд, текущие часы панели currentTime равны 18. Вы добавляете еще одну задачу, которую нужно выполнить через 10 секунд, тогда она попадет вindex=28позиция. Что, если есть еще и задача, которую нужно выполнить через 20 секунд? Явно вошелindex=18+28-30=6позиция.

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

Обратите внимание, следующее неправильное понимание: на самом деле, сначала я думал, что конструкция колеса времени полностью соответствует соотношению между часами, минутами и секундами в реальности. А почему у вас хватает терпения нарисовать не ту картинку, прочтитеВы поймете, что вы говорите):

Иерархическое колесо времени берет малый масштаб в качестве шасси и постепенно модернизируется до крупного масштаба.Со временем эта задача будет по-прежнему деградировать до низкоуровневого шасси. Если вам нужна конкретная реализация, вы можете обратиться кKafka, я надеюсь, что вы можете открыть для себя новый мир там.

Колесо высокой доступности упоминалось выше, и я кратко расскажу о нем. Нужен большой размах, например, надежность нужно рассматривать за два месяца, в общем, нужно выполнять настойчивость. Таким образом, возникает проблема проектирования схемы постоянства. Во-первых, мы не можем поместить его в локальную память, потому что большинство современных приложений являются распределенными, поэтому мы можем рассмотреть возможность размещения его в локальной памяти.Redisсередина. Что касается того, чтобы кидать все сообщенияRedis, мой ответ нет. В конце концов, общий компонент должен учитывать все аспекты: если в бизнесе много отложенных сообщений, срок действия которых истекает через несколько месяцев, все они будут загружены вRedisЭто также пустая трата ресурсов. Конечно, если вы не хотите проектировать компонент с сильной универсальностью во всех аспектах, и бизнес может принять все этоRedisСреда тоже в порядке. Например, мы можем загружать задержанные сообщения, которые появляются в течение получаса, вRedisсередина. Что касается новостей других дней и даже месяцев, то они пока использоваться не будут, поэтому их можно сохранить в файл.

Конечно, некоторые студенты скажут:RedisОн также будет зависать.Мой ответ - сделать его высокодоступным. Ведь даже если вы сэкономитеMysqlсередина,MysqlТакже требуется высокая доступность. использоватьRedisСуществующая модель данных проста и легка без необходимости разработки реляционной модели, что является огромным преимуществом.

послесловие

Эта статья представляет собой краткое изложение задачи задержки, я надеюсь, что она может быть полезна для всех. Если у вас есть лучшие решения и опыт, пожалуйста, дайте мне знать. Если есть записанный код колеса времени, вы также можете отправить его на просмотр. Наконец, я заканчиваю увещанием: желаю всем крепкого здоровья, и в будущем будет период.