目录
4.3.4.插入意向锁(Insert Intention Lock)
1.什么是锁
MySQL中为了保证数据访问的一致性与有效性等功能,实现了锁机制,MySQL中的锁是在服务器层或者存储引擎层实现的。
2.锁用来解决什么问题
锁是用来解决并发事务的访问问题,我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时进行改动操作的情况下。
一个事务进行读取操作,另一个进行改动操作,我们前边说过,这种情况下可能发生脏读、不可重复读、幻读的问题。
2.1.脏读(select)
如果一个事务读取到了另一个未提交事务修改过的数据,我们就称发生了脏读现象。
假设现在有两个事务A、B:
- 假设现在A的余额是100,事务A正在准备查询Jay的余额
- 事务B先扣减Jay的余额,扣了10,但是还没提交
- 最后A读到的余额是90,即扣减后的余额
因为事务A读取到事务B未提交的数据,这就是脏读。
2.2.不可重复读(update)
同一个事务内,前后多次读取,读取到的数据内容不一致
假设现在有两个事务A和B:
- 事务A先查询Jay的余额,查到结果是100
- 这时候事务B 对Jay的账户余额进行扣减,扣去10后,提交事务
- 事务A再去查询Jay的账户余额发现变成了90
事务A被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
2.3.幻读(insert)
如果一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录(如insert、delete、update),就意味着发生了幻读。
假设现在有两个事务A、B:
- 事务A先查询id大于2的账户记录,得到记录id=2和id=3的两条记录
- 这时候,事务B开启,插入一条id=4的记录,并且提交了
- 事务A再去执行相同的查询,却得到了id=2,3,4的3条记录了。
事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入新的数据,并提交事务,然后事务A再次查询相同的范围,两次读取到的结果集却不一样了,这就是幻读。
3.锁的分类
3.1.使用方法
3.1.1.共享锁
共享锁,Share lock,也叫读锁。它是指当对象被锁定时,允许其它事务读取该对象,也允许其它事务从该对象上再次获取共享锁,但不能对该对象进行写入。
加锁方式:
# 方式1
select ... lock in share mode;
# 方式2
select ... for share;
如果事务T1 在某对象持有共享(S)锁,则事务T2 需要再次获取该对象的锁时,会出现下面两种情况:
- 如果T2 获取该对象的共享(S)锁,则可以立即获取锁;
- 如果T2 获取该对象的排他(X)锁,则无法获取锁;
3.1.2.排他锁
排它锁,Exclusive Lock,也叫写锁或者独占锁,主要是防止其它事务和当前加锁事务锁定同一对象。同一对象主要有两层含义:
- 当排他锁加在表上,则其它事务无法对该表进行insert,update,delete,alter,drop等更新操作;
- 当排他锁加在表的行上,则其它事务无法对该行进行insert,update,delete,alter,drop等更新操作;
加锁方式:
select ... for update;
4.加锁范围
4.1.全局锁
全局锁,顾名思义,就是对整个数据库实例加锁。它是粒度最大的锁。
4.1.1.加锁方式
flush tables with read lock
指令执行完,整个数据库就处于只读状态了,其他线程执行以下操作,都会被阻塞:
- 数据更新语句被阻塞,包括 insert, update, delete语句;
- 数据定义语句被阻塞,包括建表 create table,alter table、drop table 语句;
- 更新操作事务commit语句被阻塞;
4.1.2.释放锁
在MySQL中释放锁有2种方式:
1.执行指令
unlock tables
2.加锁的会话断开,全局锁也会被自动释放
4.1.3.使用场景
全局锁的典型使用场景是做全库逻辑备份,在备份过程中整个库完全处于只读状态。如下图:
- 假如在主库上备份,备份期间,业务服务器不能对数据库执行更新操作,因此涉及到更新操作的业务就瘫痪了;
- 假如在从库上备份,备份期间,从库不能执行主库同步过来的 binlog,会导致主从延迟越来越大,如果做了读写分离,那么从库上获取数据就会出现延时,影响业务;
从上述分析可以看出,使用全局锁进行数据备份,不管是在主库还是在从库上进行备份操作,对业务总是不太友好。那不加锁行不行?我们可以通过下面还钱转账的例子,看看不加锁会不会出现问题:
- 备份前:账户A 有1000,账户B 有500
- 此时,发起逻辑备份
- 假如数据备份时不加锁,此时,客户端A 发起一个还钱转账的操作:账户A 往账户B 转200
- 当账户A 转出200完成,账户B 转入200 还未完成时,整个数据备份完成
- 如果用该备份数据做恢复,会发现账户A 转出了200,账户B 却没有对应的转入记录,这样就会产生纠纷:A 说我账户少了 200, B 说我没有收到,最后,A,B谁都不干。
既然不加锁会产生错误,加全局锁又会影响业务,那么有没有两全其美的方式呢?
有,MySQL官方自带的逻辑备份工具 mysqldump,具体指令如下:
mysqldump –single-transaction
执行该指令,在备份数据之前会先启动一个事务,来确保拿到一致性视图, 加上 MVCC 的支持,保证备份过程中数据是可以正常更新。但是,single-transaction方法只适用于库中所有表都使用了事务引擎,如果有表使用了不支持事务的引擎,备份就只能用 FTWRL 方法。
4.2.表级锁
表锁就是对整张表加锁,包含读锁和写锁,由MySQL Server实现,表锁需要显示加锁或释放锁,具体指令如下:
# 给表加写锁
lock tables tablename write;
# 给表加读锁
lock tables tablename read;
# 释放锁
unlock tables;
4.2.1读锁
代表当前表为只读状态,读锁是一种共享锁。需要注意的是,读锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:
- 加锁线程只能对当前表进行读操作,不能对当前表进行更新操作,不能对其它表进行所有操作;
- 其它线程只能对当前表进行读操作,不能对当前表进行更新操作,可以对其它表进行所有操作;
4.2.2.写锁
写锁是一种独占锁,需要注意的是,写锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:
- 加锁线程对当前表能进行所有操作,不能对其它表进行任何操作;
- 其它线程不能对当前表进行任何操作,可以对其它表进行任何操作;
4.2.3.MDL元数据锁
DDL的写锁请求优先级高于DML
元数据锁:metadata lock,简称MDL,它是在MySQL 5.5版本引进的。元数据锁不用像表锁那样显式的加锁和释放锁,而是在访问表时被自动加上,以保证读写的正确性。
加锁和释放锁规则如下:
- MDL读锁之间不互斥,也就是说,允许多个线程同时对加了 MDL读锁的表进行CRUD(增删改查)操作;
- MDL写锁,它和读锁、写锁都是互斥的,目的是用来保证变更表结构操作的安全性。也就是说,当对表结构进行变更时,会被默认加 MDL写锁,因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
- MDL读写锁是在事务commit之后才会被释放;
当一个事务开始执行SELECT或UPDATE语句时,MySQL会自动获取相关表的共享元数据锁;而当一个事务开始执行CREATE TABLE或DROP TABLE语句时,则会自动获取相应表的独占元数据锁。在事务提交或回滚后,相应的元数据锁也会自动释放。
4.2.4.意向锁
当事务A有行锁时,MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。
意向锁为什么是表锁而不是行锁?
意向锁时表锁的话在遍历的时候只需要遍历一遍就可以,但是若时行锁则会需要一行一行遍历
意向锁,Intention lock,它是一种表锁,用来标识事务打算在表中的行上获取什么类型的锁。 不同的事务可以在同一张表上获取不同种类的意向锁,但是第一个获取表上意向排他(IX) 锁的事务会阻止其它事务获取该表上的任何 S锁 或 X 锁。反之,第一个获得表上意向共享锁(IS) 的事务可防止其它事务获取该表上的任何 X 锁。
意向锁通常有两种类型:
- 意向共享锁(IS),表示事务打算在表中的各个行上设置共享锁。
- 意向排他锁(IX),表示事务打算对表中的各个行设置排他锁。
意向锁是InnoDB自动加上的,加锁时遵从下面两个协议:
- 事务在获取表中行的共享锁之前,必须先获取表上的IS锁或更强的锁。
- 事务在获取表中行的排他锁之前,必须先获取表上的IX锁。
4.2.5.AUTO-INC锁
AUTO-INC锁是一种特殊的表级锁,当表中有AUTO_INCREMENT的列时,如果向这张表插入数据时,InnoDB会先获取这张表的AUTO-INC锁,等插入语句执行完成后,AUTO-INC锁会被释放。
AUTO-INC锁可以使用innodb_autoinc_lock_mode变量来配置自增锁的算法,
innodb_autoinc_lock_mode变量可以选择三种值如下表:
4.2.6.锁的兼容性
4.3.行锁
行锁只用在事务中才能使用,若是事务提交行锁也会自动解锁,行锁是加在索引上面的
行锁是针对数据表中行记录的锁。MySQL 的行锁是在引擎层实现的,并不是所有的引擎都支持行锁,比如,InnoDB引擎支持行锁而 MyISAM引擎不支持。
InnoDB 引擎的行锁主要有三类:
- Record Lock: 记录锁,是在索引记录上加锁;
- Gap Lock:间隙锁,锁定一个范围,但不包含记录;
- Next-key Lock:Gap Lock + Record Lock,锁定一个范围(Gap Lock实现),并且锁定记录本身(Record Lock实现)
4.3.1.记录锁(Record Lock)
Record Lock:记录锁,是针对索引记录的锁,锁定的总是索引记录,而不是真正的数据记录
例如,select id from user where id = 1 for update; for update 就显式在索引id上加行锁(排他锁),防止其它任何事务 update或delete id=1 的行,但是对user表的insert、alter、drop操作还是可以正常执行。
4.3.2.间隙锁(Gap Lock)
锁住的是一个区间,而不仅仅是这个区间中的每一条数据,是在可重复读的隔离级别下 为了解决幻读引入的锁机制
Gap Lock:间隙锁,锁住两个索引记录之间的间隙上,由InnoDB隐式添加。比如(1,3) 表示锁住记录1和记录3之间的间隙,这样记录2就无法插入,间隙可能跨越单个索引值、多个索引值,甚至是空。
4.3.3.临建锁(Next-Key Lock)
可以解决幻读的问题
注意:临建锁只与非唯一索引列有关,在唯一索引列上不存在临建锁
Next-Key锁,称为临键锁,它是Record Lock + Gap Lock的组合,用来锁定一个范围,并且锁定记录本身锁,它是一种左开右闭的范围,可以用符号表示为:(a,b]。
4.3.4.插入意向锁(Insert Intention Lock)
插入意向锁,它是一种特殊的间隙锁,特指插入操作产生的间隙锁。
4.3.5.临键锁在什么情况下会降级
4.3.5.1.唯一索引等值查询
- 当查询的记录是存在的,next-key lock 会退化成【记录锁】
- 当查询的记录是不存在的,next-key lock 会退化成【间隙锁】
4.3.5.2.非唯一索引等值查询
- 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
- 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
5.乐观锁&悲观锁
在MySQL中,无论是悲观锁还是乐观锁,都是人们对概念的一种思想抽象,它们本身还是利用 MySQL提供的锁机制来实现的。其实,除了在MySQL数据,像 Java语言里面也有乐观锁和悲观锁的概念。
- 悲观锁,可以理解成:在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking),采用的是先获取锁再操作数据的策略,可能会产生死锁;
- 乐观锁,是相对悲观锁而言,一般不会利用数据库的锁机制,而是采用类似版本号比较之类的操作,因此乐观锁不会产生死锁的问题;
6.死锁&死锁检测
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。可以通过下面的指令查看死锁
show engine innodb status\G
当出现死锁以后,有两种策略:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置,InnoDB 中 innodb_lock_wait_timeout 的默认值是 50s。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其它事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启死锁检测。