【问题标题】:Django @transaction.atomic() to prevent creating objects in concurrencyDjango @transaction.atomic() 防止并发创建对象
【发布时间】:2018-09-29 00:14:46
【问题描述】:

我有一个票模型,以及它的票序列化器。票证模型有一个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)

我想知道的是,它是否有助于防止为同一个座位创建票证而调用多个请求?

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

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

【问题讨论】:

    标签: django django-database


    【解决方案1】:

    这是否有助于防止为同一个座位创建票证时调用多个请求。

    是的,它会的。 unique_together 约束加上transaction.atomic() 将确保您不能为同一个座位/节目创建两张票。

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

    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)
      

    【讨论】:

      【解决方案2】:

      您应该使用显式分布式锁来同步请求,而不是依赖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()
      

      【讨论】:

      • 好的。但这不会锁定其他用户创建不同座位票的请求吗?假设用户 1 想要座位 1 和 2 的票。用户 B 想要座位 3 和 4 的票。然后锁定请求本身将阻止用户 B,即使他的座位与用户 A 不同。我的理解是否正确?
      • 值得注意的是,这种方法仅在您只运行一个服务器时才有效
      【解决方案3】:

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

      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 将有一个外键来显示AndSeat(唯一的限制是你必须使用一些 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()
      

      【讨论】:

        猜你喜欢
        • 2020-03-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-06-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-13
        • 1970-01-01
        相关资源
        最近更新 更多