Friday, February 11, 2011

SQL Server - Transactions & Locking #2

Problemas de Concorrência Clássicos
Logo que existam dois utilizadores (ou conexões) sobre a mesma base de dados há um potencial para problemas de concorrência. Os sistemas de gestão de bases de dados existem também para os minimizar. Mas não os resolvem todos sem intervenção do programador. Por isso importa descrever os 5 efeitos de concorrência mais importantes.

Lost Updates

Ocorre quando duas ou mais transações consultam a mesma linha e a atualizam com base no valor original. Como cada uma das transações desconhece a existência das restantes, a última atualização sobrepõe todas as alterações feitas pelas restantes, resultando em perda de dados.

Exemplo:
| Transacção 1              | Transacção 2              | Valores   |
|---------------------------|---------------------------|-----------|
| SELECT A FROM B WHERE ... | (Idle)                    | X = 20    |
| (...)                     | SELECT A FROM B WHERE ... | Y = 20    |
| UPDATE B SET A = Y + 10   | (...)                     | B(A) = 30 |
| (Idle)                    | UPDATE B SET A = Y + 20   | B(A) = 40 |
Como é visível no exemplo, a actualização da primeira transação que termina a operação é perdida.

Dirty Read (Uncommited Dependency)

Ocorre quando uma dada transação acede a dados que estão a ser atualizados por outra transação. Como esta segunda transação ainda não foi terminada (COMMIT) não há garantia que os valores obtidos não serão ainda alterados. Ou mesmo que essa transação não será abordada (revertendo as suas alterações).

Exemplo:
| Transacção 1              | Transacção 2              | Valores   |
|---------------------------|---------------------------|-----------|
| (Idle)                    | (Idle)                    | B(A) = 20 |
| UPDATE B SET A = 10       | (Idle)                    | B(A) = 10 |
| (...)                     | SELECT A FROM B WHERE ... | Y = 10    |
| ROLLBACK                  | (...)                     | B(A) = 20 |

Esta situação causa que a segunda transação trabalhe com um valor de 10 quando, porque a alteração da primeira transacção foi revertida, o valor correcto seria 20.

Nonrepeatable Read (Inconsistent Analysis)

Ocorre quando uma transação acede ao mesmo registo várias vezes e obtém valores diferentes de cada vez. É semelhante ao “dirty read” excepto que a transação que está a alterar esses dados termina com sucesso (COMMIT) várias atualizações dos dados.

Exemplo:
| Transacção 1              | Transacção 2              | Valores   |
|---------------------------|---------------------------|-----------|
| (Idle)                    | (Idle)                    | B(A) = 0  |
| UPDATE B SET A = 10       | (Idle)                    | B(A) = 10 |
| COMMIT                    | (...)                     | B(A) = 10 |
| (...)                     | SELECT A FROM B WHERE ... | Y = 10    |
| UPDATE B SET A = 20       | (Idle)                    | B(A) = 20 |
| COMMIT                    | (...)                     | B(A) = 20 |
| (...)                     | SELECT A FROM B WHERE ... | Y = 20    |
| UPDATE B SET A = 30       | (Idle)                    | B(A) = 30 |
| COMMIT                    | (...)                     | B(A) = 30 |
| (...)                     | SELECT A FROM B WHERE ... | Y = 30    |

As várias leituras efetuadas pela segunda transacção, apesar de iguais, retornam sempre valores distintos.

Phantom Reads

Este problema é semelhante ao anterior, no entanto, revela-se através de diferenças no número de linhas obtidas entre cada leitura dos mesmos dados. Tipicamente, uma transação executa uma leitura de um determinado objeco (tabela) várias vezes enquanto outra transação concorrente insere ou remove linhas que afetam os resultados obtidos pela primeira.

Exemplo:
| Transacção 1              | Transacção 2              | Valores   |
|---------------------------|---------------------------|-----------|
| (Idle)                    | SELECT COUNT(*) FROM A    | N = 1000  |
| INSERT INTO A ...         | (Idle)                    | N = 1001  |
| COMMIT                    | (...)                     | N = 1001  |
| (...)                     | SELECT COUNT(*) FROM A    | N = 1001  |
| DELETE FROM A ...         | (Idle)                    | N = 1000  |
| COMMIT                    | (...)                     | N = 1000  |
| (...)                     | SELECT COUNT(*) FROM A    | N = 1000  |

As “phantom rows” podem afetar significativamente os calculados realizados pela segunda transação.

Deadlocks
Um deadlock ocorre quando duas transacções se bloqueiam mutuamente porque uma detém um objecto bloqueado a que a outra pretende aceder e vice-versa.

Exemplo:
| Transacção 1 | Transacção 2  | Obs.                          |
|--------------|---------------|-------------------------------|
| UPDATE A ... | (...)         | A tabela A é bloqueada por T1 |
| (...)        | UPDATE B ...  | A tabela B é bloqueada por T2 |
| SELECT B ... | (...)         | T1 fica à espera da tabela B  |
| (...)        | SELECT A ...  | T2 fica à espera da tabela A  |

O SQL Server implementa um mecanismo explícito para detectar e resolver estas situações automaticamente. E o fará escolhendo uma das transações para ser automaticamente abortada – de acordo com critérios relacionados com o custo associado ao ROLLBACK. Existem várias recomendações aplicáveis para evitar e lidar devidamente com este tipo de situações. Veremos alguns deles num dos próximos posts.

O SGBD existe também para minimizar e tratar graciosamente exactamente este tipo de problemas. Mas não o faz sempre de forma automática ou sem “custos” de programação e sobre o desempenho geral da base de dados. É neste terreno complexo que se revela a importância dos mecanismos de locking do servidor e da sua relação com as transações (e, mais importante, a sua correcta implementação).

No comments: