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

时间:2016-06-16 21:52:19

标签: postgresql locking knex.js

问题解释

考虑一个rewards表,其中包含type列,其中一个列的可能值为ONE_PER_PERSON

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

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

function redeemReward(userId, rewardId)

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

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

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

    INSERT INTO redeemed_rewards VALUES (${userId}, ${rewardId})
    
  4. 提交交易

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

    问题

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

    我的问题是:

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

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

1 个答案:

答案 0 :(得分:7)

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

第一个选项是使用奖励ID或用户作为信号量,以有效地序列化您对特定用户或奖励ID的所有写入。通过这种方式,您可以执行以下操作:

SELECT * FROM reward WHERE id = ? FOR UPDATE;

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

这可以通过锁定交易奖励中的行来实现。这里的主要优点是它很简单。主要的缺点是只有你的应用程序才能以这种方式序列化读取。

通过这种方式,许多人可以同时兑换不同的奖励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`

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