T-SQL-锁(LOCK)

dba 发布于 2024-01-03 阅读(33)

锁说明(LOCK)

SQL Server 支持多用户共享同一个数据库,但是,当多个用户对同一个数据库进行修改时,会产生并发操作问题,使用锁可以解决用户存取数据的这个问题,从而保证数据库的完整性和一致性。对于一般的用户,通过系统的自动锁管理机制基本可以满足使用要求,但如果对数据安全、数据库完整性和一致性有特殊要求,则需要亲自控制数据库的锁和解锁,这就需要了解 SQL Server的锁机制,掌握锁的使用方法。

锁的作用

锁机制主要是对多个活动事务执行并发控制。它可以控制多个用户对同一数据进行的操作,使用锁机制可以解决数据库的并发问题。

数据库中数的并发操作经常发生,而对数据的并发操作会带来下面一些问题:脏读、幻读、非重复性读取和丢失更新等。

1.脏读
简单来说,就是事务1处理数据已经被修改,在期间被事务2读取,事务1最后被回滚,事务读出的数据就是错误的。当一个事务读取的记录是另一个事务的一部分时,如果第一个事务正常完成,就没有什么问题;如果此时另一个事务未完成,就产生了脏读。例如,员工表中编号为1001的员工工资为1740元,如果事务1将工资修改为1900元,但还没有提交确认;此时事务2读取员工的工资为1900元;事务1中的操作因为某种原因执行了ROLLBACK回滚,取消了对员工工资的修改,但事务2已经把编号为1001的员工的数据读走了,此时就发生了脏读。

2.幻读
简单来说,就是删除/更新数据的同时读取数据。当某一数据行执行INSERT或DELETE操作,而该数据行恰好属于某个事务正在读取的范围时,就会发生幻读现象。例如,现在要对员工涨工资,将所有低于1700元的工资都涨到新的1900元,事务1使用UPDATE语句进行更新操作,事务2同时读取这一批数据,但是在其中插入了几条工资小于1900元的记录,此时事务1如果查看数据表中的数据,会发现UPDATE之后还有工资小于1900元的记录。幻读事件是在某个凑巧的环境下发生的,简而言之,它是在运行UPDATE 语句的同时有人执行了INSERT操作。因为插入了一个新记录行,所以没有被锁定,并且能正常运行。

3.非重复性读取
两个事务同时读数据,一个事务改了数据,另一个事务仍为原数据。如果一个事务不止一次地读取相同的记录,但在两次读取中间有另一个事务刚好修改了数据,则两次读取的数据将出现差异,此时就发生了非重复性读取。例如,事务1和事务2都读取一条工资为2310元的数据行,如果事务1将记录中的工资修改为2500元并提交,则事务2使用的员工的工资仍为2310元。

4.丢失更新
一个事务更新了数据库之后,另一个事务再次对数据库进行更新,此时系统只能保留最后一个数据的修改。

可锁定的资源

使用SQL Server 2019中的锁机制可以锁定不同类型的资源,即具有多粒度锁,为了使锁的成本降至最低,SQL Server会自动将资源锁定在合适的层次,锁的层次越高,它的粒度就越粗。锁定在较高的层次(例如数据表)就限制了其他事务对数据表中任意部分进行访问,但需要的资源少,因为需要维护的锁较少;锁在较小的层次(例如行)可以增加并发但需要较大的开销,因为锁定了许多行,需要控制更多的锁。对于SQL Server来说,可以根据粒度大小分为6种可锁定的资源,这些资源由粗到细分别是:

数据库:锁定整个数据库,这是一种最高层次的锁,使用数据库锁将禁止任何事务或者用户对当前数据库的访问。

数据表:锁定整个数据表,包括实际的数据行和与该表相关联的所有索引中的键。其他任何事务在同一时间都不能访问表中的任何数据。

数据表锁定的特点是占用较少的系统资源,但是数据资源占用量较大。

区段页:一组连续的8个数据页,例如数据页或索引页。区段锁可以锁定控制区段内的8个数据或索引页以及在这8页中的所有数据行。

页:锁定该页中的所有数据或索引键。在事务处理过程中,不管事务处理数据量的大小,每一次都锁定一页,在这个页上的数据不能被其他事务占用。使用页层次锁时,即使一个事务只处理一个页上的一行数据,那么该页上的其他数据行也不能被其他事务使用。

键:索引中的特定键或一系列键上的锁,相同索引页中的其他键不受影响。

行:在SQL Server2019中可以锁定的最小对象空间就是数据行,行锁可以在事务处理数据过程中,锁定单行或多行数据,行级锁占用资源较少,因而在事务处理过程中,其他事务可以继续处理同一个数据表或同一个页的其他数据,极大地降低了其他事务等待处理所需要的时间,提高了系统的并发性。

锁的类型

