Django-数据库事务
在保存订单数据中,涉及到多张表(OrderInfo、OrderGoods、SKU)的数据修改,对这些数据的修改应该是一个整体事务,即要么一起成功,要么一起失败。
Django中对于数据库的事务,默认每执行一句数据库操作,便会自动提交。我们需要在保存订单中自己控制数据库事务的执行流程。
在Django中可以通过django.db.transaction
模块提供的atomic
来定义一个事务,atomic
提供两种用法
装饰器用法:可以装饰在视图函数上
from django.db import transaction @transaction.atomic def viewfunc(request): # 这些代码会在一个事务中执行 ...
with语句用法
from django.db import transaction def viewfunc(request): # 这部分代码不在事务中,会被Django自动提交 ... with transaction.atomic(): # 这部分代码会在事务中执行 ...
在Django中,还提供了保存点的支持,可以在事务中创建保存点来记录数据的特定状态,数据库出现错误时,可以恢复到数据保存点的状态
from django.db import transaction # 创建保存点 save_id = transaction.savepoint() # 回滚到保存点 transaction.savepoint_rollback(save_id) # 提交从保存点到当前状态的所有数据库事务操作 transaction.savepoint_commit(save_id)
并发处理
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
解决办法:
悲观锁
-
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法
select stock from tb_sku where id=1 for update; SKU.objects.select_for_update().get(id=1)
悲观锁类似于我们在多线程资源竞争时添加的互斥锁,容易出现死锁现象,采用不多。
乐观锁
乐观锁并不是真实存在的锁,而是在更新的时候判断此时的库存是否是之前查询出的库存,如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新。类似如下操作
update tb_sku set stock=2 where id=1 and stock=7; SKU.objects.filter(id=1, stock=7).update(stock=2)
任务队列
例如秒杀功能,将下单的逻辑放到任务队列中(如celery),将并行转为串行,所有人排队下单。比如开启只有一个进程的Celery,一个订单一个订单的处理。
使用乐观锁改写下单逻辑:
1 def create(self, validated_data): 2 """创建订单记录:保存OrderInfo和OrderGoods信息""" 3 4 # 获取当前保存订单时需要的信息 5 # 获取当前的登录用户 6 user = self.context[‘request‘].user 7 # 生成订单编号 8 # order_id = ‘时间‘+‘user_id‘ 9 # timezone.now() == datetime类型的对象 10 # 20180706085001+000000001 11 order_id = timezone.now().strftime(‘%Y%m%d%H%M%S‘) + (‘%09d‘ % user.id) 12 # 获取地址和支付方式 13 address = validated_data.get(‘address‘) 14 pay_method = validated_data.get(‘pay_method‘) 15 16 # 明显的开启事务 17 with transaction.atomic(): 18 # 在安全的地方,创建保存点,将来操作数据库失败回滚到此 19 save_id = transaction.savepoint() 20 21 try: 22 # 保存订单基本信息 OrderInfo 23 order = OrderInfo.objects.create( 24 order_id=order_id, 25 user = user, 26 address = address, 27 total_count = 0, 28 total_amount = 0, 29 freight = Decimal(‘10.00‘), 30 pay_method = pay_method, 31 # 如果用户传入的是"支付宝支付",那么下了订单后,订单的状态要是"待支付" 32 # 如果用户传入的是"货到付款",那么下了订单后,订单的状态要是"代发货" 33 status = OrderInfo.ORDER_STATUS_ENUM[‘UNPAID‘] if pay_method==OrderInfo.PAY_METHODS_ENUM[‘ALIPAY‘] else OrderInfo.ORDER_STATUS_ENUM[‘UNSEND‘] 34 ) 35 36 # 从redis读取购物车中被勾选的商品信息 37 redis_conn = get_redis_connection(‘carts‘) 38 # 读取出所有的购物车数据 39 # redis_cart = {b‘1‘:b‘10‘, b‘2‘:b‘20‘, b‘3‘:b‘30‘} 40 redis_cart = redis_conn.hgetall(‘cart_%s‘ % user.id) 41 # cart_selected = [b‘1‘, b‘2‘] 42 cart_selected = redis_conn.smembers(‘selected_%s‘ % user.id) 43 # 定义将来要支付的商品信息的字典 44 # carts = {1:10, 2:20} 45 carts = {} 46 for sku_id in cart_selected: 47 carts[int(sku_id)] = int(redis_cart[sku_id]) 48 49 # 读取出所有要支付的商品的sku_id 50 # sku_ids = [1,2] 51 sku_ids = carts.keys() 52 # 遍历购物车中被勾选的商品信息 53 for sku_id in sku_ids: 54 55 # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 56 # 如果下单库存被更改看,但是你的sku_count依然在被更改后的库存范围内,继续下单 57 # 直到库存真的不满足条件时才下单失败 58 while True: 59 60 # 获取sku对象 61 sku = SKU.objects.get(id=sku_id) 62 63 # 获取原始的库存和销量 64 origin_stock = sku.stock 65 origin_sales = sku.sales 66 67 sku_count = carts[sku_id] 68 # 判断库存? 69 if sku_count > origin_stock: 70 # 回滚 71 transaction.savepoint_rollback(save_id) 72 raise serializers.ValidationError(‘库存不足‘) 73 74 # 模拟网络延迟 75 import time 76 time.sleep(5) 77 78 # 减少库存,增加销量 SKU? 79 # sku.stock -= sku_count 80 # origin_stock = sku.stock 81 # new_stock = origin_stock - sku_count 82 83 # sku.sales += sku_count 84 # sku.save() 85 86 # 读取要更新的库存和销量 87 new_stock = origin_stock - sku_count 88 new_sales = origin_sales + sku_count 89 90 # 使用乐观锁更新库存:在调用update()去更新库存之前,使用filter()拿着原始的库存去查询记录是否存在 91 # 如果记录不存在的,在调用update()时返回0 92 result = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) 93 if 0 == result: 94 # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 95 # 如果下单库存被更改看,但是你的sku_count依然在被更改后的库存范围内,继续下单 96 # 直到库存真的不满足条件时才下单失败 97 continue 98 99 # 修改SPU销量 100 sku.goods.sales += sku_count 101 sku.goods.save() 102 103 # 保存订单商品信息 OrderGoods 104 OrderGoods.objects.create( 105 order=order, 106 sku = sku, 107 count = sku_count, 108 price = sku.price, 109 ) 110 111 # 累加计算总数量和总价 112 order.total_count += sku_count 113 order.total_amount += (sku_count * sku.price) 114 115 # 下单成功要跳出死循环 116 break 117 118 # 最后加入邮费和保存订单信息 119 order.total_amount += order.freight 120 order.save() 121 except serializers.ValidationError: 122 # 这里不会滚的原因,是因为前面已经有了回滚的动作 123 raise 124 except Exception: 125 transaction.savepoint_rollback(save_id) 126 raise # 自动的将捕获的异常抛出,不需要给异常起别名 127 128 # 没有问题,需要明显的提交 129 transaction.savepoint_commit(save_id) 130 131 # 清除购物车中已结算的商品 132 pl = redis_conn.pipeline() 133 pl.hdel(‘cart_%s‘ % user.id, *sku_ids) 134 pl.srem(‘selected_%s‘ % user.id, *sku_ids) 135 pl.execute() 136 137 # 响应结果 138 return order
参考链接:
http://www.mamicode.com/info-detail-2383484.html