StackExchange.Redis: Pipeling
Tags: redis, programming, dotnet, braziliandevs
Published: 2023-02-19 05:44:29

## Introdução

Pipelining é um conceito extremamente importante para maximizar a taxa de transferência para o Redis. Quando você precisa executar vários comandos no Redis e os resultados intermediários podem ser temporariamente ignorados, o pipelining pode reduzir drasticamente o número de idas e voltas necessárias para o Redis, o que pode aumentar drasticamente o desempenho, pois muitas operações são centenas de vezes mais rápidas que o `Round Trip Tempo (RTT)`.

- `Round Trip Tempo (RTT)`.
    
    É uma medida do tempo que leva para um cliente enviar uma solicitação ao servidor e receber uma resposta de volta. O RTT geralmente é medido em milissegundos (ms) e depende de fatores como a distância entre o cliente e o servidor, a velocidade da conexão de rede e a ocupação do servidor.
    

Com `StackExchange.Redis`, há duas maneiras de canalizar comandos, implicitamente com a `API Async` e explicitamente com a `API IBatch`.

## Pipelining implícito com `API Async`

Se você usar a versão assíncrona de um comando, o comando será automaticamente canalizado para o Redis. Se você usar `await` para aguardar o resultado de uma tarefa despachada por um desses comandos assíncronos, ele aguardará até que o comando seja concluído antes de retornar o controle. No entanto, se você agrupar um conjunto de tarefas despachadas pelos métodos assíncronos e aguardar todas de uma vez, o `ConnectionMultiplexer` encontrará automaticamente uma maneira eficiente de canalizar os comandos para o Redis, para que você possa reduzir significativamente o número de viagens de ida e volta .

## Pipelining explícito com `IBatch`

Você também pode ser muito mais explícito sobre os comandos de pipelining. A `API IBatch` fornece apenas a interface assíncrona para comandos. Você pode configurar quantos comandos quiser canalizar com esses métodos assíncronos, preservando as tarefas, pois elas fornecerão os resultados. Quando você tiver todos os comandos que deseja enviar por pipeline, poderá chamar `Execute` para executá-los. Isso canalizará todos os seus comandos em um bloco contíguo para o Redis. Usando esse método, nenhum outro comando será intercalado com seus comandos em lote do cliente. No entanto, se houver outros clientes enviando comandos para o Redis, é possível que seus comandos sejam intercalados com os comandos em lote.

## Na prática

O pipelining pode ser crucial para aumentar a taxa de transferência em sua instância do Redis. Veremos como executar a mesma coleção de tarefas em três modos.

- Em série e sem pipeline
- Pipeline implícito
- Pipeline explicitamente com `IBatch`

### Preparando ambiente

Antes de executarmos nossos exemplos, vamos inicializar um Multiplexer, obter uma instância de um `IDatabase` e criar e iniciar um `StopWatch`:

```csharp
using System.Diagnostics;
using StackExchange.Redis;

var options = new ConfigurationOptions
{
    EndPoints = new EndPointCollection { "localhost:6379" }
};

var muxer = ConnectionMultiplexer.Connect(options);
var db = muxer.GetDatabase();

var stopwatch = Stopwatch.StartNew();
```

### Sem Pipeline

Enviar uma cadeia serial de comandos que não estão em pipeline é bastante simples. Por uma questão de consistência com os outros exemplos, usaremos a versão assíncrona do comando Ping. Vamos apenas ligar e aguardar o resultado 1000 vezes. Isso fará com que o multiplexador aguarde a conclusão do comando entre as execuções e o impedirá de canalizar os comandos automaticamente.

```csharp
for (var i = 0; i < 1000; i++)
    await database.PingAsync();
Console.WriteLine($"1000 un-pipelined commands took: {stopwatch.ElapsedMilliseconds}ms to execute");
```

```bash
// output
1000 un-pipelined commands took: 497ms to execute
```

### Pipeline implícito

Não vamos aguardar cada tarefa conforme ela é despachada. Em vez disso, coletaremos todas as tarefas e as aguardaremos em massa no final. Cada tarefa é responsável por conter os resultados do comando após a conclusão do comando:

```bash
var pingTasks = new List<Task<TimeSpan>>();
for (var i = 0; i < 1000; i++)
    pingTasks.Add(database.PingAsync());
await Task.WhenAll(pingTasks);
Console.WriteLine($"1000 automatically pipelined tasks took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
```

```bash
// output
1000 automatically pipelined tasks took: 6ms to execute, first result: 00:00:00.0011550
```

### Pipelining explícito com `IBatch`

Vamos fazer com que os nossos pings utilizem um `IBatch`. Um `IBatch` garantirá que o cliente envie todo o lote para o Redis de uma só vez, sem outros comandos intercalados no pipeline. Este é um comportamento ligeiramente diferente do nosso pipelining implícito, pois no caso de pipelining implícito, os comandos podem ser intercalados com quaisquer outros comandos que o cliente estava executando no momento.

Seguiremos um padrão semelhante, neste caso, no entanto, usaremos o método `IDatabase.CreateBatch()` para criar o lote e usaremos os métodos assíncronos do lote para 'despachar' as tarefas. É importante observar aqui que, ao contrário do nosso caso implícito, as tarefas não serão realmente despachadas até que o método `IBatch.Execute()` seja chamado. Se você tentar aguardar qualquer uma das tarefas antes disso, poderá travar acidentalmente seu comando. Depois de chamar `Execute`, você pode aguardar todas as tarefas.

```bash
var pingTasks = new List<Task<TimeSpan>>();
var batch = database.CreateBatch();
for (var i = 0; i < 1000; i++)
    pingTasks.Add(batch.PingAsync());
batch.Execute();
await Task.WhenAll(pingTasks);
Console.WriteLine($"1000 batched commands took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
```

```bash
// output
1000 batched commands took: 7ms to execute, first result: 00:00:00.0011708
```

### Código final

```csharp
using StackExchange.Redis;
using System.Diagnostics;

var options = new ConfigurationOptions
{
    EndPoints = new EndPointCollection { "localhost:6379" }
};

var conn = ConnectionMultiplexer.Connect(options);
var database = conn.GetDatabase();

await RedisPipelining.Unpipelined(database);
await RedisPipelining.ImplicitlyPipelined(database);
await RedisPipelining.ExplicitPipeliningWithIBatch(database);

public static class RedisPipelining
{
    public static async Task Unpipelined(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        for (var i = 0; i < 1000; i++)
            await database.PingAsync();
        Console.WriteLine($"1000 un-pipelined commands took: {stopwatch.ElapsedMilliseconds}ms to execute");
    }

    public static async Task ImplicitlyPipelined(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        var pingTasks = new List<Task<TimeSpan>>();
        for (var i = 0; i < 1000; i++)
            pingTasks.Add(database.PingAsync());
        await Task.WhenAll(pingTasks);
        Console.WriteLine($"1000 automatically pipelined tasks took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
    }

    public static async Task ExplicitPipeliningWithIBatch(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        var pingTasks = new List<Task<TimeSpan>>();
        var batch = database.CreateBatch();
        for (var i = 0; i < 1000; i++)
            pingTasks.Add(batch.PingAsync());
        batch.Execute();
        await Task.WhenAll(pingTasks);
        Console.WriteLine($"1000 batched commands took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
    }
}
```

## Conclusão

Ao utilizar a biblioteca em série e sem pipeline, o desempenho pode ser afetado negativamente, especialmente em cenários em que são necessárias várias operações de leitura ou escrita.

No modo com pipeline implícito, as operações de leitura e escrita são agrupadas em um pipeline automaticamente, melhorando significativamente o desempenho. Isso é particularmente útil para aplicativos que realizam muitas operações de leitura e escrita em sequência.

Já no modo com pipeline explicitamente usando a interface `IBatch`, é possível agrupar manualmente as operações em um pipeline e executá-las de forma assíncrona, o que pode melhorar ainda mais o desempenho. Este modo é especialmente útil em aplicativos que precisam realizar operações de cache em segundo plano, para evitar atrasos no processamento principal do aplicativo.

O uso da biblioteca `StackExchange.Redis` é uma ótima opção para gerenciar o cache em aplicativos .NET, e o modo de utilização mais adequado depende do cenário de uso e dos requisitos de desempenho do aplicativo.