内存存储的最佳实践

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处。

使用这些实践来组织临时数据、分配负载并应对内存存储问题。

根据数据结构类型,MemoryStoreService 强制执行 限制 内存和数据结构中项目的数量。所有数据结构也受到每个分区的全局请求限制的约束。

设计键和数据结构

请参见 使用静态键模式和前缀 在 数据存储的最佳实践 中。将这些模式应用于键和数据结构名称,以便每个服务器将相同的逻辑数据路由到相同的位置。

选择与数据保持有用时间相匹配的过期时间。不要将内存存储用于持久的玩家记录或必须在过期后存活的数据。有关选择服务的帮助,请参见 数据存储与内存存储。

处理请求失败

请参见 重试瞬态故障 在 数据存储的最佳实践 中。

优先使用 UpdateAsync 而不是 SetAsync

请参见 优先使用 UpdateAsync 而不是 SetAsync 在 数据存储的最佳实践 中。哈希映射和排序映射提供 MemoryStoreHashMap:UpdateAsync() 和 MemoryStoreSortedMap:UpdateAsync() 以实现此模式。

错开重复请求

请参见 错开重复请求 在 数据存储的最佳实践 中。

监控使用情况

使用 内存存储可观察性仪表板 监控配额使用情况、请求量和响应状态。查看内置的电子邮件警报,配置 自定义警报 以监控重要的内存存储指标,并使用 错误报告 调查故障。

在增加容量之前,减少请求、项目大小和过期时间。如果合法使用超过默认配额,请评估 扩展服务。

管理排序映射和队列限制

排序映射和队列都有最大项目数量和最大总内存的限制。此外,这些数据结构中的项目始终位于单个分区上。对这些数据结构的每个请求都是对同一分区的请求。

当排序映射或队列达到其项目或内存限制时,通过手动删除不必要的项目或添加过期策略来移除不必要的项目。如果仅内存限制导致限流,请通过从键和值中删除不必要的信息来减少项目大小。

如果您需要所有项目或由于请求吞吐量而遇到限流,唯一的解决方案是分片。

通过分片分配负载

分片是将一组相关数据存储在多个数据结构中的过程。换句话说,这意味着将现有的高吞吐量数据结构替换为多个较小的数据结构,这些数据结构共同包含与原始数据结构相同的数据集。

分片的关键挑战是找到一种方法,将数据分散到多个数据结构中,同时保持与原始数据结构相同的功能。

尽管 Roblox 已经对哈希映射进行了分区,但您可以通过在多个键之间分散请求来进一步分片。

分片排序映射

要在排序映射中分片玩家记录,请使用模运算将每个 Id 分配给固定数量的映射之一。以下示例使用四个映射,并始终将相同的用户路由到相同的映射:

分片排序映射
-- 初始化内存存储服务
local MemoryStoreService = game:GetService("MemoryStoreService")
-- 创建您的排序映射桶
local sm1 = MemoryStoreService:GetSortedMap("sm1")
local sm2 = MemoryStoreService:GetSortedMap("sm2")
local sm3 = MemoryStoreService:GetSortedMap("sm3")
local sm4 = MemoryStoreService:GetSortedMap("sm4")
local sortedMaps = { sm1, sm2, sm3, sm4 }
-- 辅助函数从项目键检索正确的桶
local function getSortedMapBucket(userId)
local bucketIndex = (userId % #sortedMaps) + 1
return sortedMaps[bucketIndex]
end
-- 用默认值 0 初始化玩家
for _, player in game:GetService("Players"):GetPlayers() do
local userId = player.User.Id
local bucket = getSortedMapBucket(userId)
bucket:SetAsync(tostring(userId), 0, 600)
end
-- 检索玩家的值
local player = game:GetService("Players"):GetPlayers()[1]
local userId = player.User.Id
local bucket = getSortedMapBucket(userId)
local playerScore = bucket:GetAsync(tostring(userId))
print(playerScore)

分片队列

分片队列比分片排序映射更棘手。尽管您希望在多个队列之间分散请求吞吐量,但添加、读取和移除操作仅在队列的前面或后面发生。

一种解决方案是使用旋转队列,这意味着创建多个队列,并在添加或读取项目时在它们之间轮换:

  1. 创建多个队列并将它们添加到数组中。
  2. 创建两个本地指针。一个表示您要从中读取和移除项目的队列。另一个表示您要添加项目的队列:
    • 对于读取操作,计算您需要从每个队列中读取的项目数量,以及要移动读取指针的位置。
    • 对于移除操作,将 ID 从读取队列传递到每个队列。
    • 对于添加操作,在添加指针指向的队列中添加,并递增指针。
分片队列
-- 初始化内存存储服务
local MemoryStoreService = game:GetService("MemoryStoreService")
-- 创建您的队列
local q1 = MemoryStoreService:GetQueue("q1")
local q2 = MemoryStoreService:GetQueue("q2")
local q3 = MemoryStoreService:GetQueue("q3")
local q4 = MemoryStoreService:GetQueue("q4")
-- 将队列放入数组中
local queueArr = { q1, q2, q3, q4 }
-- 创建两个指针,表示读取和添加队列的索引
local readIndex = 1
local addIndex = 1
-- 创建一个本地函数,适当地更新索引
local function rotateIndex(index, n)
return (index + n - 1) % 4 + 1
end
-- 创建一个本地函数,从队列中读取 n 个项目
local function readFromQueue(count, allOrNothing, waitTimeout)
local endIndex = count % 4
local countPerQueue = count // 4
local items = {}
local ids = {}
-- 遍历每个队列
for i = 1, 4, 1 do
-- 确定此队列是否将读取额外的项目
local diff = i - readIndex
if diff < 0 then
diff += 4
end
local queue = queueArr[i]
-- 从每个队列读取项目
-- 如果符合额外读取标准,则 +1 项
if diff < endIndex then
items[i], ids[i] = queue:ReadAsync(countPerQueue + 1, allOrNothing, waitTimeout)
else
items[i], ids[i] = queue:ReadAsync(countPerQueue, allOrNothing, waitTimeout)
end
end
readIndex = rotateIndex(readIndex, count)
return items, ids
end
-- 创建一个本地函数,从队列中移除 n 个项目
local function removeFromQueue(ids)
for i = 1, 4, 1 do
local queue = queueArr[i]
queue:RemoveAsync(ids[i])
end
end
-- 创建一个本地函数,将项目添加到队列
local function addToQueue(itemKey, expiration, priority)
local queue = queueArr[addIndex]
queue:AddAsync(itemKey, expiration, priority)
addIndex = rotateIndex(addIndex, 1)
end
-- 编写一些代码!
for _, player in game:GetService("Players"):GetPlayers() do
addToQueue(player.User.Id, 600, 0)
end
local players, ids = readFromQueue(20, true, -1)
removeFromQueue(ids)

哈希映射

哈希映射没有单独的内存或项目计数限制,并且会自动分片,但如果使用不当,您仍然可能遇到限流。

例如,考虑一个游戏,其中有一个哈希映射数据,存储为名为 metadata 的单个键的值。如果此元数据包含一个嵌套对象,其中包含地点 ID、玩家数量等信息,每次需要元数据时,您别无选择,只能调用 GetAsync("metadata") 并检索整个对象。在这种情况下,所有请求都指向单个键,因此指向单个分区。

与其将所有元数据存储为单个嵌套对象,不如将每个独立访问的字段存储为其自己的键,以便哈希映射可以利用自动分片。如果您需要在元数据和哈希映射的其余部分之间进行分离,请添加命名前缀,例如 metadata_user_count 而不是 user_count。

如果一个或几个键接收到频繁请求,请在多个键之间分片这些调用。例如,如果所有游戏服务器从一个哈希映射键中检索值,请求可能会导致分区限流。为了减少负载,将值复制到多个键,并将每个服务器路由到一个稳定的分片。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。