1 概述数据库
本篇文章简要对事物与锁的分析比较详细,所以就转载了。安全
2 具体内容并发
并发能够定义为多个进程同时访问或修改共享数据的能力。处于活动状态而互不干涉的并发用户进程的数量越多,数据库系统的并发性就越好。当一个正在修改数据的进程阻止了其余进程读取该数据,或者当一个正在读取数据的进程阻止了其余进程修改该数据,并发性就下降了。本文用术语“读取”或者“访问”描述数据上的SELECT操做,用“写入”或“修改”描述数据上的INSERT,UPDATE以及DELETE操做。高并发
通常地,数据库系统能够采用两种方式来管理并发数据访问,乐观并发控制和悲观并发控制。性能
对于任何一种并发控制模式,若是两个事务试图同一时刻修改数据的话都会产生冲突。这两种模式之间的区别在于,是在冲突发生前进行防止,仍是发生后采起某种方法来处理冲突。学习
对于乐观并发控制,该模型假定系统中存在很是少的相互冲突的数据修改操做,以至任何单独的事务都不太可能修改其它事务正在修改的数据。乐观并发控制默认采用行版本控制来处理并发。spa
例如,在读取数据时咱们会获得一个数据的版本version 1,当须要修改数据时,咱们先检查数据的版本是否是version 1,若是是就修改数据;若是不是,就说明在当前事务的读操做和写操做之间已经有别的事务对数据进行了修改(每次修改操做都会使得数据的版本+1),SQL Server将会产生一个错误消息,由上层应用程序响应此错误。设计
原子性(Atomicity)
SQL Server保证事务的原子性。原子性指的是每一个事务要么所有执行,要么什么都不执行。也就是说,若是一个事务提交了,它形成的全部效果都会被保留。若是停止了,其全部效果都会被撤销。3d
一致性(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采用了一种称为“键范围锁”的单独锁模式,与索引中的特定键值相关联并代表在索引中这两个键之间的全部值被锁定住了。
锁简称
简单兼容性矩阵
完整兼容性矩阵
乐观并发控制采用了一种称为行版本控制的新技术来保障事务。在使用乐观锁并发控制时会获取排他锁。乐观并发和悲观并发的区别在于乐观并发中写操做与读操做之间不会互相阻塞。换句话说就是,当被请求资源当前拥有共享锁时,申请排它锁的事务不会被阻塞,相反,当被请求资源当前拥有排他锁时,申请共享锁的进程也不会被阻塞。
一旦启用乐观并反控制,SQL Server就使用tempdb数据库来存储全部已经修改过的记录的副本,而且只要存在来自任意事务的访问需求,就会继续维持这些副本。当tempdb用来存储被修改记录的早期版本时,就其称为版本存储区。
SQL Server引入了一种新的隔离级别:快照隔离以及一种新式的无阻塞风格的已提交读隔离——已提交读(快照)。这些基于版本控制的隔离级别容许读者获取行的一个先前已提交过的值而不会产生阻塞,这样就提升了系统的并发能力。为了使它起做用,SQL Server必须在行被修改或删除时保留旧版本的记录。若是在同一行上进行屡次更新,SQL Server就可能须要维护该行的多个早起版本。鉴于此,行版本控制有时也被称为多版并发控制。
当表或索引中的一行数据被更新时,SQL Server会用执行更新的那个事务的事务序列号来标记新的行。事务序列号是一个单调递增的数字,在每一个SQL Server的实例中保证惟一。在更新一行数据时,以前的版本存放在版本存储区内,而新的行包含一个指向版本存储区中旧的行数据的指针。版本存储区里旧的行数据可能包含了指向更早版本的指针。一条行记录的全部版本串接成一个链表。SQL Server可能须要沿着链表中的几个指针才能到达一个正确的版本,只要有操做须要引用它们,行的版本就必须在版本存储区内保存。
在应用程序使用默认的悲观模型形成的并发性降低而不能使人满意时,SQL Server能够改用乐观并发控制模型。在切换到基于乐观版本控制的隔离级别以前,用户必须仔细权衡使用新型并发模型的效果。处理须要额外的管理来为版本存储区监控tempdb之外,鉴于维护旧版本锁带来的额外工做量,版本控制还会下降更新操做的性能。即便当前没有人在读取数据,更新操做也得为此买单。若是有使用行版本控制的读操做,它们必须花费额外的开销来遍历链表指针,以找到须要的行数据的合适版本。
另外,因为快照隔离的乐观并发模型假定系统不会发生不少的更新冲突,若是用户预见到在同一数据上的并发更新会产生竞争,就不该该选择快照隔离级别。快照隔离级别可以使读者不被写者阻塞,可是并发的写者仍然不被容许。在默认的悲观模型中,第一个写者会阻塞全部的后续写者,但若是采用快照隔离,后续写者实际上会接受到错误消息且应用程序须要从新提交初始请求。
已提交读快照隔离是一种语句级的快照隔离,也就是任何查询都能看到在语句开始那一刻最近提交过的数值。假设在启用了RCSI的数据库上有以下两个事务,且在事务开始运行以前Product 922的ListPrice值是8.89
注意当时间为2时,事务1所做出的修改还没有提交,所以Product ID=922的行上仍然持有锁。可是事务2不会被这个锁阻塞住,它可以访问该行数据上一次已提交的ListPrice值8.89。这仍然属于已提交读隔离级别(一个无阻塞的变种),因此不能防止“不可重复读”。
RCSI最大的益处是能够引入更好的并发性,由于读者与写者之间不会相互阻塞。可是写者之间仍是会发生阻塞,所以标准的加锁机制适用于所有的更新、删除和插入操做。
冲突发生是由于事务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/
4 版权