锁-隔离级别的一些补充
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了锁-隔离级别的一些补充相关的知识,希望对你有一定的参考价值。
参考技术A总而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁会把读和写都堵塞。
当我们执行 update / insert / delete 语句时,mysql 会自动为该语句加上排它锁。
加排它锁: select ...for update 语句(使用排他锁解决超卖)
加共享锁可以使用 select ... lock in share mode 语句
使用InnoDB引擎,如果筛选条件里面没有索引字段,就会锁住整张表,否则的话,锁住相应的行(因为innodb的行锁是针对索引建立的,没有索引升级为表锁)。
sharedLock 与 lockForUpdate 相同的地方是,都能避免同一行数据被其他 transaction 进行 update
不同的地方是:sharedLock 不会阻止其他 transaction 读取同一行
lockForUpdate 会阻止其他 transaction 读取同一行 (==需要特别注意的是,普通的非锁定读取读取依然可以读取到该行,只有 sharedLock 和 lockForUpdate 的读取会被阻止==。)
InnoDB使用间隙锁的目的有2个:
Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
幻读: 新插入的行,隔离级别是可重复读隔离级别, 幻读在当前读才可出现
执行步骤
由于表中没有id=7的记录:
表内数据
执行流程
==☆☆☆需要注意☆☆☆==
在这个例子中,lock in share mode 只锁覆盖索引,但是如果是 for update 就不一样了。 执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁,那么sessionB也会锁等待。
这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用 lock in share mode 来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将 session A 的查询语句改成 select d from t where c=5 lock in share mode。你可以自己验证一下效果。
表内数据
加锁过程
开始执行的时候,要找到第一个id=10的这行,因此next-key lock 是(5,10]。根据优化1,主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁,所以如果where=10,那么就锁10这一行。
范围查找就往后继续找,找到id=15的这行停下来,因此需要加next-key lock(10,15]( 等值查询才会退化成间隙锁 )
所以sessionA这时候锁的范围就是行锁id=10,和next-key lock(10,15] ,这样再看sessionB和session C
这里注意一点,sessionA定位查找id=10的行进行的时候,是当做等值查询来判断的,向后扫描代15的时候,用的是范围判断。
show status like \'innodb_row_lock%\'
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据结果着手指定优化方案 。
事务隔离级别
两个锁:
排他锁 被加锁的对象只能被持有锁的事务读取和修改,其他事务无法在该对象上加其他锁,也不能读取和修改该对象
共享锁 被加锁的对象可以被持锁事务读取,但是不能被修改,其他事务也可以在上面再加共享锁。
特别的,对共享锁: 如果两个事务对同一个资源上了共享锁,事务A 想更新该数据,那么它必须等待 事务B 释放其共享锁。
在运用 排他锁 和 共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。
1、一级封锁协议 (对应 read uncommited)
一级封锁协议是:事务 在对需要修改的数据上面(就是在发生修改的瞬间) 对其加共享锁(其他事务不能更改,但是可以读取-导致“脏读”),直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
一级封锁协议不能避免 丢失更新,脏读,不可重复读,幻读!
2、二级封锁协议 (对应read commited)
二级封锁协议是:1)事务 在对需要更新的数据 上(就是发生更新的瞬间) 加 排他锁 (直到事务结束) , 防止其他事务读取未提交的数据,这样,也就避免了 “脏读” 的情况。2)事务 对当前被读取的数据 上面加共享锁 (当读到时加上共享锁),一旦读完该行,立即 释放该 该行的共享锁 - 从数据库的底层实现更深入的来理解,既是,数据库会对游标当前的数据上加共享锁 , 但是当游标离开当前行的时候,立即释放该行的共享锁。
二级封锁协议除防止了“脏读”数据,但是不能避免 丢失更新,不可重复读,幻读 。
但在二级封锁协议中,由于读完数据后立即 释放共享锁,所以它不能避免可重复读 ,同时它也不能避免 丢失更新 ,如果事务A、B同时获取资源X,然后事务A先发起更新记录X,那么 事务B 将等待事务 A 执行完成,然后获得记录X 的排他锁,进行更改。这样事务 A 的更新将会被丢失。 具体情况如下:
事务A | 事务B |
读取X=100(同时上共享锁) | 读取X=100(同时上共享锁) |
读取成功(释放共享锁) | 读取成功(释放共享锁) |
UPDATE X=X+100 (上排他锁) | |
UPDATING A(等待事务A释放对X的排他锁) | |
事务成功(释放排他锁)X=200 | |
UPDATE X=X+200(成功上排他锁) | |
事务成功(释放排他锁)X=300 |
由此可以看到,事务A的提交被事务B覆盖了,所以不能防止 丢失更新。
如果要避免 丢失更新,我们需要额外的操作, 对凡是读到的数据加 共享锁 和排他锁 ,这个往往需要程序员自己编程实现,比如在Oracle 中,需要加 SELECT FOR UPDATE 语句,表明,凡是该事务读到的数据,额外的加上排他锁,防止其他数据同一时间获取相同数据,这样就防止了 丢失更新 !
3、三级封锁协议 (对应reapetable read )
三级封锁协议是:二级封锁协议加上事务 在读取数据的瞬间 必须先对其加 共享锁 ,但是 直到事务结束才释放 ,这样保证了可重复读(既是其他的事务职能读取该数据,但是不能更新该数据)。
三级封锁协议除防止了“脏”数据 和不可重复读 。但是这种情况不能避免 幻读 和 丢失更新 的情况,在事务 A 没有完成之前,事务 B 可以新增数据,那么 当事务 A 再次读取的时候,事务B 新增的数据会被读取到,这样,在该封锁协议下,幻读 就产生了。 如果事务A 和 事务B 同时读取了资源X=100,同样,如果事务A先对X进行 更新X=X+100,等待事务A执行完成X=200,那么事务B 获得X的排他锁,进行更新 X=X+200,然后提交 X=300,同样A的更新被B所覆盖!( 如果要避免 丢失更新,我们需要额外的操作, 对凡是读到的数据加 共享锁 和排他锁 ,这个往往需要程序员自己编程实现,比如在Oracle 中,需要加 SELECT FOR UPDATE 语句,表明,凡是读到的数据,我会加 排他锁,防止其他数据同一时间获取相同数据) !
进阶:repeatable read 导致死锁的情况(即便是 不同的资源在相同的顺序下获取)。 比如 事务1 读取 A,同时 事务2 也读取 A,那么事务1和事务2 同时对 A 上了共享锁,然后事务1 要UPDATE A,而此时 事务2 也要 UPDATE A,这个时候 事务1 等待 事务2 释放其在 A 上的共享锁,然后 事务2 要等待 事务1 释放其在 A 上的共享锁,这样,事务1 和 事务2 相互等待,产生死锁!(SQL Server/DB2 里面有 UPDATE LOCK 可以解决这种情况,具体的思路是,在 repeatable read 的情况下,将读取的数据 上的 UPDATE 锁,介于 共享锁 和 排他锁之间的一种锁,该锁的作用是 当出现上面这种情况后,事务1 和 事务2 对 A 上的是 UPDATE 锁,那么谁先 要修改 A,那么该事务就会将 UPDATE 锁可以顺利升级为 排他锁对该数据进行修改!)
4、最强封锁协议(对应Serialization)
四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对 事务中 所 读取 或者 更改的数据所在的表加表锁,也就是说,其他事务不能 读写 该表中的任何数据。这样所有的 脏读,不可重复读,幻读 ,都得以避免!
三 附Oracle 事务一致性原则
事务是定义和维护一致性的单位,封锁就是要保证这种一致性。如果
对封锁的要求高会增加开销,降低并发性和效率;有的事务并不严格要求
结果的质量(如用于统计的事务),如果加上严格的封锁则是不必要和不
经济的。因此有必要进行进一步的分析,考察不同级别的一致性对数据库
数据的质量及并行能力的影响。
一致性级别定义为如下的几个条件:
(1) 事务不修改其它任何事务的脏数据。脏数据是被其它事务修改过,
但尚未提交的数据。
(2) 在事务结束前不对被修改的资源解锁。
(3) 事务不读其它任何事务的脏数据。
(4) 在读前对数据加共享锁(RS)和行排它锁,直至事务结束。
* 满足条件1的事务叫第0级事务。
* 满足条件1和2的事务叫第1级一致性事务。
* 满足条件1、2和3的事务为2级一致性事务。ORACLE的读一致性保
证了事务不读其它事务的脏数据。
* 满足条件1、2、3和4的事务叫第3级一致性事务。
由ORACLE的三个性质:自动加隐式锁、在事务结束时释放锁和读一致
性,使ORACLE成为自动满足以上的0、1和2级一致性事务。因此,ORACLE
自动防止了脏读(写-读依赖)。但是,ORACLE不能自动防止丢失修改(写
-写依赖),读的不可重复性(读-写依赖),彻底解决并发性中的问题还
需满足第4个条件(3级一致性事务),这需要程序员根据实际情况编程。
方法如下:
* 如果想在一段时间内使一些数据不被其它事务改变,且在本事务内
仅仅查询数据,则可用SET TRANSACTION READ ONLY 语句达到这一
目的。
* 如果想在一事务内修改一数据,且避免丢失修改,则应在读这一数
据前用SELECT FOR UPDATE对该数据加锁。
* 如果想在一事务内读一数据,且想基于这一数据对其它数据修改,
则应在读数据前对此数据用SELECT FOR UPDATE加锁。对此种类型
的应用,用这条SQL语句加锁最恰当。
* 如果想避免不可重复读现象,可在读前用SELECT FOR UPDATE对数
据加锁,或用SET TRANSACTION READ ONLY设置只读事务。
四, 特殊情况
1) Read-Commit 的行锁导致其他事务一直被 hanging on的情况!
假设我们有 VARIANT 表, Trasaction A 对 Variant 中的字段 VariantName 1 进行了修改,但是事务未提交(假设,该事务将执行1个小时),此时 Trasaction B 要读VariantName(查询某一个VariantName 2 ),此时它会一直被Transaction A 阻塞,直到Transaction A 提交对 VariantName 的修改后,Transaction B才会得到VariantName 2 的查询结果,这样Transaction B最长可被阻塞1个小时!
这里,虽然Transaction A是针对 VariantName 1 上的修改,而 Transaction B 是读取 VariantName 2 , 对应的Variant Name不一样,但是此时,Transaction B并不知道 Transaction A 的结果(对Transaction B而言,它不清楚Transaction A提交的结果是什么),为了避免“脏读”,Transaction B会等待 Transaction A执行完事务以后,完成它对VariantName的修改后,才返回结果!
所以,在一个事务中,我们应该尽量把 SELECT Queries 放到最前面,把所有的 Update 放到最后面,避免不必要的等待!
特别的,如果上面这种情况,VariantName是Unique Index或者是Primary Key, 这个时候,Transaction B不会被Transaction A 阻塞!因为 Transaction B 知道 Transaction A提交的更改不会影响 他获取的VariantName 2 因为Transaction B 知道 VariantName 2 是唯一的!
以上是关于锁-隔离级别的一些补充的主要内容,如果未能解决你的问题,请参考以下文章
浅析SQL Server在可序列化隔离级别下,防止幻读的范围锁的锁定问题
浅析SQL Server在可序列化隔离级别下,防止幻读的范围锁的锁定问题