Amazon SQS 模仿 @RabbitListener

新到一家公司,由於對業務還不太熟悉。正好又有一個需求,由於我們公司是第三方支付公司需要把訂單狀態的更新(比如成功或者失敗等狀態)發送給調用接口的商戶。在設計這個系統的時候,由於公司的業務都是在亞馬遜雲上。而且之前使用的 MQ 就是使用的 Amazon Simple Queue Service也就是 SQS,所以這次技術選型也是 SQS 。主要用於當發送商戶失敗時候,會有一個重試策略。把發送失敗的消息發送到 SQS 裏面通過它的延時消費來做發送重度。還有就是接口限流,當單位時候內調用接口次數達到閾值就把消息發送到 SQS 裏面。主要的作用就是削峯填谷。

由於之前公司使用過 RabbitMQ ,而且我也看了一下 Spring RabbitMQ 的實現。爲了讓消息系統的擴展性更高一點所以我決定模仿Spring RabbitMQ 的實現。寫一個基於 Amazon SQS 的實現。

Spring RabbitMQ 的實現中它是對於每一個消息隊列都是對應一個MessageListenerContainer來處理的。同樣的在實現 Amazon SQS 也定義一個這樣的接口。

public interface MessageListenerContainer extends Lifecycle {

    void setupMessageListener(MessageListener messageListener);

}

添加一個 MessageListener 把真正的隊列監聽處理添加到這個容器當中。

@FunctionalInterface
public interface MessageListener {

    void onMessage(NotifyMessageContent message) ;

}

@EnableAmazonSQS 註解激活標註了 @AmazonSQSListener 註解的方法,把它生成 MessageListener,它是消息消息的真正的處理邏輯。MessageListenerContainer 中用來處理 Amazon SQS 隊列的處理。

@EnableAmazonSQS 激活 @AmazonSQSListener

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AmazonSQSBootstrapConfiguration.class)
public @interface EnableAmazonSQS {

}

配置在spring bean 的方法上面的 @AmazonSQSListener,標記這個方法是用來處理 Amazon SQS 指定隊列的。

@AmazonSQSListener

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AmazonSQSListener {

    /**
     * 隊列名稱
     * @return
     */
    String queue() default "";

    /**
     * Amazon SQS 併發消費者(值必須大於 1 且小於 CPU 核心的 2 倍)
     * @return
     */
    int consumers() default 0;

}

標註可以使用 @AmazonSQSListener 註解,通過 Import 配置類 AmazonSQSBootstrapConfiguration 來實現。

@Configuration
@EnableConfigurationProperties(AmazonSQSQueueProperties.class)
public class AmazonSQSBootstrapConfiguration {

    @Bean(name = "cn.carlzone.amazon.sqs.messageing.annotation.innerAmazonSQSListenerAnnotationBeanPostProcessor")
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public AmazonSQSListenerAnnotationBeanPostProcessor amazonSQSListenerAnnotationProcessor() {
        return new AmazonSQSListenerAnnotationBeanPostProcessor();
    }

}

AmazonSQSListenerAnnotationBeanPostProcessor 通過實現 BeanPostProcessor 增強 spring bean。當檢測到 spring bean 方法上標註了 @AmazonSQSListener 註解的 Bean 然後創建 MessageListenerContainer 用於處理這個方法上需要處理的隊列。

public class AmazonSQSListenerAnnotationBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {

    private static final int MAX_CONCURRENT_CONSUMERS_LIMIT = Runtime.getRuntime().availableProcessors() * 2;

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        ListenerMethod methodAnnotation = getMethodAnnotation(targetClass);
        if(methodAnnotation != null) {
            processAmazonSQSListener(methodAnnotation.annotations, bean, methodAnnotation.method);
        }
        return bean;
    }

    private void processAmazonSQSListener(AmazonSQSListener annotation, Object bean, Method method) {
        SimpleAmazonSQSListenerContainerFactory containerFactory = beanFactory.getBean("amazonSQSListenerContainerFactory", SimpleAmazonSQSListenerContainerFactory.class);
        Method useMethod = checkProxy(method, bean);
        String queue = annotation.queue();
        Assert.hasText(queue, "amazon simple queue service name can not be null");
        SimpleMessageListenerContainer listenerContainer = containerFactory.createListenerContainer(queue, message -> useMethod.invoke(bean, message));
        int consumers = annotation.consumers();
        if(consumers > 1 && consumers <= MAX_CONCURRENT_CONSUMERS_LIMIT) {
            listenerContainer.setMaxConcurrentConsumers(consumers);
        }
        if (listenerContainer instanceof InitializingBean) {
            try {
                ((InitializingBean) listenerContainer).afterPropertiesSet();
            }
            catch (Exception ex) {
                throw new BeanInitializationException("Failed to initialize message listener container", ex);
            }
        }
        listenerContainer.start();

    }

    private ListenerMethod getMethodAnnotation(Class<?> targetClass) {
        final List<ListenerMethod> methodAnnotations = new ArrayList<>();
        ReflectionUtils.doWithMethods(targetClass, method -> {
            ListenerMethod listenerAnnotations = findListenerAnnotation(method);
            if(listenerAnnotations != null) {
                methodAnnotations.add(listenerAnnotations);
            }
        }, ReflectionUtils.USER_DECLARED_METHODS);
        if (methodAnnotations.isEmpty() && methodAnnotations.isEmpty()) {
            return null;
        }
        return methodAnnotations.get(0);
    }

    private ListenerMethod findListenerAnnotation(Method method) {
        AmazonSQSListener ann = AnnotationUtils.findAnnotation(method, AmazonSQSListener.class);
        if(ann == null) {
            return null;
        }
        return new ListenerMethod(method, ann);
    }

    private Method checkProxy(Method methodArg, Object bean) {
        Method method = methodArg;
        if (AopUtils.isJdkDynamicProxy(bean)) {
            try {
                // Found a @RabbitListener method on the target class for this JDK proxy ->
                // is it also present on the proxy itself?
                method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
                Class<?>[] proxiedInterfaces = ((Advised) bean).getProxiedInterfaces();
                for (Class<?> iface : proxiedInterfaces) {
                    try {
                        method = iface.getMethod(method.getName(), method.getParameterTypes());
                        break;
                    }
                    catch (NoSuchMethodException noMethod) {
                    }
                }
            }
            catch (SecurityException ex) {
                ReflectionUtils.handleReflectionException(ex);
            }
            catch (NoSuchMethodException ex) {
                throw new IllegalStateException(String.format(
                        "@RabbitListener method '%s' found on bean target class '%s', " +
                                "but not found in any interface(s) for a bean JDK proxy. Either " +
                                "pull the method up to an interface or switch to subclass (CGLIB) " +
                                "proxies by setting proxy-target-class/proxyTargetClass " +
                                "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()), ex);
            }
        }
        return method;
    }

    private static class ListenerMethod {

        final Method method; // NOSONAR

        final AmazonSQSListener annotations; // NOSONAR

        ListenerMethod(Method method, AmazonSQSListener annotations) { // NOSONAR
            this.method = method;
            this.annotations = annotations;
        }

    }

}

下面是定義創建 MessageListenerContainer 實例的 AmazonSQSListenerContainerFactory 的工廠,在上面的AmazonSQSListenerAnnotationBeanPostProcessor檢測到 spring bean 方法上標註了 @AmazonSQSListener 就會創建一個 MessageListenerContainer 實例。

public interface AmazonSQSListenerContainerFactory<C extends MessageListenerContainer> {

    C createListenerContainer(String queueName, MessageListener messageListener);

}

因爲 RabbitMQ 是推模式,所以只需要定義一個回調函數就可以消費消息隊列了。而 Amazon SQS 需要拉模式所以,對於隊列裏面的消息需要自己主動去拉取並且每次拉取消息的條數只能是 10 條。所以在 @AmazonSQSListener 註解裏面定義了一個 consumers() 方法用於定義 MessageListenerContainer 創建的消費該隊列的併發數。而且 Amazon SQS 在消息拉取的時候並不保證線程安全,也就是如果線程 1 拉取了消息 A,如果沒有刪除線程 2 也會拉到了消息 B。所以在消費消息的時候需要保證冪等。以下就是定義消息拉取的邏輯。

@Slf4j
public class BlockingQueueConsumer {

    private final BlockingQueue<Message> queue;

    private final AmazonSQS amazonSQS;

    private final String queueName;

    private final Executor taskExecutor;

    volatile Thread thread;

    volatile boolean declaring;

    public BlockingQueueConsumer(AmazonSQS amazonSQS, Executor taskExecutor, String queueName, int prefetchCount) {
        this.amazonSQS = amazonSQS;
        this.taskExecutor = taskExecutor;
        this.queueName = queueName;
        this.queue = new LinkedBlockingQueue<>(prefetchCount);
    }

    public Executor getTaskExecutor() {
        return taskExecutor;
    }

    public void start() {
        log.info("Starting consumer to consume queue : " + queueName );
        this.thread = Thread.currentThread();
        this.declaring = true;
        consumeFromQueue(queueName);
    }

    public synchronized void stop() {
        this.queue.clear(); // in case we still have a client thread blocked
    }

    private void consumeFromQueue(String queueName) {
        InternalConsumer consumer = new InternalConsumer(this.amazonSQS, queueName);
        getTaskExecutor().execute(consumer);
    }

    public MessageContent nextMessage() throws InterruptedException {
        return handle(this.queue.take());
    }

    public MessageContent nextMessage(long timeout) throws InterruptedException {
        MessageContent message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS));
        return message;
    }

    protected boolean hasDelivery() {
        return !this.queue.isEmpty();
    }

    private MessageContent handle(Message message) {
        if(message == null) {
            return null;
        }
        if(StringUtils.isEmpty(message.getBody())) {
            return null;
        }
        MessageContent notifyMessageContent = JSON.parseObject(message.getBody(), MessageContent.class);
        return notifyMessageContent;
    }

    private final class InternalConsumer implements Runnable {

        private final AmazonSQS amazonSQS;

        private final String queueName;

        public InternalConsumer(AmazonSQS amazonSQS, String queueName) {
            this.amazonSQS = amazonSQS;
            this.queueName = queueName;
        }

        @Override
        public void run() {
            while(true){
                ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(queueName);
                receiveMessageRequest.withMaxNumberOfMessages(10);
                //WaitTimeSeconds 使用長輪詢
                receiveMessageRequest.withWaitTimeSeconds(15);
                //設定消息不可見時間,若消息未處理或者宕機,消息將在10分鐘後被其他通知服務消費
                receiveMessageRequest.setVisibilityTimeout(600);
                List<Message> messages = amazonSQS.receiveMessage(receiveMessageRequest).getMessages();
                if(CollectionUtils.isNotEmpty(messages)) {
                    for (Message message : messages) {
                        try {
                            log.info(Thread.currentThread().getName() + "receive message : " + message.getBody());
                            BlockingQueueConsumer.this.queue.put(message);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        } finally {
                            String receiptHandle = message.getReceiptHandle();
                            DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(queueName, receiptHandle);
                            amazonSQS.deleteMessage(deleteMessageRequest);
                        }
                    }
                }
            }
        }
    }

}

