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

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

В моем сценарии задачи добавляются в очередь из HTTP-запросов. Каждая задача связана с учетной записью, и нет ограничения скорости. Поэтому возможно, что задачи из одной учетной записи наводят очередь сообщений.

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

В настоящее время я прибегал к использованию Redis с некоторыми сценариями Lua, которые были выбраны для эмуляции очереди с циклическим циклом, но было интересно, существуют ли какие-либо существующие топологии очередей, которые это делают?

Ответ 1

Обычно я делаю это так:

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

  • Рабочие принимают очереди очередей из рабочей очереди, когда они готовы к большей работе. Когда рабочий занимает очередь учетных записей, он извлекает первую задачу, и рабочий немедленно помещает очередь учетных записей в конец рабочей очереди, если она не пуста. Затем рабочий выполняет задачу.

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

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

Я делаю это просто:

  • В каждой очереди учетных записей есть атомарное логическое значение, которое отслеживает, находится ли он в рабочей очереди. Рабочий устанавливает это значение в значение false сразу же после удаления очереди очереди. Если кто-то обнаружит, что очередь учетных записей не пуста, они могут попробовать CAS для этого логического значения к true и в случае успешного завершения очереди очереди в рабочую очередь.

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

Ответ 2

С RabbitMQ Direct Exchange и Spring AMQP вы можете реализовать топологию очереди, которая содержит очередь для каждой учетной записи, подключенной к одному обмену. Отправка сообщений в обмен с именем учетной записи в качестве ключа маршрутизации и с единственным пользователем, привязанным к нескольким очередям, потребитель получит сообщения round robin (см. "Прямой обмен и баланс нагрузки" ).

Проблема с этой настройкой заключается в том, что у вас может быть довольно много очередей (по одной для каждой учетной записи) и, по крайней мере, в моей реализации (прилагается как простое приложение Spring Boot ниже), вам придется перезапустить "потребитель каждый раз, когда приходит новая учетная запись, так как это означает, что у вас есть новая очередь для присоединения потребителя. Не знаю, хорошо ли этот весы/работает. Проверьте этот пост для максимального количества очередей в RabbitMQ, и если это может повлиять на вас.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RoundRobin.RoundRobinQueueConfiguration.class)
public class RoundRobin {

    private static final String EXCHANGE = "round-robin-exchange";

    private final List<String> tasks = Arrays.asList(   // account(a):task(t) where t holds the expected order of consumption
            "a1:t1", "a2:t2", "a3:t3",                  // make sure, a queue for every account (a) exists
            "a1:t4", "a1:t7", "a1:t9", "a1:t10",        // add "many" tasks (t) for account 1
            "a2:t5", "a2:t8", "a3:t6");                 // add further tasks for other accounts, such that a1 has to "wait"

    private final List<String> declaredQueues = new ArrayList<>();

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitAdmin rabbitAdmin;

    @Autowired
    private DirectExchange directExchange;

    @Autowired
    private SimpleMessageListenerContainer listenerContainer;

    @Test
    public void enqueuedTasksAreProcessedRoundRobin() {
        tasks.forEach(task -> {
            String[] accountAndTask = task.split(":");
            declareQueue(accountAndTask[0]);
            rabbitTemplate.convertAndSend(accountAndTask[0], accountAndTask[1] + " from account " + accountAndTask[0]);
        });
    }

    private void declareQueue(String routingKey) {
        if (!declaredQueues.contains(routingKey)) {
            Queue queue = new Queue(routingKey);
            rabbitAdmin.declareQueue(queue);
            rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(directExchange).with(routingKey));
            listenerContainer.stop();
            listenerContainer.addQueues(queue);
            listenerContainer.start();
            declaredQueues.add(routingKey);
        }
    }

    @Configuration
    public static class RoundRobinQueueConfiguration {

        @Bean
        public ConnectionFactory connectionFactory() {
            return new CachingConnectionFactory("localhost");
        }

        @Bean
        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
            RabbitTemplate template = new RabbitTemplate(connectionFactory);
            template.setExchange(EXCHANGE);
            return template;
        }

        @Bean
        public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
            return new RabbitAdmin(connectionFactory);
        }

        @Bean
        public DirectExchange directExchange(RabbitAdmin rabbitAdmin) {
            DirectExchange directExchange = new DirectExchange(EXCHANGE);
            rabbitAdmin.declareExchange(directExchange);
            return directExchange;
        }

        @Bean
        public SimpleMessageListenerContainer simpleMessageListenerContainer(ConnectionFactory connectionFactory, RabbitAdmin rabbitAdmin) {
            Queue queue = new Queue("dummy-queue"); // we need a queue to get the container started...
            rabbitAdmin.declareQueue(queue);
            SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
            container.setMessageListener(new RoundRobinMessageListener());
            container.setQueues(new Queue("dummy-queue"));
            container.start();
            return container;
        }

    }

    public static class RoundRobinMessageListener implements MessageListener {

        @Override
        public void onMessage(Message message) {
            System.out.println("Consumed message " + (new String(message.getBody())));
        }

    }

}

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

Вывод теста:

Consumed message t1 from account a1
Consumed message t2 from account a2
Consumed message t3 from account a3
Consumed message t4 from account a1
Consumed message t5 from account a2
Consumed message t6 from account a3
Consumed message t7 from account a1
Consumed message t8 from account a2
Consumed message t9 from account a1
Consumed message t10 from account a1

я думаю, это то, что вы хотели...

Ответ 3

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

Я бы написал это сам, но это не так сложно сделать.

Ответ 4

Я знаю, что WebSphere в версии 5.1 (очень старая) предоставила такой Очереди, что одна Очередь может предоставить услугу под-очередям, т.е. что в вашем случае вы создадите под-очередь для каждого клиента и в основном можете запросить в раунде -строчно, как каждая подзаголовок для следующей задачи. Но я не знаю подробностей и вообще не рекомендую WebSphere (говорить по опыту). Но я предполагаю, что программно вы можете поддерживать список очередей или очередь очередей, где каждая очередь на нижнем уровне представляет собой очередь задач от конкретного клиента. И тогда вы можете использовать свою собственную логику для выполнения задач в заказе тарифа из надлежащей очереди. Разумеется, вам нужно будет управлять очередями, то есть очищать пустые очереди и получать новую задачу, если этот клиент уже имеет выделенную очередь или нет, и соответственно добавить свою задачу в новую или существующую очередь.

Ответ 5

Я хотел бы предложить использование мультикарты Guava:

import java.util.LinkedHashSet;

public class QueueTest {
    public static void main(String[] args) {
        TreeMultimap<String, String> multimap = TreeMultimap.create();
        multimap.put("c1", "TaskC11");
        multimap.put("c1", "TaskC12");
        multimap.put("c1", "TaskC13");
        multimap.put("c2", "TaskC21");
        multimap.put("c3", "TaskC31");

        while (multimap.size() > 0) {
            for (String customer : new LinkedHashSet<>(multimap.keySet())) {
                String taskToProcess = multimap.get(customer).pollFirst();
                System.out.println(taskToProcess);
            }
        }
    }
}

Результат:

TaskC11
TaskC21
TaskC31
TaskC12
TaskC13

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