ACID与数据库隔离级别

198 阅读7分钟
原文链接: qiuzhenyuan.github.io

事务

事务,就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。

银行应用是解释事务的重要例子。假设银行的数据库有两张表:支票表和存储表。现在要从用户Tom的支票账户转移200元到他的储蓄账户,那么至少需要三个步骤:

  1. 检查支票账户的余额高于200元。
  2. 从支票账户中减去200元。
  3. 在储蓄账户余额中增加200元

上述3个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有步骤。

可以用START TRANSACTION 开始一个事务,然后用COMMIT 提交事务将修改的数据持久保留,要么使用ROLLBACK 撤销所有的修改。事务SQL语句如下:

START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 1023;
UPDATE checking SET balance=balance-200 WHERE customer_id=1023;
UPDATE savings SET balance=balance-200 WHERE customer_id=1023;
COMMIT;

数据库的ACID

ACID表示原子性(atomicity),一致性(consistency),隔离性(isolation)和持久性(durability)。一个运行良好的事务处理系统需要具备这些标准特征。

原子性

一个事务必须被视为一个不可分割的最小工作单元,整个事务中所有的操作要么全部提交成功,要么全部失败回滚,对于一个是来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性

数据库总是从一个一致性的状态转换到另一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、第四条语句之间系统崩溃,支票账户中也不会损失200元,不会出现支票扣了200元,储蓄账户却没加上的情况。因为事务没提交,事务中所有的修改也不会保存在数据库中。

隔离性

通常来说,一个事务所做的修改在最终提交之前,对其他务是不可见的。在前面的例子,当执行完第三条语句、第四条语句还没开始时,此时另一个账户汇款的程序执行,则其看到的账户并没有被减去200元。隔离性就是为了保证多个事务并发访问时,事务之间是隔离的。下文将介绍四种不同的隔离级别。

持久性

一旦事务提交,则其做的所有修改都会永久保存在数据库中。

四种隔离级别

在SQL中定义了四种隔离级别,每一种级别都规定了在一个事务中做的修改,在哪些事务内核事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统开销更低。

READ UNCOMMITTED(读未提交)

读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。这个级别会导致好多很多问题,但是从性能上来说不会比其他级别好太多。所以实际开发中一般很少用。下面通过一个例子来理解什么是脏读

事例:

老板给程序员发工资,程序员工资为2万。老板开启了发工资的事务,但是发工资的时候,不小心多按一个0,从2万变成20万。钱打到了程序员真虎,而事务还没提高。因为可以读取未提交的数据,所以程序员可以看到自己的账户余额多了20万元,以为涨工资觉得很高兴。但是老板几时发现了错误,回滚了差点就要提交的事务,将数字修改回2万再提交。

分析:

实际程序员到手的工资是2万,但是看到的是20万。而看到的这个数据,是老板还没提交的修改。读取了未提交的修改,就是脏读

READ COMMITTED(读提交)

读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
大多数的数据库系统的默认隔离级别都是READ COMMITTED(但 MySQL不是)。READ COMMITTED只能“看见“已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫不可重复读。因为一个事务中,两次相同的查询可能会得到不一样的结果。下面通过一个例子来说明什么是不可重复读

事例:

回到文章开头的例子,老王要把自己支票账户里的200元转到储蓄账户下。第一步是检查支票账户余额是否不小于200元。假设支票账户余额大于200元,就要程序就要进行扣费。但是在检查之后、扣费之前,恰好老王的老婆把支票账户内的所有钱都转到自己的储蓄账户下。这时,如果老王在这个事务内再查询一次余额,就会发现账户余额为0。老王觉得很奇怪,为什么刚刚还有钱,现在就没有了呢……

分析:

这就是READ COMMITTED模式下的不可重复读问题 ,如果在一个事务范围内,进行了两次相同的查询,要查询的数据在两次查询之间的这段时间内被另一个事务修改,就有可能导致两次查询的数据不一致。

REPEATABLE READ(可重复读)

重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。
REPEATABLE READ解决了脏读不可重复读 的问题。不过,这个级别依然存在着幻读 的问题。所谓幻读,指的是当某个事务在读取某个范围内的数据是,另外一个事务又在该范围内插入或删除了记录,当当前事务再次读取该范围内的数据时,就会出现 幻行

事例:

程序员的妻子要打印银行卡的消费清单。她开启一个事务先查看消费记录,看到消费的总额为2000元,然后她要把消费的记录打印出来。然而在她看到消费记录之后,打印消费记录之前,程序员用这个银行卡买了1万块的电脑。当妻子把清单打印出来后,发现多了一项1万元的消费记录,似乎出现了幻觉,这就是幻读
注意:幻读与不可重复读是有区别的。不可重复读是对读取的数据进行了修改操作(UPDATE)。而幻读,是读取的范围有了新增的数据,或者删除了数据(INSERT或DELETE)。

SERIALIZABLE(可串行化)

Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行 ,避免了前面所说的幻读的问题。简单来说,就是通过强制事务串行执行,避免了前面所说的幻读问题。SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中,很少用到这个隔离级别。

隔离级别总结

隔离级别 脏读可能性 不可重复度可能性 幻读可能性 加锁读
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No No No
SERIALIZABLE No No No Yes