锁定 Postgres 以进行 UPDATE / INSERT 组合

Rob*_*ler 15 postgresql concurrency locking update

我有两张桌子。一个是日志表;另一个本质上包含只能使用一次的优惠券代码。

用户需要能够兑换优惠券,这将在日志表中插入一行并将优惠券标记为已使用(通过将used列更新为true)。

当然,这里存在明显的竞争条件/安全问题。

我过去在 mySQL 的世界里做过类似的事情。在那个世界中,我会全局锁定两个表,在知道这一次只能发生一次的情况下执行逻辑安全,然后在我完成后解锁表。

Postgres 有没有更好的方法来做到这一点?特别是,我担心锁是全局的,但不是必须的——我真的只需要确保没有其他人试图输入那个特定的代码,所以也许一些行级锁会起作用?

Erw*_*ter 21

我以前听说过 MySQL 中类似的并发问题。在 Postgres 中不是这样。

默认READ COMMITTED事务隔离级别中的内置行级锁就足够了。

我建议使用带有数据修改 CTE 的单个语句(MySQL 也没有),因为将值从一个表直接传递到另一个表很方便(如果您需要的话)。如果您不需要coupon表中的任何内容,您也可以使用带有单独UPDATEINSERT语句的事务。

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;
Run Code Online (Sandbox Code Playgroud)

多次交易试图兑换同一张优惠券应该是一件罕见的事情。他们有一个唯一的编号,不是吗?然而,同时尝试多个事务的情况应该少得多。(也许是应用程序错误或有人试图玩弄系统?)

尽管如此,无论如何UPDATE只有一次交易才能成功。An在更新之前UPDATE获取每个目标行上的行级锁。如果并发事务尝试访问UPDATE同一行,它将看到该行上的锁并等待阻塞事务完成(ROLLBACKCOMMIT),然后成为锁队列中的第一个:

  • 如果提交,请重新检查条件。如果它仍然是NOT used,请锁定该行并继续。否则UPDATEnow 找不到符合条件的行并且什么都不做,也不返回任何行,所以INSERT也什么都不做。

  • 如果回滚,请锁定该行并继续。

一个竞争条件没有潜力

一个没有潜在的死锁,除非你把更多的写入同一交易或其他方式锁定不仅仅是多了一个行。

INSERT是无忧无虑。如果由于某些错误coupon_id已经在log表中(并且您在 上有 UNIQUE 或 PK 约束log.coupon_id),则整个事务将在唯一违规后回滚。将指示您的数据库中的非法状态。如果上述语句是写入log表的唯一方法,则永远不会发生这种情况。