锁
是计算机协调多个进程或线程并发访问某一资源
的机制。开发过程中通过加锁
保证数据的一致性。锁机制
为实现MySQL的各个隔离级别提供了保证。锁冲突
是影响数据库 并发访问性能 的重要因素。
1、对数据操作类型划分:读锁(共享锁),写锁(排他锁)
- 读锁(readlock):也称
共享锁(Shared Lock,S Lock)
。针对同一份数据,多个事务的读操作可以同时进行而不会相互影响,相互不阻塞。 - 写锁(writeLock):也称
排他锁(Exclusive Lock,X Lock)
。当前写操作没有完成前,它会阻断其他写锁和读锁。这样能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。 - 注: 对于innodb存储引擎来说,读锁和写锁可以加在表上,也可以加在行上。
行级读写锁
:如果一个事务T1 已经获得某个行r的读锁,那么此时另外一个事务T2 是可以去获得这个行r 的读锁,因为读取操作并未改变行r 的数据。
如果某个事务T3 想获得行r 的写锁,则它必须等待事务T1,T2 释放掉行r上的读锁才行。
1.1、锁定读
在采用 加锁 方式解决 脏读、不可重复读、幻读等问题,读取一条相关记录时需要获取该记录的S锁,相对来说不够严谨,有时需要在读取记录的时候获取记录的X锁,来禁止别的事务读写该记录。通过以下两种比较特殊的 select 语句格式实现:
- 对读取的记录加S锁:
select ··· lock in share mode;
或
select ··· for share;#举例说明
mysql> select * from rqtanc for share;
Empty set (0.12 sec)mysql> select * from rqtanc lock in share mode;
Empty set (0.01 sec)
如果当前事务执行了上述语句,那么它会为读取到的记录加 S 锁,这样允许别的事务继续获取这些记录的 S锁 。但是不能获取这些数据的 X锁,如果别的事务想要获取这些记录的 X锁,那么他们会阻塞,知道当前事务提交之后将这些记录上的 S锁 释放掉。
- 对读取记录加 X锁:
select ··· for update;#示例:
mysql> select * from rqtanc for update;
Empty set (1.32 sec)
如果当前事务执行上述语句,会对读取到的事务加 X锁,这样既不允许别的事务获取这些记录的 S锁,也不允许获取 X锁。如果别的事务想要获取这些记录的 S锁 或者 X锁,那么它们会阻塞,知道直到当前事务提交之后将这些记录上的 X锁 释放掉。
MySQL8.0新特性:
在MySQL 5.7及之前版本,select ··· for update,如果获取不到锁,会一直等待,直到 innodb_lock_wait_timeout 超时。
在MySQL8.0中,select ··· for update ,select ··· for share 添加 nowait、skip locked语法,跳过锁等待,或者跳过锁定。
- 通过添加 nowait、skip locked语法,能够立即返回。如果查询的行已经加锁。
- nowait会立即报错返回
- skip locked 也会立即返回,只是返回的结果中不包含被锁定的行。
1.2、写操作
- delete:对一条记录做delete操作的过程其实就是先在B+树中定位到这条记录的位置,然后获取这条记录的 X锁,在执行 delete mark操作,也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。
- update:在对一条记录做update操作时分为以下三种情况
- 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。
- 则先在B+树中定位到该记录的位置,然后再获取一下记录的 X锁,最后再原纪录的位置进行修改操作。我们也可以把这个定位待修改记录再B+树中的位置的过程看成一个获取 X锁 的锁定读。
- 未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。
- 则先在B+树中定位到该记录的位置,然后再获取一下记录的 X锁,将该记录彻底删除掉(即移入垃圾链表),最后再插入一条新记录。这个定位待修改记录再B+树中位置的过程看成是一个获取X锁的锁定读,新插入的记录由insert操作提供的 隐式锁 进行保护。
- 修改了该记录的键值
- 则相当于再原记录上做 delete 操作之后再来一次inset操作,加锁操作就需要按照delete 和inset 的规则进行了。
- 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。
- insert:一般情况下,新插入一条记录的操作并不加锁,通过一种称为 隐式锁 的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。
2、对数据操作粒度划分
数据库系统需要在
高并发响应
和系统性能
两方面进行平衡,产生 “锁粒度
” 的概念。
对一条记录加锁影响的也只是这条相关记录而已,则认为这个锁的粒度比较细;一个事务也可以在 表级别 进行加锁,自然就被称为 表级锁 或表锁,对一个表加锁影响整个表中记录,则认为该锁的粒度较粗。
锁的粒度主要分为 表锁、行锁、页锁
2.1、表锁
该锁会锁定整张表,是MySQL中最基本的锁策略,并
不依赖于存储引擎
,并且表锁是开销最小
的策略(因为粒度比较大)。由于表级锁一次将整个表锁定,所以可以很好的避免死锁
问题,但会导致并发率大打折扣
2.1.1、表级别S锁、X锁
- 对某个表进行增删改查是,InnoDB存储引擎是不会为该表添加表级别的S、X锁。在对某个表执行 Alter 、Drop 这类 DDL 语句时,其他事务会对该表并发执行增删改查的语句会发生阻塞。反之亦然。这个过程其实时通过在 Server层使用一种称为
元数据锁(DML锁)
结构来实现。 在特殊情况下
,如: 崩溃恢复 过程中用到,或在系统变量 autocommit=0,innodb_table_locks=1时。手动获取innodb存储引擎提供的表的 S、X锁如下所示:lock tables table_name read
:innodb存储引擎会对表加表级别的 S锁lock tables table_name write
:innodb存储引擎会对表加表级别的 X锁
- 通过一下信息查询相关表信息,
show open tables [ from database_name like [table_name | '%'] ]
;包括表名和是否存在未完成的事务,属性包含一下信息:- database:表所在数据库名称
- table:表名
- In_use:表示当前被多少个线程占用,如果是0,则表示表没有被占用
- name_locked:表名是否被锁定,1 表示被锁定,0 表示未锁定。
mysql> SHOW OPEN TABLES FROM test LIKE 'rqtanc';
+----------+--------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+--------+--------+-------------+
| test | rqtanc | 0 | 0 |
+----------+--------+--------+-------------+
1 row in set (0.26 sec)mysql> lock tables test.rqtanc write;
Query OK, 0 rows affected (0.90 sec)mysql> SHOW OPEN TABLES FROM test LIKE 'rqtanc';
+----------+--------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+--------+--------+-------------+
| test | rqtanc | 1 | 0 |
+----------+--------+--------+-------------+
1 row in set (0.12 sec)mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)mysql> SHOW OPEN TABLES FROM test LIKE 'rqtanc';
+----------+--------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+--------+--------+-------------+
| test | rqtanc | 0 | 0 |
+----------+--------+--------+-------------+
1 row in set (0.00 sec)
锁类型 | 自己可读 | 自己可写 | 自己可操作其他表 | 他人可读 | 他人可写 |
---|---|---|---|---|---|
读锁 | 是 | 否 | 否 | 是 | 否、等待 |
写锁 | 是 | 是 | 否 | 否、等待 | 否、等待 |
2.1.2、意向锁
InnoDB支持 多粒度锁,它允许 行级锁 和 表级锁 共存,而 意向锁 就是其中的一种 表锁。
- 意向锁的存在就是为了协调 行锁 和 表锁 的关系,支持 多粒度锁共存
- 意向锁是一种 不与行级锁冲突的表级锁
- 表明 某个事务正在某些行持有了锁或该事务准备去持有锁
- 意向锁分为以下两种:
意向共享锁(intention shared lock,IS lock)
:事务有意向对表中某些行加 共享锁(S锁)- 事务要获取某些行的 S锁,必须先获得表的 IS锁
select column from table ··· lock in share mode;
意向排他锁(intention exclusive lock,IX lock)
:事务有意向对表中某些行加 排他锁(X锁)- 事务要获取某些行的 X锁,必须先获得表的 IX锁
select column from table ··· for update;
备注:
意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加 共享锁/排他锁之前,InnoDB会先获取该数据行 所在数据表的对应意向锁。
2.1.3、自增锁
2.1.4、元数据锁
MySQL5.5引入 meta data lock,简称MDL锁,属于表锁范围。
MDL的作用是保证读写的准确性。比如:如果一个查询正在遍历一个表中的数据,而执行期间另外一个线程对这个 表结构做变更,增加了一列,则查询线程拿到的结果和表结构对应不上,这是由问题的。
- 当对一个表做增删改查操作的时候,加MDL读锁。
- 当要对表做结构变更操作的时候,加MDL写锁。
2.2、InnoDB行锁
行锁(Row Lock)也成为记录锁,就是锁住某一行(某条记录)。
MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。
- 优点:锁定力度小,发生
锁冲突概率低
,可以实现的并发度高
- 缺点:对于
锁的开销大
,加锁较慢,容易出现死锁
情况。
2.2.1、记录锁(Record Locks)
- 记录锁也就是仅仅把一条记录锁上,官方名称为:
lock_rec_not_gap
。 - 记录锁也有S锁和X锁,分别称之为 S型记录锁和X型记录锁
- 当一个事务获取一条记录的S型记录锁后,其他事务也可以获取该记录的S型记录锁,但不可以获取X型记录锁
- 当一个事务获取一条记录的X型记录锁后,其他事务既不可以获取该记录的S型记录锁,也不可以获取X型记录锁。
2.2.2、间隙锁(Gap Locks)
MySQL在 REPEATABLE READ 隔离级别下可以解决幻读问题,当使用加锁方式解决,无法对这些“幻影”记录 加上记录锁。InnoDB提出一种为Gap Locks的锁,仅仅是为了 防止插入“幻影”记录而提出的。
2.2.3、临键锁(Next-Key Locks)
官方名称:Lock_ordinary
有时既想锁住某条记录,又想阻止其它事务在该记录前边的间隙插入新记录,InnoDB存储引擎提出了一种称为:Next-Key Locks的锁
- Next-Key Locks 的本质就是一个
记录锁
和一个gap锁
的合体。它既能保护该条记录,又能阻止其他事务将新记录插入被保护记录前边的间隙
2.2.4、插入意向锁
- 当一个事务在 插入一条记录时需要判断以下插入位置是不是被别的事务加了 gap锁(Next-Key Locks 也包含gap锁),如果有则插入操作需要等待,直到拥有gap锁的那个事务提交。
- 但是InnoDB规定事务在等待的时候需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新数据,但现在在等待。
- InnoDB存储引擎将这种锁称为:Insert Intention Locks,即插入意向锁,该锁是一种Gap锁,而不是意向锁,在Insert操作时产生。
- 插入意向锁是在插入一条记录行前,由Insert操作产生的一种间隙锁。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间无需互相等待。
2.3、页级锁
- 页锁就是在 页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。
- 当使用页锁的时候会出现数据浪费的现象,但该浪费最多也就是一个页上的数据行。
- 页锁的开销介与表锁和行锁之间,会出现死锁问题。
- 锁定粒度介于表锁和行锁之间,并发度一般。
灭个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁的数量超过了这个层级的阈值时,就会进行锁升级。
锁升级就是用更大粒度的锁代替多个更小粒度的锁,比如:InnoDB中行锁升级为表锁,好处就是占占用的锁空间降低了,但同时数据的并发度也下降了。
3、对锁的态度划分:悲观锁、乐观锁
悲观锁和乐观锁是看待 数据并发的思维方式
,并不是正真的锁,而是锁的设计思想
3.1、悲观锁
- 对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排他性。
- 悲观锁总是假设在最坏的一种情况,每次去拿去数据的时候总认为别人会修改,所以每次拿取数据都会加锁,这样别人想拿取该数据就会阻塞直到拿到锁(
共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程
),如:行锁、表锁。
在Java中 synchronized 和 Reentrant Lock等独占锁就是悲观锁思想的实现
3.2、乐观锁
- 乐观锁认为同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下再次期间别人有没有去更新这个数据,也就是
不采用数据库自身的锁机制,而是通过程序来实现
。 - 在程序上可以采用
版本号机制
或CAS机制
实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量
。
在Java中 java.util.concurrent.atomic包下的原子变量类就是适用了乐观锁的一种实现方式:CAS实现的
3.2.1、乐观锁版本号机制
- 在表中设计一个
版本字段 version
,第一次读取的时候,会获取 version 字段的取值,然后对数据进行更新或删除操作时,会执行update set version = version + 1 where version=version
。此时如果有事务对这条数据进行更改,修改就不会成功。
SVN、CVS版本管理系统,当我们修改代码进行提交时,首先会检查当前版本号与服务器上版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。
3.2.2、乐观锁的时间戳机制
- 时间戳和版本号机制一样,也是再更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者相等则更新成功,否则就是版本冲突
3.2.3、适用场景
- 乐观锁:适合
读操作多的场景
,相对来说写的操作比较少。优点在于程序实现,不存在死锁
问题,不过适用场景也会相对乐观,阻止不了除程序以外的数据库操作。 - 悲观锁:适合
写操作多的场景
,因为写的操作具有排他性
。采用悲观锁的方式,可以再数据库层面阻止其他事务对该数据的操作权限,防止读-写 和 写-写
的冲突。
4、按加锁方式划分:显示锁、隐式锁
4.1、隐式锁
- 一个事务再执行
insert
操作时,如果即将插入的间隙
已经被其他事务加了gap锁
,那么本次insert操作会阻塞,并且当前事务会再该间隙上加一个插入意向锁
,否则一般情况下insert
操作是不加锁的。
如果一个事务首先插入一条记录(此时并没有再内存生产与该记录关联的锁结构),然后另一个事务:
- 立即使用
select ··· lock in share mode
语句读取这条记录,也就是获取该条记录的S锁,或者使用select ··· for update
语句读取这条记录,也就是获取该记录的 X锁。
- 如果允许这种情况发生,那么可能产生
脏读
问题- 立即修改这条记录,也就是获取该记录的X锁。
- 如果允许这种情况发生,那么可能产生
脏写
问题
- 一个事务对新插入的记录可以不显示加锁(生成一个锁结构),但是由于
事务id
的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己在生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁的机制
,从而来减少加锁的数量。
4.2、显示锁
- 通过特定语句进行加锁,称之为显示锁。
5、其他:全局锁、死锁
5.1、全局锁
- 就是对 整个
数据库实例加锁
。当需要让整个库处于只读状态
的时候,可以使用flush tables with read lock;
命令。之后其他线程的以下语句会被阻塞:数据更新语句、数据定义语句、更新类事务提交语句。 - 全局锁的使用场景:
做全库逻辑备份
。
5.2、死锁
- 是指多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶心循环。
产生死锁的条件
:- 两个或多个事务
- 每个事务都已经持有锁并且申请新的锁
- 锁资源同时只能被同一个事务持有或不兼容
- 事务之间因为持有锁和申请锁导致彼此循环等待。
死锁的关键在于:多个事务加锁的顺序不一致
死锁的处理方式:
- 方式一:等待,直到超时(innodb_lock_wait_timeout= 50s)(
不推荐不适用
) - 方式二:使用死锁检测进行死锁处理
- InnoDB提供了
wait-for grap算法
来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,wait-for grap 算法都会被触发。 - 这是一种较为
主动的死锁检测机制
,要求数据库保存锁的信息链表 和事务等待链表
两部分信息
- InnoDB提供了
- 方式一:等待,直到超时(innodb_lock_wait_timeout= 50s)(
死锁检测的原理是构建一个以事务为顶点,锁为边的有向图,判断有向图是否存在环,存在即有死锁。
如何避免死锁
:- 合理设计索引,使得业务SQL尽可能通过索引定位更少的行,减少锁竞争。
- 调整业务逻辑SQL执行顺序,避免update\delete长时间持有锁的SQL在事务前面
- 避免大事务,尽量将大事务拆分成多个小事务处理,小事务缩短锁定资源时间
- 在并发比较高的系统中,不要显示加锁
- 降低隔离级别。
6、锁的内存结构
6.1、符合以下情况的记录会放到一个锁结构
中:
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型时一样的
- 等待状态是一样的
6.2、InnoDB存储引擎中的 锁结构 如下图所示:
- 锁所在事务信息:
- 不论表锁还是行锁,都是在事务执行过程中生成的,哪个事务生成了这个锁结构,这里记录的这个事务的信息
- 此锁所在事务信息在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比如:事务id等。
- 索引信息:对于行锁来说,需要记录一下加锁的记录是属于哪个索引的,此处也是一个指针。
- 表锁\行锁信息:在这个位置内容是不同的
- 表锁:记载这是对哪个表加的锁,还有一些其他信息。
- 行锁:记载一下三个信息:
- Space id:记录所在表的空间
- page number:记录所在页号
- n_bits:对于行锁来说,一条记录就对应这一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪条记录加锁了。
n_bits的值一般比页面中记录条数多一些。主要是为了之后在页面中插入了新纪录后也不至于重新分配锁结构。
- type_mode:这是一个32位的数,被分成了 lock_mode(低4位)、lock_type(低5~8位)、rec_lock_type三部分
- lock_mode(锁的模式):占用低4位,可选值如下:
- lock_is(十进制的0):表示共享意向锁(IS锁)
- lock_ix(十进制的1):表示独占意向锁(IX锁)
- lock_s(十进制的2):表示共享锁(S锁)
- lock_x(十进制的3):表示独占锁(X锁)
- lock_auto_inc(十进制的4):表示AUTO_INC锁
- lock_type(锁的类型):占用低5~8位
- lock_table:当第五个比特位置为 1 时,表示表级锁
- lock_rec:当第六个比特位置为 1 时,表示行级锁
- rec_lock_type(行锁具体类型):只有lock_type的值为lock_rec时,将会划分以下内容:
- lock_ordinary:表示 next-key锁
- lock_gap:当第十个比特位置为 1 时,表示 gap锁
- lock_rec_not_gap:当第十一比特位置为 1 时,表示标准 记录锁
- lock_insert_intention:当第十二比特位置为 1 时,表示插入意向锁
- lock_wait:当第九个比特位置为 1 时,表示is_waiting为 true,也就是当前事务尚未获取到值,若为false,表示当前事务获取到锁。
- lock_mode(锁的模式):占用低4位,可选值如下:
- 其他信息:为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表
- 一堆比特位:一个比特位映射一个heap_no,即映射到页内的一条记录
7、锁监控
关于MySQL锁的监控,一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况
7.1、通过检查参数 innobd_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.00 sec)
- Innodb_row_lock_current_waits:表示当前正在等待锁定的数量
- Innodb_row_lock_time:表示系统启动到现在锁定总时长
- Innodb_row_lock_time_avg:每次平均等待所花费平均时长
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时长
- Innodb_row_lock_waits :系统启动到现在总共等待的次数
根据上述信息分析结果制定相关优化方案
7.2、其他监控方法
MySQL把事务和锁的信息记录在了 information_schema库中,主要涉及以下相关表:innodb_trx、innodb_locks、innodb_lock_waits、data_locks、innodb_lock_waits。