如何在处理会话时防止竞争状况?

问题描述 投票:0回答:1

我正在开发一个带有购物车和产品的电子商务网站。产品的主键被添加到字典中的用户会话数据中

'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();
javascript python django race-condition
1个回答
0
投票

处理竞争条件的最简单方法之一是在数据库级别合并悲观锁。为此,您需要将操作包装到数据库事务中,并在此类操作常见的对象上设置锁。

在您的情况下,如果您将会话存储在数据库中,您可以执行类似的操作

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,最好使用它而不是会话。

请考虑到,对全局对象(例如会话或用户)设置锁定可能会导致后端吞吐量下降,因为请求必须依次排队。

© www.soinside.com 2019 - 2024. All rights reserved.