ADO.NET 事务和 TransactionScope 入门教程






4.80/5 (33投票s)
本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET 中为任何 ASP.NET Web 应用程序或其他数据库驱动的应用程序实现事务。
引言
本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET
中为任何 ASP.NET
Web 应用程序或其他数据库驱动的应用程序实现事务。
背景
在日常语境中,“交易”一词表示以等价物交换商品、产品或金钱。使这种交换成为交易的事实是,商品或金钱的总量保持不变,即不会增加或减少,并且可靠性,即如果一方付出某物,则另一方将接收相同数量(不多不少)。
同样,当我们谈论数据库操作中的事务时,我们执行某些数据库操作,使得所有数据库操作要么成功,要么全部失败。这将导致在事务完成或失败后信息总量保持不变。
为了说明上述过程,假设我有两个账户持有人,一个人正试图将一些钱转给另一个人。从数据库的角度来看,此操作包含两个子操作,即:
- 从第一个账户中扣除指定金额。
- 其次,将所需金额存入第二个账户。
现在从技术角度来看,如果第一个操作成功而第二个操作失败,结果将是第一个人的账户将被扣款,而第二个账户将不会被记账,即我们丢失了信息量。反之,第二个账户将增加金额,而第一个账户甚至不会被扣款。
所以这里的关键是我们既需要两者都成功,也需要两者都失败。任何一个操作的成功都会导致结果不一致,因此即使一个操作失败,我们也需要回滚在另一个操作中所做的更改。这正是事务的用处所在。
现在让我们看一下与 .Net 框架中的事务和事务相关的一些技术细节。然后我们将看到如何在 .NET 中实现事务。
使用代码
根据定义,事务必须是原子性 (Atomic)、一致性 (Consistent)、隔离性 (Isolated) 和持久性 (Durable) 的。这些术语是什么意思?
事务的属性
- 原子性:原子性意味着构成事务的所有语句(
SQL
语句或操作)应作为一个原子操作工作,即要么全部成功,要么全部失败。 - 一致性:这意味着如果原子事务成功,数据库应处于反映更改的状态。如果事务失败,数据库应与事务开始时完全相同。
- 隔离性:如果有多个事务正在进行,那么每个事务都应独立工作,不应影响其他事务。
- 持久性:持久性意味着一旦事务提交,更改就应该是永久的,即这些更改将保存在数据库中,并且无论发生什么(如断电或其他情况)都会保留。
示例代码说明
现在为了说明如何实现事务,我们将使用一个包含单个表数据库的小型应用程序。该表包含账户 ID 和账户中的金额。该应用程序将促进账户之间的金额转账。由于涉及两个操作,我们将看到如何使用事务来执行这些操作。
示例数据库表如下所示:

用户界面将如下所示:

