Contexto
Roblox fornece um conjunto de APIs para interagir com lojas de dados via DataStoreService. O caso de uso mais comum para essas APIs é salvar, carregar e replicar dados de jogador. Ou seja, dados associados ao progresso do jogador, compras e outras características da sessão que persistem entre sessões de jogo individuais.
A maioria dos jogos no Roblox usa essas APIs para implementar alguma forma de sistema de dados de jogador. Essas implementações diferem em sua abordagem, mas geralmente buscam resolver o mesmo conjunto de problemas.
Problemas comuns
Abaixo estão alguns dos problemas mais comuns que os sistemas de dados de jogador tentam resolver:
Acesso em Memória: As requisições de DataStoreService fazem solicitações web que operam de forma assíncrona e estão sujeitas a limites de taxa. Isso é apropriado para uma carga inicial no início da sessão, mas não para operações de leitura e escrita de alta frequência durante o curso normal do jogo. A maioria dos sistemas de dados de jogador dos desenvolvedores armazena esses dados em memória no servidor Roblox, limitando as requisições de DataStoreService aos seguintes cenários:
- Leitura inicial no início de uma sessão
- Escrita final no final da sessão
- Escritas periódicas em um intervalo para mitigar o cenário em que a escrita final falha
- Escritas para garantir que os dados sejam salvos enquanto processa uma compra
Armazenamento Eficiente: Armazenar todos os dados da sessão de um jogador em uma única tabela permite que você atualize múltiplos valores de forma atômica e manipule a mesma quantidade de dados em menos requisições. Isso também elimina o risco de dessincronização entre valores e torna os retrocessos mais fáceis de raciocinar.
Alguns desenvolvedores também implementam serialização personalizada para comprimir grandes estruturas de dados (tipicamente para salvar conteúdo gerado pelo usuário no jogo).
Replicação: O cliente precisa de acesso regular aos dados de um jogador (por exemplo, para atualizar a interface do usuário). Uma abordagem genérica para replicar dados de jogador para o cliente permite que você transmita essas informações sem ter que criar sistemas de replicação sob medida para cada componente de dados. Os desenvolvedores frequentemente desejam a opção de ser seletivos sobre o que é e o que não é replicado para o cliente.
Tratamento de Erros: Quando as DataStores não podem ser acessadas, a maioria das soluções implementará um mecanismo de repetição e um fallback para dados 'padrão'. Cuidado especial é necessário para garantir que os dados de fallback não sobrescrevam posteriormente os dados 'reais' e que isso seja comunicado ao jogador de forma apropriada.
Repetições: Quando as lojas de dados estão inacessíveis, a maioria das soluções implementa um mecanismo de repetição e um fallback para dados padrão. Tenha cuidado especial para garantir que os dados de fallback não sobrescrevam posteriormente os dados "reais" e comunique a situação ao jogador de forma apropriada.
Bloqueio de Sessão: Se os dados de um único jogador forem carregados e estiverem em memória em vários servidores, podem ocorrer problemas em que um servidor salva informações desatualizadas. Isso pode levar à perda de dados e a brechas comuns de duplicação de itens.
Tratamento Atômico de Compras: Verifique, conceda e registre compras de forma atômica para evitar que itens sejam perdidos ou concedidos várias vezes.
Código de exemplo
Roblox tem código de referência para ajudá-lo a projetar e construir sistemas de dados de jogador. O restante desta página examina o contexto, detalhes de implementação e advertências gerais.
Depois de importar o modelo para o Studio, você deve ver a seguinte estrutura de pastas:

Arquitetura
Este diagrama de alto nível ilustra os sistemas principais no exemplo e como eles interagem com o código no restante do jogo.

