MySQL中的锁

先小小的总结一下,详情会在下面介绍。
Phantom Problem(幻像问题):是指在同一事务下,连续执行两次同样的SQL 语句可能导致不同的结果,第二次SQL语句可能会反悔之前不存在的行。
共享锁:允许事务读一行数据。
排它锁:允许事务删除或更新一行数据。
意向共享锁:事务想要获得一张表中某几行的共享锁。
意向排它锁:事务想要获得一张表中某几行的排它锁。
乐观锁:在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
悲观锁:在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁(locking)的机制。
一、什么是锁?
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB存储引擎会在行级别上对表数据上锁,也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
对于MyISAM引擎,其锁是表锁设计。并发情况下的读没有问题,但是并发插入时的性能就要差一些了,若插入是在“底部”,MyISAM存储引擎还是可以有一定的并发写入操作。
InnoDB存储引擎锁的实现和Oracle 数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
1.lock与latch
在数据库中,lock和latch都被称为锁。但是两者有着截然不同的含义。
latch一般称为闩(shuān)锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在 InnoDB 存储引擎中,latch又可以分为 mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务 commit 和 rollback 后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如大多数数据库中一样,是由死锁机制的。下表显示了lock与latch的不同。
这里写图片描述

2.InnoDB 存储引擎中的锁
InnoDB存储引擎事项了如下两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据。
排它锁(X Lock),允许事务删除或更新一行数据。
如果一个事务 T1 已经获得了行 r 的共享锁,那么另外的事务 T2 可以立即获得行 r 的共享锁,因为读取并没有改变行 r 的数据,称这种情况为锁兼容(Lock Compatible)。但若是有其他的事务 T3 想获得行 r 的排它锁,则必须等待事务 T1、T2释放行 r 上的共享锁,这种情况称为 锁不兼容。下图显示了共享锁和排它锁的兼容性。
这里写图片描述

从表中可以发现 X 锁与任何的锁都不兼容,而 S 锁仅和 S 锁兼容。需要特别注意的是,S 和 X锁都是行级锁,兼容是指同一记录(row)锁的兼容情况。
此外,InnoDB 存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一次额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁,如下图所示。
这里写图片描述

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。 如上图,如果需要对页上的记录 r 进行上 X 锁,那么分别需要对数据库、表、页上意向锁 IX,最后对记录 r 上 X 锁。若其中任何一部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录 r 加 X 锁之前,已经有事务对表 1 进行了 S 表锁,那么表 1 上已存在 S 锁,之后事务需要对记录 r 在表 1 上加 IX,由于不兼容,所以好事务需要等待表锁操作的完成。
InnoDB 存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计的目的主要是为了在一个事务中解释下一行将被请求的锁类型。其支持两种意向锁:
意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。
意向排它锁(IX Lock),事务想要获得一张表中某几行的排他锁。
由于InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表意外的任何请求。故表级别意向锁与行级锁的兼容性如下表

这里写图片描述

一致性非锁定读
一致性的非锁定读(consistent nonlocking read)是指 InnoDB 存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 delete 或 update 操作,这时读取操作不会因此去等待行上锁的释放。相反的,InnoDB存储引擎回去读取行的一个快照数据。

这里写图片描述
上图展现了 InnoDb 存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上 X 锁的释放。
快照数据是指该行之前的版本的数据,该实现是通过 undo 段来完成。而undo 用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据时不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
可以看到,非锁定读机制极大的提高了数据库的并发性。在 InnoDB 存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
从上图可知,快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。如图显示,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。
在事务隔离级别 read committed (提交读)和 repeatable read(可重复读,InnoDB存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用非锁定的一致性读。然后,对于快照数据的定义却不相同。在 read commited 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在repearable read 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

事务隔离级别详情:
http://blog.csdn.net/m0_37899949/article/details/79006466
二、锁的算法
行锁的三种算法
InnoDB 存储引擎有3中行锁的算法,分别是:
Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身
Record lock 总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB 存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock 是结合了Gap Lock 和 Record Lock 的一种锁定算法,在Next-Key Lock 算法下,InnoDB 对于行的查询都是采用这种算法。例如一个索引有 10,11,13和20这四个值,那么该索引可能被 Next-Key Locking的区间为:
(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)
采用Next=Key Locking 的锁定技术称为Next-Key Locking。其设计的目的是为了解 Phantom Problem(幻像问题),而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。除了 next=key locking,还有 previous-key locking 技术。同样上述的索引10、11、13和20,若采用 previous-key locking技术,那么可锁定的区间为:
(-∞,10)
[10,11)
[11,13)
[13,20)
[20,+∞)
若事务 T1 已经通过 next-key locking锁定了如下范围:
(10,11]、(11,13]
当插入新的记录 12 时,则锁定的范围会变成:
(10,11]、(11,12]、(12,13]

三、锁问题
通过所机制可以实现事务的隔离性要求,使得事务可以并发的工作。锁提高了并发,但是却会带来潜在的问题。锁会带来三种问题:
脏读
在理解脏读(Dirty Read)之前,需要理解脏数据的概念。但是脏数据和脏页是完全不同的两种概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据时不一致的,但然在刷新之前到磁盘之前,日志都已经被写入到了重做日志文件中。而所谓脏数据时指事务对缓冲池中行记录的修改,并且却还没有被提交(commit)。
对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会得到一致性,即当脏页都刷回到磁盘)。且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。
脏数据却截然不同,脏数据时指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,咋显然违反了数据库的隔离性。
脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。下面是脏读的示例
这里写图片描述

