SQL Server笔记(3):锁的种类

Posted by Kurt on March 7, 2021

Notes from Expert SQL Server Transactions and Locking by Korotkevitch, Dmitri

3.1 锁的主要种类

SQL Server使用锁来达到事务的隔离要求。每个锁是由锁管理器(lock manager)进行管理的内存结构。每个锁结构使用64 bytes(32-bit的SQL Server)/ 128 bytes(64-bit的SQL Server)的内存。

默认情况下,SQL Server使用行级锁来锁定数据行,从而最小化并发问题的可能性。但为了实现数据隔离性与一致性,有时候也会使用页级锁,表级锁甚至事务级别的锁。

SQL Server内部使用超过20种不同的锁。它们根据种类和用途的不同主要可以分为以下几类:

(1)排它锁(exclusive/x locks)

​ 排它锁由写入者获得,例如INSERT, UPDATE, DELETE, MERGE等操作。这些操作会锁定相应的行,直到事务完成。在任何时间,只有一个会话可以获得排它锁。因此,事务越长,排它锁保持的时间也越长,发生阻塞的可能性越高。

(2)意向锁(intent/i locks)

​ 即使行级锁减少了系统中的阻塞,只使用行级锁会导致性能的降低。比如,当遇到会话需要从外部获取表(如进行更改),这时如果只有行级锁,会话必须扫描整张表来检查是否有锁被放置在这里。这是效率非常低的,尤其在遇到大表的时候。

​ SQL Server通过意向锁来解决这个问题。意向锁放置在数据页和表级上,指示子对象的锁是否存在。

意向锁到底是什么_a1102325298的博客-CSDN博客 ,意向锁是一种快速判断手动加的表锁与之前可能存在的行锁冲突的机制。)

​ 当会话需要获取对象或者页级的锁,它就可以检查表或者页中其它锁的兼容性,而不需要扫描整张表来判断是否有行级锁。

(3)更新锁(update/u locks)

​ 修改数据的过程中,SQL Server使用更新锁来寻找需要更新的行。如果获得更新锁,SQL Server会读行,检查该行数据与查询操作来判断是否需要更新。如果需要更新的话,SQL Server会将更新锁转换为排它锁并执行修改(直到事务被提交的时候才会释放)。否则释放更新锁。

​ 更新锁的行为取决于执行计划。有时候,SQL Server会先在所有行获取更新锁,随后再转换为排它锁。有时候(如只依据集群索引值更新一行数据的时候),SQL Server可以不适用更新锁而直接获取排它锁。

​ 获取锁的数量也取决于执行计划。

(4)共享锁(shared/s locks)

​ 共享锁是被系统中的读操作获取的(SELECT查询)。不同共享锁之间是兼容的,不同会话可以在同一资源上获取共享锁。

