发布和订阅的实现原理是逻辑复制,可以有一个或者多个订阅者订阅一个发布者节点上的一个或者多个发布,这些订阅者从它们所订阅的发布上拉取数据。
当发布者上发生的更改会被实时推送给订阅者,订阅者收到发布者的更改后会以发布者相同是顺序应用那些数据,只有这样才能保证在订阅中与发布的事务保持一致。以这种方式实现的数据复制方法我们也称之为事务性复制。
那么发布订阅该如何使用呢,通常我们有如下的一些典型用法:
- 在数据库中发生了更改的时候,我们把增量的改变推送给订阅者。
- 触发器会在更改数据到达订阅者的时候被触发。
- 我们可以将多个数据库数据联合到一个单一的数据库中,在进行数据分析的时候可以这么干。
订阅者的数据库也可以被用来作为其他数据库的发布者,我们只需要定义它自己的发布就可以了,它与其他任何的openGauss实例是相同的。
如果订阅者只是被当做只读,不被更改的时候,单一的订阅是不会有冲突的,但是如果有应用或者对相同表集合的其他订阅者做了其他的写操作,这种情况就很有可能会发生冲突。
发布
首先我们需要先弄明白发布该如何理解?我们可以在任意的物理复制到主服务器上定义发布。那么什么又是发布者呢?发布者其实就是一个节点,这个节点定义了发布。发布可以从另一个角度来理解,发布面向的对象是表的更改,所以一个表或者说一组表的更改的集合就是发布,还可以更加简单的理解为就是更改集合或者是复制集合,发布只能在一个节点上,所以我们定义的每一个发布只能存在于一个数据库中,一个发布不可能同时存在于多个数据库中,这是不可能的。
openGauss数据库当前发布中只能包含表,每一张表都可以被加入到多个发布中。在创建发布的时候表对象必须明确地加入到发布,当然你也可以在创建发布的时候使用ALL TABLES
,这样当前数据库中所有的表都会被加入到发布中去。
这里要注意,每张表必须要设置主键,这是一个“复制标识”,这样在复制UPDATE
和DELETE
操作的时候,订阅端才能标识出更新或者删除合适的行。openGauss默认这个复制标识就是主键,当前你也可以在这个复制标识设置成另一个唯一索引 。挡在某种特别情况下没有合适的键来作为复制标识的时候,可以设置为full
,这样就标识整个数据行都将成为那个复制标识的键,当前这可以实现,但是通常我们是不会这样做的,为什么呢?很简单,这样做效率实现太低了,只有在没有其他方案的时候才可能被迫使用,故这也是慎用的。
这里还有另一个问题:如果在加入到发布的表中没有复制标识,在复制UPDATE
和DELETE
的时候会如何呢?如果出现这样的情况订阅者后续在对UPDATE
和DELETE
进行复制的时候将会导致报错。但是有一个操作不管有没有复制标识,对它都没有任何的影响,,是哪一个操作呢?显而易见,这个操作就是INSERT
。
这里讲到了发布,那么发布和订阅是什么关系呢?
这个其实和JAVA语言中发布订阅模式是一样的,一个发布可以有多个订阅者。
发布该如何创建呢?
在openGauss中我们使用命令CREATE PUBLICATION
进行创建,而且后面也可以使用相应的命令对发布进行更新和删除。
对于数据表我们可以使用命令ALTER PUBLICATION
动态地增加或者移出,ADD TABLE
以及DROP TABLE
操作都是事务性的,因此一旦该事务提交了,这张表就会以正确的快照开始或者停止复制工作。
订阅
订阅处理逻辑复制下游端,一个定义订阅端节点我们称之为订阅者,一个订阅者会定义到另一个数据库的链接以及想要订阅的发布集合,也就是可以订阅一个也可以是多个。
订阅者数据库上的一切行为都是和其他的openGauss实例是相同的,且这个订阅者还可以用来作为其他数据库的发布者,怎么实现呢?哈哈哈,很简单,说了点废话,只需要在这个订阅者节点上定义它自己的发布就可以了,使用方式和常规的订阅发布完全一样的。
事实上一个订阅者节点上可以有多个订阅,也就是在一对发布者和订阅者之间可以定义多个订阅,但这种情况下必须要确保发布对象不会有重叠,否则会产生大量的冲突大致报错终止逻辑复制操作。
这里有一个用户权限问题,如果当前的用户须有SYSADMIN
权限,那么订阅会被gs_dump
转存,如果不具有这个权限,那么这个用户就不能种pg_subscription
目录中读取所有的订阅信息到,这时候订阅就会被跳过并且写出一个警告。
到这里讲多这么多订阅,我们最关心的是这个订阅该如何创建呢?
在用openGauss中可以使用命令CREATE SUBSCRIPTION
创建订阅。
当需要修改订阅的时候可以使用命令ALTER SUBSCRIPTION
在任何时刻对订阅进行修改。当不再需要这个订阅的时候,我们还可以使用命令DROP SUBSCRIPTION
删除订阅。
这里我们思考一个问题:假设你误删除了一个订阅,然后重建了这个订阅会发生什么呢?
这个问题也很简单,虽然订阅名是一样的,但是实际上是两个完全不一样的订阅,这时候同步到信息肯定会丢失,是什么意思呢?就是说数据必须要重新同步。这是openGauss的处理方式,那么问题来了,如果是你自己来实现这个功能,你有没有更好的方式呢?欢迎在评论区天马行空哟!
复制不是所有的对象都会同步到,比如模式定义就不会被复制,当前只是针对表对象,几遍如此也是有一定限制的,比如被发布的表必须要在订阅者端存在,否则无法复制,并且只有常规的表才能成为复制的目标,比如像视图便不能复制。
发布者和订阅者之间的表名必须要是完全限定进行匹配,不支持复制到订阅者上表名都不同的表。
对于表中的列也是通过列名进行匹配的,发布表和订阅表中列的顺序可以是不一样的,列的数列也可以是不一样的,但是必须要相互兼容,比如发布端电话号码是用的bigint
类型,订阅端表中电话号码可以使用varchar
数据类型。
订阅端的表还可以具有发布端表不具有列,这些额外的列将使用订阅端表定义中指定的默认值来进行填充。
冲突处理
逻辑复制的行为其实和正常的DML操作是一样的,就算是订阅者修改了数据本地数据,逻辑复制也会根据收到的更改数据来更新数据。
假设订阅收到的数据违背了任何的约束,逻辑复制都将停止。这样的情况就被称为一个冲突。在UPDATE
和DELETE
复制时,缺失的数据是不会产生的冲突的,这类操作也将会被简单的跳过。
我们可以通过subscription_conflict_resolution
参数来控制订阅端在主键或者唯一键发生冲突时的处理方式,我们可以选择是直接报错,还是使用发布端的或者是保留本地的。当设置成应用发布端的时候。如果是insert操作发生冲突,这时候会将冲突的数据更新为发布端同步过来的新元组数据;如果是update操作发生冲突,openGauss会现将冲突的数据删除,然后执行更新操作。如果是设置成保留本地,冲突发生的时候会直接忽略。
当设置为应用远端时,当同步过来的新元组存在多行因为不同索引发生冲突时,尝试应用该元组时仍然会报错。
此外,在订阅者的服务器日志中可以找到有关冲突的详细情况,如下。
ERROR: CONFLICT: remote insert on relation t1 (local index t1_idx). Resolution: apply_remote.
DETAIL: local tuple: a[integer]:2 b[integer]:10, remote tuple: a[integer]:3 b[integer]:10, origin: pg_57351, commit_lsn: 0/143A06A0
通过更改订阅者上的数据(这样它就不会与到来的数据发生冲突)或者跳过与已有数据冲突的事务可以解决这种冲突。冲突事务的复制源名称和LSN可以从服务器日志中找到(在上面的例子中,复制源名称是pg_57351,LSN是0/143A06A0)。使用ALTER SUBSCRIPTION SET (SKIPLSN = ‘0/143A06A0’)可以跳过产生冲突的事务。也可以通过调用pg_replication_origin_advance()函数跳过该事务,函数的参数是对应于该订阅名称的node_name(即pg_57351)以及commit_lsn的下一个LSN(即0/143A06A1)。复制源头的当前位置可以在pg_replication_origin_status系统视图中看到。请注意,跳过整个事务包括跳过可能不违反任何约束的更改,使得发布端和订阅端数据不一致。
本文到此结束,要怎么使用,接下来我会在第二篇中介绍。