在Kafka Connect:处理更新和删除的策略分享!
Kafka Connect 是一款出色的工具,可让您轻松设置从一个数据源到目标数据库的连续数据流。它的配置非常简单,当您有遗留系统为您需要的业务数据提供服务时,出于某种原因或其他原因,它在不同的地方非常有用。我的典型用例是将数据从 Oracle 表移动到微服务使用的 MongoDB 集合。这允许更好的可扩展性,因为我们不必使用生产查询大量访问源表。
当您打开 Kafka Connect 手册时,不容易解释的一件事是如何处理修改已移动的现有数据的操作;或者换句话说,更新和删除。我认为这是我们使用的典型 JDBC/MongoDB 连接器对的限制。有一段时间我探索了 Debezium 连接器,它承诺捕获这些类型的事件并将它们复制到目标数据库中。使用 OracleDB 的 POC 对我们来说并不成功。我们对这些数据库的访问有限,而且这些连接器所需的配置级别并不是一个简单的解决方案。
当我们继续使用连接器时,我们发现有一些方法可以处理这些场景。我将解释两种策略。第一个是最理想的,需要在我们的源数据库中进行特定设计。如果该设计不存在且因任何原因无法更改,则第二个是替代解决方案。
基本示例
假设我们有一个处理促销活动的旧系统。为了简化我们的示例,假设我们有一个包含三列的基本表。我们需要不断地将这些数据从 SQL 数据库移动到基于文档的数据库,如 MongoDB。
基本概念
首先,我们需要对可以使用的两种 Kafka 连接器进行快速描述:增量和批量。严格来说,JDBC连接器有四种模式:bulk、timestamp、incrementing、timestamp+incrementing。我将最后三个分组为增量,因为它们共享相同的基本概念。您只想移动从源中检测到的新数据。
批量连接器始终移动整个数据集。但是,很大程度上取决于我们正在移动的数据的用例。理想情况下,增量连接器是最好的解决方案,因为在资源使用或数据准备方面更容易管理小块新数据。这里的问题是:Kafka Connect 如何使用纯 SQL 查询,以及它如何知道何时在源中插入了新数据?
源连接器配置可以使用以下两个属性之一(或两者):incrementing.column.name 和 timestamp.column.name。Incrementing 属性使用增量列(如自动生成的 id)来检测何时插入新行。Timestamp 属性使用 DateTime 列来检测新更改。Kafka Connect 持有一个偏移量,将其附加到用于从源获取数据的 SQL 查询中。
例如,如果我们的表名为“promotions”,我们将在源连接器的查询属性中使用,如下所示:
"query": "SELECT TITLE, DISCOUNT, PRODUCT_CATEGORY FROM PROMOTIONS",
"timestamp.column.name": "LAST_UPDATE_DATE"
Kafka 内部将查询修改为如下所示:
SELECT * FROM ( SELECT TITLE, DISCOUNT, PRODUCT_CATEGORY FROM PROMOTIONS)
WHERE LAST_UPDATE_DATE > {OFFSET_DATE}
在接收器连接器端,即在目标数据库中保存数据的连接器,我们需要设置一个策略来根据 ID 进行正确的 upsert。您可以在您使用的接收器连接器的文档中阅读更多相关信息。对于 MongoDB 连接器,我使用的典型设置是:
"document.id.strategy": "com.mongodb.kafka.connect.sink.processor.id.strategy.ProvidedInValueStrategy",
这表明我们文档的 _id 将来自源数据。在这种情况下,我们的源查询应该包含一个 _id 列:
"query": "SELECT PROMO_ID as \"_id\", TITLE, DISCOUNT, PRODUCT_CATEGORY FROM PROMOTIONS"
至此,我们有了检测新插入的基本配置。每次添加带有新时间戳的新促销时,源连接器都会抓取它并将其移动到所需的目的地。但是有了这个完全相同的配置,我们就可以实现检测更新和删除的总目标。我们需要的是正确设计我们的数据源。
在每次更新时修改时间戳列
如果我们想确保我们的更新被处理并反映在目标数据库中,我们需要确保在源表中进行的每个更新也更新时间戳列值。这可以通过写入它的应用程序将当前时间戳作为更新操作的参数来完成,或者创建一个监听更新事件的触发器。由于 sink 连接器根据 id 处理 upsert,更新也会反映在目标文档中。
软删除
为了能够处理删除,我们需要前面的步骤以及数据库设计中被认为是好的做法:软删除。这种做法是在需要时不删除(硬删除)数据库中的记录,而只是用一个特殊的标志来标记它,表明该记录不再有效/活动。这在可恢复性或审计方面有其自身的好处。这当然意味着我们的应用程序或存储过程需要了解这种设计并在查询数据时过滤掉不活动的记录。
如果很难更新删除记录的应用程序来进行软删除(以防数据源的设计没有考虑到这一点),我们还可以使用触发器来捕获硬删除并改为进行软删除。
为了我们的 Kafka Connect 目的,我们需要做的是在记录被标记为非活动时更改我们的时间戳列值。在此示例中,我们将 HOT SUMMER 促销设置为非活动,将 ACTIVE 列设置为 0。LAST_UPDATE_DATE 还修改为最近的日期,这将使源连接器获取记录。
当数据被移动时,例如移动到 MongoDB,为了使用它,我们还需要根据这个 ACTIVE 字段进行过滤:
db.getCollection('promotions').find({active: 1})
版本化批量
如果我们必须处理不可更改的设计,则可以使用的最后一种方法选项不允许修改源模式以具有时间戳列或活动标志。这个选项有我所说的版本化批量。正如我之前所解释的,每次调用时,批量连接器都会移动整个数据集。在大多数情况下,我遇到过增量更新总是更可取的做法,但在这种情况下,我们可以利用批量选项。
由于我们需要跟踪新插入、更新或删除的内容,因此我们可以每次移动数据,添加一个额外的列来标识数据的快照。我们还可以使用查询数据时的时间戳。由于时间戳是自然后代排序的值,如果我们想要最新的快照,我们可以很容易地通过最后一个或倒数第二个(我将解释为什么这可能更好)一旦数据移动到目标位置的快照进行过滤。
Oracle 中的查询如下所示:
"query": "SELECT PROMO_ID as \"_id\", TITLE, DISCOUNT, PRODUCT_CATEGORY,
TO_CHAR(SYSDATE, 'yyyymmddhh24miss') AS SNAPSHOT FROM PROMOTIONS"
这种方法需要一些配置,这些配置对于使用最终数据集时的正确性能至关重要。您可以想象,索引在这里很重要,更重要的是,在新的快照列中。另一个重要的考虑因素是消耗的空间。根据每个快照中的记录数量,我们可能需要删除旧版本。我们可以为此使用一些计划任务,或者像使用 MongoDB 索引一样配置 TTL。
在使用数据时,我们首先需要获取最新的快照。我提到倒数第二个可能更好。原因是最新的可能是正在进行的。换句话说,当您执行查询以使用数据时,数据可能会移动。如果您对目标数据库的查询是任何类型的聚合,您可能会得到不完整的结果。因此,对于最新的快照,我们不确定它是否处于准备好使用的状态。如果我们抓取倒数第二个,我们可以确定快照是完整的。
在下一个示例中,移动了数据的两个版本。版本 2021073012000包含三个文档。较新的版本2021080112000有两个文档,一个文档有折扣的更新版本。如您所见,每个版本都是数据源的时间快照。
这种方法有点棘手,不应该是我们的第一选择。