MySQL锁机制
MySQL行锁
通常而言,为了尽可能提升数据库的整体性能,所以每次在加锁时,锁的范围自然是越小越好,举个例子:假设此时有1000个请求,要操作zz_users表中的数据,如果以表粒度来加锁,假设第一个请求获取到的是排他锁,也就意味着其他999个请求都需要阻塞等待,其效率可想而知.... 仔细一思考:虽然此时有1000个请求操作zz_users表,但这些请求中至少90%以上,要操作的都是表中不同的行数据,因此如若每个请求都获取表级锁,显然太影响效率了,而InnoDB引擎中也考虑到了这个问题,所以实现了更细粒度的锁,即行锁。 #### 表锁与行锁之间的关系 一张表就类似于一个生活中的酒店,每个事务/请求就类似于一个个旅客,旅客住宿为了确保夜晚安全,通常都会锁门保护自己。而表锁呢,就类似于一个旅客住进酒店之后,直接把酒店大门给锁了,其他旅客就只能等第一位旅客住完出来之后才能一个个进去,每个旅客进酒店之后的第一件事情就是锁大门,防止其他旅客威胁自己的安全问题。 但假设酒店门口来了一百位旅客,其中大部分旅客都是不同的房间,因此直接锁酒店大门显然并不合理。 而行锁呢,就类似于房间的锁,门口的100位旅客可以一起进酒店,每位旅客住进自己的房间之后,将房门反锁,这显然也能保障各自的人身安全问题,同时也能让一个酒店在同一时间内接纳更多的旅客,“性能”更高。 #### InnoDB的行锁实现 在MySQL诸多的存储引擎中,仅有InnoDB引擎支持行锁,这是由于什么原因导致的呢?因为InnoDB支持聚簇索引,InnoDB中如果能够命中索引数据,就会加行锁,无法命中则会加表锁。 InnoDB会将表数据存储在聚簇索引中,每条行数据都会存储在树中的叶子节点上,因此行数据是“分开的”,所以可以对每一条数据上锁,但其他引擎大部分都不支持聚簇索引,表数据都是一起存储在一块的,所以只能基于整个表数据上锁,这也是为什么其他引擎不支持行锁的原因。 #### 记录锁(Record Lock) Record Lock记录锁,实际上就是行锁,一行表数据、一条表记录本身就是同一个含义,因此行锁也被称为记录锁,两个称呼最终指向的是同一类型的锁,那如何使用行锁呢? ```sql -- 获取行级别的 共享锁 select * from `zz_users` where user_id = 1 lock in share mode; -- 获取行级别的 排他锁 select * from `zz_users` where user_id = 1 for update; ``` 是的,你没看错,想要使用InnoDB的行锁就是这样写的,如果你的SQL能命中索引数据,那也就自然加的就是行锁,反之则是表锁。但网上很多资料都流传着一个说法:InnoDB引擎的表锁没啥用,其实这句话会存在些许误导性,因为意向锁、自增锁、DML锁都是表锁,也包括InnoDB的行锁是基于索引实现的,例如在update语句修改数据时,假设where后面的条件无法命中索引,那咋加行锁呢?此时没办法就必须得加表锁了,因此InnoDB的表锁是有用的。 #### 间隙锁(Gap Lock) 间隙锁是对行锁的一种补充,主要是用来解决幻读问题的,但想要理解它,咱们首先来理解啥叫间隙: ```sql SELECT * FROM `zz_users`; +---------+-----------+----------+----------+---------------------+ | user_id | user_name | user_sex | password | register_time | +---------+-----------+----------+----------+---------------------+ | 1 | 熊猫 | 女 | 6666 | 2022-08-14 15:22:01 | | 2 | 竹子 | 男 | 1234 | 2022-09-14 16:17:44 | | 3 | 子竹 | 男 | 4321 | 2022-09-16 07:42:21 | | 4 | 猫熊 | 女 | 8888 | 2022-09-27 17:22:59 | | 9 | 黑竹 | 男 | 9999 | 2022-09-28 22:31:44 | +---------+-----------+----------+----------+---------------------+ ``` 上述这张表最后两条数据,ID字段之间从4跳到了9,那么4~9两者之间的范围则被称为“间隙”,而间隙锁则主要锁定的是这块范围。 拿上述表举例子,现在要将ID>3的用户密码重置为1234,因此事务T1先查到了ID>3的4、9两条数据并上锁了,然后开始更改用户密码,但此时事务T2过来又插入了一条ID=6、password=7777的数据并提交,等T1修改完了4、9两条数据后,此时再次查询ID>3的数据时,结果发现了ID=6的这条数据并未被重置密码。 为了防止出现安全问题,所以T1在操作之前会对目标数据加锁,但在T1事务执行时,这条幻影数据还不存在,因此就会出现一个新的问题:不知道把锁加在哪儿,毕竟想要对ID=6的数据加锁,就是加了个寂寞。 那难道不加锁了吗?肯定得加锁,但怎么加呢?普通的行锁就已经无法解决这个问题了,总不能加表锁吧,那也太影响性能了,所以间隙锁应运而生!间隙锁的功能与它的名字一样,主要是对间隙区域加锁,举个例子: ```sql select * from `zz_users` where user_id = 6 lock in share mode; ``` 这条加锁的SQL看起来似乎不是那么合理对吧?毕竟ID=6的数据在表中还没有呀,咋加锁呢?其实这个就是间隙锁,此时会锁定{4~9}之间、但不包含4、9的区域,因为间隙锁是遵循左右开区间的原则。 #### 临键锁(Next-Key Lock) 临键锁是间隙锁的Plus版本,或者可以说成是一种由记录锁+间隙锁组成的锁: 记录锁:锁定的范围是表中具体的一条行数据。 间隙锁:锁定的范围是左闭右开的区间,并不包含最后一条真实数据。 而临键锁则是两者的结合体,加锁后,即锁定左闭右开的区间,也会锁定当前行数据。 #### 插入意向锁(Insert Intention Lock) 插入意向锁,听起来似乎跟前面的表级别意向锁有些类似,但实际上插入意向锁是一种间隙锁,这种锁是一种隐式锁,也就是咱们无法手动的获取这种锁。通常在MySQL中插入数据时,是并不会产生锁的,但在插入前会先简单的判断一下,当前事务要插入的位置有没有存在间隙锁或临键锁,如果存在的话,当前插入数据的事务则需阻塞等待,直到拥有临键锁的事务提交。 当事务执行插入语句阻塞时,就会生成一个插入意向锁,表示当前事务想对一个区间插入数据(目前的事务处于等待插入意向锁的状态)。 #### 行锁的粒度粗化 有一点要值得注意:行锁并不是一成不变的,行锁会在某些特殊情况下发生粗化,主要有两种情况: 在内存中专门分配了一块空间存储锁对象,当该区域满了后,就会将行锁粗化为表锁。 当做范围性写操作时,由于要加的行锁较多,此时行锁开销会较大,也会粗化成表锁。 当然,这两种情况其实很少见,因此只需要知道有锁粗化这回事即可,这种锁粗化的现象其实在SQLServer数据库中更常见,因为SQLServer中的锁机制是基于行记录实现的,而MySQL中的锁机制则是基于事务实现的。
顶部
收展
底部
[TOC]
目录
MySQL锁机制的由来与分类
MySQL共享锁与排他锁
MySQL表锁
MySQL行锁
MySQL页面锁、乐观锁与悲观锁
MySQL死锁
MySQL锁机制的底层实现原理
相关推荐
MySQL教程
MySQL命令
MySQL索引
MySQL事务
MySQL版本特性