InnoDB锁

1、锁的类型

  官网把锁分成8类,如下:

    

  第一个是行级别的锁(包括共享锁和排他锁),第二个是表级别的锁(也叫意向锁,也有意向共享锁和意向排他锁),后面三个Record Locks、Gap Locks、Next-Key Locks,把它们叫做锁的算法,也就是分别在什么情况下锁定什么范围。

2、表锁和行锁的理解

*  表锁和行锁的比较

  1)加锁粒度  

    表锁,顾名思义,是锁住一张表;行锁就是锁住表里的一行数据。锁定粒度表锁肯定大于行锁。

  2)加锁效率

    表锁只需要直接锁住这张表就行了,而行锁,还需要在表里面去检索这一行数据,所以表锁的加锁效率更高。

  3)冲突的概率

    锁住一张表的时候,其他任何一个事务都不能操作这张表。但是锁住了表里面的一行数据的时候,其他的事务还可以来操作表里面的其他没有被锁定的行,所以表锁的冲突概率更大。

*  行锁

  1)共享锁——也叫读锁

    获取了一行数据的读锁以后,可以用来读取数据。多个事务可以共享一把共享锁。如果一个事务给一条记录加了共享锁之后,不要在当前事务中再对这一行进行更新操作,可能出现死锁的情况。

  加锁方式:可以用 select .......lock in share mode 方式手工加上一把读锁。

  释放锁方式:只要事务结束,锁就会自动释放,包括提交事务和回滚事务。

  验证:

        

   2)排他锁——也叫写锁

    它是用来操作数据的,所以又叫写锁。只要一个事务获取了一行数据的排他锁,其他事务只能查询该记录,不能再获取这一行数据的共享锁和排他锁了。

  加锁方式:一种是自动加排他锁,在操作数据时(增删改),会默认给一行数据加排他锁;另一种是手动加锁,通过FOR UPDATE给一行数据加排他锁。

  释放锁方式:与共享锁一样。

  验证:

        

  3)查询select加锁的理解

    快照读(普通的select)用MVCC保证读一致性。加锁的读和增删改,用lock保证读一致性。

    查询分为普通查询和加锁查询。

    ——普通查询:在4个事务隔离级别中,除了在串行化(Serializable)时会加共享锁,其他的都不加锁,即快照读。如下:事务隔离级别为RC

      当事务1执行:    

BEGIN;
SELECT * FROM product WHERE id = 1;

      事务2可以正常执行update语句:

UPDATE product SET gold = 300 WHERE id = 1;

    ——加锁查询:select ....lock in share mode;select.....for update。如下:事务隔离级别为RC

      当事务1执行:

BEGIN;
SELECT * FROM product WHERE id = 1 LOCK IN SHARE MODE;

      此时事务2再执行update语句事阻塞。

 *  表锁

这里重点说几个概念上的理解

  1)意向锁,可以理解为行锁在表级的一个标志,加表锁的时候需要查看意向锁,防止加的表锁和已经有的行锁冲突。

  2)有一种说法:只有通过索引条件检索数据,InnoDB才使用行级锁(因为行锁是锁住的索引),否则,InnoDB将使用表锁!。其实是使用记录锁+间隙锁锁住了整张表,但要注意只有RR隔离级别才有间隙锁。

  3)可以通过lock tables加表级别的锁,但是该表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的。对于DML元数据的修改,MySQL会加表锁。

延伸一点:对于lock tables的注意点:

  1)仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁。

  2)在用lock tables对InnoDB锁时要注意,要将autocommit设为0,否则MySQL不会给表加锁;事务结束前,不要用unlock tables释放表锁,因为unlock tables会隐含地提交事务;COMMIT或ROLLBACK产不能释放用lock tables加的表级锁,必须用unlock tables释放表锁。

  讲到这里,推荐一篇分析文章:https://www.cnblogs.com/rjzheng/p/9950951.html

