190.事务管理与并发控制

第10章事务管理与并发控制程序员

•    10.1 事务的基本概念

 

10.1.1 事务

Ø 事务(Transaction)是构成单一逻辑工做单元的数据库操做序列。这些操做是一个统一的总体,要么所有成功执行(执行结果写到物理数据文件),要么所有不执行(执行结果没有写到任何的物理数据文件)。也能够这样理解,事务是若干操做语句的序列,这些语句序列要么所有成功执行,要么所有都不执行。所有不执行的状况是:在执行到这些语句序列中的某一条语句时,因为某种缘由(如断电、磁盘故障等)而致使该语句执行失败,这时将撤销在该语句以前已经执行的语句所产生的结果,使数据库恢复到执行这些语句序列以前的状态。数据库

【例子】对于银行转账问题,能够表述为:将账户A1上的金额x转到账户A2。这个操做过程能够用如图10.1所示的流程表示。安全

 

•         若是转账程序在恰好执行完操做③的时刻出现硬件故障,并由此致使程序运行中断,那么数据库就处于这样的状态:账号A1中已经被扣除金额x(转出部分),而账号A2并无增长相应的金额x。也就是说,已经从账号A1上转出金额x,但账号A2并无收到这批钱。显然,这种状况在实际应用决不容许出现。架构

•         若是将上述操做①至⑤定义为一个事务,因为事务中的操做要么全都执行,要么全都不执行,那么就能够避免出现上述错误的状态。这就是事务的魅力。并发

 

 

10.1.2 事务的ACID特性

Ø 做为一种特殊的数据库操做序列,事务的主要特性体现如下四个方面:post

(1)原子性(Atomicity)性能

        事务是数据库操做的逻辑工做单位。就操做而言,事务中的操做是一个总体,不能再被分割,要么所有成功执行,要么所有不成功执行。spa

(2)一致性(Consistency)3d

       事务的一致性是指出事务执行先后都可以保持数据库状态的一致性,即事务的执行结果是将数据库从一个一致状态转变为另外一个一致状态。日志

Ø  实际上,事务的一致性和原子性是密切相关的。

Ø  对于前面转账的例子,当操做操做③被执行后,出于某种客观缘由而致使操做④不能被执行时,若是操做③和④都是同一个事务中的操做,那么因为事务具备原子性,因此操做①、②和③执行的结果也自动被取消,这样数据库就回到执行操做①前的状态,从而保持数据库的一致性。

Ø  数据库的一致性状态除了取决于事务的一致性之外,还要求在事务开始执行时的数据库状态也必须一致的。不然就算事务具备一致性,但在执行该事务后并不必定可以保持数据库状态的一致性。

(3)隔离性(Isolation)

        隔离性是指多个事务在执行时不相互干扰的一种特性。事务的隔离性意味着一个事务的内部操做及其使用的数据对其余事务是不透明的,其余事务感受不到这些操做和数据的存在,更不会干扰这些操做和数据。也就是说,事务的隔离性使系统中的每一个事务都感受到“只有本身在工做”,而感受不到系统中还有其余事务在并发执行,

(4)持久性(Durability)

        持久性或称永久性(Permanence),是指一个事务一旦成功提交,其结果对数据库的改变将是永久的,即便是出现系统故障等问题。

事务的这四个特性一般被称为事务的ACID特性。一个数据库管理系统及其并发控制机制应该能确保这些特性不遭到破坏。

 

 

•    10.2 事务的管理

 

 

10.2.1 启动事务

Ø 启动事务方式有三种:显式启动、自动提交和隐式启动。

1. 显式启动

       显式启动是以BEGIN TRANSACTION命令开始的,即当执行到该语句的时SQL Server将认为这是一个事务的起点。

        BEGIN TRANSACTION的语法以下:

   BEGIN { TRAN | TRANSACTION }

       [ { transaction_name | @tran_name_variable }

      [ WITH MARK [ 'description' ] ]

    ]

   [ ; ]

 

u 其参数意义以下:

Ø transaction_name | @tran_name_variable

     指定事务的名称,能够用变量提供名称。该项是可选项。若是是事务是嵌套的,则仅在最外面的BEGIN...COMMIT或BEGIN...ROLLBACK嵌套语句对中使用事务名。

Ø WITH MARK [ 'description' ]

      指定在日志中标记事务。description 是描述该标记的字符串。若是使用了WITH MARK,则必须指定事务名。WITH MARK容许将事务日志还原到命名标记。

     显式启动的事务一般称为显式事务。本章介绍的主要是显式事务。

 

 

2. 自动提交

Ø 自动提交是指用户每发出一条SQL语句,SQL Server会自动启动一个事务,语句执行完了之后SQL Server自动执行提交操做来提交该事务。也就是说,在自动提交方式下,每一条SQL语句就是一个事务,一般称为自动提交事务,这是SQL Server的默认模式。

Ø  CREATE TABLE语句是一个事务,所以不可能出现这样的状况:在执行该语句时,有的字段被建立而有的没有被建立。

 

 

3. 隐式启动

Ø 当将SIMPLICIT_TRANSACTIONS设置为ON时,表示将隐式事务模式设置为打开,设置语句以下:

SET IMPLICIT_TRANSACTIONS ON;

 

Ø 在隐式事务模式下,任何DML语句(DELETE、UPDATE、INSERT)都自动启动一个事务,直到遇到事务提交语句或事务回滚语句,该事务才结束。结束后,自动启动新的事务,而无需用BEGIN TRANSACTION描述事务的开始。隐式启动的事务一般称为隐性事务。在隐性事务模式生下,事务会造成连续的事务链。

Ø 若是已将IMPLICIT_TRANSACTIONS设置为ON,建议随时将之设置回OFF。另外,事务的结束是使用COMMIT或ROLLBACK语句来实现,这将在下一节介绍。

 

 

10.2.2 终止事务

Ø 有启动,就必有终止。

Ø 终止方法有两种,一种是使用COMMIT命令(提交命令),另外一种是使用ROLLBACK命令(回滚命令)。这两种方法有本质上的区别:当执行到COMMIT命令时,会将语句执行的结果保存到数据库中(提交事务),并终止事务;当执行到ROLLBACK命令时,数据库将返回到事务开始时的初始状态,并终止事务。若是ROLLBACK命令是采用ROLLBACK TRANSACTION savepoint_name时,则数据库将返回到savepoint_name标识的状态。

 

 

1. 提交事务——COMMIT TRANSACTION

Ø 执行COMMIT TRANSACTION语句时,将终止隐式启动或显式启动的事务。

