Postgres“如果不存在”失败,因为序列存在

Sha*_*dow 5 postgresql ddl sequences postgresql-9.5

我正在构建的应用程序中有多个计数器,正试图使它们根据需要由应用程序动态创建。

举一个简单的例子,如果有人在脚本中输入单词,它应该返回该单词先前被输入的次数。如果输入了example单词,则可以执行此sql的示例。

CREATE SEQUENCE IF NOT EXISTS example START WITH 1;
SELECT nextval('example')
Run Code Online (Sandbox Code Playgroud)

这将1在第一次运行时返回,2第二次运行,等等。

问题是两个人同时单击按钮时。首先,请注意,我的应用程序中发生的不仅仅是这些语句,因此它们重叠的可能性比发生的所有事情要重要得多。

1> BEGIN;
2> BEGIN;
1> CREATE SEQUENCE IF NOT EXISTS example START WITH 1;
2> CREATE SEQUENCE IF NOT EXISTS example START WITH 1; -- is blocked by previous statement
1> SELECT nextval('example')  -- returns 1 to user.
1> COMMIT;  -- unblocks second connection
2> ERROR:  duplicate key value violates unique constraint 
   "pg_type_typname_nsp_index"
   DETAIL:  Key (typname, typnamespace)=(example, 109649) already exists. 
Run Code Online (Sandbox Code Playgroud)

我的印象是,通过使用“ IF NOT EXISTS”,如果该语句确实存在,则该语句应为无操作,但似乎具有这种竞争条件,而事实并非如此。我说竞态条件是因为如果这两个条件不能同时执行,它将像预期的那样工作。

我注意到这IF NOT EXISTS对postgres来说还很新,所以也许他们还没有解决所有问题?

编辑:我们正在考虑以这种方式做事的主要原因是避免过多的锁定。这样的想法是,如果两个人要同时增加,则使用一个序列将意味着两个用户都不必等待另一个(除非在此示例中,用于该序列的初始创建)

red*_*neb 5

序列是数据库模式的一部分。如果您发现自己根据数据库中存储的数据动态修改架构,那么您可能做错了。对于具有特殊属性的序列尤其如此,例如关于它们与事务相关的行为。具体来说,如果你增加一个序列(在nextval) 在事务中间,然后您回滚该事务,序列的值将不会回滚。所以很可能,这种行为是您不希望数据出现的。在您的示例中,假设用户尝试添加单词。这导致相应的序列递增。现在想象一下事务由于某种原因没有完成(例如,可能计算机崩溃了)并且它被回滚了。您最终将不会将单词添加到数据库中,而是将序列递增。

对于您提到的特定示例,有一个简单的解决方案;创建一个普通表来存储所有“序列”。像这样的事情会做到:

CREATE TABLE word_frequency (
    word text NOT NULL UNIQUE,
    frequency integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

现在我明白这只是一个例子,但如果这种方法不适用于您的实际用例,请告诉我们,我们可以根据您的需要进行调整。

编辑:以下是上述解决方案的工作原理。如果添加了新单词,请运行以下查询(仅 postgres 9.5+ 中的“UPSERT”语法):

INSERT INTO word_frequency(word,frequency)
VALUES ('foo',1)
ON CONFLICT (word)
DO UPDATE
SET frequency = word_frequency.frequency + excluded.frequency
RETURNING frequency;
Run Code Online (Sandbox Code Playgroud)

这个查询将插入一个word_frequency频率为 1的新词,或者如果这个词已经存在,它会将现有频率增加 1。现在如果两个事务同时尝试这样做会发生什么?考虑以下场景:

client 1          client 2
--------          --------
BEGIN
                  BEGIN
UPSERT ('foo',1)
                  UPSERT ('foo',1) <====
COMMIT
                  COMMIT
Run Code Online (Sandbox Code Playgroud)

将会发生的情况是,只要客户端 2 尝试增加 foo 的频率(用上面的箭头标记),该操作就会阻塞,因为该行已被不同的事务修改。当客户端 1 提交时,客户端 2 将被解除阻塞并继续而不会出现任何错误。这正是我们希望它工作的方式。另请注意,postgresql 将使用行级锁定来实现此行为,因此不会阻止其他插入。