官网链接:锁模式
一直以来,自己的数据库都学的不好,仅仅会一些普通的增删改查,对于锁是一直不了解,这次一定要把锁学会,在学习了大神的博客后,记录下自己对锁的理解,大神博客地址:数据库锁机制
数据库并发情况下要考虑死锁和锁的问题,下面对数据库的锁做一个讨论。为了方便,令T1代表一个数据库请求,T2代表另一个数据库请求。
一、锁的种类
1.共享锁——Shared lock
又称读锁(S锁),共享锁不阻塞其他事务的读操作,但阻塞写操作,同一数据对象A可以共存多个共享锁,这被称为共享锁兼容。
当T1为数据对象A加上共享锁后,可以对A进行读操作,但不能进行写操作,并且T2可以再次对A加共享锁,大家都可以正常地读A,但是在A上的共享锁释放之前,任何事务不可以对A进行写操作。
例1:
T1:select * from table
T2:update table set column1='hello'
分析:假设T1先执行,则T2必须等待T1执行完才可以执行。因为T2为写操作,需要为table加一个排他锁,而数据库规定相同资源不可以同时存在共享锁和排他锁,所以T2必须等待T1执行完,释放掉共享锁,才可以加排他锁,然后执行update。
例2:(死锁的发生)
T1:
begin Transaction t1
select * from table with (holdlock) (holdlock的意思是加共享锁,直到事务结束(提交或回滚)才会释放)
update table set column1='hello'
T2:
begin Transaction t2
select * from table with (holdlock)
update table set column1='world'
分析:假设T1和T2同时到达select语句,都为table加上了共享锁,那么当T1、T2要执行update时,根据锁机制,共享锁需要升级为排他锁,但是排他锁与共享锁不能共存,要给table加排他锁,必须等待table上的共享锁全部释放才可以,可是holdlock的共享锁必须等待事务结束才能释放,因此T1和T2都在等待对方释放共享锁,形成循环等待,造成死锁。
例3:
T1:update table set column1='hello' where id='001'
T2:update table set column1='world' where id='002'
分析:此种情况有可能造成等待,分为id列有索引与无索引两种情况。
(1)id列有索引,则T1直接定位到id='001'行,加排他锁,更新;T2直接定位到id='002'行,加排他锁,更新。互不影响。
(2)id列无索引,T1扫描全表,找到id='001'行,加排他锁后,T2为了找到id='002'行,需要全表扫描,那么就会为table加共享锁或更新锁或排他锁,但不管加什么锁,都需要等待T1释放id='001'行的排他锁,不然无法为全表加锁。
死锁可以通过直接对表加排他锁来解决,即将事务的隔离级别提高至最高级——串行读,各个事务串行执行,可是这样虽然避免了死锁,但是效率太低了,那我们干脆别发明并发这个词语好了。
2.更新锁——Update lock
更新锁(U锁)。当T1给资源A加上更新锁后,代表该资源将在稍后更新,更新锁与共享锁兼容,更新锁可以防止例2里那种一般情况的死锁发生,更新锁会阻塞其他的更新锁和排他锁。因此相同资源上不能存在多个更新锁。
更新锁允许其他事务在更新之前读取资源。但不可以修改。因为其他事务想获取资源的排他锁时,发现该资源已存在U锁,则等待U锁释放。
在T1找到需要更新的数据时,更新锁直接转为排他锁,开始更新数据,不需要等待其他事务释放共享锁啥的。
那么就问了,共享锁为什么不可以直接升级为排他锁,而必须等待其他共享锁都释放掉才可以转为排他锁呢?
这就是共享锁和更新锁的一个区别了,共享锁之间是兼容的,但是更新锁之间互不兼容,因此仅有一个更新锁直接转为排他锁是安全的,而多个共享锁问也不问直接转为排他锁,那怎么行呢,排他锁只能有一个的,这就是为什么共享锁需要等待其他共享锁释放才可以升级为排他锁的原因了。
例4:
T1:
begin
select * from table with (updlock) (加更新锁)
update table set column1='hello' (重点:这里T1做update时,不需要等T2释放什么,而是直接把更新锁升级为排他锁,然后执行update)
T2:
begin
select * from table (T1的更新锁不影响T2的select)
update table set column1='world' (T2的update需要等待T1的update执行完)
分析:(1)T1先到达,T1的select句对table加更新锁,此时T2紧接着到达,T2的select句对table加共享锁,假设T2的select先执行完,要开始T2的update,发现table已有更新锁,则T2等,T1此时执行完select,然后将更新锁升级为排他锁,开始更新数据,执行完成,事务结束,释放排他锁,此时T2才开始对table加排他锁并更新。
(2)T2先到,T1紧接着,T2加共享锁 => T1加更新锁 => 假设T2先结束select => 试图将共享锁升级为排他锁 => 发现已有更新锁 => 之后的情况同(1)
3.排他锁——Exclusive Locks
又叫独占锁,写锁,X锁,很容易理解,排他锁阻塞任何锁,假设T1为资源A假设排他锁,则其他事务不允许对资源A进行任何的读写操作。
例5:(假设id都是自增长且连续的)
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>1000
假设T1先达,T2随后至,这个过程中T1会对id<1000的记录施加排他锁.但不会阻塞T2的update。
例6:
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>900
假设T1先达,T2立刻也到,T1加的排他锁会阻塞T2的update。
4.意向锁——Intent Locks
意向锁,就是说当你给数据加锁时,必须先给他的上级加锁,用来向其他事务表明这段数据中的某些数据正在被加某某锁,你看着办吧。其实是一个节省开销的做法。
例7:
T1:
begin tran
select * from table with (xlock) where id=10 --意思是对id=10这一行强加排他锁
T2:
begin tran
select * from table with (tablock) --意思是要加表级锁
假设T1先执行,T2后执行,T2执行时,欲加表锁,为判断是否可以加表锁,数据库系统要逐条判断table表每行记录是否已有排他锁,
如果发现其中一行已经有排他锁了,就不允许再加表锁了。只是这样逐条判断效率太低了。
实际上,数据库系统不是这样工作的。当T1的select执行时,系统对表table的id=10的这一行加了排他锁,还同时悄悄的对整个表加了意向排他锁(IX),当T2执行表锁时,只需要看到这个表已经有意向排他锁存在,就直接等待,而不需要逐条检查资源了。
常用的意向锁有三种:意向共享锁(Intent Share Lock,简称IS锁);意向排他锁(Intent Exclusive Lock,简称IX锁);共享意向排它锁(Share Intent Exclusive Lock,简称SIX锁),共享意向排它锁的意思是,某事务要读取整个表,并更新其中的某些数据。
5.计划锁——Schema Locks
6.Bulk Update Locks
5和6的资料找到的不是很多,据称DDL(表结构相关)语句会加计划锁,该锁禁止其他session对表的连接。而6一般似乎不需要程序员关心,主要在批量导数据时用(比如用类似于oracle中的imp/exp的bcp命令)。
二、如何加锁
1.数据库自动加锁
其实锁在大多数情况下都是数据库自动加的,比如这么一条语句:
update table set column1='hello'
通过Profiler跟踪sql发现,他会逐行先获取U锁,然后转为X锁,更新完这一行,不释放X锁,继续获取下一行的U锁,转X......一直到全部更新结束,再逐行释放掉所有的X锁。如图:
而如果加上where条件,如:update table set column1='hello' where column2='world',并且column2无索引,则逐行获取U锁,如果符合条件,转X锁,更新,不释放X锁;如果不符合,释放U锁。如图:
但是如果column2有索引的话,则不需要逐行获取U锁 => 判断 => 转X锁或释放,而是直接获取到要更新行的X锁,更新,释放X锁即可。如图:
哦对了,所有的U,X,S之前都会首先为表或者页加意向锁。
2.手动加锁
例8:
T1:select * from table with (tablock) --对表加共享锁,且事务不完成,共享锁不释放。
T2:select * from table with (holdlock) --对涉及范围内加共享锁,事务不完成,共享锁不释放。
我感觉可能大多数情况下是不需要我们手动加锁的,因为我们不是专业搞数据库的,很多场景可能预想不到,就会导致一些错误,我们管理事务的隔离级别就可以了,数据库会根据隔离级别的不同,按照策略来加锁。
三、锁的粒度
锁的粒度指的是锁生效的范围,即行锁,页锁,或者是表锁。锁的粒度一般由数据库自主管理,不同的事物隔离级别,数据库会有不同的加锁策略(比如加什么类型的锁,加什么粒度的锁)。也可以手动指定。在下面的例子中,我们假设id是自增的主键。
例9:
T1::select * from table with (paglock) --页锁
T2::update table set column1='hello' where id>10
T1执行时,会先对第一页加共享(S)锁,读完第一页后,释放锁,再对第二页加共享锁,依此类推。假设前10行记录恰好是一页(当然,一般不可能一页只有10行记录),那么T1执行到第一页查询时,并不会阻塞T2的更新。
例10:
T1:select * from table with (rowlock) --行锁
T2:update table set column1='hello' where id=10
T1执行时,对每行加共享锁,读取,然后释放,再对下一行加锁;T2执行时,会对id=10的那一行试图加锁,只要该行没有被T1加上行锁,T2就可以顺利执行update操作。
例11:
T1:select * from table with (tablock) --表锁
T2:update table set column1='hello' where id = 10
T1执行,对整个表加共享锁。T1必须完全查询完,T2才可以允许加锁,并开始更新。
通过分析以上3个例子可以得出结论:锁的粒度与系统的并发性和系统开销密切相关。粒度越小,则并发度越大,开销越大;反之,粒度越大,则并发度越小,开销越小。
四.锁与事务隔离级别的优先级
先上结论,手工指定的锁优先。
例12:
T1:GO
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
GO
BEGIN TRANSACTION
SELECT * FROM table with (NOLOCK)
GO
T2:update table set column1='hello' where id=10
T1是事物隔离级别为最高级,串行读,数据库系统本应对后面的select语句自动加表级锁,但因为手工指定了NOLOCK,所以该select语句不会加任何锁,所以T2也就不会有任何阻塞。
五、数据库的几个 重要Hint及他们的区别
1) holdlock 对表加共享锁,且事物不完成,共享锁不释放。
2) tablock 对表加共享锁,只要statement不完成,共享锁不释放。
3) TABLOCKX 对表加排他锁,事务不完成,排他锁不释放。
4) xlock 加排他锁,和tablockx的区别:tablockx只能对整张表加锁,而xlock可以指定锁的粒度。
例13:
select * from table with (xlock paglock) --对page加排他锁
select * from table with (xlock tablock) --效果等同于select * from table with (tablockx)
六、如何提高并发效率
1.悲观锁
利用数据库本身的锁机制实现。通过上面对数据库锁的了解,可以根据具体业务情况综合使用事务隔离级别与合理的手工指定锁的方式比如降低锁的粒度等减少并发等待。
2.乐观锁
利用程序处理并发。原理都比较好理解,基本一看即懂。方式大概有以下3种:
(1)对记录加版本号。
(2)对记录加时间戳。
(3)对将要更新的数据进行提前读取、事后对比。
乐观锁这里看一下MVCC(多版本并发控制),MVCC和乐观锁挺像的,但是又不是一回事儿,我还没学明白,记下来,关于MVCC单独写一篇。