MySQL的事务

一、事务的四大特性

一般来说,事务是必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

二、事务的并发问题

脏读:事务A读取了事务B更新的数据,然后事务B回滚,那么事务A读取到的数据就是脏数据。

不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中对数据做了更新并提交,导致事务A多次读取同一数据的结果不一致,侧重于修改(update和delete)。

幻读:系统管理员A将数据库中所有的学生的成绩从具体分数改为ABCD等级,同时系统管理员B插入了或删除了一条具体分数的记录,导致A发现结束后还有一条数据没有改过来或是丢失了,就好像出现了幻觉一样,侧重于插入insert。

不可重复读和幻读的区别?

"不可重复读" 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

幻觉读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好像发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。

三、事务的隔离级别

丢失更新类型

事务的四个特性,除了隔离性都比较好理解。在多个事务同时操作数据的情况下,会引发丢失更新的场景。例如,电商有一种商品,在疯狂抢购,此时会出现多个事务同时访问商品库存的情景,这样就会产生丢失更新。一般而言,存在两种类型的丢失更新。

假设某种商品A库存数量为100,抢购时,每个用户仅允许抢购一件商品,在抢购过程中就可能出现如下场景:

可以看到,T5时刻事务1回滚,此时库存从99变为了100,事务2 的结果就丢失了。类似的,对于这样的一个事务回滚另外的一个事务提交引发的数据不一致的情况,称为第一类丢失更新 。但是,目前大部分数据库已经克服了第一类丢失更新的问题。

如果多个事务并发提交?

T5时刻提交的事务,以为在事务1中,无法感知事务2的操作,此时事务1不知道事务2 已经修改过了数据,从而产生了错误的结果,T5时刻提交事务1的事务,就会引发事务2提交结果的丢失,我们把这样的多个事务提交引发的丢失更新,称之为第二类丢失更新。

为了压制丢失更新,数据库标准提出了四类隔离级别,在不同的程度上压制丢失更新,这四类隔离级别分别是:

  • 读未提交(read_uncommitted)
  • 读已提交(read_committed)
  • 可重复读(repeatable_read)
  • 串行化(serializable)

也许我们会想到,既然是为了压制丢失更新,直接压制就好,为什么还要设置级别呢?

数据库现有技术完全可以避免丢失更新,但是这样做的代价就是付出锁的代价。但是在实际开发中,我们不仅仅需要考虑保证数据的一致性问题,也想考虑性能的问题。试想,在调用过程中使用过多的锁,一旦出现商品抢购的场景,必然会导致大量的线程被挂起和恢复,因为使用了锁之后,一个时刻只能有一个线程访问数据,这样整个系统就会十分缓慢,当系统被数万用户同时访问,过多的锁就会引发宕机,大部分的线程用户被挂起,等待持有锁实物的完成,这样用户体验会十分糟糕。因此数据库规范就为中和一致性和性能的问题,提出了四种隔离级别来在不同程度上压制丢失更新。

读未提交

读未提交时最低的隔离级别,其含义是允许一个事务读取到另一个事务未提交的数据。读未提交的优点在于并发能力高,适合对数据一致性没有要求而追求高并发的场景,但最大的坏处就是会出现脏读。

在T3时刻,因为采用读未提交,所以事务2可以读取到事务1未提交的库存数据为1,当事务2扣减库存后并提交了事务则库存为0,然后事务1在T5时刻回滚事务,此时库存不会回滚到2,最后结果变成了0,这样就出现了错误。

读已提交

读已提交隔离级别,是指一个事务只能读取到另外一个事务已经提交的数据,不能读取到未提交的数据。

在T3时刻,由于采用了读已提交的隔离级别,因此事务2不能读取到事务1中未提交的库存1,然后事务2提交事务,则库存在T4时刻就变为了1。T5时刻,事务1回滚,因为第一类丢失更新已经克服,所以结果库存为1,这是一个正确的结果。但是读写提交也会产生下面的问题。

在T3时刻事务2读取库存的时候,由于事务1没有提交,所以事务2读到的库存为1,此时事务2认为当前可扣减库存;在T4时刻,事务1已经提交事务,所以在T5时刻,它扣减库存的时候发现库存为0,扣减失败。此时库存对于事务2来说是一个可变化的值,这样的现象称为不可重复读,这就是读已提交的一个不足,为了克服这个不足,数据库隔离级别还提出了可重复读的隔离级别,它能够消除不可重读的问题。

可重复读

可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读已提交的时候,可能会出现一些值的变化,影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出了可重复读的隔离级别。

事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取,所以此时数据库就阻塞事务2的读取,直至事务1提交,事务2才能读取库存的值。此时已经是T5时刻,而读取到的值为0,这是就已经无法扣减了,显然在读写提交中出现的不可重复读的场景被消除了。但这样也会引发新的问题,就是幻读。假设现在商品交易正在进行中,而后台也有人也在进行查询分析和打印的业务。

这便是幻读现象。首先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值。幻读不是针对一条数据库记录而言,而是多条记录,例如,这51笔交易笔数就是多条数据库记录统计出来的。而可重复读是针对数据库的单一条记录,例如,商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读。

串行化

串行化是数据库最高的隔离级别,它会要求所有的sql按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性。

隔离级别的合理选择

隔离级别越高,性能越差,所以在选择隔离级别的时候,要根据业务场景进行考虑选择。

例如,一个高并发抢购的场景,如果采用串行化隔离级别,能够有效的避免数据的不一致性问题,但会导致并发的各个线程挂起,因为只有一个线程可以操作数据,这样就会导致大量的线程挂起和恢复,导致系统缓慢。后续的用户要得到系统的相应就需要等待很长的时间,从而影响了用户的体验。

所以在现实开发而言,选择隔离级别会以读已提交为主,它能够防止脏读,而不能避免不可重复读和幻读。

对于隔离级别,不同的数据库支持也是不同的,oracle只能支持读已提交和串行化,而mysql能够支持四种,对于oracle默认的隔离级别为读写提交,mysql则是可重复读。

四、事务的实现原理

事务想要做到什么效果?无非是要做到可靠性以及并发处理。

可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,就需要知道修改之前和修改之后的状态,所以就有了undo log和redo log。

并发处理:也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离。

redo log 与 undo log介绍

什么是redo log ?

redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。假设有个表叫做tb1(id,username) 现在要插入数据(3,ceshi)。

start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400; 
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
commit;

redo log有什么作用?

mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Buffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步

那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行上面图中红色的操作。这样会导致丢部分已提交事务的修改信息!

所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。

一句话:redo log是用来恢复数据的 用于保障已提交事务的持久化特性。

什么是undo log?

undo log 叫做回滚日志,用于记录数据被修改前的信息。它正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。

每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。

undo log有什么作用?

undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

一句话:undo log是用来回滚数据的,用于保障未提交事务的原子性。

mysql锁技术以及MVCC

mysql锁技术

当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。

解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,这两种锁被称为:

共享锁(shared lock),又叫做"读锁"

读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。

排他锁(exclusive lock),又叫做"写锁"

锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。

通过读写锁,可以做到读读并行,但是不能做到写读,写写并行
事务的隔离性就是根据读写锁来实现的。

MVCC基础

MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间,当然存储的并不是实际的时间值,而是系统版本号。

MVCC在mysql中的实现依赖的是undo log与read view

  • undo log :undo log 中记录某行数据的多个版本的数据。
  • read view :用来判断当前版本数据的可见性

事务的实现

前面讲的重做日志,回滚日志以及锁技术就是实现事务的基础。

  • 事务的原子性是通过 undo log 来实现的。
  • 事务的持久性性是通过 redo log 来实现的。
  • 事务的隔离性是通过 (读写锁+MVCC)来实现的。
  • 而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!

原子性,持久性,隔离性折腾半天的目的也是为了保障数据的一致性!

总之,ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性。

原子性的实现

一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,对于一个事务来说不可能只执行其中的部分操作,这就是事务的原子性。

数据库是通过回滚操作来实现的。

所谓回滚操作就是当发生错误异常或者显式的执行rollback语句时需要把数据还原到原先的模样,所以这时候就需要用到undo log来进行回滚,接下来看一下undo log在实现事务原子性时怎么发挥作用的。

undo log的生成

假设有两个表 bank和finance,表中原始数据如图所示,当进行插入,删除以及更新操作时生成的undo log如下面图所示:

从上图可以了解到数据的变更都伴随着回滚日志的产生:

(1) 产生了被修改前数据(zhangsan,1000) 的回滚日志

(2) 产生了被修改前数据(zhangsan,0) 的回滚日志

根据上面流程可以得出如下结论:

(1) 每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上

(2) 所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。

思考:为什么先写日志后写数据库?

根据undo log进行回滚

为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。

回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:

(1) 如果在回滚日志里有新增数据记录,则生成删除该条的语句

(2) 如果在回滚日志里有删除数据记录,则生成生成该条的语句

(3) 如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句

持久性的实现

事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。

先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。

为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:

读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;

写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;

上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!

因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。

于是 redo log就派上用场了。下面看下redo log是什么时候产生的。

既然redo log也需要存储,也涉及磁盘IO为啥还用它?

(1) redo log 的存储是顺序存储,而缓存同步是随机操作。

(2) 缓存同步是以数据页为单位的,每次传输的数据大小大于redo log。

隔离性实现

隔离性是事务ACID特性里最复杂的一个。在SQL标准里定义了四种隔离级别,每一种级别都规定一个事务中的修改,哪些是事务之间可见的,哪些是不可见的。

级别越低的隔离级别可以执行越高的并发,但同时实现复杂度以及开销也越大。

Mysql 隔离级别有以下四种(级别由低到高):

  • READ UNCOMMITED (未提交读)
  • READ COMMITED (已提交读)
  • REPEATABLE READ (可重复读)
  • SERIALIZABLE (串行化)

只要彻底理解了隔离级别以及他的实现原理就相当于理解了ACID里的隔离型。前面说过原子性,隔离性,持久性的目的都是为了要做到一致性,但隔离型跟其他两个有所区别,原子性和持久性是为了要实现数据的可性保障靠,比如要做到宕机后的恢复,以及错误后的回滚。

那么隔离性是要做到什么呢? 隔离性是要管理多个并发读写请求的访问顺序。 这种顺序包括串行或者是并行
说明一点,写请求不仅仅是指insert操作,又包括update操作。

总之,从隔离性的实现可以看出这是一场数据的可靠性与性能之间的权衡。

  • 可靠性性高的,并发性能低(比如 Serializable)
  • 可靠性低的,并发性能高(比如 Read Uncommited)

读未提交

在读未提交隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。

因为读不会加任何锁,所以写操作在读的过程中修改数据,所以会造成脏读。好处是可以提升并发处理性能,能做到读写并行

换句话说,读的操作不能排斥写请求。

优点:读写并行,性能高。

缺点:造成脏读。

读已提交

一个事务的修改在他提交之前的所有修改,对其他事务都是不可见的。其他事务能读到已提交的修改变化。在很多场景下这种逻辑是可以接受的。

InnoDB在 READ COMMITTED,使用排它锁,读取数据不加锁而是使用了MVCC机制。或者换句话说他采用了读写分离机制
但是该级别会产生不可重读以及幻读问题。

(1) 什么是不可重复读?

在一个事务内多次读取的结果不一样。

(2) 为什么会产生不可重复读?

这跟读未提交级别下的MVCC机制有关系,在该隔离级别下每次 select的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。

在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读:

可重复读((Mysql默认隔离级别)

在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC。

(1) 采用读写锁实现:

为什么能可重复读?只要没释放读锁,在次读的时候还是可以读到第一次读的数据。

优点:实现起来简单。

缺点:无法做到读写并行。

(2) 采用MVCC实现

为什么能可重复读?因为多次读取只生成一个版本,读到的自然是相同数据。

优点:读写并行。

缺点:实现的复杂度高。

但是在该隔离级别下仍会存在幻读的问题。

串行化

该隔离级别理解起来最简单,实现也最单。在隔离级别下除了不会造成数据不一致问题,没其他优点。

一致性的实现

一致性:数据库总是从一个一致性的状态转移到另一个一致性的状态。

下面举个例子:zhangsan 从银行卡转400到理财账户

start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400; 
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
commit;
  1. 假如执行完 update bank set balance = balance - 400;之发生异常了,银行卡的钱也不能平白无辜的减少,而是回滚到最初状态。
  2. 又或者事务提交之后,缓冲池还没同步到磁盘的时候宕机了,这也是不能接受的,应该在重启的时候恢复并持久化。
  3. 假如有并发事务请求的时候也应该做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。在涉及并发的情况下往往在性能和一致性之间做平衡,做一定的取舍,所以隔离性也是对一致性的一种破坏。

总结

实现事务采取了哪些技术以及思想?

  • 原子性:使用 undo log ,从而达到回滚
  • 持久性:使用 redo log,从而达到故障后恢复
  • 隔离性:使用锁以及MVCC,运用的优化思想有读写分离,读读并行,读写并行
  • 一致性:通过回滚,以及恢复,和在并发环境下的隔离做到一致性。

五、参考

https://www.cnblogs.com/wyc1994666/p/11367051.html

https://www.cnblogs.com/luckyhui28/p/12214694.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

codedot

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值