Django中的数据更新机制
众所周知,在Django使用ORM机制来对SQL语句做封装,从而实现简单、统一的数据查询机制,并且通过Queryset机制极大程度减少频繁的数据库存取。
但是,在我们当前的项目中,会用到高并发的定时任务,并且存在对同一条数据的字段修改的情景,这就造成了可能的脏写、甚至数据库死锁。
好在Django已经存在对类似问题的解决办法,那就是使用事务 + select_for_update。
事务
数据库中的事务概念:构成单一逻辑工作单元的操作集合。
其中的概念是非常多且复杂的,如果需要讲清楚需要单独再开一篇文章来学习。
这里只需要知道,事务是一个单元操作。
悲观锁
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
简而言之,悲观锁主要用于保护数据的完整性。当多个事务并发执行时,某个事务对数据应用了锁,则其他事务只能等该事务执行完了,才能进行对该数据进行修改操作。
实际使用
在Django中,开启事务的方式一般有两种:
- 使用装饰器
- 使用上下文管理器
个人来说,一般喜欢使用上下文管理器,也就是:
with transaction.atomic():
# 所需要定义为事务的代码逻辑
同时,Django中在进行filter时,还支持一种行级锁:select_for_update,需要注意的是必须用在事务里面,否则会查询出错,返回一个锁住行直到事务结束的查询集,如果数据库支持,它将生成一个 SELECT … FOR UPDATE 语句。
所有匹配的行将被锁定,直到事务结束。这意味着可以通过锁防止数据被其它事务修改。
一般情况下如果其他事务锁定了相关行,那么本查询将被阻塞,直到锁被释放。 如果这不想要使查询阻塞的话,使用select_for_update(nowait=True)。 如果其它事务持有冲突的锁,互斥锁, 那么查询将引发 DatabaseError 异常。你也可以使用select_for_update(skip_locked=True)忽略锁定的行。 nowait和 skip_locked是互斥的,同时设置会导致ValueError。
目前,postgresql,oracle和mysql数据库后端支持select_for_update()。 但是,MySQL不支持nowait和skip_locked参数,因此在我们的项目中也只是单纯的使用select_for_update()而不带有任何参数
使用不支持这些选项的数据库后端(如MySQL)将nowait=True或skip_locked=True转换为select_for_update()将导致抛出DatabaseError异常,这可以防止代码意外终止。
同时,这种事务机制是嵌套的,也就是说,假如我们有一段逻辑使用了select_for_update,在外层的时候我们可以统一使用with transaction.atomic(),这样一整个逻辑都是属于事务的一部分了。
参考文献:
Django事务和锁
MySQL中的悲观锁