数据库DataBase

Database(五) mysql的innodb锁相关

2020-05-14  本文已影响0人  joshuaXin

一:锁的引入

    1.锁是保证进线程同步的一种方式,是OS、DB、Java等高级语言实现同步的手段,比如OS的互斥资源;Java中的CAS、Lock等;
    2.锁的种类:悲观锁、乐观锁,在Innodb中,MVCC是的乐观锁实现形式,表锁、页锁、行锁、间隙锁是悲观锁的实现方式,Java中CAS是乐观锁的实现方式之一,Lock是悲观锁的一种实现方式;
   3.事务的特性是ACID,其中隔离性是明显需要锁机制来保证的,锁是Mysql的事务的原子性的保证,包括InnoDB的4种隔离级别,也有针对的保证,比如上述的几种锁;
  4.在高并发的情形下,对数据库的操作是很关键的因素,尤其是事务中,怎么避免死锁,以及定位死锁?
  5.下面主要从几方面进行介绍:锁的种类、锁的实现,死锁,锁优化、事务的隔离级别、MVCC等;

二:锁的种类

  1.悲观锁和乐观锁
     悲观锁和乐观锁是实现数据同步的两种方式,保证事务的隔离性的机制;
     乐观锁认为大概率不会发生冲突,常用更改版本号、时间戳等方式,MVCC是乐观锁的一种实现,可以有效的提高数据的的效率,下面会详细介绍;
     悲观锁认为大概率会产生冲突,会对可能会冲突的数据上锁,主要用在数据的修改上,是保证事务的隔离性的保证,读写锁、表锁、页锁、行锁、间隙锁、意向锁都属于悲观锁;
  2.读写锁:悲观锁,主动加的锁
  读锁又称为共享锁,lock in share mode,读锁和读锁可以兼容,但读锁不可以和写锁兼容;
  写锁又称为排它锁,它一般用在事务中,用于资源的锁定,需要autocommit关闭的情况下,;
  3.表锁、页锁、行锁、间隙锁
     这几种锁是Innodb存储引擎根据隔离级别、数据库操作自动加的,其中表、页、间隙、行,锁的颗粒度变大,并发度变大,锁的开销也变大;

三:共享锁和排它锁

   1.共享锁
       共享锁需要使用时,用lock in share mode,当使用共享锁的时候,可以继续对数据加上共享锁,不能加排它锁;
      在下面的例子中,事务A对id=4的数据进行加共享锁之后,事务B仍然可以对id=4的数据加上共享锁,但是当事务A需要对id=4的数据加排它锁之后,需要等待事务B的共享锁释放后,才可以加排它锁;

1.共享锁的使用

2.排它锁_行锁
   行锁是Innodb的读已提交隔离级别解决脏读的手段,同时也可以提高并发度,但是前提是当前的字段是要有索引的,否则用是表锁;由于行锁的开销会相对比较大,当索引失效或者代价太大的时候,会升级为表锁,和查询原理一样;
3.排它锁_间隙锁
   Gap Lock和Next key是mysql的RR可重复读的级别中,解决读场景下的幻读提出的(传统的隔离级别的定义中,应该是序列化这级解决);
   间隙锁不仅是对于一些区间型语句 id > 10,而且对于id = 10这种,也会上区间锁,用来解决幻读,此处不展开了;缺点在RR级别开启了间隙锁时,如果有在很大范围上上操作的SQL,很可能会严重影响并发程度;
   对复制和恢复有一定的影响,mysql通过binlog对insert、update、delete等更新操作时,有2个特点,一是它的恢复是SQL级别的,即重新执行binlog的SQL语句,二是Binlog是按照事务提交的先后顺序,恢复也是按照这个顺序进行的,所以要求在事务A提交前,事务B不能插入满足其锁条件的数据,否则会出现幻读,导致主从数据不一致;

三:事务的隔离级别与实现

1.数据库的隔离级别的定义:
      a)读未提交:出现脏读的现象,那么事务B读到事务A未提交的数据后,当A回滚时,数据就会混乱;
      b)不可重复读Read Committed:解决了脏读的问题,但是有不可重复读的问题,即事务A读某条数据的时候,有其他事务在操作该数据,导致事务A前后两次读取不一致;
     c)可重读读Read Repeated:利用行锁或者表锁解决了不可重复读的问题,但是存在幻读的问题,即对于一些区间操作时,有其他事务进行insert、delete操作,导致前后两次读取,会多一些数据或者少一些数据;
     d)序列化Serializable,解决了幻读问题,使得数据操作执行起来,和串行执行是一样的效果;
2.mysql的不同隔离级别解决的问题:
   innodb在可重复读RR级别的时候,采用了MVCC和间隙锁,解决了一定的幻读问题,即MVCC是快照度,解决了读的幻读问题,但是并没有解决在写场景下的幻读问题,因为写场景是及时写,比如下面的场景中,事务A的T2和T5操作,同一条件影响的数据并不一致;

