【MySQL】锁
1、MySQL 并发事务访问相同记录
1.1、并发过程分析
锁是一个内存中的结构,在事务执行前,本来是没有锁和记录关联的,当事务 T1要对这条记录做改动,就需要生成一个锁结构与之关联
- trx 信息:这个锁是哪个事务生成的
- is_waiting:当前事务是否在等待
假设之前没有别的事务为这条记录加锁,当事务
T1
改动了这条记录后,就生成了一个锁结构
与该记录关联,is_waiting
是false
,这个场景称为获取锁成功
,或者加锁成功
在事务
T1
提交之前,事务T2
也想对该记录做改动,锁结构的is_waiting
值为true
,表示当前事务需要等待,这个场景就称之为获取锁失败
,或者加锁失败
事务T1提交后,把生成的
锁结构释放
,事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting
属性设置为false
,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁
1.2、并发解决方案
并发引发的问题包括
脏读
、幻读
、不可重复度
脏读
事务 A在读取数据的时候读取到事务 B 未提交的数据
不可重复读
事务 A 中,第一次读取值为 1,此时事务 A 未结束,事务 B 将数值修改为 2并提交,事务 A再次读取时得到的数据是 2,在同一个事务 A 中两次读取的数据值不一样
幻读
只针对 insert
操作
事务 A 按条件读取到 5 条记录,此时事务 A 未结束,事务 B 向表中插入两条符合条件的数据并提交,事务 A 再次读取时得到 7 条记录,在同一个事务中两次读取的数据条数不一样,后一次比前一次多
方式一:读操作利用多版本并发控制(MVCC),写操作进行加锁
MVCC
- 生成一个
ReadView
,通过ReadView找到符合条件的记录版本(历史版本由undo日志
构建) - 查询语句只能
读
到在生成ReadView之前已提交事务所做的更改
,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的 写操作
肯定针对的是最新版本的记
录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写
操作并不冲突
方式二:读、写操作都采用
加锁
的方式
小结
- 采用
MVCC
方式的话,读-写
操作彼此并不冲突,性能更高
- 采用
加锁
方式的话,读-写
操作彼此需要排队执行
,影响性能 - 一般采用
MVCC
来解决读-写
操作并发执行的问题,在某些特殊情况下,业务要求必须采用加锁
的方式执行
2、锁的分类
2.1、数据操作的类型:读锁、写锁
数据库中并发事务的
读-读
情况并不会引起什么问题,对于写-写
、读-写
或写-读
这些情况可能会引起一些问题,MySQL实现一个由两种类型的锁组成的锁系统来解决:
读锁
:也称为共享锁
、英文用S
表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的写锁
:也称为排他锁
、英文用X
表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源
需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。
X锁 | S锁 | |
---|---|---|
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
2.1.1、锁定读
对读取的记录加 S 锁
SELECT ... LOCK IN SHARE MODE;
SELECT ... FOR SHARE;#(8.0之后)
对读取的记录加X锁
SELECT ... FOR UPDATE;
在 MySQL5.7及以前的版本,select ... for update
如果获取不到锁,会一直等到,直到innodb_lock_wait_timeout
超时。
在 MySQL8.0之后,SELECT. FOR UPDATE
,SELECT ...FOR SHARE
添加NOWAIT
、SKIP LOCKED
语法,跳过锁等待,或者跳过锁定
- NOWAIT会立即报错返回(等不到锁立即返回
- KIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行
SELECT. FOR UPDATE NOWAIT
2.1.2、写操作
-
delete
先在B+
树中定位到这条记录的位置,然后获取这条记录的X锁
,再执行delete mark
操作,可以看成是一个获取X锁
的锁定读
-
update
- 情况1: 未修改该记录的
键值
,并且被更新的列占用的存储空间在修改前后未发生变化
先在B+
树中定位到这条记录的位置,然后再获取一下记录的X锁
,最后在原记录的位置进行修改操作 - 情况2∶未修改该记录的
键值
,并且至少有一个被更新的列占用的存储空间在修改前后发生变化
先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取×锁
的锁定读
,新插入的记录由INSERT
操作提供的隐式锁
进行保护 - 情况3∶修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照
DELETE
和INSERT
的规则进行了
- 情况1: 未修改该记录的
-
insert
一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁
的结构来保护这条新插入的记录在本事务提交前不被别的事务访问
2.2、数据操作的粒度:表锁、页锁、行锁
2.2.1、表锁
S锁、X 锁
该锁会锁定整张表,它是MySQL中最基本的锁策略,并不依赖于存储引擎
,并且表锁是开销最小
的策略
一般情况下,不会使用InnoDB存储引擎提供的表级别
的S锁
和X锁
。只会在一些特殊情况下,比方说崩溃恢复
过程中用到
在系统变量autocommit=0,innodb_table_locks = 1
时,手动获取InnoDB存储引擎提供的表t 的S锁或者X锁的写法:
# 查看自动提交
mysql> show variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
# 查看innodb表锁是否打开
mysql> show variables like '%innodb_table%';
+--------------------+-------+
| Variable_name | Value |
+--------------------+-------+
| innodb_table_locks | ON |
+--------------------+-------+
1 row in set (0.00 sec)
# 临时关闭自动提交
mysql> set @@autocommit=0;
show open tables where in_use > 0; # 查看哪些表被锁了
lock tables student read # 加写锁
lock tables student write;
unlock tables; # 表解锁
意向锁
InnoDB 支持多粒度锁(multiple granularity locking)
,它允许行级锁与表级锁共存,而 意向锁 就是其中的一种表锁
1、意向锁的存在是为了协调行锁和表锁
的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突表级锁
,这一点非常重要。
3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”
- 意向共享锁 (intention shared lock, IS):事务有意向对表中的某些行加 共享锁 (S锁)
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
-- 会自动加,不用管
SELECT column FROM table ... LOCK IN SHARE MODE;
- 意向排他锁 (intention exclusive lock, IX):事务有意向对表中的某些行加 排他锁 (X锁)
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
-- 会自动加,不用管
SELECT column FROM table ... FOR UPDATE;
意向锁是由存储引擎
自己维护的
,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在数据表的对应意向锁
意向锁要解决的问题
现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的
表级别意向锁的阻塞
。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁
如图:右边加上行级排它锁,左边试图加表级共享锁失败
分析:左事务检测右事务持有teacher表的意向排他锁,就可以得知右事务必然持有该表中某些数据行的排他锁,那么左事务对teacher表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁
意向锁之间的兼容互斥性
意向共享锁(lS) | 意向排他锁(IX) | |
---|---|---|
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
意向锁与 表级别S、X锁兼容互斥性
意向共享锁(lS) | 意向排他锁(IX) | |
---|---|---|
表级共享锁(S)表 | 兼容 | 互斥 |
表级排他锁(X)表 | 互斥 | 互斥 |
意向锁总结:
- InnoDB 支持
多粒度锁
,特定场景下,行级锁可以与表级锁共存。 - 意向锁之间互不排斥,但除了 IS 与 S 兼容外,
意向锁会与 共享锁 / 排他锁 互斥
。 - IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
- 意向锁在保证并发性的前提下,实现了
行锁和表锁共存
且满足事务隔离性
的要求。
自增锁
元数据锁
2.2.2、行锁
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现
优点: 锁定力度小,发生锁冲突概率低
,可以实现的并发度高
缺点: 对于锁的开销比大
,加锁会比较慢,容易出现死锁
记录锁
LOCK_REC_NOT_GAP
记录锁也就是仅仅把一条记录锁上
如图:仅仅是锁住了id值为 8 的记录,对周围的数据没有影响
间隙锁
gap锁的提出仅仅是为了防止插入幻影记录而提出的
LOCK_GAP
不允许别的事务在id值为 8 的记录前边的间隙插入新记录
,其实就是id列的值( 3 , 8 )这个区间的新记录是不允许立即插入的
select *from student where id =5 lock in share mode;
select * from student where id =5 for update;
临键锁
Next-Key Locks
既锁住某条记录
,又阻止
其他事务在该记录前边的间隙插入新记录
用区间表示就是:(3,8]
begin;
select * from student where id <= 8 and id > 3 for update;
插入意向锁
LOCK_INSERT_INTENTION
一个事务在插入
一条记录时需要判断一下插入位置是不是被别的事务加了gap锁
(next-key锁
也包含gap锁
),如果有的话,插入操作需要等待,直到拥有gap锁
的那个事务提交。但是 InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构 ,表明有事务想在某个间隙
中插入
新记录,但是现在在等待
插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁
2.2.3、页锁
页锁就是在页的粒度
上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。 **页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般
2.3、对待锁的态度:乐观锁、悲观锁
2.3.1、悲观锁
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现
2.3.2 乐观锁
不采用数据库自身的锁机制,而是通过程序来实现
1.乐观锁的版本号机制
在表中设计一个
版本字段 version
,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version
。此时如果已经有事务对这条数据进行了更改,修改就不会成功
2. 乐观锁的时间戳机制
时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突
2.4、 加锁方式:显式锁、隐式锁
2.5、 全局锁
全局锁就是对整个数据库实例
加锁,让整个库处于只读状态
Flush tables with read lock
2.6、 死锁
-
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数
innodb_lock_wait_timeout
来设置。 -
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数
innodb_deadlock_detect
设置为on,表示开启这个逻辑
3、锁监控
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits #当前正在等待锁定的数量
Innodb_row_lock_time #从系统启动到现在锁定总时间长度;(等待总时长)
Innodb_row_lock_time_avg #每次等待所花平均时间;(等待平均时长)
Innodb_row_lock_time_max #从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits #系统启动后到现在总共等待的次数;(等待总次数)
查询正在被锁阻塞的sql语句
SELECT * FROM information_schema.INNODB_TRX\G;
查询锁等待情况
SELECT * FROM data_lock_waits\G;
*************************** 1. row ***************************
ENGINE: INNODB
REQUESTING_ENGINE_LOCK_ID: 139750145405624 :7:4:7:
REQUESTING_ENGINE_TRANSACTION_ID: 13845 #被阻塞的事务ID
REQUESTING_THREAD_ID: 72
REQUESTING_EVENT_ID: 26
REQUESTING_OBJECT_INSTANCE_BEGIN: 139747028690608
BLOCKING_ENGINE_LOCK_ID: 139750145406432 :7:4:7:
BLOCKING_ENGINE_TRANSACTION_ID: 13844 #正在执行的事务ID,阻塞了 13845
BLOCKING_THREAD_ID: 71
BLOCKING_EVENT_ID: 24
BLOCKING_OBJECT_INSTANCE_BEGIN: 139747028813248
1 row in set (0.00 sec)
查询锁的情况
SELECT * from performance_schema.data_locks\G;