如何保证生产环境下,消息队列全链路数据不丢失

当使用消息队列的时候,投递消息到消息队列,然后从MQ消费消息的这个过程,如何保证数据不丢失. ***这个一个非常重要的生成环境下问题***
Q: 在你的项目中,对于消息队列的使用过程中, 很有可能因为一些异常的情况导致消息丢失, 那么对于消息,你是采用什么方式保证数据100%不会丢失的?
前面已经说到了. 对于生产者, 可以通过设置队列的持久化与消息的持久化将信息保存到磁盘,当MQ服务重启之后就会将之前存在于MQ的内容进行恢复. 对于消费者,可以通过设置收到ACK保证消息不会在消费者接收到之后马上就被删除. 但是这些方式只能尽量的保证消息不丢失, 但是无法保证消息百分百不丢失.
如何保证生产环境下,消息队列全链路数据不丢失
文章图片

当前我们需要解决的问题就是, 应该如何保证消息一定会持久化到磁盘.
对于这一部分,我们需要保证: 只要生产者成功的投递了消息,对于MQ就一定已经将这个消息持久化到了磁盘.
RabbitMQ针对生产者投递数据丢失,已经提供了两种解决机制, 分别是 重量级的事务机制 与 轻量级的confirm机制.
事务机制:
采用类似事务的机制将消息投递到MQ,虽然可以保证消息不丢失,但是性能极差, 经测试可以导致MQ性能呈现几百倍的下降.

String message = "Hello RabbitMQ";

try {

//txSelect():开启事务

channel.txSelect();

for (int i = 0; i < 5; i++) {

channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, (message + i).getBytes("UTF-8"));

}

//txCommit():提交事务

channel.txCommit();

} catch (Exception e) {

//txRollback():事务回滚

channel.txRollback();

}

***此种方式性能极差,不采用***
confirm机制:
confirm模式需要基于channel进行设置, 一旦某个channel进入到confirm模式,那么在该channel下的所有消息都会被指派一个唯一id.一旦该消息被投递到队列之后,broker就会发送一个确认信息给生产者,如果队列与消息是可持久化的, 那么确认消息会等到消息成功写入到磁盘之后发出.
confirm的性能高,主要得益于它是异步的.生产者在将第一条消息发出之后等待确认消息的同时也可以继续发送后续的消息.当确认消息到达之后,就可以通过回调方法处理这条确认消息. 如果MQ服务宕机了,则会返回nack消息. 生产者同样在回调方法中进行后续处理.
开启confirm: channel.confirmSelect(); (不能与开启事务方法同时使用).
工作方式:
1.串行: 生产者每发送一条消息,都会调用waitForConfirms()等待broker确认消息
2.批量:生产者每发送一批消息,都会调用waitForConfirms()等待broker确认消息
3.异步: 提供回调方法.broker确定了一条或多条消息后都会调用此回调,并且不影响生产者继续发送后续消息. 不会让生产者处于等待状态. (建议使用)
public class Demo01 {

public static void main(String[] args)throws Exception {

ConnectionFactory factory = new ConnectionFactory();

factory.setUsername("guest");

factory.setPassword("guest");

factory.setVirtualHost("/");

factory.setHost("localhost");

factory.setPort(5672);


Connection conn = factory.newConnection();

Channel channel = conn.createChannel();

String exchangeName = "exchangeName";

String routingKey = "routingKey";

String queueName = "queueName";

channel.exchangeDeclare(exchangeName,"direct",true);

channel.queueDeclare(queueName,true,false,false,null);

channel.queueBind(queueName,exchangeName,routingKey);

byte [] messageBodyBytes = "你好,世界!" .getBytes();

//发送之前

//将消息写入到某一个存储空间,用来防止发送消息失败

try{

channel.confirmSelect(); // 开启confirm模式

long start= System.currentTimeMillis();

//设置监听器

channel.addConfirmListener(new ConfirmListener() {

public void handleAck(long deliveryTag, boolean multiple) throws IOException {

//删除之前临时存储空间中的消息

System.out.println("ack:deliveryTag:"+deliveryTag+",multiple:"+multiple);

}

public void handleNack(long deliveryTag, boolean multiple) throws IOException {

//从临时存储空间中拿出刚才的消息,并重新发送

System.out.println("nack:deliveryTag:"+deliveryTag+",multiple:"+multiple);

}

});


for(int i = 0; i<100; i++) {//循环发消息

channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes);

}

}catch (Exception e){

e.printStackTrace();

}finally {

channel.close();

conn.close();

}

}

}

【如何保证生产环境下,消息队列全链路数据不丢失】对于上述实现. 通过设置监听器,如果broker返回ack,生产者就会调用handleAck(). 如果返回nack,则会调用handleNack()

confirm机制的高延迟性

当开启了confirm机制, MQ不会保证何时向生产者返回ack或unack消息, 因为MQ对于消息持久化到磁盘的操作是通过异步的方式实现. 其原理是, 生产者发送的消息会首先存放在rabbitMQ的内存中.经过几百毫秒的延迟之后,再一次性批量的将消息持久化到磁盘.
对于这种批量的操作主要是为了兼顾高并发下的写入操作,因为如果是逐条写入磁盘的操作,每一次都是一个同步操作,非常影响性能.
因为在开启confirm之后, 当发送了一条消息之后,可能需要间隔几百毫秒之后才会收到rabbit回传的消息. 这就是confirm机制的高延迟性

高并发下如何保证生产者消息不丢失

基于上述流程,需要考虑两个问题.

1. 每次发送消息到MQ,为了等待ack消息(因为可能出现失败操作), 所以必须把消息写入到一块存储空间. 这个存储空间不建议是内存, 因为在高并发情况下,可能每秒都会产生上万条数据.消息ack需要等待几百毫秒的话,就会导致大量消息积压在内存中,导致OOM

2. 绝对不能用同步写消息+等待ack方式进行消息发送. 这样会导致每次发送一个消息都需要等待几百毫秒的时间.

解决方案:
用来临时存放unack消息的存储需要承载高并发写入, 因为建议采用kv存储, mysql不是首选. 并且投递消息之后等待ack的过程必须是异步的,也就是类似上面那样的代码. 当生产者实例收到一个消息ack之后,就从kv存储中删除这条临时消息;收到一个消息nack之后,就从kv存储提取这条消息然后重新投递一次即可;也可以自己对kv存储里的消息做监控,如果超过一定时长没收到ack,就主动重发消息。

    推荐阅读