.Net Core 代码中使用Azure Service Bus 去实现消息的发送和消费

前言

最近在项目中使用了Azure的Service Bus来实现一些异步下载的功能。因为下载文件前我们会去做一些大数据量的查询和文件的生成,所以用户可能会要长时间的等待,体验不好,所以想到了用消息队列的方式来异步下载这些文件。

主要的思路和步骤是:

  1. 当用户下载的时候我们先去创建一个任务的编号,这个编号保存在一个任务表中,任务表中还应该有任务的状态,创建时间等一些必要的信息的记录。
  2. 获取到任务编号后我们去给消息队列中发送一条消息,这个消息中必须带上我们的任务编号和一些必要的业务逻辑数据
  3. 一旦消息发送成功后我们就将任务编号返回,用户端展示这个任务编号,并且提示可以通过这个任务编号稍后去任务列表中心查询(任务列表中心就是将任务表中的状态和数据在某个页面展示出来)
  4. 上面的步骤完成后,消息队列中的消息会被一个服务给消费掉,拿到消息中的任务编号和业务数据去处理对应的业务并且处理完成更新任务表中的状态
  5. 任务列表中心回去轮询任务的状态(可以是通过定时请求或者websocket的方式),一旦任务状态为完成,就提供一个下载的按钮,供用户去下载,这时候用户下载的文件就是已经查询完成并且生成好的文件了。

因为我们最近所有的项目都要上Azure(微软的云),所以我这里用到的时候AzureService Bus(服务总线),所以我这里简单的记录下如何去使用Service Bus的代码。

Service Bus的使用

添加Azure.Messaging.SerivceBus的包

 PM> Install-Package Azure.Messaging.ServiceBus -Version 7.2.1

在appsetting.json中添加AzureServiceBus的配置项,这里是连接字符串。具体的获取这个字符串的方式可以去看下这个官方文档

 "ConnectionStrings": {
    "AzureServiceBus": "Endpoint=sb://sb-sne2-its-das-acrp-dev.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=riczsDd4FmMul/6O2WesWTYrJ9gRD6uwOXJHOtV5Ulg=" 
  },

创建一个client的类,我这边命名为QueueService.cs

using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using Microsoft.Extensions.Configuration;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Dtt.DDP.App_Utilities.ServiceBus
{
    public class QueueService : IQueueService
    {
        private readonly IConfiguration config;

        public QueueService(IConfiguration config)
        {
            this.config = config;
        }
        public async Task SendMessagesAsync<T>(T serviceBusMessage, string queueName)
        {

            try
            {
                ServiceBusClient client = new ServiceBusClient(config.GetConnectionString("AzureServiceBus"));//创建一个servicebus的客户端
                var options = new CreateQueueOptions(queueName)
                {
                    EnableBatchedOperations = false//是否批量去处理消息
                };
                var administrationClient = new ServiceBusAdministrationClient(config.GetConnectionString("AzureServiceBus"));//这个类是用来管理(CRUD)队列和主题的
                if (!(await administrationClient.QueueExistsAsync(queueName)))
                {
                    await administrationClient.CreateQueueAsync(options);
                }

                ServiceBusSender sender = client.CreateSender(queueName);//生成发送器
                var messageBody = JsonSerializer.Serialize(serviceBusMessage);//构建消息
                // create a message that we can send
                ServiceBusMessage message = new ServiceBusMessage(Encoding.UTF8.GetBytes(messageBody));
                await sender.SendMessageAsync(message);

            }
            catch (System.Exception ex)
            {

                throw ex;
            }


        }
    }
}

ctrl + enterQueueService生成一个接口IQueueService解耦我们的消息队列的类的使用,也就是我们这里可以用其他的消息队列来实现,比如RabbitMQ,MSMQ等等

Startup.cs文件ConfigureServices方法中配置服务注入

    services.AddSingleton<IQueueService, QueueService>();

添加一个消费者的基类Queuelistener,需要实现IHostedService的接口

using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using log4net;
using Azure.Messaging.ServiceBus;

namespace Dtt.DDP.App_Utilities.ServiceBus
{
    public class QueueListener : IHostedService//这里因为我需要开启一个host服务所以需要实现这个接口
    {
        private static ILog Logger;
        public string queueName;
        protected ServiceBusClient serviceBusClient;
        protected ServiceBusProcessor processor;
        public QueueListener(IConfiguration configuration)
        {
            Logger = LogManager.GetLogger(typeof(QueueListener));
            try
            {
                serviceBusClient = new ServiceBusClient(configuration.GetConnectionString("AzureServiceBus"));
                
            }
            catch (Exception ex)
            {
                Logger.Error($"Service Bus 初始化出错, error info : [ {ex.Message} ] ", ex);
            }

        }
        public virtual Task ProcessMessagesAsync(ProcessMessageEventArgs arg1)//这里是处理消息的任务监听方法,我这里用了一个虚方法,具体的实现需要在业务类中去重写它。
        {
            throw new NotImplementedException();
        }