ü 若是@@TRANCOUNT为1,COMMIT TRANSACTION使得自从事务开始以来所执行的全部数据修改为为数据库的永久部分,释放事务所占用的资源,并将@@TRANCOUNT减小到0。

ü 若是@@TRANCOUNT大于1,则COMMIT TRANSACTION使@@TRANCOUNT按1递减而且事务将保持活动状态。

Ø COMMIT TRANSACTION语句的语法以下:

COMMIT { TRAN | TRANSACTION } [ transaction_name | @tran_name_variable ] ]

[ ; ]

 

 

      其中,transaction_name | @tran_name_variable用于设置要结束的事务的名称(该名称是由BEGIN TRANSACTION语句指定),但SQL Server会忽略此参数,设置它的目的是给程序员看的,向程序员指明COMMIT TRANSACTION与哪些BEGIN TRANSACTION相关联,以提升代码的可读性。

 

 

【例10.1】建立关于银行转账的事务。

Ø 假设用UserTable表保存银行客户信息,该表的定义代码以下:

CREATE TABLE UserTable

(

    UserId           varchar(18)       PRIMARY KEY,            --身份证号

    username          varchar(20)    NOT NULL,                       --用户名

    account              varchar(20)     NOT NULL UNIQUE,                         --账号

    balance          float           DEFAULT  0,          --余额

    address          varchar(100)                     --地址

);

Ø 用下面两条语句分别添加两条用户记录:

INSERT INTO UserTable VALUES('430302x1','王伟志','020000y1',10000,'中关村南路');

INSERT INTO UserTable VALUES('430302x2','张宇','020000y2',100,'火器营桥');

u 如今将帐户020000y1上的2000元转到帐户430302x2上。为了使得不出现前面所述的状况(转出账号上已经被扣钱,但转入账号上的余额并无增长),咱们把转账操做涉及的关键语句放到一个事务中,这样就能够避免出现上述错误状况。下面代码是对转账操做的一个简化模拟:

BEGIN TRANSACTION virement            -- 显式启动事务
DECLARE @balance float,@x float;
-- ①设置转账金额
SET @x = 200;
-- ②若是转出账号上的金额小于x,则取消转账操做
SELECT @balance = balance FROM  UserTable WHERE account = '020000y1';
IF(@balance < @x) return;
-- 不然执行下列操做
-- ③从转出账号上扣除金额x
UPDATE UserTable SET balance = balance - @x WHERE account = '020000y1';
-- ④在转入账号上加上金额x
UPDATE UserTable SET balance = balance + @x WHERE account = '020000y2';
-- ⑤转账操做结束
GO
COMMIT TRANSACTION virement;         -- 提交事务,事务终止

Ø  利用以上启动的事务,操做③和操做④要么都对数据库产生影响,要么对数据库都不产生影响,从而避免了“转出账号上已经被扣钱,但转入账号上的余额并无增长”的状况。实际上,只是须要将操做③和操做④对应的语句放在BEGIN TRANSACTION …COMMIT TRANSACTION便可。

Ø  有时候DML语句执行失败并不必定是由硬件故障等外部因素形成的,也有多是由内部运行错误(如违反约束等)形成的,从而致使相应的DML语句执行失败。

Ø  若是在一个事务中,既有成功执行的DML语句,也有因内部错误而致使失败执行的DML语句,那么该事务会自动回滚吗?

    通常来讲,执行SQL语句产生运行时错误时,SQL Server只回滚产生错误的SQL语句,而不会回滚整个事务。若是但愿当遇到某一个SQL 语句产生运行时错误时,事务可以自动回滚整个事务,则SET XACT_ABORT选项设置为ON(默认值为OFF):SET XACT_ABORT ON

ü 即当SET XACT_ABORT为ON时,若是执行SQL语句产生运行时错误,则整个事务将终止并回滚;

ü 当SET XACT_ABORT为OFF时,有时只回滚产生错误的SQL语句,而事务将继续进行处理。

ü 若是错误很严重,那么即便SET XACT_ABORT为OFF,也可能回滚整个事务。OFF 是默认设置。

Ø  注意,编译错误(如语法错误)不受SET XACT_ABORT的影响。

 

 

【例10.2】回滚包含运行时错误的事务。

Ø 先观察下列代码:

USE MyDatabase;

GO

CREATE TABLE TestTransTable1(c1 char(3) NOT NULL, c2 char(3));

GO

BEGIN TRAN 

   INSERT INTO TestTransTable1 VALUES('aa1','aa2');       

   INSERT INTO TestTransTable1 VALUES(NULL,'bb2');   -- 违反非空约束

   INSERT INTO TestTransTable1 VALUES('cc1','cc2');

COMMIT TRAN; 

 

 

Ø 上述代码的做用是:

  (1)先建立表TestTransTable1,其中字段c1有非空约束;

  (2)建立了一个事务,其中包含三条INSERT语句,用于向表TestTransTable1插入数据。

 

Ø 第二条INSER语句违反了非空约束。根据事务的概念,因而许多读者可能会获得这样的结论:因为第二条INSERT语句违反非空约束,所以该语句执行失败,从而致使整个事务被回滚,使得全部的INSERT语句都不被执行,数据库回到事务开始时的状态——表TestTransTable1仍然为空。

 

 

Ø  但实际状况并非这样。咱们使用SELECT语句查看表TestTransTable1:

SELECT * FROM TestTransTable1;

Ø  结果如图10.2所示。

 

 

 

 

 

 

 

Ø  图10.2代表,只有第二条记录没有被插入,第一和第三条都被成功插入了,可见事务并无产生回滚。但若是将XACT_ABORT设置为ON,当出现违反非空约束而致使语句执行失败时,整个事务将被回滚。

 

 

 【例子】执行下列代码:

USE MyDatabase;

GO

SET XACT_ABORT ON;     -- 将XACT_ABORT设置为ON  xact_abort

GO

DROP TABLE TestTransTable1;

GO

CREATE TABLE TestTransTable1(c1 char(3) NOT NULL, c2 char(3));

GO

BEGIN TRAN 

   INSERT INTO TestTransTable1 VALUES('aa1','aa2');       

   INSERT INTO TestTransTable1 VALUES(NULL,'bb2');   -- 违反非空约束

   INSERT INTO TestTransTable1 VALUES('cc1','cc2');

COMMIT TRAN;

SET XACT_ABORT OFF;     -- 将XACT_ABORT改回默认设置OFF

GO

 

Ø而后用SELECT语句查询表TestTransTable1,结果发现,表TestTransTable1中并无数据。这说明,上述事务已经被回滚。

