select_for_update
为了演示常见的并发问题,我们将使用银行账户模型,开始我们为帐户实例提供一个简单的存款和撤销方法:
当两个用户同时在同一个帐户上执行操作时会发生什么?
1、用户A提取帐户 - 余额为100$。
2、用户B提取帐户 - 余额为100$。
3、用户B退出30$ - 余额更新为100$ - 30$ = 70$。
4、用户A存款50$ - 余额更新为100$ + 50$ = 150$。
这里发生了什么?用户B要求提取30$,用户A存入50$ - 我们预期余额为120$,但最终为150$。
为什么会这样呢?
在步骤4,当用户A更新余额时,他在存储器中存储的金额已经过时(用户B已经退出30$)。
为了防止这种情况发生,我们需要确保我们正在处理的资源在我们正在计算的过程中不会改变。
悲观的做法表明,您应该完全锁定资源,直到完成它 。 如果没有人可以在您处理对象时获取对象上的锁定,那么可以确保对象没有被更改。
我们使用数据库锁有几个原因:
1、 数据库非常擅长管理锁并保持一致性。
2、数据库是访问数据的最低级别 - 获取最低级别的锁也会防止其他进程尝试修改数据。 例如,DB中的直接更新,cron作业,清理任务等。
3、Django应用程序可以在多个进程 (例如工作者)上运行。 在应用程序级别维护锁将需要大量(不必要的)工作。
要在Django中锁定一个对象,我们使用select_for_update 。
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时,相关对象也被锁定。
例如,如果我们选择与用户一起select_related帐户,用户和帐户将被锁定。 如果在存款期间,例如有人正在尝试更新名字,该更新将失败,因为用户对象被锁定。
如果您正在使用PostgreSQL或Oracle,这可能不是一个问题,由于即将到来的Django 2.0 的新功能 。 在此版本中,select_for_update具有“of”选项,用于显式地声明要锁定查询中的哪些表 。