用户生成订单时出现的高并发问题
- 如果我们的商城有足够多的用户,在相差0.00000几秒的情况下基本上同时下单,这样先查询商品库存,再修改商品库存,的时候就会出现资源竞争问题
画图理解:
解决方案
-
悲观锁
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法 select stock from tb_sku where id=1 for update; SKU.objects.select_for_update().get(id=1) 悲观锁类似于我们在多线程资源竞争时添加的互斥锁,容易出现死锁现象,采用不多。
-
乐观锁
乐观锁并不是真实存在的锁,而是在更新的时候判断此时的库存是否是之前查询出的库存,如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新。类似如下操作 update tb_sku set stock=2 where id=1 and stock=7; SKU.objects.filter(id=1, stock=7).update(stock=2)
-
任务队列
将下单的逻辑放到任务队列中(如celery),将并行转为串行,所有人排队下单。比如开启只有一个进程的Celery,一个订单一个订单的处理。
乐观锁代码处理
-
流程
1. 创建死循环一直检测是否还有库存 2. 记录数据库中的库存和销量 3. 计算购买后的库存和销量 4. 根据原库存数据来判断中间是否出现了高并发问题 4.1 如果查询出数据就修改 4.2 查询不出就跳过本次循环继续检测 5. 查询出数据修改完之后break结束死循环
# 遍历商品列表
for sku in skus:
# 添加死循环 一直来检测商品库存是否还有
while True:
# 记录原数据库中商品的库存和销量
origin_stock = sku.stock
origin_sales = sku.sales
# 算出每一个商品的数量是否大于库存
count = new_cart_dict[sku.id]
if count > sku.stock:
# 库存不足返回到回滚点
transaction.savepoint_rollback(save_id)
return JsonResponse({'code': 400, 'errmsg': '库存不足'})
# 计算库存和销量
new_stock = origin_stock - count
new_sales = origin_sales + count
# time.sleep(5)
# 如果库存与保存库存的数据相等就说明中间没有人买同一个商品 过滤出数据再去修改库存和销量
# 结果返回的是过滤出的数据 有一条就返回1
result = SKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
# 如果没有过滤出数据就说明库存与一开始不同了 数据改变了 跳过本次循环 继续执行
if result == 0:
continue
# 库存减去购买的数量
# sku.stock -= count
# 销量加上购买的数量
# sku.sales += count
# 保存到数据库
# sku.save()
try:
# 保存订单商品
OrderGoods.objects.create(
order_id=order_id,
sku=sku,
count=count,
price=sku.price
)
except Exception as e:
print(e)
# 保存失败返回回滚点
transaction.savepoint_rollback(save_id)
return JsonResponse({'code': 400, 'errmsg': '保存失败'})
# 订单总数量+=每一个商品的数量 总价+=每一个商品的总价
order.total_count += count
order.total_amount += count * sku.price
# 如果执行下来没有continue就结束死循环来保存数据库
break
# 总价+=运费
order.total_amount += order.freight
# 保存到数据库
order.save()
修改MySQL的隔离级别
-
事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。
-
MySQL数据库事务隔离级别主要有四种:
Serializable:串行化,一个事务一个事务的执行。 Repeatable read:可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。 Read committed:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。 Read uncommitted:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。
MySQL数据库默认使用可重复读( Repeatable read)。
使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)。
修改方式:
windows:
- 先停止MySQL的服务 net stop MySQL服务名
- 在mysql的文件夹内修改my.ini文件 添加一行 修改MySQL的隔离级别
transaction-isolation=READ-COMMITTED
- 开启MySQL服务 net start MySQL服务名
Linux:
# 修改cnf文件
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
# 添加一行
transaction-isolation=READ-COMMITTED
# 修改完成之后重启
sudo service mysql restart