通常,您编写一个查询并获取与其匹配的所有记录(实体)。我需要做相反的事情。
假设我有 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.
}
我有 1 万名用户想要设置自定义过滤器,并在任何新客户符合他们定义的规则时收到通知。
其中一些规则在创建/更新客户时进行评估(例如)
- 有电话号码并且在我的销售区域内的客户。
- 具有电子邮件地址且 LastEmailed 为 NULL 且销售区域在 (1, 7, 11) 中的客户
其他规则将定期运行(例如)
- 今天生日的客户。
每天都会为客户节省数百万笔费用,并针对每个新/更新的客户检查 5-10,000 个自定义过滤器。
我意识到我可以使用 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 世界。
最佳答案
我会提到与其他答案不同的观点。您在代码中声称
// 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 条规则。它们很简单,但并非微不足道 - 4 个条件与 OR、字符串、日期、包含相结合。然后我生成 100 万个客户更新(您数据库中的客户数量无关紧要 - 我们只处理更新)并运行一个循环。猜猜我的常规(非服务器)PC 需要多长时间? 4 分钟。
因此,您可以在短短 4 分钟内检查一整天所有客户更新的所有规则(在适当的服务器上,它应该至少比这快 2 倍,可能更多)。根据 10K 规则检查单个更新需要几毫秒。鉴于此 - 你很可能会在任何其他地方遇到瓶颈,而不是在这里。如果您愿意,您可以在此基础上应用一些简单的优化:
折叠相同的规则。无需为每个用户检查“今天是生日”规则。
存储规则中使用的属性,并注意在客户中更新了哪些列。不要运行不使用客户中更新的列的规则。
但实际上这甚至可能会减慢你的速度,而不是加快速度,所以一切都应该衡量。
不要从进行规则检查的同一代码发送通知。将它们放入队列,让其他进程\线程处理它们。检查规则是严格的 CPU 绑定(bind)工作,发送通知(我假设,在你的情况下)是 IO 绑定(bind)的,所以你实际上可以在一台机器上,在一个进程中做到这一点。您也不希望以这种速度向给定用户发送垃圾邮件通知,您很可能会分批发送它们,我认为最多每分钟一批,所以这不会太昂贵。
至于客户更新本身——您可以将它们存储在某个队列中(如 rabbitMQ),使用数据库通知(如 postgresql pg_notify
)或每分钟轮询一次数据库以获取该期间的所有更新。同样,应该衡量不同方法的性能。
除此之外,这种任务很容易在多台机器上并行化,因此如果您要达到 1 亿客户 - 没问题,您可以再添加一台服务器(或者可能一台仍然没问题)。
关于c# - 当实体匹配用户定义的查询/过滤器/规则时执行操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43325292/