Postgres 9.3:简单 INSERT 的共享锁问题

Postgres 9.3: Sharelock issue with simple INSERT

更新:以下可能的解决方案

我有一个大型的配置文件语料库,其中包含 key/value 对,我正试图将它们推送到数据库中。许多键和值在配置文件中重复出现,因此我使用 3 个表存储数据。一个用于所有唯一键值,一个用于所有唯一对值,一个列出每个文件的所有 key/value 对。

问题: 我正在使用多个并发进程(以及连接)将原始数据添加到数据库中。不幸的是,在尝试向键和值表中添加值时,我检测到了很多死锁。我尝试了几种不同的插入数据的方法(如下所示),但总是以 "deadlock detected" 错误

结束

TransactionRollbackError: deadlock detected
DETAIL: Process 26755 waits for ShareLock on transaction 689456; blocked by process 26754. Process 26754 waits for ShareLock on transaction 689467; blocked by process 26755.

我想知道是否有人可以阐明导致这些僵局的确切原因,并可能为我指明解决问题的方法。查看我正在使用的 SQL 语句(在下面列出),我真的不明白为什么存在任何相互依赖性。

感谢阅读!

示例配置文件:

example_key this_is_the_value
other_example other_value
third example yet_another_value

Table定义:

    CREATE TABLE keys (
        id SERIAL PRIMARY KEY,
        hash UUID UNIQUE NOT NULL,
        key TEXT);

    CREATE TABLE values (
        id SERIAL PRIMARY KEY,
        hash UUID UNIQUE NOT NULL,
        key TEXT);

    CREATE TABLE keyvalue_pairs (
        id SERIAL PRIMARY KEY,
        file_id INTEGER REFERENCES filenames,
        key_id INTEGER REFERENCES keys,
        value_id INTEGER REFERENCES values);

SQL 语句:

最初我试图使用这个语句来避免任何异常:

    WITH s AS (
        SELECT id, hash, key FROM keys
            WHERE hash = 'hash_value';
    ), i AS (
        INSERT INTO keys (hash, key)
        SELECT 'hash_value', 'key_value'
        WHERE NOT EXISTS (SELECT 1 FROM s)
        returning id, hash, key
    )
    SELECT id, hash, key FROM i
    UNION ALL
    SELECT id, hash, key FROM s;

但即使像这样简单的事情也会导致死锁:

    INSERT INTO keys (hash, key)
        VALUES ('hash_value', 'key_value')
        RETURNING id;

带有保存点的 python 代码(使用 psycopg2)的完整示例:

key_value = 'this_key'
hash_val = generate_uuid(value)
try:
    cursor.execute(
        '''
        SAVEPOINT duplicate_hash_savepoint;
        INSERT INTO keys (hash, key)
            VALUES (%s, %s)
            RETURNING id;
        '''
        (hash_val, key_value)
    )

    result = cursor.fetchone()[0]
    cursor.execute('''RELEASE SAVEPOINT duplicate_hash_savepoint''')
    return result
except psycopg2.IntegrityError as e:
    cursor.execute(
        '''
        ROLLBACK TO SAVEPOINT duplicate_hash_savepoint;
        '''
    )

    #TODO: Should ensure that values match and this isn't just
    #a hash collision

    cursor.execute(
        '''
        SELECT id FROM keys WHERE hash=%s LIMIT 1;
        '''
        (hash_val,)
    )
    return cursor.fetchone()[0]

更新: 所以我相信我在 another stackexchange site:

上有提示

具体来说:

UPDATE, DELETE, SELECT FOR UPDATE, and SELECT FOR SHARE commands behave the same as SELECT in terms of searching for target rows: they will only find target rows that were committed as of the command start time1. However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). If the first updater rolls back, then its effects are negated and the second updater can proceed with updating the originally found row. If the first updater commits, the second updater will ignore the row if the first updater deleted it2, otherwise it will attempt to apply its operation to the updated version of the row.

虽然我仍然不确定相互依赖性在哪里,但似乎处理大量 key/value 对而不提交可能会导致这样的结果。果然,如果我在添加每个单独的配置文件后提交,就不会发生死锁。

看来你是这种情况:

  1. 要插入的 table 具有主键(或任何类型的唯一索引)。
  2. table 中的多个 INSERT 是在一个事务中执行的(而不是在每个事务之后立即提交)
  3. 要插入的行以随机顺序出现(关于主键)
  4. 这些行是在并发事务中插入的。

这种情况会造成以下死锁机会:

假设有两个会话,每个会话启动一个事务。

  1. 会话 #1:使用主键插入行 'A'
  2. 会话 #2:使用主键插入行 'B'
  3. 会话 #1:尝试使用 PK 插入行 'B' => 会话 #1 等待会话 #2 提交或回滚
  4. 会话 #2:尝试使用 PK 插入行 'A' => 会话 #2 等待会话 #1。

此后不久,死锁检测器发现两个会话现在都在等待对方,并终止其中一个会话并检测到致命的 死锁 错误。

如果您遇到这种情况,最简单的解决方案是在插入新条目后提交,然后再尝试将任何新行插入 table。

老实说,Postgres 以这种类型的死锁而闻名。我经常遇到这样的问题,当不同的工作人员更新有关交错实体的信息时。最近,我有一项任务是从多个 json 文件中导入大量科学论文元数据。我通过 joblib 使用并行进程同时读取多个文件。死锁一直挂在 authors(id bigint primary key, name text) table 上,因为许多文件包含同一作者的论文,因此生成的插入内容通常是同一作者。我正在使用 insert into authors (id,name) values %s on conflict(id) do nothing,但这没有帮助。我尝试在将元组发送到 Postgres 服务器之前对其进行排序,但收效甚微。真正帮助我的是在 Redis 集中保留一个已知作者的列表(所有进程都可以访问):

if not rexecute("sismember", "known_authors", author_id):
   # your logic...
   rexecute("sadd", "known_authors", author_id)

我推荐给大家。如果您限于 Windows,请使用 Memurai。悲伤但真实,Postgres 没有很多其他选择。