乐观锁
商品超卖问题:多个用户同时下单同一个商品时,可能会出现资源竞争问题,导致库存结果出现异常
乐观锁解决问题并发时总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改
乐观锁并不是真正的锁,只是更新数据的时候多加一层判断,更新的时候判断此时库存是否和之前查询的库存一样,如果一样则表示没人修改,可以进行更新;否则表示有人抢过该资源,不再进行更新。类似下面操作:
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.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':'订单创建成功'})
悲观锁
悲观锁:开启事务,然后给mysql的查询语句最后加上for update。
对于后端的服务,有可能要承受很大的并发,比如火车票购买系统,后端服务需要准确的反应出剩余的票数。
那么后端怎么处理并发呢? select … for update 是数据库层面上专门用来解决并发取数据后再修改的场景的,主流的关系数据库 比如mysql、postgresql都支持这个功能, 新版的Django ORM甚至直接提供了这个功能的shortcut。
那么我们就来讨论怎么使用 select_for_update 和 验证锁的持有情况。
from django.http import JsonResponse
import random
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status
from rest_framework.decorators import api_view
from User.Server import VersionSerializer
from 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的事务锁与数据库事务-数据库事务