Dit is een geweldige vraag. InnoDB is een vergrendelingsengine op rijniveau, maar het moet extra vergrendelingen instellen om de veiligheid met het binaire logboek te garanderen (gebruikt voor replicatie; herstel op tijd). Beschouw het volgende (naïeve) voorbeeld om het uit te leggen:
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;
Omdat instructies alleen naar het binaire logboek worden geschreven als ze eenmaal zijn vastgelegd, zou op de slave-sessie #2 eerst van toepassing zijn en een ander resultaat opleveren, leidend tot gegevenscorruptie .
Dus wat InnoDB doet, is extra vergrendelingen instellen. Als is_deleted
is geïndexeerd, voordat sessie1 commits niemand anders in staat zal zijn om aan te passen of in te voegen in het bereik van records waar is_deleted=1
. Als er geen indexen zijn op is_deleted
, dan moet InnoDB elke rij in de hele tabel vergrendelen om ervoor te zorgen dat de herhaling in dezelfde volgorde is. Je kunt dit zien als de kloof dichten , wat een ander concept is om direct te begrijpen van vergrendeling op rijniveau .
In jouw geval met die ORDER BY position ASC
, InnoDB moet ervoor zorgen dat er geen nieuwe rijen kunnen worden gewijzigd tussen de laagste sleutelwaarde en een "speciale" laagst mogelijke waarde. Als je zoiets deed als ORDER BY position DESC
.. nou, dan zou niemand in dit bereik kunnen invoegen.
Dus hier komt de oplossing:
-
Op verklaringen gebaseerde binaire logging is waardeloos. Ik kijk echt uit naar een toekomst waarin we allemaal overschakelen naar rij gebaseerde binaire logging (beschikbaar vanaf MySQL 5.1, maar niet standaard ingeschakeld).
-
Als u bij replicatie op basis van rijen het isolatieniveau wijzigt in read-committed, hoeft alleen de rij die overeenkomt te worden vergrendeld.
-
Als je een masochist wilt zijn, kun je ook innodb_locks_unsafe_for_binlog met op instructies gebaseerde replicatie.
Update 22 april :Om mijn verbeterde versie van je testcase te kopiëren en plakken (hij zocht niet 'in the gap'):
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