Explication détaillée du verrouillage MySQL et analyse de cas

Explication détaillée du verrouillage MySQL et analyse de cas

1. Une déclaration de mise à jour

Insérer la description de l'image ici

2. Introduction aux verrous MySQL

  • En termes de granularité du verrouillage, MySQL contient principalement trois types (niveaux) de mécanismes de verrouillage :

    全局锁: La base de données entière est verrouillée. Implémenté par la couche SQL de MySQL
    表级锁: une certaine table est verrouillée. Implémenté par la couche SQL de MySQL
    行级锁: il verrouille une certaine ligne de données, et peut également verrouiller les espaces entre les lignes. Implémenté par certains moteurs de stockage, tels que InnoDB.

  • Selon la fonction du verrou, il est divisé en : verrou partagé et verrou exclusif .

    Verrous partagés (verrous S) :
    1. Compatibilité : les enregistrements avec des verrous S sont autorisés à ajouter des verrous S à d'autres transactions, et les verrous X ne peuvent pas être ajoutés à d'autres transactions. 2.
    Méthode de verrouillage : sélectionnez... verrouiller dans le partage mode

    Verrous exclusifs (verrous X) :
    1. Compatibilité : les enregistrements avec des verrous X sont ajoutés et les autres transactions ne sont pas autorisées à ajouter des verrous S ou X.
    2. Méthode de verrouillage : sélectionnez... pour la mise à jour

3. Verrouillage global

Le verrou global verrouille l'intégralité de l'instance de base de données. Une fois le verrou verrouillé, l'instance entière est en lecture seule. Les instructions d'écriture MDL, les instructions DDL et les instructions de validation de transaction suivantes qui ont été mises à jour seront bloquées. Son scénario d'utilisation typique consiste à effectuer une sauvegarde logique de l'intégralité de la base de données et à verrouiller toutes les tables pour obtenir une vue cohérente et garantir l'intégrité des données.

La commande pour ajouter un verrou global est :

flush tables with read lock

La commande pour libérer le verrou global est :

unlock tables

Deuxième méthode de déverrouillage

Afficher les processus verrouillés

Si la base de données MySQL est verrouillée, vous pouvez utiliser la commande suivante pour afficher le processus verrouillé :
SHOW FULL PROCESSLIST ;
cette commande affichera toutes les connexions actuelles à la base de données et leur état, y compris l'instruction SQL en cours d'exécution, l'heure d'exécution et d'autres informations. En affichant ces informations, vous pouvez trouver les processus verrouillés et leurs informations associées.

Tuer le processus verrouillé

Après avoir trouvé le processus verrouillé, vous pouvez utiliser la commande suivante pour arrêter le processus :
KILL <ID de processus> ;
où l'ID de processus est l'ID de processus affiché dans la commande SHOW FULL PROCESSLIST. En supprimant le processus verrouillé, la base de données MySQL peut être déverrouillée.

Démonstration de verrouillage global

1. Préparation de l'environnement

CREATE TABLE mylock ( 
id int(11) NOT NULL AUTO_INCREMENT, 
NAME varchar(20) DEFAULT NULL, 
PRIMARY KEY (id) 
); 
INSERT INTO mylock (id,NAME) VALUES (1, 'a');
 INSERT INTO mylock (id,NAME) VALUES (2, 'b'); 
 INSERT INTO mylock (id,NAME) VALUES (3, 'c'); 
 INSERT INTO mylock (id,NAME) VALUES (4, 'd'); 

2. Démonstration de verrouillage global

Répondre 1 Répondre 2
1. Verrouillez la base de donnéesInsérer la description de l'image ici 2. Créez une nouvelle session pour effectuer des opérations de requêteInsérer la description de l'image ici
aucun 3. Exécutez l'instruction de suppressioneffectuer la suppression
4. Libérez le verrouInsérer la description de l'image ici 5. Exécutez à nouveau l'instruction de suppressionInsérer la description de l'image ici

4. Verrous MySQL au niveau de la table (tous implémentés par la couche serveur)

1. Introduction aux verrous au niveau de la table

Il existe quatre types de verrous au niveau des tables dans MySQL :

1. Verrous de lecture et d'écriture de table.
2. Verrouillage des métadonnées (verrouillage des métadonnées, MDL).
3. Verrous d'intention (InnoDB)
4. Verrous AUTO-INC

2. Table en lecture S, verrouillage en écriture X

1) Commandes liées au verrouillage de table

  • Variables d'état de conflit pour le verrouillage au niveau de la table implémenté dans MySQL :
show status like 'table%'; 

Insérer la description de l'image ici

  • table_locks_immediate : le nombre de verrous au niveau de la table générés ;
  • table_locks_waited : nombre d'attentes dues à un conflit de verrouillage au niveau de la table
  • Les verrous de table ont deux formes

    Verrouillage en lecture partagé de table (Table Read Lock)
    Verrouillage en écriture exclusif de table (Table Write Lock)

  • Ajouter manuellement un verrou de table

    table de verrouillage nom de la table lecture (écriture), nom de la table 2 lecture (écriture), autres ;
    1. table de verrouillage partagée au niveau de
    la table nom de la table lecture ;
    les opérations de lecture peuvent être partagées, mais aucune opération ne peut être effectuée.
    2.
    Écriture du nom de la table de verrouillage exclusif au niveau de la table ;

  • Vérifier l'état de verrouillage de la table

    afficher les tables ouvertes ;

  • Supprimer le verrouillage de la table :

    débloquer des tables ;

2) Démonstration de verrouillage de table

1. Verrou partagé au niveau de la table (verrouillage en lecture)

Peut être partagé en lecture, mais ne peut pas effectuer d'opérations d'écriture
Une fois qu'un verrou partagé est ajouté à cette table, d'autres sessions peuvent également ajouter des verrous partagés à cette table ; des verrous partagés peuvent coexister, vous pouvez ajouter un verrou et je peux également ajouter un verrou. Puisqu'il s'agit d'un verrou de lecture partagé, une fois le verrou verrouillé, la table peut être lue. La transaction en cours ne peut pas effectuer d'opérations d'écriture et les autres transactions ne peuvent pas effectuer d'opérations d'écriture. Tous les verrous de lecture doivent être libérés.

lock table 表名 read;
Répondre 1 Répondre 2
1. Ajouter un verrou partagéInsérer la description de l'image ici aucun
2. Interroger les donnéesInsérer la description de l'image ici aucun
3. Nouvelles données : indique qu'il existe un verrou en lecture et ne peut pas être mise à jourInsérer la description de l'image ici 4. Créez une nouvelle conversation et ajoutez un verrouInsérer la description de l'image ici
aucun 5. Interroger les donnéesInsérer la description de l'image ici
aucun 6. Nouvelles données : indique qu'il existe un verrou en lecture et ne peut pas être mise à jourInsérer la description de l'image ici
aucun 7. Libérez le verrou de lectureInsérer la description de l'image ici
aucun 8. Exécutez à nouveau les nouvelles données, et elles sont toujours en attente car il y a un verrou en lecture dans la première session.Insérer la description de l'image ici
9. Relâchez le verrou dès que vous répondezInsérer la description de l'image ici 10. Nouvelles données ajoutées dans la réponse 2 : succèsInsérer la description de l'image ici
2. Verrouillage exclusif au niveau de la table

Un verrou exclusif est ajouté à la table dans une session/transaction, et la session en cours peut lire et écrire des opérations sur la table ; les autres sessions seront bloquées lors de la lecture et de l'exécution d'opérations sur la table et devront attendre que le verrou soit libéré dans la transaction verrouillée.

lock table 表名 write;

Le tableau est dans un état exclusif.

Démonstration de verrouillage en écriture

Répondre 1 Répondre 2
1. Ajoutez un verrou en écriture à la table mylock dans la réponse 1Insérer la description de l'image ici aucun
2. La session en cours peut effectuer des requêtes + mises à jour + nouvelles opérations sur la table verrouillée.Insérer la description de l'image ici
3. La deuxième session est bloquée lors de l'interrogation (modification/ajout) et doit attendre que le verrou soit libéré.Insérer la description de l'image ici
4. Libérez le verrouInsérer la description de l'image ici 5. La réponse 2 a obtenu la requête de verrouillage avec succèsInsérer la description de l'image ici

3. Verrouillage des métadonnées

1) Introduction au verrouillage des métadonnées

Lorsqu'une table est interrogée dans une transaction et que les autres sessions ne sont pas autorisées à modifier la structure de la table, un verrou de métadonnées est ajouté à la table.
MDL n'a pas besoin d'être utilisé explicitement, il sera ajouté automatiquement lors de l'accès à une table. La fonction du MDL est de garantir l’exactitude de la lecture et de l’écriture. Vous pouvez imaginer que si une requête parcourt les données d'une table et que pendant l'exécution, un autre thread modifie la structure de la table et supprime une colonne, alors les résultats obtenus par le thread de requête ne correspondent pas à la structure de la table, ce qui n'est certainement pas le cas. possible. .
Par conséquent, MDL a été introduit dans MySQL version 5.5. Lors de l'ajout, de la suppression, de la modification et de l'interrogation d'une table, ajoutez des verrous de lecture MDL ; lorsque vous apportez des modifications structurelles à la table, ajoutez des verrous d'écriture MDL .
Les verrous de lecture ne s'excluent pas mutuellement, vous pouvez donc avoir plusieurs threads qui ajoutent, suppriment, modifient et interrogent une table en même temps.
Les verrous en lecture-écriture et les verrous en écriture s'excluent mutuellement pour garantir la sécurité des modifications de la structure des tables.. Par conséquent, si deux threads souhaitent ajouter des champs à une table en même temps, l'un d'eux devra attendre que l'autre ait terminé avant de pouvoir commencer l'exécution .

