Code前端首页关于Code前端联系我们

高级Django项目开发:事务操作、悲观锁和乐观锁(带有代码演示)

terry 2年前 (2023-09-24) 阅读数 68 #后端开发

事务处理(transaction)是Web应用程序开发的关键。它可以保持数据库的完整性,使整个系统更加安全。例如,当用户A通过网络给用户B转账时,A账户中的钱已经从数据库中取出,而B账户在接收过程中服务器突然崩溃。目前,数据库中的数据并不完整。添加事务处理机制后,如果事务正在进行中出现意外,程序将进行回溯,以保证数据的完整性。本文总结了Django项目开发中事务和事务管理的四大特点,并用实际代码介绍了悲观锁和乐观锁。

Django项目开发进阶:事务操作、悲观锁和乐观锁(附代码演示)

事务的四个主要特征(ACID)

如果要表明一个数据库或框架支持事务性操作,它必须满足以下四个主要特征:

  • 原子性:在整个事务期间所有操作都是要么已完成,或未完成。如果事务执行过程中出现错误,则恢复到事务开始前的状态。
  • 一致性(Consistency):事务开始前和事务结束后不违反数据库的完整性约束。
  • 隔离性:隔离性是指当多个用户同时访问数据库时,比如同时访问一张表,数据库每个用户发起的事务不能被其他事务执行的操作干扰。多个同时发生的事务 事务必须彼此分开。
  • 持久性:事务成功执行后,事务对数据库所做的更改将保留在数据库中并且无法撤消。

注意:

并非所有数据库或框架都支持事务操作。例如,在MySQL中,只有使用Innodb数据库引擎的数据库或表支持事务。

以下是我们将在后续文章中使用的一些与交易相关的常见术语。开启交易:开始交易它的默认事务行为是自动提交,这意味着所有数据库操作(例如调用 save() 方法)都会立即提交到数据库。但是,如果要将连续的 SQL 操作包装在事务中,则必须手动启动事务。

全局启用事务

