第 5 期:消息队列与事件驱动架构

作者:禅与计算机程序设计艺术

1.简介

基于消息队列技术构建可靠、异步、分布式、高效的系统架构是一个比较成熟的方案,并已经得到了广泛应用。在实际业务中,消息队列作为一个基础设施组件也逐渐成为越来越重要的组成部分。那么如何利用好消息队列来构建一个可靠、异步、高效的事件驱动架构呢?本文将会详细介绍消息队列与事件驱动架构的相关概念,以及结合实践案例,分享一些建设性意见。

2.基本概念及术语说明

2.1 消息队列(Message Queue) 消息队列是一种生产消费模型,也就是生产者把消息放入队列,然后消费者从队列中取出消息进行处理。消息队列可以解决生产者和消费者之间的耦合关系,生产者不用等待消费者处理完毕就可以继续发送新的数据,同时也可以实现负载均衡。

2.2 异步通信 异步通信指的是无需等待回复就可继续下一步工作的通信方式。采用异步通信时,生产者向队列发送消息后不必等待消费者的响应而继续执行自己的任务,这往往能够提升整体的处理性能。同时由于消费者处理消息的速度不同,可以根据自身的处理能力和消息数量动态调整自己的线程池大小,因此也避免了线程过多导致的资源浪费。

2.3 分布式事务 分布式事务指的是在多个节点间的操作要么都成功,要么都失败。举个例子,在电商网站下单过程中需要扣除用户的余额,如果扣除前的余额不足或扣款失败,则整个订单交易失败。分布式事务就是为了保证在多个数据库或微服务节点之间数据的一致性而设计的一种协议。目前国内许多公司都在探索如何利用消息队列来构建分布式事务,并且兼顾系统可用性和数据一致性。

2.4 事件驱动架构 事件驱动架构是由事件源触发的事件流派生出的架构模式。它通过事件的监听与发布机制来实现模块间的解耦合,在实际开发中,事件驱动架构通常借助于消息队列来实现。

2.5 数据流动方向 消息队列和事件驱动架构在传统的 SOA 服务架构中占据着不同角色。SOA 是面向服务的架构,主要是用来描述企业应用系统的各个构件以及它们之间的交互行为。其中最核心的一块就是服务调用,调用方通过远程过程调用的方式调用服务提供方的接口,通过网络进行通信。而在事件驱动架构中,主要依赖于事件消息的传递,事件源产生事件,通过消息中间件传递到订阅者处进行处理。

3.核心算法原理及操作步骤 3.1 容错机制 消息队列中有两种类型的异常情况,一是队列的宕机,即消息队列中的消息不能被正常获取和处理;二是消费者的宕机,即消费者在处理消息的过程中出现错误,导致消息无法正常的发布到其他消费者手上。为了应对以上两种异常情况,消息队列通常都会提供三种容错机制:

  1. 消息重复检测和去重:当消息队列收到的消息与已存在的消息相等时,则认为是重复消息,此时消息队列应该丢弃该消息。
  2. ACK 模型:消息队列可以设置 ACK 模型,当消费者成功接收到消息后,才认为消息被成功消费,否则认为消费失败。消费者超时后再次尝试消费即可。
  3. 回溯消费:如果消费者因为某种原因处理失败了,则可以选择从最近消费成功的位置开始消费,而不是从头开始重新消费。

具体的容错机制操作流程图如下所示:

3.2 集群部署 在实际项目中,通常消息队列服务器通常部署在不同的机器上以提高容灾能力和扩展性。如果只有一个消息队列服务器的话,随着消息量的增长,服务器的压力也会随之增大,甚至可能发生崩溃的情况。为了避免这种情况,消息队列服务器可以做集群部署。一般来说,集群中共同担任消息队列服务器的角色叫做主节点,每台机器只能有一个主节点,其他的节点分别称为从节点。主节点负责接收外部客户端发送来的请求,并转发给从节点进行处理;从节点只负责处理消息队列中的消息,并将结果返回给主节点。如果主节点发生故障,则可以把它的从节点升级为新的主节点,而不会影响生产环境的运行。具体集群部署架构如下图所示:

3.3 消息持久化 为了防止消息队列服务器因硬盘损坏、服务器故障等各种原因而造成消息丢失,消息队列通常都会提供消息持久化功能。消息持久化是指将队列中的消息保存在磁盘上,以便出现硬件故障、服务器崩溃、甚至宕机时仍然可以恢复并继续工作。消息持久化可以分为两种类型:

  1. 同步刷盘方式:当消费者消费完一条消息并确认消费成功后,消息队列才将该消息从内存中删除。但是,当消费者消费速度较慢或者消费者出现故障时,可能会出现消息积压问题,即消息队列中的消息堆积在内存中,等待消费者读取。同步刷盘方式就是在此情况下使用的一种方式,消费者确认消费完成后,立即将消息写入磁盘上的日志文件,确保所有的消息都被持久化。
  2. 定时刷新方式:消费者确认消费完成后,将消息暂存到内存中,然后定期刷新到磁盘上的日志文件中。定时刷新的方式适用于消费者消费能力比较强的情况,消费者消费能力比同步刷盘方式更强,这样可以减少磁盘 IO 的频率,进而减少磁盘 I/O 的开销。

