如何在PostgreSQL 跟踪数据变更?
PostgreSQL 是最受欢迎的数据库之一,被 DB-Engines Ranking 评为 2023 年度 DBMS,并且根据 HN Hiring Trends 的数据,PostgreSQL 在初创公司中的使用率超过任何其他数据库。
PostgreSQL 是初创公司中最流行的数据库。
自 2011 年以来,SQL 标准就包含了与temporal databases相关的功能,这些功能允许存储随时间变化的数据,而不仅仅是当前的数据状态。然而,关系数据库并不完全遵循标准。就 PostgreSQL 而言,它不支持这些功能,尽管已经提交了包含一些讨论的patch。
有一些 PostgreSQL 扩展(例如 periods 和temporal_tables)添加了对时态表(temporal)的支持。不幸的是,AWS、Azure 和 GCP 等云提供商不允许使用托管数据库运行自定义 C 扩展。
让我们探索 2024 年 PostgreSQL 中可用的五种替代数据更改跟踪方法。
Triggers and Audit Table 触发器和审计表
带有审计表的 PostgreSQL 触发器
PostgreSQL 允许使用对 INSERT
、 UPDATE
和 DELETE
查询的行更改执行的自定义过程 SQL 代码添加触发器。官方 PostgreSQL wiki 描述了一个通用的audit trigger function。让我们快速看一下一个简化的示例。
首先,在名为 audit
的单独架构中创建一个名为 logged_actions
的表:
sql
CREATE schema audit;
CREATE TABLE audit.logged_actions (
schema_name TEXT NOT NULL,
table_name TEXT NOT NULL,
user_name TEXT,
action_tstamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT current_timestamp,
action TEXT NOT NULL CHECK (action IN ('I','D','U')),
original_data TEXT,
new_data TEXT,
query TEXT
);
接下来,创建一个函数来插入审计记录并在您想要跟踪的表上建立触发器,例如 my_table
:
sql
CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $body$
BEGIN
IF (TG_OP = 'UPDATE') THEN
INSERT INTO audit.logged_actions (schema_name,table_name,user_name,action,original_data,new_data,query)
VALUES (TG_TABLE_SCHEMA::TEXT,TG_TABLE_NAME::TEXT,session_user::TEXT,substring(TG_OP,1,1),ROW(OLD.*),ROW(NEW.*),current_query());
RETURN NEW;
elsif (TG_OP = 'DELETE') THEN
INSERT INTO audit.logged_actions (schema_name,table_name,user_name,action,original_data,query)
VALUES (TG_TABLE_SCHEMA::TEXT,TG_TABLE_NAME::TEXT,session_user::TEXT,substring(TG_OP,1,1),ROW(OLD.*),current_query());
RETURN OLD;
elsif (TG_OP = 'INSERT') THEN
INSERT INTO audit.logged_actions (schema_name,table_name,user_name,action,new_data,query)
VALUES (TG_TABLE_SCHEMA::TEXT,TG_TABLE_NAME::TEXT,session_user::TEXT,substring(TG_OP,1,1),ROW(NEW.*),current_query());
RETURN NEW;
END IF;
END;
$body$
LANGUAGE plpgsql;
CREATE TRIGGER my_table_if_modified_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
FOR EACH ROW EXECUTE PROCEDURE if_modified_func();
完成后,在 my_table
中进行的行更改将在 audit.logged_actions
中创建记录:
sql
INSERT INTO my_table(x,y) VALUES (1, 2);
SELECT * FROM audit.logged_actions;
如果您想通过使用 JSONB 列而不是 TEXT、忽略某些列中的更改、暂停审核表等来进一步改进此解决方案,请查看此审核audit-trigger代码库及其分支中的 SQL 示例。
另一种选择是使用触发器编写的temporal_tables实现。主要区别在于,它将记录存储在一个单独的表中,其中包含版本有效的时间范围,而不仅仅是记录更改时的初始时间戳。通过选择在特定时间点有效的记录,可以更轻松地执行时间旅行查询。
缺点
-
Performance.
触发器通过在每个
INSERT
、UPDATE
和DELETE
操作上同步插入附加记录来增加性能开销。 -
Security.
具有超级用户访问权限的任何人都可以修改触发器并进行不被注意的数据更改。还建议确保审计表中的记录不能被修改或删除。
-
Maintenance. 在许多不断变化的表中管理复杂的触发器可能会变得很麻烦。在 SQL 脚本中犯一个小错误可能会破坏查询或数据更改跟踪功能。
Triggers and Notify/Listen
带 Notify 的 PostgreSQL 触发器
这种方法与前一种类似,但我们不是直接在审计表中写入数据更改,而是通过触发器通过发布/订阅机制将它们传递到另一个专门用于读取和存储这些数据更改的系统:
sql
CREATE OR REPLACE FUNCTION if_modified_func() RETURNS TRIGGER AS $body$
BEGIN
IF (TG_OP = 'UPDATE') THEN
PEFORM pg_notify('data_changes', json_build_object(
'schema_name', TG_TABLE_SCHEMA::TEXT,
'table_name', TG_TABLE_NAME::TEXT,
'user_name', session_user::TEXT,
'action', substring(TG_OP,1,1),
'original_data', jsonb_build(OLD),
'new_data', jsonb_build(NEW)
)::TEXT);
RETURN NEW;
elsif (TG_OP = 'DELETE') THEN
PEFORM pg_notify('data_changes', json_build_object(
'schema_name', TG_TABLE_SCHEMA::TEXT,
'table_name', TG_TABLE_NAME::TEXT,
'user_name', session_user::TEXT,
'action', substring(TG_OP,1,1),
'original_data', jsonb_build(OLD)
)::TEXT);
RETURN OLD;
elsif (TG_OP = 'INSERT') THEN
PEFORM pg_notify('data_changes', json_build_object(
'schema_name', TG_TABLE_SCHEMA::TEXT,
'table_name', TG_TABLE_NAME::TEXT,
'user_name', session_user::TEXT,
'action', substring(TG_OP,1,1),
'new_data', jsonb_build(NEW)
)::TEXT);
RETURN NEW;
END IF;
END;
$body$
LANGUAGE plpgsql;
CREATE TRIGGER my_table_if_modified_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
FOR EACH ROW EXECUTE PROCEDURE if_modified_func();
现在可以运行一个单独的进程作为工作线程来监听包含数据更改的消息并单独存储它们:
sql
LISTEN data_changes;
缺点
- "最多一次"交付。Listen/notify 通知不会持续存在,这意味着如果listener断开连接,它可能会错过再次重新连接之前发生的更新。
- 有效负载大小限制。默认情况下,Listen/notify 消息的最大负载大小为 8000 字节。对于较大的有效负载,建议将它们存储在数据库审计表中并仅发送记录的引用。
- 调试。由于其异步和分布式特性,对生产环境中与触发器和监听/通知相关的问题进行故障排除可能具有挑战性。
Application-Level Tracking
应用程序级跟踪
使用 PostgreSQL 审核表进行应用程序级跟踪
如果您可以控制连接 PostgreSQL 数据库并在其中进行数据更改的代码库,则还可以使用以下选项之一:
-
发出
INSERT
、UPDATE
和DELETE
查询时手动记录所有数据更改 -
使用与流行的 ORM 集成的现有开源库
例如,Ruby on Rails 的 paper_trail和 ActiveRecord 以及 Django 的 django-simple-history 。在较高级别上,他们使用回调或中间件将附加记录插入审计表中。下面是一个用 Ruby 编写的简化示例:
ruby
class User < ApplicationRecord
after_commit :track_data_changes
private
def track_data_changes
AuditRecord.create!(auditable: self, changes: changes)
end
end
在应用程序级别,还可以使用仅附加日志作为事实来源来实现Event Sourcing。但这是一个独立的、宏大的、令人兴奋的话题,值得单独写一篇博客文章。
缺点
- 可靠性。应用程序级数据更改跟踪不如数据库级更改跟踪准确。例如,应用程序外部进行的数据更改将不会被跟踪,开发人员可能会意外跳过回调,或者如果更改数据的查询成功但插入审核记录的查询失败,则可能会出现数据不一致。
- 性能。手动捕获更改并通过回调将其插入数据库会导致运行时应用程序和数据库开销。
- 可扩展性。这些审计表通常存储在同一个数据库中,很快就会变得难以管理,这可能需要分离存储、实施声明性分区和连续归档。
Change Data Capture
Change Data Capture (CDC) 是一种识别和捕获对数据库中的数据所做的更改并将这些更改发送到下游系统的模式。最常用于 ETL 将数据发送到数据仓库以进行分析。
实现 CDC 有多种方法。其中之一是基于日志的 CDC,它与我们已经讨论过的内容没有交叉。使用 PostgreSQL,可以连接到用于数据持久性、恢复和复制到其他实例的Write-Ahead Log (WAL)。
具有 PostgreSQL 逻辑复制的 CDC
PostgreSQL支持两种类型的复制:物理复制和逻辑复制。后者允许在行级别解码 WAL 更改并过滤掉它们,例如通过表名称。这正是我们使用 CDC 实现数据更改跟踪所需要的。
以下是使用逻辑复制检索数据更改所需的基本步骤:
-
将
postgresql.conf
中的wal_level
设置为logical
,然后重启数据库。 -
创建一个类似于"pub/sub channel"的发布,用于接收数据更改:
sql
CREATE PUBLICATION my_publication FOR ALL TABLES;
- 在 WAL 中创建一个类似于"cursor position"的逻辑复制槽:
sql
SELECT * FROM pg_create_logical_replication_slot('my_replication_slot', 'wal2json')
- 获取最新的未读更改:
sql
SELECT * FROM pg_logical_slot_get_changes('my_replication_slot', NULL, NULL)
要使用 PostgreSQL 实现基于日志的 CDC,我建议使用现有的开源解决方案。最受欢迎的一种是Debezium。
缺点
- 上下文有限。 PostgreSQL WAL 仅包含有关 行更改的低级信息,不包括有关触发更改的 SQL 查询的信息、有关用户的信息或任何特定于应用程序的上下文。
- 复杂。实施 CDC 会增加很多系统复杂性。这涉及到运行一个连接到 PostgreSQL 作为副本的服务器,使用数据更改并将它们存储在某个地方。
- 调优。在生产环境中运行它可能需要更深入地了解 PostgreSQL 内部结构并正确配置系统。例如,定期刷新复制槽的位置以回收 WAL 磁盘空间。
集成变更数据捕获
将 CDC 与应用程序上下文集成
为了克服 WAL 中存储的数据更改信息有限的挑战,我们可以使用一种巧妙的方法,将附加上下文直接传递给 WAL。
以下是在行更改时传递附加上下文的简单示例:
sql
CREATE OR REPLACE FUNCTION if_modified_func() RETURNS TRIGGER AS $body$
BEGIN
PERFORM pg_logical_emit_message(true, 'my_message', 'ADDITIONAL_CONTEXT');
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$body$
LANGUAGE plpgsql;
CREATE TRIGGER my_table_if_modified_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
FOR EACH ROW EXECUTE PROCEDURE if_modified_func();
pg_logical_emit_message
函数作为插件的内部函数添加到 PostgreSQL。它允许命名空间并发出将存储在 WAL 中的消息。从 PostgreSQL v14 开始,使用标准逻辑解码插件 pgoutput
就可以读取这些消息。
有一个名为 Bemi的开源项目,它不仅可以跟踪低级数据更改,还可以使用 CDC 读取任何自定义上下文并将所有内容缝合在一起。完全免责声明,我是核心贡献者之一。
例如,它可以与流行的 ORM 和适配器集成,以传递所有数据更改的应用程序特定上下文:
js
import { setContext } from "@bemi-db/prisma";
import express, { Request } from "express";
const app = express();
app.use(
// Customizable context
setContext((req: Request) => ({
userId: req.user?.id,
endpoint: req.url,
params: req.body,
}))
);
缺点
- 与实施 CDC 相关的复杂性和调优。
如果您需要一个可以在几分钟内集成并连接到 PostgreSQL 的即用型云解决方案,请查看 bemi.io。
结论
PostgreSQL 跟踪数据变更方法比较
- 如果您需要基本的数据更改跟踪,triggers with an audit table 是一个很好的初始解决方案。
- 具有Triggers with listen/notify是在开发环境中进行简单测试的不错选择。
- 如果您更看重应用程序特定的上下文(有关用户、API 端点等的信息)而不是可靠性,则可以使用应用程序级跟踪。
- 如果您优先考虑可靠性和可扩展性作为可以跨多个数据库重用的统一解决方案,那么Change Data Capture是一个不错的选择。
- 最后,如果您需要一个强大的数据更改跟踪系统并且可以集成到您的应用程序中,那么集成的更改数据捕获是您的最佳选择。如果您需要云托管解决方案,请选择 bemi.io。