在拉取消息的時候會把拉取的消息添加到 BlockingQueue 隊列當中。MessageListenerContainer 的具體實現類會調用 BlockingQueueConsumer#nextMessage 方法,判斷是否拉取到了 Amazon SQS 對應隊列的消息。如果拉取到了就調用 MessageListener 進行消息消費。

下面我們來看一下 MessageListenerContainer 的的具體實現,下面是 AbstractMessageListenerContainer 它抽象了一些公有方法:

@Slf4j
public abstract class AbstractMessageListenerContainer extends AmazonSQSAccessor implements MessageListenerContainer, DisposableBean {

    public static final int DEFAULT_PREFETCH_COUNT = 250;

    private Executor taskExecutor = new SimpleAsyncTaskExecutor();

    private final Object lifecycleMonitor = new Object();

    private volatile MessageListener messageListener;

    private String queueName;

    private volatile int prefetchCount = DEFAULT_PREFETCH_COUNT;

    private volatile boolean active = false;

    private volatile boolean running = false;

    private volatile boolean initialized;

    private boolean forceCloseChannel = true;

    public Executor getTaskExecutor() {
        return taskExecutor;
    }

    public void setTaskExecutor(Executor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public boolean isForceCloseChannel() {
        return forceCloseChannel;
    }

    public void setForceCloseChannel(boolean forceCloseChannel) {
        this.forceCloseChannel = forceCloseChannel;
    }

    public MessageListener getMessageListener() {
        return messageListener;
    }

    public void setMessageListener(MessageListener messageListener) {
        this.messageListener = messageListener;
    }

    public int getPrefetchCount() {
        return prefetchCount;
    }

    public void setPrefetchCount(int prefetchCount) {
        this.prefetchCount = prefetchCount;
    }

    public String getQueueName() {
        return queueName;
    }

    public void setQueueName(String queueName) {
        this.queueName = queueName;
        decorateTaskExecutorName(queueName);
    }

    @Override
    public void start() {
        if (isRunning()) {
            return;
        }
        if (!this.initialized) {
            synchronized (this.lifecycleMonitor) {
                if (!this.initialized) {
                    afterPropertiesSet();
                }
            }
        }
        try {
            if (log.isDebugEnabled()) {
                log.debug("Starting Rabbit listener container.");
            }
            doStart();
        } catch (Exception e) {
            throw convertAmazonSQSAccessorException(e);
        }
    }

    public final boolean isActive() {
        synchronized (this.lifecycleMonitor) {
            return this.active;
        }
    }

    protected void doStart() throws Exception {
        synchronized (this.lifecycleMonitor) {
            this.active = true;
            this.running = true;
            this.lifecycleMonitor.notifyAll();
        }
    }

    @Override
    public void stop() {
        try {
            doStop();
        }
        catch (Exception ex) {
            throw convertAmazonSQSAccessorException(ex);
        }
        finally {
            synchronized (this.lifecycleMonitor) {
                this.running = false;
                this.lifecycleMonitor.notifyAll();
            }
        }
    }

    protected void invokeListener(MessageContent message) throws Exception {
        MessageListener listener = getMessageListener();
        try {
            listener.onMessage(message);
        }
        catch (Exception e) {
            throw wrapToListenerExecutionFailedExceptionIfNeeded(e, message);
        }
    }

    protected Exception wrapToListenerExecutionFailedExceptionIfNeeded(Exception e, MessageContent message) {
        if (!(e instanceof ListenerExecutionFailedException)) {
            // Wrap exception to ListenerExecutionFailedException.
            return new ListenerExecutionFailedException("Listener threw exception", e, message);
        }
        return e;
    }

    protected void doStop() {
        shutdown();
    }

    @Override
    public final boolean isRunning() {
        synchronized (this.lifecycleMonitor) {
            return (this.running);
        }
    }

    @Override
    public final void afterPropertiesSet() {
        super.afterPropertiesSet();
        initialize();
    }

    @Override
    public void setupMessageListener(MessageListener messageListener) {
        setMessageListener(messageListener);
    }

    @Override
    public void destroy() {
        shutdown();
    }

    private void initialize() {
        try {
            doInitialize();
            this.initialized = true;
        } catch (Exception e) {
            throw convertAmazonSQSAccessorException(e);
        }
    }

    protected abstract void doInitialize() throws Exception;

    public void shutdown() {
        synchronized (this.lifecycleMonitor) {
            if (!isActive()) {
                log.info("Shutdown ignored - container is not active already");
                return;
            }
            this.active = false;
            this.lifecycleMonitor.notifyAll();
        }

        log.debug("Shutting down Rabbit listener container");

        // Shut down the invokers.
        try {
            doShutdown();
        }
        catch (Exception ex) {
            throw convertAmazonSQSAccessorException(ex);
        }
        finally {
            synchronized (this.lifecycleMonitor) {
                this.running = false;
                this.lifecycleMonitor.notifyAll();
            }
        }
    }

    protected abstract void doShutdown();

    private void decorateTaskExecutorName(String queueName) {
        if(this.taskExecutor == null) {
            return;
        }
        if(!(taskExecutor instanceof SimpleAsyncTaskExecutor)){
            return;
        }
        SimpleAsyncTaskExecutor asyncTaskExecutor = SimpleAsyncTaskExecutor.class.cast(taskExecutor);
        if(queueName.indexOf("/") != -1){
            String taskExecutorName = queueName.substring(queueName.lastIndexOf("/") + 1);
            asyncTaskExecutor.setThreadNamePrefix(taskExecutorName);
        } else {
            asyncTaskExecutor.setThreadNamePrefix(queueName);
        }
    }

}

下面MessageListenerContainer的實現類 SimpleMessageListenerContainer

@Slf4j
public class SimpleMessageListenerContainer extends AbstractMessageListenerContainer {