具体的消息持久化方式架构如下图所示:

3.4 消息投递 消息投递指的是消息的发布者将消息放入消息队列之后,消息是否能够被正确地投递到对应的消费者手上。消息投递可以分为两种类型:

  1. 点对点(Point-to-point)方式:点对点方式下,每个消息只能有一个消费者消费。点对点方式对性能要求高,因为它要求每个消费者独立处理消息,所以消费者需要准备好处理很多的消息。
  2. 发布/订阅(Publish/Subscribe)方式:发布/订阅方式下,消息可以被多个消费者消费。发布/订阅方式对性能要求低,它不需要每个消费者都处理相同的消息。发布者只管把消息投递到对应的主题中,而不需要关心谁来接收消息。

具体的消息投递方式架构如下图所示:

4.代码实例与解释说明

4.1 ActiveMQ 安装与配置

4.1.1 ActiveMQ 下载地址 ActiveMQ 可以从官网下载:http://activemq.apache.org/download.html 。选择相应版本进行下载。

4.1.2 配置 ActiveMQ 解压缩后进入安装目录下的 bin 目录,启动命令如下:

./activemq start

配置文件 activemq.xml 默认放在安装目录下的 conf 目录下。以下为 activemq.xml 文件的默认配置,供参考:



   
      
      
         
      

      
      

      
      
         
            jms.queue.test
         
      

      
      
         
            
               
            
         
      

   

以上配置中:

  • persistenceAdapter:开启消息持久化。
  • transportConnector:设置连接管理器。
  • consumerPriorityOrder:设置消费者的优先级,这里默认所有消费者都为普通优先级。
  • securityPlugin:设置管理员权限。

以上配置中没有涉及任何安全机制,建议不要在生产环境中打开。如有必要,可以通过 SSL 加密传输和 SASL 验证等安全机制进行安全配置。

4.1.3 测试 ActiveMQ 是否安装成功 可以在浏览器中输入:http://localhost:8161 来测试是否安装成功。如果显示 ActiveMQ Web Console,则表示安装成功。点击 Overview 查看各项指标。

4.2 Spring Boot + Apache Qpid JMS 示例代码

4.2.1 创建 Spring Boot 工程 4.2.2 添加 pom.xml 文件依赖 pom.xml 文件内容如下:


  4.0.0

  com.example
  demo
  0.0.1-SNAPSHOT
  jar

  demo
  http://maven.apache.org

  
    UTF-8
    2.1.8.RELEASE
    5.1.11.RELEASE
    0.50.0
  

  

    
        org.springframework.boot
        spring-boot-starter-web
    

    
        org.springframework.boot
        spring-boot-starter-actuator
    

    
        org.springframework.boot
        spring-boot-starter-test
        test
    

    
        org.springframework.integration
        spring-integration-core
    

    
        org.springframework.integration
        spring-integration-amqp
    


    
        org.springframework.boot
        spring-boot-starter-data-jpa
    

    
        org.springframework.boot
        spring-boot-starter-jdbc
    

    
        mysql
        mysql-connector-java
    

     
          org.springframework.boot
          spring-boot-starter-websocket
       

    
    
        org.springframework.boot
        spring-boot-starter-amqp
    
    
        io.projectreactor.addons
        reactor-adapter
        ${spring-messaging.version}
    
    
        org.springframework.amqp
        spring-rabbit
        ${spring-messaging.version}
    
    
        org.springframework.amqp
        spring-rabbit-test
        ${spring-messaging.version}
        test
    

     
          org.springframework.boot
          spring-boot-starter-thymeleaf
       
     
        org.springframework.boot
        spring-boot-starter-mail
    
    
        javax.mail
        mail
        1.5.0-b01
    
    
        com.fasterxml.jackson.datatype
        jackson-datatype-jsr310
        2.10.2
    

  

  
    
      
        org.springframework.boot
        spring-boot-maven-plugin
      
    
  

4.2.3 编写配置文件 application.yml application.yml 文件内容如下:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false
    username: root
    password: root
    driverClassName: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

management:
  endpoints:
    web:
      exposure:
        include: "*"

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

4.2.4 编写邮件工具类 MailUtils.java 代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

@Component
public class MailUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(MailUtils.class);

    @Autowired
    private JavaMailSender mailSender;

    public void sendSimpleEmail(String to, String subject, String text) throws Exception {
        SimpleMailMessage message = new SimpleMailMessage();

        // set email info
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);

        try {
            mailSender.send(message);

            LOGGER.info("Send a simple email successfully.");
        } catch (Exception e) {
            throw new RuntimeException("Failed to send the email.", e);
        }
    }
}

4.2.5 编写 JmsConfig 配置类 JmsConfig.java 代码如下:

import javax.annotation.Resource;
import javax.jms.ConnectionFactory;

import org.apache.qpid.jms.JmsConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.ip.tcp.TcpReconnectStrategy;
import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory;
import org.springframework.integration.ip.tcp.serializer.ByteArrayToObjectStreamSerializer;
import org.springframework.integration.ip.tcp.serializer.ObjectToByteArraySerializer;
import org.springframework.integration.support.json.JsonObjectMapper;
import org.springframework.integration.support.json.JsonToObjectTransformer;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.MessageChannel;

@Configuration
@EnableIntegration
public class JmsConfig {

    /**
     * Qpid ConnectionFactory
     */
    @Resource(lookup = "myConnectionFactory")
    private AbstractClientConnectionFactory connectionFactory;

    /**
     * Input channel for incoming messages
     */
    @Bean
    public MessageChannel input() {
        return IntegrationUtils.createDirectChannel(this.getClass(), "input");
    }

    /**
     * Output channel for outgoing messages
     */
    @Bean
    public MessageChannel output() {
        return IntegrationUtils.createDirectChannel(this.getClass(), "output");
    }

    /**
     * Configure a JSON Object mapper for serialization and deserialization of POJO objects from JSON strings in messaging payloads.
     * 
     * @return the configured JSON object mapper.
     */
    @Bean
    public ObjectMapper objectMapper() {
        JsonObjectMapper mapper = new JsonObjectMapper();

        // register custom serializers here if any...

        return mapper;
    }

    /**
     * Configure an integration flow that receives messages from RabbitMQ using IP TCP outbound adapter and converts them into POJO objects before sending them to the 'output' channel.
     * The conversion is done by using the {@link JsonToObjectTransformer}.
     * 
     * @param connectionFactory the connection factory used for connecting to RabbitMQ.
     * @param input the input channel for incoming messages.
     * @param output the output channel for outgoing messages.
     * @param jsonToObjectTransformer the transformer used for converting JSON strings into POJO objects.
     * 
     * @return the configured integration flow.
     */
    @Bean
    public IntegrationFlow receiveRabbitAndTransformIntoPojo(final ConnectionFactory connectionFactory,
                                                           final MessageChannel input,
                                                           final MessageChannel output,
                                                           final JsonToObjectTransformer jsonToObjectTransformer) {
        return IntegrationFlows
               .from(TcpOutboundGatewayBuilder.gateway(connectionFactory).outboundChannel(input),
                        c -> c
                               .poller(p -> p
                                       .fixedRate(500)
                                       .maxMessagesPerPoll(-1)))
               .handle(LoggingHandler.Level.INFO, LoggingHandler.log())
               .transform(jsonToObjectTransformer::transform)
               .channel(output)
               .get();
    }

    /**
     * Configure an integration flow that sends messages from the 'input' channel to RabbitMQ using IP TCP inbound gateway and converts them back to JSON string before sending.
     * The conversion is done by using the {@link JsonSerializer}.
     * 
     * @param connectionFactory the connection factory used for connecting to RabbitMQ.
     * @param input the input channel for incoming messages.
     * 
     * @return the configured integration flow.
     */
    @Bean
    public IntegrationFlow sendPojoAsJsonViaRabbit(final ConnectionFactory connectionFactory,
                                                  final MessageChannel input) {
        return IntegrationFlows
               .from(input)
               .handle(LoggingHandler.Level.INFO, LoggingHandler.log())
               .transform((Object o) -> SerializationUtils.serialize(o))
               .handle(LoggingHandler.Level.INFO, LoggingHandler.log())
               .get();
    }

    /**
     * Create a Qpid connection factory bean with reconnect strategy.
     * 
     * @return the created Qpid connection factory bean.
     */
    @Bean(name = "myConnectionFactory")
    public AbstractClientConnectionFactory myConnectionFactory() {
        TcpReconnectStrategy strategy = new TcpReconnectStrategy();
        strategy.setRecoveryInterval(10000);
        strategy.setMaxAttempts(3);

        JmsConnectionFactory factory = new JmsConnectionFactory("amqp://guest:guest@localhost:5672", null, "myConnectionFactory");
        factory.setReconnectStrategy(strategy);

        return factory;
    }
} 
  

4.2.6 编写 WebSocketConfig 配置类 WebSocketConfig.java 代码如下:

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer, SchedulingConfigurer {

    @Override
    public void configureWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/chat").addInterceptors().withSockJS();
    }

    @Bean
    public Executor taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.initialize();
        return scheduler;
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {
        return new DefaultHandshakeHandler(taskScheduler());
    }

    @Bean
    public WebSocketHandler chatHandler() {
        return new ChatSocketHandler();
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {

    }
}

