SQL Server之恋(10):事务的魅力(下)
软件世界
上期介绍了最简单的事务结构,这次我们对事务进行深入分析,让大家对SQL Server中的事务有个深刻的认识。并发冲突是数据库应用中经常遇到的问题,也是开发者头痛的问题,本文将告诉你怎样用事务来控制并发冲突。
上期介绍的事务结构中,明确使用了BEGIN TRAN语句告诉系统事务开始了,所以可称作“显式事务”。除此之外,在SQL Server中还有一种“隐式事务”。
隐式事务
顾名思义,“隐式事务”与“显式事务”正好相反,它不需要用BEGIN TRAN开启事务,而是由一些常见的SQL 语句(UP DATE、INSERT等)开启,后面的语句自动添加到这个事务中,最后调用COMMIT(ROLLBACK)提交(撤消)这个事务。
编写一个“隐式事务”只须把上期文章中的第二段代码做如下修改:
1.在代码开头用SET IMPLIC IT_TRANSACTIONS 语句修改事务处理模式为“隐式事务”:SET IMPLIC IT_TRANSACTIONS ON。
2. 去掉语句“BEGIN TRAN”。
3.在代码结尾恢复系统默认设置:SET IMPLICIT_TRANSACTIONS OFF。
提示:SQL Server中默认采用“显式事务”模式,笔者也建议读者在SQL Serv er中使用这种事务模式。有趣的是,在 ANSI (美国国家标准局)的规范中,“隐式事务”是主流,很多数据库系统使用类似“隐式事务”的处理方式。
下面的比喻可能会帮助朋友们理解这两种事务处理模式:“隐式事务”像高级餐厅,有周到的服务,需要什么,服务员就会送上来,最后再一次结账;“显式事务”像自助快餐厅,想要什么自己买,买一次付一次钱。
句句皆事务
在SQL Server里,每一条SQL 语句都是一个独立的、隐含的事务,这也许就是Transact-SQL名称的来源吧!按照事务原子性的要求,我们可以得出这样的推论:如果某一条SQL语句将影响多行数据,但只要在某一行发生错误(对其他数据行处理都正确),这条语句(事务)的影响就会被撤消,它不会给数据库带来任何变动。
我们用下面的例子来验证这个推论。执行“UPDATE Tran_TEST SET col1= col1*2”语句,虽然有些记录乘以2的结果小于20,没有违反Tran_TEST表的限定条件,但是因为UPDATE执行失败,事务被自动撤消,Tran_TEST表没有发生任何变化。
并发冲突
大多数数据库系统都需要为多个用户提供服务,于是就出现了这样一种情况:多人同一时刻对同一数据进行读写,这就是所谓的“并发”。可以想象,如果对并发不进行有效的控制,数据库随时都可能处于混乱状态,这种由并发带来的问题称作“并发冲突”。
锁
SQL Server通过锁来处理并发冲突问题,它的基本思想是:在一个事务中锁定需要修改的数据表,这时即使有其他并发事务执行,由于数据表已经被锁定,也不可能发生多个事务在同一时刻对同一数据进行读写的情况,从而避免了并发冲突。
不难看出,锁虽然能够解决并发冲突问题,但是使用锁的代价是高昂的:第一,锁必然导致系统并发能力降低,在繁忙的系统中,过多的锁必然导致系统效率低下,让用户无法忍受;第二,人工加锁、解锁的过程是痛苦的,对开发人员要求很高。
三种冲突,四种隔离级别
为了取得平衡,人们分析了并发冲突问题,总结出三类冲突,并且根据用户对冲突问题的承受能力,为事务设计了四种隔离级别,由数据库系统根据事先设定的事务隔离级别自动加锁、解锁,取得了提高并发能力、防范并发冲突以及减轻开发人员负担这三个方面的统一。
三类冲突问题分别是“脏读”、“不可重复读”和“幻影读”。限于篇幅,笔者不能深入介绍这三类冲突的表现形式和形成原因,只能给出这样的结论:对付不同的并发冲突问题,应该采用不同的锁方案,以节省锁资源,提高系统的并发能力。
四种事务隔离级别分别是“读未提交”(READ UNCOMMITTED)、“读已提交”(READ COMMITTED)、“可重复读”(RE PEATABLE READ)和“可串行化”(SERI ALIZABLE)。这四种隔离级别也可以看做是四种不同的锁方案。总地来说,“读未提交”是一种无锁的状态,不能对付任何并发冲突;“读已提交”可以对付“脏读”;“可重复读”可以对付“脏读”和“不可重复读”这两种并发冲突;“可串行化”可以对付所有三种并发冲突。
例子
SQL Server默认的事务隔离级别是“读已提交”。我们可以通过 SET TRANSACTION ISOLATION LEVEL来修改事务的隔离级别。
--设定事务的隔离级别是“可串行化”
SET TRANSACTION ISOLATION LEVEL SE RIALIZABLE
DECLARE @Sum1 int,@Sum2 int
BEGIN TRAN
--第一次读取col1<5的所有记录的和值
SELECT @Sum1=SUM(col1) FROM Tran_TEST WHERE Col1<=5
--等待10秒钟
WAITFOR DELAY '000;00;10'
--第二次读取col1<5的所有记录的和值
SELECT @Sum2=SUM(col1) FROM Tran_TEST WHERE Col1<=5
PRINT '@Sum1='+CAST(@Sum1 as varchar)
PRINT '@Sum2='+CAST(@Sum2 as varchar)
COMMIT
--恢复默认的事务隔离级别“读已提交”
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
在上述代码中,我们希望两次读取的和值(col1<5的所有记录的col1字段的和)是相同的。但是两次读取有10秒钟的间隔时间,如果这段时间内有其他并发用户执行了如“INSERT INTO Tran_TEST VAL UES(2)”等会改变col1值的语句,则Sum1和Sum2的值很可能不相同。实际上这是一种“幻影读”的并发冲突问题,为了防范它,我们在上述代码中对事务设定了可以对付“幻影读”的隔离级别──“可串行化”。
有了事务隔离级别,开发人员对付并发冲突就很容易了,因为麻烦的事情都交给了SQL Server。
事务不是万能的
意外有很多类型,事务可以对付其中的一部分,但是事务不是万能的。在SQL Server中,常见的SELECT、INSERT、UP DATE、DELETE这些DML语句受事务保护,而CREATE DATABASE、CREATE TABLE、CREATE PROCEDURE等DDL语句则不能出现在事务中;另外在事务中定义的各种变量也不受事务的保护。
事务是数据库的精华内容,朋友们可以结合前几次讨论过的存储过程、触发器和自定义函数,把事务融入到T-SQL编程中去,生成更有效、更可靠的代码。
编后:到这期为止,我们的“SQL Server之恋”连载就结束了。在这个连载中,我们先后介绍了“选择SQL Server”(第1期)、“SQL Server的安装和基本框架”(第2期)、“初识T-SQL”(第4期)、“存储过程”(第5期)、“用户定义函数”(第7期)、“触发器”(第8期)和“事务”(第9、10期)。连载虽然结束了,不过我们会继续关注SQL Server的应用,陆续给大家介绍安全机制、备份策略、数据转换、数据库连接和技术发展趋势等各个方面的知识,敬请大家关注。
“数据库技术”版是为广大数据库技术爱好者服务的,普及数据库技术是我们的目的。不论你是业内人士、普通爱好者还是初学者,我们都希望能提供给你丰富的实用信息。欢迎有关DBMS的最新技术动态、开发技术与应用、维护技巧和学习经验的文章,数据库系统不限,MS SQL Server/MySQL/Sybase/DB2/Oracle/Access/Informix/Inter base等均可。投稿信箱:Software@cpcw.com,PCW_soft@VIP.163.com