MySQL技术内幕(InnoDB存储引擎概述)学习笔记04-事务

概述

事务(Transaction)是数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很可能被破坏。当然有一些机制可以把文件恢复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统可能就无能为力了。例如,在需要更新两个文件时,更新完一个文件后,在更新完第二个文件之前系统重启了,就会有两个不同的文件。

这正是数据库系统引入事务的主要目的 : 事务会把数据库从一种一致状态转换成另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有的修改都不保存。

InnoDB中的事务完全符合ACID特性。

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

本章主要关注事务的原子性这一概念,并说明正确使用事务即编写正确的事务应用程序,避免在事务方面养成一些不好的习惯。

认识事务

事务可由一条简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中数据项的一个程序执行单元。在事务中的操作,要么都成功,要么都不成功,这就是事务的目的,也是事务模型区别于文件系统模型的重要特征之一。

理论上说,事务有着极其严格的定义,他必须同时满足四个特性,即通常所说的事务的ACID特性。值得注意的是,虽然理论上定义了严格的事务要求,但是数据库厂商处于各种目的,并没有严格的去满足ACID标准。例如,对于MySQL的NDB Cluster引擎来说,虽然其支持事务,但是不满足D的要求,即持久性的要求。对于Oracle数据库来说,其默认的事务隔离级别为READ COMMITTED,不满足I的要求,即隔离性的要求。虽然在大多数情况下,这并不会导致严重的后果,甚至可能带来性能的提升,但是用户首先需要知道严谨的事务标准,并在实际的生产应用中避免可能存在的潜在问题。对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足ACID特性。下面具体介绍下事务的ACID特性,并给出相关概念。

A(Atomicity),原子性。在计算机系统中,每个人都将原子性视为理所当然。例如在C语言中调用SQRT函数,要么返回正确的平凡根值,要么返回错误的代码,而不会在不可预知的情况下改变任何的数据结构和参数。如果SQRT函数被许多个程序调用,一个程序的返回值也不会是其他程序要计算的平方根。

然而在数据的事务中实现调用操作的原子性,就不是那么理所当然了。例如用户在ATM机前取款,假设取款的流程为

  1. 登录ATM机,验证密码
  2. 从远程银行的数据库中取得账户的信息
  3. 用户在ATM机上输入欲取出的金额
  4. 从远程银行的数据库中更新账户信息
  5. ATM机出款
  6. 用户取钱

整个取款的操作应该视为原子操作,即要么都做,要么都不做。不能用户钱未从ATM机上获得,但是银行卡上的钱已经被扣除了。

原子性是整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算成功。事务中任何一个SQL语句失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

如果事务中的操作都是只读的,要保持原子性其实是很简单的。一旦发生任何错误,要么重试,要么返回错误代码。因为只读操作不会改变系统中的任何相关部分。但是当事务中的操作需要改变系统中的状态时,例如插入或更新记录,那么情况就不一样了。如果操作失败,很有可能会引起状态变化,因此必须保护系统中并发用户访问受影响的部分数据。

C(Consistency),一致性。一致性是指事务将数据库从一种状态转换为下一种一致的状态。在事务开始之前和事务结束之后,数据的完整性约束没有被破坏。例如表中有一个字段为姓名,为唯一约束。如果一个事务对姓名字段进行了修改,但是事务在提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的唯一性要求,即事务将数据库从一种状态变为了另一种不一致的状态。因此,事务是一致性的单位,如果是事务中某个动作失败了,系统可以自动撤销事务,返回初始化的状态。

I(isolation),隔离性。隔离性还有其他的称呼,如并发控制(concurrency control),可串行化(serializability),锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之前的并发度。

