mysql的锁、死锁、隔离级别及其实现原理(*)

先说一下什么是事务:
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务的结束有两种,当事务中的所有步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消事务开始时的所有操作。

事务的ACID

事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
1 、原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。

2 、一致性。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。

3 、隔离性。一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

4 、持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

三种读异常(三种并发问题)

脏读Drity Read:读取了其他未提交的数据,但是未提交的事务进行了回滚。
不可重复读Non-repeatable read:事务1读取某数据,事务二进行了更新并提交,事务一再来读的时候数据已经被改变(更新)。
幻读Phantom Read:事务1读取某数据,事务二进行了新增并提交,事务1读,发现有新的数据出现。

事务的四种隔离级别

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。

低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

Read Uncommitted 读未提交(select不加锁):

all事务都能看到其他未提交事务的执行结果。性能很菜。
读取未提交的数据,被称之为脏读。

Read Committed 读已提交

(普通select快照读,锁select /update /delete 会使用记录锁,除了在外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会封锁区间)

除了Mysql之外的大多数数据库系统的默认隔离级别。一个事务只能看见已经提交事务所做的改变。解决了脏读的问题,但是依然有不可重复读的异常。

Repeatable Read可重复读

普通select快照读,锁select /update /delete 根据查询条件情况,会选择记录锁,或者间隙锁/临键锁,以防止读取到幻影记录

mysql的默认隔离级别。保证了同一个事务的多个实例在并发读取数据的时候,会看到同样的数据行。解决了脏读和不可重复读的异常,存在幻读的异常。

InnoDB和Falcon、PBXT存储引擎通过MVCC(多版本并发控制)机制进行解决该问题。

Serializable 可串行化

select隐式转化为select … in share mode,会被update与delete互斥;

强制事务排序,解决三个异常。其实就是在每一个读的数据行上加上共享锁,在这个级别,可能导致大量的超时现象和锁竞争

在这里插入图片描述

快照读和当前读

快照读:读取的是快照版本,也就是历史版本

当前读:读取的是最新版本

普通的SELECT就是快照读;
而UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

锁定读

SELECT … LOCK IN SHARE MODE(加S锁 共享锁)、SELECT … FOR UPDATE(加排它锁 X锁)就是锁定读。
在这里插入图片描述

一致性非锁定读

consistent read (一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。

Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ(读已提交和可重复读)隔离级别下普通SELECT语句默认的模式。一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们。

两段锁协议

有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。
这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)。
加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。
加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。

**解锁阶段:**当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
在这里插入图片描述

数据库中的悲观锁和乐观锁

悲观锁,正如它的名字那样,数据库总是认为别人会去修改它所要操作的数据,因此在数据库处理过程中将数据加锁。其实现依靠数据库底层。

乐观锁,如它的名字那样,总是认为别人不会去修改,只有在提交更新的时候去检查数据的状态。通常是给数据增加一个字段来标识数据的版本

解决不可重复读的原理-- MVCC

InnoDB的默认隔离级别是RR(Repeatable Read)。
在这种隔离级别下,普通的select使用快照读,一种不加锁的一致性读。其底层是使用MVCC实现的。

加锁的selectselect … in share mode(共享锁) / select … for update(排它锁) 它们的锁,依赖于它们是否在唯一索引(unique index)上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):

  1. 在唯一索引上使用唯一的查询条件,会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock)
  2. 范围查询条件,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,以及避免不可重复的读(避免幻读和不可重复读)。
    (详见下面的InnoDB实现解决幻读的原理)

MVCC原理

多版本并发控制。用于在mysql中解决不可重复读异常的情况。

MVCC只在可重复读和读已提交两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(4),因为读未提交总是读取最新的数据行,而不是符合当前事务版本的数据行。而串行化则会对所有读取的行都加锁。

由于在数据库中采用锁机制进行并发控制系统开销较大,而MVCC可以在大多数情况下代替行级锁,降低系统开销。

其原理是:保存数据在不同时间点的快照。有两种,悲观并发控制和乐观并发控制。

InnoDB中的MVCC是一个乐观并发控制,是通过在每行的记录后面保存两个隐藏的列来实现的。
在这里插入图片描述
(一列是保存了该行的创建时间,另一个保存的是该行的删除时间,这里的时间指的是系统版本号);每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号作为事务的ID。
那么RR级别下的增删改查就变成了:
在这里插入图片描述
具体如何实现解决不可重复读问题,详见这篇博客,好好看一下,有版本号的递增流程,很易懂。https://blog.csdn.net/whoamiyang/article/details/51901888

