返回介绍

事务

发布于 2024-08-13 19:52:40 字数 14711 浏览 0 评论 0 收藏 0

概述

事务: 一组逻辑操作单元,使数据从一种状态变换到另一种状态

事务处理的原则: 保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚( rollback ) 到最初状态。

事务的 ACID 特性:

一般来说,事务都需要满足以下特性:

  • 原子性(atomicity):

    事务是不可分割的工作单元,要么全部提交,要么全部回滚

  • 一致性(consistency):

    事务执行前后,从一个合法状态变换为另一个合法状态,即执行前后都满足业务的约束(符合业务的要求)

  • 隔离性(isolation):

    多个用户并发访问数据库,执行事务时,事务之间不会互相干扰

  • 持久性(durability)

    事务一旦被提交,数据的改变就是永久性的

事务的状态:

  • 活动的(active)

    事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

  • 部分提交的(partially committed)

    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘 时,我们就说该事务处在 部分提交的 状态。

  • 失败的(failed)

    当事务处在 活动的 或者 部分提交 的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败 的状态。

  • 中止的(aborted)

    如果事务执行了一部分而变为 失败 的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止 的状态。

  • 提交的(committed)

    当一个处在 部分提交 的状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了 提交 的状态。

使用事务

显式事务

步骤 1: START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。

mysql> BEGIN; 
#或者 
mysql> START TRANSACTION;

START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :

READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。

READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。

WITH CONSISTENT SNAPSHOT :启动一致性读。

步骤 2: 一系列事务中的操作(主要是 DML,不含 DDL)

步骤 3: 提交事务 或 中止事务(即回滚事务)

# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改 
mysql> ROLLBACK; 

# 将事务回滚到某个保存点。 
mysql> ROLLBACK TO [SAVEPOINT]

其中关于 SAVEPOINT 相关操作有:

# 在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
SAVEPOINT 保存点名称;
# 删除某个保存点
RELEASE SAVEPOINT 保存点名称;

隐式事务

在 MySQL 中,如果没有显示地用 START TRANSACTION 或者 BEGIN 开启事务,那么每一条 DML 语句都会被包装成一个 独立的事务 并自动提交。

有一个系统变量 autocommit ,默认为 true ,可以将其修改为 false 来关闭自动提交效果。这样的话写入的多条 DML 语句都会属于 同一个事务 ,直到手动提交或回滚。

隐式提交数据的情况

  • 当使用 DDL 语句、修改 MySQL 的表结构时,就会隐式地提交之前未提交的事务
  • 事务控制或关于锁定的语句
    • 当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会隐式的提交上一个事务。
    • autocommit 为 true
    • 使用 LOCK TABLESUNLOCK TABLES 等关于锁定的语句也会 隐式的提交 前边语句所属的事务。

事务隔离级别

数据并发问题

对于 SQL 来说,服务端可能和多个客户端建立连接,每个客户端与服务端建立的连接被称为 会话Session )。每个会话都可以在自己的会话中向服务器发送请求语句,请求语句可能是事务的一部分,当多个会话同时发送请求时,就会遇到数据并发问题。

脏写(Dirty Write)

对于了两个事务 Session ASession B ,如果 Session A 修改Session B 还未提交的数据,就发生了 脏写 。若 Session B 回滚,那么 Session A 修改的数据就不存在了。

image-20220717203218011

脏读(Dirty Read)

对于两个事务 Session ASession B ,如果 Session A 读取Session B 还未提交的数据,就发生了 脏读 。若 Session B 回滚,那么 Session A 读取的数据就是临时且失效的。

image-20220717204009186

不可重复读(Non-Repeatable Read)

对于两个事务 Session ASession B ,如果 Session A 读取 了一个字段,然后 Session B 更新了这个字段,之后 Session A 再次读取同一个字段,值就不同了,就发生了 不可重复读Session A 的两次读取是在同一个事务中,因此逻辑上来说应当相同)

image-20220717204553285

幻读(Phantom)

对于两个事务 Session ASession B ,如果 Session A 读取 了一个字段,然后 Session B 在表中插入了一些新的数据,之后 Session A 再次读取同一个字段,就会多出一些数据,就发生了 幻读 (如果 Session B 删除了一些数据,导致第二次读取到的数据少了,不属于幻读。幻读强调的是读取到了之前没有的记录)

image-20220717210231391

严重程度排序: 脏写 > 脏读 > 不可重复读 > 幻读

SQL 的四种隔离级别

SQL 标准中设立了 4 个隔离级别:

  • READ UNCOMMITTED :读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
  • REPEATABLE READ :可重复读,事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。 这是 MySQL 的默认隔离级别,MySQL 中这个级别可以通过临键锁、MVCC 解决幻读
  • SERIALIZABLE :串行化。在事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。

