并发控制指的是当多个用户同时更新行时,用于保护数据库完整性的各种技术。
其目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。
在某些情况下,这些措施保证了当用户和其他用户一起操作时,所得的结果和他单独操作时的结果是一样的。
锁是实现并发控制的主要方法,是多个用户能同时操纵同一个数据库中的数据而不发生数据不一致现象的重要保障。
并发性是用来解决多个用户对同一数据进行操作时的问题。
特别是对于网络数据库表来说,这个特点更加突出。
提高数据库的处理速度,单单依靠提高计算机的物理速度是不够的,还必须充分考虑数据库的并发性问题,提高数据库并发性的效率。
当多个用户同时读取或修改相同的数据库资源的时候,通过并发控制机制可以控制用户的读取和修改。
锁就是实现并发控制的主要方法,如果没有锁且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时就可能会发生问题,这些问题包括以下几种情况:
丢失或覆盖更新
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。
每个事务都不知道其他事务的存在。
最后的更新将重写由其他事务所做的更新,这将导致数据丢失。
例如:
事务T1读取某表中数据A=20,事务T2也读取A=20,事务T1修改A=A-1,事务T2也修改A=A-1;
最终结果A=19,事务T1的修改被丢失。
未确认的相关性(脏读)
当第二个事务选择其他事务正在更新的行时,会发生未确认的相关性问题。
第二个事务正在读取的数据还没有确认并且可能由更新此行的事务更改。
例如:
事务T1读取某表中数据A=20,并修改A=A-1,写回数据库,事务T2读取A=19,事务T1回滚了前面的操作,事务T2也修改A=A-1;
最终结果A=18,事务T2读取的就是”脏数据”。
非重复读
当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。
不一致的分析与未确认的相关性类似,因为其他事务也是正在更改第二个事务正在读取的数据。
然而,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。
而且,不一致的分析涉及多次读取同一行,而且每次信息都由其他事务更改,因而该行被非重复读取。
例如:
事务T1读取某表中数据A=20、B=30,求C=A*B,C=600,事务T1继续往下执行;
事务T2读取A=20,修改A=A*5,A=100;
事务T1又一次读取数据A=100、B=30,求C=A+B,C=130;
所以,在事务T1内两次读的数据是不一致的,即不可重复读。
幻象读
当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻象读问题。
事务第一次读的行范围显示出其中一行已不复存在于第二次读或后续读中,因为该行已被其他事务删除。
同时,由于其他事务的插入操作,事务的第二次或后续读显示有一行已不存在于原始读中。
1. 锁的基本概念
锁是防止其他事务访问指定的资源、实现并发控制的一种手段,是多个用户能够同时操纵同一个数据库中的数据而不发生数据不一致现象的重要保障。
SQL Server系统中的锁,大多数情况下都是系统自动生产的,用户通常不需要特别设置。
数据库的锁是指一种软件机制,用来指示某个用户已经占用了某种资源,从而防止其他用户做出影响本用户的数据修改或导致数据库数据的非完整性和非一致性。
这里所谓的资源,主要指用户可以操作的数据行、索引以及数据表等。
根据资源的不同,锁有多粒度的概念,也就是指可以锁定的资源的层次。
SQL Server中能够锁定的资源粒度包括数据库、表、区域、页面、键值(指带索引的行数据)、行标识符。
采用多粒度锁的用途是支持并发操作和保证数据的完整性。
SQL Server根据用户的请求,做出分析后自动给数据库加上合适的锁。
假设某用户只操作一个表中的部分行数据,系统可能会只添加几个行锁或页面锁,这样可以尽可能多地支持多用户的并发操作。
但是如果用户事务中频繁对某个表中的多条记录操作,将导致对该表的许多记录行都加上了行锁,数据系统中锁的数目会急剧增加,这样就加重了系统负荷,影响系统性能。
因此,在数据库系统中,一般都支持锁升级。
所谓锁升级是指调度锁的粒度,将多个低粒度的锁替换成少数的更高粒度锁,以此来降低系统负荷。
在SQL Server中,当一个事务中锁较多,达到锁升级门限时,系统自动将行级锁和页面锁升级为表级锁。
2. 锁的类型
数据库引擎使用不同类型的锁锁定资源,这些锁确定了并发事务访问资源的方式。
SQL Server中常见的锁有如下几种:
2.1 共享锁
共享锁(shared lock,S锁)允许并发事务在封闭式并发控制下读取资源。
资源上存在S锁时,任何其他事务都不能修改数据。
读取操作一完成,就立即释放资源上的S锁,除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留S锁。
2.2 排他锁
排他锁(exclusive lock,X锁)可以防止并发事务对资源进行访问,其他事务不能读取或修改排他锁锁定的数据。
即排他锁锁定的资源只允许进行锁定操作的程序使用,其他任何对它的操作均不会被接收。
执行数据更新命令,即insert、update或delete命令时,SQL Server会自动使用排他锁,但当对象上有其他锁存在时无法对其他排他锁。
排他锁一直到事务结束才能被释放。
2.3 更新锁
更新锁(update lock,U锁)可以防止常见的死锁。
在可重复读或可序列化事务中,此事务读取数据,即获取资源(页或行)的S锁;
然后修改数据,即此操作要求锁转换为排他锁。
如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为X锁。
共享模式锁到排他锁的转换必须等待一段时间,因为一个事务的排他锁与其他事务的共享模式锁不兼容,发生锁等待。
第二个事务试图获取X锁以进行更新。
由于两个事务都要转换为X锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。
U锁就是为了防止这种死锁而设定的。
当SQL Server准备跟新数据时,它首先对数据对象加U锁,锁定的数据将不被修改,但可以读取,所有U锁可以与S锁共存。
等到SQL Server确定要进行更新数据操作时,它会自动将U锁换为X锁,但当数据对象上有其他U锁存在时无法对其做U锁锁定。
2.4 意向锁
如果对一个资源加意向锁(intent lock,I锁),则说明该资源的下层资源正在被加锁(S锁或X锁);
对任一资源加锁时,必须先对它的上层资源加意向锁。
系统使用意向锁来最小化锁之间的冲突。
意向锁建立一个锁机制的分层结构,这种结构依据锁定的资源范围从低到高依次是行级锁层、页级锁层和表级锁层。
意向锁表示系统希望在层次低的资源上获得共享锁或排他锁。
例如,放置在表级上的意向锁表示一个事务可以在表中的页或者行上放置共享锁。
在表级上设置共享锁防止以后另外一个修改该表中页的事务在包含了该页的表上放置排他锁。
意向锁可以提高性能,这是因为系统只需要在表级上检查意向锁,确定一个事务能否在某个表上安全地获取一个锁,而不需要检查表上的每一行锁或者页锁,确定一个事务是否可以锁定整个表。
常用的意向锁有3种类型:
- 意向共享锁,简记为IS锁;
- 意向排他锁,简记为IX锁;
- 共享意向排他锁,简记为SIX锁
(1)意向共享锁:
意向共享锁表示读低层次资源的事务的意向,把共享锁放在这些单个的资源上。
也就是说,如果对一个数据对象加IS锁,表示它的后裔资源拟加S锁。
例如,要对某个元组加S锁,则要首先对关系和数据库加IS锁。
(2)意向排他锁:
意向排他锁表示修改低层次的事务的意向,把排他锁放在这些单个资源上。
也就是说,如果对一个数据对象加IX锁,表示它的后裔资源拟加X锁。
例如,要对某个元组加X锁,则要首先对关系和数据库加IX锁。
(3)共享意向排他锁:
共享意向排他锁是共享锁和意向排他锁的组合。
使用共享意向排他锁表示允许并行读取顶层资源的事务的意向,并且修改一些低层次的资源,把意向排他锁放在这些单个资源上。
也就是说,如果对一个数据对象加SIX锁,表示对它加S锁,再加IX锁,即SIX=S+IX。
例如对某个表加SIX锁,则表示该事务要读整个表(所以要对该表加S锁),同时会更新个别元组(所以要对该表加IX锁)。
2.5 模式锁
模式锁(schema lock)保证当表或索引被另外一个会话使用时,其结构模式不能被删除或修改。
SQL Server系统提供了两种类型的模式锁:
模式稳定锁(Sch-S)和模式修改锁(Sch-M)。
模式稳定锁确保锁定的资源不能被删除,模式修改锁确保其他会话不能使用正在修改的资源。
2.6 大容量更新锁
数据库引擎在将数据大容量复制到表中时,使用了大容量更新(BU)锁,并指定了tablock提示或使用sp_tableoption设置了table lock on bulk load表选项。
大容量更新锁允许多个线程将数据并发地大容量加载到同一张表中,同时防止其他不进行大容量加载数据的进程访问该表。
3. 锁定兼容性
在一个事务已经对某个对象锁定的情况下,另一个事务请求对同一个对象的锁定,此时就会出现锁定兼容性问题。
当两种锁定方式兼容时,可以同意对该对象的第二个锁定请求。
如果请求的锁定方式与已挂起的锁定方式不兼容,那么就不能同意第二个锁定请求。
相反,请求要等到第一个事务释放其锁定,并且释放所有其他现有的不兼容锁定为止。
资源锁模式有一个兼容性矩阵,显示了与在同一个资源上可以获取的其他锁相兼容的锁,如下表所示:
![](https://ichiblog.cn/wp-content/uploads/2025/01/图片-10-1024x411.png)
关于锁定兼容性的说明如下:
- 意向排他锁与意向排他锁模式兼容,因为意向排他锁表示打算更新一些行而不是所有行,还允许其他事务读取或更新部分行,只要这些行不是其他事务当前所更新的行即可。
- 模式稳定锁与除了模式修改锁之外的所有锁模式相兼容。
- 模式修改锁与所有锁模式都不兼容。
- 大容量更新锁只与模式稳定锁及其他大容量更新锁相兼容。
4. 死锁
封锁机制的引入能解决并发用户的数据不一致性问题,但也会引起事务间的死锁问题。
在事务和锁的使用过程中,死锁是一个不可避免的现象。
在数据库系统中,死锁是指多个用户锁定了一个资源,并又试图请求锁定对方已经锁定的资源,这就产生了一个锁定请求环,导致多个用户都处于等待对方释放所锁定资源的状态。
通常,根据使用不同的锁类型锁定资源,然而当某组资源的两个或多个事务之间有循环相关性时,就会发生死锁现象。
产生死锁的情况一般有如下两种:
第一种:
当两个事务分别锁定了两个单独的对象时,每一个事务都要求在另外一个事务锁定的对象上获得一个锁,因此每一个事务都必须等待另外一个事务释放占有的锁,这时就出现了死锁。
这种是最典型的死锁形式。
第二种:
当一个数据库中有若干长时间运行的事务执行并行的操作时,若查询分析器处理一种非常复杂的查询时,那么由于不能控制处理的顺序,有可能发生死锁现象。
在数据库中解决死锁常用的方法如下:
(1)
要求每个事务一次就将要使用的数据全部加锁,否则就不能继续执行。
预先规定一个顺序,所有事务都按这个顺序实现加锁,这样就不会发生死锁。
(2)
允许死锁发生,系统采用某些方式诊断当前系统中是否有死锁发生。
在SQL Server中,系统能够自动定期搜索和处理死锁问题。
系统在每次搜索中标识所有等待锁定请求的事务,如果在下一次搜索中该被标识的事务仍处于等待状态,SQL Server就开始递归死锁搜索。
当搜索检测到锁定请求环时,系统将根据事务的死锁优先级别来结束一个优先级最低的事务,此后,系统回滚该事务,并向该进程发出1205号错误信息。
这样,其他事务就有可能继续运行了。
死锁优先级的设置语句:set deadlock_priority{low|normal}。
其中,low说明该进程会话的优先级较低,在出现死锁时,可以首先中断该进程的事务。
另外,通过设置lock_timeout选项能够设置事务处于锁定请求状态的最长等待时间。
该设置的语句:set lock_timeout{timeout_period}。
其中,timeout_period以毫秒为单位。
5. 手工加锁
SQL Server系统中建议让系统自动管理锁,该系统会分析用户的SQL 语句要求,自动为该请求加上合适的锁,而且在锁的数目太多时,系统会自动进行锁升级。
如前所述,升级的门限由系统自动配置,并不需要用户配置。
在实际应用中,有时为了正确运行应用程序和保持数据的一致性,必须人为地给数据库的某个表加锁。
例如,在某应用程序的一个事务操作中,需要根据编号对几个数据表做统计操作,为保证统计数据时间的一致性和正确性,从统计第一个表开始到全部表结束,其他应用程序或事务不能再对这几个表写入数据,这个时候,该应用程序希望在从统计第一个数据表开始或在整个事务开始时能够由程序人为地锁定这几个表,这就需要用到手工加锁(显式加锁)技术。
在SQL Server的SQL 增删改查语句支持显式加锁。
这几个语句在显式加锁的语法上类似,下面仅以SELECT语句为例给出语法:
SELECT FROM [WITH]
其中,【with】指需要在该语句执行时添加在该表上的锁类型。
所指定的锁类型有如下几种:
- holdlock:在该表上保持共享锁,直到整个事务结束,而不是在语句执行完立即释放所添加的锁。
- nolock:不添加共享锁和排他锁,当这个选项生效后,可能读到未提交读的数据或”脏数据”,这个选项仅应用于SELECT语句。
- pagelock:指定添加页面锁。
- readcommitted:设置事务为读提交隔离性级别。
- readpast:跳过已经加锁的数据行,这个选项将使事务读取数据时跳过那些已经被其他事务锁定的数据行,而且不是阻塞其他事务释放锁,readpase仅应用于readcommitted隔离性级别下事务操作中的SELECT语句操作。
- readuncommitted:等同于nolock。
- repeatableread:设置事务为可重复读隔离性级别。
- rowlock:指定使用行级锁。
- serializable:设置事务为可串行的隔离性级别。
- tablock:指定使用表级锁,而不是使用行级或页面级的锁,SQL Server在该语句执行完释放这个锁,而如果同时指定了holdlock,该锁一直保持到这个事务结束。
- tablockx:指定在表上使用排他锁,这个锁可以阻止其他事务读或更新这个表的数据,直到这个语句或整个事务结束。
- updlock:指定在读表中数据时设置修改锁而不是设置共享锁,该锁一直保持到这个语句或整个事务结束,使用updlock的作用是允许用户先读取数据(而且不阻塞其他用户读数据),并且保证在后来再更新数据时,这一段时间内这些数据没有被其他用户修改。