在EC核心上下文中同步大型数据库表的最佳方法是什么



我的场景

我有三个仓库数据库(Firebird),编号为123,每个数据库共享相同的方案和相同的DbContext类。以下是产品表的型号:

public class Product
{
public string Sku { get; }
public string Barcode { get; }
public int Quantity { get; }
}

我也有一个当地的";仓库缓存";数据库(MySQL),出于缓存原因,我想定期下载所有三个仓库的内容。缓存产品的数据模型类似,只是添加了一个表示源仓库索引的数字。此表应包含来自所有三个仓库的所有产品信息。如果一个产品同时出现在13仓库(相同的Sku)中,那么我希望在本地Cache表中有两个条目,每个条目都有相应的仓库ID:

public class CachedProduct
{
public int WarehouseId { get; set; } // Can be either 1, 2 or 3
public string Sku { get; }
public string Barcode { get; }
public int Quantity { get; }
}

这个问题有多种可能的解决方案,但考虑到我的数据集的大小(每个仓库约2万个条目),它们似乎都不可行或有效,我希望有人能给我一个更好的解决方案。

问题

如果本地缓存数据库是空的,那么这很容易。只需从所有三个仓库下载所有产品,并将它们转储到缓存DB中。但是,在随后的同步中,缓存DB将不再为空。在这种情况下,我不想再次添加所有6万个产品,因为这将是对存储空间的巨大浪费。相反,我想";upstart";传入的数据进入缓存,因此新产品通常会被插入,但如果缓存中已经存在产品(匹配SkuWarehouseId),那么我只想更新相应的记录(例如,自上次同步以来,其中一个仓库中的数量可能已经更改)。这样,缓存DB中的记录数将始终恰好是三个仓库的总和;从不多也从不少。

到目前为止我尝试过的东西

贪婪的方法:这个方法可能是最简单的。对于每个仓库中的每个产品,检查缓存表中是否存在匹配的记录。如果是,则为update,否则为insert。显而易见的问题是,没有办法对此进行批处理/优化,这将导致在每次同步时执行数以万计的selectinsertupdate调用。

:清除缓存:每次同步前清除本地缓存数据库,并重新下载所有数据。我对这个的问题是,当没有缓存数据可用时,它会留下一个很小的时间窗口,这可能会导致应用程序的其他部分出现问题。

使用EF核心";Upsert";library:这似乎是FlexLabs.Upsert库中最有前途的一个,因为它似乎支持批处理操作。不幸的是,这个库似乎坏了,因为我甚至无法让他们自己的最小示例正常工作。在每"1"上插入一个新行;upstart";,而不管匹配规则如何。

完全避开EF Core:我发现了一个名为Dotmim.Sync的库,它似乎是一个DB到DB的同步库。这方面的主要问题是仓库正在运行FirebirdDB,而这个库似乎不支持它。此外,我甚至不确定是否可以进行数据转换,因为在将行添加到缓存DB之前,我必须添加WarehouseId列。


在EF Core中有没有一种方法可以尽可能有效地做到这一点?

这里有几个选项。哪些是可行的取决于缓存的过时限制。缓存必须始终100%与仓库状态相关,否则它可能会在一段时间内不同步。

首先,您绝对不应该为此使用EFCore,除非可能将其作为客户端库来执行原始SQL。EfCore针对许多小型事务进行了优化。它在处理批处理工作负载时效果不佳。

"最佳"选项可能是基于事件的系统。Firebird支持向事件侦听器发送事件,然后事件侦听器将根据事件更新缓存。这里的风险是,如果事件处理失败,您可能会失去同步。您可以通过使用某种类型的事件总线(Rabbit、Kafka)来减轻这种风险,但Firebird事件处理本身将是薄弱环节。

如果缓存可以处理某些不一致性,则可以为每个缓存条目附加一个到期时间戳。您的应用程序命中缓存,如果过期日期已过,它将重新检查仓库dbs。根据更新真相来源数据库的业务流程,您还可以破坏缓存条目(例如,如果有订单管理系统,它可以在有人下订单时破坏行项目的缓存)。

如果必须批量同步,请执行交换表。设置一个包含实时缓存数据的表,一个单独的表,加载新的缓存数据,并在应用程序中设置一个标志,说明您从哪个表中读取。当你加载到B时,你从表A中读取,然后当加载完成时,你切换到从表B中读取。

目前,我最终选择了一个简单而有效的解决方案,它完全在EF Core中。

对于每个缓存条目,我还维护一个SyncIndex列。在同步过程中,我从所有三个仓库下载所有产品,将SyncIndex设置为max(cache.SyncIndex) + 1,并将它们转储到缓存数据库中。然后,我用旧的SyncIndex从缓存中删除所有条目。这样,我总是有一些可用的缓存数据,我不会浪费很多空间,而且速度也可以接受。

相关内容