你真的了解 “事务与并发”吗?

事务与并发

         开场白还是要来下的,又过了一年,很久没有和cnblogs朋友见面了。我还看到了我上几篇MSSQL的回复里面一直说期待我的下一个好文,唉!先谢罪了,太久没发表东东了。主要是因为有点忙,加上一回去就是学习,所以没时间给大家整理资料。最近因为要给公司培训一些东西,所以以后也会出一些文章的,所谓浓缩就是精华,所以大家就期待吧。

         其实写文章总结也是对自己知识的一些反复,总结的时候可以查漏补缺,也可以给各位博友们提供一些看法和思路。我记得看过一篇架构师的曲线图里面也讲到了“写文章”总结的重要性。

         今天开场白有点多,大家耐心点。呵呵,其实一直想写一个关于如何学习东西的文章,只有下次了,好了,那么我们进入主题(我会结合MSDN讲)。

鸟瞰

a)         应用程序事务与数据库存储引擎事务

b)         应用程序处理并发与数据库存储引擎处理并发

为什么要从两个方向来考虑呢,是有原因的,首先机制处理方式,以及在软件中的所处的环境以及情节都不同。所以我们可以将事务与并发分为两个部分来讲,这文主要讲MSSQL的事务,我会在接下来的文章谈谈java或者C#处理事务以及结合Martin Fowler说到的并发架构模式探讨。

什么是事务呢?

切看MSDN给出的定义:事务是作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元必须有四个属性,称为原子性、一致性、隔离性和持久性 (ACID) 属性,只有这样才能成为一个事务。

         大家不要小看这些定义,往往从最基础的定义才能进入最深层次的东西,我经常看到一些CSDN网友或者cnblogs里面的朋友,大放阙词,说改使用啥啥啥“锁提示”,改用啥啥啥“事务”最完美,其实都是不正确的。如果对事务不的ACID不熟悉的话,可以看看

ms-help://MS.MSDNQTR.v90.chs/udb9/html/c193ad34-be19-408a-a0fa-9723a7936a3c.htm(安装了本地MSDN)。

什么是并发呢?

当多个用户同时访问数据时,那么在这种情况下就叫做并发呢。

好了,上述定义都很直观、简单,相信大多数朋友早就知道了这些东西。

         接着,我们先谈并发,MS SQL SERVER 2005给了我们两种方式来处理并发,正如我们所熟悉的使用事务隔离级别,和锁提示(不清楚定义的,可以一会在下文看到)等。了解原理的朋友们应该知道,上述都是基于“资源锁定“的。还有一种处理并发的机制,可能大家不太了解,就是基于”行版本控制的“,说白了就是维护一个行的副本,进行的一些处理机制。

         那么我们在看看MSDN的定义,

·         锁定

每个事务对所依赖的资源(如行、页或表)请求不同类型的锁。锁可以阻止其他事务以某种可能会导致事务请求锁出错的方式修改资源。当事务不再依赖锁定的资源时,它将释放锁。

·         行版本控制

当启用了基于行版本控制的隔离级别时,数据库引擎 将维护修改的每一行的版本。应用程序可以指定事务使用行版本查看事务或查询开始时存在的数据,而不是使用锁保护所有读取。通过使用行版本控制,读取操作阻止其他事务的可能性将大大降低

 

当我们在MSSQL中不使用这写方法,就可能导致一些意外的结果,所以我们可以看看一些异常的情况。(我会引用许多微软的原文,大家见谅了。我主要负责解释难懂的地方,一些提供一些代码)

测试环境:MS SQL SERVER 2005

测试工具: SSMS

先执行下面的代码,建立测试环境:

SET NOCOUNT ON

GO

 

USE [Master]

GO

 

DECLARE @Path VARCHAR(MAX);

 

 

SET @Path=(SELECT SUBSTRING(physical_name,1,CHARINDEX('master.mdf',physical_name)-1) FROM sys.master_files WHERE file_id=1 AND database_id=1);

 

SELECT @Path AS 'Your database folder'

 

 

--Detect whether the database is exist

IF DB_ID('GoodGoodStudyDayDayUp') IS NOT NULL

    DROP DATABASE GoodGoodStudyDayDayUp;

GO

 

--Create the test-driven database

