MySQL InnoDB存储引擎之锁

    概念:
        锁是用来管理对共享文件的并发访问。innodb会在行级别上对数据库上锁。不过innodb存储引擎会在数据库内部其他多个地方使用锁,从而允许对不同资源提供并发访问。例如操作缓冲池中的LRU列表,删除,添加,移动LRU列表中的元素,为了保证一致性,必须有锁的介入。MyISAM引擎是表锁,而InnoDB提供一致性的非锁定读、行级锁,且行级锁没有相关额外的开销。
    锁
        table-level locking(表级锁)
            整个表被客户锁定。根据锁定的类型,其他客户不能向表中插入记录,甚至从中读数据也受到限制MyISAM、MEMORY默认锁级别,个别时候,InnoDB也会升级为表级锁
        row-level locking(行级锁)
            只有线程当前使用的行被锁定,其他行对于其他线程都是可用的InnoDB默认行级锁。是基于索引数据结构来实现的,而不是像ORACLE的锁,是基于block的。InnoDB也会升级为表级锁,全表/全索引更新,请求autoinc锁等
        page-level locking(页级锁)
            锁定表中某些行集合(称做页),被锁定的行只对锁定最初的线程是可行。如果另外一个线程想要向这些行写数据,它必须等到锁被释放。不过其他页的行仍然可以使用BDB默认页级锁
    lock与latch
        latch称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。latch可以通过命令show engine innodb mutex来进行查看。如图: MySQL InnoDB存储引擎之锁_第1张图片
        由上图可以看出列Type显示的总是InnoDB,列Name显示latch的信息以及所在源码的行数,列Status中显示的os_waits表示操作系统等待的次数。
        lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或者rollback后释放(不同事务隔离级别释放的时间可能不一样)。有死锁机制。二则的区别如下:
        MySQL InnoDB存储引擎之锁_第2张图片
    特点:
    InnoDB是通过对索引上的索引项加锁来实现行锁。这种特点也就意味着,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
    锁的类型:
        有两种标准的行级锁:
            共享锁(S lock):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁.SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
            排它锁(X lock):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他锁.SELECT * FROM table_name WHERE ... FOR UPDATE
        InnoDB存储引擎支持意向锁且设计比较简练,分为两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。(意向锁是InnoDB自动加的)
            意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁.
            意向独占锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁.
        表级意向锁与行级锁的兼容情况如下图:
            MySQL InnoDB存储引擎之锁_第3张图片
    锁的查看
        在InnoDB1.0版本之前只能通过show engine innodb status(transactions行中查看) 或者 show full processlist来查看当前库中锁的请求。但是在这之后在information_schema架构下新增innodb_trx、innodb_locks和innodb_lock_waits三张表记录当前库中锁的情况。
        三个表的字段说明如下图 MySQL InnoDB存储引擎之锁_第4张图片 MySQL InnoDB存储引擎之锁_第5张图片
    一致性非锁定读(consistent nonlocking read)
        一致性的非锁定读是指InnoDB存储引擎通过行多版本控制(multi_versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或者update操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据(当前行数据的历史版本)。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务回滚数据,因此快照数据本身是没有额外开销。而且,读取快照数据是不需要上锁的。一致性非锁定读是InnoDB存储引擎的默认读取方式(在读取不会占用和等待表上的锁)。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。即使都是使用非锁定的一致性读,但是对于快照数据的定义格式也各不相同。在事务隔离级别READ COMMITTED(RC)和REPEATABLE READ(RR,InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义去不相同。在RC事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在RR事务隔离解绑下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
    一致性锁定读
        有上文知道,默认的事务隔离级别(RR)模式下,InnoDB存储引擎的select操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读操作加锁以保证数据逻辑的一致性。InnoDB存储引擎对于select语句支持两种一致性的锁定读操作:
            select ... for update:对读取的行记录加X锁,其他事物不能对该行加任何锁。
            select ... lock in share mode:对读取的行记录加S锁,其他事物可以对该行加S锁,但是如果加X锁,则会被阻塞。
    自增长与锁
        在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长的计数器的表进行插入操作是,这个计数器会被初始化,执行如下的语句来得到计数器的值:select max(auto_inc_col) from t for update。插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称为AUTO-INC Locking,这是一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成之后才释放,而是在完成对自增长值插入的SQL语句后会立即释放。AUTO-INC Locking在一定程度上提高了并发插入的效率,但是还存在一些性能上的问题。首先,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入的完成(不用等待事务的完成)。其次,对于insert ... select的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。从MySQL5.1.22版本开始,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。通过参数innodb_autoinc_lock_mode来控制自增长的模式(默认为1)。自增长的插入进行分类如图: MySQL InnoDB存储引擎之锁_第6张图片
        innodb_autoinc_lock_mode的参数值及其对自增长的影响如下图: MySQL InnoDB存储引擎之锁_第7张图片
        MyISAM存储引擎是表锁,自增长不用考虑并发插入的问题。需要注意的是:在InnoDB存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列,如果不是第一个列,MySQL是会抛出异常的。异常如图
    外键与锁
        外键主要用于完整性的约束检查。在InnoDB存储引擎中,对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎会自动对其加一个索引,避免表锁。对于外键值的插入或者更新,首先需要查询父表中的记录,对于父表的select操作,不是使用的一致性非锁定读的方式,因为这样会发生数据不一致的问题,所以这时使用的是select ... lock in share mode方式,即主动给父表加一个S锁。
    锁的问题
        dirty read 脏读
            脏读就是读取到脏数据(未提交的数据)。一个事务(A)读取到另一个事务(B)中修改后但尚未提交的数据,并在这个数据的基础上操作。这时,如果B事务回滚,那么A事务读到的数据是无效的。不符合一致性。如图 MySQL InnoDB存储引擎之锁_第8张图片
            首先事务的隔离级别有默认的RR改为RU,由上述例子可以看出会话B中两次select操作取得了不同的结果,并且这2条记录是会话A中并未提交的数据,这就产生了脏读。由此可以得出结论:脏读发生的条件是事务的隔离级别为RU。
        unrepeatable read 不可重复读
            事务(A)读取到了另一个事务(B)已经提交的更改数据,不符合隔离性。不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读则读到的是已经提交的数据。首先将事务隔离级别调整为RC,然后操作下边的例子: MySQL InnoDB存储引擎之锁_第9张图片
        phantom read 幻读
            事务(A)读取到了另一个事务(B)提交的新增数据,不符合隔离性。
    锁的范围(锁的算法):
        1.Record Lock :单个记录上的锁,总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
        2.Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
        3.Next-key Lock: 锁定一个范围和本身 Record Lock + Gap Lock,防止幻读。
        主键索引和唯一辅助索引 = record lock
        非唯一辅助索引 = next-key lock
    阻塞
        不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另外一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来动态的控制等待的时间(默认50秒),innodb_rollback_on_timeout用来静态的设定释放在等待超时时对进行的事务进行回滚操作(默认OFF,代表不回滚)。
    死锁

        死锁是指两个或者两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。解决死锁最简单的一种方式是超时,即当两个事务相互等待是,当一个等待时间超过设置的某一阀值时,其中一个事务进行回滚,另外一个等待的事务就能继续进行。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时时间。但若超时的事务所占权重比较大,如果事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式就不合适啦,因为回滚这个事务的时间相对于另一个事务所占用的时间可能会很多。因此,除了超时机制,当前数据库都普遍采用wait-for graph(等待图)的方式来进行死锁检测。要求数据库报错以下两种信息:a.锁的信息链表;b.事务等待链表。通过上述链表可以构造一张图,而在这个图中若存在回路,就代表存在死锁。在wait-for graph中,事务为图中的节点。如图:

MySQL InnoDB存储引擎之锁_第10张图片

        如图可以发现存在回路(1,2),因此存在死锁,这时InnoDB存储引擎选择回滚undo量最小的事务。wait-for graph的死锁检测通常采用深度优先的算法实现。        
    注意:
        1.S X IS IX,表示的是,本锁和其他锁共存的方式,是互斥还是兼容
        2.RECORD LOCK、GAP LOCK、NEXT-KEY LOCK,表示的是,这些锁要加载的范围,是行记录本身,还是行记录+间隙,甚至更大的范围
    重要的结论:
        1、任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁
        2、任何叶子节点上的S或X锁之前,都会在根节点加一个IS或IX锁,也就是表级别的IS、IX锁
        3、主键索引上的锁,都是record lock
        4、唯一索引辅助索引上的锁,也都是record lock
        5、非唯一索引辅助索引上的锁,则是next-key lock
        6、不会有单独的gap lock出现,只会伴随着record lock出现,依附于它

你可能感兴趣的:(Mysql)