【DailyFresh】课程记录6---订单模块(订单并发)

P84 订单生成——MySQL事务概念
在创建中,向订单商品表中添加一条记录之前,还有一步操作,即判断商品的库存
# TODO: 判断商品的库存
分析:假如一件商品只有两件库存,你往购物车中加了两个,另一个用户也加了两个,你俩点击提交订单时,他比你先一步付款,此时库存为0而你的购物车中依然是两件
因此,在向订单商品表中添加一条记录之前需要判断商品的库存【在 # todo: 向df_order_goods表中添加一条记录 之前】
if int(count) > sku.stock:
    return JsonResponse({'res': 6, 'errmsg': '商品库存不足'})

为了演示效果,进行休眠操作,休眠10s:
for sku in skus:
    import time
    time.sleep(10)
    ...
    # TODO: 判断商品的库存
    if int(count) > sku.stock:
        return JsonResponse({'res': 6, 'errmsg': '商品库存不足'})
    # todo: 向df_order_goods表中添加一条记录
    ...

以鸡腿为例,其id=17,在页面生成订单,在数据库清除库存模拟这个过程
update df_goods_sku set stock=0 where id=17;

此时,订单信息表中会生成一条信息,实际上这一条信息也不应该有

解决上述问题,引入MySQL事务
MySQL事务: 一组sql操作,要么都成功,要么都失败
创建订单这一系列的操作应该放在一个事务中,要么全成功,要么全失败

事务的概念:一组sql语句,要么执行,要么全部不执行
事务的特点:
1.原子性:一组sql语句,要么执行,要么全部不执行
2.稳定性:有非法数据(外键约束之类),事务撤回。《---在事务中执行一些语句的时候,如果有非法数据,比如外键约束,导致执行失败,该事务也会被撤回。
3.隔离性:事务独立运行,一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。
4.可靠性:软硬件崩溃之后,数据库的引擎表InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得。所谓的可靠性就是你执行事务的时候,他会把事务保存到日志中去,如果软硬件崩溃之后,他会在你恢复之后根据日志的记录对它进行重新执行,保证它的可靠性。

MySQL中事务控制语句
你如果需要把一些语句放在事务中时,需要自己开启一个事务

使用BEGIN和START TRANSACTION:显式地开启一个事务;
开启事务之后,在BEGIN后的语句都在事务里面。
事务最终执行的结果有两个,一个是COMMIT,一个是ROLLBACK
COMMIT: 也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;
ROLLBACK:也可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。

在数据库中进行测试BEGIN...COMMIT或BEGIN...ROLLBACK


在事务中还可以创建事务的保存点,在回滚的时候可以不把整个事务回滚,只是回滚到某一个保存点,标记点
SAVEPOINT identifier: SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有很多个SAVEPOINT;
RELEASE SAVEPOINT identifier:删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
ROLLBACK TO identifier:把事务回滚到标记点;

例子:
BEGIN;
SAVEPOINT s1;第一个保存点s1
...执行操作1修改数据库
SAVEPOINT s2;第二个保存点s2
...执行操作2修改数据库
ROLLBACK TO s2;
此时s2之前的操作都在

COMMIT;《----s2之前的操作永久生效
ROLLBACK;<---所有操作失效

P85 订单生成——django中使用事务
项目中刚才的错误:创建订单失败,但是依旧往订单信息表中添加了一条记录,但是理论上是不应该添加的,则需要把对数据库的一系列操作放入到事务中。

在项目中如何使用事务《---参考资料:
django文档--》Model 模型层---》高级---》事务【Django中使用MySQL的事务】
如何把django语句中的操作放入到事务中---》atomic
from django.db import transaction

要把一个函数中的sql语句放入一个事务中,只需要用@transaction.atomic装饰器装饰你的函数,他就会将你函数中涉及到数据库的操作放入一个事务里面

