问题
企业中使用消息中间件面临的常见问题:
1.消息莫名其妙的没了,也不知道什么情况,有丢消息的问题。
2.发送者没法确认是否发送成功,消费者处理失败也无法反馈。
消息可靠性的二种方式
1.事务,利用AMQP协议的一部分,发送消息前设置channel为tx模式(channel.txSelect();),如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。(大大得削弱消息中间件的性能)
2.消息确认(publish confirms),设置管道为confirmSelect模式(channel.confirmSelect();)
生产者与broker之间的消息确认称为public confirms,public confirms机制用于解决生产者与Rabbitmq服务器之间消息可靠传输,它在消息服务器持久化消息后通知消息生产者发送成功。
发送确认(publisher confirms)
RabbitMQ java Client实现发送确认
deliveryTag(投递的标识),当Channel设置成confirm模式时,发布的每一条消息都会获得一个唯一的deliveryTag,任何channel上发布的第一条消息的deliveryTag为1,此后的每一条消息都会加1,deliveryTag在channel范围内是唯一的。
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
public class Send {
static Long id = 0L;
static TreeSet<Long> tags = new TreeSet<>();
public static Long send(Channel channel,byte[] bytes) throws Exception{
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
contentEncoding("UTF-8").build();
channel.basicPublish("zhihao.direct.exchange","zhihao.miao.order",properties,bytes);
return ++id;
}
public static void main(String[] args) throws Exception{
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUri("amqp://zhihao.miao:123456@192.168.1.131:5672");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//是当前的channel处于确认模式
channel.confirmSelect();
//使当前的channel处于事务模式,与上面的使channel处于确认模式使互斥的
//channel.txSelect();
/**
* deliveryTag 消息id
* multiple 是否批量
* 如果是true,就意味着,小于等于deliveryTag的消息都处理成功了
* 如果是false,只是成功了deliveryTag这一条消息
*/
channel.addConfirmListener(new ConfirmListener() {
//消息发送成功并且在broker落地,deliveryTag是唯一标志符,在channek上发布的消息的deliveryTag都会比之前加1
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("=========deliveryTag==========");
System.out.println("deliveryTag: "+deliveryTag);
System.out.println("multiple: "+multiple);
//处理成功发送的消息
if(multiple){
//批量操作
for(Long _id:new TreeSet<>(tags.headSet(deliveryTag+1))){
tags.remove(_id);
}
}else{
//单个确认
tags.remove(deliveryTag);
}
System.out.println("未处理的消息: "+tags);
}
/**
* deliveryTag 消息id
* multiple 是否批量
* 如果是true,就意味着,小于等于deliveryTag的消息都处理失败了
* 如果是false,只是失败了deliveryTag这一条消息
*/
//消息发送失败或者落地失败
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("===========handleNack===========");
System.out.println("deliveryTag: "+deliveryTag);
System.out.println("multiple: "+multiple);
}
});
/**
* 当Channel设置成confirm模式时,发布的每一条消息都会获得一个唯一的deliveryTag
* deliveryTag在basicPublish执行的时候加1
*/
Long id = send(channel,"你的外卖已经送达".getBytes());
tags.add(id);
//channel.waitForConfirms();
id =send(channel,"你的外卖已经送达".getBytes());
tags.add(id);
//channel.waitForConfirms();
id = send(channel,"呵呵,不接电话".getBytes());
tags.add(id);
//channel.waitForConfirms();
TimeUnit.SECONDS.sleep(10);
channel.close();
connection.close();
}
}
channel.waitForConfirms():表示等待已经发送给broker的消息act或者nack之后才会继续执行。
channel.waitForConfirmsOrDie():表示等待已经发送给broker的消息act或者nack之后才会继续执行,如果有任何一个消息触发了nack则抛出IOException。
总结
生产者与broker之间的消息可靠性保证的基本思路就是
- 当消息发送到broker的时候,会执行监听的回调函数,其中deliveryTag是消息id(在同一个channel中这个数值是递增的,而multiple表示是否批量确认消息。
- 在生产端要维护一个消息发送的表,消息发送的时候记录消息id,在消息成功落地broker磁盘并且进行回调确认(ack)的时候,根据本地消息表和回调确认的消息id进行对比,这样可以确保生产端的消息表中的没有进行回调确认(或者回调确认时网络问题)的消息进行补救式的重发,当然不可避免的就会在消息端可能会造成消息的重复消息。针对消费端重复消息,在消费端进行幂等处理。(丢消息和重复消息是不可避免的二个极端,比起丢消息,重复消息还有补救措施,而消息丢失就真的丢失了。
Spring AMQP实现实现发送确认
示列
定义消息内容
public class Order {
private String orderId;
private String createTime;
private double price;
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
配置项:
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MQConfig {
@Bean
public ConnectionFactory connectionFactory(){
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setUri("amqp://zhihao.miao:123456@192.168.1.131:5672");
factory.setPublisherConfirms(true);
return factory;
}
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
return rabbitAdmin;
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 唯一标识,有了这个唯一标识,我们就知道可以确认(失败)哪一条消息了
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("=====消息进行消费了======");
if(ack){
System.out.println("消息id为: "+correlationData+"的消息,已经被ack成功");
}else{
System.out.println("消息id为: "+correlationData+"的消息,消息nack,失败原因是:"+cause);
}
}
});
return rabbitTemplate;
}
}
启动应用类:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ComponentScan
public class Application {
public static Order createOrder(){
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString());
order.setCreateTime(LocalDateTime.now().toString());
order.setPrice(100L);
return order;
}
public static void saveOrder(Order order){
//入库操作
System.out.println("入库操作");
}
public static void main(String[] args) throws Exception{
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
Order order = createOrder();
saveOrder(order);
ObjectMapper objectMapper = new ObjectMapper();
byte[] body = objectMapper.writeValueAsBytes(order);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("json");
Message message = new Message(body,messageProperties);
System.out.println("id: "+order.getOrderId());
//指定correlationData的值
rabbitTemplate.send("zhihao.direct.exchange","zhihao.miao.order",message,new CorrelationData(order.getOrderId().toString()));
TimeUnit.SECONDS.sleep(10);
context.close();
}
}
控制台打印:
入库操作
id: 11bc9eb3-fbcb-4777-9596-b6f6db81cafc
十月 22, 2017 7:14:14 下午 org.springframework.amqp.rabbit.connection.CachingConnectionFactory createBareConnection
信息: Created new connection: connectionFactory#50ad3bc1:0/SimpleConnection@4efc180e [delegate=amqp://zhihao.miao@192.168.1.131:5672/, localPort= 61095]
=====消息进行消费了======
消息id为: CorrelationData [id=11bc9eb3-fbcb-4777-9596-b6f6db81cafc]的消息,已经被ack成功
原理其实和java client是一样的,我们在发送消息的时候落地本地的消息表(有表示confirm字段),然后进行回调确认的方法中进行状态的更新,最后轮询表中状态不正确的消息进行轮询重发。
步骤
- 在容器中的ConnectionFactory实例中加上setPublisherConfirms属性
factory.setPublisherConfirms(true); - 在RabbitTemplate实例中增加setConfirmCallback回调方法。
- 发送消息的时候,需要指定CorrelationData,用于标识该发送的唯一id。
对比与java client的publisher confirm:
1.spring amqp不支持批量确认,底层的rabbitmq java client方式支持批量确认。
2.spring amqp提供的方式更加的简单明了。
参考资料
关于另外一种Publisher Confirms事务机制可以参考下面这篇博客,很是简单
深入学习RabbitMQ(二):AMQP事务机制