使用postgres创建顺序证书编号

时间:2012-12-18 15:37:16

标签: sql postgresql concurrency python-3.x

我有一张包含证书编号的表格。每个证书都包含一个系列字母和一个序列号。证书必须是唯一的,不允许有间隙,即如果存在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键并且事务提交。
  • 在这个精确的时刻,贝壳B抛出(预期的)postgresql.exceptions.UniqueError: duplicate key value violates unique constraint "Certificate_series_serial_key"

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

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:2)

您正在尝试创建无间隙序列。那里有很多关于它的信息。 Here's an answer I wrote on the topic a while ago和另一个here

你对Certificate表有正确的想法,虽然我会考虑一个名称,它更清楚它是一个证书系列号码表,而不是证书。例如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的并发事务。

这基本上相当于在没有SELECT ... FOR UPDATE的情况下执行UPDATE后跟常规RETURNING,这样更方便,更简单。

您会注意到,如果新系列尚不存在,则不会创建新系列。老实说,在你的情况下,最好的方法是预先分配你可能使用的所有系列,因为空间是如此受限制。在更复杂的情况下,您可以使用upsert逻辑,但要使其顺利运行相当困难,并且您必须为事务重试做好准备。见http://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/

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