class OrderCommitView1(View):
    """订单创建"""
    @transaction.atomic
    def post(self, request):
    ...

结合atomic来说一下保存点的使用,假如说,对数据库的操作分为两段,没有必要进行完全回滚,就可以在某个位置设置一个保存点,比如在TODO:创建订单核心业务往下为一整个流程
# 在django中设置保存点
django.db.transaction

savepoint<---创建一个新的保存点
savepoint_commit<---
savepoint_rollback


在 【# todo: 保存订单信息表: 向df_order_info表中添加一条记录】之前设置一个保存点
# 设置事务保存点
save_id = transaction.savepoint()

在商品信息不存在这里,事务进行回滚
transaction.savepoint_rollback(save_id)
return JsonResponse({'res': 4, 'errmsg': '商品信息不存在'})

在商品库存不足处,也要进行事务回滚

将整个涉及数据库操作放入try...except中,如果发生异常都需要回滚到该位置

# 设置事务保存点
save_id = transaction.savepoint()
try:
    ...
    order.save()
except Exception as e:
    transaction.savepoint_rollback(save_id)
        return JsonResponse({'res': 7, 'errmsg': '下单失败'})
# 没有问题的话则进行事务提交,从save_id到这里的所有数据库操作进行提交
transaction.savepoint_commit(save_id)
    

P89 订单生成——订单并发问题

订单并发的控制---见图

P90 订单并发——悲观锁

解决高并发问题的两个方案:
1.悲观锁
2.乐观锁


悲观锁对应进程锁的概念,进程操作一个公用的资源的时候,为了防止资源的一些问题,规定去拿锁,谁拿到锁,就可以修改该公用资源,拿不锁的则进行等待。只有锁被释放之后,其他进程才可以进行获取锁并修改资源。
在修改商品库存的时候,我认为别人也想改他,所以我们的规则是:在其中查询商品时,需要加一个锁,拿到【查询商品信息】这一行时,进行锁定,其余进程过来的时候也需要遵循该规则,在查询商品时,需要拿到一个锁,如果没有拿到锁则进行等待。
查询数据库信息并加锁:
select * from df_goods_sku where id=17 for update;
for update<---在获取数据的同时要拿到锁,拿到锁之后,代码才能继续往下执行。
当用户2也执行到这里时也需要去拿到锁,当拿不到锁的时候则进行等待。
此时用户1的代码继续往下走,当走完之后将库存进行更新。
执行完了 事务结束之后,锁会被释放
此时用户2的代码由阻塞状态进入运行状态
此时查询商品信息已经是用户1更新过库存之后的信息,此时库存不足,用户就不会下单。

在django中 获取商品的信息时
普通查询:
sku = GoodsSKU.objects.get(id=sku_id)
加入悲观锁查询:
# select * from df_goods_sku where id=17 for update;
sku = GoodsSKU.objects.select_for_update().get(id=sku_id)

测试:
在【从redis中获取用户所要购买的商品数量】之前
 print('user:%d  stock:%d' % (user.id, sku.stock))
# 打印完之后进行休眠
import time
time.sleep(10)


# 将鸡腿库存置位1
update df_goods_sku set stock=1 where id=17;

同时使用两个用户进行测试

通过悲观锁解决了并发的问题。
截图

P91 订单并发——乐观锁

乐观锁在查询数据的时候不加锁,在查询数据的时候不认为会有其他用户抢夺资源,不加锁,在更新的时候需要进行判断,判断更新时的库存和之前查出的库存是否一致
查询时库存为1
更新时,加and stock=1 进行库存校验
update df_gods_sku set stock=0, sales=1 where id=17 and stock=1;
如果更新成功说明没有人对库存做修改,如果更新失败则说明有人对库存进行了修改。
这就是乐观锁《----查找的时候不加锁,,更新的时候加一个判断。如果在更新的时候别人修改了库存,那么我的更新操作会失败