2) Démonstration du verrouillage des métadonnées

Insérer la description de l'image ici

1、session1: begin;--开启事务 
select * from mylock;--加MDL读锁 
2、session2: alter table mylock add f int; -- 修改阻塞 
3、session1:commit; --提交事务    或者    rollback 释放读锁 
4、session2:Query OK, 0 rows affected (38.67 sec) --修改完成 
Records: 0  Duplicates: 0  Warnings: 0
Répondre 1 Répondre 2
1. session1 : début ;– démarrer la transactionInsérer la description de l'image ici aucun
1.1 sélectionnez * dans mylock ; – ajoutez un verrou de lecture MDLInsérer la description de l'image ici 2. session2 : modifier la table mylock ajouter l'âge int ; – Modifier le blocageInsérer la description de l'image ici
3. session1 : commit ; – valide la transaction ou annule pour libérer le verrou de lectureInsérer la description de l'image ici 4. Exécution réussieInsérer la description de l'image ici

4. Verrous AUTO-INC

Le verrou AUTO-INC est un verrou spécial au niveau de la table qui se produit lorsqu'une opération d'insertion transactionnelle impliquant la colonne AUTO_INCREMENT se produit.

5. Verrous MySQL au niveau des lignes (granularité fine)

Il est nécessaire d'utiliser le moteur InnoDB

1. Introduction aux verrous au niveau des lignes

Les verrous au niveau des lignes de MySQL sont implémentés par le moteur de stockage.Ici , nous expliquons principalement les verrous au niveau des lignes d'InnoDB . Les verrous de ligne InnoDB sont implémentés
en verrouillant les entrées d'index sur l'index.Par conséquent, les caractéristiques de l'implémentation du verrouillage de ligne InnoDB signifient :InnoDB utilise uniquement des verrous au niveau des lignes pour les données récupérées via des conditions d'index, sinon InnoDB utilisera des verrous de table !

  • Les verrous au niveau des lignes d'InnoDB sont divisés en quatre types selon la portée du verrouillage :

    记录锁(Record Locks):Verrouiller un enregistrement dans l'index.
    间隙锁(Gap Locks):Soit verrouillez la valeur au milieu de l'enregistrement d'index, soit verrouillez la valeur avant le premier enregistrement d'index ou la valeur après le dernier enregistrement d'index.
    临键锁(Next-Key Locks):Il s'agit d'une combinaison d'un verrouillage d'enregistrement sur l'enregistrement d'index et d'un verrouillage d'espace avant l'enregistrement d'index (verrouillage d'espace + verrouillage d'enregistrement).
    插入意向锁(Insert Intention Locks):Le verrou sur l'identifiant de l'enregistrement ajouté lors de l'opération d'insertion.

  • Les verrous au niveau des lignes d'InnoDB sont divisés en deux types selon leurs fonctions :

    共享锁(S):Permet à une transaction de lire une ligne, empêchant ainsi d'autres transactions d'obtenir un verrou exclusif sur le même ensemble de données.
    排他锁(X):Autorisez les transactions qui obtiennent des verrous exclusifs à mettre à jour les données et empêchez d'autres transactions d'obtenir des verrous de lecture partagés et des verrous d'écriture exclusifs sur le même ensemble de données.

Pour UPDATEles instructions DELETEet , un verrou exclusif (X) sera automatiquement ajouté à l'ensemble de données concerné ; pour l' instruction ordinaire, aucun verrou ne sera ajouté. La transaction peut ajouter des verrous partagés ou des verrous exclusifs à l'ensemble d'enregistrements via les instructions suivantes .INSERTInnoDBSELECTInnoDB

Ajoutez manuellement des verrous partagés :

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE 

Ajoutez manuellement un verrou exclusif (x) :

SELECT * FROM table_name WHERE ... FOR UPDATE 

2. Verrous d'enregistrement

Utilisez des verrous d'enregistrement lors de la mise à jour en fonction des valeurs de clé primaire.

(1) Verrouillage d'enregistrement, verrouille uniquement une ligne de l'enregistrement d'index et verrouille un seul enregistrement d'index.
(2) Le verrouillage de l'enregistrement verrouille toujours l'index, pas l'enregistrement lui-même. Même s'il n'y a pas d'index sur la table, innodb créera un index de clé primaire clusterisé caché en arrière-plan, et c'est cet index de clé primaire clusterisé caché qui est verrouillé .

Par conséquent, lorsqu'une instruction SQL n'utilise aucun index, un verrou X sera ajouté après chaque index agrégé. Ceci est similaire à un verrou de table, mais en principe, il devrait être complètement différent d'un verrou de table.

-- 共享记录锁
select * from table where id = 1 lock in share mode; 
-- 排他记录锁
select * from table  where id = 1 for udpate;
Démo de verrouillage d'enregistrement partagé

