持久化

持久化可以提高RabbitMQ的可靠性,防止异常情况下的数据丢失。RabbitMQ的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。

交换器的持久化通过声明队列时将durable参数置为true实现。如果交换器不设置持久化,在RabbitMQ服务重启之后,相关交换器的元数据会丢失,但消息不会丢失,只是不能再将消息发送到这个交换器。一个长期使用的交换器来建议将其置为持久化的。

队列的持久化通过在声明队列时将durable参数置为true实现。如果队列不设置持久化,在RabbitMQ服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。

消息的持久化在消息的投递模式(BasicProperties中的DeliveryMode属性)设置为2即可实现消息的持久化。单单设置消息持久化而不设置队列的持久化毫无意义,只有同时设置队列和消息的持久化才能保证RabbitMQ服务重启后,消息依旧存在。

之前的示例出现过该代码:

var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
string message = "RabbitMQ Test"; //传递的消息内容
channel.BasicPublish("normalExchange", "normalKey", properties, Encoding.UTF8.GetBytes(message));

写入磁盘的速度比写入内存的速度慢得多,将所有的消息都设置为持久化,会严重影响RabbitMQ的性能(随机)。在选择是否要将消息持久化时,需要在可靠性和吐吞量之间做一个权衡。

将交换器、队列、消息都设置持久化并不能百分之百保证数据不丢失。消费者订阅消费队列时将autoAck参数设置为true,并在接收消息之后没来得及处理就发生宕机,这也算数据丢失。另外一种情况就是RabbitMQ接收到消息,在持久化(保存到磁盘)之前,服务节点发生了宕机、重启等异常情况,也会造成消息丢失。

生产者确认

在使用RabbitMQ的时候,我们还会考虑的一个问题就是消息的生产者将消息发送出去之后,消息有没有正确地到达服务器?默认情况下发送消息的操作不会返回任何信息给生产者,也就是默认情况下生产者是不能确认消息有没有正确地到达服务器。

RabbitMQ针对这个问题,提供了两种解决方式:

通过事务机制实现(与数据库中的事务概念并不相同);

通过发送方确认(publisher confirm)机制实现。

事务机制

事务机制相关的方法有:channel.TxSelect、channel.TxCommit和channel.TxRollback。channel.TxSelect将当前的信道设置成事务模式,channel.TxCommit用于提交事务,channel.TxRollback用于事务回滚。通过channel.TxSelect方法开启事务之后,发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ,如果在事务提交之前由于RabbitMQ异常崩溃或者其他原因抛出异常,通过执行channel.TxRollback方法来实现事务回滚。

示例代码:

using (var channel = connection.CreateModel())
{
channel.TxSelect();
try
{
//发送消息
channel.TxCommit();
}
catch (Exception)
{
channel.TxRollback();
}
}

对一个通道而言TxSelect只需执行一次,TxCommit和TxRollback则需要多次执行,如循环发送多条消息时BasicPublish、TxCommit和TxRollback方法包裹进循环内即可,TxSelect在循环外调用。

协议流转过程(左为事务确认右为事务回滚):

 

事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,但是事务机制(不管是确认还是回滚多了Tx.Select和Tx.Commit或Tx.Rollback四个步骤)会严重影响RabbitMQ的性能。事务机制在一条消息发送之后会使发送端阻塞,等待RabbitMQ回应之后才能继续发送下一条消息。

发送方确认机制

发送方确认(publisher confirm)机制是RabbitMQ提供的一个相比于事务机制的改进方案。

生产者将信道设置成confirm(确认)模式【调用channel.ConfirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式】,一旦信道进入confirm模式,在该信道上面发布的消息都会被指派一个唯一ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),告知生产者消息已经正确到达目的地。如果消息和队列是可持久化的,确认消息会在消息写入磁盘之后发出。RabbitMQ回传给生产者的确认消息中的DeliveryTag包含了确认消息的序号,RabbitMQ也可以设置channel.BasicAck方法中的multiple参数,表示这个序号之前的所有消息都已经确认。

confirm模式有普通confirm、批量confirm和异步confirm三种模式。

普通confirm

调用WaitForConfirmsOrDie等待消息返回。如果消息nack或者超时则该方法将抛出异常。异常的处理通常包括记录错误消息和/或重新尝试发送消息。

示例代码:

channel.QueueDeclare("confirm_queue", false, false, false, null);
var message = "Confirm Message";
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.BasicPublish("", "confirm_queue", properties, Encoding.UTF8.GetBytes(message));
// uses a 5 second timeout
channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));

消息的确认阻碍了所有后续消息的发布,显著减慢了发布速度,所以普通confirm是三种方式中性能最低的,但是是实现方式最简单的,对性能要求不高的程序很适合。

