如果在数据库发生更换的情况下,只需要修改配置文件的数据提供程序名称这一行即可事务
事务是一组必须全部成功或者全部失败的操作。事务的目标是保证数据总能处于有效一致的状态。例如,转账操作。
事务有 4 个被称为 ACID 属性的特征,ACID 是以下概念的缩写:
- Atomic(原子性):事务中所有步骤必须同时成功或失败
- Consist(一致性):事务使底层数据库在稳定状态间转换
- Isolated(隔离性):每个事务都是独立的实体,一个事务不应该影响同时运行的其他事物。
- Durable(持久性):在事务成功前,事务产生的变化永久的存储在媒质上,同时也必须维护日志以保证出现硬件故障数据库也能得以恢复。
这些事事务的理想特征,它们未必总能达到。执行事务时 RDBMS 需要锁定数据,这样其他用户就不能访问它了。锁越多,粒度就越大,执行事务时其他用户就更不可能完成某些任务。也就是说,需要在用户并发性和隔离性间做出权衡。
事务和 ASP.NET 应用程序
ASP.NET 可以使用 3 种基本类型的事务:
- 存储过程事务:这些事务完全在数据库中处理,存储过程事务提供最佳的性能,因为只需往返一次数据库。缺点是需要用 SQL 语句编写事务处理逻辑。
- 客户端引发(ADO.NET)的事务:这些事务由代码通过编程来控制。它们使用和存储过程事务一样的命令,代码使用了封装这些细节的 ADO.NET 对象。缺点是在事务开始和提交时需要额外往返数据库。
- COM+ 事务:COM+ 采用两步提交协议,总会带来额外开销。只有当事务需要跨越多个资源管理器的时候才需要使用 COM+事务。一个COM+可以跨越 SQL Server 及 Oracle 数据库中的交互。
尽管ADO.NET提供对事务的良好支持,但还是不应该随便使用。每次使用都会为系统带来额外的负担。另外,事务会锁定表的某些行,不必要的事务会损害你的应用程序性能。
使用事务应遵循这些实践原则以获得最佳效果:
- 事务要尽量短
- 不要在事务中间使用 SELECT 查询返回数据
- 如果事务中确实要获取记录,应该只获取确实需要的记录,这样可以减少锁定的资源数
- 可能的情况下,在存储过程中使用事务,而不是在 ADO.NET 中使用事务。这样的事务可以被更快的启动和编译,因为数据库服务器不需要与客户端(Web 应用程序)交互。
- 避免使用具有多个独立批处理任务的事务,把各个批处理任务作为单个事务
- 尽量避免影响大批量记录的更新
1. 存储过程事务
只要可能,事务存放的最佳地点是存储过程代码。它最有可能获得最佳性能,所有的活动在数据源进行而不需要任何网络通信。总之,事务的跨度越短,数据库的并发性越好,被序列化的数据库请求就越少。
下面这段伪代码演示了如何在两个账户间转移资金,它是一个简化的版本,允许账户村矿为负数:
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
if(@@error > 0)
GOTO Problem
update Accounts set balance = balance - @Amount where AccountID = @ID_B
if(@@error > 0)
GOTO Problem
-- 没有错误
commit
return
-- 有错误
Problem:
rollback
raiserror('Could not update.',16,1)
当在 Transact-SQL 中使用 @@error 值时,必须在每步操作完成后立即检查该值!!!因为 @@error 在一条 SQL 语句成功执行后自动被重设为 0 。
如果使用 SQL Server2005 或更新的版本,有一个更为现代的 try/catch 结构的优势来实现上面的示例。(GOTO 语句毕竟非议颇多)
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin try
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
update Accounts set balance = balance - @Amount where AccountID = @ID_B
-- 出错会进入 catch 块,这里可以直接提交
commit
end try
begin catch
if(@@trancount > 0)
rollback
-- 记录异常信息
declare @ErrMsg nvarchar(4000),@ErrSeverity int
select @ErrMsg = ERROR_MESSAGE(),@ErrSeverity = ERROR_SEVERITY()
raiserror(@ErrMsg,@ErrSeverity,1)
end catch
这个示例检查 @@trancount 以确定是否有事务在进行中。(变量 @@trancount 计算当前连接中进行的事务数,begin transaction 语句使之加1,而 rollback 和 commit 使之减1 。)
为了防止错误被 catch 块吞噬,使用了 raiserror 语句。 ADO.NET 把该消息封装成 SqlException 对象,后者需要你在 .NET 中进行捕获。
2. 客户端引发的 ADO.NET 事务
大多数 ADO.NET 数据提供程序都提供对数据库事务的支持。
Transaction 类有两个关键方法:
- Commit()
- Rollback()
下面这个示例演示向 Employees 表中插入两条记录:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
SqlCommand cmd1 = new SqlCommand("insert into Employees(LastName,FirstName) values('Joe','Tester')", conn);
SqlCommand cmd2 = new SqlCommand("insert into Employees(LastName,FirstName) values('Harry','Sulivan')", conn);
SqlTransaction tran = null;
try
{
// Open the connection and create the transaction
conn.Open();
tran = conn.BeginTransaction();
// Enlist two commands in the transaction
cmd1.Transaction = tran;
cmd2.Transaction = tran;
// Execute both commands
cmd1.ExecuteNonQuery();
cmd2.ExecuteNonQuery();
// Commit the transaction
tran.Commit();
}
catch
{
// In the case of error,rool back the transaction
tran.Rollback();
}
finally
{
conn.Close();
}
}
仅仅创建和提交事务是不够的,一定要设置 Command.Transaction 属性为 Transaction 对象从而把 Command 对象显式的列入事务中,事务进行中执行一个不在当前事务中的命令,会产生错误。
事务的隔离级别
隔离级别决定了事务对其他事务影响的数据的敏感度。默认情况下,当两个事务独立运行时,在第一个事务结束前,第一个事务插入的数据对其他事务不可见。
隔离级别的概念和锁的概念紧密相关,因为确定事务的隔离级别就是确定所需的锁的类型。
- 共享锁:是事务读取数据库中的数据时产生的锁。当表,行,或某个范围内有共享锁时,其他事务就不可用修改相应的数据,但多个用户可以使用共享锁并发读取数据。
- 独占锁:禁止多个事务同时修改数据。当事务更新数据且没有其他事物已经锁定数据的同时,会产生独占锁。当有独占锁时,其他用户不能读取或更新数据。
在 SQL Server 存储过程中,使用 SET TRANSACTION ISOLATION LEVEL 命令来设置隔离级别。在 ADO.NET 中,可向 Connection.BeginTransaction()方法传入 IsolationLevel 枚举值,见下表:
ReadUncommitted
无共享锁,也不会有独占锁。会导致脏数据读,但可以提升性能。
ReadCommitted
数据被事务读时会产生共享锁,避免了脏数据读,但数据在事务结束前可能已经被修改,这样可能会产生非重复读或虚幻行。(SQL Server 默认隔离级别)
Snapshot
保存一份事务正在访问的数据的副本,因此一个事务不会看到其他事务所做的修改。这个隔离级别减少了堵塞,因为当其他事务正在读取被快照隔离事务锁锁定的数据时,它可以从数据副本中读取数据。该选项仅被 SQL Server 2005 支持,而且需要通过数据库级别的设定才能够启用。
RepeatableRead
查询中涉及的所有数据均被加上共享锁。这样避免了他人修改数据,同时也避免了不可重复的读,还是可能会出现虚幻行。
Serializable
在所使用的数据上的一系列锁禁止了其他用户在该范围内更新或插入行。它是唯一可以删除虚幻行的隔离级别,但是给并发访问带来非常消极的影响,多用户场景中很少使用。
相关的数据库术语:
- 脏读:读取了其他尚未提交的事务中的数据,但该事务可能被回滚。
- 不可重复读:如果允许不可重复读,那么在同一个事务中执行多次查询可能会得到不同的数据。这是因为事务在进行过程中只读取数据并不能阻止其他用户修改数据。为了防止不可重复读,数据库服务器需要锁定事务读取的行。
- 虚幻行:指在初始读取中没有出现但在相同事务内后续读取时出现的行。事务进行过程中其他用户插入了记录,就可能出现幻行。为了防止幻行,事务进行过程中查询数据库时要根据 WHERE 子句使用一个范围锁。
这些现象究竟是无害的小缺陷还是潜在的错误取决于你的特定需求。大多数情况下,不可重复读和幻行只是小问题,使用锁阻止它们发生并发的代价有点太高了,不太值得。ReadCommitted 对于大多数事务都适用。看一下不同隔离级别的对比:
隔离级别
脏 读
不可重复读
虚幻数据
并发性
未提交读(Read uncommitted)
是
是
是
最佳
提交度(Read committed)
否
是
是
好
快照(Snapshot)
否
否
否
好
重复读(Repeatable read)
否
否
是
一般
序列化(Serializable)
否
否
否
最差
事务是一组必须全部成功或者全部失败的操作。事务的目标是保证数据总能处于有效一致的状态。例如,转账操作。
事务有 4 个被称为 ACID 属性的特征,ACID 是以下概念的缩写:
- Atomic(原子性):事务中所有步骤必须同时成功或失败
- Consist(一致性):事务使底层数据库在稳定状态间转换
- Isolated(隔离性):每个事务都是独立的实体,一个事务不应该影响同时运行的其他事物。
- Durable(持久性):在事务成功前,事务产生的变化永久的存储在媒质上,同时也必须维护日志以保证出现硬件故障数据库也能得以恢复。
这些事事务的理想特征,它们未必总能达到。执行事务时 RDBMS 需要锁定数据,这样其他用户就不能访问它了。锁越多,粒度就越大,执行事务时其他用户就更不可能完成某些任务。也就是说,需要在用户并发性和隔离性间做出权衡。
事务和 ASP.NET 应用程序
ASP.NET 可以使用 3 种基本类型的事务:
- 存储过程事务:这些事务完全在数据库中处理,存储过程事务提供最佳的性能,因为只需往返一次数据库。缺点是需要用 SQL 语句编写事务处理逻辑。
- 客户端引发(ADO.NET)的事务:这些事务由代码通过编程来控制。它们使用和存储过程事务一样的命令,代码使用了封装这些细节的 ADO.NET 对象。缺点是在事务开始和提交时需要额外往返数据库。
- COM+ 事务:COM+ 采用两步提交协议,总会带来额外开销。只有当事务需要跨越多个资源管理器的时候才需要使用 COM+事务。一个COM+可以跨越 SQL Server 及 Oracle 数据库中的交互。
尽管ADO.NET提供对事务的良好支持,但还是不应该随便使用。每次使用都会为系统带来额外的负担。另外,事务会锁定表的某些行,不必要的事务会损害你的应用程序性能。
使用事务应遵循这些实践原则以获得最佳效果:
- 事务要尽量短
- 不要在事务中间使用 SELECT 查询返回数据
- 如果事务中确实要获取记录,应该只获取确实需要的记录,这样可以减少锁定的资源数
- 可能的情况下,在存储过程中使用事务,而不是在 ADO.NET 中使用事务。这样的事务可以被更快的启动和编译,因为数据库服务器不需要与客户端(Web 应用程序)交互。
- 避免使用具有多个独立批处理任务的事务,把各个批处理任务作为单个事务
- 尽量避免影响大批量记录的更新
1. 存储过程事务
只要可能,事务存放的最佳地点是存储过程代码。它最有可能获得最佳性能,所有的活动在数据源进行而不需要任何网络通信。总之,事务的跨度越短,数据库的并发性越好,被序列化的数据库请求就越少。
下面这段伪代码演示了如何在两个账户间转移资金,它是一个简化的版本,允许账户村矿为负数:
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
if(@@error > 0)
GOTO Problem
update Accounts set balance = balance - @Amount where AccountID = @ID_B
if(@@error > 0)
GOTO Problem
-- 没有错误
commit
return
-- 有错误
Problem:
rollback
raiserror('Could not update.',16,1)
当在 Transact-SQL 中使用 @@error 值时,必须在每步操作完成后立即检查该值!!!因为 @@error 在一条 SQL 语句成功执行后自动被重设为 0 。
如果使用 SQL Server2005 或更新的版本,有一个更为现代的 try/catch 结构的优势来实现上面的示例。(GOTO 语句毕竟非议颇多)
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin try
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
update Accounts set balance = balance - @Amount where AccountID = @ID_B
-- 出错会进入 catch 块,这里可以直接提交
commit
end try
begin catch
if(@@trancount > 0)
rollback
-- 记录异常信息
declare @ErrMsg nvarchar(4000),@ErrSeverity int
select @ErrMsg = ERROR_MESSAGE(),@ErrSeverity = ERROR_SEVERITY()
raiserror(@ErrMsg,@ErrSeverity,1)
end catch
这个示例检查 @@trancount 以确定是否有事务在进行中。(变量 @@trancount 计算当前连接中进行的事务数,begin transaction 语句使之加1,而 rollback 和 commit 使之减1 。)
为了防止错误被 catch 块吞噬,使用了 raiserror 语句。 ADO.NET 把该消息封装成 SqlException 对象,后者需要你在 .NET 中进行捕获。
2. 客户端引发的 ADO.NET 事务
大多数 ADO.NET 数据提供程序都提供对数据库事务的支持。
Transaction 类有两个关键方法:
- Commit()
- Rollback()
下面这个示例演示向 Employees 表中插入两条记录:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
SqlCommand cmd1 = new SqlCommand("insert into Employees(LastName,FirstName) values('Joe','Tester')", conn);
SqlCommand cmd2 = new SqlCommand("insert into Employees(LastName,FirstName) values('Harry','Sulivan')", conn);
SqlTransaction tran = null;
try
{
// Open the connection and create the transaction
conn.Open();
tran = conn.BeginTransaction();
// Enlist two commands in the transaction
cmd1.Transaction = tran;
cmd2.Transaction = tran;
// Execute both commands
cmd1.ExecuteNonQuery();
cmd2.ExecuteNonQuery();
// Commit the transaction
tran.Commit();
}
catch
{
// In the case of error,rool back the transaction
tran.Rollback();
}
finally
{
conn.Close();
}
}
仅仅创建和提交事务是不够的,一定要设置 Command.Transaction 属性为 Transaction 对象从而把 Command 对象显式的列入事务中,事务进行中执行一个不在当前事务中的命令,会产生错误。
事务的隔离级别
隔离级别决定了事务对其他事务影响的数据的敏感度。默认情况下,当两个事务独立运行时,在第一个事务结束前,第一个事务插入的数据对其他事务不可见。
隔离级别的概念和锁的概念紧密相关,因为确定事务的隔离级别就是确定所需的锁的类型。
- 共享锁:是事务读取数据库中的数据时产生的锁。当表,行,或某个范围内有共享锁时,其他事务就不可用修改相应的数据,但多个用户可以使用共享锁并发读取数据。
- 独占锁:禁止多个事务同时修改数据。当事务更新数据且没有其他事物已经锁定数据的同时,会产生独占锁。当有独占锁时,其他用户不能读取或更新数据。
在 SQL Server 存储过程中,使用 SET TRANSACTION ISOLATION LEVEL 命令来设置隔离级别。在 ADO.NET 中,可向 Connection.BeginTransaction()方法传入 IsolationLevel 枚举值,见下表:
ReadUncommitted | 无共享锁,也不会有独占锁。会导致脏数据读,但可以提升性能。 |
ReadCommitted | 数据被事务读时会产生共享锁,避免了脏数据读,但数据在事务结束前可能已经被修改,这样可能会产生非重复读或虚幻行。(SQL Server 默认隔离级别) |
Snapshot | 保存一份事务正在访问的数据的副本,因此一个事务不会看到其他事务所做的修改。这个隔离级别减少了堵塞,因为当其他事务正在读取被快照隔离事务锁锁定的数据时,它可以从数据副本中读取数据。该选项仅被 SQL Server 2005 支持,而且需要通过数据库级别的设定才能够启用。 |
RepeatableRead | 查询中涉及的所有数据均被加上共享锁。这样避免了他人修改数据,同时也避免了不可重复的读,还是可能会出现虚幻行。 |
Serializable | 在所使用的数据上的一系列锁禁止了其他用户在该范围内更新或插入行。它是唯一可以删除虚幻行的隔离级别,但是给并发访问带来非常消极的影响,多用户场景中很少使用。 |
相关的数据库术语:
- 脏读:读取了其他尚未提交的事务中的数据,但该事务可能被回滚。
- 不可重复读:如果允许不可重复读,那么在同一个事务中执行多次查询可能会得到不同的数据。这是因为事务在进行过程中只读取数据并不能阻止其他用户修改数据。为了防止不可重复读,数据库服务器需要锁定事务读取的行。
- 虚幻行:指在初始读取中没有出现但在相同事务内后续读取时出现的行。事务进行过程中其他用户插入了记录,就可能出现幻行。为了防止幻行,事务进行过程中查询数据库时要根据 WHERE 子句使用一个范围锁。
这些现象究竟是无害的小缺陷还是潜在的错误取决于你的特定需求。大多数情况下,不可重复读和幻行只是小问题,使用锁阻止它们发生并发的代价有点太高了,不太值得。ReadCommitted 对于大多数事务都适用。看一下不同隔离级别的对比:
隔离级别 |
脏 读 |
不可重复读 |
虚幻数据 |
并发性 |
未提交读(Read uncommitted) | 是 |
是 |
是 |
最佳 |
提交度(Read committed) | 否 |
是 |
是 |
好 |
快照(Snapshot) | 否 |
否 |
否 |
好 |
重复读(Repeatable read) | 否 |
否 |
是 |
一般 |
序列化(Serializable) | 否 |
否 |
否 |
最差 |
数据提供程序无关的代码
创建工厂
工厂模型的基本思想是借助一个单一的工厂对象创建所有需要使用的提供程序相关的对象。然后就可以通过一组共同的基类以完全通用的方式与这些提供程序相关的对象交互。
有一个动态发现并创建你需要的工厂的标准类,这个类是 System.Data.Common.DbProviderFactories 。它提供了一个 GetFactory()方法,这个方法根据提供程序的名字返回相应的工厂。
string factory = "System.Data.SqlClient";
DbProviderFactory provider = DbProviderFactories.GetFactory(factory);
所有的工厂都是继承自 DbProviderFactory 的,如果你只是用 DbProviderFactory 的成员,你编写的代码就可以喝其他工厂一起正常使用。前面代码的不足在于传递的提供程序名称的字符串,通常这应该配置在 web.config 中。
要使 DbProviderFactory 类正常工作,提供程序要有一个在 machine.config 或 web.config 中注册过的工厂。
用工厂创建对象
再次强调,你必须假设你不知道正在使用的提供程序,这样你就只能通过标准的基类来与工厂所创建的对象交互。一个简单的示例可以帮助你更好的裂解这些零碎的概念是如何协作的。
在 web.config 文件中为示例配置 连接字符串,提供程序名称,以及查询语句:
<configuration>
<connectionStrings>
<add name="Northwind" connectionString="Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI"/>
</connectionStrings>
<appSettings>
<add key="factory" value="System.Data.SqlClient" />
<add key="employeeQuery" value="select * from employees" />
</appSettings>
</configuration>
基于工厂的代码:
protected void EmployeesQuery()
{
// Get the factory
string factory = WebConfigurationManager.AppSettings["factory"];
DbProviderFactory provider = DbProviderFactories.GetFactory(factory);
// Use this factory to create a connection
DbConnection conn = provider.CreateConnection();
conn.ConnectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
// Create the command
DbCommand cmd= provider.CreateCommand();
cmd.Connection = conn;
cmd.CommandText = WebConfigurationManager.AppSettings["employeeQuery"];
// Open the connection and get the DataReader
conn.Open();
DbDataReader reader = cmd.ExecuteReader();
} 如果在数据库发生更换的情况下,只需要修改配置文件的数据提供程序名称这一行即可