    private static final long DEFAULT_START_CONSUMER_MIN_INTERVAL = 10000;

    private static final long DEFAULT_STOP_CONSUMER_MIN_INTERVAL = 60000;

    private static final long DEFAULT_CONSUMER_START_TIMEOUT = 60000L;

    private static final int DEFAULT_CONSECUTIVE_ACTIVE_TRIGGER = 10;

    private static final int DEFAULT_CONSECUTIVE_IDLE_TRIGGER = 10;

    public static final long DEFAULT_RECEIVE_TIMEOUT = 1000;

    protected final Object consumersMonitor = new Object(); //NOSONAR

    private volatile int txSize = 1;

    private volatile int consecutiveActiveTrigger = DEFAULT_CONSECUTIVE_ACTIVE_TRIGGER;

    private volatile int consecutiveIdleTrigger = DEFAULT_CONSECUTIVE_IDLE_TRIGGER;

    private volatile Integer maxConcurrentConsumers;

    private final AtomicReference<Thread> containerStoppingForAbort = new AtomicReference<>();

    private volatile long lastConsumerStarted;

    private volatile long lastConsumerStopped;

    private volatile long startConsumerMinInterval = DEFAULT_START_CONSUMER_MIN_INTERVAL;

    private volatile long stopConsumerMinInterval = DEFAULT_STOP_CONSUMER_MIN_INTERVAL;

    private volatile int concurrentConsumers = 1;

    private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;

    private Set<BlockingQueueConsumer> consumers;

    private long consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT;

    public SimpleMessageListenerContainer() {}

    public SimpleMessageListenerContainer(AmazonSQS amazonSQS){
        super.setAmazonSQS(amazonSQS);
    }

    public final void setStartConsumerMinInterval(long startConsumerMinInterval) {
        Assert.isTrue(startConsumerMinInterval > 0, "'startConsumerMinInterval' must be > 0");
        this.startConsumerMinInterval = startConsumerMinInterval;
    }

    public final void setStopConsumerMinInterval(long stopConsumerMinInterval) {
        Assert.isTrue(stopConsumerMinInterval > 0, "'stopConsumerMinInterval' must be > 0");
        this.stopConsumerMinInterval = stopConsumerMinInterval;
    }

    public final void setConsecutiveActiveTrigger(int consecutiveActiveTrigger) {
        Assert.isTrue(consecutiveActiveTrigger > 0, "'consecutiveActiveTrigger' must be > 0");
        this.consecutiveActiveTrigger = consecutiveActiveTrigger;
    }

    public final void setConsecutiveIdleTrigger(int consecutiveIdleTrigger) {
        Assert.isTrue(consecutiveIdleTrigger > 0, "'consecutiveIdleTrigger' must be > 0");
        this.consecutiveIdleTrigger = consecutiveIdleTrigger;
    }

    public void setReceiveTimeout(long receiveTimeout) {
        this.receiveTimeout = receiveTimeout;
    }

    public void setTxSize(int txSize) {
        Assert.isTrue(txSize > 0, "'txSize' must be > 0");
        this.txSize = txSize;
    }

    public void setConcurrentConsumers(final int concurrentConsumers) {
        Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)");
        if (this.maxConcurrentConsumers != null) {
            Assert.isTrue(concurrentConsumers <= this.maxConcurrentConsumers,
                    "'concurrentConsumers' cannot be more than 'maxConcurrentConsumers'");
        }
        synchronized (this.consumersMonitor) {
            if (log.isDebugEnabled()) {
                log.debug("Changing consumers from " + this.concurrentConsumers + " to " + concurrentConsumers);
            }
            int delta = this.concurrentConsumers - concurrentConsumers;
            this.concurrentConsumers = concurrentConsumers;
            if (isActive()) {
                adjustConsumers(delta);
            }
        }
    }

    public void setMaxConcurrentConsumers(int maxConcurrentConsumers) {
        Assert.isTrue(maxConcurrentConsumers >= this.concurrentConsumers,
                "'maxConcurrentConsumers' value must be at least 'concurrentConsumers'");
        this.maxConcurrentConsumers = maxConcurrentConsumers;
    }

