25f44ac9ca05f60260f70c4225e42c1e.jpg

乐观锁

商品超卖问题:多个用户同时下单同一个商品时,可能会出现资源竞争问题,导致库存结果出现异常
乐观锁解决问题并发时总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改
乐观锁并不是真正的锁,只是更新数据的时候多加一层判断,更新的时候判断此时库存是否和之前查询的库存一样,如果一样则表示没人修改,可以进行更新;否则表示有人抢过该资源,不再进行更新。类似下面操作:

update tb_sku set stock=2 where id=1 and stock=7; SKU.objects.filter(id=1, stock=7).update(stock=2)

思路:

创建保存点 查询库存 更新库存时,将之前查询的库存和商品id一起作为更新的条件 当受影响行为0表示更新失败,回滚到保存点(没有操作数据库的时候) 重新查询,重复到n次不成功返回错误 (这里while True 直到库存为0停止)

示例:

  1. class OrderCommitView(View):
  2. """乐观锁"""
  3. # 开启事务装饰器
  4. @transaction.atomic
  5. def post(self,request):
  6. """订单并发 ———— 乐观锁"""
  7. # 拿到id
  8. goods_ids = request.POST.get('goods_ids')
  9. if len(goods_ids) == 0 :
  10. return JsonResponse({'res':0,'errmsg':'数据不完整'})
  11. # 当前时间字符串
  12. now_str = datetime.now().strftime('%Y%m%d%H%M%S')
  13. # 订单编号
  14. order_id = now_str + str(request.user.id)
  15. # 支付方式
  16. pay_method = request.POST.get('pay_method')
  17. # 地址
  18. address_id = request.POST.get('address_id')
  19. try:
  20. address = Address.objects.get(id=address_id)
  21. except Address.DoesNotExist:
  22. return JsonResponse({'res':1,'errmsg':'地址错误'})
  23. # 商品数量
  24. total_count = 0
  25. # 商品总价
  26. total_amount = 0
  27. # 订单运费
  28. transit_price = 10
  29. # 创建保存点
  30. sid = transaction.savepoint()
  31. order_info = OrderInfo.objects.create(
  32. order_id = order_id,
  33. user = request.user,
  34. addr = address,
  35. pay_method = pay_method,
  36. total_count = total_count,
  37. total_price = total_amount,
  38. transit_price = transit_price
  39. )
  40. # 获取redis连接
  41. goods = get_redis_goodsection('default')
  42. # 拼接key
  43. cart_key = 'cart_%d' % request.user.id
  44. for goods_id in goods_ids:
  45. # 尝试查询商品
  46. # 此处考虑订单并发问题,
  47. # redis中取出商品数量
  48. count = goods.hget(cart_key, goods_id)
  49. if count is None:
  50. # 回滚到保存点
  51. transaction.savepoint_rollback(sid)
  52. return JsonResponse({'res': 3, 'errmsg': '商品不在购物车中'})
  53. count = int(count)
  54. for i in range(3):
  55. # 若存在订单并发则尝试下单三次
  56. try:
  57. goods = Goodsgoods.objects.get(id=goods_id) # 不加锁查询
  58. # goods = Goodsgoods.objects.select_for_update().get(id=goods_id) # 加互斥锁查询
  59. except Goodsgoods.DoesNotExist:
  60. # 回滚到保存点
  61. transaction.savepoint_rollback(sid)
  62. return JsonResponse({'res':2,'errmsg':'商品信息错误'})
  63. origin_stock = goods.stock
  64. print(origin_stock, 'stock')
  65. print(goods.id, 'id')
  66. if origin_stock < count:
  67. # 回滚到保存点
  68. transaction.savepoint_rollback(sid)
  69. return JsonResponse({'res':4,'errmsg':'库存不足'})
  70. # # 商品销量增加
  71. # goods.sales += count
  72. # # 商品库存减少
  73. # goods.stock -= count
  74. # # 保存到数据库
  75. # goods.save()
  76. # 如果下单成功后的库存
  77. new_stock = goods.stock - count
  78. new_sales = goods.sales + count
  79. res = Goodsgoods.objects.filter(stock=origin_stock,id=goods_id).update(stock=new_stock,sales=new_sales)
  80. print(res)
  81. if res == 0:
  82. if i == 2:
  83. # 回滚
  84. transaction.savepoint_rollback(sid)
  85. return JsonResponse({'res':5,'errmsg':'下单失败'})
  86. continue
  87. else:
  88. break
  89. OrderGoods.objects.create(
  90. order = order_info,
  91. goods = goods,
  92. count = count,
  93. price = goods.price
  94. )
  95. # 删除购物车中记录
  96. goods.hdel(cart_key,goods_id)
  97. # 累加商品件数
  98. total_count += count
  99. # 累加商品总价
  100. total_amount += (goods.price) * count
  101. # 更新订单信息中的商品总件数
  102. order_info.total_count = total_count
  103. # 更新订单信息中的总价格
  104. order_info.total_price = total_amount + order_info.transit_price
  105. order_info.save()
  106. # 事务提交
  107. transaction.savepoint_commit(sid)
  108. return JsonResponse({'res':6,'errmsg':'订单创建成功'})

悲观锁

悲观锁:开启事务,然后给mysql的查询语句最后加上for update。
对于后端的服务,有可能要承受很大的并发,比如火车票购买系统,后端服务需要准确的反应出剩余的票数。
那么后端怎么处理并发呢? select … for update 是数据库层面上专门用来解决并发取数据后再修改的场景的,主流的关系数据库 比如mysql、postgresql都支持这个功能, 新版的Django ORM甚至直接提供了这个功能的shortcut。
那么我们就来讨论怎么使用 select_for_update 和 验证锁的持有情况。

  1. from django.http import JsonResponse
  2. import random
  3. from django.views.decorators.csrf import csrf_exempt
  4. from rest_framework import status
  5. from rest_framework.decorators import api_view
  6. from User.Server import VersionSerializer
  7. from django.db import transaction #导入相应的包
  8. from User.models import Version
  9. @transaction.atomic # 用户事务管理,必填项
  10. @csrf_exempt
  11. @api_view(['GET'])
  12. def GetVersionCode(request):
  13. save_id = transaction.savepoint() # 创建保存点
  14. try:
  15. Versions = Version.objects.all().select_for_update().first()
  16. except Version.DoesNotExist:
  17. return JsonResponse({}, safe=False, status=status.HTTP_404_NOT_FOUND)
  18. if Versions is None:
  19. transaction.savepoint_commit(save_id) # 提交保存点
  20. return JsonResponse({}, safe=False, status=status.HTTP_404_NOT_FOUND)
  21. Versions.versionCode = float(random.randint(1, 100))
  22. Versions.save()
  23. GetVersions = VersionSerializer(Versions, many=False)
  24. transaction.savepoint_commit(save_id) # 提交保存点
  25. return JsonResponse(GetVersions.data, safe=False, status=status.HTTP_200_OK)

你也许会有疑问,为什么必须要把 select_for_update 放在事务里呢?
我们可以反过来想,如果不放在事务里,什么时候释放 select_for_update 持有的锁呢。
另外通过代码尝试,如果没有把 select_for_update 放在事务里,
会报异常 select_for_update cannot be used outside of a transaction
扩展:混搭的写法会有什么效果。即在 with transaction.atomic() 中编写且执行 SQL, 也是可以的!

总结

当某一功能需要两三个数据表时,个人建议最好操作数据时最好对相关数据进行加锁,以免造成数据错误,详情信息请参考django的事务锁与数据库事务-数据库事务