Mysql的锁和隔离机制(InnoDB引擎)

对于DB来说,经常会面对并发问题,但是开发的时候DB总是能很好的解决并发的问题。那么面对并发DB是怎么进行控制的呢?之前一段时间总是对Mysql的锁机制概念十分模糊,什么时候加锁?加什么锁?锁住之后会是怎么样?

需要明确的点####

首先,锁是为了解决数据库事务并发问题引入的特性,在Mysql中锁的行为是和mysql隔离机制有关的,毕竟锁是用来解决DB的隔离性和一致性的。并不是任何操作都是需要加锁的,读操作是不加锁的,当然也可以显式的加锁(lock in share mode或for update)。

Mysql锁的类型####

Mysql因为有很多种存储引擎,导致它的实现也是五花八门,但是最常用的就应该是MyISAM和InnoDB了。对于两者的区别之前也写过,其中有一点是MyISAM锁级别是表级而InnoDB的锁级别是行级(当然InnoDB也有表级锁)。mysql锁的类别如下:
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
不同的锁粒度决定了不同引擎的应用场景,我们最常用的表级锁的引擎是MyISAM和InnoDB,行级引擎是InnoDB。至于页级锁的引擎常用的是Berkeley DB。

Mysql的锁####

Mysql的锁主要为两种:共享锁(S Lock)和排他锁(X Lock)。从字面上我们可以理解,共享锁就是多个事务可以共享,互相兼容。而排他锁则是多个事务不兼容互相排斥。
如果一个事务T1获得了r行的共享锁,那么另外一个事务T2可以立即获得r的共享锁,这种情况称为“锁兼容”。如果有T3想获得r行的排他锁必须等到T1、T2释放r行的共享锁,这种称为“锁不兼容”,下表对应的是锁兼容性:

Paste_Image.png

可以看到只有共享锁是兼容的,也就是说读请求和读请求之间是没有影响的。
InnoDB为了支持在不同粒度上加锁操作,InnoDB支持另一种加锁机制——意向锁。意向锁的意思很简单,就是有意愿进行加锁。
意向共享锁(IS Lock):事务想要获取一张表中的某几行共享锁。
意向排他锁(IX Lock):事务想要获取一张表中的某几行的排它锁。
由于InnoDB支持的行级别的锁,因此意向锁其实不会阻塞除全表扫描意外的任何请求。意向锁的兼容性如下所示:

Paste_Image.png

意向锁和意向锁之间是完全兼容的,但是意向锁和共享锁以及排它锁可能是有互斥性的。因为意向锁的锁粒度是表级锁,所以在全表扫描是往往会对表加锁,那么此时就会发生锁冲突。
之前一直不明白意向锁到底是干什么的,相信很多人和我一样,后来查了很多资料才知道,有一个很形象的例子:
如果你家小区有一个保安,那么就能避免经常有人去按你家的门锁...
保安就是意向锁,它能避免经常有请求去请求行级锁,因为访问行级锁也是有一定开销的。

上面说的东西概念性都比较强,但是千万别被误导,因为上面的概念在实际的查询中不一定全都会使用,例如mysql的读操作,通常是不会加锁的(和隔离机制有关),也就是说通常的读操作是不加锁的,而是通过mvcc去解决的,对于通常的写请求,insert、update、delete通常会加行锁、间隙锁或表锁(这和索引是有关系的),这些锁通常是排他的,会阻塞其他的事务写事务。具体的情况需要结合隔离机制。

Mysql的隔离性####