image-20220717212926651

MySQL 中设置事务的隔离级别

查看隔离级别

select @@transaction_isolation

设置隔离级别

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别; 
#其中,隔离级别格式: 
> READ UNCOMMITTED 
> READ COMMITTED 
> REPEATABLE READ 
> SERIALIZABLE

事务日志

事务有四大特性: 原子性一致性隔离性永久性隔离性锁机制 实现, 原子性一致性持久性 都由事务的 redo 日志undo 日志 来保证。

  • redo log :重做日志,提供再写入操作,恢复提交事务修改的页操作,保证事务的 持久性
  • undo log :回滚日志,回滚行记录到某个特定的版本,用来保证事务的 原子性一致性

redo 日志

innoDB 引擎是以页为单位来管理存储空间的,在真正访问磁盘中的页时,需要先把磁盘中的页缓存到内存中的 buffer pool 。所有的增删改查都必须先更新缓冲池中的数据,然后缓冲池中的数据( 脏页,指内存中改动了但还未刷新到磁盘的页 )再以一定的频率刷入磁盘( checkpoint 机制 )。

由于 checkpoint 不是每次更改都会触发的,因此如果在触发前数据库宕机,那么缓冲池中的数据就丢失了。为了保证数据库的 持久性 ,所以引入了 redo 日志。

如果取消缓冲池,每次数据有更新就刷入磁盘,也可以解决持久性的问题。但是同样是磁盘文件操作,为什么 redo 日志要优于这种方法?

  • 当我们要修改页中的一条数据时,需要先把整个页都加载到内存中进行修改,修改完成后再刷入磁盘。频繁的读取会严重浪费性能,而 redo 日志只是记录了物理日志,并不会读取页;
  • 当同时需要修改多个页时,磁盘的随机 IO 性能太差,而 redo 日志只需要做顺序 IO。

redo 日志的组成

重做日志缓冲(redo log buffer) :保存在内存中的临时缓冲数据

image-20220718194037472

redo log buffer 默认大小 16MB ,最大 4096MB,最小 1MB

查看当前 redo log buffer 大小: show variables like '%innodb_log_buffer_size%'

重做日志文件(redo log file) :保存在磁盘中的持久化文件

位置: /var/lib/mysql/ib_logfile0/var/lib/mysql/ib_logfile1

redo 日志的流程

一个更新事务的流程

  1. 将原始数据从磁盘中读入内存中的缓冲池,在其中中修改数据
  2. 生成一条 redo 日志并将日志写入 redo log buffer ,记录的是数据被修改后的值
  3. 当事务 commit 时,将 redo log buffer 中的内容追加写入到 redo log file
  4. 定期将缓冲池中的数据刷新到磁盘中

image-20220718204815192

redo log 的刷盘策略

redo log buffer 刷盘到 redo log file 的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache) 中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如 page cache 足够大了)。那么对于 InnoDB 来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB 给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit 提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为 0:表示每次事务提交时不进行刷盘操作。(系统默认 master thread 每隔 1s 进行一次重做日志的同步)
  • 设置为 1:表示每次事务提交时都将进行同步,刷盘操作( 默认值
  • 设置为 2:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由 os 自己决定什么时候同步到磁盘文件。

查看刷盘策略: show variables like 'innodb_flush_log_at_trx_commit'

不同的刷盘策略对事务的执行性能有不同的影响,性能由高到低: 0 > 2 > 1

redo log file 相关参数设置

  • innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为 ./ ,表示在数据库的数据目录下。MySQL 的默认数据目录( var/lib/mysql )下默认有两个名为 ib_logfile0ib_logfile1 的文件,log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。此 redo 日志文件位置还可以修改。
  • innodb_log_files_in_group :指明 redo log file 的个数,命名方式如:ib_logfile0,ib_logfile1... ib_logfilen。默认 2 个,最大 100 个。
  • innodb_log_file_size :单个 redo log 文件设置大小,默认值为 48M 。最大值为 512G,注意最大值指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size)不能大于最大值 512G。

日志文件组

从上面可以看出,磁盘上的 redo 日志文件不止一个,而是以一个日志文件组的形式出现的。这些文件以 ib_logfile[数字] 的形式命名,每个 redo 日志文件大小是一样的。

redo 日志在写入日志文件组时,从 ib_logfile0 开始写,如果写满了,就写 ib_logfile1 ;同理,当前一个文件写满时,就往下一个文件中写;当最后一个文件写满后,就重新回到 ib_logfile0 继续写。

image-20220718215634374

checkpoint

【mysql】关于 checkpoint 机制 - 踏雪无痕 SS - 博客园 (cnblogs.com)

