데이터 구조의 유형에 따라 MemoryStoreService는 제한을 메모리 및 데이터 구조 내 아이템 수에 적용합니다. 모든 데이터 구조는 또한 전역 파티션당 요청 제한에 의해 제한됩니다.
각 Roblox 게임에는 메모리 스토어 사용량을 모니터링하는 데 사용할 수 있는 차트 세트를 포함하는 메모리 스토어 가시성 대시보드가 있습니다.
정렬된 맵과 큐
정렬된 맵과 큐 모두 최대 아이템 수 및 최대 총 메모리에 대한 제한이 있습니다. 또한 이러한 데이터 구조의 항목은 항상 단일 파티션에 존재합니다. 이러한 데이터 구조에 대한 모든 요청은 동일한 파티션에 대한 요청입니다.
정렬된 맵 또는 큐가 아이템 또는 메모리 제한에 도달하면 가장 좋은 대처 방법은 불필요한 아이템을 수동으로 제거하거나 아이템에 대한 만료 정책을 추가하는 것입니다. 또는 메모리 제한으로 인한 스로틀링만 발생하는 경우, 키와 값에서 불필요한 정보를 제거하여 아이템의 크기를 줄일 수 있습니다.
모든 아이템이 필요하거나 요청 처리량으로 인해 스로틀링을 경험하고 있는 경우, 유일한 해결책은 샤딩입니다.
샤딩
샤딩은 관련된 데이터를 여러 데이터 구조에 분산 저장하는 과정입니다. 다시 말해, 기존의 높은 처리량 데이터 구조를 여러 개의 더 작은 데이터 구조로 교체하여 원본과 동일한 데이터 집합을 포함하게 만드는 것을 의미합니다.
샤딩의 주요 과제는 원본과 동일한 기능을 유지하면서 데이터를 여러 데이터 구조에 분산시키는 방법을 찾는 것입니다.
해시 맵의 경우, 데이터 구조가 이미 파티셔닝 되어 있지만, 샤딩은 여러 키 간에 요청을 분산시키는 방식으로 수행됩니다.
정렬된 맵 샤딩
정렬된 맵을 샤딩하려면 데이터를 알파벳 하위 섹션으로 분할하는 것을 고려하세요. 예를 들어, A-Z의 첫 글자가 있는 키만 있다고 가정하고, 현재 사용 사례와 미래 성장을 위해 4개의 정렬된 맵이 충분하다고 믿는다면:
- 첫 번째 맵은 A-G, 두 번째는 H-N, 세 번째는 O-T, 네 번째는 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)
큐 샤딩
큐 샤딩은 정렬된 맵 샤딩보다 더 복잡합니다. 여러 큐에 요청 처리량을 분산시키고 싶지만, 추가, 읽기 및 제거는 항상 큐의 앞이나 뒤에서만 발생합니다.
한 가지 해결책은 회전 큐를 사용하는 것입니다. 즉, 여러 큐를 만들고 아이템을 추가하거나 읽을 때 그 큐들 사이에서 회전하는 것입니다:
여러 큐를 생성하고 배열에 추가합니다.
두 개의 로컬 포인터를 만듭니다. 하나는 아이템을 읽고 제거할 큐를 나타내고, 다른 하나는 아이템을 추가할 큐를 나타냅니다:
- 읽기 작업의 경우 각 큐에서 필요한 아이템 수와 읽기 포인터를 이동할 위치를 계산합니다.
- 제거 작업의 경우 읽기에서 각 큐로 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[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).
또한, 하나 또는 몇 개의 키가 자주 접근된다면 이러한 호출을 많은 키에 샤딩하는 것이 중요합니다. 예를 들어, 모든 게임 서버가 해시 맵 키에서 값을 검색해야 하는 경우 파티션 스로틀링에 직면할 수 있습니다. 이를 방지하기 위해 해당 호출을 여러 키에 걸쳐 동일한 값으로 분산시켜 파티션 스로틀링이 더 이상 발생하지 않도록 할 수 있습니다.