MySQL事务的介绍

1) 事务概念

一组mysql语句,要么执行,要么全不不执行。

2) 事务的特点

1、原子性:一组事务,要么成功;要么撤回。

2、稳定性:有非法数据(外键约束之类),事务撤回。

3、隔离性:事务独立运行。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。

4、可靠性:软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得,innodb_flush_log_at_trx_commit选项 决定什么时候吧事务保存到日志里。

3) 事务控制语句

开启事务:BEGIN或START TRANSACTION;显式地开启一个事务;

提交事务:COMMIT;也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;

回滚:ROLLBACK;有可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的务,并撤销正在进行的所有未提交的修改;

保存节点:SAVEPOINT identifier;SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT;

删除事务:RELEASE SAVEPOINT identifier;删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;

回滚到保存的节点:ROLLBACK TO identifier;把事务回滚到标记点;

4) mysql事务隔离级别

SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

(1)Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

(2)Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

(3)Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

(4)Serializable(可串行化)

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:

脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。

不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几行(Row)数据,而另一个事务却在此时插入了新的几行数据,先前的事务在接下来的查询中,就会发现有几行数据是它先前所没有的。

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_乐观锁

5) 设置mysql事务的隔离级别

打开mysql配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf,添加如下行。

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_乐观锁_02

保存配置文件,重启mysql服务。

sudo service mysql restart

django中使用事务

下面以天天生鲜项目中的订单提交为例,说明django中事务的使用:

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_mysql commit work_03

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_数据库_04

1 from django.db importtransaction2 classOrderCommitView(View):3 '''订单创建'''
4
5 @transaction.atomic ## 使用事务
6 defpost(self,request):7 '''订单创建'''
8 ## 判断用户是否登录
9 user =request.user10 if notuser:11 return JsonResponse({'res':0,'errmsg':'用户未登录'})12
13 ## 1、接收数据
14 pay_method = request.POST.get('pay_method')15 addr_id = request.POST.get('addr_id')16 skus_id = request.POST.get('skus_id')17 ## 将skus_id的格式转化成列表形式
18 skus_id = skus_id.split(',')19
20
21 ## 2、校验数据
22 if notall([pay_method,addr_id,skus_id]):23 return JsonResponse({'res':1,'errmsg':'数据不完整'})24 skus =[]25 for sku_id inskus_id:26 ## 遍历skus_id,从数据库中查询sku
27 try:28 sku = GoodsSKU.objects.get(id=sku_id)29 skus.append(sku)30 exceptGoodsSKU.DoesNoExist:31 return JsonResponse({'res':2,'errmsg':'商品不存在'})32 try:33 addr = Address.objects.get(id=addr_id)34 exceptAddress.DoesNoExist:35 return JsonResponse({'res':3,'errmsg':'地址不存在'})36 ## 校验支付方式
37 if str(pay_method) not inOrderInfo.PAY_METHODS.keys():38 return JsonResponse({'res':4,'errmsg':'错误的支付方式'})39 ## 设置事务的节点
40 save_id =transaction.savepoint()41
42 ## 3、业务处理
43 ########## (1)先向订单数据库中添加一条数据,准备数据:order_id、user、addr、pay_method、total_count、total_price、transit_price
44 ## 计算total_count和total_price,先初始化为0,后面查询各个商品的数量和价格之后再进行累加,以及重新保存
45 try:46 total_count =047 total_price =048 ## 生成order_id:格式:当前的时间+sku_id
49 order_id = datetime.now().strftime('%Y%m%d%H%M%S') +str(user.id)50 ## 添加数据,创建新的订单数据
51 order = OrderInfo.objects.create(order_id=order_id,52 user=user,53 addr=addr,54 pay_method=pay_method,55 total_count=total_count,56 total_price=total_price,57 transit_price=10)58
59 ########## (2)向商品订单中添加多条数据,准备数据:order、sku、count、price
60 conn = get_redis_connection('default')61 cart_key = 'cart_%d'%user.id62 for sku inskus:63 count =conn.hget(cart_key,sku.id)64 price =sku.price65 OrderGoods.objects.create(order=order,66 sku=sku,67 count=count,68 price=price)69 if sku.stock 
71 transaction.rollback(save_id)72 return JsonResponse({'res':6,'errmsg':'库存不足'})73 ## MySQL数据库的库存减少,销量增加
74 sku.stock -=int(count)75 sku.sales +=int(count)76 sku.save()77 ## 删除Redis数据库中添加到订单中的数据
78 conn.hdel(cart_key,sku.id)79 ## 累加total_count 和total_price
80 total_count +=int(count)81 total_price += int(count)*int(price)82 ## 重新更新订单中的total_count和total_price
83 order.total_count =total_count84 order.total_price =total_price85 order.save()86
87 exceptException:88 ## 业务失败时,回滚事务
89 transaction.rollback(save_id)90 return JsonResponse({'res':7,'errmsg':'订单创建失败'})91
92 ## 4、返回应答
93 return JsonResponse({'res':5,'message':'创建订单成功'})