Si vous ajoutez un verrou d'enregistrement partagé à un certain enregistrement, d'autres sessions peuvent également ajouter un verrou d'enregistrement partagé à cet enregistrement, et vous pouvez également lire cet enregistrement ; mais vous ne pouvez pas écrire et vous ne pouvez pas ajouter un verrou exclusif à cet enregistrement ( être dans un état bloqué).
Lors de la démonstration, nous avons activé la transaction afin de pouvoir facilement voir l'effet. Si la transaction n'est pas activée, le verrou sera automatiquement libéré après l'exécution de cette instruction et l'effet sera invisible.

Répondre 1


-- 1.开启事务
BEGIN;

-- 2.查询操作
SELECT * FROM `mylock`;
-- 3.给id=2的记录添加共享记录锁
select * from mylock where id = 2 lock in share mode; 
-- 4.本地回话可以进行写操作
UPDATE mylock SET name='李四' WHERE id=2
-- 4.1.查询结果,看见更新成功
SELECT * FROM `mylock`;

-- 11.再次查询结果,看见查询的数据和步骤5一致,
--而不是回话2中修改后的,这是正确的保证一个事务中数据不可重复读
SELECT * FROM `mylock`;
-- 12提交事务
COMMIT;
-- 13 再次查询,数据最新的
SELECT * FROM `mylock`;

Répondre 2


-- 回话2
-- 5.查询id=2数据成功
select * from mylock where id = 2 
 -- 6.加记录共享锁成功
select * from mylock where id = 2 lock in share mode;
-- 7.加记录排他锁阻塞
select * from mylock where id = 2 for update; 
-- 8.更新id=2的数据阻塞
UPDATE mylock set name='李武' WHERE id =2; 
-- 9.更新其他记录成功
UPDATE mylock set name='李白' WHERE id =3; 
-- 10.查询记录,由于回话一中并没有提交id=2的修改记录,查询到的仍是旧值
SELECT * FROM mylock; 
Répondre 1 Répondre 2
COMMENCER; – 1. Démarrer la transaction
SELECT * FROM mylock; – 2. Opération de requêteInsérer la description de l'image ici
sélectionnez * dans mylock où id = 2 verrouillez en mode partage ; – 3. Ajoutez un verrou d'enregistrement partagé à l'enregistrement avec id = 2Insérer la description de l'image ici
UPDATE mylock SET name='李思' WHERE id=2 – 4. Les appels locaux peuvent effectuer des opérations d'écritureInsérer la description de l'image ici
SELECT * FROM mylock;-- 4.1. Interrogez les résultats et vérifiez que la mise à jour a réussiInsérer la description de l'image ici sélectionnez * dans mylock où id = 2 – 5. Requête id = 2 données avec succèsInsérer la description de l'image ici
aucun sélectionnez * dans mylock où id = 2 verrouiller en mode partage ; – 6. Ajouter le verrou partagé d'enregistrement avec succèsInsérer la description de l'image ici
aucun sélectionnez * depuis mylock où id = 2 pour la mise à jour ; – 7. Échec de l'ajout du verrouillage exclusif d'enregistrementInsérer la description de l'image ici
aucun UPDATE mylock set name='Li Wu' WHERE id =2 ; – 8. Échec de la mise à jour des données avec l'id=2Insérer la description de l'image ici
aucun UPDATE mylock set name=‘李白’ WHERE id =3; – 9.更新其他记录成功Insérer la description de l'image ici
SELECT * FROM mylock; – 10.查询记录,由于回话一中并没有提交id=2的修改记录,查询到的仍是旧值Insérer la description de l'image ici
SELECT * FROM mylock; – 11.再次查询结果,看见查询的数据和步骤4.1一致,而不是回话2中修改后的,这是正确的保证一个事务中数据不可重复读Insérer la description de l'image ici
COMMIT;-- 12提交事务 SELECT * FROM mylock; – 13 再次查询发现结果已是最新Insérer la description de l'image ici
排他记录锁演示

对某一条记录添加了排它锁,同一回话中可以写操作。在其他回话中可以读这个条记录,但是不能进行写,不能对这条记录加排它锁或者是共享锁(会处于阻塞状态)

回话一

-- 1.开启事务
BEGIN;
-- 2.查询操作
SELECT * FROM `mylock`;
-- 3.给id=2的记录添加排它锁
select * from mylock where id = 2 for UPDATE; 
-- 4.id=2的记录更新操作成功
UPDATE mylock SET name='张丹' WHERE id=2;

-- 9 提交事务,当前排它锁释放
COMMIT;

回话二

-- 5.加共享锁处于阻塞
select * from mylock where id = 2  lock in share mode;
-- 6.加排它锁锁处于阻塞
select * from mylock where id = 2 for UPDATE; 
-- 7.更新id=2记录处于阻塞
UPDATE mylock SET name='白冰' WHERE id=2;
-- 8.查询id=2的记录成功
SELECT * FROM `mylock` WHERE id=2;
回话1 回话2
– 1.开启事务 BEGIN;-- 2.查询操作 SELECT * FROM mylock;Insérer la description de l'image ici
– 3.给id=2的记录添加排它锁 select * from mylock where id = 2 for UPDATE; Insérer la description de l'image ici
– 4.id=2的记录更新操作成功 UPDATE mylock SET name=‘张丹’ WHERE id=2;Insérer la description de l'image ici
– 5.加共享锁处于阻塞select * from mylock where id = 2 lock in share mode;Insérer la description de l'image ici
– 6.加排它锁锁处于阻塞select * from mylock where id = 2 for UPDATE; Insérer la description de l'image ici
– 7.更新id=2记录处于阻塞UPDATE mylock SET name=‘白冰’ WHERE id=2;Insérer la description de l'image ici
– 8.查询id=2的记录成功SELECT * FROM mylock WHERE id=2;Insérer la description de l'image ici
– 9 提交事务,当前排它锁释放 COMMIT;

3、间隙锁(Gap Locks)

在记录和记录之间的范围区间就是间隙,间隙锁就是加区间之上的,防止插入数据。目的就是防止幻读。

(1)区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
(2)在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录 本身。
(3)间隙锁可用于防止幻读,保证索引间的不会被插入数据

Insérer la description de l'image ici

session1: 
begin; 
select * from t1_simple where id > 4 for update; 
--------------------------------------------------------- session2: 
insert into t1_simple values (7,100); --阻塞 
insert into t1_simple values (3,100); --成功

4、临键锁(Next-Key Locks)

就是记录锁+间隙锁
是一个左开右闭的区间。
(1,3],(3,5]…
在InnoDB中默认使用临键锁,当满足某些条件时,临键锁会退化。
(1)record lock + gap lock, 左开右闭区间,例如(5,8]。
(2)默认情况下,innodb使用next-key locks来锁定记录。select … for update
(3)但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
(4)Next-Key Lock在不同的场景中会退化:
Insérer la description de l'image ici

案例演示
CREATE TABLE `t1_simple`  ( 
`id` int(11) NOT NULL, 
`pubtime` int(11) NULL DEFAULT NULL,
 PRIMARY KEY (`id`) USING BTREE, 
INDEX `idx_pu`(`pubtime`) USING BTREE ) ENGINE = InnoDB; 
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (1, 10);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (3, 3);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (5, 100);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (10, 1);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (20, 5);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (100, 20);

Insérer la description de l'image ici

(1) 排他记录锁:使用主键索引精确匹配(=),且记录存在。
session1执行:

begin; 
-- 临键锁区间(10,20],(20,100]
select * from t1_simple where id = 20 for update; 

session2执行:

-- 16和25在临键锁区间(10,20],(20,100]内,因为有临建锁会阻塞
insert into t1_simple values (16, 19); --阻塞 
insert into t1_simple values (25, 50); --阻塞 

-- 退化成排它锁,在其他回话中不能对这条记录加排它锁
select * from t1_simple where id = 20 for update; --阻塞
insert into t1_simple values (4, 101); --成功 

(2)间隙锁: 使用主键索引精确匹配(=),且记录不存在。
Insérer la description de l'image ici
session1执行:

BEGIN;
-- 6在(5,10)之间,锁为间隙锁
SELECT * FROM t1_simple WHERE id =6 FOR UPDATE;

session2执行:


--因为回话一给(5,10)添加了间隙锁。回话二中插入id=6数据时会阻塞
INSERT INTO `t1_simple` VALUES (6, 33);

--成功
INSERT INTO `t1_simple` VALUES (28, 32);

(3)临键锁: 使用主键索引精确匹配(< 和>)
加了临建锁,在范围值内,插入数据会阻塞。
session1执行:

BEGIN;
-- 添加临键锁
SELECT * FROM `t1_simple` WHERE id<=5 FOR UPDATE;

Insérer la description de l'image ici
session2执行:

INSERT INTO `t1_simple` VALUES (2, 33); -- 阻塞

INSERT INTO `t1_simple` VALUES (5, 92); -- 阻塞

INSERT INTO `t1_simple` VALUES (18, 72); -- 成功

5、意向锁 (Intention Locks)(表级锁)

意向锁IX、IS,当数据库中某条记录上有行锁时,需要在表上增加一个标志位,代表当前表中某行记录上有行锁。
通俗的来说:

假如某行记录上有共享锁(S)那么表上标志位会变成意向共享锁(IS)
假如某行记录上有排它锁(X)那么表上标志位会变成意向排它锁(IX)
假如表中不同行记录上有共享锁(S)和排它锁(X)那么表上标志位会变成意向共享锁(IS)和意向排它锁(IX)

总结:

标记当前表中是否有行锁的存在。
行锁:S 意向锁:IS
行锁:X 意向锁:IX

1)意向锁介绍

