Идемпотентное создание записей в AppEngine с возможностью повторной проверки клиента с учетом согласованных запросов и т. Д.

Мне нужно придумать стратегию обработки клиентских повторов при создании записи в хранилище данных:

  • Клиент отправляет запрос на создание новой записи в базе данных
  • Сервер выполняет создание записи и готовит успешный ответ
  • Происходит какая-то ошибка, из-за которой клиент полагает, что запрос не был обработан (потеря пакета, ...)
  • Клиент отправляет тот же запрос, чтобы снова создать новую запись в базе данных
  • Сервер обнаруживает повторные попытки, воссоздает и отправляет оригинальный ответ, не создавая другую запись в хранилище данных.
  • Клиент получает ответ
  • Все довольны и только одна запись была создана в базе данных

У меня есть одно ограничение: сервер не работает! У него нет информации о сеансе на клиенте.

Моя текущая идея заключается в следующем:

  • Пометьте каждый запрос на создание гарантированным глобально уникальным идентификатором (вот как я их создаю, хотя они не слишком актуальны для вопроса):
    • Используя хранилище данных (и memcache), я назначаю уникальный монотонно увеличивающийся идентификатор каждому экземпляру сервера после его загрузки (назовем его SI)
    • Когда клиент запрашивает начальную страницу, экземпляр, который обслуживал запрос, генерирует уникальный монотонно увеличивающийся идентификатор загрузки страницы (PL) и отправляет SI.PL клиенту вместе с содержимым страницы.
    • Для каждого запроса на создание клиент генерирует уникальный монотонно увеличивающийся идентификатор запроса (RI) и отправляет SI.PL.RI вместе с запросом на создание.
  • Для каждого запроса создания сервер сначала проверяет, знает ли он тег создания
  • Если нет, то он создает новую запись и как-то сохраняет вместе с ней create-tag.
  • Если он знает тег, он использует его для поиска первоначально созданной записи и создает соответствующий ответ

Вот варианты реализации, о которых я сейчас думаю, и их проблемы:

  1. Сохраните тэг create как индексированное свойство внутри записи:
    • Когда сервер получает запрос, он должен использовать запрос, чтобы найти любую существующую запись
    • Проблема: так как запросы в AppEngine только в конечном итоге согласованы, он может пропустить запись
  2. Используйте тег create в качестве ключа записи:
    • Должно быть в порядке, так как гарантированно будет уникальным, если числа не переносятся (вряд ли с длинными)
    • Незначительные неудобства: это увеличивает длину ключей записей при любом будущем использовании (ненужные накладные расходы)
    • Основная проблема: это приведет к созданию последовательных ключей ввода в хранилище данных, которых следует избегать любой ценой, поскольку это создает горячие точки в хранимых данных и, таким образом, может значительно повлиять на производительность.

Одно из решений, которое я рассматриваю для варианта 2, состоит в том, чтобы использовать какую-то формулу, которая берет последовательные числа и перераспределяет их в уникальную, детерминированную, но случайную на вид последовательность вместо устранения горячих точек. Любые идеи о том, как такая формула может выглядеть?

Или, может быть, есть лучший подход в целом?

2 ответа

Решение

Хотя возможно реализовать вариант реализации 1 ( сверху), используя продуманную структуру данных, правильные транзакции и сборщик мусора, будет довольно сложно заставить его работать надежно.

Таким образом, похоже, что правильное решение - использовать обобщенную версию option (2):

Используйте некоторый уникальный идентификатор для созданного объекта в качестве ключа объекта.

Таким образом, если тот же объект создается снова при повторной попытке, либо легко найти существующую копию (как gets строго последовательны), или если вы будете писать вслепую, это просто перезапишет первую версию.

@Greg предложил в комментариях использовать хеш поверх уникально идентифицирующих данных объекта в качестве ключа. Хотя это решает проблему наличия ключей, которые равномерно распределяются по пространству параметров и, таким образом, приводит к эффективному распределению данных по физическим местам хранения, это создает новую проблему необходимости управлять (или игнорировать) хеш-коллизиями, особенно если кто-то пытается не дать ключам стать очень длинными.

