First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 2960 评论 :: 298 Trackbacks

本部分内容的完整代码可以从这里下载。

2.2 SQL Server 2000+ADO.NET实现并发控制

2.2.1 并发一致性问题

常见并发并发一致性问题包括:丢失的修改、不可重复读、读脏数据、幻影读(幻影读在一些资料中往往与不可重复读归为一类)。

2.2.1.1 丢失修改

下面我们先来看一个例子,说明并发操作带来的数据的不一致性问题。

考虑飞机订票系统中的一个活动序列:

  1. 甲售票点(甲事务)读出某航班的机票余额A,设A=16.
  2. 乙售票点(乙事务)读出同一航班的机票余额A,也为16.
  3. 甲售票点卖出一张机票,修改余额A←A-1.所以A为15,把A写回数据库.
  4. 乙售票点也卖出一张机票,修改余额A←A-1.所以A为15,把A写回数据库.

结果明明卖出两张机票,数据库中机票余额只减少1。

归纳起来就是:两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。前文(2.1.4数据删除与更新)中提到的问题及解决办法往往是针对此类并发问题的。但仍然有几类问题通过上面的方法解决不了,那就是:

2.2.1.2 不可重复读

不可重复读是指事务T1读取数据后,事务T2执行更新操作,使T1无法再现前一次读取结果。具体地讲,不可重复读包括三种情况:

  • 事务T1读取某一数据后,事务T2对其做了修改,当事务1再次读该数据时,得到与前一次不同的值。例如,T1读取B=100进行运算,T2读取同一数据B,对其进行修改后将B=200写回数据库。T1为了对读取值校对重读B,B已为200,与第一次读取值不一致。
  • 事务T1按一定条件从数据库中读取了某些数据记录后,事务T2删除了其中部分记录,当T1再次按相同条件读取数据时,发现某些记录神密地消失了。
  • 事务T1按一定条件从数据库中读取某些数据记录后,事务T2插入了一些记录,当T1再次按相同条件读取数据时,发现多了一些记录。(这也叫做幻影读)

2.2.1.3 读"脏"数据

读"脏"数据是指事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤消,这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为"脏"数据,即不正确的数据。

产生上述三类数据不一致性的主要原因是并发操作破坏了事务的隔离性。并发控制就是要用正确的方式调度并发操作,使一个用户事务的执行不受其它事务的干扰,从而避免造成数据的不一致性。

2.2.2 并发一致性问题的解决办法

2.2.2.1 封锁(Locking)

封锁是实现并发控制的一个非常重要的技术。所谓封锁就是事务T在对某个数据对象例如表、记录等操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。

基本的封锁类型有两种:排它锁(Exclusive locks 简记为X锁)和共享锁(Share locks 简记为S锁)。

排它锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其它事务在T释放A上的锁之前不能再读取和修改A。

共享锁又称为读锁。若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其它事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

2.2.2.2 封锁协议

在运用X锁和S锁这两种基本封锁,对数据对象加锁时,还需要约定一些规则,例如应何时申请X锁或S锁、持锁时间、何时释放等。我们称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。下面介绍三级封锁协议。三级封锁协议分别在不同程度上解决了丢失的修改、不可重复读和读"脏"数据等不一致性问题,为并发操作的正确调度提供一定的保证。下面只给出三级封锁协议的定义,不再做过多探讨。

  • 1级封锁协议

1级封锁协议是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。1级封锁协议可防止丢失修改,并保证事务T是可恢复的。在1级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和不读"脏"数据。

  • 2级封锁协议

2级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。2级封锁协议除防止了丢失修改,还可进一步防止读"脏"数据。

  • 3级封锁协议

3级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。3级封锁协议除防止了丢失修改和不读'脏'数据外,还进一步防止了不可重复读。

2.2.3 事务隔离级别

尽管数据库理论对并发一致性问题提供了完善的解决机制,但让程序员自己去控制如何加锁以及加锁、解锁的时机显然是很困难的事情。索性绝大多数数据库以及开发工具都提供了事务隔离级别,让用户以一种更轻松的方式处理并发一致性问题。常见的事务隔离级别包括:ReadUnCommitted、ReadCommitted、RepeatableRead和Serializable四种。不同的隔离级别下对数据库的访问方式以及数据库的返回结果有可能是不同的。我们将通过几个实验深入了解事务隔离级别以及SQL Server在后台是如何将它们转换成锁的。

2.2.3.1 ReadUnCommitted与ReadCommitted

ReadUnCommitted是最低的隔离级别,这个级别的隔离允许读入别人尚未提交的脏数据,除此之外,在这种事务隔离级别下还存在不可重复读的问题。

ReadCommitted是许多数据库的缺省级别,这个隔离级别上,不会出现读取未提交的数据问题,但仍然无法避免不可重复读(包括幻影读)的问题。当你的系统对并发控制的要求非常严格时,这种默认的隔离级别可能无法提供数据有效的保护,但对于决大多数应用来讲,这种隔离级别就够用了。

