MySQL事务

时间:2024-03-13 21:52:55

目录

一、认识事务

1.1 事务的四大特性

1.2 MySQL如何保存事务的四大特性?

1.2.1 Undo Log(回滚日志)、Redo Log(重做日志)、MVCC是什么?

1.2.2 更新/修改/删除 一条SQL语句的执行流程?

1.2.3 MySQL关键字的执行顺序

二 、MySQL中有哪些事务隔离级别?

2.1 不可重复读和幻读有什么区别? 

2.2 为什么RR(可重复读)级别有幻读问题,没有不可重复读问题?

2.3 MySQL中的隔离级别是基于什么实现的?

2.4 什么是MVCC机制?

三、如何保证缓存和 MySQL 的双写一致(一致性问题) ?

3.1 什么是双写一致?

3.2 为什么缓存比数据库快?

3.3 缓存一致性问题

3.4 如何解决一致性问题?


一、认识事务

事务是保证数据库的可靠性和稳定性的一种机制。它是数据库中的一组操作,要么全部成功执行,要么全部不执行,不存在中间状态。

1.1 事务的四大特性

  • 原子性:事务中的所有操作要么全部执行成功,要么全部失败回滚,不能只执行一部分操作。
  • 一致性:事务执行前后,数据库的完整性约束没有被破坏,数据总是从一个一致性状态转移到另一个一致性状态,例如:如果一个事务要求将某个账户的金额从A转移到B,那么无论事务是否成功,最终账户A和账户B的总金额应该保持不变。
  • 隔离性:事务之间是隔离的,每个事务对其他事务的操作是透明的,一共事务的中间结果对其他事务是不可见的。隔离性可以防止并发执行的事务之间产生胀读,幻读,不可重复读等问题。
  • 持久性:事务完成后,对数据库的修改将永远的保存在数据库中,即使故障也不会丢失。

这些特性对于许多应用场景,尤其是需要处理关键业务数据的应用,是非常重要的。例如在转账业务中,它分为两个关键性操作,首先是先扣除一个账户的钱,其次再给另一个账号增加钱。但是如果没有事务的保证,那么有可能第一次操作钱被扣了,但另一个账户钱没增加,那么这笔钱就凭空“消失"了。

在这个实例中:

  • 原子性指的是要么转账两个账户都成功,要么一起失败,不存在执行一半的情况.。
  • 一致性指的是转账之前两个账户的总额,等于转账之后的两个账号的总额(不考虑手续费的情况),这个就是一致性,不会存在总额不相等的情况。
  • 隔离性指的是正在执行的转账业务没有提交之前,其他事务能不能看到转账的详情,不同的隔离级别看到的数据是不一样的,例如最低的隔离级别读未提交,它是能读取转账执行一半未提交的数据的(但这个数据位脏数据,可能提交也可能回滚)
  • 持久性指的是转账成功之后,数据就保存到磁盘了,即使重启 MySQL 服务,数据也不会丢失。

1.2 MySQL如何保存事务的四大特性?

以默认数据引擎 InnoDB 为例,它保证事务四大特性的手段分别是:

  1. 原子性是通过 Undo Log(回滚日志)来保证的。InnoDB 使用日志(Undo Log)来记录事务的操作,包括事务开始、修改数据和事务提交等。如果事务执行失败或回滚,InnoDB 可以使用日志来撤销已经执行的操作,确保事务的原子性。
  2. 持久性是通过 Redo Log (重做日志)来保证的。在事务提交之前,InnoDB 会将事务的修改操作先写入事务日志(Redo Log),然后再将数据写入磁盘。即使在系统崩溃或断电的情况下,InnoDB 可以通过重放事务日志来恢复数据,确保事务的持久性。
  3. 隔离性是通过 MVCC(多版本并发控制)和锁机制来保证的
  4. 一致性是通过各种约束,如主键、外键、唯一性约束等,加上事务的持久性、原子性和隔离性来保证的。

1.2.1 Undo Log(回滚日志)、Redo Log(重做日志)、MVCC是什么?

