欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

MySQL事务与锁

程序员文章站 2022-06-04 13:37:29
...

事务是一组原子性的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 modeselect ... 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语句,只会加插入意向锁,这样别的事务也能插入。