数据库为什么需要加锁呢?
如果有多个并发请求存取数据,在数据就可能会产生多个事务同时操作同一行数据。如果并发操作不加控制,不加锁的话,就可能写入了不正确的数据,或者导致读取了不正确的数据,破坏了数据的一致性。因此需要考虑加锁。
在 MySQL 中,无论是悲观锁还是乐观锁,都是人们对概念的一种思想抽象,它们本身还是利用 MySQL 提供的锁机制来实现的。其实,除了在 MySQL 数据,像 Java 语言里面也有乐观锁和悲观锁的概念
InnoDB 实现了两种标准的行级锁:共享锁(简称 S 锁)、排他锁(简称 X 锁)
select ... lock in share mode;
或者 select ... for share;
select ... for update;
如果事务 T1 持有行 R 的 S 锁,那么另一个事务 T2 请求访问这条记录时,会做如下处理:
如果 T1 持有行 R 的 X 锁,那么 T2 请求 R 的 X、S 锁都不能被立即允许,T2 必须等待 T1 释放 X 锁才可以,因为 X 锁与任何的锁都不兼容
S 锁和 X 锁的兼容关系如下图表格:
X 锁和 S 锁是对于行记录来说的话,可以称它们为行级锁或者行锁。我们认为行锁的粒度就比较细,其实一个事务也可以在表级别下加锁,对应的,我们称之为表锁。给表加的锁,也是可以分为 X 锁和 S 锁的
如果一个事务给表已经加了 S 锁,则:
如果一个事务给表加了 X 锁,那么:
全局锁:对整个数据库实例加锁。它是粒度最大的锁
①:加锁:在 MySQL 中,通过执行以下指令加全局锁:
flush tables with read lock
指令执行完,整个数据库就处于只读状态了,其他线程执行以下操作,都会被阻塞:
②:释放锁: MySQl 释放锁有 2 种方式:
unlock tables
命令③:使用场景
全局锁的典型使用场景是做全库逻辑备份,在备份过程中整个库完全处于只读状态
问题:
- 假如在主库上备份,备份期间,业务服务器不能对数据库执行更新操作,因此涉及到更新操作的业务就瘫痪了;
- 假如在从库上备份,备份期间,从库不能执行主库同步过来的 binlog,会导致主从延迟越来越大,如果做了读写分离,那么从库上获取数据就会出现延时,影响业务
使用全局锁进行数据备份,不管是在主库还是在从库上进行备份操作,对业务总是不太友好
不加锁会产生错误,加全局锁又会影响业务,那么有没有两全其美的方式呢?
有:MySQL 官方自带的逻辑备份工具 mysqldump,具体指令如下:
mysqldump –single-transaction
执行该指令,在备份数据之前会先启动一个事务,来确保拿到一致性视图, 加上 MVCC 的支持,保证备份过程中数据是可以正常更新。但是,single-transaction 方法只适用于库中所有表都使用了事务引擎,如果有表使用了不支持事务的引擎,备份就只能用 FTWRL 方法
MySQL 表级锁有四种:
表锁就是对整张表加锁,包含读锁和写锁,由 MySQL Server 实现。
大多数情况下,锁的管理(包括获取和释放)都是由 MySQL 系统自动处理的,用户不需要显式地进行操作。这主要是在执行查询或更新语句时发生的。然而,有时候可能需要显式地进行锁定操作。
表锁需要显示加锁或释放锁,具体指令如下:
# 给表加写锁
lock tables tablename write;
# 给表加读锁
lock tables tablename read;
# 释放锁
unlock tables;
# 查看数据表上增加的锁
show open tables;
读锁:代表当前表为只读状态,读锁是一种共享锁
写锁:写锁是一种独占锁
特点:
那什么时候会使用表锁呢?
元数据锁:metadata lock,简称 MDL,它是在 MySQL 5.5 版本引进的。元数据锁是在访问表时被自动加上,以保证读写的正确性。
加锁和释放锁规则如下:
那什么时候会使用元数据锁这个表级锁呢?
当我们对一个表做增删改查操作的时候,会加上 MDL 读锁;当我们要对表结构做变更时,就会加 MDL 写锁
意向锁是一种不与行级锁冲突的表级锁(本质上就是空间换时间)。未来的某个时刻,事务可能要加共享或者排它锁时,先提前声明一个意向
为什么要加共享锁或排他锁时的时候,需要提前声明个意向锁呢呢?
因为 InnoDB 是支持表锁和行锁共存的。如果一个事务 A 获取到某一行的排他锁,并未提交,这时候事务 B 请求获取同一个表的表共享锁。由于共享锁和排他锁是互斥的,因此事务 B 想对这个表加共享锁时,需要保证没有其他事务持有这个表的表排他锁,同时还要保证没有其他事务持有表中任意一行的排他锁
然后问题来了:你要保证没有其他事务持有表中任意一行的排他锁的话,去遍历每一行?这样显然是一个效率很差的做法。为了解决这个问题,InnoDB 的设计者提出了意向锁
意向锁是如何解决这个问题的呢?来看下。
意向锁(InnoDB 自动加上的)分为两类:
意向锁又是如何解决这个效率低的问题呢?
如果一个事务 A 获取到某一行的排他锁,并未提交,这时候表上就有意向排他锁和这一行的排他锁。这时候事务 B 想要获取这个表的共享锁,此时因为检测到事务 A 持有了表的意向排他锁,因此事务 A 必然持有某些行的排他锁,也就是说事务B对表的加锁请求需要阻塞等待,不再需要去检测表的每一行数据是否存在排他锁啦。这样效率就高很多啦(所以当我们需要判断这个表的记录有没有被加锁时,直接判断意向锁就可以了,减少了遍历的时间,提高了效率,是典型的用空间换时间的做法)。
意向锁仅仅表明意向的锁,意向锁之间并不会互斥,是可以并行的,整体兼容性如下图所示:
那么什么时候会用到意向锁呢?
对表中的行记录加锁的时候,就会用到意向锁
自增锁是一种特殊的表级别锁。 它是专门针对 AUTO_INCREMENT
类型的列,对于这种列,如果表中新增数据时就会去持有自增锁。简言之,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值
AUTO-INC 锁可以使用 innodb_autoinc_lock_mode
变量来配置自增锁的算法,·innodb_autoinc_lock_mode· 变量可以选择三种值如下:
INSERT-LIKE
语句在语句执行结束后释放 AUTO_INC 表级锁,而不是在事务结束后释放Simple inserts
不会使用表级锁,而是使用一个轻量级锁 mutex
来生成自增值,因为 InnoDB 可以提前直到插入多少行数据。自增值生成阶段使用轻量级互斥锁来生成所有的值,而不是一直加锁直到插入完成。对于 bulk inserts
类语句使用 AUTO_INC 表级锁直到语句完成INSERT-LIKE
语句都不使用表级锁,而是使用轻量级互斥
- INSERT-LIKE:指所有的插入语句,包括:INSERT、REPLACE、INSERT…SELECT、REPLACE…SELECT,LOAD DATA等
- Simple inserts:指在插入前就能确定插入行数的语句,包括:INSERT、REPLACE,不包含INSERT…ON DUPLICATE KEY UPDATE这类语句
- Bulk inserts: 指在插入钱不能确定行数的语句,包括:INSERT … SELECT/REPLACE … SELECT/LOAD DATA
行锁是针对数据表中行记录的锁。MySQL 的行锁是在存储引擎层实现的,并不是所有的引擎都支持行锁,比如,InnoDB 引擎支持行锁而 MyISAM 引擎不支持
特点:
InnoDB 引擎的行锁主要有四类:
那么什么时候会使用行级锁呢?
当增删改查匹配到索引时,Innodb 会使用行级锁;如果没有匹配不到索引,那么就会直接使用表级锁
注意:
select ... lock in share mode
命令来指定共享锁,使用 select ... for update;
来指定排他锁记录锁:针对索引记录的锁,锁定的总是索引记录
如下 SQL:
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
如果 c1 字段是主键或者是唯一索引的话,这个 SQL 就会在 c1 字段上显示地加一个排他型的 记录锁(Record Lock)。 防止其它任何事务 update
或 delete id=1
的行,但是对 user 表的 insert、alter、drop 操作还是可以正常执行
当 SQL 语句无法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁,再由 MySQL Server 层进行过滤。但是,在 MySQL Server 层进行过滤的时候,如果发现不满足 WHERE 条件,会释放对应记录的锁。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的
所以,更新/删除操作必须要根据索引进行操作,没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,还会极大的降低了数据库的并发性能
记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB 也会隐式的创建一个索引,并使用这个索引实施记录锁。它会阻塞其他事务对这行记录的更新、删除
背景:
在MySQL中使用范围查询时,如果请求共享锁或排他锁,InnoDB 会给符合条件的已有数据的索引项加锁。如果键值在条件范围内, 而这个范围内并不存在记录,则认为此时出现了“间隙(也就是 GAP)”。InnoDB存储引擎会对这个“间隙”加锁,而这种加锁机制就是间隙锁(GAP Lock)
间隙锁:一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。这个间隙可以跨一个索引记录,多个索引记录,甚至是空的。它锁住的是一个区间
使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象
表数据如下:
SQL 语句:
update userinfo set balance = balance + 100 where id > 5 and id <16
解析:
数据表中的间隙包括id为 (3,15)、(15,20)、 (20,正无穷) 的三个区间,执行 update userinfo set balance = balance + 100 where id > 5 and id <16;
则其他事务无法在 (3,20) 这个区间内插入或者修改任何数据
间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入,所以加间隙 S 锁和加间隙 X 锁没有任何区别
间隙锁只有在可重复读事务隔离级别下才 会生效
Next-key 锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如 (5,10]
假设一个索引包含:15、18、20 ,30,49,50
可能的 Next-key 锁如下:
插入意向锁:插入一行记录操作之前设置的一种间隙锁。这个锁释放了一种插入方式的信号。它解决的问题是:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,就不会阻塞彼此
插入意向锁和插入意向锁之间互不冲突
id = 30 和 id = 49 之间如果有两个事务要同时分别插入 id = 32 和 id = 33 是没问题的,虽然两个事务都会在 id = 30 和 id = 50 之间加上插入意向锁,但是不会冲突
插入意向锁只会和间隙锁或 Next-key 锁冲突
间隙锁唯一的作用就是防止其他事务插入记录造成幻读。由于在执行 INSERT 语句时需要加插入意向锁,插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
注意:
页级锁:页面级别对数据进行加锁和释放锁。对数据的加锁开销介于表锁和行锁之间,可能会出现死锁,锁定的粒度大小介于表锁和行锁之间,并发度一般