并发、事务和锁
并发,在操作系统中,是指一个很短的时间段中有几个程序都处于已启动运行到运行完毕之间,并发程序之间有相互制约关系,直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争同一资源,如处理机、缓冲区、数据等。在数据库系统中,并发主要是指资源的争用,当两个进程同时在访问或更新同一个数据时,产生资源的争用,资源争用会引起一系列的问题,比如数据不一致、查询阻塞、死锁等。
一,并发模式
在数据库系统中,当多个进程访问同一资源时,默认情况下,SQL Server会通过各种类型的锁来协调资源的访问,确保在并发环境下数据保持一致的状态。而锁的作用范围是在事务中,事务建立在并发模式下。并发模式控制当发生读写冲突时,数据应该如何处理以保证数据的一致性。注意,写和写之间永远冲突。
1,乐观并发模式
对于乐观并发模式,SQL Server假设只有少量的冲突发生,默认的机制是使用快照技术,在写进程完成修改数据之前,先把数据的行版本保存到tempdb中。由于数据的旧数据已经保存,读进程可以直接读取已经保存的行版本,而不会受到写进程的影响。
乐观并发使得读写进程不会相互阻塞,但是,这会导致一个潜在的问题,读进程可能会读取到老的数据。
2,悲观并发模式
这是默认的并发模式,在悲观并发模式下,SQL Server认为有大量的写操作发生,并且写操作会受到写操作的影响。也就是说,悲观并发模式对任何正在访问的数据进行加锁,以避免多个进程同时修改或读取数据,造成数据的不一致。在默认的隔离级别下,读和写是相互阻塞的。
二,事务
不管是在悲观模式下,还是在乐观模式下,都会涉及到事务。事务在逻辑上是一个整体,是一个工作单元,具有4个特性:
- 原子性:事务整体是一个工作单元,对数据的修改操作,要么全部执行,要么完全不执行,没有第三种状态。
- 一致性:在一个事务执行之前和执行之后数据库都必须处于逻辑上的一致性状态,数据在不同的事务中是相同的。
- 隔离性:并发执行的事务之间是相互隔离的,一个事务内部的状态,对其他事务是不可见的。
- 持久性:当系统发生故障时,持久性确保已提交事务的更新不会丢失,也就是说一旦一个事务提交,DBMS保证数据的改变是永久性的,持久性通过事务日志来保证。
事务有两种触发方式,隐式事务和显式事务。
1,隐式事务
默认情况下,单个语句自动触发隐式事务,在语句执行时,事务自动开始;当语句执行成功,事务自动提交;当语句执行失败,事务自动回滚。隐式事务只能包含一个语句,当语句执行成功之后,事务就自动提交了。隐式事务不能回滚,除非语句运行失败。
2,显式事务
使用begin tran命令开启一个事务;使用commit tran提交一个事务;使用rollback tran回滚一个事务。在事务内,可以包含多个语句,这些语句作为一个整体,具有事务的ACID属性。
三,事务的隔离级别
每一个事务都运行在一个特定的隔离级别内,该隔离级别是由会话(session)决定的。SQL Server定义的四个隔离级别,都是为了定义读数据的行为:
- READ UNCOMMITTED :允许脏读、数据不可重复读和数据范围不可重复读
- READ COMMITTED:防止脏读(读取未提交的数据更新),允许数据不可重复读和数据范围不可重复读
- REPEATABLE READ:防止脏读和数据不可重复读,允许数据范围不可重复读
- SERIALIZABLE:防止脏读、数据不可重复读和数据范围不可重复读
注意:隔离级别只能定义读操作的行为,无法定义写操作的行为,写操作跟写操作与读操作都是互斥的。
1,脏读
当事务运行在 READ UNCOMMITTED 隔离级别下时,有可能会读取到其他事务未提交的数据更新。
- 例如,事务A修改了数据,把数据由1修改2,但是还没有提交,
- 之后,另一个事务B读取了该数据,结果是2,
- 在事务B读取数据之后,事务A回滚,那么该数据的最终值是1。
出现脏读的原因是读操作不加共享锁,读操作不会被写操作阻塞,使得读操作可能会读取到写事务未提交的值。
2,数据不可重复读
当事务运行在 READ COMMITTED 隔离级别下时,在同一个事务内,两次读取同一个数据,可能读取到不同的结果,这就是数据不可重复读现象。
- 例如,事务A读取了一个数据,值是1,事务未提交
- 之后,事务B修改了该数据,把数据值由1修改为2,并提交事务。
- 在事务B提交之后,事务A重新读取该书,值为2
出现数据不可重复读的原因是事务在读取数据时申请了共享锁,但是在语句执行完成之后,就立马释放了共享锁。要想避免出现数据不可重复读的现象,事务必须一直持有共享锁,直到事务提交或回滚。
3,数据范围不可重复读
当使用where子句限定了数据范围之后,事务在两次执行相同的查询,获得的数据范围不同,也就是说,在相同的数据范围内,增加了新的数据。
- 例如,事务A查询Age在10和20之间的男生,共有7个,事务未提交
- 事务B向数据表中插入一条新的数据,Age是15,事务提交
- 事务A重新查询Age在10和20之间的男生,共有8个
出现数据范围不可重复读的原因是事务只申请了共享锁,没有申请范围锁,这就导致其他写事务可以向特定的数据范围内新增数据。
四,锁
锁是一种机制,锁施加在资源上,表示锁定该资源。锁的所有者是事务,事务的特性决定了锁的特性。
1,锁定的资源
事务可以在数据行、数据页、键范围、分区和表上加锁,
锁定的资源类型,按照粒度的级别,从低向高分别是:
- RID:堆中的一行
- KEY:聚集索引中的键
- KEY Range:键范围
- PAGE:堆表的数据页或B-Tree的索引页
- EXTENT:分区,连续的8个Page
- HoBT:堆或B-Tree, 用于保护堆(没有聚集索引的表)和堆中的B 树
- IDX:索引中的数据行
- OBJECT:表(整个表)
2,锁模式
共享锁(S锁):读操作在读取的数据上施加共享锁,用于select子句中
互斥锁(X锁):写操作在更新的数据上施加互斥锁,一个数据上,只能施加一个互斥锁,X锁和任意锁都是互斥的。
更新锁(U锁):U锁和S锁是兼容的,在写操作中,数据的更新过程分为两步,第一步是找到更新的数据,对于找到的数据施加U锁;第二步是在真正执行数据更新时,把U锁转换为X锁。
意向锁(I锁):意向锁有 IS、IX和IU 三种类型,当事务申请低粒度锁时,事务会在相同的对象上,依次申请高粒度资源的意向锁,直到表上的意向锁。例如,事务A申请在数据行R上施加X锁,同时,事务A会在包含该数据行R的数据页上申请IX锁,同时申请表级的IX锁,也就是说,在一行上申请X锁,那么该行所在的页和表都会申请IX锁。在这种情况下,其他事务就很容易探测到有事务在更新表中的数据,避免锁住整个表。
键范围锁(Key-Range):键范围锁出现在SERIALIZABLE隔离级别,如果事务需要扫描一个范围的数据,事务使用键范围锁,锁定表中的特定范围,避免其他事务向范围中插入新的数据,键范围锁与特定的索引键关联。
3,锁持续的时间
X锁持续到事务结束,S锁的持续时间受到事务隔离级别的影响。
- READ UNCOMMITTED :不申请S锁
- READ COMMITTED:申请S锁,S锁在读取操作完成时就立马释放,S锁持续的时间是语句执行的时间
- REPEATABLE READ:申请S锁,当事务结束之后,立马释放S锁,S锁持续的时间是事务执行的时间
- SERIALIZABLE:申请S锁,锁定的资源是键范围,当事务结束之后,立马释放S锁,S锁持续的时间是事务执行的时间
4,锁的兼容性
自行百度
5,锁升级
锁升级是指锁施加的资源的粒度增加,从低粒度升级到高粒度,这会使数据库系统并发度降低,增加阻塞和死锁的风险,但是带来的好处是降低锁管理的开销、保证查询执行的速度。
锁升级的第一种方式是锁占用的内存达到阈值。通常来说,一个锁结构大概需要96B的内存空间,当数据库中存在大量的锁结构时,锁结构会占用较多的内存空间。当SQL Server使用超过24%的Buffer Poll用于存储锁结构时,SQL Server 引擎会选择一些正在持有的锁的会话,把锁升级到高层次的级别,使锁锁定的资源粒度变大,降低锁的数量。
锁升级的第二种方式是锁的数量达到阈值。在一个表上,当一个会话申请的锁的数量达到一定的阈值,阈值的默认值是5000,也就是说,当一个会话持有锁的数量超过5000时,SQL Server就自动把锁升级。
锁升级是自动进行的,也可以使用LOCK HINT,手动控制锁的粒度。