我们使用下面的实验来进行测试:

首先配置SQL Server 2000数据库,附加DBApp数据库。然后在Visual Studio .net中建立一管理控制台应用程序,添加必要的命名空间引用:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

然后建立两个数据库链接,并分别采用不同的事务隔离级别:

   private static SqlConnection conn1;
   private static SqlConnection conn2;
   private static SqlTransaction tx1;
   private static SqlTransaction tx2;
   private static void Setup()
   {
      conn1 = new SqlConnection(connectionString);
      conn1.Open();
      tx1 = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);
      conn2 = new SqlConnection(connectionString);
      conn2.Open();
      tx2 = conn2.BeginTransaction(IsolationLevel.ReadCommitted);
   }

其中事务1允许读入未提交的数据,而事务2只允许读入已提交数据。

在主程序中,我们模拟两个人先后的不同操作,以产生并发一致性问题:

   public static void Main()
   {
      Setup();
      try
      {
         ReadUnCommittedDataByTransaction1();
         UnCommittedUpdateByTransaction2();
         ReadUnCommittedDataByTransaction1();
         tx2.Rollback();
         Console.WriteLine("\n-- Transaction 2 rollbacked!\n");
         ReadUnCommittedDataByTransaction1();
         tx1.Rollback();
      }
      catch
      {
         ……
      }
   }

第一步,使用ReadUnCommittedDataByTransaction1方法利用事务1从数据库中读入id值为1的学生信息。此时的信息是数据库的初始信息。

第二步,调用UnCommittedUpdateByTransaction2方法,从第2个事务中发送一UPDATE命令更新数据库,但尚未提交。

第三步,再次调用ReadUnCommittedDataByTransaction1,从事务1中读取数据库数据,你会发现由事务2发布的尚未提交的更新被事务1读取出来(ReadUnCommitted)。

第四步,事务2放弃提交,回滚事务tx2.Rollback();。

第五步,再次调用ReadUnCommittedDataByTransaction1();,读取数据库中的数据,此次是已经回滚后的数据。

程序运行结果如下:

-- Read age from database:
Age:20
-- Run an uncommitted command:
UPDATE student SET age=30 WHERE id=1
-- Read age from database:
Age:30
-- Transaction 2 rollbacked!
-- Read age from database:
Age:20

关于ReadUnCommittedDataByTransaction1()与UnCommittedUpdateByTransaction2()的方法定义如下:

   private static void UnCommittedUpdateByTransaction2()
   {
      string command = "UPDATE student SET age=30 WHERE id=1";
      Console.WriteLine("\n-- Run an uncommitted command:\n{0}\n", command);
      SqlCommand cmd = new SqlCommand(command, conn2);
      cmd.Transaction = tx2;
      cmd.ExecuteNonQuery();
   }
   private static void ReadUnCommittedDataByTransaction1()
   {
      Console.WriteLine("-- Read age from database:");
      SqlCommand cmd = new SqlCommand("SELECT age FROM student WHERE id = 1", conn1);
      cmd.Transaction = tx1;
      try
      {
         int age = (int)cmd.ExecuteScalar();
         Console.WriteLine("Age:{0}", age);
      }
      catch(SqlException e)
      {
         Console.WriteLine(e.Message);
      }
   }

从上面的实验可以看出,在ReadUnCommitted隔离级别下,程序可能读入未提交的数据,但此隔离级别对数据库资源锁定最少。

本实验的完整代码可以从"SampleCode\Chapter 2\Lab 2-6"下找到。

让我们再来做一个实验(这个实验要求动作要快的,否则可能看不到预期效果)。首先修改上面代码中的Setup()方法代码,将

tx1 = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);

改为:

tx1 = conn1.BeginTransaction(IsolationLevel.ReadCommitted);

再次运行代码,你会发现程序执行到第三步就不动了,如果你有足够的耐心等下去的话,你会看到"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"的一条提示,这条提示究竟是什么意思呢?让我们探察一下究竟发生了什么:

第一步,在做这个实验之前,先将SQL Server 2000的企业管理器打开,然后再将SQL Server事件探察器打开并处于探察状态。

第二步,运行改动后的程序,程序执行到一半就暂停了。此时迅速切换到企业管理器界面,右击"管理"下面的"当前活动",选择"刷新"(整个过程应在大约15秒内完成即可,如图 2-8所示),我们便得到了数据库当前进程的一个快照。

图 2-8 使用企业管理器查看当前活动

我们发现此时进程出现了阻塞,被阻塞者是52号进程,而阻塞者是53号进程。也就是说53号进程的工作妨碍了52号进程继续工作。(不同实验时进程号可能各不相同)