*  意向锁

  意向锁是由数据库自己维护的,无法通过命令去操作。意向锁与意向锁和行锁都兼容,只与表锁互斥。当给一行数据加共享锁之前,数据库会自动在这张表上面加一个意向共享锁;当给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁。

  意向锁的作用:当准备给一张表加上表锁的时候,必须先去判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加上表锁。没有意向锁的情况下,就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,加表锁的效率会很低。引入了意向锁之后,只要判断这张表上面有没有意向锁,如果有,就直接返回失败。如果没有,就可以加锁成功。所以意向锁可以理解为一个标志,其并不锁定任何资源,是为了提高加表锁的效率

3、行锁的原理

  InnoDB的行锁,就是通过锁住索引来实现的,因为一张表一定会有主键索引。通过非聚集索引给一行数据加锁,主键索引也会被锁定,因为回表操作。

4、行锁算法

  先了解三种范围概念。下面以整型主键索引为例来理解三种范围,划分标准就是主键值,字符类型则根据ASCII码来排序。

                          

  1)记录Record:数据库里面存在的主键值,把它们叫做记录,所以上面就有4个Record。

  2)间隙Gap:根据主键,Record隔开的不存在数据的区间,叫做间隙,它是一个左开右开的区间。

  3)临键Next-key:间隙Gap连同它右边的记录Record,叫做临键区间,它是一个左开右闭的区间。

(1)记录锁

  第一种情况,对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这种情况使用的就是记录锁。

    比如:where id = 1  4  7  10。

  此时使用不同的主键去加锁不会冲突,因为记录锁只锁住这条记录Record。

(2)间隙锁

  第二种情况,当查询的记录不存在,没有命中任何一个Record,无论是用等值查询还是范围查询,使用的都是间隙锁。

    比如:where id > 4 and id < 7,where id = 6。

    

  间隙锁主要是阻塞插入insert,相同的间隙锁之间不冲突。

  间隙锁只存在于RR隔离级别中,如果要关闭间隙锁,就是把事务隔离级别设置成RC,并且吧innodb_locks_unsafe_for_binlog设置为ON,这种情况下除了外键约束和唯一性检查会加间隙锁,其他情况都不会。

(3)临键锁

  第三种情况,当使用范围查询时,不仅命中了Record记录,还包含了Gap间隙,这种情况使用临键锁。它是MySQL默认的行锁算法,相当于记录锁加上间隙锁。

    比如:where id > 5 and id < 9,它不仅包好了记录不存在的区间,也包含了一个Record 7。

    

  临键锁锁住最后一个key的下一个左开右闭的区间,如:

select*fromt2 whereid>5andid<=7forupdate; -- 锁住(4,7]和(7,10] 
select*fromt2 whereid>8andid<=10forupdate; -- 锁住 (7,10],(10,+∞)

  间隙锁和临键锁都解决了幻读问题。

5、再探InnoDB事务隔离级别

*  事务隔离级别的实现

                       

(1)Read Uncommited

  RU:不加锁

(2)Serializable

  Serializable 所有的 select 语句都会被隐式转化为 select .... lock in share mode,会和update、delete互斥。

(3)Repeatable Read

  RR隔离级别下,普通的 select 使用快照读(snapshot read),底层使用MVCC来实现。

  加锁的 select(select ...lock in share mode 、select ... for update)以及更新操作update、delete、insert等语句使用当前读(current read,即读取记录),底层使用记录锁、或者间隙锁、临键锁。

  通过间隙锁或临键锁可以解决幻读的问题,因为会锁住间隙或临键区间,所以其他事务不能该区间插入新纪录

(4)Read Committed

  RC隔离级别下,普通的 select 都是快照读,使用MVCC。

  加锁的 select 都使用记录锁,因为没有间隙锁(Gap Lock)。

  除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间。

  所以RC会出现幻读的问题,因为其没有间隙锁。但是通过加锁可以解决不可重复读问题,因为加记录锁,可以保证同一个事务中两次读取同一条纪录期间,该记录是不能被修改的

