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')
- 在票证序列化器上,验证时的序列化器检查是否有具有所需座位和演出的票证
- 如果有票,则检查票是否已购买。
- 如果买了,就会报错。
- 如果没有买,那么在5分钟内查看是否已订票。
- 如果在 5 分钟内被预订,则报错。
- 否则如果预订时间超过 5 分钟,则删除旧票并且 return 有效。
- 如果没有票则return有效
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 创建两张工单。
也就是说,您当前的方法存在几个问题:
我认为没有必要包装整个视图以及在 atomic()
中进行保存的位 - 你不需要两者都做并将整个视图包装在一个事务以性能成本为代价。在交易中包装 serializer.save()
应该就足够了。
不建议在事务中捕获异常 - 请参阅 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()
我有一个票证模型,以及它的票证序列化器。工单模型有一个 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 分钟,则删除旧票并且 return 有效。
- 如果有票,则检查票是否已购买。
- 如果没有票则return有效
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 创建两张工单。
也就是说,您当前的方法存在几个问题:
我认为没有必要包装整个视图以及在
atomic()
中进行保存的位 - 你不需要两者都做并将整个视图包装在一个事务以性能成本为代价。在交易中包装serializer.save()
应该就足够了。不建议在事务中捕获异常 - 请参阅 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()