目的
之前,我们学习了利用websocket进行订阅和发布的方式,连接数是N(N-1)/2,随着节点的增加,连接数迅速增加,只适合于小的集群。我们利用RabbitMQ重写一下事件广播。我们只需重写ClusterEventMulticaster,改为AMQPEventMulticaster。作为小例子演示,我们简单地选择fanout方式,由节点对收到的消息进行过滤。
AMQPEventMulticaster的代码
public class AMQPEventMulticaster extends SimpleApplicationEventMulticaster{
private static final Logger log = LogManager.getLogger();
private static final String EXCHANGE_NAME = "AMQPMessagingTest";
private static final String HEADER = "X-Wei-Cluster-Node";
private final BasicProperties.Builder builder = new BasicProperties.Builder();
private Connection amqpConnection;
private Channel amqpChannel;
private String queueName;
@Override
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
try{
super.multicastEvent(event, eventType);
}finally{
try{
if(event instanceof ClusterEvent && !((ClusterEvent)event).isRebroadcasted())
this.publishClusteredEvent((ClusterEvent)event);
}catch(Exception e){
log.error("Failed to broadcast distributable event to cluster.",e);
}
}
}
protected void publishClusteredEvent(ClusterEvent event) throws IOException{
this.amqpChannel.basicPublish(EXCHANGE_NAME, "", this.builder.build(),
SerializationUtils.serialize(event));
}
@PostConstruct
public void setupRabbitConnection() throws IOException, TimeoutException{
try{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("191.8.1.107");
factory.setUsername("test");
factory.setPassword("123456");
this.amqpConnection = factory.newConnection();
this.amqpChannel = this.amqpConnection.createChannel();
this.amqpChannel.exchangeDeclare(EXCHANGE_NAME, "fanout");
this.queueName = this.amqpChannel.queueDeclare().getQueue();
this.amqpChannel.queueBind(this.queueName, EXCHANGE_NAME, "");
Map<String, Object> headers = new Hashtable<>();
headers.put(HEADER, this.queueName);
this.builder.headers(headers);
Consumer consumer = new DefaultConsumer(amqpChannel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
Object header = properties.getHeaders().get(HEADER);
if(header == null || !header.toString().equals(queueName)){
ClusterEvent event = (ClusterEvent)SerializationUtils.deserialize(body);
event.setRebroadcasted();
multicastEvent(event,null);
}
}
};
this.amqpChannel.basicConsume(this.queueName, true, consumer);
}catch(Exception e){
log.error(e);
}
}
@PreDestroy
public void shutdownRabbitConnection() throws IOException, TimeoutException {
this.amqpChannel.close();
this.amqpConnection.close();
}
}
这和之前学习的利用websocket进行事件广播,和RabbitMQ的学习并没有太大的区别。唯一不同的是,注意到这次将临时生成的队列名字放在自定义的Header中,相关代码如下:
Map<String, Object> headers = new Hashtable<>();
headers.put(X-Wei-Cluster-Node, queueName);
BasicProperties.Builder builder = new BasicProperties.Builder()
builder.headers(headers);
basicPublish(EXCHANGE_NAME, "", this.builder.build(), SerializationUtils.serialize(event));
使用消息队列需要小心
在多个节点之间进行消息的传递或者广播,使用同步还是异步,消息在不同节点之间接收存在时延是否对系统有系统,发送方是否需要知道消息已经正确被处理?这些都需要在设计的时候考虑。在《微服务设计》一书中,作者提到过一个教训。他们设计了一个消息队列,当超时不能完成时,消息会被重新放入队列中,将被重新处理。有一次,他们有某个消息,这个消息的会引发处理者worker崩溃,导致超时未能处理,消息重新放入队列,引发其他的worker崩溃,就这样,单点的故障被蔓延到整个系统。后来他们将这些处理有问题的(包括超时)的消息放入另一个队列中(作为消息医院)。类似我们自己在做项目的时候,会将告警发送到告警服务那里,先进行人工干预,看看有没有必要排入需求。使用消息队列,会涉及多个节点,多个相同的或者不同的模块,一般项目具有一定规模,因此需要小心和谨慎。