美文网首页数据库Java学习笔记程序员
Mysql的锁和隔离机制(InnoDB引擎)

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

作者: 一只小哈 | 来源:发表于2016-10-08 00:12 被阅读893次

    对于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

    相关文章

      网友评论

      本文标题:Mysql的锁和隔离机制(InnoDB引擎)

      本文链接:https://www.haomeiwen.com/subject/eunkyttx.html