当实体匹配用户定义的查询/过滤器/规则时执行操作

Posted

技术标签:

【中文标题】当实体匹配用户定义的查询/过滤器/规则时执行操作【英文标题】:Execute action when entity matches user-defined query/filter/rule 【发布时间】:2017-09-05 14:56:37 【问题描述】:

通常您编写一个查询并获取与其匹配的所有记录(实体)。我需要做相反的事情。

假设我有 100 万客户和几十个非规范化属性:

public class Customer 
  public string Name get;set;
  public string Email get;set;
  public string Phone get;set;
  public DateTime Birthday get;set;
  public DateTime LastEmailed get;set;
  public DateTime LastCalled get;set;
  public int AgeInYears get  return DateTime.UtcNow.Year - birthdate.Year;
  public int SalesTerritoryId get;set;
  // etc.


我有 10k 用户想要设置自定义过滤器,并在任何新客户符合他们定义的规则时收到通知。

在创建/更新客户时评估其中一些规则(例如)

拥有电话号码且位于我的销售区域的客户。 拥有电子邮件且 LastEmailed 为 NULL 且销售区域位于 (1, 7, 11) 的客户

其他规则将定期运行(例如)

今天过生日的客户。

每天将为客户节省数百万次,并针对每个新/更新的客户检查 5-10k 自定义过滤器。

我意识到我可以将Expression Trees 用于用户的过滤器,但最终会做这样的事情:

public class CustomerRule : IRule 

  public bool IsMatch() 
    // Expression Tree Stuff
  

  public bool DoAction() 
    // Notification Stuff
  


public class CustomerService 

  public void SaveOrUpdate 
    IList<IRule> rules = GetRules();

    // this isn't going to handle 1M save/updates * 10k rules very well
    foreach (var rule in rules)
      if(rule.IsMatch()) 
        rule.DoAction();
                
          
  


我知道其他人已经解决了这个问题,但我很难弄清楚到底要寻找什么。一般的指导表示赞赏,具体的模式、代码、工具等甚至更好。我们主要使用 C#,但如果需要,可以走出 .NET 世界。

【问题讨论】:

我首先想到的是将规则检查引擎与 CRUD 服务分离的方法。将任何客户更改事件放入队列中,并使用另一个服务异步处理该队列,该服务将检查任何规则匹配。这将在不使主要服务超载的情况下进行扩展。 计划是这样,但我们仍然需要每天处理数百万个规则检查/操作,并且必须有比在每个规则上运行一个巨大的 foreach 循环更好的方法。 我很抱歉,但我想不出并行负载的任何部分(也许也使用Parallel.ForEach)。如果您必须检查 10k 条规则,那么您需要执行 10k 次操作,不少于。也许另一种方法是通过将来自不同用户的类似规则组合在一起来减少检查次数(例如,只执行一次规则“IsCustomerMale”)。 事件和通知之间的延迟是否可以接受?说应该立即发送通知,或者例如延迟 X 分钟是可以的。 自定义规则是否总是AND条件?从来没有OR 条件? 【参考方案1】:

我会提到与其他答案不同的观点。您在代码中声称

// this isn't going to handle 1M save/updates * 10k rules very well

但是你真的证实了这一点吗?考虑这段代码:

public class Program 
    static List<Func<Customer, bool>> _rules = new List<Func<Customer, bool>>();
    static void Main(string[] args) 
        foreach (var i in Enumerable.Range(0, 10000)) 
            // generate simple expression, but joined with OR conditions because 
            // in this case (on random data) it will have to check them all
            // c => c.Name == ".." || c.Email == Y || c.LastEmailed > Z || territories.Contains(c.TerritoryID)

            var customer = Expression.Parameter(typeof(Customer), "c");
            var name = Expression.Constant(RandomString(10));
            var email = Expression.Constant(RandomString(12));
            var lastEmailed = Expression.Constant(DateTime.Now.AddYears(-20));
            var salesTerritories = Expression.Constant(Enumerable.Range(0, 5).Select(c => random.Next()).ToArray());
            var exp = Expression.OrElse(Expression.OrElse(Expression.OrElse(
            Expression.Equal(Expression.PropertyOrField(customer, "Name"), name),
            Expression.Equal(Expression.PropertyOrField(customer, "Email"), email)),
            Expression.GreaterThan(Expression.PropertyOrField(customer, "LastEmailed"), lastEmailed)),
            Expression.Call(typeof(Enumerable), "Contains", new Type[] typeof(int), salesTerritories, Expression.PropertyOrField(customer, "SalesTerritoryId")));
            // compile
            var l = Expression.Lambda<Func<Customer, bool>>(exp, customer).Compile();
            _rules.Add(l);
        

        var customers = new List<Customer>();
        // generate 1M customers
        foreach (var i in Enumerable.Range(0, 1_000_000)) 
            var cust = new Customer();
            cust.Name = RandomString(10);
            cust.Email = RandomString(10);
            cust.Phone = RandomString(10);
            cust.Birthday = DateTime.Now.AddYears(random.Next(-70, -10));
            cust.LastEmailed = DateTime.Now.AddDays(random.Next(-70, -10));
            cust.LastCalled = DateTime.Now.AddYears(random.Next(-70, -10));
            cust.SalesTerritoryId = random.Next();
            customers.Add(cust);
        
        Console.WriteLine($"Started. Customers customers.Count, rules: _rules.Count");
        int matches = 0;
        var w = Stopwatch.StartNew();
        // just loop
        Parallel.ForEach(customers, c => 
            foreach (var rule in _rules) 
                if (rule(c))
                    Interlocked.Increment(ref matches);
            
        );
        w.Stop();
        Console.WriteLine($"matches matches, elapsed w.ElapsedMillisecondsms");
        Console.ReadKey();
    

    private static readonly Random random = new Random();
    public static string RandomString(int length)
    
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new string(Enumerable.Repeat(chars, length)
          .Select(s => s[random.Next(s.Length)]).ToArray());
    


