c#通过Redis实现轻量级消息组件

最近在开发一个轻量级ASP.NET MVC开发框架,需要加入日志记录,邮件发送,短信发送等功能,为了保持模块的独立性,所以需要通过消息通信的方式进行处理,为了保持框架在部署,使用,二次开发过程中的简易便捷性,所以没有选择传统的MQ,而是基于Redis的订阅发布实现一个系统内部消息组件,话不多说,上码!

数据结构定义

消息实体包含几个部分,订阅通道名称,信息头,信息体,信息差异化额外信息字典,信息头主要包含消息标识,消息日期,信息体包含信息内容,信息实体类型等

   public class Message
    {
        public string MessageChannel { set; get; }
        public MessageHead @MessageHead { set; get; }
        public MessageBody @MessageBody { set; get; }

        [JsonExtensionData]
        public Dictionary<string,Object> @MessageExtra { set; get; }

        public Message()
        {

        }

        public void AddExtra(string Name, string Value)
        {
            if (@MessageExtra == null)
            {
                @MessageExtra = new Dictionary<string, object>();
            }
            @MessageExtra.Add(Name, Value);
        }

        public Object GetExtra(string Name)
        {
            return @MessageExtra[Name];
        }
    }

    public class MessageHead
    {
        public string MessageID { set; get; }
        public DateTime MessageDate { set; get; }

        public MessageHead()
        {
            MessageID = CommonUtil.CreateCommonGuid();
            MessageDate = DateTime.Now;
        }
    }

    public class MessageBody
    {
        public string MessageJsonContent { set; get; }
        public Type MessageMapperType { set; get; }
    }

注:因为消息订阅发布传递过程中,我是通过Json序列化传输的,使用过程中可能需要一些额外的键值对信息,这里在对象中定义的是Dictinary对象,但是Dictinary本身是不支持序列化的,所以需要加上注解JsonExtensionData

订阅通道声明

我们需要达到的效果是,在系统启动时,所有消息通道可以根据系统中的应用自动订阅,这里就需要一个注解来标识我们的订阅通道接收消息的实现类

[AttributeUsage(AttributeTargets.Class)]
    public class MessageChanelAttribute : Attribute
    {
        private string _ChannleName;
        public string ChannelName
        {
            get
            {
                return this._ChannleName;
            }
            set
            {
                this._ChannleName = value;
            }

        }
    }

消息的个性化策略处理

Redis的三方库我这里使用的是StackExchange.Redis.dll,在消息订阅时,需要为Channel指定接收到消息时的处理委托,我们在自动订阅的过程中肯定也要收集好各类消息处理类并与Channel一一对应,这时候我们就需要一个基类FastDefaultMessageHandler,我们的具体的消息处理类继承自FastDefaultMessageHandler,重写处理方法即可

 [Component]
    [MessageChanelAttribute(ChannelName = "DefaultMessage")]
    public class FastDefaultMessageHandler : IFastMessageHandle
    {
        [AutoWired]
        public DBUtil @DBUtil;

        public void HandleMessage(RedisChannel ChannelName, RedisValue Message)
        {
            FastExecutor.Message.Design.Message Entity = JsonConvert.DeserializeObject<FastExecutor.Message.Design.Message>(Message);
            try
            {
                if (!CheckMessageIsConsume(Entity))
                {
                    this.CustomHandle(Entity);
                }
            }
            catch (Exception e)
            {
                StringBuilder ExceptionLog = new StringBuilder();
                ExceptionLog.AppendFormat("异常Message所属Channel:{0}", Entity.MessageChannel + Environment.NewLine);
                ExceptionLog.AppendFormat("异常Message插入时间:{0}", Entity.MessageHead.MessageDate.ToString() + Environment.NewLine);
                ExceptionLog.AppendFormat("异常Message内容:{0}", Message + Environment.NewLine);
                ExceptionLog.AppendFormat("异常信息:{0}", e.Message + Environment.NewLine);
                LogUtil.WriteLog("Logs/MessageErrorLog", "log_", ExceptionLog.ToString() + Environment.NewLine);
                ExceptionLog.AppendFormat("========================================================================================================================================================================" + Environment.NewLine);
                MessageACK.MoveMessageToExceptionChannel(Entity.MessageChannel, Entity);
            }
            finally
            {
                MessageACK.ConfirmMessageFinish(Entity.MessageChannel, Entity.MessageHead.MessageID);
            }

        }

        public virtual void CustomHandle(FastExecutor.Message.Design.Message @Message)
        {

        }

        public virtual bool CheckMessageIsConsume(FastExecutor.Message.Design.Message @Message)
        {
            return false;
        }
    }

