开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情
丢消息原因
一条消息从生产到被消费,经过MQ,生产者、消费者这几个环节。
生产者丢失
生产者往MQ中写数据,可能出现网络故障,消息没到MQ内部,生产者自己不知道,没有捕获异常,那么这条消息就丢失了。
或者当消息刚到MQ,MQ坏掉了,但是生产者自己也不知道,导致消息丢失
MQ丢失
消息到MQ内部后,消息会存在内存中,然后再持久化到磁盘。如果在消息处于内存没到磁盘这个阶段,MQ所在服务器寄了,那消息就丢失了。
当然,消息存到磁盘,但是磁盘没有备份,损坏了也会导致消息丢失。
消费者丢失
消费者的 ack 没设置或者设置了 auto 选项,那么消费者获取到消息后会第一时间直接 offset 到MQ,MQ就认为整个流程结束了。如果在这个时候,消费者没有消费完成消息,即处理消息对应的业务逻辑,机器寄了,那消息丢失了。
保证不丢失
生产者环节
有两种方式可以解决:
- 通过事物机制解决
- 通过发送方确认机制实现
事物机制
用 channel.txSelect 开启事务,使用 channel.txCommit 和 channel.txRollback 分别用来提交事务和回滚事务。
与数据库的事务有稍许不同,数据库每次都需要打开事务,且最后与之对应的有commit或者rollback,而RabbitMQ中channel中的事务只需要开启一次,可以多次commit或者rollback
代码示例
config
@Component
public class RabbitConfig {
@Autowired
private ConfigurableApplicationContext applicationContext;
public static final String TEST_ACK = "test-ack";
public static final String TEST_CONFIRM = "confirm.queue";
public static final String CONFIRM_EXCHANGE = "confirm.exchange";
// 获取RabbitMQ服务器连接
public Connection getConnection() {
Connection connection = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(applicationContext.getEnvironment().getProperty("spring.rabbitmq.addresses"));
factory.setPort(Integer.parseInt(applicationContext.getEnvironment().getProperty("spring.rabbitmq.port")));
factory.setUsername(applicationContext.getEnvironment().getProperty("spring.rabbitmq.username"));
factory.setPassword(applicationContext.getEnvironment().getProperty("spring.rabbitmq.password"));
connection = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
生产者
public void testChannel(){
Connection connection = rabbitConfig.getConnection();
try {
Channel channel = connection.createChannel();
//channel开启事务
channel.txSelect();
//创建队列
channel.queueDeclare(RabbitConfig.TEST_ACK, true, false, false, null);
//发送3条消息
String msgTemplate = "测试事务消息内容[%d]";
channel.basicPublish("", RabbitMQConfig.TEST_ACK, new AMQP.BasicProperties(), String.format(msgTemplate,1).getBytes(StandardCharsets.UTF_8));
channel.basicPublish("", RabbitMQConfig.TEST_ACK, new AMQP.BasicProperties(), String.format(msgTemplate,2).getBytes(StandardCharsets.UTF_8));
channel.basicPublish("", RabbitMQConfig.TEST_ACK, new AMQP.BasicProperties(), String.format(msgTemplate,3).getBytes(StandardCharsets.UTF_8));
//消息回滚
channel.txRollback();
//成功提交
channel.basicPublish("", RabbitMQConfig.TEST_ACK, new AMQP.BasicProperties(), String.format(msgTemplate,4).getBytes(StandardCharsets.UTF_8));
channel.txCommit();
// 5、释放资源
channel.close();
connection.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
消费者代码就不再展示,普通的接收并打印参数
上述生产者只发送了第四条消息,前三条都回滚了。
虽然事务可以保证消息一定被提交到服务器,而且在客户端编码方面足够简单。但是它也不是那么完美,在性能方面事务会带来较大的性能影响。如果对性能要求不是特别高的采用事务方式也是可以的,如果有性能方面的要求,可以使用Channel的确认机制。
confirm机制
通过 channel.confirmSelect 开启confirm 模式,confirm与事务机制不能共存。开启confirm 之后,每次发送消息后都会产生唯一的Id,如果消息投递成功 消费者 就会给 MQ客户端发送一个 ACK 确认,通过唯一ID我们就知道哪个消息发送成功。事务需要每次发送完成之后commit 或者 rollback ,导致不能连续发送,必须等到 MQ 响应。
confirm的发送和ack不冲突,相对异步,比事务的效率高
代码示例
生产者
public void testConfirm(){
Connection connection = rabbitConfig.getConnection();
try {
Channel channel = connection.createChannel();
//创建Exchange
channel.exchangeDeclare(RabbitConfig.CONFIRM_EXCHANGE, BuiltinExchangeType.DIRECT, true, false, new HashMap<>());
//创建Queue
channel.queueDeclare(RabbitConfig.TEST_CONFIRM , true, false, false, new HashMap<>());
//绑定路由
channel.queueBind(RabbitConfig.TEST_CONFIRM , RabbitConfig.CONFIRM_EXCHANGE, "confirm");
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("ack : deliveryTag = {},multiple = {}", deliveryTag, multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.error("nack : deliveryTag = {},multiple = {}", deliveryTag, multiple);
}
});
String msgTemplate = "测试消息[%d]";
for (int i = 0; i < 5; i++) {
channel.basicPublish(RabbitConfig.CONFIRM_EXCHANGE, "confirm", new AMQP.BasicProperties(), String.format(msgTemplate, i).getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
e.printStackTrace();
}
}
MQ环节
对于MQ服务器来说,要保证两个方面不出问题:1.消息成功持久化到磁盘;2.消息有多个副本
1.基于 Dledger 的 broker 主从架构,每个主 broker 需要挂至少 2 个 slave broker。
2.采用同步刷盘策略。
同步刷盘指 消息存到cache 后,将cache中的消息数据保存到磁盘,整个流程完成,返回success 给生产者。
异步刷盘指 消息存到cache 后,直接返回success 给生产者,同时进行将cache 数据保存到磁盘。
消费者
关闭ack 的 auto,手动处理完业务逻辑提交 offset。(详情看 RabbitMQ - Consumer Ack 这一篇)
不使用异步线程池处理消息。
总结
大多数保证消息不丢失就是取消异步操作,牺牲系统的性能:复杂生产者的逻辑,MQ服务器的吞吐量降低,消费者不能异步。具体结合实际业务来决定消息能不能丢失,比如:订单、交易等涉及钱的业务。