MySQL数据库锁与事务隔离级别

数据库锁
共享锁(Shared lock)
例1:
----------------------------------------
T1: select * from table (请想象它需要执行1个小时之久,后面的sql语句请都这么想象)
T2: update table set column1='hello'

过程:T1运行 (加共享锁)
T2运行等待T1运行完之后再运行T2

之所以要等,是因为T2在执行update前,试图对table表加一个排他锁,而数据库规定同一资源上不能同时共存共享锁和排他锁。所以T2必须等T1执行完,释放了共享锁,才能加上排他锁,然后才能开始执行update语句。

例2:
----------------------------------------
T1: select * from table
T2: select * from table

这里T2不用等待T1执行完,而是可以马上执行。

分析:

T1运行,则table被加锁,比如叫lockA
T2运行,再对table加一个共享锁,比如叫lockB。

两个锁是可以同时存在于同一资源上的(比如同一个表上)。这被称为共享锁与共享锁兼容。这意味着共享锁不阻止其它session同时读资源,但阻止其它session update

例3:
----------------------------------------
T1: select * from table
T2: select * from table
T3: update table set column1='hello'

这次,T2不用等T1运行完就能运行,T3却要等T1和T2都运行完才能运行。因为T3必须等T1和T2的共享锁全部释放才能进行加排他锁然后执行update操作。

例4:(死锁的发生)
----------------------------------------

T1:begin tran
select * from table (holdlock) (holdlock意思是加共享锁,直到事务结束才释放)
update table set column1='hello'

T2:begin tran
select * from table(holdlock)
update table set column1='world'

假设T1和T2同时达到select,T1对table加共享锁,T2也对加共享锁,当T1的select执行完,准备执行update时,根据锁机制,T1的共享锁需要升级到排他锁才能执行接下来的update.在升级排他锁前,必须等table上的其它共享锁释放, 但因为holdlock这样的共享锁只有等事务结束后才释放 ,所以因为T2的共享锁不释放而导致T1等(等T2释放共享锁,自己好升级成排他锁),同理,也因为T1的共享锁不释放而导致T2等。死锁产生了。

例5:
----------------------------------------
T1:begin tran
update table set column1='hello' where id=10

T2:begin tran
update table set column1='world' where id=20

这种语句虽然最为常见,很多人觉得它有机会产生死锁,但实际上要看情况,如果id是主键上面有索引,那么T1会一下子找到该条记录(id=10的记录),然后对该条记录加排他锁,T2,同样,一下子通过索引定位到记录,然后对id=20的记录加排他锁,这样T1和T2各更新各的,互不影响。T2也不需要等。

但如果id是普通的一列,没有索引。那么当T1对id=10这一行加排他锁后,T2为了找到id=20,需要对全表扫描,那么就会预先对表加上共享锁或更新锁或排他锁(依赖于数据库执行策略和方式,比如第一次执行和第二次执行数据库执行策略就会不同)。但因为T1已经为一条记录加了排他锁,导致T2的全表扫描进行不下去,就导致T2等待。

死锁怎么解决呢?一种办法是,如下:
例6:
---------------------------------------
T1:begin tran
select * from table (xlock) (xlock意思是直接对表加排他锁)
update table set column1='hello'

T2:begin tran
select * from table(xlock)
update table set column1='world'

这样,当T1的select 执行时,直接对表加上了排他锁,T2在执行select时,就需要等T1事务完全执行完才能执行。排除了死锁发生。但当第三个user过来想执行一个查询语句时,也因为排他锁的存在而不得不等待,第四个、第五个user也会因此而等待。在大并发情况下,让大家等待显得性能就太友好了,所以,这里引入了更新锁。

更新锁(Update lock)
为解决死锁,引入更新锁

例7:
----------------------------------------
T1:begin tran
select * from table(updlock) (加更新锁)
update table set column1='hello'

T2:begin tran
select * from table(updlock)
update table set column1='world'

更新锁的意思是:“我现在只想读,你们别人也可以读,但我将来可能会做更新操作,我已经获取了从共享锁(用来读)到排他锁(用来更新)的资格”。一个事务只能有一个更新锁获此资格。

T1执行select,加更新锁。
T2运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。 (T2加的是更新锁,更新锁与更新锁不兼容, 如果加的是共享锁, 共享锁和更新锁可以兼容,即T1,T2不可同时进行,但是T3,T4,T5只要不是事务,还是可以正常查询)

当后来有user3、user4...需要查询table表中的数据时,并不会因为T1的select在执行就被阻塞,照样能查询,相比起例6,这提高了效率

例8:
----------------------------------------
T1:begin
select * from table(updlock) (加更新锁)
update table set column1='hello' (重点:这里T1做update时,不需要等T2释放什么,而是直接把更新锁升级为排他锁,然后执行update)

T2:begin
select * from table (T1加的更新锁不影响T2读取)
update table set column1='world' (T2的update需要等T1的update做完才能执行)

我们以这个例子来加深更新锁的理解,

