附A:事务简介
一个事务是一系列动作作为单个逻辑单位来执行,这意味着这些动作要么全部成功,要么全部失败。如果最后一个动作失败了,那么以前的动作应该依次回滚,整个状态回到原先事务开始的状态。例如,有1000美圆从一个银行帐户扣除,转入另外一个帐户,那么事务保证两个事件只能一起成功,只要有一个失败,那么实际就不会发生任何动作,两个帐户没有发生任何改变,钱没有被存入和扣除。
ACID属性
有四个属性经常被用来描述事务:原子性(atomicity), 一致性(consistency), 隔离性(isolation), 持久性(durability)。在一个理想的环境里,事务符合这四个标准,但是一个事务的隔离级别是可以设置的,原因是设置一个事物的隔离级别为最佳的“可序列化”会增加一种被称为“死锁“情况发生的机率。死锁是两个事务试图去锁住已经被对方锁住的资源,最后一个事务需要回滚,这样另外一个才得以继续。为了避免这样的情况发生,你要求降低一个事务的隔离级别来减低它的争夺力。
◆ 原子性:事务要作为一个原子单位工作,要不全部执行成功,要么全部失败
◆ 一致性:事务结束后需要保持所有数据的一致性,在一个关系数据库里,所有的规则要发生在事务引起的变动上,来保持数据的一致性,所有的内部数据结构,如B-tree索引、双向联结必须在事务结束后保持正确。
◆ 隔离性:并发事务所作出的变动必须和其他任何并发事务做的变更相隔离。一个事务要么看到其他事务更改它之前的数据,要么看到其他事务更改之后的数据,它不能看到临时的数据,。。。。。。
◆ 持久性:一个事务结束后,它的结果会持久地存在与系统中,它的变动甚至在系统瘫痪时候也会存在。
事务支持级别
表2:事务支持级别
对于Asp.net,页面的默认事务级别是“隔绝”,在企业级服务里面默认级别是“需要”,一般地,我们会在需要事务的第一层对象上设置级别为“需要”,被调用的对象设置为“支持”。这意味着顶层的对象会创建一个事务,每个被调用的对象被激活时,将进入这个事务的上下文中。
事务隔离级别
在一个程序中,依据事务的隔离级别将会有三种情况发生。
◆ 脏读:一个事务会读进还没有被另一个事务提交的数据,所以你会看到一些最后被另一个事务回滚掉的数据。
◆ 读值不可复现:一个事务读进一条记录,另一个事务更改了这条记录并提交完毕,这时候第一个事务再次读这条记录时,它已经改变了。
◆ 幻影读:一个事务用Where子句来检索一个表的数据,另一个事务插入一条新的记录,并且符合Where条件,这样,第一个事务用同一个where条件来检索数据后,就会多出一条记录。
表3:事务隔离级别
一个事务所选择的隔离级别影响着数据库怎样对待其他事务拥有的锁,你所选择的隔离级别依赖于你的系统和商务逻辑。例如,一个银行在客户取钱之前会检查它的帐户余额,这种情况下,就需要一个隔离级别为可序列化的事务,这样另外一个取钱动作在这次完成之前将不能执行。如果它们仅需要提供一个帐户余额,“读提交的“将是合适的级别,因为他们仅需要查询余额的值,级别的降低会使事务运行更快。
附B 事务开发的建议
◆ 在一定负荷下面测试事务控制器来保证负荷下死锁不会发生
◆ 尽可能使用存储过程,它们在各种情况仍拥有最好的性能
◆ 企业级服务为事务的控制提供了一个高级的方式,但使用要谨慎,因为它使性能降低
◆ 分析你的系统,来决定使用哪一种隔离级别的事务。银行系统应该使用可序列化的隔离级别
附C 监控事务
没有一种特别的机制来追踪数据库事务和ADO.NET事务。
ASP.NET和企业级服务的事务可以被控制台的组件服务程序来监控,组件服务在开始菜单的管理工具中可以找到。从这个工具中,你可以监控所有通过MS DTC来运行的事务,也就是ASP.NET和企业级服务的事务。你也可以监视事务的执行时间,甚至事务提交、回滚、取消的整个过程。
附D 死锁
当两个或两个以上进程(或线程)互相拥有对方需要的锁,就会发生死锁,一般是在数据库系统中,但也可能发生在任何的多线程程序中,比如:
在这个时刻,两个事务都不能继续进行,因为它们都试图锁定对方已经锁定的资源,并且根据事务的规则,两个事务都不能得知对方的状态。Sql server会检测到这种情况的发生,会选择其中的一个回滚,这样起码有一个事务可以正常完成了。在这个时刻,控制哪个事务被牺牲的程序不得不决定是重启这个事务还是抛出错误给用户。一般地,Sql Server会牺牲那个后开始的事务,或者是可以解开最多死锁的那个事务。
死锁中的一个问题是数据库会自动做一个回滚,这样会引起中间层的回滚代码里的一个问题,如果你遇到一个死锁,并且在中间层可以截获异常,当中间层进行回滚的时候会得到另外一个异常,这样你就产生了第二个异常,这是因为Sql Server已经为你做了一次回滚,所以你实际上是进行了第二次回滚,这时Sql Server却找不到可以回滚的事务。所以一个良好的习惯是把回滚的代码放到一个Try Catch里面,或者把这个异常匹配到SqlException,并且检测SqlException异常的代码,如果是1205,那么就是死锁的情况发生了。
例如:
try
{
// Run SQL Commands in ADO.NET transaction
catch (SqlException sqlex)
{
// Specific catch for deadlock
if (sqlex.Number != 1205)
{
tx.Rollback();
}
orderId = 0;
throw(sqlex);
}
避免死锁的方法
有几个方法可以降低事务发生死锁的几率
◆ 试图在事务中用同样的顺序去访问表,在前面的例子中,如果两个事务都先访问表A后访问表B,那么就可以避免死锁,两个事务都能成功。
◆ 避免在事务中有用户参与/尽量是事务短小,运行时间长的事务会增加和其他事务发生死锁的机会,在存储过程中运行事务可能会用最短的时间。
◆ 用一个低的隔离级别,把你的事务的隔离级别设置为最低会使你的业务降低发生死锁的风险,缺省的,Sql Server把隔离级别设置为“读取提交的”,这意味着如果一个事务为了读而锁定一些数据,另外一个事务允许锁定它去读、写、删除和更改,这样会产生“幻影读“的情况,但在一般的业务中是可以接受的。
◆ 使用bound connections,在这种条件下,两个连接共享一个事务/锁空间,从而避免锁的冲突。