Skip to content

Latest commit

 

History

History
58 lines (36 loc) · 8.34 KB

Transcation.md

File metadata and controls

58 lines (36 loc) · 8.34 KB

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。通俗地讲,事务是一组原子性的 SQL 查询,事务内的操作完全执行,要么这些操作全不执行,不存在中间状态。事务由 begin transaction 和 end transaction 之间执行的全体操作组成,这些步骤集合必须作为一个单一的、不可分割的单元出现。

不同的数据库系统有不同的事务行为。有些数据库系统根本不支持事务。有些数据库系统支持事务,但是不支持两步提交(2PC)协议。这类事务被称为支持本地事务。有些数据库系统既支持本地事务,又支持 2PC。这类事务被称为支持分布式事务,或者全局事务。

ACID

一般说到事务,就会想到它的特性:ACID,下面结合一个现实中的银行转账例子来说明:A B两同学在同一家银行ZSBANK的账号都有 1,000 块钱,A通过ZSBANK银行向B转了100块钱,这个事务分为两个操作,即从A账号扣100和向B同学账号增加100。那么:

  • 原子性(Atomicity):事务中所有操作要么全部提交成功,要么全部失败回滚。对事务来说,不可能只执行其中的一部分。也就是说上面例子中不可能出现扣除了A的钱,但没增加B的钱的情况。
  • 一致性(Consistency):数据库总是从一个一致的状态转换到另一个一致的状态。**一致性指的是语义上的一致性,即业务逻辑层面的一致。**也就是说A、B两人在转账钱的总和是2,000,转账后两人的总和也必须是 2,000。不会因为这次转账事务破坏这个状态。如果帐户A上的钱减少了,而帐户B上的钱却没有增加,那么我们认为此时数据处于不一致的状态。
  • 隔离性(Isolation):一个事务所做的修改在最终提交之前,对其它事务是不可见的。因此,每个事务都感觉不到系统中有其它事务在并发地执行。也就是说如果A转出100但事务没有确认提交,这时候银行人员对其账号查询时,看到的应该还是1,000,而不是900。
  • 持久性(Durability):一旦事务提交,其所做的修改就会永久保存到数据库中。此时,即使系统崩溃,修改的数据也不会丢失。也就是说转账一但完成,那么A余额为900,B为1,100,这个结果将永远保存在银行的数据库中,直到他们下次交易事务的发生。

在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。MySQL数据库innodb的事务,是通过redo log(innodb log),undo log,锁机制来维护一致性的

隔离级别

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

  • READ UNCOMMITED(未提交读):事务中的修改,即使没有提交,对其它事务也都是可见的。可能发生脏读:即事务可以读取未提交的数据。(实际中很少使用)
  • READ COMMITTED(提交读):满足事务隔离性的简单定义:一个事务开始时,只能看见已经提交的事务所做的修改。这个级别有时候也叫做不可重复读,因为它不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。考虑下面这种情况:A事务读取一个数据项var=1,然后B事务更新该数据var=2并提交,A事务接着又重新读了该数据,结果var=2,第二次读到的数据和第一次的不相同。大多数数据库系统的默认隔离级别是 READ COMMITTED(但是MySQL不是)。
  • REPEATABLE READ(可重复读):只允许读取已经提交的数据,而且在一个事务两次读取一个数据项期间,其它事务不得更新该数据。但该事务不要求和其它事务可串行化。可能存在幻读问题:当某个事物在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。可重复读是 MySQL 的默认事务隔离级别。
  • SERIALIZABLE(可串行化):最高的隔离级别,通过强制事务串行执行,避免前面的幻读问题。会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。

隔离级别与避免的问题如下表所示:

隔离级别 脏读可能性 不可重复读可能性 幻读可能性
READ UNCOMMITED ✔︎ ✔︎ ✔︎
READ COMMITTED ✔︎ ✔︎
REPEATABLE READ ✔︎
SERIALIZABLE

允许不可重复读的隔离级别

事务的实现

为了实现原子性,需要通过日志:将所有对数据的更新操作都写入日志,如果一个事务中的一部分操作已经成功,但以后的操作,由于断电/系统崩溃/其它的软硬件错误而无法继续,则通过回溯日志,将已经执行成功的操作撤销,从而达到“全部操作失败”的目的。最常见的场景是,数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash recovery的过程:读取日志进行REDO(重演将所有已经执行成功但尚未写入到磁盘的操作,保证持久性),再对所有到崩溃时尚未成功提交的事务进行UNDO(撤销所有执行了一部分但尚未提交的操作,保证原子性)。crash recovery结束后,数据库恢复到一致性状态,可以继续被使用。

日志的管理和重演是数据库实现中最复杂的部分之一。如果涉及到并行处理和分布式系统(日志的复制和重演是数据库高可用性的基础),会比上述场景还要复杂得多。

但是,原子性并不能完全保证一致性。在多个事务并行进行的情况下,即使保证了每一个事务的原子性,仍然可能导致数据不一致的结果。例如,事务1需要将100元转入帐号A:先读取帐号A的值,然后在这个值上加上100。但是,在这两个操作之间,另一个事务2修改了帐号A的值,为它增加了100元。那么最后的结果应该是A增加了200元。但事实上, 事务1最终完成后,帐号A只增加了100元,因为事务2的修改结果被事务1覆盖掉了。

为了保证并发情况下的一致性,引入了隔离性,即保证每一个事务能够看到的数据总是一致的,就好象其它并发事务并不存在一样。用术语来说,就是多个事务并发执行后的状态,和它们串行执行后的状态是等价的。怎样实现隔离性,原则上无非是两种类型的锁:

  • 一种是悲观锁,即当前事务将所有涉及操作的对象加锁,操作完成后释放给其它对象使用。为了尽可能提高性能,发明了各种粒度(数据库级/表级/行级……)/各种性质(共享锁/排他锁/共享意向锁/排他意向锁/共享排他意向锁……)的锁。为了解决死锁问题,又发明了两阶段锁协议/死锁检测等一系列的技术。
  • 一种是乐观锁,即不同的事务可以同时看到同一对象(一般是数据行)的不同历史版本。如果有两个事务同时修改了同一数据行,那么在较晚的事务提交时进行冲突检测。实现也有两种,一种是通过日志UNDO的方式来获取数据行的历史版本,一种是简单地在内存中保存同一数据行的多个历史版本,通过时间戳来区分。

锁也是数据库实现中最复杂的部分之一。同样,如果涉及到分布式系统(分布式锁和两阶段提交是分布式事务的基础),会比上述场景还要复杂得多。

更多阅读

脏读、幻读和不可重复读 + 事务隔离级别
数据库事务及锁机制介绍
当我们谈事务时,我们在谈什么?
数据库事务原子性、一致性是怎样实现的?