隔离性是指一个事务所做的修改在最终提交之前,对其他的事务是不可见的。
mysql的隔离性分为四个隔离级别,不同的隔离级别有不同的特点和实现:
1.Read Uncommitted(脏读):从隔离级别的名称可知,事务可以读取到其他没有commit的事务的修改,所以称为脏读,因为读取到了本来不应该读到的记录,此事务隔离级别一般是不会用的,因为如果后面另一个事务rollback掉了,岂不是悲剧了?
2.Read Committed(提交读,也叫不可重复读):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。对于此级别的隔离,比较上面的脏读是会严格一些的,例如事务1开始查询了一条记录,但是随后另一个事务2修改了本条记录,此时事务1再次进行读取,此时是读取不到的因为事务2没有进行commit,随后事务2commit,事务1再次读取,可以读到最新修改后的记录。这比脏读更加严格了一些,因为读取不到未提交的数据,但是此种隔离级别在同一个事务(事务1中)两次读取,读取到了不同的结果,这也就是不可重复读。
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
一个例子:

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `stu_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_student_id` (`stu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
|  1 | 语文 |      1 |
|  2 | 数学 |      2 |
|  3 | 英语 |      1 |
+----+------+--------+
3 rows in set

上面是student表内的数据,接下来设置事务隔离级别为RC
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';
接下来测试一下update的行锁:

T1 T2
update student set name = '生物' where stu_id = 2;
update student set name = '生物' where stu_id = 2;
更新成功 阻塞
commit
更新成功

上面的update例子说明,在更新记录的时候会对此记录加行锁,在事务没有commit之前不会释放锁,所以事务2的更新会阻塞等待事务1的排它锁,当事务1Commit后,行锁释放事务2获得行锁,更新成功。
其实mysql的锁机制是通过对索引加锁,但是一旦更新不走索引会怎么样,答案是会全表扫描,锁表。所以在更新的时候尽量走索引,避免不必要的麻烦,具体这种索引和锁的问题推荐一篇博客:http://hedengcheng.com/?p=771#_Toc374698322
接下来实验一下RC基本写的不可重复读:
事务1:

mysql> begin;
Query OK, 0 rows affected

mysql> select * from student where stu_id = 2;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
|  2 | 生物 |      2 |
+----+------+--------+
1 row in set

事务2:

mysql> begin;
Query OK, 0 rows affected

mysql> update student set name = '地理' where stu_id = 2;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected

接下来事务1再次查询:

mysql> select * from student where stu_id = 2;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
|  2 | 地理 |      2 |
+----+------+--------+
1 row in set

上述过程可见,带事务1的一个事务中,两次请求得到了不同的结果,就导致了不可重复读的现象。

3.Repeatable Read(可重读或者叫幻读):RR解决了脏读的问题,该级别保证了在同一个事务中多次读取同样记录的结果是一致的。
例子和上面RC中的例子一样,只不过在事务2提交时,事务1再次查询是看不到事务1更新的记录的,所以叫可重复读,但是理论上这种方式只能解决更新问题,但是解决不了新增的问题,因为无论RC还是RR,mysql都是通过Mvcc(Multi-Version Concurrency Control )机制去实现的。
Mvcc是多版本的并发控制协议,它和基于锁的并发控制最大的区别和优点是:读不加锁,读写不冲突。它将每一个更新的数据标记一个版本号,在更新时进行版本号的递增,插入时新建一个版本号,同时旧版本数据存储在undo日志中。
而对于读操作,因为多版本的引入,就分为快照读和当前读。快照读只是针对于目标数据的版本小于等于当前事务的版本号,也就是说读数据的时候可能读到旧的数据,但是这种快照读不需要加锁,性能很高。当前读是读取当前数据的最新版本,但是更新等操作会对数据进行加锁,所以当前读需要获取记录的行锁,存在锁争用的问题。
RC和RR都是基于Mvcc实现,但是读取的快照数据是不同的。RC级别下,对于快照读,读取的总是最新的数据,也就出现了上面的例子,一个事务中两次读到了不同的结果。而RR级别总是读到小于等于此事务的数据,也就实现了可重读。
下面是快照读和当前读的常见操作:

  1. 快照读:就是select
    select * from table ....;
  2. 当前读:特殊的读操作(加共享锁或排他锁),插入/更新/删除操作,需要加锁。
    select * from table where ? lock in share mode;
    select * from table where ? for update;
    insert;
    update ;
    delete;

其实Mysql实现的Mvcc并不纯粹,因为在当前读的时候需要对记录进行加锁,而不是多版本竞争。下面是具体操作时的Mvcc机制:

  1. SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
  2. INSERT时,保存当前事务版本号为行的创建版本号
  3. DELETE时,保存当前事务版本号为行的删除版本号
  4. UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

上面说明了RR是如何解决重读问题,但是众所周知,RR有一个致命的问题就是幻读,即只能解决另一个事务2更新对事务1不可见的问题,但是当事务2新插入一行数据的时候,事务1还是可见,这就是幻读问题。但是在实际使用中,我们发现并没有发生“幻读”问题。那么,Mysql是如何解决幻读问题的呢?

我们分两个方面说:

1.快照读:对于快照读,其实是不会出现幻读问题的,通过上面我们得知,select时只会读取小于等于当前事务版本的行,但是新行的版本号是高于读事务的,那么新插入的行对之前的读事务是不可见的。

2.当前读:因为当前读,读到的往往是最新的行数据,但是对于事务1更新了一行,同时事务2插入了一个新行(利用一个非唯一索引进行更新),那么会利用gap锁去控制新行的插入来避免这个问题。一个例子看一下:

首先开启事务A:

mysql> begin;
Query OK, 0 rows affected

mysql> select * from student where stu_id =3;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
|  2 | 化学 |      3 |
+----+------+--------+
1 row in set
mysql> update student set name = "物理" where stu_id = 3;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

接下来开启事务B:

mysql> begin;
Query OK, 0 rows affected

mysql> insert into student(id,name,stu_id) values (5,"历史",3);
Query OK, 1 row affected

我们可以看到,事务A在更新之后,事务B进行插入操作的时候会阻塞,但是这里使用的不是行锁,这就是因为rr隔离模式下,mysql使用的是next-keylocking机制防止“当前读”的幻读问题。如果不阻塞新插入的数据,那么就会导致更新之后,再次查询时会发现部分数据没有更新,本意是按照索引更新所有的行,但是新插入的行没有更新,这就会令我们很奇怪。

那需要先说说Mysql里面特殊的锁——Next-Key锁:
Next-Key锁是行锁和Gap锁(间隙锁)的合体(可以理解为二者相加,因为gap锁是开区间的,加上行锁正好是闭区间)。间隙锁,顾名思义,是对一个间隙进行加锁,间隙是索引的间隙,也就是说,更新的时候必须走索引,否则会将全表锁住。导致其他所有的写操作全部阻塞。next-key锁主要是针对非唯一索引,因为唯一索引和主键索引每次只会定位到单条记录,所以不需要next-key锁,下面盗一张图来理解下:

Paste_Image.png

当按照id(非唯一索引,不是主键,主键是name)进行更新或删除的时候会先对id索引进行加锁,但加的是next_key锁。因为在RR隔离级别下,需要防止“当前读”的幻读问题,加上next-keylock之后,在[6-10]区间和[10-11]区间进行插入时会阻塞,因为已经加了next-key锁,为什么用next-key锁?因为新增加的记录只能在10的左边和10的右边或者就是10。那么锁住范围后就能保证防止“幻读”。

4.Serializable(可串行化):这个隔离级别,在并发效果上最差的,因为读加共享锁,写加排他锁,读写互斥。也就是说此级别下select是需要加锁的。此模式下可以保证数据安全,适用于并发比较低,同时数据安全性要求比较高的场景。

总结:mysql的锁机制和事务隔离级别有关。并不是说所有的读操作都不加锁,写操作加锁,加什么锁也和索引类型、有无索引有关。

国庆纠结了几天,总结了一下,如果有什么错误还请指出。还有明天得上班了=_=,I am angry~

参考:
https://book.douban.com/subject/23008813/
https://book.douban.com/subject/5373022/
http://tech.meituan.com/innodb-lock.html
http://hedengcheng.com/?p=771

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容