terça-feira, 11 de setembro de 2012

Aplicações com muitas Transações Concorrentes

Uma vez uma pessoa com quem trabalhei, por um breve período, me contou uma experiência que ela teve ao reescrever a lógica de uma aplicação diretamente no banco de dados. Eles passaram a usar triggers, stored procedures e funções que eram escritas e executadas no servidor de banco de dados. Eu perguntei a esta pessoa qual foi a razão deles terem feito isso. A pessoa me explicou que eles tiveram problemas de forçar a consistência das regras da aplicação quando muitos usuários acessavam a aplicação ao mesmo tempo. Por exemplo: A aplicação era usada em uma universidade para permitir que os alunos se registrassem para diferentes disciplinas. Ela reforçava regras para limitar a quantidade de alunos por disciplina e também para evitar que os alunos escolhecem disciplinas com horários conflitantes.

A pessoa envolvida nesta reescrita me explicou que não era possível reforçar as regras da aplicação somente na linguagem de programação cliente. Que em ambientes como muitas transações concorrentes a aplicação cliente do banco de dados vez ou outra vai ler dados obsoletos e gerar resultados errados. Eu não estava tão certo quanto esta pessoa de que isso era verdade. Eu sei que os bancos de dados providenciam comandos, que podem ser iniciados a partir do cliente, para iniciar transações e persistir os dados envolvidos ou simplesmente não persistir nada. E também temos os níveis de isolamento de transações.

Eu resolvi escrever um pequeno programa para testar se é realmente impossível escrever lógica de negócios usando uma linguagem de programação cliente, em um ambiente de muitas transações concorrentes. Para simular as muitas transações concorrentes eu vou usar o utilitário Apache Benchmark ou ab.

O banco de dados (dump postgres) é bem simples e consiste de apenas três tabelas: students(id, name, date_of_birth), courses(id, name, workload) e students_courses(id_student, id_course). O servidor é postgres versão 8.4. Quanto aos dados temos 5000 estudantes e 500 cursos. A regra de negócio bem simples, cada curso pode ter no máximo 10 estudantes registrados. Ou seja, na tabela sutdents_courses um curso pode ter no máximo 10 linhas com seu id na coluna id_course.

Quando o código da aplicação é executado ele escolhe um estudante e um curso ao acaso e tenta registrar este estudante para este curso escolhido. Primeiro a aplicação faz uma verificação para ver se o curso está disponível (verifica se existem menos de 10 estudantes registrados para o curso). Caso o curso esteja lotado o estudante não é registrado. Caso contrário uma linha é inserida na tabela students_courses.

A minha primeira tentativa foi usando PHP junto com a versão mais recente do CodeIgniter. Que pode ser baixada aqui. O código principal é este aqui:

Eu usei os comandos trans_start e trans_complete de acordo com a documentação deles.

O teste de concorrência foi feito chamando-se o ab da seguinte maneira:
ab -c 50 -t 30 http://localhost/www/transactions/index.php/course_registration_codeigniter_version

Os parâmetros indicam que o servidor será acessado durante 30 segundos por 50 conexões simultâneas. O último parâmetro é a url do servidor.

Depois do teste executando-se a seguinte consulta:

Eu percebi que a regra da aplicação tinha sido violada. Existiam cursos com mais de 10 alunos. Após este primeiro teste me perguntei se isso poderia ter sido resultado da implementação interna do CodeIgniter. Não cheguei a investigar como o CodeIgniter implementa transações mas resolvi fazer outro teste. Desta vez usando um script php simples que por sua vez usa PDO para acesso ao banco de dados. A parte mais importante do código está logo abaixo:

Mais uma vez o comando ab é executado apenas mudando-se a url:
ab -c 50 -t 30 http://localhost/www/transactions/course_registration_pdo_sem_locks.php

E mais uma vez eu vejo que a regra da aplicação foi quebrada com cursos com mais de 10 estudantes. Será que esta pessoa com quem trabalhei tinha razão?

No meu teste usando-se apenas transações não foi o suficiente para garantir a integridade das regras da aplicação. Fiz algumas pesquisas e achei este documento. Foi nele que ouvi falar do SELECT FOR UPDATE. O que também não funcionou. O problema é que a opção bloqueia as linhas retornadas pelo SELECT executado de sofrerem operações as operações UPDATE, DELETE e SELECT FOR UPDATE. Como meu objetivo não é alterar nenhuma das linhas retornadas mas sim inserir na tabela o SELECT FOR UPDATE não adianta neste caso.

O que eu realmente preciso é bloquear a tabela para que não sofra atualizações enquanto eu estou checando a regra da aplicação. Uma operação de LOCK. Basicamente vou bloquear a tabela para que não sofra alterações, checar a regra da aplicação e desbloqueá-la no fim da transação. A parte principal do código está logo abaixo:
Novamente testo com o comando ab:
ab -c 50 -t 30 http://localhost/www/transactions/course_registration_pdo.php

E agora sim. A regra da aplicação foi satisfeita, não temos mais cursos com mais de 10 alunos. Uma coisa que percebi foi que as consultas durante o teste aparentemente ficaram bem mais lentas, o LOCK de toda a tabela deve cobrar seu preço. Outra observação sobre o LOCK é que ele não faz parte do padrão SQL, portanto um código que possa rodar em diferentes bancos de dados deve ficar sob a responsabilidade do desenvolvedor.

Conclusão

Sim é possível utilizarmos uma linguagem de programação cliente para validarmos e reforçarmos as regras de aplicação com alta concorrência de transações. Mas para isso temos que usar recursos oferecidos pelo próprio banco de dados.

Acredito que o uso de linguagens de programação cliente nestes casos traz facilidades de desenvolvimento e manutenção associadas aos recursos das linguagens e das bibliotecas disponíveis. Mas também temos que lembrar que existe o outro lado da moeda, apesar de não ter testado isso, acredito que haveria ganhos de velocidade significativos programando-se a lógica da aplicação diretamente no banco de dados.

Como sempre não existe bala de prata, você deve analisar o contexto, requisitos e recursos disponíveis para tomar estas decisões.

Todo o código da aplicação pode ser obtido aqui. Não se esqueça configurar os dados de acesso ao banco de dados nos arquivos pdo e no arquivo application/config/database.php.