作者:禅与计算机程序设计艺术
基于消息队列技术构建可靠、异步、分布式、高效的系统架构是一个比较成熟的方案,并已经得到了广泛应用。在实际业务中,消息队列作为一个基础设施组件也逐渐成为越来越重要的组成部分。那么如何利用好消息队列来构建一个可靠、异步、高效的事件驱动架构呢?本文将会详细介绍消息队列与事件驱动架构的相关概念,以及结合实践案例,分享一些建设性意见。
2.1 消息队列(Message Queue) 消息队列是一种生产消费模型,也就是生产者把消息放入队列,然后消费者从队列中取出消息进行处理。消息队列可以解决生产者和消费者之间的耦合关系,生产者不用等待消费者处理完毕就可以继续发送新的数据,同时也可以实现负载均衡。
2.2 异步通信 异步通信指的是无需等待回复就可继续下一步工作的通信方式。采用异步通信时,生产者向队列发送消息后不必等待消费者的响应而继续执行自己的任务,这往往能够提升整体的处理性能。同时由于消费者处理消息的速度不同,可以根据自身的处理能力和消息数量动态调整自己的线程池大小,因此也避免了线程过多导致的资源浪费。
2.3 分布式事务 分布式事务指的是在多个节点间的操作要么都成功,要么都失败。举个例子,在电商网站下单过程中需要扣除用户的余额,如果扣除前的余额不足或扣款失败,则整个订单交易失败。分布式事务就是为了保证在多个数据库或微服务节点之间数据的一致性而设计的一种协议。目前国内许多公司都在探索如何利用消息队列来构建分布式事务,并且兼顾系统可用性和数据一致性。
2.4 事件驱动架构 事件驱动架构是由事件源触发的事件流派生出的架构模式。它通过事件的监听与发布机制来实现模块间的解耦合,在实际开发中,事件驱动架构通常借助于消息队列来实现。
2.5 数据流动方向 消息队列和事件驱动架构在传统的 SOA 服务架构中占据着不同角色。SOA 是面向服务的架构,主要是用来描述企业应用系统的各个构件以及它们之间的交互行为。其中最核心的一块就是服务调用,调用方通过远程过程调用的方式调用服务提供方的接口,通过网络进行通信。而在事件驱动架构中,主要依赖于事件消息的传递,事件源产生事件,通过消息中间件传递到订阅者处进行处理。
3.核心算法原理及操作步骤 3.1 容错机制 消息队列中有两种类型的异常情况,一是队列的宕机,即消息队列中的消息不能被正常获取和处理;二是消费者的宕机,即消费者在处理消息的过程中出现错误,导致消息无法正常的发布到其他消费者手上。为了应对以上两种异常情况,消息队列通常都会提供三种容错机制:
具体的容错机制操作流程图如下所示:
3.2 集群部署 在实际项目中,通常消息队列服务器通常部署在不同的机器上以提高容灾能力和扩展性。如果只有一个消息队列服务器的话,随着消息量的增长,服务器的压力也会随之增大,甚至可能发生崩溃的情况。为了避免这种情况,消息队列服务器可以做集群部署。一般来说,集群中共同担任消息队列服务器的角色叫做主节点,每台机器只能有一个主节点,其他的节点分别称为从节点。主节点负责接收外部客户端发送来的请求,并转发给从节点进行处理;从节点只负责处理消息队列中的消息,并将结果返回给主节点。如果主节点发生故障,则可以把它的从节点升级为新的主节点,而不会影响生产环境的运行。具体集群部署架构如下图所示:
3.3 消息持久化 为了防止消息队列服务器因硬盘损坏、服务器故障等各种原因而造成消息丢失,消息队列通常都会提供消息持久化功能。消息持久化是指将队列中的消息保存在磁盘上,以便出现硬件故障、服务器崩溃、甚至宕机时仍然可以恢复并继续工作。消息持久化可以分为两种类型:
具体的消息持久化方式架构如下图所示:
3.4 消息投递 消息投递指的是消息的发布者将消息放入消息队列之后,消息是否能够被正确地投递到对应的消费者手上。消息投递可以分为两种类型:
具体的消息投递方式架构如下图所示:
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
以上配置中:
以上配置中没有涉及任何安全机制,建议不要在生产环境中打开。如有必要,可以通过 SSL 加密传输和 SASL 验证等安全机制进行安全配置。
4.1.3 测试 ActiveMQ 是否安装成功 可以在浏览器中输入:http://localhost:8161 来测试是否安装成功。如果显示 ActiveMQ Web Console,则表示安装成功。点击 Overview 查看各项指标。
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
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;
/**