考虑下表:
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()
是在提交时间之前计算的,因此您可能会遇到尚未提交的正在进行的事务导致间隙的情况。你可能会遇到这样的竞争状况:
在一般情况下,竞争条件可能非常小,但它仍然是可能的,并且它发生的概率随着系统的负载而增加。
到目前为止,我想到的最好的,但肯定不是优雅的解决方案是始终选择时间:
select * from entries
where sequence_number > :newest_sequence_number
or time >= :newest_timestamp
理论上这应该是模数闰秒和漂移时钟 - 保证看到较旧的条目,代价是获取最后一批中出现的行。消费者应该想要保留一组可以忽略的已经看到过的条目。闰秒和漂移时钟可以通过用一些不科学的秒数填充时间戳来解释。缺点是它将不断读取一堆冗余行。它只是感觉有点笨重和手工波浪。
稍微更直观但更确定的方法是维护未记录的待处理事件表,并在我们从中读取时始终从中删除。这有两个缺点:一个是表现,显然。另一个是,由于可能有任何数量的消费者,我们必须为每个消费者生产一个事件,这反过来意味着我们必须在事件发射时通过某种独特的ID来识别消费者,当然还有垃圾 - 当消费者不再存在时收集未使用的事件。
令我感到震惊的是,比未记录的表更好的方法是使用LISTEN
/ NOTIFY
,并将条目的ID作为有效负载。这有利于首先避免轮询,尽管这不是一个巨大的胜利,因为此应用程序中的消费者的对象只是偶尔醒来并且减少工作在系统上。另一方面,我能看到的唯一主要缺点是,可以在飞行中的消息数量存在限制(尽管很大),如果通知无法发生,交易将开始失败。然而,这可能是一个合理的妥协。
与此同时,我脑子里的一些东西告诉我,必须有一种数学上更优雅的方式来做更少的工作。
答案 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)
字段。
只要发出这些声明的交易仍然开放,消费者就会互相锁定。因此,请保持良好的并发性。