浅尝Django(旧版本)的并发问题及其基本解决办法
为单用户服务的桌面系统的日子已经过去了 - 网络应用程序现在正在为数百万用户提供服务,许多用户出现了广泛的新问题 - 并发问题。
在本文中,我将介绍在Django模型中管理并发性的两种方法 问题 为了演示常见的并发问题,我们将使用银行账户模型:
class Account(models.Model):
id = models.AutoField( #用户id
primary_key=True,
)
user = models.ForeignKey( # 用户
User,
)
balance = models.IntegerField( #存款金额
default=0,
)
开始我们为帐户实例提供一个简单的存款和撤销方法:
# 存款
def deposit(self, amount):
self.balance += amount
self.save()
# 取钱
def withdraw(self, amount):
if amount > self.balance:
raise errors.InsufficientFunds()
self.balance -= amount
self.save()
这似乎是足够简单的,甚至可能通过本地主机的单元测试和集成测试。 但是,当两个用户同时在同一个帐户上执行操作时会发生什么?
1、用户A提取帐户30$ - 初始余额为100 $。
2、用户B存款帐户30$ - 初始余额为100 $。
3、用户B提取后 - 余额更新为100 $ - 30 $ = 70 $。
4、用户A存款后 - 余额更新为100 $ + 50 $ = 150 $。
这里发生了什么?
用户B要求提取 30,用户A存入 50, 我们预期余额为120,但最终为150 。
为什么会这样呢?
在步骤4,当用户A更新余额时,他在存储器中存储的金额已经过时(用户B已经退出30 $)。 为了防止这种情况发生,我们需要确保我们正在处理的资源在我们正在计算的过程中不会改变。
悲观的做法
悲观的做法表明,您应该完全锁定资源,直到完成它 。 如果没有人可以在您处理对象时获取对象上的锁定,那么可以确保对象没有被更改。 我们使用数据库锁有几个原因:
1、 数据库非常擅长管理锁并保持一致性。
2、数据库是访问数据的最低级别 - 获取最低级别的锁也会防止其他进程尝试修改数据。 例如,DB中的直接更新,cron作业,清理任务等。
3、Django应用程序可以在多个进程 (例如工作者)上运行。 在应用程序级别维护锁将需要大量(不必要的)工作。
要在Django中锁定一个对象,我们使用select_for_update
。 让我们用悲观的方法来实行安全的存款和取款:
@classmethod
def deposit(cls, id, amount):
with transaction.atomic():
account = (
cls.objects
.select_for_update()
.get(id=id)
)
account.balance += amount
account.save()
return account
@classmethod
def withdraw(cls, id, amount):
with transaction.atomic():
account = (
cls.objects
.select_for_update()
.get(id=id)
)
if account.balance < amount:
raise errors.InsufficentFunds()
account.balance -= amount
account.save()
return account
按以下步骤:
1、我们在我们的查询器上使用select_for_update
来告诉数据库锁定对象,直到事务完成。
2、在数据库中锁定一行需要一个数据库事务 - 我们使用Django的装饰器transaction.atomic()
来定义事务。
3、我们使用类方法而不是实例方法 - 我们告诉数据库要上锁,然后它会返回锁的对象给我们。 为了实现这一点,我们需要从数据库中获取对象。 如果我们使用self,那么就是在操作一个已经从数据库中获取出来的对象,这个对象无法保证自己是没有被上锁的。
4、帐户中的所有操作都在数据库事务中执行。
让我们看看如何通过我们的新方法来阻止前面说的情况:
1、用户A要求取出30 $:
- 用户A获取帐户上的锁。
- 余额为100美元。
2、用户B要求存入50 $:
- 尝试获取锁定帐户失败(由用户A锁定)。
- 用户B等待锁释放 。
3、用户A成功取出30 $:
- 余额是70 $。
- 帐户上的用户A的锁定被释放 。
4、用户B获取帐户上的锁。
-余额是70 $。
-新余额为70 $ + 50 $ = 120 $。
5、账号上用户B的锁定被释放,余额为120 $。
Bug消失了!
这里你需要了解select_for_update
1、在我们的方案中,用户B等待用户A释放锁,我们可以告诉Django 不要等待锁释放并引发DatabaseError。 为此,我们可以将select_for_update的nowait参数
设置为True, …select_for_update(nowait=True)
。
2、选择相关对象也被锁定 -当使用select_for_update
与select_related
时,相关对象也被锁定。
乐观的方法
与悲观的方法不同,乐观的方法不需要锁定对象。 乐观的方法假定冲突不是很常见 ,并且指出只应确保在更新时对对象没有做任何更改。
我们如何用Django来实现这样的事情?
首先,我们添加一列以跟踪对该对象所做的更改:
version = models.IntegerField(
default=0,
)
然后,当我们更新一个对象时,我们确保版本没有改变:
def deposit(self, id, amount):
updated = Account.objects.filter(
id=self.id,
version=self.version,
).update(
balance=balance + amount,
version=self.version + 1,
)
return updated > 0
def withdraw(self, id, amount):
if self.balance < amount:
raise errors.InsufficentFunds()
updated = Account.objects.filter(
id=self.id,
version=self.version,
).update(
balance=balance - amount,
version=self.version + 1,
)
return updated > 0
等等...