多用户并发操作数据库的时候,如果严格执行串行准则(Serializable ),当然是最稳妥安全的做法——比如我们去车站买票,只开放一个窗口,人人都得排队,依次买票或退票。但是这个世界是追求效率的,于是乎,车站可能会多开几个窗口,让买票速度变快。随之而来,会产生资源冲突——同一个座位可能被卖给多个人、最后一张票可能被卖给多个人、系统显示某个座位已售售票员以为是这个人买的其实是另一个人买的、一个人正在退票呢可其他窗口的售票员却告诉大家票已经售光了回家去吧,等等。
数据库访问也同理,如果要把引发一系列问题归纳一下,基本上是:读脏数据、不可重复读取、幻读、丢失更新(读这篇),为了避免这些问题,可采取【锁】机制。
最基本的锁类型:
共享锁 = 读锁,当一个事务对某几行或某表读锁时,允许其他事务对这部分数据进行读操作或上读锁,但不允许其他事务做写操作或上排他锁;为啥不允许写或写锁呢?是为了避免脏读;
排他锁 = 写锁,当一个事务对某几行或某表写锁时,只允许其他事务读,不允许其他事务写或上任何锁——凡是涉及到写操作的,都要上排他的锁,也是为了避免脏读。
说句废话,读操作select和读锁是两个概念,一般情况下,读操作可以不加锁,也可以加读锁,在必要的情况下,还可以加排他锁。这取决于想要达到的目的。
当前事务的锁 |
其他事务的行为 | |||
读 | 读锁 | 写 | 写锁 | |
共享锁 | 允许 | 允许 | 不许 | 不许 |
排它锁 | ** | 不许 | 不许 | 不许 |
**备注:部分数据(某行或某表)上了排他锁,在有些数据库或某些版本中,是允许其他事务做读操作的。
避免死锁或提高性能的锁类型:
更新锁,为解决死锁,引入更新锁。更新锁的意思是,当前还在只读,并没有使用到排他锁,但已经预定了未来会使用排他锁。怎么预定的呢?就是当前获得一个更新锁,相当于获得了未来使用排他锁的资格,类似于学位,上学前就必须获得这个学位,未来才有资格上学。注意: 1)一个事务只能有一个更新锁获此资格,类似于一户只能有一个学位; 2)同一时间不能在同一资源上有两个更新锁,这个容易理解,即,更新锁之间是排他的; 3)排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。用学位来比喻的话,就是说一个家庭的小学学位正在被使用(上了排他锁),是不可以同时被这个家庭或其他家庭用来预定学位的(不能上更新锁),必须等这个家庭的6年学位使用完,才能被预定(上更新锁)或被使用(上排他锁); 3)共享锁和更新锁可以同时在同一个资源上的,即他俩兼容。
锁兼容关系 | 共享锁 | 排他锁 | 更新锁 |
共享锁 | 兼容 | 不容 | 兼容 |
排他锁 | 不容 | 不容 | 不容 |
更新锁 | 兼容 | 不容 | 不容 |
意向锁,举个例子,某行修改时,会加上排他锁,同时,数据库会默默给该表加意向锁,表示里面有记录正被锁定。其他事务如果也想在这个表的某些行加表锁,直接在表这一层级检查表本身是否有意向锁,提高效率。如果没有意向锁这个类似指示灯的东西存在,其他人加表锁之前就得扫描全表,查看是否有记录正被锁定。可以认为意向锁就是个标记,其他事务只需要看到这个表已经有意向排他锁存在,就直接等待。
其实上面还提及了一个维度,即加锁的范围/粒度,比如
行锁,会发生死锁,加锁慢,但锁定粒度小,并发度高
页锁,会发生死锁,开销、粒度、并发度介于中间
表锁,不会发生死锁,开销小,但因为锁粒度大,并发度最低。
--------------------------------------------------------------------------------------------------------
看一个场景,事务A和事务B同时对同一张表进行操作,前两个动作都是先读后写。
步骤 | 事务A | 事务B |
第一步 | 读 1~5 行 | 读 6~10 行 |
第二步 | 写 6~10 行 | 写 1~5 行 |
第一步读数据,考虑到后面还有写操作,如果数据库设计者允许一个事务读另一个事务未提交的数据(READ_COMMITTED)可以吗?这样会发生脏读。如果设计师没设计加锁机制可以吗?会发生不可重复读,幻读的问题,实际造成的问题要看这个读或写的具体动作。所以,第一步应该是加上共享锁,让AB都能同时读,提高效率的同时能阻塞其他事务发出的排他锁。
第二步是读,自然是想到上排他锁,无论是A先写、B先写、还是他们恰好同时抵达写步骤,排他锁都只有一个,最终会有个先后次序。截止目前,看似一切安好,但其实忽略了一点,即前面的共享锁是否已经释放了???如果要求共享锁在操作完成后就立即释放,即A或B任何一个读完数据后就立即释放共享锁并上一把排他锁,毫无疑问会造成另一方B或A发生不可重复读或幻读。如果要求共享锁必须在一个事务完成后才释放,不会出现刚刚两个问题,但会造成死锁——想想看,共享锁不释放,即使A先读完,排他锁也没法获得,要等B事务结束才行获得;这边B好不容易读完了,也开始排队申请排他锁,需要等A事务结束;两个事务产生了循环等待链条,我等待你的资源,你却等待我的资源,谁都不释放,永远无法结束。
步骤 | 加锁 | 可能发生的问题 |
第一步 | 共享锁 | 情况OK |
第二步 | 排他锁 |
2 事务结束后才释放共享锁: 死锁 |
只读已提交(READ_COMMITTED)的机制解决了脏读。共享锁和排他锁解决了不可重复读等问题,但是因为共享锁释放的时间点不同,却带来了新的问题——死锁,解决方法是,引入更新锁。
看看更新锁是怎么运作的?
更新锁其实就是对事务先上共享锁再上排他锁。还是这个例子,可以看出,事务A和B如果直接在第一步就使用更新锁,无论加在A上还是加在B上,都可以解决死锁的问题(当然,如果还存在其他事务并发,最好是都加上更新锁):
事务A | 事务B | 结果 |
有更新锁 | 无更新锁 |
第一步无论A和B谁先开始读(select),都可以共享资源/发出共享锁; 无论谁先读完,第二步都是A先写数据(insert,update,delete),B等待A。 |
无 | 有 |
第一步无论A和B谁先开始读(select),都可以共享资源/发出共享锁; 第二步都是B先获得写数据(insert,update,delete)的锁,A等待B。 |
有 | 有 |
第一步无论A和B谁先开始读(select),先读的那个会先使用更新锁,允许另一个事务读; 第二步,还是那个先读的事务先写数据,另一事物等待。 |
-------------------------------------------------------------------------------------------------------------
各种锁的兼容关系表:
锁模式 Request Mode
|
IS | S | U |
IX |
SIX |
X | |
意向 共享锁 | Intent shared lock (IS) | 兼容 | 兼容 | 兼容 | 兼容 |
兼容 | 不容 |
共享锁 | Shared lock (S) | 兼容 | 兼容 | 兼容 | 不容 | 不容 | 不容 |
更新锁 | Update lock (U) | 兼容 | 兼容 | 不容 | 不容 | 不容 | 不容 |
意向 排他锁 | Intent exclusive lock (IX) | 兼容 | 不容 | 不容 | 兼容 | 不容 | 不容 |
意向 排他 共享锁 | Shared with intent exclusive lock (SIX) | 兼容 | 不容 | 不容 | 不容 | 不容 | 不容 |
排他锁 / 独占锁 | Exclusive lock (X) | 不容 | 不容 | 不容 | 不容 | 不容 | 不容 |
还有计划锁,Sch-M,Sch-S:
对数据库结构改变时用Sch-M (DDL语句会加Sch-M modification 锁,比如alter table语句;该锁不允许任何其它session会话连接该表);
对查询进行编译时用架构稳定性锁Sch-S (用jdbc向数据库发送了一条新的sql语句"select * from table_A",数据库要先对之进行编译,在编译期间,也会加Sch-S stability 锁,且此段时间其他的session可以对表A做任何update,delete,加排他锁等操作,但不能做DLL中的alter table等操作)—— 这两种锁不会阻塞任何事务锁,包括独占锁。使用WITH(NOLOCK)的会话也可能被正在执行的还未提交的Schema Change阻塞,如ALTER TABLE table_A ADD Grade VARCHAR(10)。还有一种情况,以SQL Server为例,当使用者在用 WITH(NOLOCK) 的时候,本意是让执行语句不加任和锁,相当于 READ UNCOMMITTED 隔离模式;实际上,使用了WITH(NOLOCK)后,数据库依然对该表对象生成Sch-S(架构稳定性)锁以及DB类型的共享锁,这是数据库的默认机制。
参考资料:
SQL SERVER数据库锁机制 https://blog.csdn.net/samjustin1/article/details/52210125#reply
SQL SERVER事务的加锁顺序 https://fengjianchao-vip.iteye.com/blog/1190319
SQL SERVER中WITH(NOLOCK)浅析 https://www.cnblogs.com/kerrycode/p/3946268.html
MYSQL 行锁和表锁详解 https://blog.csdn.net/nicajonh/article/details/78814987
END