    @Override
    protected void doStart() throws Exception {
        super.doStart();
        synchronized (this.consumersMonitor) {
            if (this.consumers != null) {
                throw new IllegalStateException("A stopped container should not have consumers");
            }
            int newConsumers = initializeConsumers();
            if (this.consumers == null) {
                log.info("Consumers were initialized and then cleared (presumably the container was stopped concurrently)");
                return;
            }
            if (newConsumers <= 0) {
                if (log.isInfoEnabled()) {
                    log.info("Consumers are already running");
                }
                return;
            }
            Set<AsyncMessageProcessingConsumer> processors = new HashSet<>();
            for (BlockingQueueConsumer consumer : this.consumers) {
                AsyncMessageProcessingConsumer processor = new AsyncMessageProcessingConsumer(consumer);
                processors.add(processor);
                getTaskExecutor().execute(processor);
            }
            for (AsyncMessageProcessingConsumer processor : processors) {
                AmazonSQSStartupException startupException = processor.getStartupException();
                if (startupException != null) {
                    throw new AmazonSQSIllegalStateException("Fatal exception on listener startup", startupException);
                }
            }
        }
    }

    @Override
    protected void doShutdown() {
        Thread thread = this.containerStoppingForAbort.get();
        if (thread != null && !thread.equals(Thread.currentThread())) {
            log.info("Shutdown ignored - container is stopping due to an aborted consumer");
            return;
        }

        try {
            List<BlockingQueueConsumer> canceledConsumers = new ArrayList<>();
            synchronized (this.consumersMonitor) {
                if (this.consumers != null) {
                    Iterator<BlockingQueueConsumer> consumerIterator = this.consumers.iterator();
                    while (consumerIterator.hasNext()) {
                        BlockingQueueConsumer consumer = consumerIterator.next();
                        canceledConsumers.add(consumer);
                        consumerIterator.remove();
                        if (consumer.declaring) {
                            consumer.thread.interrupt();
                        }
                    }
                }
                else {
                    log.info("Shutdown ignored - container is already stopped");
                    return;
                }
            }
            log.info("Workers not finished.");
            if (isForceCloseChannel()) {
                canceledConsumers.forEach(consumer -> {
                    if (log.isWarnEnabled()) {
                        log.warn("Closing channel for unresponsive consumer: " + consumer);
                    }
                    consumer.stop();
                });
            }
        } catch (Exception e) {
            Thread.currentThread().interrupt();
            log.warn("Interrupted waiting for workers.  Continuing with shutdown.");
        }

        synchronized (this.consumersMonitor) {
            this.consumers = null;
        }

    }

    protected int initializeConsumers() {
        int count = 0;
        synchronized (this.consumersMonitor) {
            if (this.consumers == null) {
                this.consumers = new HashSet<>(this.concurrentConsumers);
                for (int i = 0; i < this.concurrentConsumers; i++) {
                    BlockingQueueConsumer consumer = createBlockingQueueConsumer();
                    this.consumers.add(consumer);
                    count++;
                }
            }
        }
        return count;
    }

    private BlockingQueueConsumer createBlockingQueueConsumer() {
        String queueName = getQueueName();
        int actualPrefetchCount = getPrefetchCount() > this.txSize ? getPrefetchCount() : this.txSize;
        BlockingQueueConsumer consumer = new BlockingQueueConsumer(getAmazonSQS(), getTaskExecutor(), queueName, actualPrefetchCount);
        return consumer;
    }

    @Override
    protected void doInitialize() throws Exception {
        // do nothing
    }

    private boolean isActive(BlockingQueueConsumer consumer) {
        boolean consumerActive;
        synchronized (this.consumersMonitor) {
            consumerActive = this.consumers != null && this.consumers.contains(consumer);
        }
        return consumerActive && this.isActive();
    }

    private final class AsyncMessageProcessingConsumer implements Runnable {

        private final BlockingQueueConsumer consumer;

        private final CountDownLatch start;

        private volatile AmazonSQSStartupException startupException;

        private AmazonSQSStartupException getStartupException() throws TimeoutException, InterruptedException {
            if (!this.start.await(
                    SimpleMessageListenerContainer.this.consumerStartTimeout, TimeUnit.MILLISECONDS)) {
                log.error("Consumer failed to start in "
                        + SimpleMessageListenerContainer.this.consumerStartTimeout
                        + " milliseconds; does the task executor have enough threads to sqs the container "
                        + "concurrency?");
            }
            return this.startupException;
        }

