文章目录
- 事务
- ACID
- 并发带来的隔离问题
- 幻读(虚读)
- 不可重复读
- 脏读
- 丢失更新
- 隔离级别
- Read Uncommitted (读未提交)
- Read Committed (读已提交)
- Repeatable Read (可重复读)
- Serializable (可串行化)
- 事务的使用
- 事务的实现
- Redo
- undo
事务
事务指逻辑上的一组操作。
我们在 MySQL 存储引擎 | MyISAM 与 InnoDB 中提到,MyISAM引擎
并不支持事务,所以本文内容主要与 InnoDB引擎
相关。
这篇博客写事务也写得很好:我以为我对Mysql事务很熟,直到我遇到了阿里面试官
ACID
谈到事务,那肯定少不了ACID的特性,ACID是以下几个单词的缩写:
原子性(atomicity):
事务是一个不可分割的工作单位,对数据的修改要么全部执行成功,要么全部失败。
举个例子:事务A中要进行转账,那么转出的账号要扣钱,转入的账号要加钱,这两个操作都必须同时执行成功,从而确保数据的一致性。
一致性(consistency):
事务操作前与操作后数据库的状态始终一致。
如何理解呢?就好比我们此时有用户A和用户B,他们的余额分别为300元和700元,此时两人总金额为1000元。此时若是用户B向用户A转账200元,则两者的此时都有500元,总金额还是1000元。
也就是说,无论我们两个怎么转账,总金额它只会是1000,既不会多,也不会少。这就是事务操作前后的状态始终一致。倘若钱多了或者少了,都代表着事务将数据库从一种状态变为了另外一种状态,此时就不再符合一致性了。
隔离性(isolation):
隔离性指的是每个读写事务的对象之间相互隔离,即该事务提交前对其他事务都不可见。
持久性(durability):
持久性指的是事务一旦提交,这个事务的状态会被持久化到数据库中。 即使发生了服务器宕机的事故,数据库也能成功的将数据给恢复。
但是需要注意的是,只能保证数据库本身发生的问题后可以恢复,但并不是事务提交后所有变化都是永久的,倘若是由于外部原因如:RAID卡损坏、天灾人祸导致数据库发生问题,那么即使事务提交了,也可能会丢失。
基于上述原因,持久性只能保证事务系统的高可靠性,而无法保证其高可用性。
总结:
原子性、隔离性、持久性都是为了保障一致性而存在的,一致性也是最终的目的。
并发带来的隔离问题
幻读(虚读)
幻读指 在同一事务中,用同样的操作读取两次,得到的记录数却不一样(针对 insert 操作)。 举个例子:
- 第一个事务对表中的所有数据行进行修改;
- 同时,第二个事务向表中插入了一行。这样也就导致了操作第一个事务的用户发现表中还有没修改的数据行,像发生了幻觉一样。
明明在 会话A
的第一次查询中,大于 2
的数只有行只有一行,而由于 会话B
插入了新行后,对于 会话A
而言就凭空多出来了一行,像出现了幻觉一样。
不可重复读
不可重复读指的是在一个事务中多次读取同一行数据,但是多次读取的数据却不一样(针对 update 操作)。导致这一问题的主要原因就是一个事务读取到了其他事务已提交的数据。
例如:
- 账户1中有300元,账户2中有500元
- 事务A读取账户B的内容,里面显示有500元
- 事务B将账户1的300元全部转给账户2,并提交事务
- 事务A再次读取账户B,此时里面有800元。
由于其他事务的干扰,对于事务A来说,两次读取的金额都不一样。
因为不可重复读读到的是已经提交的数据,由于其本身并不会带来很大的问题,所以大部分数据库厂商都会允许这种情况的发生。
脏读
脏读即一个事务读取到了另外一个事务中未提交的数据,也就是可能因为其他事务对数据进行修改或者回滚导致的问题。
会话B
在第一次查看时表中只有一条数据,但是在第五阶段中 会话A
向表中插入了另一条数据(但还未commit【提交】
),这就导致了 会话B
在读取的时候得到的结果就不再一样,因为它读取到了脏数据。
脏读的现象并不会经常发生,因为脏读发生的条件是需要事务的隔离级别为 READ UNCOMMITTED(读未提交)
,而大部分数据库的默认隔离级别都为 READ COMMITTED(读已提交)
。
丢失更新
丢失更新就是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。例如以下案例:
事务A
将行记录r
更新为1
,但是事务A
并未提交;- 同时,
事务B
将行记录r
更新为2
,事务B
也未提交; - 事务A提交;
- 事务B提交。
此时由于 B
将 A
的修改覆盖,导致 A
虽然提交,但是更新却丢失了,只剩下了 B
的更新。
但是在当前数据库的任何隔离级别下,都不会导致理论意义上的丢失更新问题,即使是隔离级别最低的 Read Uncommitted
,也由于加锁保护,所以 事务B
的修改操作会被阻塞,直到 事务A
提交。
隔离级别
为了解决上述问题,MySQL中实现了以下四种隔离级别,隔离级别由低到高依次是:
- 读未提交(READ UNCOMMITTED)
- 读已提交 (READ COMMITTED)
- 可重复读 (REPEATABLE READ)
- 串行化 (SERIALIZABLE)
隔离级别越高,事务请求的锁也就越多,保持锁的时间也就越长。所以隔离性越强,并发的效率也就越低。
Read Uncommitted (读未提交)
在该隔离级别下,所有事务都可以看到其他未提交事务的执行结果,容易产生脏读问题。
在该级别下,虽然并发的效率最高,但是安全性完全没有得到保护,所以很少用于实际应用。
Read Committed (读已提交)
该隔离级别是大部分数据库默认的隔离级别,如 Oracle
、SQL Server
等。该隔离级别下,一个事务只能看见提交了的事务所做的改变,容易产生不可重复读的问题。
虽然它还有,但不可重复读本身并不是一个大问题,所以为了兼顾到性能,大部分数据库都会容许这种问题的产生。
Repeatable Read (可重复读)
这是 MySQL
中 InnoDB
默认的隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,容易产生幻读的问题。
InnoDB
可以借助 MVCC
中的 Next-Key Locking
的加锁方式来解决这个问题,详见本文。
Serializable (可串行化)
这是最高的隔离级别,通过强制事务进行排序,使事务之间不可能互相冲突,从而解决了其他隔离级别无法解决的幻读问题。
由于其在每个读的数据行上加了共享锁,所以在该隔离级别下可能会导致大量的超时现象以及锁竞争。
这四种隔离级别分别可能发生的问题如下图所示:
事务的使用
开启事务
START TRANSACTION
或者
BEGIN
(由于MySQL的数据分析器会自动将BEGIN识别为BEGIN...END,所以在存储过程中只能使用START TRANSACTION来开启事务)
提交事务
COMMIT
回滚事务
//回滚整个事务
ROLLBACK//回滚至某个保存点
ROLLBACK TO SAVEPOINT [保存点ID]
设置保存点
SAVEPOINT 保存点ID
删除保存点
RELEASE SAVEPOINT 保存点ID
查看隔离级别
// 可以看到 MySQL 的 InnoDB存储引擎 的默认隔离级别为可重复读
mysql> SELECT @@TRANSACTION_ISOLATION;
+-------------------------+
| @@TRANSACTION_ISOLATION |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
设置隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
// GLOBAL 也可以换成 SESSION,前者表示全局的,后者表示当前会话,也就是当前窗口有效。
PS:当设置完隔离级别后对于之前打开的会话,是无效的,要重新打开一个窗口设置隔离级别才生效。
事务的实现
- 事务的持久性主要依靠 Redo log(重做日志)来完成。
- 原子性、一致性则通过 Undo log(撤销日志)来完成。
- 隔离性,则通过锁来完成。
先简单说说 Redo 和 Undo :
Redo/Undo机制: 将所有对数据的更新操作都写到日志中。
Redo log
用来记录某数据块被修改后的值,可以用来恢复未写入data file
的已成功事务更新的数据。Redo
通常是物理日志,记录的是页的物理修改操作。Undo log
是用来记录数据更新前的值,保证数据更新失败能够回滚。Undo
是逻辑日志,根据每行记录进行记录。
举个例子:
假如某个时刻数据库崩溃,当数据库重启进行 crash-recovery
时,就会通过 Redo log
将已经提交事务的更改写到数据文件,而还没有提交的就通过 Undo log
进行 回滚roll back
。
Redo
Redo log
由两部分组成:
- 一是存在于内存中的 重做日志缓冲(redo log buff),由于存在内存中,所以其具有易失性。
- 二是 重做日志文件(redo log file),其存在于硬盘中,所以是持久的。
Redo
主要通过 Force Log at Commit机制
来实现事务的持久性。
步骤如下:
为了确保日志写入文件中,每次将日志缓冲写入日志文件后,都会发起一次 异步操作(fsync) 。
为什么需要这个异步调用呢?
因为重做日志文件打开时并没有使用 O_DIRECT 选项
,所以重做日志缓冲会先写入文件系统缓冲,为了保证其能够成功写入磁盘,必须发起一次异步调用。由于异步调用的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,即数据库性能。
undo
undo
是撤销日志,其中保留了数据库各个版本的状态,我们可以借助 undo
逻辑地将数据库恢复到原来地样子。除了进行回滚之外, undo
的另一个作用就是实现 MVCC
。
首先看看 undo log
的生成流程:
每当事务发生变更的时候,都会伴随着 undo log
的产生,并且为了防止其丢失,undo log 会比数据先持久化到硬盘上。
由于 undo log
是逻辑日志,所以其中记录的都是对于数据库的操作指令。而事务的回滚,其实也就是根据这个操作来进行一个逆向操作。如下面几种:
- 当执行一个
insert
指令时,其逆向指令为delete
; - 当执行一个
delete
指令时,其逆向指令为insert
; - 当执行一个
update
指令时,其逆向指令为update
。
原子性就是借助以上机制实现,倘若事务中的某一个步骤未能成功完成,则借助 undo log
中存储的记录来回滚到事务的最原始状态,即一个失败全体失败。
而至于一致性,则主要依靠上述的其他三种特性来实现,也就是说一致性是目的,而原子性、隔离性、持久性则是数据库实现一致性的手段,只有满足这三个性质,才能够保证一致性。