如何使用 Django 和 PostgreSQL 安全且原子地递减计数器?

JK *_*iho 8 django postgresql transaction-isolation

我一直在阅读 PostgreSQL 事务隔离以及它与 Django 的关系transaction.atomic()(例如本文PostgreSQL 文档),但我对这个主题还很不熟悉,而且我不确定我是否理解我所读的内容。

我们有一个 PostgreSQL 支持的 Django 应用程序,其中涉及配额对象。简单来说,就是这样:

class Quota(models.Model):
    obj = models.OneToOneField(AnotherModel)
    count = models.PositiveIntegerField()
Run Code Online (Sandbox Code Playgroud)

该实例控制可以针对该实例执行特定操作的次数objcount被初始化为某个数字,并且只会递减,直到达到零。

任意数量的进程/线程可以同时执行这些操作。基本上,我们需要以原子方式递减(使用 UPDATE)count单个数据库行的 ,而不会出现死锁,并且不会有两个进程/线程,例如从count100 开始,并且都试图将其递减到 99。

我天真的做法是这样的:

with transaction.atomic():
    cursor = connection.cursor()
    cursor.execute('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE')
    Quota.objects.filter(obj=instance).update(count=F('count')-1)
Run Code Online (Sandbox Code Playgroud)

但是,我不确定这是否受此问题的影响,来自链接的文章:

如果在 COMMIT 时数据库无法确定该事务是否可以相对于其他事务的读/写串行执行,那么它将失败并出现 django.db.DatabaseError。即使他们更新了不同的行,这种情况也可能发生。

所有对其执行操作的进程/线程obj都会减少同一行的同一列,所以......也许?我实际上不知道 PostgreSQL“确定事务可以串行执行”涉及什么。

另一种方法可能是:

with transaction.atomic():
    Quota.objects.select_for_update().filter(obj=instance).update(count=F('count')-1)
Run Code Online (Sandbox Code Playgroud)

这似乎执行行级锁定,我的理解是不需要更改隔离级别,但我不知道这是否足以正确处理并发操作。

这些方法中的一种是优选的吗?是否仍然需要进行一些修改来保证原子性和避免死锁?我们python-redis-lock还可以使用类似的方法来阻止 Django 视图级别的并发数据库操作,但这感觉更适合在数据库级别执行。

Lau*_*lbe 3

我无法告诉你在 Django 中做什么,但我可以在 SQL 级别上解释它。

仅修改单行永远不会导致死锁。您所能获得的只是一个活锁,其中一个更新事务必须等待前一个事务提交。这种活锁是无法避免的,它是数据库序列化同一行修改的方式。

导致死锁的唯一方法是多个数据库事务尝试以不同的顺序锁定相同的对象(复数!)。

您可以使用以下一些技巧来避免出现问题:

  • 使数据库事务尽可能短,这样就没有人需要长时间等待锁定。这也降低了死锁的风险。

  • 不要在单个事务中修改超出一致性绝对必要的数据。修改(锁定)的行越多,死锁的风险就越大。

  • 将计数器更新为提交之前的最后一个活动(或尽可能晚),以便锁定行尽可能短。如果您始终将计数器更新为最后一个活动,则永远不会因为该更新而陷入死锁!

  • 如果要确保列永远不会超过某个值,请对列使用检查约束。