高级Django项目开发:事务操作、悲观锁和乐观锁(带有代码演示)
事务处理(transaction)是Web应用程序开发的关键。它可以保持数据库的完整性,使整个系统更加安全。例如,当用户A通过网络给用户B转账时,A账户中的钱已经从数据库中取出,而B账户在接收过程中服务器突然崩溃。目前,数据库中的数据并不完整。添加事务处理机制后,如果事务正在进行中出现意外,程序将进行回溯,以保证数据的完整性。本文总结了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_requests
def 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
使用 在操作过程中,我们经常设置保存明确地。保存点)。如果发生异常或错误,则使用savepoint_rollback方法将程序恢复到指定的保存点。如果没有问题,使用savepoint_commit方法提交事务。示例代码如下: 注意:虽然SQLite支持保存点,但 有时我们希望在提交当前交易后立即执行额外的任务,例如在客户下单后立即通过电子邮件通知卖家。这种情况下,可以使用Django的 在数据流量等高并发场景下,无法通过开启事务来避免贸易冲突。例如,用户A和用户B获取某种产品的库存并尝试对其进行修改。 A、B查询的产品库存均为5种。结果,A 订购了 5 件商品,B 订购了 5 件商品。这会导致一个问题。解决方案是在操作(查询或修改)过程中锁定某个产品的库存信息。 常见的锁有悲观锁和乐观锁。接下来我们看看Django项目中的代码是如何实现的: 如果你想在 Django 中使用悲观锁来锁定一个对象,你应该使用 通常情况下,如果其他事务锁定了相关行,则该查询将被阻塞,直到锁被释放。如果您不想禁用轮询,请使用 同时使用 注意: 乐观锁 实现通常使用记录的版本号在数据表中添加版本 ID(版本)字段。每次数据更新操作成功,版本号+1。每次进行更新操作时,系统都会判断当前版本号是否为数据的最新版本号。如果这并不意味着数据同时发生了变化,则放弃更新,并且必须重新获取目标对象才能执行更新。Django项目中的 乐观锁可以使用第三方库 下面的例子中,a和b同时获取模型对象pk=1的信息,并尝试修改其name字段。由于成功调用a.save()方法后对象的版本号增加了1,因此当b再次调用b.save()方法时,会出现错误消息 那么问题来了,什么时候使用悲观锁,什么时候使用乐观锁锁?为此,必须考虑4个因素: 本文总结了Django项目开发中事务和事务管理的四大特点,并用实际代码介绍了悲观锁和乐观锁。你都学会了吗?代码
并且视图包含在装饰中在事务内运行。有时我们只想在一小段代码的视图方法中使用事务。这种情况下,我们可以借助
transaction.atomic()
显式启动事务,如下: from django.db import transaction
def viewfunc(request):
# 默认自动提交
do_stuff()
# 显式地开启事务
with transaction.atomic():
# 下面这段代码在事务中执行
do_more_stuff()
倒回事务中的保存点
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")
sqlite3
中的错误导致它们难以使用。 发送交易后的回调函数
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'))
悲观锁和乐观锁
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_update
和select_lated
方法时,❀定义的相关对象也会被关闭。您可以使用 select_for_update(of=(...))
方法指定要锁定的关联对象,如下所示: # 只会锁定entry(self)和category,不会锁定作者author
entries = Entry.objects.select_related('author', 'category'). select_for_update(of=('self', 'category'))
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)同时。
nowait
和 of
选项。 乐观锁 的 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)
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()
总结
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。