*  事务隔离级别的选取

  RU 和 Serializable 肯定不能用。为什么有些公司要用 RC,或者说网上有些文章推荐用RC?RC和RR主要有几个区别:

    1)RR的间隙锁会导致锁定范围的扩大。

    2)条件列未使用到索引,RR锁表,RC锁行。

    3)RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。

    在RC中,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足 update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。

*  RR和RC隔离级别下扫全表的情况

  首先,对于一个更新语句没有使用索引,一定会锁全表的理解:这句话是没有错的,但是也不全对,因为在RC隔离级别下,会有自动释放锁的行为。接下来是亲自测试案例:

CREATE TABLE `test` (
  `id` int(4) NOT NULL,
  `t1` int(11) DEFAULT NULL,
  `t2` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_t2` (`t2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

  数据如下:

      

  1)RR隔离级别下

    

 

   2)RC隔离级别下

           

 

   可以看到,在RC级别下,当事务2更新 t1=3 这行记录时会阻塞,但是当事务2更新其他行记录时就会成功,说明RC级别下只锁定了 t1=3 这一行记录。

  总结:当更新语句没有用到索引时,RR隔离级别下会锁表,是覆盖全表的临键锁(Next-key Lock);RC隔离级别下在全表扫描时会锁表,当记录更新完,全表扫描结束之后只锁定要查找的行,是记录锁(Record Lock)。

  原理分析:RC级别下执行没有使用索引的更新语句的加锁过程:

    1)确实需要全表扫描,在扫描过程中每一行都加上排他锁;

    2)当读到了 t1=3 的行,就执行更新;

    3)当整个表都读完了,col1=3 的行也就全部更新完了,这时就会把 t1<>3 的行锁释放;

    4)所以,UPDATE test SET t1 = 5 WHERE t1 = 1 才会执行成功,因为 t1=1 这一行数据已经没有了事务1加的排他锁了。    

6、死锁

(1)锁的释放

  事务结束(commit,rollback),客户端连接断开

(2)死锁的发生与检测

  MySQL有一个参数(innodb_lock_wait_timeout)来控制获取锁的等待时间,默认是50秒。

    

  在第一个事务中,检测到了死锁,马上退出了,第二个事务获得了锁,不需要等待50秒。

    [Err]1213-Deadlockfoundwhentryingtogetlock;tryrestartingtransaction 

  为什么可以直接检测到呢?是因为死锁的发生需要满足一定的条件,所以在发生死锁时,InnoDB一般都能通过算法(wait-for graph)自动检测到。那么死锁需要满足什么条件?死锁的产生条件:

    因为锁本身是互斥的,(1)同一时刻只能有一个事务持有这把锁,(2)其他的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺,(3)当多个事务形成等待环路的时候,即发生死锁。

  如果锁一直没有释放,就有可能造成大量阻塞或者发生死锁,造成系统吞吐量下降。

(3)查看锁信息

  SHOW STATUS命令中,包括了一些行锁的信息:show status like 'innodb_row_lock_%';

                         

    Innodb_row_lock_current_waits:当前正在等待锁定的数量;

    Innodb_row_lock_time :从系统启动到现在锁定的总时间长度,单位 ms;

    Innodb_row_lock_time_avg :每次等待所花平均时间;

    Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间;

    Innodb_row_lock_waits :从系统启动到现在总共等待的次数。

    SHOW命令是一个概要信息。InnoDB还提供了三张表来分析事务与锁的情况:

        

  如果一个事务长时间持有锁不释放,可以 kill 事务对应的线程 ID,也就是INNODB_TRX表中的trx_mysql_thread_id,例如执行kill 4,kill 7,kill 8。

 (4)死锁的避免

  1) 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路);

  2)批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路);

  3)申请足够级别的锁,如果要操作数据,就申请排它锁;

  4)尽量使用索引访问数据,避免没有where条件的操作,避免锁表;

  5)如果可以,大事务化成小事务;

  6)使用等值查询而不是范围查询查询数据,命中记录,避免间隙锁对并发的影响。

 

  

 

posted @ 2020-05-13 15:31  jingyi_up  阅读(43)  评论(0编辑  收藏  举报