Mysql
 sql >> Database >  >> RDS >> Mysql

Quante righe saranno bloccate da SELECT ... ORDER BY xxx LIMIT 1 FOR UPDATE?

Questa è un'ottima domanda. InnoDB è un motore di blocco a livello di riga, ma deve impostare blocchi aggiuntivi per garantire la sicurezza con il log binario (usato per la replica; ripristino temporizzato). Per iniziare a spiegarlo, considera il seguente (ingenuo) esempio:

session1> START TRANSACTION;
session1> DELETE FROM users WHERE is_deleted = 1; # 1 row matches (user_id 10), deleted.
session2> START TRANSACTION;
session2> UPDATE users SET is_deleted = 1 WHERE user_id = 5; # 1 row matches.
session2> COMMIT;
session1> COMMIT;

Poiché le istruzioni vengono scritte nel log binario solo dopo il commit, nella sessione slave n. 2 si applicherebbe per prima e produrrebbe un risultato diverso, portando al danneggiamento dei dati .

Quindi ciò che fa InnoDB è impostare blocchi aggiuntivi. Se is_deleted è indicizzato, quindi prima del commit di session1 nessun altro sarà in grado di modificare o inserirlo nell'intervallo di record in cui is_deleted=1 . Se non ci sono indici su is_deleted , quindi InnoDB deve bloccare ogni riga dell'intera tabella per assicurarsi che la riproduzione sia nello stesso ordine. Puoi pensare a questo come a bloccare il divario , che è un concetto diverso da cogliere direttamente dal blocco a livello di riga .

Nel tuo caso con quel ORDER BY position ASC , InnoDB deve assicurarsi che nessuna nuova riga possa essere modificata tra il valore della chiave più basso e un valore "speciale" più basso possibile. Se hai fatto qualcosa come ORDER BY position DESC .. beh, allora nessuno potrebbe inserirsi in questo range.

Quindi ecco la soluzione:

  • La registrazione binaria basata su istruzioni fa schifo. Non vedo l'ora che arrivi un futuro in cui tutti passiamo alla riga registrazione binaria basata (disponibile da MySQL 5.1, ma non attivo per impostazione predefinita).

  • Con la replica basata su riga, se modifichi il livello di isolamento in read-committed, solo la riga corrispondente deve essere bloccata.

  • Se vuoi essere un masochista, puoi anche attivare innodb_locks_unsafe_for_binlog con replica basata su istruzioni.

Aggiornamento 22 aprile :Per copiare e incollare la mia versione migliorata del testcase (non cercava "nel vuoto"):

session1> CREATE TABLE test (id int not null primary key auto_increment, data1 int, data2 int, INDEX(data1)) engine=innodb;
Query OK, 0 rows affected (0.00 sec)

session1> INSERT INTO test VALUES (NULL, 1, 2), (NULL, 2, 1), (5, 2, 2), (6, 3, 3), (3, 3, 4), (4, 4, 3);
Query OK, 6 rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0

session1> start transaction;
Query OK, 0 rows affected (0.00 sec)

session1> SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

session2> INSERT INTO test values (NULL, 0, 99); # blocks - 0 is in the gap between the lowest value found (1) and the "special" lowest value.

# At the same time, from information_schema:

localhost information_schema> select * from innodb_locks\G
*************************** 1. row ***************************
    lock_id: 151A1C:1735:4:2
lock_trx_id: 151A1C
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
*************************** 2. row ***************************
    lock_id: 151A1A:1735:4:2
lock_trx_id: 151A1A
  lock_mode: X
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
2 rows in set (0.00 sec)

# Another example:
select * from test where id < 1 for update; # blocks