事务
概述
事务: 一组逻辑操作单元,使数据从一种状态变换到另一种状态
事务处理的原则: 保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( 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 TABLES
、UNLOCK TABLES
等关于锁定的语句也会 隐式的提交 前边语句所属的事务。
- 当我们在一个事务还没提交或者回滚时就又使用
事务隔离级别
数据并发问题
对于 SQL 来说,服务端可能和多个客户端建立连接,每个客户端与服务端建立的连接被称为 会话 ( Session )。每个会话都可以在自己的会话中向服务器发送请求语句,请求语句可能是事务的一部分,当多个会话同时发送请求时,就会遇到数据并发问题。
脏写(Dirty Write)
对于了两个事务 Session A
、 Session B
,如果 Session A
修改 了 Session B
还未提交的数据,就发生了 脏写 。若 Session B
回滚,那么 Session A
修改的数据就不存在了。
脏读(Dirty Read)
对于两个事务 Session A
、 Session B
,如果 Session A
读取 了 Session B
还未提交的数据,就发生了 脏读 。若 Session B
回滚,那么 Session A
读取的数据就是临时且失效的。
不可重复读(Non-Repeatable Read)
对于两个事务 Session A
、 Session B
,如果 Session A
读取 了一个字段,然后 Session B
更新了这个字段,之后 Session A
再次读取同一个字段,值就不同了,就发生了 不可重复读 ( Session A
的两次读取是在同一个事务中,因此逻辑上来说应当相同)
幻读(Phantom)
对于两个事务 Session A
、 Session B
,如果 Session A
读取 了一个字段,然后 Session B
在表中插入了一些新的数据,之后 Session A
再次读取同一个字段,就会多出一些数据,就发生了 幻读 (如果 Session B
删除了一些数据,导致第二次读取到的数据少了,不属于幻读。幻读强调的是读取到了之前没有的记录)
严重程度排序: 脏写 > 脏读 > 不可重复读 > 幻读
SQL 的四种隔离级别
SQL 标准中设立了 4 个隔离级别:
READ UNCOMMITTED
:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。READ COMMITTED
:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。REPEATABLE READ
:可重复读,事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。 这是 MySQL 的默认隔离级别,MySQL 中这个级别可以通过临键锁、MVCC 解决幻读 。SERIALIZABLE
:串行化。在事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
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) :保存在内存中的临时缓冲数据
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 日志的流程
一个更新事务的流程
- 将原始数据从磁盘中读入内存中的缓冲池,在其中中修改数据
- 生成一条 redo 日志并将日志写入
redo log buffer
,记录的是数据被修改后的值 - 当事务 commit 时,将 redo log buffer 中的内容追加写入到
redo log file
中 - 定期将缓冲池中的数据刷新到磁盘中
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_logfile0
和ib_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
继续写。
checkpoint
在整个日志文件组中还有两个重要的属性: write pos、checkpoint
- write pos 记录当前的位置,一边写一边后移
- checkpoint 是要擦除的位置
上图中,checkpoint 左边是已经刷盘的数据,可以放心覆盖;右边是还未刷盘的数据,需要等待刷盘,checkpoint 向前走之后才能覆盖
小结
undo 日志
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 的指针
undo 日志的回滚
针对上面的例子,执行 rollback 的流程:
- 通过 undo no=3 的日志把 id=2 的数据删除
- 通过 undo no=2 的日志把 id=1 的数据的 deletemark 还原成 0
- 通过 undo no=1 的日志把 id=1 的数据的 name 还原成 Tom
- 通过 undo no=0 的日志把 id=1 的数据删除
undo 日志的删除
- 对于 insert undo log:由于只对事务本身可见,因此可以在事务提交后直接删除,不需要进行 purge 操作
- 对于 update undo log:由于对其他事务可见,可能会需要提供 MVCC 机制。因此事务提交时,会被放入 undo 日志链表,等待 purge 线程进行删除。
purge 的作用主要是 清理 undo 页 和 删除普通页中被标记的数据 。在 InnoDB 中,事务中的 DELETE 操作只会把数据行标记为删除,需要等待 purge 线程执行真正的删除操作。
小结
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论