暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 逻辑复制原理浅析3-使用限制和无限循环的双向复制

逻辑复制使用限制

1.数据库的模式和DDL不会复制

这里可能会有个疑问,当我们在主库上创建订阅的时候指定for all tables,然后建新表是否会自动同步?

我们来测试一下。

--发布实例CREATE PUBLICATION testpub FOR ALL TABLES;hr=# CREATE PUBLICATION testpub FOR all tables;CREATE PUBLICATIONhr-# \dRp                                 List of publications  Name   |  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Via root ---------+----------+------------+---------+---------+---------+-----------+---------- testpub | postgres | t          | t       | t       | t       | t         | f(1 row)hr=# select * from pg_publication_tables; pubname | schemaname | tablename ---------+------------+----------- testpub | public     | jobs(1 row)
复制

当前我的发布实例,hr数据库中有一张jobs表在发布组里面。此时我在订阅实例创建订阅,jobs表之前已存在。此时会发生关于jobs表上数据的同步。

--订阅实例hr=# create subscription testsub connection 'host=192.168.56.119 port=5432 dbname=hr user=replication' publication testpub;hr=# \d       List of relations Schema | Name | Type  | Owner --------+------+-------+------- public | jobs | table | hr(1 row)
复制

我执行ddl进行测试。

--发布实例hr=# alter table jobs add a1 integer;ALTER TABLE--订阅实例hr=# \d+ jobs                                             Table "public.jobs"   Column   |         Type          | Collation | Nullable | Default | Storage  | Stats target | Description ------------+-----------------------+-----------+----------+---------+----------+--------------+------------- job_id     | character varying(10) |           | not null |         | extended |              |  job_title  | character varying(35) |           | not null |         | extended |              |  min_salary | integer               |           |          |         | plain    |              |  max_salary | integer               |           |          |         | plain    |              | Access method: heap
复制

可见发布实例上执行完ddl,并不会同步到订阅实例。

此时若执行create table,我们看看会发生什么?

