MySQL的事务和锁

什么是事务

事务:是数据库操做的最小工做单元,是做为单个逻辑工做单元执行的一系列操做;这些操做做为一个总体一
起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操做集合(工做逻辑单元);mysql

事务的简单操做

显式启动事务语句,begin或者start transaction;sql

提交commit;数据库

回滚rollback;缓存

SET AUTOCOMMIT=0 禁止自动提交安全

SET AUTOCOMMIT=1 开启自动提交并发

事务的四大特性(ACID)

  • 原子性(Atomicity):事务一个事务(transaction)中的全部操做,要么所有完成,要么所有不完成,不会结束在中间某个环节。事务在执行过程当中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务历来没有执行过同样。
  • 一致性(Consistency):一致性是指事务从一种一致性状态转变成另外一种一致性状态。在事务开始以前和事务结束以后,数据库的完整性约束没有破坏。即:A和B一共5000元,不管双方进行多少次转帐,他们的总和都只能是5000元。或者说表中有一个字段为name,为惟一约束,即在表中姓名不能重复。若是一个事务对name字段进行了修改,可是事务提交或者事务操做发生回滚后,表中的姓名变得非惟一了,这就破坏了事务的一致性要求。所以,事务是一致性单元,若是事务中某个动做失败了,系统能够自动撤销事务,返回初始状态。
  • 隔离性(Ioslation):隔离性是当多个用户并发访问数据库时,好比操做同一张表时,数据库为每个用户开启的事务,不能被其余事务的操做所干扰,多个并发事务之间要相互隔离。即该事务提交前对其余事物都不可见。经过锁或者MVCC实现,MVCC(多版本并发控制)在可重复读的位置举例介绍。
  • 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即使是在数据库系统遇到故障的状况下也不会丢失提交事务的操做。

事务的隔离性经过锁和MVCC机制实现,原子性、一致性和持久性经过redo/undo log 来完成。redo log 称为重作日志,用来保证事务的原子性和持久性。undo log 称为撤销日志,用来保证事务的一致性。mvc

redo log/undo log

redo log

redo log重作日志,用来保证事务的原子性和持久性。由两部分组成:一是内存中的重作日志缓冲(redo log buffer),其是易失的;二是重作日志文件,其是持久的。InnoDB存储引擎当事务提交时,必须先将该事务的全部日志写入重作日志进行持久化,待事务的commit操做完成才算完成。当数据库挂了以后,经过扫描redo日志,就能找出那些没有刷盘的数据页(在崩溃以前可能数据页仅仅在内存中修改了,可是还没来得及写盘),保证数据不丢。函数

因为重作日志文件打开并无使用O_DIRECT选项,所以重作日志缓冲先写入文件缓存系统。为了确保每第二天志都写入重作日志文件,在每次将重作日志缓冲写入重作日志后,InnoDB存储引擎都须要调用一次fsync操做。高并发

O_DIRECT在执行磁盘IO时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)或者裸IO(raw IO)。性能

fsync函数的功能是确保文件全部已修改的内容已经正确同步到硬盘上,该调用会阻塞等待直到设备报告IO完成。

事务更新数据操做流程:

1.当事务执行更新数据的操做时,会先从mysql中读取出数据到内存中,而后对内存中数据进行修改操做。

2.生成一条重作日志并写入redo log buffer,记录的是数据被修改后的值。

3.按期将内存中修改的数据刷新到磁盘中,这是由innodb_flush_log_at_trx_commit决定的,重作日志文件打开并无使用O_DIRECT选项,所以重作日志缓冲先写入文件缓存系统,最后经过执行fsync将数据写入磁盘。

  • 当设置该值为 1 时,每次事务提交都要作一次 fsync,这是最安全的配置,即便宕机也不会丢失事务;
  • 当设置为 2 时,则在事务提交时只作 write 操做,只保证写到文件系统的缓存,不进行fsync操做。所以mysql数据库发生宕机而操做系统不发生宕机时不会丢失数据。操做系统宕机会丢失文件系统缓存中未刷新到重作日志中的事务;
  • 当设置为 0 时,事务提交不会触发 redo 写操做,而是留给后台线程每秒一次的fsync操做,所以数据库宕机将最多丢失一秒钟内的事务。

4.commit提交后数据写入redo log file中,而后将数据写入到数据库。

undo log

