5.1Mysql中,有哪些锁?
在Mysql中,根据加锁的范围,可以分成全局锁、表级锁、行锁三类。
【全局锁】
flush tables with read lock
这条命令执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:<br />(1)对数据的增删改操作,比如insert、delete、update等语句。<br />(2)对表结构的更改操作,比如alter table、drop table等语句。<br />如果要释放全局锁,需要执行以下命令
unlock tables
全局锁的应用场景:全局锁主要应用于做全库备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期不一致的问题。<br />备份数据库数据的时候,使用全局锁会影响业务,有什么其他方式可以避免?<br />如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建Read View,然后整个事务执行期间都在用这个Read View,并且由于MVCC的支持,备份期间业务依然可以对数据进行更新操作。<br />因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的Read View,这样备份期间备份的数据一直是在开启事务时的数据。<br />备份数据库的工具是mysqldump,在使用mysqldump时加上-single-transaction参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持可重复读隔离级别下的事务的存储引擎。<br />InnoDB存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。但是对于MyIsam不支持事务的引擎,在备份数据库时就必须使用全局锁的方式了。<br />【表级锁】<br />(1)表锁
lock tables t_student read; //表级别的读锁
lock tables t_student write;//表级别的写锁
unlock tables //释放当前会话所有表锁
需要注意的是,表锁除了会限制别的线程的读写外,也会限制本线程的读写操作。举个例子:如果当前线程对t_student表加了共享锁,那么除了其他线程对学生表进行写操作时会被阻塞,当前线程对学生表执行写操作也是会被阻塞的。
(2)元数据锁:我们不需要显示的使用MDL,因为当我们对数据库表进行操作时,会自动给这个表加上MDL。
对一张表进行CRUD操作时,加的是MDL读锁;
对一张表进行表结构的更改时,加的是MDL写锁;
MDL是为了保证当用户对表执行CRUD操作时,防止其他线程对这个表结构做了变更。
举个例子:
(1)当有线程执行select语句的时候,会对当前的数据库表加MDL读锁,如果有其他线程要更改变的结构(申请MDL写锁)的时候就会被阻塞住,直到当前线程执行完select语句,释放了MDL读锁后。
(2)如果当前线程正在执行alter table的语句对表结构进行变更时(加MDL)写锁,如果期间有其他线程执行CRUD操作(申请MDL读锁),那么就会被阻塞住,直到表结构变更完成,释放了MDL写锁。
MDL不需要显示调用,它在事务提交后才会释放,这就意味着事务执行期间,MDL是一直持有的。
申请MDL读的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现MDL写锁等待,会阻塞后续该表所有的CRUD操作。所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看是否有事务已经对表加了MDL读锁,如果可以考虑kill掉这个长事务,然后再对做表结构的变更。
(3)意向锁
(1)一个事务在获取任意一行或多行记录的S锁之前,需要在表级别加上一个意向共享锁。
(2)一个事务在获取任意一行或多行记录的X锁之前,需要先在表级别上一个意向排他锁。
也就是说当一个执行对表中的某个数据记录执行update、delete、insert操作的时候,需要先对表加上意向排他锁,然后再对该记录加排他锁。而普通的select是不会加行级锁的,普通的select语句利用MVCC实现一致性读,是无锁的。
意向共享锁和意向排它锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突。只和共享表锁和独占表锁发生冲突(lock tables…read 和 lock tables…write)。
如果没有意向锁,那么在加独占表锁的时候,就需要遍历表里所有记录,查看是否有记录存在独占锁。这样效率就很慢。
如果有了意向锁,由于对记录加排它锁之前,先会在表级别加意向排他锁,那么在加独占表锁时,就可以直接查看该表是否有意向独占锁,如果有就意味着表里已经有记录被加了排它锁,这样就不用遍历表里的所有记录。
所以,意向锁的目的就是为了快速判断表里是否有记录被加了锁。
(4)AUTO-INC锁
在为某个字段声明AUTO_INCREAMTN属性后,之后在插入数据时,可以不指定该字段的值,数据库会自动会该字段赋值递增的值,这主要也是通过AUTO-INC锁实现的,比如最常见的自增主键id。
AUTO-INC锁是特殊的表锁机制,锁不再是一个事务提交后才释放,而是执行完插入语句后就会立即释放。
在插入数据时,会加一个表级别的AUTO-INC锁,然后为被修饰的字段赋予递增的值,等插入语句执行完成后,就会把锁释放掉。
【行级锁】
InnoDB引擎是支持行级锁的,而MyIsam引擎是不支持行级锁的。
行级锁的类型主要有三类:
(1)Record Lock:记录锁,也就是仅仅把一条记录锁上;
(2)Gap Lock:间隙锁,锁定一个范围,不包含记录本身;
(3)Next-Key-Lock:记录锁+间隙锁的组合,锁定一个范围,并且锁定记录本身。
普通的select语句是不会对记录加锁的,如果在查询时需要对记录加行级锁,可以使用以下两种方式:
select … lock in share mode;
select … for update;
5.2Mysql是怎么加行级锁的?
加锁规则,两个原则、两个优化和一个Bug。
(1)原则1:加锁的基本单位是next-key-lock,next-key-lock是前开后闭的区间。
(2)原则2:查找过程中访问到的对象才会加锁。
(3)优化1:索引上的等值查询,给唯一索引加的锁,next-key-lock退化成行锁。
(4)优化2:索引上的等值查询,向右遍历到最后一个值不满足等值条件的时候,next-key-lock退化成间隙锁。
(5)bug:唯一索引上的范围查询会访问到不满足条件的第一个值位置。
【唯一索引上的等值查询】:
(1)锁记录存在
分析:
(1)加锁的基本单位是next-key-lock,所以加锁的范围是(8,16];
(2)但由于是用唯一索引进行等值查询,且查询的记录存在,所以next-key-lock退化成行锁,因此最终锁只会加在id=16这一行。
所以会话2会被锁住,会话3能够正常执行。
(2)如果锁记录不存在
分析:
(1)加锁的基本单位是next-key-lock,因为加锁的范围是(8,16]
(2)索引上的等值查询,遍历到不满足等值条件的时候会退化成间隙锁,因此加锁的范围是(8,16)。
所以insert id=9的这条记录被锁住,update id=16可以正常执行。
【唯一索引上的范围查询】:
虽然这两句话的查询结果是一样的,但是加锁范围是不一样的。
语句1根据加锁规则会退化成行锁,只会在id=8的这条记录上加行锁。
分析语句2:
(1)最开始要找的行的id=8,因此next-key-lock(4,8],id是唯一索引,记录存在,所以退化成行锁,加在id=8的这条记录上,
(2)由于是范围查找,会继续往后找存在的记录,找到id=16这一行停下来,然后加(8,16],但由于id=16不满足id<9,所以会退化成间隙锁,加锁范围是(8,16)。
【非唯一索引上的等值查询】:
当查询的记录存在时,除了会在非唯一索引上加
分析加锁过程:
(1)先对普通索引b加上next-key-lock,范围是(4,8]
(2)因为是非唯一索引,且查询记录存在,所以还会继续向右遍历到不符合条件的16,加上间隙锁(8,16],因为16不符合等值条件,所以退化成间隙锁(8,16)。
(2)如果锁记录不存在
加锁分析:加next-key-lock(8,16],但是由于查询的记录是不存在的,所以会退化成间隙锁。最终加锁范围是(8,16)。
【非唯一索引上的范围查询】:
加锁分析:
(1)加next-key-lock(4,8],不会退化成行锁或间隙锁。
(2)继续往右遍历,加锁(8,16]。由于是范围查询所以不会退化成间隙锁。
5.3死锁
因为事务A在select..for update的时候会加next-key-lock(1006,正无穷],事务B也加的是(1006,正无穷]。所以事务A插入orderId为1007的数据记录会被事务B的next-key-lock挡住,事务B插入orderId为1008的数据记录会被事务A的next-key-lock挡住,这样就形成了死锁的局面。
【如何避免死锁】?
死锁形成的四个必要条件:
(1)互斥:同一时间只能有一个线程抢占到了资源。
(2)不可剥夺条件:一个线程已经占用的资源,在释放之前不会被其他线程抢占。
(3)请求和保持条件:线程等待获取资源的过程中不会释放已占有的资源
(4)循环等待条件:多个线程互相等待对方释放资源。
在数据库层面,有两种策略通过打破环迅等待条件来解除死锁状态:
(1)设置事务等待锁的超时时间:当一个事务的等待时间超过该值后,就会这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在InnoDBZhong,参数innodb_lock_wait_timeout是用来设置超时时间的,默认是50s。
(2)主动开启死锁检测:主动死锁监测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,就可以开启这个逻辑了。另外Innodb会选择将回滚代价小的事务进行回滚。