SQL Server 2019中提供了多种锁类型,在这些类型的锁中,有些类型的锁之间可以兼容,有些类型的锁之间是不可以兼容的。锁类型决定了并发事务访问资源的方式。下面将介绍几种常用的锁类型。

更新锁:一般用于可更新的资源,可以防止多个会话在读取、锁定以及可能进行的资源更新时出现死锁的情况,当一个事务查询数据以便进行修改时,可以对数据项施加更新锁,如果事务修改资源,则更新锁就会转化成排他锁,否则会转换成共享锁。一次只有一个事务可以获得资源上的更新锁,它允许其他事务对资源的共享访问,但阻止排他式的访问。

排他锁:用于数据修改操作,例如INSERT、UPDATE或DELETE.确保不会同时对同一资源进行多重更新。

共享锁:用于读取数据操作,允许多个事务读取相同的数据,但不允许其他事务修改当前数据,如SELECT 语句。当多个事务读取一个资源时,资源上存在共享锁,任何其他事务都不能修改数据,除非将事务隔离级别设置为可重复读或者更高的级别,或者在事务生存周期内用锁定提示对共享锁进行保留,那么一旦数据完成读取,资源上的共享锁立即得以释放。

键范围锁:可防止幻读。通过保护行之间键的范围,还可以防止对事务访问的记录集进行幻象插入或删除。

架构锁:执行表的数据定义操作时使用架构修改锁,在架构修改锁起作用的期间,会防止对表的并发访问。这意味着在释放架构修改锁之前,该锁之外的所有操作都将被阻止。

锁的粒度

锁的粒度选择:
image

锁的粒度区分:
image

事务和锁

事务和锁概念区别#

事务将一段Transact-SQL语句作为一个单元来处理,这些操作要么全部成功,要么全部失败。事务包含4个特性:原子性、一致性、隔离性和持久性。事务的执行方式分为自动提交事务、显式事务、隐式事务和分布式事务。事务以 BEGIN TRAN 语句开始,并以 COMMIT TRAN 或ROLLBACK TRAN 语句结束。

锁是另一个和事务紧密联系的概念,对于用户系统,使用锁来保护指定的资源。在事务中使用锁,防止其他用户修改另外一个尚未完成的事务正在访问的数据。SQL Server中有多种类型的锁,允许事务锁定不同的资源。

事务和锁有什么关系#

SQL Server 2019中可以使用多种机制来确保数据的完整性,例如约束、触发器以及本章介绍的事务和锁等。事务和锁的关系非常紧密。事务包含一系列的操作,这些操作要么全部成功,要么全部失败,通过事务机制管理多个事务,保证事务的一致性,事务中使用锁保护指定的资源,防止其他用户修改另外一个尚未完成的事务正在访问的数据。

锁语法

查看锁#

SELECT * FROM sys.dm_tran_locks;

锁定行#

GOSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTEDSELECT *FROM stu_info
ROWLOCKWHERE s_id=2;
GO

锁定表#

GOSELECT s_ageFROM stu_info
TABLELOCKXWHERE s_age=18;
GO

排他锁#

创建名为transactionl和 transaction2的事务,在transactionl 事务上面添加排他锁,事务1执行10s之后才能执行 transaction2 事务。

GOBEGIN TRAN transaction1UPDATE stu_info SET s_score = 88 WHERE s_name='许三';
WAITFOR DELAY '00:00:10';COMMIT TRAN

GOBEGIN TRAN transaction2SELECT* FROM stu_info WHERE s_name='许三';COMMIT TRAN
GO

共享锁#

创建名为transactionl和transaction2的事务,在transactionl 事务上面添加共享锁,许两个事务同时执行查询操作,如果第二个事务要执行更新操作,必须等待10秒。

GOBEGIN TRAN transaction1SELECT s_score,s_sex,s_age FROM stu_info WITH(HOLDLOCK) WHERE s_name='许三';
WAITFOR DELAY '00:00:10';COMMIT TRAN
GOBEGIN TRAN transaction2SELECT*FROM stu_info WHERE s name='许三';COMMIT TRAN
GO

解锁语法#

设置5秒后锁自动解锁

Set Lock TimeOut 5000

设置立即解锁

Set Lock TimeOut 0

死锁

说明#

在两个或多个任务中,如果每个任务锁定了其他任务试图锁定的资源,会造成这些任务永久阻塞,从而出现死锁。此时系统处于死锁状态。
image

死锁的原因
在多用户环境下,死锁的发生是由于两个事务都锁定了不同的资源而又都在申请对方锁定的资源,即一组进程中的各个进程均占有不会释放的资源,但因互相申请其他进程占用的不会释放的资源而处于一种永久等待的状态。

形成死锁条件#

