我正在开发一个带有购物车和产品的电子商务网站。产品的主键被添加到字典中的用户会话数据中
'cart_content'
。该字典将产品的主键作为键,将给定产品的数量作为值。
我通过触发
click()
两次,成功重现了竞争条件错误,并且下次购买的产品数量将售完。因此,这两个请求将购物车增加了一倍,但库存不足。
如何防止像上面给出的示例或多用户情况下那样发生竞争条件?例如,是否有某种锁定系统可以防止
add_to_cart()
异步执行?
核心/models.py:
class Product(models.Model):
…
number_in_stock = models.IntegerField(default=0)
@property
def number_on_hold(self):
result = 0
for s in Session.objects.all():
amount = s.get_decoded().get('cart_content', {}).get(str(self.pk))
if amount is not None:
result += int(amount)
return result
…
购物车/views.py:
def add_to_cart(request):
if (request.method == "POST"):
pk = request.POST.get('pk', None)
amount = int(request.POST.get('amount', None))
if pk and amount:
p = Product.objects.get(pk=pk)
if amount > p.number_in_stock - p.number_on_hold:
return HttpResponse('1')
if not request.session.get('cart_content', None):
request.session['cart_content'] = {}
if request.session['cart_content'].get(pk, None):
request.session['cart_content'][pk] += amount
else:
request.session['cart_content'][pk] = amount
request.session.modified = True
return HttpResponse('0')
return HttpResponse('', status=404)
购物车/urls.py:
urlpatterns = [
…
path("add-to-cart", views.add_to_cart, name="cart-add-to-cart"),
…
]
购物车/模板/购物车/html/atoms/add_to_cart.html:
<div class="form-element add-to-cart-amount">
<label for="addToCartAmount{{ product_pk }}"> {% translate "Amount" %} : </label>
<input type="number" id="addToCartAmount{{ product_pk }}" />
</div>
<div class="form-element add-to-cart">
<button class="btn btn-primary button-add-to-cart" data-product-pk="{{ product_pk }}" data-href="{% url 'cart-add-to-cart' %}"><span> {% translate "Add to cart" %} </span></button>
</div>
购物车/静态/购物车/js/main.js:
$(".button-add-to-cart").click(function(event) {
event.preventDefault();
let product_pk = $(this).data("productPk");
let amount = $(this).parent().parent().find("#addToCartAmount" + product_pk).val();
$.ajax({
url: $(this).data("href"),
method: 'POST',
data: {
pk: product_pk,
amount: amount
},
success: function(result) {
switch (result) {
case '1':
alert(gettext('Amount exceeded'));
break;
case '0':
alert(interpolate(gettext('Successfully added %s items to the cart.'), [amount]))
break;
default:
alert(gettext('Unknown error'));
}
}
});
});
用于重现竞争条件的 Javascript。当然,第一次并不总是有效。只需重复两到三次,直到得到我提到的行为:
async function test() {
document.getElementsByClassName('button-add-to-cart')[0].click();
}
test(); test();
处理竞争条件的最简单方法之一是在数据库级别合并悲观锁。为此,您需要将操作包装到数据库事务中,并在此类操作常见的对象上设置锁。
在您的情况下,如果您将会话存储在数据库中,您可以执行类似的操作
from django.db import transaction
@transaction.atomic # decorate function with transaction.atomic or use it as a context manager
def add_to_cart(session, product_id, amount):
p = Product.objects.get(pk=product_id)
s = Session.objects.select_for_update().get(pk=session.pk) # NOTE: select_for_update() here
if amount > p.number_in_stock - p.number_on_hold:
return HttpResponse('1')
if not request.session.get('cart_content', None):
request.session['cart_content'] = {}
if request.session['cart_content'].get(pk, None):
request.session['cart_content'][pk] += amount
else:
request.session['cart_content'][pk] = amount
request.session.modified = True
def add_to_cart(request):
if (request.method == "POST"):
pk = request.POST.get('pk', None)
amount = int(request.POST.get('amount', None))
if pk and amount:
add_to_cart(request.session, pk, amount) # <-- moved logic for cleaner code
return HttpResponse('0')
return HttpResponse('', status=404)
如果您已经验证了用户 ID,最好使用它而不是会话。
请考虑到,对全局对象(例如会话或用户)设置锁定可能会导致后端吞吐量下降,因为请求必须依次排队。