Undo log是InnoDB MVCC事务特性的重要组成部分。当咱们对记录作了变动操做时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可使用独立的Undo 表空间。
在Innodb当中,INSERT操做在事务提交前只对当前事务可见,Undo log在事务提交后即会被删除,由于新插入的数据没有历史版本,因此无需维护Undo log。而对于UPDATE、DELETE,责须要维护多版本信息。 在InnoDB当中,UPDATE和DELETE操做产生的Undo log都属于同一类型:update_undo。(update能够视为insert新数据到原位置,delete旧数据,undo log暂时保留旧数据)。

​ Session1(如下简称S1)和Session2(如下简称S2)同时访问(不必定同时发起,但S1和S2事务有重叠)同一数据A,S1想要将数据A修改成数据B,S2想要读取数据A的数据。没有MVCC只能依赖加锁了,谁拥有锁谁先执行,另外一个等待。可是高并发下效率很低。InnoDB存储引擎经过多版本控制的方式来读取当前执行时间数据库中行的数据,若是读取的行正在执行DELETE或UPDATE操做,这是读取操做不会所以等待行上锁的释放。相反的,InnoDB会去读取行的一个快照数据(Undo log)。在InnoDB当中,要对一条数据进行处理,会先看这条数据的版本号是否大于自身事务版本(非RU隔离级别下当前事务发生以后的事务对当前事务来讲是不可见的),若是大于,则从历史快照(undo log链)中获取旧版本数据,来保证数据一致性。而因为历史版本数据存放在undo页当中,对数据修改所加的锁对于undo页没有影响,因此不会影响用户对历史数据的读,从而达到非一致性锁定读,提升并发性能。

另外,若是出现了错误或者用户手动执行了rollback,系统能够利用undo log中的备份将数据恢复到事务开始以前的状态。与redo log不一样的是,磁盘上不存在单独的undo log 文件,他存放在数据库内部的特殊段(segment)中。

事务的隔离等级

mysql默认的隔离等级是可重复读,若是想要在mysql启动时就修改mysql的隔离等级,须要修改配置文件,在[mysqld]中添加以下内容:

[mysqld]
transaction-isolation = READ-COMMITTED

若是想要查看当前事务隔离级别,可使用:

mysql>select @@tx_isolation\G;

脏读:事务A读取了事务B更新、未提交的数据,而后B回滚操做,那么A读取到的数据是脏数据(没有用的数据)。

不可重复读:事务 B 在事务A屡次读取的过程当中,对数据做了更新操做并提交,致使事务A两次读取同一数据不一致。主要针对数据更新的。

幻读:同一事务中,两次按相同条件查询到的记录不同。形成幻读的缘由在于事务处理没有结束时,其余事务对同一数据集合增长或删除了记录。在mysql中MVCC在必定程度上解决了幻读,但并无彻底解决。以下:

事务在插入已经检查过不存在的记录时,插入失败,出现冲突显示这条数据已经存在了。好比:A查询id为2的数据不存在则插入一条id为2的数据,在事务A查询完毕后,事务B插入了一条id为2的数据,并提交了。此时事务A向表中插入id为2的数据插入失败。第二次的insert其实也属于隐式的读取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。错误提示以下:

Duplicate entry 2 for key 'id'  # 关键字id的重复条目2

注意:不可重复读和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。

读未提交

最低级别,任何状况都没法保证,可能形成脏读、幻读、不可重复读,效率最高,但最不安全。

读提交

可避免脏读的发生。

可重复读

可避免脏读、不可重复读的发生。(mysql默认的级别)

可重复读就是在一个事务内,对于同一个查询请求,屡次执行,获取到的数据集是同样的。这通常是经过保存事务的快照实现的。MVCC会保存某个时间点上的快照,意味着事务能够看到一个一致性的状态。可是不一样事务在同一个时间点上看到同一个表中的数据多是不一样的。

串行化

可避免脏读、不可重复读、幻读的发生。可是效率最低。事务的最高级别,在每一个读的数据行上,加上锁,使之不可能相互冲突。若是有事务对该数据行操做,那么其余事务就要等他结束才能进行操做。

MVCC

可重复读使用的是一种叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本并发控制。InnoDB在每行记录后面保存两个隐藏的列来,分别保存了这个行的建立时间和行的删除时间。这里存储的并非实际的时间值,而是系统版本号。每开启一个新事务,事务的版本号就会递增。因此增删改查中对版本号的做用以下:

insert:把当前系统(事务)版本号做为行记录的版本号。

  • 建立一个事务,ID为1,插入两条数据。

    begin;
    insert into user(name) values('xiaoqi');
    insert into user(name) values('dada');
    commit;
    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined

select:事务每次只能读到行记录的版本号小于等于这次系统版本号的记录,这样能够确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。在事务自身执行过程当中,不会读取到其余事务进行的操做。

