在表存储上执行事件源预测

时间:2019-07-09 17:24:39

标签: c# .net azure-table-storage event-sourcing

我正在创建一个微型的事件源样式的函数应用程序,该函数的每次调用都会在表存储中写入一个事件。这样的事件的一个例子是:

+------------+---------------+-----------------+
|   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   |
+------------+---------------+-----------------+

如何在此桌子上创建投影?

我需要回答的主要问题是:

每个目的地当前有多少个连接?

4 个答案:

答案 0 :(得分:7)

我想表中会有很多记录,并且对所有记录进行迭代是不可能的。
因此,这里有一些想法:

  1. 您不能只跟踪连接数吗?

    那将是最简单的解决方案。我不了解您的应用及其与 Azure 的通信方式,但至少有triggers(尽管从supported bindings table来看,您需要使用一些额外的服务...例如队列存储)。并且在它们中,您应该能够将与每个目标的当前连接数存储在单独的表中,在Connect事件中增加,在Disconnect中减少。

    但是,如果您只有一个编写器(与 Azure 进行通信的单个服务器),则可以在代码内部跟踪连接。

    您还可以在一个额外的字段中保存与表的当前连接数。另外,您可以在过去的任何给定时间立即获得大量连接(以内存成本计)。

  2. 当您谈论事件源时...那么也许您应该再次使用它?想法仍然是相同的:您可以跟踪ConnectDisconnect事件,但要在某些外部接收器中进行。在您编写事件源样式的功能应用程序时,我相信创建它应该很容易。而且您不必依赖额外的 Azure 服务。

    然后,与第一个想法的唯一区别是,如果接收器死了,断开连接或发生某种事情-请记住接收到的最后事件,并且当接收器重新联机时,仅在较年轻的事件上进行迭代。

    您应该记住的最后收到的事件(加上计数器)本质上是其他人在评论中谈论的快照。

答案 1 :(得分:5)

投影应该与事件流分离,因为它们是业务驱动的,而事件流纯粹是技术方面的。

我假设您将使用SQL保留预测以简化答案,但是任何键/值数据存储都可以。

您可以使用以下结构创建一个DestinationEvents表:

+------------------+-----------------+-------------------+
|   Destination    |   Connections   |   Disconnections  |
+------------------+-----------------+-------------------+
| sftp.alex.com    |        3        |        1          |
| sftp.liza.com    |        1        |        1          |
+------------------+-----------------+-------------------+

通过适当的索引编制,这应该可以实现快速读取和写入。为了提高速度,请考虑使用Redis之类的东西来缓存您的投影。

棘手的问题在解决方案设计中,您希望它可以扩展。 幼稚的方法可能是为事件流中的每次写入都设置一个SQL触发器,但是如果您有大量写入操作,这会降低您的速度。

如果要扩展性,您需要开始考虑预算(时间和金钱)和业务需求。预测是否需要实时可用?

  • 如果没有,则可以安排一个计划的过程,该过程以一定的时间间隔计算/更新预测:每天,每小时,每周等。
  • 如果是,则需要开始研究队列/消息代理(RabbitMQ,Kafka等)。现在,您要输入生产者/消费者逻辑。您的应用是生产者,它发布事件。 EventStream和Projections存储是使用者,它们侦听,转换和保留传入的事件。队列/ MessageBroker本身可以替换事件流表,而使用Kafka则很容易。

如果您只是想学习,请首先使用Dictionary<string, (int Connections, int Disconnections)>定义内存中的投影存储,其中Destination作为键,而(int Connections, int Disconnections)是一个元组/类。

如果要支持其他Projection,则内存中的存储可以是Dictionary<string, Dictionary<string, (int Connections, int Disconnections)>>,其中外部词典Key是Projection名称。

答案 2 :(得分:0)

基本思想是在聚合上重播事件以获取当前状态。下面是说明它的代码。 警告:这不是生产代码,甚至无法编译。

public class ConnectionCounters
{
    private Dictionary<string, ConnectionCounter> _counters = new Dictionary<string, ConnectionCounter>();

    public IEnumerable<ConnectionCounter> GetCounters()
    {
        return _counters.Values;
    }

    public void Handle(ConnectionEvent @event)
    {
        var counter = GetOrCreateCounter(@event.Destination);
        if (@event is ConnectEvent)
            counter.ConnectionCount += 1;
        if (@event is DisconnectEvent)
            counter.ConnectionCount -= 1;
    }

    private ConnectionCounter GetOrCreateCounter(string destination)
    {
        if (_counters.ContainsKey(destination))
            return _counters[destination];

        var counter = new ConnectionCounter() { Destination = destination };
        _counters[destination] = counter;
        return counter;
    }
}

public class ConnectionCounter
{
    public string Destination { get; set; }
    public int ConnectionCount { get; set; }
}

public class ConnectEvent : ConnectionEvent { }

public class DisconnectEvent : ConnectionEvent { }

public class ConnectionEvent 
{
    public string Destination { get; set; }
}

// .....

private ConnectionCounters _connectionCounters = new ConnectionCounters();
public void Main()
{
    var events = ReadEvents(); // read events somehow
    foreach (var @event in events)
    {
        _connectionCounters.Handle(@event);
    }

    foreach (var counter in _connectionCounters.GetCounters())
        Console.WriteLine($"{counter.Destination} has {counter.ConnectionCount} connections.")
}

答案 3 :(得分:0)

这是一个简单的计数器,可以在线程之间安全地共享该计数器,以计数每个事件目标的连接,您可以将其作为服务注入所有获得连接和断开事件的位置

用法示例:

    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

ConnectionsManager代码:

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);
    }
}