简单记录Sql Server常见锁类型

2023-08-17

闲来无聊,撸撸Sql Server锁

sp_lock 查看锁

我们可以使用sp_lock语句查看Sql Server当前存在哪些锁。

image-20230726155330008

参考:SQLSERVER各种锁——实例sqlserver sp_lock飞行的数据的博客-CSDN博客

  • spid:进程id。可以使用kill spid来杀死进程释放锁。

  • dbid:数据库id

  • Objid:数据库内对象id

  • Indid:持有锁的索引标识号。

  • type:锁的资源类型。

    • RID:行锁——表中单个行的锁,由行标识符 (RID) 标识。
    • KEY:索引内保护可串行事务中一系列键的锁。
    • PAG:数据页或索引页的锁。
    • TAB:表锁——TAB = 整个表(包括所有数据和索引)的锁。
    • DB:数据库的锁
    • EXT:对某区的锁
    • FIL:数据库文件的锁
    • APP:执行的应用程序资源的锁
    • MD:元数据或目录信息的锁。
    • HBT:堆或 B 树索引的锁。在 SQL Server 中此信息不完整。
    • AU:分配单元的锁。在 SQL Server 中此信息不完整。
  • mode:请求锁的类型

    • Sch-S:架构稳定性。确保在任何会话持有对架构元素(例如表或索引)的架构稳定性锁时,不删除该架构元素
    • Sch-M:架构修改。必须由要更改指定资源架构的任何会话持有。确保没有其他会话正在引用所指示的对象。
    • S:共享。授予持有锁的会话对资源的共享访问权限。
    • U: 更新。指示对最终可能更新的资源获取的更新锁。用于防止一种常见的死锁,这种死锁在多个会话锁定资源以便稍后对资源进行更新时发生。
    • X:排他。授予持有锁的会话对资源的独占访问权限。
    • IS:意向共享。指示有意将 S 锁放置在锁层次结构中的某个从属资源上。
    • IU:意向更新。指示有意将 U 锁放置在锁层次结构中的某个从属资源上。
    • IX:意向排他。指示有意将 X 锁放置在锁层次结构中的某个从属资源上。
    • SIU:共享意向更新。指示对有意在锁层次结构中的从属资源上获取更新锁的资源进行共享访问。
    • SIX:共享意向排他。指示对有意在锁层次结构中的从属资源上获取排他锁的资源进行共享访问。
    • UIX:更新意向排他。指示对有意在锁层次结构中的从属资源上获取排他锁的资源持有的更新锁。
    • BU:大容量更新。用于大容量操作。
    • ...
  • status:锁的状态

    • CNVRT:锁正在从另一种模式进行转换,但是转换被另一个持有锁(模式相冲突)的进程阻塞。
    • GRANT:已获取锁。
    • WAIT:等待获取锁。锁可能被另一个持有锁(模式相冲突)的进程阻塞。

Sql Server加锁语句

Sql Server加锁语句,尽量使用WITH(锁)形式,直接写锁有时不管用

Sql Server常见锁

  • ROWLOCK:行锁。
  • TABLOCK:表锁。
  • UPDLOCK:更新锁。
  • XLOCK:排它锁。
  • TABLOCKX:表的排他锁。
  • NOLOCK:没有锁。
  • HOLDLOCK:共享锁。
  • PAGLOCK:在通常使用单个表锁的地方采用页锁。

接下来我们来演示添加锁之后,对表内数据进行增删改查,看看有什么影响。

数据库隔离级别:READ COMMITTED(默认)

ROWLOCK——行锁——READ COMMITTED

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

小结:ROWLOCK——行锁,隔离级别为READ COMMITTED,与索引无关,当前事务执行中,其他事务或语句可立即查看、插入(新数据、注意唯一限定)、修改删除数据,相当于ROWLOCK单独使用,并没有起到 锁定数据的效果。在select语句中,不使用组合时,RowLock是没有意义的,一般会与UPDLOCK组合使用

ROWLOCK——行锁2——REPEATABLE READ

目前我们数据库的隔离级别是默认的——READ COMMITTED,此时我们改为更高级的隔离级别即REPEATABLE READ再重复上面的操作

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801183704106

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801183833647

小结:ROWLOCK——行锁,隔离级别为REPEATABLE READ,当前事务执行中,其他事务或语句可立即查看(带锁查询要分情况)、插入(新数据、注意唯一限定)数据,其他事务或语句对锁定行执行修改、删除操作需要等待当前事务提交(锁释放)后才会执行,对未锁定的数据可立即更新、删除。

本节演示完后,将数据库的隔离级别恢复至默认隔离级别——READ COMMITTED

TABLOCK——表锁——READ COMMITTED

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

小结:TABLOCK——表锁,隔离级别为READ COMMITTED,与索引无关,当前事务执行中,其他事务或语句可立即查看、插入(新数据、注意唯一限定)、更新、删除数据,相当于TABLOCK单独使用,并没有起到 锁定数据的效果。

TABLOCK——表锁2——REPEATABLE READ

目前我们数据库的隔离级别是默认的——READ COMMITTED,此时我们改为更高级的隔离级别即REPEATABLE READ重复上面的操作

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230803163158354

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230803164132563

小结:TABLOCK——表锁,隔离级别为REPEATABLE READ,与索引无关,当前事务执行中,其他事务或语句可立即查看(带锁查询要分情况)数据,其他事务或语句对表内数据执行插入(新数据、注意唯一限定)、更新、删除需要等待当前事务提交(锁释放)后才会执行。

本节演示完后,将数据库的隔离级别恢复至默认隔离级别——READ COMMITTED

UPDLOCK——更新锁

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230727113258872

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230727113759504

小结:UPDLOCK——更新锁。

无索引的情况下,事务1查询会全表扫描(Table scan)导致扫描过的行都会被锁定,造成"锁表"现象。

无索引的情况下,当前事务执行中,其他事务或语句可立即查看(带锁查询要分情况)、插入数据(新数据、注意唯一限定),其他事务或语句对表内数据进行更新、删除操作需要等待当前事务提交(锁释放)后才会执行.

有索引的情况下,事务1查询不会进行全表扫描(Table scan)——不过我测试的时候还是走了全表扫描(Table scan),不知道是不是数据不够的原因。当前事务执行中,其他事务或语句可立即查看(带锁查询要分情况)、插入数据(新数据、注意唯一限定),其他事务或语句对锁定的数据进行更新、删除操作需要等待当前事务提交(锁释放)后才会执行,对未锁定的数据可立即更新、删除。

XLOCK——排它锁

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801163725601

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801164523713

小结:XLOCK——排它锁

与索引无关,当前事务执行中,其他事务或语句可立即查看(带锁查询要分情况)、插入数据(新数据、注意唯一限定),其他事务或语句对表内数据进行更新、删除操作需要等待当前事务提交(锁释放)后才会执行.

TABLOCKX——表的排他锁

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801173801576

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801175039905

小结:TABLOCKX——表的排他锁

与索引无关,当前事务执行中,其他事务或语句对表内数据进行查询(带锁查询要分情况)、插入(新数据、注意唯一限定)、更新、删除操作需要等待当前事务提交(锁释放)后才会执行.

NOLOCK——没有锁

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况,发现与事务执行之前没有差异,说明NoLOCK不会对任何数据(行表库)加锁。

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况,发现与事务执行之前没有差异,说明NOLOCK不会对任何数据(行表库)加锁。

小结:NOLOCK——没有锁

与索引无关,当前事务执行中,其他事务或语句对表内数据进行查询、插入(新数据、注意唯一限定)、更新、删除操作立即执行.

HOLDLOCK——共享锁

新建一个查询窗口执行事务1waitfor delay用于延长事务结束时间,在该事务结束之前我们还需要去执行一些语句。

再新建一个查询窗口,执行事务1的同时,执行语句1事务1、语句1执行完了之后,再次执行事务1,然后执行语句2,依次执行完所有语句,并观察执行效果。

没有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801180543155

新增id非唯一非聚集索引

有索引的情况下,各语句的执行效果:

在事务执行中时,我们可以先通过sp_lock查看当前锁的情况

image-20230801181112470

小结:HOLDLOCK——共享锁

与索引无关,当前事务执行中,其他事务或语句可立即查询(带锁查询要分情况)数据,其他事务或语句对表内数据执行插入(新数据、注意唯一限定)、更新、删除操作需要等待当前事务提交(锁释放)后才会执行.

悲观锁和乐观锁

悲观锁和乐观锁并不是指的"某种锁",我更愿意理解为对待并发的"处理态度"。

一种执悲观的态度,认为并发是经常发生的。所以我要对数据加锁,在处理数据的这段时间,不允许别人修改该数据,从而保证每一个线程都能达到目的。由于需要加锁等待并发的效率不高,适合并发不高的场景下使用,悲观锁一定会成功

一种执乐观的态度,认为并发是极少发生的。所以我不需要对数据加锁,我只需要确定在我读取该数据到我修改该数据的这段时间内该数据有没有被修改过就行了。若未被修改,则修改该数据,若被修改了,则不修改该数据并告诉用户(重试)。乐观处理,无需加锁,并发的效率较高,适合高并发的场景下使用,乐观锁不一定成功(修改)。

参考:数据库对并发的处理-乐观锁与悲观锁 - 努力--坚持 - 博客园 (cnblogs.com)

场景及问题

假如我们有一个事务用于查询并修改某条数据A,但是该事务可能要执行(等待)一段时间,在这段时间里,另一个事务或语句可能也要修改这条数据A(制造并发场景)。

我们有如下sql语句用于查询Age值,并更新Age值为 @age+1。正常情况下,若Age初始值为4,执行完该事务,Age值则变为5。

此时我们再准备一条sql语句,在执行事务updateAge的同时,执行update语句。正常情况下,若Age初始值为4,执行完该语句,Age值则变为5。

事务updateAge执行结果:

image-20230816161344130

update语句执行结果:

image-20230816161443142