public class Customer 
    public string Name  get; set; 
    public string Email  get; set; 
    public string Phone  get; set; 
    public DateTime Birthday  get; set; 
    public DateTime LastEmailed  get; set; 
    public DateTime LastCalled  get; set; 

    public int AgeInYears
    
        get  return DateTime.UtcNow.Year - Birthday.Year; 
    

    public int SalesTerritoryId  get; set; 

这里我以表达式的形式生成 10K 条规则。它们很简单,但并非微不足道 - 用 OR 连接的 4 个条件,包括字符串、日期、包含。然后我生成 1M 客户更新(您数据库中的客户数量无关紧要 - 我们只处理更新)并运行一个循环。猜猜我的普通(非服务器)PC 需要多长时间? 4 分钟。

因此,您可以在 4 分钟内检查全天所有客户更新的所有规则(在适当的服务器上,它应该至少比这快 2 倍,可能更多)。根据 10K 规则检查单个更新需要几毫秒。鉴于此 - 您很可能会在任何其他地方遇到瓶颈,而不是在这里。如果您愿意,您可以在此之上应用一些微不足道的优化:

折叠相同的规则。无需为每个用户检查“今天是生日”规则。

存储在规则中使用的属性,并注意在 Customer 中更新了哪些列。不要运行不使用在 Customer 中更新的列的规则。

但实际上这可能会减慢你的速度,而不是加快速度,所以一切都应该被衡量。

不要从执行规则检查的同一代码发送通知。将它们放入队列并让其他进程\线程处理它们。检查规则是严格的 CPU 绑定工作,发送通知(我假设,在你的情况下)是 IO 绑定的,所以你实际上可以在一台机器上的一个进程中做到这一点。您也不希望以这种速度向给定用户发送垃圾邮件通知,您很可能会分批发送它们,我认为最多每分钟一批,所以这不会太昂贵。

至于客户更新本身 - 您可以将它们存储在某个队列中(如 rabbitMQ),使用数据库通知(如 postgresql pg_notify)或每分钟轮询数据库以获取该期间的所有更新。同样,应该衡量不同方法的性能。

除此之外,这种任务很容易在多台机器上并行化,因此,如果您的客户数达到 1 亿 - 没问题,您可以再添加一台服务器(或者可能仍然可以)。

【讨论】:

感谢您的代码。不幸的是,4分钟是行不通的。用户期望几乎立即收到新匹配客户的通知。一些规则将按计划运行(例如今天的生日),但大多数规则旨在在其他系统添加/更新客户时执行。 @KyleWest,但 4 分钟是每天一整批的所有更新和所有规则(你说你每天 有 100 万次更新)。因此,如果您等待一整天,然后想要通知用户所有发生的更新 - 这将需要 4 分钟。显然,您不会这样做,而是会在更新时检查它们。针对单次更新检查 10K 条规则需要几毫秒,因此您的用户几乎可以实时收到通知。 @KyleWest 当然,我假设您检测到客户何时被插入或更新,并且只针对该插入或更新运行规则检查,而不是每次都针对整个客户集。我看到我的数字有点令人困惑,因为你们都有 100 万客户和“数百万”每天的更新。我的示例显示处理 100 万次更新需要 4 分钟,客户数量无关紧要。 这就是我所想的。我不相信你能取得比这更好的表现。 @KyleWest 如果还不清楚,或者您有一些问题或疑问 - 请随时提问。自从你的赏金开始后你就很安静:)【参考方案2】:

基本问题是:

您如何定义和存储您的自定义过滤器(规则)?

您提到“要检查 5-10k 自定义过滤器”。如果数字很大,您可能有一些灵活的规则结构,例如