什么是Undo Log?

Undo Log(回滚日志)是数据库用于实现事务原子性和回滚操作的日志。每当事务对数据库进行修改时数据库系统都会记录一条对应的 Undo Log。这些日志记录了原始数据的备份以及修改前的状态。在事务提交之前,如果发生了错误或者事务被用户手动回滚,Undo Log 就会被用来撤销该事务的所有修改,恢复数据库到事务开始前的状态。

什么是 Redo Log?

Redo Log(重做日志)是数据库用于保证事务持久性和 crash recovery(崩溃恢复)的日志。每当事务对数据库进行修改时,数据库系统也会记录一条对应的 Redo Log。这些日志记录了对数据库进行修改的具体操作和内容。在事务提交时,Redo Log 会被刷新到磁盘中。如果在事务提交后发生系统崩溃,数据库系统可以使用 Redo Log 来重新执行所有未完成的修改操作,确保事务的持久性,即使在系统崩溃后也能恢复到一致状态。

1.2.2 更新/修改/删除 一条SQL语句的执行流程?

主要为以下步骤:

  1. 客户端发送请求:客户端构建一条SQL语句发送给MySQL服务器。
  2. 查询解析和优化:MySQL服务器收到请求之后,先进行语法解析,在经过查询优化器,最后生成执行计划。
  3. 加锁和数据读取:根据执行计划,MVSOL需要对受影响的数据行进行加锁,以确保事务的隔离性和一致性。对于可重复读和读已提交隔离级别,InnoDB 使用 Next-Key Locking(一种行锁机制)来防止幻读。加锁后,MySQL从磁盘读取需要更新的数据行。
  4. Undo Log记录:在更新数据之前,InnoDB会为每一行被修改的数据创建一个UndoLog条目,记录原始数据的备份。用于事务回滚时能够恢复数据到更新前的状态。
  5. 数据更新到内存:更新后的数据放入内存的Buffer Pool中。
  6. Redo Log 写入:修改数据的同时,MySQL会将更新操作记录到Redo Log 中,Redo Log包含足够的信息来重新执行更新操作。
  7. Flush 和 Sync:当Redo Log缓冲区达到一定大小或者经过一定时间后,MySQL会将Redo Log缓冲区的内容刷新到磁盘中,并调用操作系统级别的 fsync() 函数同步到磁盘,确保Redo Log的持久性。
  8. 事务提交并更新 Redo Log:当所有更新操作完成并且 Redo Log 已经持久化到磁盘后,MySQL 可以提交事务,并将 Redo Log 的相应部分标记为已提交(commit 状态)。
  9. 解锁和清理:提交事务后,MVSOL会释放对数据行的锁定,允许其他事务访问这些数据。如果没有其他未提交事务依赖于 Undo Log,InnoDB 会在适当的时候清理 Undo Log,释放空间。

为什么不直接将内存的Buffer Pool中的数据写入到磁盘中呢?

预防主机掉电等问题导致的写入失败,有RedoLog相当于多了一份保证。因为Mysql其实是会检查redolog里面的内容的,如果发现里面的内容都是完整的(状态正常,保存数据都存入磁盘了),那么就不会发生什么,但是如果检查时候发现数据有问题,那么就会在所以程序,主机都正常的情况下,将redolog里面未刷入到磁盘中的数据(有问题的数据)再次刷入到磁盘中。

1.2.3 MySQL关键字的执行顺序

以下是 MySQL 中查询的逻辑执行顺序:

  1. FROM: 指定要查询的表,包括连接操作(JOIN)。

  2. WHERE: 对表中的数据进行条件过滤。

  3. GROUP BY: 将结果集按照指定的列分组。

  4. HAVING: 对分组后的结果进行条件过滤。

  5. SELECT: 选择要返回的列。

  6. ORDER BY: 对结果集进行排序。

  7. LIMIT: 限制返回的行数。

二 、MySQL中有哪些事务隔离级别?

MySOL 中有事务隔离级别总共有以下4种:

读未提交(Read Uncommitted):最低的隔离级别,事务中未提交的修改数据,可以被其他事务读取到。                                                                                                                                   

  • a.优点:并发性能最好,读取到的数据最新。
  • b.缺点:存在脏读(Dirty Read)问题,即读取到未提交的数据,可能导致数据不一致性。

读已提交(Read Committed):事务中未提交的修改数据,不会被其他事务读取到,此隔离级别看到的数据,都是其他事务已经提交的数据。

  • a.优点:避免了脏读的问题。
  • b.缺点:存在不可重复读(Non-Repeatable Read)问题,即同一个事务中,不同时间读取到的数据可能不一样。

可重复读(Repeatable Read):MySQL 默认的隔离级别,事务在开始时会创建一个视图,事务在3 .其整个执行期间始终能看到这个视图中的数据,所以不会出现不可重复读的问题。但是,仍然可能发生“幻读”问题,即在同一个事务中,前后两次执行同样的范围查询可能会返回不同的行数,因为其他事务在这两次查询之间插入了新的行。

  • a.优点:避免了不可重复读的问题。
  • b.缺点:存在幻读(Phantom Read)问题,即在一个事务中,两次查询同一个范围的记录,但第二次查询却发现了新的记录。

串行化(Serializable):最高的隔离级别,将所有的事务串行执行(一个执行完,另一个再执4.
行),保证了数据的完全隔离。

  • a.优点:避免了幻读的问题。
  • b.缺点:并发性能最差,可能导致大量的锁等待和死锁。

MySOL 中的事务隔离级别就是为了解决脏读、不可重复读和幻读等问题的,这 4 种隔离级别与这 3 个问题之间的对应关系如下:

2.1 不可重复读和幻读有什么区别? 

不可重复读(Non-Repeatable Read)是指在一个事务中,多次执行相同的查询语句可能会得到不同的结果,因为其他并发事务在该事务正在进行时修改了数据。

幻读(Phantom Read)是指在一个事务中,多次执行相同的查询语句可能会返回不同的结果集,因为其他并发事务在该事务正在进行时插入了新的数据行。

不可重复读 VS 幻读

不可重复读和幻读都是并发事务引起的读一致性问题,但两者关注的侧重点和解决方案不同。

1.侧重点不同

不可重复读关注的是一行数据的变化,它是指在同一个事务中,多次读取同一行数据的结果不。致。这是由于其他并发事务对同一行数据做了修改(例如更新操作),导致两次读取之间数据发生了变化。

幻读关注的是范围数据的变化,它是指在同一个事务中,多次查询同一个范围的数据时,结果集的。行数发生变化。这是由于其他并发事务在查询范围内插入了新的数据或者从中删除了数据,导致两次查询之间结果集中的行数发生了变化。

2. 解决方案不同

  1. 不可重复读通常使用行锁来解决,因为它关注的是一行数据。
  2. 幻读通常使用间隙锁来解决,因为它关注的是范围数据。

2.2 为什么RR(可重复读)级别有幻读问题,没有不可重复读问题?

RR 级别Repeatable Read,可重复读级别)中是没有不可重复读问题的,但存在幻读问题,这是因为,RR 级别通过使用多版本并发控制(MVCC)机制,通过执行事务之前生成的快照(Snapshot)视图来解决了不可重复读的问题。在该级别下,读操作会读取事务开始时的快照数据,而不受其他并发事务的修改影响,从而保证了读操作的一致性,解决了不可重复读的问题。

然而,幻读问题不同于不可重复读问题,它发生在范围查询中,而不是在读取同一行数据时。也就是说快照机制能保证读取已存在记录,但是幻读是新增或删除数据,这些数据之前没有快照,因此就无法使用快照机制来解决此问题了,例如新增,之前是没有快照数据的,所以此时就会出现幻象行的数据(新增行的数据),这就是为什么 RR级别可以解决不可重复读问题,但解决不了幻读的问题的主要原因了。

RR 级别可以通过快照机制,缓存并读取数据,而不受其他并发事务的影响,但幻读问题是新增或删除、是不能使用快照机制的,所以 RR 只能通过 MVCC 的快照机制解决不可重复读问题,但解决不了幻读问题。

