.Net Core下使用RabbitMQ比较完备的两种方案(虽然代码有点惨淡,不过我会完善)

简介: .Net Core下使用RabbitMQ比较完备的两种方案

一、前言


   上篇说给大家来写C#和Java的方案,最近工作也比较忙,迟到了一些,我先给大家补上C#的方案,另外如果没看我上篇博客的人最好看一下,否则你可能看的云里雾里的,这里我就不进行具体的方案画图了;传送门


二、使用的插件


   HangFire

   一个开源的.NET任务调度框架,最大特点在于内置提供集成化的控制台,方便后台查看及监控,支持多种存储方式;在方案中主要使用定时任务做补偿机制,后期可能会封装一些,能通过页面的形式直接添加任务;

  NLog

  日志记录框架,方案中使用记录日志,后期可能回集成多个日志框架;

  Autofac

  依赖注入的框架,应该不用做过多介绍;

 SqlSugar

 ORM框架,这个从刚开始我就在使用了,在现在公司没有推行起来,不过在上两家公司都留下的遗产,据说还用的可以,当然我还是最佩服作者;

 Polly

 容错服务框架,类似于Java下的Hystrix,主要是为了解决分布式系统中,系统之间相互依赖,可能会因为多种因素导致服务不可用的而产生的一套框架,支持服务的超时重试、限流、熔断器等等;

 RabbitMQ.Client

 官方提供的C#连接RabbitMQ的SDK;


三、方案


 模拟一个简单订单下单的场景,没有进行具体的实现。同时建议下游服务不要写在web端,最好以服务的形式奔跑,代码中是Web端实现的,大家不要这么搞。整体上还是实现了之前提到的两种方案:一是入库打标,二是延时队列(这块没有进行很好的测试,但是估计也没有很大的问题);当然也是有一些特点:RabbitMQ宕机情况下无需重启服务,网络异常的情况下也可以进行断线重连。接下来聊下代码和各方插件在系统中的具体应用:

 项目结构:

 1005447-20190523183424937-482663427.png

 RabbitMQExtensions:

 采用Autofac按照单例的形式注入,采用Polly进行断线重连,也开启了自身断线重连和心跳检测机制,配置方面采用最简单的URI规范进行配置,有兴趣参考下官方,整体上这块代码还相对比较规范,以后可能也不会有太多调整;

/// <summary>
    /// rabbitmq持久化连接
    /// </summary>
    public interface IRabbitMQPersistentConnection
    {
        bool IsConnected { get; }
        bool TryConnect();
        IModel CreateModel();
    }
     /// <summary>
    /// rabbitmq持久化连接具体实现
    /// </summary>
    public class DefaultRabbitMQPersistentConnection : IRabbitMQPersistentConnection
    {
        private readonly IConnectionFactory connectionFactory;
        private readonly ILogger<DefaultRabbitMQPersistentConnection> logger;
        private IConnection connection;
        private const int RETTRYCOUNT = 6;
        private static readonly object lockObj = new object();
        public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger)
        {
            this.connectionFactory = connectionFactory;
            this.logger = logger;
        }
        public bool IsConnected
        {
            get
            {
                return connection != null && connection.IsOpen;
            }
        }
        public void Cleanup()
        {
            try
            {
                connection.Dispose();
                connection.Close();
                connection = null;
            }
            catch (IOException ex)
            {
                logger.LogCritical(ex.ToString());
            }
        }
        public IModel CreateModel()
        {
            if (!IsConnected)
            {
                connection.Close();
                throw new InvalidOperationException("连接不到rabbitmq");
            }
            return connection.CreateModel();
        }
        public bool TryConnect()
        {
            logger.LogInformation("RabbitMQ客户端尝试连接");
            lock (lockObj)
            {
                if (connection == null)
                {
                    var policy = RetryPolicy.Handle<SocketException>()
                        .Or<BrokerUnreachableException>()
                        .WaitAndRetry(RETTRYCOUNT, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>    
                        {
                            logger.LogWarning(ex.ToString());
                        });
                    policy.Execute(() =>
                    {
                        connection = connectionFactory.CreateConnection();
                    });
                }
                if (IsConnected)
                {
                    connection.ConnectionShutdown += OnConnectionShutdown;
                    connection.CallbackException += OnCallbackException;
                    connection.ConnectionBlocked += OnConnectionBlocked;
                    logger.LogInformation($"RabbitMQ{connection.Endpoint.HostName}获取了连接");
                    return true;
                }
                else
                {
                    logger.LogCritical("无法创建和打开RabbitMQ连接");
                    return false;
                }
            }
        }
        private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e)
        {
            logger.LogWarning("RabbitMQ连接异常,尝试重连...");
            Cleanup();
            TryConnect();
        }
        private void OnCallbackException(object sender, CallbackExceptionEventArgs e)
        {
            logger.LogWarning("RabbitMQ连接异常,尝试重连...");
            Cleanup();
            TryConnect();
        }
        private void OnConnectionShutdown(object sender, ShutdownEventArgs reason)
        {
            logger.LogWarning("RabbitMQ连接异常,尝试重连...");
            Cleanup();
            TryConnect();
        }
    }

 OrderDal

 SqlSugar的一些简单封装,有些小特点:大家可以可以通过配置来实现读写分离,采用仓储设计。如果不太喜欢这么写,也可以参考杰哥的做法;