第三步,为了进一步查明原因真相,我们切换到事件探察器窗口,看看这两个进程都是干什么的。如图 2-9所示,事件探察器显示了这两个进程的详细信息。从图中我们可以看出,52号进程对应我们的事务1,53号进程对应我们的事务2。事务2执行了UPDATE命令,但尚未提交,此时事务1去读尚未提交的数据便被阻塞住。从图中我们可以看出52号进程是被阻塞者。

此时如果事务2完成提交,52号进程便可以停止等待,得到需要的结果。然而我们的程序没有提交数据,因此52号进程就要无限等下去。所幸SQL Server 2000检测到事务2的运行时间过长(这就是上面的错误提示"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"),所以将事务2回滚以释放占用的资源。资源被释放后,52号进程便得以执行。

图 2-9 事件探察器探察阻塞命令

第四步,了解了上面发生的事情后,我们现在可以深入讨论一下共享锁和排它锁的使用情况了。重新回到企业管理器界面,让我们查看一下两个进程各占用了什么资源。从图 2-10中我们可以看出,53号进程(事务2)在执行更新命令前对相应的键加上了排它锁(X锁),按照前文提到的1级封锁协议,该排它锁只有在事务2提交或回滚后才释放。现在52号进程(事务1)要去读同一行数据,按照2级封锁协议,它要首先对该行加共享锁,然而 该行数据已经被事务2加上了排它锁,因此事务1只能处于等待状态,等待排它锁被释放。因此我们就看到了前面的"阻塞"问题。

图 2-10 进程执行写操作前首先加了排它锁

图 2-11 进程读操作前要加共享锁,但被阻塞

当事务1的事务隔离级别是ReadUnCommitted时,读数据是不加锁的,因此排它锁对ReadUnCommitted不起作用,进程也不会被阻塞,不过确读到了"脏"数据。

2.2.3.2 RepeatableRead

RepeatableRead是指可重复读,它的隔离级别要比ReadCommitted级别高。它允许某事务执行重复读时数据保持不变,但是仍然无法解决幻影读的问题。为了更深入的了解RepeatableRead所能解决的问题,我们还是使用下面的实验来加以印证:

第一步,事务1与事务2同时设置为ReadCommitted,并同时开启事务。

private static void Setup()
{
   conn1 = new SqlConnection(connectionString);
   conn1.Open();
   tx1 = conn1.BeginTransaction(IsolationLevel.ReadCommitted);
   conn2 = new SqlConnection(connectionString);
   conn2.Open();
   tx2 = conn2.BeginTransaction(IsolationLevel.ReadCommitted);
}

第二步,事务1读取数据库中数据。注意此时并没有通过提交或回滚的方式结束事务1,事务1仍然处于活动状态。

private static int ReadAgeByTransaction1()
{
   return (int)ExecuteScalar("SELECT age FROM student WHERE (id = 1)");
}
private static object ExecuteScalar(string command)
{
   Console.WriteLine("-- Execute command: {0}", command);
   SqlCommand cmd = new SqlCommand(command, conn1);
   cmd.Transaction = tx1;
   return cmd.ExecuteScalar();
}

第三步,事务2修改年龄数据并提交修改。

private static void ModifyAgeByTransaction2()
{
   string command = "UPDATE student SET age=30 WHERE id=1";
   Console.WriteLine("-- Modify age by transaction2, command:{0}", command);
   SqlCommand cmd = new SqlCommand(command, conn2);
   cmd.Transaction = tx2;
   try
   {
      cmd.ExecuteNonQuery();
      tx2.Commit();
   }
   catch(Exception e)
   {
      Console.WriteLine(e.Message);
      tx2.Rollback();
   }
}

第四步,事务1重复读取年龄数据,此时会发现读取出来的数据是修改过的数据,与上次读取的数据不一样了!顾名思义,不可重复读。主程序代码如下:

public static void Main()
{
   Setup();
   try
   {
      int age1 = ReadAgeByTransaction1(); 
      ModifyAgeByTransaction2();
      int age2 = ReadAgeByTransaction1();
      Console.WriteLine("\nFirst Read: age={0}\nSecond Read: age={1}", age1, age2);
   }
   catch(Exception e)
   {
      Console.WriteLine("Got an error! " + e.Message);
   }
   finally
   {
      CleanUp();
   }
}

程序的运行结果如下:

-- Execute command: SELECT age FROM student WHERE (id = 1)
-- Modify age by transaction2, command:UPDATE student SET age=30 WHERE id=1
-- Execute command: SELECT age FROM student WHERE (id = 1)
First Read: age=20
Second Read: age=30

之所以出现了重复读时读取的数据与第一次读取的不一样,是因为事务1被设置成了ReadCommitted隔离类型,该隔离级别无法防止不可重复读的问题。要想在一个事务中两次读取数据完全相同就必须使用RepeatableRead事务隔离级别。

让我们修改上面的Setup()方法中的代码,将事务1的隔离级别设置为RepeatableRead:

tx1 = conn1.BeginTransaction(IsolationLevel.RepeatableRead);

再次运行该程序,你会发现程序执行到第二步就暂停了,如果等待一段时间后你就会看到"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"的错误提示,此时,重复读的数据确和第一次读完全一样。程序执行结果如下:

-- Execute command: SELECT age FROM student WHERE (id = 1)
-- Modify age by transaction2, command:UPDATE student SET age=30 WHERE id=1
超时时间已到。在操作完成之前超时时间已过或服务器未响应。
-- Execute command: SELECT age FROM student WHERE (id = 1)
First Read: age=20
Second Read: age=20

为了探明原因,还是象上一个案例一样,再次执行该程序,当出现暂停时迅速切换到企业管理器中查看当前活动的快照,并检查阻塞进程中数据锁定情况,你会发现如图 2-12和图 2-13所示的内容:

图 2-12 RepeatableRead在读数据时加S锁,直到事务结束才释放

图 2-13 修改数据要求加X锁,但被阻塞

根据3级封锁协议,事务T在读取数据之前必须先对其加S锁,直到事务结束才释放。因此,事务1在第一次读取数据时便对数据加上了共享锁,第一次数据读取完成后事务并未结束,因此该共享锁并不会被释放,此时事务2试图修改该数据,按照2级封锁协议,在写之前要加排它锁,但数据上的共享锁尚未被释放,导致事务2不得不处于等待状态。当事务2等待时间超时后,SQL Server就强制将该事务回滚。尽管事务2执行失败,但保证了事务1实现了可重复读级别的事务隔离。

RepeatableRead事务隔离级别允许事务内的重复读操作,但是这并不能避免出现幻影读的问题,如果您的程序中存在幻影读的潜在问题的话,就必须采用最高的事务隔离级别:Serializable。

2.2.3.3 Serializable

Serializable隔离级别是最高的事务隔离级别,在此隔离级别下,不会出现读脏数据、不可重复读和幻影读的问题。在详细说明为什么之前首先让我们看看什么是幻影读。

所谓幻影读是指:事务1按一定条件从数据库中读取某些数据记录后,事务2插入了一些符合事务1检索条件的新记录,当事务1再次按相同条件读取数据时,发现多了一些记录。让我们通过以下案例来重现幻影读的问题:

第一步,将事务1和事务2均设为RepeatableRead隔离级别,并同时开启事务。

private static void Setup()
{
   conn1 = new SqlConnection(connectionString);
   conn1.Open();
   tx1 = conn1.BeginTransaction(IsolationLevel.RepeatableRead);
   conn2 = new SqlConnection(connectionString);
   conn2.Open();
   tx2 = conn2.BeginTransaction(IsolationLevel.RepeatableRead);
}

第二步,事务1读取学号为1的学生的平均成绩以及所学课程的门数。此时读到学生1学了3门课程,平均成绩为73.67。注意,此时事务1并未提交。

private static double ReadAverageMarksByTransaction1()
{
   return (double)ExecuteScalar("SELECT AVG(mark) AS AvgMark FROM SC WHERE (id = 1)");
}
private static int ReadTotalCoursesByTransaction1()
{
   return (int)ExecuteScalar("SELECT COUNT(*) AS num FROM SC WHERE (id = 1)");
}
private static object ExecuteScalar(string command)
{
   Console.WriteLine("-- Execute command: {0}", command);
   SqlCommand cmd = new SqlCommand(command, conn1);
   cmd.Transaction = tx1;
   return cmd.ExecuteScalar();
}

第三步,事务2向数据库插入一条新记录,让学号为1的同学再学1门课程,成绩是80。然后提交修改到数据库。

private static void InsertRecordByTransaction2()
{
   string command = "INSERT INTO SC VALUES(1, 5, 80)";
   Console.WriteLine("-- Insert to table SC by transaction 2");
   Console.WriteLine("-- Command:{0}\n", command);
   SqlCommand cmd = new SqlCommand(command, conn2);
   cmd.Transaction = tx2;
   try
   {
      cmd.ExecuteNonQuery();
      tx2.Commit();
   }
   catch(Exception e)
   {
      Console.WriteLine(e.Message);
      tx2.Rollback();
   }
}

第四步,事务1再次读取学号为1的学生的平均成绩以及所学课程的门数。此时读到确是4门课程,平均成绩为75.25。与第一次读取的不一样!居然多出了一门课程,多出的这门课程就像幻影一样出现在我们的面前。测试用主程序如下:

public static void Main()
{
   Setup();
   try
   {
      Console.WriteLine(">>>> Step 1");
      double avg = ReadAverageMarksByTransaction1(); 
      int total = ReadTotalCoursesByTransaction1();
      Console.WriteLine("avg={0,5:F2}, total={1}\n", avg, total); 
      Console.WriteLine(">>>> Step 2");
      InsertRecordByTransaction2();
      Console.WriteLine(">>>> Step 3");
      avg = ReadAverageMarksByTransaction1(); 
      total = ReadTotalCoursesByTransaction1();
      Console.WriteLine("avg={0,5:F2}, total={1}\n", avg, total); 
   }
   catch(Exception e)
   {
      Console.WriteLine("Got an error! " + e.Message);
   }
   finally
   {
      CleanUp();
   }
}

