事务消息与数据库的事务类似,只是MQ中的消息是要保证消息是否全部发送成功,防止丢失消息的一种策略。
RabbitMQ有两种的方式来解决这个问题:
1、通过AMQP提供的事务机制实现
2、使用发送者确认模式实现
开启事务
/**启动事务,启动事务以后所有写入队列
* 中的消息,必须显示调用事务的txCommit()函数
* 来提交事务获取txRollback()回滚事务
* */
/**开启事务
* */
channel.txSelect();
提交事务
/**提交事
* */
channel.txCommit();
注意:如果调用channel.txSelect()函数来开启事务,那么必须显示的调用事务的提交函数,否则消息不会进入到消息队列中
回滚事务
/**回滚事务
* */
channel.txRollback();
注意:回滚事务必须在channel关闭之前。
案例
发送方
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("transactionQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("directTransactionExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("transactionQueue",
"directTransactionExchange","transactionRoutingKey");
/**发送消息
* */
String message="事务测试消息";
/**启动事务,启动事务以后所有写入队列
* 中的消息,必须显示调用事务的txCommit()函数
* 来提交事务获取txRollback()回滚事务
* */
/**开启事务
* */
channel.txSelect();
/**发送消息到队列
* */
channel.basicPublish("directTransactionExchange",
"transactionRoutingKey",null,
message.getBytes("utf-8"));
/**提交事务
* */
channel.txCommit();
System.out.println("消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally{
if(channel!=null){
try {
/**回滚事务
* */
channel.txRollback();
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
接收方
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("transactionQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("directTransactionExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("transactionQueue",
"directTransactionExchange","transactionRoutingKey");
/**开启事务
* 当消费者开启事务后,即使不作为事务的
* 提交,那么依然可以获取队列中的事务的消息
* 并且消息从队列中移除掉
* 注意:
* 此时的事务暂时对消息的接收者没有影响
* */
channel.txSelect();
/**获取消息
* */
channel.basicConsume("transactionQueue",
true,"",
new DefaultConsumer(channel){
/**获取队列中消息函数
* */
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message =new String(body);
System.out.println(message);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
消息的发送者确认模式
消息的发送者确认模式的使用和事务类似,也是通过channel进行发送确认的,但该模式和事务有着本质的区别就是发送消息丢失的时候不会像事务一样停止发送消息,而是补发消息直到消息发送到对方确认为止。
三种实现方式
方式1
channel.waitForConfirms()普通发送消息确认模式
该模式是不断的等待接收方来返回消息,如果有返回消息则说明消息发送成功
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("confirmQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("confirmExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("confirmQueue",
"confirmExchange","confirmRoutingKey");
/**发送消息
* */
String message="普通发送者确认模式,测试消息";
/**启动发送者确认模式
* */
channel.confirmSelect();
/**发送消息到队列
* */
channel.basicPublish("confirmExchange",
"confirmRoutingKey",null,
message.getBytes("utf-8"));
/**确认消息
* 堵塞线程等待服务器返回响应
* 如果服务器确认消费者已经发送完成
* 则返回true
* */
boolean flag= channel.waitForConfirms();
System.out.println("消息发送成功"+flag);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
boolean flag= channel.waitForConfirms();会堵塞线程等带服务器来返回确认消息。可以为这个函数指定一个毫秒值用于等待服务器的确认超时时间。如果抛出异常表示服务器出了问题,需要补发消息。无论是返回false还是抛出异常都有可能消息发送成功或者没有发送成功。
补发消息可以将消息缓存到Redis中稍后使用定时任务来补发,或者使用递归的方法来补发。
方式2
channel.waitForConfirmsOrDie()函数批量确认模式
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("confirmQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("confirmExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("confirmQueue",
"confirmExchange","confirmRoutingKey");
/**发送消息
* */
String message="普通发送者确认模式,测试消息";
/**启动发送者确认模式
* */
channel.confirmSelect();
/**发送消息到队列
* */
channel.basicPublish("confirmExchange",
"confirmRoutingKey",null,
message.getBytes("utf-8"));
/**批量消息确认
* */
channel.waitForConfirmsOrDie();
System.out.println("消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
channel.waitForConfirmsOrDie();该函数会同时向服务中确认之前当前通道中发送消息是否已经全部写入成功,该函数没有返回值,如果服务器中没有一条消息能够发送成功或者向服务器发送确认时服务不可访问都被认定为消息发送失败。可能消息发送成功,也可能消息没发送成功
channel.waitForConfirmsOrDie();也可以指定一个毫秒值来用于等带服务器的确认时间,如果超过这个时间就抛出异常,表示确认失败需要补发
注意:
批量确认消息比普通确认要快,但是如果一但出现了消息补发的情况,就不能确定是哪条消息需要补发,所以就会将本次发送的所有消息进行补发。
方式3
channel.addConfirmListener()异步监听发送确认模式。需要new ConfirmListener()来实现里面的回调函数
public void handleAck(long l, boolean b) throws IOException
该函数在消息确认后执行
参数 | 含义 |
long l | 为被确认的消息编号,从1开始自动递增用于标记当前第几个消息 |
boolean b | 为当前消息是否同时确认了多个。当为true时表示本次确认同时确认了多条消息。消息等于当前参数1的所有消息全部被确认了, 如果为false则表示值确认了当前编号的消息 |
public void handleNack(long l, boolean b) throws IOException
该函数是消息没有发送成功时执行
参数 | 含义 |
long l | 为被确认的消息编号,从1开始自动递增用于标记当前第几个消息 |
boolean b | 为当前消息是否同时没有确认多个,当为true时表示小于当前编号的所有消息可能都没有成功需要进行消息的补发 当为false时表示当前编号的消息没法发送成功需要补发 |
由于该方式是异步监听所以需要在监听打开后才能发送消息
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("confirmQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("confirmExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("confirmQueue",
"confirmExchange","confirmRoutingKey");
/**发送消息
* */
String message="普通发送者确认模式,测试消息";
/**启动发送者确认模式
* */
channel.confirmSelect();
/**添加监听器
* */
channel.addConfirmListener(new ConfirmListener() {
/**消息确认以后的回调函数
* */
public void handleAck(long l, boolean b) throws IOException {
System.out.println("消息编号:"+l+" 消息是否多条确认: "+b);
}
/**消息没有确认的回调函数
* 如果该函数执行表示消息没有发送成功
* 需要补发
* */
public void handleNack(long l, boolean b) throws IOException {
System.out.println("消息编号:"+l+" 消息是否多条确认: "+b);
}
});
for (int i = 0; i < 10; i++) {
/**发送消息到队列
* */
channel.basicPublish("confirmExchange",
"confirmRoutingKey",null,
message.getBytes("utf-8"));
}
System.out.println("消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
注意:由于这个是异步操作所以当关闭了channel的时候会导致消息确认不全
执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TpzZSEYD-1614324944517)(E:\TangJiachang\TypreaPic\RabbitMQ异步消息确认.jpg)]
方式4
消费者确认模式为了保证消息从队列中到达消费者,RabbitMQ提供了消息确认机制。消费者在说明队列时,可以指定noAck参数时,当noAck=false时,RabbitMQ会等待消息者显式发回ack信号后才从内存中移除消息(如果消息是持久化的化就保存在磁盘中)
channel.basicConsume("confirmQueue",
true,"",
new DefaultConsumer(channel)
参数2为消息确认机制,true表示自动消息确认,确认消息后消息回从队列中移除掉。当读取完以后就会自动确认。如果为false为手动确认消息
手动确认主要使用以下函数:
函数 | 含义 |
basicAck() | 用于肯定确认,muiltiple参数用于多个消息确认 |
basicRecover() | 是路由不成功的消息可以使用recovery重新发送到队列中 |
basicReject() | 一次只能拒绝接受消息,可以设置是否返回到队列中还是丢失 |
basicNack() | 可以拒绝多条消息 |
消费者监听消息,如果消费者中的代码出现异常,就会导致自动确认的消息会丢失掉
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("confirmQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("confirmExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("confirmQueue",
"confirmExchange","confirmRoutingKey");
/**获取消息
* */
channel.basicConsume("confirmQueue",
false,"",
new DefaultConsumer(channel){
/**获取队列中消息函数
* */
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message =new String(body);
System.out.println(message);
/**获取消息编号
* 根据消息的编号来确认消息
* */
long tag= envelope.getDeliveryTag();
/**获取当前匿名内部类中获取channel
* */
Channel channel1=this.getChannel();
/**手动确认消息已经处理了
* */
channel1.basicAck(tag,true);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
事务对接受确认的影响
当开启事务的时候,如果没有提交事务就导致消费者读取到的消息在消息队列中没有被移除掉
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.79.141");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection=null;
Channel channel=null;
try {
connection=factory.newConnection();
channel=connection.createChannel();
channel.queueDeclare("confirmQueue",true,
false,false,null);
/**创建交换机
* */
channel.exchangeDeclare("confirmExchange",
"direct",true);
/**将交换机和队列进行绑定
* */
channel.queueBind("confirmQueue",
"confirmExchange","confirmRoutingKey");
/**开启事务
* */
channel.txSelect();
/**获取消息
* */
channel.basicConsume("confirmQueue",
false,"",
new DefaultConsumer(channel){
/**获取队列中消息函数
* */
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message =new String(body);
System.out.println(message);
/**获取消息编号
* 根据消息的编号来确认消息
* */
long tag= envelope.getDeliveryTag();
/**获取当前匿名内部类中获取channel
* */
Channel channel1=this.getChannel();
/**手动确认消息已经处理了
* */
channel1.basicAck(tag,true);
channel1.txCommit();
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
boolean isRedeliver=envelope.isRedeliver();当返回false表示消息没有确认过,返回true表示消息确认过了。使用该函数可以防重复处理