纠正postgres锁定以防止重复插入

Jon*_*nah 8 postgresql locking knex.js

问题解释

考虑一个rewards表,它有一type列,其中一个可能的值是ONE_PER_PERSON.

还有一个redeemed_rewards链接表,用于跟踪哪些用户兑换了哪些奖励.它有两列:user_idreward_id.

现在考虑一个负责赎回奖励的业务逻辑的高级职能.该函数的签名如下所示:

function redeemReward(userId, rewardId)
Run Code Online (Sandbox Code Playgroud)

具体而言,考虑ONE_PER_PERSON奖励被兑换的情况.在高级别,该案例的函数逻辑如下:

  1. 开始交易
  2. 查询以确保此用户之前未兑换奖励.也就是说,确保以下查询返回0的计数:

    SELECT COUNT(*) FROM redeemed_rewards 
    WHERE user_id = ${userId} AND reward_id = ${rewardId}
    
    Run Code Online (Sandbox Code Playgroud)
  3. 假设没有,请插入兑换的奖励:

    INSERT INTO redeemed_rewards VALUES (${userId}, ${rewardId})
    
    Run Code Online (Sandbox Code Playgroud)
  4. 提交交易

这种逻辑的问题在于它很容易受到竞争条件的影响.由于理论上可以通过多个线程调用该函数,因此可以想到2个线程都可以绕过步骤2,每个线程在另一个线程到达步骤4之前,导致两个插入的记录,从而违反ONE_PER_PERSON约束.

我认为正确的解决方案是在步骤1中锁定表格,并将其锁定直到第4步.

我的问题是:

  1. 对于这种情况,适当的postgres锁是什么?理想情况下,我可以锁定仅包含指定userId的插入,以便其他用户不受影响.但是,我不相信这是可能的.那么我应该使用哪种类型的表锁?
  2. 额外问题:如何在knex查询中实现此锁定?文档似乎没有提到关于锁的任何事情......

请注意

对我来说,独特的指数不是一个可行的解决方案.并非所有类型的奖励都是每个用户一个.另外,我特别想了解postgres锁定以及如何在需要它的情况下正确使用它.

Chr*_*ers 7

这提出了一个问题,因为没有某种显式锁定,重复插入有点难以防止.

第一种选择是使用奖励ID或用户作为信号量,以有效地序列化您对特定用户或奖励ID的所有写入.通过这种方式,你可以这样做:

SELECT * FROM reward WHERE id = ? FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

然后,每次兑换相同奖励的尝试都会等待其他尝试先通过.您可以根据您的流量方法对用户执行相同的操作.然后,当事务完成时,锁被释放.

这通过锁定交易的奖励行来工作.这里的主要优点是它很简单.主要缺点是只有您的应用程序才能以这种方式序列化读取.

通过这种方式,许多人可以同时兑换不同的奖励ID,但是对于每个奖励ID,它将锁定奖励表中的行并等待阻止其他人执行相同的操作,直到它提交为止.

第二种方法是使用咨询锁.这里有一些优点,但也有一些缺点,所以如果行锁不能满足你的需要,我会看一下(并花一些时间阅读文档).

编辑:

实际上可能有一个唯一的索引,但要做到这一点,你必须重新考虑一下你的数据库.正如您所提到的,(user_id, reward_id)组合对于某些奖励类型是唯一的.

因此,您需要做的是在奖励表上创建一个唯一索引reward_id, reward_type,然后在redeemed_rewards 表中将reward_type添加到您的外键中.

CREATE UNIQUE INDEX redeemed_rewards_only_one_per_user 
    ON redeemed_rewards (user_id, reward_id) 
 WHERE reward_type = 'ONE_PER_USER`
Run Code Online (Sandbox Code Playgroud)

当只有这些类型的奖励被赎回多次时,PostgreSQL会抛出一个错误.这样做的好处是可以使用db直接强制执行逻辑,因此您不必担心使用杂散查询或管理操作搞乱.

  • 咨询锁的一个大问题是命名空间的问题。因此,如果你走这条路,你就有效地排除了使用咨询锁来处理除此之外的事情。这可能很容易,但战略上并不便宜。 (2认同)
  • 当行不存在时,“ SELECT ... FOR UPDATE”如何工作?根据我对[PostgreSQL docs](https://www.postgresql.org/docs/9.6/explicit-locking.html#LOCKING-ROWS)的阅读,这只能锁定`DELETE`,`UPDATE`等。未列出。 (2认同)