当前位置: 首页 > 后端技术 > PHP

谈谈MySQL学习的锁和分类

时间:2023-03-29 23:23:19 PHP

对于阅读来说,在RR级别的MVCC下,开启一个事务的时候,会产生一个ReadView,然后通过ReadView可以找到符合条件的历史版本,而这个版本就是由构建的undolog决定,而在生成ReadView的时候,实际上生成的是快照,所以此时的SELECT查询也是快照读(或一致性读)。我们知道在RR下,一个事务在执行过程中只有第一次执行SELECT操作后才生成一个ReadView,在后续的SELECT操作中会复用这个ReadView,从而避免了对一个事务的不可重复读和幻读很大程度上。对于写,由于在快照读或一致性读时没有对表中的任何记录进行锁操作,并且ReadView的事务是历史版本,写操作的最新版本不冲突,所以其他事务可以自由进行对表中的记录进行更改。2??所有读写操作都被锁定。如果我们的某些业务场景不允许读取旧版本的记录,而必须每次都读取最新版本的记录。比如在银行存款的交易中,需要先把账户的余额读出来,然后加上这笔存款的金额,最后写入数据库。读出账户余额后,不允许其他交易再次访问余额,其他交易无法访问账户余额,直到存款交易执行完成。这样,在读取记录的时候,就需要加锁,也就是说读操作和写操作也和写操作一样排队执行。对于脏读,是因为当前事务读取了另一个未提交的事务写入的记录,但是如果另一个事务在写入记录的同时锁定了记录,则当前事务无法继续读取记录,所以不会出现脏读问题。对于不可重复读,是因为当前事务先读取了一条记录,在另一个事务修改了这条记录并提交后,当前事务再次读取时会得到不同的值。如果当前事务读取记录时记录被锁定,其他事务就不能修改记录,自然不会出现不可重复读。对于幻读,是因为当前事务读取了一个范围内的记录,然后另一个事务向该范围内插入了一条新记录。当当前事务再次读取记录范围时,发现了一条新插入的新记录。我们把新插入的那些记录称为幻象记录。这个区间怎么理解?如下:假设表user中只有一条id=1的数据。当事务A执行id=1的查询操作时,可以查询到数据。如果是范围查询,比如idin(1,2),只会查询一条数据。此时事务B执行了一个新的id=2的操作并提交。此时事务A再次执行id为in(1,2)的查询,会读取到2条记录,所以出现幻读。注意:由于RR的可重复读,id=2的记录实际上是找不到的,所以如果执行一次update...whereid=2,通过范围查询就可以找到了。通过加锁解决幻读的问题并不容易,因为那些幻读记录在当前事务第一次读取记录的时候是不存在的,所以读的时候加锁有点麻烦,因为不知道谁锁的。那么InnoDB是如何解决的呢?我们先来看看InnoDB存储引擎有哪些锁。MySQL中的锁和分类在MySQL官方文档中,InnoDB存储引擎介绍了以下几种锁:同样的,现在看来我们还是一头雾水,但是我们可以按照我们在JDK中学习锁的方式进行分类:什么是锁的粒度?是锁的粒度吗?所谓锁粒度就是你要锁的范围有多大。比如在家上厕所,只要把马桶锁上就可以了。你不需要锁上整个房子来防止你的家人进入。厕所是你的锁粒度。什么是合理的锁定粒度?其实,马桶不仅是用来上厕所的,还可以用来洗澡和洗手。这涉及优化锁定粒度。你在厕所洗澡的时候,其实其他人也可以同时在里面洗手,只要分开就行。如果马桶、浴缸、洗脸盆是分开的,相对独立的(干湿分离是的),其实马桶是可以三个人同时使用的,当然三个人做不到一样。这样就细化了加锁的粒度。洗澡的时候,只要关上卫生间的门,其他人还是可以进去洗手的。如果当初设计厕所时不把不同的功能区域划分和隔离,就不可能最大限度地利用厕所资源。同样,MySQL中也有锁的粒度。通常分为三种,行锁、表锁和页锁。3.1行锁在共享锁和排他锁的介绍中其实是针对某一行记录的,所以也可以称为行锁。锁定一条记录只影响这条记录,所以行锁的锁定粒度在MySQL中是最细的。InnoDB存储引擎默认的锁是行锁。它具有以下特点:锁冲突概率最低,并发度高。由于行锁的粒度小,发生锁资源争用的概率也最小,所以发生锁冲突的概率低,并发度高。开销大,慢速加锁非常耗性能。试想一下,如果在数据库中锁定多条数据,势必会占用大量的资源。对于加锁,需要等到之前的锁被释放后,再加锁。会出现死锁关于什么是死锁,大家可以往下看。3.2表锁表级锁就是锁住整个表的表级锁,可以很好的避免死锁。它也是MySQL中最细粒度的锁定机制。MyISAM存储引擎默认的锁是表锁。它具有以下特点:低开销和快速锁定。因为它锁定了整个表,所以它肯定比锁定单个数据要快。如果没有死锁,整个表都被锁住了,其他事务根本拿不到锁,自然不会有死锁。锁粒度大,锁冲突概率高,并发度低。3.3页面锁页面级锁是MySQL中独有的一种锁定级别,在其他数据库管理软件中并不常见。页级锁的粒度介于行级锁和表级锁之间,因此获取锁所需的资源开销和它所能提供的并发处理能力也介于上述两者之间。此外,页级锁与行级锁一样,也会导致死锁。行锁、表锁、页锁,锁粒度小,两者加锁效率慢和快,两者冲突概率低,高并发性能,性能开销一般,两者之间是否存在死锁二、或者是否是锁兼容性分类MySQL中数据的读取主要分为当前读取和快照读取:快照读取和快照读取读取快照数据,普通的没有锁的SELECT属于快照读取。1SELECT*FROMtableWHERE...当前读取当前读取是读取最新数据,不是历史数据,锁定的SELECT,或者数据的增删改查都会是当前读取。12345SELECT*FROMtableLOCKINSHAREMODE;SELECTFROMtableFORUPDATE;INSERTINTOtablevalues...DELETEFROMtableWHERE...UPDATEtableSET...大多数情况下,我们操作的数据库是当前读取的并发场景,既要让read-read的情况不受影响,又要让write-write、read-write或write-read的操作互相阻塞,就需要用到MySQL中的共享锁和独占锁。4.1共享锁和独占锁共享锁(SharedLocks)也可以称为读锁,简称S锁。可以并发读取数据,但没有事务可以修改数据。独占锁(ExclusiveLocks)也可以称为独占锁或写锁,简称X锁。如果某个对象在某行上有独占锁,则只有该事务可以读取和写入它。在这个事务结束之前,其他事务不能给它加任何锁。其他进程可以读但不能写。需要等待它的发布。我们来分析一下获取锁的情况:如果有事务A和事务B,事务A获取了一条记录的S锁,此时事务B也想获取这条记录的S锁,那么事务B也可以获取锁,即事务A和事务B同时持有这条记录的S锁。如果事务B想获取记录上的X锁,这个操作会被阻塞,直到事务A提交后S锁被释放。如果事务A先获取到X锁,那么无论事务B是想获取S锁还是记录的X锁,都会被阻塞,直到事务A提交。因此,我们可以说S锁兼容S锁,S锁不兼容X锁,X锁不兼容X锁。4.2意向锁意向共享锁(IntentionSharedLock),简称IS锁。当一个事务要对一条记录加S锁时,需要先在表级加IS锁。意向排他锁(IntentionExclusiveLock),简称IX锁。当一个事务要给记录加X锁时,需要先在表级加IX锁。意向锁是表级锁。它们只是为了在后面加表级S锁和X锁时,快速判断表中的记录是否被锁,避免使用遍历检查表是否被锁。的记录。也就是说,IS锁兼容IS锁,IX锁兼容IX锁。为什么需要意向锁?InnoDB的意向锁主要是利用了多粒度锁的共存。比如事务A需要给一张表加S锁。如果表中某行已经被事务B用X锁锁定,那么申请锁也应该被阻塞。如果表中的数据很多,逐行检查锁标志的开销会很大,会影响系统的性能。比如表中有1亿条记录,事务A锁定了其中的几条记录,那么事务B就需要对表加表级锁。如果没有意向锁,则必须去表中查找这1亿条记录是否被锁定。如果有意向锁,那么如果事务A在更新一条记录之前加了意向锁,然后加了X锁,事务B首先检查表是否有意向锁,现有的意向锁是否与表冲突锁定它要添加。如果有冲突,则等到事务A被释放,而不检查每条记录。事务B在更新表的时候,其实不需要知道哪一行被锁了,反正只要知道有一行被锁就行了。说白了意向锁的主要作用就是处理行锁和表锁的矛盾。它可以表明一个事务正在对某一行持有锁,或者即将持有锁。表级各种锁的兼容性:SISXIXSCompatibleCompatibleIncompatibleIncompatibleISCompatibleCompatibleIncompatibleIncompatibleIncompatibleXIncompatibleIncompatibleIncompatibleIncompatibleIncompatibleIncompatibleISCompatibleCompatibleIncompatibleIncompatibleIncompatible4.3读操作的锁对于MySQL,读操作有两种方式锁定。1??SELECT*FROMtableLOCKINSHAREMODE如果当前事务执行了这条语句,它会对读到的记录加S锁,这样其他事务就可以继续为这些记录获取S锁(比如其他事务也使用SELECT...LOCKINSHAREMODE语句读取这些记录),但无法获取这些记录的X锁(例如使用SELECT...FORUPDATE语句读取这些记录,或者直接修改这些记录)。如果其他事务要获取这些记录的X锁,则会阻塞,直到当前事务提交后释放这些记录上的S锁2??SELECTFROMtableFORUPDATE如果当前事务执行了这条语句,则会加X锁到读取的记录,这样就不允许其他事务获取这些记录的S锁(例如其他事务使用SELECT...LOCKINSHAREMODE语句读取这些记录),也不允许获取这些记录X锁定记录(例如,使用SELECT...FORUPDATE语句读取这些记录,或直接修改这些记录)。如果其他事务想要获取这些记录的S锁或X锁,它们将阻塞,直到当前事务提交后这些记录的X锁被释放。4.4写操作的锁对于MySQL的写操作,常用的有DELETE、UPDATE、INSERT。隐式加锁,自动加锁,解锁。1??DELETE对一条记录进行DELETE操作的过程是先定位这条记录在B+树中的位置,然后获取这条记录的X锁,然后执行删除标记操作。我们也可以把这个在B+树中定位待删除记录位置的过程看成是一个锁读获取X锁。2??INSERT一般情况下,插入一条新记录的操作是不加锁的。InnoDB使用所谓的隐式锁来保护这个新插入的记录在事务提交之前不被其他事务访问。3??UPDATE在对一条记录进行UPDATE操作时分为三种情况:①如果记录的key值没有被修改,并且修改前后更新列占用的存储空间没有变化,先定位到这条记录在B+树中记录位置,然后获取记录的X锁,最后修改原来的记录位置。其实我们也可以把这个在B+树中定位待修改记录位置的过程看成是获取X锁的加锁读。②如果记录的键值没有被修改,并且至少有一个更新的列占用的存储空间在修改前后发生了变化,则先定位这条记录在B+树中的位置,然后获取该记录的X锁record,and记录被完全删除(即记录被完全移入垃圾列表),最后插入一条新记录。将要修改的记录在B+树中定位的过程看成是一次加锁读获取X锁,新插入的记录受到INSERT操作提供的隐式锁的保护。③如果修改了记录的key值,相当于在对原记录进行DELETE操作后进行了INSERT操作,需要根据DELETE和INSERT的规则进行加锁操作。PS:为什么写锁加锁后其他事务还能读?因为InnoDB有MVCC机制(多版本并发控制),所以可以不阻塞地使用快照读取。锁粒度分类什么是锁粒度?所谓锁粒度就是你要锁的范围有多大。比如在家上厕所,只要把马桶锁上就可以了。你不需要锁上整个房子来防止你的家人进入。厕所是你的锁粒度。什么是合理的锁定粒度?其实,马桶不仅是用来上厕所的,还可以用来洗澡和洗手。这涉及优化锁定粒度。你在厕所洗澡的时候,其实其他人也可以同时在里面洗手,只要分开就行。如果马桶、浴缸、洗脸盆是分开的,相对独立的(干湿分离是的),其实马桶是可以三个人同时使用的,当然三个人做不到一样。这样就细化了加锁的粒度。洗澡的时候,只要关上卫生间的门,其他人还是可以进去洗手的。如果当初设计厕所时不把不同的功能区域划分和隔离,就不可能最大限度地利用厕所资源。同样,MySQL中也有锁的粒度。通常分为三种,行锁、表锁和页锁。4.1行锁在共享锁和排他锁的介绍中其实是针对某一行记录的,所以也可以称为行锁。锁定一条记录只影响这条记录,所以行锁的锁定粒度在MySQL中是最细的。InnoDB存储引擎默认的锁是行锁。它具有以下特点:锁冲突概率最低,并发度高。由于行锁的粒度小,发生锁资源争用的概率也最小,所以发生锁冲突的概率低,并发度高。开销大,慢速加锁非常耗性能。试想一下,如果在数据库中锁定多条数据,势必会占用大量的资源。对于加锁,需要等到之前的锁被释放后,再加锁。会出现死锁关于什么是死锁,大家可以往下看。4.2表锁表级锁就是锁住整个表的表级锁,可以很好的避免死锁。它也是MySQL中最细粒度的锁定机制。MyISAM存储引擎默认的锁是表锁。它具有以下特点:低开销和快速锁定。因为它锁定了整个表,所以它肯定比锁定单个数据要快。如果没有死锁,整个表都被锁住了,其他事务根本拿不到锁,自然不会有死锁。锁粒度大,锁冲突概率高,并发度低。4.3页面锁页面级锁是MySQL独有的一种锁级别,在其他数据库管理软件中并不常见。页级锁的粒度介于行级锁和表级锁之间,因此获取锁所需的资源开销和它所能提供的并发处理能力也介于上述两者之间。此外,页级锁与行级锁一样,也会导致死锁。行锁,表锁,页锁,锁粒度小,两者之间加锁效率慢和快,两者冲突概率低,高并发性能,性能开销一般,之间是否存在死锁二、是否是算法实现分类对于上面锁的介绍,其实我们可以知道主要区别在于锁的粒度,而InnoDB中使用的锁是行锁,也叫记录锁,但需要注意的是,这条记录指的是索引Item上的索引是被锁定的。InnoDB的行锁实现特性意味着InnoDB只有在通过索引条件检索数据时才使用行级锁,否则InnoDB使用表锁。无论是使用主键索引、唯一索引还是普通索引,InnoDB都使用行锁来锁定数据。行锁只有在执行计划真正使用索引的情况下才能使用:即使条件中使用了索引字段,是否使用索引检索数据是由MySQL通过判断不同执行计划的成本来决定的。如果MySQL认为全表扫描效率更高,比如一些非常小的表,它就不会使用索引。在这种情况下,InnoDB将使用表锁而不是行锁。同时,当我们使用范围条件而不是相等条件来检索数据并请求锁时,InnoDB将锁定满足条件的现有数据记录的索引项。但是即使是行锁,InnoDB也分很多种。也就是说,即使是对同一条记录加行锁,如果类型不同,效果也会不同。通常有以下几种常用的行锁类型。5.1RecordLock记录锁,对单个索引记录的锁。RecordLock总是锁定索引,不包括记录本身。即使表上没有索引,InnoDB也会在后台创建隐藏聚簇主键索引,然后锁定隐藏聚簇主键索引。记录锁分为S锁和X锁。当一个事务为一条记录获取了S型记录锁时,其他事务可以继续获取该记录的S型记录锁,但不能继续获取X型记录锁。;一个事务为一条记录获取X型记录锁后,其他事务不能继续为这条记录获取S型记录锁和X型记录锁。5.2间隙锁间隙锁锁定索引前后的间隙,而不是索引本身。MySQL可以解决REPEATABLEREAD隔离级别下的幻读问题。解决方案有两种,可以使用MVCC方案解决,也可以使用加锁方案解决。但是使用加锁的方案有个问题,就是当事务第一次执行读操作的时候,那些幻象记录还不存在,我们不能给这些幻象记录加记录锁。所以我们可以使用间隙锁来锁定它。如果有这样一张表:123456789101112CREATETABLEtest(idINT(1)NOTNULLAUTO_INCREMENT,numberINT(1)NOTNULLCOMMENT'number',PRIMARYKEY(id),KEYnumber(number)USINGBTREE)ENGINE=INNODBAUTO_INCREMENT=1默认字符集=utf8;插入以下数据INSERTINTOtestVALUES(1,1);插入测试值(5,3);插入测试值(7、8);插入测试值(11、12);如下:开启一个事务A:123BEGIN;SELECT*FROMtestWHEREnumber=3FORUPDATE;这时,((1,1),(5,3))和((5,3),(7,8))之间的锁。如果此时启动一个事务B插入数据,如下:1234BEGIN;块INSERTINTOtest(id,number)VALUES(2,2);结果如下:为什么不能插入?因为要插入的记录(2,2),在索引号上,刚好落在((1,1),(5,3))和((5,3),(7,8))之间,是吗被锁定,因此不允许插入。如果在范围之外,当然可以插入,如:1INSERTINTOtest(id,number)VALUES(8,8);5.3Next-KeyLocksnext-key锁是索引记录上的记录锁和索引记录之前的间隙锁的组合,包括记录本身,每个next-key锁是一个前开后闭区间,也就是说gaplock只是锁的gap,recordline是不加锁的,next-key锁基本都是gaplock右边界行加锁。默认情况下,InnoDB在REPEATABLEREAD隔离级别运行。在这种情况下,InnoDB使用Next-KeyLocks进行搜索和索引扫描,从而防止幻读的发生。乐观锁和悲观锁乐观锁和悲观锁其实都不是具体的锁,而是一种锁的思想,不仅在MySQL中有体现,Redis等常见的中间件都可以应用这种思想。https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...6.1乐观锁所谓乐观锁就是持有乐观的态度。当我们更新一条记录时,假设在这段时间里没有其他人会去操作这条数据。乐观锁的一种常见实现方式是在表中增加一个version字段,控制版本号,每次数据修改后加1。每次更新数据前,先查询数据的版本号,然后进行业务操作,然后将查到的版本号与当前数据库中的版本号进行比较,再更新数据。如果相同,说明没有其他线程修改过数据,否则会处理相应的异常。