Django @transaction.atomic() 防止并发创建对象

Django @transaction.atomic() to prevent creating objects in concurrency

我有一个票证模型,以及它的票证序列化器。工单模型有一个 bought 和一个 booked_at 字段。还有一个用于显示和座位的 unique_together 属性。

class Ticket(models.Model):
    show = models.ForeignKey(Show, on_delete=models.CASCADE)
    seat = models.ForeignKey(Seat, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    booked_at = models.DateTimeField(default=timezone.now)
    bought = models.BooleanField(default=False)

    class Meta:
        unique_together = ('show', 'seat')

TicketSerializer:

class TicketSerializer(serializers.Serializer):
    seat = serializers.PrimaryKeyRelatedField(queryset=Seat.objects.all())
    show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all())
    user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
    bought = serializers.BooleanField(default=False)

    def validate(self, attrs):
        if attrs['seat']:
            try:
                ticket = Ticket.objects.get(show=attrs['show'], seat=seat)
                if not ticket.bought:
                    if ticket.booked_at < timezone.now() - datetime.timedelta(minutes=5):
                        # ticket booked crossed the deadline
                        ticket.delete()
                        return attrs
                    else:
                        # ticket in 5 mins range
                        raise serializers.ValidationError("Ticket with same show and seat exists.")
                else:
                    raise serializers.ValidationError("Ticket with same show and seat exists.")
            except Ticket.DoesNotExist:
                return attrs
        else:
            raise serializers.ValidationError("No seat value provided.")

在视图中,我正在使用 @transaction.atomic() 来确保 ticket/s 仅在所有有效时才创建,或者如果无效则不创建任何票证。

@transaction.atomic()
@list_route(
    methods=['POST'],
    permission_classes=[IsAuthenticated],
    url_path='book-tickets-by-show/(?P<show_id>[0-9]+)'
)
def book_tickets_by_show(self, request, show_id=None):
    try:
        show = Show.objects.get(id=show_id)
        user = request.user
        ...
        ...
        data_list = [...]
        with transaction.atomic():
            try:
                serializer = TicketSerializer(data=data_list, many=True)
                if serializer.is_valid():
                    serializer.save()
                    ....
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
            except (Seat.DoesNotExist, ValueError, ConnectionError) as e:
                return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
    except (Show.DoesNotExist, IntegrityError) as e:
        return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)

我想知道的是,它是否有助于防止调用多个请求来为同一个 seat/s 创建 ticket/s?

假设,用户A想订5,6号座位的票。用户 B 想要预订 3、6 号座位的机票,另一个用户 C 想要预订 2、3、4、5、6 号座位的机票。

上述方法是否会阻止为所有用户预订各自座位的门票,而只为一个用户(可能是第一个交易的用户)创建门票?或者,如果有更好的方法,请告诉我如何做。我希望我说得很清楚。如果不是请追问。

您应该使用显式分布式锁来同步请求,而不是依赖 transaction.atomic 这并不意味着是锁。

various ways 来实现锁,但在我们的 Django/Gunicorn 项目中,我们使用 Python 自己的 multiprocessing.Lock 来确保请求进入一个块一次编码一个。这是一个对我们有用的相对简单的解决方案。

import multiprocessing

_lock = multiprocessing.Lock()

_lock.acquire()
try:
    # Some code that needs to be accessed by one request a time
finally:
    _lock.release()

Will it help in preventing when more than one requests are called for creating the ticket/s for same seat/s.

是的,会的。 unique_together 约束加上 transaction.atomic() 将确保您不能为同一个 seat/show 创建两张工单。

也就是说,您当前的方法存在几个问题:

  1. 我认为没有必要包装整个视图以及在 atomic() 中进行保存的位 - 你不需要两者都做并将整个视图包装在一个事务以性能成本为代价。在交易中包装 serializer.save() 应该就足够了。

  2. 不建议在事务中捕获异常 - 请参阅 the warning in the documentation。通常最好在尽可能靠近可以生成异常的代码处捕获异常,以避免混淆。我建议将代码重构为类似这样的内容。

    try:
        show = Show.objects.get(id=show_id)
    # Catch this specific exception where it happens, rather than at the bottom.
    except Show.DoesNotExist as e:
        return Response({'detail': str(e)}
    
    user = request.user
    ...
    ...
    data_list = [...]
    
    try:
        serializer = TicketSerializer(data=data_list, many=True)
        if serializer.is_valid():
            try:
                # Note - this is now *inside* a try block, not outside
                with transaction.atomic():
                    serializer.save()
                    ....
            except IntegrityError as e:
                return Response({'detail': str(e), status=status.HTTP_400_BAD_REQUEST}
    
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    # Retained from your code - althought I am not sure how you would 
    # end up with ever get a Seat.DoesNotExist or ValueError error here
    # Would be better to catch them in the place they can occur.
    except (Seat.DoesNotExist, ValueError, ConnectionError) as e:
        return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
    

下面的怎么样 创建间歇性 table

class showAndSeat(models.Model):
    show = models.ForeignKey(Show, on_delete=models.CASCADE)
    seat = models.ForeignKey(Seat, on_delete=models.CASCADE)
    showtime = models.DateTimeField(default=timezone.now)
    ...

    class Meta:
        unique_together = ('show', 'seat', 'showtime')

您现有的 class Ticket 将有一个用于 showAndSeat 的外键(唯一的限制是您必须使用一些 cron 创建 showAndSeat )

将现有视图更改为

def book_tickets_by_show(self, request, show_id=None):
    ....
    ...
    ...
    try:
        with transaction.atomic():
           seat_list_from_user = [1,2,3,4] # get the list from the request
           lock_ticket = showAndSeat.objects.select_for_update(nowait=True).filter(seat__number__in=seat_list_from_user,show = selected_show_timings)
           serializer = TicketSerializer(data=data_list, many=True)
            if serializer.is_valid():
                serializer.save()
           return GOOD_Response()
    except  DatabaseError :
        # Tickets are locked by some one else 
    except (Show.DoesNotExist, IntegrityError) as e:
        return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)        
    except :
        # some other unhandled error 
        return BAD_RESPONSE()