增加计数器并在超过阈值时触发操作

Dav*_* R. 3 python django postgresql concurrency

我有这样的模特

class Thingy(models.Model):
    # ...
    failures_count = models.IntegerField()
Run Code Online (Sandbox Code Playgroud)

我有需要执行此操作的并发进程(Celery任务):

  1. 做某种处理
  2. 如果处理失败failures_counter则相应的增量Thingy
  3. 如果failures_counter超过某些阈值Thingy,发出警告,但只有一个警告.

我有一些关于如何在没有竞争条件的情况下执行此操作的想法,例如使用显式锁(via 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
    )
Run Code Online (Sandbox Code Playgroud)

或者通过使用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)
Run Code Online (Sandbox Code Playgroud)

两种解决方案都使用锁 因为我正在使用PostgreSQL,有没有办法在没有锁定的情况下实现这一点?


我正在编辑问题以包含答案(感谢Sean Vieira,见下面的答案).问题是关于避免锁定的方法,这个答案是最佳的,因为它利用了PostgreSQL实现的多版本并发控制(MVCC).

这个特定问题明确允许使用PostgreSQL功能,虽然许多RDBMS实现UPDATE ... RETURNING,但它不是标准SQL,Django的ORM不支持开箱即用,所以它需要使用原始SQL raw().相同的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)
Run Code Online (Sandbox Code Playgroud)

Sea*_*ira 5

据我所知,Django的ORM不支持开箱即用 - 但是,这并不意味着它无法完成,你只需要深入到SQL级别(暴露在Django的ORM中)Managerraw方法)使它工作.

如果您正在使用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)
Run Code Online (Sandbox Code Playgroud)