データ構造の種類に応じて、MemoryStoreService は 制限 をメモリとデータ構造内のアイテム数に対して強制します。すべてのデータ構造は、グローバルなパーティションごとのリクエスト制限にも制約されています。
各Robloxゲームには、メモリストアの使用状況を監視するために使用できる一連のチャートを含む Memory Store Observability Dashboard があります。
ソートされたマップとキュー
ソートされたマップとキューはどちらも、アイテム数と合計メモリの最大値に制限があります。さらに、これらのデータ構造内のアイテムは常に単一のパーティションに存在します。これらのデータ構造への各リクエストは、同じパーティションへのリクエストです。
ソートされたマップまたはキューがアイテムまたはメモリ制限に達した場合、最善の行動は、不要なアイテムを手動で削除するか、アイテムの期限ポリシーを追加することです。あるいは、メモリ制限のみがスロットリングを引き起こしている場合は、キーと値から不要な情報を取り除くことでアイテムのサイズを削減することを検討できます。
すべてのアイテムが必要であったり、リクエストスループットによりスロットリングが発生している場合は、シャーディングが唯一の解決策です。
シャーディング
シャーディングは、関連するデータのセットを複数のデータ構造に保存するプロセスです。言い換えれば、既存の高スループットデータ構造を小さな複数のデータ構造に置き換え、元のデータセットを保持することを意味します。
シャーディングの主な課題は、元の機能を維持する方法でデータを複数のデータ構造に分散させることです。
ハッシュマップの場合、データ構造は既にパーティション化されていますが、シャーディングはリクエストを複数のキーに分散させることで行われます。
ソートされたマップのシャーディング
ソートされたマップをシャーディングするには、データを文字範囲でアルファベット順のサブセクションに分割することを考慮してください。たとえば、最初の文字がA-Zのキーのみがあると仮定し、現在のユースケースと将来の成長に対して4つのソートされたマップが十分であると信じている場合:
- 最初のマップはA-Gをカバーし、2番目はH-N、3番目はO-T、4番目はU-Zをカバーします。
- アイテムを挿入または取得するために、アイテムの最初の文字に基づいて適切なマップを使用します。
ソートされたマップのシャーディング
-- メモリストアサービスを初期化
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)
キューのシャーディング
キューのシャーディングは、ソートされたマップのシャーディングよりも難しいです。リクエストスループットを複数のキューに分散させたい場合でも、追加、読み取り、および削除は常にキューの前または後でのみ発生します。
一つの解決策は、回転キューを使用することです。これは、複数のキューを作成し、アイテムを追加または読み取るときにそれらの間で回転させることを意味します:
いくつかのキューを作成し、それらを配列に追加します。
1つはアイテムを読み取って削除したいキュー、もう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 }
-- 読み取りキューと追加キューのインデックスを表す2つのポインタを作成
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 とするのではなく)。
さらに、1つまたは少数のキーが頻繁にアクセスされる場合、これらの呼び出しをたくさんのキーにシャーディングすることが重要です。たとえば、すべてのゲームサーバーが1つのハッシュマップキーから値を取得する必要がある場合、パーティションスロットリングに直面する可能性があります。これを防ぐために、同じ値を持つ複数のキーの間でそれらの呼び出しを分散させ、パーティションスロットリングが発生しないようにすることができます。