批量confirm

批量confirm表示我们发布一批消息,对整批消息执行等待确认。

批量confirm示例代码(一次确认10条消息):

for (int i = 0; i < 10; i++)
{
var msg = $"Confirm Message {i}";
channel.BasicPublish("", "confirm_queue", null, Encoding.UTF8.GetBytes(msg));
}
channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));

上面的代码只是最简单的示例,实际使用中可能需要增加消息缓存的代码,批量confirm异常时对这批消息进行重新发布,确认成功是情况消息缓存。

批量confirm出现返回Basic.Nack或者超时时,客户端需要将这一批次的消息全部重发会造成重复消息的情况,这一点需要注意。

异步confirm

异步confirm是为channel怎加两个事件BasicAcks和BasicNacks,分别用来处理RabbitMQ回传的Basic.Ack和Basic.Nack。两个事件的回调函数都有一个对应的 EventArgs 参数(BasicNackEventArgs类型) ,包含如下两个参数:

DeliveryTag:表示对应消息的序号

Multiple:表示是确认一条消息(false)还是确认当前序号前的所有消息(false)。

示例代码:

var outstandingConfirms = new ConcurrentDictionary<ulong, string>();

channel.BasicAcks += (sender, ea) =>
{
if (ea.Multiple)
{
var confirmed = outstandingConfirms.Where(k => k.Key <= ea.DeliveryTag);
foreach (var entry in confirmed)
{
outstandingConfirms.TryRemove(entry.Key, out _);
}
}
else
{
outstandingConfirms.TryRemove(ea.DeliveryTag, out _);
}
};
channel.BasicNacks += (sender, ea) =>
{
outstandingConfirms.TryGetValue(ea.DeliveryTag, out string body);
Console.WriteLine($"Message with body {body} has been nack-ed. Sequence number: {ea.DeliveryTag}, multiple: {ea.Multiple}");
//同理BasicAcks维护outstandingConfirms
};
var msg = "Async Msg";
outstandingConfirms.TryAdd(channel.NextPublishSeqNo, msg);
channel.BasicPublish("", "confirm_queue", null, Encoding.UTF8.GetBytes(msg));

上述代码定义了一个ConcurrentDictionary对象将消息和对应的消息序号关联起来,发布消息前通过channel.NextPublishSeqNo获取消息的序号,然后在回调确认时清理消息缓存字典。

消息分发

当RabbitMQ队列有多个消费者时,默认情况下,队列收到的消息将以轮询(round-robin)分发的方式发送给消费者,每条消息只发送给订阅列表里的一个消费者。如果有n个消费者,那么RabbitMQ会将第m条消息分发给第m%n(取余的方式)个消费者。如果个别消费者来不及消费那么多的消息,而其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这就造成整体应用吞吐量的下降,此时轮询分发机制就不是那么的优雅了。可以借助channel.BasicQos方法限制允许信道上的消费者所能保持的最大未确认消息的数量,未确认消息达到上限后RabbitMQ就不会再向这个消费者再发送消息,直至消费者确认了某条消息。

BasicQos方法参数介绍:

prefetchSize,预取大小服务器将传递的最大内容量(以八位字节为单位),如果不受限制,则为0。默认值:0。

prefetchCount,服务器一次请求将传递的最大邮件数,如果没有限制,则为0。调用此方法时,该值必填。默认值:0

global,是否将设置应用于整个频道,而不是每个消费者
默认值:false,应用于本身(一个消费者)
true:应用于整个频道

关于BasicQos的示例RabbitMQ官方文档Fair Dispatch部分有完成的示例代码:https://www.rabbitmq.com/tutorials/tutorial-two-dotnet.html

消息顺序性

消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。如果生产者发布的消息分别为msg1、msg2、msg3,那么消费者也是按照msg1、msg2、msg3的顺序进行消费的。

但是实际使用时会有很多情况打破消息顺序性,如生成者启用事务机制,某种原因进行了事务回滚由一个新线程补发消息以及消息设置了优先级等。消息的顺序必然不会和生产者发送消息的顺序一致。

如果要保证消息的顺序性,根据具体业务处理,比如在消息体内添加全局有序标识来实现。

Github

https://github.com/MayueCif/RabbitMQ

.Net RabbitMQ实战指南——进阶(二)的更多相关文章

  1. .Net RabbitMQ实战指南——进阶(一)

    备份交换器 备份交换器,英文名称为Alternate Exchange,简称AE.通过在声明交换器(调用channel.ExchangeDeclare方法)时添加alternate-exchange参 ...

  2. 【RabbitMQ 实战指南】一 RabbitMQ 开发

    1.RabbitMQ 安装 RabbitMQ 的安装可以参考官方文档:https://www.rabbitmq.com/download.html 2.管理页面 rabbitmq-management ...

  3. 【RabbitMQ 实战指南】一 延迟队列

    1.什么是延迟队列 延迟队列中存储延迟消息,延迟消息是指当消息被发送到队列中不会立即消费,而是等待一段时间后再消费该消息. 延迟队列很多应用场景,一个典型的应用场景是订单未支付超时取消,用户下单之后3 ...

  4. 学习笔记《Java多线程编程实战指南》二

    2.1线程属性 属性 属性类型及用途  只读属性  注意事项 编号(id) long型,标识不同线程  是  不适合用作唯一标识 名称(name) String型,区分不同线程  否  设置名称有助于 ...

  5. Java多线程编程模式实战指南(二):Immutable Object模式

    多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁.而锁本身又会带来一些问题和开销.Immutable Object模式使得我们可以在不使用锁的情况下,既保证共享变量访问的线 ...

  6. Java多线程编程模式实战指南(二):Immutable Object模式--转载

    本文由本人首次发布在infoq中文站上:http://www.infoq.com/cn/articles/java-multithreaded-programming-mode-immutable-o ...

  7. 【RabbitMQ 实战指南】一 死信队列

    1.死信队列 DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器.当消息在一个队列中变成死信(dead message)之后,它能被发送到另一个交换器中,这个交换器就是DL ...

  8. 【RabbitMQ 实战指南】一 过期时间TTL

    RabbitMQ 可以对消息和队列设置过期时间(TTL) 1.设置消息的TTL 目前有两种方式可以设置消息的TTL 第一种方式是通过队列属性设置,队列中所有消息都有相同的过期时间 第二种方式是对消息本 ...

  9. 【RabbitMQ 实战指南】一 RabbitMQ入门

    1.消息中间件 1.1.什么是消息中间件 消息中间件(Message Queue Middleware,简称 MQ)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通道来进行分布式系 ...

随机推荐

  1. PowerShell-6.文件操作

    1.显示文本内容 Get-Content "°C:\\Program Files (x86)\\PsUpdate\\b.dat" 2.得到b.dat文件内容,然后把里面的所有'C' ...

  2. 【JavaScript】【dp】Leetcode每日一题-解码方法

    [JavaScript]Leetcode每日一题-解码方法 [题目描述] 一条包含字母 A-Z 的消息通过以下映射进行了 编码 : 'A' -> 1 'B' -> 2 ... 'Z' -& ...

  3. Gentoo 后的几个细节的完善

    Gentoo 后的几个细节的完善 目录 Gentoo 后的几个细节的完善 细节一:引导分区与 cdrom 开机正确挂载 细节二:可预见的命名规则的网络接口名称改为传统的 eth0 细节三:为管理员用户 ...

  4. Linux 中如何使用 IP 命令

    老版本的 Linux 中都是使用 ifconfig 命令检查和配置网络接口,但是该命令目前已经没有维护了,取而代之的是 ip 命令 ip 命令和 ifconfig 命令很相似,但是 相比起来,ip命令 ...

  5. Vue(1):用Vue-cli构建Vue3项目

    使用Vue-cli构建Vue3项目 1.检查node版本 node -v 以上node版本位14.15.0满足Vue3项目的创建条件(Vu3需要node 版本8以上) 为什么需要安装node? vue ...

  6. sscanf的应用

    1.提取字符串 2.提取指定长度的字符串 3.提取指定字符为止的字符串 4.取仅包含指定字符集的字符串 5.取到指定字符集为止的字符串 #include <stdio.h> int mai ...

  7. OO随笔之魔鬼的第一单元——多项式求导

    OO是个借助Java交我们面向对象的课,可是萌新们总是喜欢带着面向过程的脑子去写求导,然后就是各种一面(main)到底.各种方法杂糅,然后就是被hack的很惨. 第一次作业:萌新入门面向对象 题目分析 ...

  8. Java7中Switch为什么只支持byte、short、char、int、String

    Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进.到目前为止switch支持这样几种数据类型:byte short int char String .但是,作 ...

  9. Nginx导航

    简介 最近都在弄微服务的东西,现在来记录下收获.我从一知半解到现在能从0搭建使用最大的感触有两点 1.微服务各大组件的版本很多,网上很多博客内容不一定适合你的版本,很多时候苦苦琢磨都是无用功 2.网上 ...

  10. Jmeter 设置中文

    1.正常启动jmeter.bat 2.点击[Options]选项,弹出下拉菜单选择[Choose Language]选项3.选择[Choose Language]选项,弹出下一级菜单选择[Chines ...