4.2.7 编写 MessageController 控制器类 MessageController.java 代码如下:

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.MessageService;
import com.example.dto.ChatMessage;
import com.example.utils.SerializationUtils;
import com.fasterxml.jackson.databind.ObjectMapper;

@RestController
public class MessageController {

    @Autowired
    private MessageService service;

    @Autowired
    private ObjectMapper mapper;

    @PostMapping("/sendMessage")
    public ResponseEntity sendMessage(@RequestParam("username") String username,
                                      @RequestParam("msg") String msg) {
        List headers = new ArrayList<>();
        headers.add("@type:" + ChatMessage.TYPE_MESSAGE);
        headers.add("user:" + username);
        headers.add("sentAt:" + LocalDateTime.now().toString());
        ChatMessage message = new ChatMessage("", "", headers, msg);

        service.publish(SerializationUtils.serialize(mapper.valueToTree(message)));

        return ResponseEntity.ok().body("{\"status\": \"success\"}");
    }
}

4.2.8 编写 MessageService 服务类 MessageService.java 代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Service;

import com.example.dto.ChatMessage;
import com.example.utils.SerializationUtils;

@Service
public class MessageService {

    @Autowired
    private MessageChannel publisher;

    @Value("${rabbitmq.exchange}")
    private String exchangeName;

    public void publish(byte[] data) {
        try {
            publisher.send(
                    MessageBuilder
                           .withPayload(SerializationUtils.deserialize(data))
                           .setHeader("destination", exchangeName)
                           .build());
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }
}

4.2.9 编写 ChatMessage DTO 对象 ChatMessage.java 代码如下:

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonProperty;

public class ChatMessage {

    public static final String TYPE_JOIN = "join";
    public static final String TYPE_LEAVE = "leave";
    public static final String TYPE_MESSAGE = "message";

    private String id;
    private String type;
    private List headers;
    private String body;

    public ChatMessage() {

    }

    public ChatMessage(String id, String type, List headers, String body) {
        this.id = id;
        this.type = type;
        this.headers = headers!= null? Collections.unmodifiableList(new ArrayList<>(headers)) : null;
        this.body = body;
    }

    @JsonProperty("_id")
    public String getId() {
        return id;
    }

    @JsonProperty("_type")
    public String getType() {
        return type;
    }

    @JsonProperty("_headers")
    public List getHeaders() {
        return headers;
    }

    @JsonProperty("_body")
    public String getBody() {
        return body;
    }

    public static ChatMessage join(String user) {
        List headers = new ArrayList<>();
        headers.add("@type:" + TYPE_JOIN);
        headers.add("user:" + user);
        headers.add("joinedAt:" + LocalDateTime.now().toString());

        return new ChatMessage(null, TYPE_JOIN, headers, "");
    }

    public static ChatMessage leave(String user) {
        List headers = new ArrayList<>();
        headers.add("@type:" + TYPE_LEAVE);
        headers.add("user:" + user);
        headers.add("leavedAt:" + LocalDateTime.now().toString());

        return new ChatMessage(null, TYPE_LEAVE, headers, "");
    }

    public static ChatMessage message(String author, String content) {
        List headers = new ArrayList<>();
        headers.add("@type:" + TYPE_MESSAGE);
        headers.add("author:" + author);
        headers.add("createdAt:" + LocalDateTime.now().toString());

        return new ChatMessage(null, TYPE_MESSAGE, headers, content);
    }

    public Map extractHeaderValues() {
        Map result = null;

        if (headers!= null &&!headers.isEmpty()) {
            result = headers.stream()
                          .filter(header -> header.contains(":"))
                          .map(header -> header.split(":", 2))
                          .collect(Collectors.toMap(parts -> parts[0].trim(), parts -> parts[1].trim()));
        }

        return result;
    }
}

4.2.10 编写 SerializationUtils 工具类 SerializationUtils.java 代码如下:

import java.io.IOException;

import org.apache.commons.lang3.SerializationUtils;

public class SerializationUtils {

    public static byte[] serialize(Object obj) {
        return SerializationUtils.serialize((Serializable) obj);
    }

    public static  T deserialize(byte[] bytes) {
        return (T) SerializationUtils.deserialize(bytes);
    }

}

4.2.11 编写 JsonSerializer 对象序列化器 JsonSerializer.java 代码如下:

```java import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map;

import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode;

/**

  • Utility methods for serializing and deserializing Java objects as JSON strings. Provides customization points to support various types like collections, maps etc.<|im_sep|>

你可能感兴趣的:(Python,机器学习,自然语言处理,人工智能,语言模型,编程实践,开发语言,架构设计)