基于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 相关文章推荐
教你如何在Django 1.6中正确使用 Signal
Jun 22 Python
跟老齐学Python之print详解
Sep 28 Python
Python函数中*args和**kwargs来传递变长参数的用法
Jan 26 Python
python判断字符串编码的简单实现方法(使用chardet)
Jul 01 Python
Python基于正则表达式实现检查文件内容的方法【文件检索】
Aug 30 Python
python删除服务器文件代码示例
Feb 09 Python
python 图片去噪的方法示例
Jul 09 Python
Python如何基于selenium实现自动登录博客园
Dec 16 Python
OpenCV+Python3.5 简易手势识别的实现
Dec 21 Python
Matlab求解数组中的最大值及它所在的具体位置
Apr 16 Python
python编程项目中线上问题排查与解决
Nov 01 Python
ubuntu安装jupyter并设置远程访问的实现
Mar 31 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编写的抽奖程序中奖概率算法
2015/05/14 PHP
thinkphp微信开之安全模式消息加密解密不成功的解决办法
2015/12/02 PHP
PHP实现将几张照片拼接到一起的合成图片功能【便于整体打印输出】
2017/11/14 PHP
PHP耦合设计模式实例分析
2018/08/08 PHP
PHP实现的文件浏览器功能简单示例
2019/09/12 PHP
php创建多级目录与级联删除文件的方法示例
2019/09/12 PHP
jQuery中siblings()方法用法实例
2015/01/08 Javascript
jQuery老黄历完整实现方法
2015/01/16 Javascript
Javascript控制input输入时间格式的方法
2015/01/28 Javascript
js光标定位文本框回车表单提交问题的解决方法
2015/05/11 Javascript
jQuery实现鼠标滑向当前图片高亮显示并且其它图片变灰的方法
2015/07/27 Javascript
Underscore源码分析
2015/12/30 Javascript
Node.js操作Firebird数据库教程
2016/03/04 Javascript
jQuery使用$.each遍历json数组的简单实现方法
2016/04/18 Javascript
AngularJS ng-style中使用filter
2016/09/21 Javascript
javascript匀速动画和缓冲动画详解
2016/10/20 Javascript
Bootstrap 模态框实例插件案例分析
2016/12/28 Javascript
Node.JS中快速扫描端口并发现局域网内的Web服务器地址(80)
2017/09/18 Javascript
12条写出高质量JS代码的方法
2018/01/07 Javascript
详解VueJS应用中管理用户权限
2018/02/02 Javascript
Webpack中publicPath路径问题详解
2018/05/03 Javascript
vue2.0 实现导航守卫的具体用法(路由守卫)
2018/05/17 Javascript
Layui多选只有最后一个值的解决方法
2019/09/02 Javascript
JS回调函数 callback的理解与使用案例分析
2019/09/09 Javascript
jquery实现上传文件进度条
2020/03/26 jQuery
python语言使用技巧分享
2016/05/31 Python
python plotly绘制直方图实例详解
2019/07/22 Python
浅谈pytorch中的BN层的注意事项
2020/06/23 Python
AmazeUI 手机版页面的顶部导航条Header与侧边导航栏offCanvas的示例代码
2020/08/19 HTML / CSS
详解HTML5布局和HTML5标签
2020/10/26 HTML / CSS
2014年公司迎新年活动方案
2014/02/24 职场文书
销售顾问岗位职责
2014/02/25 职场文书
2015年消费者权益日活动总结
2015/02/09 职场文书
会议室管理制度范本
2015/08/06 职场文书
导游词之云南丽江-泸沽湖
2019/09/26 职场文书
用python自动生成日历
2021/04/24 Python