网络异常,图片无法展示
|
View Code

 OrderCommon

 定义全局异常的中间件,还有包含一些用到的实体等等,这部分代码还可优化拆分一下;

 OrderService

 生产者和消费者的具体实现,这块我还想在改造一番,将消费和业务分割开,现在写的很凌乱,不建议这么写,先把代码放出来,看看大家赞同不赞同我的这些用法,可以讨论,也欢迎争论,虽然这块代码写的不好,但是其实里面涉及一些RabbitMQ回调函数的用法,也是比较重要的,没有这些函数也就实现不了我上面说那两个特点

//RabbitMQ宕机以后回调
//客户端这块大家不要采用递归调用恢复链接
//具体为什么大家可以测试下,这里留点小疑问哈哈
connection.ConnectionShutdown += OnConnectionShutdown;
//消费端异常以后回调
consumerchannel.CallbackException += OnOnConsumerMessageAndWriteMessageLogException

Order

 具体的调用者,大家应该根据方法名字就能区分出我上面提到的两种方案的设计,整体的设计思路都是最终一致,延时队列发送消息这块最终也是可以通过定时任务来实现最终一致,实现方式有很多种,简单来说下可以通过入库时生成的缓存机制,通过定时任务来进行补偿实现,这块我没有进行具体实现,有兴趣我们可以探讨下这个方案;

