Система инвентаря: что ломается при параллельных операциях
Классическая система инвентаря работает по схеме «прочитал — изменил — записал»: запрос берёт текущее состояние предмета, меняет его в памяти и сохраняет обратно. Пока обращения идут по одному, это незаметно. При одновременных обменах или крафте оба запроса читают одну и ту же версию данных, и второй молча перезаписывает результат первого.
На совместном крафте это даёт минусовые остатки: два клиента списывают последний экземпляр ресурса, сервер подтверждает обе операции. Обмены дают зеркальный эффект — один предмет уходит двум получателям, когда списание и выдача не выполняются как атомарные транзакции предметов. Проверка владения проходит успешно у обоих, потому что между проверкой и записью состояние ещё не изменилось.
Дубли предметов приходят и с повторными запросами. Игрок нажимает покупку, ответ не доходит из-за обрыва связи, клиент повторяет запрос, а сервер обрабатывает его как новый: без идентификатора операции начисление проходит дважды, и в инвентаре появляется второй экземпляр.
Потеря покупок при обрыве связи — обратная сторона той же механики: платёж подтверждён платформой, а запись в инвентарь откатилась или не дошла. Клиент показывает старое состояние из локального кеша, рассинхрон клиента и сервера накапливается и расходится при каждом входе, пока серверное состояние не станет для игрока неожиданным.
Дальше включается ручной разбор: поток обращений в поддержку растёт, правки вносят прямо в живую базу без трассировки, кто и когда менял запись. Разбираем, как это закрывается.
Что даёт атомарность операций: целостность, скорость, отказы
Атомарность — это когда обмен, крафт или передача предметов фиксируются целиком либо не фиксируются вовсе. Состояния, в котором предмет исчез у одного игрока и не появился у другого, не остаётся даже при обрыве связи в момент записи. Для проекта это разница между единичным обращением в поддержку и волной жалоб после массового события.
Целостность при массовых обменах, крафте и передаче предметов
Каждая операция проходит серверную проверку и ложится в журнал изменений одной записью: списание и начисление идут внутри общей транзакции с блокировкой затронутых ячеек. Крафт, расходующий несколько стопок, и обмен между игроками подчиняются одному правилу, поэтому «половина» рецепта примениться не может. Идемпотентность запросов закрывает повторную отправку: если игрок не дождался ответа и нажал ещё раз, второе списание не произойдёт — повтор распознаётся по ключу операции, и в инвентаре остаётся ровно одно изменение.
Отклик под пиковой нагрузкой и уход класса отказов
На пиковой нагрузке — старт сезона, ивент, раздача наград — узким местом становится не сама логика, а конкуренция за одни и те же ячейки. Мы разносим блокировки по минимальному набору объектов и держим частые операции в памяти, а тяжёлые — в очереди с последовательной обработкой. Время отклика на обычные действия с инвентарём при этом не растёт вместе с числом игроков.
Класс отказов меняется качественно: вместо «предмет пропал» или «предмет задублировался» остаются только отказы вида «операция отклонена, состояние не изменилось». Такие случаи игрок видит как понятное сообщение и может повторить действие, а не как потерю прогресса. На приёмке это проверяется просто: расхождения после массовых обменов, доля дубликатов и время отклика на пике — три метрики, по которым видно, что система инвентаря выдерживает нагрузку живой игры.
Как устроена система инвентаря: режимы хранения и синхронизации
Выбор между журналом событий и текущим состоянием
Журнал событий хранит каждое действие игрока отдельной записью и позволяет восстановить инвентарь на любой момент времени. Текущее состояние держит готовый снимок — читается дешевле, но история изменений теряется, если не писать её отдельно. Чаще берут гибрид: снимок как источник для отдачи клиенту плюс журнал последних операций для разбора спорных ситуаций. Версионирование записи — номер версии у каждого изменения — даёт откат к предыдущему состоянию и защищает от повторного применения одной и той же операции.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Журнал событий | Нужна полная история, разбор жалоб, откат | Растёт объём, чтение требует пересборки |
| Текущее состояние | Частое чтение, простой инвентарь | Нет истории, откат только из резервной копии |
| Снимок + журнал | Баланс скорости чтения и разбираемости | Сложнее схема и перенос данных |
Где проходит граница между клиентом и сервером
Клиент рисует инвентарь и показывает результат сразу, но не является источником истины: любое изменение подтверждается сервером. Сервер валидирует операцию, ставит блокировку операций игрока на время транзакции и только затем фиксирует изменение — так два параллельных запроса не спишут один предмет дважды. Если подтверждение не пришло, интерфейс откатывает показанный результат и перечитывает состояние с сервера.
Этапы внедрения: от аудита операций до передачи регламентов
Внедрение системы инвентаря начинается не с кода, а с разбора того, как предметы сейчас попадают к игроку и что с ними происходит дальше. Мы описываем каждую операцию — выдача за награду, покупка, обмен, расход, возврат при отмене — и отмечаем места, где баланс может разойтись: обрыв связи, повторный запрос, одновременные действия с одного аккаунта.
Аудит текущих операций и модели данных
- Собираем перечень операций и правил: лимиты на предметы, сроки жизни, привязка к событию или сезону, кто инициирует изменение — клиент, игровой сервер или внешний сервис.
- Разбираем текущую модель данных: где хранится состояние, как связаны предметы и стеки, есть ли журнал изменений и можно ли по нему восстановить инвентарь на любой момент.
- Проектируем целевую схему: журнал операций с уникальным ключом запроса, чтобы повторная отправка не выдавала предмет дважды, и правила блокировок при параллельных покупках.
Проверка на сбоях и повторных запросах
- Прогоняем сценарии отказов: обрыв связи в момент списания, дублирование запроса, одновременная покупка последнего предмета, падение сервиса между списанием и выдачей — состояние обязано сходиться.
- Переносим реестр на новую схему в теневом режиме: старый и новый расчёт идут параллельно, расхождения сверяются построчно, откат доступен до момента переключения.
- Передаём регламент эксплуатации: метрики расхождений, оповещения на аномалии, порядок ручных правок с обязательной записью в журнал аудита и понятный откат на предыдущую версию.
Комплект передачи: схемы данных, политики доступа, регламенты
Комплект передачи собираем так, чтобы команда заказчика развивала систему инвентаря без нас: новый инженер по нему за день понимает структуру предметов, а дежурный — что делать при расхождении балансов. Всё описанное проверено на рабочем контуре, а не восстановлено по памяти на выходе из проекта.
- Схема данных предметов: сущности, связи, правила стекирования, идентификаторы экземпляров и запреты, которые нельзя нарушать.
- Политики доступа и роли администраторов: матрица прав на выдачу, изъятие и правку балансов.
- Набор тестов на целостность: двойное списание, параллельная выдача, откат незавершённой транзакции.
- Документация по восстановлению и сверке: точки восстановления, порядок действий при сбое, признаки сходимости.
- Регламенты эксплуатации: ротация ключей, выдача прав, эскалация инцидентов.
- Журнал изменений схемы: версии, миграции, совместимость ранее сохранённых инвентарей.
Политики доступа и роли администраторов
Права разводим по минимальной достаточности: рядовой администратор выдаёт предметы по заявке, старший правит балансы, изменение схемы доступно узкому кругу. Каждое действие попадает в неизменяемый журнал с автором — спор «кто выдал» закрывается за минуту.
Документация по восстановлению и сверке
Описываем два сценария: откат до целостного состояния и сверку после частичного сбоя. Указаны порядок действий, ответственные и признаки, по которым дежурный понимает, что инвентарь снова сходится.
Кейс: убрали дубли предметов и потери покупок под нагрузкой
Проект — мобильная игра с внутриигровой экономикой: предметы, наборы, покупки за внутреннюю валюту и за платежи. К нам пришли после серии инцидентов на сезонных событиях, когда выросший онлайн обнажил слабое место системы инвентаря.
Что было до: ручная сверка и поток обращений
Учёт предметов шёл в двух местах: часть состояния держалась в профиле игрока, часть — в очереди операций. При обрыве связи между списанием и записью игрок мог получить предмет дважды или потерять оплаченную покупку. Поддержка каждую неделю собирала жалобы, а инженеры вручную сверяли реестр с журналом операций — на сверку уходил рабочий день, и до половины расхождений находили уже по обращениям. В первые минуты события число таких жалоб вырастало кратно.
Что стало после: метрики и снятые риски
Выдачу предметов перевели на атомарные транзакции с ключом операции: повторный запрос не создаёт вторую запись, а возвращает результат первой. Проект работает на Unity, но серверную часть инвентаря мы держим независимой от клиента — обновление игры не ломает учёт.
По итогам контрольного периода: дубли предметов в реестре — ноль, обращения по потерянным покупкам сократились примерно в шесть раз, автоматическая сверка реестра занимает минуты вместо рабочего дня. Нагрузочный пик прошёл без расхождений между реестром и журналом операций. Снятые риски — двойная выдача на повторных запросах, потеря оплаченной покупки при обрыве связи, расхождение балансов между сервисами.
Подтверждающие источники — панели мониторинга, журнал транзакций и выгрузки обращений поддержки за сопоставимые периоды до и после.
Чем атомарная транзакция отличается от блокировки записи?
Блокировка записи разводит операции во времени: первый поток захватывает запись, остальные ждут освобождения. Атомарная транзакция отвечает за другое — набор изменений применяется целиком либо откатывается, не оставляя половину состояния. Поэтому выбор не между ними, а в том, какие блокировки живут внутри транзакции и как долго.
При конфликте операций пессимистичный подход заставляет второго участника ждать, и на массовых операциях отклик растёт вместе с длиной очереди — вплоть до отказов по времени. Оптимистичный подход не ждёт: он читает состояние с версией, при записи сверяет её и, если данные уже изменили, повторяет операцию заново. Быстрый путь — один проход, дорогой — повтор.
Выбор зависит от частоты конфликтов на одну строку. Пока за один предмет конкурируют редко — награды приходят в разные ячейки, — оптимистичный подход даёт нулевые ожидания и почти не тратит ресурс на повторы. Если же несколько потоков постоянно пишут в один и тот же стек, повторы идут лавиной, и пессимистичный подход с коротким удержанием блокировки предсказуемее.
Для инвентаря важно, чтобы обмен, сборка предмета из ресурсов и передача между игроками шли одной атомарной транзакцией: частично применённый обмен хуже отказа. Мы ограничиваем число повторов, ведём ключ идемпотентности против дублей при повторной отправке запроса и следим за долей конфликтов — она подсказывает, когда схему пора менять.
Ограничения и подводные камни: частые технические вопросы
Перед стартом технические сомнения касаются не логики предметов, а того, как новый слой уживается с работающим сервером. Ниже — то, что чаще всего всплывает на оценке: включение на живом проекте, накопленные дубли, обрывы связи, миграция и права доступа.
Можно ли внедрять без остановки игры?
Да, остановка не нужна. Новый слой выносится отдельным сервисом со своей схемой данных, старая логика продолжает обслуживать инвентарь. Включение идёт за переключателем и сначала на часть игроков — если проверки расходятся с ожидаемым, переключатель возвращается назад.
Что делать с уже накопленными дублями?
Их разбирают до включения. Сверка идёт по журналу операций: для каждой позиции восстанавливается цепочка начислений и определяется один корректный остаток. Лишнее списывается отдельной операцией с записью в аудит, чтобы историю можно было объяснить игроку и поддержке.
Что произойдёт при обрыве связи или повторной отправке запроса?
Каждая операция несёт уникальный идентификатор: повторный запрос не создаёт второй предмет — сервер распознаёт его и возвращает результат первого. Транзакция либо подтверждается целиком, либо откатывается, поэтому состояния «списано, но не начислено» не возникает.
На ком миграция и как разграничиваются права?
Миграцию ведём мы: переносим остатки, сверяем сходимость до и после, держим путь отката. От заказчика нужны доступ к хранилищу и человек, подтверждающий правила по спорным предметам. Права делим по ролям: поддержка видит историю, но не меняет остатки; начисления в пределах лимита — у экономистов; справочник предметов — у администраторов.
Обсудим задачу: что прислать для оценки и плана работ
Чтобы вернуться с оценкой работ, а не с вилкой «от и до», нужны исходные данные по системе инвентаря: правила выдачи и списания предметов, ограничения на конкурентные операции, целевые платформы. Чем конкретнее вводные, тем точнее план внедрения и состав команды.
- Правила инвентаря: категории предметов, стеки, лимиты слотов, привязка к персонажу или аккаунту.
- Схема данных и текущее хранилище: структура таблиц, где хранится состояние, есть ли журнал операций.
- Требования к атомарности: какие операции должны проходить целиком — продажа, обмен, создание предмета, награда за бой — и что происходит при обрыве связи.
- Сценарии нагрузки: пиковое число одновременных игроков, частота операций, сезонные всплески.
- Целевые платформы и клиентские ветки: где приложение уже выпущено, какие ограничения есть у магазинов.
- Доступы: репозиторий, тестовое окружение, выгрузка обезличенных данных для проверки гипотез.
- Ограничения проекта: сроки, этапность выпуска, кто на стороне заказчика принимает решения по правилам игры.
Дальше мы собираем это в порядок работ: сначала контур данных и транзакционная модель, затем серверная логика, затем клиентская часть и проверки на конкурентных сценариях. На выходе — оценка работ, план внедрения по этапам и состав команды под ваш объём. Опишите задачу — вернёмся с оценкой и планом.







