修改触发器GDQ
通用修改触发器和广义数据队列规范
问题陈述
解决不同的问题
- 像 Slony/Londiste 事务队列一样的复制
- 复制到异构系统,可能是非事务性的
- 在 物化视图 中的差异更新。
当前“队列表”实施的问题
- 数据写入和查找/扫描开销。
- 以及真空开销
- 没有好的方法来包含事务性 DDL 更改
- 大型数据的问题
- 无法确保复制触发器最后被触发
- “轮询”模型固有的延迟
“队列表”实施的好处
- 可以确保只有已提交的数据被复制
- 持久存储
实施建议
按 Itagaki 的说法
- 用于行修改的非表格存储
- 将项目发送到另一个服务器
- 实施非持久性选项(全局临时表)
按 Josh 的说法
- 基于当前约束触发器代码编写一个可延迟触发器。
- 研究用于行存储的紧凑格式(protobuf?)。
- 使用第三方队列 (AMQ、AMQP、Spread?) 进行传输。
- 想出一种方法将 DDL 在适当的时候注入到队列中。
建议(cbbrowne)... 这在两个方面似乎是一个正交问题- 捕获 DDL 信息与捕获数据修改不同
- 捕获将 DDL 注入队列的时间点是独立的,也许可以使用以下组合
- 共享序列指示排序
- 提交时间戳信息(也可能是一种序列号)来控制对更改集之间的边界的访问
按 Marko 的说法
- 复制触发器不能最后触发,而应该在所有 BEFORE 触发器之后和所有(非复制)AFTER 触发器之前触发...
- DDL 事件定位是一个已解决的问题——通常的 AFTER 触发器“一致排序”逻辑也适用于此
- 启动事务。
- 执行 DDL,这将对适当的对象进行锁定。
- 从 seq 中获取事件编号——这里所有可能冲突的操作都在等待锁,因此还没有从 seq 中分配它们的事件 ID。
- 将事件插入队列。
- 提交,释放锁。
- 队列表效率似乎也是一个已解决的问题
- 使用高效的获取查询 (PgQ)
- 表格应该是只插入的,并且经常轮换——例如每 5-10 分钟——以使表格尽可能小。查看 PgQ 如何在没有锁定问题的情况下做到这一点。
- 最小的读取开销——活动数据在内存中。
- 最小的 VACUUM 开销。
- 最小的索引维护开销。
设计
触发器
修改触发器必须在所有 BEFORE 触发器之后和所有 AFTER 触发器之前触发。它们可以访问语句类型(INSERT/UPDATE/DELETE)和旧元组和新元组,但不能修改新元组,也不能取消语句,除非出现错误情况。
DDL 触发器在 独立部分 中进行了讨论。但是,我们可能在队列层处理 TRUNCATE,因为在某些情况下,TRUNCATE 之前的修改事件将被丢弃。
队列
我们有一些选择来记录队列中的修改;完整描述与仅主键。它们有权衡,因此我们最好有选择使用哪一种的选项。
- 完整描述队列
- 它们不仅包括修改元组的主键,还包括非键字段。它们被 Slony 和 Londiste 使用。
- 优点
- 在回放时,我们可以避免对队列和原始表进行 JOIN。
- 我们可以回放所有实际的操作。
- 缺点
- 与仅 PK 队列相比,队列的大小将相对较大。
- 仅 PK 队列
- 它们只包括修改元组的主键。PK 的更新将以 DELETE 和 INSERT 的组合记录。它们被 Oracle 的物化视图日志使用。
- 优点
- 与完整描述队列相比,队列的大小将相对较小。
- 我们可以轻松地合并对相同元组的多个 UPDATE 操作。
- 缺点
- 在回放时,我们需要对队列和原始表进行 JOIN 以检索实际数据。
- 我们只能重建表的最终状态。过渡状态是不可能的。
实施
另请参阅 GDQ 实施 中的讨论。
队列可以使用标准表实现,Slony 和 PgQ 也使用它们,但我们可以使用另一种数据结构来实现它们,因为 GDQ 只需要 INSERT/SELECT/TRUNCATE。标准表的全部功能并不总是需要的。但是,队列的使用者(订阅者)并不总是以 FIFO 方式读取数据。一些使用者以随机顺序读取队列,因为它们需要基于快照的修改记录分组。
- 队列表应该由多个只插入的表组成。我们以一定的周期轮换它们,并截断旧的日志记录以避免 DELETE 的开销。
- 队列可以使用内存中或非 WAL 日志存储引擎实现。如果我们使用这种优化,复制服务器必须在主服务器崩溃后重新复制所有表内容。
- 使用非表格存储来存储队列,比如一些 WAL 风格的平面文件,以避免开销。
- 使用自定义 WAL 记录进行复制。WALSender 可以将复制记录发送到备用服务器。
- 使用 WAL 的一个缺点是,在某些情况下,使用者可能需要旧记录。WALSender 需要从归档中读取旧的 WAL 记录,但这是不可能的。
- GDQ 的自定义存储可能类似于 SQL/MED 外部表。我们可以扩展 SQL 标准以处理外部表,以处理 INSERT 命令,并将其用作存储。
语法
我们有选择用于创建队列和注册队列使用者的语法的选择。哪一个更好?
- 基于函数
- PgQ 有 pgq.create_queue() 和 pgq.register_consumer()。
- 灵活,但表名以字符串文字(单引号)形式出现。
- 基于 SQL
- Oracle 有 CREATE MATERIALIZED VIEW LOG 和 CREATE MATERIALIZED VIEW。
- 另一个 SQL 想法是使用 ALTER TABLE,比如 ADD MODIFICATION QUEUE。
参考文献
广义数据队列应该有足够的强大功能来支持现有的复制产品,比如 Slony 和 Londiste。
Slony 的 sl_log
Slony 的 sl_log 存储要传播到订阅者节点的每个更改。日志由两个表组成,sl_log_1 和 sl_log_2,以有效地清除旧记录。
CREATE TABLE sl_log ( log_origin integer, -- Origin node from which the change came log_xid xxid, -- Transaction ID on the origin node log_tableid integer, -- The table ID that this log entry is to affect log_actionseq bigint, log_cmdtype char(1), -- U = Update, I = Insert, D = DELETE log_cmddata text, -- The data needed to perform the log action );
在版本 2.1 中,log_cmdtype 扩展了一点,并且预计在 2.2 中会进一步扩展。值是
值 | 描述 |
---|---|
I | 插入 |
U | 更新 |
D | 删除 |
T | 截断 |
S | DDL 脚本 |
Londiste 的 PgQ
PgQ 定义了一个基于文本的广义队列,Londiste 通过 触发器 使用这些队列。每个队列都由多个表组成(默认情况下为 3 个),原因与 Slony 相同。
CREATE TABLE pgq.event_template ( ev_id bigint, -- event's id, supposed to be unique per queue ev_time timestamptz, -- when the event was inserted ev_txid bigint, -- transaction id which inserted the event ev_owner int4, -- subscription id that wanted to retry this ev_retry int4, -- how many times the event has been retried, NULL for new events ev_type text, -- I/U/D ev_data text, -- partial SQL statement or column values urlencoded ev_extra1 text, -- table name ev_extra2 text, -- (not used in Londiste) ev_extra3 text, -- (not used in Londiste) ev_extra4 text -- (not used in Londiste) );
Oracle 数据库中的物化视图日志
Oracle 数据库支持基于物化视图日志的行级复制。日志用于在 物化视图 中进行差异更新,但也用于结合 DATABASE LINK 进行复制。有关详细信息,请参阅 规划您的复制环境。PostgreSQL 可能从 Oracle 数据库中学习复制/广义数据队列的设计,其中基于行的逻辑复制基于 3 个基本模块
- 广义数据队列
- 具有差异更新的物化视图
- 数据库间连接
物化视图日志默认情况下只包括主键,但用户可以在日志中添加任何列。简单的物化视图只需要主键用于差异更新。实际数据似乎是使用与主键连接的原始表检索的。另一方面,复杂的物化视图(例如,包括聚合)需要引用列数据。请注意,与 Slony 和 Londiste 不同,Oracle 的日志不包含修改或插入的列值。主键的更改被描述为 DELETE 和 INSERT 的组合。
CREATE TABLE oracle_mlog_pk ( pk_1 pk_type_1, -- first primary key pk_2 pk_type_2, -- second primary key ... pk_N pk_type_N, -- Nth primary key snaptime timestamptz, -- first snapshot sets initial refresh time dmltype char(1), -- Type of DML old_new char(1), -- O/N/U (= D/I/U in Slony) change_vector bytea -- Used for subquery and LOB snapshots (?) );