SQL Server创建事务——锁

前面了解了用C#创建事物以及一些特性,下面看看用SQL Server来创建,使用事物。

 

事务定义:

事务是作为单个逻辑单元执行的一系列操作,它是一个不可分割的工作逻辑单元。它包含了一组数据库操作命令,这组命令要么全部执行,要么全部不执行。

举个例子,我们经常用到的 ATM 存取款机,比如转账的时候,是先减去转出账户的金额,然后再在指定转入账户的金额加上转出的金额。如果刚好这个时候转出的操作已经执行完成,但是由于系统的故障,导致转入的操作失败了。那么怎么办?这就需要用到事务了,只要事务里面有一条命令未成功执行,那么数据就会回滚到事务开始之前的状态。

事务特性:

1、原子性(Atomicity):事务是一个完整的操作, 事务中所有操作命令必须作为一个整体提交或回滚。如果事务中任何操作命令失败,则整个事务将因失败而回滚。

2、一致性(Consistency):当事务完成时,数据都处于一致状态。

3、隔离性(Isolation): 对数据进行修改的所有并发事务是彼此隔离的,它不以任何方式依赖或影响其他事务。

4、持久性(Durability):事务提交之后,数据是永久性的,不可再回滚。

事务操作:

1、begin transaction:开始事务。

2、commit transaction:提交事务。

3、rollback transaction:回滚事务。

4、save transaction:事务保存点。即事务回滚时,可以指定回滚到保存点,而不进行全部回滚。

事务分类:

1、显式事务:用 begin transaction 明确指定事务的开始,由 commit transaction 提交事务、rollback transaction 回滚事务到事务结束。

2、隐式事务:通过设置 set implicit_transactions on 语句,将隐式事务模式设置为打开。当以隐式事务模式操作时,不必使用 begin transaction 开启事务,当一个事务结束后,这个模式会自动启用下一个事务,只需使用 commit transaction 提交事务或 Rollback Transaction 回滚事务即可。

3、自动提交事务: 这是 SQL Server 的默认模式,它将每条单独的 T-SQL 语句视为一个事务。如果成功执行,则自动提交。如果错误,则自动回滚。

 

先看一下数据:

SQL Server创建事务——锁_第1张图片

SQL Server创建事务——锁_第2张图片

begin tran        -- 开启事务,transcation 的简写
declare @errorNo int    --定义变量,用于记录事务执行过程中的错误次数
set @errorNo=0
begin try
    update Student set C_S_Id='2' where S_StuNo='003'
    set @errorNo=@errorNo+@@ERROR
    select 'S_StuNo=003 已经修改啦'

    update Student set C_S_Id='3' where S_StuNo='002' 
    set @errorNo=@errorNo+@@ERROR            -- @@ERROR 系统全局变量,记录错误次数,出现一次错误 @@ERROR 值+1
    select 'S_StuNo=002 已经修改啦'

    if(@errorNo>0)
    begin
        --抛出自定义的异常,在最后的catch块中统一处理异常
        RAISERROR(233333,16,3)
    end

end try
begin catch
    select ERROR_NUMBER() errorNumber,        --错误代码
           ERROR_SEVERITY() errorSeverity,    --错误严重级别,级别小于10 try catch 捕获不到
           ERROR_STATE() errorState,        --错误状态码
           ERROR_PROCEDURE() errorProcedure,    --出现错误的存储过程或触发器的名称
           ERROR_LINE() errorLine,        --发生错误的行号
           ERROR_MESSAGE() errorMessage        --错误的具体信息

    if(@@trancount>0)    -- @@trancount 系统全局变量,事务开启 @@trancount 值+1,判断事务是否开启
    begin
        rollback tran;        -- 回滚事务
    end
end catch

if(@@trancount>0)
begin
    commit tran;        -- 提交事务
end

select * from Student

 

SQL Server创建事务——锁_第3张图片

这里由于外键约束的原因,所以第二条 update 语句导致失败,以上结果可以看出第一条数据肯定是执行过了,但是在 catch 语句里面回滚了,所以数据还是原来的状态。

现在只需要把第二条 update 语句 C_S_Id 列的值改为 5 即可。

begin tran        -- 开启事务,transcation 的简写
declare @errorNo int    --定义变量,用于记录事务执行过程中的错误次数
set @errorNo=0
begin try
    update Student set C_S_Id='2' where S_StuNo='003'
    set @errorNo=@errorNo+@@ERROR
    select 'S_StuNo=003 已经修改啦'

    update Student set C_S_Id='5' where S_StuNo='002' 
    set @errorNo=@errorNo+@@ERROR            -- @@ERROR 系统全局变量,记录错误次数,出现一次错误 @@ERROR 值+1
    select 'S_StuNo=002 已经修改啦'

    if(@errorNo>0)
    begin
        --抛出自定义的异常,在最后的catch块中统一处理异常
        RAISERROR(233333,16,3)
    end

end try
begin catch
    select ERROR_NUMBER() errorNumber,        --错误代码
           ERROR_SEVERITY() errorSeverity,    --错误严重级别,级别小于10 try catch 捕获不到
           ERROR_STATE() errorState,        --错误状态码
           ERROR_PROCEDURE() errorProcedure,    --出现错误的存储过程或触发器的名称
           ERROR_LINE() errorLine,        --发生错误的行号
           ERROR_MESSAGE() errorMessage        --错误的具体信息

    if(@@trancount>0)    -- @@trancount 系统全局变量,事务开启 @@trancount 值+1,判断事务是否开启
    begin
        rollback tran;        -- 回滚事务
    end
end catch

if(@@trancount>0)
begin
    commit tran;        -- 提交事务
end

select * from Student

SQL Server创建事务——锁_第4张图片

关于 RAISERROR 自定义抛出异常可以看这里:http://www.cnblogs.com/Brambling/p/6687068.html

 

设置 xact_abort:

设置 xact_abort on/off , 指定是否回滚当前事务,为 on 时如果当前 sql 出错,回滚整个事务,为 off 时如果 sql 出错回滚当前 sql 语句,其它语句照常运行读写数据库。

如果在存储过程开始处将set xact_abort on,那么当有错误发生时,系统会将当前事务置为不可提交状态,即会将xact_state()置为-1,此时只可以对事务进行rollback操作,不可进行提交commit操作。

那么我们在catch语句块中就可以根据xact_state()的值来判断是否有事务处于不可提交状态,如果有则可以进行rollback操作了。

如果在存储过程开始处将set xact_abort off,那么当有错误发生时,系统不会将xact_state()置为-1,那么我们在catch块中就不可以根据该函数值来判断是否需要进行rollback了。

xact_state()有三种值:-1表示事务不可提交;1表示事务可提交;0表示没有事务,此时commit或者rollback会报错。

但是我们可以根据@@Trancount全局变量来判断,如果在catch块中判断出@@Trancount数值大于0,代表还有未提交的事务,既然进入catch语句块了,那么还存在未提交的事务,该事务应该是需要rollback的,但是这种方法在某些情况下可能判断的不准确。

xact_abort 只对运行时出现的错误有用。

set xact_abort off

begin tran        -- 开启事务,transcation 的简写
declare @errorNo int    --定义变量,用于记录事务执行过程中的错误次数
set @errorNo=0
begin try
    update Student set C_S_Id='2' where S_StuNo='003'
    set @errorNo=@errorNo+@@ERROR
    select 'S_StuNo=003 已经修改啦'

    update Student set C_S_Id='3' where S_StuNo='002' 
    set @errorNo=@errorNo+@@ERROR            -- @@ERROR 系统全局变量,记录错误次数,出现一次错误 @@ERROR 值+1
    select 'S_StuNo=002 已经修改啦'

    if(@errorNo>0)
    begin
        --抛出自定义的异常,在最后的catch块中统一处理异常
        RAISERROR(233333,16,3)
    end

