考虑一个rewards
表,其中包含type
列,其中一个列的可能值为ONE_PER_PERSON
。
还有一个redeemed_rewards
链接表,用于跟踪哪些用户兑换了哪些奖励。它有两列:user_id
和reward_id
。
现在考虑一个负责赎回奖励的业务逻辑的高级职能。函数的签名如下所示:
function redeemReward(userId, rewardId)
具体而言,考虑兑换ONE_PER_PERSON
奖励的情况。在高级别,此案例的函数逻辑如下:
查询以确保此用户之前未兑换奖励。也就是说,确保以下查询返回0的计数:
SELECT COUNT(*) FROM redeemed_rewards
WHERE user_id = ${userId} AND reward_id = ${rewardId}
假设没有,请插入已兑换的奖励:
INSERT INTO redeemed_rewards VALUES (${userId}, ${rewardId})
提交交易
这种逻辑的问题在于它很容易受到竞争条件的影响。由于理论上可以通过多个线程调用该函数,因此可以想象2个线程可以绕过步骤2,每个线程在另一个线程到达步骤4之前,导致两个插入的记录,从而违反ONE_PER_PERSON
约束
我认为正确的解决方案是在步骤1中锁定表格,并将其锁定直到第4步。
我的问题是:
独特的指数对我来说不是一个可行的解决方案。并非所有类型的奖励都是每个用户一个。另外,我特别想了解postgres锁定以及如何在需要它的情况下正确使用它。
答案 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直接强制执行逻辑,因此您不必担心使用杂散查询或管理操作搞乱。