提高Sql Server数据库分布式事务速度性能的经验总结

浙江恒生平易科技有限公司 苟安廷

Sql Server数据库在企业里面使用非常广泛,为保证数据的一致性,数据库中经常会用到事务,所谓事务,简单说就是一组“写”操作,必须都成功才算成功,任何一个环节出现错误,事务必须回滚,相当于全部失败,不允许部分成功部分失败,举个例子,比如张三要给李四转100元,第一步,从张三的账面上扣100,第二步,给李四的账面上加100元,两个都成功了才算成功,不能张三扣100成功了,但李四加100失败了,从而导致张三的100元不翼而飞了,一个典型的事务操作如下:

--事务开始

BEGIN TRAN

    --1.先扣除张三100

    UPDATE  账号信息 SET 余额=余额-100 WHERE 账号信息='张三AND 余额>=100

    --扣除失败,事务回滚

    IF @@ERROR<>0 OR @@ROWCOUNT<>1

    BEGIN

       ROLLBACK TRAN

       RETURN

    END

   

    --2.再把李四的账面上加100

    UPDATE  账号信息 SET 余额=余额+100 WHERE 账号信息='李四'

    --增加失败,事务回滚

    IF @@ERROR<>0 OR @@ROWCOUNT<>1

    BEGIN

       ROLLBACK TRAN

       RETURN

    END

--操作完成,没有错误,提交事务

COMMIT TRAN

当然,以上只是模拟的,只是为了说明什么是事务,这样的事务写法非常常见,我就不再赘述了,今天要说的是如何提高分布式事务性能问题。

上面的事务,是在一台服务器里面的数据库中完成的,因为业务需要,部分场合需要多台服务器协作,不仅仅是一台服务器要通过事务保证数据的一致性,而是要求相关的多台服务器都要成功才算成功,这种事务,我们称之为“分布式事务”,要在多台服务器之间实现分布式事务,首选需要彼此之间建立“链接服务器”,具体操作方式这里不做介绍,如果你还没有相关经验,建议百度一下,为描述方便,我们假设已经有两台数据库服务器A和B,互相和对方建立了链接服务器,在A服务器上,链接到B服务器名称为“LinkToB”,同样,在B服务器上,链接到A的链接名称为“LinkToA”。分布式事务和一般事务语法上没什么大区别,就是多了一个关键字“DISTRIBUTED”而已,即:

--事务开始

BEGIN DISTRIBUTED TRAN

    数据库操作语句1....

    数据库操作语句2....

    ......

    数据库操作语句N....

COMMIT TRAN

如果连接到A服务器上,要操作B服务器上的表,参考语法如下:

UPDATE  [LinkToB].[DBName].[dbo].[TableName] SET Field1='new value' WHERE id=5

显然,语法上和本地差别不大,只是告诉数据库引擎,这个是要操作链接服务器上,某个数据库下面的某个表,由于需要操作其他服务器,其性能远远低于在本地服务器上操作,尤其是在网络条件不好时(比如租用第三方带宽),多台服务器进行分布式事务很容易拖垮整个系统。

由本人组织开发的汽车站售票系统,为便于多车站联网售票、统一对外、动态补员等业务需求,采用了“集中分布式”架构,存储过程中大量用到了分布式事务,但国庆客流井喷,该架构性能出现了瓶颈,虽然不至于导致车站瘫痪,但用户体验急剧下降,甚至官方互联网售票平台“巴巴快巴”都无法正常出票,造成了不良的影响,为此,团队针对并发性能不足问题,进行了深入探讨,在后期改进后的极限压力测试中,性能得到了成倍提高,为此,下面将整个项目研发中(不仅仅是本次优化)关于如何提高Sql Server分布式事务性能做一总结,希望能给有相同困惑的朋友一些借鉴,数据库设计的一些常识,如索引、关联、约束、链接服务器等,限于篇幅,这里就做探讨了。

提高Sql Server数据库分布式事务速度性能的经验总结_第1张图片

特别说明,文中的代码通常无法真正执行,且相关业务也十分不严谨,仅仅是为了简洁、直观表达和突出重点!

