C# 在表存储上执行事件源预测
我正在创建一个小型的事件源类型函数应用程序,其中每次调用函数都会将事件写入表存储。此类事件的一个例子是:C# 在表存储上执行事件源预测,c#,.net,azure-table-storage,event-sourcing,C#,.net,Azure Table Storage,Event Sourcing,我正在创建一个小型的事件源类型函数应用程序,其中每次调用函数都会将事件写入表存储。此类事件的一个例子是: +------------+---------------+-----------------+ | Event | Timestamp | Destination | +------------+---------------+-----------------+ | Connect | 7/1/2019 4:52 | sftp.alex.com | |
+------------+---------------+-----------------+
| Event | Timestamp | Destination |
+------------+---------------+-----------------+
| Connect | 7/1/2019 4:52 | sftp.alex.com |
| Disconnect | 7/1/2019 4:53 | sftp.liza.com |
| Connect | 7/1/2019 4:54 | sftp.yomama.com |
| Connect | 7/1/2019 4:54 | sftp.alex.com |
| Connect | 7/1/2019 4:59 | sftp.liza.com |
| Disconnect | 7/1/2019 4:59 | sftp.alex.com |
| Disconnect | 7/1/2019 4:59 | sftp.yomama.com |
| Connect | 7/1/2019 5:03 | sftp.alex.com |
+------------+---------------+-----------------+
如何在此表上创建投影?
我需要回答的主要问题是:
每个目标当前有多少个连接?基本思想是通过聚合重播事件以获取当前状态。下面是说明它的代码。警告:这不是一个生产代码,它甚至没有编译
公共类连接计数器
{
专用字典_计数器=新字典();
公共IEnumerable GetCounters()
{
返回_counters.Values;
}
公共无效句柄(ConnectionEvent@event)
{
var counter=GetOrCreateCounter(@event.Destination);
if(@event是ConnectEvent)
counter.ConnectionCount+=1;
if(@event是DisconnectEvent)
counter.ConnectionCount-=1;
}
专用连接计数器GetOrCreateCounter(字符串目标)
{
如果(_counters.ContainsKey(目的地))
返回计数器[目的地];
var counter=newconnectioncounter(){Destination=Destination};
_计数器[目的地]=计数器;
返回计数器;
}
}
公共类连接计数器
{
公共字符串目标{get;set;}
public int ConnectionCount{get;set;}
}
公共类ConnectEvent:ConnectionEvent{}
公共类DisconnectEvent:ConnectionEvent{}
公共类连接事件
{
公共字符串目标{get;set;}
}
// .....
私有连接计数器_ConnectionCounters=新连接计数器();
公共图书馆
{
var events=ReadEvents();//以某种方式读取事件
foreach(事件中的var@event)
{
_connectionCounters.Handle(@event);
}
foreach(在_connectionCounters.GetCounters()中的变量计数器)
Console.WriteLine($“{counter.Destination}有{counter.ConnectionCount}个连接。”)
}
我想表中会有很多记录,对所有记录进行迭代是不可取的。这里有几个想法:
Connect
事件中递增,在Disconnect
事件中递减
但如果您只有一个编写器(一个与Azure通信的服务器),您就可以在代码内部跟踪连接
此外,您还可以将表的当前连接数保存在一个额外字段中。作为奖励,您将能够在过去的任何给定时间立即获得大量连接(以内存为代价)Connect
和Disconnect
事件,但在一些外部接收器中。当你正在编写事件源风格的功能应用程序时,我相信创建一个应该很容易。而且你不必依赖额外的Azure服务
那么,与第一个想法的唯一区别是,如果接收者死亡或断开连接或其他什么——只要记住它接收到的最后一个事件,当接收者重新联机时,只迭代较年轻的事件
您应该记住的最后一个接收到的事件(加上计数器)本质上就是其他人在评论中谈论的快照投影应该与事件流分离,因为它们是业务驱动的,而事件流纯粹是一个技术方面 我假设您将使用SQL来持久化预测以简化答案,但是任何键/值数据存储都可以 您可以创建具有以下结构的
DestinationEvents
表:
+------------------+-----------------+-------------------+
| Destination | Connections | Disconnections |
+------------------+-----------------+-------------------+
| sftp.alex.com | 3 | 1 |
| sftp.liza.com | 1 | 1 |
+------------------+-----------------+-------------------+
通过适当的索引,这将提供快速读取和写入。对于额外速度,考虑一些像ReDIS来缓存你的投影。
棘手的一点是在解决方案设计中,您希望它能够扩展。
一种简单的方法可能是为事件流中的每次写入设置一个SQL触发器,但如果您有大量写入,这将降低您的速度
如果您想要可伸缩性,您需要开始考虑预算(时间和金钱)和业务需求。预测是否需要实时可用
- 如果没有,您可以有一个按一定间隔(每天、每小时、每周等)计算/更新预测的计划流程
- 如果是,您需要开始查看队列/消息代理(RabbitMQ、Kafka等)。现在进入生产者/消费者逻辑。你的应用程序是制作人,它发布事件。EventStream和Projections存储是消费者,它们侦听、转换和持久化传入事件。队列/MessageBroker本身可以替换事件流表,使用Kafka很容易
字典定义内存中的投影存储,其中Destination充当键,(int-Connections,int-Disconnections)
是元组/类
如果您想支持其他投影,内存中的存储可以是一个字典,其中外部字典键是投影名称。这是一个简单的计数器,可以在线程之间安全地共享,以计算每个事件目标的连接数,您可以将其作为服务注入到所有获得连接和断开连接事件的位置
用法示例:
static void Main(string[] args)
{
ConnectionsManager connectionsCounter = new ConnectionsManager();
connectionsCounter.Connnect("sftp.alex.com");
connectionsCounter.Connnect("sftp.liza.com");
connectionsCounter.Connnect("sftp.alex.com");
connectionsCounter.Disconnnect("sftp.alex.com");
connectionsCounter.Connnect("sftp.alex.com");
Console.WriteLine($"Count of {"sftp.alex.com"} is {connectionsCounter.GetConnectionCount("sftp.alex.com")}");
Console.WriteLine(Environment.NewLine + "Count : " + Environment.NewLine);
foreach (var kvp in connectionsCounter.GetAllConnectionsCount())
{
Console.WriteLine($"Count of {kvp.Key} is {kvp.Value}");
}
}
输出:
Count of sftp.alex.com is 2
Count :
Count of sftp.alex.com is 2
Count of sftp.liza.com is 1
连接管理器代码:
public class ConnectionsManager
{
private ConcurrentDictionary<string, long> _destinationCounter;
public ConnectionsManager()
{
_destinationCounter = new ConcurrentDictionary<string, long>();
}
public long Connnect(string destination)
{
long count = _destinationCounter.TryGetValue(destination, out long currentCount)
? currentCount + 1 : 1;
_destinationCounter[destination] = count;
return count;
}
public long Disconnnect(string destination)
{
if (_destinationCounter.TryGetValue(destination, out long count))
{
count--;
if (count < 0) { } // Something went wrong
_destinationCounter[destination] = count;
return count;
}
throw new ArgumentException("Destionation not found", nameof(destination));
}
public long GetConnectionCount(string destination)
{
if (_destinationCounter.TryGetValue(destination, out long count))
return count;
throw new ArgumentException("Destionation not found", nameof(destination));
}
public Dictionary<string, long> GetAllConnectionsCount()
{
return new Dictionary<string, long>(_destinationCounter);
}
}
pub
public class ConnectionsManager
{
private ConcurrentDictionary<string, long> _destinationCounter;
public ConnectionsManager()
{
_destinationCounter = new ConcurrentDictionary<string, long>();
}
public long Connnect(string destination)
{
long count = _destinationCounter.TryGetValue(destination, out long currentCount)
? currentCount + 1 : 1;
_destinationCounter[destination] = count;
return count;
}
public long Disconnnect(string destination)
{
if (_destinationCounter.TryGetValue(destination, out long count))
{
count--;
if (count < 0) { } // Something went wrong
_destinationCounter[destination] = count;
return count;
}
throw new ArgumentException("Destionation not found", nameof(destination));
}
public long GetConnectionCount(string destination)
{
if (_destinationCounter.TryGetValue(destination, out long count))
return count;
throw new ArgumentException("Destionation not found", nameof(destination));
}
public Dictionary<string, long> GetAllConnectionsCount()
{
return new Dictionary<string, long>(_destinationCounter);
}
}