保存订单
1. 后端接口设计
请求方式 : POST /orders/
请求参数: JSON 或 表单
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
address | int | 是 | 收货地址id |
pay_method | int | 是 | 支付方式 |
返回数据: JSON
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
order_id | char | 是 | 订单编号 |
2. 后端实现
在orders/views.py中创建视图
class SaveOrderView(CreateAPIView):
"""
保存订单
"""
permission_classes = [IsAuthenticated]
serializer_class = SaveOrderSerializer
在orders/serializers.py中创建序列化器
class SaveOrderSerializer(serializers.ModelSerializer):
"""
下单数据序列化器
"""
class Meta:
model = OrderInfo
fields = ('order_id', 'address', 'pay_method')
read_only_fields = ('order_id',)
extra_kwargs = {
'address': {
'write_only': True,
'required': True,
},
'pay_method': {
'write_only': True,
'required': True
}
}
def create(self, validated_data):
"""保存订单"""
pass
保存订单的思路
def create(self, validated_data):
# 获取当前下单用户
# 生成订单编号
# 保存订单基本信息数据 OrderInfo
# 从redis中获取购物车结算商品数据
# 遍历结算商品:
# 判断商品库存是否充足
# 减少商品库存,增加商品销量
# 保存订单商品数据
# 在redis购物车中删除已计算商品数据
数据库事务
在保存订单数据中,涉及到多张表(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)
保存订单数据create方法实现
def create(self, validated_data):
"""
保存订单
"""
# 获取当前下单用户
user = self.context['request'].user
# 组织订单编号 20170903153611+user.id
# timezone.now() -> datetime
order_id = timezone.now().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)
address = validated_data['address']
pay_method = validated_data['pay_method']
# 生成订单
with transaction.atomic():
# 创建一个保存点
save_id = transaction.savepoint()
try:
# 创建订单信息
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal(0),
freight=Decimal(10),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID']
)
# 获取购物车信息
redis_conn = get_redis_connection("cart")
redis_cart = redis_conn.hgetall("cart_%s" % user.id)
cart_selected = redis_conn.smembers('cart_selected_%s' % user.id)
# 将bytes类型转换为int类型
cart = {}
for sku_id in cart_selected:
cart[int(sku_id)] = int(redis_cart[sku_id])
# 一次查询出所有商品数据
skus = SKU.objects.filter(id__in=cart.keys())
# 处理订单商品
for sku in skus:
sku_count = cart[sku.id]
# 判断库存
origin_stock = sku.stock # 原始库存
origin_sales = sku.sales # 原始销量
if sku_count > origin_stock:
transaction.savepoint_rollback(save_id)
raise serializers.ValidationError('商品库存不足')
# 用于演示并发下单
# import time
# time.sleep(5)
# 减少库存
new_stock = origin_stock - sku_count
new_sales = origin_sales + sku_count
sku.stock = new_stock
sku.sales = new_sales
sku.save()
# 累计商品的SPU 销量信息
sku.goods.sales += sku_count
sku.goods.save()
# 累计订单基本信息的数据
order.total_count += sku_count # 累计总金额
order.total_amount += (sku.price * sku_count) # 累计总额
# 保存订单商品
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)
# 更新订单的金额数量信息
order.total_amount += order.freight
order.save()
except ValidationError:
raise
except Exception as e:
logger.error(e)
transaction.savepoint_rollback(save_id)
raise
# 更新redis中保存的购物车数据
pl = redis_conn.pipeline()
pl.hdel('cart_%s' % user.id, *cart_selected)
pl.srem('cart_selected_%s' % user.id, *cart_selected)
pl.execute()
return order
3. 并发处理
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
解决办法:
悲观锁
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法
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,一个订单一个订单的处理。
4. 使用乐观锁改写下单逻辑
def create(self, validated_data):
"""
保存订单
"""
# 获取当前下单用户
user = self.context['request'].user
# 组织订单编号 20170903153611+user.id
# timezone.now() -> datetime
order_id = timezone.now().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)
address = validated_data['address']
pay_method = validated_data['pay_method']
# 生成订单
with transaction.atomic():
# 创建一个保存点
save_id = transaction.savepoint()
try:
# 创建订单信息
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal(0),
freight=Decimal(10),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID']
)
# 获取购物车信息
redis_conn = get_redis_connection("cart")
redis_cart = redis_conn.hgetall("cart_%s" % user.id)
cart_selected = redis_conn.smembers('cart_selected_%s' % user.id)
# 将bytes类型转换为int类型
cart = {}
for sku_id in cart_selected:
cart[int(sku_id)] = int(redis_cart[sku_id])
# # 一次查询出所有商品数据
# skus = SKU.objects.filter(id__in=cart.keys())
# 处理订单商品
sku_id_list = cart.keys()
for sku_id in sku_id_list:
while True:
sku = SKU.objects.get(id=sku_id)
sku_count = cart[sku.id]
# 判断库存
origin_stock = sku.stock # 原始库存
origin_sales = sku.sales # 原始销量
if sku_count > origin_stock:
transaction.savepoint_rollback(save_id)
raise serializers.ValidationError('商品库存不足')
# 用于演示并发下单
# import time
# time.sleep(5)
# 减少库存
# sku.stock -= sku_count
# sku.sales += sku_count
# sku.save()
new_stock = origin_stock - sku_count
new_sales = origin_sales + sku_count
# 根据原始库存条件更新,返回更新的条目数,乐观锁
ret = SKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
if ret == 0:
continue
# 累计商品的SPU 销量信息
sku.goods.sales += sku_count
sku.goods.save()
# 累计订单基本信息的数据
order.total_count += sku_count # 累计总金额
order.total_amount += (sku.price * sku_count) # 累计总额
# 保存订单商品
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)
# 更新成功
break
# 更新订单的金额数量信息
order.total_amount += order.freight
order.save()
except serializers.ValidationError:
raise
except Exception as e:
logger.error(e)
transaction.savepoint_rollback(save_id)
raise
# 更新redis中保存的购物车数据
pl = redis_conn.pipeline()
pl.hdel('cart_%s' % user.id, *cart_selected)
pl.srem('cart_selected_%s' % user.id, *cart_selected)
pl.execute()
return order
5. 需要修改MySQL的事务隔离级别
事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。
MySQL数据库事务隔离级别主要有四种:
- Serializable 串行化,一个事务一个事务的执行
- Repeatable read 可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响
- Read committed 读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值
- Read uncommitted 读取为提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。
MySQL数据库默认使用可重复读( Repeatable read),而使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交Read committed。
修改方法:
6. 前端实现
修改place_order.js文件
// 提交订单
on_order_submit: function(){
if (this.order_submitting == false){
this.order_submitting = true;
axios.post(this.host+'/orders/', {
address: this.nowsite,
pay_method: this.pay_method
}, {
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
location.href = '/order_success.html?order_id='+response.data.order_id
+'&amount='+this.payment_amount
+'&pay='+this.pay_method;
})
.catch(error => {
this.order_submitting = false;
alert(error.response.data[0]);
})
}
}