1. 理解ACID 特性

 

1.1 A (Atomicity)   原子性

 事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。

 

打个比方,张三从他的工行卡里转账10万元到农行卡. 执行这个过程, 如果不计手续费, 只有下列两种情况:

A:转账失败, 工行卡和农行卡的金额保持不变

B:转账成功, 以下两个语句组为一个单元执行完成。

1) UPDATE GongHangsavings
SET Balance = Balance - 100,000
WHERE Account = ‘zhangsan'


2) UPDATE NongHangsavings
SET Balance = Balance + 100,000
WHERE Account = ‘zhangsan'

 

1.2 C (Consistency) 一致性

事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。

 

一致性要求只有有效的数据才能被写入。一个事务成功执行后,数据库的状态也必须满足原来的规则。 这里包含两个层面:

1)约束规则:如果在数据库字段上建立了约束规则, 如果事务的执行违反了数据库的约束规则,那么整个事务将会回滚,数据库将恢复到原来的状态,确保符合约束规则.

2)业务逻辑规则:即张三的资金总额要保持不变, 张三工行卡的余额或者不变,或者-100,000,于此对应农行卡余额或者不变,或者+100,000。

 

数据库事务的一致性原则好比一把保护伞,这是一个守规则的世界,任何试图入侵的外来力量都无法破坏已经约定的规则. 生活在数据库中的数据在一致性原则的保护下是幸福的. 如果现实社会中,有那么一种保护机制,能够确保弱势群体的利益永不受侵犯,任何强大的利益集团都无法入侵就好了. 

 

1.3 I (Isolation)   隔离性

由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。

 

隔离性确保了多个事务的独立性, .NET中的App Domain和事务的隔离性是一致的,确保了不同的应用程序相互不干扰.

 



1.4 D (Durability)  持久性

 事务完成之后,它对于系统的影响是永久性的。该修改即使出现系统故障也将一直保持。

 

对于这个特性,我更倾向于把它理解成数据库的特性,而非作为事务特性来理解。 数据库就是用来持久存放关系数据的。 

 

2 事务(Transaction)

 

在谈及事务之前,考虑以下情形:

张三从他的工行卡里转账10万元到农行卡.  这个场景中,我们需要执行下列一个过程:

 

1) UPDATE GongHangsavings
SET Balance = Balance - 100,000
WHERE Account = ‘zhangsan'


2) UPDATE NongHangsavings
SET Balance = Balance + 100,000
WHERE Account = ‘zhangsan'

 如果交易过程中,只执行了1), 那么结果怎样?  张三平白无故地损失了100,000元. 显然张三无法接受这个事实.

 

为解决上述场景, 事务登场了.

 

事务是作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元必须有四个属性,称为原子性、一致性、隔离性和持久性 (ACID) 属性,只有这样才能成为一个事务。如果某一事务成功,则在该事务中进行的所有数据更改均会提交,成为数据库中的永久组成部分。如果事务遇到错误且必须取消或回滚,则所有数据更改均被清除。

 

 2.1 完整的事务

 

BEGIN a transaction: 设置事务的起始点

COMMIT a transaction: 提交事务,使事务提交的数据成为持久,不可更改的部分.

ROLLBACK a transaction:撤消一个事务,使之成为事务开始前的状态.

SAVE a transaction:建立一个标签,做为部分回滚时使用,使之恢复到标签初的状态.

 

2.1.1 语法:

 

BEGIN TRAN[SACTION] [<transaction name>|<@transaction variable>][WITH MARK [’<description>’]][;] 

COMMIT [TRAN[SACTION] [<transaction name>|<@transaction variable>]][;]

ROLLBACK TRAN[SACTION] [<transaction name>|<save point name>|<@transaction variable>|<@savepoint variable>][;]

SAVE TRAN[SACTION] [<save point name>| <@savepoint variable>][;]

 

2.1.2 一个完整的例子:

 

