roblox-data-stores
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRoblox data stores and cloud services
Roblox 数据存储与云服务
Choose the service
选择服务
| Need | Service | Notes |
|---|---|---|
| Player progress, inventory, settings | DataStore via ProfileStore (session-locked) | One key per player ( |
| Global leaderboards | | Integer values only; periodic updates, cached reads. |
| Fast, temporary, cross-server state | | Sorted maps, queues, hash maps; everything expires (TTL ≤ 45 days). |
| Matchmaking queues, live events, server lists | MemoryStore queue / sorted map | Not durable — never the only copy of valuable data. |
| Broadcast to all servers | | ≤ 1 kB per message, best effort, no delivery guarantee. |
| Values changed by live-ops without publishing | Experience configs ( | See |
| 需求 | 服务 | 备注 |
|---|---|---|
| 玩家进度、库存和设置 | 通过 ProfileStore 使用 DataStore(会话锁定) | 每名玩家一个键 ( |
| 全局排行榜 | | 仅限整数值;定期更新,缓存读取结果。 |
| 快速、临时的跨服务器状态 | | 有序映射、队列、哈希映射;所有数据都会过期(TTL ≤ 45 天)。 |
| 匹配队列、实时活动、服务器列表 | MemoryStore 队列/有序映射 | 不持久——绝不要将其作为有价值数据的唯一副本。 |
| 向所有服务器广播 | | 每条消息 ≤ 1 kB,尽力而为,不保证送达。 |
| 无需发布即可由 live-ops 修改的值 | 体验配置( | 参见 |
Player data: use session locking
玩家数据:使用会话锁定
Two servers must never own the same player's data at once (teleports, fast rejoins), or items duplicate
and progress is lost. ProfileStore (loleris) implements session locking, auto-save, and
flushing, and is the recommended default. Install it from Wally/the Creator Store and put
it in .
BindToCloseServerScriptServiceluau
-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)
local TEMPLATE = {
version = 1,
coins = 0,
inventory = {} :: { [string]: number },
}
type PlayerData = typeof(TEMPLATE)
local PlayerStore = ProfileStore.New("PlayerData", TEMPLATE)
local profiles: { [Player]: typeof(PlayerStore:StartSessionAsync("")) } = {}
local function onPlayerAdded(player: Player)
local profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {
Cancel = function()
return player.Parent ~= Players -- stop waiting if the player left
end,
})
if profile == nil then
player:Kick("Could not load your data. Please rejoin.")
return
end
profile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage
profile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created
profile.OnSessionEnd:Connect(function()
profiles[player] = nil
player:Kick("Your data was loaded on another server. Please rejoin.")
end)
if player.Parent == Players then
profiles[player] = profile
else
profile:EndSession() -- left while loading
end
end
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = profiles[player]
if profile then
profile:EndSession() -- saves and releases the lock
end
end)
local DataService = {}
function DataService.get(player: Player): PlayerData?
local profile = profiles[player]
return profile and profile.Data
end
return DataServiceMutate directly on the server (it is cached in memory and auto-saved). Don't
per change. Check the ProfileStore docs for the exact current API before relying on less
common features (global updates, , mock stores for Studio).
profile.DataSetAsyncMessageAsync同一名玩家的数据绝不能同时由两个服务器持有(传送、快速重连),否则物品会重复且进度会丢失。ProfileStore(作者 loleris)实现了会话锁定、自动保存和 刷盘,是推荐的默认方案。可通过 Wally/Creator Store 安装,并将其放入 。
BindToCloseServerScriptServiceluau
-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)
local TEMPLATE = {
version = 1,
coins = 0,
inventory = {} :: { [string]: number },
}
type PlayerData = typeof(TEMPLATE)
local PlayerStore = ProfileStore.New("PlayerData", TEMPLATE)
local profiles: { [Player]: typeof(PlayerStore:StartSessionAsync("")) } = {}
local function onPlayerAdded(player: Player)
local profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {
Cancel = function()
return player.Parent ~= Players -- stop waiting if the player left
end,
})
if profile == nil then
player:Kick("Could not load your data. Please rejoin.")
return
end
profile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage
profile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created
profile.OnSessionEnd:Connect(function()
profiles[player] = nil
player:Kick("Your data was loaded on another server. Please rejoin.")
end)
if player.Parent == Players then
profiles[player] = profile
else
profile:EndSession() -- left while loading
end
end
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = profiles[player]
if profile then
profile:EndSession() -- saves and releases the lock
end
end)
local DataService = {}
function DataService.get(player: Player): PlayerData?
local profile = profiles[player]
return profile and profile.Data
end
return DataService在服务器上直接修改 (数据会缓存在内存中并自动保存)。不要针对每次变更调用 。在依赖不太常用的功能(全局更新、、用于 Studio 的模拟存储)之前,请查阅 ProfileStore 文档以了解确切的当前 API。
profile.DataSetAsyncMessageAsyncRaw DataStoreService rules (when not using a wrapper)
原始 DataStoreService 规则(不使用包装器时)
- Buffer in memory: load once on join, mutate the in-memory table, save periodically (e.g. every
2–5 minutes with random jitter), on leave, at checkpoints (purchases), and in .
game:BindToClose - over
UpdateAsyncwhenever the write depends on the old value or multiple servers may write the key. The transform function must be pure and fast (no yields); returnSetAsyncto cancel.nil - Retry transient errors with exponential backoff + jitter, capped attempts; process retries for a key in order (an old retry must not overwrite newer data). Treat a failed write as "outcome unknown".
- Never save s,
Instance,Vector3, functions, or mixed/sparse tables: store plain tables, numbers, strings, booleans, andCFrames. Serialize datatypes (buffer). Validate UTF-8 strings.{ x, y, z } - Keys and data store names ≤ 50 characters; value ≤ 4,194,304 characters after JSON encoding.
- Stable key patterns (), never display names. Few data stores, few keys per player.
User_{UserId} - Studio needs File → Experience Settings → Security → Enable Studio Access to API Services; use a separate test universe or a different data store name so testing never touches production data.
Complete raw implementation (session lock, autosave, BindToClose, retries):
references/raw-datastore.md.
- 在内存中缓冲:加入时加载一次,修改内存中的表;定期保存(例如每 2–5 分钟一次并加入随机抖动)、离开时保存、在检查点(购买)保存,以及在 中保存。
game:BindToClose - 当写入依赖于旧值或多个服务器可能写入同一个键时,使用 而不是
UpdateAsync。转换函数必须纯粹且快速(不能产生 yield);返回SetAsync以取消写入。nil - 使用指数退避和抖动重试瞬时错误,并限制尝试次数;按顺序处理某个键的重试(旧重试不得覆盖较新的数据)。将失败的写入视为“结果未知”。
- 绝不保存 、
Instance、Vector3、函数或混合/稀疏表:只应存储普通表、数字、字符串、布尔值和CFrame。将数据类型序列化(buffer)。验证 UTF-8 字符串。{ x, y, z } - 键名和数据存储名称均 ≤ 50 个字符;JSON 编码后的值 ≤ 4,194,304 个字符。
- 使用稳定的键模式(),绝不要使用显示名称。数据存储应尽量少,每名玩家的键也应尽量少。
User_{UserId} - Studio 需要 文件 → 体验设置 → 安全 → 启用对 API 服务的 Studio 访问权限;请使用独立的测试 universe 或不同的数据存储名称,确保测试永远不会触及生产数据。
完整的原始实现(会话锁定、自动保存、BindToClose、重试):
references/raw-datastore.md。
Budgets and limits (2026)
预算和限制(2026)
| Limit | Value |
|---|---|
| Experience-wide standard reads / writes | |
| Default per-server reads/writes | |
| Ordered writes per server | |
| Per-key throughput | 25 MB/min read, 4 MB/min write |
| Storage (latest versions, compressed) | |
| Queue per request type | 30 pending requests, then errors 301–306 |
- consumes both read and write budget.
UpdateAsync - Check before bulk work; tune per-server limits once at startup with
DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite).SetRateLimitForRequestType - Don't pre-compress data; Roblox compresses automatically. Shard only genuinely hot keys.
- Use versions (,
ListVersionsAsync) to restore data instead of writing backup keys.GetVersionAtTimeAsync
| 限制 | 数值 |
|---|---|
| 整个体验的标准读取/写入 | |
| 每台服务器的默认读取/写入 | |
| 每台服务器的 Ordered 写入 | |
| 单键吞吐量 | 读取 25 MB/分钟,写入 4 MB/分钟 |
| 存储空间(最新版本,已压缩) | 每个体验 |
| 每类请求的队列 | 30 个待处理请求,随后返回 301–306 错误 |
- 会同时消耗读取和写入预算。
UpdateAsync - 在批量操作前检查 ;在启动时使用
DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite)一次性调优每服务器限制。SetRateLimitForRequestType - 不要预压缩数据;Roblox 会自动压缩。仅对真正的热点键进行分片。
- 使用版本(、
ListVersionsAsync)恢复数据,而不是写入备份键。GetVersionAtTimeAsync
Schema versioning and migrations
Schema 版本管理和迁移
Store a field. On load, run ordered migration functions until the data reaches the current
version, then reconcile defaults. Never delete fields in the same release that stops writing them;
never reuse field names with different meanings.
versionluau
--!strict
type Data = { [string]: any }
local MIGRATIONS: { (Data) -> () } = {
[1] = function(data) -- v1 -> v2: "gold" renamed to "coins"
data.coins = data.gold or 0
data.gold = nil
end,
[2] = function(data) -- v2 -> v3: inventory list -> counts
local counts: { [string]: number } = {}
for _, itemId in data.inventory or {} do
counts[itemId] = (counts[itemId] or 0) + 1
end
data.inventory = counts
end,
}
local CURRENT_VERSION = #MIGRATIONS + 1
local function migrate(data: Data): Data
local version = data.version or 1
while version < CURRENT_VERSION do
MIGRATIONS[version](data)
version += 1
end
data.version = CURRENT_VERSION
return data
end
print(migrate({ gold = 5, inventory = { "Sword", "Sword" } }))存储一个 字段。加载时,按顺序运行迁移函数,直至数据达到当前版本,然后补齐默认字段。不要在停止写入某个字段的同一次发布中删除该字段;也不要将具有不同含义的字段名重复用于其他用途。
versionluau
--!strict
type Data = { [string]: any }
local MIGRATIONS: { (Data) -> () } = {
[1] = function(data) -- v1 -> v2: "gold" renamed to "coins"
data.coins = data.gold or 0
data.gold = nil
end,
[2] = function(data) -- v2 -> v3: inventory list -> counts
local counts: { [string]: number } = {}
for _, itemId in data.inventory or {} do
counts[itemId] = (counts[itemId] or 0) + 1
end
data.inventory = counts
end,
}
local CURRENT_VERSION = #MIGRATIONS + 1
local function migrate(data: Data): Data
local version = data.version or 1
while version < CURRENT_VERSION do
MIGRATIONS[version](data)
version += 1
end
data.version = CURRENT_VERSION
return data
end
print(migrate({ gold = 5, inventory = { "Sword", "Sword" } }))Leaderboards (OrderedDataStore)
排行榜(OrderedDataStore)
- Values must be integers. Write on a schedule or on meaningful change, not every increment.
- Read top N with on one server loop (e.g. every 60 s), cache the result, and replicate to clients. Don't let every server/client read constantly.
GetSortedAsync(false, 100) - For "this week" boards, include the period in the store name () or use a MemoryStore sorted map.
Wins_2026W39
- 值必须是整数。按计划或在发生有意义的变更时写入,不要每次数值增加都写入。
- 在单个服务器循环中(例如每 60 秒)通过 读取前 N 名,缓存结果并复制到客户端。不要让每个服务器/客户端持续读取。
GetSortedAsync(false, 100) - 对于“本周”排行榜,请在存储名称中包含周期(),或使用 MemoryStore 有序映射。
Wins_2026W39
MemoryStore essentials
MemoryStore 要点
- Quotas are experience-wide: memory ; requests
64 KB + 1.2 KB × usersunits/min.1000 + 120 × CCU - : ranked data, server browsers, live leaderboards (
MemoryStoreSortedMapwith expiration,SetAsync,GetRangeAsync).UpdateAsync - : matchmaking/work queues (
MemoryStoreQueue,AddAsync+ReadAsyncwith the returned id after successful processing, invisibility timeout for crash safety).RemoveAsync - : high-volume key-value without sorting (
MemoryStoreHashMap,SetAsync,GetAsync,UpdateAsync).ListItemsAsync - Always set expirations; handle throttling (+ retry); keep items small.
pcall
- 配额按整个体验共享:内存为 ;请求为
64 KB + 1.2 KB × users单位/分钟。1000 + 120 × CCU - :排名数据、服务器浏览器、实时排行榜(带过期时间的
MemoryStoreSortedMap、SetAsync、GetRangeAsync)。UpdateAsync - :匹配/工作队列(
MemoryStoreQueue、AddAsync,并在成功处理后使用返回的 id 调用ReadAsync;使用不可见超时保障崩溃安全)。RemoveAsync - :无需排序的高吞吐量键值数据(
MemoryStoreHashMap、SetAsync、GetAsync、UpdateAsync)。ListItemsAsync - 始终设置过期时间;处理限流(+ 重试);保持条目较小。
pcall
MessagingService essentials
MessagingService 要点
- Message ≤ 1 kB. Per server: sends/min;
600 + 240 × playerssubscriptions.20 + 8 × players - Delivery is best effort and not ordered: use it for notifications ("refresh your cache", global announcements), never as the source of truth. Pair with DataStore/MemoryStore for state.
- Wrap /
SubscribeAsyncinPublishAsync; they can fail or yield.pcall
More code (MemoryStore queue matchmaking, cross-server announcement, RTBF): references/memory-and-messaging.md.
- 消息 ≤ 1 kB。每台服务器每分钟可发送 条消息;可使用
600 + 240 × players个订阅。20 + 8 × players - 投递采用尽力而为方式且不保证顺序:适用于通知(“刷新你的缓存”、全局公告),绝不要将其作为唯一真实数据源。应与 DataStore/MemoryStore 配合使用来保存状态。
- 使用 包装
pcall/SubscribeAsync;它们可能失败或产生 yield。PublishAsync
更多代码(MemoryStore 队列匹配、跨服务器公告、RTBF):
references/memory-and-messaging.md。
Right to be forgotten
被遗忘权
Use static key patterns so automated RTBF (configured in Creator Dashboard) can delete player data
templates like . Otherwise handle the right-to-erasure webhook (see
). Link keys to user IDs: , return them as the second value from an
transform, or call ProfileStore's .
PlayerData/User_{userId}roblox-open-cloudSetAsync(key, value, { userId })UpdateAsyncAddUserId使用静态键模式,使 Creator Dashboard 中配置的自动化 RTBF 能够删除 等玩家数据模板。否则,需要处理擦除权 webhook(参见 )。将键与用户 ID 关联:调用 ,将用户 ID 作为 转换函数返回的第二个值,或调用 ProfileStore 的 。
PlayerData/User_{userId}roblox-open-cloudSetAsync(key, value, { userId })UpdateAsyncAddUserIdRelated skills
相关 skill
roblox-securityroblox-monetizationroblox-open-cloudroblox-teleport-matchmakingroblox-securityroblox-monetizationroblox-open-cloudroblox-teleport-matchmaking