--发布实例CREATE TABLE departments (        department_id smallint NOT NULL,        department_name varchar(30NOT NULL,        manager_id integer,        location_id smallint) ;hr=# select * from pg_publication_tables; pubname | schemaname |  tablename  ---------+------------+------------- testpub | public     | departments testpub | public     | jobs(2 rows)--订阅实例hr=# \dRs           List of subscriptions  Name   |  Owner   | Enabled | Publication ---------+----------+---------+------------- testsub | postgres | t       | {testpub}(1 row)
复制

可以看到如果创建新表,它会自动加到发布列表中,如果你在订阅实例有同样的表就可以复制,如果没有,它是不会自动创建的。

虽然不能复制DDL,但是我们可以绕行一下,我们可以使用触发器来实现DDL的复制。简单来讲就是把DDL的语句记录下来存到表里面,然后发送给订阅侧,订阅侧写入DDL记录,与此同时这张表上的触发器也会触发,然后执行DDL操作。

2.Truncate命令的部分限制

Truncate命令在PostgreSQL属于DML语言,这个在我之前些的文章《PostgreSQL 查LAST DDL TIME的三种方法》讨论过。那么它应该可以复制,确实如此它是可以复制的。虽然它可以复制但是它有一个需要注意的地方:“表上存在主外键关系。”

--发布侧和订阅侧都创建CREATE TABLE departments (        department_id smallint NOT NULL,        department_name varchar(30NOT NULL,        manager_id integer,        location_id smallint) ;CREATE TABLE employees (        employee_id integer NOT NULL,        first_name varchar(20),        last_name varchar(25NOT NULL,        email varchar(25NOT NULL,        phone_number varchar(20),        hire_date timestamp NOT NULL,        job_id varchar(10NOT NULL,        salary double precision,        commission_pct real,        manager_id integer,        department_id smallint) ;hr=# ALTER TABLE employees ADD PRIMARY KEY (employee_id);ALTER TABLEhr=# ALTER TABLE departments ADD CONSTRAINT dept_mgr_fk FOREIGN KEY (manager_id) REFERENCES employees(employee_id) ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE;ALTER TABLEhr=# select * from pg_publication_tables; pubname | schemaname |  tablename  ---------+------------+------------- testpub | public     | employees testpub | public     | departments testpub | public     | jobs(3 rows)
复制

此时在发布侧执行truncate主键表。

hr=# truncate table employees;ERROR:  cannot truncate a table referenced in a foreign key constraintDETAIL:  Table "departments" references "employees".HINT:  Truncate table "departments" at the same timeor use TRUNCATE ... CASCADE.hr=# truncate table employees CASCADE;NOTICE:  truncate cascades to table "departments"TRUNCATE TABLE
复制

这里提示报错需要指定CASCADE选项级联truncate。我们执行runcate .... CASCADE,可以正常同步到订阅端。这是因为我们的发布端中有这2个表。如果我们的发布端,这两张表属于两个发布,订阅端也使用两个独立开的订阅。这个truncate就会存在问题。我们来测试一下。

--发布端hr=# create publication testpub1 FOR table employees; hr=# create publication testpub2 FOR table departments;--订阅端hr=# create subscription testsub1 connection 'host=192.168.56.119 port=5432 dbname=hr user=replication' publication testpub1; hr=# create subscription testsub2 connection 'host=192.168.56.119 port=5432 dbname=hr user=replication' publication testpub2; 
复制

现在我们在发布端执行truncate主表

hr=# truncate table employees cascade;
复制

然后在订阅端的日志马上就会出现下列报错。

2021-03-25 21:17:31.131 CST [1775] ERROR:  cannot truncate a table referenced in a foreign key constraint2021-03-25 21:17:31.131 CST [1775] DETAIL:  Table "departments" references "employees".2021-03-25 21:17:31.131 CST [1775] HINT:  Truncate table "departments" at the same time, or use TRUNCATE ... CASCADE.2021-03-25 21:17:31.132 CST [1466] LOG:  background worker "logical replication worker" (PID 1775) exited with exit code 12021-03-25 21:17:31.134 CST [1794] LOG:  logical replication apply worker for subscription "testsub1" has started
复制

直接导致logical replication worker
进程出现报错退出。所以说truncate这里还是需要我们注意一下的。

3.无法复制对象

逻辑复制只能复制普通表上的数据,无法复制物化视图、索引、序列、外部表、UNLOGGED TABLE、INHERIT TABLE 、分区根表,还有大对象。

4.使用了SERIAL列和序列的值需要注意

当我们使用SERIAL列或者插入序列的值的时候,需要注意的一点是,它是直接复制的SQL语句,并没有真正的在订阅侧再次调用序列。我们来实测一下。

--发布侧CREATE TABLE t(a SERIAL, b CHAR);create publication testpub1 FOR table t; --订阅侧CREATE TABLE t(a SERIAL, b CHAR);create subscription testsub1 connection 'host=192.168.56.119 port=5432 dbname=hr user=replication' publication testpub1;
复制

搭建好之后,我们在发布侧执行插入语句。

--发布侧hr=# insert into t(b) values('A');INSERT 0 1hr=# insert into t(b) values('B');INSERT 0 1hr=hr=# select * from t; a | b ---+--- 1 | A 2 | B(2 rows)
复制

然后我们到订阅侧去插入一条语句看看。

hr=# select * from t; a | b ---+--- 1 | A 2 | B(2 rows)hr=# insert into t(b) values('B');INSERT 0 1hr=# select * from t; a | b ---+--- 1 | A 2 | B 1 | B(3 rows)
复制

可以很明显的看到,我们插入一条数据,SERIAL列的值还是1。序列也是一样。所以这就有可能会导致你在备库上执行事务出现较低的序列值,或者是主键冲突错误。比较建议的一个做法是手动在订阅端把这个序列值设置的足够大。

5.触发器的限制

PostgreSQL触发器属性上可以分FOR EACH ROW
FOR EACH STATEMENT
。两者的区别是,FOR EACH ROW
操作修改时每行调用一次,而FOR EACH STATEMENT
,不管修改了多少行,每个语句标记的触发器只执行一次。所以这就在订阅复制的时候,只有FOR EACH ROW
触发器能执行。而FOR EACH STATEMENT
则只会在初始化数据的时候执行。

要让触发器能正常工作,我们还需要设置ALTER TABLE ENABLE ALWAYS
或者REPLICA TRIGGER
语句。

6.双向复制

既然是逻辑复制,一定是可以支持双向复制的。但是很遗憾,它能够配置,但是WAL会出现循环。

我们来测试一下。

--正向 发布CREATE TABLE t(a SERIAL, b CHAR);create publication testpub1 FOR table t; --正向 订阅CREATE TABLE t(a SERIAL, b CHAR);create subscription testsub1 connection 'host=192.168.56.119 port=5432 dbname=hr user=replication' publication testpub1;--反向  发布create publication testpub2 FOR table t; --反正 订阅create subscription testsub2 connection 'host=192.168.56.170 port=5432 dbname=hr user=replication' publication testpub2;
复制

至此我打造了一个双向循环复制,如图所示。

此时我在发布端插入一条数据就会出现环绕现像。此时就会出现死循环。不停的从A复制到B,再从B复制到A,直到把数据库搞死为止。

刚刚才配好不到1分钟,已经循环复制了2685条。

hr=# select count(1) from t; count -------  2685(1 row)hr=# select count(1) from t; count -------  3600(1 row)hr=# select count(1) from t; count -------  3784(1 row)
复制

双向复制需要使用不同的表来实现。使用同样的表会产生WAL循环

7.逻辑复制和流复制组合使用的限制

逻辑复制可以和流复制一起使用,但是也有限制。就是逻辑复制不能在standby实例上创建,因为standby是只读的,不能创建逻辑复制插槽。因此逻辑解码插件也无法在standby实例上执行。

后记

今天的限制及好玩的双向复制就到这里。未完待续....

励志成为PostgreSQL大神

长按关注吧

文章转载自励志成为postgresql大神,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论

关注
暂无图片
获得了26次点赞
暂无图片
内容获得15次评论
暂无图片
获得了95次收藏