这一节,咱们来聊一下mysql锁的内容。mysql锁是为了协调多个用户访问同一个资源,保障并发时的一致性和有效性。mysql
按照锁的范围划分,咱们能够分为sql
全局锁是在整个数据库加上读锁。让数据库处于只读状态,别的进程执行一下命令会阻塞住:数据更新语句(增删改查),数据定义语句(建表,表结构修改)和更新类事务的提交语句。数据库
全局锁的语句为:安全
FLUSH TABLES WITH READ LOCK;
复制代码
简称 FTWRL, 解锁语句为:bash
UNLOCK TABLES;
复制代码
全局锁使用场景是作全库备份,让整个库只读,这听上去很危险session
那么为何还要使用全局锁呢?这是为了防止数据不一致。举个栗子: 好比一个购物网站,其中有两张表 account(帐户表)和order(订单表),咱们下了总价100元的订单,操做步骤以下:并发
如今咱们不使用全局锁备份数据。在备份时,正好有人下了个100元的订单,那么备份出来的数据有以下几种状况:工具
也就是说若是不加锁,备份的数据会可能不会在同一个逻辑点,数据的逻辑是不一致的。那么有没有更好的备份方案呢,既能够备份且保持数据一致性,又能够不影响业务运行?还真有这样一个方案:测试
使用官方自带的mysqldump工具,使用时加上--single-transaction。
复制代码
使用时会在启动一个事务,来保证一致性。因为MVCC的支持,这个过程是能够正常更新的,不用停业务。网站
固然此方法仅支持带事务功能的存储引擎,MyISAM引擎就不支持。
全局锁目的
全局锁使用场景:
语句为:
当存储引擎支持事务时,可使用以下工具作替换方案
表级锁,顾名思义就是锁住整张表,MYSQL中表锁分为两张状况:表锁和原数据锁(MDL)。
表锁又分为表读锁和表写锁。
表读锁语句:
LOCK TABLES [tablename] READ
复制代码
表写锁的语句:
LOCK TABLES [tablename] WRITE
复制代码
解锁语句:
UNLOCK TABLES
复制代码
在还没出现更细的锁颗粒度时,咱们经常使用来处理并发状况,但在InnerDB这种数据引擎下通常不用。
咱们先新建一个数据库:
CREATE TABLE `t16` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL COMMENT '惟一索引',
`b` int(11) NOT NULL,
`c` int(11) NOT NULL COMMENT '普通索引',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_a` (`a`) USING BTREE,
KEY `idx_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
复制代码
再插入一条纪录:
INSERT INTO `t16`(`a`,`b`,`c`) VALUES(1,1,1);
复制代码
咱们先来看一下研究一下读锁:
事务A | 事务B |
---|---|
LOCK TABLES t16 READ; Query OK, 0 rows affected (0.00 sec) 给表加读锁 |
|
SELECT * FROM t16; 1 row in set (0.00 sec) 正常返回结果 |
SELECT * FROM t16; 1 row in set (0.00 sec) 正常返回结果 |
INSERT INTO t16(a,b,c) VALUES(2,2,2); ERROR 1099 (HY000): Table 't16' was locked with a READ lock and can't be updated 报错 |
INSERT INTO t16(a,b,c) VALUES(3,3,3);锁住 |
UNLOCK TABLES; Query OK, 0 rows affected (0.00 sec) 解锁 |
Query OK, 1 row affected (1 min 18.19 sec)写入成功 |
咱们再来研究一下写锁:
事务A | 事务B |
---|---|
LOCK TABLES t16 WRITE; Query OK, 0 rows affected (0.00 sec) 给表加写锁 |
|
SELECT * FROM t16; 2 row in set (0.00 sec) 正常返回结果 |
SELECT * FROM t16;等待 |
UNLOCK TABLES;解锁 |
2 rows in set (58.85 sec)正常返回结果 |
LOCK TABLES t16 WRITE; Query OK, 0 rows affected (0.00 sec) 给表加写锁 |
|
INSERT INTO t16(a,b,c) VALUES(2,2,2); Query OK, 1 row affected (0.00 sec) 正常 |
INSERT INTO t16(a,b,c) VALUES(4,4,4);锁住 |
UNLOCK TABLES; Query OK, 0 rows affected (0.00 sec) 解锁 |
Query OK, 1 row affected (1 min 18.19 sec)写入成功 |
根据上面的栗子,咱们能够得出下面这个逻辑:
本事务读 | 本事务写 | 其余事务读 | 其余事务写 | |
---|---|---|---|---|
加读锁后 | 正常 | 报错 | 正常 | 等待 |
加写锁后 | 正常 | 正常 | 等待 | 等待 |
MDL不须要显式使用,在访问一个表的时候会被自动加上。MDL的做用是,保证读写的正确性。你能够想象一下,若是一个查询正在遍历一个表中的数据,而执行期间另外一个线程对这个表结构作变动,删了一列,那么查询线程拿到的结果跟表结构对不上,确定是不行的。 所以,在 MySQL 5.5 版本中引入了 MDL,当对一个表作增删改查操做的时候,加 MDL 读锁;当要对表作结构变动操做的时候,加 MDL 写锁。
咱们在工做中,不少时候须要考虑MDL的存在,不然可能致使锁等待或者链接长时间打满的状况。咱们来看下面的栗子:
session1 | session2 | session3 |
---|---|---|
SELECT a,b,SLEEP(60) FROM t16; | ||
ALTER TABLE t16 ADD COLUMN f int; | ||
SELECT * FROM t16 WHERE id=1; | ||
4 rows in set (4 min 0.02 sec)4条纪录因此4分钟后返回结果 |
Query OK, 0 rows affected (3 min 57.35 sec)session1执行完后当即执行 |
1 row in set (3 min 35.10 sec)session2执行完后当即执行 |
session1中又一条慢sql须要100s返回,他致使了session2中的修改表结构的语句阻塞,而session2中的语句又致使了,其余session中的语句的阻塞。因此短期内数据库链接很容易被打满了。那要怎么作呢?可使用kill语句来强制结束session1或session2中的语句。 所以对于开发来讲,在工做中应该尽可能避免慢查询、尽可能保证事务及时提交、避免大事务等,固然对于 DBA 来讲,也应该尽可能避免在业务高峰执行 DDL 操做。
标级锁分为表锁和原数据锁(MDL) 表级别读锁和写锁的区别为
元数据锁
** 在Innodb中,行锁是在须要时才加上,在事务提交时解锁,这就是两阶段锁的协议。**
咱们知道这个设定对咱们使用事务有什么帮助呢? 若是你的事务要锁多行,须要把最可能形成锁冲突的锁日后放 举个栗子: A顾客须要在电影院B买票,咱们简化一个流程:
若是单从锁影响并发的方面考虑,应该若是规划他们之间的顺序呢?
根据两阶段锁协议,不论你怎样安排语句顺序,全部的操做须要的行锁都是在事务提交的时候才释放的。因此,若是你把语句 2 安排在最后,好比按照 三、一、2 这样的顺序,那么影院帐户余额这一行的锁时间就最少。这就最大程度地减小了事务之间的锁等待,提高了并发度。
Innodb的锁按照功能分,能够分为共享锁(读锁)和排他锁(写锁)。
对于普通 select 语句,InnoDB 不会加任何锁,事务能够经过如下语句显式给记录集加共享锁或排他锁:
接下来咱们分析一下RC隔离级别下的锁的状况。 咱们分为三种状况
测试表咱们沿用上面的t16表,其中a惟一索引,b无索引,c非惟一索引。清空表数据,并在表中加入几条数据
truncate table t16;
insert into t16(a,b,c) values (1,1,1),(2,2,2),(3,3,3),(4,4,3);
复制代码
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE b=1 for update;正常 |
|
SELECT * FROM t16 WHERE b=3 for update;等待 |
|
commit; | session1结束,结果正常返回 |
commit; |
表面看来,session1只给b=1加了排他锁,实际在没有索引的清空下,他给整张表加了排他锁。下图是加锁的逻辑图:
没有索引的状况下,InnoDB 的当前读会对全部记录都加锁。因此在工做中应该特别注意 InnoDB 这一特性,不然可能会产生大量的锁冲突。
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE a=1 for update;正常 |
|
SELECT * FROM t16 WHERE a=2 for update;正常 |
|
SELECT * FROM t16 WHERE c=1 for update;等待 |
|
commit; | session1结束,结果正常返回 |
commit; |
session1 给了 a=1 这一行加了排他锁,在 session2 中请求其余行的排他锁时,不会发生等待;可是在 session2 中请求 a=1 这一行的排他锁时,会发生等待。看下图:
若是查询的条件是惟一索引,那么 SQL 须要在知足条件的惟一索引上加锁,而且会在对应的聚簇索引上加锁。
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE c=3 for update;正常 |
|
SELECT * FROM t16 WHERE a=2 for update;正常 |
|
SELECT * FROM t16 WHERE a=4 for update;等待 |
|
commit; | session1结束,结果正常返回 |
commit; |
咱们在知足条件 c=3 的数据上加了排他锁,如上面结果,就是第 三、4 行。所以第 一、2 行的数据没被锁,而 三、4 行的数据被锁了。以下图:
若是查询的条件是非惟一索引,那么 SQL 须要在知足条件的非惟一索引上都加上锁,而且会在它们对应的聚簇索引上加锁。
在研究RR隔离级别下,咱们先来看一道小题目:
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 设置会话隔离级别为 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE c=3 for update;Result1 |
|
INSERT INTO t16(a,b,c) VALUES(5,5,3); | |
commit; | |
SELECT * FROM t16 WHERE c=3 for update;Result1 |
|
commit; |
Result1和Result2分别是多少?
咱们通过测试Result1为:
咱们通过测试Result2为:
咱们发现Result2比Result1多了一行,这也就是咱们所讲的幻读。
那为何会出现幻读,咱们来看下面一张图:
从图中能够看出,RC 隔离级别下,只锁住了知足 c=3 的当前行,而不会对后面的位置(或者说间隙)加锁,所以致使 session1 的写入语句能正常执行并提交。
为了解决幻读问题,RR隔离级别引入了间隙锁。
咱们从新建一张表,并插入数据:
CREATE TABLE `t17` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL COMMENT '惟一索引',
`b` int(11) NOT NULL,
`c` int(11) NOT NULL COMMENT '普通索引',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_a` (`a`) USING BTREE,
KEY `idx_c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
INSERT INTO t17(id, a, b, c) VALUES(1,1,1,1),(2,2,2,2),(4,4,4,4),(6,6,6,4);
复制代码
咱们同样分为三种状况:
session1 | session2 | session3 |
---|---|---|
set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ |
begin; | begin; | begin; |
SELECT * FROM t17 WHERE b=1 for update;正常 |
||
SELECT * FROM t17 WHERE a=4 for update;等待 |
||
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);等待 |
||
commit; | session1结束,结果正常返回 |
session1结束,结果正常返回 |
rollback; | rollback; |
其加锁的逻辑图以下:
如图,全部记录都有 X 锁,除此以外,每一个 GAP 也被加上了 GAP 锁。所以这张表在执行完 select * from t17 where b=1 for update; 到 commit 以前,除了不加锁的快照读,其它任何加锁的 SQL,都会等待,若是这是线上业务表,那就是件很是恐怖的事情了。
RR 隔离级别下,非索引字段作条件的当前读不但会把每条记录都加上 X 锁,还会把每一个 GAP 加上 GAP 锁。再次说明,条件字段加索引的重要性。
session1 | session2 | session3 |
---|---|---|
set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ |
begin; | begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;正常 |
||
SELECT * FROM t17 WHERE a=4 for update;等待 |
||
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,4);等待 |
||
commit; | session1结束,结果正常返回 |
session1结束,结果正常返回 |
rollback; | rollback; |
其加锁的逻辑图以下:
与 RC 隔离级别下的图类似,可是有个比较大的区别是:RR 隔离级别多了 GAP 锁。
如上图,首先须要考虑哪些位置能够插入新的知足条件 c=4 的项:
为了保证这几个区间不会插入新的知足条件 c=4 的记录,MySQL RR 隔离级别选择了 GAP 锁,将这几个区间锁起来。 而上面,咱们插入了(id=5, c=4)的数据,因此被锁住了。
RR 隔离级别下,非索引字段作条件的当前读不但会把每条记录都加上 X 锁,还会把每一个 GAP 加上 GAP 锁。再次说明,条件字段加索引的重要性。
所以以惟一索引为条件的当前读,不会有 GAP 锁。因此 RR 隔离级别下的惟一索引当前读加锁状况与 RC 隔离级别下的惟一索引当前读加锁状况一致。这里就再也不实验了。
session1 | session2 |
---|---|
set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ |
begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;正常 |
|
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);等待 |
|
commit; | session1结束,结果正常返回 |
rollback; |
咱们发现插入语句居然等待了,为何? 由于间隙锁锁的是位置,根据上面这张图,session1的sql语句锁的范围是 “(2,无穷大]”,致使凡在这个区间都会被锁住。
因此间隙锁的引入会致使锁的范围更大,影响并发。 有不少公司使用RC隔离级别+日志ROW模式。
1)行级锁是两阶段的锁,一个事务中在须要时锁住,在commit时释放锁。
2)行级锁按照功能分为共享锁和排他锁。
3)在RC隔离级别下,会对数据加上纪录锁,但会有幻读的问题
4)在RR隔离级别下,会对数据加上纪录锁和间隙锁,解决了幻读的问题,但影响并发。
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而致使恶性循环的现象。 咱们使用t17表来举个栗子,看如何会产生死锁:
session1 | session2 |
---|---|
set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 设置会话隔离级别为 RR*/ |
begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;sql1正常 |
SELECT * FROM t17 WHERE c=1 for update;sql2正常 |
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);sql3等待 |
|
INSERT INTO t17(id, a, b, c) VALUES(7,7,7,1);sql4报死锁错误 |
咱们来分析一下,刚开始执行sql1时 session1锁住的范围时 (2,正无穷],执行sql2后session2锁住范围时(负无穷大,2) , sql3范围在session1锁的范围内,因此等待session1锁释放,而此时,sql4又在等待session2的释放。也就是session1和sesison2相互等待各自释放。因此就成了死锁了。
InnoDB 中解决死锁问题有两种方式:
通常咱们采用第一种方案,由于第二种方案等待50秒时间过长,业务上没法接受,若是把时间调少,好比1秒,又有可能会误杀一些正常的锁。但第一种方案也会形成额外的cpu开销。
1) 死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而致使恶性循环的现象。 2) 咱们通常使用InnoDB自带的检索机制来检索是否死锁。
有三张表,用户帐户表,商品库存表,订单表。如今用户须要买商品,购买流程是
咱们改如何操做这些表来完成上面的购买流程,而且支持更多的并发?