CREATE DATABASE GoodGoodStudyDayDayUp;

GO

 

--Change the context

USE GoodGoodStudyDayDayUp;

GO

丢失更新

当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其他事务的存在。最后的更新将覆盖由其他事务所做的更新,这将导致数据丢失。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。

 

很显然的,如果你和你同事同时修改一个代码,当你们都搞定准备提交的时候,那么可怕的事情就发生了,因为晚提交的往往才胜利,他将会覆盖你的版本,意味着你白忙活了。同样的,可以把这种事情隐射到许多情况之下。

未提交读(脏读)(Example 1

当第二个事务选择其他事务正在更新的行时,会发生未提交的依赖关系问题。第二个事务正在读取的数据还没有提交并且可能由更新此行的事务所更改。

 

例如,一个编辑人员正在更改电子文档。在更改过程中,另一个编辑人员复制了该文档(该副本包含到目前为止所做的全部更改)并将其分发给预期的用户。此后,第一个编辑人员认为目前所做的更改是错误的,于是删除了所做的编辑并保存了文档。分发给用户的文档包含不再存在的编辑内容,并且这些编辑内容应视为从未存在过。如果在第一个编辑人员保存最终更改并提交事务之前,任何人都不能读取更改的文档,则可以避免此问题。

T-SQL演示:

会话1

USE [GoodGoodStudyDayDayUp]

GO

 

BEGIN TRAN

 

--编辑人员开始读取第一条记录

SELECT * FROM dbo.Test1 WHERE TestID = 1

 

--最终修改编辑

UPDATE dbo.Test1 SET String='AAA' WHERE TestID = 1

 

WAITFOR DELAY '00:00:04';

 

--发现错误了,取消提交

ROLLBACK

会话2

USE [GoodGoodStudyDayDayUp]

GO

 

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

 

--编辑人员开始读取第一条记录,然后发给其他人

SELECT * FROM dbo.Test1 WHERE TestID = 1

 

--或者直接运行

 

SELECT * FROM dbo.Test1 WITH(NOLOCK) WHERE TestID=1

说明:新建两个查询,在查询1、2中分别复制上述代码,然后先运行1,再运行2。

我们发现,会话一先更新了记录,然后延时4秒以后,回滚,那么最终的记录还是A,而会话2就在更新的同时马上读取了读取(也就是未提交的数据,所以要设置隔离级别为READ UNCOMMITTED就是为提交的意思)脏数据,因为会话一还没确认提交。

    那么,这里头又有什么蹊跷了,为什么我使用的NOLOCK“锁提示”(再一次出现,不急),和设置隔离级别为脏读取就好了呢?嘿嘿,要成为高手,就要细心看到,什么!@!#@,我是菜鸟,呃。。。。

    那其中又用到了什么呢,其实我

们在看到“锁提示”就暗示着使用的是锁定来控制的并发。

微软为我们提供了一系列的视图和存储过程来观察锁定。

T-SQL:

USE GoodGoodStudyDayDayUp;

GO

--2000

SP_LOCK

 

-2005

SELECT * FROM sys.dm_tran_locks

回到刚才的问题,我们来观察下,在会话1的时候,都获取了一些什么锁。

先执行会话一,马上执行会话三(就是查询锁定信息的会话)

结果:

resource_type                        request_mode

PAGE                                                                          IX

OBJECT                                                                    IX

KEY                                                                                     X

 

结果显示在更新的事务中,分别请求了资源PAGE,OBJECT,KEY上的意向排他锁,意向排他锁和排他锁。锁的概念我一个个解释,我们先锁定KEY来看看,KEY(键值),我们更新的时候是通过主键锁定的记录,所以请求的锁资源就是KEY,那么request_mode就表示请求的什么锁类型,X就表示排他锁,大家请谨记哦,所谓排他,我们可以在MSDN的锁兼容性中看到他与所有其他种类的锁都不兼容,那意味着什么呢?当我们在某种资源上请求了一种锁的时候,那么就不能获取任何其他的锁。

下面做个简单的测试:

我们只要修改一下我们刚才的代码,去掉

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

以及WITH(NOLOCK)

那么我们先执行会话一进行更新,现在KEY上面请求了一个排他锁,那么马上执行修改后的会话二,在执行会话三查询所有请求的锁资源。

结果:

PAGE         IS

PAGE         IX

OBJECT    IS

OBJECT    IX

KEY          X

KEY          S

我们发现,在页级别,OBJECT对象级别,以及KEY级别都多了一个锁,IS,S他们分别是意向共享锁和共享锁,我先解释下共享锁,当我们执行查询语句的时候,都会请求一个共享锁(共享资源的嘛)。

所以 会话二发生锁等待就能解释了,因为会话一获取了KEY(ID=1)资源上的排他锁,而会话二想获取KEY上面的共享锁,刚才前面说了,X排他锁不和任何其他锁兼容,所以会话二就会被阻止。

         那么,我们再来看看官方对S,X锁的定义:

共享 (S)

用于不更改或不更新数据的读取操作,如 SELECT 语句。

排他 (X)

用于数据修改操作,例如 INSERTUPDATE DELETE。确保不会同时对同一资源进行多重更新。

所以就得到了解释,我们会话一致性的UPDATE语句,嘿嘿,你很聪明,可能也猜到了为什么设置了隔离级别和锁提示不会阻止会话二了,设置为“允许脏读(读取未提交)”隔离级别的时候,是不会请求资源上的共享锁的,那么测试下,把刚才的那些会话修改过来,进行测试:

PAGE         IX

OBJECT    IX

KEY          X

所以当我们使用

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTEDWITH(NOLOCK)

的时候不请求资源上的共享锁,还有一种情况不会请求共享锁,就是行版本控制技术。一会和大家说。

         通过上面的例子,我想大家,应该明白了一些东西,什么时候会请求共享锁,什么时候会请求排他锁了吧。先和大家解释个概念“锁提示”:可以在 SELECTINSERTUPDATE DELETE 语句中为单个表引用指定锁提示。提示指定 Microsoft SQL Server 数据库引擎实例用于表数据的锁类型或行版本控制。当需要对对象所获得锁类型进行更精细控制时,可以使用表级锁提示。这些锁提示覆盖会话的当前事务隔离级别。我们再看下微软的建议:

注意:

数据库引擎查询优化器几乎总是会选择正确的锁级别。建议只在必要时才使用表级锁提示来更改默认的锁行为。禁止锁级别反过来会影响并发。

所以你不是一个DBA专家的话,就不要使用锁定提示了哈。

         OK,告一段落,大家脑袋里面构思下,在看看前面做的测试,我提供了下载,在Example 1文件夹中。在想的差不多的时候,看下如下定义,那是MSDN的官方定义,我总结的在好,和官方总有一些出入,看清楚最原始的,我的只是便于理解。大家认真看下吧:(我已经从MSDN提出来了)

锁定是 Microsoft SQL Server 数据库引擎 用来同步多个用户同时对同一个数据块的访问的一种机制。

在事务获取数据块当前状态的依赖关系(比如通过读取或修改数据)之前,它必须保护自己不受其他事务对同一数据进行修改的影响。事务通过请求锁定数据块来达到此目的。锁有多种模式,如共享或独占。锁模式定义了事务对数据所拥有的依赖关系级别。如果某个事务已获得特定数据的锁,则其他事务不能获得会与该锁模式发生冲突的锁。如果事务请求的锁模式与已授予同一数据的锁发生冲突,则数据库引擎 实例将暂停事务请求直到第一个锁释放。

当事务修改某个数据块时,它将持有保护所做修改的锁直到事务结束。事务持有(所获取的用来保护读取操作的)锁的时间长度,取决于事务隔离级别设置。一个事务持有的所有锁都在事务完成(无论是提交还是回滚)时释放。

应用程序一般不直接请求锁。锁由数据库引擎 的一个部件(称为锁管理器)在内部管理。当数据库引擎 实例处理 Transact-SQL 语句时,数据库引擎 查询处理器会决定将要访问哪些资源。查询处理器根据访问类型和事务隔离级别设置来确定保护每一资源所需的锁的类型。然后,查询处理器将向锁管理器请求适当的锁。如果与其他事务所持有的锁不会发生冲突,锁管理器将授予该锁。

      当你看到这里的时候,是不是清醒了很多,先恭喜下你又长进了。那么继续吧。。。。刚才的学习测试中,我们看到了其他的一些锁,比如IX,IS等等。他们又是什么呢?

      Microsoft SQL Server 数据库引擎 具有多粒度锁定,允许一个事务锁定不同类型的资源。为了尽量减少锁定的开销,数据库引擎 自动将资源锁定在适合任务的级别。锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问。但其开销较低,因为需要维护的锁较少

数据库引擎 通常必须获取多粒度级别上的锁才能完整地保护资源。这组多粒度级别上的锁称为锁层次结构。例如,为了完整地保护对索引的读取,数据库引擎 实例可能必须获取行上的共享锁以及页和表上的意向共享锁关于锁粒度和层次结果可以看看这里

      关于每种锁的用途的,可以看看这里和每种锁之间的兼容行。我强烈建议大家,记熟悉用熟悉这些锁的用途,当真正的了解到以后,才能更好的做资源并发处理。如果你没有看这个锁用途,那么接下来的文章,你将话费更大的力气,所以看完了再继续。对了还有关于数据库引擎中的隔离级别

不一致的分析(不可重复读)(Example 2)

当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。不一致的分析与未提交的依赖关系类似,因为其他事务也是正在更改第二个事务正在读取的数据。但是,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。此外,不一致的分析涉及多次(两次或更多)读取同一行,而且每次信息都被其他事务更改,因此我们称之为不可重复读

例如,编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果在编辑人员完成最后一次读取文档之前,作者不能更改文档,则可以避免此问题。

同样的,我们用T-SQL来模拟这种环境:

会话一:

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:不可重复读测试会话一

 

*/

USE GoodGoodStudyDayDayUp;

GO

 

BEGIN TRAN

--编辑人员读取了一条记录

SELECT * FROM dbo.Test1 WHERE TestID = 1

 

--他处理一些东西,上了个厕所,等待了秒

WAITFOR DELAY '00:00:05'

 

--处理完以后,再次查询

SELECT * FROM dbo.Test1 WHERE TestID = 1

 

COMMIT

会话2

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:不可重复读测试会话二

 

*/

 

BEGIN TRAN

 

--编辑人员,在会话一的同时修改了数据

UPDATE dbo.Test1 SET String='AA' WHERE TestID=1

 

COMMIT TRAN

 

--还原记录

--UPDATE dbo.Test1 SET String='A' WHERE TestID=1

会话3

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:不可重复读测试会话三

 

*/

 

USE GoodGoodStudyDayDayUp;

GO

 

SELECT * FROM sys.dm_tran_locks

 

我们分别执行会话123

会话一执行要等待5秒,立即执行会话2,因为会话1中的查询语句获取的共享锁是一瞬间的事情,查询完成以后马上就释放了,所以会话2不发生锁等待,直接更新成功,然后执行会话3,因为会话一的共享锁可能已经释放,而会话2更新也是一瞬间的事情,所以将获取不到其他的锁,只能获取3个数据库的共享锁,因为开启了3个会话。(你当然可以在会话2中放置WAITFOR DELAY来延时,那么在会话3中观察到X锁,自己测试吧。)

最后会话的结果为:

TestID      String

----------- --------------------

1           A

 

(1 row(s) affected)

 

TestID      String

----------- --------------------

1           AA

 

(1 row(s) affected)

很明显的,出现了MSDN描述的不可重复读的问题。编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。

如果你看了前面我推荐的关于锁的文章,数据库引擎中的隔离级别这个的话,那么你应该知道“已提交读”READ COMMITTED是不能防止这种问题的,为什么呢?请问大屏幕。

已提交读的原理是,在有更新的资源上要去获取S共享锁。但是,这是的情况是,先进行查询,资源获取共享锁以后,立即释放,然后这个时候会话2更新资源,当会话一继续的时候,那么读取的就是更新后的数据了。也就是说,防止脏读的隔离级别,只能保证数据在取的那一瞬间,是保证是其他事务已经提交的数据,否则发生锁等待,等待其他事务/会话释放X锁,然后交给当前事务获取S共享锁,所以在一瞬间中,不能保证整个事务读取的东西都不不受其他事务影响,已提交读,只保证自己获取的数据是别人已经提交的了。

      那么下面,我通过申明“可重复读“来防止这个问题:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

在会话一设置以后,运行会话一,立即运行会话3观察下获取的锁,然后运行会话2,在运行行会话3

会话3分别显示的结果是:

第一次:

PAGE       IS

OBJECT     IS

KEY         S

 

第二次:

PAGE    IX

PAGE    IS

OBJECT IX

OBJECT IS

KEY     S

KEY     X

第一次结果显示说明了,当我们指定了这个隔离级别的时候,将在所指定的资源中获取共享锁,而且会一直持续到事务接触,那么就意味着,我们的会话二肯定会受到阻塞,因为他要获取资源的排他锁。可以从第2次的结果中看出来,那么也就解决了当编辑人员1处理文档的过程中一致都不会受其他会话的影响,使多次读取的结果不一致。

我们再来看看MSDN对这个隔离级别的详细定义: 指定语句不能读取已由其他事务修改但尚未提交的行,并且指定,其他任何事务都不能在当前事务完成之前修改由当前事务读取的数据。对事务中的每个语句所读取的全部数据都设置了共享锁,并且该共享锁一直保持到事务完成为止。这样可以防止其他事务修改当前事务读取的任何行。其他事务可以插入与当前事务所发出语句的搜索条件相匹配的新行。如果当前事务随后重试执行该语句,它会检索新行,从而产生幻读。由于共享锁一直保持到事务结束,而不是在每个语句结束时释放,所以并发级别低于默认的 READ COMMITTED 隔离级别。此选项只在必要时使用。

红色部分也就证明我们刚才说是放置读取资源的共享锁一直到事务结束的猜测是正确的。同时他提到了,这种方式不能防止幻读。那么我们就来研究一下这个东西。

当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻读问题。由于其他事务的删除操作,事务第一次读取的行的范围显示有一行不再存在于第二次或后续读取内容中。同样,由于其他事务的插入操作,事务第二次或后续读取的内容显示有一行并不存在于原始读取内容中。

幻读(Example 3)

例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主副本时,发现作者已将未编辑的新材料添加到该文档中。与不可重复读的情况相似,如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免此问题。

我们还是用3个会话来模拟这种环境:

会话一:

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:幻读测试会话一

 

*/

 

--先删除ID为的这条记录

--DELETE FROM dbo.Test1 WHERE TestID=5

 

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRAN

 

SELECT * FROM dbo.Test1 WHERE TestID<=7

WAITFOR DELAY '00:00:05'

 

COMMIT

会话2:

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:幻读测试会话二

 

*/

 

SET IDENTITY_INSERT dbo.Test1 ON

INSERT INTO dbo.Test1(TestID,String) VALUES(5,'E');

SET IDENTITY_INSERT dbo.Test1 OFF

会话3:

/*

 

Author:浪客

DateTime:2009.06.15 20:13

Location:成都

Description:幻读测试会话三

 

*/

 

USE GoodGoodStudyDayDayUp;

GO

 

SELECT * FROM sys.dm_tran_locks

我们分别运行会话1,会话3,会话2,会话3。那么3最终的结果为:

PAGE    IX

PAGE    IS

KEY     RangeS-S

 

KEY RangeI-N

OBJECT IX

OBJECT IS

KEY     RangeS-S

KEY     RangeS-S

KEY     RangeS-S

KEY     RangeS-S

KEY     RangeS-S

KEY     RangeS-S

KEY     RangeS-S

我们看到结果中存在一个RangeI-N锁,我们来看下MSDN的定义:

RangeI

Null

RangeI-N

插入范围,空资源锁;用于在索引中插入新键之前测试范围。

再看下我们的测试中,其实有一条记录是不存在的,所以也会有一个这样的锁。关于范围锁定看看这里

 

好了,几种情况也和大家描述了,具体的情况还是要读者们自己去研究,毕竟要全部说清楚还是要自己去领悟对吧。

暂时就说到这里,其他的以后想写文章了帮大家总结,其实所有东西MSDN讲述的已经够清楚了,只不过有点凌乱,呵呵:)。
这个文章是草稿,是给公司做培训用的,修改好的不能发哈,不好意思了
/Files/bhtfg538/T-SQL.rar

posted @ 2009-08-04 23:41  Sai~  阅读(14820)  评论(18编辑  收藏  举报