Django @action.atomic() для предотвращения одновременного создания объектов

У меня есть модель билета, и его сериалазер билетов. Модель билета имеет 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')
  • На сериализаторе билетов проверяющий сериализатор проверяет, есть ли билет с требуемым местом, и показывает
    • Если есть билет, он проверяет, был ли билет куплен или нет.
      • Если это куплено, то это вызовет ошибку.
      • Если его не купили, то проверьте, был ли билет забронирован в течение 5 минут.
        • Если его забронировали в течение 5 минут, то возникнет ошибка.
        • В противном случае, если забронированное время превышает 5 минут, удалите старый билет и верните действительный.
  • Если нет билета, верните действительный

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() чтобы убедиться, что тикет (ы) созданы только в том случае, если все они действительны, или не создавайте ЛЮБОЙ тикет, если он недействителен.

@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)

Я хотел бы знать, поможет ли это в предотвращении вызова более одного запроса для создания билетов / билетов на одно и то же место?

Предположим, пользователь А хочет забронировать билет на места 5,6. Пользователь B хочет забронировать билет на места 3,6, а другой пользователь C хочет забронировать билет на места 2,3,4,5,6.

Будет ли вышеуказанный метод предотвращать бронирование билетов на соответствующие места для всех пользователей и создавать билеты только для одного пользователя (возможно, чья транзакция была первой)? Или, если есть лучший способ, подскажите, пожалуйста, как. Надеюсь, мне было ясно. Если нет, пожалуйста, спросите.

3 ответа

Решение

Поможет ли это в предотвращении вызова более одного запроса для создания тикета (ов) на одно и то же место?

Да, это будет. unique_together ограничение плюс transaction.atomic() гарантирует, что вы не сможете создать два билета на одно место / шоу.

Тем не менее, есть несколько проблем с вашим текущим подходом:

  1. Я думаю, что нет необходимости оборачивать весь вид, а также бит, который делает сохранение в atomic() - вам не нужно делать и то, и другое, а завершение транзакции приводит к снижению производительности. Упаковка serializer.save() в сделке должно быть достаточно.

  2. Не рекомендуется перехватывать исключения внутри транзакции - см. Предупреждение в документации. Также обычно желательно отлавливать исключения как можно ближе к коду, который может их генерировать, чтобы избежать путаницы. Я бы предложил рефакторинг кода в нечто вроде этого.

    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)
    

Вы должны использовать явную распределенную блокировку для синхронизации запросов, а не полагаться на transaction.atomic который не должен быть замком.

Существуют различные способы реализации блокировки, но в нашем проекте 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()

Как насчет следующего создания прерывистой таблицы

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')

Ваш существующий класс Ticket будет иметь внешний ключ для showAndSeat (единственное ограничение - создание showAndSeat с использованием некоторого cron)

Изменить существующее представление на

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()
Другие вопросы по тегам