zoukankan      html  css  js  c++  java
  • Azure Messaging ServiceBus/Message Queue on Azure

    最近一直在总结Azure Messaging ServiceBus Messaging相关的技术:消息顺序、消息持久化、复杂对象消息的序列化、消息事务、消息回执等机制。

    感觉有必要补充一篇消息队列技术的基本概念,无论RabbitMQ、ActiveMQ还是其他,都有的一些基本概念、术语、机制,分享给大家,希望大家在搞消息队列技术的时候能够快速

    理解、排上用场。

    1. 消息生产者、消息者、队列、主题

    消息生产者Producer:发送消息到消息队列。

    消息消费者Consumer:从消息队列接收消息。

    消息队列Queue:一个先进先出的消息存储区域。消息按照顺序发送接收,一旦消息被消费处理,该消息将从队列中删除。

    主题Topic:一种支持消息多个订阅者的机制。

    2. 点对点/Queue消息队列模型

    一个生产者向一个特定的队列发送消息,一个消费者从该队列中接收消息;

    消息的生产者和消费者可以不同时处于运行状态。

    每一个成功处理的消息都由消息消费者签收确认(Acknowledge)。如图:

    3. 发布订阅消息模型Topic

    发布订阅模型中,支持向一个特定的消息主题Topic发布消息。0个或多个订阅者可能对接收来自特定消息主题的消息感兴趣。在这种模型下,发布者和订阅者彼此不知道对方,这种

    模式被概括为:

    多个消费者可以获得消息在发布者和订阅者之间存在时间依赖性,即必须先订阅,再发送消息,而后接收订阅的消息,这个操作顺序必须保证。如图:

    4. 消息的顺序性保证

    基于Queue消息模型,利用FIFO先进先出的特性,可以保证消息的顺序性。

    5. 消息的ACK机制

    即消息的Ackownledge确认机制,

    为了保证消息不丢失,消息队列提供了消息Acknowledge机制,即ACK机制,当Consumer确认消息已经被消费处理,发送一个ACK给消息队列,此时消息队列便可以删除这个消

    息了。如果Consumer宕机/关闭,没有发送ACK,消息队列将认为这个消息没有被处理,会将这个消息重新发送给其他的Consumer重新消费处理。

    6. 消息的同步和异步收发

    同步:消息的收发支持同步收发的方式。同时还有另一种同步方式:同步收发场景下,消息生产者和消费者双向应答模式,例如:张三写封信送到邮局中转站,然后李四从中转站获

    得信,然后在写一份回执信,放到中转站,然后张三去取,当然张三写信的时候就得写明回信地址。

    消息的接收如果以同步的方式(Pull)进行接收,如果队列中为空,此时接收将处于同步阻塞状态,会一直等到消息的到达。

    异步:消息的收发同样支持异步方式:异步发送消息,不需要等待消息队列的接收确认;异步接收消息,以Push的方式触发消息消费者接收消息。

    7. 消息的事务支持

    消息的收发处理支持事务,例如:在任务中心场景中,一次处理可能涉及多个消息的接收、处理,这应该处于同一个事务范围内,如果一个消息处理失败,事务回滚,消息重新回到

    队列中。

    8. 消息的持久化

    消息的持久化,对于一些关键的核心业务来说是非常重要的,启用消息持久化后,消息队列宕机重启后,消息可以从持久化存储恢复,消息不丢失,可以继续消费处理。

    9. 消息队列的高可用性

    在实际生产环境中,使用单个实例的消息队列服务,如果遇到宕机、重启等系统问题,消息队列就无法提供服务了,因此很多场景下,我们希望消息队列有高可用性支持,例如

    Azure ServiceBus Messaging就有高可用保障机制;RabbitMQ有镜像+HAProxy的高可用性方案,ActiveMQ也有基于LevelDB+ZooKeeper的高可用性方案。这点大家在

    实际技术选型时需要重要考虑,云端的MQ服务,比如Azure Messaging的SLA就承诺了99.9%, 也是非常推荐的。

    以上是最近这一年研究消息队列MQ技术的一些简单梳理和归纳,分享给大家,希望对大家有帮助。

    前段时间研究了Window Azure ServiceBus Messaging消息队列技术,搞了很多技术研究和代码验证,最近准备总结一下,分享给大家。

    首先,Windows Azure提供了两种类型的消息队列机制:Azure Queues和ServiceBus Queues。

    其中,Azure Queues,作为Azure Storage基础设施的一部分,提供了一套简单的基于Rest的Interface,面向不同的服务间提供可靠的、持久化的消息队列。

    ServiceBus Queues作为Azure Messaging基础设施的一部分,支持队列和发布订阅模式,支持WebService和集成模式

    本系列主要介绍ServiceBus Queues。关于二者的选型比较,各位可以参考:

    https://azure.microsoft.com/zh-cn/documentation/articles/service-bus-azure-and-service-bus-queues-compared-contrasted

    Service Bus(服务总线)支持两种不同的消息模式:relayed messaging and brokered messaging. 即:中继消息模式和代理消息模式。

    中继消息模式Relayed messaging,最大的特点就是:收发消息要求服务端和客户端同时在线,不支持异步模式。

    消息处理过程:

    当Client发送请求到ServiceBus,Azure的负载均衡器将请求路由到任意一个网关节点(Gateway Nodes)。如果请求是一个监听类的请求,网关节点创建一个新的
    中继(Relay)。如果请求是一个连接到指定中继(Relay)的请求,请求被转向指定中继所在的网关节点,这个网关节点发送一个交会的请求给监听Client,要求监听Client创建一个临时的通道来接收消息。当中继连接建立之后,

    Client之间通过网关节点(Gateway Node)就可以交换消息了。

    代理消息模式Borkered messaging:异步、解耦、消息发送者(Producer)和消费者(Consumer)不需要同时在线,消息存储在Broker中(代理),等待消息消费者消费处理。其核心组件包含:队列(Queue)、主题(Topic)、发布订阅等。

    消息处理过程:

    当Client发送请求到ServiceBus,Azure的负载均衡器将请求路由到任意一个网关节点(Gateway Nodes)。如果这个请求中包含一个消息实体(队列、主题、订阅),网关节点(Gateway Node)首先在网关存储(Gateway Store)中查找定位这个消息实体应该存储到哪个消息存储(Message Store)以及消息存储对应的消息代理节点(Messaging Broker),然后将消息发送到指定的消息代理节点,消息代理节点处理请求、存储消息并更新消息实体状态(Delivered)。消息代理节点发送一个响应返回给请求所在网关节点,进而再返回给请求的Client。

    代理消息模式Borkered messaging是一种最常见的通用的消息模式,业界的ActiveMQ、RabbitMQ等消息中间件都支持,也是我们接下来深入研究的重点。

    各位,上一篇基本概念和架构中,我们介绍了Window Azure ServiceBus的消息队列技术的概览。接下来,我们进入编程模式和详细功能介绍模式,一点一点把ServiceBus技术研究出来。

    本章我们主要介绍ServiceBus的编程SDK编程入门。

    首先,微软提供了两个主要的Nuget Packages:

    Microsoft Azure Service Bus 3.4.0

    主要的NameSpace有以下几个:

    1. Microsoft.ServiceBus,这个下面有两个主要的类:TokenProvider(用于创建Azure ServiceBus连接Token)NamespaceManager(管理ServiceBus的命名空间)。

    2. Microsoft.ServiceBus.Messaging,这个命名空间下面主要提供了:MessageSession、BrokeredMessage、QueueClient、TopicClient、TopicDescription、QueueDescription、SubscriptionClient、SubscriptionDescription等核心类。

    在正式编码之前,需要我们在Windows Azure的Portal上建立ServiceBus的NameSpace:

     

    我们新建一个命名空间:servicebustest,选择的类型是:消息

    新建完成后处于活动(可用)的状态:

    接下来,我们要获取两个重要的配置:连接串和主秘钥

    请将连接字符串拷贝下来,备用。

    请将主秘钥拷贝下来,备用。

    启动我们的ServiceBus的连接编码,首先需要在应用程序配置文件中增加ServiceBus的连接信息:

    <appSettings>
        <!-- Service Bus specific app setings for messaging connections -->
        <add key="Microsoft.ServiceBus.ConnectionString" value="Endpoint=sb://servicebustest.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=主秘钥"/>
      </appSettings>

    后续,所有的ServiceBus的连接操作,都会读取这个配置节。我们通过case by case的方式,show 给大家:

    首先:ServiceBus的命名空间管理器:Microsoft.ServiceBus.NamespaceManager

    这里,我们主要用以下两个方法来实例化:

    public static NamespaceManager Create(); 例如:

    var namespaceClient = NamespaceManager.Create();

    通过NamespaceManager这个类,我们可以创建、删除、获取、重命名、更新、判断是否存在:队列、主题、订阅、规则等

    1. 消息队列的创建、是否存在判断、删除操作

    复制代码
      private void CreateQueue(string queueName, bool isRequiresSession = true)
            {
                var nm = NamespaceManager.Create();
                if (nm.QueueExists(queueName))
                {
                    nm.DeleteQueue(queueName);
                }
    
                var queue = new QueueDescription(queueName) { RequiresSession = isRequiresSession };
                nm.CreateQueue(queue);
    
                nm.DeleteQueue(queueName);
            }
    复制代码

    2. 创建QueueClient,用于发送、接收消息

    复制代码
            /// <summary>
            /// 创建队列客户端
            /// </summary>
            /// <returns>队列客户端</returns>
            public QueueClient CreateQueueClient(string queueName, ReceiveMode mode = ReceiveMode.ReceiveAndDelete)
            {
                var namespaceClient = NamespaceManager.Create();
                return QueueClient.Create(queueName,mode);
            }
    复制代码

    3. 主题的创建、删除、是否存在判断

    复制代码
     private void CreateTopic(string topicName)
            {
                var nm = NamespaceManager.Create();
                if (nm.TopicExists(topicName))
                {
                    nm.DeleteTopic(topicName);
                }
    
                var topic = new TopicDescription(topicName);
                nm.CreateTopic(topic);
    
                nm.DeleteTopic(topicName);
            }
    复制代码

    4. 创建TopicClient,用于主题的发送和订阅接收消息

    复制代码
     /// <summary>
            /// 创建主题客户端
            /// </summary>
            /// <returns>主题客户端</returns>
            public TopicClient GetTopicClient(string topicName)
            {
                var namespaceClient = NamespaceManager.Create();            
                return TopicClient.Create(topicName);
            }
    复制代码

    5. 创建消息BrokeredMessage,设置消息的属性

    复制代码
       /// <summary>
            /// 构造消息
            /// </summary>
            /// <param name="serializableObject">可序列化的对象</param>
            /// <returns>消息</returns>
            public static BrokeredMessage Create(Object serializableObject)
            {            
                var serializer = new DataContractSerializer(serializableObject.GetType(),new DataContractSerializerSettings(){ IgnoreExtensionDataObject = true, PreserveObjectReferences = false});
                var message = new BrokeredMessage(serializableObject);              
                message.Properties.Add("Type", serializableObject.GetType().ToString());
    
                return message;
            }
    复制代码

    6.发送消息

    复制代码
            /// <summary>
            /// 发送多条消息
            /// </summary>
            /// <param name="receivePayBills">收发货订单集合</param>
            public void Send(List<ReceivePayBill> receivePayBills)
            {
                var queueClient = GetQueueClient(queueName);
                foreach (var receivePayBill in receivePayBills)
                {
                    var message = this.Create(receivePayBill);
                    queueClient.Send(message);
                }
                
                queueClient.Close();
            }
    复制代码

    7. 接收消息

    复制代码
            /// <summary>
            /// 接收消息
            /// </summary>
            /// <returns>收付款订单</returns>
            public List<ReceivePayBill> Receive()
            {
                var bills = new List<ReceivePayBill>(2);
                var queueClient = GetReceiveQueueClient(queueName, ReceiveMode.ReceiveAndDelete);
                BrokeredMessage message;
                while ((message = queueClient.Receive()) != null)
                {
                    bills.Add(message.GetBody<ReceivePayBill>());
                }
    
                return bills;
            }
    复制代码


    还有很多其他的示例代码。本文只是一个简单的入门级教程,接下来我们将按MQ场景逐个展示ServiceBus Messaging的特性。

    上一篇:Window Azure ServiceBus Messaging消息队列技术系列2-编程SDK入门  http://www.cnblogs.com/tianqing/p/5944573.html

    介绍了Azure Service Bus的编程SDK(主要的编程接口)

    本文中我们以实际的使用场景来说明Azure Messaging是否支持以及如何编码实现:消息的收发顺序保证

    消息的收发在实际业务中往往是有顺序的:发送时1-2-3-4-5,接收时也必须是1-2-3-4-5,即FIFO特性。

    在本文的Demo中,我们模拟销售订单消息队列异步处理场景,消息体是一条SalesOrder,顺序发送,顺序接收。

    1. 我们还是使用上篇博客中在Windows Azure的Portal上建立好的NameSpaceservicebustest

    销售订单队列名称:OrderQueue

    2.简单封装一个Service Bus的工具类:ServiceBusUtils: 用于创建队列、删除队列、创建QueueClient、创建BrokerdMessage

    复制代码
    using Microsoft.ServiceBus;
    using Microsoft.ServiceBus.Messaging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AzureMessaging.FIFO
    {
        /// <summary>
        /// ServiceBus工具类
        /// </summary>
        class ServiceBusUtils
        {
            //Namespace名称
            private static readonly string namespaceName = "servicebustest";
    
            /// <summary>
            /// 创建队列
            /// </summary>
            /// <param name="queueName">队列名称</param>
            /// <param name="isSession">是否支持会话</param>
            public void CreateQueue(string queueName, bool isSession = true)
            {
                var namespaceClient = NamespaceManager.Create();
                if (namespaceClient.QueueExists(queueName))
                {
                    namespaceClient.DeleteQueue(queueName);
                }
    
                var queue = new QueueDescription(queueName) { RequiresSession = isSession };
                namespaceClient.CreateQueue(queue);
            }
    
            /// <summary>
            /// 删除队列
            /// </summary>
            /// <param name="queueName">队列名称</param>        
            public void DeleteQueue(string queueName)
            {
                var namespaceClient = NamespaceManager.Create();
                if (namespaceClient.QueueExists(queueName))
                {
                    namespaceClient.DeleteQueue(queueName);
                }
            }
    
    
            /// <summary>
            /// 创建队列客户端
            /// </summary>
            /// <returns>队列客户端</returns>
            public QueueClient GetQueueClient(string queueName, bool isSession = false, ReceiveMode mode = ReceiveMode.ReceiveAndDelete)
            {
                return QueueClient.Create(queueName, mode);
            }
            /// <summary>
            /// 创建队列客户端
            /// </summary>
            /// <returns>队列客户端</returns>
            public QueueClient GetReceiveQueueClient(string queueName, ReceiveMode mode = ReceiveMode.PeekLock)
            {
                var namespaceClient = NamespaceManager.Create();
                return QueueClient.Create(queueName, mode);
            }
    
            /// <summary>
            /// 构造消息
            /// </summary>
            /// <param name="serializableObject">可序列化的对象</param>
            /// <returns>消息</returns>
            public BrokeredMessage Create(Object serializableObject)
            {
                var serializer = new DataContractSerializer(serializableObject.GetType(), 
                    new DataContractSerializerSettings() { IgnoreExtensionDataObject = true, PreserveObjectReferences = false });
                var message = new BrokeredMessage(serializableObject);
                message.Properties.Add("Type", serializableObject.GetType().ToString());
    
                return message;
            }
        }
    }
    复制代码

    2. 示例SalesOrder实体类

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AzureMessaging.FIFO
    {
        /// <summary>
        /// 销售订单类
        /// </summary>
        public class SalesOrder
        {
            /// <summary>
            /// 订单ID
            /// </summary>
            public string OrderID { get; set; }
    
            /// <summary>
            /// 订单编号
            /// </summary>
            public string Code { get; set; }
    
            /// <summary>
            /// 创建时间
            /// </summary>
            public DateTime CreateTime { get; set; }
    
            /// <summary>
            /// 总价格
            /// </summary>
            public Decimal TotalPrice { get; set; }
    
            /// <summary>
            /// 产品ID
            /// </summary>
            public int ProductID { get; set; }
        }
    }
    复制代码

    3. 消息顺序发送

    向OrderQueue发送10条消息订单消息,输出每条消息的顺序号以及MessageID

    复制代码
    private static readonly string queueName = "OrderQueue";
        /// <summary>
            /// 发送消息
            /// </summary>
            private static void MessageSend()
            {
                var sbUtils = new ServiceBusUtils();
    
                //创建队列
                sbUtils.CreateQueue(queueName, false);
    
                //顺序发送消息到OrderQueue
                var queueSendClient = sbUtils.GetQueueClient(queueName);
                for (int i = 0; i < 10; i++)
                {
                    var order = new SalesOrder() { OrderID = i.ToString(), Code = "SalesOrder_" + i, CreateTime = DateTime.Now, ProductID = 17967, TotalPrice = new decimal(19999) };
                    var message = sbUtils.Create(order);
                    queueSendClient.Send(message);
                    Console.WriteLine(string.Format("Send {0} Message: {1}", i, message.MessageId));
                }
    
                Console.WriteLine("Send Completed!");
            }
    复制代码

    程序输出:

    4. 消息顺序接收

    消费OrderQueue中的消息,验证消息的接收顺序

    复制代码
    private static readonly string queueName = "OrderQueue";
     /// <summary>
            /// 接收消息
            /// </summary>
            private static void MessageReceive()
            {
                int index = 0;
                BrokeredMessage msg = null;
                var sbUtils = new ServiceBusUtils();
                var queueReveiveClient = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.ReceiveAndDelete);
                while ((msg = queueReveiveClient.Receive(TimeSpan.FromMilliseconds(3))) != null)
                {
                    Console.WriteLine(string.Format("Received {0} Message: {1}", index, msg.MessageId));
                    index++;
                }
    
                ////删除队列
                //sbUtils.DeleteQueue(queueName);
    
                Console.WriteLine("Receive Completed!");
            }
    复制代码

    程序输出:

    可以看出,Azure Messaging中ServiceBus对消息的收发是有顺序保证的。

    下一篇我们继续其他特性的验证和介绍。

    在上一篇中,我们介绍了消息的顺序收发保证:

    Azure Messaging-ServiceBus Messaging消息队列技术系列3-消息顺序保证

    在本文中我们主要介绍下复杂对象消息是否需要支持序列化以及消息的持久化。

    在实际的业务应用开发中,我们经常会将复杂业务对象放到消息里面,实现异构系统之间的集成、模块间的解耦等等。

    同时,我们还比较关注消息队列服务是否支持消息的持久化,消息队列如果宕机后持久化的消息是否可以还原?

    在Azure Messaging的官方说明中,没有特地的介绍复杂对象消息是否需要支持序列化的要求,但是,我们在上篇博文中,有个消息创建方法,as following,

    BrokeredMessage类的构造函数:

    复制代码
    //
            // Summary:
            //     Constructor that creates a BrokeredMessage from a given object using the
            //     provided XmlObjectSerializer
            //
            // Parameters:
            //   serializableObject:
            //     The serializable object.
            //
            //   serializer:
            //     The serializer object.
            //
            // Exceptions:
            //   System.ArgumentNullException:
            //     Thrown when null serializer is passed to the method with a non-null serializableObject
            //
            // Remarks:
            //     You should be aware of the exceptions that their provided Serializer can
            //     throw and take appropriate actions. Please refer to for a possible list of
            //     exceptions and their cause.
            public BrokeredMessage(object serializableObject, XmlObjectSerializer serializer);
    复制代码

    看来消息的构造,支持动态传入XmlObjectSerializer, so,

    复制代码
     1         /// <summary>
     2         /// 构造消息
     3         /// </summary>
     4         /// <param name="serializableObject">可序列化的对象</param>
     5         /// <returns>消息</returns>
     6         public BrokeredMessage Create(Object serializableObject)
     7         {
     8             var serializer = new DataContractSerializer(serializableObject.GetType(),
     9                 new DataContractSerializerSettings() { IgnoreExtensionDataObject = true, PreserveObjectReferences = true });
    10             var message = new BrokeredMessage(serializableObject, serializer);
    11             message.Properties.Add("Type", serializableObject.GetType().ToString());
    12 
    13             return message;
    14         }
    复制代码

    接下来,我们用上一篇中的代码,做一个复杂对象消息收发的测试,我们还是用上次的SalesOrder类,但是增加一个SalesOrderItem集合和双向关联,来描述销售订单和销售订单明细的的1:n的业务领域模型。

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AzureMessaging.FIFO
    {
        /// <summary>
        /// 销售订单类
        /// </summary>
        public class SalesOrder
        {
            /// <summary>
            /// 订单ID
            /// </summary>
            public string OrderID { get; set; }
    
            /// <summary>
            /// 订单编号
            /// </summary>
            public string Code { get; set; }
    
            /// <summary>
            /// 创建时间
            /// </summary>
            public DateTime CreateTime { get; set; }
    
            /// <summary>
            /// 总价格
            /// </summary>
            public Decimal TotalPrice { get; set; }
    
            /// <summary>
            /// 产品ID
            /// </summary>
            public int ProductID { get; set; }
    
            private List<SalesOrderItem> items;
    
            /// <summary>
            /// 销售订单明细
            /// </summary>
            public List<SalesOrderItem> Items
            {
                get
                {
                    if (items == null)
                        items = new List<SalesOrderItem>();
    
                    return items;
                }
                set
                {
                    items = value;
                }
            }
        }
    }
    复制代码
    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AzureMessaging.FIFO
    {
        /// <summary>
        /// 销售订单明细
        /// </summary>
        public class SalesOrderItem
        {
            /// <summary>
            /// 标识
            /// </summary>
            public string ID { get; set; }
    
            /// <summary>
            /// 客户ID
            /// </summary>
            public int CustomerID { get; set; }
    
            /// <summary>
            /// 所属的销售订单ID
            /// </summary>
            public string SalesOrderID
            {
                get
                {
                    if (Order != null)
                        return Order.OrderID;
    
                    return string.Empty;
                }
            }
    
            /// <summary>
            /// 所属的销售订单
            /// </summary>
            public SalesOrder Order { get; set; }
        }
    }
    复制代码

    创建销售订单实例类方法:

    复制代码
      private static SalesOrder CreateSalesOrder(int i)
            {
                var order = new SalesOrder() { OrderID = i.ToString(), Code = "SalesOrder_" + i, CreateTime = DateTime.Now, ProductID = 17967, TotalPrice = new decimal(19999) };
                order.Items.Add(new SalesOrderItem() { ID = Guid.NewGuid().ToString(), Order = order, CustomerID = 1234567 });
    
                return order;
            }
    复制代码

    在构造SalesOrder和SalesOrderItems时,我们做了双向关联。
    消息顺序收发测试:

    复制代码
     1 using Microsoft.ServiceBus.Messaging;
     2 using System;
     3 using System.Collections.Generic;
     4 using System.Linq;
     5 using System.Text;
     6 using System.Threading.Tasks;
     7 
     8 namespace AzureMessaging.FIFO
     9 {
    10     class Program
    11     {
    12         private static readonly string queueName = "OrderQueue";
    13         static void Main(string[] args)
    14         {
    15             MessageSend();
    16             Console.ReadKey();
    17 
    18             MessageReceive();
    19             Console.ReadKey();
    20         }
    21 
    22         /// <summary>
    23         /// 发送消息
    24         /// </summary>
    25         private static void MessageSend()
    26         {
    27             var sbUtils = new ServiceBusUtils();
    28 
    29             //创建队列
    30             sbUtils.CreateQueue(queueName, false);
    31 
    32             //顺序发送消息到OrderQueue
    33             var queueSendClient = sbUtils.GetQueueClient(queueName);
    34             for (int i = 0; i < 10; i++)
    35             {
    36                 var order = CreateSalesOrder(i);
    37                 var message = sbUtils.Create(order);
    38                 queueSendClient.Send(message);
    39                 Console.WriteLine(string.Format("Send {0} MessageID: {1}", i, message.MessageId));
    40             }
    41 
    42             Console.WriteLine("Send Completed!");
    43         }
    44 
    45         /// <summary>
    46         /// 接收消息
    47         /// </summary>
    48         private static void MessageReceive()
    49         {
    50             int index = 0;
    51             BrokeredMessage msg = null;
    52             var sbUtils = new ServiceBusUtils();
    53             var queueReveiveClient = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.ReceiveAndDelete);
    54             while ((msg = queueReveiveClient.Receive(TimeSpan.FromMilliseconds(3))) != null)
    55             {
    56                 Console.WriteLine(string.Format("Received {0} MessageID: {1}", index, msg.MessageId));
    57                 index++;
    58             }
    59 
    60             ////删除队列
    61             //sbUtils.DeleteQueue(queueName);
    62 
    63             Console.WriteLine("Receive Completed!");
    64         }
    65 
    66         private static SalesOrder CreateSalesOrder(int i)
    67         {
    68             var order = new SalesOrder() { OrderID = i.ToString(), Code = "SalesOrder_" + i, CreateTime = DateTime.Now, ProductID = 17967, TotalPrice = new decimal(19999) };
    69             order.Items.Add(new SalesOrderItem() { ID = Guid.NewGuid().ToString(), Order = order, CustomerID = 1234567 });
    70 
    71             return order;
    72         }
    73     }
    74 }
    复制代码

    可以看出,复杂对象消息只要指定适当的XmlObjectSerializer,即可。

    在双向引用这种领域模型的设计场景下,我们配置了PreserveObjectReferences = true

    var serializer = new DataContractSerializer(serializableObject.GetType(),
                    new DataContractSerializerSettings() { IgnoreExtensionDataObject = true, PreserveObjectReferences = true });

    解决了序列化时循环引用的问题。
    关于消息的持久化,Azure messaging有官方的说明:所有的队列都是持久化的,持久化存储是SQL Server,不提供内存中的消息队列。

    毕竟是PaaS层的消息队列服务,消息的持久化和高可用性微软还是有保障的。

    本篇中我们介绍并验证了Azure Messaging Service Bus复杂对象消息是否需要支持序列化和消息持久化,下一篇我们继续介绍消息的重复发送问题。

    上篇博客中,我们用实际的业务场景和代码示例了Azure Messaging-ServiceBus Messaging对复杂对象消息的支持和消息的持久化:

    Azure Messaging-ServiceBus Messaging消息队列技术系列4-复杂对象消息是否需要支持序列化和消息持久化

    本文中我们主要研究并介绍Azure Messaging对重复消息的支持。

    MessageReceiver 对象创建时可以指定消息接收模式: ReceiveAndDelete 和 PeekLock (默认),其中:

    1. 使用 ReceiveAndDelete 模式时,接收是单步操作,即当 Service Bus 收到请求时,它将消息标记为“正在使用”,然后将其返回给应用程序。ReceiveAndDelete 模式是最简

    单的模型,并且最适合在出现故障时应用程序能够容许不处理消息的场景。理解此模式时,可考虑这种情况:使用者发出了接收请求,但在处理消息之前发生崩溃。由于 Service B

    us已将消息标记为“正在使用”,因此当应用程序重新启动并重新开始使用消息时,它就会错过在崩溃前已使用的消息。

    2. 在 PeekLock 模式下,接收变成两阶段操作,因此可以支持不能容许错过消息的应用程序。当 Service Bus 收到请求时,它会找到下一条要使用的消息,将其锁定以防止其他使

    用者接收它,然后将其返回给应用程序。应用程序完成消息处理(或将消息可靠地存储以便将来处理)后,会对收到的消息调用 Complete 以完成接收过程的第二阶段。当Service

    Bus 看到 Complete 时,会将该消息标记为“正在使用”。另外两个结果也是可能的。第一个结果,如果由于某种原因应用程序无法处理该消息,它可以对收到的消息Abandon(而

    不是 Complete)。这将导致 Service Bus 解锁该消息,并使该消息可以重新被同一使用者或其他竞争的使用者接收。第二个结果,即存在与锁定关联的超时,如果应用程序在锁

    定超时到期前无法处理改消息(例如,应用程序崩溃)则 Service Bus 将解锁该消息并使其可以重新被接收。如果应用程序在处理该消息后崩溃,但此时尚未发出 Complete 请

    求,则在应用程序重新启动时,该消息将重新传递给应用程序。这通常称为“至少一次”处理。这意味着每条消息都将至少处理一次,但在某些情况下可能会重新传递同一消息。如果

    方案不能容许重复处理,则需要在应用程序中添加检测重复项的逻辑。这可以基于消息的 MessageId 属性来实现。此属性的值在传递尝试过程中保持不变。这称为“恰好一次”处、

    理。

    接下来,我们通过Code show一下消息的重复发送和重复接收。

    消息重复发送:同一个消息BrokeredMessage发送两次

    复制代码
    /// <summary>
            /// 发送消息
            /// </summary>
            private static void MessageMultiSendTest()
            {
                var sbUtils = new ServiceBusUtils();
    
                //创建队列
                sbUtils.CreateQueue(queueName, false);
    
                //多次发送消息到OrderQueue
                var queueSendClient = sbUtils.GetQueueClient(queueName);
    
                var order = CreateSalesOrder(1);
                var message = sbUtils.Create(order);
                queueSendClient.Send(message);
                queueSendClient.Send(message);
    
                Console.WriteLine("Send Completed!");
            }
    复制代码

    实际执行过程中是出错的:

    由此可以得出:

    Azure Messaging 不支持同一个消息发送多次,必须通过new多个BrokeredMessage实例实现。

    同时,消息的唯一性由消息的MessageID来标识!

    PeekAndLock模式下消息的重复接收:

    接收模式PeekAndLock,同一个队列,第一个Consumer接收消息,但是不Complete;然后第二个Consumer继续接收消息,此时第一个Consumer未Complete的消息有一个

    TTL,在TTL时间区间之内,第二个Consumer可以继续接收当前队列未锁定的消息,当TTL时间到达后,释放第一个Consumer锁定的消息,第二个Consumer读取到了第一个

    Consumer未Complete的消息。

    The duration of a peek lock; that is, the amount of time that the message is locked for other receivers. The maximum value for LockDuration is 5 minutes;

    the default value is 1 minute.

    复制代码
     /// <summary>
            /// 接收消息
            /// </summary>
            private static void MessageReceive()
            {
                int index = 0;            
                var sbUtils = new ServiceBusUtils();
                var queueReveiveClient1 = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.PeekLock);
                for (int i = 0; i < 10; i++)
                {
                    var msg = queueReveiveClient1.Peek();
                    Console.WriteLine(string.Format("Received {0} MessageID: {1}", i, msg.MessageId));
                }
    
                var queueReveiveClient2 = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.PeekLock);
                for (int i = 0; i < 10; i++)
                {
                    var msg = queueReveiveClient2.Receive();
                    Console.WriteLine(string.Format("Second received {0} MessageID: {1}", i, msg.MessageId));
                    msg.Complete();
                }
    
                ////删除队列
                //sbUtils.DeleteQueue(queueName);
    
                Console.WriteLine("Receive Completed!");
            }
    复制代码

    第一个队列Consumer使用Peek模式接收到消息,只是取出消息,不从消息队列中移出。

    第二个队列Consumer使用Receive模式同样可以接收消息。

    如果两个队列Consumer都使用Receive模式接收消息,只有第一个Consumer可以接收到,第二个Consumer则接收不到,一直在等待消息的入队!

    复制代码
     /// <summary>
            /// 接收消息
            /// </summary>
            private static void MessageReceive()
            {
                int index = 0;            
                var sbUtils = new ServiceBusUtils();
                var queueReveiveClient1 = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.PeekLock);
                for (int i = 0; i < 10; i++)
                {
                    var msg = queueReveiveClient1.Receive();
                    Console.WriteLine(string.Format("Received {0} MessageID: {1}", i, msg.MessageId));
                }
    
                var queueReveiveClient2 = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.PeekLock);
                for (int i = 0; i < 10; i++)
                {
                    var msg = queueReveiveClient2.Receive();
                    Console.WriteLine(string.Format("Second received {0} MessageID: {1}", i, msg.MessageId));
                    msg.Complete();
                }
    
                ////删除队列
                //sbUtils.DeleteQueue(queueName);
    
                Console.WriteLine("Receive Completed!");
            }
    复制代码

    因为消息已经被第一个Consumer消费。

    通过本篇我们了解了Azure Messaging-ServiceBus Messaging对重复消息的处理机制。

    上篇博文中我们介绍了Azure Messaging的重复消息机制、At most once 和At least once.

     Azure Messaging-ServiceBus Messaging消息队列技术系列5-重复消息:at-least-once at-most-once

    本文中我们主要研究并介绍Azure Messaging的消息回执机制:实际应用场景:

    同步收发场景下,消息生产者和消费者双向应答模式,例如:张三写封信送到邮局中转站,然后李四从中转站获得信,然后在写一份回执信,放到中转站,然后张三去取,当然张三写信的时候就得写明回信地址。还

    有,生成订单编号场景,发送一个生成订单编号的消息,消息消费者接收生成订单编号的消息,并通过消息回执返回。

    Azure Messaging的消息回执机制主要通过:基于带会话的Queue/Topic、SessionId、ReplyTo属性来实现

    在代码实现中,我们需要:

    1. 两个工作线程,一个线程用于消息发送和接收回执消息,一个线程用于消息接收和发送消息回执。

    2. 一个会话标识:ReceiptSession  

    3. 两个队列Queue:RequestQueue:发送消息、接收消息,ResponseQueue:发送回执消息,接收回执消息。

    直接Show Code:

    首先,我们在ServiceBusMQManager增加一个线程安全的创建带回话的QueueClient方法:

    复制代码
    private static object syncObj = new object();
            /// <summary>
            /// 获取要求会话带Session的QueueClient
            /// </summary>
            /// <param name="queueName">队列名称</param>
            /// <returns>QueueClient</returns>
            public QueueClient GetSessionQueueClient(string queueName)
            {
                var namespaceClient = NamespaceManager.Create();
                if (!namespaceClient.QueueExists(queueName))
                {
                    lock (syncObj)
                    {
                        if (!namespaceClient.QueueExists(queueName))
                        {
                            var queue = new QueueDescription(queueName) { RequiresSession = true };
                            namespaceClient.CreateQueue(queue);
                        }
                    }
                }
    
                return QueueClient.Create(queueName, ReceiveMode.ReceiveAndDelete);
            }
    复制代码

    然后我们定义一些常量:

    复制代码
            private static readonly string ReplyToSessionId = "ReceiptSession";
    
            const double ResponseMessageTimeout = 20.0;
    
            private static readonly string requestQueueName = "RequestQueue";
    
            private static readonly string responseQueueName = "ResponseQueue";
    复制代码

    实现发送并接收回执消息的方法:

    复制代码
            /// <summary>
            /// 发送并接收回执消息
            /// </summary>
            /// <param name="bills"></param>
            public static void SendMessage()
            {
                var manager = new ServiceBusUtils();
                var responseClient = manager.GetSessionQueueClient(responseQueueName);
                var requestClient = manager.GetSessionQueueClient(requestQueueName);
    
                var messsageReceiver = responseClient.AcceptMessageSession(ReplyToSessionId);
                var order = CreateSalesOrder(1);
    
                //发送消息
                var message = new BrokeredMessage(order);
                message.Properties.Add("Type", order.GetType().ToString());
                message.SessionId = ReplyToSessionId;
                message.MessageId = "OrderMessage001";
                message.ReplyTo = responseQueueName;
                requestClient.Send(message);
                Console.WriteLine("Send message: " + message.MessageId + ", SalesOrder ID: " + order.OrderID);
    
                //接收消息回执
                var receivedMessage = messsageReceiver.Receive(TimeSpan.FromSeconds(ResponseMessageTimeout * 2));
    
                var receivedOrder = receivedMessage.GetBody<SalesOrder>();
                Console.WriteLine("Receive receipt message: " + receivedMessage.MessageId + ", SalesOrder ID: " + receivedOrder.OrderID);
                messsageReceiver.Close();
            }
    复制代码

    实现接收消息并发送回执方法:

    复制代码
     1         /// <summary>
     2         /// 接收消息并回执
     3         /// </summary>
     4         public static void ReceiveMessage()
     5         {
     6             var manager = new ServiceBusUtils();
     7 
     8             var requestClient = manager.GetSessionQueueClient(requestQueueName);
     9             var session = requestClient.AcceptMessageSession();
    10             var requestMessage = session.Receive();
    11            
    12             if (requestMessage != null)
    13             {
    14                 var receivedOrder = requestMessage.GetBody<SalesOrder>();
    15                 Console.WriteLine("Receive message: " + requestMessage.MessageId + ", SalesOrder ID: " + receivedOrder.OrderID);
    16 
    17                 var responseMessage = new BrokeredMessage(receivedOrder);
    18                 responseMessage.Properties.Add("Type", receivedOrder.GetType().ToString());
    19                 responseMessage.ReplyToSessionId = ReplyToSessionId;
    20                 responseMessage.MessageId = "ResponseOrderMessage001";
    21                 responseMessage.SessionId = requestMessage.SessionId;
    22                
    23                 //发送回执消息
    24                 var responseClient = manager.GetSessionQueueClient(requestMessage.ReplyTo);
    25                 responseClient.Send(responseMessage);
    26                 Console.WriteLine("Send receipt message: " + responseMessage.MessageId + ", SalesOrder ID: " + receivedOrder.OrderID);               
    27             }
    28         }
    复制代码

    Main方法中,启动两个工作线程:一个线程用于消息发送和接收回执消息,一个线程用于消息接收和发送消息回执。

    因为涉及到Azure Messaging中队列的第一次创建,Azure Messaging是不支持多个请求同时创建同一个队列的,因此,我们两个线程间做一个简单的Task.Delay(3000).Wait();

    复制代码
     1         static void Main(string[] args)
     2         {
     3             var sendTask = Task.Factory.StartNew(() => { SendMessage(); });
     4             Task.Delay(3000).Wait();
     5             var receiveTask = Task.Factory.StartNew(() => { ReceiveMessage(); });
     6 
     7             Task.WaitAll(sendTask, receiveTask);
     8 
     9             Console.ReadKey();           
    10         }
    复制代码

    我们看看程序输出:

    Azure 服务总线中的队列:

    可以看出:Azure Messaging-ServiceBus Messaging 基于带会话的Queue/Topic、SessionId、ReplyTo属性来实现消息回执机制。

    上篇博文中我们介绍了Azure Messaging-ServiceBus Messaging消息回执机制。

    Azure Messaging-ServiceBus Messaging消息回执机制

    本文中我们主要研究消息的事务。直奔主题:

    • Service Bus Queues支持事务,基于TransactionScope
    • Service Bus Queues provide support for local transactions in the context of a single queue.
    • 事务的限制:事务只能包含一个Queue或者Topic,订阅不能放在事务中,同时事务不支持其他系统,例如数据库

    那消息事务的实际应用场景有哪些呢?例如:

    1.启动一个事务性的会话,将发送更新订单状态消息和更新账户余额消息放到一个事务中,消息发送失败后 rollback,确认消息未被发送。
    2.发送更新订单状态消息和更新账户余额消息成功后,启动一个事务性的会话,接收并处理这两条消息。

    那我们先从同一个队列中发送多条消息这个场景验证:

    复制代码
     1   public static void SendMessageTransactional()
     2         {
     3             var sbUtils = new ServiceBusUtils();
     4 
     5             //创建队列
     6             sbUtils.CreateQueue(queueName, false);
     7 
     8             //多次发送消息到OrderQueue
     9             var queueSendClient = sbUtils.GetQueueClient(queueName);
    10 
    11             using (var trans = new TransactionScope())
    12             {
    13                 var order1 = CreateSalesOrder(1);
    14                 var order2 = CreateSalesOrder(2);
    15                 var message1 = sbUtils.Create(order1);
    16                 var message2 = sbUtils.Create(order2);
    17                 queueSendClient.Send(message1);
    18                 queueSendClient.Send(message2);
    19                 Console.WriteLine("Send but uncomplete!");
    20                 trans.Complete();
    21 
    22                 Console.WriteLine("Complete!");
    23             }            
    24         }
    复制代码

    发送消息完成,但是未提交事务前,队列是这样的:

    事务提交后Complete:

    然后,我们继续研究验证同一个队列接收消息的事务性:有个前提要求:

    消息接收时,如果启动事务,消息消费接收模式必须是PeekAndLock模式。

    消息接收完成,如果事务不Complete,消息仍旧在消息队列中。

    复制代码
     1 public static void ReceiveMessageTransactional()
     2         {
     3             var sbUtils = new ServiceBusUtils();
     4             var queueReveiveClient = sbUtils.GetReceiveQueueClient(queueName, ReceiveMode.PeekLock);
     5             using (var trans = new TransactionScope())
     6             {
     7                 var message1 = queueReveiveClient.Receive();
     8                 message1.Complete();
     9                 var message2 = queueReveiveClient.Receive();
    10                 message2.Complete();
    11                 Console.WriteLine("Received but uncomplete!");
    12                 trans.Complete();
    13 
    14                 Console.WriteLine("Complete!");
    15             }
    16         }
    复制代码

    当接收完消息,事务未提交时:

    队列中的消息是:

    事务提交后:

    Azure Service Bus 中消息:

    消息已经被消费。

    以上就是Azure ServiceBus 中对消息事务的支持。

    上篇博文中我们介绍了Azure ServiceBus Messaging的消息事务机制:

    Azure Messaging-ServiceBus Messaging消息队列技术系列7-消息事务(2017-03-30 22:12)

    本文中我们介绍一下ServiceBus Messaging的一些配额,或者说使用说明和限制。原文请参考:

    https://docs.microsoft.com/zh-cn/azure/service-bus-messaging/service-bus-quotas

    我们着重介绍在实际消息应用开发时,会用到的、需要重点关注的几个配额选项。

    1. 命名空间上限:即每个 Azure 订阅的基本/标准命名空间数上限:100,

    2. 队列/主题大小,即我们使用的Queue或者Topic的容量,白话一点就是消息队列能承载多少消息,我们在实际应用时,一定要做一个评估,评估有两个方面,一个是我们业务高峰期会有多少消息,即消息的QPS,另一个是,未来1~2年,随着业务规模的扩张,现有的Queue/Topic的容量是否满足我们未来发展的要求。

        Azure ServiceBus Messaging中已经显式的说明:系统将拒绝传入消息,且调用代码将收到异常。

        队列/主题的大小,在声明时指定,Azure ServiceBus Messaging提供了1、2、3、4 或 5 GB选项,同时,如果已启用分区,最大队列/主题大小是 80 GB。

        相信这个大小是Azure已经评估过的,大多数场景和规模都可以满足。

    3. 并发连接数,并发连接数,Azure在命名控制和队列、主题、订阅这个层面统一做了限制。其中:

       命名空间上的并发连接数:NetMessaging:1,000,AMQP:5,000

       队列/主题/订阅实体上的并发连接数:受每个命名空间的并发连接限制的约束;

       队列/主题/订阅实体上的并发接收请求数:5,000

       当超过并发连接数时:系统将拒绝后续的附加连接请求,且调用代码将收到异常。 同时,REST 操作不计入并发 TCP 连接数。

       这点大家要注意!

    4. 每个服务命名空间的主题/队列数:即我们在每个服务的Namespace上可以创建多少个队列或者主题,这项配额是10000,服务命名空间中主题和队列的数目之和必须小于或等

       于 10,000。如果超过这个配额:将拒绝后续的在服务命名空间中创建新主题或队列的请求。 因此,如果是通过 Azure 门户配置的,将生成错误消息。 如果是通过管理 API 调

       用的,调用代码将收到异常。

       这个队列数和主题数限制对于大规模消息应用的开发来说是有影响的,我们如果使用多个Namespace就可以横向扩展队列和主题的个数,这有点上业界主流的Sharding机制。赞

       一个。

    5. 任一消息实体路径的最大大小:队列或主题,即消息队列或者主题名称的长度是:260个字符

    6. 队列/主题/订阅实体的消息大小:这个选项对我们来说非常重要,直接影响我们构造消息的大小。最大消息大小:256KB(标准层)/1MB(高级层)。 注意由于系统开销问

    题,此限制通常略小一点,最大标头(消息头)大小:64KB,属性包(消息属性)中的最大标头属性数:byte/int.MaxValue,属性包中属性的最大大小:没有明确的限制。 受最

    大标头大小限制。

    7. 队列/主题/订阅实体的消息属性大小:每个属性的最大消息属性大小为 32K。 所有属性的累计大小不得超过 64K。如果超过这个配额:将生成 SerializationException 异

    常。

    8. 每个主题的订阅数:即每个Topic可以有多少订阅,2000,如果超过这个配额:

       Azure将拒绝后续的为主题创建附加订阅的请求。 因此,如果是通过门户配置的,将显示错误消息。 如果是通过管理 API 调用的,调用代码将收到异常。

  • 相关阅读:
    使用FolderBrowserDialog组件选择文件夹
    使用OpenFileDialog组件打开多个文
    使用OpenFileDialog组件打开对话框
    获取弹出对话框的相关返回值
    PAT 甲级 1139 First Contact (30 分)
    PAT 甲级 1139 First Contact (30 分)
    PAT 甲级 1138 Postorder Traversal (25 分)
    PAT 甲级 1138 Postorder Traversal (25 分)
    PAT 甲级 1137 Final Grading (25 分)
    PAT 甲级 1137 Final Grading (25 分)
  • 原文地址:https://www.cnblogs.com/lingdanglfw/p/13985590.html
Copyright © 2011-2022 走看看