SQL之恋(9):事务的魅力(上)

软件世界

  如果说有一样东西能把数据库与文件系统区别开,那么它就是支持事务的能力。如果你正在往一个普通文件中写入数据,系统突然崩溃,则这个文件很可能遭到破坏。而如果你往一个数据库文件里写数据时遇到了这种情况,则可以利用事务使数据库恢复到它在写操作开始之前的状态。

  事务是一系列在它们的结果变成永久性之前必须全部成功结束的事件。事务是一个逻辑工作单元,由一条或多条SQL语句组成,这些语句被当作一个整体看待──要么全执行,要么全不执行。

  不成功,就失败

  广义上讲,任何一个需要数据库系统完成的业务逻辑,都可以称作事务。贷记信用卡还款业务就是典型的事务应用实例:在月末,银行从客户指定的储蓄账户中转出一定数额的钱款到信用卡账户,以冲抵上月信用卡的透支消费欠款。假设客户上月透支消费2000元,银行月末就应先从储蓄账户转出2000元,然后再往信用卡账户转入2000元。

  这个事务很简单,整个过程一瞬间就可完成。可是在这一瞬间也可能发生意外──储蓄账户的钱被扣走了2000元,而信用卡账户的欠款没有被冲消!这种结果显然是我们不愿意看到的。于是我们很容易达成下面的共识:宁愿无所作为,也不能半途而废。因为“什么都不做”和“做成功”时,数据库都处于准确、完整的状态;而“事情只做一半”将导致数据库处于不正确、不一致、不完整的状态。也就是说,事务执行的结果只有两种:全部成功和彻底失败。

  事务的特性

  事务必须具备ACID四个特性,其中ACID是原子性、一致性、隔离性和持久性这四个词语对应的英文首字母的组合。

  ●原子性(Atomicity)

  不论内部情况如何,从外部看,事务是一个整体──像原子那样不能分割。原子性并不是说事务和原子一样简单。根据业务逻辑的复杂程度,事务可以简单到只有一两行SQL语句,也可以复杂到包含上千行SQL语句。

  ●一致性(Consistency)

  事务能保证数据库的一致性。事务将数据库从一个一致的状态转变为另一个一致的状态。

  ●隔离性(Isolation)

  事务往往是存在并发的。为了简化系统编写的难度,数据库系统必须保证事务之间是相互隔离的,即在一个事务执行时,开发人员不需要关心是否存在并发。

  ●持久性(Durability)

  一个事务执行完成以后,数据库系统必须保证事务对数据库的影响已经永久保存下来。

  最简单的事务结构

  学习事务关键就在于理解事务的四个特性,以及如何实现这四个特性。从前文的叙述中,朋友们不难得出这样的结论:事务的持久性更多地由数据库系统负责;事务的一致性主要取决于开发人员对业务逻辑的把握;而保证事务的原子性和隔离性则需要数据库系统和开发人员一起努力,相互配合。下面笔者着重讲解有关事务原子性和隔离性的问题。

  我们先通过下面的代码建立一个测试用表Tran_TEST。

  --建立测试用数据表。col1字段加了约束,取值必须小于20。

  USE NorthWind

  CREATE TABLE Tran_TEST(id int identity(1,1),col1 int not null check(col1<20  

  GO

  DECLARE @i AS int

  SELECT @i=1

  --使用循环结构,插入10行数据

  WHILE @i<=10

  BEGIN

  INSERT INTO Tran_TEST VALUES(@i)

  SELECT @i=@i+1

  END

  GO

  我们用代码控制事务的开始和结束,这里需要引入三个命令。

  BEGIN TRAN:启动事务,它告诉SQL Server一个事务开始了,要求系统按照ACID的标准来处理事务中的语句。

  COMMIT:提交事务,它告诉SQL Server事务执行成功,可以把BEGIN TRAN以后的所有SQL 语句造成的数据变动永久写入磁盘。

  ROLLBACK:撤消事务,它告诉SQL Server把BEGIN TRAN以后的所有SQL 语句造成的数据变动撤消,就像它们从来没有执行过一样。

  下面我们看一个事务实例的代码。

  --@ErrorCode为0,代表事务执行过程中未出现错误

  DECLARE @ErrorCode int

  SELECT @ErrorCode=0

  --启动一个事务

  BEGIN TRAN

  INSERT INTO Tran_TEST VALUES(121)

  --把INSERT语句执行是否完成的结论放入@ErrorCode

  SELECT @ErrorCode=@@Error

  --如果@ErrorCode不为0,代表前面的SQL语句执行过程中发生错误(这时应撤消事务)

  IF @ErrorCode=0

  BEGIN

  UPDATE Tran_TEST SET col1=col1-3

  --把 UPDATE 语句执行是否完成的结论放入@ErrorCode

  SELECT @ErrorCode=@@Error

  END

  --如果@ErrorCode不为0,代表前面的SQL语句执行过程中发生错误(这时应撤消事务)

  IF @ErrorCode=0

  BEGIN

  INSERT INTO Tran_TEST VALUES(15)

  --把 INSERT语句执行是否完成的结论放入@ErrorCode

  SELECT @ErrorCode=@@Error

  END

  --如果@ErrorCode=0,表示整个事务执行过程中没有发生错误,应该提交事务

  IF @ErrorCode=0

  BEGIN

  COMMIT

  PRINT '提交事务'

  END

  ELSE

  BEGIN

  ROLLBACK

  PRINT '撤消事务'

  END

  代码分析

  在上述代码中,从BEGIN TRAN到COMMIT或ROLLBACK之间的语句就是所谓的“逻辑工作单元”。在这个事务中包括两条INSERT语句和一句UPDATE语句,只有它们都执行成功才提交事务,反之就撤消它。上述代码的执行结果是“撤消事务”,因为121与Tran_TEST表的约束条件发生冲突。请朋友们注意观察事务执行前后的数据变化。

  SQL Server里有这样一个规则:在一个批次中,如果一句SQL语句执行错误,SQL Server并不会停下来,而是继续往下执行(这是SQL Server的默认行为,可以改变)。以下面的代码为例,虽然第一条IN SERT语句违反了col1字段上的约束(col1<20),导致这条语句执行失败,但SQL Server会继续执行下面的UPDATE和IN SERT语句。代码末尾,无论我们调用COMMIT提交事务还是调用ROLLBACK撤消事务,都得不到这三条SQL语句“要么全执行,要么全不执行”的效果。我们可以从Tran_TEST表中数据的变化看出上面的代码与下面的代码执行效果的差异。

  BEGIN TRAN

  INSERT INTO Tran_TEST VALUES(121)

  UPDATE Tran_TEST SET col1=col1-3

  INSERT INTO Tran_TEST VALUES(15)

  --下面两句代码任选一句

  COMMIT

  ROLLBACK

  由此可见,保留事务中每一句SQL语句的执行状态(@ErrorCode),才能确保事务的原子性。这种用一个值跟踪语句执行状态的方法是一种通用的出错处理方法,请朋友们仔细体会本文的第二段代码。

  (未完待续)