在 Web 应用程序中处理事务的常见方法是将每个请求包装到事务中。要全局启用事务,您只需将 ATOMIC_REQUESTS 数据库配置元素设置为 True,如下所示:

 DATABASES = {     'default': {         'ENGINE': 'django.db.backends.mysql',         'NAME': 'db1',         'HOST': 'dbhost',         'PORT': '3306',         'USER': 'dbuser',         'PASSWORD': 'password',          #全局开启事务,绑定的是http请求响应整个过程         'ATOMIC_REQUESTS': True,      }

它的工作原理是:当请求到达时,Django 在调用视图方法之前打开一个事务。如果请求被处理并且结果正确返回,Django 就会提交事务。否则,Django 将回滚事务。

如果全局启用事务,您仍然可以使用 non_atomic_requests 装饰器从事务检查中排除某些视图方法,如下所示:

from django.db import transaction
@transaction.non_atomic_requestsdef my_view(request):    do_stuff()
# 如有多个数据库,让使用otherdb的视图不受事务控制@transaction.non_atomic_requests(using='otherdb')def my_other_view(request):    do_stuff_on_the_other_database()

虽然全局启用 Django 很容易。 no 不建议启用全局事务。因为当事务与HTTP请求绑定时,每个请求都会启动一个事务,当访问量上升到一定程度时,会造成巨大的性能损失。在实际开发过程中,很多GET请求根本不涉及事务操作。更好的方法是部分授权交易并按需使用。

部分开启事务

在Django项目中,可以使用transaction.atomic方法来部分开启事务。它可用于创建原子代码块。一旦代码块正常运行,所有更改都会传输到数据库。否则,如果出现异常,更改将被恢复。

原子通常用作装饰器,如下所示:

# 案例一:函数视图 from django.db import transaction
 @transaction.atomic def viewfunc(request):     # This code executes inside a transaction.     do_stuff()
 # 案例二:基于类的视图 from django.db import transaction from rest_framework.views import APIView
 class OrderAPIView(APIView):       # 开启事务,当方法执行完以后,自动提交事务       @transaction.atomic         def post(self, request):           pass 

# 案例一:函数视图 from django.db import transaction
 @transaction.atomic def viewfunc(request):     # This code executes inside a transaction.     do_stuff()
 # 案例二:基于类的视图 from django.db import transaction from rest_framework.views import APIView
 class OrderAPIView(APIView):       # 开启事务,当方法执行完以后,自动提交事务       @transaction.atomic         def post(self, request):           pass 

使用代码并且视图包含在装饰中在事务内运行。有时我们只想在一小段代码的视图方法中使用事务。这种情况下,我们可以借助transaction.atomic()显式启动事务,如下:

 from django.db import transaction
 def viewfunc(request):     # 默认自动提交     do_stuff()            # 显式地开启事务     with transaction.atomic():         # 下面这段代码在事务中执行         do_more_stuff()

倒回事务中的保存点

在操作过程中,我们经常设置保存明确地。保存点)。如果发生异常或错误,则使用savepoint_rollback方法将程序恢复到指定的保存点。如果没有问题,使用savepoint_commit方法提交事务。示例代码如下:

 from django.db import transaction
 def viewfunc(request):     # 默认自动提交     do_stuff()
     # 显式地开启事务     with transaction.atomic():         # 创建事务保存点         sid = transaction.savepoint()
         try:             do_more_stuff()         except Exception as e:             # 如发生异常,回滚到指定地方。             transaction.savepoint_rollback(sid)                   # 如果没有异常,显式地提交一次事务         transaction.savepoint_commit(sid)
     return HttpResponse("Success")

注意:虽然SQLite支持保存点,但sqlite3中的错误导致它们难以使用。

发送交易后的回调函数

有时我们希望在提交当前交易后立即执行额外的任务,例如在客户下单后立即通过电子邮件通知卖家。这种情况下,可以使用Django的 on_commit 方法如下:

# 例1 from django.db import transaction
 def do_something():     pass  # send a mail, invalidate a cache, fire off a Celery task, etc.
 transaction.on_commit(do_something)
 # 例2:调用celery异步任务 transaction.on_commit(lambda: some_celery_task.delay('arg1'))

悲观锁和乐观锁

在数据流量等高并发场景下,无法通过开启事务来避免贸易冲突。例如,用户A和用户B获取某种产品的库存并尝试对其进行修改。 A、B查询的产品库存均为5种。结果,A 订购了 5 件商品,B 订购了 5 件商品。这会导致一个问题。解决方案是在操作(查询或修改)过程中锁定某个产品的库存信息。

常见的锁有悲观锁和乐观锁。接下来我们看看Django项目中的代码是如何实现的:

  • 操作数据时的悲观锁。该操作假定会引起数据冲突,因此在整个数据处理过程中数据保持锁定状态。悲观锁的实现往往依赖于数据库提供的锁定机制。
  • 乐观锁的意思是他在操作数据库的时候非常乐观,相信这个操作不会产生冲突。操作数据时不进行其他特殊处理,更新时评估是否存在冲突。 。乐观锁不是数据库提供的锁,我们要自己实现。

Django 实现悲观锁

如果你想在 Django 中使用悲观锁来锁定一个对象,你应该使用 select_for_update() 方法。这本质上是一个行级锁,可以锁定所有匹配的行,直到事务结束。两个应用示例如下:

 # 案例1:类视图,锁定id=10的SKU对象 class OrderView(APIView):
     @transaction.atomic     def post(self, request):         # select_for_update表示锁,只有获取到锁才会执行查询,否则阻塞等待。         sku = GoodsSKU.objects.select_for_update().get(id=10)
         # 等事务提交后,会自动释放锁。         return Response("xxx")
 # 案例2:函数视图,锁定所有符合条件的文章对象列表。 from django.db import transaction
 with transaction.atomic():     entries = Entry.objects.select_for_update().filter(author=request.user)     for entry in entries:         ...

通常情况下,如果其他事务锁定了相关行,则该查询将被阻塞,直到锁被释放。如果您不想禁用轮询,请使用 select_for_update(nowait=True)

同时使用select_for_updateselect_lated方法时,❀定义的相关对象也会被关闭。您可以使用 select_for_update(of=(...)) 方法指定要锁定的关联对象,如下所示:

 # 只会锁定entry(self)和category,不会锁定作者author entries = Entry.objects.select_related('author', 'category'). select_for_update(of=('self', 'category'))

注意:

  1. select_for_update
    # 例1 from django.db import transaction
     def do_something():     pass  # send a mail, invalidate a cache, fire off a Celery task, etc.
     transaction.on_commit(do_something)
     # 例2:调用celery异步任务 transaction.on_commit(lambda: some_celery_task.delay('arg1'))

    与事务一起使用(transaction)同时。

  2. MySQL 版本必须为 8.0.1 或更高版本才能支持 nowait of 选项。

乐观锁 的 Django 实现

乐观锁 实现通常使用记录的版本号在数据表中添加版本 ID(版本)字段。每次数据更新操作成功,版本号+1。每次进行更新操作时,系统都会判断当前版本号是否为数据的最新版本号。如果这并不意味着数据同时发生了变化,则放弃更新,并且必须重新获取目标对象才能执行更新。Django项目中的

乐观锁可以使用第三方库django-concurrency来实现。您可以向模型添加 version 字段,版本将在每次保存操作时自动添加。号+1。

 from django.db import models from concurrency.fields import IntegerVersionField
 class ConcurrentModel( models.Model ):     version = IntegerVersionField( )     name = models.CharField(max_length=100)

下面的例子中,a和b同时获取模型对象pk=1的信息,并尝试修改其name字段。由于成功调用a.save()方法后对象的版本号增加了1,因此当b再次调用b.save()方法时,会出现错误消息RecordModifiedError。这可以防止 a 和 b 同时改变。相同的对象信息会导致数据冲突。

 a = ConcurrentModel.objects.get(pk=1) a.name = '1'
 b = ConcurrentModel.objects.get(pk=1) b.name = '2'
 a.save() b.save()

那么问题来了,什么时候使用悲观锁,什么时候使用乐观锁锁?为此,必须考虑4个因素:

  • 并发:如果并发程度不大,不允许脏读,可以使用悲观锁来解决并发问题;但如果系统的并行度非常高,那么悲观锁的后果就非常大。
  • 响应速度:如果需要很高的反应速度,建议使用乐观锁方案。如果成功,就会实施。如果你失败了,你就会失败。您不必等待另一个同时来解锁锁。乐观锁并没有真正被束缚,而且非常有效率。
  • 冲突频率:如果冲突频率很高,建议使用悲观锁,以保证成功率。冲突频率高,选择乐观锁需要多次重试才能成功,成本相对较高。
  • 重试成本:如果重试成本较高,建议使用悲观锁。悲观锁依赖于数据库锁,效率低下。更新失败的概率比较小。

总结

本文总结了Django项目开发中事务和事务管理的四大特点,并用实际代码介绍了悲观锁和乐观锁。你都学会了吗?

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门