Django模型中的并发控制
我该如何在Django模型中处理并发问题?我不希望同一个记录的修改被另一个用户覆盖,尤其是当他们也在查看这个记录的时候。
4 个回答
我同意Joe Holloway的介绍性解释。
我想分享一个与他回答最后部分相关的工作示例(“在Django中,乐观并发控制可以通过重写模型类的保存方法来实现...”)。
你可以使用下面这个类作为你自己模型的基础。
如果你在一个数据库事务中(比如在外部作用域中使用transaction.atomic),以下的Python语句是安全且一致的。
实际上,通过一次操作,这些语句的过滤和更新就像是在记录上进行了一种测试和设置:它们会验证版本并在数据库层面上对这一行进行隐式锁定。
所以,下面的“保存”操作能够更新记录的字段,确保这是唯一一个在操作该模型实例的会话。
最后的提交(例如,通过在transaction.atomic中自动执行的_exit_)会释放这一行的隐式数据库锁:
class ConcurrentModel(models.Model):
_change = models.IntegerField(default=0)
class Meta:
abstract = True
def save(self, *args, **kwargs):
cls = self.__class__
if self.pk:
rows = cls.objects.filter(
pk=self.pk, _change=self._change).update(
_change=self._change + 1)
if not rows:
raise ConcurrentModificationError(cls.__name__, self.pk)
self._change += 1
super(ConcurrentModel, self).save(*args, **kwargs)
我觉得“保持一个版本号或时间戳”这个方法不太靠谱。
当 self.version == self.read_current_version()
这个条件为 True
时,还是有可能在你调用 super().save()
之前,版本号被其他会话修改了。
简单来说,这个问题其实并不是一个关于Django的问题。
并发控制通常被看作是一个技术性的问题,但从某种程度上来说,它更多的是关于功能需求的问题。你希望或者需要你的应用程序如何工作?在我们弄清楚这一点之前,很难给出任何Django特定的建议。
不过,我想多说几句,所以接着说……
当我遇到需要进行并发控制的情况时,我通常会问自己两个问题:
- 两个用户同时修改同一条记录的可能性有多大?
- 如果用户对记录的修改丢失了,会对他/她造成什么影响?
如果发生冲突的可能性相对较高,或者丢失修改的影响很严重,那么你可能需要考虑某种形式的悲观锁。在悲观锁的方案中,每个用户在修改记录之前必须先获取一个逻辑锁。
悲观锁会带来很多复杂性。你需要同步访问这些锁,考虑容错能力,锁的过期时间,超级用户是否可以覆盖锁,用户是否能看到谁拥有锁,等等。
在Django中,这可以通过创建一个单独的锁模型或者在被锁定的记录上添加一个“锁定用户”的外键来实现。使用锁表可以让你在存储锁的获取时间、用户、备注等方面有更多的灵活性。如果你需要一个通用的锁表,可以用来锁定任何类型的记录,那么可以看看django.contrib.contenttypes框架,但这很快可能会变得过于抽象。
如果冲突的可能性不大,或者丢失的修改可以轻松恢复,那么你可以使用乐观并发技术。这种技术简单且更容易实现。基本上,你只需跟踪一个版本号或修改时间戳,并拒绝任何你检测到的异常修改。
从功能设计的角度来看,你只需考虑如何将这些并发修改错误呈现给用户。
在Django中,乐观并发控制可以通过重写模型类的保存方法来实现……
def save(self, *args, **kwargs):
if self.version != self.read_current_version():
raise ConcurrentModificationError('Ooops!!!!')
super(MyModel, self).save(*args, **kwargs)
当然,无论是哪种并发机制,要确保其稳健性,你都需要考虑事务控制。如果不能保证事务的ACID特性,这两种模型都无法完全发挥作用。