程序执行结果如下:

>>>> Step 1
-- Execute command: SELECT AVG(mark) AS AvgMark FROM SC WHERE (id = 1)
-- Execute command: SELECT COUNT(*) AS num FROM SC WHERE (id = 1)
avg=73.67, total=3
>>>> Step 2
-- Insert to table SC by transaction 2
-- Command:INSERT INTO SC VALUES(1, 5, 80)
>>>> Step 3
-- Execute command: SELECT AVG(mark) AS AvgMark FROM SC WHERE (id = 1)
-- Execute command: SELECT COUNT(*) AS num FROM SC WHERE (id = 1)
avg=75.25, total=4

大家可以思考一下,为什么RepeatableRead隔离模式并不能使得两次读取的平均值一样呢?(可以从锁的角度来解释这一现象)。

仍然象前面的做法一样,我们看看究竟发生了什么事情。在探察之前,先将Setup方法中事务1的隔离级别设置为Serializable,再次运行程序,当发现程序运行暂停时,查看数据库当前活动快照,你会发现如图 2-14和图 2-15所示的锁定问题:

图 2-14 Serializable隔离模式对符合检索条件的数据添加了RangeS-S锁

图 2-15 当试图插入符合RangeIn条件的记录时,只能处于等待状态

从图中我们可以看出,在Serializalbe隔离模式下,数据库在检索数据时,对所有满足检索条件的记录均加上了RangeS-S共享锁。事务2试图去插入一满足RangeIn条件的记录时,必须等待这些RangS-S锁释放,否则就只能处于等待状态。在等待超时后,事务2就会被SQL Server强制回滚。

修改后的程序运行结果如下:

>>>> Step 1
-- Execute command: SELECT AVG(mark) AS AvgMark FROM SC WHERE (id = 1)
-- Execute command: SELECT COUNT(*) AS num FROM SC WHERE (id = 1)
avg=73.67, total=3
>>>> Step 2
-- Insert to table SC by transaction 2
-- Command:INSERT INTO SC VALUES(1, 5, 80)
超时时间已到。在操作完成之前超时时间已过或服务器未响应。
>>>> Step 3
-- Execute command: SELECT AVG(mark) AS AvgMark FROM SC WHERE (id = 1)
-- Execute command: SELECT COUNT(*) AS num FROM SC WHERE (id = 1)
avg=73.67, total=3

事务2的运行失败确保了事务1不会出现幻影读的问题。这里应当注意的是,1、2、3级封锁协议都不能保证有效解决幻影读的问题。

2.3 建议

通过上面的几个例子,我们更深入的了解了数据库在解决并发一致性问题时所采取的措施。锁机制属于最底层的保证机制,但很难直接使用。我们可以通过不同的事务隔离模式来间接利用锁定机制确保我们数据的完整一致性。在使用不同级别的隔离模式时,我们也应当注意以下一些问题:

  • 一般情况下ReadCommitted隔离级别就足够了。过高的隔离级别将会锁定过多的资源,影响数据的共享效率。
  • 你所选择的隔离级别依赖于你的系统和商务逻辑。
  • 尽量避免直接使用锁,除非在万不得已的情况下。
  • 我们可以通过控制WHERE短语中的字段实现不同的更新策略,防止出现丢失的修改问题。但不必要的更新策略可能造成SQL命令执行效率低下。所以要慎用时间戳和过多的保护字段作为更新依据。

 

posted on 2006-02-14 15:35 吕震宇 阅读(6290) 评论(51)  编辑 收藏 所属分类: 数据库应用基础

评论

#1楼  2006-02-15 01:55 wch [未注册用户]
很喜欢

  回复  引用    

#2楼  2006-02-16 00:01 蜡人张      
吕兄可以考虑出书呵,肯定大卖,写得太精僻了!
  回复  引用  查看    

#3楼 [楼主] 2006-02-16 10:07 吕震宇      
@蜡人张

呵呵,是有这个想法,等全部写完后看看再说。
  回复  引用  查看    

#4楼  2006-02-17 10:42 atu123 [未注册用户]
很实在的好文章,感谢作者的无私贡献。
  回复  引用    

#5楼  2006-02-17 13:33 40hood [未注册用户]
共享锁又称为读锁。若事务T对数据对象A加上S锁,“则事务可以T读A但不能修改A”???,其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其它事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。


  回复  引用    

#6楼  2006-02-17 14:55 录一事流      
好文!!!
  回复  引用  查看    

#7楼  2006-02-17 16:31 gxh973121      
想问一下,如果象火车售票这样数据流量较大的系统,如何控制并发呢,一不小心会挂掉
  回复  引用  查看    

#8楼 [楼主] 2006-02-17 20:54 吕震宇      
@40hood

谢谢你的提示!确实在这里存在问题,应当是笔误。现在我改正过来了,改成“若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。”。
  回复  引用  查看    

#9楼 [楼主] 2006-02-17 21:06 吕震宇      
@gxh973121

这个我也不太清楚究竟是如何实现的。不过我想问题应当不是非常严重,毕竟一趟火车中票数是有限的。另外在和一个朋友聊并发问题时,聊到了类似的内容,他提出的建议给了我非常深刻的印象。

他谈的是医药批发。当你和顾客谈生意,顾客有意向购买比如1000件药品,此时你的库房还有1010件,你如何处理?如果考虑并发因素的话,假如在你签和约时,另外一个销售员将库房中的500件卖了出去,结果刚才查看库存时还有1000件,等真正卖时就剩510件了,怎么办?

当时我的想法是,当顾客有意向时,将1000件库存商品暂时锁定,不允许其它销售员访问,直到和约签完或放弃和约。结果我的朋友告诉我大错特错,商家的目的是利润,能卖一件是一件。如果你锁定了1000件的资源,结果没谈成,而导致另外一个销售员500件的生意泡汤。所以不到最后一刻不锁定任何资源。

我想火车站卖票应当是相同的道理,即使检索时还有票,但真正销售时就没准了。(这纯属我的猜想,不知道真实情况是否如此)。
  回复  引用  查看    

#10楼  2006-02-18 14:09 gxh973121      
就火车票问题,我想客户端是无法查询到准确数据的,如果要真正准确,就要用Serializable 范围锁定,防止鬼魅数据,但这是不可能的
  回复  引用  查看    

#11楼  2006-02-18 14:14 gxh973121      
我是指在查询剩余火车票数量的时候
  回复  引用  查看    

#12楼  2006-02-18 17:25 NeedForSleep      
我觉得,火车票也许是按座位锁定的。
记得有一次,放寒假回家,我去买票。我前面那位和我一样,是去同一个地方,他去问的时候,售票员查出没票了,但他买好无座票后,我也是同一趟车,居然又有了两张票。我当时的理解是,这两张票刚才是被其他售票窗口的计算机锁定着,当不要后,才被解锁。然后,我的这个售票窗口刚好又去访问,就得到了。
不知道是不是这种可能?
  回复  引用  查看    

#13楼 [楼主] 2006-02-18 20:32 吕震宇      
@NeedForSleep

也可能是有人退票,谁知道呢?毕竟锁定资源的代价太大了。
  回复  引用  查看    

#14楼  2006-02-18 21:55 gxh973121      
锁定肯定是要的,这种系统并发量太大,策略更新是不行的,我已基本想通,如同
NeedForSleep 所说,就是加锁,这也是没有办法的事
  回复  引用  查看    

#15楼  2006-03-03 16:27 xdd [未注册用户]
   写的太详细了,good!
  回复  引用    

#16楼  2006-03-05 11:46 liuyun [未注册用户]
感谢吕老师,我找了很久这样的资料都没有找到,今天终于解决了我很久以来的疑问。我想无论是卖药还是卖票的议题大家还应该讨论下去,最终得出一个可行的解决方案。
  回复  引用    

记得上学的时候,老师是用VFP给大家讲的。当时没有DEMO听起来很抽象,就和天数差不多。呵呵,虽然现在已经......但是看了以后还是很有收获!

曾经有一个...我们能珍惜,如果时间可以导流,我将......... 唉! 悔啊!
  回复  引用    

#18楼  2006-03-20 18:17 SW515(Unlogin)      
2级封锁协议
2级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。2级封锁协议除防止了丢失修改,还可进一步防止读"脏"数据。

3级封锁协议
3级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。3级封锁协议除防止了丢失修改和不读'脏'数据外,还进一步防止了不可重复读。
===============================

“3级封锁协议是: 1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,”这句话是不是和 2级封锁协议 中的第一句话重复了?? 不解~ 敬请明示。
  回复  引用  查看    

#19楼  2006-03-20 18:19 SW515(Unlogin)      
哦,明白了。原来一个是读完后即可释放S锁,而3级封锁是要等到整个事务结束后才释放该S锁。3Q...
  回复  引用  查看    

#20楼  2006-03-20 22:08 草 [未注册用户]
始终不能明白为什么当tx1和tx2都为RepeatableRead级别时不会出现阻塞。
tx1读数据,可是并没有提交事务,这个时候tx2修改不是应该会阻塞的吗?

  回复  引用    

#21楼  2006-03-21 21:49 xuwen      
一张表只能使用一个X锁,可以拥有多个S锁,
使用X锁时其它事务操作不能对此表进入读写操作,
使用S锁时其它事务操作可以进行读,但不能写的操作

  回复  引用  查看    

