最近在开发一个轻量级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

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

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

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

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

    Kafka作为时下最流行的开源消息系统,被广泛地应用在数据缓冲.异步通信.汇集日志.系统解耦等方面.相比较于RocketMQ等其他常见消息系统,Kafka在保障了大部分功能特性的同时,还提供了超一流的 ...

  2. Redis 学习笔记(六)Redis 如何实现消息队列

    一.消息队列 消息队列(Messeage Queue,MQ)是在分布式系统架构中常用的一种中间件技术,从字面表述看,是一个存储消息的队列,所以它一般用于给 MQ 中间的两个组件提供通信服务. 1.1 ...

  3. Spring Cloud(7):事件驱动(Stream)分布式缓存(Redis)及消息队列(Kafka)

    分布式缓存(Redis)及消息队列(Kafka) 设想一种情况,服务A频繁的调用服务B的数据,但是服务B的数据更新的并不频繁. 实际上,这种情况并不少见,大多数情况,用户的操作更多的是查询.如果我们缓 ...

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

    官方网址:http://api.zeromq.org/4-0:zmq zmq(7) 0MQ Manual - 0MQ/3.2.5 Name zmq – ØMQ 轻量级消息传输内核 Synopsis # ...

  5. 我心中的核心组件(可插拔的AOP)~第五回 消息组件

    回到目录 之所以把发消息拿出来,完全是因为微软的orchard项目,在这个项目里,将公用的与领域无关的功能模块进行抽象,形成了一个个的组件,这些组件通过引用和注入的方式进行工作,感觉对于应用程序的扩展 ...

  6. 我心中的核心组件(可插拔的AOP)~第六回 消息组件~续

    回到目录 上一回写消息组件已经是很久之前的事了,这一次准备把消息组件后续的东西说一下,事实上,第一篇文章主要讲的是发消息,而这一讲最要讲的是收消息,简单的说,就是消息到了服务器之后,如何从服务器实时的 ...

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

    回到目录 为什么要有本篇文章 本篇文章主要实现了RTX消息生产者,并且完成了整体的设计方式,之前在设计时消息生产者全局使用单一的生产方式,即一个项目里使用了Email就不能使用SMS,这种设计方法和实 ...

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

    返回目录 为何要设计多功能消息组件 之前写过一篇装饰器模式的文章,感觉不够深入,这次的例子是实现项目中遇到的,所以把它拿出来,再写写,之前也写过消息组件的文章,主要采用了策略模式实现的,即每个项目可以 ...

  9. Redis+php-resque实现消息队列

      服务器硬件配置 Dell PowerEdge R310英特尔单路机架式服务器 Intel Xeon Processor X3430 2.4GHz, 8MB Cache 8GB内存(2 x 4GB) ...

随机推荐

  1. .NETCore Docker实现容器化与私有镜像仓库管理

    一.Docker介绍 Docker是用Go语言编写基于Linux操作系统的一些特性开发的,其提供了操作系统级别的抽象,是一种容器管理技术,它隔离了应用程序对基础架构(操作系统等)的依赖.相较于虚拟机而 ...

  2. Nacos(九):Nacos集群部署和遇到的问题

    前言 前面的系列文章已经介绍了Nacos的如何接入SpringCloud,以及Nacos的基本使用方式 之前的文章中都是基于单机模式部署进行讲解的,本文对Nacos的集群部署方式进行说明 环境准备 J ...

  3. lua&C#学习整理

    1.Lua中有8个基本类型分别为:nil.boolean.number.string.userdata.function.thread和table. 2.pairs 和 ipairs区别 pairs: ...

  4. C++ socket bind()函数报错 不存在从 "std::_Binder<std::_Unforced, SOCKET &, sockaddr *&, size_t &>" 到 "int" 的适当转换函数

    昨天还可以正常运行的程序,怎么今天改了程序的结构就报错了呢?我明明没有改动函数内部啊!!! 内心无数只“草泥马”在奔腾,这可咋办呢?于是乎,小寅开始求助于亲爱的度娘...... 由于小寅知识水平有限, ...

  5. vue+vscode+nodejs 开发环境搭建

    nodejs安装配置 1.下载 地址:https://nodejs.org/en/ 2.默认安装 安装完成后,执行npm -v 出现版本号则表示安装成功. 3.配置 在node安装目录下新建两个文件夹 ...

  6. Linux配置使用SSH Key登录并禁用root密码登录

    Linux系统大多数都支持OpenSSH,生成公钥.私钥的最好用ssh-keygen命令,如果用putty自带的PUTTYGEN.EXE生成会不兼容OpenSSH,从而会导致登录时出现server r ...

  7. CF - 1107 E Vasya and Binary String DP

    题目传送门 题解: dp[ l ][ r ][ k ] 代表的是[l, r]这段区间内, 前面有k-1个连续的和s[l]相同且连续的字符传进来的最大值. solve( l, r, k) 代表的是处理 ...

  8. CodeForces 1083 E The Fair Nut and Rectangles 斜率优化DP

    The Fair Nut and Rectangles 题意:有n个矩形,然后你可以选择k个矩形,选择一个矩形需要支付代价 ai, 问 总面积- 总支付代价 最大能是多少, 保证没有矩形套矩形. 题解 ...

  9. 杭电第四场 hdu6336 Problem E. Matrix from Arrays 打表找规律 矩阵前缀和(模板)

    Problem E. Matrix from Arrays Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 262144/262144 ...

  10. 天梯杯 L2-023 图着色问题

    L2-023. 图着色问题 时间限制 300 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 图着色问题是一个著名的NP完全问题.给定无向图 G ...