同步发生数据更新时,防止数据的不一致。
这些就是数据库特有的术语了。数据库事务是指作为单个逻辑工作单元执行的一系列操作。
设想网上购物的一次交易,其付款过程至少包括以下几步数据库操作:
·
更新客户所购商品的库存信息
·
保存客户付款信息
--
可能包括与银行系统的交互
·
生成订单并且保存到数据库中
·
更新用户相关信息,例如购物数量等等
正常的情况下,这些操作将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在这一系列过程中任何一个环节出了差错,例如在更新商品库存信息时发生异常、该顾客银行帐户存款不足等,都将导致交易失败。一旦交易失败,数据库中所有信息都必须保持交易前的状态不变,比如最后一步更新用户信息时失败而导致交易失败,那么必须保证这笔失败的交易不影响数据库的状态
--
库存信息没有被更新、用户也没有付款,订单也没有生成。否则,数据库的信息将会一片混乱而不可预测。
数据库事务正是用来保证这种情况下交易的平稳性和可预测性的技术。
数据库事务的
ACID
属性
事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的
ACID(
原子性、一致性、隔离性和持久性
)
属性:
·
原子性
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
·
一致性
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如
B
树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
·
隔离性
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
·
持久性
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
DBMS
的责任和我们的任务
企业级的数据库管理系统(
DBMS
)都有责任提供一种保证事务的物理完整性的机制。就常用的
SQL Server2000
系统而言,它具备锁定设备隔离事务、记录设备保证事务持久性等机制。因此,我们不必关心数据库事务的物理完整性,而应该关注在什么情况下使用数据库事务、事务对性能的影响,如何使用事务等等。
本文将涉及到在
.net
框架下使用
C#
语言操纵数据库事务的各个方面。
体验
SQL
语言的事务机制
作为大型的企业级数据库,
SQL Server2000
对事务提供了很好的支持。我们可以使用
SQL
语句来定义、提交以及回滚一个事务。
如下所示的
SQL
代码定义了一个事务,并且命名为
"MyTransaction"
(限于篇幅,本文并不讨论如何编写
SQL
语言程序,请读者自行参考相关书籍):
DECLARE @TranName VARCHAR(20) SELECT @TranName = 'MyTransaction' BEGIN TRANSACTION @TranNameGOUSE pubs GO UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%' GO COMMIT TRANSACTION MyTransaction GO
这里用到了
SQL Server2000
自带的示例数据库
pubs
,提交事务后,将为所有畅销计算机书籍支付的版税增加
10%
。
打开
SQL Server2000
的查询分析器,选择
pubs
数据库,然后运行这段程序,结果显而易见。
可是如何在
C#
程序中运行呢?我们记得在普通的
SQL
查询中,一般需要把查询语句赋值给
SalCommand.CommandText
属性,这里也就像普通的
SQL
查询语句一样,将这些语句赋给
SqlCommand.CommandText
属性即可。要注意的一点是,其中的
"GO"
语句标志着
SQL
批处理的结束,编写
SQL
脚本是需要的,但是在这里是不必要的。我们可以编写如下的程序来验证这个想法:
//TranSql.csusing System; using System.Data; using System.Data.SqlClient; namespace Aspcn {
public class DbTranSql
{
file://
将事务放到
SQL Server
中执行
public void DoTran()
{
file://
建立连接并打开
SqlConnection myConn=GetConn();myConn.Open();
SqlCommand myComm=new SqlCommand();
try
{
myComm.Connection=myConn;
myComm.CommandText="DECLARE @TranName VARCHAR(20) ";
myComm.CommandText+="SELECT @TranName = 'MyTransaction' ";
myComm.CommandText+="BEGIN TRANSACTION @TranName ";
myComm.CommandText+="USE pubs ";
myComm.CommandText+="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%' ";
myComm.CommandText+="COMMIT TRANSACTION MyTransaction ";
myComm.ExecuteNonQuery();
}
catch(Exception err)
{
throw new ApplicationException("
事务操作出错,系统信息:
"+err.Message);
}
finally
{
myConn.Close();
}
}
file://
获取数据连接
private SqlConnection GetConn()
{
string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password=";
SqlConnection myConn=new SqlConnection(strSql);
return myConn;
}
}
public class Test
{
public static void Main()
{
DbTranSql tranTest=new DbTranSql();
tranTest.DoTran();
Console.WriteLine("
事务处理已经成功完成。
");
Console.ReadLine();
}
} }
注意到其中的
SqlCommand
对象
myComm
,它的
CommandText
属性仅仅是前面
SQL
代码字符串连接起来即可,当然,其中的
"GO"
语句已经全部去掉了。这个语句就像普通的查询一样,程序将
SQL
文本事实上提交给
DBMS
去处理了,然后接收返回的结果(如果有结果返回的话)。
很自然,我们最后看到了输出
"
事务处理已经成功完成
"
,再用企业管理器查看
pubs
数据库的
roysched
表,所有
title_id
字段以
"PC"
开头的书籍的
royalty
字段的值都增加了
0.1
倍。
这里,我们并没有使用
ADO.net
的事务处理机制,而是简单地将执行事务的
SQL
语句当作普通的查询来执行,因此,事实上该事务完全没有用到
.net
的相关特性。
了解
.net
中的事务机制
如你所知,在
.net
框架中主要有两个命名空间
(namespace)
用于应用程序同数据库系统的交互:
System.Data.SqlClient
和
System.Data.OleDb
。前者专门用于连接
Microsoft
公司自己的
SQL Server
数据库,而后者可以适应多种不同的数据库。这两个命名空间中都包含有专门用于管理数据库事务的类,分别是
System.Data.SqlClient.SqlTranscation
类和
System.Data.OleDb.OleDbTranscation
类。
就像它们的名字一样,这两个类大部分功能是一样的,二者之间的主要差别在于它们的连接机制,前者提供一组直接调用
SQL Server
的对象,而后者使用本机
OLE DB
启用数据访问。
事实上,
ADO.net
事务完全在数据库的内部处理,且不受
Microsoft
分布式事务处理协调器
(DTC)
或任何其他事务性机制的支持。本文将主要介绍
System.Data.SqlClient.SqlTranscation
类,下面的段落中,除了特别注明,都将使用
System.Data.SqlClient.SqlTranscation
类。
事务的开启和提交
现在我们对事务的概念和原理都了然于心了,并且作为已经有一些基础的
C#
开发者,我们已经熟知编写数据库交互程序的一些要点,即使用
SqlConnection
类的对象的
Open()
方法建立与数据库服务器的连接,然后将该连接赋给
SqlCommand
对象的
Connection
属性,将欲执行的
SQL
语句赋给它的
CommandText
属性,于是就可以通过
SqlCommand
对象进行数据库操作了。对于我们将要编写的事务处理程序,当然还需要定义一个
SqlTransaction
类型的对象。并且看到
SqlCommand
对象的
Transcation
属性,我们很容易想到新建的
SqlTransaction
对象应该与它关联起来。
基于以上认识,下面我们就开始动手写我们的第一个事务处理程序。我们可以很熟练地写出下面这一段程序:
//DoTran.csusing System; using System.Data; using System.Data.SqlClient; namespace Aspcn {
public class DbTran
{
file://
执行事务处理
public void DoTran()
{
file://
建立连接并打开
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
SqlTransaction myTran=new SqlTransaction();
try
{
myComm.Connection=myConn;
myComm.Transaction=myTran;
file://
定位到
pubs
数据库
myComm.CommandText="USE pubs";
myComm.ExecuteNonQuery();
file://
更新数据
file://
将所有的计算机类图书
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'";
myComm.ExecuteNonQuery();//
提交事务
myTran.Commit();
}
catch(Exception err)
{
throw new ApplicationException("
事务操作出错,系统信息:
"+err.Message);
}
finally
{
myConn.Close();
}
}
file://
获取数据连接
private SqlConnection GetConn()
{
string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password=";
SqlConnection myConn=new SqlConnection(strSql);
return myConn;
}
}
public class Test{public static void Main()
{
DbTran tranTest=new DbTran();
tranTest.DoTran();
Console.WriteLine("
事务处理已经成功完成。
");
Console.ReadLine();
} } }
显然,这个程序非常简单,我们非常自信地编译它,但是,出乎意料的结果使我们的成就感顿时烟消云散:
error CS1501:
重载
"SqlTransaction"
方法未获取
"0"
参数
是什么原因呢?注意到我们初始化的代码:
SqlTransaction myTran=new SqlTransaction();
显然,问题出在这里,事实上,
SqlTransaction
类并没有公共的构造函数,我们不能这样新建一个
SqlTrancaction
类型的变量。在事务处理之前确实需要有一个
SqlTransaction
类型的变量,将该变量关联到
SqlCommand
类的
Transcation
属性也是必要的,但是初始化方法却比较特别一点。在初始化
SqlTransaction
类时,你需要使用
SqlConnection
类的
BeginTranscation()
方法:
SqlTransaction myTran; myTran=myConn.BeginTransaction();
该方法返回一个
SqlTransaction
类型的变量。在调用
BeginTransaction()
方法以后,所有基于该数据连接对象的
SQL
语句执行动作都将被认为是事务
MyTran
的一部分。同时,你也可以在该方法的参数中指定事务隔离级别和事务名称,如:
SqlTransaction myTran; myTran=myConn.BeginTransaction(IsolationLevel.ReadCommitted,"SampleTransaction");
关于隔离级别的概念我们将在随后的内容中探讨,在这里我们只需牢记一个事务是如何被启动,并且关联到特定的数据链接的。
先不要急着去搞懂我们的事务都干了些什么,看到这一行:
myTran.Commit();
是的,这就是事务的提交方式。该语句执行后,事务的所有数据库操作将生效,并且为数据库事务的持久性机制所保持
--
即使系统在这以后发生致命错误,该事务对数据库的影响也不会消失。
对上面的程序做了修改之后我们可以得到如下代码(为了节约篇幅,重复之处已省略,请参照前文):
//DoTran.cs……
}
file://
执行事务处理
public void DoTran() {
file://
建立连接并打开
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
file://SqlTransaction myTran=new SqlTransaction();
file://
注意,
SqlTransaction
类无公开的构造函数
SqlTransaction myTran;
file://
创建一个事务
myTran=myConn.BeginTransaction();
try
{
file://
从此开始,基于该连接的数据操作都被认为是事务的一部分
file://
下面绑定连接和事务对象
myComm.Connection=myConn;
myComm.Transaction=myTran; file://
定位到
pubs
数据库
myComm.CommandText="USE pubs";
myComm.ExecuteNonQuery();//
更新数据
file://
将所有的计算机类图书
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'";
myComm.ExecuteNonQuery();
file://
提交事务
myTran.Commit();
}
catch(Exception err)
{
throw new ApplicationException("
事务操作出错,系统信息:
"+err.Message);
}
finally
{
myConn.Close();
} } ……
到此为止,我们仅仅掌握了如何开始和提交事务。下一步我们必须考虑的是在事务中可以干什么和不可以干什么。