Bases de Dados (CC2005), Dep. Ciência de Computadores, FCUP
Eduardo R. B. Marques, DCC/FCUP
Objectivos: Introdução a transações em bases de dados.
Referências: Transações ("slides" das aulas teóricas)
Baixe o ficheiro account.sql que contém:
mysql> source account.sql
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.05 sec)
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM ACCOUNT;
+-----------+-------+
| AccountId | Value |
+-----------+-------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
| 4 | 400 |
| 5 | 500 |
+-----------+-------+
5 rows in set (0.00 sec)
Execute a seguinte sequências de instruções, notando que esta se inicia com START TRANSACTION, que define o início de uma transação:
START TRANSACTION;
INSERT INTO ACCOUNT(AccountId, Value) VALUES(6, 600);
UPDATE ACCOUNT SET Value = Value - 100 WHERE AccountId = 1;
UPDATE ACCOUNT SET Value = Value + 100 WHERE AccountId = 2;
SELECT * FROM ACCOUNT;
Deverá obter:
+-----------+-------+
| AccountId | Value |
+-----------+-------+
| 1 | 0 |
| 2 | 300 |
| 3 | 300 |
| 4 | 400 |
| 5 | 500 |
| 6 | 600 |
+-----------+-------+
Execute de seguida:
ROLLBACK;
SELECT * FROM ACCOUNT;
A instrução ROLLBACK desfaz os efeitos da transação. Deverá observar que o estado anterior ao início da transação foi reposto, i.e., haverá 5 contas em que a conta i volta a ter o valor de i * 100.
Agora repita as instruções no passo 1, e execute de seguida
COMMIT;
SELECT * FROM ACCOUNT;
Deverá observar que os efeitos da transação se tornaram persistentes.
Recarregue account.sql e considere o código inicial do procedimento transfer.
Observe que transfer executa uma transação onde é debitado um montante amount de uma conta id1 e credita esse valor em outra conta id2. Adicionalmente, é retornado no parâmetro de saída done o valor TRUE.
Por exemplo, tendo em conta o estado inicial da BD, executando:
CALL transfer(4, 5, 100, @done);
SELECT @done;
SELECT * FROM ACCOUNT;
deverá obter:
+-------+
| @done |
+-------+
| 1 |
+-------+
1 row in set (0.00 sec)
+-----------+-------+
| AccountId | Value |
+-----------+-------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
| 4 | 300 |
| 5 | 600 |
+-----------+-------+
5 rows in set (0.00 sec)
Observe que transfer completa a transação com sucesso mesmo que as contas id1 e/ou id2 não existam!
Por exemplo execute:
source account.sql
CALL transfer(3, 666, 300, @done1);
CALL transfer(666, 4, 400, @done2);
SELECT * FROM ACCOUNT;
Obterá o seguinte estado na BD, em que as contas 3 e 4 foram respectivamente debitadas e creditadas de forma "indevida":
+-----------+-------+
| AccountId | Value |
+-----------+-------+
| 1 | 100 |
| 2 | 200 |
| 3 | 0 |
| 4 | 800 |
| 5 | 500 |
+-----------+-------+
Modifique o código por forma à transação ser desfeita com ROLLBACK caso alguma das contas não exista e devolva done = FALSE nesse caso.
Outro problema é que transfer não verifica se o saldo da conta debitada (id1) é suficiente para a transferência
Por exemplo execute:
source account.sql
CALL transfer(1, 2, 200, @done);
SELECT * FROM ACCOUNT;
Obterá um valor negativo para a conta 1:
+-----------+-------+
| AccountId | Value |
+-----------+-------+
| 1 | -100 |
| 2 | 400 |
| 3 | 300 |
| 4 | 400 |
| 5 | 500 |
+-----------+-------+
Modifique novamente o código de transfer por forma a que seja verificado que o saldo da conta debitada (id1) é igual ou superior a amount. Em caso negativo, analogamente ao exercício anterior, a transação deve ser desfeita com ROLLBACK e o parâmetro de saída done deve preenchido com FALSE.
Seria adequado também já agora validar se id1 != id2, e desfazer a transação quando id1 = id2. Embora os efeitos de débito e crédito se anulem neste caso, a operação não faz sentido lógico do ponto de vista de "regras de negócio". Modifique o código de novo.
Considere que
T1: CALL transfer(a,b,100,@done1)
e
T2: CALL transfer(c,d,200,@done2)
são transações que executam concorrentemente, e que poderão falhar (no sentido de serem desfeitas com ROLLBACK) nas condições descritas no exercício anterior (ids de contas não existentes; saldo insuficiente nas contas debitadas).
Assuma o estado inicial da BD definido em account.sql (conta AccountId = i tem um saldo de Value = i * 100 para i=1,...,5). Indique se T1 e/ou T2 completam ou desfeita, e quais poderão ser o(s) estado(s) da BD depois de uma execução serializável de T1 e T2 em concorrência. e considere os seguintes casos:
Caso | a | b | c | d |
---|---|---|---|---|
1 | 1 | 2 | 3 | 4 |
2 | 666 | 3 | 3 | 4 |
3 | 2 | 1 | 1 | 4 |
4 | 2 | 3 | 3 | 4 |
5 | 2 | 1 | 2 | 4 |
Dica: Os estados(s) possíveis são dados por T1 seguida de T2, ou vice-versa. Analise o que deverá obter em cada caso. Para validar a sua análise, se transfer estiver definido correctamente, pode por exemplo executar:
SET @a = ...;
SET @b = ...;
SET @c = ...;
SET @d = ...;
-- T1 >> T2
source account.sql;
CALL transfer(@a, @b, 100, @done1);
CALL transfer(@c, @d, 200, @done2);
SELECT * FROM ACCOUNT;
-- T2 >> T1
source account.sql;
CALL transfer(@c, @d, 200, @done2);
CALL transfer(@a, @b, 100, @done1);
SELECT * FROM ACCOUNT;
Faça uma análise semelhante aos casos do exercício anterior considerando a execução de 4 transações ilustrada abaixo, onde T1 e T2 executam concorrentemente seguidas de T3 e T4 que também executam concorrentemente.
Dica: Os casos a considerar para uma execução serializável são 4, dependendo da ordem relativa dos efeito lógicos de T1 e T2, e de T3 e T4.