Django并发请求下如何避免重复创建实例引发商品超卖问题
问题根因
当前代码的商品查询、支付创建、商品状态修改是三个完全独立的非原子操作,没有任何并发控制逻辑:多个并发请求会在商品状态被改为不可售之前,同时查到同一条on_sell=true的商品记录,各自生成对应的支付对象,等所有请求都创建完支付才挨个修改商品售罄状态,必然出现超卖。代码里加的sleep只是把读写之间的时间窗口放大,让问题更容易复现,哪怕不加延迟,只要并发量足够高,读写间隙的时间窗口依然会被多个请求穿透,超卖问题一样存在。
解决方案
按落地优先级从高到低排列:
- 数据库行级排他锁+原子事务(最稳妥的通用方案)
利用数据库本身的锁机制,查询可售商品时直接给命中的记录加排他锁,拿到锁的请求才能处理对应商品,其他请求会阻塞等待锁释放,从根源避免同一条商品被多个请求同时读取处理。以Django ORM为例,实现代码如下:
事务执行完(代码块运行结束)锁才会释放,此时商品已经被标记为不可售,后续请求再查询就不会命中这条记录,根本不会走到创建支付的逻辑。from django.db import transaction import time # 整个操作包裹在数据库事务中 with transaction.atomic(): # select_for_update 会给查询到的商品记录加行级排他锁,事务提交前其他事务无法读写这些记录 products_available = Product.objects.select_for_update().filter(on_sell=True) for product in products_available: payment = Payment() payment.buyer = request.user # 替换为实际的买家取值逻辑 payment.seller = product.owner payment.save() time.sleep(10) # 即使存在响应延迟,锁未释放时其他请求拿不到该商品记录 product.on_sell = False product.save() - 乐观锁校验(无锁阻塞,适合中等并发场景)
不需要加锁阻塞请求,在更新商品状态时附加状态校验条件,只有商品当前仍为可售状态时更新才会生效,根据更新影响的行数判断是否出现并发冲突,出现冲突就回滚已创建的无效支付即可,实现代码如下:
这个方案没有锁等待开销,但是并发冲突率高的时候会有较多请求触发回滚,不适合秒杀类极高并发场景。from django.db import transaction import time with transaction.atomic(): products_available = Product.objects.filter(on_sell=True) for product in products_available: payment = Payment() payment.buyer = request.user payment.seller = product.owner payment.save() time.sleep(10) # 更新时附加校验条件,仅当商品仍为可售状态才执行更新 updated_rows = Product.objects.filter(id=product.id, on_sell=True).update(on_sell=False) if updated_rows == 0: # 商品已被其他并发请求下单,删除无效支付,回滚事务 payment.delete() raise ValueError("当前商品已被他人购买") - 数据库层面唯一约束兜底
无论用上面哪种方案,都建议给Payment表加商品维度的联合唯一约束,比如一个商品仅允许对应一个有效支付单,靠数据库的强制唯一性做最后一道防线,哪怕上层逻辑出现漏洞,也不会生成多个绑定同一商品的支付单。
注意:不要靠服务内存变量、非原子性的缓存操作做防超卖判断,所有核心的并发控制逻辑最终要落数据库的原子能力,否则服务多实例分布式部署时依然会出现超卖问题。
内容的提问来源于stack exchange,提问作者devnext
相关产品推荐
相关产品推荐