1. 更好的硬件,更好的网络

废话,谁都知道硬件能弥补软件性能的不足,但硬件是要钱的,网络按月付费,更贵,企业的钱也不是大风刮来的,升级硬件就像……,哎呀,谁扔的鸡蛋……

2. 更强劲的数据库引擎

用过Sql server2000的都知道,该数据库开久了性能急剧下降,而Sql Server 2005在稳定性上有了质的飞跃,Sql Server 2008已经非常非常强悍了,但大家内心里面,都潜意识地认为,高性能大容量数据库用Oracle,一般企业采用Sql Server,据说(也只是听说,没有验证过),Sql Server 2014已经具备和Oracl叫板的能力了,性能更是提高了10倍左右,不管是真是假,选择更强劲的数据库引擎,比如2014版本,肯定不会错,当然,你要纠结于Sql Server 2014更贵,就当我没说,或者说,如果企业有钱或者一直用的“免费”版,不妨升级一下。

3. 将表锁变成行锁

事务之所以可以回滚,是因为事务开始后,凡事改过的内容都被加锁了,别人无法操作,所以,需要回滚时才有条件还原,锁的种类很多,这里不探讨,只要知道操作开始,系统会自动加锁就可以了,我们看看下面的语句:

UPDATE 表名 SET 字段=新内容 WHERE 条件表达式

在这个执行过程中,满足条件表达式的记录都会被改成新的值,根据我们的想象,一个人执行上面的操作时,锁住了满足条件的记录,防止别人操作,另外的人操作其他记录是没有关系的,也就是我们内心里面,认为这是“行锁”,然而事实上,Sql Server采用的是“表锁”,也就是说,一旦某个人执行了上面的操作,不管改了几条记录,在完成之前,另外的人就只能排队等了,这样很就容易造成并发冲突,我们得想办法把表锁变成行锁,操作很简单,加一个选项“WITH(ROWLOCK)”即可:

UPDATE 表名 WITH(ROWLOCKSET 字段=新内容 WHERE 条件表达式

把表锁变为行锁,除了加选项,还需要一个条件,就是该表必须有主键,这点切记!!!你可以很容易测试行锁和表锁,打开Sql Server Management Studio,连接到数据库,然后打开两个查询窗口,每个查询窗口其实就是一个连接,在一个窗口中执行:

BEGIN TRAN

   UPDATE 表名 SET 字段=内容 WHERE 条件表达式

   --强制等待10秒钟

   WAITFOR DELAY '0:0:10'

COMMIT TRAN

然后在第二个查询窗口中,UPDATE同一张表,当然,条件表达式过滤的记录不能和第一个查询窗口的记录重叠(这点不难理解吧),执行一下,你会发现,没有WITH(ROWLOCK)选项时,第二个窗口的操作会一直等第一个执行完了,才能执行完成,而添加了WITH(ROWLOCK)选项后,第二个窗口立即就执行完成了。再次强调,以下三种情况,还是会引起表锁:

(1)      两个过滤条件有重叠的记录

(2)      表没有主键

(3) 修改的内容和其他记录有关,比如修改了唯一索引对应的字段

还有一种情况,如果第一个查询窗口要修改的值和原来的值一样,也就是实际没有改变,那么,第二个查询窗口也会很快执行完成,也就是窗口1根本没有锁表。

将表锁变成行锁,对UPDATEDELETE有效,但对INSERT却不起作用,不能不说是个遗憾。

4. 查询时,尽量忽略锁

一个业务操作过程中,绝大部分都是查询,真正写的机会并不多,如果表被锁住了,查询也会排队,对于业务来说,查询对数据严谨性要求并不是那么高,更新时进行强制性验证效果更好,为此,我们需要在SELECT语句中使用WITH(NOLOCK)选项:

SELECT 字段列表 FROM 表名 WITH(NOLOCKWHERE 条件表达式

这时,SELECT就不等待其他操作是否完成了,当然,查询的结果可能有写脏读的数据,但概率极低,故我们在用到这些数据更新到数据库时,就要求严谨一些了,比如,查询时,余额还有100元,现在需要扣100,我们查询时账号余额是够的,可以直接用:

UPDATE 账户表 SET余额=余额-100 WHERE ID=5

IF @@ERROR<>0OR@@ROWCOUNT<>1

BEGIN

        ROLLBACKTRAN

        RETURN

END

但仔细一想,可能有问题,虽然我们查询的时候余额充足(也可能正在被人更新),但正式提交时,可能数据不一致了,故我们需要在更新语句中再次判断余额,把UPDATE语句改成如下的:

UPDATE 账户表 SET 余额=余额-100 WHERE ID=AND 余额>=100

这样一来,就不会出现扣款后余额为负数的情况了。

5. 尽量不要对远程服务器上的表进行写操作

比如当前用户连接的是A服务器,现在需要操作B服务器上的表,如上文中说的,我们可以这么写:

UPDATE [LinkToB].[DBName].[dbo].[TableName] SET Field1='new value' WHERE 操作时间 BETWEEN  @Dt1 AND @Dt2

在数据少的时候,可能感觉不明显,但一天几十万客流的时候,虽然我们只更新其中一点点数据,但这个操作也会消耗大量资源,甚至直接拖垮系统,你可以测试一下,在本地服务器执行:

UPDATE  [TableName]  SET Field1='new value 'WHERE 操作时间 BETWEEN @Dt1 AND @Dt2

上面这句话直接在当前服务器执行,配合索引,瞬间即可完成,但通过链接服务器执行,需要很长时间,并发时,很容易造成死锁。

既然如此,就的想办法把这句话扔给B服务器去执行,我们可以这么操作:

DECLARE @sql NVARCHAR(4000)

SELECT @sql='USE DBName

UPDATE [TableName] SET Field1=''new value''  WHERE 操作时间  BETWEEN '''+CONVERT(NVARCHAR(20),@Dt1,120)+' AND '+CONVERT(NVARCHAR(20),@Dt2)

EXEC (@sql)AT[LinkToB]

也就是说,我们把需要执行的SQL语句组合成一个字符串,然后把字符串扔给B服务器,由B服务器在本地引擎执行,这样速度可以得到极大的提高,和在A服务器本地执行几乎没有区别。

当然,对于一个具体业务,你也可以在B服务器上新建存储过程,由A服务器调用该存储过程,并传人参数,效果一样,这里要表达的意思是,对于写操作,一定要由本地数据库引擎去执行。

AT关键字是Sql Server 2005以后版本才有的,如果是SqlServer 2000,可以在远程建一个存储过程,在存储过程里面执行,如:

CREATE PROCEDURE ExecSql

        @SqlNVARCHAR(4000)

AS

BEGIN

        SETNOCOUNTON;

        EXEC(@Sql)

END

我们远程调用该存储过程也可以起到相同的效果:

EXEC [LinkToB].[DBName].[dbo].[ExecSql] @Sql=@Sql

6. 尽量在本地生成数据

业务过程中,会产生大量的数据,如果能在服务器本地生成,就尽量不要在远程生成,然后传送到本地。比如售票员注册一个票段,有1万张票,那么,会产生1万条可用车票的记录,且需要保存到A、B两台服务器上,为了简单,我们可以在A服务器上生成1万条记录到临时表,然后插入到A服务器,再插入到B服务器,这时,问题来了,A服务器是本地插入,很快,而B服务器是远程的,插入时,一定会把A服务器这1万条记录传过去,需要消耗大量的网络资源,造成性能低下。我们可以换一种方式,两台服务器上,各写一个存储过程,我们传人起始票号、结束票号,由两台服务器数据库引擎在其本地生成1万条记录,再插入到正式表,就可以大大加快速度,经过实测,在生产环境(4M带宽)中,优化前,注册3万张票,需要3分钟以上的时间,都浪费在数据传输上了,优化后,只要几秒钟。

优化前类似如下代码:

CREATE TABLE #temp(票号 BIGINT,其他字段列表)

WHILE  @i<10000

BEGIN

    INSERT #temp(票号,其他字段列表)VALUES(XXX,XXXX.....)

    SET@I=@I+1

END

 

BEGIN DISTRIBUTEDTRAN

    --插入到本地,速度快

INSERT INTO可用票号SELECT*FROM#TEMP

    IF @@ERROR<>0 OR@@ROWCOUNT<>10000

    BEGIN

       ROLLBACK TRAN

       RETURN

   END

    --下面这句话会把临时表的数据传到对方去,非常耗时

    INSERT INTO [LinkToB].[DBName].dbo.可用票号

    SELECT *FROM #temp

    --其他相关语句

    ....

--提交事务

COMMIT TRAN

显然,直接把1万条记录传到B服务器,占用了大量带宽,耗时较多,且在事务里面,很容易造成死锁。

优化方法原理,在两台服务器都写一个存储过程,事务开始,用起止票号作为参数调用两台服务器的存储过程,使其在本地完成注册操作,我真正的做法是写了两个存储过程,一个是准备记录,也就是两边各生成1万条记录,放入专门的一张准备表,准备完成后,事务开始,调用另外一个存储过程,把刚才准备的生产记录插入到正式表,然后提交事务,这样以来,准备数据比较耗时的过程,不放人事务,仅仅生成1万条记录在一个备用表里面而已,事务开始,两边把备用表的数据插入到正式表,瞬间就可以完成,示意代码如下:

存储过程1:

CREATE PROCEDUREPrepare

    @Start BIGINT,

    @End BIGINT

AS

Begin

    WHILE @I<@End

    BEGIN

       INSERT INTO 准备表 SELECT@I

       SELECT @I=@I+1

    END

END

 

存储过程2内容:

CREATE PROCEDUREDone

AS

BEGIN

    INSERT INTO正式表 SELECT*FROM准备表

END

注册时,先调用两台服务器的存储过程1,这一步不需要事务,然后开始事务,调用两台服务器的存储过程2,类似于:

--本地生成1万条记录到准备表

EXEC Prepare1,10000

--远程自己生成1万条记录到自己的服务器

EXEC [linkToB].DBName.dbo.Prepare1,10000

--开始事务,每台服务器从自己的准备表插入到正式表

BEGIN DISTRIBUTED

    EXEC DONE

    EXEC [linkToB].DBName.dbo.done

COMMIT TRAN

当然,第二个存储过程这里可以省略,参照第5点的方法,组合一个SQL语句,扔给对方去执行。

7. 数据交互时,切忌直接向远程插入数据

必须向远程插入数据时,请不要直接插入,而是由远程过来读取,也就是说,下面两句话的效率有天壤之别:

(1)      往远程直接插入

INSERT INTO [LinkToB].DBName.dbo.TableName

SELECT * FROM 生产记录表 WITH(NOLOCK)

(2)      从远程读,在本地插入

INSERT INTOTableName

SELECT * FROM  [LinkToA].DBName.dbo.生产记录表 WITH(NOLOCK)

方法(2)快得多,车站需要每天生成可售的班次、票价、座位号等数据,优化前,为了省力,在一台服务器(中心服务器)上把生产数据准备好,然后插入到各个车站服务器,大约需要10几分钟,节假日数据量大,最多需要半个小时,优化后,整个过程1分钟不到。

8. 尽最大可能减少事务执行时长

大家都知道,当车流和马路情况不变时,车速越慢,越容易堵车,如果车子跑得快,嗖的一下就没影了,那么,是很难堵车的,也就是相同情况下,车速越快,单位时间通过的车辆越多。以此类推,事务是要锁表的,硬件情况一定的情况下,事务执行的越快,就能支持更大的并发,死锁的概率就越低,因此,我们要想方设法减少事务的执行时间。

我们先来看一个典型的业务执行过程:

① 数据合法性验证

② 创建生产记录临时表

③ 将临时表插入到远程正式表

④ 将临时表插入到本地正式表

以上4个过程,②是可选的。如果把以上几个过程都放入事务,显然不合理,我们真正需要放入事务的,其实是③和④,由于远程相对慢一些,所以,先操作远程的,这样可以尽量减少锁本地表的时间,如果先操作本地的,那么,在操作远程服务器时,本地表一直被锁着,显然不划算,这里有个例外,如果本地和远程都相互都有写操作时,必须先写同一台,再写另外一台,而不是先远程再本地了,防止每台服务器都写了对方,然后写自己时,发现被对方给锁住了,换句话说,写的服务器顺序首先要一致,如果互相不冲突时,可以先远程再本地。

但第③步隐藏了一个步骤,就是把数据传输到远程服务器,这一步非常耗时,导致远程的表被长时间锁住,因此,我们必须进行优化,我的做法是:

① 数据合法性验证

② 不要创建临时表,而是放入一个真正的表,但和正式表结构基本相同的备用表,我们内部称之为准备表,用途相当于临时表,只不过临时表不能持久化,而准备表本身是真实的表,可以持久化而已。

如果每台服务器都能根据业务在本地创建准备表内容,那最好,调用远程的存储过程,让其自己准备,如果远程无法自己创建相关内容,那么,本地先插入数据到准备表,然后调用远程的存储过程(也可以写一个SQL语句,在远程执行,参见第5点),通知远程服务器从本服务器来读取准备表内容到他自己的服务器里面,之所以不直接插入到远程准备表,原因参见第7点。

③ 事务开始,通知远程把准备表写入正式表

④ 本地把准备表写入正式表,提交事务

整个过程说穿了很简单,就是前面做好相关准备,事务开始,双方各自把本地的准备表数据插入到正式表。这样,前期比较耗时的判断、准备、传输等,都在事务之外了,真正事务开始,就是把本地准备表的内容插入到本地正式表,因为都在本地执行,所以速度非常快,大大降低了并发死锁的的概率。

9. 聚集索引尽量不要设置在需要更新字段上

索引是提高数据库检索的重要保证手段,其中,聚集索引逻辑顺序和物理顺序一致,速度最快,正因为物理顺序和逻辑顺序一致,故每张表只能设置一个,非常珍贵,通常用在查询频繁的字段中,但不能滥用,如果用在需要更新的列上面就适得其反了,因为数据更新后,必须重新计算存储位置(物理顺序要改变),效率就非常低了。

10.             减少表中数据量

显然,一张表如果记录数很多,要增删改查一定相对会慢,数据越多,差距越大,因此,我们要尽量减少表中的数据量,但生产数据总不能扔掉吧,我们得想办法解决。售票系统中,售票记录最为庞大,但售票记录有一个特点,今日之前的数据,通常就不会再修改了(发车日期过了),因此,我们建一个一模一样结构的历史表,把今日之前的数据,移到历史表中,那么,当前表里面的内容就少了很多了,仅仅是今日售出的,或之前预售但发车日期在今日之后的了,这样后面的操作速度就快很多,当然,统计分析时,要考虑从两个表里面取数据了。

如果历史表过于庞大,还可以考虑分服务器、分库,本人组织开发的另外一个GPS平台,就把海里的定位信息分摊到多台服务器上,每年一个库,每天一张表,同样大大提高了操作效率,扯远了,就此打住。

当然,把当前库的数据移到历史库里面去,通常是在凌晨自动完成,大白天肯定会影响生产的。

11.             合理分配资源

既然是分布式事务,一定有多台服务器,那么,每台服务器的资源要合理分配,不要闲的闲死,忙的忙死。介绍这一点之前,先看看我们售票系统的数据库服务器部署示意图:

提高Sql Server数据库分布式事务速度性能的经验总结_第2张图片

总共设置1台中心服务器,主要用于协调资源,每个车站设置一台自己的服务器,显然,中心服务器和每个车站相通,资源丰富,为简化编程,前期我把什么事都直接扔给中心,方便嘛,谁让他资源丰富呢?造成的结果就是中心服务器(1台)要处理大量业务(含对外接口服务等),而车站服务器(每个车站一台),就是当当二传手,把售票员的指令转给中心,然后存储一下生产数据,利用率极低。显然,这种做法虽然编程省力,但资源分配严重不合理。后来修改了算法,车站处理本车站的主要业务,如果需要其他车站协调,如联网售票、补员时,才把请求发给中心,这样,中心负荷大大减轻,服务器负载相对更科学合理。

 

12. 去掉不必要的分布式事务

并不是所有数据都需要分布式事务,一些对其他业务影响不大的,在本地完成,然后空余时间(如凌晨)同步过去,以售票系统为例,驾驶员报班信息等就没必要实时传到中心,车站里面有就可以满足业务需求了,因为分布式事务需要多服务器协调,速度慢,也可以将分布式事务变成多个本地事务,这样,每个事务在本地执行,由于各自独立执行,可能会数据不一致,故需要补偿机制,确保一致。

13. 合理拆分表

如果一张表涉及到多个高频业务的写操作,可以考虑将此表拆分成多个表,如“售票记录”表,在售票、取票(电子票可以在车站换取实体票)、检票中都要用到,我们可以拆分成两张表:基础信息表和附加信息表,二者通过主键关联,基础信息内容是不变的,售票时插入,而附加信息表在后续的取票、检票、退票等业务中会改变,也就是说,基础表只有插入操作,后续的修改不会再涉及该表,从而减少了因其他业务修改(行锁)时导致插入阻塞,或者售票时插入锁表,导致其他业务更改时阻塞。这里有个小技巧,就是附加信息表何时生成,如果在插入基础表时同时生成,也必然可能会引起阻塞,我采取的方式是售票时不产生该表,在取票、检票等业务操作时,再生产该表,且支持幂等操作,也就是如果没有附加信息,则添加,已经生成了,且在服务器本地生成,不启用分布式事务。这样一来,售票只是在分布式事务中插入基础表,而其他相关业务中先创建附加表(不启动事务),再更新附加表(启用分布式事务,修改是行锁,影响很小)。

14. 防止死锁

死锁是大敌,一旦死锁,整个系统瘫痪,解决死锁相关文章较多,需要说明的是,有并发就有阻塞,但不会造成死锁,有事务就可能把阻塞变成死锁。因此,阻塞不可怕,顶多排队,但死锁就要命了,轻而易举就导致系统崩溃。这里列举本次改进用到的几个机制:

(1)      严格按统一顺序操作表

生产系统往往有多个功能,部分表是到处都需要操作,故必须整理出来,按同一顺序操作表(当然是事务中写操作),因为大家都按同一个顺序,并发时,不会出现交叉,自然也就有效避免了死锁。

(2)      关于行锁和表锁的先后顺序

业务中,通常既有添加(表锁)又有删改(行锁),那么,如何安排先后顺序呢?过去想当然先行锁(删改),再表锁(添加),因为行锁影响小,可以提高并发,只在最后才表锁,貌似可以将影响降到最低,然而,这样一来,恰恰事与愿违,比如两个线程同时访问,各自都行锁了(通常是成功的),再都要求表锁,结果,两个线程都各自锁了一行,都要求表锁,必然要等待对方释放,这时,死锁就发生了。故应该反过来,先表锁,再行锁(都表锁了,是否行锁已经无所谓了),例如A线程把表锁住了,在做其他操作时,B线程开始锁表,必然失败,只能排队,形成阻塞而已,待A全部完成,B再开始,如果所有业务都按这个顺序来,就可以避免死锁。

(3) 部分地方可以取消表锁
插入时,通常是表锁,显然,不利于并发,那么,如果插入时不表锁是不是更好呢?要实现这点,必须先理解为什么要表锁,我们要插入一条记录,这条记录会不会和其他记录冲突呢?显然,有的会,比如主键、唯一索引、聚集索引,为了保证新的记录和老的记录不冲突,插入时,必须先锁表,然后操作,因此,我们反过来理解,如果没有这些限制,是不是就可以“并发”插入呢?经测试,还真是这样,因此,对于一些临时中转的、高频的表,不要使用主键、唯一索引、聚集索引,简单说,每条记录都是独立的,和其他记录无关,这时,插入、更新、删除都是“行锁”,不会表锁。

(4)      先中心再节点

车站站务系统设置了中心服务器,存放所有数据,而车站只存放了本车站数据,过去想当然为了提高并发,总是先远程再本地,因为远程慢,执行远程时,本地表是不锁的,然而实事证明这个想法是有问题的,中心和车站互为远程,如果都先远程(中心锁了车站,车站锁了中心),再锁自己的本地时,和对方事务就造成死锁。改进后,都先统一锁中心,中心成功后,再锁车站,可以有效规避死锁,换句话说,中心充当了临时排队机的作用。

(5)      终极武器:LOCK_TIMEOUT

无论我们怎么小心,由于业务太复杂,程序员水平、责任心参差不齐,难免有疏漏,万一还是不幸死锁了咋办?这时,就必须启用终极武器:LOCK_TIMEOUT

关于LOCK_TIMEOUT的用法,很多人会误会,以为是超过多少时间就报错,如想当然认为下面的语句执行会报超时的错误:

 

--设置超时等待1

SET LOCK_TIMEOUT 1000

BEGIN TRAN

    UPDATE [dbo].[tc_scheme] WITH(ROWLOCKSET  Name='CHN11021'  WHERE scheme_sn=13

    --故意延迟3

    WAITFOR DELAY  '00:00:03'

ROLLBACK TRAN --测试用,不真正改数据,故回滚

本来设想是设置超时时长1秒,执行事务时,却故意执行了3秒,按理应该产生异常,退出本次执行,但事实却成功了,没有按预期来,这是很多人一开始都很容易碰到的疑惑。

其实,该方法的真正意义是:如果资源被“别人”锁住了,本线程(或者说本连接)最多等多久,注意,是别人锁住,自己等多久,而不是自己等自己,因此,我们做下面一个实验,打开两个查询窗口,在一个窗口中开启事务,修改表,但不要提交,这时,相关记录必然被锁住:

--查询窗口1:开始事务但不提交

BEGIN TRAN

UPDATE [dbo].[tc_scheme] WITH(ROWLOCKSET Name='CHN11021' WHERE scheme_sn=13

这里千万不能提交或回滚,要不然锁表就结束了,无法模拟并发。

接下来,另外开一个查询接口(注意,是另外打开一个查询哦),执行下面的语句:

--查询窗口2:测试超时

SET LOCK_TIMEOUT 3000

UPDATE [dbo].[tc_scheme] WITH(ROWLOCKSET Name ='USA11021' WHERE scheme_sn=13

这时,你会发现,3秒后,系统报错,本次操作结束。

消息 1222,级别 16,状态 45,第 3

已超过了锁请求超时时段。

语句已终止。

 

如果每个事务开始时,都根据实际情况设置“LOCK_TIMEOUT”的值(单位:毫秒),就能完全杜绝因设计等缺陷造成的系统瘫痪,也就是顶多本次操作失败,操作员再重试一次而已。

15. 启用连接限制

互联网访问是海量的,再强悍的服务器也架不住互联网暴风骤雨般的访问请求,如果碰上恶意扒票等操作,很容易造成系统瘫痪,为此,必须在对外接口中对各种指令启用身份认证,并限制并发数,超过并发数后,排队,排队过多时,直接返回服务器繁忙错误。通过连接数限制,可以极大保护业务系统正常运转。

16. 多监控实际执行情况

严格说,这点在普通的数据库中都要注意,网上有很多介绍监控sql server性能的方法、语句,生产业务高峰时,多看看,抓取耗时长的指令,看看有没有优化的地方,索引是否合理,从系统中根据实际运行情况抓出的语句,肯定比自己单纯估计、猜想的可能影响性能的语句要靠谱得多,很多工具还提供合理化建议等。我们自己做了一些小工具,主要是监控锁的数量(就是简单记录sp_lock执行结果的数量)、发生阻塞时对应的SQL语句是什么、每步操作耗时多少等。

以上是本人的一些粗浅总结,主要是团队自己摸索出来的,因为知识水平有限,很多更科学的办法尚未掌握,欢迎各位批评指正,尤其是能提出更好的建议,本人将不胜感激!


你可能感兴趣的:(SQL,Server,数据库,个人见解)