锁种类(Innodb)
通过锁粒度划分:行锁,间隙锁,表锁
- 行级锁
- 行锁是作用在索引上的
- InnoDB行锁是通过给索引上的索引项加锁来实现的, 这也就意味着,只有通过索引条件检索的数据,Innodb才使用行锁,否则,innodb将使用表锁
- 也不是说表锁,innodb在RR的隔离级别下,会加间隙锁。如果没有索引,会锁住所有行 + 所有间隙,相当于锁住了整个表
- 行锁的产生和释放(两阶段锁协议)
- 行锁是在需要时被加上(比如事务中执行update语句,那么从这条update语句后加上行写锁)
- 事务结束后才释放(并非执行完update就释放了)
- 两阶段锁协议测试
session A
中begin; select * from tb1 where id = 1 for update
(加排他锁)commit;
session B
中select * from tb1 where id = 1
,正常读,不阻塞select * from tb1 where id = 1 lock in share mode
, 加共享锁,阻塞select ... for update/ update/delete
排他锁,阻塞- 阻塞状态下,session A commit后释放锁,session B正常执行
- 锁相关配置参数
Innodb_row_lock_current_waits
Innodb_row_lock_time:
锁等待总时长Innodb_row_lock_time_avg:
锁等待平均时长Innodb_row_lock_time_max
Innodb_row_lock_waits
: 锁等待总次数
- InnoDB行锁是通过给索引上的索引项加锁来实现的, 这也就意味着,只有通过索引条件检索的数据,Innodb才使用行锁,否则,innodb将使用表锁
- 行锁是作用在索引上的
- 间隙锁Gap
- 间隙锁的出现是为了解决RR隔离级别,非唯一索引下的幻读问题,至于Innodb隔离级别和幻读,在事务章节中有详解
- 幻读,简单点说,在可重复读的隔离级别下,一个事务A中
update....a = "xxx"
了一行数据,在事务未提交的情况下,另一个事务Binsert a = "xxx"
的一条数据,那么事务A将产生幻读,造成数据不一致(binlog恢复异常)和语义不明确(事务A中“我要锁住a = "xxx"行的声明失效”) - 间隙锁阻塞了在
gap
中的插入行为(gap是基于所使用索引的gap)
- 幻读,简单点说,在可重复读的隔离级别下,一个事务A中
- gap锁发生的场景
- RR隔离级别下,在一个事务中加排它锁(
update/delete/select...for update
)会触发间隙锁,但是会根据索引类型和查询条件有所退化,下面详细说明
- RR隔离级别下,在一个事务中加排它锁(
- gap如何理解
- 首先要确定这条加锁语句,触发了哪个索引
- 命中所有索引的间隙,和所有索引前后边界,都形成了一些间隙,这些间隙会加锁,举例
+----+-----------+--------+-----+-----+-------+---------------------+---------------------+ | id | client_id | api_id | qpm | qpd | state | ctime | mtime | +----+-----------+--------+-----+-----+-------+---------------------+---------------------+ | 3 | 123 | 1 | 10 | 0 | 0 | 2021-03-26 11:23:21 | 2021-03-26 11:23:21 | | 4 | 123 | 2 | 15 | 0 | 0 | 2021-03-26 11:23:21 | 2021-03-26 11:23:21 | | 5 | 126 | 4 | 10 | 1 | 0 | 2021-04-21 13:19:04 | 2021-04-22 11:17:05 | | 6 | 130 | 4 | 10 | 1 | 0 | 2021-04-21 13:19:04 | 2021-04-22 11:17:43 | 这个表里,client_id是非唯一索引,那么这个表目前就有这些间隙 (-∞, 123], (123, 126], (126, 130], (130, +supremum] (这里使用了左开右闭的形式,即next key锁。next key = gap + 行) 执行语句 `select * from t where client_id = 126 for update` 将锁住(123, 126], (126, 130]的间隙,此区间的insert行为将阻塞,知道上面语句的事务提交
- gap锁不是加在记录上,而是加在行行之间的间隙
- 间隙锁的出现是为了解决RR隔离级别,非唯一索引下的幻读问题,至于Innodb隔离级别和幻读,在事务章节中有详解
- Next key 锁
- Next key = 间隙锁 + 行锁, 左开右闭区间
- Next key 锁是加锁的基本单位,查找过程中访问到的对象才会加锁
- 等值查询,命中唯一索引,Next Key锁退化为行锁。此时不会锁住间隙
- 等值查询,如果扫描行的右边界值不等于该等值,Next key锁退化为间隙锁。此时会锁住所有间隙,但是右边界值不会被锁住
- 表级锁
- 锁全表的情况(这时其实不是表锁,二是间隙锁+行锁):
- 全表扫描: 没有索引,执行当前读操作:
delete users where uid = 55
,uid
字段没有索引- RC隔离级别下: uid没有索引,innodb会做全表扫描,但是他不知道改锁谁(没有索引),这个时候会全表锁住。并把查询的结果给mysql server, 让mysql server过滤查询结果。
- RR隔离级别下: 为了解决幻读问题,会加间隙锁,那么所有的记录,以及记录之间的间隙,全部被锁住了。
- 全表扫描: 没有索引,执行当前读操作:
- 表级锁的种类
- 一是表锁,语法
lock tables tb1 read, tb2 write
可以强制加表锁- 表锁情况下,其他线程对tb1只读,写阻塞,tb2读写阻塞
- 二是元数据锁
MDL: metadata lock
,
- 一是表锁,语法
- 锁全表的情况(这时其实不是表锁,二是间隙锁+行锁):
通过类型划分:共享锁,排它锁
- 共享锁(S)
- 读锁: 加读锁后,可以再加读锁,不能加写锁(手动加)
select .... lock in share mode
- 读锁: 加读锁后,可以再加读锁,不能加写锁(手动加)
- 排他锁(X)
- 写锁: 加锁后不能加读锁,也不能再加写锁(mysql自动加)
insert、update、delete
自动加写锁select .... for update
- 写锁: 加锁后不能加读锁,也不能再加写锁(mysql自动加)
通过情感划分:乐观锁,悲观锁
乐观锁和悲观锁不是锁实体,而是在处理业务逻辑时对于加锁行为的态度。
- 乐观锁:我觉得在我处理数据期间,该数据不会被更改。所以我不对该数据加锁
- 这种情况需要维护版本概念,当我提交数据时,服务端数据版本和我查询时的版本不同,需要重新加载后处理
- 悲观锁:我觉得在我处理数据期间,该数据肯定会被更改。所以我在处理数据前要排它锁
锁相关查询语句
-
查看正在锁的事务:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
The INNODB_LOCKS table provides information about each lock that an InnoDB transaction has requested but not yet acquired, and each lock that a transaction holds that is blocking another transaction.
INNODB_LOCKS
表记录这样的锁信息,每个锁都是被一个INNODB事务“请求锁但未得”;并且每个锁都是有一个事务持有,并阻塞其他事务- 总结下来就是,这个表数据展示所有有阻塞的锁信息。官网详情
-
查看正在等待锁的事务:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
The INNODB_LOCK_WAITS table contains one or more rows for each blocked InnoDB transaction, indicating the lock it has requested and any locks that are blocking that request.
INNODB_LOCK_WAITS
表包含所有阻塞的innodb事务,这些事务是由于请求锁等待- 总结下来就是,这个表记录所有处于锁等待中的事务信息。官网详情
-
查看正在等待的行锁
SHOW STATUS LIKE 'innodb_row_lock%';
补充知识点
自增锁
为了维护自增主键的唯一性和递增性,Mysql提供了自增锁概念:
插入语句在获取主键值时,会先申请自增锁,然后获取主键值
MySQL5.1.22版本后新增参数innodb_autoinc_lock_mode
,默认值为1
0: 表示采用MySQL5.0版本的自增锁策略,一个insert语句执行前获取锁,执行完成后才释放锁(肉眼可见,影响并发插入的性能)
1: 普通insert语句,自增锁申请后马上释放(获取主键值后,不等待语句执行);类似insert ... select...
这种批量插入语句,自增锁还是等语句执行后释放
2:所有插入语句都是申请主键锁获取主键后立即释放
思考
- 行锁是加在索引上的,那么对于同一行记录,我通过二级索引字段加锁(锁是加在二级索引上),另一个事务通过主键加锁(锁是加载主键索引上的),这是也会阻塞,是为什么?