我有一个这样的模型
class Thingy(models.Model):
# ...
failures_count = models.IntegerField()
我有需要执行此操作的并发进程(Celery 任务):
- 做一些处理
- 如果处理失败,则相应
Thingy
的failures_counter
递增 - 如果
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 不支持开箱即用,因此它需要通过 raw()
使用原始 SQL。相同的 SQL 语句可以在其他 RDBMS 中工作,但每个引擎都需要针对同步、事务隔离和并发模型进行自己的讨论(例如,带有 MyISAM 的 MySQL 仍会使用锁)。
def report_failure(thingy_id):
with transaction.commit_on_success():
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)
最佳答案
据我所知,Django 的 ORM 不支持开箱即用 - 然而,这并不意味着它不能完成,你只需要深入到 SQL 级别(暴露,在 Django 的ORM 通过 Manager
的 raw
method ) 使其工作。
如果您使用的是 PostgresSQL >= 8.2,那么您可以使用 RETURNING
在没有任何额外锁定的情况下获得 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)
关于python - 增加计数器并在超过阈值时触发操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16335926/