第一种情况:T1先达,T2紧接到达;在这种情况中,T1先对表加更新锁,T2对表加共享锁,假设T2的select先执行完,准备执行update,发现已有更新锁存在,T2等。T1执行这时才执行完select,准备执行update,更新锁升级为排他锁,然后执行update,执行完成,事务结束,释放锁,T2才轮到执行update。

第二种情况:T2先达,T1紧接达;在这种情况,T2先对表加共享锁,T1达后,T1对表加更新锁,假设T2 select先结束,准备update,发现已有更新锁,则等待,后面步骤就跟第一种情况一样了。这个例子是说明: 排他锁与更新锁是不兼容的,它们不能同时加在同一子资源上。

排他锁(独占锁,Exclusive Locks)
这个简单,即其它事务既不能读,又不能改排他锁锁定的资源。
例9
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。

例10 (假设id都是自增长且连续的)
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>900

如同例9,T1先达,T2立刻也到,T1加的排他锁会阻塞T2的update.

意向锁(Intent Locks)
意向锁就是说在屋(比如代表一个表)门口设置一个标识,说明屋子里有人(比如代表某些记录)被锁住了。另一个人想知道屋子里是否有人被锁,不用进屋子里一个一个的去查,直接看门口标识就行了。

当一个表中的某一行被加上排他锁后,该表就不能再被加表锁。数据库程序如何知道该表不能被加表锁?一种方式是逐条的判断该表的每一条记录是否已经有排他锁,另一种方式是直接在表这一层级检查表本身是否有意向锁,不需要逐条判断。显然后者效率高。

例11:
----------------------------------------
T1: begin tran
select * from table (xlock) where id=10 --意思是对id=10这一行强加排他锁
T2: begin tran
select * from table (tablock) --意思是要加表级锁  

假设T1先执行,T2后执行,T2执行时,欲加表锁,为判断是否可以加表锁,数据库系统要逐条判断table表每行记录是否已有排他锁,如果发现其中一行已经有排他锁了,就不允许再加表锁了。只是这样逐条判断效率太低了。

实际上,数据库系统不是这样工作的。 当T1的select执行时,系统对表table的id=10的这一行加了排他锁,还同时悄悄的对整个表加了意向排他锁(IX) ,当T2执行表锁时,只需要看到这个表已经有意向排他锁存在,就直接等待,而不需要逐条检查资源了。

计划锁(Schema Locks)
例12:
----------------------------------------
alter table .... (加schema locks,称之为Schema modification (Sch-M) locks

DDL语句都会加Sch-M锁,该锁不允许任何其它session连接该表。连都连不了这个表了,当然更不用说想对该表执行什么sql语句了。

事务隔离级别
Read Uncommit (未提交读。允许脏读)
事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是 脏读

一级封锁协议:
一级封锁协议,事务在对需要修改的数据上面(就是在发生修改的瞬间) 对其加共享锁( 其他事务不能更改,但是可以读取-导致“脏读” ),直到事务结束才释放。

Read committed(读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。)
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了( 第二次检测金额当然要等待妻子转出金额事务提交完 )。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了 一个事务范围内两个相同的查询却返回了不同数据 ,这就是 不可重复读

二级封锁协议:
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覆盖了

Repeatable read(重复读,就是在开始读取数据(事务开启)时,不再允许修改操作)
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时( 事务开启,不允许其他事务的UPDATE修改操作 ),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是, 不可重复读对应的是修改,即UPDATE操作 。但是可能还会有 幻读 问题。因为 幻读问题对应的是插入INSERT操作,而不是UPDATE操作

什么时候会出现幻读?
事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即 新增INSERT 了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。

三级封锁协议:
三级封锁协议是:二级封锁协议加上事务 在读取数据的瞬间 必须先对其加  共享锁  , 但是  直到 事务结束才释放   ,这样保证了可重复读(既是其他的事务职能读取该数据,但是不能更新该数据)。
三级封锁协议除防止了 “脏”数据  和 不可重复读  。但是这种情况不能避免  幻读 和 丢失更新   的情况,在事务 A 没有完成之前,事务 B 可以新增数据 ,那么 当事务 A 再次读取的时候,事务B 新增的数据会被读取到

进阶:repeatable read 导致死锁的情况,参考上面讲解共享锁的文章

Serializable 序列化
Serializable 是最高的事务隔离级别,在该级别下,事务 串行化顺序执行 ,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

四级封锁协议:
四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对 事务中 所 读取 或者 更改的数据所在的表加表锁,也就是说,其他事务不能 读写 该表中的任何数据。这样所有的  脏读,不可重复读,幻读  ,都得以避免!


值得一提的是:大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。


悲观锁(Pessimistic Lock),正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。


事务的特性: 
1.原子性  事务是数据库逻辑的工作单元,事务包括的所有操作,要么都做,要么都不做。 
2.一致性  事务执行的结果是使数据库从一个一致性状态变成另一个一致性状态。一致性与原子性是密切相关的。 
3.隔离性  一个事务的执行不能被其他事务干扰。 
4.持久性  一个事务一旦提交,它对数据库中数据的改变应该是永久性的。

猜你喜欢

转载自blog.csdn.net/caohao0591/article/details/79887028