Обработка состояния гонки в model.save()
Как следует обрабатывать возможное состояние гонки в методе модели save()
?
Например, следующий пример реализует модель с упорядоченным списком связанных элементов. При создании нового элемента текущий размер списка используется как его позиция.
Из того, что я могу сказать, это может пойти не так, если одновременно создаются несколько элементов.
class OrderedList(models.Model):
# ....
@property
def item_count(self):
return self.item_set.count()
class Item(models.Model):
# ...
name = models.CharField(max_length=100)
parent = models.ForeignKey(OrderedList)
position = models.IntegerField()
class Meta:
unique_together = (('parent','position'), ('parent', 'name'))
def save(self, *args, **kwargs):
if not self.id:
# use item count as next position number
self.position = parent.item_count
super(Item, self).save(*args, **kwargs)
Я столкнулся с @transaction .commit_on_success()
, но это, похоже, применимо только к представлениям. Даже если это применимо к методам модели, я все равно не знаю, как правильно обрабатывать неудачную транзакцию.
Я все время обрабатываю его так, но он больше похож на хак, чем на решение
def save(self, *args, **kwargs):
while not self.id:
try:
self.position = self.parent.item_count
super(Item, self).save(*args, **kwargs)
except IntegrityError:
# chill out, then try again
time.sleep(0.5)
Любые предложения?
Обновление:
Другая проблема с вышеупомянутым решением состоит в том, что цикл while
никогда не будет заканчиваться, если IntegrityError
вызван конфликтом name
(или любым другим уникальным полем, если это важно).
Для записи, вот что я до сих пор делал, что мне нужно:
def save(self, *args, **kwargs):
# for object update, do the usual save
if self.id:
super(Step, self).save(*args, **kwargs)
return
# for object creation, assign a unique position
while not self.id:
try:
self.position = self.parent.item_count
super(Step, self).save(*args, **kwargs)
except IntegrityError:
try:
rival = self.parent.item_set.get(position=self.position)
except ObjectDoesNotExist: # not a conflict on "position"
raise IntegrityError
else:
sleep(random.uniform(0.5, 1)) # chill out, then try again
Ответы
Ответ 1
Это может показаться вам взломанным, но для меня это похоже на законную и разумную реализацию "оптимистического подхода concurrency" - попробуйте сделать что угодно, обнаружите конфликты, вызванные условиями гонки, если это произойдет, повторите попытку немного позже. Некоторые базы данных систематически используют это вместо блокировки, и это может привести к значительно лучшей производительности, за исключением систем с большим количеством загрузок (что довольно редко встречается в реальной жизни).
Мне это очень нравится, потому что я рассматриваю его как общий пример принципа Хоппера: "легко просить прощения, чем разрешение", которое широко применяется в программировании (особенно, но не исключительно в Python), язык Hopper обычно зачисленный, в конце концов, Cobol; -).
Одним из улучшений, которые я бы рекомендовал, является ожидание случайного количества времени - избегайте "условия мета-гонки", в котором два процесса одновременно пытаются найти конфликты и одновременно повторить попытку, на "голод". time.sleep(random.uniform(0.1, 0.6))
или тому подобное должно быть достаточно.
Более совершенное улучшение заключается в увеличении ожидаемого ожидания, если будет достигнуто больше конфликтов - это то, что называется "экспоненциальным откатом" в TCP/IP (вам не пришлось бы удлинять вещи по экспоненте, т.е. постоянным множителем > 1 каждый раз, конечно, но этот подход имеет хорошие математические свойства). Это только гарантировало ограничение проблем для очень загружаемых систем (где множественные конфликты во время попыток записи происходят довольно часто), и это, вероятно, не стоит того, чтобы в вашем конкретном случае.
Ответ 2
Добавить необязательное предложение FOR UPDATE в QuerySets http://code.djangoproject.com/ticket/2705
Ответ 3
Я использую решение Shawn Chin, и это очень полезно. Единственное изменение, которое я сделал, это заменить
self.position = self.parent.item_count
с
self.position = self.parent.latest('position').position
чтобы убедиться, что я имею дело с последним номером позиции (который в моем случае может не быть item_count из-за некоторых зарезервированных неиспользуемых позиций)