2.3 MySQL中的隔离级别是基于什么实现的?

MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。

SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED(读已提交) 和 REPEATABLE-READ(可重复读) 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。

2.4 什么是MVCC机制?

MVCC(Multi-Version Concurrency Control)是一种并发控制机制,用于解决数据库并发访问中,数据一致性问题。所谓的一致性问题,就是在并发事务执行时,应该看到哪些数据和不应该看到哪些数据。

在 MVCC 机制中,每个事务的读操作都能看到事务开始之前的一致性数据快照,而不受其他并发事务的修改的影响。核心思想是通过创建多个数据版本,保持事务的一致性和隔离性。
使用 MVCC 机制解决了 RR 隔离级别中,部分幻读问题,但又没把全部幻读问题都解决。

  • MVCC 解决了 RR 隔离级别中,快照读的幻读问题。多次査询快照读时,因为 RR 级别是复用 Read View(读视图),所以没有幻读问题。
  • 但 MVCC 解决不了 RR 隔离级别中,当前读中发生的幻读问题。

快照读:是指在一个事务中,读取的数据版本是在事务开始时已经存在的数据版本,而不是最新的数据版本。这种读取方式提供了事务在执行期间看到的数据视图的一致性,select 查询就是快照读。

当前读:是指在事务中读取最新的数据版本,以下几种操作都是当前读:

  • select ... for update;
  • select ... lock in share mode;
  • insert ...
  • update ..

如何解决当前读的幻读问题?

  • 加锁
  • 升级事务的隔离级别为串行化

三、如何保证缓存和 MySQL 的双写一致(一致性问题) ?

3.1 什么是双写一致?

在分布式系统中,数据库和缓存会搭配一起使用,以此来保证程序的整体查询性能。

也就说,分布式系统为了缓解数据库查询的压力,会将查出来的数据保存在缓存中,下次再查询时,直接走缓存系统,而不再查询数据库,这样就极大的提高了整体的查询性能.

3.2 为什么缓存比数据库快?

缓存之所以比数据库快的主要原因有以下 3 点:

  1. 内存访问速度快:缓存通常将数据存储在内存中,而数据库将数据存储在磁盘上。相比于磁盘访问,内存访问速度更快,可以达到纳秒级别的读取速度,远远快于数据库的毫秒级别的读取速度。
  2. IO 操作次数少:数据库通常需要进行磁盘 IO 操作,包括读取和写入磁盘数据。而缓存将数据存储在内存中,避免了磁盘 IO 的开销。内存访问不需要进行磁盘寻址和机械运动,相对来说速度更快。
  3. 特殊的数据结构:缓存的数据结构通常为 key-value 形式的,也就是说缓存可以做到任何数据量级下的查询数据复杂度为 O(1),所以它的查询效率是非常高的;而数据库采用的是传统数据结构设计,可能需要查询二叉树、或全文搜索、或回表查询等操作,所以其查询性能是远低于缓存系统的。

3.3 缓存一致性问题

虽然缓存可以极大的提高查询性能,但同时也带来的新的问题:数据库和缓存一致性的问题。

具体来说,在一个常见的应用场景中,当更新数据库的操作完成后,需要同步更新缓存,以保证缓存中的数据与数据库中的数据保持一致。然而,由于数据库和缓存是两个不同的组件,它们的数据更新操作是异步的,可能存在以下问题:

  1. 数据延迟:数据库更新和缓存更新之间存在时间延迟,导致缓存中的数据不是最新的。这可能会引起数据的不一致,当其他请求读取数据时,可能会读取到旧的数据。
  2. 更新失败:在尝试更新缓存时,可能出现更新失败的情况。例如,缓存节点暂时不可用,网络故障等。如果更新缓存失败而未进行适当的处理,也会导致数据库和缓存之间的数据不一致。

也就说,因为以上原因,可能会导致 A 用户和 B 用户执行了同一个查询操作,但是得到了完全不同的结果,这就是数据库和缓存的一致性问题。

