2

我有一张包含证书编号的表格。每个证书由一个系列字母和一个序列号组成。证书必须是唯一的并且不允许有间隙,即如果存在 A-1234,那么 A-1233 也必须存在。每个系列都有自己的系列,即A-1234和B-1234可以愉快地共存。

保存信息的表是这样的:

CREATE TABLE "Certificate"
(
    id serial NOT NULL,
    series character(1) NOT NULL,
    serial integer NOT NULL,
    CONSTRAINT "Certificate_pkey" PRIMARY KEY (id ),
    CONSTRAINT "Certificate_series_serial_key" UNIQUE (series , serial )
)
WITH (
    OIDS=FALSE
);

现在的问题是如何在给定特定系列的情况下创建下一个序列号。Postgres 序列似乎不是要走的路,因为如果插入因任何原因失败或者如果需要来自序列的新值的事务被回滚,这些将产生间隙。

我的想法如下:

#! /usr/bin/env python3.2

import postgresql.driver.dbapi20 as pgdb

credentials = #my db credentials

SERIES = 'B'

dbcon = pgdb.connect (**credentials)
cursor = dbcon.cursor ()

cursor.execute ('''insert into "Certificate" ("series", "serial")
    (select %s, coalesce (max ("serial"), 0) + 1
    from "Certificate"
    where "series" = %s) ''', [SERIES] * 2)

input () #just to keep the transaction open some time

cursor.close ()
dbcon.commit ()
dbcon.close ()

不幸的是,这不是线程安全的。如果我在两个 shell(比如 A 和 B)上启动此脚本两次,则会发生以下情况:

  • 我在 shell A 上启动脚本,它保存在事务中input ()
  • 我在 shell B 上启动脚本,它保存在事务中input ()
  • 我在 shell A 上按 enter 并提交事务。
  • 在这个精确的时刻,shell B 抛出一个 (expected) postgresql.exceptions.UniqueError: duplicate key value violates unique constraint "Certificate_series_serial_key"

如何根据给定的系列字母以线程安全的方式将下一个证书插入此表中?

4

2 回答 2

2

只需捕获异常并重试

while True:
    try:
        cursor.execute ('''
            insert into "Certificate" ("series", "serial")
            select %s, coalesce (max ("serial"), 0) + 1
            from "Certificate"
            where "series" = %s
            ''', [SERIES] * 2)
        dbcon.commit ()
        break
    except postgresql.exceptions.UniqueError:
        continue
于 2012-12-18T16:22:53.737 回答
2

您正在尝试创建一个无间隙序列。那里有很多关于它的信息。这是我不久前就该主题写的一个答案和另一个here

您对证书表有正确的想法,尽管我会考虑一个更清楚地表明它是证书序列号表而不是证书的名称。certificate_series_number例如。

在事务中,您想要可靠地生成新 ID 的方法是:

    curs.execute("UPDATE certificate SET serial = serial + 1 WHERE series = %s RETURNING serial")
    newserial = curs.fetchone()[0]

UPDATE锁定该行,series这会导致对该行的其他更新series阻塞,直到该事务提交或回滚。它不会干扰影响其他 series的并发事务。

这基本上相当于做 aSELECT ... FOR UPDATE后跟一个UPDATE没有的常规RETURNING,它更方便和更简单。

您会注意到,如果它不存在,这不会创建一个新系列。老实说,在您的情况下,最好的方法是预先分配您可能使用的所有系列,因为空间非常有限。在更复杂的情况下你可以使用 upsert 逻辑,但它很难让它顺利工作,你必须为事务重试做好准备。见http://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/

请注意,如果事务在单个事务中使用多个系列,它们可能会相互死锁,除非他们非常小心地始终以相同的顺序从系列中获取系列,例如按系列 ID 的字母顺序。

于 2012-12-18T23:19:49.577 回答