2.写场景下的幻读情况

   3.MVCC是否实现了Serializable
     InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。
     不过RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离,下面是两个例子:
     第一个例子,如果在事务中第一次读取采用非加锁读,第二次读取采用加锁读,则如果在两次读取之间数据发生了变化,两次读取到的结果不一样,因为加锁读时不会采用MVCC。
    第二个例子:事务A先进行查询 0<id<5,这个时候仅有数据record1,然后事务B插入一条数据record2:id =3,这个时候事务A去update 0<id<5,会update2条数据,这是因为MVCC是行上的链,而不是范围的链条;

四:MVCC多版本并发控制 Multiple Version Concurrency Control 

1.简介
   MVCC是为了解决幻读问题提出的,并且可以在不解锁的情况下,进行数据的读取,它的数据属于快照读;
2.实现:利用指针指向不同事务产生的版本
   1)Innodb中的每行数据都有隐藏列,这些隐藏的列中包含了本行数据的事务id、指向undo log的指针等;
   2)基于undo log的版本链,undo log是实现事务的原子性的手段,可以用来回滚事务操作,undo log会指向更早版本的undo log,形成一条以事务提交顺序的undo log链;
   3)快照读ReadView:事务在某一时刻给整个事务系统进行快照比较,从而判断是否可见,判断的方法主要通过下面几个id:
      low_limit_id:分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该ReadView不可见;
      up_limit_id:生成ReadView时,当前系统活跃的读写事务中最小的事务id,如果事务的id小于up_limit_id,则对该ReadView可见。
      rw_trx_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在low_limit_id和up_limit_id之间,则需要判断事务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中,因此数据对ReadView不可见,即读已提交;如果不在,说明生成ReadView时事务已经提交了,因此数据对ReadView可见。
3.基于undo log的版本链,是保证事务原子性的基础,又称为回滚日志
   实现原子性的关键是事务回滚时,能够撤销所有已经成功执行的sql语句,靠的就是undo log;当事务对数据进行修改时,Innodb会生成对应的undo log,如果事务失败或者调用rollback,就会根据undo log中的信息进行事务回滚;
   undo log属于逻辑日志,事务对数据修改时,生成的undo log一般都是逆向操作,比如update操作时,会生成相应的update,保证可以恢复操作;

五:死锁的检测与解决

1.死锁的概念
   死锁有4个条件,互斥、请求保持、不剥夺、循环等待;在Innodb中,由于MVCC的存在,在读操作中,数据都不是互斥的,不会发生死锁的情况,但在写操作中,由于行锁、间隙锁的存在,在RC、RR级别下,修改、删除、插入都有可能导致死锁的发生;
2.死锁的发生
    在数据库中,发生死锁一般会带来超时的现象,长时间等待后,由DB主动的检测死锁的事务,并断开当前的事务;
    例子:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
3.死锁在应用层次的解决
    1) 查询是否锁表show OPEN TABLES where In_use > 0;如果发现有表的信息,需要根据表的信息进行判断,当前的表是否发生了死锁;
    2)查看当前的事务
         SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
         查看当前锁定的事务
         SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
         查看当前等锁的事务
         SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
  如下例中,是一次死锁的过程,事务A分别对表test1申请读锁,对parent申请写锁;事务B分别对test1申请写锁,对parent申请读锁

mysql> select * from information_schema.innodb_locks\G
***************************
1. row
***************************
lock_id:  21305:145:3:4
lock_trx_id: 21305
lock_mode: S
lock_type: RECORD
lock_table: `test1`.`parent`
lock_index: PRIMARY
lock_space: 145 
lock_page: 3 
lock_rec: 4 
lock_data: 3
***************************
2. row
***************************   
lock_id: 21303:145:3:4
lock_trx_id: 21303 
lock_mode: X
lock_type: RECORD
lock_table: `test1`.`parent`
lock_index: PRIMARY
lock_space: 145 
lock_page: 3 
lock_rec: 4 
lock_data: 32
rows in set, 1 warning (0.01 sec)

4.减低死锁的措施
   1)隔离级别:尽量使用较低的隔离级别,但是受制于复制和恢复的考虑,一般还是会选择RR级别,这种级别下,会开启间隙锁 
   2)缩小上锁的范围,比如设计索引,可以上行锁,而不是表锁;尽量少使用大范围的查询或者修改语句,这样会使大范围上锁,加大死锁的可能;
  3)选择合理的事务大小,小事务发生死锁的概率也较低;
  4)锁粗化,识别好场景,对于一些易死锁的多表操作,建议申请足够的锁,来避免死锁;
  5)应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;

上一篇下一篇

猜你喜欢

热点阅读