行的删除版本要么未定义,要么大于当前事务版本号(这能够确保事务读取到的行,在事务开始以前未被删除), 只有条件一、2同时知足的记录,才能返回做为查询结果。

delete:把当前系统版本号做为行记录的删除版本号。

  • 建立第二个事务,ID为2,进行删除处理。

    begin;
    select * from user;   # 执行事务2的第一步
    select * from user;   # 执行事务2的第二步
    commit;

假设1:在执行ID为2的事务第一步的时候,有另外一个事务ID为3往这个表里插入了一条数据;

  • 建立第三个事务,ID为3

    begin;
    insert into user(name) values('jianren');
    commit;

    表数据以下:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined
    3 jianren 3 undefined

    而后,继续执行ID为2的事务的第二步,因为id=3的数据的建立时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,因此id=3的数据行并不会在执行事务2中的第二步被检索出来,在事务2中的两条select 语句检索出来的数据均以下表:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined

假设2:在执行ID为2的事务第一步以后,假设执行完ID为3的事务后,接着又执行了ID为4的事务。

  • 建立第四个事务,ID为4

    begin; 
    delete from user where id=1; 
    commit;

    此时数据库中表以下:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 4
    2 dada 1 undefined
    3 jianren 3 undefined

    而后执行ID为2的事务的第二步。根据SELECT 检索条件能够知道,它会检索建立时间(建立事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务ID的行,而id=3的行上面已经说过,而id=1的行因为删除时间(删除事务的ID)大于当前事务的ID,因此事务2的第二步select * from user;也会把id=1的数据检索出来。最终,事务2中的两条select 语句检索出来的数据都以下:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 4
    2 dada 1 undefined

update:插入一条新记录,并把当前事务版本号做为行记录的版本号,同时保存当前系统版本号到原有的行做为删除版本号。

假设3:假设在执行完事务2的第一步后,其它用户执行了事务三、4,这时,又有一个用户对这张表执行了UPDATE操做,建立了第五个事务,ID为5。

  • 第五个事务,ID为5

    begin; 
    update user set name='dazi' where id=2;
    commit;

    此时数据库中表以下:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 4
    2 dada 1 5
    3 jianren 3 undefined
    2 dazi 5 undefined

    继续执行事务2的第二步,根据select 语句的检索条件,获得下表:

    id name 建立时间(事务ID) 删除时间(事务ID)
    1 xiaoqi 1 4
    2 dada 1 5

    仍是和事务2中第一步select 获得相同的结果。

这就是mysql中事务的可重复读,即一个事务执行后,不管其余事务对其进行怎样的修改读到的数据都是同样的,也就是能够重复读多少次结果都同样。不过这就可能出现同一时刻两个用户读到的数据不一样。

快照读和当前读

  • 快照读:读取的是快照版本,也就是历史版本
  • 当前读:读取的是最新版本
  • 普通的select就是快照读,而update,delete,insert,select...LOCK In SHARE MODE(共享锁),SELECT...for update(排他锁)就是当前读,其实执行update,delete,insert的时候也是先进行读取数据,而后进行操做。

InnoDB存储引擎中的锁

锁的类型

共享行锁

共享锁数据行锁,容许不一样事务共享加锁读取,但不容许其它事务修改或者加入排他锁。若是有修改必须等待一个事务提交完成,才能够执行,容易出现死锁

例如:事务1已经得到了行r的共享锁,那么另外的事务2能够当即得到行r的共享锁,由于读取并无改变行r的数据,称这种状况为锁兼容。

select * from user where id = 1 lock in share mode;  # 共享锁的写法

排他行锁

排他锁属于行锁,当一个事物加入排他锁后,不容许其余事务加共享锁或者排它锁读取,更加不容许其余事务修改加锁的数据行。

例如:事务1已经得到了行r的共享锁,如有事务3想得到行r的排他锁,则必须等事务1释放行r上的共享锁,这种状况称为锁不兼容。

select * from user where id = 1 for update;  # 排他锁的写法

InnoDB存储引擎支持多粒度锁定,这种锁定容许事务在行级上的锁和表级上的锁同时存在。为了支持在不一样粒度上进行加锁操做,InnoDB存储引擎还提供了一种额外的加锁方式,称为意向锁,意向排他锁是表级别的锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务但愿在更细粒度上进行加锁。

意向锁

意向锁的做用:意图锁是表级锁,指示事务稍后须要对表中的行使用哪一种类型的锁(共享或独占)。意向锁意味着事务但愿在更细粒度上进行加锁。

为何没有意向锁,表锁和行锁不能共存?

假设事务A写锁锁住了某一行,其余事务就不可能修改这一行。这与”事务B锁住整个表就能修改表中的任意一行“造成了冲突。因此,没有意向锁的时候,行锁与表锁共存就会存在问题!

有了意向锁以后,前面例子中的事务A在申请行锁(写锁)以前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,由于表上有意向排他锁以后事务B申请表的写锁时会被阻塞。

若将上锁的对象当作一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先须要对粗粒度的对象上锁。如上图,若是对页上的记录上排他锁,那么分别须要对数据库A、表、页上意向锁,最后对记录上排他锁。若其中任何一个部分致使等待,那么该操做须要等待粗粒度锁的完成。这其实也是为何Myisam数据库引擎的查询速度更优于InnoDB数据库引擎的缘由,锁加的多了天然就慢了。

InooDB存储引擎意向锁的设计目的主要在于为了在一个事务中解释下一行即将被请求的锁类型。其支持两种意向锁:

意向共享锁(IS Lock):事务在获取表中某行上的共享锁以前,必须先获取表上的IS锁。

意向排他锁(IX Lock):事务在获取表中某行的排他锁以前,必须先获取该表的IX锁。

表级锁类型的兼容性汇总在下表

意向共享锁 意向排他锁 共享表锁 排他表锁
意向共享锁 兼容 兼容 兼容 不兼容
意向排他锁 兼容 兼容 不兼容 不兼容
共享表锁 兼容 不兼容 兼容 不兼容
排他表锁 不兼容 不兼容 不兼容 不兼容

记录锁(Record Lock)

记录锁定是对索引记录的锁定。例如, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 能够防止从插入,更新或删除行,其中的值的任何其它交易t.c110

记录锁定始终锁定索引记录,即便没有定义索引的表也是如此。对于这种状况,请 InnoDB建立一个隐藏的汇集索引,并将该索引用于记录锁定。

间隙锁(Gap Lock)

间隙锁定是对索引记录之间的间隙的锁定,或者是对第一个或最后一个索引记录以前的间隙的锁定,锁定一个范围,但不包含记录自己。例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;阻止其余事务将value 15插入column中t.c1,不管该列 中是否已经存在该值,由于范围内全部现有值之间的间隙都被锁定。

间隙可能跨越单个索引值,多个索引值,甚至为空。

对于使用惟一索引来锁定惟一行来锁定行的语句,不须要间隙锁定。(这不包括搜索条件仅包含多列惟一索引的某些列的状况;在这种状况下,会发生间隙锁定。)例如,若是该id列具备惟一索引,则如下语句仅使用一个索引记录锁定id值为100 的行,其余会话是否在前面的间隙中插入行都没有关系:

SELECT * FROM child WHERE id = 100;

若是id未创建索引或索引不惟一,则该语句会锁定前面的间隙。

间隙锁能够共存。一个事务进行的间隙锁定不会阻止另外一事务对相同的间隙进行间隙锁定。共享和专用间隙锁之间没有区别。它们彼此不冲突,而且执行相同的功能。

Next-Key Lock

是Record Lock+Gap Lock的组合,锁定一个记录范围,并锁定记录自己。

例如一个索引有10,11,13和20这四个值,那么该索引可能被锁定的区间为:

(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

若是查询的索引是汇集索引时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引自己,而不是范围。

create table t(a int primary key);
insert into t values(1);
insert into t values(2);
insert into t values(5);

接着执行sql

建立事务A

begin;
select * from t where a=5 for update;
commit;

若是在事务A提交以前,建立事务B并插入一条数据,且提交。

# 事务B
begin;
insert into t values(4);
commit;  # 成功不须要阻塞

表t中有1,2,5三个值,在事务A对a = 5进行排他锁锁定。而因为a是主键且惟一,所以锁定的仅是5这个值,而不是(2,5)这个范围,这样事务B插入值4时能够当即插入,不会阻塞。Next-Key Lock降级为Record Lock,从而提升并发性。

如果辅助索引,即则会阻塞等待上一事务提交后才能继续执行。好比:一个表a中有辅助索引四个值m为一、二、四、6,一个辅助索引使用Next-Key Lock锁定条件是4,代码以下,那么他锁定的范围是(2,4),但特别注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(4,6)的锁。所以若执行插入m=5的sql语句会被阻塞。

select * from a where m=4 for update;

经过Next-Key Lock能够避免幻读的出现。

相关文章
相关标签/搜索