        AsyncMessageProcessingConsumer(BlockingQueueConsumer consumer) {
            this.consumer = consumer;
            this.start = new CountDownLatch(1);
        }

        @Override
        public void run() {
            if (!isActive()) {
                return;
            }
            int consecutiveIdles = 0;
            int consecutiveMessages = 0;
            try {
                try {
                    this.consumer.start();
                    this.start.countDown();
                } catch (Throwable t) {
                    this.start.countDown();
                    throw t;
                }
                while (isActive(this.consumer) || this.consumer.hasDelivery()) {
                    boolean receivedOk = receiveAndExecute(this.consumer);
                    if (receivedOk) {
                        if (isActive(this.consumer)) {
                            if (consecutiveMessages++ > SimpleMessageListenerContainer.this.consecutiveActiveTrigger) {
                                considerAddingAConsumer();
                                consecutiveMessages = 0;
                            }
                        }
                    } else {
                        consecutiveMessages = 0;
                        if (consecutiveIdles++ > SimpleMessageListenerContainer.this.consecutiveIdleTrigger) {
                            considerStoppingAConsumer(this.consumer);
                            consecutiveIdles = 0;
                        }
                    }
                }
            } catch (InterruptedException e) {
                log.debug("Consumer thread interrupted, processing stopped.");
                Thread.currentThread().interrupt();
            } catch (Error e){
                log.error("Consumer thread error, thread abort.", e);
            } catch (Throwable t) {
                log.debug("Consumer raised exception, processing can restart if the connection factory supports it", t);
            }			// In all cases count down to allow container to progress beyond startup
            this.start.countDown();
            if (!isActive(this.consumer)){
                log.debug("Cancelling " + this.consumer);
                this.consumer.stop();
            }
        }
    }

    private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws Throwable {
        return doReceiveAndExecute(consumer);
    }

    private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Throwable { //NOSONAR
        for (int i = 0; i < this.txSize; i++) {
            log.trace("Waiting for message from consumer.");
            MessageContent message = consumer.nextMessage(this.receiveTimeout);
            if (message == null) {
                break;
            }
            try {
                invokeListener(message);
            } catch (Throwable e) { //NOSONAR
                if (log.isDebugEnabled()) {
                    log.debug("process message fail the reason is : " + e.getMessage() + "the message is " + message);
                }
                break;
            }
        }
        return true;
    }

    protected void adjustConsumers(int delta) {
        synchronized (this.consumersMonitor) {
            if (isActive() && this.consumers != null) {
                if (delta > 0) {
                    Iterator<BlockingQueueConsumer> consumerIterator = this.consumers.iterator();
                    while (consumerIterator.hasNext() && delta > 0
                            && (this.maxConcurrentConsumers == null
                            || this.consumers.size() > this.maxConcurrentConsumers)) {
                        BlockingQueueConsumer consumer = consumerIterator.next();
                        consumerIterator.remove();
                        delta--;
                    }
                }
                else {
                    addAndStartConsumers(-delta);
                }
            }
        }
    }

    private void considerAddingAConsumer() {
        synchronized (this.consumersMonitor) {
            if (this.consumers != null
                    && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) {
                long now = System.currentTimeMillis();
                if (this.lastConsumerStarted + this.startConsumerMinInterval < now) {
                    this.addAndStartConsumers(1);
                    this.lastConsumerStarted = now;
                }
            }
        }
    }

    private void considerStoppingAConsumer(BlockingQueueConsumer consumer) {
        synchronized (this.consumersMonitor) {
            if (this.consumers != null && this.consumers.size() > this.concurrentConsumers) {
                long now = System.currentTimeMillis();
                if (this.lastConsumerStopped + this.stopConsumerMinInterval < now) {
                    this.consumers.remove(consumer);
                    if (log.isDebugEnabled()) {
                        log.debug("Idle consumer terminating: " + consumer);
                    }
                    this.lastConsumerStopped = now;
                }
            }
        }
    }