Ø相似地,例10.1也有一样的问题。好比,若是用CHECK将字段balance设置在必定的范围内,那么余额超出这个范围时会违反这个CHECK约束。但定义的事务virement在出现违反约束状况下却没法保证数据的一致性。显然,经过将XACT_ABORT设置为ON,这个问题就能够获得解决。

 

 

 

2. 回滚事务——ROLLBACK TRANSACTION

        回滚事务是利用ROLLBACK TRANSACTION语句来实现,它能够将显式事务或隐性事务回滚到事务的起点或事务内的某个保存点(savepoint)。该语句的语法以下: 

ROLLBACK { TRAN | TRANSACTION }

     [ transaction_name | @tran_name_variable

     | savepoint_name | @savepoint_variable ]

[ ; ] 

 

Ø  transaction_name | @tran_name_variable 

      该参数用于指定由BEGIN TRANSACTION语句分配的事务的名称。嵌套事务时,transaction_name 必须是最外面的BEGIN TRANSACTION语句中的名称。

 

 

Ø savepoint_name | @savepoint_variable

        该参数为SAVE TRANSACTION语句中指定的保存点。指定了该参数,则回滚时数据库将恢复到该保存点时的状态(而不是事务开始时的状态)。不带savepoint_name和transaction_name的ROLLBACK TRANSACTION语句将使事务回滚到起点。

Ø  根据在ROLLBACK TRANSACTION语句中是否使用保存点,能够将回滚分为所有回滚和部分回滚。

    (1)所有回滚

 

【例10.3】所有回滚事务。

       下面代码先定义表TestTransTable2,而后在事务myTrans1中执行三条插入语句,事务结束时用ROLLBACK TRANSACTION语句所有回滚事务,以后又执行两条插入语句,以观察所有回滚事务的效果。代码以下:

USE MyDatabase;

GO

CREATE TABLE TestTransTable2(c1 char(3), c2 char(3));

GO

DECLARE @TransactionName varchar(20) = 'myTrans1';