end try
begin catch
    select ERROR_NUMBER() errorNumber,        --错误代码
           ERROR_SEVERITY() errorSeverity,    --错误严重级别,级别小于10 try catch 捕获不到
           ERROR_STATE() errorState,        --错误状态码
           ERROR_PROCEDURE() errorProcedure,    --出现错误的存储过程或触发器的名称
           ERROR_LINE() errorLine,        --发生错误的行号
           ERROR_MESSAGE() errorMessage        --错误的具体信息

    if(@@trancount>0)    -- @@trancount 系统全局变量,事务开启 @@trancount 值+1,判断事务是否开启
    begin
        rollback tran;        -- 回滚事务
    end
end catch

select * from Student

SQL Server创建事务——锁_第5张图片

xact_abort 设置为 off 时,虽然也出现了异常,但是可以看出第一条数据还是修改了,并没有回滚。因为它只是回滚出错的 sql 语句,并不全部回滚。

set xact_abort on

begin tran        -- 开启事务,transcation 的简写
declare @errorNo int    --定义变量,用于记录事务执行过程中的错误次数
set @errorNo=0
begin try
    update Student set C_S_Id='2' where S_StuNo='003'
    set @errorNo=@errorNo+@@ERROR
    select 'S_StuNo=003 已经修改啦'

    update Student set C_S_Id='3' where S_StuNo='002' 
    set @errorNo=@errorNo+@@ERROR            -- @@ERROR 系统全局变量,记录错误次数,出现一次错误 @@ERROR 值+1
    select 'S_StuNo=002 已经修改啦'

    if(@errorNo>0)
    begin
        --抛出自定义的异常,在最后的catch块中统一处理异常
        RAISERROR(233333,16,3)
    end

end try
begin catch
    select ERROR_NUMBER() errorNumber,        --错误代码
           ERROR_SEVERITY() errorSeverity,    --错误严重级别,级别小于10 try catch 捕获不到
           ERROR_STATE() errorState,        --错误状态码
           ERROR_PROCEDURE() errorProcedure,    --出现错误的存储过程或触发器的名称
           ERROR_LINE() errorLine,        --发生错误的行号
           ERROR_MESSAGE() errorMessage        --错误的具体信息

    if(@@trancount>0)    -- @@trancount 系统全局变量,事务开启 @@trancount 值+1,判断事务是否开启
    begin
        rollback tran;        -- 回滚事务
    end
end catch

select * from Student

SQL Server创建事务——锁_第6张图片

xact_abort 设置为 on 时,出现了异常,回滚整个事务。

 

事务死锁:

打开两个查询窗口,把下面的语句,分别放入2个查询窗口,在5秒内运行2个事务模块。

begin tran 
  update Student set C_S_Id='2' where S_StuNo='002'

  waitfor delay '0:0:5'

  update Student set C_S_Id='5' where S_StuNo='003'
commit tran

select * from Student
begin tran 
  update Student set C_S_Id='5' where S_StuNo='003'

  waitfor delay '0:0:5'

  update Student set C_S_Id='2' where S_StuNo='002'
commit tran

select * from Student

SQL Server创建事务——锁_第7张图片

SQL Server创建事务——锁_第8张图片

因为事务在执行过程中会将事务中用到的表和数据进行锁定,直到事务结束(提交或回滚),才会释放。

在很多用户都同时使用事务访问同一个数据资源的情况下,就会造成以下几种数据错误:

1、更新丢失:多个用户同时对一个数据资源进行更新,必定会产生被覆盖的数据,造成数据读写异常。

2、不可重复读:如果一个用户在一个事务中多次读取一条数据,而另外一个用户则同时更新啦这条数据,造成第一个用户多次读取数据不一致。

3、脏读:第一个事务读取第二个事务正在更新的数据表,如果第二个事务还没有更新完成,那么第一个事务读取的数据将是一半为更新过的,一半还没更新过的数据。

4、幻读:第一个事务读取一个结果集后,第二个事务,对这个结果集进行增删改操作,然而第一个事务中再次对这个结果集进行查询时,数据发现丢失或新增。

