9、Subscribe订阅
一个EasyNetQ订阅者订阅一种消息类型(消息类的.NET 类型)。一旦通过调用Subscribe方法对一个类型建立了订阅,一个持久化的队列就会在RabbitMQ broker代理服务器上被创建,这个类型的任何消息都会被发送到这个队列上。订阅者无论什么时候连接上,RabbitMQ都会把消息从队列中发送给订阅者。
订阅时需要指定收到消息该怎么处理,我们一般会传递一个Action<T>泛型委托:
bus.Subscribe<MyMessage>("my_subscription_id", msg => Console.WriteLine(msg.Text));
现在每次MyMessage实例被发布后(经RabbitMQ再发送给订阅者),EasyNetQ会调用我们的委托方法,打印MyMessage的Text属性到控制台。
你传给Subscribe方法的订阅Id是非常重要的。
EasyNetQ将会在RabbitMQ Broker代理服务器上为特定的消息类型和订阅id的组合创建唯一的队列。
(消息队列名称为: 消息命名空间名.消息类名:消息类库文件名_订阅时指定的id)如 Model.MyMessage:Models_my_subscription_id
每一次调用Subscribe方法会创建一个新的队列消费者。如果你用相同的消息类型和订阅id调用Subscribe两次(即上面黄底组合内容相同),你将会创建两个消费者去消费同一个队列。然后RabbitMQ将依次向每个消费者轮转方式发送后续消息。这对于扩展和工作分配非常有用。比如说,你创建了一个处理特殊消息的服务(做消费者),但是他已经超负荷工作了(假如消息/工作处理很耗时)。简单的创建几个新的服务实例(即增加消费者),无论在同一个机器上,还是不同的机器上,不用配置任何东西,你自动就得到了伸缩性。同理,如果消费者大部分时间很空闲,也可以关掉几个消费者。。这也就是分布式服务。
假如相同的消息类型,用不同的订阅id调用了两次Subscribe,你将创建两个队列,每一个队列有自己的消费者。每一个消息的副本将会路由到每个队列,因此不同的消费者都将得到所有消息(这个类型的)。假如你有几个不同的服务都关心相同类型的消息,这样很棒。
写订阅回调委托时的注意事项
消费者通过EasyNetQ订阅后,每当它从RabbitMQ消息队列接收到消息,消息就被放置在消费者的内存队列中。
EasyNetQ会创建单个线程循环从内存队列读取消息,调用你之前注册的委托方法(处理消息)。因为是在单个线程上,委托一次只能处理一个消息,所以你要避免长时间地同步IO操作。你应该尽快从委托返回控制。
使用异步订阅 SubscribeAsync
SubscribeAsync方法允许你的订阅者委托到一个异步方法,它能立即返回Task,然后异步地执行长时间IO操作。一旦长时间运行的订阅完成后,就简单的完成这个任务。
在下面的例子中,我们使用一个异步IO操作(即DownloadStringTask)请求一个web service。当这个task完成时,写一行信息到控制台。
1 bus.SubscribeAsync<MyMessage>("subscribe_async_test", message => 2 new WebClient().DownloadStringTask(new Uri("http://localhost:1338/?timeout=500")) 3 .ContinueWith(task => 4 Console.WriteLine("Received:'{0}',Downloaded:'{1}'", 5 message.Text, 6 task.Result)))
下一个列子是如果有错误发生,返回结果会有异常抛出,那么消息将会被放到一个默认的错误队列中。
1 _bus.SubscribeAsync<MessageType>("Queue_Identifier", 2 message => Task.Factory.StartNew(() => 3 { 4 //这里执行一些操作 5 //如果这里有一个异常,那么在这个Task执行完毕后,这个异常会作为结果返回, 6 // 然后任务将继续执行下去。 7 }).ContinueWith(task => 8 { 9 if ( task.IsCompleted && ! task.IsFaulted) 10 { 11 // 一切工作正常时 12 } 13 else 14 { 15 // 这里不要Catch 异常,否则异常会进一步被嵌套,results结果会被发送到RabbitMQ上默认的错误队列 16 throw new EasyNetQException("Message processing exception - look in t the default error quenue(broker)"); 17 } 18 }));
撤销订阅
所有的Subscribe订阅方法都会返回一个ISubscriptionResult接口实例。它包含返回IExchange和IQueue的属性(实际在底层是通过IConsumer实现的),如果需要还可以使用高级API IAdvancededBus进一步操作这些属性。
你可以在任何时间撤销一个订阅者,通过调用ISubscriptionResult实例上的Dispose方法,或者在它之上的 ConsumerCancellation属性。
var subscriptionResult = bus.Subscribe<MyMessage>("sub_id", MyHandler); ... subscriptionResult.Dispose(); // 这个等价与 subscriptionResult.ConsumerCancellation.Dispose();
这将停止EasyNetQ对队列的消费,并且关闭这个消费者的channel。
注意:上面代码不同于IBus和IAndvancedBus的dispose方法,后两者会撤销所有消费者,并关闭与RabbitMQ的连接。
不要在消息处理(委托方法)中调用 subscriptionResult.Dispose()。这将在EasyNetQ ack确认消息和subscriptionResult.Dispose()调用关闭Channel之间产生一个竞争。(即到底会先ack确认?还是先关闭信道?)因为在EasyNetQ的内部架构中,这两件事是在不同的线程上被执行,所以在时间上存在不确定性。