Bloqueio de linhas do InnoDB - como implementar

13

Eu estive olhando ao redor agora, lendo o site mysql e ainda não consigo ver exatamente como ele funciona.

Desejo selecionar e bloquear a linha do resultado para escrever, escrever a alteração e liberar a trava. o audocommit está ativado.

esquema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

Selecione um item com status pendente e atualize-o para funcionar. Use uma gravação exclusiva para garantir que o mesmo item não seja coletado duas vezes.

assim;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

obter o ID do resultado

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

Preciso fazer alguma coisa para liberar a trava e funciona como fiz acima?

Wizzard
fonte

Respostas:

26

O que você quer é SELECT ... FOR UPDATE no contexto de uma transação. SELECT FOR UPDATE coloca um bloqueio exclusivo nas linhas selecionadas, como se você estivesse executando UPDATE. Também é executado implicitamente no nível de isolamento READ COMMITTED, independentemente do nível em que o nível de isolamento está definido explicitamente. Lembre-se de que SELECT ... FOR UPDATE é muito ruim para simultaneidade e só deve ser usado quando for absolutamente necessário. Também tem uma tendência a se multiplicar em uma base de código à medida que as pessoas cortam e colam.

Aqui está uma sessão de exemplo do banco de dados Sakila que demonstra alguns dos comportamentos das consultas FOR UPDATE.

Primeiro, só para esclarecer, defina o nível de isolamento da transação como REPEATABLE READ. Isso normalmente é desnecessário, pois é o nível de isolamento padrão para o InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

Na outra sessão, atualize esta linha. Linda se casou e mudou seu nome:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

De volta à sessão1, por estarmos em REPEATABLE READ, Linda ainda é LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

Mas agora, queremos acesso exclusivo a essa linha, por isso chamamos FOR UPDATE na linha. Observe que agora recuperamos a versão mais recente da linha, que foi atualizada na sessão2 fora desta transação. Isso não é REPETÍVEL, LEIA COMPROMISSO

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

Vamos testar o bloqueio definido na sessão1. Observe que a sessão2 não pode atualizar a linha.

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Mas ainda podemos selecionar

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

E ainda podemos atualizar uma tabela filho com um relacionamento de chave estrangeira

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

Outro efeito colateral é que você aumenta sua probabilidade de causar um impasse.

No seu caso específico, você provavelmente deseja:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

Se a parte "faça outras coisas" for desnecessária e você realmente não precisar manter as informações sobre a linha, o SELECT FOR UPDATE é desnecessário e desperdício, e você poderá executar uma atualização:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

Espero que isso faça algum sentido.

Aaron Brown
fonte
3
Obrigado. Parece não resolver o meu problema, quando dois threads estão chegando com "SELECT id FROM itemsWHERE status= 'pendente' LIMIT 1 FOR UPDATE;" e ambos vêem a mesma linha, um trava o outro. Eu estava esperando que de alguma forma ele seria capaz de by-pass a linha bloqueada e vá para o próximo item que estava pendente ..
Wizzard
1
A natureza dos bancos de dados é que eles retornam dados consistentes. Se você executar essa consulta duas vezes antes de o valor ser atualizado, você receberá o mesmo resultado. Não existe uma extensão SQL "obtenha-me o primeiro valor que corresponda a esta consulta, a menos que a linha esteja bloqueada". Parece suspeito que você esteja implementando uma fila no topo de um banco de dados relacional. É esse o caso?
Aaron Brown
Aaron; Sim, é isso que estou tentando fazer. Eu olhei para usar algo como artesão - mas isso foi um fracasso. Você tem algo mais em mente?
Wizzard
Acho que você deveria ler o seguinte: engineyard.com/blog/2011/… - para filas de mensagens, existem muitas por aí, dependendo do idioma de sua escolha. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ, etc.
Aaron Brown
Como faço para que a sessão 2 bloqueie a leitura até que a atualização na sessão 1 seja confirmada?
CMCDragonkai
2

Se você estiver usando o mecanismo de armazenamento InnoDB, ele usará o bloqueio no nível da linha. Em conjunto com a versão múltipla, isso resulta em boa simultaneidade de consulta porque uma determinada tabela pode ser lida e modificada por diferentes clientes ao mesmo tempo. As propriedades de simultaneidade no nível da linha são as seguintes:

Clientes diferentes podem ler as mesmas linhas simultaneamente.

Clientes diferentes podem modificar linhas diferentes simultaneamente.

Clientes diferentes não podem modificar a mesma linha ao mesmo tempo. Se uma transação modificar uma linha, outras transações não poderão modificar a mesma linha até que a primeira transação seja concluída. Outras transações também não podem ler a linha modificada, a menos que estejam usando o nível de isolamento READ UNCOMMITTED. Ou seja, eles verão a linha original não modificada.

Basicamente, você não precisa especificar o bloqueio explícito O InnoDB lida com ele por si só, embora em algumas situações seja necessário fornecer detalhes explícitos sobre o bloqueio explícito abaixo:

