Pular para conteúdo

List: a fila da dungeon

Lição 101-03 · Lab · 8 min

Uma fila em que a ordem de chegada importa é uma List: sequência ordenada em que inserir e remover nas pontas custa O(1), não importa o tamanho. RPUSH coloca quem chegou no fim, LPOP entrega a quem espera há mais tempo. E BRPOP deixa um worker esperando trabalho sem polling; ao custo de prender uma conexão enquanto espera, tema de Conexões bloqueantes.

O que o lab faz

  • Colocar cinco jogadores na fila com um único RPUSH
  • Medir e listar a fila com LLEN e LRANGE 0 -1
  • Achar a posição de Nix com LPOS
  • Formar uma party tirando os dois primeiros com LPOP ... 2
  • Esperar 1 segundo numa fila vazia com BRPOP e ver o nil chegar no tempo certo

Faça agora

./quest run 101-03 jedis
./quest run 101-03 lettuce    # opcional: mesmo lab, outro client
./quest verify 101-03

O código

try (RedisClient jedis = Clients.jedis()) {
    jedis.unlink(queue, raid);
    long queued = jedis.rpush(queue, "brom", "lyra", "nix", "seraphine", "ysolde");   // 5

    long size = jedis.llen(queue);                          // 5
    List<String> line = jedis.lrange(queue, 0, -1);         // [brom, lyra, nix, seraphine, ysolde]
    Long pos = jedis.lpos(queue, "nix");                    // 2

    List<String> party = jedis.lpop(queue, 2);              // [brom, lyra]
    long waiting = jedis.llen(queue);                       // 3

    // blocking preview: 1 second on an empty list, then null
    List<String> popped = jedis.brpop(1, raid);             // null after ~1000 ms
}
try (StatefulRedisConnection<String, String> connection = Clients.lettuceConnection()) {
    RedisCommands<String, String> redis = connection.sync();
    redis.unlink(queue, raid);
    Long queued = redis.rpush(queue, "brom", "lyra", "nix", "seraphine", "ysolde");   // 5

    Long size = redis.llen(queue);                          // 5
    List<String> line = redis.lrange(queue, 0, -1);
    Long pos = redis.lpos(queue, "nix");                    // 2

    List<String> party = redis.lpop(queue, 2);              // [brom, lyra]
    Long waiting = redis.llen(queue);                       // 3

    // blocking commands get their own connection: the shared one keeps serving
    try (StatefulRedisConnection<String, String> blocking = Clients.lettuceConnection()) {
        KeyValue<String, String> popped = blocking.sync().brpop(1, raid);   // null after ~1000 ms
    }
}

No Redis Insight

No Browser, quest:queue:dungeon aparece como List com três elementos e nix no índice 0: quem estava em terceiro virou o primeiro da fila. quest:queue:raid não aparece: uma List vazia não existe como chave, e o BRPOP que esperou nela não criou nada. Abra o Profiler e rode o lab: repare no intervalo de um segundo entre o BRPOP e o comando seguinte, com a conexão parada esperando.

Por dentro
Comando O que faz
RPUSH chave a b c Insere no fim (direita) e devolve o tamanho novo; LPUSH insere na frente
LLEN chave Tamanho da lista; 0 se a chave não existe
LRANGE chave 0 -1 Fatia por índices, inclusive; índices negativos contam do fim
LPOS chave elemento Índice da primeira ocorrência, O(n); nil se não estiver na lista
LPOP chave 2 Remove e devolve N elementos da frente (Redis 6.2+); RPOP faz o mesmo no fim
BRPOP chave 1 Como RPOP, mas espera até 1 s por um elemento; nil quando o tempo acaba
LMOVE origem destino LEFT RIGHT Move um elemento entre listas de forma atômica; base de filas com "em processamento"
flowchart LR
    R["RPUSH: entra no fim"] --> L["brom | lyra | nix | seraphine | ysolde"]
    L --> P["LPOP 2: sai pela frente"]
    P --> party["party: brom, lyra"]

Bloqueio no Lettuce pede conexão dedicada

O Lettuce multiplexa uma conexão para a aplicação inteira. Um BRPOP nela segura todos os comandos que chegarem atrás durante a espera. Por isso o lab abre uma segunda conexão só para bloquear e fecha em seguida. No Jedis cada comando pega uma conexão do pool, então o bloqueio ocupa uma conexão do pool inteira, o que é o mesmo custo com outra roupa.

Em produção
  • O timeout do bloqueio tem que ser menor que o timeout do client. O Jedis usa 2 s de socket timeout por padrão: um BRPOP ... 5 estoura SocketTimeoutException antes de o Redis responder. Ajuste socketTimeoutMillis ou mantenha bloqueios curtos. Mais em Timeouts, pool e retry.
  • Cada BRPOP prende uma conexão inteira enquanto espera. Com 30 conexões no plano free, cinco workers bloqueados já são um sexto do limite. Dimensione os workers pensando nisso.
  • Lista é fila simples: se o worker morre depois do LPOP, o item se perde. Para reconhecimento (ack), reentrega e vários consumidores, use Streams com consumer groups. Para logs recentes, limite o tamanho com LTRIM chave 0 999 depois de cada LPUSH.
Desafio

Suba uma Thread que faça RPUSH quest:queue:raid dorian meio segundo depois de o lab começar a esperar e veja o BRPOP voltar na hora com dorian, sem esperar o segundo inteiro (o verify continua passando: a fila da raid volta a ficar vazia). Depois troque LPOP por RPOP e note que o matchmaker passa a pegar quem chegou por último: a mesma lista virou uma pilha.