18.3 基于事务的订单数据保存与乐观锁

问题分析1

提交订单时,涉及到多个表的操作,任何一个环节出了问题,就会造成数据不一致的问题,有可能出现订单已经生成,但商品数量未减少的情况。

在Django中可采用事务来保证一组操作要么全部成功,要么全部撤回(回滚)。

问题分析2:

当某商品数量为15,用户A与用户B几乎同时尝试购买这一商品,用户A买8个,用户B买10个,他们查询到的数量都为15,看似都能买到,但他们两个用户的总数为18,超过了库存15。

针对这个问题,可采用加锁的方式来保护资源。mysql提供两种锁的方式,悲观锁和乐观锁。悲观锁是在查询某条记录时对数据加锁,防止他人修改数据,这种处理方式会影响系统的并发量。乐观锁是在更新数据时通过条件判断来确定是否可以更新,如果条件正常,则更新;否则,表示资料被抢占,不再进行更新。

本项目采用乐观锁的方式。

实现

OrderCommitView视图实现了事务及乐观锁后的代码如下:

class OrderCommitView(LoginRequiredJSONMixin, View):
    """提交订单"""

    def post(self, request):
        # 接收参数
        json_dict = json.loads(request.body.decode())
        address_id = json_dict.get('address_id')
        pay_method = json_dict.get('pay_method')

        if not all([address_id, pay_method]):
            return HttpResponseForbidden('缺少必要参数')

        try:
            address = Address.objects.get(id=address_id)
        except Address.DoesNotExist:
            return HttpResponseForbidden('参数address_id错误')

        if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'], OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
            return HttpResponseForbidden('参数pay_method错误')

        # 获取用户
        user = request.user
        # 拼接订单id
        order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)

        # 显式开启一个事务
        with (transaction.atomic()):
            try:
                # 创建事务保存点
                save_id = transaction.savepoint()

                order = OrderInfo.objects.create(
                    order_id=order_id,
                    user=user,
                    address=address,
                    total_count=0,
                    total_amount=Decimal('0.00'),
                    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中读取购物车中被勾选的商品  {b'1':b'1',b'2:b'2'}
                redis_conn = get_redis_connection('carts')
                redis_cart = redis_conn.hgetall('carts_%s' % user.id)
                # 从redis数据库中选出被勾选的商品sku_id  [b'1']
                selected = redis_conn.smembers('selected_%s' % user.id)

                carts = {}
                for sku_id in selected:
                    carts[int(sku_id)] = int(redis_cart.get(sku_id))

                # 通过sku模型类查询出在结算订单页面需要展示的商品sku
                sku_ids = carts.keys()
                # 遍历购物车中被勾选的商品
                for sku_id in sku_ids:

                    while True:  # ---------------
                        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):
                            # if (sku_count > sku.stock):
                            # 出错则回滚
                            transaction.savepoint_rollback(save_id)
                            return JsonResponse({'code': RETCODE.STOCKERR, 'errmsg': '库存不足'})

                        # 库存与销量更新
                        # sku.stock -= sku_count
                        # sku.sales += sku_count
                        # sku.save()

                        # 更新库存与销量
                        new_stock = origin_stock - sku_count
                        new_sales = origin_sales - sku_count
                        #
                        result = SKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
                        if result == 0:
                            continue

                        # 修改spu销量
                        sku.spu.sales += sku_count
                        sku.spu.save()

                        # 保存订单商品信息
                        OrderGoods.objects.create(
                            order=order,
                            sku=sku,
                            count=sku_count,
                            price=sku.price,
                        )

                        order.total_count += sku_count
                        order.total_amount += sku.price * sku_count

                        break  # ------------------
                # 添加邮费
                order.total_amount += order.freight
                order.save()
            except Exception as e:
                logger.error(e)
                transaction.savepoint_rollback(save_id)
                return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '下单失败'})
            # 显式一次性提交事务
            transaction.savepoint_commit(save_id)
        #         清除购物车中已结算商品
        pl = redis_conn.pipeline()
        pl.hdel('carts_%s' % user.id, *selected)
        pl.sadd('selected_%s' % user.id, *selected)

        pl.execute()

        return JsonResponse({'code': RETCODE.OK, 'errmsg': '下单成功', 'order_id': order.order_id})

修改mysql的配置

命令行下,mysql -u 用户名 -p 登录mysql数据库,然后执行下面的语句。

mysql> set global transaction isolation level READ COMMITTED;
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值