1.3. 事务
如果不了解事务,你就不可能了解数据库系统的更高级的特性。所谓事务是指,数据库将一组SQL语句当做“原子化”来处理,作为一个独立的工作单元。如果数据库引擎可以执行整组的SQL语句,那么它就执行整组语句;如果组中任何一个SQL语句因为某种原因而失败,那么任何一条语句都不会实际生效。这就是“要么全部要么没有”的策略。
本节只有很少的内容是针对MySQL的。如果你已经对ACID事务比较熟悉,那么你可直接跳到“节1.3.4 MySQL中的事务”。
一个银行应用可以作为典型的例子来说明为什么需要事务。设想一个银行的数据库有两张表,checking和savings。从Jane的checking帐户中转200美元到savings帐户中,你可能需要进行至少三步操作:
1、确定她的checking帐户中的余额超过200美元
2、从她的checking帐户中减去200美元
3、给她的savings帐户中增加200美元的余额
整个操作应该被包在一个事务中来执行,这样来看如果其中任何一步失败了整个步骤就可以回滚。
你可以用“START TRANSACTION”语句来开始一个事务,然后要么利用“COMMIT”来将改变的数据持久化,要么利用“ROLLBACK”来放弃前面的操作。因此我们前面提到的事务例子的SQL语句可能如下这样
但是事务并不是全部。当数据库服务器在执行第4条语句时出现错误会发生什么事呢?这个谁知道?也许客户正好损失了200美元。如果另外的进程在当前进程执行到第3句和第4句之间的时候把checking帐户的余额全部取走又会发生什么事情呢?那么银行将会在不知情的情况下给用户200美元。
除非系统通过了ACID测试,否则事务就是不完善的。ACID代表Atomicity原子性,Consistency一致性,Isolation隔离性,Durability持久性。这些紧密联系的标准必须为一个表现良好的事务处理系统所遵守:
Atomicity原子性
一个事务必须像一个不可分割的工作单元一样工作,这样一来可以使得整个事务要么被执行要么回滚。当事务是原子的时候,就不存在部分完成的事务,也即“要么全部要么没有”。
Consistency一致性
数据库应该总是从一个一致的状态到另外一个。在我们的例子中,一致性保证了第3句和第4句之间的错误不会导致200美元从checking帐户中消失。因为事务如果没有被提交,那么之前在事务内所做的任何改变将不会反映到数据库里。
Isolation隔离性
一个事务的结果“通常”对于其他事务是不可见,除非当前的事务已经结束了。这保证了,当程序执行到第3行到第4行之间的时候还能看到200美元在checking帐户中。当我们在后面谈到隔离级别时,你将会明白为什么说“通常”不可见。
Durability持久性
一旦被提交,一个事务的改变将是持久性的。这代表着所做的改变将会被记录,系统的崩溃不至于导致这些数据的丢失。持久性是一个有些令人犯晕的要概念,因为它实际上有很多级别。一些持久化策略提供了比其他策略更加强大的安全保证,同时不是所有的东西都能持久化的。我们将在后台的章节中继续讨论在MySQL中持久化到底意味着什么,更多的内容可以在“InnoDB I/O 调优”中看到。
ACID事务保证了银行不会弄丢你的钱。这个利用应用程序逻辑一般极难甚至根本不可能做到。一个兼容ACID的数据库服务器必须完成各种你可能都没有意识到的复杂事情来达到ACID的标准。
就如加大锁的粒度会增加开销一样,这种额外的安全会让数据库服务器做更多的工作。一个支持ACID事务的数据库服务器一般也会要求更多的CPU使用率、内存以及硬盘空间。正如我们前面已经多次说到的那样,这就是MySQL的存储引擎架构的优势所在。你可以自行决定你的应用是否需要事务。如果你真的不需要事务的话,你可以选择一个非事务的存储引擎,以期对某些种类的查询得到更加可观的执行效率。你可能会利用“LOCK TABLES”命令在没有事务的情况下提供某种程度的保护。这完全取决于你。
1.3.1. 隔离级别
隔离比它看起来要复杂得多。SQL标准定义了四种隔离级别,每种级别都定义了特定的规则,哪些改变对事务内部或者外部可见或者不可见。一般来说隔离级别越低,并发性也就越高,同时开销也就越小。
每一种存储引擎对于隔离级别的实现会有些微差别,而且它们可能与其他的数据库产品会有所不同。因此你需要阅读对应的存储引擎的手册来决定你是否用它的隔离级别。
让我们来快速地看一下四个隔离级别:
READ UNCOMMITED未提交读
在脏读级别中,事务可以看到未提交的事务的结果。在这个级别上,许多问题都可能发生,除非你真的真的知道你要做什么并且知道为什么这么做。这个级别一般在实践中很少乃至,因此它的性能并不比其他的级别高,而且也没有其他级别的优点。读取未提交数据一般被称为“脏读”。
READ COMMITED 提交读
大多数数据库系统(不包括MySQL)提供的默认隔离级别是提交读。它完全符合了早期隔离的定义:一个事务只能看到其他事务已经提交的数据,并且它改变的数据在未提交之前是不会被其他事务所看到的。这个级别又被称为“不可重复读”。这代表你可能会运行同一个语句两次而得到两个不同的结果。
REPEATABLE READ 可重复读
可重复读解决了“未提交读”中存在的问题。它保证了在同一个事务内,所有的语句都会得到相同的数据,但是理论上来说它也会带来另外一个很诡异的问题:幻读。简单地来说,当你选择一个区间的行时,另外一个事务插入了一个新的行到了这个区间里,那么当你再重新选择这个区间时,你就会得到之前的结果,而这个结果里并没有新的行,因此产生了“幻影”。InnoDB和Falcon存储引擎利用多版本并发控制解决了幻读的问题,这个问题在后面的章节会详细说明。
可重复读是MySQL中的默认事务隔离级别。InnoDB和Falcon存储引擎遵守这个设置,后面在第6章我们会看到如何设置改变这个设置。其他的一些存储引擎也会遵守这个约定,但是这还是取决于存储引擎本身。
SERIALIZE序列化读
序列化读是最高的隔离级别,它通过强制事务必须顺序执行来解决幻读的问题。简单地说,序列化读就是在它读的每一行上放了一个锁。在这个级别上,会发生很多的超时和锁竞争。我们很少能看到有人用这个隔离级别,但是你的应用的需求也许会强迫你在数据稳定性的前提下提交比较差的并发性。
表1-1总结了各种隔离级别以及各自的缺点。
表格 1‑1 ANSI SQL 隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁读取 |
未提交读 | 是 | 是 | 是 | 否 |
提交 读 | 否 | 是 | 是 | 否 |
可重复读 | 否 | 否 | 是 | 否 |
序列化读 | 否 | 否 | 否 | 是 |
1.3.2. 死锁
当两个或者更多的事务持有并且请求同一个资源的锁,并且形成了一个依赖环的时候就会发生死锁。死锁一般发生在当事务试图锁定用不同的顺序来锁定资源时。举个例子来说,考虑下面运行在StockPrice表上的两个事务:
事务1
事务2
如果你很不幸,每个事务都运行它的第一条语句并且更新了数据,各自持有了这两行的锁。每个事务接下来会试图更新它的第二行数据,却发现它已经被锁定了。这两个事务将会永远互相等待对方结果,除非别的事情能够介入打破这个死锁。
为了解决这个问题,数据库系统实现了各种死锁检测和超时机制。越是成熟的系统,比如InnoDB存储引擎,将会注意循环依赖并立即返回一个错误。这是一个很好的事情,否则的话,死锁将表现为一个慢查询。其他的一些存储引擎只是在等待一段锁超时后放弃本条查询语句,这个就不是很友好了。目前InnoDB处理死锁的方式是回滚有最少互斥行锁的事务(一个大概的原因是这是最容易回滚的一个)。
锁的表现和顺序是依赖于存储引擎的,因此一些存储引擎可能在一些特定顺序的语句上死锁而其他的一些不会。死锁有一个双重特性:一些是不可避免的,因为真实的数据是冲突的,而另外一些是依赖于存储引擎的工作方式。
死锁只能通过回滚其中一个事务来完成打破。他们在事务系统中是无法更改的事实,而且你的应用应该来处理这个问题。许多应用只是简单的不断从头重试他们的事务。
1.3.3. 事务日志
事务日志可以使得事务更加有效。不是每次有改变时就更新硬盘上的表,存储引擎只是修改它在内存中的数据。这是很快的。存储引擎会写一条改变记录到事务日志中去,事务日志位于硬盘上因此是持久化的。这是一个相对快速的操作,因为添加日志事件只是涉及到硬盘上一小块区域的顺序IO而不是在许多地方的随机IO。然后在,后面某个时间,一个进程会把表更新到硬盘。因此许多存储引擎使用这个技术(即所谓的“write-ahead loging”),这最终会有两次写盘。
如果在更新被写到事务日志之后而不是之前发生了问题,存储引擎仍然可以通过重启来恢复这些改变。恢复的方法随着存储引擎不同而不同。
1.3.4. MySQL中的事务
MySQL AB提供了三种支持事务的存储引擎:InnoDB,NDB Cluter以及Falcon。另外还有一些第三方的引擎也两样支持事务,其中目前最著名的是solidDB和PBXT。我们将在接下来的章节中详细讨论每个存储引擎的一些特定属性。
1.3.4.1. 自动提交
MySQL默认是工作在AUTOCOMMIT状态的。这意味着除非你显式地开始一个事务,否则它会自动地将每一个查询在独立的事务中执行。你可以通过设置一个变量来打开或者关闭AUTOCOMMIT功能。
其中AUTOCOMMIT设置为1或者ON都是可以的,正如0和OFF同样被MySQL接受一样。当你把AUTOCOMMIT设置为0时,你总是处于一个事务中,除非你执行了一个COMMIT或者ROLLBACK命令,在这之后MySQL又会立即重新开始一个新的事务。在一个不支持事务的表上改变AUTOCOMMIT没有任何作用,比如MyISAM或者Memory表,它们总是工作在AUTOCOMMIT模式之上。
一些特定的命令在执行之前也会导致当前的事务被MySQL提交。它们的典型代表是数据操纵语言(DDL),它们会执行一些特定的操作,比如ALTER TABLE,除此之外LOCK TABLES和其他一些语句也具有这个效果。你可以查询你当前使用的MySQL的文档来得到一个会自动提交事务的命令列表。
通过SET TRASACTION ISOLATION LEVEL命令来设置MySQL的隔离级别,这种改变会在下一个事务开始时体现。你也可以在服务器的配置文件(具体见第六章)中设置隔离级别或者只是调整一个你的会话的设置。
MySQL能够识别所有的ANSI标准隔离级别,并且InnoDB支持所有的这些级别。其他的存储引擎可能对于ANSI标准隔离级别上有些许差异。
1.3.4.2. 混合存储引擎的事务
MySQL并不在服务器级别来管理事务,它将这个工作推迟到底层的引擎引擎实现中。这意味着你不能在同一个事务中混合使用多个不同的存储引擎。MySQL AB为服务器提供了一个更高级别的事务管理服务来提供这种混合存储引擎事务问题。非到万不得已还是不要使用这个功能。
如果你在同一个事务中混合使用事务式和非事务式表(如InnoDB和MyISAM表),如果你的工作一切都顺利的,那么这个事务是没有问题的。但是如果你遇到了问题必须使用一个ROLLBACK的话,在非事务表上做的修改将无法被恢复。这将使得数据库处于一种数据不一致的状态,从这种状态很恢复或者应用整个的事务审查点。这就是为什么一直强调要选择合适的存储引擎的重要性。
如果你在一个非事务表进行一些事务操作,一般来说MySQL是不会警告或者报错的。有时候回滚一个事务可能会生成一个警告“一些非事务的表不能被回滚”,但是大多数时候,你可能根本看不出来你正在工作在非事务表上。
1.3.4.3. 隐式锁和显式锁
InnoDB使用一个两段式锁协议。它使得你可以在事务中的任何时候获得一个锁,但是在COMMIT或者ROLLBACK之前它不会释放这些锁,它会在同一时间释放所有的锁。之前描述的锁都是隐式的。InnoDB根据你的隔离级别来自动处理这些锁。
但是,InnoDB也支持显式锁定,而这一点在标准SQL中根本没有提及。
MySQL也支持LOCK TABLES和UNLOCK TABLES命令,这些命令是在服务器级别而不是存储引擎级别实现的。这些命令有它们的用处,但是它们不能被用作事务的替代品。如果你需要事务就使用一个事务型的存储引擎。
我们经常会看到一些从MyISAM转换到InnoDB的应用仍然继续使用LOCK TABLES命令。这个命令不再必要,因为InnoDB提供了行级别的锁,如果继续使用这种命令可能会导致严重的性能问题。
注:LOCK TALES和事务之间的交互是很复杂的,并且在一些服务器版本 中可能会出现一个不可预料的结果。因此,不管你在使用什么存储引擎,我们推荐不要使用LOCK TABLES,除非你正在一个事务中并且AUTOCOMMIT已经被禁止。