我们将实现此页面的三个版本,以展示三种不同的方法。我们将看到如何使用 SQL
事务在数据库级别处理事务,如何使用 ADO.NET
事务对象实现事务,最后,我们将看到如何使用 TransactionScope
对象实现事务。
注意:本文中的代码是为了阐述事务功能而编写的,不符合编码标准,容易受到 SQL
注入攻击。不应将其视为可用于生产环境的代码。这仅仅是示例代码,还有很大的改进空间。
在 SQL 中创建和实现事务
事务也可以在 SQL
级别处理。用于处理事务的 SQL
结构如下:
BEGIN TRY
BEGIN TRANSACTION
-- update first account and debit from it
-- update second account and credit in it
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
因此,如果出现问题,SQL
本身将负责回滚事务。
因此,对于我们的示例应用程序,我们可以编写以下代码来使此代码在启用 SQL
事务的情况下工作。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
SqlConnection con = null;
// they will be used to decide whether to commit or rollback the transaction
bool result = false;
string updateCommand = @"BEGIN TRY
BEGIN TRANSACTION
update Account set Amount = Amount - {0} where ID = {1}
update Account set Amount = Amount + {2} where ID = {3}
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH";
try
{
con = new SqlConnection(CONNECTION_STRING);
con.Open();
// Let us do a debit first
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = string.Format(updateCommand,
amount, debitAccountID,
amount, creditAccountID);
// check if 2 records are effected or not
result = cmd.ExecuteNonQuery() == 2;
}
}
catch
{
// do application specific cleanup or show message to user about the problem
}
finally
{
con.Close();
}
}
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
使用 ADO.NET DbTransaction 对象创建和实现事务
现在,在 SQL
级别进行事务处理是完美的解决方案,前提是所有操作都在一个地方进行。我可以创建一个存储过程来处理所有事务。但是,如果所有操作都发生在不同的类甚至不同的程序集中,情况会怎样?在这种情况下,需要使用 ADO.NET
事务在代码中处理事务。
处理事务的另一种方法是从代码本身使用 ADO.NET
DbTransaction
对象。为此,应执行以下步骤:
- 创建连接。
- 创建事务。
- 为事务内的所有操作创建命令。
- 打开连接。
- 开始事务。
- 将所有命令与上述事务对象关联。
- 执行命令。
- 单独检查命令状态。
- 如果任何命令失败,则回滚事务。
- 如果所有命令都成功,则提交事务。
为了说明上述过程,让我们编写一些代码来在我们的示例应用程序中执行事务。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
SqlTransaction transaction = null;
SqlConnection con = null;
// they will be used to decide whether to commit or rollback the transaction
bool debitResult = false;
bool creditResult = false;
try
{
con = new SqlConnection(CONNECTION_STRING);
con.Open();
// lets begin a transaction here
transaction = con.BeginTransaction();
// Let us do a debit first
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = string.Format(
"update Account set Amount = Amount - {0} where ID = {1}",
amount, debitAccountID);
// assosiate this command with transaction
cmd.Transaction = transaction;
debitResult = cmd.ExecuteNonQuery() == 1;
}
// A dummy throw just to check whether the transaction are working or not
//throw new Exception("Let see..."); // uncomment this line to see the transaction in action
// And now do a credit
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = string.Format(
"update Account set Amount = Amount + {0} where ID = {1}",
amount, creditAccountID);
// assosiate this command with transaction
cmd.Transaction = transaction;
creditResult = cmd.ExecuteNonQuery() == 1;
}
if (debitResult && creditResult)
{
transaction.Commit();
}
}
catch
{
transaction.Rollback();
}
finally
{
con.Close();
}
}
此代码块确保与事务关联的所有操作要么成功,要么都不成功。
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
使用 TransactionScope 对象创建和实现事务
使用 ADO.NET
事务创建和使用事务没有问题,只要我们对事务对象执行提交或回滚操作。如果我们忘记这样做并离开代码,就会导致一些问题。
为了解决这些问题,还有另一种处理事务的方法,即使用 TransactionScope
对象。TransactionScope
在处理事务时更像是一种语法糖。它还能确保,如果事务未提交且代码超出作用域,则事务将被回滚。
要使用 TransactionScope
对象处理事务,需要执行以下操作:
- 在一个 using 块中创建一个
TransactionScope
对象。 - 在此
TransactionScope
using 块中创建连接。 - 在此处创建所有命令。
- 使用命令执行所有操作。
- 如果所有操作都成功,则调用
TransactionScope
对象上的 Complete 函数。 - 如果任何命令失败,让控制流跳出作用域,事务将被回滚。
为了说明这个过程,让我们尝试使用 TransactionScope
对象在我们的示例应用程序中重新实现该功能。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
// they will be used to decide whether to commit or rollback the transaction
bool debitResult = false;
bool creditResult = false;
try
{
using (TransactionScope ts = new TransactionScope())
{
using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
{
con.Open();
// Let us do a debit first
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = string.Format(
"update Account set Amount = Amount - {0} where ID = {1}",
amount, debitAccountID);
debitResult = cmd.ExecuteNonQuery() == 1;
}
// A dummy throw just to check whether the transaction are working or not
throw new Exception("Let see..."); // uncomment this line to see the transaction in action
// And now do a credit
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = string.Format(
"update Account set Amount = Amount + {0} where ID = {1}",
amount, creditAccountID);
creditResult = cmd.ExecuteNonQuery() == 1;
}
if (debitResult && creditResult)
{
// To commit the transaction
ts.Complete();
}
}
}
}
catch
{
// the transaction scope will take care of rolling back
}
}
此代码块确保 TransactionScope
作用域内的所有操作要么成功,要么都不成功。
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
为了给开发人员提供更多控制,除了 TransactionScope
的作用域控制外,还有一些选项可以与 TransactionScope
关联。
-
Required
:如果当前作用域内已实例化另一个TransactionScope
,则此TransactionScope
对象将加入其中。 -
RequiresNew
:即使当前作用域内已实例化另一个 TransactionScope,此TransactionScope
对象也将创建一个新的事务,该事务将在其作用域内工作。 -
Supress
:即使当前作用域内已实例化另一个TransactionScope
,此TransactionScope
对象现在也会将作用域内的所有操作排除在现有事务之外。
这些选项可以通过以下方式传递到 TransactionScope
的构造函数中:
using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required))
{
// Code goes here
}
关于分布式事务的说明
如果一个事务跨越多个进程,那么它就是一个分布式事务。也就是说,如果我需要在 SqlServer
中执行一个操作,在 Oracle 中执行另一个操作,并且有一个事务与之关联,那么它将是一个分布式事务。
这很重要,原因有两个。如果事务不是分布式事务,事务的隔离性将由 LTM
(轻量级事务管理器)来保证。但如果它是分布式事务,那么分布式事务控制器(DTC
)将接管控制。
当我们使用 TransactionScope
类时,如果事务在一个进程中开始,LTM
将继续处理;但如果另一个进程中的另一个操作完成,DTC
将接管,事务随后将自动提升为分布式事务。因此,TransactionScope
类可以轻松创建可提升的事务。
但是,即使对于非分布式事务,也应仅将 transactionScope 对象与 SqlServer 2005
或更高版本一起使用,因为早期 SqlServer
产品不了解 LTM
和 DTC
。因此,对于早期产品,ADO.NET
DbTransaction
对象是从代码处理事务的理想方式。
关注点
在本文中,我们尝试从非常初学者的角度探讨什么是事务以及如何在 .NET 中处理事务。从主题的角度来看,这仅仅是冰山一角。我们没有讨论隔离级别和分布式事务(这本身就是一个庞大的主题)。但我们确实讨论了在 .NET 中处理事务的各种方法。
历史
- 2013 年 1 月 5 日:初稿。