        // handle any errors when receiving messages
        static Task ErrorHandler(ProcessErrorEventArgs args)
        {
            return Task.CompletedTask;
        }
        public async Task Register()
        {
            try
            {
                processor = serviceBusClient.CreateProcessor(queueName, new ServiceBusProcessorOptions());//创建消息处理器
                processor.ProcessMessageAsync += ProcessMessagesAsync;//绑定消息处理任务
                processor.ProcessErrorAsync += ErrorHandler;//绑定错误处理任务
                await processor.StartProcessingAsync();//开启处理
            }
            catch (Exception ex)
            {
                Logger.Error($"Service Bus 注册消费者监听出错!Error: [ {ex.Message} ]", ex);
            }

        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            await Register();
            //Task.CompletedTask;
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            await processor.StopProcessingAsync();
            await processor.CloseAsync();
            await this.serviceBusClient.DisposeAsync();
        }
    }
}

新建一个类继承QueueListener去重写虚方法ProcessMessagesAsync实现不同业务逻辑的处理。我这里是导出报表

using Azure.Messaging.ServiceBus;
using Dtt.DDP.App_Utilities.ServiceBus;
using Dtt.DDP.Areas.DDP1000_DeclationMaintenance.BLL;
using Dtt.DDP.Areas.DDP1000_DeclationMaintenance.Models;
using Dtt.DDP.Areas.ZZZ0000_Common.Enums;
using Dtt.DDP.Areas.ZZZ0000_Common.Utils;
using Dtt.DDP.Models;
using Dtt.Framework.App_Utilities.DI;
using Microsoft.Extensions.DependencyInjection;
using Dtt.Framework.Repository;
using log4net;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Dtt.DDP.Areas.DDP1000_DeclationMaintenance.MQ
{
    public class DeclarationReportListener : QueueListener
    {
        private static ILog _logger;
        private readonly IServiceProvider _services;    
        private DeclarationListBLL bll;
        private CommonRepository taskRepository;
        public DeclarationReportListener(IServiceProvider services, IConfiguration configuration) : base(configuration)
        {            
            _logger = LogManager.GetLogger(typeof(DeclarationReportListener));
            base.queueName = EnumUtil.GetEnumDes(ServiceBusQueueEnum.ExportDeclaraationReport);
            taskRepository = services.GetService<CommonRepository>();
            bll = services.GetService<DeclarationListBLL>();

        }
        public override async Task ProcessMessagesAsync(ProcessMessageEventArgs args)
        {

            decimal taskID = 0;

            try
            {
                _logger.Info($"{typeof(DeclarationReportListener).FullName} 开始消费, args: [ {args} ]");
                //处理 message

                // MQMessageModel
                var jsonStr = Encoding.UTF8.GetString(args.Message.Body.ToArray());
                MQMessageModel mQMessageModel = JsonSerializer.Deserialize<MQMessageModel>(jsonStr);
                taskID = mQMessageModel.TaskID;

                DeclarationReportSearchModel bizPara = JsonSerializer.Deserialize<DeclarationReportSearchModel>(mQMessageModel.BizMessageBody);

                taskRepository.UpdateTaskToStart(taskID);
                // do business
                //string fileName =bll.ExportScheduleConflictDetail(bizPara, mQMessageModel.TaskCreatedBy);
                string fileName = await bll.ExportDeclarationReport(bizPara, mQMessageModel.TaskCreatedBy);

                taskRepository.UpdateTaskToSucceeded(taskID, fileName);
                await args.CompleteMessageAsync(args.Message);
            }
            catch (Exception ex)
            {

                taskRepository.UpdateTaskToFailed(taskID, $"异步导出 Excel 出错!Error: [ {ex.Message} ]" + ex.StackTrace + ex.InnerException?.Message + ex.InnerException?.StackTrace);
                _logger.Error($"异步导出 Excel 出错!Error: [ {ex.Message} ]", ex);
                // 消息已经接受,处理完成
                await Task.CompletedTask;
            }
        }
    }
}

EnumUtil.GetEnumDes(ServiceBusQueueEnum.ExportDeclaraationReport)这段代码是为了通过枚举去定义不同的业务场景,具体的QueueName可以按照自己的业务去实现。

在Startup.cs中的ConfigureServices方法中去启动一个host服务

services.AddHostedService<DeclarationReportListener>();

到这里,我们的Service bus基本就配置完成了,剩下就是在具体的业务中去调用发消息的方法。当然我上面的代码只是在我项目中的,可能具体到你们的项目中需要做一些修改和引用的添加,这里仅供参考~

posted @ 2021-07-21 09:35  clarenceez  阅读(444)  评论(0)    收藏  举报