Sqlserver (转载)事物与锁
1 概述
本篇文章简要对事物与锁的分析比较详细,因此就转载了。
2 具体内容
并发可以定义为多个进程同时访问或修改共享数据的能力。处于活动状态而互不干涉的并发用户进程的数量越多,数据库系统的并发性就越好。当一个正在修改数据的进程阻止了其他进程读取该数据,或者当一个正在读取数据的进程阻止了其他进程修改该数据,并发性就降低了。本文用术语“读取”或者“访问”描述数据上的SELECT操作,用“写入”或“修改”描述数据上的INSERT,UPDATE以及DELETE操作。
一般地,数据库系统可以采用两种方式来管理并发数据访问,乐观并发控制和悲观并发控制。
并发控制模型
对于任何一种并发控制模式,如果两个事务试图同一时刻修改数据的话都会产生冲突。这两种模式之间的区别在于,是在冲突发生前进行防止,还是发生后采取某种方法来处理冲突。
悲观并发控制
乐观并发控制
对于乐观并发控制,该模型假定系统中存在非常少的相互冲突的数据修改操作,以致任何单独的事务都不太可能修改其它事务正在修改的数据。乐观并发控制默认采用行版本控制来处理并发。
例如,在读取数据时我们会得到一个数据的版本version 1,当需要修改数据时,我们先检查数据的版本是不是version 1,如果是就修改数据;如果不是,就说明在当前事务的读操作和写操作之间已经有别的事务对数据进行了修改(每次修改操作都会使得数据的版本+1),SQL Server将会产生一个错误消息,由上层应用程序响应此错误。
事务处理
ACID属性
原子性(Atomicity)
SQL Server保证事务的原子性。原子性指的是每个事务要么全部执行,要么什么都不执行。也就是说,如果一个事务提交了,它造成的所有效果都会被保留。如果中止了,其所有效果都会被撤销。
一致性(Consistency)
一致性属性确保事务不允许系统到达一个不准确的逻辑状态——数据必须总是保持逻辑上的正确。即使在发生系统故障时,约束和规则必须得到保证。(一致性一般被原子性、隔离性以及持久性所涵盖,并且概念上会产生重复)
隔离性(Isolation)
隔离性会将并发事务与其他并发事务的更新操作分隔开。当该事务正在执行时,其他事务是无法看到进行中的任务的。SQL Server会在事务之间自动实现隔离。它采用锁定数据或者行版本使得多个并发事务能够并发操作数据,以防止导致不正确结果。
隔离性意味着事务必须在不干扰其他事务的前提下独立执行。换言之,在事务执行完毕之前,其所访问的数据不能受系统其他部分的影响。
持久性(Durability)
当事务提交之后,SQL Server的持久性属性就会确保该事务的作用持续存在(即使发生系统故障)。如果在事务进行过程中发生系统故障,事务就会被完全撤销,不会在数据上遗留部分作用。如果在事务的提交确认被发送到调用的程序之后立刻发生故障,数据库会确保该事务的存在。预写式日志以及SQL Server启动恢复阶段的事务自动回滚/自动重做机制能够确保持久性。
一致性问题
丢失更新
时间 | 取款事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000 | |
T4 | 查询账户余额为1000 | |
T5 | 取出100,存款余额为900 | |
T6 | 取出300,存款余额为700 | |
T7 | 提交事务 | |
T8 | 提交事务 |
最终账户余额为900,取款事务A的更新丢失了。丢失更新是这些行为中唯一一个用户可能在所有情况下都想避免的行为。
脏读
时间 | 查询事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000 | |
T4 | 取出100,存款余额为900 | |
T5 | 查询账户余额为900 | |
T6 | 撤销事务,恢复为1000 | |
T7 | 提交事务 |
查询事务A读取到取款事务B还未提交的余额900。
默认情况下,脏读是不允许的。谨记:更新数据的事务是无法控制别的事务在它提交之前读取其数据的,这是由读取数据的事务来决定是否想要读取未必会被提交的数据。
不可重复读
时间 | 查询事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000 | |
T4 | 取出100,存款余额为900 | |
T5 | 查询账户余额为900 | |
T6 | 提交事务 | |
T7 | 提交事务 |
查询事务A两次读取余额获取到不同结果。
幻读
时间 | 取款记录处理事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询到5条取款记录 | |
T4 | 查询余额为1000元 | |
T5 | 取出100,存款余额为900 | |
T6 | 查询到6条取款记录 | |
T7 | 提交事务 | |
T8 | 提交事务 |
对于取款记录处理事务A,两次查询的结果集不同。
隔离级别
SQL Server支持五种隔离级别来控制读操作的行为。其中三个只在悲观并发模型中可用,一个只在乐观并发模型中可用。剩下的一个在两个模式下都是可用的。
未提交读
除了丢失更新以外,上面提到的其他行为都可能发生。未提交读是通过使读操作不占用任何锁来实现的,当前事务能够读取其他事务已经修改过但是尚未提交的数据。
当采用未提交读时,用户是放弃了对高一致性数据的把握而趋向于支持系统的高并发能力,使用户不会再互相锁定对方。那么,何时才应该选择未提交读呢?显然,每笔数据都须保证平衡的金融交易是不适合的。而对于某些决策支持分析来说可能会很适合(譬如,需要察看销售走势时),因为完全没有必要做到完全精确而且会带来并发性能的提升,因此是相当值得的。
已提交读
已提交读是数据库引擎的默认级别。SQL Server 2005支持两种已提交读的隔离级别,这种隔离级别既可以是乐观的也可以是悲观的,默认采用悲观并发控制。为了区分,悲观实现称“已提交读(锁定)”,乐观实现称为”已提交读(快照)”。
已提交读隔离级别保证了一个操作不会读到别的程序已经修改但是尚未提交的数据。如果别的事务正在更新数据并因此在数据行上持有排它锁,当前的事务就必须等待这些锁释放后才能使用这个数据(无论是读取还是修改)。同样地,事务必须至少在要被访问的数据上加上共享锁,其他事务可以读取数据但是不能修改数据。默认,共享锁在数据读取过后就被释放掉,而无需在事务的持续时间内保留。
已提交读(快照),也能保证一个操作不会读到未提交数据,但不是通过迫使其他进程等待的方式。对于已提交读(快照),每当一行数据被修改后,SQL Server就会生成该行数据前一次已提交值的一个版本(version),被修改的数据仍旧被锁定着,但是其他进程可以看到该数据在更新操作开始之前的版本。
可重复读
可重复读是一种悲观的隔离级别。它在已提交读的基础上增加了新的属性:确保当事务重新访问数据或查询被再一次执行时,数据将不再发生改变。换句话说,在一个事务中执行相同的查询两次是不会看到由其他事务所造成的任何数据的改变的。然而,可重复读隔离级别还是允许幻读的出现。
在某些情况下,防止不可重复读是用户向往的一种安全措施。但是世上没有免费的午餐,这种额外的措施所带来的开销是事务中所有的共享锁必须保留到事务完成为止。
排它锁必须总是保留到事务结束为止,无论采用何种隔离级别或者并发模型,这样事务才能在需要时被回滚。如果锁提前释放了,就不太可能完成撤销操作,因为其他并发事务可能已经使用了同一数据,并且修改了它的值。
只要事务是打开的,没有其他用户可以修改被该事务所访问的数据。显然这会严重降低并发性和性能。因此,如果事务不保持简短或者编写应用程序时没有能够注意到这样潜在的锁竞争问题,将会导致大量的事务因为等待锁释放而挂起。
快照
快照隔离是一种乐观隔离级别,类似于已提交读(快照),如果当前版本被锁定住时,它允许其他事务读取已提交数据的早期版本。快照隔离和已提交读(快照)的区别与(早期版本该有多早、保留多少个早期版本)这个问题相关,我们在行版本控制小节中详述。尽管快照隔离所避免的行为和可串行化所避免的是相同的,但是快照隔离并不是真正意义上的可串行化隔离级别。对于快照隔离,可能会有两个个事务同时执行,并引起一个任何序列化执行都不可能产生的结果。
可串行化
可串行化也是一种悲观隔离级别。可串行化隔离级别在可重复读的基础上增加了新的属性:确保在重新执行查询时,SQL Server不会在中间的过渡期增加新的行。换句话说,如果同一事物在相同的查询被执行两次的话,幻读不会出现。可串行化也因此成为最健壮的悲观隔离级别,因为防止了之前所描述的所有可能的“不一致问题“。
额外的安全措施必定会带来额外的开销。可串行化隔离级别下,事务中的所有共享锁都必须保留到事务完成为止。另外,执行可串行化隔离级别不仅需要锁定已读数据,还需要锁定那些不存在的数据,参看后面的键范围锁。
锁定
锁定的基本概念
SQL Server可以使用几种不同方式来锁定数据,举例来说,读操作获取共享锁而写操作获取排他锁。更新锁在更新操作的开头部分获取。SQL Server会自动获取并释放所有这些类型的锁。它还负责管理锁定模式之间的兼容性,解决死锁问题,并在需要的时候进行锁升级。它在表、表的分页、索引键以及单独的数据行上支配锁。
锁定类型
共享锁
排它锁
更新锁
更新锁本身不足以使用户能够修改数据——所有的数据修改都要求被修改的数据资源上存在一个排它锁。只要有一个事务对资源持有更新锁,其它事务就无法获取该资源的更新锁或者排他锁了。持有更新锁的事务能够将其转换成该资源上的排它锁,因为更新锁避免了与其他进程之间的锁的不兼容。可以将更新锁看作是“意图更新锁”,这才是它实际所扮演的角色。更新锁会保留到事务结束或者当它转换成排他锁。
不要被锁的名字误导,更新锁并不只是针对更新操作而设计的。SQL Server使用更新锁适用于任何需要进行实际修改之前搜索数据的数据修改操作。这样的操作包括受限更新及删除,也包括在带有聚集索引的表上进行的插入操作。对于后面一种情况,SQL Server必须先搜索数据(使用聚集索引)以找到正确的位置来插入新的记录。当SQL Server只进行到搜索阶段时,它会采用更新锁来保护数据,而只有当它找到正确的位置并开始插入以后才将更新锁升级为排他锁。
意向锁
键范围锁
锁的粒度
键锁
SQL Server支持两种类型的键锁,而它采用哪种类型则取决于当前事务的隔离级别。如果隔离级别是已经提交读、可重复读或者快照,SQL Server会在处理查询时尝试锁定实际被访问的索引键。对于聚集索引的表而言,数据行就是索引的叶级别,而用户可以看到所获取的键锁。如果表是堆结构的话,用户可能会看到非聚集索引上的键锁以及实际数据上的行锁。
如果隔离级别是可串行化,情况就有所不同了。为了防止幻读,如果一个事务中扫描了一个范围内的数据就需要充分锁定住该表以确保没人能够插入新值到已扫描的范围内。在SQL Server早期版本中是通过锁定整个分页甚至整张表来保证这一点的。在许多情况下,这可能导致了更大范围的数据被锁定住了,造成了不必要的资源竞争。SQL Server 2005采用了一种称为“键范围锁”的单独锁模式,与索引中的特定键值相关联并表明在索引中这两个键之间的所有值被锁定住了。
锁的兼容性
锁简称
简单兼容性矩阵
完整兼容性矩阵
行级锁定VS分页锁定
锁升级
死锁
行级版本控制
乐观并发控制采用了一种称为行版本控制的新技术来保障事务。在使用乐观锁并发控制时会获取排他锁。乐观并发和悲观并发的区别在于乐观并发中写操作与读操作之间不会互相阻塞。换句话说就是,当被请求资源当前拥有共享锁时,申请排它锁的事务不会被阻塞,相反,当被请求资源当前拥有排他锁时,申请共享锁的进程也不会被阻塞。
一旦启用乐观并反控制,SQL Server就使用tempdb数据库来存储所有已经修改过的记录的副本,并且只要存在来自任意事务的访问需求,就会继续维持这些副本。当tempdb用来存储被修改记录的早期版本时,就其称为版本存储区。
行版本控制的实现
SQL Server引入了一种新的隔离级别:快照隔离以及一种新式的无阻塞风格的已提交读隔离——已提交读(快照)。这些基于版本控制的隔离级别允许读者获取行的一个先前已提交过的值而不会产生阻塞,这样就提高了系统的并发能力。为了使它起作用,SQL Server必须在行被修改或删除时保留旧版本的记录。如果在同一行上进行多次更新,SQL Server就可能需要维护该行的多个早起版本。鉴于此,行版本控制有时也被称为多版并发控制。
当表或索引中的一行数据被更新时,SQL Server会用执行更新的那个事务的事务序列号来标记新的行。事务序列号是一个单调递增的数字,在每个SQL Server的实例中保证唯一。在更新一行数据时,之前的版本存放在版本存储区内,而新的行包含一个指向版本存储区中旧的行数据的指针。版本存储区里旧的行数据可能包含了指向更早版本的指针。一条行记录的所有版本串接成一个链表。SQL Server可能需要沿着链表中的几个指针才能到达一个正确的版本,只要有操作需要引用它们,行的版本就必须在版本存储区内保存。
在应用程序使用默认的悲观模型造成的并发性下降而不能令人满意时,SQL Server可以改用乐观并发控制模型。在切换到基于乐观版本控制的隔离级别之前,用户必须仔细权衡使用新型并发模型的效果。处理需要额外的管理来为版本存储区监控tempdb以外,鉴于维护旧版本锁带来的额外工作量,版本控制还会降低更新操作的性能。即使当前没有人在读取数据,更新操作也得为此买单。如果有使用行版本控制的读操作,它们必须花费额外的开销来遍历链表指针,以找到需要的行数据的合适版本。
另外,由于快照隔离的乐观并发模型假定系统不会发生很多的更新冲突,如果用户预见到在同一数据上的并发更新会产生竞争,就不应该选择快照隔离级别。快照隔离级别能够使读者不被写者阻塞,但是并发的写者仍然不被允许。在默认的悲观模型中,第一个写者会阻塞所有的后续写者,但如果采用快照隔离,后续写者实际上会接受到错误消息且应用程序需要重新提交初始请求。
基于快照的隔离级别
已提交读快照隔离(RCSI)
已提交读快照隔离是一种语句级的快照隔离,也就是任何查询都能看到在语句开始那一刻最近提交过的数值。假设在启用了RCSI的数据库上有如下两个事务,且在事务开始运行之前Product 922的ListPrice值是8.89
注意当时间为2时,事务1所作出的修改尚未提交,因此Product ID=922的行上仍然持有锁。但是事务2不会被这个锁阻塞住,它能够访问该行数据上一次已提交的ListPrice值8.89。这仍然属于已提交读隔离级别(一个无阻塞的变种),所以不能防止“不可重复读”。
RCSI最大的益处是可以引入更好的并发性,因为读者与写者之间不会相互阻塞。但是写者之间还是会发生阻塞,因此标准的加锁机制适用于全部的更新、删除和插入操作。
快照隔离(SI)
更新冲突
冲突发生是因为事务2在Quantity值为324的时候开始,当这个值被事务1更新后,行版本324被存储到版本存储区内。事务2会在事务的持续时间内继续读取该行数据。如果两个更新操作都被允许成功执行的话,就会产生经典的更新丢失情形。事务1增加了200个数量,然后事务2会在初始值上增加300个数量并存储。由第一个事务增加的那200个产品就会彻底丢失,SQL Server不会允许这样的情况发生。
当事务2开始尝试执行更新时,并不会立刻得到一个错误——仅仅是被阻塞。事务1在行上拥有一个排他锁,因此事务2尝试获取排他锁时会被阻塞。如果事务1回滚,那么事务2就能够完成更新。但事务1最终被提交了,SQL Server检测到一个冲突并产生错误。
冲突只可能发生在SI模式下,因为SI隔离级别是基于事务而不是基于语句的。如果上述例子在一个采用RCSI的数据库中执行,事务2执行的更新语句不会使用该数据的原来值。当试图读取当前的Quantity值时,它会被阻塞住,而接着事务1完成时,它就能读取更新过的Quantity将其作为当前值并再增加300,没有一个更新会丢失。
如果用户选择工作在SI模式下就需要注意可能发生的冲突,它们能够被减少到最低限度,但是如同死锁一样,用户不能保证不发生冲突。用户必须写程序来合理地处理冲突,并且不能想当然地认为更新已经成功了。如果冲突只是偶然发生,用户可能需要将其作为使用SI模式的部分代价考虑在内,但如果冲突太过频繁,就需要额外措施来避免冲突。
3 参考文献
【01】http://blog.jobbole.com/104445/