django订单部分--数据库事务
Posted cl-python
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了django订单部分--数据库事务相关的知识,希望对你有一定的参考价值。
订单数据库设计
订单号不再采用数据库自增主键,而是由后端生成创建。
终端命令python manage.py startapp orders 创建订单应用orders,编辑模型类models.py
from django.db import models from meiduo_mall.utils.models import BaseModel from users.models import User, Address from goods.models import SKU # Create your models here. class OrderInfo(BaseModel): """ 订单信息 """ PAY_METHODS_ENUM = { "CASH": 1, "ALIPAY": 2 } PAY_METHOD_CHOICES = ( (1, "货到付款"), (2, "支付宝"), ) ORDER_STATUS_ENUM = { "UNPAID": 1, "UNSEND": 2, "UNRECEIVED": 3, "UNCOMMENT": 4, "FINISHED": 5 } ORDER_STATUS_CHOICES = ( (1, "待支付"), (2, "待发货"), (3, "待收货"), (4, "待评价"), (5, "已完成"), (6, "已取消"), ) order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号") user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户") address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收获地址") total_count = models.IntegerField(default=1, verbose_name="商品总数") total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额") freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费") pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式") status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态") class Meta: db_table = "tb_order_info" verbose_name = ‘订单基本信息‘ verbose_name_plural = verbose_name class OrderGoods(BaseModel): """ 订单商品 """ SCORE_CHOICES = ( (0, ‘0分‘), (1, ‘20分‘), (2, ‘40分‘), (3, ‘60分‘), (4, ‘80分‘), (5, ‘100分‘), ) order = models.ForeignKey(OrderInfo, related_name=‘skus‘, on_delete=models.CASCADE, verbose_name="订单") sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品") count = models.IntegerField(default=1, verbose_name="数量") # max_digits :最大的位数。decimal_places :小数点后面保留多少位 price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价") comment = models.TextField(default="", verbose_name="评价信息") score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name=‘满意度评分‘) is_anonymous = models.BooleanField(default=False, verbose_name=‘是否匿名评价‘) is_commented = models.BooleanField(default=False, verbose_name=‘是否评价了‘) class Meta: db_table = "tb_order_goods" verbose_name = ‘订单商品‘ verbose_name_plural = verbose_name
settings.py配置文件(红色斜体):
INSTALLED_APPS = [ ‘django.contrib.admin‘, ‘django.contrib.auth‘, ‘django.contrib.contenttypes‘, ‘django.contrib.sessions‘, ‘django.contrib.messages‘, ‘django.contrib.staticfiles‘, ‘rest_framework‘, ‘corsheaders‘, ‘ckeditor‘, # 富文本编辑器 ‘ckeditor_uploader‘, # 富文本编辑器上传图片模块 ‘django_crontab‘, # 定时任务 ‘haystack‘,#对接Elasticsearch ‘users.apps.UsersConfig‘,#注册用户模块应用 ‘verifications.apps.VerificationsConfig‘,#验证模块 ‘oauth.apps.OauthConfig‘,#第三方登录 ‘areas.apps.AreasConfig‘,#省市区数据 ‘contents.apps.ContentsConfig‘,#主页广告内容 ‘goods.apps.GoodsConfig‘,#商品信息 ‘orders.apps.OrdersConfig‘, # 订单 ]
生成迁移文件并执行迁移:
python manage.py makemigrations
python manage.py migrate
订单结算
订单结算页面所需的数据从购物车中勾选而来。
在orders/serialziers.py中创建序列化器
class CartSKUSerializer(serializers.ModelSerializer): """ 购物车商品数据序列化器 """ count = serializers.IntegerField(label=‘数量‘) class Meta: model = SKU fields = (‘id‘, ‘name‘, ‘default_image_url‘, ‘price‘, ‘count‘) class OrderSettlementSerializer(serializers.Serializer): """ 订单结算数据序列化器 """ freight = serializers.DecimalField(label=‘运费‘, max_digits=10, decimal_places=2) skus = CartSKUSerializer(many=True)
在orders/views.py中编写视图:
from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from django_redis import get_redis_connection from rest_framework.response import Response from decimal import Decimal from goods.models import SKU from . import serializers class OrderSettlementView(APIView): """ 订单结算 """ permission_classes = [IsAuthenticated] def get(self, request): """ 获取 """ user = request.user # 从购物车中获取用户勾选要结算的商品信息 redis_conn = get_redis_connection(‘carts‘) # 存放的是redis中所有的购物车数据(包含了勾选的和未勾选的) redis_cart = redis_conn.hgetall(‘cart_%s‘ % user.id) # 读取出购物车中被勾选的商品的sku_id cart_selected = redis_conn.smembers(‘selected_%s‘ % user.id) 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] # 运费 # float 1.23 ==> 123 * 10 ^ -2 ==> 1.2299999999... # Decimal 1.23 1(整数部分) 23(小数部分) ==> 1.23 freight = Decimal(‘10.00‘) serializer = serializers.OrderSettlementSerializer({‘freight‘: freight, ‘skus‘: skus}) return Response(serializer.data)
配置主路由(红色加粗斜体):
urlpatterns = [ url(r‘^admin/‘, admin.site.urls), #验证码 url(r‘^‘,include(‘verifications.urls‘)), #用户 url(r‘^‘,include(‘users.urls‘)), #QQ登录 url(r‘^oauth/‘,include(‘oauth.urls‘)), # 省市区 url(r‘^‘, include(‘areas.urls‘)), # CKEditor url(r‘^ckeditor/‘, include(‘ckeditor_uploader.urls‘)), # 商品 url(r‘^‘, include(‘goods.urls‘)), # 购物车 url(r‘^‘, include(‘carts.urls‘)), # 订单 url(r‘^‘, include(‘orders.urls‘)), ]
orders/urls.py中添加路由:
from django.conf.urls import url from . import views urlpatterns = [ # 确认订单 url(r‘^orders/settlement/$‘, views.OrderSettlementView.as_view()), ]
前端:修改place_order.html,增加Vue变量,新建place_order.js
保存订单
在orders/views.py中创建视图:
class CommitOrderView(CreateAPIView): """提交订单""" # 登录用户才能访问 permission_classes = [IsAuthenticated] # 指定序列化器 serializer_class = serializers.CommitOrderSerializer
在orders/serializers.py中创建序列化器:
class CommitOrderSerializer(serializers.ModelSerializer): """提交订单""" class Meta: model = OrderInfo # order_id :输出;address 和 pay_method : 输入 fields = (‘order_id‘, ‘address‘, ‘pay_method‘) read_only_fields = (‘order_id‘,) # 指定address 和 pay_method 为输出 extra_kwargs = { ‘address‘: { ‘write_only‘: True, }, ‘pay_method‘: { ‘write_only‘: 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) # 提交从保存点到当前状态的所有数据库事务操作 transaction.savepoint_commit(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 # 提交事务 transaction.savepoint_commit(save_id) # 更新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
并发处理
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
解决办法:
悲观锁
-
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法
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,一个订单一个订单的处理。
使用乐观锁改写下单逻辑:
def create(self, validated_data): """创建订单记录:保存OrderInfo和OrderGoods信息""" # 获取当前保存订单时需要的信息 # 获取当前的登录用户 user = self.context[‘request‘].user # 生成订单编号 # order_id = ‘时间‘+‘user_id‘ # timezone.now() == datetime类型的对象 # 20180706085001+000000001 order_id = timezone.now().strftime(‘%Y%m%d%H%M%S‘) + (‘%09d‘ % user.id) # 获取地址和支付方式 address = validated_data.get(‘address‘) pay_method = validated_data.get(‘pay_method‘) # 明显的开启事务 with transaction.atomic(): # 在安全的地方,创建保存点,将来操作数据库失败回滚到此 save_id = transaction.savepoint() try: # 保存订单基本信息 OrderInfo order = OrderInfo.objects.create( order_id=order_id, user = user, address = address, total_count = 0, total_amount = 0, freight = Decimal(‘10.00‘), pay_method = pay_method, # 如果用户传入的是"支付宝支付",那么下了订单后,订单的状态要是"待支付" # 如果用户传入的是"货到付款",那么下了订单后,订单的状态要是"代发货" status = OrderInfo.ORDER_STATUS_ENUM[‘UNPAID‘] if pay_method==OrderInfo.PAY_METHODS_ENUM[‘ALIPAY‘] else OrderInfo.ORDER_STATUS_ENUM[‘UNSEND‘] ) # 从redis读取购物车中被勾选的商品信息 redis_conn = get_redis_connection(‘carts‘) # 读取出所有的购物车数据 # redis_cart = {b‘1‘:b‘10‘, b‘2‘:b‘20‘, b‘3‘:b‘30‘} redis_cart = redis_conn.hgetall(‘cart_%s‘ % user.id) # cart_selected = [b‘1‘, b‘2‘] cart_selected = redis_conn.smembers(‘selected_%s‘ % user.id) # 定义将来要支付的商品信息的字典 # carts = {1:10, 2:20} carts = {} for sku_id in cart_selected: carts[int(sku_id)] = int(redis_cart[sku_id]) # 读取出所有要支付的商品的sku_id # sku_ids = [1,2] sku_ids = carts.keys() # 遍历购物车中被勾选的商品信息 for sku_id in sku_ids: # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 # 如果下单库存被更改看,但是你的sku_count依然在被更改后的库存范围内,继续下单 # 直到库存真的不满足条件时才下单失败 while True: # 获取sku对象 sku = SKU.objects.get(id=sku_id) # 获取原始的库存和销量 origin_stock = sku.stock origin_sales = sku.sales sku_count = carts[sku_id] # 判断库存? if sku_count > origin_stock: # 回滚 transaction.savepoint_rollback(save_id) raise serializers.ValidationError(‘库存不足‘) # 模拟网络延迟 import time time.sleep(5) # 减少库存,增加销量 SKU? # sku.stock -= sku_count # origin_stock = sku.stock # new_stock = origin_stock - sku_count # sku.sales += sku_count # sku.save() # 读取要更新的库存和销量 new_stock = origin_stock - sku_count new_sales = origin_sales + sku_count # 使用乐观锁更新库存:在调用update()去更新库存之前,使用filter()拿着原始的库存去查询记录是否存在 # 如果记录不存在的,在调用update()时返回0 result = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if 0 == result: # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 # 如果下单库存被更改看,但是你的sku_count依然在被更改后的库存范围内,继续下单 # 直到库存真的不满足条件时才下单失败 continue # 修改SPU销量 sku.goods.sales += sku_count sku.goods.save() # 保存订单商品信息 OrderGoods OrderGoods.objects.create( order=order, sku = sku, count = sku_count, price = sku.price, ) # 累加计算总数量和总价 order.total_count += sku_count order.total_amount += (sku_count * sku.price) # 下单成功要跳出死循环 break # 最后加入邮费和保存订单信息 order.total_amount += order.freight order.save() except serializers.ValidationError: # 这里不会滚的原因,是因为前面已经有了回滚的动作 raise except Exception: transaction.savepoint_rollback(save_id) raise # 自动的将捕获的异常抛出,不需要给异常起别名 # 没有问题,需要明显的提交 transaction.savepoint_commit(save_id) # 清除购物车中已结算的商品 pl = redis_conn.pipeline() pl.hdel(‘cart_%s‘ % user.id, *sku_ids) pl.srem(‘selected_%s‘ % user.id, *sku_ids) pl.execute() # 响应结果 return order
需要修改mysql的事务隔离级别
事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。
MySQL数据库事务隔离级别主要有四种:
- Serializable 串行化,一个事务一个事务的执行
- Repeatable read 可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响
- Read committed 读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值
- Read uncommitted 读取为提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。
MySQL数据库默认使用可重复读( Repeatable read),而使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交Read committed。
修改方法:
在orders/urls.py文件中添加路由(红色斜体):
urlpatterns = [ # 确认订单 url(r‘^orders/settlement/$‘, views.OrderSettlementView.as_view()), # 提交订单 url(r‘^orders/$‘, views.CommitOrderView.as_view()), ]
以上是关于django订单部分--数据库事务的主要内容,如果未能解决你的问题,请参考以下文章