在MySQL中,事务是一个数据库操作的最小执行单元,它由一个或多个SQL语句组成,这些SQL语句要么全部执行成功,要么全部失败回滚。
所以,事务是一种机制,用来保证一系列操作要么全部执行成功,要么全部失败回滚,从而保持数据库的一致性和完整性。MySQL中只有使用支持事务的存储引擎(如InnoDB)才能使用事务功能。
如果数据库中没有事务机制,那会怎么样呢?
★ 超级典型的金融案例,案例改编自《高性能MySQL》第四版:
假设银行对两个用户账号进行转账:操作用户账户表(包括转账源头 和 转账目标)。现在要从用户A的账户转账 1000 元到用户B的账户中,那么需要至少三个步骤:
上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。
可以用 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脚本:
所以,金融类系统需要有严格的ACID测试,ACID是指原子性 (atomicity)、一致性(consistency)、隔离性(isolation)和持久性durability)。一个运行良好的事务处理系统,必须具备这些标准特征。
一般来说,衡量事务必须满足四个特性:ACID,即 原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
读取事务未提交数据
脏读就是指当一个事务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; |
前后多次读取数据不一致
不可重复读指的是在事务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元,得到了不同的结果。
前后多次读取,数据不一致
在事务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前后两次读取到的数据总量应该一致
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改
所以,从上面的几个案例可以看到,并发的事务可能导致其他事务的问题包括:
如何解决,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括
SQL92标准中事务的隔离性(Isolation)定义了四种隔离级别,并规定了每种隔离级别下上述几个(脏读、不可重复读、幻读)问题是否被解决。
一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与数据库读的3个问题的关系如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交:Read Uncommitted | ✔ | ✔ | × |
读已提交:Read Committed | × | ✔ | × |
可重复读:Repeatable Read | × | × | ✔ |
串行化:Serializable | × | × | × |
不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷,它本质上也是InnoDB不同的锁策略(Locking Strategy)产生的效果 。接下来我们对这几种事务隔离机制详细介绍下:
读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读的情况。
这种情况下select语句不加锁:SELECT statements are performed in a nonlocking fashion.,所以这是并发最高,一致性最差的隔离级别。
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,不会出现脏读和幻读,但出现不可重复读。
这种隔离级别用的比较多,也是互联网业务最常用的隔离级别,在Read Committed 下:
这种事务的隔离级非常严格,在这种串行情况下不存在脏读、不可重复读、幻读的问题了。
所有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; |
这个明显效率太慢了,在大数据量,大并发量的互联网场景下,基本上是不会使用上述这种隔离级别。
RR(可重复读)是InnoDB默认的隔离级别,在R这种隔离级别下:
可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。但是可能导致“幻读”。