基于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实现apahce网站日志分析示例
Apr 02 Python
python进阶教程之模块(module)介绍
Aug 30 Python
Python实现抓取页面上链接的简单爬虫分享
Jan 21 Python
整理Python中的赋值运算符
May 13 Python
Python实现七彩蟒蛇绘制实例代码
Jan 16 Python
python爬虫之模拟登陆csdn的实例代码
May 18 Python
使用Python快速搭建HTTP服务和文件共享服务的实例讲解
Jun 04 Python
Python实现的简单计算器功能详解
Aug 25 Python
python进程池实现的多进程文件夹copy器完整示例
Nov 27 Python
python中68个内置函数的总结与介绍
Feb 24 Python
keras 解决加载lstm+crf模型出错的问题
Jun 10 Python
python基础之停用词过滤详解
Apr 21 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
初识Laravel
2014/10/30 PHP
php绘制一条直线的方法
2015/01/24 PHP
PHP获取本周所有日期或者最近七天所有日期的方法
2018/06/20 PHP
用正则获取指定路径文件的名称
2007/02/27 Javascript
jQuery开发者都需要知道的5个小技巧
2010/01/08 Javascript
ExtJs事件机制基本代码模型和流程解析
2010/10/24 Javascript
AeroWindow 基于JQuery的弹出窗口插件
2011/06/27 Javascript
jquery实现手机发送验证码的倒计时代码
2014/02/12 Javascript
用JavaScript实现用一个DIV来包装文本元素节点
2014/09/09 Javascript
js函数内变量的作用域分析
2015/01/12 Javascript
javascript模块化简单解析
2016/04/07 Javascript
js实现的简练高效拖拽功能示例
2016/12/21 Javascript
JavaScript输入框字数实时统计更新
2017/06/17 Javascript
vue.js学习笔记之v-bind和v-on解析
2018/05/03 Javascript
vue表单自定义校验规则介绍
2018/08/28 Javascript
js中async函数结合promise的小案例浅析
2019/04/14 Javascript
微信小程序 确认框的实现(附代码)
2019/07/23 Javascript
详解NodeJs项目 CentOs linux服务器线上部署
2019/09/16 NodeJs
vue项目初始化到登录login页面的示例
2019/10/31 Javascript
python连接字符串的方法小结
2015/07/13 Python
Python遍历文件夹和读写文件的实现方法
2017/05/10 Python
python用户管理系统的实例讲解
2017/12/23 Python
基于Python对数据shape的常见操作详解
2018/12/25 Python
Python实现的多进程拷贝文件并显示百分比功能示例
2019/04/09 Python
python basemap 画出经纬度并标定的实例
2019/07/09 Python
Python填充任意颜色,不同算法时间差异分析说明
2020/05/16 Python
完美解决python针对hdfs上传和下载的问题
2020/06/05 Python
50个强大璀璨的CSS3/JS技术运用实例
2010/02/27 HTML / CSS
详解CSS3的opacity属性设置透明效果的用法
2016/05/09 HTML / CSS
维珍澳洲航空官网:Virgin Australia
2017/09/08 全球购物
工地门卫岗位职责
2013/12/30 职场文书
制药工程专业职业生涯规划范文
2014/03/10 职场文书
村主任群众路线个人对照检查材料
2014/09/26 职场文书
初中生活随笔
2015/08/15 职场文书
小程序后台PHP版本部署运行 LNMP+WNMP
2021/04/01 Servers
HTML中的表格元素介绍
2022/02/28 HTML / CSS