根据另一列自动增加一列,避免竞争条件

问题描述 投票:0回答:2

假设我有一个模型文档。

Document
document_id -> int
client_id -> int

我希望文档 ID 根据客户端 ID 自动递增,以避免竞争条件。我对 document_id、client_id 有一个独特的约束。

 client_id   document_id 
 1           1
 1           2
 2           1
 1           3

方法 1:现在,我在 before_create 回调上设置文档 id,但它会导致竞争条件,并在同一客户端同时收到多个文档时设置相同的值。

创建之前:设置标识符 def 设置标识符 self.document_id = client.documents.maximum(:document_id).next 结束

同时创建 2 个文档时,会引发以下错误。

ActiveRecord::RecordNotUnique PG::UniqueViolation:错误:重复的键值违反了唯一约束

方法 2:尝试在 postgresql 上使用数据库触发器。效果很好。但唯一的问题是,更新的值最初并不反映在分配的变量上。我需要对变量进行强制重新加载。

数据库功能:

CREATE OR REPLACE FUNCTION set_document_identifier()
RETURNS TRIGGER AS $$
DECLARE
max_identifier INTEGER;
BEGIN
  SELECT MAX(document_id) INTO max_identifier
  FROM documents
  WHERE client_id = NEW.client_id;
IF max_identifier IS NULL THEN
    max_identifier := 1;
END IF;
NEW.document_id := max_identifier;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

数据库触发器:

CREATE TRIGGER before_insert_trigger_documents
BEFORE INSERT ON documents
FOR EACH ROW
EXECUTE FUNCTION set_document_identifier();

导轨:

doc = Document.create!(client_id: 1)
doc.document_id -> nil
doc.reload.document_id -> 1

rails 是否提供任何其他可能的 OTB 解决方案或可以在触发器上完成的解决方法,以便在事务提交之前设置该值?

ruby-on-rails postgresql triggers race-condition before-save
2个回答
0
投票

显然,您不能为每个

client_id
提供唯一的递增值,并且还不能让多个用户同时生成自己的下一个值。宇宙就是这样运作的。

处理此问题的最简单方法是(正如您所发现的)在数据库中有一个完成此操作的点。

您还应该 (a) 适当锁定以防止并发插入,或者 (b) 预期重复的键错误并处理它们。


0
投票
  1. 如果您所展示的确实是触发器的工作原理,那么您忘记了增量。这会将第一个
    document_id
    设置为
    1
    。下一次插入相同的
    client_id
    时只会发现
    1
    ,这使得它成为
    max()
    ,并且该插入也将使用相同的
    1
    值。你会一直得到
    1
    演示。如果您添加
    +1
    ,它将实现 工作demo2
    SELECT MAX(document_id)+1 INTO max_identifier
    
  2. 即使您解决了这个问题,当您同时为单个
    client_id
    插入多个文档时,它们都会找到并使用相同的
    max()
    ,从而导致两个具有相同 id 的不同文档。

如果您使用生成的、基于种子的序列,就像我提到的类似线程中那样,它也适用于并发插入:demo3

create function seeded_sequence_nextval(seed text, 
                                        owner_table regclass default null,
                                        owner_column text default 'ctid') 
returns int as $f$
declare sequence_name text:=concat_ws('__','seeded_sequence',
                                           owner_table,
                                           owner_column,
                                           seed);
begin execute format('create sequence if not exists %I owned by %s;',
                     sequence_name,
                     case when owner_table is null then 'none'
                          else format('%s.%I',owner_table,owner_column)
                     end);
      return nextval(format('%I',sequence_name));
end $f$ language plpgsql;
CREATE OR REPLACE FUNCTION set_document_identifier()
RETURNS TRIGGER AS $f$
BEGIN
SELECT seeded_sequence_nextval(NEW.client_id::text,'documents','document_id')
  INTO NEW.document_id;
RETURN NEW;
END;
$f$ LANGUAGE plpgsql;

CREATE TRIGGER before_insert_trigger_documents BEFORE INSERT ON documents
FOR EACH ROW EXECUTE FUNCTION set_document_identifier();
© www.soinside.com 2019 - 2024. All rights reserved.