InnoDB也实现了表级锁,也就是意向锁,意向锁是mysql内部使用的,不需要用户干预。意向锁和行锁可以共存,意向锁的主要作用是为了【全表更新数据】时的性能提升否则在全表更新数据时,需要先检索该范是否某些记录上面有行锁

  1. 表明“某个事务正在某些行持有了锁、或该事务准备去持有锁”
  2. 意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
  3. 例子:事务A修改user表的记录r,会给记录r上一把行级的排他锁(X),同时会给user表上一把意向排他锁(IX),这时事务B要给user表上一个表级的排他锁就会被阻塞。意向锁通过这种方式实现了行锁和表锁共存且满足事务隔离性的要求。
  4. 1)意向共享锁(IS锁):事务在请求S锁前,要先获得IS锁
    2)意向排他锁(IX锁):事务在请求X锁前,要先获得IX锁

2)意向锁的作用

当我们需要给表加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);
(1)如果意向锁是行锁,则需要遍历每一行数据去确认;
(2)如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。

3)意向锁和共享锁、排他锁的兼容关系

下图表示意向锁和表共享锁、表排他锁的兼容关系

Insérer la description de l'image ici
意向锁相互兼容,因为IX、IS只是表明申请更低层次级别元素(比如 page、记录)的X、S操作。
因为上了表级S锁后,不允许其他事务再加X锁,所以表级S锁和X、IX锁不兼容。
上了表级X锁后,会修改数据,所以表级X锁和 IS、IX、S、X(即使是行排他锁,因为表级锁定的行肯定包括行级锁定的行,所以表级X和IX、行级X)不兼容。

注意: 上了行级X锁后,行级X锁不会因为有别的事务上了IX而堵塞,一个mysql是允许多个行级X锁同时存在的,只要他们不是针对相同的数据行。

6、行锁加锁规则

1)主键索引

  1. 等值查询
    (1)命中记录,加记录锁。
    (2)未命中记录,加间隙锁。
  2. 范围查询
    (1)没有命中任何一条记录时,加间隙锁。
    (2)命中1条或者多条,包含where条件的临键区间,加临键锁

2)辅助索引

  1. 等值查询
    (1)命中记录,命中记录的辅助索引项+主键索引项加记录锁,辅助索引项两侧加间隙锁。
    (2)未命中记录,加间隙锁
  2. 范围查询
    (1)没有命中任何一条记录时,加间隙锁。
    (2)命中1条或者多条,包含where条件的临键区间加临键锁。命中记录的id索引项加记录锁。

7、插入意向锁(Insert Intention Locks)

在开启事务之后插入数据之前需要开启插入意向锁(把id锁上),防止其他的回话也插入相同的记录。
(1)插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
(2)在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
(3)假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之
间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
(4)插入意向锁不会阻止任何锁,对于插入的记录会持有一个记录锁。

8、锁相关参数

Innodb所使用的行级锁定争用状态查看:

show status like 'innodb_row_lock%';

Insérer la description de l'image ici

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  • Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
  • Innodb_row_lock_time_avg:每次等待所花平均时间;
  • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
  • Innodb_row_lock_waits:系统启动后到现在总共等待的次数;

对于这5个状态变量,比较重要的主要是:

  • Innodb_row_lock_time_avg(等待平均时长)
  • Innodb_row_lock_waits(等待总次数)
  • Innodb_row_lock_time(等待总时长)这三项。

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。

查看事务、锁的sql:

select * from information_schema.innodb_locks;
select * from information_schema.innodb_lock_waits;
select * from information_schema.innodb_trx;

六、行锁分析实战

在介绍完一些背景知识之后,接下来将选择几个有代表性的例子,来详细分析MySQL的加锁处理。当然,还是从最简单的例子说起。经常有朋友发给我一个SQL,然后问我,这个SQL加什么锁?就如同下面两条简单的SQL,他们加什么锁?

  • SQL1:
select * from t1 where id = 10;
  • SQL2:
delete from t1 where id = 10;

针对这个问题,该怎么回答?能想象到的一个答案是:

  • SQL1:不加锁。因为MySQL是使用多版本并发控制的,读不加锁。
  • SQL2:对id = 10的记录加写锁 (走主键索引)。

这个答案对吗?说不上来。即可能是正确的,也有可能是错误的,已知条件不足,这个问题没有答案。
必须还要知道以下的一些前提,前提不同,能给出的答案也就不同。要回答这个问题,还缺少哪些前提条件?

前提一: id列是不是主键?
前提二:当前系统的隔离级别是什么?
前提三:id列如果不是主键,那么id列上有索引吗?
前提四:id列上如果有二级索引,那么这个索引是唯一索引吗?
前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?

没有这些前提,直接就给定一条SQL,然后问这个SQL会加什么锁,都是很业余的表现。而当这些问题有了明确的答案之后,给定的SQL会加什么锁,也就一目了然。下面,我们将这些问题的答案进行组合,然后按照从易到难的顺序,逐个分析每种组合下,对应的SQL会加哪些锁?

注:下面的这些组合,需要做一个前提假设,也就是有索引时,执行计划一定会选择使用索引进行过滤 (索引扫描)。但实际情况会复杂很多,真正的执行计划,还是需要根据MySQL输出的为准。

组合一:id列是主键,RC隔离级别
组合二:id列是二级唯一索引,RC隔离级别
组合三:id列是二级非唯一索引,RC隔离级别
组合四:id列上没有索引,RC隔离级别
组合五:id列是主键,RR隔离级别
组合六:id列是二级唯一索引,RR隔离级别
组合七:id列是二级非唯一索引,RR隔离级别
组合八:id列上没有索引,RR隔离级别
组合九:Serializable隔离级别

排列组合还没有列举完全,但是看起来,已经很多了。真的有必要这么复杂吗?事实上,要分析加锁,就是需要这么复杂。但是从另一个角度来说,只要你选定了一种组合,SQL需要加哪些锁,其实也就确定了。接下来,就让我们来逐个分析这9种组合下的SQL加锁策略。

注:在前面八种组合下,也就是RC,RR隔离级别下SQL1:select操作均不加锁,采用的是快照读,因此在下面的讨论中就忽略了,主要讨论SQL2:delete操作的加锁。

1)组合一:id主键+RC

分析

这个组合,是最简单,最容易分析的组合。id是主键Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只需要将主键上id = 10的记录加上X锁即可。如下图所示:

Insérer la description de l'image ici

结论

id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

2)组合二:id唯一索引+RC

分析

这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 需要加什么锁呢?见下图:
Insérer la description de l'image ici
Insérer la description de l'image ici

此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。由于id是 unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后:
首先 会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引);
然后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁

为什么聚簇索引上的记录也要加锁?
试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。

结论

若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的【name=’d’,id=10】的记录。

3)组合三:id非唯一索引+RC

分析

相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行
过滤where条件
,那么此时会持有哪些锁?同样见下图:
Insérer la description de l'image ici

根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,这些记录对
应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只有一个满足等值查询的
记录,而组合三会将所有满足查询条件的记录都加锁。

结论:

若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键
索引上的记录,也会被加锁。

4)组合四:id无索引+RC

分析

相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引where id = 10; 这个过滤条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。
对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?
这个答案也有很多: 有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。
那么实际情况呢? 请看下图:
Insérer la description de l'image ici
由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有 两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不 是加表锁,也不是在满足条件的记录上加行锁。

有人可能会问?为什么不是只在满足条件的记录上加锁呢?
这是由于MySQL的实现决定的。如果一个条 件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行 过滤。因此也就把所有的记录,都锁上了

注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用 unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满 足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

结论:

id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放 锁动作不会省略。同时,优化也违背了2PL的约束。

5)组合五:id主键+RR

上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在 Repeatable Read隔离级别下的加锁行为。
组合五,id列是主键列Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed] 一致。

6)组合六:id唯一索引+RR

与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed] 一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。

7)组合七:id非唯一索引+RR

分析

还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一致。那么RR隔离级别下,如 何防止幻读呢?问题的答案,就在组合七中揭晓。
组合七,Repeatable Read隔离级别id上有一个非唯一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这幅图:
Insérer la description de l'image ici
Insérer la description de l'image ici
此图,相对于组合三:[id列上非唯一锁,Read Committed] 看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间 的位置,GAP锁有何用?
其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。所谓幻读,就是同一个事务,连续做两次当 前读 (例如:select * from t1 where id = 10 for update;),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。

如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会 插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。

如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可以插入[10, aa];[10,b] 与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;而 [11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与 [11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。

Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经 被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁 上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后 续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。

有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是 RR隔离级别,却不需要加GAP锁呢?
首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次 当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了 GAP锁的使用。其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能 够省略?此问题留给大家思考。

结论:

Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

8)组合八:id无索引+RR

分析

组合八,Repeatable Read隔离级别下的最后一种情况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图所示:
Insérer la description de l'image ici
如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP 锁。试想,如果表上有1000万条记录呢?

在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新, 不能删除,不能插入,全表被锁死。
当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent readsemi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL 会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时 不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了 innodb_locks_unsafe_for_binlog 参数。

结论:

Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi- consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问 题,不建议使用。

9)组合九:Serializable

分析

针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2来说,Serializable隔离 级别与Repeatable Read隔离级别完全一致,因此不做介绍。

delete from t1 where id = 10

Serializable隔离级别,影响的是SQL1这条SQL

select * from t1 where id = 10 

RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说 快照读不复存在,MVCC并发控制降级为Lock-Based CC。

结论:

MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable 隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

七、一条复杂SQL的加锁分析

分析

