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

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

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

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

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

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

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

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

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

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

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

  1. /// <summary>
  2. /// MQ消费到指定的服务接口
  3. /// </summary>
  4. [HttpPost]
  5. public async Task<ConsumerProcessEventResponse> ConsumerProcessEventAsync([FromBody]ConsumerProcessEventRequest request)
  6. {
  7. ConsumerProcessEventResponse response = new ConsumerProcessEventResponse();
  8. try
  9. {
  10. _logger.LogInformation($"MQ准备执行ConsumerProcessEvent方法,RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}");
  11. using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
  12. {
  13. //获取绑定该routingKey的服务地址集合
  14. var subscriptions = await StackRedis.Current.GetAllList(request.RoutingKey);
  15. if (!subscriptions.Any())
  16. {
  17. //如果Redis中不存在 则从数据库中查询 加入Redis中
  18. var queryRoutingKeyApiUrlResponse = _apiHelperService.PostAsync<QueryRoutingKeyApiUrlResponse>(ServiceAddress.QueryRoutingKeyApiUrlAsync, new QueryRoutingKeyApiUrlRequest { RoutingKey = request.RoutingKey });
  19. if (queryRoutingKeyApiUrlResponse.Result != null && queryRoutingKeyApiUrlResponse.Result.ApiUrlList.Any())
  20. {
  21. subscriptions = queryRoutingKeyApiUrlResponse.Result.ApiUrlList;
  22. Task.Run(() =>
  23. {
  24. StackRedis.Current.SetLists(request.RoutingKey, queryRoutingKeyApiUrlResponse.Result.ApiUrlList);
  25. });
  26. }
  27. }
  28. if(subscriptions!=null && subscriptions.Any())
  29. {
  30. foreach (var apiUrl in subscriptions)
  31. {
  32. Task.Run(() =>
  33. {
  34. _logger.LogInformation(request.MQBodyMessage);
  35. });
  36.  
  37. //这里需要做判断 假如MQ要发送到多个服务接口 其中一个消费失败 应该将其单独记录到数据库 而不影响这个消息的确认
    //先做个备注 以后添加这个处理
  38. await _apiHelperService.PostAsync(apiUrl, request.MQBodyMessage);
  39. }
  40. _logger.LogInformation($"MQ执行ProcessEvent方法完成,RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}");
  41. }
  42. }
  43. }
  44. catch(Exception ex)
  45. {
  46. response.Successful = false;
  47. response.Message = ex.Message;
  48. _logger.LogError(ex, $"MQ消费失败 RoutingKey:{request.RoutingKey} Message:{request.MQBodyMessage}");
  49. }
  50.  
  51. return response;
  52. }

这个WebApi只有这一个方法,就是根据RoutingKey查找对应的MQ配置,然后根据配置的接口地址调用指定的接口,比较简单哈,之前也写过,就不细说了。

我们来看接收MQ消息的Windows服务端,MQ首次使用都需要重新绑定Routingkey、队列和交换器,所以我在Monitor服务里写了一个绑定的方法,在Windows服务端启动的时候调用一次:

  1. public class MQConsumerService
  2. {
  3. private readonly IApiHelperService _apiHelperService;
  4. private ILog _logger;
  5.  
  6. public MQConsumerService(IApiHelperService apiHelperService,ILog logger)
  7. {
  8. _apiHelperService = apiHelperService;
  9. _logger = logger;
  10. }
  11.  
  12. /// <summary>
  13. /// 发送MQ到MQ消费服务端
  14. /// </summary>
  15. /// <param name="routingKey"></param>
  16. /// <param name="message"></param>
  17. public void ProcessEvent(string routingKey, string message)
  18. {
  19. try
  20. {
  21. _logger.Info($"MQ准备执行ProcessEvent方法,RoutingKey:{routingKey} Message:{message}");
  22. _apiHelperService.PostAsync<ConsumerProcessEventResponse>(ServiceUrls.ConsumerProcessEvent,new ConsumerProcessEventRequest { RoutingKey=routingKey,MQBodyMessage=message});
  23. }
  24. catch(Exception ex)
  25. {
  26. _logger.Error($"MQ发送消费服务端失败 RoutingKey:{routingKey} Message:{message}",ex);
  27. }
  28. }
  29.  
  30. /// <summary>
  31. /// MQ初始化 调用队列交换器绑定接口
  32. /// </summary>
  33. /// <returns></returns>
  34. public async Task MQSubscribeAsync()
  35. {
  36. try
  37. {
  38. var response= await _apiHelperService.PostAsync<MQSubscribeResponse>(ServiceUrls.MQSubscribe, new MQSubscribeRequest());
  39. if(!response.Successful)
  40. {
  41. _logger.Error($"MQ绑定RoutingKey队列失败: {response.Message}");
  42. }
  43. }
  44. catch(Exception ex)
  45. {
  46. _logger.Error($"MQ绑定RoutingKey队列失败",ex);
  47. }
  48. }
  49. }

