Dans MySQL, la lecture fantôme signifie que lorsque l'utilisateur lit une certaine plage de lignes de données, une autre transaction insère une nouvelle ligne dans la plage. Lorsque l'utilisateur lit les lignes de données dans la plage, il constatera qu'il y a de nouvelles lignes. la gamme. La ligne "Phantom". La lecture dite fantôme signifie que l'ensemble de données interrogé via SELECT n'est pas un ensemble de données réel. L'utilisateur demande via l'instruction SELECT qu'un certain enregistrement n'existe pas, mais qu'il peut exister dans la table réelle.
L'environnement d'exploitation de ce tutoriel : système windows7, version mysql8, ordinateur Dell G3.
Regardons d'abord le niveau d'isolement de la transaction
Ensuite, avant de parler de la lecture fantôme, permettez-moi d'abord de parler de ma compréhension de la lecture fantôme :
La soi-disant lecture fantôme, le point clé est " Le mot "illusion" est très rêveur et mystérieux. On ne sait pas si c'est vrai ou non. C'est comme être recouvert d'une couche de brouillard. Vous ne pouvez pas vraiment voir l'autre personne, donner aux gens le sentiment d'illusion. La lecture dite fantôme signifie que l'ensemble de données que vous interrogez via SELECT n'est pas un ensemble de données réel. Un certain enregistrement que vous interrogez via l'instruction SELECT n'existe pas, mais il peut exister dans la table réelle.
C'est ainsi que je comprends la lecture fantôme et la lecture non répétable :
La lecture fantôme
consiste à savoir si elle existe ou non : si elle n'existait pas avant, mais maintenant elle existe, c'est une lecture fantôme li>幻读
说的是存不存在的问题:原来不存在的,现在存在了,则是幻读不可重复读
说的是变没变化的问题:原来是A,现在却变为了B,则为不可重复读幻读,目前我了解的有两种说法:
说法一:事务 A 根据条件查询得到了 N 条数据,但此时事务 B 删除或者增加了 M 条符合事务 A 查询条件的数据,这样当事务 A 再次进行查询的时候真实的数据集已经发生了变化,但是A却查询不出来这种变化,因此产生了幻读。
这一种说法强调幻读在于某一个范围内的数据行变多或者是变少了,侧重说明的是数据集不一样导致了产生了幻读。
说法二:幻读并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:A事务select 某记录是否存在,结果为不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。产生这样的原因是因为有另一个事务往表中插入了数据。
我个人更赞成第一种说法。
说法二这种情况也属于幻读,说法二归根到底还是数据集发生了改变,查询得到的数据集与真实的数据集不匹配。
对于说法二:当进行INSERT的时候,也需要隐式的读取,比如插入数据时需要读取有没有主键冲突,然后再决定是否能执行插入。如果这时发现已经有这个记录了,就没法插入。所以,SELECT 显示不存在,但是INSERT的时候发现已存在,说明符合条件的数据行发生了变化,也就是幻读的情况,而不可重复读指的是同一条记录的内容被修改了。
举例来说明:说法二说的是如下的情况:
有两个事务A和B,A事务先开启,然后A开始查询数据集中有没有id = 30的数据,查询的结果显示数据中没有id = 30的数据。紧接着又有一个事务B开启了,B事务往表中插入了一条id = 30的数据,然后提交了事务。然后A再开始往表中插入id = 30的数据,由于B事务已经插入了id = 30的数据,自然是不能插入,紧接着A又查询了一次,结果发现表中没有id = 30的数据呀,A事务就很纳闷了,怎么会插入不了数据呢。当A事务提交以后,再次查询,发现表中的确存在id = 30的数据。但是A事务还没提交的时候,却查不出来?
其实,这便是可重复读
的作用。
过程如下图所示:
上图中操作的t表的创建语句如下:
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) -- 创建索引 ) ENGINE=InnoDB; INSERT INTO t VALUES(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
MySQL使用的InnoDB引擎默认的隔离级别是可重复读
Lecture non répétable
concerne le problème du changement : c'était autrefois A, mais maintenant il est devenu B, alors c'est une lecture non répétable< /li>
lecture répétable
. 🎜🎜Le processus est le suivant : 🎜🎜🎜🎜L'instruction de création de la table t opérée dans la figure ci-dessus est la suivante : 🎜SELECT * FROM player WHERE ...
Repeatable Read
, qui signifie que dans la même transaction, si vous exécutez deux fois la même requête, les résultats devraient être les mêmes. Par conséquent, bien que la transaction B ait ajouté des données à la table avant la fin de la transaction A, afin de maintenir une lecture reproductible, les données nouvellement ajoutées ne peuvent pas être interrogées, quelle que soit la manière dont elles sont interrogées dans la transaction A. Mais pour le vrai tableau, les données du tableau ont effectivement augmenté. 🎜
A查询不到这个数据,不代表这个数据不存在
。查询得到了某条数据,不代表它真的存在。这样是是而非的查询,就像是幻觉一样,似真似假,故为幻读
。
产生幻读的原因归根到底是由于查询得到的结果与真实的结果不匹配。
幻读 VS 不可重复读
幻读重点在于数据是否存在
。原本不存在的数据却真实的存在了,这便是幻读。在同一个事务中,第一次读取到结果集和第二次读取到的结果集不同。(对比上面的例子,当B事务INSERT以后,A事务中再进行插入,此次插入相当于一次隐式查询)。引起幻读的原因在于另一个事务进行了INSERT
操作。不可重复读重点在于数据是否被改变了
。在一个事务中对同一条记录进行查询,第一次读取到的数据和第二次读取到的数据不一致,这便是可重复读。引起不可重复读的原因在于另一个事务进行了UPDATE
或者是DELETE
操作。简单来说:幻读是说数据的条数发生了变化,原本不存在的数据存在了。不可重复读是说数据的内容发生了变化,原本存在的数据的内容发生了改变
。
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在 当前读 下才会出现。
什么是快照读,什么是当前读?
快照读读取的是快照数据。不加锁的简单的 SELECT都属于快照读,比如这样:
SELECT * FROM player WHERE ...
当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT,或者对数据进行增删改都会进行当前读。这有点像是 Java 中的 volatile 关键字,被 volatile 修饰的变量,进行修改时,JVM 会强制将其写回内存,而不是放在 CPU 缓存中,进行读取时,JVM 会强制从内存读取,而不是放在 CPU 缓存中。这样就能保证其可见行,保证每次读取到的都是最新的值。如果没有用 volatile 关键字修饰,变量的值可能会被放在 CPU 缓存中,这就导致读取到的值可能是某次修改的值,不能保证是最新的值。
说多了,我们继续来看,如下的操作都会进行 当前读。
SELECT * FROM player LOCK IN SHARE MODE; SELECT * FROM player FOR UPDATE; INSERT INTO player values ... DELETE FROM player WHERE ... UPDATE player SET ...
说白了,快照读就是普通的读操作,而当前读包括了 加锁的读取 和 DML(DML只是对表内部的数据操作,不涉及表的定义,结构的修改。主要包括insert、update、deletet) 操作。
比如在可重复读的隔离条件下,我开启了两个事务,在另一个事务中进行了插入操作,当前事务如果使用当前读 是可以读到最新的数据的。
当隔离级别为可重复读的时候,事务只在第一次 SELECT 的时候会获取一次 Read View
,而后面所有的 SELECT 都会复用这个 Read View。也就是说:对于A事务而言,不管其他事务怎么修改数据,对于A事务而言,它能看到的数据永远都是第一次SELECT时看到的数据。这显然不合理,如果其它事务插入了数据,A事务却只能看到过去的数据,读取不了当前的数据。
既然都说到 Read View 了,就不得不说 MVCC (多版本并发控制) 机制了。MVCC 其实字面意思还比较好理解,为了防止数据产生冲突,我们可以使用时间戳之类的来进行标识,不同的时间戳对应着不同的版本。比如你现在有1000元,你借给了张三 500 元, 之后李四给了你 500 元,虽然你的钱的总额都是 1000元,但是其实已经和最开始的 1000元不一样了,为了判断中途是否有修改,我们就可以采用版本号来区分你的钱的变动。
如下,在数据库的数据表中,id,name,type 这三个字段是我自己建立的,但是除了这些字段,其实还有些隐藏字段是 MySQL 偷偷为我们添加的,我们通常是看不到这样的隐藏字段的。
我们重点关注这两个隐藏的字段:
db_trx_id : L'ID de transaction qui exploite cette ligne de données, qui est le dernier ID de transaction qui a inséré ou mis à jour les données. Chaque fois que nous démarrons une transaction, nous obtiendrons un identifiant de transaction (c'est-à-dire le numéro de version de la transaction) de la base de données. Cet identifiant de transaction augmente automatiquement grâce à la taille de l'ID, nous pouvons juger de la séquence temporelle de la transaction.
db_roll_ptr : pointeur de restauration, pointant vers les informations Undo Log de cet enregistrement. Qu'est-ce que l'annulation du journal ? On peut comprendre que lorsque nous devons modifier un certain enregistrement, MySQL craint que la modification puisse être révoquée à l'avenir et ramenée à l'état précédent, donc avant de modifier, enregistrez les données actuelles dans un fichier, puis modifiez-le. , Annuler le journal Cela peut être compris comme ce fichier d'archive. C'est comme lorsque nous jouons à un jeu. Après avoir atteint un certain niveau, nous sauvegardons d'abord un fichier, puis continuons à défier le niveau suivant, au lieu de revenir au point de sauvegarde précédent. repartir de zéro.
Dans le mécanisme MVCC (Multiple Version Concurrency Control), plusieurs transactions mettant à jour le même enregistrement de ligne généreront plusieurs instantanés historiques, et ces instantanés historiques sont enregistrés dans Undo Log. Comme le montre la figure ci-dessous, le pointeur de restauration enregistré dans la ligne actuelle pointe vers son état précédent, et le pointeur de restauration de son état précédent pointe vers l'état précédent de l'état précédent. De cette façon, nous pouvons théoriquement trouver n'importe quel état de la ligne de données en parcourant le pointeur de restauration.
Diagramme schématique d'Undo Log
Nous ne nous attendions pas à ce que ce que nous avons vu ne soit qu'une seule donnée, mais MySQL a stocké plusieurs versions de ces données en coulisses et a enregistré de nombreux fichiers pour ces données. Voici la question. Lorsque nous démarrons une transaction, nous voulons interroger une certaine donnée dans la transaction, mais chaque donnée correspond à plusieurs versions. À ce stade, quelle version de l'enregistrement de ligne devons-nous lire ?
À ce stade, nous devons utiliser le mécanisme Read View
, qui nous aide à résoudre le problème de visibilité des lignes. Read View enregistre une liste de toutes les transactions actives (pas encore validées) lorsque la transaction en cours est ouverte.
Il y a plusieurs attributs importants dans Read VIew :
Comme nous l'avons dit précédemment, il y a un champ caché dans chaque ligne d'enregistrementsdb_trx_id, qui représente l'ID de transaction qui exploite cette ligne de données, et l'ID de transaction est auto-augmentant Grâce à la taille de l'ID, nous pouvons juger de la séquence temporelle de la transaction.
Après avoir démarré la transaction, nous allons interroger un certain enregistrement et trouver le db_trx_id < up_limit_id de l'enregistrement. Cela signifie que cet enregistrement doit avoir été soumis avant le début de cette transaction. Pour la transaction en cours, il s'agit de données historiques et nous pouvons donc certainement trouver cet enregistrement via select.
Mais s'il est trouvé, le db_trx_id > up_limit_id de l'enregistrement à interroger. Qu'est-ce que cela signifie ? Cela signifie que lorsque j'ai ouvert la transaction, cet enregistrement ne devait pas encore exister. Il a été créé plus tard et ne devrait pas être vu par la transaction en cours. À ce moment, nous pouvons utiliser le pointeur rollback + Undo Log. Retrouvez la version historique du dossier et remettez-la à la transaction en cours. Dans cet article Qu'est-ce que la lecture fantôme ? Un exemple donné dans ce chapitre. Lorsque la transaction A est démarrée, il n'y a aucun enregistrement (30, 30, 30) dans la base de données. Une fois la transaction A démarrée, la transaction B insère l'enregistrement (30, 30, 30) dans la base de données. À ce stade, la transaction A ne peut pas interroger cet enregistrement lors de l'utilisation de select sans verrouiller pour effectuer une lecture instantanée de l'enregistrement nouvellement inséré. est conforme à nos attentes. Pour la transaction A, le db_trx_id de cet enregistrement (30, 30, 30) doit être supérieur au up_limit_id lorsque la transaction A est démarrée, cet enregistrement ne doit donc pas être vu par la transaction A.
Si letrx_id de l'enregistrement qui doit être interrogé satisfait à la condition de up_limit_id < trx_id < la transaction creator_trx_id actuelle est créée. , peut être encore active, nous devons donc parcourir la collection trx_ids Si trx_id existe dans la collection trx_ids, cela prouve que cette transaction trx_id est toujours active. et invisible. Si l'enregistrement a un journal d'annulation, nous pouvons parcourir le pointeur de restauration et interroger les données de version historique de l'enregistrement. Si trx_id n'existe pas dans la collection trx_ids, cela prouve que la transaction trx_id a été soumise et l'enregistrement de la ligne est visible.
从图中你能看到回滚指针将数据行的所有快照记录都通过链表的结构串联了起来,每个快照的记录都保存了当时的 db_trx_id,也是那个时间点操作这个数据的事务 ID。这样如果我们想要找历史快照,就可以通过遍历回滚指针的方式进行查找。
最后,再来强调一遍:事务只在第一次 SELECT 的时候会获取一次 Read View
因此,如下图所示,在 可重复读 的隔离条件下,在该事务中不管进行多少次 以WHERE heigh > 2.08为条件 的查询,最终结果得到都是一样的,尽管可能会有其它事务对这个结果集进行了更改。
即便是给每行数据都加上行锁,也无法解决幻读,行锁只能阻止修改,无法阻止数据的删除。而且新插入的数据,自然是数据库中不存在的数据,原本不存在的数据自然无法对其加锁,因此仅仅使用行锁是无法阻止别的事务插入数据的。
为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)
。顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个记录,这就产生了 7 个间隙。
表 t 主键索引上的行锁和间隙锁
也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。现在你知道了,数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。但是间隙锁跟我们之前碰到过的锁都不太一样。
SELECT * FEOM t FOR UPDATE
要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (负无穷,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, 正无穷]。怎么加间隙锁呢?使用写锁(又叫排它锁,X锁)时自动生效,也就是说我们执行 SELECT * FEOM t FOR UPDATE
时便会自动触发间隙锁。会给主键加上上图所示的锁。
如下图所示,如果在事务A中执行了SELECT * FROM t WHERE d = 5 FOR UPDATE
以后,事务B则无法插入数据了,因此就避免了产生幻读。
数据表的创建语句如下
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) -- 创建索引 ) ENGINE=InnoDB; INSERT INTO t VALUES(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
需要注意的是,由于创建数据表的时候仅仅只在c字段上创建了索引,因此使用条件WHERE id = 5
查找时是会扫描全表的。因此,SELECT * FROM t WHERE d = 5 FOR UPDATE
实际上锁住了整个表,如上图所示,产生了七个间隙,这七个间隙都不允许数据的插入。
因此当B想插入一条数据(1, 1, 1)时就会被阻塞住,因为它的主键位于位于(0, 5]这个区间,被禁止插入。
还需要注意的一点是,间隙锁和间隙锁是不会产生冲突的
。读锁(又称共享锁,S锁)和写锁会冲突,写锁和写锁也会产生冲突。但是间隙锁和间隙锁是不会产生冲突的
如下:
A事务对id = 5的数据加了读锁,B事务再对id = 5的数据加写锁则会失败,若B事务加读锁则会成功。读锁和读锁可以兼容,读锁和写锁则不能兼容。
A事务对id = 5的数据加了写锁,B事务再对id = 5的数据加写锁则会失败,若B事务加读锁同样也会失败。
在加了间隙锁以后,当A事务开启以后,并对(5, 10]这个区间加了间隙锁,那么B事务则无法插入数据了。
但是当A事务对(5, 10]加了间隙锁以后,B事务也可以对这个区间加间隙锁。
Le but du verrou d'espacement est d'empêcher l'insertion de données dans cet intervalle. Par conséquent, une fois la transaction A ajoutée, la transaction B continue d'ajouter des verrous d'espacement. Mais c’est différent pour les verrous en écriture et les verrous en lecture.
Un verrou en écriture ne permet pas aux autres transactions de lire ou d'écrire, tandis qu'un verrou en lecture permet l'écriture, il y a donc un conflit sémantique. Bien entendu, vous ne pouvez pas ajouter ces deux verrous en même temps.
Il en va de même pour les verrous en écriture et les verrous en écriture. Les verrous en écriture ne permettent pas la lecture ou l'écriture. Pensez-y, la transaction A ajoute un verrou en écriture aux données, ce qui signifie qu'elle ne souhaite pas que d'autres transactions opèrent sur les données. Ensuite, si d'autres données peuvent être utilisées pour ces données, l'ajout d'un verrou en écriture équivaut à effectuer une opération sur les données, ce qui viole la signification du verrou en écriture et n'est naturellement pas autorisé.
【Recommandations associées : tutoriel vidéo mysql】
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!