Sunday, February 13, 2011

SQL Server - Transactions & Locking #5

Deadlocks

Um deadlock é uma condição de erro que pode acontecer em qualquer sistema de multiprocessamento ou multi-threading e que, por isso, não é exclusivo de sistemas de gestão de bases de dados.

No contexto do SQL Server, que nos interessa por agora, um deadlock ocorre quando duas ou mais operações (transações) se bloqueiam permanentemente. Porque cada uma das transações possui um lock sobre um recurso que qualquer outra está a tentar bloquear.

Eis um exemplo muito simples:

    * A transação A adquire um lock de leitura do registo 1.
    * A transação B adquire um lock de leitura do registo 2.
    * A transação A solicita um lock de escrita sobre o registo 2 e fica bloqueada até que a transação B termine e liberte o lock que possui.
    * A transação B solicita um lock de escrita sobre o registo 1 e fica bloqueada, da mesma forma, pela transação A.

Nenhuma das transações tem condições para terminar. Por isso o SQL Server implementa um mecanismo – Deadlock Monitor – que periodicamente analisa as transações activas para detectar este tipo de situações.
Quando uma condição de dealock é encontrada, o monitor termina automaticamente com erro uma das transações de forma que a outra transação possa terminar normalmente.
A transação abortada é conhecida como “dealock victim” e é escolhida segundo critérios baseados no custo envolvido no respectivo ROLLBACK. Fica ao critério da aplicação tratar o erro ocorrido e, eventualmente, submeter novamente a transação.

Tipos de Deadlocks

O exemplo que apresentei ilustra, desde logo, que os deadlocks podem ocorrer sobre vários tipos de recursos – registos, índices, páginas, tabelas ou bases de dados – e por várias operações (não é necessário que exista um lock exclusivo antes da leitura dos dados pela segunda transação).
Na realidade, é possível a ocorrência de dealocks no SQL Server também sobre outros recursos do sistema não directamente relacionados com os dados como, por exemplo, blocos de memória ou worker threads.

Tratamento de Deadlocks


Quando o motor da base de dados detecta um dealock:

    * Selecciona uma das transações como vítima,
    * Cancela a operação em curso,
    * Desfaz todas as operações realizadas por essa transação (ROLLBACK) e
    * Devolve a mensagem de erro 1205.

Em teoria qualquer aplicação que submeta queries à base de dados pode vir a ser seleccionada como vítima num deadlock. Por isso, qualquer aplicação deve incluir um tratamento de erros específico para este efeito (capaz de tratar o código de erro 1205).
Então o “error handler” poderá voltar a submeter a mesma transação de modo que o utilizador poderá mesmo nem se aperceber do erro ocorrido. No entanto, é recomendável que a aplicação seja parada por um período de tempo aleatório antes de o fazer de modo a dar tempo à outra transação para terminar e libertar os recursos que causaram o erro. É importante que esse tempo seja aleatório para evitar que, se, por exemplo, duas transações sejam canceladas para terminar o dealock não voltem a tentar a transação em simultâneo, potenciando a ocorrência de novo deadlock, agora entre essas transações.

Como minimizar a ocorrência de dealocks?

Para além das regras básicas de implementação de transações:

    * Não programar interações com o utilizador dentro de uma transação.
    * Reduzir ao máximo a duração e o número de operações de cada transação.

É muito importante:

    * Aceder aos recursos pela mesma ordem nas várias transações concorrentes.

E ainda é possível, dependendo de cada caso particular:

    * Utilizar um nível de isolamento inferior (para reduzir a duração dos locks).
    * Utilizar “bound connections” (quando usadas, todas as conexões abertas pela mesma aplicação podem cooperar entre si, “partilhando” os locks adquiridos por cada uma, logo nunca se bloqueando entre si).

Conclusões

Um deadlock é uma das condições de erro mais fácil de compreender e explicar a um programador de bases de dados. No entanto, também é um dos erros mais difíceis de diagnosticar e resolver porque envolve, na maioria das vezes, um nível de concorrência ou de sincronização nos testes que é difícil de reproduzir em laboratório. Por outro lado, na perspectiva do utilizador é um erro muito difícil de aceitar até pela mensagem erro apresentada (fala de ter sido vítima de uma coisa, o que parece logo muito mais grave).

Portanto, a abordagem mais correta será, garantidos os princípios básicos de programação de transações (que os minimizam), efectivamente ter um tratamento de erros adequado e pessimista.
Nos próximos posts, já em jeito de conclusão, vamos analisar formas práticas de tratamento de erros de locking (incluindo deadlocks).

No comments: