根据数据结构的类型,MemoryStoreService 对内存和数据结构中项目的数量施加了 限制。所有数据结构均受到每个分区的全局请求限制的约束。
每个 Roblox 游戏都有 Memory Store 可观察性仪表板,其中包含一组图表,可用于监控内存存储的使用情况。
排序地图和队列
排序地图和队列对最大项目数量和最大总内存都有限制。此外,这些数据结构中的项目总是位于单个分区上。对这些数据结构的每个请求都是对同一分区的请求。
当排序地图或队列达到其项目或内存限制时,最佳解决方案是手动删除不必要的项目或为项目添加过期策略。或者,如果仅内存限制导致限流,则可以尝试通过剥离密钥和值中的不必要信息来减少项目的大小。
如果你需要所有项目或由于请求吞吐量而遭遇限流,唯一的解决方案是分片。
分片
分片是将一组相关数据存储在多个数据结构中的过程。换句话说,这意味着将现有的高吞吐量数据结构替换为多个较小的结构,它们一起包含与原始数据相同的数据集。
分片的关键挑战是找到一种方法,以使数据在多个数据结构中分散,从而保持与原始相同的功能。
对于哈希地图,尽管数据结构已经分区,但通过在多个键之间分配请求来完成分片。
分片一个排序地图
要分片一个排序地图,可以考虑将数据拆分为具有字符范围的字母子部分。例如,假设你只有以 A 到 Z 的首字母为键,并且你相信四个排序地图足以满足你当前的用例和未来的增长:
- 第一个地图可以覆盖 A-G,第二个 H-N,第三个 O-T,第四个 U-Z。
- 为了插入或检索项目,请根据项目的起始字符使用合适的地图。
分片一个排序地图
-- 初始化 MemoryStore 服务
local MemoryStoreService = game:GetService("MemoryStoreService")
-- 创建你的排序地图桶
local sm_AtoG = MemoryStoreService:GetSortedMap("AtoG")
local sm_HtoM = MemoryStoreService:GetSortedMap("HtoM")
local sm_NtoT = MemoryStoreService:GetSortedMap("NtoT")
local sm_UtoZ = MemoryStoreService:GetSortedMap("UtoZ")
-- 辅助函数从项目键检索正确的桶
local function getSortedMapBucket(itemKey)
if (itemKey >= "a" and itemKey < "h") then
return sm_AtoG
elseif (itemKey < "n") then
return sm_HtoM
elseif (itemKey < "u") then
return sm_NtoT
else
return sm_UtoZ
end
end
-- 初始化玩家名称,默认值为 0
for _, player in game:GetService("Players"):GetPlayers() do
local bucket = getSortedMapBucket(player)
bucket:SetAsync(player, 0, 600)
end
-- 检索玩家的值
local player = "myPlayer"
local bucket = getSortedMapBucket(player)
local playerScore = bucket:GetAsync(player)
print(playerScore)
分片一个队列
分片一个队列比分片一个排序地图要复杂。尽管你想将请求吞吐量分散到多个队列中,但添加、读取和移除操作只能在队列的前端或后端发生。
一种解决方案是使用一个旋转队列,这意味着创建多个队列,并在添加或读取项目时在它们之间轮换:
创建几个队列并将它们添加到数组中。
创建两个本地指针。一个代表你想从中读取和移除项目的队列。另一个代表你想添加项目的队列:
- 对于读取操作,计算每个队列中所需项目的数量,以及如何移动读取指针。
- 对于移除操作,将读取的 ID 传递给每个队列。
- 对于添加操作,在添加指针所指向的队列中添加,并递增指针。
分片一个队列
-- 初始化 MemoryStore 服务
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[readIndex]
queue:RemoveAsync(ids[i])
end
end
-- 创建一个本地函数,向队列中添加项目
local function addToQueue(itemKey, expiration, priority)
local queue = queueArr[readIndex]
queue:AddAsync(itemKey, expiration, priority)
addIndex = rotateIndex(addIndex, 1)
end
-- 写一些代码!
for _, player in game:GetService("Players"):GetPlayers() do
addToQueue(player, 600, 0)
end
local players, ids = readFromQueue(20, true, -1)
removeFromQueue(ids)
哈希地图
哈希地图没有单独的内存或项目计数限制,并且会自动进行分片,但如果使用不当,依然可能会遇到限流。
例如,考虑一个游戏,它有一个哈希地图数据,保存在名为 metadata 的单个键的值中。如果该元数据包含一个嵌套对象,其中包含例如位置信息 ID、玩家数量等信息,则每次需要元数据时,你别无选择,只能调用 GetAsync("metadata") 并检索整个对象。在这种情况下,所有请求都指向单个键,因此也指向单个分区。
与其将所有元数据作为单个嵌套对象存储,不如将每个字段存储为自己的键,以便哈希地图可以利用自动分片。如果你需要在元数据和哈希地图的其余部分之间保持分离,可以添加命名前缀(例如 metadata_user_count 而不仅仅是 user_count)。
此外,如果一个或几个键被频繁访问,分片这些调用到多个键中是很重要的。例如,如果所有游戏服务器必须从一个哈希地图键中检索一个值,你可能会遇到分区限流。为了防止这种情况,你可以将这些调用分散到几个具有相同值的键中,直到不再发生分区限流。