Django宝典:事务操作,悲观锁及乐观锁

【摘要】
事务处理(transaction)对于Web应用开发至关重要, 它可以维护数据库的完整性, 使整个系统更加安全。比如用户A通过网络转账给用户B,数据库里A账户中的钱已经扣掉,而B账户在接收过程中服务器突然发生了宕机,这时数据库里的数据就不完整了。加入事务处理机制后,如果在这连续交易过程中发生任何意外, 程序将回滚,从而保证数据的完整性。本文将总结事务的四大特性以及Django项目开发中如何操作事务,并以实际代码演示悲观锁和乐观锁。

【正文】

一事务

1.1事务的四大特性(ACID)

  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成。事务在执行过程中发生错误,会被回滚到事务开始前的状态。
  • 一致性 (Consistency):事务开始之前和事务结束后,数据库的完整性约束没有被破坏。
  • 隔离性(Isolation):隔离性是指当多个用户并发访问数据库时,比如同时访问一张表,数据库每一个用户开启的事务,不能被其他事务所做的操作干扰,多个并发事务之间,应当相互隔离。
  • 持久性(Durability):事务执行成功后,该事务对数据库的更改是持久保存在数据库中的,不会被回滚。

1.2Django默认事务行为

Django是支持事务操作的,它的默认事务行为是自动提交,具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务。

1.3全局开启事务

Web应用中,常用的事务处理方式是将每次请求都包裹在一个事务中。全局开启事务只需要将数据库的配置项ATOMIC_REQUESTS设置为True,如下所示:
Django宝典:事务操作,悲观锁及乐观锁_第1张图片
它的工作原理是这样的:每当有请求过来时,Django会在调用视图方法前开启一个事务。如果完成了请求处理并正确返回了结果,Django就会提交该事务。否则,Django会回滚该事务。
如果你全局开启了事务,你仍然可以使用non_atomic_requests装饰器让某些视图方法不受事务控制,如下所示:
Django宝典:事务操作,悲观锁及乐观锁_第2张图片
虽然全局开启事务很简单,但Django并不推荐开启全局事务。因为一旦将事务跟 HTTP 请求绑定到一起时,每一个请求都会开启事务,当访问量增长到一定的时候会造成很大的性能损耗。在实际开发过程中,很多GET请求根本不涉及到事务操作,一个更好的方式是局部开启事务按需使用。

1.4局部开启事务

Django项目中局部开启事务,可以借助于transaction.atomic方法。使用它我们就可以创建一个具备原子性的代码块,一旦代码块正常运行完毕,所有的修改会被提交到数据库。反之,如果有异常,更改会被回滚。
atomic经常被当做装饰器来使用,如下所示:
Django宝典:事务操作,悲观锁及乐观锁_第3张图片

二悲观锁与乐观锁

在电商秒杀等高并发场景中,仅仅开启事务还是无法避免数据冲突。比如用户A和用户B获取某一商品的库存并尝试对其修改,A, B查询的商品库存都为5件,结果A下单5件,B也下单5件,这就出现问题了。解决方案就是操作( 查询或修改)某个商品库存信息时对其加锁。
常见的锁有悲观锁和乐观锁,接下来我们来看下在Django项目中如何通过代码实现:

  • 悲观锁就是在操作数据时,假定此操作会出现数据冲突,在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制实现的。
  • 乐观锁是指操作数据库时想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理, 而在进行更新时,再去判断是否有冲突了。乐观锁不是数据库提供的锁,需要我们自己去实现。

2.1悲观锁

Django中使用悲观锁锁定一个对象,需要使用select_for_update()方法。它本质是一个行级锁,能锁定所有匹配的行,直到事务结束。两个应用示例如下所示:
Django宝典:事务操作,悲观锁及乐观锁_第4张图片
一般情况下如果其他事务锁定了相关行,那么本次查询将被阻塞,直到锁被释放。如果不想要使查询阻塞的话,使用select_for_update(nowait=True)。
当你同时使用select_for_update与select_related方法时,select_related指定的相关对象也会被锁定。你可以通过select_for_update(of=(…))方法指定需要锁定的关联对象,如下所示:
在这里插入图片描述
注意:

  • select_for_update方法必须与事务(transaction)同时使用。
  • MySQL版本要在8.0.1+ 以上才支持 nowait和 of选项。

2.2乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据
Django宝典:事务操作,悲观锁及乐观锁_第5张图片
解析以上代码,先查询数据库库存数量,定义个库存变量,然后当保存订单的时候已该字段为过滤条件进行过滤修改,如果修改失败,则返回0,并重试3次(自定义重试次数),该方法要注意数据库的隔离级别一定要设置为Read Committed 。
那么问题来了,什么时候该用悲观锁,什么时候该用乐观锁呢?这主要需要考虑4个因素:

  • 并发量:如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题, 建议乐观锁。
  • 响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。
  • 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
  • 重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

你可能感兴趣的:(Django)