使用这些实践来组织临时数据、分配负载并应对内存存储问题。
根据数据结构类型,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)分片队列
分片队列比分片排序映射更棘手。尽管您希望在多个队列之间分散请求吞吐量,但添加、读取和移除操作仅在队列的前面或后面发生。
一种解决方案是使用旋转队列,这意味着创建多个队列,并在添加或读取项目时在它们之间轮换:
- 创建多个队列并将它们添加到数组中。
- 创建两个本地指针。一个表示您要从中读取和移除项目的队列。另一个表示您要添加项目的队列:
- 对于读取操作,计算您需要从每个队列中读取的项目数量,以及要移动读取指针的位置。
- 对于移除操作,将 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。
如果一个或几个键接收到频繁请求,请在多个键之间分片这些调用。例如,如果所有游戏服务器从一个哈希映射键中检索值,请求可能会导致分区限流。为了减少负载,将值复制到多个键,并将每个服务器路由到一个稳定的分片。