其中的HandleMessage方法就是我们在订阅Channel时对应的委托,会调用类中的CustomHandle的虚方法,子类继承重写该方法就会基于多态进行策略调用,CheckMessageIsConsume方法是用于确认消息是否重复消费的,也可以被重写,下面看一个访问日志类的实例,使用MessageChanelAttribute标注声明该实现类需要订阅发布的Channel名称为Visit,CustomHandle方法中实现了插入数据库操作,CheckMessageIsConsume方法判断该条日志数据是否已消费(已经存在于数据库)

    [MessageChanelAttribute(ChannelName = "Visit")]
    public class VisitLog : FastDefaultMessageHandler
    {
        public override void CustomHandle(Message.Design.Message Message)
        {
            Frame_VisitLog LogEntity = JsonConvert.DeserializeObject<Frame_VisitLog>(Message.MessageBody.MessageJsonContent);
            @DBUtil.Insert(LogEntity);
            base.CustomHandle(Message);
        }

        public override bool CheckMessageIsConsume(Message.Design.Message Message)
        {
            Frame_VisitLog LogEntity = JsonConvert.DeserializeObject<Frame_VisitLog>(Message.MessageBody.MessageJsonContent);
            DBRow Row = new DBRow("Frame_VisitLog", "RowGuid", LogEntity.RowGuid);
            if (Row.IsExist())
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }

消息自动订阅

我们希望系统在启动时就寻找出定义好Channel和实现类,自动实现订阅,这里就需要用到IOC容器,启动系统时将所有的消息处理类放入容器中,在自动订阅时全部取出来,根据消息处理类中声明的Channel名称进行自动订阅

  public void Init()
        {
            List<Type> HandlerTypeList = InjectUtil.Container.GetRegistType(typeof(IFastMessageHandle));
            foreach (Type HandlerType in HandlerTypeList)
            {
                MessageChanelAttribute Channel = Attribute.GetCustomAttribute(HandlerType, typeof(MessageChanelAttribute)) as MessageChanelAttribute;
                RedisUtil.Subscribe(Channel.ChannelName, ((FastDefaultMessageHandler)InjectUtil.Container.Resolve(HandlerType)).HandleMessage);
            }
        }

注:

1.这里的IOC容器是我自己实现的,地址:https://gitee.com/grassprogramming/FastIOC,大家可以用AutoFac代替

2.RedisUtil是对StackExchange.Redis.dll封装的处理类,地址:https://gitee.com/grassprogramming/FastUtil

消息发送

消息只需要调用Redis的发布方法即可,将Channel名称与定义好的数据实体类传入,序列化为Json

     public void SendMessage<T>(string ChannleName, T CustomMessageEntity, Dictionary<string, string> ExtraData = null)
        {
            FastExecutor.Message.Design.Message MessageEntity = new Design.Message();
            MessageEntity.MessageChannel = ChannleName;
            MessageHead Head = new MessageHead();
            MessageBody Body = new MessageBody();
            Body.MessageMapperType = typeof(T);
            Body.MessageJsonContent = JsonConvert.SerializeObject(CustomMessageEntity);
            MessageEntity.MessageHead = Head;
            MessageEntity.MessageBody = Body;
            if (ExtraData != null)
            {
                foreach (var item in ExtraData)
                {
                    MessageEntity.AddExtra(item.Key, item.Value);
                }
            }
            RedisUtil.Publish(ChannleName, MessageEntity);
            MessageACK.CopyMessageToACKList(ChannleName, MessageEntity);
        }

消息确认与存储

Redis作订阅发布模式作为消息组件的问题有两方面

问题:消息消费完没有确认机制

解决方案

基于Redis的Hash存储方式建立一个消息存储字段,在发送消息时拷贝到消息Hash字典中,消费完毕后再删除,对应SendMessage中的MessageACK.CopyMessageToACKList方法和FastDefaultMessageHandler中的MessageACK.ConfirmMessageFinish方法,本质就是对Hash字典的增加与删除功能

问题:消息处理端挂了再次重启消息会丢失

解决方案

确认机制已经保证了消息即使没有被消费完但是处理端宕机消息也不会丢失,需要注意的是,消息没有丢失仅仅是Hash字典中有存储,但是消息通道中不存在了,所以我们在系统每次启动时扫描这个Hash字典,重新发布消息到Channel,这样可能导致重复消费,所以需要靠FastDefaultMessageHandler中的CheckMessageIsConsume方法判断,同时消息处理者本身处理异常我们也需要记录下来,比如发短信供应商接口有问题,消息处理异常会进入Redis的ChannelException通道,我们可以根据需求实现一个可视化界面决定是否通过手动恢复

最后

Message组件相关代码地址:https://gitee.com/grassprogramming/FastExecutor/tree/master/code/FastExecutor/FastExecutor.Message

存在不足问题:如果消息是单纯记录日志问题,没办法确认消息是否消费了

如果大家有什么好的建议,可留言一起交流学习,共同进步

原文地址:https://www.cnblogs.com/yanpeng19940119/p/11603865.html

时间: 2024-10-10 11:20:58

c#通过Redis实现轻量级消息组件的相关文章

Kafka、Redis和其它消息组件比较

Kafka作为时下最流行的开源消息系统,被广泛地应用在数据缓冲.异步通信.汇集日志.系统解耦等方面.相比较于RocketMQ等其他常见消息系统,Kafka在保障了大部分功能特性的同时,还提供了超一流的读写性能. 针对Kafka性能方面进行简单分析,相关数据请参考:https://segmentfault.com/a/1190000003985468,下面介绍一下Kafka的架构和涉及到的名词: Topic:用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上. Parti

.NET轻量级ORM组件Dapper修炼手册

一.摘要 1.1.为什么叫本次的分享课叫<修炼手册>? 阿笨希望本次的分享课中涉及覆盖的一些小技巧.小技能给您带来一些帮助.希望您在日后工作中把它作为一本实际技能手册进行储备,以备不时之需,一旦当手头遇到与Dapper修炼手册中相似用法的地方和场景,可以直接拿来进行翻阅并灵活的运用到项目中.最后阿笨建议您可以根据自己在工作中碰到的不同的使用场景,不断的完善此本修炼手册. 废话不多说,直接上干货,我们不生产干货,我们只是干货的搬运工. 四.涉及覆盖的知识点 1.C# Linq To Xml技术.

nodejs+socketio+redis实现前端消息实时推送

nodejs+socketio+redis实现前端消息实时推送 1. 后端部分 发送redis消息 可以参考此篇实现(直接使用Jedis即可) http://www.cnblogs.com/binyue/p/4763352.html 2.后端部分: 接收redis消息 var redis; if(process.argv.length <= 2){ redis = require('redis').createClient(); }else{ redis = require('redis').c

基于Redis实现分布式消息队列(汇总目录)

基于Redis实现分布式消息队列(1)– 缘起 http://blog.csdn.net/stationxp/article/details/45595733 基于Redis实现分布式消息队列(2)– 分布式消息队列功能设计 http://blog.csdn.net/stationxp/article/details/45596619 基于Redis实现分布式消息队列(3)– Redis功能分析 http://blog.csdn.net/stationxp/article/details/457

ZeroMQ接口函数之 :zmq - 0MQ 轻量级消息传输内核

zmq(7) 0MQ Manual - 0MQ/3.2.5 Name zmq – ØMQ 轻量级消息传输内核 Synopsis #include <znq.h> cc [flags] files –lzmq [libraries] Description ØMQ轻量级消息传输内核是一个从标准socket接口的扩展而来的链接库,这些接口通常是由一些专门的传送中间设备来提供.ØMQ提供了一个步消息传送.多模式消息传送.消息过滤(订阅).对多种传输协议无缝接入的集合. 本文档呈现了ØMQ的概念,描述

如何使用NODEJS+REDIS开发一个消息队列

作者: RobanLee 原创文章,转载请注明: 萝卜李 http://www.robanlee.com MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法.应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们>.消 息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术.排队指的是应用程序通过 队列来通信.队列的使用除去了接收和发送应用程序同时执行的要求.其

我心中的核心组件(可插拔的AOP)~消息组件~完善篇

回到目录 为什么要有本篇文章 本篇文章主要实现了RTX消息生产者,并且完成了整体的设计方式,之前在设计时消息生产者全局使用单一的生产方式,即一个项目里使用了Email就不能使用SMS,这种设计方法和实际不相符,虽然在性能上是最佳的(采用了单例模型,维护一个生产者,使用策略模式进行生产者的实现,使用工厂模式生产具体生产者),实际项目中,应该是可以自动选择消息生产者的,当然为了程序的性能,我们还必须使用单例模式来生产具体生产者,这种单例,在本程序中,采用了具体类型,具体单例的方法,即Email自己维

说说设计模式~装饰器模式(Decorator)~多功能消息组件的实现

返回目录 为何要设计多功能消息组件 之前写过一篇装饰器模式的文章,感觉不够深入,这次的例子是实现项目中遇到的,所以把它拿出来,再写写,之前也写过消息组件的文章,主要采用了策略模式实现的,即每个项目可以通过配置进行一种消息的订制,如,你可以订制email,sms,rtx,qq等,但不能同时采用多种机制完成消息的发送,这在一些情况下是没有问题的,但有时,我们也需要同时为客户提供多种消息的推送,这在目前还是挺现时的,如在用户下单后,同时为它发email 和短信进行通过,并对每个订单的过程进行跟踪并通知

redis使用基础(四) ——Redis排序与消息通知

redis使用基础(四) --Redis排序与消息通知 (转载请附上本文链接--linhxx) 一.排序 1.命令 SORT key [ALPHA] [DESC] [LIMIT start end],对列表.集合和有序集合进行排序,当加上alpha参数后,则可以按照字典顺序排序,加上desc则倒序排序,加上limit则支持分页. 2.关键参数 by参数:by key:*->val,可以指定排序的标准,可以自己传入一个list,也可以指定某个列进行排序. get参数:get key:*->val