(https://www.cnblogs.com/nickup/p/9804020.html,如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。)

3.2 锁的兼容性,行为和寿命

锁的兼容性表:

table structure

**Picture from Expert SQL Server Transactions and Locking by Korotkevitch, Dmitri*

主要的兼容性规则:

(1) 意向锁之间可以互相兼容。(因为只是用来指示锁是否存在)、

(2) 排它锁之间以及与其它种类的锁都不兼容。

(3) 更新锁之间以及与排它锁不兼容。

(4) 更新锁与共享锁可以兼容。

排它锁的行为不取决于事务的隔离级别。写操作总是会获取排它锁,并保持到事务结束。更新锁也一样,除了SNAPSHOT的隔离级别。

共享锁的行为取决于事务的隔离级别。

在读未提交隔离级别下,共享锁不被获取。因此,读操作可以读到被其它持有排它锁的会话修改过的行。这种隔离级别减少了系统阻塞,因为它消除了读和写的冲突,但同时也会有数据一致性的问题,可能会出现脏读。

在读提交的隔离级别下,SQL Server可以获取共享锁,并在读行之后马上释放,从而保证事务不会读到其它会话未提交的数据。这种隔离级别中,SQL Server会在SELECT过程中一直保持共享锁,因此这时不应select不必要的列或者使用SELECT *,否则会影响性能。

在可重复读的隔离级别下,SQL Server获取共享锁并保持到事务结束,保证读到的数据不会被其它会话修改。

在序列化的隔离级别下,共享锁也是被保持到事务结束。但是,SQL Server会使用另一种锁,称为范围锁。范围锁(可以是共享锁或者排它锁),保护的是一段索引键范围内的数据而不是单独的行。

乐观隔离级别(读提交快照,快照)不获取共享锁。读操作遇到持有排它锁的行时,它们会读取储存在tempdb中的旧版本的数据。写操作和未提交的数据修改不会阻塞读操作。

从阻塞和并发的角度来看,读提交快照的行为和读提交一样。它们都没有读写阻塞的问题。但是,读提交快照中不能获取未提交的数据,没有脏读。大部分情况下,应该选择读提交快照,而不使用读提交。

事务的隔离级别和共享锁行为:

table structure

**Picture from Expert SQL Server Transactions and Locking by Korotkevitch, Dmitri*

所有的隔离级别(除了快照)都会在更新扫描时使用更新锁,在数据修改时使用排它锁,都可能导致读写阻塞。快照级别也在数据修改时使用排它锁,但在更新扫描时不使用更新锁,而是读tempdb中的旧版本数据,从而大大减少读写阻塞,除非多个会话同时更新同一行。

3.3 事务隔离级别与数据一致性

下面基于事务隔离级别的行为分析为什么会有数据读取的问题。

脏读:当事务读到其它未提交事务所修改的数据时会发生脏读。这种现象可能发生在读未提交的隔离级别中,如会话没有获取共享锁又忽视了其它会话的排它锁时。所有其它隔离级别则不受脏读影响。悲观隔离级别使用共享(S)锁,并在尝试访问持有其上排他(X)锁的未提交行时被阻止。乐观隔离级别从版本存储中读取行的旧(先前)已提交版本。

不可重复读:从同一事务中多次读取同一数据获得不同的结果。数据的不一致性可能发生在读取时其它事务修改甚至删除了数据的时候。这种情况可能发生在读未提交以及读提交快照的隔离级别,它们都不使用共享锁。如果读提交的会话是立即获取或释放共享锁的话,也可能发生不可重复读。可重复读和序列化的隔离级别会保持共享锁,直到事务结束,阻止数据在读取之后还被修改。快照隔离级别由于使用了数据的快照,也不会受此影响。

幻读:如果同一事务多次读取会返回新的行,就是发生了脏读。只有序列化和快照隔离级别不会受此影响。序列化使用了范围锁,快照则在事务开始之后读取的是数据的快照。

另外两种现象与索引键的值变化而导致的数据移动有关。以下两种都不会在乐观隔离级别中发生。

重复读(Duplicated Reads,应该如何翻译?):如果一个查询多次返回了同样的行,称为重复读。

跳过行(Skipped Rows,应该如何翻译?):若查询不返回任何行,称为跳过行。序列化,读提交快照,快照隔离级别不会发生此情况。

table structure

**Picture from Expert SQL Server Transactions and Locking by Korotkevitch, Dmitri*

由表可知,只有序列化和快照级别可以完全保护数据一致性。但是,序列化可能会导致阻塞和死锁,快照则可能导致tempdb负载以及写入冲突。

3.4 锁定相关的语句(TABLE HINTS)

可以通过(UPDLOCK)和(XLOCK)的表提示来控制读操作所获得的锁种类,分别控制SELECT查询使用更新锁和排它锁。

另外有一些提示可以控制锁的粒度。(TABLOCK)和(TABLOCKX)会强制SQL Server获取共享锁或者排它锁。使用(TABLOCK)时,读操作会获取共享锁,写操作获取排它锁。(TABLOCKX)总是会获取排它锁。

(PAGLOCK)强制SQL Server使用页级锁。(ROWLOCK)强制使用行级锁。

(READPAST)会允许会话跳过持有不兼容的锁的行。(NOWAIT)会在SQL Server遇到不兼容的行或页级锁的时候触发error。

只要不冲突的话可以使用多个锁。

SET LOCK_TIMEOUT选项可以控制会话会等一个锁多长时间。但它不会覆盖SQL Client的CommandTimeout值。

3.5 转换锁

SQL Server会使用转换锁来将一个已经获得的全锁(full lock)与一个额外的意向锁扩展,或者一个已经获得的意向锁与其它种类的全锁扩展。

一些种类的转换锁如共享意向排它锁(SIX),共享意向更新锁(SIU),更新意向排它锁(UIX)。

第三章结束。上班之后还想有时间和精力学习真是不容易,读书的速度远远追不上书单增加的速度。