A lista a seguir descreve os tipos de bloqueio disponíveis e seus efeitos:

LER

Trava uma mesa para leitura. Um bloqueio de leitura bloqueia uma tabela para consultas de leitura, como SELECT, que recuperam dados da tabela. Ele não permite operações de gravação como INSERT, DELETE ou UPDATE que modificam a tabela, mesmo pelo cliente que mantém o bloqueio. Quando uma tabela está bloqueada para leitura, outros clientes podem ler da tabela ao mesmo tempo, mas nenhum cliente pode gravar nela. Um cliente que deseja gravar em uma tabela com bloqueio de leitura deve aguardar até que todos os clientes que estão lendo atualmente tenham finalizado e liberado seus bloqueios.

ESCREVER

Trava uma mesa para escrever. Um bloqueio WRITE é um bloqueio exclusivo. Só pode ser adquirido quando uma tabela não está sendo usada. Uma vez adquirido, apenas o cliente que mantém o bloqueio de gravação pode ler ou gravar na tabela. Outros clientes não podem ler nem escrever nele. Nenhum outro cliente pode bloquear a tabela para leitura ou gravação.

LER LOCAL

Bloqueia uma tabela para leitura, mas permite inserções simultâneas. Uma inserção simultânea é uma exceção ao princípio "leitores bloqueadores de gravadores". Aplica-se apenas às tabelas MyISAM. Se uma tabela MyISAM não possui furos no meio resultantes de registros excluídos ou atualizados, as inserções sempre ocorrem no final da tabela. Nesse caso, um cliente que está lendo uma tabela pode bloqueá-lo com um bloqueio READ LOCAL para permitir que outros clientes sejam inseridos na tabela enquanto o cliente que está segurando o bloqueio de leitura lê nela. Se uma tabela MyISAM tiver furos, você poderá removê-los usando OPTIMIZE TABLE para desfragmentar a tabela.

Mahesh Patil
fonte
obrigado pela resposta. Como tenho esta tabela e 100 clientes verificando itens pendentes, eu estava tendo muitas colisões - 2-3 clientes obtendo a mesma linha pendente. O bloqueio da tabela está lento.
Wizzard
0

Outra alternativa seria adicionar uma coluna que armazenasse o tempo do último bloqueio bem-sucedido e, em seguida, qualquer outra coisa que desejasse bloquear a linha precisaria esperar até que ela fosse limpa ou que 5 minutos (ou o que fosse) tivesse decorrido.

Algo como...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock é um int, pois armazena o timestamp unix, pois é mais fácil (e talvez mais rápido) comparar.

// Desculpe a semântica, eu não verifiquei se eles rodam agudamente, mas devem estar próximos o suficiente, se não o fizerem.

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

Em seguida, verifique quantas linhas foram atualizadas, porque as linhas não podem ser atualizadas por dois processos ao mesmo tempo; se você atualizou a linha, obteve o bloqueio. Supondo que você esteja usando PHP, você usaria mysql_affected_rows (), se o retorno for 1, você o bloqueou com sucesso.

Em seguida, você pode atualizar o lastlock para 0 depois de fazer o que precisa ou ficar preguiçoso e aguardar 5 minutos quando a próxima tentativa de bloqueio tiver êxito.

EDIT: você pode precisar de um pouco de trabalho para verificar se funciona conforme o esperado durante as alterações no horário de verão, pois os relógios retornariam uma hora, talvez anulando o cheque. Você precisaria garantir que os carimbos de data e hora unix estivessem no UTC - que podem ser de qualquer maneira.

Steve Childs
fonte
-1

Como alternativa, você pode fragmentar os campos de registro para permitir a gravação paralela e ignorar o bloqueio de linhas (estilo de pares de json fragmentados). Portanto, se um campo de um registro de leitura composto fosse um número inteiro / real, você poderia ter o fragmento 1-8 desse campo (8 registros / linhas de gravação em vigor). Em seguida, some os fragmentos round-robin após cada gravação em uma pesquisa de leitura separada. Isso permite até 8 usuários simultâneos em paralelo.

Como você está trabalhando apenas com cada fragmento criando um total parcial, não há colisão e atualizações paralelas verdadeiras (ou seja, você escreve bloqueia cada fragmento em vez de todo o registro de leitura unificado). Obviamente, isso funciona apenas em campos numéricos. Algo que depende de modificação matemática para armazenar um resultado.

Assim, vários fragmentos de gravação por campo de leitura unificado por registro de leitura unificado. Esses fragmentos numéricos também se prestam a ECC, criptografia e transferência / armazenamento em nível de bloco. Quanto mais fragmentos de gravação houver, maiores serão as velocidades de acesso de gravação paralela / simultânea em dados saturados.

O MMORPG sofre bastante com esse problema, quando um grande número de jogadores começa a se bater com as habilidades da Área de Efeito. Esses vários jogadores precisam gravar / atualizar todos os outros jogadores exatamente ao mesmo tempo, em paralelo, criando uma tempestade de bloqueio de linha de gravação nos registros unificados de jogadores.

Mick Saunders
fonte