写到这里,其实MySQL的加锁实现也已经介绍的八八九九。只要将本文上面的分析思路,大部分的 SQL,都能分析出其会加哪些锁。而这里,再来看一个稍微复杂点的SQL,用于说明MySQL加锁的另外 一个逻辑。SQL用例如下:
Insérer la description de l'image ici
如图中的SQL,会加什么锁?假定在Repeatable Read隔离级别下 (Read Committed隔离级别下的加锁 情况,留给学生们分析),同时,假设SQL走的是idx_t1_pu索引。
在详细分析这条SQL的加锁情况前,还需要有一个知识储备,那就是一个SQL中的where条件如何拆分? 在这里,我直接给出分析后的结果:

  • Index key: pubtime > 1 and puptime < 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。
  • Index Filter: userid = ‘hdc’ 。此条件,可以在idx_t1_pu索引上进行过滤,但不属于Index Key
  • Table Filter:comment is not NULL。此条件,在idx_t1_pu索引上无法过滤,只能在聚簇索引上过滤。

在分析出SQL where条件的构成之后,再来看看这条SQL的加锁情况 (RR隔离级别),如下图所示:
Insérer la description de l'image ici
从图中可以看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP锁; Index Filter锁给定的条件 **(userid = ‘hdc’)**何时过滤,视MySQL的版本而定,在MySQL 5.6版本之前,不 支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤,在5.6后支持了Index Condition Pushdown,则在index上过滤。若不支持ICP,不满足Index Filter的记录,也需要加上记录X 锁;若支持ICP,则不满足Index Filter的记录,无需加记录X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应的过滤条件,则在聚簇索引中读取后,在MySQL Server层 面过滤,因此聚簇索引上也需要X锁。最后,选取出了一条满足条件的记录[8,hdc,d,5,good],但是加锁的数量,要远远大于满足条件的记录数量。

结论:

在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。

  • Index Key确定的范围,需要加上GAP锁;
  • Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记 录,不加X锁,否则需要X锁;
  • Table Filter过滤条件,无论是否满足,都需要加X锁。

八、死锁原理与分析

Insérer la description de l'image ici

本文前面的部分,基本上已经涵盖了MySQL/InnoDB所有的加锁规则。深入理解MySQL如何加锁,有两个比较重要的作用:

  • 可以根据MySQL的加锁规则,写出不会发生死锁的SQL;
  • 可以根据MySQL的加锁规则,定位出线上产生死锁的原因;

下面,来看看两个死锁的例子 (一个是两个Session的两条SQL产生死锁;另一个是两个Session的一条 SQL,产生死锁):
Insérer la description de l'image ici
Insérer la description de l'image ici
上面的两个死锁用例。第一个非常好理解,也是最常见的死锁,每个事务执行两条SQL,分别持有了一 把锁,然后加另一把锁,产生死锁。

第二个用例,虽然每个Session都只有一条语句,仍旧会产生死锁。要分析这个死锁,首先必须用到本文 前面提到的MySQL加锁的规则。针对Session 1,从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条 件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后 [6,hdc,10]。而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引 上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反, 如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。

结论:

Le fait qu'un blocage se produise ou non ne dépend pas du nombre d'instructions SQL contenues dans la transaction. [La clé du blocage est] : l'[ordre de verrouillage] de deux (ou plus) sessions est incohérent. En utilisant la méthode mentionnée ci-dessus dans cet article, analysez les règles de verrouillage de chaque instruction SQL dans MySQL, analysez l'ordre de verrouillage de chaque instruction, puis vérifiez s'il existe des verrous dans l'ordre opposé entre plusieurs SQL simultanés, vous pouvez analyser diverses impasses potentielles Les situations peuvent être identifiées et les causes des blocages en ligne peuvent également être analysées.

Comment éviter une impasse ?

Par défaut, MySQL détectera activement les blocages et annulera une transaction avec un impact minimal. Attendez la fin de l'exécution d'une autre transaction, puis réexécutez la transaction.

Comment éviter une impasse

1. Faites attention à la logique du programme

La raison fondamentale est l'ordre de la logique du programme. La plus courante est la mise à jour croisée
Transaction 1 : Mettre à jour la table A -> Mettre à jour la table B
Transaction 2 : Mettre à jour la table B -> Mettre à jour la table A
La transaction obtient deux ressources

2. Gardez les transactions légères

Plus la transaction est légère, moins elle occupe de ressources de verrouillage, ce qui réduit le risque de blocage.

3. Améliorer la vitesse de course

Évitez d'utiliser des sous-requêtes, essayez d'utiliser des clés primaires, etc.

4. Soumettez les transactions le plus rapidement possible pour réduire le temps de maintien des verrous

Plus tôt la transaction est validée, plus tôt le verrou est libéré

Neuf excellents liens

1. Comment obtenir les verrous MySQL (verrous globaux, verrous au niveau des tables, verrous au niveau des lignes)
2. Base de données MySQL : mécanisme de verrouillage
3. Résumé des questions d'entretien courantes dans la base de données MySQL

Article précédent : Index, moteur de stockage (type d'index, structure de données d'index, moteur de stockage MyISAM et InnoDB, index de clé primaire, index auxiliaire, index combiné, index de couverture, poussée d'index sous le moteur de stockage innodb

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43811057/article/details/130328730
conseillé
Classement