乐观锁在项目中的使用:
在django中 获取商品的信息时
使用普通查询:
sku = GoodsSKU.objects.get(id=sku_id)

在更新的时候:
# 修改商品表中的数据(更新库存和销量)
orgin_stock = sku.stock
new_stock = orgin_stock - int(count)
new_sales = sku.sales + int(count)

# 乐观锁
# update df_goods_sku set stock=new_stock, sales = new_sales where id=sku.id and stock=origin_stock
# res 返回的是一个数字,返回受影响的行数,你根据这个条件查出来之后进行更新,更新的几行会把最终结果进行返回
# 在这条代码中根据条件id=sku.id, stock=origin_stock进行查询,出来的要么是1要么是0
# 如果返回为0代表更新是失败的
res = GoodsSKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales)

# 如果更新失败,回滚到save_id保存点
if res == 0:
    transaction.savepoint_rollback(save_id)
    return JsonResponse({'res': 7, 'errmsg': ''下单失败2})


测试:
在乐观锁更新之前
# print('user:%d  time:%d stock:%d' % (user.id, i, sku.stock))
# import time
# time.sleep(10)

# 将鸡腿库存置位1
update df_goods_sku set stock=1 where id=17;

两个用户同时下单:一个下单成功,一个下单失败2

悲观锁:在查询的时候,先获取锁,再进行操作
乐观锁:在查询的时候不加锁,,在更新的时候要进行判断。默认没有其他用户,在更新的时候判断数据有没有被修改

乐观锁有种情况:
假设现在库存为3
update df_goods_sku set stock=3 where id=17;
两个用户同时下单,下单均应该成功。进行测试时,一个成功,一个失败。如何解决这一问题?

乐观锁:
我们认为别人没有跟我抢,更新数据的时候会进行判断,如果跟我之前取得的数据不一样,就说明已经有人更改了资源,此时更新失败。
但是更新失败并不意味着商品库存不足。这里不能直接进行ROLLBACK。
更新失败说明确实有人更改了库存,但是我们应该回过头来再尝试更新,将更新代码放在循环中,尝试三次 ,如果连着三次都未更新成功,则说明下单失败。
for sku_id in sku_ids:
    for i in range(3):
        try:
            ...
        except:
            ...
        ...
        
        # 打印出第几次
        print('user:%d  time:%d stock:%d' % (user.id, i, sku.stock))
        # import time
        # time.sleep(10)

        # todo:
        # update df_goods_sku set stock=new_stock, sales=new_sales
        # where id=sku_id and stock = orgin_stock
        # 返回受影响的行数res, 0为失败
        res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock,
                                                               sales=new_sales)  # 乐观锁
        if res == 0:  # 返回0表示更新失败
            if i == 2:  # 尝试到第三次还没成功,则默认下单失败
                    transaction.savepoint_rollback(save_id)
                    return JsonResponse({'res': 8, 'errmsg': '下单失败2'})
            continue

        # todo: 向订单商品表中添加信息
        OrderGoods.objects.create(order=order,
                      sku=sku,
                      count=count,
                      price=sku.price
        )

        # 将每个商品的数量和小计进行累加得到总的金额和件数
        total_price += sku.price * int(count)
        total_count += int(count)


        # 只要有一次下单成功,则跳出循环
        break


一般情况下,库存充足时,按照概率尝试三次是可以成功的
在这里进行测试的时候,第一个用户下单成功之后,第二个用户尝试了三次获取到的stock一直是3,而此时实际库存已经是2了
分析这种情况:
数据库的事务之间具有隔离性

隔离性分为:
读未提交:读取未提交的内容
假如说有两个事务,分别为事务A和事务B,在事务A中执行一条插入语句,执行完插入语句后事务还没有被提交,最终有可能被提交也有可能被回滚。
在这个隔离级别,不管事务A有没有被提交,事务B都有可能拿到insert中的内容,这就是读取未提交的内容。
这种情况下会出现脏读的情况。

