基于Django的乐观锁与悲观锁解决订单并发问题详解


Posted in Python onJuly 31, 2019

前言

订单并发这个问题我想大家都是有一定认识的,这里我说一下我的一些浅见,我会尽可能的让大家了解如何解决这类问题。

在解释如何解决订单并发问题之前,需要先了解一下什么是数据库的事务。(我用的是mysql数据库,这里以mysql为例)

1)     事务概念

一组mysql语句,要么执行,要么全不不执行。

 2)  mysql事务隔离级别

Read Committed(读取提交内容)

如果是Django2.0以下的版本,需要去修改到这个隔离级别,不然乐观锁操作时无法读取已经被修改的数据

RepeatableRead(可重读)

这是这是Mysql默认的隔离级别,可以到mysql的配置文件中去修改;

transcation-isolation = READ-COMMITTED

在mysql配置文件中添加这行然后重启mysql就可以将事务隔离级别修改至Read Committed

其他事务知识这里不会用到就不浪费时间去做介绍了。

悲观锁:开启事务,然后给mysql的查询语句最后加上for update。

这是在干什么呢。可能大家有些不理解,其实就是给资源加上和多线程中加互斥锁一样的东西,确保在一个事务结束之前,别的事务无法对该数据进行操作。

下面是悲观锁的代码,加锁和解锁都是需要消耗CPU资源的,所以在订单并发少的情况使用乐观锁会是一个更好的选择。

class OrderCommitView(View):
  """悲观锁"""
  # 开启事务装饰器
  @transaction.atomic
  def post(self,request):
    """订单并发 ———— 悲观锁"""
    # 拿到商品id
    goods_ids = request.POST.getlist('goods_ids')
 
    # 校验参数
    if len(goods_ids) == 0 :
      return JsonResponse({'res':0,'errmsg':'数据不完整'}) 
    # 当前时间字符串
    now_str = datetime.now().strftime('%Y%m%d%H%M%S') 
    # 订单编号
    order_id = now_str + str(request.user.id)
    # 地址
    pay_method = request.POST.get('pay_method')
    # 支付方式
    address_id = request.POST.get('address_id')
    try:
      address = Address.objects.get(id=address_id)
    except Address.DoesNotExist:
      return JsonResponse({'res':1,'errmsg':'地址错误'}) 
    # 商品数量
    total_count = 0
    # 商品总价
    total_amount = 0 
     # 获取redis连接
    conn = get_redis_connection('default')
    # 拼接key
    cart_key = 'cart_%d' % request.user.id  
    #
    # 创建保存点
    sid = transaction.savepoint() 
    order_info = OrderInfo.objects.create(
      order_id = order_id,
      user = request.user,
      addr = address,
      pay_method = pay_method,
      total_count = total_count,
      total_price = total_amount
    ) 
    for goods_id in goods_ids:
      # 尝试查询商品
      # 此处考虑订单并发问题,
      try:
        # goods = Goods.objects.get(id=goods_id) # 不加锁查询
        goods = Goods.objects.select_for_update().get(id=goods_id) # 加互斥锁查询
      except Goodsgoods.DoesNotExist:
        # 回滚到保存点
        transaction.rollback(sid)
        return JsonResponse({'res':2,'errmsg':'商品信息错误'})
      # 取出商品数量
      count = conn.hget(cart_key,goods_id)
      if count is None:
        # 回滚到保存点
        transaction.rollback(sid)
        return JsonResponse({'res':3,'errmsg':'商品不在购物车中'}) 
      count = int(count) 
      if goods.stock < count:
        # 回滚到保存点
        transaction.rollback(sid)
        return JsonResponse({'res':4,'errmsg':'库存不足'}) 
      # 商品销量增加
      goods.sales += count
      # 商品库存减少
      goods.stock -= count
      # 保存到数据库
      goods.save() 
      OrderGoods.objects.create(
        order = order_info,
        goods = goods,
        count = count,
        price = goods.price
      ) 
      # 累加商品件数
      total_count += count
      # 累加商品总价
      total_amount += (goods.price) * count 
    # 更新订单信息中的商品总件数
    order_info.total_count = total_count
    # 更新订单信息中的总价格
    order_info.total_price = total_amount + order_info.transit_price
    order_info.save()
 
    # 事务提交
    transaction.commit() 
    return JsonResponse({'res':5,'errmsg':'订单创建成功'})

