事务
数据库事务具有四个特征,简称为事务的ACID特性
1.什么是事务
开启一个事务可以包含一堆sql语句,这些sql语句要么都成功,要么一个也别想成功
2.事务作用
保证了对数据操作的数据安全性
3.事务的四大特性
- 原子性(atomicity): 一个事务是一个不可分割的工作单位,事务中包含的诸多操作要么都成功,要么都不成功
- 一致性(consistency): 事务必须是使数据从一个一致状态变到另一个一致状态,一致性与原子性是密切相关的
- 隔离性(isolation): 一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并行的个事务之间不能互相干扰
- 持久性(durability): 持久性也成永久性(permanence),指一个事务一旦提交,他对数据库中数据的改变就应该是永久性的,接下来的其他操作或故障不应该对其有任何影响
4.事务使用
修改数据之前先开启事务操作
start transaction;
# 修改操作
update user set balance=900 where name='wsb'; #买支付100元
update user set balance=1010 where name='egon'; #中介拿走10元
update user set balance=1090 where name='ysb'; #卖家拿到90元
# 回滚到上一个状态
rollback;
# 开启事务后,只要没有执行commit操作,数据其实都没有真正刷到硬盘
commit;
'''
开启事务检测操作是否完整,不完整主动回滚到上一个状态,如果完整就应该执行commit操作
'''
# python代码角度,应该实现的伪代码逻辑
try:
update user set balance=900 where name='wsb'; #买支付100元
update user set balance=1010 where name='egon'; #中介拿走10元
update user set balance=1090 where name='ysb'; #卖家拿到90元
except 异常:
rollback;
else:
commit;
5.什么是事务隔离
事务的隔离性是指在并发环境中,并发的事务是相互隔离的,
可以理解为多个事务同一时间段对数据库的增删改时是要隔离的
隔离种类
SQL标准中定义了四种数据库事务隔离级别,级别从低到高分别为:
- 读未提交(Read Uncommitted)、-> 脏读
- 读已提交(Read Committed)、 -> 不可重复读
- 可重复读(Repeatable Read)、-> 换读
- 串行化(Serializable)。
在事务的并发操作中会出现脏读、不可重复读、幻读。在事务的并发操作中第二类更新丢失可以通过乐观锁和悲观锁解决。
读未提交(Read Uncommitted)
- 该隔离级别,所有事务都可以看到其他交事务未提的执行结果。通俗地讲就是,在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。
- 该隔离级别可能导致的问题是脏读。因为另一个事务可能回滚,所以在第一个事务中读取到的数据很可能是无效的脏数据,造成脏读现象。
set tx_isolation='READ-UNCOMMITTED';
案例:
脏读(读取未提交数据)
A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
这种情况常发生于转账与取款操作
读已提交(Read Committed)
- 这是大多数数据库系统的默认隔离级别(但不是mysql默认的)
- 一个事务只能看见已经提交事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。
- 该隔离级别可能导致的问题是不可重复读。因为两次执行同样的查询,可能会得到不一样的结果。
set tx_isolation='READ-COMMITTED';
案例:
不可重复读(前后多次读取,数据内容不一致)
事务A在执行读取操作,由于整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据和之前的不重复(不一样)了,系统不可以读取到重复的数据,称为不可重复读
可重复读(Repeatable Read)
- 这是MySQL的默认事务隔离级别
- 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。通俗来讲,可重复读在一个事务里读取数据,怎么读都不会变,除非提交了该事务,再次进行读取。
- 该隔离级别存在的问题是幻读
set tx_isolation='REPEATABLE-READ';
案例:
幻读(前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
串行化(Serializable)
- 这是最高的隔离级别
- 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。通俗地讲就是,假如两个事务都操作到同一数据行,那么这个数据行就会被锁定,只允许先读取/操作到数据行的事务优先操作,只有当事务提交了,数据行才会解锁,后一个事务才能成功操作这个数据行,否则只能一直等待
- 该隔离级别可能导致大量的超时现象和锁竞争。
set tx_isolation='SERIALIZABLE';
不可重复读和幻读到底有什么区别呢?
(1)不可重复读是读取了其他事务更改的数据,针对insert与update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
乐观锁
乐观锁本身不是锁.而是一种判断机制
在更新语句中增加过滤条件,进行版本的判断,可以是这条记录的某个字段值。然后通过updata()所影响行数来判断是否更新成功。影响条数为0则更新失败,代表这条记录被其他事务修改了.
注意:
Django2.0以下版本使用乐观锁时需要把隔离机制设置成不可重复读
eg: 事务+乐观锁的使用
# django中使用 事务 + 乐观锁
# 这是一个商城订单提交要修改数据库案例
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction # 导入事务
class Creat(APIView):
@transaction.atomic # 装饰事务
def post(self,request):
......
sid=transaction.savepoint() # 对下面代码创建事务
for product in all_product:
product.product_id=str(product.product_id)
order_data['order_total']+=product.price*buy_list[product.product_id]
order_data['quantity']+=buy_list[product.product_id]
#创建子订单
for i in range(3):
stock=product.stock.quantity # 商品在库中的数量
new_stock=stock-buy_list[product.product_id] # 减掉用户购买数量
if new_stock<0: # 库存数小于用户购买数量
transaction.savepoint_rollback(sid) # 回滚
return Response({"code":203,"msg":f"{product.name}库存不足"})
# 使用乐观锁: quantity=字段, 在执行update前,锁定该字段的是否没有被改动,若改动了,则update影响调试为0
res=models.Stock.objects.filter(quantity=stock,stock_id=product.stock.stock_id).update(quantity=new_stock)
if not res:
if i==2:
transaction.savepoint_rollback(sid) # 回滚
return Response({"code": 203, "msg": f"创建订单失败"})
else:
continue
else:
break
悲观锁
可以理解为就是一把互斥索,
悲观锁认为:事务A在操作数据时,这条数据同时也在被别的事务操作,所以事务在操作数据时加悲观锁(互斥锁),让这条数据不能被其他事务操作
select_for_update()
在查询数据时使用select_for_update()查询,就表示为该数据进行加锁,只有commint()完成后才会释放锁
案例:
class Creat(APIView):
@transaction.atomic # 装饰事务
def post(self,request):
# 创建保存点
sid = transaction.savepoint()
......
try:
# goods=Goods.objects..get(id=goods_id) # 不加锁
goods=Goods.objects.select_for_update().get(id=goods_id) # 加互斥锁
execpt Goodsgoods.DoesNOTexist:
# 回滚
transaction.rollback(sid)
return JsonResponse({'res':2, 'err':'商品信息有误'})
......
transaction.commint() # 提交事务
return JsonResponse({'res':0, 'err':'订单创建成功'})
悲观锁分为共享锁和排它锁。
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,共享锁是用来读取数据的。另外,一个事务获取了同一数据的共享锁,其他事务就不能获取该数据的排它锁。
排它锁又称为写锁,简称X锁,顾名思义,排它锁就是不能与其他锁并存,如一个事务获取了一个数据行的排它锁,其他事务就不能再获取该行的其它锁,包括共享锁和排它锁。另外不存什么事务隔离级别,update/insert/delete会自动获取排它锁
共享锁获取方式:select * from account where name='jack' lock in share mode;
排它锁获取方式:select * from account where name='jack' for update;
MySQL分为表级锁和行级锁,共享锁和排它锁是行级锁。表级锁在此不做讨论。