因为事务A执行完插入语句之后并没有被提交,也就是事务B在事务A提交之前就可以拿到该数据,但是最终事务A有可能会将插入操作进行回滚,此时事务B拿到的数据实际上是不存在的,这就是脏读。

读已提交:读取提交的内容《-----大部分数据库系统默认的隔离级别,但是不是MySQL默认的!!!!!!
假如说有两个事务,分别为事务A和事务B,在事务A中执行一条插入语句,在事务A提交之前,事务B无法无法拿到这条插入的数据,只有事务A提交之后,事务B才能拿到该数据。

可重复读《-----这是MySQL的默认事务隔离级别
这个中出现的情况:
假如说有两个事务,分别为事务A和事务B,
就事务A而已,此时将商品库存数量改为2,此时事务B还是最开始拿到的库存数量3,事务A更新完后事务B也拿不到更新之后的数据,此为可重复读《---刚才出现的情况就是可重复读,这是MySQL默认事务之间的隔离级别。该隔离级别会出现幻读。

幻读:在一个事务的两次查询中数据笔数不一致,即数据的列数不一致。例如有一个事务查询了几列数据,而另一个事务在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。比如最开始事务B查询到的是3列数据,事务A往数据库中又插入了3列数据,事务B又去重新查库,此时发现是6列数据,这就是所谓的幻读。

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


因为MySQL的事务默认隔离级别是可重复读,此时,会获取不到另一个用户修改后的库存数量而一直是最开始的库存数量。因此,将MySQL的事务隔离级别修改为读已提交(读取提交内容)。

关于MySQL的事务隔离级别设置:
找到MySQL的日志文件:
sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
# * Basic Settings
user
pid-file
...
skip-external-locking
# 设置隔离级别为读取提交内容
transaction-isolation = READ-COMMITTE

设置完成后重启MySQL服务
sudo service mysql restart

此时事务的隔离级别为读已提交,这个时候,只要有用户更新库存,另一个用户一定能获取到更新后的库存

测试:
设置库存量为3
使用两个用户进行下单
至此,乐观锁解决并发完成

乐观锁:查询数据的时候不会加锁,但是在更新的时候要做判断,如果判断和之前的内容不一致,说明有人修改了数据,此时更新会失败。但是即使更新失败并不意味着商品库存是不足的,所以要多尝试几次,一般尝试三次之后还不成功的话订单就失败。如果库存充足,超过三次还不成功的概率微乎其微。
*** 一定要设置MySQL事务的隔离界别,否则即使下单三次还是会失败。
截图


P92 订单并发——总结

 

在冲突比较少的时候使用乐观锁,提高性能
1.在冲突比较少的时候使用乐观锁一次就能成功
2.使用乐观锁在查询数据的时候没有加锁,以及释放锁,减少了加锁释放锁的开销


在冲突比较多的时候使用悲观锁,直接进行锁定
当冲突比较多的时候乐观锁需要不停地尝试,尝试也是耗时的
乐观锁重复操作的代价比较大,或者操作时间比较长时,就可以使用悲观锁


悲观锁在事务操作数据的时候,对数据进行加锁。只要获取到锁之后别人在操作之前都按照这个规则,都需要先拿到锁,你拿到了,别人在操作的时候拿不到,就会进行等待。当你把锁释放之后,别人才能继续往下执行。

乐观锁的话,不存在上述情况,查询数据的时候不加锁,但是在进行更新或者其他操作的时候需要进行判断。即判断更新的时候所使用的数据有没有被改变。如果改变的话,更新就会失败。因为在更新的时候必须加条件id=17andstock=1,即必须和我原来查询到的数据是一致的。这里即使失败也不一定是库存不足或者是有错,所以需要多进行几次尝试。在这里在更新失败之后需要多进行几次尝试,一共做三次尝试

上述即为使用两个锁解决并发问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值