然后就是乐观锁查询了,相比悲观锁,乐观锁其实并不能称为是锁,那么它是在做什么事情呢。

其实是在你要进行数据库操作时先去查询一次数据库中商品的库存,然后在你要更新数据库中商品库存时,将你一开始查询到的库存数量和商品的ID一起作为更新的条件,当受影响行数返回为0时,说明没有修改成功,那么就是说别的进程修改了该数据,那么你就可以回滚到之前没有进行数据库操作的时候,重新查询,重复之前的操作一定次数,如果超过你设置的次数还是不能修改那么就直接返回错误结果。

该方法只适用于订单并发较少的情况,如果失败次数过多,会带给用户不良体验,同时适用该方法要注意数据库的隔离级别一定要设置为Read Committed 。

最好在使用乐观锁之前查看一下数据库的隔离级别,mysql中查看事物隔离级别的命令为

select @@global.tx_isolation;

class OrderCommitView(View):
  """乐观锁"""
  # 开启事务装饰器
  @transaction.atomic
  def post(self,request):
    """订单并发 ———— 乐观锁"""
    # 拿到id
    goods_ids = request.POST.get('goods_ids')
    
    if len(goods_ids) == 0 :
      return JsonResponse({'res':0,'errmsg':'数据不完整'})
    # 当前时间字符串
    now_str = datetime.now().strftime('%Y%m%d%H%M%S')
    # 订单编号
    order_id = now_str + str(request.user.id)
    # 地址
    pay_method = request.POST.get('pay_method')
    # 支付方式
    address_id = request.POST.get('address_id')
    try:
      address = Address.objects.get(id=address_id)
    except Address.DoesNotExist:
      return JsonResponse({'res':1,'errmsg':'地址错误'}) 
    # 商品数量
    total_count = 0
    # 商品总价
    total_amount = 0
    # 订单运费
    transit_price = 10 
    # 创建保存点
    sid = transaction.savepoint() 
    order_info = OrderInfo.objects.create(
      order_id = order_id,
      user = request.user,
      addr = address,
      pay_method = pay_method,
      total_count = total_count,
      total_price = total_amount,
      transit_price = transit_price
    )
    # 获取redis连接
    goods = get_redis_goodsection('default')
    # 拼接key
    cart_key = 'cart_%d' % request.user.id
 
    for goods_id in goods_ids:
      # 尝试查询商品
      # 此处考虑订单并发问题, 
      # redis中取出商品数量
      count = goods.hget(cart_key, goods_id)
      if count is None:
        # 回滚到保存点
        transaction.savepoint_rollback(sid)
        return JsonResponse({'res': 3, 'errmsg': '商品不在购物车中'})
      count = int(count) 
      for i in range(3):
        # 若存在订单并发则尝试下单三次
        try:
 
          goods = Goodsgoods.objects.get(id=goods_id) # 不加锁查询
          # goods = Goodsgoods.objects.select_for_update().get(id=goods_id) # 加互斥锁查询
        except Goodsgoods.DoesNotExist:
          # 回滚到保存点
          transaction.savepoint_rollback(sid)
          return JsonResponse({'res':2,'errmsg':'商品信息错误'}) 
        origin_stock = goods.stock
        print(origin_stock, 'stock')
        print(goods.id, 'id') 
        if origin_stock < count: 
          # 回滚到保存点
          transaction.savepoint_rollback(sid)
          return JsonResponse({'res':4,'errmsg':'库存不足'}) 
 
        # # 商品销量增加
        # goods.sales += count
        # # 商品库存减少
        # goods.stock -= count
        # # 保存到数据库
        # goods.save() 
        # 如果下单成功后的库存
        new_stock = goods.stock - count
        new_sales = goods.sales + count
        res = Goodsgoods.objects.filter(stock=origin_stock,id=goods_id).update(stock=new_stock,sales=new_sales)
        print(res)
        if res == 0:
          if i == 2:
            # 回滚
            transaction.savepoint_rollback(sid)
            return JsonResponse({'res':5,'errmsg':'下单失败'})
          continue
        else:
          break 
      OrderGoods.objects.create(
        order = order_info,
        goods = goods,
        count = count,
        price = goods.price
      ) 
      # 删除购物车中记录
      goods.hdel(cart_key,goods_id)
      # 累加商品件数
      total_count += count
      # 累加商品总价
      total_amount += (goods.price) * count 
    # 更新订单信息中的商品总件数
    order_info.total_count = total_count
    # 更新订单信息中的总价格
    order_info.total_price = total_amount + order_info.transit_price
    order_info.save() 
    # 事务提交
    transaction.savepoint_commit(sid) 
    return JsonResponse({'res':6,'errmsg':'订单创建成功'})

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python中的条件判断语句与循环语句用法小结
Mar 21 Python
Python运算符重载详解及实例代码
Mar 07 Python
Python中使用支持向量机SVM实践
Dec 27 Python
python机器学习理论与实战(五)支持向量机
Jan 19 Python
Python实现识别手写数字 简易图片存储管理系统
Jan 29 Python
对numpy数据写入文件的方法讲解
Jul 09 Python
python实现简单的单变量线性回归方法
Nov 08 Python
使用Python进行中文繁简转换的实现代码
Oct 18 Python
pyinstaller打包单文件时--uac-admin选项不起作用怎么办
Apr 15 Python
python和php哪个容易学
Jun 19 Python
如何通过安装HomeBrew来安装Python3
Dec 23 Python
详解python网络进程
Jun 15 Python
django解决订单并发问题【推荐】
Jul 31 #Python
python opencv将图片转为灰度图的方法示例
Jul 31 #Python
Django中使用极验Geetest滑动验证码过程解析
Jul 31 #Python
Python对接六大主流数据库(只需三步)
Jul 31 #Python
Python爬虫 scrapy框架爬取某招聘网存入mongodb解析
Jul 31 #Python
python爬虫 模拟登录人人网过程解析
Jul 31 #Python
Python爬虫 bilibili视频弹幕提取过程详解
Jul 31 #Python
You might like
php array的学习笔记
2012/05/10 PHP
PHP实现下载功能的代码
2012/09/29 PHP
php实现基于PDO的预处理示例
2017/03/28 PHP
cssQuery()的下载与使用方法
2007/01/12 Javascript
Javascript延迟执行实现方法(setTimeout)
2010/12/30 Javascript
用jQuery实现的智能隐藏、滑动效果的返回顶部代码
2014/03/18 Javascript
jQuery右下角旋转环状菜单特效代码
2015/08/10 Javascript
利用JS提交表单的几种方法和验证(必看篇)
2016/09/17 Javascript
Jquery AJAX POST与GET之间的区别详细介绍
2016/10/17 Javascript
Jquery on绑定的事件 触发多次实例代码
2016/12/08 Javascript
JS实现点击表头表格自动排序(含数字、字符串、日期)
2017/01/22 Javascript
微信小程序实现导航栏选项卡效果
2020/06/19 Javascript
微信小程序select下拉框实现效果
2019/05/15 Javascript
vue-cli3 配置开发与测试环境详解
2019/05/17 Javascript
[05:28]刀塔密之一:团结则存
2014/07/03 DOTA
python self,cls,decorator的理解
2009/07/13 Python
在Windows8上的搭建Python和Django环境
2014/07/03 Python
Python中dictionary items()系列函数的用法实例
2014/08/21 Python
Django Admin 实现外键过滤的方法
2017/09/29 Python
python实现发送邮件功能代码
2017/12/14 Python
Python3实现爬取简书首页文章标题和文章链接的方法【测试可用】
2018/12/11 Python
css3背景图片透明叠加属性cross-fade简介及用法实例
2013/01/08 HTML / CSS
video下autoplay属性无效的解决方法(添加muted属性)
2020/05/19 HTML / CSS
SIXPAD智能健身仪英国官网:革命性的训练装备品牌
2018/09/27 全球购物
Unix/Linux开发面试题
2016/08/16 面试题
应用数学专业求职信
2014/03/14 职场文书
美术指导求职信
2014/03/17 职场文书
三方合作协议书范本
2014/04/18 职场文书
2014年秋季开学演讲稿
2014/05/24 职场文书
学生夜不归宿检讨书
2014/09/23 职场文书
离婚协议书标准格式
2014/10/04 职场文书
2014年销售工作总结范文
2014/12/01 职场文书
法制主题班会教案
2015/08/13 职场文书
合作意向书范本
2019/04/17 职场文书
pytorch 运行一段时间后出现GPU OOM的问题
2021/06/02 Python
CSS 一行代码实现头像与国旗的融合
2021/10/24 HTML / CSS