MySQL之事务设计

2021/12/7 19:21:32

本文主要是介绍MySQL之事务设计,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

事务的作用?

讲解事务之前先来看一个例子,以转账为例:

小明现在有10000块钱,小红向小明借1000块,小明给小红转了1000块,现在问题是小红没有收到钱,但是小明扣了1000块,剩余9000块,那问题出现在哪里呢?

有以下两种情况:

  • 小明给小红转账的过程中报错,小明的钱扣完以后到执行把钱给小红加上的过程中出现了异常,导致小红没有执行成功。
  • 小明给小红转账的过程中,小明扣成功,到执行小红加钱的时候服务宕机了,导致小红没有执行成功。

要解决这个问题,就要保证转账过程中所有数据库的操作要么全部执行成功 ,要么全部失败,中间不能出现异常等问题。

为了防止以上有可能出现的情况,MySQL引入了事务(Transaction),事务就是针对数据库的操作,可以由一条或者多条SQL语句组成,事务具备同步的特点,如果执行语句过程中发生异常,那么在操作事务期间对数据库做的所有操作会回滚(Rollback)到开始执行前的状态。

事务的特性(ACID)

InnoDB引擎支持事务操作,而MyISAM引擎不支持事务操作,为了保证数据的安全性,MySQL的引擎都是用InnoDB来操作事务。

接下来看一下实现事务必须坚持的4大特性:

原子性(Atomicity)

原子性指一个事务是一个不可分割的工作单位,一个事务中的所有操作,要么全部成功,要么全部失败。如果事务中一个SQL语句执行失败,则已经执行完的语句也必须回滚,数据库回滚到事务执行前的状态。

一致性(Consistency)

一致性指事务执行结束后,数据库的完整性约束没有被破坏,即事务执行前和执行后都必须处于一致性状态,比如转账:小明和小红两个人的钱加起来是500,无论小明和小红之间怎么转账,事务结束后两人的金额加起来还是500,这就是事务的一致性。

隔离性(Isolation)

隔离性是指多个用户并发访问数据库,操作同一张表时,数据库为每个用户开启事务,并发执行的各个事务之间相互隔离,不能互相干扰,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

持久性(Durability)

事务一旦被提交了,那么对数据库中数据的修改就是永久的,即便是在数据库系统遇到故障也不会丢失。

那么InnoDB引擎是如何保证这4个特性的呢?

  • 原子性和持久性通过 redo log (重做日志)来保证。
  • 一致性通过 undo log(回滚日志) 来保证的。
  • 隔离性通过 MVCC(多版本并发控制) 或锁机制来保证。
如果大家想了解这个可以看一下这篇文章:

聊聊MySQL里面的undo Log、redo Log和bin Log日志的原子性和持久性已经复制和恢复数据实现过程

事务并发执行会有什么问题?

在没有事务隔离的时候,多个事务在同一时刻对同一数据的操作可能会影响到我们所期望的结果。可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。

接下来我们以图讲解,看一下这些问题是怎么发生的。

脏读

在一个事务处理过程里读取了另外一个未提交的事务中的数据,这种现象称为脏读。

在这里插入图片描述
事务A从数据库中读取小明的余额,如何执行修改小明余额的操作,此时事务A还没有提交事务,而此时事务B也从数据库中读取小明的余额,事务B读取到的余额是事务A修改金额后还没有提交的事务,因为事务A事务没有提交,可能会发生回滚操作,如果事务A发生回滚,那么事务B读取到的数据就是脏数据。

不可重复读

在同一事务中,多次读取同一数据,前后两次读到的数据不一致,这种现象称为不可重复读。
在这里插入图片描述
事务A从数据库中读取小明的余额,然后继续执行其他的逻辑,此时事务B更新了小明的余额,并且提交了事务,当事务A再次读取小明的余额时,发现前后两次读取到的数据不一致,这时发生了不可重复读。

幻读

同一事务中,用同样的操作读取两次,得到的记录数不相同 ,这种现象称为幻读。

在这里插入图片描述
事务A从数据库中查询余额大于500的记录数,发现一共有5条,此时事务B也按照相同的条件查询出5条记录数,这个时候事务A新增了一条大于500的数据,并且提交了事务,此时数据库中大于500余额的记录数变成了6条,如何事务B再次查询余额大于500的记录数,此时查询到的数据是6条,前后读取到记录数不一致,就好像产生幻觉一样,这种现象称为幻读。

事务的四种隔离级别

上面我们讲解了并发时事务产生的问题:

  • 脏读:读到其他事务未提交的数据
  • 不可重复读:前后读取的数据不一致
  • 幻读:前后读取的记录数量不一致

SQL标准定义了四种隔离级别来规避这些问题,隔离级别越高,性能效率就越低,如下:

读未提交(read uncommitted)

一个事务可以读取另一个未提交事务的数据。

读提交(read committed)

一个事务要等另一个事务提交后才能读取数据。

可重复读(repeatable read)

一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。InnoDB引擎的默认隔离级别。

串行化(serializable )

这个会对操作的数据加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突时,后面访问的事务必须等前一个事务执行完成,才能继续执行,串行化是事务最高的隔离级别,但是这种事务隔离级别效率低下,比较耗数据库性能,我们一般不使用。

在MySQL中,实现了这四种隔离级别,可能会产生以下问题:
在这里插入图片描述

  • 读未提交隔离级别下,可能发生脏读、不可重复读和幻读现象
  • 读提交隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象
  • 可重复读隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象
  • 串行化隔离级别下,脏读、不可重复读和幻读现象都不可能会发生

从上面我们可以知道,要解决脏读问题,需要使用读提交以上的隔离级别,要解决不可重复读问题,需要使用到可重复读的隔离级别。

最后要解决幻读问题是不建议将隔离级别升级到串行化(serializable ),串行化级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,虽然事务是安全的,但是这样会导致数据库在并发事务操作时性能非常差。

解决幻读可以使用next-key lock 锁 包含(记录锁(行锁)、间隙锁),记录锁是加在索引上的锁,间隙锁是加在索引之间的,将当前数据行与上一条数据和下一条数据之间的间隙锁定,防止其他事务在这个记录之间新增新的数据,保证此范围内读取的数据是一致的,这样就避免了幻读现象。

四种事务隔离级别如何实现的

  • 对于使用读未提交(read uncommitted)隔离级别的事务来说,由于可以读到未提交事务修改过的数据,所以直接读取数据的最新版本就可以了
  • 对于使用串行化(serializable )隔离级别的事务来说,通过使用加锁的方式来并发访问数据
  • 对于使用(read committed)和(repeatable read)隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说,假如另一个事务已经修改了记录,但还没有提交,是不能直接读取最新版本的数据的
而(read committed)和(repeatable read)隔离级别的就是我们接下来需要讲解的部分,

有个问题就是:需要判断一下版本链中的哪个版本是当前事务可见的?

InnoDB中提出了一个ReadView的概念,而读提交(read committed)和可重复读(repeatable read)隔离级别是通过Read View来实现的,而它们之间一个非常大的区别就是它们创建ReadView的时机不同:

  • 读提交隔离级别是在每次读取数据前都创建一个 Read View
  • 可重复读在第一次读取数据时创建一个ReadView

我们来看一下大概结构图:
在这里插入图片描述
这个ReadView中主要包含4个比较重要的内容:

  • m_ids:指的是在创建ReadView时当前系统中活跃的读写事务的事务id列表
  • min_trx_id:指的是在创建ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值
  • max_trx_id:指的是在创建ReadView时系统中应该分配给下一个事务的id值
    • 注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4
  • creator_trx_id:指的是在创建该ReadView的事务的事务id
    • 只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0

以上了解了Read View 的字段,还需要了解InnoDB引擎中聚族索引记录中的两个隐藏列,如下图所示:

在这里插入图片描述

  • trx_id (事务字段): 当一个事务去操作某个行的数据时,会将自己的事务Id赋值给trx_id字段
  • roll_pointer( 回滚指针) :当一个事务更新了一个字段的时候,并不会直接删除掉之前的字段,而是将该指针指向之前的字段存储到undo Log

了解完以上讲解的内容之后,接下来讲解可重复读和读提交隔离级别是如何实现的。

基于可重复读

如果事务A、事务B和事务C差不多同一时刻启动,那这三个事务创建的Read View 如下图所示:
在这里插入图片描述
下面举个例子,现在有一个事务A,它的事务 id 为20,向表中新插入了一条为小明的用户金额1000的数据,此时对应的undo Log如下图所示:

在这里插入图片描述
由于是新插入的数据,所以这行数据是第一个版本,也就是它没有上一个数据版本,所以roll_pointer 为 null。

这时事务B将这行数据的金额修改为2000,因为事务B的id为21,所以此时的trx_id为21,同样也会记录一条undo Log,这条undo Log的roll_pointer指针会指向上一个数据版本的undo Log,也就是指向事务A写入的那一行 undo Log,如下图所示:
在这里插入图片描述
紧接着事务C将这行数据的金额修改为3000,因为事务C的id为22,所以此时的trx_id为22,如下图所示:
在这里插入图片描述
从上面我们可知,只要事务修改了数据,那么就会记录一条对应的undo Log,一条undo Log对应这行数据的一个版本,当这行数据有多个版本时,就会有多条undo Log日志,于是最新记录和旧版本记录通过roll_pointer指针连接,这样就形成了一个 undo Log版本链。

讲解了新增、修改操作后,接下来讲解事务读取数据时是怎么实现的。

下面举个例子来解释一下ReadView机制下,数据的读取规则。首先我们假设有一条数据,它的 trx_id=15,roll_pointer 为 null,那么此时undo Log版本链如下图所示:
在这里插入图片描述
如果此时事务A(trx_id=20)去读取数据,找到记录后,会先看这条记录的trx_id,发现trx_id = 15,通过和事务A的Read View中m_ids字段比较,发现该记录的事务id并不在活跃事务的列表中,并且小于事务A的事务id,意味着这条记录的事务早在事务A之前就提交过了,因此事务A可以读取到这条记录的值。

