数据库事务
基本概念
事务是指作为单个逻辑工作单元执行的一系列操作,这些操作具有原子性,即这些操作要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。
事务属性
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
- 原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
事务举例
- 银行转账
假定A账户目前有100元,B账户和C账户同时向A账户转账100元。这个时候,我们需要保证:
- B账户减少100元后,A账户增加100元。假使交易途中发生故障,B账户不应减少100元,A账户也不应增加100元。(原子性、一致性)
- C账户减少100元后,A账户增加100元。假使交易途中发生故障,C账户不应减少100元,A账户也不应增加100元。(原子性、一致性)
- B账户交易、C账户交易过程中,应有先后次序,交易不可在同一个余额上重复执行,以确保交易后A账户余额正确。(隔离性)
- 交易记录应在交易完成后永久记录。(持久性)
可见,转账事务具有上述四个属性,因此在处理转账时,需要将其对待为事务。
- 公众号抢票操作
假定A活动目前有100张余票,B同学和C同学同时需要抢票一张。这个时候,我们需要保证:
- A活动减少1张票后,B同学手中应多一张活动票。假使抢票途中发生故障,B不应获得1张活动票,A活动也不应减少1张票。(原子性、一致性)
- A活动减少1张票后,C同学手中应多一张活动票。假使抢票途中发生故障,C不应获得1张活动票,A活动也不应减少1张票。(原子性、一致性)
- B同学抢票、C同学抢票过程中,应有先后次序,交易不可在同一个余票基础上重复执行,以确保交易后A活动余票正确。(隔离性)
- 抢票记录应在抢票完成后永久记录。(持久性)
可见,抢票事务具有上述四个属性,因此在处理抢票时,需要将其对待为事务。
MySQL中的事务管理
事务控制语句
- BEGIN或START TRANSACTION;显式地开启一个事务;
- COMMIT;也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改成为永久性的;
- ROLLBACK;有可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
- SAVEPOINT identifier;SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT;
- RELEASE SAVEPOINT identifier;删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
- ROLLBACK TO identifier;把事务回滚到标记点;
- SET TRANSACTION;用来设置事务的隔离级别。InnoDB存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。
MySQL事务处理主要使用的方法
- 用 BEGIN, ROLLBACK, COMMIT来实现
- BEGIN 开始一个事务
- ROLLBACK 事务回滚
- COMMIT 事务确认
- 直接用 SET 来改变 MySQL 的自动提交模式:
- SET AUTOCOMMIT=0 禁止自动提交
- SET AUTOCOMMIT=1 开启自动提交
在 Django 中进行事务管理
在上面的例子中我们看到,若是直接在项目中使用MySQL数据库并使用原始的SQL语句进行事务管理,会是一件相当麻烦的事情。但是不必担心,如果后端使用的是Django框架的话,Django框架就已经提供了好几种方式来控制和管理数据库事务,并且与使用数据库无关。
Django框架默认的事务行为
SQL的标准中指出,除非已经存在一个开启的事务,否则每个SQL查询都会开启一个新事务。这些事务后续必须被明确的提交或者回滚。
这对应用开发者来说并不是很方便。为了降低这种不便性,大部分数据库提供了自动提交模式。Django亦直接提供了自动提交模式,并默认打开了自动提交。它表现形式为:每次数据库操作会立即被提交到数据库中,除非这个事务仍然处于激活状态。
显式地控制事务
Django 提供了单独 API 来控制事务。
atomic(using=None, savepoint=True)
原子性是数据库事务的一个属性。使用上述 **atomic **,我们就可以创建一个具备原子性的代码块。一旦代码块正常运行完毕,所有的修改会被提交到数据库。反之,如果有异常,更改会被回滚。
被atomic管理起来的代码块还可以内嵌到方法中。这样的话,即便内部代码块正常运行,如果外部代码块抛出异常的话,它也没有办法把它的修改提交到数据库中。
atomic 的使用方法如下:
- 当做装饰器来使用
from django.db import transaction
@transaction.atomic
def viewfunc(request):
## This code executes inside a transaction.
do_stuff()
- 当作上下文管理器来使用
from django.db import transaction
def viewfunc(request):
## This code executes in autocommit mode (Django's default).
do_stuff()
with transaction.atomic():
## This code executes inside a transaction.
do_more_stuff()
一旦把 atomic 代码块放到 try / except 中,完整性错误就会被自然的处理掉了,比如下面这个例子:
from django.db import IntegrityError, transaction
@transaction.atomic
def viewfunc(request):
create_parent()
try:
with transaction.atomic():
generate_relationships()
except IntegrityError:
handle_exception()
add_children()
这个例子中,即使 generate_relationships() 中的代码打破了数据完整性约束,我们仍然可以在 add_children() 中执行数据库操作,并且 create_parent() 产生的更改也有效。需要注意的是,在调用 handle_exception() 之前,generate_relationships() 中的修改就已经被安全的回滚了。因此,如果有需要,照样可以在异常处理函数中操作数据库。
下面是在Django中使用atomic对事务进行管理时,Django其行为的总结:
- 进入最外层atomic代码块时开启一个事务;
- 进入内部atomic代码块时创建保存点;
- 退出内部atomic时释放或回滚事务;
- 退出最外层atomic代码块时提交或者回滚事务;
使得事务间具有隔离性
上文的介绍已经可以使得我们在Django中保证事务的原子性、一致性与持久性,但是事务的隔离性还不能充分保证。如何保证事务的隔离性呢?答案就是使用 select for update 进行数据库查询。
select … for update 是数据库层面上专门用来解决并发取数据后再修改的场景的,主流的关系数据库比如MySQL、PostgreSQL都支持这个功能,而良心的 Django 直接提供了这个功能的shortcut 。
select_for_update(nowait=False, skip_locked=False, of=())¶
select_for_update 将返回一个查询集,该查询集将锁定被查询行直到事务结束,并在受支持的数据库上生成SQL语句 SELECT … FOR UPDATE。
例如:
rom django.db import transaction
entries = Entry.objects.select_for_update().filter(author=request.user)
with transaction.atomic():
for entry in entries:
...
在获取查询集后,所有匹配的条目将被锁定,直到事务块结束,这意味着将阻止其他事务更改或获取锁定。默认情况下,select_for_update()锁定查询选择的所有行。通常,如果另一个事务已经对其中一个选定行获取了锁,则新来的查询将阻塞,直到锁被释放。
实战
有了如上的基础,我们可以轻松实现上面提到的抢票事务。下面部分中,handle是处理抢票事务类中的关键方法。
@transaction.atomic ## 轻松开启事务
def handle(self):
## 测试是否存在此用户
try:
## 锁定被查询行直到事务结束
user =
User.objects.select_for_update().get(open_id=self.user.open_id)
except User.DoesNotExist:
raise BaseError(-1, 'User does not exist.')
## 测试用户是否绑定
if not user.student_id:
return self.reply_text(self.get_message('bind_account'))
## 处理文本信息的情况
if self.is_msg_type('text'):
act_key = self.input['Content'][len("抢票 "):]
## 锁定被查询行直到事务结束
acts =
Activity.objects.select_for_update().filter(key=act_key)
## 处理点击事件的情况
else:
act_id = int(self.input['EventKey'].split('_')[-1])
## 锁定被查询行直到事务结束
acts = Activity.objects.select_for_update().filter(id=act_id)
## 检查活动
if len(acts) == 0:
return self.reply_text("【 抢票失败 】 对不起,这儿没有对应的活动:(")
act = acts[0]
if act.status != Activity.STATUS_PUBLISHED:
return self.reply_text("【 抢票失败 】 对不起,这儿没有对应的活动:(")
current_timestamp = timezone.now().timestamp()
book_start_timestamp = act.book_start.timestamp()
book_end_timestamp = act.book_end.timestamp()
if current_timestamp < book_start_timestamp
or book_end_timestamp < current_timestamp:
return self.reply_text("【 抢票失败 】 对不起,现在不是抢票时间:(")
if act.remain_tickets <= 0:
return self.reply_text("【 抢票失败 】 对不起,已经没有余票了:(")
## 检查重复抢票的情况
## 锁定被查询行直到事务结束
tickets_valid_in_the_same_activity =
Ticket.objects.select_for_update().filter(
student_id = user.student_id,
activity = act,
status = Ticket.STATUS_VALID
)
if len(tickets_valid_in_the_same_activity) > 0:
return self.reply_text("【 抢票失败 】 请不要重复抢票")
## 锁定被查询行直到事务结束
tickets_used_in_the_same_activity =
Ticket.objects.select_for_update().filter(
student_id = user.student_id,
activity = act,
status = Ticket.STATUS_USED
)
if len(tickets_used_in_the_same_activity) > 0:
return self.reply_text("【 抢票失败 】 请不要重复抢票")
## 票充足,处理活动表格、电子票表格,失败、成功都则返回对应信息
act.remain_tickets = act.remain_tickets - 1
ticket_unique_id = str(uuid.uuid1()) + str(user.student_id)
if len(ticket_unique_id) > 64:
ticket_unique_id = ticket_unique_id[:64]
Ticket.objects.create(
student_id = user.student_id,
unique_id = ticket_unique_id,
activity = act,
status = Ticket.STATUS_VALID
)
act.save()
return self.reply_single_news({
'Title': "【 抢票成功 】 " + act.name,
'Description': act.description,
'Url': self.url_activity(act.id),
})
参考
- 数据库事务 - 百度百科https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1/9744607
- MySQL 事务 - 菜鸟教程http://www.runoob.com/mysql/mysql-transaction.html
- Database transactionshttps://docs.djangoproject.com/en/1.9/topics/db/transactions/
- Race Condition & Transactionhttps://kuanyui.github.io/2015/03/14/django-db-transaction/
- select_for_update()https://docs.djangoproject.com/en/1.9/ref/models/querysets/##select-for-update