代码
USE AdventureWorks2008; -- We’re making our own table - what DB doesn’t matter
--
Create table to work with
CREATE TABLE MyTranTest
(
OrderID
INT PRIMARY KEY IDENTITY
);
-- Start the transaction
BEGIN TRAN TranStart;
-- Insert our first piece of data using default values.
--
Consider this record No1. It is also the 1st record that stays
--
after all the rollbacks are done.
INSERT INTO MyTranTest
DEFAULT VALUES;
-- Create a "Bookmark" to come back to later if need be
SAVE TRAN FirstPoint;
-- Insert some more default data (this one will disappear
--
after the rollback).
--
Consider this record No2.
INSERT INTO MyTranTest
DEFAULT VALUES;
-- Roll back to the first savepoint. Anything up to that
--
point will still be part of the transaction. Anything
--
beyond is now toast.
ROLLBACK TRAN FirstPoint;
INSERT INTO MyTranTest
DEFAULT VALUES;
-- Commit the transaction
COMMIT TRAN TranStart;
-- See what records were finally committed.
SELECT TOP 2 OrderID
FROM MyTranTest
ORDER BY OrderID DESC;
-- Clean up after ourselves
DROP TABLE MyTranTest;

 

2.2 隐式事务

 

2.2.1 定义

无须描述事务的开始,只需提交或回滚每个事务。隐性事务模式生成连续的事务链。

为连接将隐性事务模式设置为打开之后,当数据库引擎实例首次执行下列任何语句时,都会自动启动一个事务:

CREATE
ALTER TABLE
GRANT
REVOKE
SELECT
UPDATE
DELETE
INSERT

TRUNCATE TABLE
DROP
OPEN
FETCH

 

在发出 COMMIT 或 ROLLBACK 语句之前,该事务将一直保持有效。在第一个事务被提交或回滚之后,下次当连接执行以上任何语句时,数据库引擎实例都将自动启动一个新事务。该实例将不断地生成隐性事务链,直到隐性事务模式关闭为止。

 

为了更好理解上述内容,举个例子:

 

代码
1 SET QUOTED_IDENTIFIER OFF;
2  GO
3  SET NOCOUNT OFF;
4  GO
5  USE TestDB;
6  GO
7  CREATE TABLE ImplicitTran
8 ( Colb char(3) NOT NULL);
9  GO
10  SET IMPLICIT_TRANSACTIONS ON;
11  GO
12
13  BEGIN transaction
14  SAVE transaction A
15  INSERT INTO ImplicitTran VALUES ('AAA');
16  ROLLBACK TRANSACTION A
17
18  INSERT INTO ImplicitTran VALUES ('BBB');
19  COMMIT transaction

在 INSERT INTO ImplicitTran VALUES ('BBB')时,  已经隐式创建一个transaction ,直到提交或回滚.

 

 

默认情况下,隐性事务模式设置为关闭。

开启隐性事务模式语法:

SET IMPLICIT_TRANSACTIONS ON;

 

2.2.2 一个隐式事务例子

隐式事务开始后,可以使用 COMMIT TRANSACTION 或 ROLLBACK TRANSACTION语句可以结束每个事务。 

 

代码
1 SET QUOTED_IDENTIFIER OFF;
2  GO
3  SET NOCOUNT OFF;
4  GO
5  USE AdventureWorks2008R2;
6  GO
7  CREATE TABLE ImplicitTran
8 (Cola int PRIMARY KEY,
9 Colb char(3) NOT NULL);
10  GO
11  SET IMPLICIT_TRANSACTIONS ON;
12  GO
13  -- First implicit transaction started by an INSERT statement.
14  INSERT INTO ImplicitTran VALUES (1, 'aaa');
15  GO
16  INSERT INTO ImplicitTran VALUES (2, 'bbb');
17  GO
18  -- Commit first transaction.
19  COMMIT TRANSACTION;
20  GO
21  -- Second implicit transaction started by a SELECT statement.
22  SELECT COUNT(*) FROM ImplicitTran;
23  GO
24  INSERT INTO ImplicitTran VALUES (3, 'ccc');
25  GO
26  SELECT * FROM ImplicitTran;
27  GO
28  -- Commit second transaction.
29  COMMIT TRANSACTION;
30  GO
31  SET IMPLICIT_TRANSACTIONS OFF;
32  GO
33  

 

 

注意:使用隐式事务是一个需要注意的领域,一般情况下,建议设置为关闭隐式事务。

 

2.3 题外话--几种常用的事务

 

2.3.1 Sql Server 数据库级别事务

  略 

2.3.2 ADO.NET 级别事务

 

代码
SqlConnection myConnection = new SqlConnection(conString);
myConnection.Open();
//启动一个事务
SqlTransaction myTrans = myConnection.BeginTransaction();
//为事务创建一个命令
SqlCommand myCommand = new SqlCommand();
myCommand.Connection
= myConnection;
myCommand.Transaction
= myTrans;
try
{
myCommand.CommandText
= "update P_Product set Name='电脑2' where Id=52";
myCommand.ExecuteNonQuery();
myCommand.CommandText
= "update P_Product set Name='电脑3' where Id=53";
myCommand.ExecuteNonQuery();
myTrans.Commit();
//提交
Response.Write("两条数据更新成功");
}
catch (Exception ex)
{
myTrans.Rollback();
//遇到错误,回滚
Response.Write(ex.ToString());
}
finally
{
myConnection.Close();
}

 

 

2.3.3 ASP.NET页面级别事务

 

代码
1 <%@ Page Transaction="Required" Language="C#" AutoEventWireup="true"
2 CodeBehind="WebForm3.aspx.cs" Inherits="WebApplication4.WebForm3" %>
3
4
5
6  protected void Button1_Click(object sender, EventArgs e)
7 {
8 try
9 {
10 Work1();
11 Work2();
12 ContextUtil.SetComplete(); //提交事务
13   }
14 catch (System.Exception except)
15 {
16 ContextUtil.SetAbort(); //撤销事务
17   Response.Write(except.Message);
18 }
19 }

 

 

2.3.4 分布式级别事务(COM+)

   略,可查看"企业级服务COM+事务"相关技术文档。

 

2.3.5 System.Transactions 事务处理

在 .NET Framework 2.0 中增加了System.Transactions,这是一种新的命名空间,完全专注于控制事务性行为。引入了执行事务性工作的更简单方法及一些新的性能优化。System.Transactions 提供了一个“轻量级”的、易于使用的Transaction 框架。

用法:

1) 引用:using System.Transactions;。
2) 将事务操作代码放在 TransactionScope 中执行。

1 using (TransactionScope ts = new TransactionScope())
2 {
3   //事务操作代码
4    ts.Complete();
5 }

 

 

3 锁定

 

3.1 定义

锁定是 Microsoft SQL Server 数据库引擎用来同步多个用户同时对同一个数据块的访问的一种机制。

在事务获取数据块当前状态的依赖关系(比如通过读取或修改数据)之前,它必须保护自己不受其他事务对同一数据进行修改的影响。 事务通过请求锁定数据块来达到此目的。 锁有多种模式,如共享或独占。 锁模式定义了事务对数据所拥有的依赖关系级别。 如果某个事务已获得特定数据的锁,则其他事务不能获得会与该锁模式发生冲突的锁。 如果事务请求的锁模式与已授予同一数据的锁发生冲突,则数据库引擎实例将暂停事务请求直到第一个锁释放。

 

3.2 四大冲突问题

 

脏读

不重复读

幻读

更新丢失

 

3.2.1 脏读

某个事务读取的数据是另一个事务正在处理的数据。而另一个事务可能会回滚,造成第一个事务读取的数据是错误的。

 

3.2.2 不可重复读

在一个事务里两次读入数据,但另一个事务已经更改了第一个事务涉及到的数据,造成第一个事务读入旧数据。

 

3.2.3 幻读

幻读是指当事务不是独立执行时发生的一种现象。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

 

3.2.4 更新丢失

多个事务同时读取某一数据,一个事务成功处理好了数据,被另一个事务写回原值,造成第一个事务更新丢失。

 

 3.3 锁粒度和层次结构

 

Microsoft SQL Server 数据库引擎具有多粒度锁定,允许一个事务锁定不同类型的资源。 为了尽量减少锁定的开销,数据库引擎自动将资源锁定在适合任务的级别。 锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁。 锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问。 但其开销较低,因为需要维护的锁较少。

数据库引擎通常必须获取多粒度级别上的锁才能完整地保护资源。 这组多粒度级别上的锁称为锁层次结构。 例如,为了完整地保护对索引的读取,数据库引擎实例可能必须获取行上的共享锁以及页和表上的意向共享锁。

下表列出了数据库引擎可以锁定的资源。

资源 说明

RID

用于锁定堆中的单个行的行标识符。

KEY

索引中用于保护可序列化事务中的键范围的行锁。

PAGE

数据库中的 8 KB 页,例如数据页或索引页。

EXTENT

一组连续的八页,例如数据页或索引页。

HoBT

堆或 B 树。 用于保护没有聚集索引的表中的 B 树(索引)或堆数据页的锁。

TABLE

包括所有数据和索引的整个表。

FILE

数据库文件。

APPLICATION

应用程序专用的资源。

METADATA

元数据锁。

ALLOCATION_UNIT

分配单元。

DATABASE

整个数据库。

 

 

 3.4 锁模式

 

Microsoft SQL Server 数据库引擎使用不同的锁模式锁定资源,这些锁模式确定了并发事务访问资源的方式。

 

下表显示了数据库引擎使用的资源锁模式。

锁模式 说明

共享 (S)

用于不更改或不更新数据的读取操作,如 SELECT 语句。

更新 (U)

用于可更新的资源中。 防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。

排他 (X)

用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。 确保不会同时对同一资源进行多重更新。

意向

用于建立锁的层次结构。 意向锁包含三种类型:意向共享 (IS)、意向排他 (IX) 和意向排他共享 (SIX)。

架构

在执行依赖于表架构的操作时使用。 架构锁包含两种类型:架构修改 (Sch-M) 和架构稳定性 (Sch-S)。

大容量更新 (BU)

在向表进行大容量数据复制且指定了 TABLOCK 提示时使用。

键范围

当使用可序列化事务隔离级别时保护查询读取的行的范围。 确保再次运行查询时其他事务无法插入符合可序列化事务的查询的行。

  

3.4.1 共享锁
共享锁(S 锁)允许并发事务在封闭式并发控制下读取 (SELECT) 资源。 有关详细信息,请参阅并发控制的类型。 资源上存在共享锁(S 锁)时,任何其他事务都不能修改数据。 读取操作一完成,就立即释放资源上的共享锁(S 锁),除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留共享锁(S 锁)。
 
3.4.2 更新锁(U 锁)
更新锁在共享锁和排他锁的杂交。 更新锁意味着在做一个更新时,一个共享锁在扫描完成符合条件的数据后可能会转化成排他锁。
 
这里面有两个步骤:
1) 扫描获取Where条件时。这部分是一个更新查询,此时是一个更新锁。
2) 如果将执行写入更新。 此时该锁升级到排他锁。 否则,该锁转变成共享锁。
 
更新锁可以防止常见的死锁。 在可重复读或可序列化事务中,此事务读取数据 [获取资源(页或行)的共享锁(S 锁)],然后修改数据 [此操作要求锁转换为排他锁(X 锁)]。 如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排他锁(X 锁)。 共享模式到排他锁的转换必须等待一段时间,因为一个事务的排他锁与其他事务的共享模式锁不兼容;发生锁等待。 第二个事务试图获取排他锁(X 锁)以进行更新。 由于两个事务都要转换为排他锁(X 锁),并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。

若要避免这种潜在的死锁问题,请使用更新锁(U 锁)。 一次只有一个事务可以获得资源的更新锁(U 锁)。 如果事务修改资源,则更新锁(U 锁)转换为排他锁(X 锁)。  

更新锁只与共享锁和意向共享锁兼容。

  

3.4.3 排他锁
排他锁(X 锁)可以防止并发事务对资源进行访问。 排他锁不与其他任何锁兼容。 使用排他锁(X 锁)时,任何其他事务都无法修改数据;仅在使用 NOLOCK 提示或未提交读隔离级别时才会进行读取操作。
 
3.4.4 意向锁
数据库引擎使用意向锁来保护共享锁(S 锁)或排他锁(X 锁)放置在锁层次结构的底层资源上。 意向锁之所以命名为意向锁,是因为在较低级别锁前可获取它们,因此会通知意向将锁放置在较低级别上。

意向锁有两种用途:

  • 防止其他事务以会使较低级别的锁无效的方式修改较高级别资源。
  • 提高数据库引擎在较高的粒度级别检测锁冲突的效率。

例如,在该表的页或行上请求共享锁(S 锁)之前,在表级请求共享意向锁。 在表级设置意向锁可防止另一个事务随后在包含那一页的表上获取排他锁(X 锁)。 意向锁可以提高性能,因为数据库引擎仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁。 而不需要检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。

 

意向锁包括意向共享 (IS)、意向排他 (IX) 以及意向排他共享 (SIX)。

意向锁 锁模式 说明

意向共享 (IS)

保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁。

意向排他 (IX)

保护针对层次结构中某些(而并非所有)低层资源请求或获取的排他锁。 IX 是 IS 的超集,它也保护针对低层级别资源请求的共享锁。

意向排他共享 (SIX)

保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁。 顶级资源允许使用并发 IS 锁。 例如,获取表上的 SIX 锁也将获取正在修改的页上的意向排他锁以及修改的行上的排他锁。 虽然每个资源在一段时间内只能有一个 SIX 锁,以防止其他事务对资源进行更新,但是其他事务可以通过获取表级的 IS 锁来读取层次结构中的低层资源。

意向更新 (IU)

保护针对层次结构中所有低层资源请求或获取的更新锁。 仅在页资源上使用 IU 锁。 如果进行了更新操作,IU 锁将转换为 IX 锁。

共享意向更新 (SIU)

S 锁和 IU 锁的组合,作为分别获取这些锁并且同时持有两种锁的结果。 例如,事务执行带有 PAGLOCK 提示的查询,然后执行更新操作。 带有 PAGLOCK 提示的查询将获取 S 锁,更新操作将获取 IU 锁。

更新意向排他 (UIX)

U 锁和 IX 锁的组合,作为分别获取这些锁并且同时持有两种锁的结果。

 
  
3.4.5 架构锁  

数据库引擎在表数据定义语言 (DDL) 操作(例如添加列或删除表)的过程中使用架构修改 (Sch-M) 锁。 保持该锁期间,Sch-M 锁将阻止对表进行并发访问。 这意味着 Sch-M 锁在释放前将阻止所有外围操作。

某些数据操作语言 (DML) 操作(例如表截断)使用 Sch-M 锁阻止并发操作访问受影响的表。

数据库引擎在编译和执行查询时使用架构稳定性 (Sch-S) 锁。 Sch-S 锁不会阻止某些事务锁,其中包括排他 (X) 锁。 因此,在编译查询的过程中,其他事务(包括那些针对表使用 X 锁的事务)将继续运行。 但是,无法针对表执行获取 Sch-M 锁的并发 DDL 操作和并发 DML 操作。 

 
3.4.5 大容量更新锁  
数据库引擎在将数据大容量复制到表中时使用了大容量更新 (BU) 锁,并指定了 TABLOCK 提示或使用 sp_tableoption 设置了 table lock on bulk load 表选项。 大容量更新锁(BU 锁)允许多个线程将数据并发地大容量加载到同一表,同时防止其他不进行大容量加载数据的进程访问该表。 
  
3.4.6 键范围锁

在使用可序列化事务隔离级别时,对于 Transact-SQL 语句读取的记录集,键范围锁可以隐式保护该记录集中包含的行范围。 键范围锁可防止幻读。 通过保护行之间键的范围,它还防止对事务访问的记录集进行幻像插入或删除。

 

 3.5 锁之间的兼容性

 更多内容,参看锁兼容性 http://msdn.microsoft.com/zh-cn/library/ms186396.aspx 

 

3.6  事务隔离级别 

SQL Server通过SET TRANSACTION ISOLATION LEVEL语句设置事务隔离级别: 

SET TRANSACTION ISOLATION LEVEL

    { READ UNCOMMITTED

    | READ COMMITTED

    | REPEATABLE READ

    | SNAPSHOT

    | SERIALIZABLE

    }