    protected void addAndStartConsumers(int delta) {
        synchronized (this.consumersMonitor) {
            if (this.consumers != null) {
                for (int i = 0; i < delta; i++) {
                    if (this.maxConcurrentConsumers != null
                            && this.consumers.size() >= this.maxConcurrentConsumers) {
                        break;
                    }
                    BlockingQueueConsumer consumer = createBlockingQueueConsumer();
                    this.consumers.add(consumer);
                    AsyncMessageProcessingConsumer processor = new AsyncMessageProcessingConsumer(consumer);
                    if (log.isDebugEnabled()) {
                        log.debug("Starting a new consumer: " + consumer);
                    }
                    getTaskExecutor().execute(processor);
                    try {
                        AmazonSQSStartupException startupException = processor.getStartupException();
                        if (startupException != null) {
                            this.consumers.remove(consumer);
                            throw new AmazonSQSIllegalStateException("Fatal exception on listener startup", startupException);
                        }
                    }
                    catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                    }
                    catch (Exception e) {
                        consumer.stop();
                        log.error("Error starting new consumer", e);
                        this.consumers.remove(consumer);
                    }
                }
            }
        }
    }

}

它的作用就是創建線程來拉取 Amazon SQS 隊列裏面的消息,並且通過 AmazonSQSListenerAnnotationBeanPostProcessor 創建的 MessageListener 來消費消息。主要是通過他的內部類 AsyncMessageProcessingConsumer 來創建一個我們上面提到的 BlockingQueueConsumer 拉取隊列裏面的消息,然後死循環調用 SimpleMessageListenerContainer#receiveAndExecute 它會調用 BlockingQueueConsumer#nextMessage 方法判斷是否拉到到消息。如果有消息的話,就會調用實現 BeanPostProcessAmazonSQSListenerAnnotationBeanPostProcessor 傳入的 MessageListener 也就是在 spring bean 標註 @AmazonSQSListener 的方法。上面就是消息處理的整個邏輯了。

由於 Amazon SQS 必須在 Amazon 雲上才能使用,本地無法調試。所以我通過在 local 環境就 mock 了一下它。

public class MockAmazonSQS extends AbstractAmazonSQSAsync {

    @Getter
    private static final Map<String, Message> messages = new ConcurrentHashMap<>();

    static {
        mockMessage();
    }

    @Override
    public ReceiveMessageResult receiveMessage(ReceiveMessageRequest request) {
        ReceiveMessageResult result = new ReceiveMessageResult();
        String queueUrl = request.getQueueUrl();
        Message message = messages.get(queueUrl);
        if(message != null) {
            result.setMessages(Lists.newArrayList(message));
        }
        return result;
    }

    @Override
    public SendMessageResult sendMessage(SendMessageRequest request){
        String queueUrl = request.getQueueUrl();
        Message message = new Message();
        message.setBody(request.getMessageBody());
        messages.put(queueUrl, message);
        return new SendMessageResult();
    }

    private static void mockMessage() {
        messages.put("amazon.local.mock", genMessage("mock message"));
    }

    @Override
    public DeleteMessageResult deleteMessage(DeleteMessageRequest deleteMessageRequest){
        String queueUrl = deleteMessageRequest.getQueueUrl();
        messages.remove(queueUrl);
        return new DeleteMessageResult();
    }


    private static Message genMessage(String message) {
        Message localMessage = new Message();
        MessageContent localMessageContent = new MessageContent();
        localMessageContent.setMessage(message);
        localMessage.setBody(JSON.toJSONString(localMessageContent));
        return localMessage;
    }

}

並且定義了一下添加消息的方法。可以在 Controller 裏面添加消息,模擬隊列裏面有消息需要消費。最後就可以直接定義一個監聽者用於消費 SQS 裏面的消息了。

@Component
public class LocalMessageListener {

	@AmazonSQSListener(queue = "amazon.local.mock")
	public void onMessage(MessageContent messageContent){
		System.out.println(messageContent);
	}

}

由於我在 MockAmazonSQS 實例初始化的時候添加了一條消息。所以在項目啓動的時候就會消費這條消息。所以在控制檯打印出了這條消息:
在這裏插入圖片描述
然後我通過 Controller 向 MockAmazonSQS 添加消息。這條消息同樣也可以消費,以下是 Postman 發送消息:

在這裏插入圖片描述
下面是控制檯打印消費消息的日誌:

在這裏插入圖片描述
這寫這篇 blog 的時候,由於需要創建 demo。發現 spring 提供了 amazon simple queue server 的自動依賴,它的功能肯定比我的強大。發現自己是多此一舉,但是爲了把自己的思路分享給大家還是決定寫這篇 blog。

源碼地址:sqs-demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章