这里为了简单起见,交换器和队列使用的都是同一个,路由方式是“direct”模式,之后会继续修改的,先跑起来再说:

  1. static void Main(string[] args)
  2. {
  3. //交换器(Exchange)
  4. const string BROKER_NAME = "mi_event_bus";
  5. //队列(Queue)
  6. var SubscriptionClientName = "RabbitMQ_Bus_MI";
  7. //log4net日志加载
  8. ILoggerRepository repository = LogManager.CreateRepository("MI.WinService.MQConsumer");
  9. XmlConfigurator.Configure(repository, new FileInfo("log4net.config"));
  10. ILog log = LogManager.GetLogger(repository.Name, "MI.WinService.MQConsumer");
  11. //依赖注入加载
  12. IServiceCollection serviceCollection = new ServiceCollection();
  13. //WebApi调用类
  14. serviceCollection.AddTransient<IApiHelperService, ApiHelperService>();
  15. var serviceProvider = serviceCollection.AddHttpClient().BuildServiceProvider();
  16. serviceProvider.GetService<ILogger>();
  17. var apiHelperService = serviceProvider.GetService<IApiHelperService>();
  18. //MQ消费类(发送MQ消息调用接口、绑定队列交换器)
  19. MQConsumerService consumerService = new MQConsumerService(apiHelperService,log);
  20.  
  21. //MQ连接类
  22. ConnectionFactory factory = new ConnectionFactory
  23. {
  24. UserName = "",
  25. Password = "",
  26. HostName = ""
  27. };
  28.  
  29. var connection = factory.CreateConnection();
  30. var channel = connection.CreateModel();
  31.  
  32. channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
  33.  
  34. channel.QueueDeclare(queue: SubscriptionClientName, durable: true, exclusive: false, autoDelete: false, arguments: null);
  35.  
  36. var consumer = new EventingBasicConsumer(channel);
  37. consumer.Received += (ch, ea) =>
  38. {
  39. //发送到MQ消费服务端
  40. var message = Encoding.UTF8.GetString(ea.Body);
  41. log.Info($"MQ准备消费消息 RoutingKey:{ea.RoutingKey} Message:{message}");
  42.  
  43. //发送到MQ消费服务端MQStationServer
  44. Task result= Task.Run(() =>
  45. {
  46. consumerService.ProcessEvent(ea.RoutingKey, message);
  47. });
  48. if(!result.IsFaulted)
  49. {
  50. //确认ack
  51. channel.BasicAck(ea.DeliveryTag, false);
  52. }
  53. };
  54. channel.BasicConsume(SubscriptionClientName, false, consumer);
  55. Console.WriteLine("消费者已启动!");
  56.  
  57. //绑定队列RoutingKey
  58. Task taskResult= Task.Run(async() =>
  59. {
  60. await consumerService.MQSubscribeAsync();
  61. });
  62.  
  63. taskResult.Wait();
  64.  
  65. Console.WriteLine("队列RoutingKey绑定完成!");
  66.  
  67. Console.ReadKey();
  68. channel.Dispose();
  69. connection.Close();
  70. }

最后梳理下消费端消费MQ流程:

MQ发布后,Windows服务端会受到MQ消息,然后通过调用接口将消息发送到MQ消费服务端,通过RoutingKey从数据库查找对应的MQ和接口配置,调用指定接口,当然,这里只是简单的代码列子,想用在生产中必须要做好完善的日志调用记录、性能监控、健康检查以及服务器层面的集群防止单点故障。

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

  1. .Net Core 商城微服务项目系列(七):使用消息队列(RabbitMQ)实现服务异步通信

    RabbitMQ是什么,怎么使用我就不介绍了,大家可以到园子里搜一下教程.本篇的重点在于实现服务与服务之间的异步通信. 首先说一下为什么要使用消息队列来实现服务通信:1.提高接口并发能力.  2.保证 ...

  2. .Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关

    1.服务注册 在上一篇的鉴权和登录服务中分别通过NuGet引用Consul这个包,同时新增AppBuilderExtensions类: public static class AppBuilderEx ...

  3. .Net Core 商城微服务项目系列(八):购物车

    最近加班有点多,一周五天,四天加班到11点+,心很累.原因是我当前在的这个组比较特殊,相当于业务的架构组,要为其它的开发组提供服务和监控.所以最近更新的也少,不过这个元旦三天假应该会更新三篇. 这篇是 ...

  4. .Net Core 商城微服务项目系列(一):使用IdentityServer4构建基础登录验证

    这里第一次搭建,所以IdentityServer端比较简单,后期再进行完善. 1.新建API项目MI.Service.Identity,NuGet引用IdentityServer4,添加类InMemo ...

  5. .Net Core 商城微服务项目系列(十三):搭建Log4net+ELK+Kafka日志框架

    之前是使用NLog直接将日志发送到了ELK,本篇将会使用Docker搭建ELK和kafka,同时替换NLog为Log4net. 一.搭建kafka 1.拉取镜像 //下载zookeeper docke ...

  6. .Net Core 商城微服务项目系列(六):搭建自己的Nuget包服务器

    当我们使用微服务架构之后,紧接而来的问题便是服务之间的程序集引用问题,可能没接触过的同学不太理解这句话,都已经微服务化了为什么还要互相引用程序集,当然可以不引用.但是我们会有这样一种情况,我们的每个接 ...

  7. .Net Core 商城微服务项目系列(十四):分布式部署携程Apollo构建配置中心

    一.开场白 在系统设计里我们有很多配置希望独立于系统之外,而又能够被系统实时读取.但是在传统的系统设计里,配置信息通常是耦合在系统内的,比如.net里通常会放在App.config或者web.conf ...

  8. .Net Core 商城微服务项目系列(五):使用Polly处理服务错误

    项目进行微服务化之后,随之而来的问题就是服务调用过程中发生错误.超时等问题的时候我们该怎么处理,比如因为网络的瞬时问题导致服务超时,这在我本人所在公司的项目里是很常见的问题,当发生请求超时问题的时候, ...

  9. .Net Core 商城微服务项目系列(十):使用SkyWalking构建调用链监控(2019-02-13 13:25)

    SkyWalking的安装和简单使用已经在前面一篇介绍过了,本篇我们将在商城中添加SkyWalking构建调用链监控. 顺带一下怎么把ES设置为Windows服务,cd到ES的bin文件夹,运行ela ...

随机推荐

  1. C#装箱与拆箱总结

    装箱和拆箱是值类型和引用类型之间相互转换是要执行的操作.  1. 装箱在值类型向引用类型转换时发生 2. 拆箱在引用类型向值类型转换时发生 光上述两句话不难理解,但是往深处了解,就需要一些篇幅来解释了 ...

  2. python中,一个函数想使用另一个函数中的变量

    问题: 第一个函数中用到了变量a:第二个函数也想使用变量a. 解决方法: 在第一个函数中将变量a定义为全局变量,然后在第二个函数中,也写上global a即可. 示例: def func1(): gl ...

  3. 洛谷 P1070 道路游戏 DP

    P1070 道路游戏 题意: 有一个环,环上有n个工厂,每个工厂可以生产价格为x的零钱收割机器人,每个机器人在购买后可以沿着环最多走p条边,一秒走一条,每条边不同时间上出现的金币是不同的,问如何安排购 ...

  4. poj 3468 A Simple Problem with Integers(原来是一道简单的线段树区间修改用来练练splay)

    题目链接:http://poj.org/problem?id=3468 题解:splay功能比线段树强大当然代价就是有些操作比线段树慢,这题用splay实现的比线段树慢上一倍.线段树用lazy标记差不 ...

  5. Gym 101470 题解

    A:Banks 代码: #include<bits/stdc++.h> using namespace std; #define Fopen freopen("_in.txt&q ...

  6. Idea各种快捷生成Live Template的代码整合

    Idea各种快捷生成整合 快速生成method方法注释 配置方法 打开Idea ---> Settings , 搜索 live 点击右边的 + 号,创建模板组 Template Group,之后 ...

  7. js 大量数据优化,通用方法

    当页面渲染太多标签时,会出现卡顿的,典型就是类似table数据太多时,非常卡顿.如果选择分页,没必要讨论,这儿只讨论采用滚动的情况.解决思路很简单,就是页面不展示出来的元素,从页面上删除掉,最难点在于 ...

  8. ie表单提交提示下载文件

    使用jquery的ajaxform提交form表单 如果在html中多了   enctype ="multipart/form-data"   属性值 提交时就会在ie8中提示下载 ...

  9. fastjson对象,JSON,字符串,map之间的互转

    1.对象与字符串之间的互转 将对象转换成为字符串 String str = JSON.toJSONString(infoDo); 字符串转换成为对象 InfoDo infoDo = JSON.pars ...

  10. eclipse中离线安装activit插件

    离线安装activiti教程: 1.先下载压缩包和jar包 链接:https://pan.baidu.com/s/1hSToZt_4A262rUxc8KToCw 密码:j5r1 2.将下载好的jars ...