实体与用户定义的查询/过滤器/规则

时间:2017-04-10 14:01:02

标签: c# .net notifications rule-engine

通常,您编写查询并获取与其匹配的所有记录(实体)。我需要反过来。

假设我有1M个客户,有十几个非规范化属性:

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的客户为空且销售区域为IN(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世界之外。

6 个答案:

答案 0 :(得分:9)

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

// 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.ElapsedMilliseconds}ms");
        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分钟内检查全天所有客户更新的所有规则(在适当的服务器上,它应该至少比此快x2,可能更多)。根据10K规则检查单个更新需要几毫秒。鉴于此 - 你很可能在任何其他地方都有瓶颈,而不是在这里。如果您愿意,可以在此基础上应用几个简单的优化:

  • 折叠相同的规则。无需检查&#34;今天是生日&#34;每个用户的规则。

  • 存储规则中使用的属性,并记录在Customer中更新了哪些列。不要运行不使用客户更新列的规则。

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

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

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

除此之外,这种任务很容易在多台机器上并行化,所以如果你能够遇到100M客户 - 没问题,你可以再添加一台服务器(或者可能还有一台服务器)。

答案 1 :(得分:2)

基本问题是:

  

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

您提及要检查的5-10k个自定义过滤器&#39;。如果数字太大,你可能会有一些灵活的规则结构,比如

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

所有种类都包含<field><operator><value>的值。

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

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

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

当然,您可能会遇到一些有限的规则,这些规则很复杂,只能从.NET代码中检查。可以在发布时为foreach循环播放,只要此类规则的数量不应该很大。

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

答案 2 :(得分:2)

使用1M更新和10k规则,您需要减少要检查的规则数。由于您只有几十个属性,因此这应该是您运行规则的选择条件。首先根据规则中存在的属性过滤规则,并将其与更新的属性进行比较。

  • 将SearchParameters字段添加到规则中,并为其指定值 010405如果规则仅包含参数01(名称),04(生日)和 05(lastemailed)。
  • 将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存储在规则表中,所以你得到下表:

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查询的结果是一个包含指向该特定客户记录的recordID的列表。

希望这有点帮助。

答案 3 :(得分:2)

每次用户提出请求时按顺序执行所有过滤器即使不是不可能立即完成也很困难。

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

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

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

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

答案 4 :(得分:2)

答案 5 :(得分:2)

您绝对不想将记录保存到数据库以运行规则。在IsMatch()或DoAction()中发生的任何错误都可能会中止正在保存的数据。我会假设这样一个事实,即某人的生日并不像将这个人真正添加到数据库那样重要。

我认为将添加/更新事件添加到排队系统。现在不要把排队系统想象成一个堆积起来并等待很长时间的地方! Windows操作系统是一个排队系统,它几乎可以使用消息队列。因此,您发布的CustomerService.SaveOrUpdate方法会向您的&#34; UpdatedUser&#34;发送一个事件(或消息,我会发现它更容易将其视为一个事件)。队列。此队列将有一个或多个侦听器,等待事件显示。然后他们会接受该事件并找到符合其数据的任何规则并执行相应的操作。

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

队列/侦听器方法最好的一点是它非常易于扩展。如果队列开始备份并且您的规则引擎无法跟上,那么您可以选择!最快/最简单的方法来降低队列...启动另一个侦听同一队列的规则引擎!没错,你可以有一个队列的多个监听器,根据你设置的方式,你可以确保一个消息只发送一个监听器。

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

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

所以你将它与Evk所说的结合起来......你的表达式树的解决方案并不慢,至少一旦规则在内存中。在这个主题上,你会希望在内存中有这些规则&#34;定期刷新。你可以有一个固定的时间段,比如每15分钟一次,或者你可以更精细,并在调用规则的SaveOrUpdate方法时触发事件。我可能会选择事件解雇,但这一切都取决于业务需求。

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

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

我想我在这方面有很多话要说,而且实际上没有编码示例,因为你需要选择一个队列技术,而且可能只需要完成他们的#34;入门&#34;文档。

祝你好运