数据库系列:事务的4种隔离级别
数据库系列:MySQL慢查询分析和性能优化
数据库系列:MySQL索引优化总结(综合版)
数据库系列:高并发下的数据字段变更
数据库系列:覆盖索引和规避回表
数据库系列:数据库高可用及无损扩容
数据库系列:使用高区分度索引列提升性能
数据库系列:前缀索引和索引长度的取舍
数据库系列:MySQL引擎MyISAM和InnoDB的比较
数据库系列:InnoDB下实现高并发控制
1 事务概念和必要性
在MySQL中,事务是一个数据库操作的最小执行单元,它由一个或多个SQL语句组成,这些SQL语句要么全部执行成功,要么全部失败回滚。
所以,事务是一种机制,用来保证一系列操作要么全部执行成功,要么全部失败回滚,从而保持数据库的一致性和完整性。MySQL中只有使用支持事务的存储引擎(如InnoDB)才能使用事务功能。
如果数据库中没有事务机制,那会怎么样呢?
★ 超级典型的金融案例,案例改编自《高性能MySQL》第四版:
假设银行对两个用户账号进行转账:操作用户账户表(包括转账源头 和 转账目标)。现在要从用户A的账户转账 1000 元到用户B的账户中,那么需要至少三个步骤:
- 检查账户A的余额高于 1000 元。
- 从账户A余额中减去 1000 元。
- 在账户B的余额中增加 1000 元。
上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。
可以用 START TRANSACTION 语句开始一个事务,然后要么使用 COMMIT 提交事务将修改的数据持久保留,要么使用 ROLLBACK 撤销所有的修改。
事务SQL的样本如下:
/* 开始事务 */
START TRANSACTION;
/* 检查账户A(123456)的余额高于 1000 元 */
SELECT balance FROM acount WHERE customer_id=123456;
/* 从账户A(123456)余额中减去 1000 元 */
UPDATE acount SET balance=balance-1000.00 WHERE customer_id=123456;
/* 在账户B(123457)余额中增加 1000 元 */
UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457;
/* 提交事务 */
COMMIT;
解读下这个SQL脚本:
- 如果执行到第四条语句时服务器崩溃了,用户A可能会损失1000元,而用户B也没有接收到1000元。
- 如果执行到第三条语句和第四条语句之间时,另外一个进程要消费掉A账户的所有余额,那么结果可能就是银行在不知道这个逻辑的情况下白白给了B账户1000元。
所以,金融类系统需要有严格的ACID测试,ACID是指原子性 (atomicity)、一致性(consistency)、隔离性(isolation)和持久性durability)。一个运行良好的事务处理系统,必须具备这些标准特征。
2 事务的四个特性(ACID)
一般来说,衡量事务必须满足四个特性:ACID,即 原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
- 原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable),下面会详细说明。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,会持久化到硬盘上,即便系统故障也不会丢失。
3 如何保证事务的隔离性
3.1 数据库并发下事务的三种现象
3.1.1 脏读
读取事务未提交数据
脏读就是指当一个事务A正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务B也访问这个数据,读了未提交事务操作的数据,然后使用了这个脏数据。举个例子:
时间序列 | A事务 | B事务 |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询B账户有100元余额★SELECT balance FROM acount WHERE customer_id=123456; |
|
T4 | B账户增加1000元转账(未提交)★UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457; |
|
T5 | 查询B账户有1100元余额(读脏数据) | |
T6 | 入账失败,回滚1000元转账款 | |
T7 | 提交事务★commit; |
3.1.2 不可重复读
前后多次读取数据不一致
不可重复读指的是在事务A中先后多次读取同一个数据,读取的结果不一样,因为另外一个事务也访问该同一数据,并且可能修改这个数据,这种现象称为不可重复读。
脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
时间序列 | A事务 | B事务 |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询B账户有100元余额★SELECT balance FROM acount WHERE customer_id=123456; |
|
T4 | B账户增加1000元转账(未提交)★UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457; |
|
T5 | 提交事务★commit; |
|
T6 | 查询B账户有1100元余额(不可重复读) |
按照正确逻辑,事务B前后两次读取到的数据应该一致,这边一次读到的是100元,一次读到的是1100元,得到了不同的结果。
3.1.3 幻读
前后多次读取,数据不一致
在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。通俗点就是已提交事务B对事务A产生的影响,导致B执行有误,这个影响叫做“幻读”。
时间序列 | A事务 | B事务 |
---|---|---|
T1 | 开始事务 | 开始事务 |
T2 | 第一次查询数据库账户表有2条数据,键 pay_id是1和2 | |
T3 | 给账户表转账1000元,所以新增一条 pay_id为3的转账数据 | |
T4 | 提交事务成功 | |
T5 | 因为上面查到的pay_id=2, 所以这边新增一条pay_id=3的消费数据,insert的时候提示key冲突 |
按照正确逻辑,事务B前后两次读取到的数据总量应该一致
3.14 不可重复读和幻读的区别
-
不可重复读是读取了其他事务更改的数据,针对update操作
★ 解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。 -
幻读是读取了其他事务新增的数据,针对insert与delete操作
★ 解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改
所以,从上面的几个案例可以看到,并发的事务可能导致其他事务的问题包括:
- 读脏数据(最后事务并未提交成功)
- 不可重复读
- 幻读
如何解决,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括
- 读未提交(read uncommitted)
- 读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
3.2 事务的隔离级别
SQL92标准中事务的隔离性(Isolation)定义了四种隔离级别,并规定了每种隔离级别下上述几个(脏读、不可重复读、幻读)问题是否被解决。
一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与数据库读的3个问题的关系如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交:Read Uncommitted | ✔ | ✔ | × |
读已提交:Read Committed | × | ✔ | × |
可重复读:Repeatable Read | × | × | ✔ |
串行化:Serializable | × | × | × |
不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷,它本质上也是InnoDB不同的锁策略(Locking Strategy)产生的效果 。接下来我们对这几种事务隔离机制详细介绍下:
3.2.1 读未提交(Read Uncommitted)
读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读的情况。
这种情况下select语句不加锁:SELECT statements are performed in a nonlocking fashion.
,所以这是并发最高,一致性最差的隔离级别。
3.2.2 读已提交(Read Committed)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,不会出现脏读和幻读,但出现不可重复读。
这种隔离级别用的比较多,也是互联网业务最常用的隔离级别,在Read Committed 下:
- 普通读是快照读取
- 加锁的select, update, delete等语句,除了在外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会封锁区间,其他时刻都只使用记录锁;
3.2.3 串行化(Serializable)
这种事务的隔离级非常严格,在这种串行情况下不存在脏读、不可重复读、幻读的问题了。
所有select请求语句都会被隐式的转化为:
select ... in share mode.
这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的select都会被阻塞住,直到之前的事务执行完成。
这是一致性最好的,但并发性最差的隔离级别。执行顺序参考如下:
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(9,'初三九班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | commit; |
这个明显效率太慢了,在大数据量,大并发量的互联网场景下,基本上是不会使用上述这种隔离级别。
3.2.4 可重复读(Repeated Read, RR)
RR(可重复读)是InnoDB默认的隔离级别,在R这种隔离级别下:
-
select操作使用快照读(snapshot read),这是一种不加锁的一致性读(Consistent Nonlocking Read),底层使用MVCC来实现,MVCC 参考作者的这篇:数据库系列:InnoDB下实现高并发控制
-
加锁的
select(select ... in share mode / select ... for update), update, delete
等语句条件的实现,其实是依赖于它们是否在唯一索引(unique index)上使用,如:- 唯一的查询条件(unique search condition)
- 范围查询条件(range-type search condition)
可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。但是可能导致“幻读”。
4 总结
- 认识ACID(原子性、一致性、隔离性、持久性)特性及其实现原理
- 了解事务的脏读、幻读、不可重复读
- 了解InnoDB实现deSQL92标准中的四种隔离级别
- InnoDB默认的隔离级别是RR,互联网用得最多的隔离级别是RC,它解决了脏读和幻读,保证了数据隔离性和一致性。