<field> <operator> <value> (e.g. <LastEmailed> <is> <NULL>)

&lt;field&gt;&lt;operator&gt;&lt;value&gt; 的值各不相同。

如果是这样,那么对于新/更新的客户,您可以选择所有满足其数据的规则。它可以通过单个查询或具有某种复杂程度的存储过程来完成。这实际上取决于您的数据库的设计。

我的主要观点是:如果您的规则存储在您的数据库中,那么您可以使用纯 SQL 检查某些数据是否符合规则。

从性能角度来看,这种针对约 10k 规则的检查不应该花费太多。同样,它实际上取决于您的数据库结构和应该加入“编译”并检查规则的表的大小。

当然,您可能有一些有限的规则集,这些规则足够复杂,只能从 .NET 代码中检查。正如您发布的那样,可以为它们循环foreach,只要此类规则的数量不应该很大。

我同意 Federico Dipuma 的观点,即异步处理是一种选择。但是,如果上述方法不起作用,它应该是您的第二选择。您更有可能选择异步方法来对匹配的规则执行操作,因为此类操作通常非常耗时(例如电子邮件发送或其他通知、数据库中的 INSERT 或 UPDATE 等)。

【讨论】:

所有规则都遵循该模板 (&lt;field&gt; &lt;operator&gt; &lt;value&gt;),但是,它们也可以组合以创建更复杂的规则 - 想想在 iTunes 中创建智能播放列表:评分超过 3 并且上次播放不是在过去 30 天内,以及(摇滚、爵士)的流派。 我们没有进行任何需要 .NET 代码才能完成的计算,因此可以将其包含在 SQL 数据库中。 获得与新/更新记录匹配的简单规则列表后,您可以轻松找到匹配的复杂规则。您可以仅评估包含简单规则之一的复杂规则,也可以从匹配的简单规则创建位图,然后通过将复杂规则分解为“产品总和”将其与从复杂规则创建的位图进行比较(我承认后者需要进一步思考,但这只是指向不同方向的指针,可能会有所帮助)【参考方案3】:

对于 1M 更新和 10k 规则,您需要减少要检查的规则数量。由于您只有几十个属性,这应该是您运行规则的选择标准。首先过滤规则以检查规则中存在哪些属性,并将其与更新的属性进行比较。

向规则添加一个 SearchParameters 字段,并为其指定值 010405 如果规则仅包含参数 01(name)、04(birthday) 和 05(最后通过电子邮件发送)。 将 SearchParameters(和规则链接)存储在一个单独的表中,按升序排列。 当用户更新他们的记录时,获取按数字更新的参数,如果这些参数更新,则为 02、06 和 07。 比在 SearchParameters 列表中找到包含更新的 SearchParameters 的所有值(以及规则的相应链接)。由于这是一个有序列表,因此可以非常有效地完成。 现在您有了一个精简的规则列表,其中只有包含至少一个已更改参数的规则。您需要为每个循环检查的规则列表减少了。

我希望这个想法很清楚,这里有一个不同/更好的实现选项。

我认为更有效的实现可以使用 2D 布尔数组来完成,其中每一行是一个规则,每一列是一个参数。所以是这样的:

rules  | param1 | param2 | param3 | ...
rule1  |   0    |   1    |   0    | ...
rule2  |   1    |   0    |   1    | ...
rule3  |   1    |   1    |   1    | ...

比更新时只需获取适当参数的列并获取参数为 1 的所有规则。

另一种选择(认为最好和最快的)是完全基于 SQL。基本思路还是差不多的,只是规则要以SQL的形式存储在rules表中,所以得到如下表:

rule_table
ruleNr  | param1 | param2 | param3 | rule
   1    |   0    |   1    |   0    | SELECT recordID FROM Customer WHERE name LIKE 'Will%' AND location = US; 
   2    |   1    |   0    |   1    | SELECT recordID FROM Customer WHERE name = 'West' AND ...;
   3    |   1    |   1    |   1    | SELECT recordID FROM Customer WHERE ...;

在更新或创建客户时运行以下查询,这将选择包含更新参数之一的所有规则。所有更新的参数都应该在查询中。

  SELECT rule FROM rule_table WHERE param1 = 1 OR param4 = 1 OR ....

此查询提供了一个适用的 SQL 规则列表,这些规则应该已经以正确的方式格式化。循环遍历每个 SQL 查询并处理结果。存储在表中的 SQL 查询结果是一个包含指向该特定客户记录的记录 ID 的列表。

希望这会有所帮助。

【讨论】:

【参考方案4】:

每次用户发出请求时按顺序执行所有过滤器,如果不是不可能几乎立即完成,这将是困难的。

如何设置消息队列,然后将过滤器分解为您在用户保存时添加的不同执行任务?

