本文内容
- T-SQL中的事务处理
- ADO.NET中的事务处理
- LINQ to SQL中的隐式事务处理
- 分布式事务处理
释义
分布式事务跨越两个或多个称为资源管理器的服务器。称为事务管理器的服务器组件必须在资源管理器之间协调事务管理。如果分布式事务由 Microsoft 分布式事务处理协调器 (MS DTC) 之类的事务管理器或其他支持 X/Open XA 分布式事务处理规范的事务管理器来协调,则每个 SQL Server Database Engine 实例都可以作为资源管理器来运行。有关详细信息,请参阅 MS DTC 文档。
跨越两个或多个数据库的单个数据库引擎 实例中的事务实际上是分布式事务。该实例对分布式事务进行内部管理;对于用户而言,其操作就像本地事务一样。
对于应用程序而言,管理分布式事务很像管理本地事务。当事务结束时,应用程序会请求提交或回滚事务。不同的是,分布式提交必须由事务管理器管理,以尽量避免出现因网络故障而导致事务由某些资源管理器成功提交,但由另一些资源管理器回滚的情况。通过分两个阶段(准备阶段和提交阶段)管理提交进程可避免这种情况,这称为两阶段提交 (2PC)。
准备阶段:
当事务管理器收到提交请求时,它会向该事务涉及的所有资源管理器发送准备命令。然后,每个资源管理器将尽力使该事务持久,并且所有保存该事务日志映象的缓冲区将被刷新到磁盘中。当每个资源管理器完成准备阶段时,它会向事务管理器返回准备成功或准备失败的消息。
提交阶段:
如果事务管理器从所有资源管理器收到准备成功的消息,它将向每个资源管理器发送一个提交命令。然后,资源管理器就可以完成提交。如果所有资源管理器都报告提交成功,那么事务管理器就会向应用程序发送一个成功通知。如果任一资源管理器报告准备失败,那么事务管理器将向每个资源管理器发送一个回滚命令,并向应用程序表明提交失败。
数据库引擎 应用程序可以通过 Transact-SQL 或数据库 API 来管理分布式事务。
测试数据库
数据库名称:test
数据库表名称:Users
表结构:
列名 |
数据类型 |
允许为空 |
注释 |
UsersID |
int |
false |
主键,自增标识 |
UserName |
varchar(50) |
false |
用户名,唯一约束 |
Password |
varchar(50) |
false |
在文章开始之前,我们首先要明确:事务是单个的工作单元。如果某一事务成功,则在该事务中进行的所有数据修改均会提交,成为数据库中的永久组成部分。如果事务遇到错误且必须取消或回滚,则所有数据修改均被清除。也就是说,当我们使用SQL Server的查询分析器中执行了了一行代码“insert into t values(x, y, z)”,数据成功地添加到了数据库中,但这并不是说这里没有使用事务处理,而是系统帮我们将常用的操作自动地做了一些处理(我猜想应该是代理模式)。
SQL Server数据库为我们提供了以下几种事务模式:
自动提交事务 每条单独的语句都是一个事务。
显式事务 每个事务均以 BEGIN TRANSACTION 语句显式开始,以 COMMIT 或 ROLLBACK 语句显式结束。
隐式事务 在前一个事务完成时新事务隐式启动,但每个事务仍以 COMMIT 或 ROLLBACK 语句显式完成。
批处理级事务 只能应用于多个活动结果集 (MARS),在 MARS 会话中启动的 Transact-SQL 显式或隐式事务变为批处理级事务。当批处理完成时没有提交或回滚的批处理级事务自动由 SQL Server 进行回滚。
第一节 T-SQL中的事务处理
下面的代码演示了如何使用T-SQL语句创建一个显式事务:
--开启一个事务 begin transaction --使用try…catch结构捕获异常 begin try --插入两条数据(相同的用户名) INSERT INTO [test].[dbo].[Users] ([UserName] ,[Password]) VALUES ('张三','123') INSERT INTO [test].[dbo].[Users] ([UserName] ,[Password]) VALUES ('张三','456') commit transaction --提交事务 end try --如果出现了异常,进入catch代码段 begin catch rollback transaction --回滚事务 select ERROR_MESSAGE() as [Message] --输出错误信息 end catch
以上代码执行后会失败,错误信息为:“违反了 UNIQUE KEY 约束 'UQ_Users_UserName'。不能在对象 'dbo.Users' 中插入重复键”。
需要注意的是,在SQL Server中,如果没有显式声明事务,那么系统将分配隐性事务。当数据库引擎 实例首次执行下列任何语句时,都会自动启动一个事务:
ALTER TABLE |
INSERT |
CREATE |
OPEN |
DELETE |
REVOKE |
DROP |
SELECT |
FETCH |
TRUNCATE TABLE |
GRANT |
UPDATE |
第二节 ADO.NET中的事务处理
以下代码演示了如何使用SqlClient组件来创建一个显式事务:
using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); SqlCommand command = connection.CreateCommand(); SqlTransaction transaction; // 启动一个本地事务 transaction = connection.BeginTransaction(); // 在启动一个本地事务之前,需要为Command对象指派Connection对象与Transaction对象 command.Connection = connection; command.Transaction = transaction; try { command.CommandText = "INSERT INTO [test].[dbo].[Users] ([UserName] ,[Password]) VALUES ('张三','123')"; command.ExecuteNonQuery(); command.CommandText = "INSERT INTO [test].[dbo].[Users] ([UserName] ,[Password]) VALUES ('张三','123')"; command.ExecuteNonQuery(); // 尝试提交事务 transaction.Commit(); Console.WriteLine("所有的记录已经提交至数据库"); } catch (Exception ex) { Console.WriteLine("引发的异常类型为: {0}", ex.GetType()); Console.WriteLine("异常信息: {0}", ex.Message); // 尝试回滚事务 try { transaction.Rollback(); } catch (Exception ex2) { // 此catch块将处理任何可能发生在服务器上的,导致回滚失败的错误。如连接已关闭。 Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType()); Console.WriteLine(" Message: {0}", ex2.Message); } } }
以上代码执行后会失败,错误信息为:“违反了 UNIQUE KEY 约束 'UQ_Users_UserName'。不能在对象 'dbo.Users' 中插入重复键”。
第三节 LINQ to SQL中的隐式事务处理
下面的代码演示了如何使用LINQ to SQL技术进行事务处理:
using (TestDataContext db = new TestDataContext()) { // 在控制台中输出T-SQL语句 db.Log = Console.Out; //创建两个Users对象(注意,用户名是相同的) Users u1 = new Users() { UserName = "张三", Password = "123" }; Users u2 = new Users() { UserName = "张三", Password = "456" }; try { db.Users.InsertAllOnSubmit(new Users[] { u1, u2 }); // 尝试提交事务 db.SubmitChanges(); Console.WriteLine("所有的记录已经提交至数据库"); } catch (Exception ex) { Console.WriteLine("引发的异常类型为: {0}", ex.GetType()); Console.WriteLine("异常信息: {0}", ex.Message); } }
以上代码执行后会失败,错误信息为:“违反了 UNIQUE KEY 约束 'UQ_Users_UserName'。不能在对象 'dbo.Users' 中插入重复键”。
在代码中,当调用 SubmitChanges 时,LINQ to SQL 会检查此调用是否在 Transaction 的作用域内或者 Transaction 属性 (IDbTransaction) 是否设置为由用户启动的本地事务。如果这两个事务它均未找到,则 LINQ to SQL 启动本地事务 (IDbTransaction),并使用此事务执行所生成的 SQL 命令。当所有 SQL 命令均已成功执行完毕时,LINQ to SQL 提交本地事务并返回。这就是LINQ to SQL的“隐式事务”。
第四节 分布式事务处理
本文进行到这里,将使用一个问题引入标题内容“分布式事务处理”是怎样的一种操作:如何将两个DataContext对象的提交过程当做一个事务来处理?
可能你对这个问题不是太明白,下面的代码演示了这个问题是一个怎样的过程:
//创建两个TestDataContext对象 TestDataContext db1 = new TestDataContext(); TestDataContext db2 = new TestDataContext(); //创建两个Users对象(注意,用户名是相同的) Users u1 = new Users() { UserName = "张三", Password = "123" }; Users u2 = new Users() { UserName = "张三", Password = "456" }; try { //将两个Users对象分别加入到不同的TestDataContext对象中 db1.Users.InsertOnSubmit(u1); db2.Users.InsertOnSubmit(u2); // 尝试提交事务 db1.SubmitChanges(); db2.SubmitChanges(); Console.WriteLine("所有的记录已经提交至数据库"); } catch (Exception ex) { Console.WriteLine("引发的异常类型为: {0}", ex.GetType()); Console.WriteLine("异常信息: {0}", ex.Message); }
以上代码执行后同样会失败,错误信息为:“违反了 UNIQUE KEY 约束 'UQ_Users_UserName'。不能在对象 'dbo.Users' 中插入重复键”。但是明显的,数据库中将成功添加一条数据——db1.SubmitChanges()执行时提交的数据。
那么,如何才能让这个过程符合我们的要求呢?以下代码演示了这一点:
//创建两个TestDataContext对象 TestDataContext db1 = new TestDataContext(); TestDataContext db2 = new TestDataContext(); //创建两个Users对象(注意,用户名是相同的) Users u1 = new Users() { UserName = "张三", Password = "123" }; Users u2 = new Users() { UserName = "张三", Password = "456" }; //将两个Users对象分别加入到不同的TestDataContext对象中 db1.Users.InsertOnSubmit(u1); db2.Users.InsertOnSubmit(u2); // 使用TransactionScope对象,使代码块成为事务性代码。 using (TransactionScope rs = new TransactionScope()) { try { // 尝试提交事务 db1.SubmitChanges(); db2.SubmitChanges(); rs.Complete(); Console.WriteLine("所有的记录已经提交至数据库"); } catch (Exception ex) { Console.WriteLine("引发的异常类型为: {0}", ex.GetType()); Console.WriteLine("异常信息: {0}", ex.Message); } }
当决定使用SQL Server分布式事务处理模式时,必须启动名称为Distributed Transaction Coordinator的Windows服务项,你可以在服务管理器中启动它(开始菜单-运行- services.msc),也可以在命令提示符中直接启动它(net start msdtc)。
这时,包含在TransactionScope对象代码块中的两个DataContext对象,其执行SubmitChanges方法时将处于事务之中,如果此时出现了异常,那么之前的所有操作将会回滚。也就是说如果在事务范围中(即从初始化 TransactionScope 对象到调用其 Dispose 方法之间)发生异常了异常,事务都将结束,并回滚至初始状态。这意味着,即使执行了Complete方法,在这之后发生的异常也将导致事务失败。所以,尽量不要在执行Complete方法与Dispose方法之间,加入可能会导致事务失败的代码。
如果TransactionScope对象成功执行了Complete方法,那么所有数据将提交至数据库中。需要注意的是,如果在TransactionScope对象代码块中未执行TransactionScope对象的Complete方法,那么事务也将终止并回滚至初始状态。
同样的,TransactionScope对象代码块也适用于其他代码级数据库访问,例如将多个SqlConnection对象的提交过程放在同一个TransactionScope对象代码块中,就可以方便地控制事务的执行方式。