[ ; ]

Read Committed是SQL Server的预设隔离等级。

 

3.6.1 READ COMMITTED

一旦创建共享锁的语句执行完成, 该锁顶便释放。

Read Committed是SQL Server的预设隔离等级。

Read Committed只可以防止脏读。

 举例:

--先创建表:
CREATE TABLE tb(id int,val int)
INSERT tb VALUES(1,10)
INSERT tb VALUES(2,20)

然后在连接1中,执行:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRANSACTION
    SELECT * FROM tb;  --这个SELECT结束后,就会释放掉共享锁
    
    WAITFOR DELAY '00:00:05'  --模拟事务处理,等待5秒
    
    SELECT * FROM tb;   --再次SELECT tb表
ROLLBACK  --回滚事务

在连接2中,执行
UPDATE tb SET
    val = val + 10
WHERE id = 2;

--------
回到连接1中.可以看到.两次SELECT的结果是不同的.
因为在默认的READ COMMITTED隔离级别下,SELECT完了.就会马上释放掉共享锁.


 

 

3.6.2 READ UNCOMMITTED

Read UnCommitted事务可以读取事务已修改,但未提交的的记录。

Read UnCommitted事务会产生脏读(Dirty Read)。

Read UnCommitted事务与select语句加nolock 的效果一样,它是所有隔离级别中限制最少的。

 

3.6.3 REPEATABLE READ

REPEATABLE READ事务不会产生脏读,并且在事务完成之前,任何其它事务都不能修改目前事务已读取的记录。

其它事务仍可以插入新记录,但必须符合当前事务的搜索条件——这意味着当前事务重新查询记录时,会产生幻读(Phantom Read)。

 

3.6.4  SERIALIZABLE

SERIALIZABLE可以防止除更新丢失外所有的一致性问题, 即:

 

1.语句无法读取其它事务已修改但未提交的记录。

2.在当前事务完成之前,其它事务不能修改目前事务已读取的记录。

3.在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。

 

Serializable事务与select语句加holdlock的效果一样。

举例:

--示例2.REPEATABLE READ

连接1:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
    SELECT * FROM tb;  --这个SELECT结束后,就会释放掉共享锁
    
    WAITFOR DELAY '00:00:05'  --模拟事务处理,等待5秒
    
    SELECT * FROM tb;   --再次SELECT tb表
ROLLBACK  --回滚事务


连接2:
UPDATE tb SET
    val = val + 10
WHERE id = 2;

---
可以看到连接2被阻塞了.而且连接1中.两次SELECT的结果是一样的.
因为REPEATABLE READ隔离级别下的共享锁保留到事务结束.
所以别的事务的X锁与S锁不兼容.所以连接2等待.

 

3.6.5 SNAPSHOT

Snapshot事务中任何语句所读取的记录,都是事务启动时的数据。

这相当于事务启动时,数据库为事务生成了一份专用“快照”。在当前事务中看到不其它事务在当前事务启动之后所进行的数据修改。

Snapshot事务不会读取记录时要求锁定,读取记录的Snapshot事务不会锁住其它事务写入记录,写入记录的事务也不会锁住Snapshot事务读取数据。

 

 

3.7    悲观锁和悲观锁


3.7.1 悲观锁

悲观锁是指假设并发更新冲突会发生,所以不管冲突是否真的发生,都会使用锁机制。
悲观锁会完成以下功能:锁住读取的记录,防止其它事务读取和更新这些记录。其它事务会一直阻塞,直到这个事务结束.
悲观锁是在使用了数据库的事务隔离功能的基础上,独享占用的资源,以此保证读取数据一致性,避免修改丢失。

悲观锁可以使用Repeatable Read事务,它完全满足悲观锁的要求。


3.7.2 乐观锁

乐观锁不会锁住任何东西,也就是说,它不依赖数据库的事务机制,乐观锁完全是应用系统层面的东西。

如果使用乐观锁,那么数据库就必须加版本字段,否则就只能比较所有字段,但因为浮点类型不能比较,所以实际上没有版本字段是不可行的。

 

