目录
5.4. 插入意向锁(Insert Intention Lock)
? ? 锁是计算机用以协调多个进程间并发访问同一共享资源的一种机制。MySql 中为了保证数据访问的一致性与有效性等功能,实现了锁机制,MySql 中的锁是在服务器层或者存储引擎层实现的。
? ? 在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改一部分数据时,MySql 会通过锁定防止其他用户读取同一数据。
一条 update 语句执行流程:
MySql 中的锁有很多,按照模式、粒度等可以分为如下几种类型:
共享锁 Shared Locks(S 锁,也叫读锁):当事务对数据加上读锁后,其他事务只能对该数据加读锁,不能加写锁。为了方便理解,下文我们全部使用读锁来称呼加了读锁的记录。
加锁方式:
# 方式1
select ... lock in share mode;
# 方式2
select ... for share;
如果事务 T1 在某对象持有共享(S)锁,则事务 T2 需要再次获取该对象的锁时,会出现下面两种情况:
举例:
? ? 排他锁?Exclusive Locks(X 锁,也叫写锁或独占锁):主要是防止其它事务和当前加锁事务锁定同一对象。同一对象主要有两层含义:
加锁方式:select…for update
MySql InnoDB 引擎默认 insert、update、delete 都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁类型。
举例:
? ? 全局锁,顾名思义,就是对整个数据库实例加锁,它是粒度最大的锁。当加全局锁时,该数据库下面所有的表都处于只读状态,不管是当前事务还是其他事务,对于库下面所有的表只能读,不能执行 insert、update、delete、alter、drop 等更新操作。
全局锁加锁方式为(FTWRL):
flush tables with read lock;
指令执行完,整个数据库就处于只读状态了,其他线程执行以下操作,都会被阻塞:
全局锁释放方式为:
方法一:执行 unlock tables
方法二:加锁的会话断开,全局锁也会被自动释放
使用场景:
全局锁的典型使用场景是做全库逻辑备份,在备份过程中整个库完全处于只读状态。如下图:
使用全局锁进行数据备份,不管是在主库还是在从库上进行备份操作,对业务总是不太友好。那不加锁行不行?我们可以通过下面还钱转账的例子,看看不加锁会不会出现问题:
既然不加锁会产生错误,加全局锁又会影响业务,那么有没有两全其美的方式呢?
有,MySql 官方自带的逻辑备份工具 mysqldump,具体指令如下:
mysqldump –single-transaction
执行该指令,在备份数据之前会先启动一个事务,来确保拿到一致性视图, 加上 MVCC 的支持,保证备份过程中数据是可以正常更新。但是,single-transaction 方法只适用于库中所有表都使用了事务引擎,如果有表使用了不支持事务的引擎,备份就只能用 FTWRL 方法。
MySql 表级锁有两种:表锁、元数据锁(Metadata Lock,MDL)。
? ? 表锁就是对整张表加锁,由 MySQL Server 实现,行锁则是存储引擎实现,不同的引擎实现的不同。在 MySql 的常用引擎中 InnoDB 支持行锁,而 MyISAM 则只能使用 MySQL Server 提供的表锁。
表锁的特点:
表锁包含读锁和写锁,且需要显示加锁或释放锁,具体指令如下:
-- 给表加写锁
lock tables tablename write;
-- 给表加读锁
lock tables tablename read;
-- 释放锁
unlock tables;
除了使用 unlock tables 显示释放锁之外,会话持有其他表锁时执行 lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行 start transaction 或者 begin 开启事务时,也会释放之前持有的锁。
表读锁:代表当前表为只读状态,读锁是一种共享锁。需要注意的是,读锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:
表写锁:写锁是一种独占锁,需要注意的是,写锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:
实例演示:
1. 加表锁
2. 解除表锁:
第一步:找出被锁的表(show processlist)
第二步:kill 掉锁表的进程
kill 21;
kill 22;
再次更新 user 表数据
发现可以可以正常更新了。
? ? 元数据锁:Metadata Lock,简称 MDL,它是在 MySQL 5.5 版本引进的。元数据锁不用像表锁那样显式的加锁和释放锁,而是在访问表时被自动加上,以保证读写的正确性。加锁和释放锁规则如下:
? ? AUTO-INC 锁是一种特殊的表级锁,当表中有 AUTO_INCREMENT 的列时,如果向这张表插入数据时,InnoDB 会先获取这张表的 AUTO-INC 锁,等插入语句执行完成后,AUTO-INC 锁会被释放。
? ? AUTO-INC 锁可以使用 innodb_autoinc_lock_mode 变量来配置自增锁的算法,innodb_autoinc_lock_mode 变量可以选择三种值如下表:
innodb_autoinc_lock_mode | 含义 |
---|---|
0 | 传统锁模式,采用 AUTO-INC 锁 |
1 | 连续锁模式,采用轻量级锁 |
2 | 交错锁模式(MySQL8默认),AUTO-INC和轻量级锁之间灵活切换 |
锁的兼容性:
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
? ? 页级锁是 MySql 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 引擎支持页级锁。
? ? 行锁是针对数据表中行记录的锁。MySql 的行锁是在引擎层实现的,并不是所有的引擎都支持行锁,比如,InnoDB 引擎支持行锁而 MyISAM 引擎不支持。
行锁的特点:
InnoDB 引擎的行锁主要有4类:
? ? 行锁是作用在 索引 上的,哪怕你在建表的时候没有定义一个索引,InnoDB 也会创建一个聚簇索引并将其作为锁作用的索引。
? ? 每一个 InnoDB 表都需要一个聚簇索引,有且只有一个。如果你为该表定义一个主键,那么MySql 将使用主键作为聚簇索引;如果你没有定义主键,那么 MySql 将会把第一个唯一索引(而且要求 NOT NULL)作为聚簇索引;如果上诉两种情况都不满足,那么 MySql 将自动创建一个名字为 GEN_CLUST_INDEX 的隐藏聚簇索引。
? ? 因为是聚簇索引,所以 B+树上的叶子节点都存储了数据行,那么如果现在是二级索引呢?InnoDB 中的二级索引的叶节点存储的是主键值(或者说聚簇索引的值),所以通过二级索引查询数据时,还需要将对应的主键去聚簇索引中再次进行查询。
更新单行记录的加锁原理:
update user set age = 10 where id = 49;
update user set age = 10 where name = 'Tom';
更新多行记录的加锁原理:
update user set age = 10 where id > 49;
示例:
? ? Record Lock:记录锁,是针对索引记录的锁,锁定的总是索引记录。行锁是加在索引上的,如果当你的查询语句不走索引的话,那么它就会升级到表锁,最终造成效率低下,所以在写 SQL 语句时需要特别注意。
? ? 例如,select id from user where id = 1 for update; for update 就显式在索引 id 上加行锁(排他锁),防止其它任何事务 update 或 delete id=1 的行,但是对 user 表的 insert、alter、drop 操作还是可以正常执行。
? ? Gap Lock:间隙锁,锁住两个索引记录之间的间隙,由 InnoDB 隐式添加。比如(1,3)表示锁住记录1和记录3之间的间隙,这样记录2就无法插入,间隙可能跨越单个索引值、多个索引值,甚至是空。间隙锁只是锁住间隙内部的范围,在间隙外的 insert/update 操作不会受影响。
? ? 间隙锁是锁在两个存在的索引之间,是一个开区间。间隙锁是可以共存的,共享间隙锁与独占间隙锁之间是没有区别的,两者之间并不冲突。其存在的目的都是防止其他事务往间隙中插入新的记录,故而一个事务所采取的间隙锁是不会去阻止另外一个事务在同一个间隙中加锁的。
示例:
select * from user where id < 10 for update;
即所有在 [1,10)区间内的记录行都会被锁住,所有 id 为 1、2、3、4、5、6、7、8、9 的数据行的插入会被阻塞。
? ? Next-Key 锁,称为临键锁,它是 Record Lock + Gap Lock 的组合,用来锁定一个范围,并且锁定记录本身锁,它是一种左开右闭的范围,可以用符号表示为:(a,b]。
? ? 当我们使用范围条件而不是相等条件去检索,并请求锁时,InnoDB 就会给符合条件的记录的索引项加上锁;而对于键值在条件范围内但并不存在(参考上面所说的空闲块)的记录,就叫做间隙,InnoDB 在此时也会对间隙加锁。
? ? Next-Key Lock 被用来解决可重复读隔离级别下?幻读?的现象。
? ? 插入意向锁,它是一种特殊的间隙锁,特指插入操作产生的间隙锁。如果多个事务 INSERT 到同一个索引间隙之间,但没有在同一位置上插入,则不会产生任何的冲突。
? ? 假设有值为 4 和 7 的索引记录,现在有两事务分别尝试插入值为 5 和 6 的记录,在获得插入行的排他锁之前,都使用插入意向锁锁住 4 和 7 之间的间隙,但两者之间并不会相互阻塞,因为这两行并不冲突。
? ? 插入意向锁 只会和 间隙锁 或者 Next-key锁 冲突,正如上面所说,间隙锁作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
? ? 由于表锁和行锁虽然锁定范围不同,但是会相互冲突。当你要加表锁时,势必要先遍历该表的所有记录,判断是否有排他锁,这种遍历检查的方式显然是一种低效的方式,为了快速的判断表中是否存在行锁,MySql 引入了意向锁,来检测表锁和行锁的冲突。
? ? 意向锁,Intention Lock,它是一种表锁,用来标识事务打算在表中的行上获取什么类型的锁。 不同的事务可以在同一张表上获取不同种类的意向锁,但是第一个获取表上意向排他锁(IX)的事务会阻止其它事务获取该表上的任何 S锁 或 X锁。反之,第一个获得表上意向共享锁(IS)的事务可防止其它事务获取该表上的任何 X锁。
意向锁通常有两种类型:
意向锁是 InnoDB 自动加上的,加锁时遵从下面两个协议:
意向锁之间是不会产生冲突的,它只会阻塞表级读锁或写锁。意向锁不与行级锁发生冲突。下表是各种锁之间的兼容情况:
排他锁 X | 写意向锁 IX | 共享锁 S | 读意向锁 IS | |
---|---|---|---|---|
排他锁 X | ||||
写意向锁 IX | 兼容 | 兼容 | ||
共享锁 S | 兼容 | 兼容 | ||
读意向锁 IS | 兼容 | 兼容 | 兼容 |
注意:上面的 X 与 S 是说表级的 X锁和 S锁,意向锁不和行级锁发生冲突。
如果一个事务请求的锁模式与当前的锁兼容,InnoDB 就将请求的锁授予该事务;如果两者不兼容,那么该事务就需要等待锁的释放。
? ? 乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。实现方式:乐观锁一般会使用版本号机制或 CAS 算法实现。
乐观锁适合读取频繁的场景。
使用乐观锁
乐观锁思想就是,有线程过来,先放过去修改,如果正式更新时确认记录没被其他线程修改过,就可以修改成功,如果已被其他线程修改过,就修改失败或者重试。
实现方式:一般会使用「版本号机制」或「CAS算法」实现。这里的版本号可以是一个数字(每次更新时比较版本号,更新的时候同步更新版本号+1),也可以是该条记录的最后一次更新时间戳(每次更新时比较时间戳,更新的时候同步更新这个时间戳)。
? ? 悲观锁她专一且缺乏安全感了,她的心只属于当前事务,每时每刻都担心着它心爱的数据可能被别的事务修改,所以一个事务拥有(获得)悲观锁后,其他任何事务都不能对数据进行修改,只能等待锁被释放才可以执行。
悲观锁适合写入频繁的场景。
使用悲观锁
悲观锁思想就是,当前线程要进来修改数据时,别的线程都得拒之门外。
实现方式:可以使用 select…for update
select * from User where name=‘jay’ for update
以上这条 sql 语句会锁定了 User 表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前,别的线程都无法修改这些记录。
? ? 在 MySql 中,无论是悲观锁还是乐观锁,都是人们对概念的一种思想抽象,它们本身还是利用 MySql 提供的锁机制来实现的。
悲观锁,可以理解成:在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking),采用的是先获取锁再操作数据的策略,可能会产生死锁;
乐观锁,是相对悲观锁而言,一般不会利用数据库的锁机制,而是采用类似版本号比较之类的操作,因此乐观锁不会产生死锁的问题;
示例:
假设有 A、B 两个用户同时各购买一件 id=1 的商品,用户 A 获取到的库存量为 1000,用户 B 获取到的库存量也为 1000,用户 A 完成购买后修改该商品的库存量为 999,用户 B 完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。有两种解决方案:
悲观锁方案:每次获取商品时,对该商品加排他锁。也就是在用户 A 获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录:
begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;
乐观锁方案:每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新:
select * from goods where id = 1;
begin;
-- 更新 stock 值,这里需要注意 where 条件 stock = cur_stock,只有程序中获取到的库存量与数据库中的库存量相等才执行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;
1. 意向锁是 InnoDB 自动加的,不需要用户干预。
2. 对于 INSERT、UPDATE 和 DELETE 语句,InnoDB 会自动给涉及的数据集加上排他锁。
3. 对于普通的 SELECT 语句,InnoDB 不会加任何锁,事务可以通过以下语句显示给记录集添加共享锁或排他锁:
? ? 在执行这个 select 查询语句的时候,会将对应的索引访问条目加上排他锁(X锁),也就是说这个语句对应的锁就相当于 update 带来的效果。
? ? 使用场景:为了确保自己查找到的数据一定是最新数据,并且查找到的数据值允许自己来修改,此时就需要用到 select for update 语句。
? ? 性能分析:select for update 语句相当于一个 update 语句。在业务繁忙的情况下,如果事务没有及时地 commit 或者 rollback 可能会造成事务长时间的等待,从而影响数据库的并发使用效率。
? ? in share mode 子句的作用就是将查找的数据加上一个 share 锁,这个就是表示其他的事务只能对这些数据进行简单的 select 操作,而不能进行 DML 操作。
? ? 使用场景:为了确保自己查询的数据不会被其他事务正在修改,也就是确保自己查询到的数据是最新的数据,并且不允许其他事务来修改数据。与 select for update 不同的是,本事务在查找完之后不一定能去更新数据,因为有可能其他事务也对同数据集使用了 in share mode 的方式加上了 S锁。
? ? 性能分析:select lock in share mode 语句是一个给查找的数据上一个共享锁(S 锁)的功能,它允许其他的事务也对该数据上 S锁,但是不允许对该数据进行修改。如果不及时的 commit 或者rollback 也可能会造成大量的事务等待。
可以通过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0 |
| InnoDB_row_lock_time | 0 |
| InnoDB_row_lock_time_avg | 0 |
| InnoDB_row_lock_time_max | 0 |
| InnoDB_row_lock_waits | 0 |
+-------------------------------+-------+
5 rows in set (0.01 sec)
? ? 当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
可以通过下面的指令查看死锁:
show engine innodb status\G
? ? MyISAM 表锁是 deadlock free 的,这是因为 MyISAM 总是一次性获得所需的全部锁,要么全部满足要么等待,因此不会出现死锁。但是在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,这就决定了 InnoDB 发生死锁是可能的。
要避免死锁,一般就是切断环路,尽量避免并发形成环路:
查看死锁日志 show engine innodb status
找出死锁 sql
分析 sql 加锁情况
找出死锁原因
制定解决方案