此时事务B将这行数据修改了,将小明的金额改成2000,那么就会记录一条对应的undo Log,并以链表的方式串联起来,形成版本链,如下图:
在这里插入图片描述
如果这时事务A再次去读取数据,发现这条记录的trx_id为21,比自己的事务id还要大,并且比下一个事务的id小,这表示事务A读取的是和自己同一时刻启动的事务B修改的数据,这时事务A并不会读取这条记录,而是沿着undo Log的版本链向前找,接着会找到该行数据的上一个版本,直到找到trx_id等于或者小于事务A的事务id的第一条记录,所以事务A再一次读取到trx_id为15的记录。

可重复读隔离级别事务读取某条数据时,就会按照如下规则来决定当前事务能读取到什么数据:

  • 如果记录的trx_id比该事务的Read View中的creator_trx_id要小,并且不在m_ids列表里,表示这条记录的事务早就在该事务前提交过了,所以该记录对该事务可见
  • 如果记录的trx_id比该事务的Read View中的creator_trx_id要大,且在m_ids列表里,表示该事务读到的是和自己同时启动的另外一个事务修改的数据,这时读取不到这条记录,而是沿着undo Log链条往下找旧版本的记录,直到找到trx_id等于或者小于该事务id的第一条记录

这种通过记录的版本链来控制并发事务访问同一个记录时的行为,就叫做MVCC(多版本并发控制)

基于读提交

读提交隔离级别是在每次查询都会生成一个新的Read View,这就可能导致事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

下面举个例子,来解释一下ReadView机制下,数据的读取规则

在这里插入图片描述
假设现在有事务A和事务B并发执行,事务 A 的事务id为 20,事务B的事务id为30。

在这里插入图片描述
如果此时事务 A(creator_trx_id=20)去读取数据,那么在undo Log版本链中,数据最新版本的trx_id为15,发现这个值小于事务A的ReadView里creator_trx_id的值,这表示这个数据的版本是事务A开启之前,其他事务提交的,因此事务A可以读取到记录数据。
在这里插入图片描述
接着事务B(creator_trx_id=30)去修改数据,将数据金额修改为2000 ,但是事务B还未提交。虽然不提交事务,但是仍然会记录一条undo Log,因此这条数据的undo Log的版本链就有两条记录了,新的这条undo Log的roll_pointer指针会指向前一条undo Log。如下图所示:
在这里插入图片描述
接着事务A(creator_trx_id=20)去读取数据,那么在undo Log版本链中,数据最新版本的事务trx_id为 30,这时事务A在找到小明这条金额为2000元的记录时,看到这条记录的trx_id,发现比事务A的Read View中的creator_trx_id还要大,而且还在m_ids列表中,这表示这个版本的数据是和自己同一时刻启动的事务B修改的,因此这个版本的数据,事务A读取不到,所以需要沿着undo Log的版本链向前找,接着会找到该行数据的上一个版本,也就是trx_id = 15的记录,由于这条记录的trx_id小于事务A,因此事务A能读取到该版本的值。

在这里插入图片描述
当事务B修改数据并且提交事务后,那么此时事务A再去读取数据,它能读取到什么值呢?

在这里插入图片描述
这时事务A(creator_trx_id=20)去读取数据,找到小明对应金额2000元的记录,会看这条记录的 trx_id,发现和事务A的Read View中creator_trx_id还要大,而且不在m_ids列表里,说明该记录的 trx_id的事务已经提交过了,于是事务A就可以读取这条记录,这就是读已提交机制。

总结

本文主要讲解了事务并发执行的时候,可能会导致脏读、不可重复读、幻读等这些问题,而为了避免这些问题的出现,SQL标准提出了四种隔离级别规避这种问题:

  • 读未提交
  • 读已提交
  • 可重复读
  • 串行化
隔离级别越高,性能越差,而InnoDB引擎默认隔离级别是可重复读。

解决脏读问题,需要使用读提交以上的隔离级别。
解决不可重复读问题,需要使用到可重复读的隔离级别。
解决幻读问题是不建议将隔离级别升级到串行化(serializable ),串行化级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,虽然事务是安全的,但是这样会导致数据库在并发事务操作时性能非常差。

解决幻读可以使用next-key lock 锁 包含(记录锁(行锁)、间隙锁),记录锁是加在索引上的锁,间隙锁是加在索引之间的,将当前数据行与上一条数据和下一条数据之间的间隙锁定,防止其他事务在这个记录之间新增新的数据,保证此范围内读取的数据是一致的,这样就避免了幻读现象。

读提交(read committed)和可重复读(repeatable read)隔离级别是通过Read View来实现的,而它们之间一个非常大的区别就是它们创建ReadView的时机不同:

  • 读提交隔离级别是在每次读取数据前都创建一个Read View,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
  • 可重复读在第一次读取数据时创建一个ReadView,事务期间都在使用这个Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录
通过几个例子,配合画图,详细分析了ReadView结合undo Log的版本链是如何来实现让当前事务读取到另一个版本的数据的,这也就是MVCC(多版本并发控制)机制的核心实现原理。

这篇关于MySQL之事务设计的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程