我有这样的模型
class Thingy(models.Model):
# ...
failures_count = models.IntegerField()
我有需要执行此操作的并发进程(Celery任务):
failures_counter
Thingy
failures_counter
超过某些Thingy
的阈值,请发出警告,但只发出一次警告。我对如何在没有竞争条件的情况下执行此操作有一些想法,例如使用显式锁定(通过select_for_update
):
@transaction.commit_on_success
def report_failure(thingy_id):
current, = (Thingy.objects
.select_for_update()
.filter(id=thingy_id)
.values_list('failures_count'))[0]
if current == THRESHOLD:
issue_warning_for(thingy_id)
Thingy.objects.filter(id=thingy_id).update(
failures_count=F('failures_count') + 1
)
或者通过使用Redis(它已经存在)进行同步:
@transaction.commit_on_success
def report_failure(thingy_id):
Thingy.objects.filter(id=thingy_id).update(
failures_count=F('failures_count') + 1
)
value = Thingy.objects.get(id=thingy_id).only('failures_count').failures_count
if value >= THRESHOLD:
if redis.incr('issued_warning_%s' % thingy_id) == 1:
issue_warning_for(thingy_id)
两种解决方案都使用锁。因为我正在使用PostgreSQL,有没有办法实现这个而不锁定?
我正在编辑问题以包含答案(感谢Sean Vieira,请参阅下面的答案)。问题是关于避免锁定的方法,这个答案是最佳的,因为它利用了multi-version concurrency control (MVCC) as implemented by PostgreSQL。
这个特定问题明确允许使用PostgreSQL功能,虽然许多RDBMS实现UPDATE ... RETURNING
,但它不是标准SQL,Django的ORM不支持开箱即用,因此需要通过{{1}使用原始SQL }}。相同的SQL语句可以在其他RDBMS中使用,但每个引擎都需要自己讨论同步,事务隔离和并发模型(例如,带MyISAM的MySQL仍然会使用锁)。
raw()
答案 0 :(得分:5)
据我所知,Django的ORM不支持开箱即用 - 但是,这并不意味着它无法完成,你只需要深入到SQL级别(暴露在Django的ORM通过Manager
的{{3}})来使其发挥作用。
如果您使用的是PostgresSQL> = 8.2,那么您可以使用raw
method获取failure_count
的最终值而无需任何额外的锁定(数据库仍将锁定,但只能设置为价值,没有额外的时间丢失与你沟通):
# ASSUMPTIONS: All IDs are valid and IDs are unique
# More defenses are necessary if either of these assumptions
# are not true.
failure_count = Thingy.objects.raw("""
UPDATE Thingy
SET failure_count = failure_count + 1
WHERE id = %s
RETURNING failure_count;
""", [thingy_id])[0].failure_count
if failure_count == THRESHOLD:
issue_warning_for(thingy_id)
答案 1 :(得分:0)
我真的不知道你必须在没有锁定的情况下完成这项工作的原因,你有多少任务同时运行?
但是,我认为有一种方法可以做到这一点而不会像这样锁定:
你应该有另一个模型,例如失败:
class Failure(models.Model):
thingy = models.ForeignKey(Thingy)
你的* report_failure *应该是这样的:
from django.db import transaction
@transaction.commit_manually
def flush_transaction():
transaction.commit()
@transaction.commit_on_success
def report_failure(thingy_id):
thingy = Thingy.objects.get(id=thingy_id)
#uncomment following line if you found that the query is cached (not get updated result)
#flush_transaction()
current = thingy.failure_set.count()
if current >= THRESHOLD:
issue_warning_for(thingy_id)
Failure.objects.create(thingy=thingy)
我知道这种方法非常糟糕,因为它会创建大量的失败记录。但这是我能想到的唯一想法。对不起。