乐观锁
商品超卖问题:多个用户同时下单同一个商品时,可能会出现资源竞争问题,导致库存结果出现异常
乐观锁解决问题并发时总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改
乐观锁并不是真正的锁,只是更新数据的时候多加一层判断,更新的时候判断此时库存是否和之前查询的库存一样,如果一样则表示没人修改,可以进行更新;否则表示有人抢过该资源,不再进行更新。类似下面操作:
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停止)
示例:
class OrderCommitView(View):"""乐观锁"""# 开启事务装饰器@transaction.atomicdef post(self,request):"""订单并发 ———— 乐观锁"""# 拿到idgoods_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')# 拼接keycart_key = 'cart_%d' % request.user.idfor 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.stockprint(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 - countnew_sales = goods.sales + countres = 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':'下单失败'})continueelse:breakOrderGoods.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_priceorder_info.save()# 事务提交transaction.savepoint_commit(sid)return JsonResponse({'res':6,'errmsg':'订单创建成功'})
悲观锁
悲观锁:开启事务,然后给mysql的查询语句最后加上for update。
对于后端的服务,有可能要承受很大的并发,比如火车票购买系统,后端服务需要准确的反应出剩余的票数。
那么后端怎么处理并发呢? select … for update 是数据库层面上专门用来解决并发取数据后再修改的场景的,主流的关系数据库 比如mysql、postgresql都支持这个功能, 新版的Django ORM甚至直接提供了这个功能的shortcut。
那么我们就来讨论怎么使用 select_for_update 和 验证锁的持有情况。
from django.http import JsonResponseimport randomfrom django.views.decorators.csrf import csrf_exemptfrom rest_framework import statusfrom rest_framework.decorators import api_viewfrom User.Server import VersionSerializerfrom django.db import transaction #导入相应的包from User.models import Version@transaction.atomic # 用户事务管理,必填项@csrf_exempt@api_view(['GET'])def GetVersionCode(request):save_id = transaction.savepoint() # 创建保存点try:Versions = Version.objects.all().select_for_update().first()except Version.DoesNotExist:return JsonResponse({}, safe=False, status=status.HTTP_404_NOT_FOUND)if Versions is None:transaction.savepoint_commit(save_id) # 提交保存点return JsonResponse({}, safe=False, status=status.HTTP_404_NOT_FOUND)Versions.versionCode = float(random.randint(1, 100))Versions.save()GetVersions = VersionSerializer(Versions, many=False)transaction.savepoint_commit(save_id) # 提交保存点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的事务锁与数据库事务-数据库事务