然而锁定,就是为解决这些问题的,它的存在使得一个事务对它自己的数据块进行操作的时候,而另外一个事务则不能插足这些数据块。这就是所谓的锁定。

 

锁兼容性具体参见:http://msdn.microsoft.com/zh-cn/library/ms186396.aspx

锁粒度和层次结构参见:http://msdn.microsoft.com/zh-cn/library/ms189849(v=sql.105).aspx

 

什么是死锁,为什么会产生死锁。见上面的例子。

例子是这样的:

第一个事务(称为A):先更新表 Student S_StuNo='003' 这条数据 --->>停顿5秒---->>更新表 Student S_StuNo='002' 这条数据

第二个事务(称为B):先更新表 Student S_StuNo='002' 这条数据--->>停顿5秒---->>更新表 Student S_StuNo='003' 这条数据

先执行事务A----5秒之内---执行事务B,出现死锁现象。

过程是这样子的:

  1. A更新表 Student S_StuNo='003' 这条数据,请求排他锁,成功。
  2. B更新表 Student S_StuNo='002' 这条数据,请求排他锁,成功。
  3. 5秒过后
  4. A更新表 Student S_StuNo='002' 这条数据,请求排它锁,由于B占用着表 Student S_StuNo='002' 这条数据,等待。
  5. B更新表 Student S_StuNo='003' 这条数据,请求排它锁,由于A占用着表 Student S_StuNo='003' 这条数据,等待。

这样相互等待对方释放资源,造成资源读写拥挤堵塞的情况,就被称为死锁现象,也叫做阻塞。而为什么会产生,上例就列举出来啦。

然而数据库并没有出现无限等待的情况,是因为数据库搜索引擎会定期检测这种状况,一旦发现有情况,立马选择一个事务作为牺牲品。牺牲的事务,将会回滚数据。

但是我们可以指定具体哪个事务作为牺牲品:

语法:

1 set deadlock_priority  <级别>

死锁处理的优先级别为 low < normal < high,不指定的情况下默认为normal,牺牲品为随机。如果指定,牺牲品为级别低的。

还可以使用数字来处理标识级别:-10 到 -5 为 low,-5 为 normal,-5 到 10 为 high。

 

死锁耗时耗资源,然而在大型数据库中,高并发带来的死锁是不可避免的,尽管死锁不能完全避免,但遵守特定的编码惯例可以将发生死锁的机会降至最低。将死锁减至最少可以增加事务的吞吐量并减少系统开销,因为只有很少的事务:

  • 回滚,撤消事务执行的所有工作。

  • 由于死锁时回滚而由应用程序重新提交。

下列方法有助于将死锁减至最少:

1、按同一顺序访问数据库对象资源。

2、避免事务中的用户交互,即事务中等待用户输入、提交等操作。

3、保持事务简短并处于一个批处理中,在同一数据库中并发执行多个需要长时间运行的事务时通常会发生死锁。事务的运行时间越长,它持有排他锁或更新锁的时间也就越长,从而会阻塞其他活动并可能导致死锁。

4、使用较低的隔离级别,确定事务是否能在较低的隔离级别上运行。实现已提交读允许事务读取另一个事务已读取(未修改)的数据,而不必等待第一个事务完成。使用较低的隔离级别(例如已提交读)比使用较高的隔离级别(例如可序列化)持有共享锁的时间更短。这样就减少了锁争用。

5、尽可能使用分区表,分区视图,把数据放置在不同的磁盘和文件组中,分散访问保存在不同分区的数据,减少因为表中放置锁而造成的其它事务长时间等待。

6,避免占用时间很长并且关系表复杂的数据操作。

7,使用较低的隔离级别,使用较低的隔离级别比使用较高的隔离级别持有共享锁的时间更短。这样就减少了锁争用。

可参考:http://msdn.microsoft.com/zh-cn/library/ms191242(v=sql.105).aspx

 

查看锁和事务活动情况:

