活学“骚”用——读CAP部分代码有感之好玩的Channel
之前写一个电商项目,其中运用了分布式、微服务等听起来就很厉害的东西,其中最让我感兴趣的就是消息队列了,当时选择的是CAP至于CAP是什么可以点这个:
本项目码云地址,请点击此处。(谢谢观赏,如果发现bug请尽快评论谢谢,因为我在使用o(╥﹏╥)o)
说起这个就想起当时第一次使用,简直觉得神奇,只用加一个Attribute就能够订阅你所发布的消息队列,只需要用publish就能够发布队列,简直nice(๑•̀ㅂ•́)و✧。这么简单的用法就导致我后来一直以为消息队列能自己找到对应的方法执行o(╥﹏╥)o,后来一次偶然的机会让我重新认识了消息队列,认识了RabbitMq的几种消息模式,知道了CAP用的是哪种模式也让我对CAP产生了兴趣,于是乎我就去翻了源代码~~~
不过大佬写的就是大佬写的,看不懂 ̄□ ̄||,于是我就用了一种方式来调试源代码,这里也分享给大家,很简单,就是把你用到的类库添加到你的测试项目中,然后一步一步调试,如下图:
然后我就发现了我看不懂的地方:
1、BackgroundService(不懂这个是啥)
2、Channel(看起来是通道的意思,有点感觉)
3、Task.WhenAll(开启了我的多线程之旅)
于是我本着刨根问底的原则开启了对这三个东西的了解,在这儿部分仅做赘述(引经据典):
1、BackgroundService:
2、Task
3、Channel
在这儿就简单介绍一下:
Channel.CreateBounded 创建的 channel 是一个有消息上限的通道
Channel.CreateUnbounded 创建的 channel 是一个无消息上限的通道
最重要的是它——收发隔离!
在我看到它这个特性的时候就觉得它跟消息队列有点像,消息队列也不就是收发隔离么?
所以我就想用它来实现单体项目的消息队列模式,让方法与方法之间解耦,可以通过通知来告诉方法它要执行了!有点像Event的订阅与触发,只不过Event是同步的,而我这个是异步的(๑•̀ㅂ•́)و✧,于是乎我就研究它的基本操作:
发送:
static Channel<string> channel = Channel.CreateUnbounded<string>();
static async Task Main(string[] args)
{
await channel.Writer.WriteAsync("Hello World!");
}
接收(还有另外一种接收方式,在这儿就不写了):
static Channel<string> channel = Channel.CreateUnbounded<string>();
static async Task Main(string[] args)
{
await foreach (var message in channel.Reader.ReadAllAsync())
{
Console.WriteLine(message);
}
}
注意:Channel一定要是同一个才能收发相应的信息。
此时我研究到这里,突然灵光乍现Σ(⊙▽⊙"a,我要是设置一个静态的Channel,然后用BackgroundService,如此这般,如此那般那不就实现了一个消息处理的流程么!(阅读到此处必须晓得BackgroundService是干啥的,如不了解,请往上翻)
请看如下示例:
BackgroundService:
全局静态Channel:
测试发送10条消息:
查看结果:
嗯(ˇˍˇ) 想~!跟我想的一模一样想要的就是这种效果,棒棒哒(๑•̀ㅂ•́)و✧。
可是这样有些简陋哎,我也想实现CAP那种发布跟订阅哎,这样的话才会显得高级许多,O(∩_∩)O哈哈~(年轻人就是喜欢玩些花的)
那怎么才能通过Attribute找到对应的方法呢?
想必大家都已经想到了,那就是百度一下(X),当然是反射(√),反射反射程序员的快乐,源代码也不能白看不是么,通过反射找到相应的Attribute对应的方法,然后在接收到信息的时候通过反射执行方法顺便将方法的参数带过去,不就实现了发布订阅?!
万里长征第一步,那就开始先建个项目吧哈哈哈哈(在下面我会讲解大概思路,贴出代码的实现及源代码,有兴趣的小伙伴可以去下载下来看看)!
第一步创建Attribute作为反射的依据:
/// <summary>
/// 订阅Attribute 只有带了此标识才会被订阅方法
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class ChannelSubscriberAttribute : Attribute
{
public ChannelSubscriberAttribute(string subscriberName, string groupName)
{
if (string.IsNullOrWhiteSpace(subscriberName))
{
throw new ArgumentException(nameof(subscriberName));
}
if (string.IsNullOrWhiteSpace(subscriberName))
{
throw new ArgumentException(nameof(groupName));
}
SubscriberName = subscriberName;
GroupName = groupName;
}
/// <summary>
/// 订阅者名称
/// </summary>
public string SubscriberName { get; private set; }
/// <summary>
/// 分组名称
/// </summary>
public string GroupName { get; private set; }
}
第二步定义需要存储的参数信息:
internal class ChannelMessageConfig
{
/// <summary>
/// 控制器方法信息集合
/// 这里用了元组存储,不了解元组是什么的可以百度一下,或者向别人请教
/// </summary>
internal static readonly List<(string, string, Type, MethodInfo, List<Type>)> Actions = new List<(string, string, Type, MethodInfo, List<Type>)>();
/// <summary>
/// Channel队列信息
/// </summary>
internal static Channel<(string, string, string)> MessageChannel = Channel.CreateUnbounded<(string, string, string)>(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false, AllowSynchronousContinuations = false });
/// <summary>
/// 线程数量
/// </summary>
internal static int ThreadCount
{
get
{
return Actions.Select(x => x.Item1).Distinct().Count();
}
}
}
第三步反射获取订阅的方法:
/// <summary>
/// 注册订阅者信息
/// </summary>
void RegisterSubscriber()
{
//这里我默认只能在控制器内订阅,所以只检测了入口程序
var assemblie = Assembly.GetEntryAssembly();
foreach (var type in assemblie.GetTypes())
{
var methods = type.GetMethods().Where(x => (x.IsPublic || x.IsPrivate) && x.CustomAttributes.Any(x => x.AttributeType == typeof(ChannelSubscriberAttribute))).ToList();
if (methods.Count() > 0)
{
foreach (var method in methods)
{
var serviceTypes = new List<Type>();
var constructors = type.GetConstructors();
foreach (var constructor in constructors)
{
var parameters = constructor.GetParameters();
foreach (var item in parameters)
{
serviceTypes.Add(item.ParameterType);
}
}
var channelSubscriberAttribute = method.GetCustomAttribute<ChannelSubscriberAttribute>();
if (channelSubscriberAttribute != null)
{
ChannelMessageConfig.Actions.Add((channelSubscriberAttribute.SubscriberName, channelSubscriberAttribute.GroupName, method.GetParameters().FirstOrDefault()?.ParameterType, method, serviceTypes));
}
}
}
}
}
第三步写接收到的消息进行处理:
/// <summary>
/// 接收消息
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
async Task Receive(CancellationToken stoppingToken)
{
await foreach ((string subscriberName, string conetnt, string messageId) in ChannelMessageConfig.MessageChannel.Reader.ReadAllAsync(stoppingToken))
{
var actions = ChannelMessageConfig.Actions.Where(((string name, string groupName, Type type, MethodInfo methodInfo, List<Type> serviceTypes) x) => x.name.Equals(subscriberName)).ToList();
if (actions.Count > 0)
{
if (actions.Count == 1)
{
var (name, groupName, type, methodInfo, obj) = actions.FirstOrDefault();
Processing(actions.FirstOrDefault(), conetnt, messageId);
}
else
{
Task.WaitAll(actions.Select(((string name, string groupName, Type type, MethodInfo methodInfo, List<Type> serviceTypes) x) => Task.Run(() =>
{
Processing(x, conetnt, messageId);
})).ToArray());
}
}
}
}
/// <summary>
/// 执行方法
/// </summary>
/// <param name="param"></param>
/// <param name="content"></param>
/// <param name="publishMessageId"></param>
void Processing((string name, string groupName, Type type, MethodInfo method, List<Type> serviceTypes) param, string content, string publishMessageId)
{
if (!string.IsNullOrWhiteSpace(param.name))
{
var msgContent = Utils.JsonSerializer(new ReceiveContentValueModel()
{
PublishMsgId = publishMessageId,
Content = content
});
var messageId = SnowflakeId.Default().NextId().ToString();
InvokeAction(content, param.type, param.method, param.serviceTypes, SuccessAction, (msg) => ErrorAction(msg));
void SuccessAction()
{
_dataStorage.InsertReceiveMessage(new ReceiveMessageValueModel()
{
Content = msgContent,
Group = param.groupName,
SubscriberName = param.name,
Id = messageId,
ExecuteMessage = "成功",
Status = MessageStatusEnum.Succeeded
});
}
void ErrorAction(string errorMsg)
{
_dataStorage.InsertReceiveMessage(new ReceiveMessageValueModel()
{
Content = msgContent,
Group = param.groupName,
SubscriberName = param.name,
Id = messageId,
ExecuteMessage = errorMsg,
Status = MessageStatusEnum.Failed
});
}
}
}
/// <summary>
/// 执行方法
/// </summary>
/// <param name="content"></param>
/// <param name="type"></param>
/// <param name="method"></param>
/// <param name="serviceTypes"></param>
/// <param name="successAction"></param>
/// <param name="errorAction"></param>
void InvokeAction(string content, Type type, MethodInfo method, List<Type> serviceTypes, Action successAction = null, Action<string> errorAction = null)
{
var services = new List<object>();
var scope = _serviceProvider.CreateScope();
foreach (var serviceType in serviceTypes)
{
services.Add(scope.ServiceProvider.GetService(serviceType));
}
object demethodInstance = Activator.CreateInstance(method.DeclaringType, services.ToArray());
try
{
if (!string.IsNullOrWhiteSpace(content))
{
method.Invoke(demethodInstance, new object[] { Utils.JsonDeserialize(content, type) });
}
else
{
method.Invoke(demethodInstance, null);
}
successAction?.Invoke();
}
catch (Exception ex)
{
try
{
errorAction?.Invoke(ex.InnerException == null ? ex.Message : ex.InnerException.Message);
}
catch (Exception e)
{
}
}
scope.Dispose();
}
最后一步BackgroundService来处理消息接收分发:
/// <summary>
/// 执行方法
/// </summary>
/// <param name="stoppingToken">取消token</param>
/// <returns></returns>
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_dataStorage.InitializationTable();//初始化消息表
RegisterSubscriber();//注册订阅消息
RetryReceiveMessageProcessing();//重试失败的接收消息
RetryPublishMessageProcessing();//重试失败的发送消息
DelExpiresMessage();//删除过期的消息
//因为考虑到了大量的消息订阅,所以用了多线程(可能不太合适,有大佬的话可以提一下解决方案)
var tasks = new Task[ChannelMessageConfig.ThreadCount];
for (int i = 0; i < ChannelMessageConfig.ThreadCount; i++)
{
tasks[i] = Task.Run(() => Receive(stoppingToken), stoppingToken);
}
return Task.CompletedTask;
}
忘了个东西,写一个接口用来处理发送队列(具体实现看源代码啦):
//具体实现看源代码啦
public interface IChannelPublisher
{
/// <summary>
/// 写入队列(异步)
/// </summary>
/// <typeparam name="T">参数类型</typeparam>
/// <param name="subscriberName">订阅者名称</param>
/// <param name="contentObj">参数内容</param>
/// <returns></returns>
Task WriteAsync<T>(string subscriberName, T contentObj) where T : class, new();
/// <summary>
/// 写入队列(异步)
/// </summary>
/// <param name="subscriberName">订阅者名称</param>
/// <returns></returns>
Task WriteAsync(string subscriberName);
/// <summary>
/// 写入队列(同步)
/// </summary>
/// <typeparam name="T">参数类型</typeparam>
/// <param name="subscriberName">订阅者名称</param>
/// <param name="contentObj">参数内容</param>
/// <returns></returns>
void Write<T>(string subscriberName, T contentObj) where T : class, new();
/// <summary>
/// 写入队列(同步)
/// </summary>
/// <param name="subscriberName">订阅者名称</param>
/// <returns></returns>
void Write(string subscriberName);
}
还有个东西,测试一下:
发送:
订阅:
结果展示:
至于为啥这么像CAP,没有为啥,因为这就是大佬带给我的影响,膜拜(o´ω`o)ノ
源代码已经开源了,有兴趣的可以去看看,我自己也想慢慢去发展这个东西,大家一起进步,奥利给ヾ(◍°∇°◍)ノ゙