跟踪附加到PostgreSQL

时间:2016-10-06 02:35:45

标签: sql postgresql transactions

考虑下表:

create table entries (
  sequence_number integer
    default nextval('entries_sequence_number_seq')
    primary key,
  time timestamp default now()
);

此表用作仅附加的更改流。写入中可能涉及其他表,但作为每个事务中的最后一个SQL语句,我们向该表插入一行。换句话说,我们的事务可能很大且耗时,但最终我们会编写此行并立即提交。

现在我们希望一个或多个消费者可以跟踪更改,因为它们会附加到此表中:

  • 每个消费者需要定期循环以大致按时间顺序获取下一批更改 - 换句话说,自上次消费者轮询以来附加到条目的新行的增量。
  • 消费者总是及时前进,不会倒退。
  • 每位消费者都会获得所有数据。不需要选择性分发。
  • 消费顺序并不重要。但是,消费者最终必须看到所有已提交的条目:如果正在进行的事务向表提交新条目,则必须将其拾取。
  • 我们希望尽量减少两次看同一行的可能性,但如果发生这种情况,我们可以容忍它。

概念:

select * from entries where sequence_number > :high_watermark

...其中high_watermark是消费者看到的最高数字。

但是,由于nextval()是在提交时间之前计算的,因此您可能会遇到尚未提交的正在进行的事务导致间隙的情况。你可能会遇到这样的竞争状况:

  • 假设世界从序号0开始。
  • Writer A txn:Inserts,获取序列号1。
  • Writer B txn:插入,获取序列号2。
  • 作家B txn提交。
  • 最新序列号现为2。
  • 消费者选择> 0,找到序列号为2的条目,将其设置为high_watermark。
  • 作家A txn提交。
  • 消费者选择> 2,因此永远不会看到序号为1的条目。

在一般情况下,竞争条件可能非常小,但它仍然是可能的,并且它发生的概率随着系统的负载而增加。

到目前为止,我想到的最好的,但肯定不是优雅的解决方案是始终选择时间:

select * from entries
where sequence_number > :newest_sequence_number
or time >= :newest_timestamp

理论上这应该是模数闰秒和漂移时钟 - 保证看到较旧的条目,代价是获取最后一批中出现的行。消费者应该想要保留一组可以忽略的已经看到过的条目。闰秒和漂移时钟可以通过用一些不科学的秒数填充时间戳来解释。缺点是它将不断读取一堆冗余行。它只是感觉有点笨重和手工波浪。

稍微更直观但更确定的方法是维护未记录的待处理事件表,并在我们从中读取时始终从中删除。这有两个缺点:一个是表现,显然。另一个是,由于可能有任何数量的消费者,我们必须为每个消费者生产一个事件,这反过来意味着我们必须在事件发射时通过某种独特的ID来识别消费者,当然还有垃圾 - 当消费者不再存在时收集未使用的事件。

令我感到震惊的是,比未记录的表更好的方法是使用LISTEN / NOTIFY,并将条目的ID作为有效负载。这有利于首先避免轮询,尽管这不是一个巨大的胜利,因为此应用程序中的消费者的对象只是偶尔醒来并且减少工作在系统上。另一方面,我能看到的唯一主要缺点是,可以在飞行中的消息数量存在限制(尽管很大),如果通知无法发生,交易将开始失败。然而,这可能是一个合理的妥协。

与此同时,我脑子里的一些东西告诉我,必须有一种数学上更优雅的方式来做更少的工作。

1 个答案:

答案 0 :(得分:0)

WHERE time >= :newest_timestamp改进后的想法受到相同竞争条件的影响,因为无法保证时间戳处于提交顺序。进程偶尔会进入睡眠状态。

为每个初始化为boolean的使用者添加consumed_n字段FALSE。消费者 n 然后使用:

    UPDATE entries
       SET consumed_n = TRUE
       WHERE NOT consumed_n
       RETURNING sequence_number, time;

部分索引ON entries(1) WHERE NOT consumed_n会有所帮助。

如果根据您的口味占用太多存储空间,请为每位消费者使用一个bit(n)字段。

只要发出这些声明的交易仍然开放,消费者就会互相锁定。因此,请保持良好的并发性。