“死锁”的定义:
当某组资源的两个或多个线程之间有循环相关性时,将发生死锁。
这里面有一个关键字需要注意——“循环”。也就是说,真正意义上的“死锁”必然存在着循环引用。A需要的资源B正使用中,因此A不能提交;然而B也在等待着A提交以便释放B所需要的资源,才会发生“死锁”。其实这种循环引用不只是在关系数据库管理系统中存在,操作系统中也同样存在。而今天我们要讨论的不是这种“死锁”,而是“阻塞”。 也就是当一个事务锁定了另一个事务需要的资源,第二个事务等待锁被释放,这种情况下,第二个事务是被“阻塞”了而不是形成了“死锁”,但由于这种情况下,第二个以上的事务无法正常继续运行,类似于“死锁”的状态,必然影响了程序正常运行。这时,如果打开SQL Server的企业管理器,依次展开服务器节点的“管理”-“当前活动”-“锁/进程 ID”可以看到阻塞和被阻塞的进程上有醒目的红色标记。这时,除非执行第一个事务的程序退出,否则第二个事务一直处于等待状态,形同死机。我们的系统遇到的“死锁”一般属于这种情况。
根据上面的描述,我们不难理解“阻塞”的原因。不妨做个比喻,如果一个人(事务)要去上厕所,厕所里有一定数量的马桶(表或其它资源),这个人上厕所占用了一个马桶,这就是“锁”定了一个“表”,那么后进来的人肯定不能使用该马桶,除非等到这个人离开。如果这个人一直长时间不离开(有点可笑,但确实很多时候是由我们自己的失误造成),那必然导致后续一系列矛盾冲突的发生了。由此可见,“锁”是正常的,数据库的机制决定了要保证数据的完整性就必然产生锁定和释放的现象,锁并不可怕,锁定表并没有问题,但如果锁定资源一直不释放,那就有可能产生阻塞(甚至产生死锁)。
阻塞是如何造成的
(1)程序的漏洞。在PB编程中,我们一般通过数据窗口与数据库交互,有时也会直接使用嵌入式SQL语句直接操作数据库。数据窗口执行了Update方法后,我们必须根据其返回值来决定是否进行提交(Commit)或回滚(Rollback)才能真正结束这个事务,释放操作所锁定的表或记录。这是很明显的道理,但在很多情况下,我们还是会犯这种似乎是不应该犯的错误。比如:
a.写错了事务对象。本来是应该提交A事务对象,写成了B或者忘记写(不写PB会将其默认成SQLCA事务对象)。由于我们的518系统牵涉到三个事务对象(ERP_SYS_MESSAGE、ERP_SET_MESSAGE、SQLCA),因此如果不注意的话就会犯这种错误。
b.在函数嵌套情况下忽略了事务处理,或对IF判断语句路径的处理不严密。例如,在主程序中,我们调用了一个函数,此函数中有对数据库进行Update的操作,依程序员本意来讲,他没有在函数中做提交或回滚,是希望在主程序中统一进行提交。不幸的是,主程序中却是根据数据窗口是否有更新来决定是否提交的,如果数据窗口有更新情况下,Update,成功则提交,失败则回滚。这似乎没有问题,然而如果用户一进来直接就执行主程序,并未对数据窗口操作呢?这样自然在调用函数中执行了语句,锁定了表,由于后面判断数据窗口并未更改,因此也没有提交或回滚操作。这是比较奇怪的锁表现象之一。
c.函数阻断执行问题。这也是比较常见的、初学者易犯的错误之一。基本情况是:判断SQLCA的返回值是否为-1,如果为-1表示出错,这时,如果不掌握事务的知识,就会写一个MessageBox函数,提示用户相关出错信息,然后Rollback……一般情况下这不会出现问题,关键问题是,MessageBox是一个隔离型的函数,所谓隔离型是说如果Messagebox这个函数调出的对话框没有被响应,那么它就会一直阻止程序向下执行。如果正好这个操作是一个耗时较长的操作(如物料需求运算),而用户又离开了计算机,不能及时响应这个对话框。系统就会一直处于等待状态,而不会回滚。相关的表资源也就一直处于占用状态。再有第二个事务进入时,阻塞产生。
(2)SQL Server本身对锁处理的问题。说到这里要谈一下锁的“粒度”。SQL Server具有多粒度锁定,允许一个事务锁定不同类型的资源。为了使锁定的成本减至最少,SQL Server自动将资源锁定在适合任务的级别。锁定在较小的粒度(例如行)可以增加并发但需要较大的开销,因为如果锁定了许多行,则需要控制更多的锁。锁定在较大的粒度(例如表)就并发而言是相当昂贵的,因为锁定整个表限制了其它事务对表中任意部分进行访问,但要求的开销较低,因为需要维护的锁较少。按照粒度增加顺序依次为:RID(行标识符)、键、页、扩展盘区、表、DB。一般情况下,使用SQL语句读取数据时(或使用数据窗口的Retrieve方法提取数据),用到的是页级或行级锁。也就是说它读完一页就会释放再读下一页。但如果对一些数据量比较大的表,出现的锁比较多,SQL Server会自动升级为表级锁,对整个表进行锁定。这也没什么问题。关键是在有些情况下(目前不知道是哪个版本解决的),SQL Server会升级表锁而用完不释放!这就会产生问题,遇到这种情况,可考虑给SQL Server打SP4补丁,经试验打上Sp4后不再出现读取数据锁表问题。
(3)隔离级别设置问题。事务准备接收不一致数据的级别称为隔离级别。隔离级别是一个事务必须与其它事务进行隔离的程度。较低的隔离级别可以增加并发,但代价是降低数据的正确性。相反,较高的隔离级别可以确保数据的正确性,但可能对并发产生负面影响。应用程序要求的隔离级别确定了SQL Server使用的锁定行为。SQL-92定义了“未提交读”、“提交读”、“可重复读”、“可串行读”四种隔离级别,SQL Server支持这四种级别(关于这些隔离级别的论述参考SQL Server的联机丛书),并默认为“提交读”。在这种隔离级别下,正常的读取操作是不会锁定表的,但是如果在SQLCA的Lock属性中指定了更严格的隔离级别,就可能导致在读取过程中一直锁定整个表造成其它事务的等待(有时这也是必须的)。在Select语句后使用Holdlock关键字也会显式地指定数据库在读取数据期间锁定整个表。默认情况下,SQLCA的Lock属性不用修改,但如果怀疑是这个属性出了问题,可在PB中调试时检验Lock属性值是不是被修改。(Lock=‘RC’是针对SQL Server的默认值)。
上文中总结了程序中可能遇到的几种造成资源占用和阻塞的情况。对这些情况有了清晰的了解后我们就很容易进行防范了。首先还是编程的逻辑要严谨,避免一时疏忽造成的程序错误;其次,采用统一的编程风格、养成良好的编程习惯也有助于发现和解决锁表问题。最后就是SQL Server数据库的版本及事务对象的隔离级别设置(不太常见,但数据量较大时应当多加留意)。