最近在开发一个轻量级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. 二分查找法---scala方式

    二分查找法---scala方式 ,b) } }

  2. C/C++ 修改系统时间,导致sem_timedwait 一直阻塞的问题解决和分析

    修改系统时间,导致sem_timedwait 一直阻塞的问题解决和分析 介绍 最近修复项目问题时,发现当系统时间往前修改后,会导致sem_timedwait函数一直阻塞.通过搜索了发现int sem_ ...

  3. [翻译] C# 8.0 接口默认实现

    原文: Default implementations in interfaces 随着上周的 .NET Core 3.0 Prview 5 和 Visual Studio 2019 version ...

  4. PHP文件基础操作

    文件的基本操作:(更多) fopen():文件打开 $file = fopen("file.txt","r+"); fopen()函数的参数是目标文件的路径和文 ...

  5. EOJ 2019.2月赛 D. 进制转换

    https://acm.ecnu.edu.cn/contest/140/problem/D/ 题意 求一个区间L,R中,在K进制表示下末尾恰有m个0的数字个数. 思路 末尾有m个0,就表示的是K^m的 ...

  6. zstu19一月月赛 duxing201606的原味鸡树

    duxing201606的原味鸡树 题意: 给定一颗有n(n<=1e9)个节点的完全二叉树,1e5次询问,问某个节点有几个子节点. 思路: 自己在月赛上没有思路,问了zfq才知道. 设两个指标, ...

  7. shell 的while语句

    转 http://blog.chinaunix.net/uid-25880122-id-2901409.html while循环的格式   while expression do command co ...

  8. tomcat,nginx日志定时清理

    1. Crontab定时任务 Crontab 基本语法 t1 t2 t3 t4 t5 program 其中 t1 是表示分钟,t2 表示小时,t3 表示一个月份中的第几日,t4 表示月份,t5 表示一 ...

  9. 如何从 if-else 的参数校验中解放出来?

    背景 在开发中经常需要写一些字段校验的代码,比如非空,长度限制,邮箱格式验证等等,导致充满了if-else 的代码,不仅相当冗长,而且很让人抓狂. hibernate validator(官方文档)提 ...

  10. Spring Cloud(三):声明式调用

    声明式服务调用 前面在使用spring cloud时,通常都会利用它对RestTemplate的请求拦截来实现对依赖服务的接口调用,RestTemplate实现了对http的请求封装处理,形成了一套模 ...