要理解MVCC的原理知道上面的就行了,其实还有文章深入研究:
InnoDB会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。
**DB_TRX_ID表示最后一个事务的ID,**每开启一个新事务,事务的版本号就会递增;
DB_ROLL_PTR指向当前记录项的undo log信息;
DB_ROW_ID标识插入的新的数据行的id。

InnoDB解决幻读的原理

在可重复读的基础上加next-key锁。

  1. Record Locks(记录锁):在索引记录上加锁。
  2. Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
  3. Next-Key Locks(临键锁):在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于是Record Locks与Gap Locks的一个结合。
    在这里插入图片描述在这里插入图片描述

总之:在这里插入图片描述

数据库中的锁

InnoDB中锁的实现原理:
InnoDB中的行锁是通过给索引加上锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。

so,只有通过索引条件检索数据才会使用行锁,否则就使用表锁。
索引分为主键索引和二级索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了二级索引,MySQL会先锁定该二级索引,再锁定相关的主键索引。

读锁(Share Locks共享锁 S锁)

一个事务对数据对A加上读锁,其他事务只能再对A加读锁不可加写锁。

保证了事务在读A的时候不能被修改。

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

写锁(Exclusive Locks 排它锁 X锁)

一个事务对A加了写锁,其他事务不能再加任何锁。
更新操作(insert update delete)过程中用写锁。

SELECT * FROM table_name WHERE ... FOR UPDATE;

行锁和表锁

行锁,顾名思义,是加在索引行(对!是索引行!不是数据行!)上的锁。比如select * from user where id=1 and
id=10 for update,就会在id=1和id=10的索引行上加Record Lock。

当在InnoDB的默认隔离级别下(可重复读),如果检索条件有索引(包括主键索引)时,默认加锁方式为next-key锁(行锁,防止幻读);
若检索条件没有索引,更新数据时就会锁住整张表**(表锁)**。

事务隔离级别为串行化时,读写数据都会锁住整张表

意向共享锁和意向排他锁

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB引入了两种内部使用的意向锁(Intention Locks),这两种锁都是表锁。

意向锁是InnoDB自动加的,不需要用户的干预。

意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。

意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

间隙锁 Gap 锁 和Next-key锁

间隙锁,它会锁住两个索引之间的区域。比如select * from user where id>1 and id<10 for update,就会在id为(1,10)的索引区间上加Gap Lock。

next-key也叫间隙锁or临键锁,锁名有点迷,它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from user where id>=1 and id<=10 for update,就会在id为[1,10]的索引闭区间上加Next-Key Lock。

在这里插入图片描述

死锁

死锁就是两个or两个以上的事务相互等待对方释放锁,形成死循环所造成的。
MYISAM是不会出现死锁的,因为MYISAM都是一次性获取所有需要的锁(表锁)

InnoDB中死锁发生的情况

  1. 多个事务按照不同的顺序锁定相同的数据集导致的死锁:(两个or多个session之间的加锁顺序不一致)如果多个事务按不同的顺序锁定相同的数据集,此时事务之间就会形成循环等待造成死锁,这是一种最常见也比较容易理解的死锁。
  2. 索引不合理导致的死锁:由于InnoDB的锁是加在索引上的,因此索引不合理将直接导致锁定范围增大,发生锁冲突和死锁的的概率也随着增加。
  3. 插入Intention Gap锁与间隙锁冲突导致的死锁
    在这里插入图片描述
  4. 唯一键值冲突导致的死锁
    在这里插入图片描述
    死锁举例:
    这个文章中的例子很容易明白:https://www.cnblogs.com/zejin2008/p/5262751.html
    当执行"UPDATE date =1011 WHERE date=1000" 语句的时候会锁定date索引,由于date是非主键索引,所以Mysql还会去请求锁定ID索引;
    当另一个SQL语句与语句1几乎同时执行时:“UPDATE date=1010 WHERE ID=1” 对于语句2 Mysql会先锁定ID索引,由于语句2操作了date字段,所以Mysql还会请求锁定date索引。这时。彼此锁定着对方需要的索引,又都在等待对方释放锁定。所以出现了"死锁"的情况。

避免死锁的常用方法:

如果出现死锁,可以用mysql> show engine innodb status\G命令来确定最后一个死锁产生的原因。
返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。
在这里插入图片描述

  1. 减少事务操作的记录数

  2. (1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

  3. (2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

  4. (3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/88943771
今日推荐