开发 / ROBLOX
Roblox UpdateAsync:为什么两次保存会丢失一次修改
两个虚构的工坊服务器都读到 100,各自增加 5,保存结果却只有 105。本文区分覆盖旧快照与转换最新状态,画出冲突顺序,并用本地纯函数测试解释重新计算。示例不连接真实数据存储。
先调查顺序,再判断保存按钮 #
设想一个统计完成配送次数的共享练习计数器。两个独立处理函数分别增加五次,各自报告工作完成,但总数只增加一次。如果它们都根据同一份旧状态计算,就可能出现这个现象。不过,现象本身不能证明你的游戏发生了哪种错误。先记录动作、写入条件和计算依据,再决定修改哪段代码。
本篇延续首次保存教程,但处理的是另一个问题:新值依赖于其他服务器可能修改的状态。我们只使用隔离的教学计数器,不涉及玩家钱包、购买记录或完整档案。下面的数字和执行顺序都是虚构的;没有为了这篇文章启动两个真正的 Roblox 服务器。
画出旧快照覆盖的顺序 #
写下五行:存储为 100;处理函数 A 读取 100;B 读取 100;A 写入 105;B 写入自己算出的 105。B 不必有恶意,也会覆盖掉 A 的增量,因为它的结果在 A 保存以前已经计算完成。两次加五应该得到 110,两次写入相同的 105 却只留下 105。
把这张顺序图放在任务旁边。它能帮助区分处理函数根本没有运行、网络调用失败,以及写入成功但依据过时。局部练习只需记录操作名、虚构订单编号、输入值和建议结果。为了说明这个问题,不需要收集玩家隐私或登录凭据。
| 步骤 | 动作 | 存储值 |
|---|---|---|
| 1 | 开始 | 100 |
| 2 | A 读取 100 | 100 |
| 3 | B 读取 100 | 100 |
| 4 | A 写入 105 | 105 |
| 5 | B 写入 105 | 105 |
覆盖与转换表达不同意图 #
SetAsync 在设置键值时不会在该方法内部先读取旧值。如果要用一个不依赖历史状态的已知值替换记录,这符合覆盖的意图。但要给当前计数器加五,计算依据就十分重要。Roblox 建议在写入依赖当前值或多个服务器可能写同一键时使用 UpdateAsync。
仅替换方法名称,却在 callback 中返回事先算好的 105,并不能修复上述顺序。提议仍然来自旧快照。应使用 callback 收到的 current 参数计算新值。在扩展到背包表之前,先说明区别:根据当前状态提出转换,与重复提交自己过去的快照,不是同一件事。
Callback 可能再次执行 #
如果其他服务器在读取与提交之间修改键,UpdateAsync 可以丢弃先前的提议,再次调用转换函数。因此,一次方法调用并不意味着 callback 只执行一次。在虚构顺序中,A 先提出 105,B 抢先保存 105,A 随后从新的 105 重新计算,提出 110。
再次执行的转换函数不应再发一件物品、发送奖励事件或修改外部游戏对象。它负责计算建议记录。游戏后果需要根据已确认状态另外决定。即使把发放物品移到成功调用之后,也不能单凭这一改动证明其他处理函数不会再次独立请求相同业务操作。
不依赖隐藏状态的教学函数 #
我们的 CounterTransform ModuleScript 提供 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 的 ServerScriptService 放入 CounterTransform。服务器 Script 获取 DataStoreService,选择教学计数器存储,并用 require 加载模块。向 UpdateAsync 传入返回 CounterTransform.Add(current, 5) 的 callback。不要在转换内放 task.wait、资源加载或其他会暂停执行的操作。
UpdateAsync 调用本身是网络操作,应放入 pcall,并另外检查返回的值。pcall 成功仅说明没有捕获错误;如果 callback 返回 nil 取消写入,就不能说已经增加了五。若仍使用正式 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
取消更新不等于默默归零 #
Callback 返回 nil 会取消更新。在练习中,已有值类型异常或提议超过上限时,这很有用。不要为了继续流程,把错误字符串自动转换成零。这样可能掩盖数据问题,并覆盖需要调查的记录。没有记录与已有记录格式错误,必须分开处理。
诊断时记录没有得到预期保存数值,并停止这条教学路径后续步骤。紧凑的纯函数不会提供完整拒绝原因。如果项目需要记录原因,应确保重复 callback 不会变成额外游戏动作,也不会改变下一次计算的依据。
分开测试顺序模型和函数 #
先重现错误顺序:两份 100、两个 105 提议、两次覆盖。然后建立重新计算模型:准备 A 的提议,应用 B 的修改,丢弃 A 的旧提议,从 105 重新计算。最终应该得到 110。这是两个确定执行顺序的模型,不是 Roblox 全部内部规则的模拟器。
还应测试缺失记录、普通整数、字符串、小数、负数、无穷大和上限。重复相同输入不能因隐藏状态而增加结果。把表当成数字传入时,原表必须保持不变。本地测试只检查这些明确性质,不验证正式服务器的可靠性。
| 本地检查 | 预期结果 |
|---|---|
| 没有记录;加 5 | 5 |
| 当前 100;加 5 | 105 |
| 当前 105;加 5 | 110 |
| 字符串代替数字 | nil:取消 |
| 999995;加 5 | 1000000 |
| 999996;加 5 | nil:取消 |
重复 callback 与重复业务操作不同 #
一次更新内部重复 callback,是为了让提议适应已经变化的状态。第二次独立请求“增加五”则是另一个操作,即使使用 UpdateAsync,也可能再增加五。因此,这个方法不是防止重复奖励、重复订单或重复处理购买的完整机制。
这种业务问题需要明确操作身份,并定义怎样把已经完成的状态检查与数据修改放在一起。本例不实现这个机制。单一数字计数器不保存操作历史。把这一限制与代码一起交给下一个开发聊天,避免把教学演示误认为完整钱包系统。
响应失败还留下另一个问题 #
网络调用报错并不能自动证明后端没有写入。Roblox 文档描述了结果未知的写入:保存可能已经完成,但服务器没有收到成功响应。如果无条件发起新的“加五”请求,就可能重复独立操作。这不同于方法内部重新计算 callback。
不要给例子增加无限重试循环。真实系统应先定义未知结果处理与每个键的操作顺序,再选择临时错误的有限重试和合适延迟。本地测试没有制造 Roblox 网络故障,也不证明已解决这个问题;它仍是单独的设计任务。
一个方法不等于完整档案管理 #
完整档案可能包含相互关联的字段、会话所有权和元数据。一个数字的例子没有展示更新时如何保留它们。它也没有指定会话持有者,不能阻止玩家切换服务器后旧服务器保存旧快照。把这些任务统称为 UpdateAsync 已解决,会隐藏尚未完成的工作。
不要把教学计数器用于 Robux 购买或正式游戏货币。生产方案需要自己的数据结构、兼容检查、失败处理和奖励规则。先阅读 Roblox 的 player data 与 purchasing 指南,再验证具体实现。本例是理解转换的小实验,不是档案管理库。
给开发者留下诚实的交接材料 #
分享错误与重新计算两种顺序图、隔离键名称、数字规则和已完成的本地检查。明确注明没有测试真实服务器与网络竞争。单独列出未解决要求:未知写入结果、重复业务操作、会话所有权以及其他档案字段的保留。
比较方案时,询问每个提议基于哪个值,以及哪些效果在 callback 外发生。答案必须能够解释具体顺序。如果只有没有第二个写入者时才正确,冲突问题仍未解决。把这篇与首次保存教程一起保存,区分初次访问存储和后续协调变化。
原始资料
Roblox Creator Hub — Data storesRoblox Creator Hub — GlobalDataStore
Roblox Creator Hub — Data store best practices