【问题标题】:Sending only updated rows to a client仅向客户端发送更新的行
【发布时间】:2014-11-05 08:23:33
【问题描述】:

我想创建一个 Web 服务,允许客户端获取表中的所有行,然后允许客户端仅获取新的或更新的行。

最简单的实现似乎是将当前时间戳发送给客户端,然后让客户端在以下请求中请求比时间戳更新的行。

这似乎是可行的,方法是在更新和插入触发器中保留一个时间戳设置为 NOW() 的“updated_at”列,然后查询较新的行,并传递 NOW() 的值。

问题是如果有未提交的事务,这些事务会将updated_at设置为事务的开始时间,而不是提交时间

因此,这个简单的实现不起作用,因为行可能会丢失,因为它们可能带有过去的时间戳。

尽管这似乎是一个非常普遍的需求,但我一直无法找到任何简单的解决方案:有什么想法吗?

可能的解决方案:

  1. 在表中保留单调时间戳,在每个事务开始时将其更新为 MAX(NOW(), last_timestamp + 1) 并将其用作行时间戳。问题:这实际上意味着所有写入事务都完全序列化并锁定整个数据库,因为它们在更新时间表上发生冲突。

  2. 在事务结束时,添加从 NOW() 到更新表中时间的映射,如上述解决方案。这似乎需要显式锁定并使用序列来生成非临时“时间戳”,因为仅在单行上使用 UPDATE 会导致 SERIALIZABLE 模式下的回滚。

  3. 不知何故,PostgreSQL 在提交时迭代所有更新的行并将 updated_at 设置为单调时间戳

  4. 不知何故,PostgreSQL 自己维护了一个事务提交时间表,目前它似乎没有这样做

使用内置的 xmin 列似乎也是不可能的,因为 VACUUM 会丢弃它。

如果能够在数据库中执行此操作而无需修改应用程序中的所有更新,那就太好了。

通常的做法是什么?

天真的解决方案的问题

如果不明显,这是使用 NOW() 或 CLOCK_TIMESTAMP() 的问题:

  1. 在时间 1,我们在事务中运行 NOW() 或 CLOCK_TIMESTAMP(),它给出 1,我们更新行设置时间 1 作为更新时间
  2. 在时间 2,客户端获取所有行,我们告诉他我们在时间 2 之前提供了所有行
  3. 在时间 3,事务在 updated_at 字段中以“时间 1”提交
  4. 客户端从时间 2(他从上一次完整提取请求中获得的时间)开始请求更新的行,我们查询 updated_at >= 2 并且不返回任何内容,而不是返回刚刚添加的行
  5. 该行已丢失,客户将永远无法看到

【问题讨论】:

  • 那还不是提交时间,所以它不起作用。 IE。事务可能会在调用 clock_timestamp() 后一秒提交,如果客户端在此期间更新,则更新丢失。
  • 出于同样的原因,这也不起作用(除非您在锁定下进行)。
  • 也遇到了这个问题,正在寻找答案。
  • 我最好的想法是让表的更新也插入到队列表中。队列表中的作业被弹出并稍后更新“updated_at”字段。

标签: postgresql time transactions


【解决方案1】:

您的整个提议与 PostgreSQL 等符合 ACID 的 RDBMS 的一些基本原理背道而驰。事务开始时间(例如current_timestamp())和其他基于时间的指标对于衡量特定客户是否收到了什么是没有意义的。放弃整个想法。

假设您的客户端通过持久会话连接到数据库,您可以按照以下步骤操作:

  • 会话开始时,CREATE TEMP UNLOGGED TABLE 为会话用户。此表只包含您要从中获取数据的表的 PK 和上次更新时间。
  • 客户端轮询新数据并仅接收那些PK 尚未在临时表中或现有PK 但更新时间较新的记录。当前未提交的事务是不可见的,但将在下一次轮询时检索新记录或更新记录。需要更新时间,因为无法从所有并发客户端的临时表中删除记录。
  • 检索记录的PK和上次更新时间存储在临时表中。
  • 当用户关闭会话时,临时表被删除。

如果您想在每个客户端的多个会话中保留检索到的记录,或者客户端在每次查询后断开连接,那么您需要一个常规表,但我建议还添加用户的 oid,以便所有用户都可以使用用于跟踪检索到的记录的单个表。在后一种情况下,您可以使用您的数据在表上创建一个AFTER UPDATE 触发器,该触发器从带有提取记录的表中删除 PK,用于一次扫描中的所有用户。在他们的下一次民意调查中,客户将获得更新的记录。

【讨论】:

    【解决方案2】:

    添加一列,用于跟踪已发送给客户端的记录:

    alter table table_under_view
      add column access_order int null;
    
    create sequence table_under_view_access_order_seq
      owned by table_under_view.access_order;
    
    create function table_under_view_reset_access_order()
      returns trigger
      language plpgsql
    as $func$
      new.access_order := null;
    $func$;
    
    create trigger table_under_view_reset_access_order_before_update
      before update on table_under_view
      for each row execute procedure table_under_view_reset_access_order();
    
    create index table_under_view_access_order_idx
      on table_under_view (access_order);
    
    create index table_under_view_access_order_where_null_idx
      on table_under_view (access_order)
      where (access_order is null);
    

    (您也可以使用before insert on table_under_view 触发器,以确保仅将NULL 值插入access_order)。

    您需要在此表上与INSERTs 和UPDATEs 的事务完成后,但在任何客户端查询您的数据之前更新此列。在事务完成之后 你不能做任何事情,所以让我们在查询发生之前做。您可以使用函数 f.ex 来做到这一点:

    create function table_under_access(from_access int)
      returns setof table_under_view
      language sql
    as $func$
      update table_under_view
      set    access_order = nextval('table_under_view_access_order_seq'::regclass)
      where  access_order is null;
    
      select *
      from   table_under_view
      where  access_order > from_access;
    $func$;
    

    现在,您的第一个“块”数据(将获取表中的所有行)如下所示:

    select *
    from   table_under_access(0);
    

    这之后的关键元素是您的客户端需要处理每个“块”数据以确定它最后得到的最大access_order(除非您使用 f.ex. 窗口函数将其包含在结果中,但是如果你要处理结果——这似乎很有可能——你不需要那个)。始终将其用于后续调用。

    如果您愿意,也可以添加 updated_at 列来对结果进行排序。

    您还可以在最后一部分(而不是函数)使用视图 + rule(s),使其更加透明。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-24
      • 2019-09-10
      • 1970-01-01
      相关资源
      最近更新 更多