事务的隔离级别进行了更换,由默认的repeatable read 换成了 read uncommitted。因此在会话A中,在事务并没有提交的前提下,会话B中的两次 select 操作取得了不同的结果,并且 2 这条记录是在会话A 中并未提价的数据,即产生了脏读,违反了事务的隔离性。
脏读现象在生产环境中并不常发生,从上面的例子或者能够就可以发现,脏读发生的条件是需要事务的隔离级别为 read uncommitted ,而目前绝大部分的数据库都至少设置成 read committed。InnnoDB 存储引擎默认的事务隔离级别为 read repeatable,Microsoft SQL Server 数据库为 read committed ,Oracle 数据库同样也是 read committed。
脏读隔离看似毫无用处,但在一些比较特殊的情况下还是可以将事务的隔离界别设置为 read uncommitted。例如replication 环境中的 slave 节点,并且在该 salve上的查询并不需要特别精确的返回值。
不可重复读
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没结束时,另外一个事务也访问该同一数据集合,并做了一些DML(数据操纵语言:对数据的增删改查等)操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据时不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是:脏读是读到为提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。可以通过下面一个例子来观察不可重复读的情况。
这里写图片描述

扫描二维码关注公众号,回复: 2425640 查看本文章

在会话A 中开始一个事务,第一次读取到的记录是 1,在另一个会话 B 中开始了另一个事务,插入一条为 2 的记录,在没有提交之前,对会话 A 中的事务进行再次读取时,读到记录还是 1 ,没有发生脏读现象。但会话 B 中事务提交后,在对会话 A 中的事务进行读取时,这时读到的是 1 和 2 两条记录。这个例子的前提是,在事务开始前,会话A 和会话 B的事务隔离级别偶读调整为 read committed。
一般来说不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商(如Oracle、Microsoft SQL Server)将其数据库事务的默认隔离级别设置为 read committed,在这种隔离级别下允许不可重复读的现象。
在InnoDB存储引擎中,通过使用 Next-Key Lock 算法来避免不可重复读的问题。在MySQL 官方文档中将不可重复读的问题定义为 Phantom Problem,即幻像问题。在 Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围内的插入都是不允许的。这样就避免了另外的事务在这和个范围内插入数据导致不可重复读的问题。因此,InnoDB 存储引擎的默认事务隔离级别是 read repeatable,采用Next-Key Lock算法,避免了不可重复读的现象
丢失更新
丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。例如:
1)事务 T1 将行记录 r 更新为 v1,但是事务 T1 并未提交。
2)与此同时,事务 T2 将行记录 r 更新为 v2 ,事务 T2 未提交。
3)事务 T1 提交。
4)事务 T2 提交。
但是,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是read uncommitted 的事务隔离级别,对于行的 DML操作,需要对行或其他粗粒度级别的对象加锁。因此上述步骤 2) 中,事务T2 并不能对行记录 r 进行更新操作,其会被阻塞,直到事务 T1 提交。
虽然数据库能组织丢失更新问题的产生,但是在生产应用中还有另一个逻辑意义的丢失更新问题,而导致该问题的并不是因为数据库本身的问题。实际上,在所有多用户计算机系统环境下都有可能差生这个问题。简单来说,出现下面的情况时,就会发生丢失更新:
1)事务 T1 查询一行数据,放入本地内存,并显示给一个终端用户 User1.
2)事务 T2 也查询该行数据,并将取得的数据显示给终端用户 User2.
3)User1 修改这行记录,更新数据库并提交。
4)User2 修改这行记录,更新数据库并提交。
显然,这个过程中用户 User1的修改更新操作“丢失”了,而这可能会导致一个“恐怖”的结果。设想银行发生丢失更新现象,例如一个用户账号有1000元人民币,他用两个网上银行的客户端分别进行转账操作。第一次转账9000,因为网络和数据的关系,这时需要等待。但是这时用户操作另一个网上银行客户端,转账1元,如果最终两笔操作都成功了,用户的账号余款是 9999,第一次转的 9000 并没有的到更新,但是在转账的另一个账号却会收到这 9000 元,这导致的结果就是钱变多,而帐不平。
要避免丢失更新发生,需要让食物在这种情况下的操作编程串行化,而不是并行的操作。即在上述的四个步骤的 1)中,对用户的读取记录加上一个排他X锁。同样,在步骤2)的操作过程中,用户同样也需要加一个排它X锁。通过这种方式,步骤2)就必须等待步骤1)和步骤3)完成,最后完成步骤4)
下表演示了如果避免逻辑上丢失更新问题产生。
这里写图片描述

阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其实为了确保事务可以并发且正常运行。

死锁
死锁是指两个或两个以上的事务在执行过程中,引争夺资源而造成的的一种互相等待的现象。
若无外力作用,事务将无法推进下去。解决死锁最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。毫无疑问,这的确可以避免死锁问题的产生。然而在线上环境中,这可能导致并发性能的下降,甚至任何一个事务都不能进行。而这所带来的问题远比死锁更为严重,因为这很难被发现并且浪费资源。
解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈(yu)值时,其中一个事务进行回滚,另一个等待的事务就能继续进行,在InnoDB存储引擎中,参数 innodb_lock_wait_timeout 用来设置超时的时间。
超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其是根据FIFO(First Input First Output)的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的 undo log(所有没有COMMIT的事务回滚到事务开始之前的状态),这时采用 FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多。
因此,除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB 存储引擎也采用的这种方式。
wait-for graph 要求数据库保存一下两种信息:
锁的信息链表
事务等待链表
通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因为资源间相互发生等待。在 wait-for graph 中,事务为图中的节点。而在图中,事务 T1 指向 T2 边的定义为:
事务T1 等待事务 T2 所占用的资源
事务 T1 最终等待 T2 所占用的资源,也就是事务之间在等待相同的资源,而事务 T1 发生在事务 T2 的后面
下面来看一个例子,当前事务和锁的状态如下图
这里写图片描述
示例事务状态和锁的信息

在 Transaction Wait Lists 中可以看到共有 4 个事务t1、t2、t3、t4,故在 wait-for graph 中应有 4 个结点。而事务t2 对 row1 占用 x锁,事务t1 对row2占用 s 锁。事务 t1主要等待事务t2中row1 的资源,因此在wait-for graph 中有条边从节点 t1 指向节点 t2。事务 t2 需要等待事务 t1、t4 锁占用的row2对象,故而存在节点t2 到节点t1、t4的边。同样,存在节点 t3到节点t1、t2、t4的边,因此最终的
wait-for graph 如下图所示。
通过下图可以发现回路(t1,t2),因此存在死锁。通过上述介绍,可以发现 wait-for graph 是一种较为主动的死锁监测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说 InnoDB 存储引擎选择回滚 undo 量最小的事务。
这里写图片描述
wait-for graph 的死锁检测通常采用深度优先的算法实现,在 InnoDB 1.2 版本之前,都是采用递归方式实现。而从1.2版本开始,对wait-for graph 的死锁检测进行了优化,将递归用非递归的方式实现,从而进一步提高了 InnoDB 存储引擎的性能。

死锁的事例
如果程序是串行的,那么不可能发生死锁。死锁只存在于并发的情况,而数据库本身就是一个并发运行的程序,因此会发生死锁。下表演示了死锁的一种经典的情况,即A等待B,B在等待A,这种死锁问题呗称为AB-BA死锁。
这里写图片描述

在上述操作中,会话B中的事务抛出了 1213这个错误提示,即表示事务发生了死锁。死锁的原因是会话A和B的资源在相互等待。大多数的死锁 InnoDB 存储引擎本身可以侦测到,不需要认为进行干预。但是在上面的例子中,在会话 B 中的事务抛出死锁异常后,会话A 中马上得到了记录为2 的这个资源,这其实是因为会话B中的事务发生了回滚,否则会话A中的事务是不可能得到该资源的。InnoDB 存储引擎并不会回滚大部分的错误异常,但是死锁除外。发现死锁后,InnoDB 存储引擎会马上回滚一个事务,这点是需要注意的。因此如果在应用程序中捕获了1213 这个错误,其实并不需要对其进行回滚。
这里写图片描述

可以看到,会话A中已经对记录4持有了X锁,但是会话A中插入记录3时会导致死锁发生。这个问题的产生是由于会话B中请求记录4的S锁而发生等待,但之前请求的锁对于主键值记录1、2都已经成功,还需要向后获得记录3的锁,这样就显得优点不合理。因此 InnoDB存储引擎在这里主动选择了 死锁,而回滚的是 undo log记录大的事务,这与AB-BA死锁的处理方式又有所不同。

猜你喜欢

转载自blog.csdn.net/m0_37899949/article/details/79040543