一)MYSQL中常见的锁讲解:
并发事务访问的情况大概可以分成三种:
1)读读情况:就是并发事务读取相同的记录,读取操作本身不会对任何事物有影响,也不会出现什么问题,所以允许这种情况的发生
2)写写情况:就是并发事务相继对相同的记录进行改动,在这种情况下会出现脏写的问题,任何一种事务隔离级别都是不允许此情况的发生,所以多个未提交事务相继在进行针对同一条记录进行改动的时候,就让他们排队执行,这个排队的过程本身是依靠锁来实现的,这个所谓的锁本质上是内存中的结构,在事务执行前是没有锁记录和这个纪录相关联的,当时一个事务想要针对于这条记录做改动的时候,首先会在内存中看看有没有和这条记录相关联的锁结构,如果没有的话就会在内存中生成一个与之相关的锁结构与之相关联,比如说事务T1想要对这条记录做改动就会生成一个锁结构与之相关联:
锁结构是和具体的事务相关的,每一个事务都有着自己的锁结构;
不加锁:那么就意味着这个事务不需要在内存中生成相应的锁结构,可以直接执行该操作
获取到锁或者是加锁成功:意思就是在内存中成功的生成了相应的锁结构,并且将锁结构的isWaitting设置成false,也就是说事务可以继续执行操作
获取锁失败,或者说加锁失败或者说没有获取到锁:意思就是说在内存中生成了相应的锁结构不过锁结构的isWaitting属性设置成true,也就是说事务需要等待,不能继续执行此操作
并发问题的解决方案:就是遇到脏读幻读不可重复读问题的时候,该如何来进行解决呢?
1)读操作采用多版本并发控制,写操作直接进行加锁
所谓的MVCC就是生成一个readView,通过这个读视图来找到当前符合要求的历史版本,历史版本就是存放在undolog的版本链中,查询记录只能读取到生成读视图之前的已经提交的事务做出的修改,在生成读视图之前未提交的事务或者是之后所开启的事务所作的修改是看不到的,但是写操作肯定针对的是当前最新的版本记录,读记录的历史版本和改动记录的最新版本本身并不冲突,所以在进行采用MVCC的时候,读写操作并不冲突
普通的select语句和read committed和repeatable read隔离级别下会使用到MVCC来读取数据记录:
a)当在READ COMMITED情况下,一个事务在执行过程中每一次执行select操作时都会生成一个读视图,读视图的存在就已经保证了事务内部可以读取到已提交的事务所做的更改,也就是避免了脏读,ReadView中记录的是读已提交的数据,记录的是已提交事务,此时ReadView操作只能读到已经提交的数据,因为每一次Select操作都会生成一个readView
b)在第三种隔离级别下:第一次select操作才会生成ReadView,也就是说只能读到第一次select之前提交的数据,后续事务修改过的记录和后续事务添加过的记录都读不出来,这样就解决了不可重复读和幻读的情况;
2)第二种方法就是读和写都采取加锁的方式来进行解决:
脏读的问题就是当前事务读取到了另一个未提交事务写的一条记录,如果说另一个事务在写记录的时候就给这一条记录进行加锁,那么当前事务也就无法读取该记录了,也就不会有脏读问题的产生了
InnoDB加的锁更细,有行级锁,使得并发执行性能更高
写锁一定是排他锁,读锁也可以是排他锁也可以是共享锁
从数据操作的类型来进行划分:
都知道数据库的并发事务的读读操作并不会出现什么问题,但是对于写写操作和写读操作可能会出现问题,所以要采用MVCC和加锁的方式来解决他们,由于既要保证读读情况下不受影响,又要使得写写,读写,写读情况下的相互阻塞,所以说此时MYSQL实现了一种两种类型的锁组成的锁结构来解决,这两种类型的锁称之为是共享锁和排他锁,也叫做读锁或者是写锁
1)读锁:也被称之为是共享锁,英文用S来表示,针对同一份数据多个事务的读操作可以同时进行而不会受到影响,相互之间是不阻塞的
2)写锁:也被称之为是排他锁,英文用X来表示,当前写操作没有完成前,他会阻断其他写锁和读锁,这样可以确保在给定的时间内,只有一个事务能够写入,并且防止其他事务读取正在写入的同一资源
排他锁:又称为写锁、独占锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁,这就保证了其他事务在T释放A上的锁之前不能再读取和修改A
需要特别注意的是,对于InnoDB引擎来说,读锁和写锁既可以加在表上,也可以加在行上
行级读写锁:假设此时有一个事务T1已经获取到某一行的读锁,那么其他的事务也是可以获取到这一行的读锁的,因为读取操作并没有改变行r的数据,但是如果出现某一个事务T3想要获取到行r的写锁,那么必须等待到事务T1,T2释放掉r上的读锁才可以
windows命令下cls清屏
此时我们先开启事务1,给整张表加上一个表的S锁
现在开启一个事物2,给整张表加上一个S锁,枷锁成功,但是在进行尝试给整张表加上X锁的时候失败了,因为事务2给表加上S锁以后,说明此时事务1和事务2都可以进行并发读取数据了,但是此时如果出现(假设事务2加上X锁成功),事务1就有可能读取到事务2修改一半的临时数据
但是此时如果将事务2进行提交,那么此时就发现事务1可以获取到排他锁,此时update操作成功,一个事务是可以同时获取到共享锁和排他锁的,就是事务A在获取到公共资源的共享锁以后还可以获取到这条公共资源的排他锁
知识补充:在进行采取加锁的方式来解决脏读幻读不可重复读的时候,读取一条记录的时候需要获取该纪录的S锁,其实本质上是不太严谨的,有的时候需要读取记录时就需要获取记录的X锁,来禁止其他事务读写该记录,所以MYSQL提出了两种Select方式:
1)selectXXXlock in share mode,是针对于读取的记录加S锁,在普通的select语句后面加上lock in share mode,如果说当前事务执行了该语句,那么他会为当前读取到的记录加S锁,这样还可以允许其他的事务来继续获取这条记录的S锁,比如说其他事务也针对于这条记录进行select XX lock in share mode来读取这条记录,但是不能来获取这条记录的X锁,比如说使用select XX for update或者是直接修改这一条记录,如果别的事务想要获取到这条记录的X锁,那么他们会阻塞,知道当前事务提交之后将这条记录上的S锁释放掉
2)select XXX for update,在这样的语句后面加上for update,如果当前事务执行了这一条语句,那么它会给这条读取的记录加上X锁,比如说使用select XX for update或者是直接修改这一条记录,这样既不允许其他记录来获取到这条事务的X锁,也不允许获取到这一条记录的S锁,比如说其他事务也针对于这条记录进行select XX lock in share mode来读取这条记录,如果别的事务想要获取到这条记录的S锁或者是X锁,必须等到事务提交之后将X锁释放掉
3)比如说T1给表中的数据加了一个X锁,不管是T2的事务操作这一行数据加的是X锁还是S锁,实际上都是需要进行阻塞的
写的时候一定要加上排他锁,update user set name="李四" where id=3;
读锁叫锁,是因为如果后续有个写锁的话,那这个写操作要被阻塞到读锁释放之后
一)lock in share mode:
语法:select ...... lock in share mode
1)使用之前必须先开启事务,如果这个规则不在事务中使用的话,那么不会执行此规则
2)当select语句中的where条件查询的时候使用到了索引,那么会对查询结果所在的行进行锁定,也就是行级锁,如果查询没有使用到了索引,就会升级到表级锁;
3)某一个事务添加的锁,只有该事务可以解锁,当事务结束或者是回滚的时候才会释放锁
4)当一个事务对某张表或表中的行加了共享锁,则当前事务内可以对锁住的对象进行查询、修改和删除操作,可以再次获取到排他锁,其他事务对已经加了共享锁的事务只能查询,当要修改或删除对应的记录时,将会阻塞等待(因为排他锁不能再次进行获取)
5)当一个事务对某张表或表中的行加了共享锁,其他事务也是可以对同一对象添加共享锁,即同一张表或行是可以被多个不同的事务同时添加共享锁的
二)for update
语法:select ....for update
4)一个事务对某张表或表中的行就是某一个共享资源加了独占锁,则其他事务不能再锁定相应的表或表中的行了,不管其他事务要加共享锁还是独占锁,命令都会阻塞等待,直到别的事务把锁释放
5)当一个事务对某张表或表中的行加了独占锁,则当前事务内可以对锁住的对象进行查询、修改和删除操作
6)由此我们还可以发现,一个事务在单独执行的过程中,可以获取到了排他锁之后,还可以重复的获取共享锁和排他锁,由此得知也是一个可重入锁
1)为了尽可能地提升事务的并发率,每一次锁定的数据范围是越小越好的,理论上每一次只需要锁定当前操作的数据的方案可以达到最大的并发度,但是本身管理锁是很消耗资源的事情,涉及到检查,获取,释放锁等操作,如果锁粒度比较小,那么开销也就越大,因此数据库系统需要在并发响应和系统性能两方面进行平衡,于是就产生了锁粒度这样的概念;
2)对于一条记录加锁影响的也只是这一条记录而已,我们就说这个锁的粒度比较细,其实一个事务也可以在表级别进行加锁,自然也就称之为是表级锁或者是表锁,对一张表进行加锁影响整张表的记录,就说这个锁的粒度比较粗,锁的粒度主要分为行级锁,页级锁和页锁
3)表锁会锁定整张表,他是MYSQL中最基本的锁策略,并不依赖于存储引擎不管是什么样子的存储引擎对于表的策略都是一样的,并且表锁是开销最小的策略,因为锁的粒度比较大,可以很好的避免死锁问题,当然,锁的粒度所带来的最大影响就是锁资源竞争的概率也会比较高,导致并发大打折扣
4)避免在InnoDB引擎上面的表使用Lock Tables这样的手动锁表语句,他们并不会提供什么额外的保护,只是单纯的降低并发性能,InnoDB的厉害之处在于实现了更细粒度的行锁
加锁:lock tables+表名+read,lock tables+表名+write
释放锁:unlock tables;
查看表中是否有锁:show open tables where in_user>0,看看in use这这一列
三)lock tables+TableName+read,表共享读锁
1)使用该命令对表进行加锁的时候,无需开启事务也是可以生效的
2)是应该命令对表加读锁的时候,当前会话只允许读记录,是不允许修改表中的记录的
3)一个会话使用该命令对这张表加读锁,其他的会话可以正常查询表,也可以对表加读锁(多个会话可以同时对同一张表加读锁),但是无法修改表
4)会话只能释放自己所拥有的锁,不能释放别的会话所拥有的锁
5)加上这把锁以后,当前会话不能读取其他表和更改其他表的数据
会话1针对于这张表加读锁,会话2不能进行修改这张表的任何数据
但是会话2开启事务之后却可以加读锁:
同理事务2想要进行插入也是不可以的:
假设现在lock tables user read;
四)lock tables+TableName+write,表共享写锁
1)使用该命令对表进行加锁的时候,无需开启事务也是可以生效的
2)一个会话对于表加了写锁,当前会话可以查询和修改表
3)一个会话对表加了写锁,其他会话查询或者是修改表的时候会阻塞等待,知道对应的表锁被释放了
4)会话只能释放自己所拥有的锁,不能释放别的会话所拥有的锁
释放锁:
1)如果会话执行unlock tables,命令会释放当前会话所加的锁
2)如果会话在已经持有锁的情况下发出lock tables语句来获取锁,则在授予新锁之前隐式地释放其现有的锁,但是lock in share mode和for update没有这个操作,下面两个事务是按照顺序来进行执行的
3)如果会话开始事务,将执行隐式解锁表,从而释放现有的锁
下面是两个事务同时在执行:
锁与事务的关系:
1)lock tables不是事务安全的,在试图锁表之前会隐式提交任何活动事务。2)unlock tables隐式提交任何活动事务,但前提是lock tables已用于获取表锁。即有表锁的情况下,执行unlock tables命令,只会释放锁,不提交事务。在没有表被锁的情况下,会释放锁和提交事务
3)start transaction命令会释放锁,下面的等是等待
加上读锁以后,自己是可以读这张表的数据的,但是加锁的事物本身不可以修改这张表的数据,自己本身也不可以操作其他表,但是其他事务可以读取这张表,其他事务如果想修改这张表就会发生阻塞,直到对应事务释放这张表锁才可以执行成功
MYSIM存储引擎在执行查询语句之前,会给涉及到的所有的表加读锁,再进行增删改查操作之前,会给所有的表加写锁,但是InnoDB存储引擎一般是不会给这个表加读锁或者是写锁的
当前针对于表加了读锁,当前客户端可以读,其他客户端也是可以读的
当前加读锁的客户端是不可以写的,其他客户端也是不可以写的,除非拥有读锁的客户端自动释放锁
五)表级别的锁,意向锁:
意向锁就是个标志,标志当前表是不是有行锁 别人看见了这个标记就不用遍历每一行是不是有行锁了
在innoDB存储引擎来说一共支持两种表级锁:意向锁和元数据锁,本身的InnoDB是支持多粒度锁的,他本身是支持行级锁和表级锁是共存的,而意向锁就是表锁中的一种,不会导致性能降低;
1)意向锁的存在是为了协调行锁和表锁之间的关系,支持多粒度表锁和行锁共存
2)意向锁的存在是不和行级锁冲突表级锁,这一点非常重要
3)意向锁表名某个事务正在某一些行持有了锁或者是该事务准备去持有锁
4)意向锁是有存储引擎自己所维护的,用户是无法手动操作意向锁的,在为数据行添加共享锁或者是排他锁之前
首先了解意向锁要解决的问题是什么?
1)假设此时T1和T2是两个事务,T2想要在表级别上添加共享锁或者是排他锁,能够添加的前提,首先要检查有没有其他事务对这张表或者某一行加锁从而来进行判断
2)假设T1这个事务已经对某一行数据加上了X行锁,T2事务想在整张表加锁肯定加不了,肯定会阻塞,你说不能加是因为你看到了,但是此时机器还是要进行判断是否加没加,但是假设表中有10000条数据,T2就要检查10000条记录,遍历去找1000条记录有没有加X行锁,这样子性能就会很低
3)此时如果T1针对于某一行记录进行加X锁,那么此时就会给这个行记录的表加上一把意向锁(在上一层加上一个意向X锁),这个时候T2想加上一个全表的排他锁的时候就不需要遍历表中的所有记录,这个时候添加直接判断表中的X意向锁阻塞了,这个时候就不需要再进行表中的所有记录了,这时候性能很棒;
意向锁主要分成两类:
如果事务想要获取到数据表中的某一些记录的共享锁,就需要在数据表上添加意向共享锁
如果事务想要获取到数据表中的某一项记录的排他锁,就需要在数据表上添加意向排他锁
1)意向共享锁(IS):事务有意向某一些行上加上共享锁,事务想要获取某一些行的S锁,必须要先获取表的IS锁,select column from table lock in share mode;
2)意向排他锁(IX):事务有意向某一行上加上排他锁,事务想要获取到某一行的X锁,必须先要获取到表的IX锁,select column from table for update:
1)比如说当下一个事务中执行了一条语句,select * from user for update,当执行这一条记录的时候,会给这条记录加上一个X锁,也会给整张表加上一个意向排他锁
2)此时在开启一个事务进行执行:此时这个事务也想要给这张表加上一个X锁,但是看到这张表的意向排他锁已经被占用了,所以就不能再给这张表加上意向排他锁了,但是这个表锁是意向共享锁这个事务是可以加锁成功的;
此时因为行记录排他锁和表级别的共享锁是互斥的,所以说第二个事务想要对user表加上读锁的时候此时就会发生互斥,想要添加成功,就必须满足两个条件:
a)当前事务不存在针对于这张表的排他锁
b)当前没有其他事务持有user表中的任意一行的排他锁
3)但是此时如果将第一个事务进行提交,意向排他锁和这条记录的排他锁IX都会释放,那么第二个事务就加锁成功,此时可以看到这个上行锁的时间也比较长
意向锁是加锁后的一个标记,真正起作用的还是行锁或表锁
虽然意向排他锁是相互兼容的,但是如果想锁表的话,还是只能有一个成功的,其实感觉不能说是他们相互兼容,应该是说行级别的锁只要不是同一行就是相互兼容的
1)InnoDB支持多粒度锁,在特定场景下,行级锁可以和表级锁共存
2)意向锁之间胡不排斥,但是除了IS和S兼容之外,意向锁会和表级的X和S发生冲突
3)IX和IS是表级锁,不会和行级的X和S锁发生冲突,只是回合表级的X和S发生冲突
4)意向锁是在满足并发性的前提下,实现了行锁和表锁共存况且还满足了隔离级别的需要
六)自增锁:不是针对于某一个字段起作用,而是针对于整张表,所以这是一个表级锁
确保在给主键赋值的时候是全局唯一性的,防止主键出现非unique的场景
自增锁:给主键赋值具有全局唯一性,防止数据出现非unique的场景
在插入时,mysql采用自增锁的方式来实现,当向使用auto_increment列插入数据时需要获取一种特殊的表级锁,在插入语句时加一个自增锁,然后再语句执行后,再把自增锁释放掉一个事务再持有锁时,其他事务的插入语句都要被阻塞,所以并发性并不高
七)元数据锁:简单来说就是行锁是表记录锁,元数据锁是表结构锁
1)在MYSQL5.5的时候引入了元数据锁,是属于表锁的范畴,MDL语句的作用就是保证读写的正确性,比如说一个查询正在遍历表中的数据,而执行期间另一个线程对这张表的表结构做变更,新增加了一列,那么此时查询的线程和具体的表结构对不上,肯定是不行的,因此当对一张表进行MDL操作的时候加MDL读锁,当要针对于表结构做变更的时候,加MDL写锁
2)读锁和读锁之间并不互斥,因此你可以有多个线程同时针对一张表进行增删改查,但是读锁和写锁,写锁和写锁之间是互斥的,用来保证表变更时候的表结构操作的安全性,解决了DML和DDL操作之间的一致性问题,不需要显示使用,在访问一张表的时候会被自动加上
3)当表的结构修改成功,元数据所会被成功释放
元数据所:DDL操作和其他的X锁阻塞
先执行左边在执行右边,发现阻塞
当对一张表进行增删改查的时候,加MDL读锁共享,当对表结构进行变更操作的时候,加MDL写锁排他,注意select * from user也会加MDL读锁,只有两个MDL读锁的时候是可以并发执行的,但是本来有一个MDL的读锁,你有加上一个写锁,MDL的写锁出现导致被阻塞,这个时候又加了一个MDL读锁,所以此时也会阻塞,此时读也会阻塞,此时两个读操作不能并发执行;
1)当执行update语句的时候,实际上会对记录加独占锁(X锁),另外其他事务对持有独占锁的记录进行修改的时候会被阻塞,这个锁并不是执行完update语句才会释放,而是会等事务结束时才会释放,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句
2)UPDATE是自带锁的,只有SELECT才需要显式FOR SHARE/UPDATE
3)先执行上面的语句,再执行下面的语句
之前说的脏读问题,就给你修改的纪录上加一个锁,不让别人去访问,针对于不可重复读,你就针对于这一行记录加一个锁,不让别人去修改
行锁本身又被称之为是记录锁,顾名思义就是锁住某一行,需要注意的是MYSQL的服务器并没有实现行锁机制,行级锁只是在存储引擎层出现
优点:锁的粒度小发生锁冲突的概率比较低,并发执行效率高
缺点:对于锁的开销比较大,频繁的进行加锁或者是解锁,很容易出现死锁情况
记录锁是存在S和X之分的,称之为是S型记录锁和X型记录锁
1)当一个事务获取到了一条记录的S型记录锁以后,其他事务也是可以获取到这条记录的S型记录锁,但是不可以获取到这条记录的X型记录锁
2)当一个事务获取到了一条记录的X型记录所以后,其他事务既不可以获取到该记录的X型记录锁,也是不可以获取到该记录的X型记录锁
八)InnoDB中的行锁之间隙锁
1)针对于唯一索引进行等值匹配的时候对已经存在的记录进行等值匹配的时候,将会自动优化成行锁
2)innodn的行锁是针对于索引加的锁,不通过索引条件来进行检索数据,那么Innodb会对表中的所有记录进行加锁,此时就会升级成表锁
MYSQL在RR隔离级别下是可以解决幻读问题的,解决方案一共有两种可以使用MVCC机制或者是加锁来解决,但是此时加锁方案有一个大问题,就是事务再进行第一次读取操作的时候,那些幻像行数据并不存在,就无法给这些幻想行记录加上间隙锁,InnoDB提出了一种名字是Gap Locks的锁
如果一个事务加上了间隙锁,那么事务本身是可以自己添加数据
先执行右边,在执行左边
在间隙中添加for update还是lock in share都是加的是间隙锁,间隙锁是可以重复添加的,先看上面再看下面,况且此时在看到,给3-10开区间插入数据是会失败的
还可以看到间隙锁在一个会话中是可以重复的去添加的:
4)但是此时不在(3,10)区间范围内的间隙是可以插入记录的
5)当我们去尝试添加的间隙锁是锁住的是比表中的最大记录还大的时候,此时间隙锁锁定的范围就是(表中的最大记录,+无穷)
九)临间锁=记录+间隙
有的时候既想锁住某一条记录,又想组织其他事务在该前面的间隙插入记录,假设先在给(3,8)之间的记录加上间隙锁,然后再给8这条记录加上临建锁,此时给(3,8)之间的数据加上了一个间隙锁,并且给8这条数据加上了X锁
默认情况下,InnoDB在RR隔离级别下运行,InnoDB默认使用临建锁进行搜索和索引扫描,来防止幻读:
1)索引上的等值查询,给不存在的纪录进行加锁的时候,优化成间隙锁,此时加的是间隙锁,间隙锁不包含当前的记录,直到间隙锁释放以后,别的事物才可以插入数据
2)索引上的等值查询(普通索引),向右进行扫描遍历最后的一个值不满足要求的时候,临建锁退化成间隙锁,会在满足查询要求的值和右边的第一个不满足查询要求的值加上间隙锁,因为普通二级索引还是一颗B+树,就是为了防止其他记录新插入要查询的新纪录
在下面的例子中不仅对等值查询的数据加上了行锁,还针对于一定范围加上了间隙锁
3)针对于索引上的范围查询,也就是唯一索引,会访问到不满足条件的第一值为止:
此时的这条语句加的就是针对于3这条记录的行锁和(3,7)之间的间隙锁和(7,+无穷的)数据
此时如果开启另一个事务进行插入,此时就会插入失败
十)插入意向锁:
什么是MySQL插入意向锁?__江南一点雨的博客-CSDN博客
意向锁是一个表级锁,插入意向锁是一个行级锁,一个事务再进行插入一条记录的时候需要进行判断插入位置是不是被别的记录加上了gap锁,如果有的话,那么插入数据需要阻塞等待,直到拥有gap锁的那一个事务进行了提交操作,但是innodb本身规定事务在进行等待的时候需要在内存中也要生成一个锁结构,表明有事务想要在某一个间隙插入记录,但是西安在在等待,插入意向锁是一种间隙锁,不是意向锁,是在insert的过程中产生的
插入意向锁是在临建锁或者是间隙锁场景下才有插入意向锁,当前事务想要插入数据没有临建锁或者是间隙锁的情况下
插入意向锁是在插入一条记录前由Insert操作产生的一种间隙锁,该锁表明插入意向,当多个事务在同一个区间(gap)中插入位置不同的多条数据的时候,事物之间不需要相互等待,假设存在两条值是4和7的记录,,两个十五分别尝试分别插入值是5和6的记录的时候,每一个事物在获取插入行上面的独占的排他锁前,都会尝试获取到(4,7)之间的间隙锁,但是数据行本身并不会冲突,所以两个十五不会发生冲突,阻塞等待
插入意图锁:当其他事务1想要插入数据加上了一个间隙锁,但是事务2想要插入数据,是一定会插入失败的,但是此时事务2获得了一个插入意向锁,并处于waitting状态,当事务1释放临建锁的时候,事务2还是有机会插入数据的
1)如图所示,第一个事务开启,并给大于25的记录添加了一个意向锁:
2)此时事务2开启了一个事务,进行想要插入数据,发现插入失败了
1)如图所示,我们开启了三个会话窗口,事务1插入间隙锁,此时事务2和事务3想要插入数据会发生阻塞
2)此时将事务1提交,发现事务2和事务3都成功插入了数据
十一)页锁:
页锁就是在锁的粒度上进行锁定,锁定的数据资源要比行锁多得多,因为一个页中可以有多条行记录,当我们使用页锁的时候会出现数据浪费的情况,但是这样的浪费最多也就是一个页上面的数据行,页锁的开销在行锁和表锁之间,也会发生死锁,并发度一般
假设事务A锁定了页1,事务B锁定了页2,事务A想要想页2中插入数据发生阻塞,事务B想要向页1中插入数据,这样子就发生了死锁
十二)MYSQL中的死锁:
create table account(userID int primary key auto_increment,username varchar(30),money int); insert into account values(1,"张三",200),(2,"李四",200);
1给2转账,2给1转账
1)演示MYSQL间隙锁导致的死锁:但是表锁不会出现死锁
1)初始情况下,两个事务都处于开启状态:
2)现在两个事务都加上了间隙锁,当事务1加上了间隙锁以后,事务2就不可以再进行添加操作了,只能由事务1进行添加,但是当事务2加上了间隙锁以后,事务1就不可以再进行添加操作了,只能由事务2进行添加,此时死锁就发生了,其实事务1如果想要插入数据,只能等到事务2释放间隙锁,但是此时事务2永远也不会释放间隙锁,此时就发生了死锁
3)此时发生死锁:双方互相僵持,此时事务1拥有间隙锁1,事务2拥有间隙锁2,事务1想要插入记录但是需要等待事务2释放间隙锁2,此时事务2也想要插入记录必须也要等待事务1释放间隙锁1,此时就发生了死锁,这个时候事务1主动释放锁,此时事务2也就添加成功了
2)由于行锁加上排他锁而导致的死锁:
此时事务1和事务2都进行了开启,并且事务1给id=1的记录加锁,事务2给id=2的记录加锁
此时事务1想想获取到id=2的排他锁此时就会发生阻塞:
但是此时事务2想要获取到id=1的记录的排他锁此时就会发生阻塞,并且会发生死锁
这个时候事务1在进行等待事务2释放id=2的行锁,而事务2在进行等待事务1释放id=1的行锁,事务1和事务2都是在进行相互等待对方的资源释放,就是进入了死锁状态,此时MYSQL解决死锁状态一共有两种策略:
九)MYSQL是如何解决死锁的?
1)一种策略是直接进入等待,直到超时,这个超时时间可以使用参数innodb_lock_wait_timeout来进行设置,但是在innodb存储引擎中,这个参数的默认值是50,那就意味着如果采用第一种策略,当出现死锁以后,第一个锁住的线程要超过50s以后才能超时退出,这往往是对线上的服务是无法接受的,但是又不能将这个参数设置成一个较小的值,因为虽然可以在出现死锁的时候很快可以解决,但是如果不是死锁,而是简单的锁等待就比较尴尬了,就是当两个事务再进行相互等待的时候,当一个事务等待时间超过设置的这个阈值的时候就进行将其回滚,另外一个事务再继续执行,这种方法简单有效
缺点:默认时间是50,对于线上服务来说这个时间显然是不可以接受的,如果将值设置的太短,那么可能会破坏到普通的锁等待 ,因为如果不是死锁的话,等待时间超过50s也会发生回滚
2)另一种策略就是,发起死锁检测,当MYSQL发现死锁以后,MYSQL服务器会主动进行回滚死锁链条上的某一个事务,将持有最少持有行级排他锁的事务进行回滚,将innodb_deadlock_detect设置成on,表示开启这个逻辑
十)乐观锁和悲观锁
1)悲观锁,本质上就是一种思想,就是很悲观,对数据被其他事务修改持保守态度,会通过数据库自身的锁机制来是实现,从而来保证数据操作的排他性
悲观锁总是假设最坏的情况,认为每一次拿数据的时候都会认为别人会修改,所以在每一次拿数据的时候都会上锁,同乐观锁不同,乐观锁还能拿到数据操作数据,但是会在县城提交数据的时候进行并发检测,但是悲观锁连这个数据都拿不到,这样子别人想拿这个数据就只能进行阻塞等待(共享资源只会给一个线程来使用,其他线程会处于阻塞状态,用完以后再把这个锁转给其他线程),比如说行锁表锁,读锁,写锁,都是在操作之前先上锁,其他线程想要访问数据的时候,都需要被阻塞挂起
案例1:假设现在在商品的秒杀过程中,库存数量减少,就会出现超卖的情况,比如说商品表中现在有一个字段叫做quantity表示当前该商品的库存量,假设现在华为商品,id是1001就是表示当前商品的编号,quantity是100个,如果不使用锁的情况下,那么操作的情况下如下图所示:
1)首先查询出商品库存:select quantity from items where id=1001 2)如果库存大于0,那么就生成订单,并且根据当前商品的信息生成订单: insert into orders(id) values(1001) where quantity>0 3)件商品的库存数量减去1 update item set quantity=quantity-1 where id=1001
上面的这种操作在低并发量情况下是没有任何问题的,但是如果是在高并发的秒杀场景下,就很有可能出现问题了
所以说现在解决的一个思路就是两个线程并发执行,假设现在有100台手机,两个线程并发执行执行订单,减库存操作,就出现了超卖的情况,所以说我们可以通过加锁的方式让两个线程串行执行
1)首先查询出商品库存:select quantity from items where id=1001 for update 2)如果库存大于0,那么就生成订单,并且根据当前商品的信息生成订单: insert into orders(id) values(1001) where quantity>0 3)件商品的库存数量减去1 update item set quantity=quantity-1 where id=1001
上面的id一定要加索引,否则行锁就变成表锁了,锁的是索引,没有索引就是表锁
select XXXX for update是MYSQL中的悲观锁,此时在items表中,id是1001的拿一条记录就被锁定了,其他事务想要去执行select quanitity from items where id=1001 for update,语句的事务必须等到本次事务提交之后才可以执行,这样就可以保证当前的数据不会被其他事务所修改,注意selectXXX for update语句在执行的过程中会把所有扫描的行都会锁上,因此MYSQL中使用悲观锁必须确定是使用到了索引,而不是全表扫描,否则会把整张表都锁住
悲观锁不适用的场景比较多,他存在着一些不足,因为悲观锁大多数情况下是依靠数据库的锁机制来进行实现的,来保证程序的并发访问性,同时这样对于数据库的性能开销影响也是特别大的,因此这个时候就需要乐观锁
2)乐观锁:在乐观锁中认为同一数据的并发操作不会发生,属于是小概率事件,不用每一次都对数据上锁,但是在更新的时候会进行判断一下在此期间有没有别人去更新过这条数据,也就是说不会采用数据库的锁机制,而是通过程序来进行实现,在程序上使用版本号机制和CAS来进行实现,乐观锁适用于多读的情况,这样子可以提高吞吐量,可以根据时间戳或者是版本号来进行判断当前拿到的数据是否是最新的
可能查的时候使用的是从机,修改的时候使用的是主机,希望查的时候也是用主机,可能会出现数据不一致的情况,防止因为主从复制造成超卖
十一)隐式锁和显示锁:
如果事务1新插入了一条记录,如果事务2能够访问到这条记录,如果事务2能够查询到这条记录,那么就是脏读的问题,如果事务2可以修改这一条记录,那么就是脏写的问题,这个饮隐式锁是延迟加载的,受到影响的时候,在受到别的事务访问的时候才加的,加他的目的就是为了防止别的事务在当前的事务没有结束的情况下访问导致并发问题
1)当事务1添加了一条记录以后事务1还没有结束,那么当前叶子节点新插入的数据的transactionID记录的就是事务1,所以说官方此时就称之为有了一个隐式锁,这个时候通过指令来查看表中有哪些锁是查看不到的,隐式的
2)此时现在的一个事务2进来了,想要去访问新插入的这一条记录,无论是读的情况也好,写的情况也好,发现insert语句记录的这个事务是活跃的,那么此时事务2是不可以操作这一条记录的,此时事务2会帮助事务1构建一个锁结构,里面的iswaitting是false,自己还会创建一个锁结构,iswaitting属性是true,相当于自己阻塞等待,此时事务1提交,事务2就将iswaiiting修改成false,于是事务2就可以操作这条记录了
3)隐式锁是延迟访问的,当有其他事务想要访问新插入的记录,此时就会加载隐式锁
select * from performance_schema.data_lock_waits\G,可以通过这个指令来查看锁
首先先执行这三步:
然后再进行开启一个事物,进行查询操作,加共享锁,此时由于隐式锁的作用,加锁失败
此时如果要是将事务1进行提交操作,此时事务2就会加锁成功
十二)全局锁:
十三)锁的内部结构:
1)锁所在的事务信息:无论是是行锁还是表锁,都是在事务的执行过程中生成的,哪一个事务生成了锁结构,这里就记录着哪一个事务的信息,锁所在的事务信息本质上是一个指针,通过这个指针就可以找到内存中有关于事务的更多的信息,比如说事务ID等
2)索引信息:对于行锁来说需要记录一下加锁的记录是属于哪一个索引的,这里也是一个指针
nbits:现在是针对于行锁来说的,那么此时这个锁可能是锁住了这个表的多行数据,又因为说当前生成的锁结构是针对于具体的页来说的,在一张表里面有100条记录,有的记录给锁住了,有的记录没有被锁住,需要记录这张表中有的记录锁住了,有的记录没有锁住,真正的记录这个事情是一堆比特位来做的
十四)锁的监控指标:
直接select *库名.表的名字即可