Todo mundo duelando pelo mesmo recurso

Concorrência

Você tem problemas de concorrência?

Essa é uma dor que conheço bem, e acredito que muitos passam por isso várias vezes na vida, inclusive você! que está ai as 2hrs da manhã tentando corrigir um bug e parou aqui 😉

Veja a situação hipotética mas que acontece bastante.

Temos uma funcionalidade que precisa, para cada vez que um Pedido/Orçamento/Pagamento/Etc seja criado, gerar um numero sequencial.
Não podemos ter numeros sequencias repetidos, e obviamente eles precisam ser incrementais, tipo, PED-1, PED-2, PED-3.

Quais são as soluções que encontramos por ai?

Uma delas é abrir uma transaction e informar o Isolation Level para bloquear leitura, com READ_COMMITED ou qualquer outra combinação, em seguida pega-se o último registro gerado, se incremenda o valor, salva o pedido e pronto, ta resolvido.

Outra opção é usar a combinação (TABLOCK, HOLDLOCK) e ver o mal de uma vez por todas, a solução final é semelhante as combinações de transaction e isolation.

Só que isso gera um grande gargalo, temos um lock em uma tabela que pode ser muito acessada, e então várias partes do sistema vão parar de funcionar porque nós travamos a tabela para poder gerar um número, tudo porque o cliente quis ter um número sequencial, ou protocolo.

Mas a vida não para por ai, os problemas de lentidão e aplicação parada não vai demorar a aparecer, se você der sorte já pode estar longe, ou então deu o azar de ser quem vai corrigir 🙂

Como então se resolve esse problema?

Bem, uma solução seria migrar este controle para uma segunda tabela, por exemplo, PedidoSequencial, e agora a tabela PedidoSequencial vai ter os números gerados. Em seguida fazemos a mesma coisa de antes, colocamos o isolation level, (TABLOCK, HOLDLOCK), incrementamos e salvamos na tabela Pedido a entidade com o valor.

Só que o problema continua, agora o sistema inteiro não fica mais travado esperando a transação, dessa forma somente as situações onde um número de pedido deve ser gerado (provavelmente apenas na criação) vão apresentar problemas de performance, pois só nesses casos em que acontece o lock na tabela de PedidoSequencial.

Não importa a ação que seja tomada, o problema vai persistir, e se chama “concorrência”.

Escalando o problema para situações multi-thread

Se retirarmos essa lógica do banco, outros problemas surgem. Por exemplo, poderiamos criar uma variável que fica em memória e ela é carregada com o último registro do banco, a partir daí todo o controle de incremento é feito via aplicação. Isso significa que o objeto que controla a geração do número vai ter um monte de “locks”, pois em uma aplicação multi-thread temos vários clientes vão criar pedidos a todo instante.

Quando a situação ficar terrívelmente complexa, trocamos os locks por outros mecanismos que apenas escondem o problema, por exemplo trocar lock por Monitor.Enter e Monitor.Exit, singletons e etc.

Como então podemos fazer essa lógica sem usar locks? sem compartilhar o estado da aplicação e sem complexidade?
Parece até propaganda da tekpix, vamos lá!

Se o nosso problema é resolver a concorrência de acesso a um recurso, quer seja para manipular seu valor ou executar algum procedimento, o ponto central a ser resolvido é construir um mecanismo que gerencie a concorrência entre as threads dos clientes que realizam a requisição. Como “building blocks” da nossa solução podemos utilizar filas, mensagens, eventos, etc, mas tudo isso ainda é muito complexo para o que queremos aqui.

Não precisamos sair implementando padrões de concorrência, precisamos apenas de uma biblioteca que faça isso por nós, enquanto nos concentramos em resolver a estória do usuário, que é nosso objetivo principal.

Para isso podemos utilizar o ZeroMQ. Já abordamos várias vezes sobre do que se trata esta library, e neste artigo vamos nos focar em utilizar o ZeroMQ resolvendo o monstro que temos embaixo da cama, vamos utiliza-lo para resolver a concorrência no acesso ao nosso mecanismo de geração de números.

Resolvendo o problema com ZeroMQ

A solução é muito simples, no nosso cenário estamos lidando com uma aplicação monolítica que precisa gerar números no momento de se criar uma entidade.

Para isso vamos subir uma thread que vai ter o nosso socket de “server” e que vai atender cada request de numero de pedido que chegar, só isso. Dessa forma não ficamos manipulando lock em variável, controlamos a concorrencia da aplicação, e paramos de travar o banco para cada ação no sistema.

O cliente é meramente uma classe que faz uma chamada no nosso server solicitando um número de pedido, não importa se as chamadas são síncronas ou assíncronas, nosso server recebe uma requisição, lida com ela e devolve a resposta, ou seja, todas as requisições são empilhadas e processadas uma a uma.

Vamos analisar o código do nosso server:

Server

Task taskA = new Task(() =>
{
    int pedId = 0;

    using (var server = new RouterSocket("@inproc://localhost:6000"))
    {
        while (true)
        {
        var message = server.ReceiveMultipartMessage();
        pedId++;

        var messageToRouter = new NetMQMessage();
        messageToRouter.Append(message[0]);
        messageToRouter.AppendEmptyFrame();
        messageToRouter.Append($"PED-{pedId}");

        server.SendMultipartMessage(messageToRouter);
        }
    }
});

taskA.Start();

return taskA;

Nós criamos uma task que vai conter o nosso socket servidor, isso pode ser feito no Startup da aplicação, Global.asax, qualquer que seja o template de projeto que você utilize provavelmente existirá o ponto de entrada inicial da aplicação.

Criamos nossa variável pedId e iniciamos ela com o valor de entrada, em geral este valor provavelmente virá do banco.
Logo na sequência nos indicamos no server que vamos aguardar por uma mensagem, e nesse momento o servidor aguarda os clientes enviarem a requisição.

Assim que o server recebe um request de algum cliente, nós incrementamos o valor e retornamos para ele o número de pedido gerado, e aguardamos por uma nova mensagem de requisição, e pronto, está feito nosso servidor para geração de protocolos.

Vamos agora analisar o código do cliente.

Client

var tasksClients = new Task[numberOfClients];

for (var i = 0; i < numberOfClients; i++)
{
    tasksClients[i] = Task.Run(() =>
    {
        var threadId = Guid.NewGuid().ToString();
        using (var client = new DealerSocket())
        {
            client.Options.Identity = Encoding.UTF8.GetBytes(threadId);

            Console.WriteLine($"{threadId} calling");

            var msg = new NetMQMessage();
            msg.AppendEmptyFrame();
            msg.Append(threadId);

            client.Connect("inproc://localhost:6000");
            client.SendMultipartMessage(msg);

            var result = client.ReceiveMultipartMessage();

            Console.WriteLine($"{threadId} response {result[1].ConvertToString()}");
        }
    });
}
return tasksClients;

Na função acima, estamos criando vários clientes baseados na quantidade informada com o parâmetro numberOfClients, meramente ilustrativo para simularmos um ambiente assíncrono.

Atribuimos uma identificação para o nosso cliente, e na sequência enviamos uma mensagem para o servidor que não possui muita informação, basicamente apenas sinaliza que desejamos obter um número de pedido.

Temos então vários clientes, que em uma aplicação asp.net simboliza a requisição, ou seja, cada POST que você recebe na API vai ser um cliente requisitanto número.
O resultado, simulando 5 clientes assíncronos é:

fa48a091-f696-4874-88ea-51355be197bf calling
eb314fac-3aa4-4604-ab2d-4e918d03cdec calling
2698e504-498b-4f19-ae5c-1646a7a6c438 calling
34994b58-147d-4989-b6c6-3fc7e8e51ae0 calling
fb376fa1-154c-4021-8638-3d73cddea123 calling
34994b58-147d-4989-b6c6-3fc7e8e51ae0 response PED-4
eb314fac-3aa4-4604-ab2d-4e918d03cdec response PED-1
fa48a091-f696-4874-88ea-51355be197bf response PED-5
fb376fa1-154c-4021-8638-3d73cddea123 response PED-3
2698e504-498b-4f19-ae5c-1646a7a6c438 response PED-2

Não controlamos o start de cada thread, isso não importa para nós pois não sabemos quando uma requisição vai chegar.
O que estamos preocupados em controlar é que independentemente da quantidade de requisições assíncronas que recebemos, conseguimos processa-las de forma sequêncial sem compartilhar variáveis, sem locks.

Estamos criando sockets inproc, ou seja, que vivem apenas dentro da thread do processo atual (da sua aplicação dotnet core por exemplo), mas podemos escalar isso facilmente, utilizando TCP com o socket iniciado em outro processo ou servidor, ou seja, de graça temos um código que escala facilmente.

Github

https://github.com/dougramalho/concurrentzmq

Créditos

Abertura: Photo by Quino Al on Unsplash

Tags: | | | | | | | | | |

Sobre o Autor

Douglas Ramalho
Douglas Ramalho

Pesquisador apaixonado por filmes, música e bits, muitos bits. Sou um profissional viciado em tecnologia, com 11 anos de experiência em TI e um caminho repleto de suor, sangue e bugs. Gosto não apenas de estudar, mas também de promover o conhecimento, dessa forma meu objetivo é compartilhar toda a minha experiência para que possamos evoluir juntos, e também ajudar outras pessoas neste processo, para que em um grande grupo de apaixonados possamos elevar o nível de excelência a um patamar ainda maior.

0 Comentários

Deixe uma resposta

O seu endereço de e-mail não será publicado.