我在一个Electron 商务网站与购物车和产品.产品的主键被添加到字典'cart_content'中的用户会话数据.该字典将产品的主键作为键,将给定产品的数量作为值.

我成功地复制了一个竞争条件错误,我同时添加了两个产品,数量足够大,所以两个添加中只有一个卖完了产品(例如,剩下3个产品,再添加3个).这两个请求将购物车增加两倍,但没有足够的库存(在我们的示例中,剩余3个,但购物车中有6个).

如何防止竞争条件发生,如上面给出的例子或在多用户情况下?例如,是否有某种锁定系统可以防止add_to_cart()被异步执行?

Edit: I switched from SQLite to PostgreSQL because I wanted to test 100.

Core/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', 0))
        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"),
    …
]

Cart/templates/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>

Cart/Static/Cart/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'));
            }
        }
    });
});

用于重现争用条件的Java脚本.当然,它并不总是在第一次就奏效.只要重复两到三次,直到你得到我提到的行为:

async function test() {
    document.getElementsByClassName('button-add-to-cart')[0].click();
}

test(); test();

推荐答案

Edit with full solution - Short answer

@transaction.atomicselect_for_update()一起使用,并手动将会话与save()一起保存.您必须 Select 所有会话,并使用list()进行判断,这样它将锁定每个会话.这是因为存在多用户场景.This isn’t working for SQLite:

@transaction.atomic
def _add_to_cart_atomic(request, product_id, amount):
    p = Product.objects.get(pk=product_id)
    list(Session.objects.select_for_update())  # Force evaluation to lock all the sessions
    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(product_id, None):
        request.session['cart_content'][product_id] += amount
    else:
        request.session['cart_content'][product_id] = amount
    request.session.save()
    return HttpResponse('0')

def add_to_cart(request):
    if (request.method == "POST"):
        pk = request.POST.get('pk', None)
        amount = int(request.POST.get('amount', 0))
        if pk and amount:
            return _add_to_cart_atomic(request, pk, amount)
    return HttpResponse('', status=404)

Long answer

Using @transaction.atomic alone

使用@transaction.atomic个decorator 似乎解决了一半的问题,即使我不能真正证明这是@transaction.atomic%有效的.这对SQLite和PostgreSQL都有效.

Wikipedia人起:

原子事务是一系列不可分割且不可减少的数据库操作,它们要么全部发生,要么一个都不发生.原子性保证可以防止发生部分数据库更新,因为它们可能比直接拒绝整个系列造成更大的问题.

@transaction.atomic
def add_to_cart_atomic(request, pk, 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')

def add_to_cart(request):
    if (request.method == "POST"):
        pk = request.POST.get('pk', None)
        amount = int(request.POST.get('amount', 0))
        if pk and amount:
            return add_to_cart_atomic(request, pk, amount)
    return HttpResponse('', status=404)

But it raises another problem.当我重做我的双击javascript测试时,有时,我会收到两个请求的"成功"警告,即使服务器并不真的向购物车添加两次.我认为这是因为删除了SQL查询而没有引发任何错误. 有时,它会按预期工作,第一个请求成功,第二个请求"超出数量".

在当前会话上使用锁定

这差不多是Ilya Khrustalev’s answer个了.如果我们在当前课程中加select_for_update()分.This does not work for SQLite:

@transaction.atomic
def _add_to_cart_atomic(request, product_id, amount):
    p = Product.objects.get(pk=product_id)
    Session.objects.select_for_update().get(session_key = request.session.session_key)
    …

然后这将只锁定当前会话,所以我们的小JavaScript测试将通过.但是像这样的多用户场景呢?:

async def add_one_to_cart(session_key):
    async with aiohttp.ClientSession(cookies={'sessionid': session_key}) as session:
        async with session.post('http://localhost:8000/cart/add-to-cart', data={'pk': 1, 'amount': 1}, cookies={'sessionid': session_key}) as response:
            return await response.text()

async def main():
    result = await asyncio.gather(
        add_one_to_cart('session_key_A'),
        add_one_to_cart('session_key_B'),
        add_one_to_cart('session_key_C')
    )
    print(result)

asyncio.run(main())

结果:

['0', '0', '0']

这意味着来自三个不同会话的每个请求都会通过,即使只剩下一个产品!所以我们有number_on_hold分,超过了2number_on_stock分!

在所有会话上使用锁定

这是我的"简短回答".

Javascript相关问答推荐

如何使用JavaScript用等效的功能性HTML替换标记URL格式?

将json数组项转换为js中的扁平

IMDB使用 puppeteer 加载更多按钮(nodejs)

react/redux中的formData在expressjs中返回未定义的req.params.id

如何粗体匹配的字母时输入搜索框使用javascript?

覆盖TypeScrip中的Lit-Element子类的属性类型

将内容大小设置为剩余可用空间,但如果需要,可在div上显示滚动条

以Angular 实现ng-Circle-Progress时出错:模块没有导出的成员

如何在Svelte中从一个codec函数中调用error()?

根据一个条件,如何从处理过的数组中移除一项并将其移动到另一个数组?

JavaScript不重定向配置的PATH

变量在导入到Vite中的另一个js文件时成为常量.

当从其他文件创建类实例时,为什么工作线程不工作?

Node.js API-queryAll()中的MarkLogic数据移动

是否设置以JavaScript为背景的画布元素?

我为什么要使用回调而不是等待?

如何设置时间选取器的起始值,仅当它获得焦点时?

将字体样式应用于material -UI条形图图例

如何在Java脚本中添加一个可以在另一个面板中垂直调整大小的面板?

正在发出错误的URL请求