数据库事务学习总结

在数据库中,事务(Transaction)指的是一组相关的SQL语句,它们在业务逻辑上组成一个原子单元。数据库管理系统必须保证一个事务中的所有操作全部提交或者全部撤销。


最常见的数据库事务就是银行账户之间的转账操作。例如,从账户A转出1000元到账户B。

上图是一个简化的转账流程,实际上银行转账还需要检查账户的状态、判断是否收取转账费用等。以上流程包括以下几个步骤:
(1)开始转账流程。查询账户A的余额是否足够,如果其余额不足,则终止转账。
(2)从账户A扣除1000元。
(3)往账户B存入1000元。
(4)在系统中记录本次转账的交易流水。
(5)提交并结束本次转账流程。


数据库管理系统必须保证以上所有操作要么全部成功,要么全部失败。如果从账户A扣除1000元成功,但是往账户B存入1000元失败,就意味着账户A将会损失1000元。用数据库的术语来说,这种情况导致了数据的不一致。

数据库管理系统必须保证以上所有操作要么全部成功,要么全部失败。如果从账户A扣除1000元成功,但是往账户B存入1000元失败,就意味着账户A将会损失1000元。用数据库的术语来说,这种情况导致了数据的不一致。


数据库管理系统的最重要功能就是确保数据的一致性和完整性。在用户执行操作的过程中,数据库可能会遇到系统崩溃、介质失效等故障,此时数据库必须能够从失败的状态恢复到一致的状态。为了实现这些核心功能,数据库中的事务需要满足4种基本的属性。

事务的ACID属性

按照SQL标准,数据库中的事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)以及持久性(Durability),也就是ACID属性:

原子性

指的是一个事务中的操作要么全部成功,要么全部失败。例如,某个事务需要更新100条记录,但是在更新到一半时系统出现故障,数据库必须保证能够撤销已经修改过的数据,就像没有执行过任何更新一样。


一致性

意味着在事务开始之前数据库处于一致性的状态,事务完成之后数据库仍然处于一致性的状态。例如,在银行转账事务中如果一个账户扣款成功,但是另一个账户入账失败,就会出现数据不一致(此时需要撤销已经执行的扣款操作)的情况。另外,数据库还必须确保数据的完整性,比如账户扣款之后不能出现余额为负数的情况(可以通过余额字段上的检查约束实现)。

隔离性

与并发事务有关,表示一个事务的修改在提交之前对其他事务不可见,多个并发的事务之间相互隔离。例如,在账户A向账户B转账的过程中,账户B查询的余额应该是转账之前的数目。如果多个账户同时向账户B转账,最终账户B的余额也应该保持一致性,和多个账户依次进行转账的结果一样。SQL标准定义了4种不同的事务隔离级别。

持久性

表示已经提交的事务必须永久生效,即使发生断电、系统崩溃等故障,数据库也不会丢失数据。数据库管理系统通常使用重做日志(REDO)或者预写式日志(WAL)实现事务的持久性。

简单来说,它们都是在提交之前将数据的修改记录到日志文件中,当数据库出现崩溃时就可以利用这些日志重做之前的修改,从而避免数据的丢失。

事务控制语句

SQL标准定义了以下用于管理数据库事务的事务控制语句:

  • START TRANSACTION,开始一个新的事务。
  • COMMIT,提交一个事务。
  • ROLLBACK,撤销一个事务。
  • SAVEPOINT,设置一个事务保存点,用于撤销部分事务。
  • RELEASE SAVEPOINT,释放事务保存点。
  • ROLLBACK TO,将事务撤销到保存点,保存点之前的修改仍然保留。


5种主流数据库对于SQL事务控制语句的支持如下图所示。

并发事务与隔离级别

在企业应用中,数据库通常需要支持多用户的并发访问,这就意味着我们在操作数据的同时,其他人或者应用程序可能也在操作相同的数据。此时数据库管理系统必须保证多个用户之间不会产生相互影响,数据不会出现不一致性。

并发问题

数据库的并发事务意味着多个用户同时访问相同的数据,比如账户A和账户C同时给账户B转账。数据库的并发访问可能会带来以下问题:


脏读(Dirty Read)。

当一个事务允许读取另一个事务修改但未提交的数据时,就可能发生脏读。例如,账户B的初始余额为0,账户A向账户B转账1000元但没有提交。如果此时账户B能够看到账户A转过来的1000元,并且取款1000元,然后A账户取消了转账操作,就意味着银行损失了1000元。显然,银行不会允许这种事情发生。

不可重复读(Nonrepeatable Read)。

