数据库事务隔离级别和使用锁实现机制

一、Mysql的共享锁和排他锁

1、共享锁
共享锁也叫读锁(Shared Lock),简称S锁,原理:一个事务获取了一个数据行的共享锁,其他事务能获得该行对应的共享锁,但不能获得排他锁,即一个事务在读取一个数据行的时候,其他事务也可以读,但不能对该数据行进行增删改。
2、排他锁
排他锁也叫写锁(Exclusive Lock),简称x锁,原理:一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁(排他锁或者共享锁),即一个事务在读取一个数据行的时候,其他事务不能对该数据行进行增删改查。

二、事务隔离级别的实现原理

1、读未提交(READ_UNCOMMITED)
(1)读取:事务对当前被读取的数据不加锁。
(2)更新:事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。

2、读已提交(READ_COMMITED)
(1)读取:事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,一旦读完该行立即释放该行级共享锁
(2)更新:事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。

3、可重复读(REPEATABLE_READ)
(1)读取:事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放
(2)更新:事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。

4、可串行化(SERIALIZABLE)
(1)读取:事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放。
(2)更新:事务在更新数据时,必须先对其加表级排他锁,直到事务结束才释放。

在运用 排他锁共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(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 是唯一的!

五、互联网项目中MySQL用什么事务隔离级别

Mysql默认的事务隔离级别是可重复读(Repeatable Read),那互联网项目中Mysql也是用默认隔离级别,不做修改么? OK,不是的,我们在项目中一般用读已提交(Read Commited)这个隔离级别! 居然是读已提交。
我们先来思考一个问题,在Oracle、SqlServer中都是选择读已提交(Read Commited)作为默认的隔离级别,为什么Mysql不选择读已提交(Read Commited)作为默认隔离级别,而选择可重复读(Repeatable Read)作为默认的隔离级别呢?
我们先明白一点!项目中是不用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别,原因有二:
①采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!
②采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带的分布式事务功能时才使用该隔离级别!
也就是说,我们该纠结都只有一个问题,究竟隔离级别是用读已经提交呢还是可重复读? 接下来对这两种级别进行对比,讲讲我们为什么选读已提交(Read Commited)作为事务隔离级别!

MySQL间隙锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(GAP LOCK)。举例来说,假如user表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

select * from  user where user_id > 100 for update;

这是一个范围条件的检索且要求加上排他锁,InnoDB不仅会对符合条件的user_id值为101的记录加锁,也会对user_id大于101(这些记录并不存在)的“间隙”加锁。

InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了user_id大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需要。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件;当然,对一条不存在的记录加锁,也会有间隙锁的问题。
间隙锁在InnoDB的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,所以间隙锁不分什么共享锁与排它锁。如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁,而不会使用Next-Key Lock的方式,也就是说不会对索引之间的间隙加锁。
要禁止间隙锁的话,可以把隔离级别降为读已提交,或者开启参数innodb_locks_unsafe_for_binlog。

RC与RR在锁方面的区别
1、RR要用到间隙锁,而RC则没有间隙锁。因为MySQL的RR需要间隙锁来解决幻读问题。而RC隔离级别则是允许存在不可重复读和幻读的。所以RC的并发一般要好于RR;在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多;
2、 RC 隔离级别,通过 where 条件过滤之后,不符合条件的记录上的行锁,会被释放掉,但是RR隔离级别,即使不符合where条件的记录,也不会释放行锁和间隙锁,所以从锁方面来看,RC的并发应该要好于RR;
3、RC隔离级别时,事务中的每一条select语句会读取到他自己执行时已经提交了的记录,也就是每一条select都有自己的一致性读ReadView; 而RR隔离级别时,事务中的一致性读的ReadView是以第一条select语句的运行时,作为本事务的一致性读snapshot的建立时间点的,只能读取该时间点之前已经提交的数据。

参考:
https://blog.csdn.net/fuzhongmin05/article/details/91126936
https://blog.csdn.net/DGenerationX/article/details/83647467
https://blog.csdn.net/qq_21359547/article/details/88824901
https://blog.csdn.net/chenyiminnanjing/article/details/82714341
数据库常用的锁有哪些:https://blog.csdn.net/Michaeles/article/details/86484385

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值