之前使用MQ的时候是通过封装成dll发布Nuget包来使用,消息的发布和消费都耦合在使用的站点和服务里,这样会造成两个问题:
1.增加服务和站点的压力,因为每次消息的消费就意味着接口的调用,这部分的压力都加在了使用的站点和服务的机器上。
2.增加修改的复杂性,如果我们需要加两条消费日志,都需要再发布一个版本重新通过dll引用。
所以我们需要做以下两方面的工作:
1.MQ的接收拆分为Windows服务,通过zokeerper实现主从防止单点故障。
2.MQ的消费这里做成单独的WebApi服务。
这样做的好处有以下几方面:
1.解耦。MQ的消费从使用的站点和服务中被拆分出来,减轻服务压力。
2.增加程序的可维护和可调试性。
3.单独部署提高吞吐量。
首先我们先来看下MQ的消费服务端,其实就是把之前调接口的方法单独放到了WebApi中,这样可以单独部署,减轻服务器压力:
////// MQ消费到指定的服务接口 /// [HttpPost] public async Task ConsumerProcessEventAsync([FromBody]ConsumerProcessEventRequest request) { ConsumerProcessEventResponse response = new ConsumerProcessEventResponse(); try { _logger.LogInformation($"MQ准备执行ConsumerProcessEvent方法,RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}"); using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) { //获取绑定该routingKey的服务地址集合 var subscriptions = await StackRedis.Current.GetAllList(request.RoutingKey); if (!subscriptions.Any()) { //如果Redis中不存在 则从数据库中查询 加入Redis中 var queryRoutingKeyApiUrlResponse = _apiHelperService.PostAsync (ServiceAddress.QueryRoutingKeyApiUrlAsync, new QueryRoutingKeyApiUrlRequest { RoutingKey = request.RoutingKey }); if (queryRoutingKeyApiUrlResponse.Result != null && queryRoutingKeyApiUrlResponse.Result.ApiUrlList.Any()) { subscriptions = queryRoutingKeyApiUrlResponse.Result.ApiUrlList; Task.Run(() => { StackRedis.Current.SetLists(request.RoutingKey, queryRoutingKeyApiUrlResponse.Result.ApiUrlList); }); } } if(subscriptions!=null && subscriptions.Any()) { foreach (var apiUrl in subscriptions) { Task.Run(() => { _logger.LogInformation(request.MQBodyMessage); }); //这里需要做判断 假如MQ要发送到多个服务接口 其中一个消费失败 应该将其单独记录到数据库 而不影响这个消息的确认
//先做个备注 以后添加这个处理
await _apiHelperService.PostAsync(apiUrl, request.MQBodyMessage); } _logger.LogInformation($"MQ执行ProcessEvent方法完成,RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}"); } } } catch(Exception ex) { response.Successful = false; response.Message = ex.Message; _logger.LogError(ex, $"MQ消费失败 RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}"); } return response; }
这个WebApi只有这一个方法,就是根据RoutingKey查找对应的MQ配置,然后根据配置的接口地址调用指定的接口,比较简单哈,之前也写过,就不细说了。
我们来看接收MQ消息的Windows服务端,MQ首次使用都需要重新绑定Routingkey、队列和交换器,所以我在Monitor服务里写了一个绑定的方法,在Windows服务端启动的时候调用一次:
public class MQConsumerService { private readonly IApiHelperService _apiHelperService; private ILog _logger; public MQConsumerService(IApiHelperService apiHelperService,ILog logger) { _apiHelperService = apiHelperService; _logger = logger; } ////// 发送MQ到MQ消费服务端 /// /// /// public void ProcessEvent(string routingKey, string message) { try { _logger.Info($"MQ准备执行ProcessEvent方法,RoutingKey:{routingKey} Message:{message}"); _apiHelperService.PostAsync (ServiceUrls.ConsumerProcessEvent,new ConsumerProcessEventRequest { RoutingKey=routingKey,MQBodyMessage=message}); } catch(Exception ex) { _logger.Error($"MQ发送消费服务端失败 RoutingKey:{routingKey} Message:{message}",ex); } } /// /// MQ初始化 调用队列交换器绑定接口 /// /// public async Task MQSubscribeAsync() { try { var response= await _apiHelperService.PostAsync (ServiceUrls.MQSubscribe, new MQSubscribeRequest()); if(!response.Successful) { _logger.Error($"MQ绑定RoutingKey队列失败: {response.Message}"); } } catch(Exception ex) { _logger.Error($"MQ绑定RoutingKey队列失败",ex); } } }
这里为了简单起见,交换器和队列使用的都是同一个,路由方式是“direct”模式,之后会继续修改的,先跑起来再说:
static void Main(string[] args) { //交换器(Exchange) const string BROKER_NAME = "mi_event_bus"; //队列(Queue) var SubscriptionClientName = "RabbitMQ_Bus_MI"; //log4net日志加载 ILoggerRepository repository = LogManager.CreateRepository("MI.WinService.MQConsumer"); XmlConfigurator.Configure(repository, new FileInfo("log4net.config")); ILog log = LogManager.GetLogger(repository.Name, "MI.WinService.MQConsumer"); //依赖注入加载 IServiceCollection serviceCollection = new ServiceCollection(); //WebApi调用类 serviceCollection.AddTransient(); var serviceProvider = serviceCollection.AddHttpClient().BuildServiceProvider(); serviceProvider.GetService (); var apiHelperService = serviceProvider.GetService (); //MQ消费类(发送MQ消息调用接口、绑定队列交换器) MQConsumerService consumerService = new MQConsumerService(apiHelperService,log); //MQ连接类 ConnectionFactory factory = new ConnectionFactory { UserName = "", Password = "", HostName = "" }; var connection = factory.CreateConnection(); var channel = connection.CreateModel(); channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); channel.QueueDeclare(queue: SubscriptionClientName, durable: true, exclusive: false, autoDelete: false, arguments: null); var consumer = new EventingBasicConsumer(channel); consumer.Received += (ch, ea) => { //发送到MQ消费服务端 var message = Encoding.UTF8.GetString(ea.Body); log.Info($"MQ准备消费消息 RoutingKey:{ea.RoutingKey} Message:{message}"); //发送到MQ消费服务端MQStationServer Task result= Task.Run(() => { consumerService.ProcessEvent(ea.RoutingKey, message); }); if(!result.IsFaulted) { //确认ack channel.BasicAck(ea.DeliveryTag, false); } }; channel.BasicConsume(SubscriptionClientName, false, consumer); Console.WriteLine("消费者已启动!"); //绑定队列RoutingKey Task taskResult= Task.Run(async() => { await consumerService.MQSubscribeAsync(); }); taskResult.Wait(); Console.WriteLine("队列RoutingKey绑定完成!"); Console.ReadKey(); channel.Dispose(); connection.Close(); }
最后梳理下消费端消费MQ流程:
MQ发布后,Windows服务端会受到MQ消息,然后通过调用接口将消息发送到MQ消费服务端,通过RoutingKey从数据库查找对应的MQ和接口配置,调用指定接口,当然,这里只是简单的代码列子,想用在生产中必须要做好完善的日志调用记录、性能监控、健康检查以及服务器层面的集群防止单点故障。