Repetições
Classe: DataStoreWrapper
Contexto
Como DataStoreService faz solicitações web nos bastidores, suas requisições não são garantidas para ter sucesso. Quando isso acontece, os métodos DataStore lançam erros, permitindo que você os trate.
Um "pegadinha" comum pode ocorrer se você tentar lidar com falhas de loja de dados assim:
local MAX_ATTEMPTS = 5
local BASE_DELAY = 2
local MAX_DELAY = 32
local function retrySetAsync(dataStore, key, value)
for attempt = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
return result
end
if attempt < MAX_ATTEMPTS then
local backoff = math.min(MAX_DELAY, BASE_DELAY * (2 ^ (attempt - 1)))
local jitter = math.random() * backoff
task.wait(math.min(MAX_DELAY, backoff + jitter))
end
end
endTente repetir falhas transitórias com um backoff exponencial e jitter aleatório para que os servidores não tentem repetir simultaneamente. Limite o atraso e o número de tentativas.
Mesmo com esse padrão de atraso, esse mecanismo de repetição não é adequado para requisições de DataStoreService porque não garante a ordem em que as requisições são feitas. Preservar a ordem das requisições é importante para requisições de DataStoreService porque elas interagem com o estado. Considere o seguinte cenário:
- A requisição A é feita para definir o valor da chave K como 1.
- A requisição falha, então uma repetição é agendada para ser executada após o atraso de backoff.
- Antes que a repetição ocorra, a requisição B define o valor de K como 2, mas a repetição da requisição A imediatamente sobrescreve esse valor e define K como 1.
Mesmo que UpdateAsync opere na versão mais recente do valor da chave, as requisições de UpdateAsync ainda devem ser processadas em ordem para evitar estados transitórios inválidos (por exemplo, uma compra subtrai moedas antes que uma adição de moedas seja processada, resultando em moedas negativas).
Nosso sistema de dados de jogador usa uma nova classe, DataStoreWrapper, que fornece repetições que garantem ser processadas em ordem por chave.
Abordagem

DataStoreWrapper fornece métodos correspondentes aos métodos DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() e DataStore:RemoveAsync().
Esses métodos, quando chamados:
Adicionam a requisição a uma fila. Cada chave tem sua própria fila, onde as requisições são processadas em ordem e em série. A thread solicitante aguarda até que a requisição seja concluída.
Essa funcionalidade é baseada na classe ThreadQueue, que é um agendador de tarefas baseado em corrotinas e limitador de taxa. Em vez de retornar uma promessa, ThreadQueue aguarda a thread atual até que a operação seja concluída e lança um erro se falhar. Isso é mais consistente com padrões assíncronos idiomáticos de Luau.
Se uma requisição falhar, ela tenta novamente com um backoff exponencial configurável. Essas repetições fazem parte do callback enviado para o ThreadQueue, então são garantidas para serem concluídas antes que a próxima requisição na fila para essa chave comece.
Quando uma requisição é concluída, o método de requisição retorna com o padrão success, result.
DataStoreWrapper também expõe métodos para obter o comprimento da fila para uma chave dada e limpar requisições obsoletas. Esta última opção é particularmente útil em cenários em que o servidor está sendo desligado e não há tempo para processar qualquer requisição, exceto as mais recentes.
Advertências
DataStoreWrapper segue o princípio de que, fora de cenários extremos, cada requisição de loja de dados deve ser permitida a completar (com sucesso ou não), mesmo que uma requisição mais recente a torne redundante. Quando uma nova requisição ocorre, requisições obsoletas não são removidas da fila, mas são permitidas a completar antes que a nova requisição seja iniciada. A justificativa para isso está enraizada na aplicabilidade deste módulo como uma utilidade genérica de loja de dados, em vez de uma ferramenta específica para dados de jogador, e é a seguinte:
É difícil decidir um conjunto intuitivo de regras para quando uma requisição é segura para ser removida da fila. Considere a seguinte fila:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
O comportamento esperado é que GetAsync() retornaria 1, mas se removermos a requisição SetAsync() da fila devido a ela ter se tornado redundante pela mais recente, retornaria 0.
A progressão lógica é que, quando uma nova requisição de escrita é adicionada, apenas elimine requisições obsoletas até a mais recente requisição de leitura. UpdateAsync(), de longe a operação mais comum (e a única usada por este sistema), pode tanto ler quanto escrever, então seria difícil reconciliar isso dentro deste design sem adicionar complexidade extra.
DataStoreWrapper poderia exigir que você especificasse se uma requisição UpdateAsync() tinha permissão para ler e/ou escrever, mas isso não teria aplicabilidade ao nosso sistema de dados de jogador, onde isso não pode ser determinado antecipadamente devido ao mecanismo de bloqueio de sessão (coberto em mais detalhes mais adiante).
Uma vez removida da fila, é difícil decidir uma regra intuitiva para como isso deve ser tratado. Quando uma requisição DataStoreWrapper é feita, a thread atual é aguardada até que seja concluída. Se removêssemos requisições obsoletas da fila, teríamos que decidir se retornar false, "Removido da fila" ou nunca retornar e descartar a thread ativa. Ambas as abordagens têm suas próprias desvantagens e transferem complexidade adicional para o consumidor.
Em última análise, nossa visão é que a abordagem simples (processar cada requisição) é preferível aqui e cria um ambiente mais claro para navegar em questões complexas como bloqueio de sessão. A única exceção a isso é durante DataModel:BindToClose(), onde limpar a fila se torna necessário para salvar os dados de todos os usuários a tempo e o valor que chamadas de função individuais retornam não é mais uma preocupação contínua. Para mais contexto, veja Dados do Jogador.
Bloqueio de Sessão
Classe: SessionLockedDataStoreWrapper
Contexto
Os dados do jogador são armazenados em memória no servidor e são lidos e escritos nas lojas de dados subjacentes apenas quando necessário. Você pode ler e atualizar os dados do jogador em memória instantaneamente sem precisar de requisições web e evitar exceder os limites de DataStoreService.
Para que este modelo funcione como pretendido, é imperativo que não mais de um servidor consiga carregar os dados de um jogador na memória a partir do DataStore ao mesmo tempo.
Por exemplo, se o servidor A carrega os dados de um jogador, o servidor B não pode carregar esses dados até que o servidor A libere seu bloqueio durante uma gravação final. Sem um mecanismo de bloqueio, o servidor B poderia carregar dados de jogador desatualizados da loja de dados antes que o servidor A tenha a chance de salvar a versão mais recente que tem em memória. Então, se o servidor A salvar seus dados mais novos após o servidor B carregar os dados desatualizados, o servidor B sobrescreveria esses dados mais novos durante sua próxima gravação.
Mesmo que o Roblox permita que um cliente esteja conectado a um único servidor por vez, você não pode assumir que os dados de uma sessão são sempre salvos antes que a próxima sessão comece. Considere os seguintes cenários que podem ocorrer quando um jogador sai do servidor A:
- O servidor A faz uma requisição DataStore para salvar seus dados, mas a requisição falha e requer várias repetições para ser concluída com sucesso. Durante o período de repetição, o jogador se junta ao servidor B.
- O servidor A faz muitas chamadas UpdateAsync() para a mesma chave e é limitado. A requisição de gravação final é colocada em uma fila. Enquanto a requisição está na fila, o jogador se junta ao servidor B.
- No servidor A, algum código conectado ao evento PlayerRemoving aguarda antes que os dados do jogador sejam salvos. Antes que essa operação seja concluída, o jogador se junta ao servidor B.
- O desempenho do servidor A se degradou a tal ponto que a gravação final é atrasada até depois que o jogador se junta ao servidor B.
Esses cenários devem ser raros, mas ocorrem, particularmente em situações onde um jogador desconecta de um servidor e conecta a outro em rápida sucessão (por exemplo, enquanto está teleportando). Alguns usuários mal-intencionados podem até tentar abusar desse comportamento para completar ações sem que elas persistam. Isso pode ser particularmente impactante em jogos que permitem que jogadores troquem itens e é uma fonte comum de exploits de duplicação de itens.
O bloqueio de sessão aborda essa vulnerabilidade garantindo que, quando a chave DataStore de um jogador é lida pela primeira vez pelo servidor, o servidor grava atômica e simultaneamente um bloqueio nos metadados da chave dentro da mesma chamada UpdateAsync(). Se esse valor de bloqueio estiver presente quando qualquer outro servidor tentar ler ou escrever a chave, o servidor não prossegue.
Abordagem

SessionLockedDataStoreWrapper é um meta-wrapper em torno da classe DataStoreWrapper. DataStoreWrapper fornece funcionalidade de enfileiramento e repetição, que SessionLockedDataStoreWrapper complementa com bloqueio de sessão.
SessionLockedDataStoreWrapper passa cada requisição DataStore—independentemente de ser GetAsync, SetAsync ou UpdateAsync—através de UpdateAsync. Isso ocorre porque UpdateAsync permite que uma chave seja lida e escrita atômica e simultaneamente. Também é possível abandonar a escrita com base no valor lido retornando nil no callback de transformação.
A função de transformação passada para UpdateAsync para cada requisição realiza as seguintes operações:
Verifica se a chave é segura para acessar, abandonando a operação se não for. "Segura para acessar" significa:
O objeto de metadados da chave não inclui um valor LockId não reconhecido que foi atualizado pela última vez há menos do que o tempo de expiração do bloqueio. Isso leva em conta o respeito a um bloqueio colocado por outro servidor e ignora esse bloqueio se ele expirou.
Se este servidor já colocou seu próprio valor LockId nos metadados da chave anteriormente, então esse valor ainda está nos metadados da chave. Isso leva em conta a situação em que outro servidor assumiu o bloqueio deste servidor (por expiração ou por força) e depois o liberou. Alternativamente, mesmo que LockId seja nil, outro servidor ainda poderia ter substituído e removido um bloqueio no tempo desde que você bloqueou a chave.
UpdateAsync realiza a operação DataStore que o consumidor de SessionLockedDataStoreWrapper solicitou. Por exemplo, GetAsync() se traduz em function(value) return value end.
Dependendo dos parâmetros passados na requisição, UpdateAsync bloqueia ou desbloqueia a chave:
Se a chave deve ser bloqueada, UpdateAsync define o LockId nos metadados da chave para um GUID. Este GUID é armazenado em memória no servidor para que possa ser verificado na próxima vez que acessar a chave. Se o servidor já tiver um bloqueio nesta chave, não faz alterações. Também agenda uma tarefa para avisá-lo se você não acessar a chave novamente para manter o bloqueio dentro do tempo de expiração do bloqueio.
Se a chave deve ser desbloqueada, UpdateAsync remove o LockId nos metadados da chave.
Um manipulador de repetição personalizado é passado para o DataStoreWrapper subjacente para que a operação seja repetida se for abortada na etapa 1 devido ao bloqueio da sessão.
Uma mensagem de erro personalizada também é retornada ao consumidor, permitindo que o sistema de dados do jogador relate um erro alternativo em caso de bloqueio de sessão ao cliente.
Advertências
O regime de bloqueio de sessão depende de um servidor sempre liberar seu bloqueio em uma chave quando terminar de usá-la. Isso deve sempre acontecer através de uma instrução para desbloquear a chave como parte da gravação final em PlayerRemoving ou BindToClose().
No entanto, o desbloqueio pode falhar em certas situações. Por exemplo:
- O servidor travou ou DataStoreService estava inoperante para todas as tentativas de acessar a chave.
- Devido a um erro na lógica ou bug semelhante, a instrução para desbloquear a chave não foi feita.
Para manter o bloqueio em uma chave, você deve acessá-la regularmente enquanto estiver carregada na memória. Isso normalmente seria feito como parte do loop de salvamento automático que roda em segundo plano na maioria dos sistemas de dados de jogador, mas este sistema também expõe um método refreshLockAsync se você precisar fazê-lo manualmente.
Se o tempo de expiração do bloqueio foi excedido sem que o bloqueio tenha sido atualizado, então qualquer servidor está livre para assumir o bloqueio. Se um servidor diferente assumir o bloqueio, tentativas do servidor atual de ler ou escrever a chave falham, a menos que estabeleça um novo bloqueio.
Processamento de Produtos do Desenvolvedor
Singleton: ReceiptHandler
Contexto
O callback ProcessReceipt realiza a tarefa crítica de determinar quando finalizar uma compra. ProcessReceipt é chamado em cenários muito específicos. Para seu conjunto de garantias, veja MarketplaceService.ProcessReceipt.
Embora a definição de "manipular" uma compra possa diferir entre jogos, usamos os seguintes critérios:
A compra não foi manipulada anteriormente.
A compra é refletida na sessão atual.
Isso requer a realização das seguintes operações antes de retornar PurchaseGranted:
- Verifique se o PurchaseId não foi registrado como manipulado.
- Conceda a compra nos dados do jogador em memória.
- Registre o PurchaseId como manipulado nos dados do jogador em memória.
- Escreva os dados do jogador em memória no DataStore.
O bloqueio de sessão simplifica esse fluxo, pois você não precisa mais se preocupar com os seguintes cenários:
- Os dados do jogador em memória no servidor atual potencialmente estarem desatualizados, exigindo que você busque o valor mais recente do DataStore antes de verificar o histórico do PurchaseId.
- O callback para a mesma compra sendo executado em outro servidor, exigindo que você leia e escreva o histórico do PurchaseId e salve os dados do jogador atualizados com a compra refletida atômica e simultaneamente para evitar condições de corrida.
O bloqueio de sessão garante que, se uma tentativa de escrever nos dados do jogador no DataStore for bem-sucedida, nenhum outro servidor leu ou escreveu com sucesso nos dados do jogador entre os dados sendo carregados e salvos neste servidor. Em resumo, os dados do jogador em memória neste servidor são a versão mais atualizada disponível. Existem algumas advertências, mas elas não impactam esse comportamento.
Abordagem
Os comentários em ReceiptProcessor delineiam a abordagem:
Verifique se os dados do jogador estão atualmente carregados neste servidor e se foram carregados sem erros.
Como este sistema usa bloqueio de sessão, essa verificação também confirma que os dados em memória são a versão mais atualizada.
Se os dados do jogador ainda não foram carregados (o que é esperado quando um jogador entra em um jogo), aguarde o carregamento dos dados do jogador. O sistema também escuta o jogador saindo do jogo antes que seus dados sejam carregados, pois não deve aguardar indefinidamente e bloquear esse callback de ser invocado novamente neste servidor para esta compra se o jogador retornar.
Verifique se o PurchaseId não está registrado como processado nos dados do jogador.
Devido ao bloqueio de sessão, o array de PurchaseIds que o sistema tem em memória é a versão mais atualizada. Se o PurchaseId estiver registrado como processado e refletido em um valor que foi carregado ou salvo no DataStore, retorne PurchaseGranted. Se estiver registrado como processado, mas não refletido no DataStore, retorne NotProcessedYet.
Atualize os Dados do Jogador localmente neste servidor para "conceder" a compra.
ReceiptProcessor adota uma abordagem de callback genérica e atribui um callback diferente para cada DeveloperProductId.
Atualize os dados do jogador localmente neste servidor para armazenar o PurchaseId.
Envie uma requisição para salvar os dados em memória no DataStore, retornando PurchaseGranted se a requisição for bem-sucedida. Caso contrário, retorne NotProcessedYet.
Se esta requisição de salvamento não for bem-sucedida, uma requisição posterior para salvar os dados da sessão em memória do jogador ainda pode ter sucesso. Durante a próxima chamada de ProcessReceipt, a etapa 2 lida com essa situação e retorna PurchaseGranted.
Dados do jogador
Singletons: PlayerData.Server, PlayerData.Client
Contexto
Módulos que fornecem uma interface para o código ler e escrever dados de sessão do jogador de forma síncrona são comuns em jogos do Roblox. Esta seção cobre PlayerData.Server e PlayerData.Client.
Abordagem
PlayerData.Server e PlayerData.Client lidam com o seguinte:
- Carregar os dados do jogador na memória, incluindo lidar com casos em que falha ao carregar
- Fornecer uma interface para o código do servidor consultar e alterar os dados do jogador
- Replicar alterações nos dados do jogador para o cliente para que o código do cliente possa acessá-los
- Replicar erros de carregamento e/ou salvamento para o cliente para que ele possa mostrar diálogos de erro
- Salvar os dados do jogador periodicamente, quando o jogador sai e quando o servidor é desligado
Carregar dados do jogador

SessionLockedDataStoreWrapper faz uma requisição getAsync para a loja de dados.
Se esta requisição falhar, os dados padrão são usados e o perfil é marcado como "com erro" para garantir que não seja escrito na loja de dados mais tarde.
Uma opção alternativa é expulsar o jogador, mas recomendamos deixar o jogador jogar com dados padrão e uma comunicação clara sobre o que ocorreu, em vez de removê-lo do jogo.
Um payload inicial é enviado para PlayerDataClient contendo os dados carregados e o status de erro (se houver).
Qualquer thread aguardando usando waitForDataLoadAsync para o jogador é retomada.
Fornecer uma interface para o código do servidor
- PlayerDataServer é um singleton que pode ser requerido e acessado por qualquer código do servidor que esteja rodando no mesmo ambiente.
- Os dados do jogador são organizados em um dicionário de chaves e valores. Você pode manipular esses valores no servidor usando os métodos setValue, getValue, updateValue e removeValue. Todos esses métodos operam de forma síncrona sem aguardar.
- Os métodos hasLoaded e waitForDataLoadAsync estão disponíveis para garantir que os dados tenham sido carregados antes de você acessá-los. Recomendamos fazer isso uma vez durante uma tela de carregamento antes que outros sistemas sejam iniciados para evitar ter que verificar erros de carregamento antes de cada interação com os dados no cliente.
- Um método hasErrored pode consultar se o carregamento inicial do jogador falhou, fazendo com que ele use dados padrão. Verifique este método antes de permitir que o jogador faça qualquer compra, pois compras não podem ser salvas nos dados sem um carregamento bem-sucedido.
- Um sinal playerDataUpdated é acionado com o player, key e value sempre que os dados de um jogador são alterados. Sistemas individuais podem se inscrever para isso.
Replicar alterações para o cliente
- Qualquer alteração nos dados do jogador em PlayerDataServer é replicada para PlayerDataClient, a menos que essa chave tenha sido marcada como privada usando setValueAsPrivate
- setValueAsPrivate é usado para denotar chaves que não devem ser enviadas para o cliente
- PlayerDataClient inclui um método para obter o valor de uma chave (get) e um sinal que é acionado quando é atualizado (updated). Um método hasLoaded e um sinal loaded também estão incluídos, para que o cliente possa aguardar o carregamento e replicação dos dados antes de iniciar seus sistemas
- PlayerDataClient é um singleton que pode ser requerido e acessado por qualquer código do cliente que esteja rodando no mesmo ambiente
Replicar erros para o cliente
- Status de erro encontrados ao salvar ou carregar dados do jogador são replicados para PlayerDataClient.
- Acesse essas informações com os métodos getLoadError e getSaveError, junto com os sinais loaded e saved.
- Existem dois tipos de erros: DataStoreError (a requisição de DataStoreService falhou) e SessionLocked (veja Bloqueio de Sessão).
- Use esses eventos para desativar prompts de compra do cliente e implementar diálogos de aviso. Esta imagem mostra um exemplo de diálogo:

Salvar dados do jogador

Quando o jogador sai do jogo, o sistema toma as seguintes medidas:
- Verifique se é seguro escrever os dados do jogador na loja de dados. Cenários em que seria inseguro incluem os dados do jogador falhando ao carregar ou ainda em processo de carregamento.
- Faça uma requisição através do SessionLockedDataStoreWrapper para escrever o valor atual em memória na loja de dados e remover o bloqueio da sessão uma vez concluído.
- Limpe os dados do jogador (e outras variáveis como metadados e status de erro) da memória do servidor.
Em um loop periódico, o servidor escreve os dados de cada jogador na loja de dados (desde que seja seguro salvar). Essa redundância bem-vinda mitiga a perda em caso de uma falha do servidor e também é necessária para manter o bloqueio da sessão.
O exemplo inicia um loop compartilhado após AUTO_SAVE_INTERVAL segundos (180 por padrão) e então salva cada jogador carregado em paralelo. Esse loop não desloca servidores ou jogadores, então servidores que começam em tempos semelhantes podem descarregar juntos.
Desloque o primeiro salvamento de cada jogador por uma duração aleatória dentro do intervalo para que servidores ativos não escrevam todos ao mesmo tempo:
local AUTO_SAVE_INTERVAL = 180local function startAutoSave(player)task.spawn(function()task.wait(math.random() * AUTO_SAVE_INTERVAL)while player.Parent doif canSave(player) thensavePlayerData(player)endtask.wait(AUTO_SAVE_INTERVAL)endend)endQuando uma requisição para desligar o servidor é recebida, o seguinte ocorre em um callback BindToClose:
- Uma requisição é feita para salvar os dados de cada jogador no servidor, seguindo o processo normalmente seguido quando um jogador sai do servidor. Essas requisições são feitas em paralelo, já que callbacks de BindToClose têm apenas 30 segundos para serem concluídos.
- Para acelerar os salvamentos, todas as outras requisições na fila de cada chave são limpas do DataStoreWrapper subjacente (veja Repetições).
- O callback não retorna até que todas as requisições tenham sido concluídas.