MySQL事务与锁
事务是一组原子性的SQL语句,或者说是一个独立的工作单元。
- 如果数据库引擎(比如InnoDB)能够成功地对数据库应用这组SQL语句,那么就执行;
- 如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有语句都不会执行,也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
开始一个事务使用begin或者start transaction,结束事务使用commit和rollback。
InnoDB的锁
事务是基于MVCC实现的多版本并发读控制系统,锁是MySQL提供的另一种安全性的保证。在了解事务前,先了解Mysql的锁。
准备工作
首先,初始化一张表以供后续测试,打开两个mysql的命令行终端,分别称为terminalA、terminalB。在其中任意一个终端中建立一张表,插入1条初始的数据:
create database testdb;
use testdb;
CREATE TABLE IF NOT EXISTS `test_tbl`(
`test_id` INT UNSIGNED AUTO_INCREMENT,
`test_title` VARCHAR(100) NOT NULL,
`test_author` VARCHAR(40) NOT NULL,
`submission_date` DATE,
PRIMARY KEY ( `test_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
随便插入几条数据,表中目前有以下数据。
mysql> select * from test_tbl ;
+---------+-------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+-------------+-------------+-----------------+
| 12 | testX | txB | NULL |
| 13 | testX | txB | NULL |
| 14 | testX | txB | NULL |
| 15 | testX | txB | NULL |
| 16 | testX | txB | NULL |
| 18 | testX | txB | NULL |
| 19 | testX | txB | NULL |
| 20 | testPlus | txA | NULL |
| 21 | testPlus | txA | NULL |
| 22 | testPlus | txA | NULL |
| 23 | testPlusMax | admin | NULL |
| 24 | testPlusMax | admin | NULL |
| 25 | testPlusMax | admin | NULL |
+---------+-------------+-------------+-----------------+
13 rows in set (0.00 sec)
索引避免表锁
InnoDB行锁是基于索引实现的。行锁锁定的是索引,而不是记录本身。
一般常说的共享锁与排他锁都是行锁,只有被锁定的时的where条件没有对应索引时,它们才是表锁。为了避免出现表锁,影响测试,创建两个索引。
alter table test_tbl add index title (test_title);
alter table test_tbl add index author (test_author);
表锁的原因:假设查询以title为查询条件,且未建立title的索引,当事务A对title=“testX”的这些行加锁后,事务B如果需要找到title=“testX”的这些行,就需要通过一级索引全表扫描,但因为事务A已经对这些行加了排他锁,导致事务B全表扫描执行不下去,事务B阻塞。
测试用例如下:在事务B中查询其它行,阻塞了,无法加共享锁,证明了整张表都被事务A加了排他锁。
// 删除索引title
drop index title on test_tbl;
// 事务A
begin;
update test_tbl set test_title="viptest" where test_title="testX";
// 事务B
begin;
select * from test_tbl where test_title="testPlus" lock in share mode;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
但是如果将查询条件换成有索引的author字段,就不会阻塞:
// 事务A
rollback;
begin;
update test_tbl set test_author="auth3" where test_author="txA";
// 事务B
rollback;
begin;
select * from test_tbl where test_author="txB" lock in share mode;
+---------+------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+------------+-------------+-----------------+
| 12 | testX | txB | NULL |
| 13 | testX | txB | NULL |
| 14 | testX | txB | NULL |
| 15 | testX | txB | NULL |
| 16 | testX | txB | NULL |
| 18 | testX | txB | NULL |
| 19 | testX | txB | NULL |
+---------+------------+-------------+-----------------+
共享锁
本节的共享锁仅针对行级锁,下一节的排他锁也仅针对行级锁。
语句:select ... lock in share mode
- 不允许其它事务加入排他锁
- 不允许其它事务更新、删除被共享锁锁定的行
- 允许其它事务加共享锁
- 允许其它事务读整张表
- 允许其它事务删除、更新未被锁定的行
- 允许其它事务插入,但是其它事务插入后,当前事务就不能再次执行
select ... lock in share mode
了。
示例:
首先在事务A中,对test_author=txB
的行加共享锁,但是不提交,分别验证上述几个点。
// 事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_tbl where test_author="txB" lock in share mode;
+---------+------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+------------+-------------+-----------------+
| 12 | testX | txB | NULL |
| 13 | testX | txB | NULL |
| 14 | testX | txB | NULL |
| 15 | testX | txB | NULL |
| 16 | testX | txB | NULL |
| 18 | testX | txB | NULL |
| 19 | testX | txB | NULL |
+---------+------------+-------------+-----------------+
7 rows in set (0.00 sec)
1、不允许其他事物加排他锁
在事务B,尝试使用for update添加排他锁,失败。
// 事务B
mysql> select * from test_tbl where test_author="txB" for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
2 、不允许其它事务删除、更新被锁定的行
在事务B,尝试删除、更新被共享锁锁定的行(test_author == txB
),失败。
// 事务B
mysql> update test_tbl set test_author="updateAuthor" where test_author="txB";
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> delete from test_tbl where test_author="txB";
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
3、允许其它事务加共享锁
在事务B中,加共享锁并读取整张表,成功。
// 事务B
mysql> select * from test_tbl lock in share mode;
+---------+-------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+-------------+-------------+-----------------+
| 12 | testX | txB | NULL |
| 13 | testX | txB | NULL |
| 14 | testX | txB | NULL |
| 15 | testX | txB | NULL |
| 16 | testX | txB | NULL |
| 18 | testX | txB | NULL |
| 19 | testX | txB | NULL |
| 20 | testPlus | txA | NULL |
| 21 | testPlus | txA | NULL |
| 22 | testPlus | txA | NULL |
| 23 | testPlusMax | admin | NULL |
| 24 | testPlusMax | admin | NULL |
| 25 | testPlusMax | admin | NULL |
+---------+-------------+-------------+-----------------+
13 rows in set (0.00 sec)
4、允许其他事务读
在事务B中,读取整张表,成功,所以,共享锁不影响读操作。
// 事务B
mysql> select * from test_tbl;
+---------+-------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+-------------+-------------+-----------------+
| 12 | testX | txB | NULL |
| 13 | testX | txB | NULL |
| 14 | testX | txB | NULL |
| 15 | testX | txB | NULL |
| 16 | testX | txB | NULL |
| 18 | testX | txB | NULL |
| 19 | testX | txB | NULL |
| 20 | testPlus | txA | NULL |
| 21 | testPlus | txA | NULL |
| 22 | testPlus | txA | NULL |
| 23 | testPlusMax | admin | NULL |
| 24 | testPlusMax | admin | NULL |
| 25 | testPlusMax | admin | NULL |
+---------+-------------+-------------+-----------------+
13 rows in set (0.00 sec)
5、允许其它事务删除、更新未被锁定的行
尝试删除其它行(test_author != txB
),可以看出是删除成功了,最后,回滚以保留这些数据供后续使用。这也证明了共享锁是行级锁。
// 事务B
mysql> update test_tbl set test_author="updateAuthor" where test_author="txA";
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3 Changed: 3 Warnings: 0
mysql> delete from test_tbl where test_author="txA";
Query OK, 0 rows affected (0.00 sec)
mysql> rollback;
Query OK, 0 rows affected (0.05 sec)
6 允许其它事务插入
事务B中成功插入了一条数据。
// 事务B
mysql> insert into test_tbl (test_title, test_author) values ("testshareLockInsert","sharelock");
Query OK, 1 row affected (0.00 sec)
虽然能够这么做,但是可能会产生死锁:首先分别在事务A与事务B中拿到共享锁、分别插入一条数据:
// 事务A
mysql> insert into test_tbl (test_title, test_author) values ("testshareLockInsert","tx");
Query OK, 1 row affected (0.00 sec)
// 事务B
mysql> insert into test_tbl (test_title, test_author) values ("testshareLockInsert","tx");
Query OK, 1 row affected (0.00 sec)
最后,在事务A中查询,会开始阻塞,然后在事务B中查询,就出现死锁。
// 死锁,InnoDB要求事务A或B主动释放锁
mysql> select * from test_tbl where test_author="tx" lock in share mode;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
出现死锁的原因是:事务A、B各插入一条数据后,事务A查询数据,事务A想通过lock in share mode
加共享锁,发现有一行记录加了排他锁(事务B新增的数据),所以事务A的查询会阻塞,
在阻塞超时前,事务B查询数据,发现有一条记录被事务A加了排他锁,所以也会阻塞,这样,事务A与事务B都在等待对方释放锁,InnoDB检测到死锁后,会主动选择其中一个它认为代价更小的事务回滚。
死锁的出现有多种情况,但原理都是一样的,即某一行记录被事务X锁定,事务M尝试对该行加锁,因此事务M阻塞,事务X又尝试获取事务M持有的锁,所以陷入了彼此等待锁的场景,即是死锁。
排他锁
语句: 删除、更新、修改、带有for update后缀的查询,MySQL都会加入排他锁。
select ... for update
delete from table where ...
update table set ... where...
insert into table () values ...
- 不允许其他事务加共享锁、排他锁
- 不允许其他事务修改加锁的行
小结
无论是共享锁还是排他锁,它们的不同点是:
- 共享锁允许其它事务加共享锁,而排他锁不允许
- 共享锁允许其它事务加共享锁读,而排他锁不允许其它事务加锁读,归根结底,还是与第一个不同点一样。
它们的相同点是:
- 都不允许其它事务对加锁的行进行修改
- 都允许其他事务不加锁的读
- 都允许其它事务插入,前提是共享锁和排他锁都是行级锁,如果锁被加在表上(加锁条件未建立二级索引),不允许插入。
它们都会有死锁的风险,且底层原理一致:某一行记录被事务X锁定,事务M尝试对该行加锁,因此事务M阻塞,事务X又尝试获取事务M持有的锁,所以陷入了彼此等待锁的场景,即是死锁。
为了减少死锁出现,有以下优化措施:
- 在读取数据时,尽量少的使用共享锁、排他锁
- 事务不要写在应用层,避免一个事务的时间过长
- 在事务中使用索引条件,没有索引条件的加锁,会锁住整张表。
新特性
Mysql8新特性:
- 新的动态变量innodb_deadlock_detect,可以禁用死锁检查。在高并发的系统中,无数个线程等待同一个锁,死锁检查可能会引起系统宕机。有时,禁用死锁检查更有效。当死锁发生时,可以依赖innodb_lock_wait_timeout设置让事务回滚。
- 对于select…for share和select…for update 锁读语句,支持nowait和skip locked选项。Nowait表示如果请求的行被去其他事务锁住了立即返回。SKIP LOCKED则会从结果集中移除上锁的行。
InnoDB其它行锁
记录锁
记录锁称为Record Lock,针对特定的某一行记录,所以记录锁也属于行锁。前文介绍的共享锁与排他锁,表现出的都是记录锁的特征,除非使用不当(比如加锁的条件无对应索引)。
InnoDB还有:间隙锁、插入意向锁,它们针对的是区间,这两个锁是InnoDB引擎在RR或序列化事务级别主动加的。
间隙锁
间隙锁:锁定一个范围,但不包括记录本身。
产生场景(范围查询):
- 用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB不仅会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁
- 使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁
对于索引B+树叶子节点的数据是顺序排列的,两个叶子之间的数据称为间隙,比如有一张用户表,有字段age,类型为int,插入两条age为10和20的数据,那么1020之间即为间隙。间隙锁是用来避免其它事务在1020之间插入数据的,目的是避免幻读。
MySQL的查询有两类:
- 常规select:
select .. where ...
- 带锁select:
select .. where ... in share mode
、select ... where ... for update
在MySQL的RR事务隔离级别中(见下节),读取方式如下:
- 针对常规select,采用快照读的方式:第一次读时建立快照,之后无论其他事务是否新增/删除了记录,读取的永远都是那个快照对应的数据,可能会存在幻读的问题。
- 针对带锁select,当读取数据记录时,除了锁住记录本身(Record Lock),同时将符合条件的间隙锁起来(Gap Lock),预防第一次读和第二次读的期间,其他事务往间隙插入了新数据,可以避免幻读的问题。
**间隙锁仅在可重复读级别及串行化级别有效。**串行化级别我们基本不会使用,所以仅介绍下在可重复读级别下的间隙锁。
// 测试用表
CREATE TABLE IF NOT EXISTS `gaptest`(
`test_id` INT UNSIGNED AUTO_INCREMENT,
`num` INT,
PRIMARY KEY ( `test_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
alter table gaptest add index num (num);
insert into gaptest (num) values (1);
insert into gaptest (num) values (2);
insert into gaptest (num) values (5);
insert into gaptest (num) values (10);
insert into gaptest (num) values (100);
select * from gaptest;
+---------+------+
| test_id | num |
+---------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 5 |
| 4 | 10 |
| 5 | 100 |
+---------+------+
5 rows in set (0.00 sec)
表结构如上,共有5条数据,Mysql默认事务隔离级别RR支持间隙锁,直接开始测试。
按照间隙锁的规定,锁的范围是开区间。对于表gaptest
,间隙锁的分布区间:(-∞, 1)、(1, 2)、(2,5)、(5,10)、(10,100)、(100,+∞)
唯一索引
对于唯一索引,间隙锁是不存在的。只有记录锁。
普通索引
对于普通索引,间隙锁的范围又根据等值查询与范围查询有所不同。
等值查询
- 若查询结果不为空,则锁定该记录左右两侧的开区间。
- 若查询结果为空,则锁定该记录所在的开区间。
示例:
下表为查询结果不为空时,7、20插入失败,101和4插入成功,因此,num=10的左右两个区间被锁定,即(5,10)、(10,100)
时序 | 事务A | 事务B |
---|---|---|
1 | begin; | |
2 | select * from gaptest where num=10 for update; | begin; |
3 | mysql> insert into gaptest (num) values (7); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
|
4 | mysql> insert into gaptest (num) values (20); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
|
5 | mysql> insert into gaptest (num) values (101); Query OK, 1 row affected (0.12 sec) |
|
6 | mysql> insert into gaptest (num) values (4); Query OK, 1 row affected (0.13 sec) |
|
7 | rollback; | rollback; |
下表为查询结果为空时,4、11插入失败,6和8插入成功,因此,num=7所在的区间(5,10)被锁定。
时序 | 事务A | 事务B |
---|---|---|
1 | begin; | begin; |
2 | mysql> select * from gaptest where num=7 for update; Empty set (0.00 sec) |
|
3 | mysql> insert into gaptest (num) values (4); Query OK, 1 row affected (0.00 sec) |
|
4 | mysql> insert into gaptest (num) values (11); Query OK, 1 row affected (0.00 sec) |
|
5 | mysql> insert into gaptest (num) values (8); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
|
6 | mysql> insert into gaptest (num) values (6); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
|
7 | rollback; | rollback; |
范围查询
范围查询时,间隙锁范围就是查询范围。
示例如下,在事务B中插入num=4、num=6、num=7
的数据,只有num=7
的数据插入成功,值得注意的是,num=6
的行虽然也被锁定,但已是行锁的范畴了。
// 事务A
select * from gaptest where num < 6 lock in share mode;
// 事务B
mysql> insert into gaptest (num) values (4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into gaptest (num) values (6);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into gaptest (num) values (7);
Query OK, 1 row affected (0.00 sec)
RR级别下间隙锁才会生效
其实,序列化级别下,间隙锁也是生效的,因为这个事务隔离级别效率过低,所以此处不验证了,只比较RR与RC级别下的间隙锁。RR级别的效果前面已经看到了,将事务隔离级别设置为RC,进行对比验证。
可以看出,在RC级别下,事务A加的共享锁(换成排他锁也是一样的)不影响数据的插入。所以间隙锁没有生效。因此,只有RR或更高的序列化级别,间隙锁才是生效的。
set transaction_isolation='read-uncommitted';
// 事务A
begin;
select * from gaptest where num = 6 lock in share mode;
select * from gaptest where num < 6 lock in share mode;
// 事务B
begin;
mysql> insert into gaptest (num) values (6);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.13 sec)
mysql> insert into gaptest (num) values (4);
Query OK, 1 row affected (0.25 sec)
mysql> insert into gaptest (num) values (7);
Query OK, 1 row affected (0.16 sec)
mysql> insert into gaptest (num) values (8);
Query OK, 1 row affected (0.13 sec)
Next-key lock
Next-key lock 等于 行锁+间隙锁。
插入意向间隙锁
间隙锁导致事务B等待事务A释放间隙锁后,才能在10~20之间插入数据,插入意向锁是一种特殊的间隙锁,作用是为了提高并发插入的性能
-
插入意向锁
是一种特殊的间隙锁
——间隙锁
可以锁定开区间内的部分记录。 -
插入意向锁
之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键
、唯一索引
)不冲突,那么事务之间就不会出现冲突等待。
当一条记录执行插入时,会首先获取插入意向锁,获取插入意向锁的前提是,没有间隙锁,因此,只要在delete、update操作上加间隙锁,在insert操作上加插入意向锁,就既能保证不会幻读,又能保证插入的效率。
即:对于select ... for update、select ... lock in share mode、delete、update
语句,会加间隙锁阻止别的事务插入,对于insert
语句,只会加插入意向锁,这样别的事务也能插入。
事务隔离级别
MySQL有四种事务隔离级别:Read uncommited、Read commited、Repeatable read、Serializable
默认的是Repeatable read
。
脏读
在事务隔离级别为Read uncommited
时,事务A可以读取到其他事务未提交的数据。其他事务如果最终没有提交,回滚了,那么事务A读取到的数据就是无效的脏数据,称为脏读。
示例如下,事务B先开始,然后更新test_author
为“txB”
,事务A设置隔离级别为Read uncommited
,事务A开始、查询、提交,然后事务B回滚。
//时序 // 终端A对应事务A // 终端B对应事务B
1 begin;
2 update test_tbl set test_author="txB";
3 set transaction_isolation='read-uncommitted';
4 begin;
5 select * from test_tbl;
6 commit;
7 rollback;
然后分别在终端A、B查看数据,可以看到数据不一致,在终端A的读取到的数据是脏数据。
// 终端A
mysql> select * from test_tbl;
+---------+------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+------------+-------------+-----------------+
| 1 | test1 | txB | NULL |
+---------+------------+-------------+-----------------+
1 row in set (0.00 sec)
// 终端B
mysql> select * from test_tbl;
+---------+------------+-------------+-----------------+
| test_id | test_title | test_author | submission_date |
+---------+------------+-------------+-----------------+
| 1 | test1 | txA | NULL |
+---------+------------+-------------+-----------------+
1 row in set (0.00 sec)
为了解决脏读的问题,可规定:只允许读取其他事务已提交的数据,这就是事务隔离级别:Read commited。而Read Uncommited,是数据库事务隔离的最低级别。
不可重复读
read-committed
存在的问题是不可重复读。如下,在事务A中前后两次获取到的数据不一致。
//时序 // 终端A对应事务A // 终端B对应事务B
1
2 set transaction_isolation='read-committed';
3 begin;
4 select * from test_tbl;
5 begin;
6 update test_tbl set test_author="txB";
7 commit;
8 select * from test_tbl;
9 commit;
为了解决不可重复读的问题,可以将隔离级别设置为:Repeatable read
幻读
Repeatable read
隔离级别存在的问题是有可能会出现幻读。幻读与可重复读是完全不同的概念,网络上很多博客将这两个概念以新增/删除其它行为幻读,以更新已有行为不可重复读,是错误的。
幻读:无论读取多少次,读取到的都是一样的,且读取结果是错误的。幻读强调的是不同的事务之间的读取差异,而不可重复读强调的是同一个事务中前后读的数据不一致
数据库中存在一些记录,其中appId为testappid
的数据有10条。
- 事务B查询appId为
testappid
的数据,发现不存在,事务B准备后续插入testappid
的记录 - 事务A插入一条appId为
testappid
的数据 - 事务B执行插入appId为
testappid
的数据,失败,此时事务B再去执行查询,发现确实没有appId为testappid
的数据,但就是插入不进去,这就是幻读。
为了解决幻读的问题,有以下两种方法:
- 在读取数据的事务开启时,锁定整张表。这就是事务隔离的*别:Serializable,序列化。
- 在Repeatable read级别下,添加共享锁或排他锁,innoDB引擎会主动加间隙锁,从而避免幻读。
所以,如果需要避免幻读,更好的方法还是采用第二种方案。
默认级别RR
MySQL默认的隔离级别是:Repeatable read。
一般地,实际项目中不会考虑Read uncommited 和 Serializable这两种隔离级别。原因为:
- Read uncommited导致出现脏读。
- Serializable对整张表加锁,性能差。
那么还剩下两种:Read committed、Repeatable read,为了方便,分别简称RC、RR。
采用RR的原因如下:
MySQL的binlog用来记录数据的更改,用于主从同步,有以下几种格式:
- statement:记录的是修改SQL语句
- row:记录的每行实际数据的变更
- mixed:statement与row的混合
statement方式在RC隔离级别下存在主从同步的bug。
如下表,事务执行顺序是:A开始、B开始、B提交、A提交
。Binlog要求SQL语句串行化,顺序以commit为序,两会话的执行时序是 B开始、B提交、A开始、A提交
,这就是slave上的执行顺序。
在RC隔离级别下,事务A可以读取到事务B已提交的数据。所以在master上,事务A先开始,先删后插,读取到一条记录,在slave上,先插后删,读取到nothing。出现了主从数据不一致的问题。
在RR隔离级别下,事务A进行删除操作后,Mysql会加一把test_id在0~200的间隙锁,所以事务B的插入会阻塞,最后还是A先提交,因此master上与slave的执行顺序均是:A开始、A结束、B开始、B结束
。
时间 | 事务A | 事务B |
---|---|---|
1 | set transaction_isolation='read-committed'; |
set transaction_isolation='read-committed'; |
2 | begin; |
|
3 | delete from test_tab where test_id < 200; |
|
4 | begin; |
|
5 | insert into test_tbl (test_title, test_author) values ("test3", "txB"); |
|
6 | commit; |
|
7 | commit; |
总结
共享锁与排他锁可以用于行、也可以用于表。用于表的话效率很差,所以,提到这两把锁一般指行级锁。本文也不去讨论Mysql的表锁。
1、而Mysql行锁是通过索引实现的,实际上锁的是索引,如果没有建立索引,会锁定一级索引树,因此,看起来像是一个表锁。所以,如果需要使用行锁提高效率,第一步是建立索引。
2、共享锁与排他锁区别:
- 不同点:共享锁可以和共享锁共存,而排他锁与共享锁不能共存。
- 相同点:共享锁与排他锁都不允许其他事务更新、删除被加锁的行,都允许其他事务更新、删除未加锁的行。共享锁与排他锁都允许所有的事务不加锁的读。
归根到底,可以简单的将排他锁理解为共享锁的升级版。二者都是加锁。
3、隔离级别:
- 事务具有ACID的特性,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性、一致性自不必说,隔离性通过MVCC实现,持久性通过redolog实现。
- 事务的隔离级别就是隔离性,它依赖于MVCC实现
- MySQL的默认隔离级别是RR(可重复读),原因是解决binlog的statement方式在RC隔离级别下存在主从同步的bug。
- 幻读强调的是不同事务同一条SQL的执行结果之间的差异,不可重复读强调的是同一个事务同一条SQL前后两次执行的结果之间的差异。
- 解决幻读的方式有两种:一种是序列化隔离级别,一种是在RR隔离级别下,对查询的SQL语句添加锁(必须),这样MySQL会主动添加间隙锁,阻塞别的事务写操作导致的幻读。显然,第二种方式更好。
4、log
简单区分下各种日志:
- redo log 保证持久性
- undo log 事务回滚
- binlog 主从同步
5、间隙锁:
唯一索引没有间隙锁,普通索引才会有间隙锁。原因是:普通索引允许出现值重复的情况,其他事务可以插入重复的值,这样当前事务读的数据就是幻读的(RR级别下),而唯一索引不存在幻读的问题,因为MySQL拒绝插入重复的值。间隙锁是用来解决RR级别下幻读的问题的,它的范围:
- 等值查询,若查询结果不为空,则锁定该记录左右两侧的开区间。若查询结果为空,则锁定该记录所在的开区间。
- 范围查询,间隙锁范围是查询范围减去记录锁,因为记录锁不属于间隙锁,记录锁+间隙锁=Next key lock。
6、插入意向锁
插入意向锁是一种间隙锁,用于并发插入,对于select ... for update、select ... lock in share mode、delete、update
语句,会加间隙锁阻止别的事务插入,对于insert
语句,只会加插入意向锁,这样别的事务也能插入。
上一篇: 微信小程序左上角返回刷新
下一篇: 大钟穴的主要作用和取穴方法