[Route("api/[controller]/[action]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly IBaseDal<OrderMessageLogEntity> orderBaseDal;
        private readonly IMessageService<OrderMessageLogEntity> messageService;
        private readonly IConsumerMessageService consumerMessageService;
        private const string EXCHANGENAME = "order";
        private const string QUEUENAME = "order";
        private const string ROUTINGKEY = "order";
        public OrderController(IBaseDal<OrderMessageLogEntity> orderBaseDal, IMessageService<OrderMessageLogEntity> messageService,IConsumerMessageService consumerMessageService)
        {
            this.orderBaseDal = orderBaseDal;
            this.messageService = messageService;
            this.consumerMessageService = consumerMessageService;
        }
        /// <summary>
        /// 创建订单
        /// </summary>
        /// <returns></returns>
        public ActionResult<bool> CreateOrder(long userId)
        {
            //创建订单成功
            OrderEntity orderEntity = new OrderEntity();
            Random random= new Random();
            orderEntity.OrderId = random.Next();
            orderEntity.OrderNo = random.Next();
            orderEntity.UserId = userId;
            orderEntity.OrderInfo = random.Next() + "详情";
            //bool isCreateOrderSuccress = orderService.CreateOrder(orderId);
            //if (!isCreateOrderSuccress)
            //{
            //    throw new Exception("创建订单失败");
            //}
            //创建订单成功以后开始入消息记录库
            //消息建议设计的冗余一些方便以后好查询
            //千万级以后连表太困难
            //建议冗余的信息有用户信息、订单信息、方便以后按照这个核对信息
            //消息表的建议是按照不同的业务进行分表存储
            Random messageRandom = new Random();
            OrderMessageLogEntity orderMessageLog = new OrderMessageLogEntity();
            orderMessageLog.MessageId = messageRandom.Next();
            orderMessageLog.MessageInfo = orderEntity.OrderId+"订单信息";
            orderMessageLog.Status = (int)MessageStatusEnum.SENDING;
            orderMessageLog.OrderId = orderEntity.OrderId;
            orderMessageLog.UserId = orderEntity.UserId;
            orderMessageLog.CreateTime = DateTime.Now;
            orderMessageLog.UpdateTime = DateTime.Now;
            orderMessageLog.TryCount = 0;
            orderMessageLog.NextRetryTime = DateTime.Now.AddMinutes(5);
            //必须保证消息先落库
            bool isCreateOrderMessageLosSuccess = orderBaseDal.Insert(orderMessageLog);
            if (!isCreateOrderMessageLosSuccess)
                throw new Exception("消息入库异常");
            Message message = new Message();
            message.ExchangeName = EXCHANGENAME;
            message.QueueName = QUEUENAME;
            message.MessageId = orderMessageLog.MessageId;
            message.RoutingKey = ROUTINGKEY;
            message.Body = Encoding.UTF8.GetBytes(orderMessageLog.MessageInfo);
            //落库成功以后开始发送消息到MQ
            //这个地方采用最终一致而不去使用分布式事物最终一致
            messageService.SendMessage(message, orderMessageLog);
            return true;
        }
        /// <summary>
        /// 消费订单
        /// </summary>
        /// <returns></returns>
        public ActionResult<bool> ConsumerOrder()
        {
            Message message = new Message();
            message.ExchangeName = EXCHANGENAME;
            message.QueueName = QUEUENAME;
            message.RoutingKey = ROUTINGKEY;
            consumerMessageService.ConsumerMessage();
            return true;
        }
        /// <summary>
        /// 通过延时队列发送消息
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public ActionResult<bool> CreateDelayCreateOrder(long userId)
        {
            //创建订单成功
            OrderEntity orderEntity = new OrderEntity();
            Random random = new Random();
            orderEntity.OrderId = random.Next();
            orderEntity.OrderNo = random.Next();
            orderEntity.UserId = userId;
            orderEntity.OrderInfo = random.Next() + "详情";
            //bool isCreateOrderSuccress = orderService.CreateOrder(orderId);
            //if (!isCreateOrderSuccress)
            //{
            //    throw new Exception("创建订单失败");
            //}
            //创建订单成功以后开始入消息记录库
            //消息建议设计的冗余一些方便以后好查询
            //千万级以后连表太困难
            //建议冗余的信息有用户信息、订单信息、方便以后按照这个核对信息
            //消息表的建议是按照不同的业务进行分表存储
            Random messageRandom = new Random();
            OrderMessageLogEntity orderMessageLog = new OrderMessageLogEntity();
            orderMessageLog.MessageId = messageRandom.Next();
            orderMessageLog.MessageInfo = orderEntity.OrderId + "订单信息";
            orderMessageLog.Status = (int)MessageStatusEnum.SENDING;
            orderMessageLog.OrderId = orderEntity.OrderId;
            orderMessageLog.UserId = orderEntity.UserId;
            orderMessageLog.CreateTime = DateTime.Now;
            orderMessageLog.UpdateTime = DateTime.Now;
            orderMessageLog.TryCount = 0;
            orderMessageLog.NextRetryTime = DateTime.Now.AddMinutes(5);
            ////必须保证消息先落库
            //bool isCreateOrderMessageLosSuccess = orderBaseDal.Insert(orderMessageLog);
            //if (!isCreateOrderMessageLosSuccess)
            //    throw new Exception("消息入库异常");
            Message message = new Message();
            message.ExchangeName = EXCHANGENAME;
            message.QueueName = QUEUENAME;
            message.MessageId = orderMessageLog.MessageId;
            message.RoutingKey = ROUTINGKEY;
            message.Body = Encoding.UTF8.GetBytes(orderMessageLog.MessageInfo);
            //这里的设计是不进行落库
            //假如两条消息都失败必须借助定时任务去对比消息库和订单库的消息id然后进行再补发
            //剩下的只要有一条发送成功其实就能保证下游必然会消费调这条消息,排除下游消费异常的情况 这个地方我不在进行实现自己可脑补一下
            //开始发送消息到MQ
            messageService.SendMessage(message, orderMessageLog);
            //发送延时消息
            messageService.SendDelayMessage(message, orderMessageLog);
            return true;
        }
        /// <summary>
        /// 消费消息以后并入库
        /// </summary>
        /// <returns></returns>
        public ActionResult<bool> ConsumerOrderAndWirteMessageLog()
        {
            consumerMessageService.ConsumerMessageAndWriteMessageLog();
            return true;
        }
        /// <summary>
        /// 消费延时消息
        /// 进行二次检查核对
        /// </summary>
        /// <returns></returns>
        public ActionResult<bool> ConsumerDelayOrder()
        {
            consumerMessageService.ConsumerDelayMessage();
            return true;
        }
    }


 HangfireExtensions

 Hangfire定时框架,采用Mysql作为持久层的存储,写的也比较清晰,后期就是针对这些进行扩展,实现在界面就能添加定时任务;


四、结束


 生产端和消费端这段代码写的凌乱,希望大家不要介意这一点,是有原因的,这里我就不说了。希望大家看到闪光点,不要在一点上纠结;下次会加入Elasticsearch和监控部分的时候我会把这块代码改掉,还大家一片整洁的世界;

 Github地址:https://github.com/wangtongzhou520/rabbitmq.git  有什么问题大家可以问我

相关实践学习
快速体验阿里云云消息队列RocketMQ版
本实验将带您快速体验使用云消息队列RocketMQ版Serverless系列实例进行获取接入点、创建Topic、创建订阅组、收发消息、查看消息轨迹和仪表盘。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
消息中间件 Java 数据库
RocketMQ实战—9.营销系统代码初版
本文主要介绍了实现营销系统四大促销场景的代码初版:全量用户推送促销活动、全量用户发放优惠券、特定用户推送领取优惠券消息、热门商品定时推送。
RocketMQ实战—9.营销系统代码初版
|
11月前
|
开发框架 .NET C#
ASP.NET Core Blazor 路由配置和导航
大家好,我是码农刚子。本文系统介绍Blazor单页应用的路由机制,涵盖基础配置、路由参数、编程式导航及高级功能。通过@page指令定义路由,支持参数约束、可选参数与通配符捕获,结合NavigationManager实现页面跳转与参数传递,并演示用户管理、产品展示等典型场景,全面掌握Blazor路由从入门到实战的完整方案。
765 6
|
开发框架 .NET 开发者
简化 ASP.NET Core 依赖注入(DI)注册-Scrutor
Scrutor 是一个简化 ASP.NET Core 应用程序中依赖注入(DI)注册过程的开源库,支持自动扫描和注册服务。通过简单的配置,开发者可以轻松地从指定程序集中筛选、注册服务,并设置其生命周期,同时支持服务装饰等高级功能。适用于大型项目,提高代码的可维护性和简洁性。仓库地址:&lt;https://github.com/khellang/Scrutor&gt;
914 5
|
消息中间件 存储 JSON
Net使用EasyNetQ简化与RabbitMQ的交互
EasyNetQ是专为.NET环境设计的RabbitMQ客户端API,简化了与RabbitMQ的交互过程。通过NuGet安装EasyNetQ,可轻松实现消息的发布与订阅,支持多种消息模式及高级特性。文中提供了详细的安装步骤、代码示例及基础知识介绍,帮助开发者快速上手。关注公众号“Net分享”获取更多技术文章。
663 1
Net使用EasyNetQ简化与RabbitMQ的交互
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
605 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
开发框架 算法 中间件
ASP.NET Core 中的速率限制中间件
在ASP.NET Core中,速率限制中间件用于控制客户端请求速率,防止服务器过载并提高安全性。通过`AddRateLimiter`注册服务,并配置不同策略如固定窗口、滑动窗口、令牌桶和并发限制。这些策略可在全局、控制器或动作级别应用,支持自定义响应处理。使用中间件`UseRateLimiter`启用限流功能,并可通过属性禁用特定控制器或动作的限流。这有助于有效保护API免受滥用和过载。 欢迎关注我的公众号:Net分享 (239字符)
543 1
|
开发框架 缓存 .NET
GraphQL 与 ASP.NET Core 集成:从入门到精通
本文详细介绍了如何在ASP.NET Core中集成GraphQL,包括安装必要的NuGet包、创建GraphQL Schema、配置GraphQL服务等步骤。同时,文章还探讨了常见问题及其解决方法,如处理复杂查询、错误处理、性能优化和实现认证授权等,旨在帮助开发者构建灵活且高效的API。
597 3
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
1032 0
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
890 5
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
533 7