本文转自:
在C#程序设计中我们通常在try语句块中进行数据库操作,所有我们这里就将事务的启动与结束设置在try中数据库操作的前后,而在catch异常处理中使用回滚(RollBack)动作。从而保证一旦对数据库失败,则回滚到初始状态。
事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性:
◆原子性
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
◆一致性
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
◆隔离性
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
◆持久性
C#数据库事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
示例:
使用到的数据库、表以及存储过程如下:
-- 创建数据库Test和工资表tb_Salary
CREATE DATABASE Test
CREATE TABLE tb_Salary
(
salID INT IDENTITY(1,1) PRIMARY KEY NOT NULL, -- 编号
userID INT NOT NULL,
userName NVARCHAR(20) NOT NULL, -- 员工姓名
salary MONEY NOT NULL -- 目前工资
)
GO
-- 向表中插入两条数据
INSERT dbo.tb_Salary VALUES(1001,'张三',3600);
INSERT dbo.tb_Salary VALUES(1002,'李四',4000);
go
-- 指定用户编号的员工增减指定的工资
CREATE PROC sp_PlusSalary
@salary MONEY, -- 增量
@userID INT -- 员工编号
AS
UPDATE dbo.tb_Salary SET salary=salary+@salary WHERE userID=@userID
go
CREATE PROC sp_SubtractSalary
@salary MONEY, -- 减量
@userID INT, -- 员工编号
@fail INT OUTPUT -- 返回参数
AS
IF ((SELECT salary FROM dbo.tb_Salary WHERE @salary">userID=@userID)>@salary OR (SELECT salary FROM dbo.tb_Salary WHERE userID=@userID)=@salary)
BEGIN
SET @fail=1;
UPDATE dbo.tb_Salary SET salary=salary-@salary WHERE userID=@userID
END
ELSE
BEGIN
SET @fail=0;
END
go
C#代码示例如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
namespace Class1125
{
/// <summary>
/// 事务
/// </summary>
class Tran
{
private static void Main(string[] args)
{
// 实例化类
Tran tr = new Tran();
tr.PlusSubtractSalary();
Console.Read();
}
/// <summary>
/// 增减工资
/// </summary>
public void PlusSubtractSalary()
{
// 指定转账的金额
decimal money = 2000;
// 记录事务过程状态,如果在事务过程中出现错误failure为false,否则为true
bool failure = true;
// 数据库连接字符串
string str = @"Data Source=LENOVO-943EB9E1\SQLEXPRESS;Initial Catalog=Test;Integrated Security=True";
SqlConnection con = new SqlConnection(str);
// 在事务进行之前必须先打开数据库连接
con.Open();
// 调用BeginTransaction方法开始一个事务
SqlTransaction tran = con.BeginTransaction();
// 构建一个SqlCommand对象
SqlCommand cmd1 = new SqlCommand();
// 指定连接数据库的链接
cmd1.Connection = con;
// 指定存储过程的名称
cmd1.CommandText = "sp_SubtractSalary";
// 说明执行的是存储过程而非SQL语句
cmd1.CommandType = System.Data.CommandType.StoredProcedure;
// 通过指定存储过程的变量
SqlParameter para1 = cmd1.Parameters.AddWithValue("@salary", money);
SqlParameter para2 = cmd1.Parameters.AddWithValue("@userID", 1001);
// @fail参数是返回参数,同时赋初始值0
SqlParameter para3 = cmd1.Parameters.AddWithValue("@fail", 0);
// 说明@fail参数的数据类型,这个不是必须的,可以不指定。
para3.SqlDbType = System.Data.SqlDbType.Int;
// 因为@fail参数是返回输出参数,所以这里必须把Direction属性指定为ParameterDirection.Output
para3.Direction = System.Data.ParameterDirection.Output;
// 把事物对象赋值给命令对象的Transaction属性,这样当执行该命令时就会把该命令作为事务的一部分来执行
cmd1.Transaction = tran;
//构建第二个SqlCommand对象
SqlCommand cmd2 = new SqlCommand();
cmd2.Connection = con;
// 指定存储过程的名称
cmd2.CommandText = "sp_PlusSalary";
cmd2.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter para4 = cmd2.Parameters.AddWithValue("@salary", money);
SqlParameter para5 = cmd2.Parameters.AddWithValue("@userID", 1002);
// 把事物对象赋值给命令对象的Transaction属性,这样当执行该命令时就会把该命令作为事务的一部分来执行
cmd2.Transaction = tran;
try
{
// 定义一个整形变量记录命令执行影响的行数(执行ExecuteNonQuery命令返回的是数据库表中受影响的行数)
// 非查询命令使用ExecuteNonQuery
int count2 = cmd1.ExecuteNonQuery();
// 定义一个整形变量接受存储过程的返回参数,使用参数的Value获取
int count1 = (int)para3.Value;
// 检测受影响行数是否为1,是则命令正确执行,否则命令没有正确执行
// 检查存储过程返回参数的值是否为1,是则存储过程正常执行,否则余额不足,存储过程没有正常执行
if (count2 != 1 || count1 ==0)
{
if (count1 == 0)
{
Console.WriteLine("余额不足!{0}", count1);
}
// 记录事务过程状态为
failure = false;
// 命令没有正确执行则事务回滚到最初始状态
tran.Rollback();
return;
}
// 执行第二个命令,检查受影响的行
//检查存储过程返回参数的值是否为1,是则存储过程正常执行,否则余额不足,存储过程没有正常执行
count2 = cmd2.ExecuteNonQuery();
if (count2 != 1 || count1 == 0)
{
failure = false;
tran.Rollback();
return;
}
}
catch (Exception ex)
{
failure = false;
Console.WriteLine("异常消息:{0}", ex);
// 如果出现异常,事务回滚到最初始状态
tran.Rollback();
}
finally
{
// 如果命令全部正确执行,则提交事务
if (failure)
{
tran.Commit();
Console.WriteLine("命令成功执行");
}
else
{
Console.WriteLine("命令执行过程中遇到问题,事务终止!");
}
// 释放资源,关闭连接
con.Close();
cmd1.Dispose();
cmd2.Dispose();
}
}
}
}
上述代码运行第一次之后的结果如下:
注:因为在第一次运行程序之前张三账户上是3600,李四账户账户上是3000,张三的账户余额大于2000(2000是我们给程序事先设定的),运行程序一次之后从张三的账户上减去2000,在李四的账户上加上2000。这样可以执行。
上述代码运行第二次之后的结果如下:
注:在运行了一次程序之后再第二次再运行程序时,这时张三的账户上是1600,小于2000,所以当第二次运行程序时存储过程sp_SubtractSalary返回的参数@fail的值是0,这时存储过程中的UPDATA语句没有执行,所以事务回滚到最初始状态。