D(durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,事务提交后,所有的变化都是永久的。即使当数据库 因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。但若不是数据库本身发生故障,而是一些外部原因,如RAID卡损坏,自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合完整。

分类

从事务理论的角度来说,可以把事务分为以下几种类型

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transaction with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

扁平事务是事务中最简单的一种,但是在实际生产环境中可能是最频繁的事务。在扁平事务中,所有操作都处于统一层次,由begin work开始,由commit work或rollback work结束,其间的操作是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序称为院子操作的基本组成模块。

因为其简单,故基本每个数据库系统都实现了对扁平事务的支持。

扁平事务的主要限制是不能提交或回滚事务的某一部分,或分几个步骤提交。

带有保存点的扁平事务,除了支持扁平事务的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都失效,当其整个事务不合乎要求,开销也太大。保存点(Savepoints)用来通知系统应该记住事务当前的状态以便之后发生错误时,事务能回到保存点当时的状态。

对于扁平的事务来说,其隐式的设置了一个保存点。然后再整个事务中,只有这一个保存点,因此,回滚只能回滚到事务开始的状态。保存点用save work函数来建立通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回滚到最近一个保存点还是其他更早的保存点。

链事务可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有保存点都将消失,因为其保存点是易失(volatile)的而非持久的(persistent)。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。

链事务的思想是 : 在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。

链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。对于锁的处理,两者也不同。链事务在执行commit后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

嵌套事务是一个层次结构框架。由一个顶层事务(top level transaction)控制着每个层次的事务。顶层事务之下前台的事务被称为子事务(substransaction),其控制每一个局部的变换

下面给出Moss对前台事务的定义

  1. 嵌套事务是由若干事务组成的一棵树,子树既可以是前台事务,也可以是扁平事务。
  2. 处在叶节点的事务是扁平事务。但是每个子事务从根到叶节点的距离可以是不同的。
  3. 位于根节点的事务称为顶层事务,其他事务称为子事务。事务的前驱(predecessor)称为父事务(parent),事务的下一层称为儿子事务(child)。
  4. 子事务既可以提交也可以回滚。但是他的提交操作并不马上生效,除非其父事务已经提交。因此可以推论出,任何子事务都在顶层事务提交后才真正提交。
  5. 树中的任意一个事务的回滚会引起他的所有子事务一同回滚,故子事务仅保留A,C,I特性,不具有D特性。

在Moss的理论中,实际的工作是交由叶子节点来完成的,即只有叶子节点的事务才能访问数据库,发送消息,获取其他类型资源。而高层的事务仅负责逻辑控制,决定何时调用相关的子事务。即使一个系统不支持嵌套事务,用户也可以通过保存点技术来模拟嵌套事务。

使用保存点技术模拟的嵌套事务在锁的持有方便还是与嵌套事务有所区别。当通过保存点即使来模拟事务时,用户无法选择哪些需要被子事务几次,哪些需要被父事务保留。这就是说,无论有多少个保存点,所有被锁住的对象都可以被得到和访问。而在嵌套事务中,不同子事务在数据库对象上持有的锁是不同的。

然而,如果系统支持在嵌套事务中并行的执行各个子事务,在这种情况下,采用保存点的扁平事务来模拟嵌套事务就不切实际了。这从另一个方面反映出,想要实现事务间的并行性,需要真正的支持嵌套事务。

分布式事务通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。例如跨行转账

InnoDB支持扁平事务,带有保存点的事务,链事务,分布式事务。对于嵌套事务,其原生不支持,因此,对有并行事务需求的用户来说,MySQL数据库或InonoDB引擎就显得无能为力了。

事务的实现

事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的redo log和undo log来完成,redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性

redo和undo的作用都可以视为一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的页的物理修改操作。undo是逻辑日志,根据每行记录进行记录

redo

基本概念

重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成 : 一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的.

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交时(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,事务的COMMIT操作才算完成。这里的日志指的是重做日志,在InnoDB中,由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上是需要进行随机读写的。

为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB都需要调用一个fsync操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能能决定事务提交的性能,也就是数据库的性能。

InnoDB允许用户手动设置成非持久性,以此提高数据库的性能,即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。由于并非强制在事务提交时进行一次fsync操作,显然这可以显著提高数据的性能。但是当数据库宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。

参数innodb_flush_log_at_trx_commit可以用来控制重做日志刷新到磁盘的策略。具体的可以百度。

在MySQL数据库中海油一种二进制日志(binlog),其用来进行POINT-TIME(PIT)的回复及主从复制(Relication)环境的简历。从表面上看其和重做日志非常相似,都是记录了对于数据库的操作的日志。然而,从本质上来说,两者有着非常大的不同。

首先,重做日志是InnoDB存储引擎层产生的,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎都会产生二进制日志。

其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录对应的SQL语句。而InnoDB的重做日志是物理格式日志,其记录的是对每个页的修改。

此外,两种日志记录写入磁盘的时间不同,二进制日志只在事务提交完成后进行一次写入。而InnoDB的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。

二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于InnoDB的重做日志,由于其记录的是物理操作日志,因此每个事务对于多个日志条目,并且事务的重做日志是并发写入的,并非在事务提交时写入的,故其在文件中记录的顺序并非是事务开始的顺序。

log block

在InnoDB中,重做日志都是以512字节进行存储的。这意味着重做日志缓存,重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。

若一个页中产生的日志数量大于512字节,那么需要分隔为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。

...略...

undo

基本概念

重做日志记录了事务的行为,可以很好地通过其对页进行重做操作。但是数据有时还需要回滚,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户只需的事务或语句由于某种原因失败了,又或者用户用一条rollback语句请求回滚,这样就可以利用这些undo信息将数据回滚到修改之前的样子。

redo存档在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。

用户通常对undo有这样的误解 : undo用于将数据库物理地恢复到执行语句或事务之前的样子,但事实并非如此。undo是逻辑日志,因此只是将数据库的逻辑的回复到原来的样子。所有的修改都没逻辑的取消了,但是数据结构和页本身在回滚后可能大不相同。这是因为在多用户并发的系统中,可能会有数十上百甚至上千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几个记录,同时还有别的事务在对同一个页中的 另几条记录进行修改。因此不能将一个页回滚到事务开始时的样子,因为这会影响到其他事务正在进行的工作。

例如,用户执行了一个insert 10w条记录的事务,这个事务会导致分配一个新的段,即表空间增大。在用户执行rollback时,会将插入的事务进行回滚,但是表空间的大小并不会因此缩减。因此,当InnoDB存储引擎回滚时,它实际上做的是与之前相反的操作。对于每个insert操作,都会去完成一个delete;对于每个delete,都会完成一个insert;对于每个update,都对完成一个相反的update。

除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来实现的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取到之前的行版本信息,以此实现非锁定读。

最后很重要的一点,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

... 略 ...

事务控制语句

在MySQL的命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句就会马上执行COMMIT操作。因此要显式地开启一个事务需使用命令BEGIN,START TRANSACTION,或者执行命令SET AUTOCOMMIT=0,禁用当前会话的自动提交。注意,每个数据库厂商自动提交的设置都不相同。下面看看有哪些事务控制语句

  • START TRANSACTION : 显式的开启一个事务
  • COMMIT : 这个语句的最简形式就是COMMIT。也可以详细一些,用COMMIT WORK,不过二者几乎是等价的。COMMIT会提交事务,并使已对数据做的所有修改称为永久性的。
  • ROLLBACK : 这个语句的最简形式就是ROLLBACK。也可以详细一些,用ROLLBACK,不过二者几乎是等价的。回滚并结束用户的事务,并撤销事务中所有未提交的修改。
  • SAVEPOINT identifier : 在事务中创建一个保存点,一个事务可以有多个SAVEPOINT.
  • RELEASE SAVEPOINT identifier : 删除一个事务的保存点。
  • ROLLBACK TO [SAVEPOINT] identifier : 这个语句和SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何操作
  • SET TRANSACTION : 用来设置事务的隔离级别。InnoDB提供的事务级别有 : READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE。

START TRANSACTION,BEGIN语句都可以在命令行下显式的开启一个事务。但是在存储过程中,MySQL数据库的分析器会将BEGIN识别成BEGIN...END,因此在存储过程中只能使用START TRANSACTION语句开启一个事务。

COMMIT和COMMIT WORK语句基本是一致的,都是用来提交事务的。不同之处在于COMMIT WORK用来控制事务结束后的行为是CHAIN还是RELEASE的,如果是CHAIN方式,那就变成了链事务。

可以通过参数completion_type来进行控制,该参数默认为0,标识没有任何操作,在这种设置下COMMIT和COMMIT WORK是完全等价的。当参数为1时,COMMIT WORK等同于COMMIT AND CHAIN,表示马上自动开启一个相同隔离级别的事务。如下

create table t(`id` int);
set @@completion_type=1;
start transaction;
insert into t value (1);
commit work;
insert into t value (2);
rollback;
select * from t;
# 只能查询出id为1的一条记录

当completion_type为2时,COMMIT WORK等同于COMMIT AND RELEASE。在事务提交后会自动断开与服务器的连接。

ROLLBACK和ROLLBACK WORK与COMMIT和COMMIT WORK的用法一致。

SAVEPOINT记录了一个保存点,可以通过ROLLBACK TO SAVEPOINT来回滚到某个保存点,但是如果回滚到一个不存在的保存点,则会抛出异常

InnoDB存储引擎的中的事务都是原子性的,这说明下述两种情况 : 构成事务的每条语句都会提交(称为永久)或者全部回滚。这种保护还延伸到每个语句。一条语句要么完全成功,要么完全回滚(注意,这里是针对单条语句的回滚)。因此一条语句执行失败并抛出异常时,并不会导致先前事务中已经执行的语句回滚。所有的执行都会被保留,必须由用户自己来决定是否对其进行提交或回滚操作。如下:

drop table if exists `t`;
create table t(`id` int,primary key(`id`));
start transaction;
insert into t value (1);
insert into t value (1);
# 抛出主键不能重复的异常
select * from t;
# 可以查询出id为1的一条记录

另一个容易犯的错误是ROLLBACK TO SAVEPOINT,虽然有ROLLBACK,但其并不是真正的结束一个事务,因此即使只需了ROLLBACK TO SAVEPOINT,之后也需要显式的执行COMMIT或ROLLBACK命令。如下 :

drop table if exists `t`;
create table t(`id` int,primary key(`id`));
start transaction;
insert into t value (1);
savepoint t1;
insert into t value (2);
savepoint t2;
release savepoint t1;
insert into t value (2);
# 抛出主键重复的异常
rollback to savepoint t2;
select * from t;
# 依然可以查询到两条记录
rollback;
# 全部回滚
select * from t;
# 查询不到记录了

隐式提交的SQL语句

以下SQL语句会产生一个隐式的提交操作,就是不能进行回滚

  • DDL语句: ALTER DATABASEUPGRADE DATA DIRECTORY NAME,ALTER EVENT, ALTER PROCEDURE, ALTER TABLE, ALTER VIEW,CREATE DATABASE, CREATE EVENT, CREATE INDEX, CREATE PROCEDURE, CREATE TABLE, CREATE TRIGGER, CREATE VIEW,DROP DATABASE, DROP EVENT, DROP INDEX, DROP PROCEDURE,DROP TABLE, DROP TRIGGER, DROP VIEW, RENAME TABLE,TRUNCATE TABLE。
  • 用来隐式地修改 MySQL架构的操作: CREATE USER、 DROP USER、 GRANT 、RENAME USER、 REVOKE、 SET PASSWORD。
  • 管理语句: ANALYZE TABLE、 CACHE INDEX、 CHECK TABLE、 LOAD INDEX INTO CACHE、 OPTIMIZE TABLE、 REPAIR TABLE。

InnoDB的应用需要在考虑每秒请求数(Question Per Second,QPS)的同事,应该关注每秒事务的处理能力(Transaction Per Second, TPS)。

计算TPS的方法(com_commit+com_collback)/time。但是利用这种方法进行计算的前提是 : 所有事务必须是显式提交的,隐式存在的提交和回滚(默认autocommit=1),不会计算到这两个变量中。

事务的隔离级别

...略...

分布式事务

MySQL数据库的分布式事务

InnoDB提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务是指允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。

XA事务允许不同数据库之前的分布式事务,如一台服务器是MySQL数据库,另一台是Oracle(或者其他的),只要参与在全局事务中的每个节点都支持XA事务。

内部XA事务

之前说的分布式事务是外部事务,即资源管理器是MySQL数据库本身。在MySQL数据库中还存在另外一种分布式事务,其在存储引擎与插件之前,又或者在存储引擎与存储引擎之间,称之为内部XA事务。

最常见的内部XA事务存在于binlog与InnoDB存储引擎之间。
...略...

不好的事务习惯

在循环中提交

提交事务时会写一次重做日志,如果是循环一千次,那么就会写一千次重做日志。当是整个循环结束之后再提交时,那么就只会写一次。

使用自动提交

...略...

使用自动回滚

...略...

长事务

顾名思义就是执行时间长的事务。有的事务执行可能需要非常长的时间。可能1个小时,可能4,5个小时,这取决于数据硬件的配置。DBA和开发人员本身能做的事情非常少。然而,由于ACID的特性,这个操作被封装在一个事务中完成。这就产生了一个问题,在执行过程中,当数据库或操作系统,硬件发生问题时,重新开始事务变得不可接受。数据库需要回滚所有已经发生的变化,而这个过程可能比产生这些变化的时间还长。因此对于长事务的问题,有时可以转化为小批量的事务进行处理。当发生错误时,只需要从发生错误的位置继续执行就行。

标签: none

添加新评论