我们观察一下事务updateAge执行情况:在执行事务updateAge的同时,执行update语句,update语句立即执行,Age值变为5。事务updateAge执行完后查询,Age的值也是5。那就有问题了是不是,因为我们执行了update语句和事务updateAge,都会去更改Age值,按理来说此时的Age的值应该是6(4+1+1),而不是5。相当于我们执行了两次更新操作,其中有一次更新是无效的,但是并没有提示用户。在我们看来两次更新操作都成功了,但是Age的值没有达到我们期望的值。

接下来我们分别演示一下使用悲观锁和乐观锁如何处理这个问题

悲观锁

修改Age为初始值4

我们有如下sql语句用于查询Age值(加了更新锁),并更新Age值为 @age+1。正常情况下,若Age初始值为4,执行完该事务,Age值则变为5。

此时我们再准备一条sql语句,在执行事务updateAge的同时,执行update语句。正常情况下,若Age初始值为4,执行完该语句,Age值则变为5。

事务updateAge执行结果:

image-20230816162429664

update语句执行结果:

image-20230816162507438

我们观察一下事务updateAge执行情况:由于事务updateAge加了UPDLOCK锁,在执行事务updateAge的同时,执行update语句,update语句需要等待事务updateAge提交执行完毕(锁释放)后才会执行。事务updateAge执行完后查询,Age的值是5,update语句执行完后查询,Age的值是6(4+1+1),符合情况。

悲观锁一定会成功,我们可以发现事务updateAge和update语句都依次成功执行了。

乐观锁

修改Age为初始值4

由于乐观锁需要一个版本或时间戳字段的支持才能完成,我们需要先加一下这个字段。推荐使用时间戳字段,因为时间戳字段无法也无需手动更新,每次成功更新数据时间戳字段都会自动更新,版本字段需要每次更新数据的同时手动更新版本字段,比较麻烦。

我们有如下sql语句用于查询Age值,并更新Age值为 @age+1,更新的时候加上时间戳判断条件。这还没完,事务的最后我们还需要获取当前事务被修改行数@@ROWCOUNT若修改行数大于0,则表示修改成功,否则修改失败,提示用户(重试)。

正常情况下,若事务updateAge执行的这段时间内没有更新该数据,Age初始值为4,执行完该事务,Age值则变为5。

若事务updateAge执行的这段时间内有其他语句或者事务更新了该数据,那么该数据的时间戳就会发生改变,导致事务updateAge执行更新操作找不到数据(时间戳对不上),最终更新数据失败。

此时我们再准备一条sql语句,在执行事务updateAge的同时,执行update语句。正常情况下,若Age初始值为4,执行完该语句,Age值则变为5。

事务updateAge执行结果:

image-20230816173038341

update语句执行结果:

image-20230816172438737

我们观察一下事务updateAge执行情况:在执行事务updateAge的同时,执行update语句,update语句立即执行,Age值变为5。事务updateAge执行完后查询,Age的值也是5。

唉,怎么Age的值都是5,那不是跟原来一样的问题吗?

不不不,我们可以看一下事务updateAge执行结果图,里面是不是有一个修改失败的提示,在实际开发中我们可以把被修改行数@@ROWCOUNT作为事务是否成功执行的另一条件,若修改行数大于0,则表示修改成功,否则修改失败,提示用户(重试)。

乐观锁认为并发是极少发生的,乐观锁不一定成功(修改),在此场景下,update语句执行成功了,但是事务updateAge并没有执行"成功"(修改)

.Net Core代码使用锁

以悲观锁和乐观锁的场景,看看.Net Core如何使用锁。EF使用锁比较麻烦,需要借助ExecuteSqlRawFromSqlRaw方法来执行原生Sql语句来实现锁。

Nuget包版本

image-20230817120217918

准备数据

准备三个接口:ChangeAgeByPCC(悲观锁)、ChangeAgeByOCC(乐观锁)、ChangeAgeNoLock(直接修改)

分别执行ChangeAgeByPCC、ChangeAgeByOCC接口,触发执行后接着执行ChangeAgeNoLock接口,观察各个接口执行的情况。

User实体类:

ChangeAgeNoLock接口实现逻辑:

悲观锁

修改Age为初始值4

ChangeAgeByPCC接口实现逻辑:

在执行ChangeAgeByPCC的同时执行ChangeAgeNoLock,我们看一下效果:

image-20230817113158850

我们可以看出,ChangeAgeNoLock会等待ChangeAgeByPCC执行完毕后再执行,两个接口都执行成功。执行完后,Age的值由4变为6(4+1+1)。

乐观锁

修改Age为初始值4

ChangeAgeByOCC接口实现逻辑:

在执行ChangeAgeByOCC的同时执行ChangeAgeNoLock,我们看一下效果:

image-20230817112256665

我们可以看出,在ChangeAgeByOCC执行的过程中执行ChangeAgeNoLock,ChangeAgeNoLock成功修改Age值,但是ChangeAgeByOCC修改Age的值却提示失败,说明此次修改不起作用。

我们再单独执行ChangeAgeByOCC接口,看一下效果:

image-20230817112648640

我们可以看出,因为在ChangeAgeByOCC执行的过程中没有修改该数据,执行成功。

 

花了半个多月,终于撸完了,希望有帮助,去搬砖了。