请求与保持条件:获取资源的进程可以同时申请新的资源。
非剥夺条件:已经分配的资源不能从该进程中剥夺。
循环等待条件:多个进程构成环路,并且其中每个进程都在等待相邻进程正占用的资源。
互斥条件:资源只能被一个进程使用。

可能会造成死锁的资源#

每个用户会话可能有一个或多个代表它运行的任务,其中每个任务可能获取或等待获取各种资源。以下类型的资源可能会造成阻塞,并最终导致死锁。
(1)锁。等待获取资源(如对象、页、行、元数据和应用程序)的锁可能导致死锁。例如,事务T1在行r1上有共享锁(S锁)并等待获取行r2的排他锁(X锁)。事务T2在行r2上有共享锁(S锁)并等待获取行r1的排他锁(X锁)。这将导致一个锁循环,其中,T1和T2都等待对方释放已锁定的资源。

(2)工作线程。排队等待可用工作线程的任务可能导致死锁。如果排队等待的任务拥有阻塞所有工作线程的资源,则将导致死锁。例如,会话S1启动事务并获取行r1的共享锁(S锁)后,进入睡眠状态。在所有可用工作线程上运行的活动会话正尝试获取行1的排他锁(X锁)。因为会话SI无法获取工作线程,所以无法提交事务并释放行1的锁。这将导致死锁。

(3)内存。当并发请求等待获得内存,而当前的可用内存无法满足其需要时,可能发生死锁。例如,两个并发查询(Q1和Q2)作为用户定义函数执行,分别获取 10MB和20MB的内存。如果每个查询还需要30MB,而可用总内存为20MB,则Q1和O2必须等待对方释放内存,这将导致死锁。

(4)并行查询执行的相关资源。通常与交换端口关联的处理协调器、发生器或使用者线程至少包含一个不属于并行查询的进程时,可能会相互阻塞,从而导致死锁。此外,当并行查询启动执行时,SQL Server 将根据当前的工作负荷确定并行度或工作线程数。如果系统工作负荷发生意外更改,例如,当新查询开始在服务器中运行或系统用完工作线程时,则可能发生死锁。

减少死锁的策略#

复杂的系统中不可能百分之百地避免死锁,从实际出发为了减少死锁,可以采用以下策略:

在所有事务中以相同的次序使用资源。

使事务尽可能简短并且在一个批处理中。

为死锁超时参数设置一个合理范围,如3~30分钟;超时,则自动放弃本次操作,避免进程挂起。

避免在事务内和用户进行交互,减少资源的锁定时间。

使用较低的隔离级别,相比较高的隔离级别能够有效减少持有共享锁的时间,减少锁之间的竞争。

使用Bound Connections.Bound Connections允许两个或多个事务连接共享事务和锁,而且任何一个事务连接都要申请锁,如同另一个事务要申请锁一样,因此可以运行这些事务共享数据而不会有加锁冲突。

使用基于行版本控制的隔离级别。持快照事务隔离和指定 READ_COMMITTED 隔离级别的事务使用行版本控制,可以将读与写操作之间发生死锁的机率降至最低。

SET ALLOW_SNAPSHOT_ISOLATION ON 事务可以指定 SNAPSHOT 事务隔离级别;

SET READ_COMMITTED_SNAPSHOT ON 指定 READ_COMMITTED隔离级别的事务将使用行版本控制而不是锁定。

在默认情况下,SELECT语句会对请求的资源加S(共享)锁,而开启了此选项后,SELECT不会对请求的资源加S锁。


处理死锁的办法#

随着应用系统复杂性的提高,不可能百分之百地避免死锁,但是采取一些相应的规则,可以有效地减少死锁,可以采用的规则有:
按同一顺序访问对象:如果所有并发事务按同一顺序访问对象,则发生死锁的可能性会降低。例如,如果两个并发事务先获取 suppliers表上的锁,然后获取fruits表上的锁,则在其中一个事务完成之前,另一个事务将在suppliers 表上被阻塞。当第一个事务提交或回滚之后,第二个事务将继续执行,这样就不会发生死锁。将存储过程用于所有数据的修改可以使对象的访问顺序标准化。

避免事务中的用户交互:避免编写包含用户交互的事务,因为没有用户干预的批处理的运行速度远快于用户必须手动响应查询时的运行速度,例如回复输入应用程序请求的参数的提示,对于这种情况,如果事务正在等待用户输入,而用户去吃午餐甚至回家过周末了,则用户就耽误了事务的完成。这将降低系统的吞吐量,因为事务持有的任何锁只有在事务提交或回滚后才会释放。即使不出现死锁的情况,在占用资源的事务完成之前,访问同一资源的其他事务也会被阻塞。

保持事务简短并处于一个批处理中:在同一个数据库中并发执行多个需要长时间运行的事务时通常会发生死锁。事务的运行时间越长,它持有排他锁更新锁的时间也就越长,从而会阻塞其他事务的运行并可能导致死锁。