MySQL事务原理以及4种隔离级别

MySQL事务原理及4种隔离级别

什么是事务

事务是关系型数据区区别于其他NoSQL数据库的一个主要方面,是保障数据一致性的重要手段。

事务会把数据库从一种一致性的状态转移到另外一种一致性的状态。在数据库事务提交的时候,事务可以确保要么所有的修改都已经生效保存,要么所有的修改都不保存。

拿一个常见的银行转账作为例子:A账户给B账户转账1000块买东西。在这个交易的过程中,有几个问题需要思考:

  • 如何同时保证上述交易中A账户总金额减少1000,B账户总金额增加1000? (A)
  • A 账户如果同时在和C账户交易,如何让这两笔交易互不影响?(I)
  • 如果交易完成时数据库突然崩溃,如何保证交易数据成功保存在数据库中?(D)
  • 如何在支持大量交易的同时,保证数据的合法性(没有钱凭空产生或消失)?(C)

要保证交易正常可靠地进行,数据库就得解决上面的四个问题,这也就是事务诞生的背景,它能解决上面的四个问题,对应地,它拥有四大特性:

  • 原子性(Atomicity):一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,对于一个事务来说不可能只执行其中的部分操作。
  • 隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。下文讨论隔离级别的时候,就会发现为什么要说“通常来说”。
  • 持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时,即使系统崩溃,修改的数据也不会丢失。
  • 一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。

事务的实现原理

事务的最终目的是为了保证数据的一致性,因此,ACID中的AID就是为了实现C而做的铺垫。下面就分别看看原子性(A)、隔离性(I)、持久性(D)的实现原理。

原子性实现原理(MySQL)

Undo Log是MySQL实现事务原子性的基础。当事务对数据库进行修改时,InnoDB会生成对应的Undo Log;如果事务实行失败或者调用了roolback,导致事务需要回滚,便可以利用Undo Log中的信息将数据回滚到修改之前的样子。

Undo Log属于逻辑日志,它记录的是sql执行的相关信息。当发生回滚时,InnoDB会根据Undo Log的内容做与之前相反的工作:对于每个Insert,回滚时会delete;每个delete,回滚时会insert;每个update就做一个相反的update,改回原数据。

拿上文A转账给B账户的例子来说

假设初始时候,A账户有10000,B账号有4000

账户表-account

id account_name balance
1 A 10000
2 B 4000

执行了一个转账事务之后

1
2
3
4
start transaction;
update account set balance = balance - 1000 where id = 1;
update account set balance = balance + 1000 where id = 2;
commit

如果事务成功提交了,account表的内容就变成了

账户表-account

id account_name balance
1 A 9000
2 B 5000

另外,MySQL还会给这个事务生成Undo Log

type info
update table: account id:1 balance:10000
update table:account id:2 balance:4000

如果此时事务需要回滚,那么就可以根据Undo Log的信息,把数据恢复到修改之前的状态了。

隔离性实现原理(MySQL)

隔离性研究或者保证的是不同事务之间的影响。事务内部的操作和其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

隔离性主要是通过锁机制和MVCC保证的。

锁机制保证一个事务的操作不对另外一个事务的操作产生影响

MVCC保证一个事务的操作不对另外一个事务的操作产生影响

锁机制

锁机制的原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得相应的锁之后,事务才可以修改数据;在当前事务操作期间,其他事务如果要来修改,只能等待当前事务提交或者回滚后释放锁。

MySQL锁按照粒度可以分为:

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。支持引擎-MyISAM、MEMORY、InnoDB
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率最低,并发度也最高。支持引擎-InnoDB
  • 页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般。支持引擎-BDB
  • 全局所:对整个数据库实例加锁

MVCC

MVCC全称是多版本并发控制(MultiVersion Concurrency Control)。InnoDB的MVCC是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事物,系统版本号都会自动递增。事物开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号作比较。

持久性实现原理(MySQL)

持久性原理是通过Redo Log实现的。

数据库的数据是存放到磁盘中的,但如果每次读写数据都进行磁盘ID,效率无疑会很低下。因此,InnoDB引入了Buffer pool。Buffer pool包含了部分数据页的映射,作为缓冲。数据先从Buffer Pool中读取,如果没有,从磁盘读取后再放入Buffer Pool;同理,写入的话,也是先写入到Buffer Pool中,然后定期刷回磁盘。

Buffer Pool的使用大大提高了读写数据的效率,但也产生了新的问题:如果在Buffer Pool中的新数据还没有刷新到磁盘之前数据库就宕机了,就会导致数据丢失,误报保证事务的持久性。

于是Redo Log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在Redo Log中记录这次操作;当事务提交时,会调用fsync接口对Redo Log进行刷盘。如果MySQL宕机,重启的时候可以读取Redo Log中的数据,对数据库进行恢复。

Redo Log采用的是WAL(Write-Ahead Loggin,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因为MySQL宕机二小时,从而满足了持久性要求。

事务的隔离级别

SQL标准中定义了四种隔离级别。级别越低的可以执行越高的并发, 但同时实现复杂度以及开销也越大。

MySQL中隔离级别有以下四种(级别由低到高):

  • READ UNCOMMITED (读未提交)
  • READ COMMITED(读提交)
  • REPEATABLE READ(可重复读)
  • SERIALIZABLE(串行化)

我们还是从银行转账的例子举例说明这四个隔离级别。

READ UNCOMMITED(读未提交)

时间 事务T1 账户A余额 事务T2 结果
1 start transaction 10000 start transaction
2 update account set balance = balance - 1000 where id = 1 10000 select balance from account where id = 1 9000
3 commit
4 commit
5 9000

在时间1,发起了两个事务T1和T2,T1给账号A扣除1000,T2查询账号A余额。在时间点2的时候,T1更新了A的余额,但是没有提交,此时T2查询A的余额,取得9000。在时间点4的时候,事务T1 commit。此时余额才真正为9000.

试想一下,如果事务T1在时间点4执行的时候发生异常,回滚了,那么A账号的余额还是为10000,那么事务在时间点2读取到的就是脏数据了。这就叫脏读

READ COMMITED(读提交)

还是两个事务T1和T2修改账号A余额的例子。

时间点 事务T1 结果 事务T2 结果
1 start transaction 余额1000
2 select balance from account where id = 1 余额10000 start transaction 余额10000
3 update account set balance = balance - 1000 where id = 1 余额10000
4 commit 余额9000
5 select balance from account where id = 1 余额9000
6 commit

造成这种情况的原因就是,事务T1在执行而未提交的过程中,事务T2修改了事务T1的数据项,并且这个修改对事务T1可见,造成事务T1两次读取到的数据不一致。这就叫不可重复读

Repeatable Read(可重复读)

在可重复读这个级别,是当前事务读取的数据是不会读取到其他事务对当前事务数据的修改的。但是它无法避免读取到新的数据。

时间点 事务T1 结果 事务T2 结果
1 start transaction 10000;
4000
2 select balance from account; 10000
4000
start transaction 10000
4000
3 insert into account values(3, “C”, 8000) 10000
4000
4 commit 10000
4000
8000
5 select balance from account; 10000
4000
8000
6 commit

Serializable(串行化)

最高级别,这个级别下,所有的事务都串行执行。

参考资料

深入理解数据库事务

深入学习MySQL事务:ACID特性的实现原理

0%