django中使用事务

参考文档:

订单并发处理

1、订单并发问题

假设数据库中鸡腿的库存只有1,这个时候有两个用户同时提交订单,就会出现下面的情况:

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_乐观锁_05

一个用户使用一个进程,当用户1执行到2的时候,这时判断库存是1,刚好切换到进程2,所以用户2顺利下单,再切换到用户1 的时候,用户1用于之前判断库存为1,所以也能够顺利下单,这就造成了只有1个库存,却下了两个订单,显然这是不允许的,所以就必须要给进程加锁,下面是两种解决方案:悲观锁和乐观锁

2、订单并发解决

(1)悲观锁

悲观锁的原理:在查询数据库时加锁,当用户拿到锁时,其它用户不能对数据库进行操作,直到该用户对数据库操作结束时才释放锁。

加锁的语句:

select * from df_goods_info where id=sku_id for update;

django中查询数据库加锁:

sku = GoodsSKU.objects.select_for_update().get(id=sku_id)

mysql 事务提交才写binlog还是未提交就写binlog mysql提交事务超时默认多少_数据_06

悲观锁获取数据时对数据行了锁定,其他事务要想获取锁,必须等原事务结束。

(2)乐观锁

乐观锁原理:查询时不锁数据,提交更改时进行判断

加锁的语句:

update df_goods_sku set stock=0, sales=1 where id=sku_id and stock=1;

django更新数据库时加锁的语句:

res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)

这里注意一下:

乐观锁是判断更新完的数据是否与查询时的数据一样,如果一样就返回1,否则返回0。这里就存在一个问题:有两个用户同时下单,假如库存为3,当两个用户查询结束时,此时两个用户查询到的库存都是3,其中用户A拿到乐观锁,进行数据库的修改,将数据库的库存改为2,用户B拿到乐观锁后,由于此时库存与用户B之前查到的数据不一致,所以用户B下单失败。

解决方法:

通常我们会给用户3次机会进行数据库修改,所以可以将查询语句以及修改语句放在for循环里面。并且MySQL默认是 Repeatable Read(可重读),也就是用户A更新完的数据用户B是拿不到的,所以需要将MySQL事务隔离级别改为Read Committed(读取提交内容)

具体实例:

classOrderCommitView(View):'''订单创建'''@transaction.atomicdefpost(self, request):'''订单创建'''
#设置事务保存点
save_id =transaction.savepoint()try:for sku_id insku_ids:for i in range(3):#获取商品的信息
try:
sku= GoodsSKU.objects.get(id=sku_id)except:#商品不存在
transaction.savepoint_rollback(save_id)return JsonResponse({'res':4, 'errmsg':'商品不存在'})#从redis中获取用户所要购买的商品的数量
count =conn.hget(cart_key, sku_id)#todo: 判断商品的库存
if int(count) >sku.stock:
transaction.savepoint_rollback(save_id)return JsonResponse({'res':6, 'errmsg':'商品库存不足'})# todo: 更新商品的库存和销量
orgin_stock = sku.stock
new_stock = orgin_stock - int(count)
new_sales = sku.sales + int(count)
# 返回受影响的行数
res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)
if res == 0:
if i == 2:
# 尝试的第3次
transaction.savepoint_rollback(save_id)
return JsonResponse({'res': 7, 'errmsg': '下单失败2'})
continue
#todo: 向df_order_goods表中添加一条记录
OrderGoods.objects.create(order=order,
sku=sku,
count=count,
price=sku.price)#todo: 累加计算订单商品的总数量和总价格
amount = sku.price*int(count)
total_count+=int(count)
total_price+=amount#跳出循环
break
#todo: 更新订单信息表中的商品的总数量和总价格
order.total_count =total_count
order.total_price=total_price
order.save()exceptException as e:
transaction.savepoint_rollback(save_id)return JsonResponse({'res':7, 'errmsg':'下单失败'})#提交事务
transaction.savepoint_commit(save_id)#返回应答
return JsonResponse({'res':5, 'message':'创建成功'})

(3)悲观锁和乐观锁的使用场景

冲突比较少的时候,使用乐观锁。

冲突比较多的时候,使用悲观锁。