Я пишу онлайн-игру, используя Django, и я планирую использовать Postgres в качестве движка базы данных.
Одной из особенностей игры будет симуляция акций. биржевой и товарный рынок. Если пользователь A говорит «Я хочу купить яблоко за 50 долларов», а пользователь B говорит «Я хочу продать яблоко за 50 долларов», то эти два заказа должны быть сопоставлены друг с другом, в результате чего пользователь B должен продать яблоко пользователю A .
Однако я не уверен, как обеспечить, чтобы выполнение заказов было изолированным и не вносило несоответствий.
Я еще не написал никакого реального кода для этой функции , но давайте предположим, что классы моделей выглядят так:
class Account(models.Model):
number_of_dollars = models.BigIntegerField(validators=[MinValueValidator(0)], default=0)
number_of_apples = models.BigIntegerField(validators=[MinValueValidator(0)], default=0)
class AppleOrder(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
price = models.IntegerField(validators=[MinValueValidator(0)], default=0)
direction = models.IntegerField(choices=[(1, 'buy'), (2, 'sell')])
status = models.IntegerField(choices=[(1, 'open'), (2, 'canceled'), (3, 'filled')])
Некоторые веб-запросы могут привести к тому, что игра выполнит поиск заказов, соответствующих друг другу, и выполнит их. Но как я могу убедиться, что заказы выполняются правильно?
Наивно, я хотел бы сделать это так:
def execute_orders(buy_order, sell_order, price)
# We assume that "buy_order" and "sell_order" are matching orders, "price"
# is the correct price to execute the order at, the buyer has sufficient
# money, and the seller has an apple.
buyer = buy_order.account
seller = sell_order.account
with transaction.atomic():
buyer.number_of_dollars = F('number_of_dollars') - price
seller.number_of_dollars = F('number_of_dollars') + price
buyer.number_of_apples = F('number_of_apples') + 1
seller.number_of_apples = F('number_of_apples') - 1
buy_order.status = 3
sell_order.status = 3
buyer.save()
seller.save()
buy_order.save()
sell_order.save()
Ах, но есть проблема! Что если два потока вызывают эту функцию одновременно, для одной и той же пары заказов, и оба потока выполняют одни и те же шаги в одно и то же время? Заказы будут выполнены дважды!
Как я могу предотвратить это?
Одна из моих идей заключалась в том, чтобы при смене статусов заказов на «заполненные» они менялись. используя оператор, такой как UPDATE AppleOrders SET status = 3 WHERE id = $1 AND status = 1
, и проверить количество затронутых строк, чтобы убедиться, что заказ действительно имел статус 1 непосредственно перед тем, как я обновил его. Однако я слышал, что оператор, выполняющийся в транзакции, может видеть старые данные, поэтому, возможно, мой код подумает , что он устанавливает статус на 3, когда на самом деле статус уже был установлен на 3 другой веткой. И, кроме того, я не знаю, как выполнить sh с помощью Django.
Еще одна идея, которая у меня возникла, состояла в том, чтобы запустить эту транзакцию на "сериализуемом" уровне изоляции. Тем не менее, я понимаю, что, когда несколько транзакций выполняются одновременно, они гарантированно сериализуются только в том случае, если все из них работают на "сериализуемом" уровне изоляции. Если один из них находится на уровне сериализации, а другой нет, то я боюсь, что они будут мешать друг другу. Чтобы гарантировать, что все сериализуемо, мне нужно запустить всю игру на уровне сериализации ... что может быть хорошей идеей.
Наконец, я уверен, что смогу прийти придумать какое-то сложное решение, включающее множество дополнительных операций чтения и записи, а также проверки и противовесы для обеспечения того, чтобы все происходило последовательно. Но я знаю, что одновременные модификации очень, очень легко понять неправильно и очень, очень трудно понять правильно.
Какой хороший способ гарантировать, что заказы выполняются атомарно c, последовательным и изолированным способом