.Net Core 商城微服务项目系列(十一):MQ消费端独立为Window服务+消息处理服务

之前使用MQ的时候是通过封装成dll发布Nuget包来使用,消息的发布和消费都耦合在使用的站点和服务里,这样会造成两个问题:

1.增加服务和站点的压力,因为每次消息的消费就意味着接口的调用,这部分的压力都加在了使用的站点和服务的机器上。

2.增加修改的复杂性,如果我们需要加两条消费日志,都需要再发布一个版本重新通过dll引用。

 

所以我们需要做以下两方面的工作:

1.MQ的接收拆分为Windows服务,通过zokeerper实现主从防止单点故障。

2.MQ的消费这里做成单独的WebApi服务。

 

这样做的好处有以下几方面:

1.解耦。MQ的消费从使用的站点和服务中被拆分出来,减轻服务压力。

2.增加程序的可维护和可调试性。

3.单独部署提高吞吐量。

 

首先我们先来看下MQ的消费服务端,其实就是把之前调接口的方法单独放到了WebApi中,这样可以单独部署,减轻服务器压力:

        /// <summary>
        /// MQ消费到指定的服务接口
        /// </summary>
        [HttpPost]
        public async Task<ConsumerProcessEventResponse> 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<QueryRoutingKeyApiUrlResponse>(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;
        }

        /// <summary>
        /// 发送MQ到MQ消费服务端
        /// </summary>
        /// <param name="routingKey"></param>
        /// <param name="message"></param>
        public void ProcessEvent(string routingKey, string message)
        {
            try
            {
                _logger.Info($"MQ准备执行ProcessEvent方法,RoutingKey:{routingKey} Message:{message}");
                _apiHelperService.PostAsync<ConsumerProcessEventResponse>(ServiceUrls.ConsumerProcessEvent,new ConsumerProcessEventRequest { RoutingKey=routingKey,MQBodyMessage=message});
            }
            catch(Exception ex)
            {
                _logger.Error($"MQ发送消费服务端失败 RoutingKey:{routingKey} Message:{message}",ex);
            }
        }

        /// <summary>
        /// MQ初始化 调用队列交换器绑定接口
        /// </summary>
        /// <returns></returns>
        public async Task MQSubscribeAsync()
        {
            try
            {
                var response= await _apiHelperService.PostAsync<MQSubscribeResponse>(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<IApiHelperService, ApiHelperService>();
            var serviceProvider = serviceCollection.AddHttpClient().BuildServiceProvider();
            serviceProvider.GetService<ILogger>();
            var apiHelperService = serviceProvider.GetService<IApiHelperService>();
            //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和接口配置,调用指定接口,当然,这里只是简单的代码列子,想用在生产中必须要做好完善的日志调用记录、性能监控、健康检查以及服务器层面的集群防止单点故障。

posted @ 2019-03-17 22:05  名字都被注册了  阅读(1082)  评论(0编辑  收藏  举报