#22楼  2006-05-17 15:02 哈哈:) [未注册用户]
(我也有这个疑问:)
始终不能明白为什么当tx1和tx2都为RepeatableRead级别时不会出现阻塞。
tx1读数据,可是并没有提交事务,这个时候tx2修改不是应该会阻塞的吗?

  回复  引用    

#23楼  2006-06-03 16:23 roy1 [未注册用户]
這樣的講解風格和貢獻精神讓人感動﹐非常感謝﹐講解的非常清楚﹐希望看到你更多精辟的講議和所出的書籍。
  回复  引用    

#24楼  2006-07-13 13:33 仰天一笑      
如果大学中能遇到吕老师,我大概彻底献身技术,哈哈
  回复  引用  查看    

#25楼  2006-08-08 15:10 永远的不倒翁      
吕老师,最近很忙吗?
期待你的后续文章中。。。
  回复  引用  查看    

#26楼  2006-08-11 11:37 wonder      
这种文章mssql帮助文件里面很多...

期待老师...写一些关于锁范围的文章....................


  回复  引用  查看    

#27楼  2006-08-13 17:16 niuniu [未注册用户]
您在大作中提到:
--------------------------------------------------
1级封锁协议是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。1级封锁协议可防止丢失修改,并保证事务T是可恢复的。在1级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和不读"脏"数据。
--------------------------------------------------
既然1级封锁“事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。”,如果T事务修改->保存->撤销,也没有谁可以在中途读到其修改并保存的数据,也即是,谁也不会拿到"脏"数据。那么1级封锁协议为什么“不能保证不读"脏"数据。“?


  回复  引用    

#28楼  2006-08-22 12:06 FREND1008 [未注册用户]
吕老师,我想学习数据库编程,我学的是计算机信息管理(大专),现在我不知从哪学起,特请您指教。
  回复  引用    

#29楼  2006-09-15 12:48 asd [未注册用户]
我觉得无论哪种隔离级别好象都不能解决丢失的修改问题:因为无论哪种隔离级别,别人都可以同时来读,例于张三和李四都读了R,张三先提交,李四后提交,李四就会覆盖张三的修改,出现这种情况怎么办呢?在.net中应该怎么解决呢?
  回复  引用    

#30楼  2006-10-14 18:56 摸索中 [未注册用户]
从中吸取了精华,就这样默默的走了太不人道了,所以走之前留点东西!
真的很谢谢!
  回复  引用    

#31楼  2006-11-05 14:36 陈小心 [未注册用户]
真的多谢了,先顶了再看
  回复  引用    

#32楼  2006-11-05 14:59 陈小心 [未注册用户]
请问吕老师,如果在vfp中锁定sqlserver中的行呢?
  回复  引用    

正在找这方面的资料,呵呵
又学了一招,谢谢啦
  回复  引用    

#34楼  2007-02-01 21:41 木木三 [未注册用户]
(我也有这个疑问:)
始终不能明白为什么当tx1和tx2都为RepeatableRead级别时不会出现阻塞。
tx1读数据,可是并没有提交事务,这个时候tx2修改不是应该会阻塞的吗?



看了以后也有这个疑问
请麻烦回答
就是在最后的第三个例子当中
按照第二个例子当中
TX1读的时候就加锁了
那TX2应该不可以再修改数据..
为什么这时候还可以插入呢?
疑惑中

  回复  引用    

#35楼 [楼主] 2007-02-06 14:00 吕震宇      
@木木三
“TX1读的时候就加锁了”--锁的是旧数据。
“那TX2应该不可以再修改数据.. 为什么这时候还可以插入呢? ”--对旧数据的锁并不能阻止新数据的进入。
  回复  引用  查看    

#36楼  2007-02-15 13:47 木木山 [未注册用户]
非常清晰了
谢谢老师指导
  回复  引用    

#37楼  2007-02-21 17:45 ahjoe.com [未注册用户]
前面回复中提到的那个卖药的问题,我想,不到签合同的时候不需要锁定吧。

签合同前的那一刻,再锁定。如果锁定后发现数量不够,合同就不用签了,解锁。
如果合同签了,也解锁。
  回复  引用    

#38楼  2007-02-26 15:26 wengwen [未注册用户]
@吕震宇
写的太好了
  回复  引用    

#39楼  2007-04-11 10:18 Lucie [未注册用户]
"我们可以通过控制WHERE短语中的字段实现不同的更新策略", 这句话的意思是什么呢?
我还想问一下,如果我的Transaction中包含复杂的select 语句以及update语句,其中select主查询包含多个子查询,并且涉及多张表,如下
SELECT
JobA_Id,
dbo.fn_job_sequencedate(JobA_Id,GETDATE(),T1.EndDate,NULL)
FROM dbo.JobA T1
INNER JOIN dbo.JobB T2 ON T1.Job_Id = T2.Job_Id WHERE
T1.Updated > T2.Updated
AND NOT EXISTS
(
SELECT *
FROM dbo.JobC TT1
INNER JOIN dbo.JobD TT2 ON TT1.Status = TT2.JobD_Id
WHERE
TT1.JobC_Id = T1.JobA_Id AND
TT2.IsCompleted = 0
)
而且执行时给予Serializable的隔离级别,我的问题是:
在这个查询完成时,他会在子查询所涉及的表的行上加S锁(还是也是RangeS-S?)并保持到整个Tran结束才释放吗?还是读完就释放了,只是保持主查询所涉及的表行上的RangeS-S?

  回复  引用    