您可以为不同类型的过滤器(生日/位置/行业/等)设置多个队列,然后让不同的工作人员观察队列的变化。每天执行一次生日队列中的消息,连续执行用户创建和更新等,并让更多的工作人员对抗更重的工作人员以更快地处理消息。您可以在高峰时段开启更多工作人员,并在停机时段关闭一些工作人员。

您可以将工作人员分解为某些过滤器计数/结果。因此,为不同类型的过滤器或运行时间更长的过滤器设置不同的工作人员并组合结果(或在过滤器完成时添加/删除结果)。它们可以在任务进入时并行运行,同时处理不同的过滤器。

将结果存储在文档数据库中或将它们缓存在 Redis 服务器中并从中提取结果。

【讨论】:

【参考方案5】:

使用OData怎么样?

asp.net 中的示例:https://docs.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options

【讨论】:

【参考方案6】:

您绝对不想延迟将记录保存到数据库以运行规则。 IsMatch() 或 DoAction() 中发生的任何错误都可能会中止正在保存的数据。我会假设提醒人们今天是某人的生日这一事实并不像实际将某人添加到数据库中那么重要。

我想将添加/更新事件添加到排队系统。现在不要把排队系统想成是让事情堆积起来并等待很长时间的地方! Windows 操作系统是一个队列系统,它使用消息队列来处理几乎所有事情。因此,您发布的 CustomerService.SaveOrUpdate 方法将向您的“UpdatedUser”队列发送一个事件(或消息,我会发现将其视为一个事件更容易)。该队列将有一个或多个侦听器,等待事件出现。然后他们会接受该事件并找到与其数据匹配的任何规则并执行适当的操作。

使用排队系统的好处在于,您可以将处理工作卸载到专用机器上,而不会破坏负责将数据保存到数据存储中的系统。负责处理规则的队列侦听器可以将规则加载到内存中,这将允许它找到应用哪些规则比从数据库中加载规则要快得多,因为每天有成千上万的更新。我敢说 GetRules() 是一个相当密集的过程,因为它可能会从数据库中读取生成的规则并将它们各自转换为表达式树对象。拥有一个专门的规则引擎来监听队列以应用它的规则会更快!

队列/侦听器方法的最大优点之一是它具有很强的可扩展性。如果队列开始备份并且您的规则引擎无法跟上,您可以选择!保持队列较低的最快/最简单的方法...启动另一个规则引擎来侦听同一队列!没错,您可以有多个侦听器到一个队列,并且根据您的设置方式,您可以确保向一个且只有一个侦听器发送消息。

另一个好处是,当您需要更新规则引擎时,您可以停止使用现有的引擎,替换它并启动新的代码库。您不必担心丢失任何内容,队列会继续对事件进行排队,当您启动新代码时,它将开始处理这些事件。

队列/侦听器在测试时很容易设置。我已经将 MSMQ 用于我的几个 Microsoft 堆栈解决方案。我还将 activeMQ 用于基于 java 的解决方案。

因此,您将其与Evk 所说的相结合...您使用表达式树的解决方案并不慢,至少一旦规则在内存中。在这个主题上,您可能希望定期刷新这些“内存中的规则”。您可以有一个固定的时间段,例如每 15 分钟一次,或者您可以更详细地在调用规则的 SaveOrUpdate 方法时触发事件。我可能会选择事件触发,但这一切都取决于业务需求。

您也可以跳过队列方法,只创建一个服务,比如 WCF,它会接受客户数据并处理规则。如果您的警报在保存数据的客户端内触发,他们可以等待回复,或者您可以使用双工服务,该服务可以将警报推送到客户端。这种方法的唯一缺点是只会使用一个特定的服务,因此您无法通过简单地启动第二个服务来使吞吐量翻倍。您可以添加从队列/侦听器向客户端推送通知的功能,这只是多一点工作。

无论如何,长话短说 - 为时已晚!有一些选项可以使您当前的表达式树实现非常可行。我个人认为你在正确的轨道上。我的印象是,您的需求是让最终用户创建和维护这些规则,因此它们不能太死板,因此创建任何类型的分组/二进制解决方案来快速消除大量规则不会成为一种选择.您最终会花更多时间来管理规则组,而不是节省任何时间。

我想我对此有很多话要说,实际上没有编码示例,因为您需要选择一种队列技术,并且可能只是浏览他们的“入门”文档。

祝你好运

【讨论】:

以上是关于当实体匹配用户定义的查询/过滤器/规则时执行操作的主要内容,如果未能解决你的问题,请参考以下文章

iptables 实际操作 之 规则查询 2

iptables iptables实际操作之规则查询

Meteor,ReactJS,MongoDB:当用户离开页面时执行某些操作

应用电子商务操作过滤器时,Big Query 和 Google Analytics UI 不匹配

避免实体框架查询返回的列表中的内存泄漏

HAProxy的ACL调度规则