BEGIN TRAN @TransactionName

    INSERT INTO TestTransTable2 VALUES('aa1','aa2’);

    INSERT INTO TestTransTable2 VALUES('bb1','bb2’);

    INSERT INTO TestTransTable2 VALUES('cc1','cc2');

ROLLBACK TRAN @TransactionName  -- 回滚事务

INSERT INTO TestTransTable2 VALUES('dd1','dd2');

INSERT INTO TestTransTable2 VALUES('ee1','ee2');

SELECT * FROM TestTransTable2

 

 

     执行上述代码,结果如图10.3所示

 

Ø  以上能够看到,事务myTrans1中包含的三条插入语句并无实现将相应的三条数据记录插入到表TestTransTable2中,

     缘由:在于ROLLBACK TRAN语句对整个事务进行所有回滚,使得数据库回到执行这三条插入语句以前的状态。事务myTrans1以后又执行了两条插入语句,这时是处于事务自动提交模式(每一条SQL语句就是一个事务,而且这种事务结束后会自动提交,而没有回滚)下,所以这两条插入语句成功地将两条数据记录插入到数据库中。

Ø  根据ROLLBACK的语法,在本例中,BEGIN TRAN及其ROLLBACK TRAN后面的@TransactionName能够省略,其效果是同样的。

 

 

 

(2)部分回滚

Ø 若是在事务中设置了保存点(即ROLLBACK TRANSACTION语句带参数savepoint_name | @savepoint_variable)时,ROLLBACK TRANSACTION语句将回滚到由savepoint_name或@savepoint_variable指定的保存点上。

Ø 在事务内设置保存点是使用SAVE TRANSACTION语句来实现,其语法以下:

SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable }

[ ; ]

 

 

savepoint_name | @savepoint_variable是保存点的名称,必须指定。

 

 

【例10.4】部分回滚事务。

     在例10.3所定义的事务中利用SAVE TRANSACTION语句增长一个保存点save1,同时修改ROLLBACK语句,其余代码相同。全部代码以下:

USE MyDatabase;

GO

DROP TABLE TestTransTable2;

CREATE TABLE TestTransTable2(c1 char(3), c2 char(3));

GO

DECLARE @TransactionName varchar(20) = 'myTrans1';

BEGIN TRAN @TransactionName

INSERT INTO TestTransTable2 VALUES('aa1','aa2');

    INSERT INTO TestTransTable2 VALUES('bb1','bb2');

    SAVE TRANSACTION save1;          -- 设置保存点

    INSERT INTO TestTransTable2 VALUES('cc1','cc2');         

ROLLBACK TRAN save1;   

INSERT INTO TestTransTable2 VALUES('dd1','dd2');

INSERT INTO TestTransTable2 VALUES('ee1','ee2');

SELECT * FROM TestTransTable2

 

 

 

 

      执行结果如图10.4所示。

 

 

 

    

    此结果代表,只有第三条插入语句的执行结果被撤销了。其缘由在于,事务myTrans1结束时ROLLBACK TRAN语句回滚保存点save1处,即回滚到第三条插入语句执行以前,故第三条插入语句的执行结果被撤销,其余插入语句的执行结果是有效的。

 

10.2.3 嵌套事务

Ø 事务是容许嵌套的,即一个事务内能够包含另一个事务。当事务嵌套时,就存在多个事务同时处于活动状态。

 

Ø 系统全局变量@@TRANCOUNT可返回当前链接的活动事务的个数。对@@TRANCOUNT返回值有影响的是BEGIN TRANSACTION、ROLLBACK TRANSACTION和COMMIT语句。具体影响方式以下:

ü 每执行一次BEGIN TRANSACTION命令就会使@@TRANCOUNT的值增长1;

ü 每执行一次COMMIT命令时,@@TRANCOUNT的值就减1;

ü 一旦执行到ROLLBACK TRANSACTION命令(所有回滚)时,@@TRANCOUNT的值将变为0;

ü 但ROLLBACK TRANSACTION savepoint_name(部分回滚)不影响@@TRANCOUNT的值。

 

 

【例10.5】嵌套事务。

       本例中,先建立表TestTransTable3,而后在有三个嵌套层的嵌套事务中向该表插入数据,并在每次启动或提交一个事务时都打印@@TRANCOUNT的值。代码以下:

USE MyDatabase;

GO

CREATE TABLE TestTransTable3(c1 char(3), c2 char(3));

GO

if(@@TRANCOUNT!=0) ROLLBACK TRAN;  -- 先终止全部事务

BEGIN TRAN Trans1

      PRINT '启动事务Trans1后@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10));

       INSERT INTO TestTransTable3 VALUES('aa1','aa2’);

       BEGIN TRAN Trans2

    PRINT '启动事务Trans2后@@TRANCOUNT的值'+CAST(@@TRANCOUNT AS VARCHAR(10));

   INSERT INTO TestTransTable3 VALUES('bb1','bb2');

      BEGIN TRAN Trans3

            PRINT '启动事务Trans3后@@TRANCOUNT的值'+CAST(@@TRANCOUNT AS VARCHAR(10));

            INSERT INTO TestTransTable3 VALUES('cc1','cc2');        

            SAVE TRANSACTION save1;         -- 设置保存点

            PRINT '设置保存点save1后@@TRANCOUNT的值'+CAST(@@TRANCOUNT AS VARCHAR(10));

            INSERT INTO TestTransTable3 VALUES('dd1','dd2');         

            ROLLBACK TRAN save1;

            PRINT '回滚到保存点save1后@@TRANCOUNT的值'+CAST(@@TRANCOUNT AS VARCHAR(10));  

           INSERT INTO TestTransTable3 VALUES('ee1','ee2');

       COMMIT TRAN Trans3

       PRINT '提交Trans3后@@TRANCOUNT的值'+CAST(@@TRANCOUNT AS VARCHAR(10));

       INSERT INTO TestTransTable3 VALUES('ff1','ff2’);

              COMMIT TRAN Trans2

              PRINT '提交Trans2后@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10));

     COMMIT TRAN Trans1

     PRINT '提交Trans1后@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10));

 

 

 

Ø  执行上述代码,结果如图13.5所示。

 

 

 

 

Ø  从图10.5中也能够能够看出,每执行一次BEGIN TRANSACTION命令就会使@@TRANCOUNT的值增长1,每执行一次COMMIT命令时,@@TRANCOUNT的值就减1,但ROLLBACK TRANSACTION savepoint_name不影响@@TRANCOUNT的值。

 

Ø  若是遇到ROLLBACK TRANSACTION命令,无论该命令以后是否还有其余的COMMIT命令,系统中全部的事务都被终止(不提交),@@TRANCOUNT的值为0。

Ø  执行上述嵌套事务后,表TestTransTable3中的数据如图10.6所示。

 

 

 

Ø  若是将上述代码中的语句COMMIT TRAN Trans1(倒数第二条)改成ROLLBACK TRAN(不带参数),则表TestTransTable3中将没有任何数据。这说明,对于嵌套事务,无论内层是否使用COMMIT命令来提交事务,只要外层事务中使用ROLLBACK TRAN来回滚,那么整个嵌套事务都被回滚,数据库将回到嵌套事务开始时的状态。

 

•    10.3 并发控制

 

10.3.1 并发控制的概念

Ø 数据共享是数据库的基本功能之一。一个数据库可能同时拥有多个用户,这意味着在同一时刻系统中可能同时运行上百上千个事务。而每一个事务又是由若干个数据库操做构成的操做序列,如何有效地控制这些操做的执行对提升系统的安全性和运行效率有着十分重要的意义。

Ø 在单CPU系统中,事务的运行有两种方式,一种是串行执行,一种是并发执行。串行执行是指每一个时刻系统中只有一个事务在运行,其余事务必须等到该事务中全部的操做执行完了之后才能运行。这种执行方式的优势是方便控制,但其缺点倒是十分突出,那就是整个系统的运行效率很低。由于在串行方式中,不一样的操做须要不一样的资源,但一个操做通常不会使用全部的资源且使用时间长短不一,因此串行执行的事务会使许多系统资源处于空闲状态。

Ø  若是可以充分利用这些空闲的资源,无疑能够有效提升系统的运行效率,这是考虑事务并发控制的主要缘由之一。另外,并发控制能够更好保证数据的一致性,从而实现数据的安全性。

Ø  在并发执行方式中,系统容许同一个时刻有多个事务在并行执行。这种并行执行其实是经过事务操做的轮流交叉执行来实现的。虽然在同一时刻只有某一个事务的某一个操做在占用CPU资源,但其余事务中的操做可使用该操做没有占用的有关资源,这样能够在整体上提升系统的运行效率。

Ø  对于并发运行的事务,若是没有有效地控制其操做,就可能致使对资源的不合理使用,对数据库而言就可能致使数据的不一致性和不完整性等问题。所以,DBMS必须提供一种容许多个用户同时对数据进行存取访问的并发控制机制,以确保数据库的一致性和完整性。

Ø  简而言之,并发控制就是针对并发执行的事务,如何有效地控制和调度其交叉执行的数据库操做,使各事务的执行不相互干扰,以免出现数据库的不一致性和不完整性等问题。

 

 

10.3.2 几种并发问题

        当多个用户同时访问数据库时,若是没有必要的访问控制措施,可能会引起数据不一致等并发问题,这是诱发并发控制的主要缘由。为进行有效的并发控制,首先要明确并发问题的类型,分析不一致问题产生的根源。

1. 丢失修改(Lost Update)

       下面看一个经典的关于民航订票系统的例子。它能够说明多个事务对数据库的并发操做带来的不一致性问题。

例子】假设某个民航订票系统有两个售票点,分别为售票点A和售票点B。假设系统把一次订票业务定义为一个事务,其包含的数据库操做序列以下:

T:Begin Transaction

     读取机票余数x;

     售出机票y张,机票余数x ← x – y;

     把x写回数据库,修改数据库中机票的余数;

Commit;

 

 

Ø  假设当前机票余数为10张,售票点A和售票点B同时进行一次订票业务,分别有用户订4张和3张机票。因而在系统中同时造成两个事务,分别记为TA和TB。若是事务TA和TB中的操做交叉执行,执行过程如图10.7所示。

 

 

 

 

 

 

 

 

 

 

 

 

 

       事务TA和TB执行完了之后,因为B_op3是最后的操做,因此数据库中机票的余数6。而实际状况是,售票点A售出4张,售票点B售出3张,因此实际剩下10-(4+3) = 3张机票。这就形成了数据库反映的信息与实际状况不符,从而产生了数据的不一致性。这种不一致性是由操做B_op3的(对数据库的)修改结果将操做A_op3的修改结果覆盖掉而产生的,即A_op3的修改结果“丢了”,因此称为丢失修改。

 

2. 读“脏”数据(Dirty Read)

Ø 事务TC对某一数据处理了之后将结果写回到数据区,而后事务TD从数据区中读取该数据。但事务TC出于某种缘由进行回滚操做,撤消已作出的操做,这时TD刚读取的数据又被恢复到原值(事务TC开始执行时的值),这样TD读到的数据就与数据库中的实际数据不一致了,而TD读取的数据就是所谓的“脏”数据(不正确的数据)。“脏”数据是指那些被某事务更改、但尚未被提交的数据。

 

 

   【例子】  在订票系统中,事务TC在读出机票余数10并售出4张票后,将机票余数10-4=6写到数据区(还没来得及提交),恰在此时事务TD读取机票余数6,而TC出于某种缘由(如断电等)进行回滚操做,机票余数恢复到了原来的值10并撤销这次售票操做,但这时事务TD仍然使用着读到的机票余数6,这与数据库中实际的机票余数不一致,这个“机票余数6”就是所谓的“脏”数据,如图10.8所示。

 

 

 

 

 

3. 不可重复读(Non-Repeatable Read)

Ø  事务TE按照必定条件读取数据库中某数据x,随后事务TF又修改了数据x,这样当事务TE操做完了之后又按照相同条件读取数据x,但这时因为数据x已经被修改,因此此次读取值与上一次不一致,从而在进行一样的操做后却获得不同的结果。因为另外一个事务对数据的修改而致使当前事务两次读到的数据不一致,这种状况就是不可重复读。这与读“脏”数据有类似之处。

   【例子】  在图10.9中c表明机票的价格,n表明机票的张数。机票查询事务TE读取机票价格c = 800和机票张数n = 7,接着计算这7张票的总价钱5600(可能有人想查询7张机票总共须要多少钱);刚好在计算总价钱完后,管理事务TF(相关航空公司执行)读取c = 800并进行六五折降价处理后将c = 520写回数据库;这时机票查询事务TE重读c(可能为验证总价钱的正确性),结果获得c=520,这与第一次读取值不一致。显然,这种不一致性会致使系统给出错误的信息,这是不容许的。

 

 

 

 

4. 幻影读(Phantom Row)

Ø 假设事务TG按照必定条件两次读取表中的某些数据记录,在第一次读取数据记录后事务TH在该表中删除(或添加)某些记录。这样在事务TG第二次按照一样条件读取数据记录时会发现有些记录“幻影”般地消失(或增多)了,这称为幻影(Phantom Row)读。

Ø 致使以上四种不一致性产生的缘由是并发操做的随机调度,这使事务的隔离性遭到破坏。为此,须要采起相应措施,对全部数据库操做的执行次序进行合理而有效的安排,使得各个事务都可以独立地运行、彼此不相互干扰,保证事务的ACID特性,避免出现数据不一致性等并发问题。

 

 

10.3.3 基于事务隔离级别的并发控制

Ø 保证事务的隔离性能够有效防止数据不一致等并发问题。事务的隔离性有程度之别,这就是事务隔离级别。在SQL Server中,事务的隔离级别用于表征一个事务与其余事务进行隔离的程度。隔离级别越高,就能够更好地保证数据的正确性,但并发程度和效率就越低;相反,隔离级别越低,出现数据不一致性的可能性就越大,但其并发程度和效率就越高。经过设定不一样事务隔离级别能够实现不一样层次的访问控制需求。

 

 

Ø 在SQL Server中,事务隔离级别分为四种:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE,它们对数据访问的限制程度依次从低到高。设置隔离级别是经过SET TRANSACTION ISOLATION LEVEL语句来实现,其语法以下:

ET TRANSACTION ISOLATION LEVEL

    { READ UNCOMMITTED

    | READ COMMITTED

    | REPEATABLE READ

    | SERIALIZABLE

    }

[ ; ]

 

 

 

 

 

1. 使用READ UNCOMMITTED

Ø 该隔离级别容许读取已经被其余事务修改过但还没有提交的数据,实际上该隔离级别根本就没有提供事务间的隔离。这种隔离级别是四种隔离级别中限制最少的一种,级别最低。

Ø 其做用可简记为:容许读取未提交数据。

  【例10.6】使用READ UNCOMMITTED隔离级别,容许丢失修改。

    当事务的隔离级别设置为READ UNCOMMITTED时,SQL Server容许用户读取未提交的数据,所以会形成丢失修改。为观察这种效果,按序完成下列步骤:

(1)建立表TestTransTable4并插入两条数据:

CREATE TABLE TestTransTable4(flight char(4), price float, number int);

INSERT INTO TestTransTable4 VALUES('A111',800,10);

INSERT INTO TestTransTable4 VALUES('A222',1200,20);

 

     其中,flight、price、number分别表明航班号、机票价格、剩余票数。

 

 

(2)编写事务TA和TB的代码:

-- 事务TA的代码

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 设置事务隔离级别

BEGIN TRAN TA

      DECLARE @n int;

      SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’;

      WAITFOR DELAY '00:00:10'              -- 等待事务TB读数据

      SET @n = @n - 4;

      UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111';

COMMIT TRAN TA

 

-- 事务TB的代码

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

BEGIN TRAN TB

     DECLARE @n int;

     SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’;

     WAITFOR DELAY '00:00:15'         -- 等待,以让事务TA先提交数据

     SET @n = @n - 3;

    UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111';

COMMIT TRAN  TB

 

 

 

(3)打开两个查询窗口,分别在两个窗口中前后执行事务TA和TB(执行TA后应该在10秒之内执行TB,不然看不到预设的结果),分别如图10.10和图10.11所示。

 

 

 

 

 

 

(4)查询表中的数据:

          SELECT * FROM TestTransTable4;

          结果如图10.12所示。

 

 

 

 

        由代码可知,事务TA和TB分别售出了4张和3张票,所以应该剩下10-(4+3) = 3张票。但由图10.12能够看到,系统还剩下7张票。这就是丢失修改的结果。当隔离级别为READ UNCOMMITTED时,事务不能防止丢失修改。

        实际上,对于前面介绍的四种数据不一致状况,READ UNCOMMITTED隔离级别都不能防止它们。这是READ UNCOMMITTED隔离级别的缺点。其优势是可避免并发控制所需增长的系统开销,通常用于单用户系统(不适用于并发场合)或者系统中两个事务同时访问同一资源的可能性为零或几乎为零。

 

 

2. 使用READ COMMITTED

Ø 在使用该隔离级别时,当一个事务已经对一个数据块进行了修改(UPDATE)但还没有提交或回滚时,其余事务不容许读取该数据块,即该隔离级别不容许读取未提交的数据。它的隔离级别比READ UNCOMMITTED高一层,能够防止读“脏”,但不能防止丢失修改,也不能防止不可重复读和“幻影”读。 

Ø 其做用可简记为:不容许读取已修改但未提交数据。

Ø READ COMMITTED是SQL Server默认的事务隔离级别。

  【例10.7】使用READ COMMITTED隔离级别,防止读“脏”数据。

     先恢复表TestTransTable4中的数据:

DELETE FROM TestTransTable4;

INSERT INTO TestTransTable4 VALUES('A111',800,10);

INSERT INTO TestTransTable4 VALUES('A222',1200,20);

 

 

Ø  为观察读“脏”数据,先将事务的隔离级别设置为READ UNCOMMITTED,分别在两个查询窗口中前后执行事务TC和TD:

-- 事务TC的代码
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 设置事务隔离级别
BEGIN TRAN TC
    DECLARE @n int;
    SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’;
    SET @n = @n - 4;
    UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111’;
    WAITFOR DELAY '00:00:10'             -- 等待事务TD读“脏”数据
ROLLBACK TRAN TC                 -- 回滚事务

 

 

 

 

-- 事务TD的代码

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

BEGIN TRAN TD

     DECLARE @n int;

     SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111'; -- 读“脏”数据

     PRINT '剩余机票数:'+CONVERT(varchar(10),@n);

COMMIT TRAN    TD

 

Ø  结果事务TD输出以下的结果:     剩余机票数:6

Ø  在等待事务TC执行完了之后,利用SELECT语句查询表TestTransTable4,结果发现剩余机票数为10。6就是事务TD读到的“脏”数据。

Ø  为了不读到这个“脏”数据,只需将上述的隔离级别由READ UNCOMMITTED改成READ COMMITTED便可(其余代码不变)。但在将隔离级别更改了之后,咱们发现事务TD要等事务TC回滚了之后(ROLLBACK)才执行读操做。READ COMMITTED虽然能够比READ UNCOMMITTED具备更好解决并发问题的能力,可是其效率较后者低。

 

 

3. 使用REPEATABLE READ

Ø 在该隔离级别下,若是一个数据块已经被一个事务读取但还没有做提交操做,则任何其余事务都不能修改(UPDATE)该数据块(但能够执行INSERT和DELETE),直到该事务提交或回滚后才能修改。该隔离级别的层次又在READ COMMITTED之上,即比READ COMMITTED有更多的限制,

Ø 它能够防止读“脏”数据和不可重复读。但因为一个事务读取数据块后另外一个事务能够执行INSERT和DELETE操做,因此它不能防止“幻影”读。另外,该隔离级别容易形成死锁。例如,将它用于解决例10.6中的丢失修改问题时,就形成死锁。

Ø 其做用可简记为:不容许读取未提交数据,不容许修改已读数据。

 

 

【例10.8】使用REPEATABLE READ隔离级别,防止不可重复读。

Ø 先看看存在不可重复读的事务TE: 

-- 事务TE的代码

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;  -- 设置事务隔离级别

BEGIN TRAN TE

       DECLARE @n int, @c int;

       -- 顾客先查询张机票的价格

      SELECT @c = price FROM TestTransTable4 WHERE flight = 'A111';  -- 第一次读

      SET @n = 7;

      PRINT CONVERT(varchar(10),@n)+'张机票的价格:'+CONVERT(varchar(10),@n*@c)+'元’;

      WAITFOR DELAY '00:00:10'   -- 为观察效果,让该事务等待10秒

      -- 接着购买张机票

      SELECT @c = price FROM TestTransTable4 WHERE flight = 'A111';  -- 第二次读

      SET @n = 7;

        PRINT '总共'+CONVERT(varchar(10),@n)+'张机票,应付款:'+CONVERT(varchar(10),@n*@c)+'';

COMMIT TRAN TE -- 提交事务

 

Ø  另外一事务TF的代码以下:

-- 事务TF的代码

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;  -- 设置事务隔离级别

BEGIN TRAN TF

      UPDATE TestTransTable4 SET price = price*0.65 WHERE flight = 'A111'; -- 折价65折

COMMIT TRAN TF

 

 

Ø  分别在两个查询窗口中前后运行事务TE和事务TF(时间间隔要小于10秒),事务TE输出的结果如图10.13所示。

 

 

 

 

 

 

Ø  该结果说了事务TE出现了不可重复读:在相同条件下,利用两次读取的信息来计算的机票价格却不同。缘由在于,当事务TE处于10秒等待期时,事务TF对机票价格(price)进行六五折处理,结果致使了在同一事务中的两次读取操做得到不一样的结果。

Ø  若是将事务隔离级别由原来的READ COMMITTED改成REPEATABLE READ(其余代码不变),则就能够防止上述的不可重复读,如图10.14所示。这是由于REPEATABLE READ隔离级别不容许对事务TE已经读取的数据(价格)进行任何的更新操做,这样事务TF只能等待事务TE结束后才能对价格进行五六折处理,从而避免不可重复读问题。显然,因为出现事务TF等待事务TE的状况,所以使用REPEATABLE READ隔离级别时要比使用READ COMMITTED的效率低。

 

4. 使用SERIALIZABLE

Ø SERIALIZABLE是SQL Server最高的隔离级别。在该隔离级别下,一个数据块一旦被一个事务读取或修改,则不容许别的事务对这些数据进行更新操做(包括UPDATE, INSERT, DELETE),直到该事务提交或回滚。也就是说,一旦一个数据块被一个事务锁定,则其余事务若是须要修改此数据块,它们只能排队等待。SERIALIZABLE隔离级别的这些性质决定了它可以解决“幻影”读问题。

Ø 其做用可简记为:事务必须串行执行。

 

 

【例10.9】使用SERIALIZABLE隔离级别,防止“幻影”读。

Ø 先看看存在“幻影”读的事务TG:

-- 事务TG的代码

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ         -- 设置事务隔离级别 

BEGIN TRAN TG

     SELECT * FROM TestTransTable4 WHERE price <= 1200;    -- 第一次读

     WAITFOR DELAY '00:00:10'                      -- 事务等待10秒

     SELECT * FROM TestTransTable4 WHERE price <= 1200;   -- 第二次读

COMMIT TRAN TG -- 提交事务

 

Ø 构造另外一事务TH:

-- 事务TH的代码

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 设置事务隔离级别

BEGIN TRAN TH

     INSERT INTO TestTransTable4 VALUES('A333',1000,20);

COMMIT TRAN TH

 

 

 

Ø  分别在两个查询窗口中前后运行事务TG和事务TH(时间间隔要小于10秒,且先恢复表TestTransTable4中的数据),事务TG中的两条SELECT语句输出的结果分别如图10.15和图10.16所示:

 

 

 

 

 

 

 

Ø  在事务TG中彻底相同的两个查询语句在两次执行后获得的结果不同,其中在第二次查询结果中“幻影”般地增长了一个票价为1000元的航班信息。可见,REPEATABLE READ隔离级别虽然比前两者均高,但仍是不能防止“幻影”读。

 

Ø  若是将事务隔离级别由原来的REPEATABLE READ改成SERIALIZABLE(其余代码不变),按照上述一样方法执行这两个事务后,事务TG中的两次查询获得的结果均如图10.15所示。这代表“幻影”读已经不复存在了,隔离级别SERIALIZABLE能够防止上述的“幻影”读。若是这时进一步查询表TestTransTable4中的数据,能够看到其结果与图10.16所示的结果同样。这是由于,在SERIALIZABLE隔离级别下,事务TG执行完了之后再执行事务TH,即串行事务TG和TH,所以事务TH中的语句不会影响到事务TG,从而避免“幻影”读。

Ø  须要说明的是,REPEATABLE READ和SERIALIZABLE隔离级别对系统性能的影响都很大,特别是SERIALIZABLE隔离级别,不是非不得以,最好不要使用。

Ø   根据以上分析,四种隔离级对事务“读”和“写”操做的处理关系说明如表10.1所示。

 

 

 

 

表10.1中,“读”、“写”、“插”和“删”分别指SELECT、UPDATE、INSERT和DELETE操做。
“读了,可再读”表述的意思是,执行了SELECT后,在事务尚未提交或回滚以前,还能够继续执行SELECT;
“读了,不可再写”是指,执行了SELECT后,在事务尚未提交或回滚以前,是不容许执行UPDATE操做的。其余项的意思能够照此类推。

 

Ø  根据表10.1,咱们可进一步总结四种隔离级别对支持解决并发问题的状况,结果如表10.2所示。

 

 

                 注:√表示“防止”,×表示“不必定防止”      

严格说,REPEATABLE READ和SERIALIZABLE是不支持解决丢失修改问题的,由于它们用于此类问题时,容易形成死锁。

  【例子】对于例10.6中的事务TA和TB,若是将其中的UNCOMMITTED替换成REPEATABLE READ或SERIALIZABLE,而后按照例10.6中的方法执行这两个事务,结果虽然没有形成数据的不一致,但出现了死锁(死锁最后是由SQL Server自动终止一个事务来解除)。隔离级别的方法并不能彻底解决涉及的并发问题。

 

 

 

10.3.4 基于锁的并发控制

Ø 锁定是指对数据块的锁定,是SQL Server数据库引擎用来同步多个用户同时对同一个数据块进行访问的一种控制机制。这种机制的实现是利用锁(LOCK)来完成的。一个用户(事务)能够申请对一个资源加锁,若是申请成功的话,则在该事务访问此资源的时候其余用户对此资源的访问受到诸多的限制,以保证数据的完整性和一致性。

Ø SQL Server提供了多种不一样类型的锁。有的锁类型是兼容的,有的是不兼容的。不一样类型的锁决定了事务对数据块的访问模式。SQL Serve经常使用的锁类型主要包括:

(1)共享锁(S):容许多个事务并发读取同一数据块,但不容许其余事务修改当前事务加锁的数据块。一个事务对一个数据块加上一个共享锁后,其余事务也能够继续对该数据块加上共享锁。这就是说,当一个数据块被多个事务同时加上共享锁的时候,全部的事务都不能对这个数据块进行修改,直到数据读取完成,共享锁释放。

 

 

(2)排它锁(X):也称独占锁、写锁,当一个事务对一个数据块加上排它锁后,它能够对该数据块进行UPDATE、DELETE、INSERT等操做,而其余事务不能对该数据块加上任何锁,于是也不能执行任何的更新操做(包括UPDATE、DELETE和INSERT)。通常用于对数据块进行更新操做时的并发控制,它能够保证同一数据块不会被多个事务同时进行更新操做,避免由此引起的数据不一致。

(3)更新锁:更新锁介于共享锁和排它锁之间,主要用于数据更新,能够较好地防止死锁。一个数据块的更新锁一次只能分配给一个事务,在读数据的时候该更新锁是共享锁,一旦更新数据时它就变成排他锁,更新完后又变为共享锁。但在变换过程当中,可能出现锁等待等问题,且变换自己也须要时间,所以使用这种锁时,效率并不十分理想。

 

 

(4)意向锁:表示SQL Server须要在层次结构中的某些底层资源上(如行,列)获取共享锁、排它锁或更新锁。

     【例子】表级放置了意向共享锁,就表示事务要对表的页或行上使用共享锁;在表的某一行上上放置意向锁,能够防止其它事务获取其它不兼容的锁。意向锁的优势是能够提升性能,由于数据引擎不须要检测资源的每一列每一行,就能判断是否能够获取到该资源的兼容锁。它包括三种类型:意向共享锁,意向排他锁,意向排他共享锁。

(5)架构锁:架构锁用于在修改表结构时,阻止其余事务对表的并发访问。

(6)键范围锁:用于锁定表中记录之间的范围的锁,以防止记录集中的“幻影”插入或删除,确保事务的串行执行。

(7)大容量更新锁:容许多个进程将大容量数据并发的复制到同一个表中,在复制加载的同时,不容许其它非复制进程访问该表。

 

 

 

     在这些锁当中,共享锁(S锁)和排他锁(X锁)尤其重要,它们之间的相容关系描述以下:

Ø 若是事务T对数据块D成功加上共享锁,则其余事务只能对D再加共享锁,不能加排他锁,且此时事务T只能读数据块D,不能修改它(除非其余事务没有对该数据块加共享锁)。

Ø 若是事务T对数据块D成功加上排他锁,则其余事务不能再对D加上任何类型的锁,也对D进行读操做和写操做,而此时事务T既能读数据块D,也又能修改该数据块。

 

Ø  下面主要是结合SQL Server提供的表提示(table_hint),介绍共享锁和排他锁在并发控制中的使用方法。加锁状况的动态信息能够经过查询系统表sys.dm_tran_locks得到。

Ø  经过在SELECT、INSERT、UPDATE及DELETE语句中为单个表引用指定表提示,能够实现对数据块的加锁功能,实现事务对数据访问的并发控制。

Ø  为数据表指定表提示的简化语法以下:

{SELECT| INSERT| UPDATE| DELECT … | MERGE …} [ WITH ( <table_hint> ) ]
<table_hint> ::=
[ NOEXPAND ] {
    INDEX ( index_value [ ,...n ] ) | INDEX = ( index_value )
  | FASTFIRSTROW
  | FORCESEEK
  | HOLDLOCK
  | NOLOCK
  | NOWAIT
  | PAGLOCK
  | READCOMMITTED
  | READCOMMITTEDLOCK 
  | READPAST 
  | READUNCOMMITTED 
  | REPEATABLEREAD 
  | ROWLOCK 
  | SERIALIZABLE 
  | TABLOCK 
  | TABLOCKX 
  | UPDLOCK 
  | XLOCK 
} 

 

 

 

 

u 表提示语法中有不少选项,下面主要介绍与表级锁有密切相关的几个选项:

Ø HOLDLOCK 

   表示使用共享锁,使用共享锁更具备限制性,保持共享锁直到事务完成。而不是不管事务是否完成,都在再也不须要所需表或数据页时当即释放共享锁。HOLDLOCK不能被用于包含FOR BROWSE选项的SELECT语句。它同等于SERIALIZABLE隔离级别。

Ø NOLOCK

 表示不发布共享锁来阻止其余事务修改当前事务在读的数据,容许读“脏”数据。它同等于等同于READ UNCOMMITTED隔离级别。

Ø PAGLOCK

表示使用页锁,一般使用在行或键采用单个锁的地方,或者采用单个表锁的地方。

Ø READPAST

    指定数据库引擎跳过(不读取)由其余事务锁定的行。在大多数状况下,这一样适用于页。数据库引擎跳过这些行或页,而不是在释放锁以前阻塞当前事务。它仅适用于READ COMMITTED或REPEATABLE READ隔离级别的事务中。

 

 

Ø  ROWLOCK

     表示使用行锁,一般在采用页锁或表锁时使用。

Ø  TABLOCK

     指定对表采用共享锁并让其一直持有,直至语句结束。若是同时指定了HOLDLOCK,则会一直持有共享表锁,直至事务结束。

Ø  TABLOCKX

    指定对表采用排他锁(独占表级锁)。若是同时指定了HOLDLOCK,则会一直持有该锁,直至事务完成。在整个事务期间,其余事务不能访问该数据表。

Ø  UPDLOCK

   指定要使用更新锁(而不是共享锁),并保持到事务完成。

 

     注意:若是设置了事务隔离级别,同时指定了锁提示,则锁提示将覆盖会话的当前事务隔离级别。

 

 

【例10.10】使用表级共享锁。

Ø 对于数据表TestTransTable4,事务T1对其加上表级共享锁,使得在事务期内其余事务不能更新此数据表。事务T1的代码以下:

BEGIN TRAN T1

     DECLARE @s varchar(10);

     -- 下面一条语句的惟一做用是对表加共享锁

    SELECT @s = flight FROM TestTransTable4 WITH(HOLDLOCK,TABLOCK) WHERE 1=2;

    PRINT '加锁时间:'+CONVERT(varchar(30), GETDATE(), 20);

   WAITFOR DELAY '00:00:10'               -- 事务等待10秒

   PRINT '解锁时间:'+CONVERT(varchar(30), GETDATE(), 20);

COMMIT TRAN T1

Ø 为观察共享锁的效果,进一步定义事务T2:

BEGIN TRAN T2

     UPDATE TestTransTable4 SET price = price*0.65 WHERE flight = 'A111’;

    PRINT '数据更新时间:'+CONVERT(varchar(30), GETDATE(), 20); 

COMMIT TRAN T2

 

 

 

Ø  而后分别在两个查询窗口中前后运行事务T1和事务T2(时间间隔要小于10秒),事务T1和T2输出的结果分别如图10.17和图10.18所示。

 

 

 

 

 

 

 

 

 

Ø  对比图10.17和图10.18,事务T1对表TestTransTable4的更新操做(包括删除和添加)必须等到事务T2解除共享锁之后才能进行(但在事务T1期内,事务T2可使用SELECT语句查询表TestTransTable4)。

 

Ø  使用HOLDLOCK和TABLOCK能够避免在事务期内被锁定对象受到更新(包括删除和添加),于是能够避免“幻影”读;但因为T1在进行UPDATE操做后,T2可以继续SELECT数据,所以这种控制策略不能防止读“脏”数据;共享锁也不能防止丢失修改。

Ø  若是同时在T1和T2中添加读操做和写操做,则容易形成死锁。

      【例子】若是在例10.6的两个事务TA和TB中改用共享锁进行并发控制,一样会出现死锁的现象。但更新锁可以自动实如今共享锁和排他锁之间的切换,完成对数据的读取和更新,且在防止死锁方面有优点。若是在例10.6的两个事务TA和TB中改用更新锁,结果是能够对这两个事务成功进行并发控制的。

 

【例10.11】利用更新锁解决丢失修改问题。

       对于例10.6的两个事务TA和TB,用事务隔离级别的方法难以解决丢失修改问题,但用更新锁则能够较好地解决这个问题。更新锁是用UPDLOCK选项来定义,修改后事务TA和TB的代码以下:

-- 事务TA的代码

BEGIN TRAN TA 

DECLARE @n int; 

SELECT @n = number FROM TestTransTable4 WITH(UPDLOCK,TABLOCK) WHERE flight = 'A111';

WAITFOR DELAY '00:00:10'         -- 等待10秒,以让事务TB读数据

SET @n = @n - 4;

UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111';

COMMIT TRAN   TA

 

-- 事务TB的代码

BEGIN TRAN TB

DECLARE @n int;

SELECT @n = number FROM TestTransTable4 WITH(UPDLOCK,TABLOCK) WHERE flight = 'A111';

WAITFOR DELAY '00:00:15'           

SET @n = @n - 3;

UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111';

COMMIT TRAN  TB

 

 

 

 

  【例10.12】   利用排他锁来实现事务执行的串行化。

        下面代码是为表TestTransTable4加上表级排他锁(TABLOCKX),并将其做用范围设置为整个事务期:

BEGIN TRAN T3

     DECLARE @s varchar(10);

     -- 下面一条语句的惟一做用是对表加排他锁

     SELECT @s = flight FROM TestTransTable4 WITH(HOLDLOCK,TABLOCKX) WHERE 1=2;

     PRINT '加锁时间:'+CONVERT(varchar(30), GETDATE(), 20);

     WAITFOR DELAY '00:00:10'            -- 事务等待10秒   

     PRINT '解锁时间:'+CONVERT(varchar(30), GETDATE(), 20);

COMMIT TRAN T3

进一步定义事务T4:

BEGIN TRAN T4

      DECLARE @s varchar(10);

     SELECT @s = flight FROM TestTransTable4;

     PRINT '数据查询时间:'+CONVERT(varchar(30), GETDATE(), 20);

COMMIT TRAN T4

 

 

 

 

Ø  与例13.10相似,分别在两个查询窗口中前后运行事务T3和事务T4(时间间隔要小于10秒),事务T3和T4输出的结果分别如图10.19和图10.20所示。

 

 

Ø  事务T3经过利用TABLOCKX选项对表TestTransTable4加上排他锁之后,事务T4对该表的查询操做只能在事务T3结束以后才能进行,其余更新操做(如INSERT、UPDATE、DELETE)更是如此。所以,利用排他锁能够实现事务执行的串行化控制。

 

 

 

相关文章
相关标签/搜索