MySQL高级进阶(七)、锁

小景哥哥博客

MySQL高级进阶(七)、锁

InnoDB存储引擎默认在行级别上对表数据上锁,数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。

在数据库中,locklatch都可以理解为锁,但二者有着截然不同的含义。

latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临街资源的正确性,并且通常没有死锁检测机制。

lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commitrollback后进行释放。lock有死锁检测机制。

共享锁和排它锁

InnoDB存储引擎实现了两种标准级别的行级锁:

  • 共享锁(S Lock),允许事务读一行数据。
  • 排它锁(X Lock),允许事务删除或更新一行数据。
X S
X 不兼容 不兼容
S 不兼容 兼容

X锁与任何锁都不兼容,S锁仅与S锁兼容。S锁和X锁都是行锁,兼容是指针对同一行记录锁的兼容情况。

意向锁

InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级别上的锁和表级别上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

  • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。
  • 意向排它锁(IX Lock),事务想要获得一张表中某几行的排它锁。

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实也不会阻塞除全表扫以外的任何请求。

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

一致性非锁定读

一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DeleteUpdate操作,这时也不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读行的一个快照数据。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来实现的。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同的事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

在事务隔离级别READ COMMITEDREPREATABLE READ下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPREATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

一致性锁定读

在默认配置下,即事务的隔离级别为REPREATABLE READ模式下,InnoDB存储引擎的select操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于select的只读操作。InnoDB存储引擎对于select语句支持两种一致性的锁定读操作:

  • select ... for update
  • select ... lock in share mode

select ... for update对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。select ... lock in share mode对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被锁定。

自增长锁

自增长在数据库中是非常常见的一种属性,对每个含有自增长值的表都有一个自增长计数器,当对含有自增长计数器的表进行插入操作时,这个计数器会被初始化。插入操作会依据这个自增长的计数器加1赋予自增长列。这个实现方式称为Auto-Inc Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是一个事物完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。

虽然Auto-Inc Locking从一定程度上提高了并发插入的效率,但还是存在一些性能上的问题。首先,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入的完成。其次,对于Insert ... select的大量数据的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。

MySQL5.1.22开始,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。InnoDB存储引擎还提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,该值的默认为1

innodb_autoinc_lock_mode tips
0 这是在MySQL 5.1.22 之前使用AUTO-INC Locking的方式来实现主键的自增长
1 这是该参数的默认值。对于simple inserts,该值会使用互斥量(mutex)去对内存中的计数器进行累加的操作。对于bulk inserts还是会使用传统表锁的AUTO-INC Locking的方式。在这种配置下,如果不考虑回滚操作,对于自增长列的键值还是连续的。这种方式下,statement-basedreplication还是能很好地工作的。
2 这种模式下,对于所有”INSERT-LIKE”自增长值的产生都是通过互斥量,而不是”AUTO-INC Locking”的方式。显然,这是性能最高的方式。但是这种模式在并发插入时,产生的自增长的值可能不是连续的。此外,在这种模式下,statement-basedreplication 会出现问题。因此使用这种模式时需要使用 row-basedreplication

InnoDB存储引擎中,自增长的列必须是索引,同时必须是索引的第一个列。

外键和锁

InnoDB存储引擎中,对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁。这比Oracle数据库做得好,Oracle数据库不会自动添加索引,用户必须自己手动添加,这也导致了Oracle数据库中可能产生死锁。

对于外键值的插入或更新,首先需要查询父表中的记录,即select父表。但是对于父表的select操作,不是使用一致性非锁定读的方式,因为这样会发生数据的不一致的问题,因此这时使用的是select ... lock in share mode方式,即主动对父表加一个S锁。如果这时父表上已经这样加了X锁,子表上的操作会被阻塞。

锁的算法

InnoDB存储引擎有3种行锁的算法:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
  • Next-Key LockGap Lock+ Record Lock,锁定一个范围,并且锁定记录本身。

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。

Next-Key LockGap LockRecord Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB存储引擎对于行的查询都是采用这种锁定算法。

采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计目的是为了解决Phantom Problem。利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。除了Next-Key Locking,还有Previous-Key Locking技术,差别就是锁定范围的开闭区间,仅此而已。

然而,当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降为Record Lock,即仅锁住索引本身,而不是范围。Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,其上加的是Next-Key LockInnoDB存储引擎还会对辅助索引下一个键值加上一个Gap Lock

Gap Lock的作用是为了阻止多个事务将记录插入到同一个范围内,而这会导致Phantom Problem问题的产生。用户可以通过下面两种方式来显式地关闭Gap Lock

  • 将事务的隔离级别设置为READ COMMITED
  • 将参数innodb_locks_unsafe_for_binlog设置为1

在上述配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。

InnoDB存储引擎中,对于Insert的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。

对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。

在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom ProblemPhantom Problem是指在同一事务下,连续执行两次相同的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

锁问题

脏读

脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。而所谓的脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交。

对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘异步造成的,这并不影响数据的一致性,或者说两者最终会达到一致性,即脏页都刷回磁盘。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。

脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事物可以读到另一个事务中未提交的数据,则显然违反了数据库的隔离性。

脏读指的是在不同事务下,当前事务可以读到另外事务未提交的数据。脏读现象在生产环境中并不常发生,脏读发生的条件是需要事务的隔离级别为READ UMCOMMITTED,而目前绝大多数数据库至少设置成READ COMMITTEDInnoDB存储引擎默认事务隔离级别为READ REPEATABLEMicrosoft SQL Server数据库为READ COMMITTEDOracle数据库同样为READ COMMITTED

不可重复读

不可重复读是指在一个事物内多次读取同一数据集合。在这个事务还没有结束时,另一个事务也访问该同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在第一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

不可重复读和脏读的区别是,脏读是读到未提交的数据,而不可重复读读到的是已提交的数据,但是其违反了数据库事务一致性的要求。

一般来说,不可重复读的问题是可以接受的,因为其读到的是已提交的数据,本身并不会带来很大的问题。数据库厂商一般将其数据库事务默认设置成READ COMMITTED,在这种隔离级别下允许不可重复读的现象。

InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中,将不可重复读定义为Phantom Problem。在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描的索引,而且还锁住这些索引覆盖的范围。因此在这个范围内的插入都是不允许的,这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。因此,InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE,采用Next-Key Lock算法,避免了不可重复读的现象。

丢失更新

一个事务的更新操作会被另一个事务的更新操作覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。因为即使是READ UMCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁。

死锁

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。

解决死锁问题的最简单的一个方法是超时,即两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时的时间。

超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其实根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对于另一个事务所占用的时间可能会很多。

除了超时机制,当前数据库还都普遍采用了wait-for graph的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用这种方式。wait-for graph要求数据库保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表

通过上述链表可以构造出一张图,而在这张图中若存在回路,就代表存在死锁,因此资源间相互发生等待。

wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

wait-for graph的死锁检测机制通常采用深度优先的算法实现,在InnoDB1.2之前,都是采用递归方式实现。从1.2版本之后,对wait-for graph的死锁检测进行了优化,将递归用非递归的方式实现,从而进一步提了了InnoDB存储引擎的性能。

锁升级

锁升级是将当前锁的粒度降低。数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。如果数据库在设计的时候认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级的现象。

InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式,因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。

你可能感兴趣的:(MySQL高级进阶,MySQL,mysql,数据库,sql)