Есть способы справиться с этими столкновениями. Например: в случае коллизии сравните фактическое содержимое, чтобы проверить, действительно ли оно является дубликатом, и, если нет, добавьте "1" к ключу. Затем посмотрите, существует ли этот ключ, и, если так, проверьте еще раз, имеет ли он тот же контент. Если нет, то вместо этого добавьте "2", еще раз проверьте наличие коллизий и т. Д. Хотя это работает, все становится довольно грязно.

Или вы можете просто сказать, что коллизии хешей настолько редки, что в базе данных никогда не будет достаточно пользовательских данных, чтобы увидеть их. Лично мне не нравятся подобные виды "держи пальцы скрещенными", но во многих случаях это может быть приемлемым способом.

Но, к счастью, у меня уже есть глобальный уникальный идентификатор данных без столкновений: тэг create. И оказывается, что две проблемы, которые я видел при его использовании, легко устраняются путем некоторого умного перетасовки:

Используя те же идентификаторы, что и в исходном вопросе, мой тег создания SI.PL.RI состоит из SI, который будет постоянно увеличиваться, PL, который сбрасывается в 0 каждый раз при создании нового экземпляра сервера, и RI, который сбрасывается для каждого новый клиентский сеанс. Таким образом, RI, вероятно, всегда крошечный, PL будет оставаться небольшим, а SI будет постепенно увеличиваться.

Учитывая это, я мог бы, например, построить ключ следующим образом (начиная с самых значимых битов):

- Lowest 10 bits of PL
- Lowest  4 bits of RI
- Lowest 17 bits of SI
- 1 bit indicating whether there are any further non-zero values
- Next lowest 10 bits of PL
- Next lowest  4 bits of RI
- Next lowest 17 bits of SI
- 1 bit indicating whether there are any further non-zero values
- ... until ALL bits of RI, PL, and SI are used (eventually breaking 10-4-17 pattern)

Таким образом, сгенерированные ключи хорошо распределяются по пространству параметров, если они отсортированы в лексическом порядке (как это делает AppEngine), и первые ключи имеют только половину длины автоматически сгенерированных, и они становятся длиннее только по мере необходимости.

В сторону 1:

Фактически, если ни один экземпляр сервера никогда не остается в живых достаточно долго, чтобы обслуживать более тысячи загрузок страниц, и ни один клиент никогда не создает более 16 новых объектов в одном сеансе, и экземпляры сервера не создаются быстрее, чем один раз в 5 минут. в среднем, пройдет больше года, прежде чем ключи получат в среднем более 4 байтов.

И если ни один экземпляр сервера никогда не остается в живых достаточно долго, чтобы обслуживать более миллиона загрузок страниц, и ни один клиент никогда не создает более 256 новых объектов в одном сеансе, и экземпляры сервера не создаются быстрее, чем в среднем каждую секунду, это пройдет еще 500 лет, прежде чем ключи получат в среднем более 8 байт (и, следовательно, дольше, чем автоматически сгенерированные). Все должно быть в порядке...:)

В сторону 2:

Если мне нужно использовать ключи для индексации Java HashMap, hashCode() функция моего ключевого объекта может вместо этого возвращать целое число, построенное из первых 4 байтов ключа в обратном порядке, чтобы распределить ключи по сегментам.

Как назначить ключ новому объекту?

Если вы создаете ключ самостоятельно, проблема решена. Повторяющаяся сущность просто перезапишет существующую сущность, поскольку она имеет тот же ключ. Примером может служить создание сущности продукта, в которой для генерации ключа используется SKU продукта.

Если ключ назначен хранилищем данных, то, когда истечет время ожидания запроса, покажите сообщение об ошибке пользователю и перезагрузите данные клиенту. Затем пользователь увидит, была ли сущность уже создана.

Это не так красиво, как "случайные последовательности", но проще и надежнее:)

Другие вопросы по тегам