一个事务读取某条记录后,该数据被另一个事务修改并提交,该事务再次读取相同的记录时,结果发生了变化。例如,在对账户B进行查询时的初始余额为0,此时账户A向账户B转账1000元并且提交成功,之后对账户B再次查询时,发现余额变成了1000元。这种情况并不会导致数据的不一致。

幻读(Phantom Read)。

一个事务第一次读取数据后,另一个事务增加或者删除了某些记录,导致该事务再次读取时返回结果的数量发生了变化。幻读和不可重复读有点类似,都是由于其他事务修改数据导致了结果发生变化。

更新丢失(Lost Update)。

第一类更新丢失指的是,当两个事务更新相同的数据时,第一个事务被提交,之后第二个事务被撤销,这导致第一个事务的更新也被撤销。所有遵循SQL标准的数据库都不会产生第一类更新丢失。第二类更新丢失指的是,当两个事务同时读取某个记录后分别进行修改提交,造成先提交事务的修改丢失。


为了解决数据库并发访问可能导致的各种问题,SQL标准定义了事务的隔离级别。

隔离级别

SQL标准定义了4种不同的事务隔离级别,它们(从低到高排列)可能产生的问题如下图所示。

读未提交隔离级别最低:

一个事务可以看到其他事务未提交的修改,相当于不隔离。该级别可能产生各种并发异常。Oracle不支持读未提交隔离级别。PostgreSQL读未提交隔离级别的实现等同于读已提交隔离级别的实现。

读已提交隔离级别:

使用该隔离级别,只能看到其他事务已经提交的数据,因此不会出现脏读,但是存在不可重复读、幻读和第二类更新丢失问题。“读已提交”是大部分数据库的默认隔离级别,包括Oracle、Microsoft SQL Server以及PostgreSQL。

可重复读隔离级别:

使用该隔离级别,可能出现幻读。MySQL(InnoDB)和PostgreSQL在可重复读隔离级别消除了幻读,但是存在第二类更新丢失问题。MySQL(InnoDB)默认使用可重复读隔离级别。Oracle不支持可重复读隔离级别。

序列化隔离级别:

使用该隔离级别,它要求事务只能一个接着一个地执行,不支持并发访问。SQLite实际上实现的就是这种隔离级别。


事务的隔离级别越高,越能保证数据的一致性,但同时会对并发带来更大的影响。我们通常使用数据库的默认隔离级别,至少可以避免脏读,同时拥有不错的并发性能。尽管可能存在不可重复读(某些数据库不会)、幻读以及更新丢失的问题,但是它们并不一定会导致数据的不一致性,而且我们在必要时可以通过应用程序进行处理或者设置为更高的隔离级别。

案例分析

通过一个案例演示读已提交隔离级别的作用和存在的问题,以Oracle为例:

现有一张电影信息表,拿这张表的数据为例。

在会话1中查询到的记录如下图:

先打开一个会话,在会话2中新增一条记录。

 这时候还未提交事物,在会话2中可以查询到新增的ID为4的记录。

这时再回到会话1中进行查询:

在会话1中ID为4的记录并未查询到,说明没有出现数据的脏读问题。

然后我们在会话2中提交事务:

 

接着再次查询会话1中的记录:

查询到了ID为4的记录,意味着会话1读取了会话2提交后的结果,同时也意味着发生了数据的不可重复读。

接着我们在会话2中开始一个新的事务,删除ID为4的记录并提交:

我们再次查询会话1中的记录:

查询没有ID为4的数据,意味着此时产生了数据的幻读。

最后,演示一下并发修改可能导致的更新丢失问题。

在会话1中修改ID为4的记录但不提交事务:

继续在会话2中开始一个新的事务,并且也更新ID为4的记录:

此时,会话2将会处于等待状态。因为当一个事务已经修改某个记录但未提交时,另一个事务不允许同时修改该记录,数据库的并发写操作一定是按照顺序执行的。


然后在会话1中提交事务,提交之后会话2中的更新语句可以正常执行,在会话2中也提交事务。

最后,ID为4的记录,class为200。ID为4的记录,总共更新了两次,正确的结果应该是“300”,会话1中的更新丢失了。问题的原因在于会话2无法得知会话1的修改。

对于这个示例比较好解决,可以使用以下语句替换上面的UPDATE语句:

每次更新记录时基于字段值进行更新,而不是使用之前查询返回的结果。


提示:解决更新丢失问题的其他方法包括悲观锁、乐观锁以及设置更高的隔离级别等。 

摘自:《SQL编程思想》 — 董旭阳

猜你喜欢

转载自blog.csdn.net/liangmengbk/article/details/124288425