3.4 如何解决一致性问题?

要保证 缓存 和 MySQL 的数据一致性,从大的角度来说,无非就两种思路:

1.修改

  • 先修改数据库,再修改 缓存
  • 先修改 缓存,再修改数据库

2.删除

  • 先删除 缓存,再操作数据库
  • 先操作数据库,再删除 缓存

这四种方式在正常情况下,都可以保证 Redis 和 MySQL 的数据一致性,但是在异常情况下,就只有一种方式才能真正保证 Redis 和 MySQL 的双写一致。

当上述方式,每个方案都是执行了前半部分后,主机掉电了 。

① 先修改数据库,再修改 Redis(掉电)

这种方式,Redis 中是旧数据,而 MySQL 中是新数据,当应用程序访问 Redis 时,发现有数据,直接就返回给用户了,显然这种方式是不合理的。

② 先修改 Redis,再修改数据库(掉电)

这种方式,Redis 中是新数据,而 MySQL 中是旧数据,当应用程序访问 Redis 时,发现有数据,直接就返回给用户了,此时 Redis 和数据库中的数据也不一致,下一次 Redis 还需要从数据库中同步数据的,显然这种方式也不合理。

③ 先删除 Redis,再操作数据库(掉电)      

这种方式,不管主机有没有掉电,Redis 始终没数据,始终 都是要从数据库中同步数据的,所以这种方式  Redis 和 MySQL 的数据一定是一致的。

④ 先操作数据库,再删除 Redis(掉电)

这种方式也是行不通的,操作完数据库,主机掉电,此时 Redis 中还是有数据的,而且是旧数据,而 MySQL 中的数据已经更新了,导致双写不一致。

【并发场景下的问题】

        虽然先删除 Redis,再操作数据库,这种方式看似解决了 Redis 和 MySQL 中的双写一致问题,但是它的前提是要在单线程的情况下。

在多线程的场景下,由于操作系统的随机调度,可能会出现以下情况:

  1. 线程 1 执行完删除 Redis,然后时间片用完了,
  2. 线程 2 执行查询操作,由于线程 1 把 Redis 删除了,所以线程 2 拿到了数据库的旧数据,然后时间片用完了,
  3. 线程 1 继续执行更新数据库,并把新数据缓存至 Redis,执行结束,
  4. 线程 2 苏醒,将刚刚查到的数据,缓存一份至 Redis.

此时 Redis 和数据库中的数据又变成不一致了,那该怎办 ?

 【解决办法】延时双删

        在线程 2 执行完之后后,再删除一次 Redis,以便下次查询还得走数据库,此时拿到的就是最新的数据了。(此时线程 1 的缓存 Redis  就可以不必执行了)

如果在面试中碰到这个问题,回到到这里其实已经差不多了,如果遇到了刁钻一点的面试官,那么我们以上看似美好的推理过程,其实还是存在问题的。

面试官问:延时双删是否彻底解决了 Redis 和 MySQL 双写一致的问题 ?

        答案是不一定,它只是最大限度的解决双写一致性问题。因为延时,延长的时间是固定的,而操作系统的调度是随机的,极端情况下,还是可能会存在线程 1 第二次删除操作处在线程 2 的将旧数据缓存至 Redis 这个操作之前。(极端情况下,线程 2  是个饥饿线程)

那这个问题该怎么完美解决 ?

        这个问题我也问过一些大佬,他们给出的结论是从理论上来说,目前没有更好的解决方案,但是前面也说了只有极端情况下,连延时双删都解决不了,因为它发生的概率也是比较小的,所以可以忽略不计了,并且使用延时双删几乎可以解决 99.99% 的双写一致问题了。

        如果硬要追求完美主义,可以给它设置一个过期时间,等到键值过期了,自然会把数据库中新的数据给保存到 Redis 中;

        另外,对于主机掉电,另一半操作没有执行的问题,可以通过加入消息队里欸来解决(保证业务的完整性),消费者再消费任务的时候,即使掉电,下一次还是会继续消费的。