Roblox GuidebookБаза знаний
Русский ⌄

Разработка / ROBLOX

UpdateAsync в Roblox: почему два сохранения могут потерять одно изменение

Разбираем вымышленный счётчик мастерской: два сервера прочитали 100, каждый добавил 5, а в хранилище осталось 105. Покажем различие между заменой значения и преобразованием свежего состояния, составим журнал конфликта и проверим чистую функцию локально.

Обновлено:

Не начинай с обвинения кнопки сохранения #

Представь общий учебный счётчик доставок. Два независимых обработчика должны добавить по пять единиц. На экране каждый сообщает о выполненной работе, но итоговое значение увеличилось только один раз. Такой симптом может появиться, если оба обработчика рассчитали новую запись из одинаковой старой копии. Он не доказывает конкретную причину в твоей игре: сначала нужен журнал действий и условий записи.

Этот материал продолжает тему первого сохранения, но решает другую задачу — зависимость новой записи от значения, которое мог изменить другой сервер. Мы используем условный ключ учебного счётчика, а не кошелёк игрока, покупку или полный профиль. Все числа и последовательности ниже придуманы для объяснения. Два настоящих Roblox-сервера в рамках этой статьи не запускались.

Нарисуй последовательность до исправления #

Запиши пять строк: в хранилище 100; обработчик A читает 100; обработчик B читает 100; A записывает 105; B записывает свои 105. Во второй записи нет намерения украсть прогресс. Просто B заменяет значение результатом расчёта, выполненного до изменения A. После двух прибавлений ожидается 110, но две одинаковые замены оставляют 105.

Сохрани этот рисунок рядом с задачей. Он помогает различить «обработчик вообще не вызвался», «вызов завершился ошибкой» и «оба вызова использовали устаревшее основание». Добавь в журнал название операции, идентификатор учебного задания, старое значение и предлагаемый результат. Не собирай пароли или приватные данные ради диагностики; для локального упражнения достаточно вымышленных идентификаторов.

Старая копия теряет изменениеОткрыть изображение крупнее ↗
Вымышленный порядок двух замен. Это объяснение потери изменения, а не журнал настоящих серверов.
ШагДействиеВ хранилище
1Начало100
2A читает 100100
3B читает 100100
4A записывает 105105
5B записывает 105105

Замена и преобразование — разные намерения #

SetAsync задаёт значение ключа без предварительного чтения в рамках этого метода. Такой выбор соответствует намерению заменить запись заранее известным значением, которое не зависит от её прошлого состояния. Если же требуется прибавить число к текущему счётчику, основание расчёта имеет значение. Документация Roblox рекомендует UpdateAsync для изменений, зависящих от текущего состояния, и при возможной записи из нескольких серверов.

Нельзя исправить пример простой заменой имени метода, оставив внутри callback возврат заранее вычисленного 105. В таком варианте результат по-прежнему получен из старой копии. Преобразование должно использовать current, переданный самому callback. Объясни это словами прежде, чем переносить решение на таблицу инвентаря: «предложить новую запись на основании свежего значения» отличается от «повторить мой старый снимок».

Callback может быть вызван повторно #

Если другой сервер изменил ключ между чтением значения и попыткой записи, UpdateAsync может вызвать функцию преобразования снова и отбросить прежний результат. Поэтому один вызов метода не означает один запуск callback. В нашем условном порядке A предлагает 105, B успевает сохранить 105, а затем A рассчитывает новое предложение уже из 105 и получает 110.

Повтор функции преобразования не должен заново выдавать предмет, отправлять наградное событие или изменять внешний объект игры. Callback рассчитывает предлагаемую запись. Игровые последствия требуют отдельного решения и подтверждённого состояния. Даже перенос выдачи предмета после успешного вызова сам по себе не доказывает, что одна бизнес-операция не будет повторно запрошена другим обработчиком.

Пересчёт из свежего значенияОткрыть изображение крупнее ↗
Вымышленный пересчёт после другого писателя. Локальная модель не подтверждает реальную работу сети Roblox.

Учебная функция без скрытого состояния #

Наш ModuleScript CounterTransform содержит Add(current, delta). Для отсутствующей записи он использует ноль, принимает целые неотрицательные значения и положительную прибавку, ограничивает результат миллионом. Эти правила выбраны для учебного счётчика. В другой игре допустимые значения и начальное состояние могут отличаться; копировать их как универсальные правила профиля нельзя.

Функция возвращает новое число и не меняет внешние переменные. На одинаковых входах она даёт одинаковый результат. Переданное неверное значение, отрицательная прибавка, дробь или превышение ограничения возвращают nil. У функции нет сети, ожиданий, выдачи наград и обращения к игроку. Перед публикацией мы проверили её локальным Luau-тестом, но это не проверка реального Roblox-хранилища.

-- Pure transform for an isolated teaching counter, not a player profile.
local Transform = {}
local LIMIT = 1000000

local function validInteger(value)
    return type(value) == "number" and value == math.floor(value)
        and value >= 0 and value <= LIMIT
end

function Transform.Add(current, delta)
    if not validInteger(delta) or delta == 0 then return nil end
    if current == nil then current = 0 end
    if not validInteger(current) then return nil end
    if delta > LIMIT - current then return nil end
    return current + delta
end

return Transform

Где подключить преобразование #

В отдельной тестовой experience помести CounterTransform в ServerScriptService. Серверный Script получает DataStoreService, выбирает изолированное хранилище учебного счётчика и подключает модуль через require. Внутри UpdateAsync передай callback, который возвращает CounterTransform.Add(current, 5). Не вставляй в него task.wait, загрузку ресурса или другой вызов, который может приостановить выполнение.

Сам вызов UpdateAsync является сетевой операцией: оберни его в pcall и отдельно проверь возвращённое значение. Успешное завершение pcall говорит об отсутствии пойманной ошибки. Если callback отменил запись через nil, это не подтверждение сохранения новой пятёрки. Имя учебного хранилища не делает тест автоматически безопасным, если ты всё равно работаешь в production experience с реальными ключами.

-- Server Script in an isolated test experience only.
-- This demonstrates one numeric teaching key, not a player profile.
local DataStoreService = game:GetService("DataStoreService")
local CounterTransform = require(
    game:GetService("ServerScriptService"):WaitForChild("CounterTransform")
)
local store = DataStoreService:GetDataStore("GuidebookCounterExercise_v1")

local ok, result = pcall(function()
    return store:UpdateAsync("CounterExercise", function(current)
        return CounterTransform.Add(current, 5)
    end)
end)

if not ok then
    warn("Write response failed; outcome is not established", result)
elseif result == nil then
    warn("Update cancelled; no new saved counter was returned")
else
    print("Updated teaching counter", result)
end

Отмена не должна становиться обнулением #

Возврат nil из callback отменяет обновление. В упражнении это полезно, когда текущая запись имеет неожиданный тип или новое число выходит за предел. Не заменяй повреждённую строку нулём только ради продолжения: так можно скрыть проблему данных и переписать значение, которое требует исследования. Отсутствующая запись и запись неправильного формата — разные случаи.

Для наблюдаемой диагностики зафиксируй, что результат не соответствует ожидаемому сохранённому числу, и останови дальнейшие действия этого учебного маршрута. Наша чистая функция не сохраняет подробный текст причины; это ограничение компактного примера. Если нужен журнал причин отказа, проектируй его так, чтобы повтор callback не становился повторной игровой операцией и не менял основание следующего расчёта.

Проверка на бумаге и локальный тест #

Сначала повтори плохой порядок со старой копией: прочитать 100 дважды, вычислить два раза 105, записать обе замены. Затем повтори хороший порядок пересчёта: подготовить предложение A, применить изменение B, отбросить старое предложение A и рассчитать его из 105. Последняя запись должна содержать 110. Это детерминированная модель двух порядков, а не эмуляция всех внутренних правил Roblox.

Отдельно проверь отсутствие записи, нормальное число, строку вместо числа, дробь, отрицательное значение, бесконечность и границу ограничения. Включи повтор вызова с теми же входами: результат функции не должен увеличиваться из-за скрытого состояния. Передача таблицы вместо числа не должна изменять эту таблицу. Фактический локальный тест проверяет именно эти утверждения; он не доказывает производственную устойчивость сервера.

Локальная проверкаОжидаемый результат
Нет записи; добавить 55
Текущее 100; добавить 5105
Текущее 105; добавить 5110
Строка вместо числаnil: отмена
999995; добавить 51000000
999996; добавить 5nil: отмена

Повтор callback и повтор операции не одно и то же #

Повтор callback внутри одной попытки согласует предложение с изменившимся состоянием. Второй самостоятельный запрос «добавить пять» — другое действие: он может добавить ещё пять даже при использовании UpdateAsync. Поэтому этот метод нельзя выдавать за готовую защиту от двойной награды, повторного заказа или повторной обработки покупки.

Для такой бизнес-задачи нужна отдельно определённая идентичность операции и правило, как проверить её уже выполненное состояние вместе с изменением нужных данных. Здесь мы этого механизма не реализуем. Учебный ключ хранит только число, поэтому в нём нет истории идентификаторов. Когда другой чат будет проектировать экономику игры, передай ему это ограничение вместе с кодом, чтобы демонстрацию не приняли за готовый кошелёк.

Ошибка ответа оставляет отдельный вопрос #

При ошибке сетевого вызова нельзя автоматически заключать, что backend ничего не записал. Документация Roblox отдельно описывает записи с неизвестным исходом: сервер может не получить успешный ответ после фактической записи. Безусловный новый запрос «прибавить пять» тогда способен повторить самостоятельную операцию. Это отличается от внутреннего повторного расчёта callback.

Не добавляй бесконечный цикл повторов в пример. Для настоящей системы сначала определи обработку неизвестного исхода и порядок операций одного ключа. Затем выбирай ограниченные повторы временных ошибок и задержки по подходящим правилам. Наш локальный тест не вызывает сетевой сбой Roblox и не подтверждает решение этой задачи. Такой отказ остаётся отдельным пунктом будущего проектирования.

Одного метода недостаточно для полного профиля #

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

Не используй учебный счётчик для Robux-покупок или настоящих игровых денег. При переносе на production нужны отдельная схема данных, проверки совместимости, обработка отказов и требования к выдаче наград. Начни с официальных рекомендаций по player data и purchasing, а затем проверяй конкретный проект. Мы предлагаем узкий эксперимент, который помогает понять преобразование, а не библиотеку управления профилями.

Что сохранить для следующего разработчика #

Передай рисунок плохого и хорошего порядка, название изолированного ключа, правила числа и список пройденных локальных проверок. Подпиши отдельно: «реальные серверы и сетевые конфликты не тестировались». Добавь нерешённые задачи: неизвестный исход записи, повтор бизнес-операции, владение сессией и сохранение остальных полей профиля.

Когда будешь сравнивать варианты, проси разработчика объяснить, из какого значения он рассчитывает новую запись и какие действия происходят вне callback. Правильный ответ должен быть проверяемым на конкретной последовательности. Если итог выглядит верно только при отсутствии второго писателя, задача конфликта ещё не решена. Сохрани этот гайд вместе с первым руководством по сохранению, чтобы различать начальный доступ к хранилищу и дальнейшее согласование изменений.

Первоисточники

Roblox Creator Hub — Data stores
Roblox Creator Hub — GlobalDataStore
Roblox Creator Hub — Data store best practices