#40楼 [楼主] 2007-04-11 22:00 吕震宇      
@Lucie

1、"我们可以通过控制WHERE短语中的字段实现不同的更新策略",是我在上一篇文章中所谓的“关键字”、“关键字和已修改字段”、“关键字和可更新字段”、“关键字和时间戳”以及其它的Where短语生成策略,是解决丢失的修改的手段之一。

2、你提到的问题太复杂了,我手边也没有测试数据库,而且你的查询中似乎还用到了用户自定义函数。所以我很难给出答案。只能猜测一下,我想子查询会被加Range锁,否则将无法保证Serializable的事务隔离级别。但仅仅是猜测而已,你可以参考本文的代码,自己设计一段程序,并在企业管理器中查看一下究竟是什么锁。
  回复  引用  查看    

#41楼  2007-05-10 20:47 mama [未注册用户]
既然1级封锁“事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。”,如果T事务修改->保存->撤销,也没有谁可以在中途读到其修改并保存的数据,也即是,谁也不会拿到"脏"数据。那么1级封锁协议为什么“不能保证不读"脏"数据。“?


  回复  引用    

#42楼  2007-05-16 16:03 Ricky [未注册用户]
@mama

With an exclusive (X) lock, no other transactions can modify data; read operations can take place only with the use of the NOLOCK hint or read uncommitted isolation level

So we can read under 1st level locking protocol which will result in "dirty read"
  回复  引用    

#43楼  2007-06-29 13:10 Topboy      
哇塞,请问这些资料怎么找的阿,能教我吗 ?
  回复  引用  查看    

#44楼  2007-08-16 17:37 Archer [未注册用户]
这里事务隔离级别只是起到对读操作进行的限制吧 ?加锁都是sql server对特定语句加特定的锁 好比 update操作的时候就会自动加上排它锁 其他事务在此事务完成前不能对这个表做任何操作,而如果是select的话就加的共享锁 其他事务可对此表进行读的操作,而不能有写的操作?但很奇怪的是我为什么实验的时候 可以写?
  回复  引用    

#45楼  2008-05-23 13:24 asdasdasd [未注册用户]
@ahjoe.com
根本不符合业务
  回复  引用    

用SqlServer 2000来测试的话是回阻塞的,因为修改数据会造成两次读的结果不一致。

RE: 始终不能明白为什么当tx1和tx2都为RepeatableRead级别时不会出现阻塞。 tx1读数据,可是并没有提交事务,这个时候tx2修改不是应该会阻塞的吗?


  回复  引用    

这种情况数据库本身不能解决,因为进行的都是两个合法的事务。但是可以在设计的时候考虑好,参考下面订票系统的做法。

RE:我觉得无论哪种隔离级别好象都不能解决丢失的修改问题:因为无论哪种隔离级别,别人都可以同时来读,例于张三和李四都读了R,张三先提交,李四后提交,李四就会覆盖张三的修改,出现这种情况怎么办呢?在.net中应该怎么解决呢?


  回复  引用    

楼主例子的解决办法很简单,更新数据库的时候利用数据库当前的值减1即可,而不是用自己算出的余额。
譬如:Update Flight set AvailableTickets = AvailableTickets – 1 where flight_number = “MU566”
而不是用:Update Flight set AvailableTickets = 15 where flight_number = “MU566”


这里多列举一个针对火车票订票系统的:

飞机票只管总数,到了机场在确定座位,火车票不是,是售票的时候就确定了座位,所以计数据库一趟车的每张票都会在表有一条记录,假设表就是Tickets,那么会有一个字段Available标记这张票是否被卖出。假设甲和已分别检索到这张机票是Available的,想要卖这张票,那么这个Transaction应该怎么写呢?这儿列出针对买票员点击了售票按钮以后的事务:
set transaction isolation level repeatable read
Begin Tran:
1. Update Tickets Set Available = False where Train_No = ‘D211’ and Seat_No = 110 and Available = True;
D211列车110号座位,注意Available = True条件非常重要,如果两个事务并发执行这条语句,第二个事务会在这儿阻塞,等第一个事务提交完成后,第二个事务会执行成功,但是不更改任何数据,根据这样的执行结果,我们其实知道第二个订票失败了。

2. Commit

  回复  引用    

吕老师,写的太好了!!!!
  回复  引用    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-02-17 20:51 编辑过


相关链接: