Django 中的并发请求

Ben*_*ueg 4 django postgresql transactions django-rest-framework

我有 2 个模型:ProductOrder.

Product有一个股票的整数字段,而Order有一个状态和一个外键Product

class Product(models.Model):
    name = models.CharField(max_length=30)
    stock = models.PositiveSmallIntegerField(default=1)

class Order(models.Model):
    product = models.ForeignKey('Product')
    DRAFT = 'DR'; INPROGRESS = 'PR'; ABORTED = 'AB'
    STATUS = ((INPROGRESS, 'In progress'),(ABORTED, 'Aborted'),)
    status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT)
Run Code Online (Sandbox Code Playgroud)

我的目标是让每个新订单的产品库存减少一个,每个订单取消增加一个。为此,我重载了模型的save方法Order(受Django启发:保存时,如何检查字段是否已更改?):

from django.db.models import F

class Order(models.Model):
    product = models.ForeignKey('Product')
    status = models.CharField(max_length = 2, choices = STATUS, default = DRAFT)

    EXISTING_STATUS = set([INPROGRESS])

    __original_status = None

    def __init__(self, *args, **kwargs):
        super(Order, self).__init__(*args, **kwargs)
        self.__original_status = self.status

    def save(self, *args, **kwargs):
        old_status = self.__original_status
        new_status = self.status
        has_changed_status = old_status != new_status
        if has_changed_status:
            product = self.product
            if not old_status in Order.EXISTING_STATUS and new_status in Order.EXISTING_STATUS:
                product.stock = F('stock') - 1
                product.save(update_fields=['stock'])
            elif old_status in Order.EXISTING_STATUS and not new_status in Order.EXISTING_STATUS:
                product.stock = F('stock') + 1
                product.save(update_fields=['stock'])
        super(Order, self).save(*args, **kwargs)
        self.__original_status = self.status
Run Code Online (Sandbox Code Playgroud)

使用 RestFramework,我创建了 2 个视图,一个用于创建新订单,一个用于取消现有订单。两者都使用简单的序列化程序:

class OrderSimpleSerializer(serializers.ModelSerializer):

    class Meta:
        model = Order
        fields = (
            'id',
            'product',
            'status',
        )
        read_only_fields = (
            'status',
        )

class OrderList(generics.ListCreateAPIView):
    model = Order
    serializer_class = OrderSimpleSerializer

    def pre_save(self, obj):
        super(OrderList,self).pre_save(obj)
        product = obj.product
        if not product.stock > 0:
            raise ConflictWithAnotherRequest("Product is not available anymore.")
        obj.status = Order.INPROGRESS

class OrderAbort(generics.RetrieveUpdateAPIView):
    model = Order
    serializer_class = OrderSimpleSerializer

    def pre_save(self, obj):
        obj.status = Order.ABORTED
Run Code Online (Sandbox Code Playgroud)

以下是访问这两个视图的方法:

from myapp.views import *

urlpatterns = patterns('',
    url(r'^order/$', OrderList.as_view(), name='order-list'),
    url(r'^order/(?P<pk>[0-9]+)/abort/$', OrderAbort.as_view(), name='order-abort'),
)
Run Code Online (Sandbox Code Playgroud)

我正在使用 Django 1.6b4、Python 3.3、Rest Framework 2.7.3 和 PostgreSQL 9.2。

我的问题是并发请求可以增加比原始库存更高的产品库存!

这是我用来证明这一点的脚本:

import sys
import urllib.request
import urllib.parse
import json

opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor)

def create_order():
    url = 'http://127.0.0.1:8000/order/'
    values = {'product':1}
    data  = urllib.parse.urlencode(values).encode('utf-8')
    request = urllib.request.Request(url, data)
    response = opener.open(request)
    return response

def cancel_order(order_id):
    abort_url = 'http://127.0.0.1:8000/order/{}/abort/'.format(order_id)
    values = {'product':1,'_method':'PUT'}
    data  = urllib.parse.urlencode(values).encode('utf-8')
    request = urllib.request.Request(abort_url, data)
    try:
        response = opener.open(request)
    except Exception as e:
        if (e.code != 403):
            print(e)
    else:
        print(response.getcode())

def main():
    response = create_order()
    print(response.getcode())
    data = response.read().decode('utf-8')
    order_id = json.loads(data)['id']
    time.sleep(1)
    for i in range(2):
        p = Process(target=cancel_order, args=[order_id])
        p.start()

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

对于库存为 1 的产品,此脚本提供以下输出:

201 # means it creates an order for Product, thus decreasing stock from 1 to 0
200 # means it cancels the order for Product, thus increasing stock from 0 to 1
200 # means it cancels the order for Product, thus increasing stock from 1 to 2 (shouldn't happen)
Run Code Online (Sandbox Code Playgroud)

编辑

我添加了一个示例项目来重现该错误:https : //github.com/ThinkerR/django-concurrency-demo

小智 5

看看django-concurrency。它使用乐观并发控制模式处理并发编辑。