Django / MySQL LOCK TABLE с транзакцией .atomi c дает точку сохранения не существует - PullRequest
0 голосов
/ 02 мая 2020

У меня есть действия, которые могут иметь несколько держателей 'action', где каждый 'action_holder' может иметь динамическую c роль. Например, «гимнаст-лидер», «студент» и др. c. Эти роли могут быть созданы клиентом и не являются фиксированными. Также «первичный» action_holder_role денормируется в модели, потому что в противном случае фильтрация QuerySet заняла бы много времени (более 2000 операций загружаются на одну страницу, если бы я использовал обычный кеш вместо денормализации базы данных ie, потребовалось бы 20 + секунды вместо 2/3 секунд). Для простоты в примере выбрано много полей. Обратите внимание, что это создает условие гонки, поэтому необходим LOCK TABLE.

class Activity(models.Model):
    data = models.DateField()
    action_holders = models.ManyToManyField(settings.AUTH_USER_MODEL, through='ActivityUserRole', related_name='realactivities')
    cached_primary_action_holder = models.ForeignKey('ActivityUserRole', null=True, blank=True, on_delete=models.SET_NULL, related_name='activity_primary_action_holder_role')
    order = models.IntegerField()

class ActivityUserRole(models.Model):
    role = models.ForeignKey('Role', on_delete=models.PROTECT)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE)
    activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
    primary = models.BooleanField(default=False)  # primary for realactivity

 class Role(models.Model):
    name = models.CharField(max_length=255)

Моя проблема в том, что когда я создаю новое действие, я даю new_action_holder в качестве аргумента для действия и вызываю set_new_action_holder создать соответствующие ActivityUserRoles после сохранения нового действия (поскольку ActivityUserRole нуждается в действии, поэтому оно должно быть сохранено первым).

Получите набор запросов заказа, чтобы заполнить фильтры заказов в держателе new_action. Если я сохраняю одно действие за раз, оно прекрасно работает. Но если я сохраню, скажем, 3 в то же время. Все они получают один и тот же порядок, потому что get_ordering_queryset фиксируется в базе данных до вызова сохранения первого действия.

Я исправил это, добавив:

@contextmanager
def lock_table(read=[], write=[]):
    with transaction.atomic():

        cursor = get_connection().cursor()
        lock = 'LOCK TABLES '

        lock = lock + ', '.join([f'{wri._meta.db_table} WRITE' for wri in write] +
                                [f'{rea._meta.db_table} READ' for rea in read])

        # print(lock)
        cursor.execute(lock)
        try:
            yield
        finally:
            cursor.execute('UNLOCK TABLES')
            cursor.close()

и создав WRITE блокировка Activity, ActivityUserRole и блокировка READ для любой другой связанной таблицы (их много!) с момента, когда я вызываю функцию get_ordering_queryset (), до функции set_new_action_holder. Это прекрасно работает. Порядок теперь правильный. Но а) это похоже на взлом, и б) мне нужно ПРОЧИТАТЬ блокировку 9 таблиц и ЗАПИСЬ блокировать 4 таблицы, частично из-за сигналов post_save на ActivityUserRole и многих других таблицах.

И ... Я не могу больше использовать транзакцию.atomi c. Если я использую его, я получаю «ошибка точки сохранения не существует». Мне действительно нужен транзакции.atomi c, потому что активность должна сохраняться вместе с «ExtraInfoActivity», и в случае сбоя одного из них я хочу, чтобы транзакция откатилась и не было активности без данных ExtraInfoActivity.

Есть ли лучший способ, чем LOCK TABLE?

Есть ли способ использовать транзакцию.atomi c () в сочетании с LOCK TABLE?

1 Ответ

1 голос
/ 05 мая 2020

LOCK TABLE обычно не то, что вы хотите. Он предотвращает все другие записи в эти таблицы, резко снижая параллельную производительность.

Без кода представления сложно точно сказать, как структурировать, но я думаю, что вы хотите использовать select_for_update() на некоторых наборах запросов, которые вы прочитали перед написанием. Это заблокирует только указанные c строки.

(И, кстати, я поддерживаю пакет Django - MySQL с TableLock классом , который реализует LOCK TABLES для тех немногих случаев, когда блокировка таблицы имеет смысл. Ее немного проще использовать, чем ваш фрагмент, так как она находит имена таблиц из классов моделей.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...