1 --查看锁活动情况
2 select * from sys.dm_tran_locks
3 --查看事务活动情况
4 dbcc opentran

可参考:http://msdn.microsoft.com/zh-cn/library/ms190345.aspx

 

事务隔离级别:

事物隔离级别,分为5种。就是并发事务对同一资源的读取深度层次。

1、read uncommitted:这个隔离级别最低,可以读取到一个事务正在处理的数据,但事务还未提交,这种级别的读取叫做脏读。

2、read committed:这个级别是默认选项,不能脏读,不能读取事务正在处理没有提交的数据,但能修改。

3、repeatable read:不能读取事务正在处理的数据,也不能修改事务处理数据前的数据。

4、snapshot:指定事务在开始的时候,就获得了已经提交数据的快照,因此当前事务只能看到事务开始之前对数据所做的修改。

5、serializable:最高事务隔离级别,只能看到事务处理之前的数据。 

语法:

1 -- 设置事务隔离级别
2 set tran isolation level <级别>

read uncommitted 隔离级别的例子:

复制代码

1 begin tran 
2   set deadlock_priority low        -- 设置死锁处理的优先级别为 low
3 
4   update Student set C_S_Id='2' where S_StuNo='002'
5 
6   waitfor delay '0:0:5'        -- 等待5秒执行下面的回滚事务
7 rollback tran
8 
9 select * from Student

复制代码

5秒内在另外一个查询窗口执行下面语句:

复制代码

1 set tran isolation level read uncommitted        -- 设置事务隔离级别为 read uncommitted 
2 
3   select * from Student        -- 读取的数据为正在修改的数据,脏读
4 
5   waitfor delay '0:0:5'        -- 5秒之后数据已经回滚
6 
7 select * from Student        -- 回滚之后的数据

复制代码

SQL Server创建事务——锁_第9张图片

read committed 隔离级别的例子:

复制代码

1 begin tran 
2   set deadlock_priority low        -- 设置死锁处理的优先级别为 low
3 
4   update Student set C_S_Id='2' where S_StuNo='002'        -- 修改为 2
5 
6   waitfor delay '0:0:5'        -- 等待5秒执行下面的回滚事务
7 rollback tran
8 
9 select * from Student

复制代码

复制代码

1 set tran isolation level read committed        -- 设置事务隔离级别为 read committed 
2 
3   select * from Student        -- 读取不到正在修改的数据,不能脏读
4 
5   update Student set C_S_Id='5' where S_StuNo='002'        -- 修改为 5
6 
7   waitfor delay '0:0:5'        -- 5秒之后上一个事务已经回滚
8 
9 select * from Student        -- 修改之后的数据

复制代码

SQL Server创建事务——锁_第10张图片

SQL Server创建事务——锁_第11张图片

 

设置锁超时时间:

发生死锁的时候,数据库引擎会自动检测死锁,解决问题,然而这样子是很被动,只能在发生死锁后,等待处理。

然而我们也可以主动出击,设置锁超时时间,一旦资源被锁定阻塞,超过设置的锁定时间,阻塞语句自动取消,释放资源,报1222错误。

任何事情都具有两面性,调优的同时,也有他的不足之处,那就是一旦超过时间,语句取消,释放资源,但是当前报错事务,不会回滚,会造成数据错误,你需要在程序中捕获1222错误,用程序处理当前事务的逻辑,使数据正确。

1 --查看锁超时时间,默认为-1
2 select @@lock_timeout
3 
4 --设置锁超时时间
5 set lock_timeout 0    --为0时,即为一旦发现资源锁定,立即报错,不再等待,当前事务不回滚,设置时间需谨慎处理

 

之前从没接触过事务,今天跟着学习了一下,事务还挺好理解的,不过事务并发死锁这类问题就需要实际经验了。

所以我理解也不是太深刻,不过大家可以看下面这篇文章,我就是跟着学习的,个人觉得写的很不错。

参考地址:

http://www.cnblogs.com/knowledgesea/p/3714417.html

你可能感兴趣的:(ADO.NET,SQL,Server)