3.8 死锁

当二或多个工作各自具有某个资源的锁定,但其它工作尝试要锁定此资源,而造成工作永久封锁彼此时,会发生死锁。例如:
1.           事务 A 取得数据列 1 的共享锁定。
2.           事务B 取得数据列 2 的共享锁定。
3.           事务A 现在要求数据列 2 的独占锁定,但会被封锁直到事务B 完成并释出对数据列 2 的共享锁定为止。
4.           事务B 现在要求数据列 1 的独占锁定,但会被封锁直到事务A 完成并释出对数据列 1 的共享锁定为止。
等到事务B 完成后,事务A 才能完成,但事务B 被事务A 封锁了。这个状况也称为「循环相依性」(Cyclic Dependency)。事务A 相依于事务B,并且事务B 也因为相依于事务A 而封闭了这个循环。

 

SQL Server遇到死锁时会自动杀死其中一个事务,而另一个事务会正常结束(提交或回滚)。

SQL Server对杀死的连接返回错误代码是1205,异常提示是:

Your transaction (process ID #52) was deadlocked on {lock | communication buffer | thRead} resources with another process and has been chosen as the deadlock victim. Rerun your transaction.

 


例如以下操作就会产生死锁,两个连接互相阻塞对方的update。

连接1:
begin tran

select * from customers 

update customers set CompanyName = CompanyName

 
waitfor delay '00:00:05'

 
select * from Employees
–因为Employees被连接2锁住了,所以这里会阻塞。
update Employees set LastName = LastName 
commit tran
 
连接2:
begin tran

select * from Employees

update Employees set LastName = LastName

 
waitfor delay '00:00:05'

 
select * from customers 
--因为customers被连接1锁住了,所以这里会阻塞。
update customers set CompanyName = CompanyName
commit tran

 

 

3.9 如何避免死锁

(1).按同一顺序访问对象。(注:避免出现循环)
(2).避免事务中的用户交互。(注:减少持有资源的时间,较少锁竞争)
(3).保持事务简短并处于一个批处理中。(注:同(2),减少持有资源的时间)
(4).使用较低的隔离级别。(注:使用较低的隔离级别(例如已提交读)比使用较高的隔离级别(例如可序列化)持有共享锁的时间更短,减少锁竞争)
(5).使用基于行版本控制的隔离级别:2005中支持快照事务隔离和指定READ_COMMITTED隔离级别的事务使用行版本控制,可以将读与写操作之间发生的死锁几率降至最低:
SET ALLOW_SNAPSHOT_ISOLATION ON --事务可以指定 SNAPSHOT 事务隔离级别;
SET READ_COMMITTED_SNAPSHOT ON  --指定 READ_COMMITTED 隔离级别的事务将使用行版本控制而不是锁定。默认情况下(没有开启此选项,没有加with nolock提示),SELECT语句会对请求的资源加S锁(共享锁);而开启了此选项后,SELECT不会对请求的资源加S锁。
注意:设置 READ_COMMITTED_SNAPSHOT 选项时,数据库中只允许存在执行 ALTER DATABASE 命令的连接。在 ALTER DATABASE 完成之前,数据库中决不能有其他打开的连接。数据库不必一定要处于单用户模式中。
(6).使用绑定连接。(注:绑定会话有利于在同一台服务器上的多个会话之间协调操作。绑定会话允许一个或多个会话共享相同的事务和锁(但每个回话保留其自己的事务隔离级别),并可以使用同一数据,而不会有锁冲突。可以从同一个应用程序内的多个会话中创建绑定会话,也可以从包含不同会话的多个应用程序中创建绑定会话。在一个会话中开启事务(begin tran)后,调用exec sp_getbindtoken @Token out;来取得Token,然后传入另一个会话并执行EXEC sp_bindsession @Token来进行绑定(最后的示例中演示了绑定连接)。

 

  

4 参考资料

1. Sql 2008 Programming  Rob Vieira著

2. msdn官方技术文档 http://msdn.microsoft.com/zh-cn/library/ms190615.aspx

3. .NET深入体验与实战精要 李天平著

4.  csdn 等论坛及博客文章