PostgreSQL 的乐观锁

Optimistic Locking with PostgreSQL

问题陈述:我正在使用存储库模式通过 pg npm 模块。如果两个用户几乎同时尝试修改同一条记录,则第一个用户提交的更改将被第二个用户提交的更改覆盖。我想阻止提交第二个用户的更改,直到他们用第一个用户的更改更新了他们的本地记录副本。

我知道像 CouchDB 这样的数据库有一个“_rev”属性,它在这种情况下用于检测从陈旧快照更新的尝试。随着我研究的深入,我发现这称为乐观锁定 ()。因此,我将在我的 table 中添加一个 rev 列,并在我的 SQL 更新语句中使用它。

UPDATE tableX
SET field1 = value1,
    field2 = value2,
    ...,
    rev_field = uuid_generate_v4()
WHERE id_field = id_value
  AND rev_field = rev_value

但是,如果 id_value 匹配而 rev_value 不匹配,则不会告诉我的存储库代码该记录已过时,只有 0 行受到查询的影响。

所以我有一个我在 pgAdmin 中编写的脚本,它将检测 update 影响 0 行的情况,然后检查 rev_field.

DO $$
DECLARE
    i_id numeric;
    i_uuid uuid;
    v_count numeric;
    v_rev uuid;
BEGIN
    i_id := 1;
    i_uuid := '20b2e135-42d0-4a49-94c0-5557dd09abd1';

    UPDATE account_r
    SET account_name = 'savings',
        rev = uuid_generate_v4()
    WHERE account_id = i_id
      AND rev = i_uuid;

    GET DIAGNOSTICS v_count = ROW_COUNT;
    
    IF v_count < 1 THEN
        SELECT rev INTO v_rev
        FROM account_r
        WHERE account_id = i_id;
        
        IF v_rev <> i_uuid THEN
            RAISE EXCEPTION 'revision mismatch';
        END IF;
    END IF;
    
    RAISE NOTICE 'rows affected: %', v_count;
END $$;

虽然我很乐意table将此代码改编成存储过程并从 Node 调用它,但我希望有一个解决方案,它不会那么复杂。一方面,将这些函数移至数据库将清理我的 JS 代码,另一方面,这需要编写大量样板文件 SQL,因为必须为 UPDATE 完成和 DELETE 每个 table.

有没有更简单的方法来完成这项工作? (也许 处的代码是更简单的方法?)我是否应该查看 ORM 来帮助减少这里的头痛?

无需维护 rev 值。您可以获得 table 行的 md5 哈希值。

SQL Fiddle Here

create table mytable (
  id int primary key,
  some_text text,
  some_int  int,
  some_date timestamptz
  );

insert into mytable 
  values (1, 'First entry', 0, now() - interval '1 day'),
         (2, 'Second entry', 1, now()),
         (3, 'Third entry', 2, now() + interval '1 day');

select *, md5(mytable::text) from mytable order by id;

fiddle 包括其他查询以证明计算的 md5() 是基于行的值。

使用该散列进行乐观锁定,更新可以采用以下形式:

update mytable
   set some_int = -1
 where id = 1
   and md5(mytable::text) = <md5 hash from select>
returning *

您仍然需要检查没有 return 行,但这可以在节点端抽象掉。

看起来 result.row_count 包含受影响的行数,因此您不需要 returning * 部分。