在整个日志文件组中还有两个重要的属性: write pos、checkpoint

  • write pos 记录当前的位置,一边写一边后移
  • checkpoint 是要擦除的位置

image-20220718221331569

上图中,checkpoint 左边是已经刷盘的数据,可以放心覆盖;右边是还未刷盘的数据,需要等待刷盘,checkpoint 向前走之后才能覆盖

小结

image-20220718222602729

undo 日志

参考: MySQL 回滚日志(undo log)总结_每天都要进步一点点的博客-CSDN 博客_undolog

undo 日志用于存储每一个事务在执行更新数据之前的 原始数据 ,以便于在事务回滚后恢复原数据,从而保证原子性。

undo 日志的作用

回滚数据

undo 日志并不会将数据库 物理 地恢复到执行事务之前的样子,undo 是逻辑日志,只能将数据库逻辑地恢复到原来的样子,数据结构和页本身并不能完全恢复。

MVCC(多版本并发控制)

当读取的某一行被其他事务锁定时,它可以从 undo log 中分析出该行记录以前的数据版本是怎样的,从而让用户能够读取到当前事务操作之前的数据(快照读)

undo 日志的存储结构

InnoDB 对 undo log 的存储采用了分段方式进行存储( 回滚段 rollback segment )。

一个回滚段中有 1024 个 undo 日志段,也就是说一个回滚段支持 1024 个 undo 日志操作。在 InnoDB1.1 之前,只支持 1 个回滚段。从 InnoDB1.1 开始,可以支持 128 个回滚段。从 InnoDB1.2 开始,可以通过参数对回滚段进行一些设置:

  • innodb_undo_directory :设置回滚段的存放路径,默认值为 InnodDB 存储引擎的目录
  • innodb_undo_logs :设置回滚段的数量,默认为 128
  • innodb_undo_tablespaces :设置构成回滚段的表空间文件的数量

回滚段和事务的关系

  • 每个事务使用一个回滚段,每个回滚段在同一时刻可以服务于多个事务
  • 事务产生的 undo 日志会不断填充回滚段中的区,当前的区不够使用时,会扩展至下一个区。如果回滚段中的所有区都被占满,事务会覆盖最初的区(在可被覆盖的情况下)。

回滚段中的数据分类

  • 未提交的回滚数据:该数据所关联的事务并未提交,不能被其他事务的数据覆盖
  • 已经提交但未过期的回滚数据:该数据所关联的事务已经提交,但是不能被其他事务数据覆盖
  • 已经提交并且已经过期的数据:该数据所关联的事务已经提交,并且已经过期,会被优先覆盖

事务提交后并不能马上删除 undo 日志,因为可能还有其他事务要通过 undo 日志来得到之前版本的数据。

undo 日志的类型

  • insert undo log:事务在 insert 过程中产生的 undo log ,只对事务本身可见,因此在事务提交后可以直接删除。
  • update undo log:事务在 update 和 delete 过程中产生的 undo log ,对其他事务可见,需要提供 MVCC 机制,因此不能再事务提交时删除。提交时放入 undo log 链表,等待 purge 线程进行最后的删除。

undo 日志的生命周期

undo 日志的生成

对于 InnoDB 引擎来说,每个行记录除了记录本身的数据外,还有几个隐藏列:

  • DB_ROW_ID :如果没有显式地为表指定主键,并且表中也没有唯一索引,那么 InnoDB 会自动为每一行生成,作为隐藏主键
  • DB_TRX_ID :每个事务都会分配一个事务 ID,当事务对某条记录执行改动操作时,就会把这个 ID 写入到这里
  • DB_ROLL_PTR :指向 undo log 的指针

image-20220720004708044

image-20220720005112963

image-20220720005345629

image-20220720005457522

undo 日志的回滚

针对上面的例子,执行 rollback 的流程:

  1. 通过 undo no=3 的日志把 id=2 的数据删除
  2. 通过 undo no=2 的日志把 id=1 的数据的 deletemark 还原成 0
  3. 通过 undo no=1 的日志把 id=1 的数据的 name 还原成 Tom
  4. 通过 undo no=0 的日志把 id=1 的数据删除

undo 日志的删除

  • 对于 insert undo log:由于只对事务本身可见,因此可以在事务提交后直接删除,不需要进行 purge 操作
  • 对于 update undo log:由于对其他事务可见,可能会需要提供 MVCC 机制。因此事务提交时,会被放入 undo 日志链表,等待 purge 线程进行删除。

purge 的作用主要是 清理 undo 页删除普通页中被标记的数据 。在 InnoDB 中,事务中的 DELETE 操作只会把数据行标记为删除,需要等待 purge 线程执行真正的删除操作。

小结

image-20220720011210688

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文