事务(Transaction)是数据库区别于文件系统的重要特性之一。
数据库引入事务的主要目的:事务会把数据库从一种抑制状态转换为另一种一致状态。在数据库提交工作时,要么所有修改都已经保存了,要么所有修改都不保存。
InnoDB中的事务完全符合ACID的特性
- A(atomicity),原子性是整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算成功。事务中任何一个SQL语句失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。如果事务中的操作都是只读的,要保持原子性其实是很简单的。一旦发生任何错误,要么重试,要么返回错误代码。因为只读操作不会改变系统中的任何相关部分。但是当事务中的操作需要改变系统中的状态时,例如插入或更新记录,那么情况就不一样了。如果操作失败,很有可能会引起状态变化,因此必须保护系统中并发用户访问受影响的部分数据。
- C(Consistency),一致性。一致性是指事务将数据库从一种状态转换为下一种一致的状态。在事务开始之前和事务结束之后,数据的完整性约束没有被破坏。例如表中有一个字段为姓名,为唯一约束。如果一个事务对姓名字段进行了修改,但是事务在提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的唯一性要求,即事务将数据库从一种状态变为了另一种不一致的状态。因此,事务是一致性的单位,如果是事务中某个动作失败了,系统可以自动撤销事务,返回初始化的状态。(原子性表述的是一个整体,一致性是一个局部,一条数据一致性被破坏,整个事务回滚来体现原子性)
- I(isolation),隔离性。隔离性还有其他的称呼,如并发控制(concurrency control),可串行化(serializability),锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之前的并发度。《mysql锁机制》
- D(durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,事务提交后,所有的变化都是永久的。即使当数据库 因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。但若不是数据库本身发生故障,而是一些外部原因,如RAID卡损坏,自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合完整。
事务的实现
事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的redo log和undo log来完成,redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
Undo log
保证事务的一致性, 回滚而记录的这些东西称之为撤销日志--undo log.
- 1.nnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来
- 2.一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志
- 3.undo日志会被从0开始编号
- 4.undo 日志是放在页面类型为FIL_PAGE_UNDO_LOG的页面中。表空间中的回滚段《InnoDB存储结构》。
undo日志的格式
INSERT操作对应的undo日志
- 1.因为需要回滚时候主要是删除该记录,因此日志主要是记录该记录的主键。
- 2.end of record:本条undo日志结束,吓一跳开始时在页面中的地址
- 3.undo type:undo的类型
- 3.
undo no
:undo的编号:在一个事务中是从0开始递增的,也就是说只要事务没提交,每生成一条undo日志,那么该条日志的undo no就增1 - 4.table id:本条undo日志对应的表
- 5.主键各列的信息:包含列占用的存储空间大小和真实值
- 6.start of record 上一条undo日志结束,本条开始时在页面中的地址。
DELETE,UPDATE操作对应的undo日志
- 前6条与insert undo log 的格式相同
- update_vector 表示 update操作导致发生改变的列数据。
记录更新过程
下面演示下事务对某行记录的更新过程:
1. 初始数据行
F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。
2.事务1更改该行的各字段的值
- 用排他锁锁定该行
- 记录redo log(undo log page也被记录到里面)
- 把该行修改前的值Copy到undo log,即上图中下面的行
- 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
- commit提交,释放排它锁(否则其它事务如果改这记录将阻塞等待提交)
3.事务2修改该行的值
与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。
因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。
MVCC
undo log中的行就是MVCC中的多版本.
通过read view,进行版本可见性控制
- trx_sys 代表明在执行的事务链表
- data_trx_id(try_id)就是事务ID
- up_try_id 是事务队列 最早末提交的事务ID
- low_try_id 是事务队列 最晚末提交的事务ID
事务链表
- MySQL中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中。
- 事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。
- RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)
- RC隔离级别下,在每个语句开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)
何时创建(build view)
RR级别下,事务中的第一个SELECT请求才开始创建read view;
- try_id < up_try_id , 数据在事务开始前已经提交改可见
- up_try_id < try_id < low_try_id ,数据在事务开启前末提交,不可见
- try_id>low_try_id, 数据在事务启动后被提交,不可见(后面的事务提交速度超过当前事务)
RC级别下,事务中每次SELECT请求都会重新创建read view;
- try_id < up_try_id , 数据在事务开始前已经被修改可见
- up_try_id < try_id < low_try_id ,数据在事务开启前末提交,不可见
- try_id>low_try_id, 数据在事务启动后被提交,可见
8.5.3 Optimizing InnoDB Read-Only Transactions 优化InnoDB 只读事务
InnoDB能够避免对一个只读事物设置事物ID(TRX_ID属性)的相关开销。一个事物ID只有在写事物或者是锁读例如SELECT .... FOR UPDATE时才需要。
排除不需要的事物id减少了内部数据结构的大小,该数据结构在每次查询中都用到或者是在数据改变语句中构造一个读试图。
目前, InnoDB 只读事务:
以START TRANSACTION READ ONLY 语句开始
试图对数据库进行修改(对于InnoDB, MyISAM, or other types of tables) 会产生错误
redo
其由两部分组成 : 一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的.
日志条目基本格式
redo_log_type (1字节) | space (压缩后可能<4字节) | page_no | redo_log_body |
- reod_log_type: 占用1字节,表示重做日志类型。各种不同操作有不同的重做日志格式,但有基本的格式。
- space:表空间的ID,采用压缩的方式,占用空间可能小于4字节。
- page_no:页的偏移量,同样采用压缩方式
- redo_log_body:每个重做日志的数据部分,恢复时需要调用相应的函数解析,不同page页有不同的body《InnoDB存储结构》。
日志块
Redo log的存储都是以 块(block) 为单位进行存储的,每个块的大小为512字节。同磁盘扇区大小一致,可以保证块的写入是原子操作。
块由三部分所构成,分别是 日志块头(log block header),日志块尾(log block tailer),日志本身(日志条目集合)。日志头占用12字节,日志尾占用8字节。故每个块实际存储日志的大小为492字节。
日志组
一个日志文件由多个块所构成,多个日志文件形成一个重做日志文件组(redo log group)。不过,log group是一个逻辑上的概念,真实的磁盘上不会这样存储。
重做日志写入过程
重做日志存储的就是之前在log buffer中保存的块,因此也是根据块的方式进行物理存储的管理。block=512bytes。 InnoDB存储引擎运行过程中, log buffer根据一定的规则将log block刷新到磁盘:
- 事务提交时
- 当log buffer中一半的空间已经被使用
- log checkpoint时 (page页被刷新到磁盘的时候)
log block 写入追加到redo log file的最后部分,当一个redo log file写满时,会写入下一个redo log file。 这种方式:round-robin.看起来是顺序的,其实不然,除了保存log buffer刷新到磁盘的log block,还保存了一些其他信息,这些信息占:2KB,即redo log file 的前2KB不保存log block的信息。2KB的信息:保存 4 * 512字节的 块。
名称 | 大小(字节) |
log file header | 512 |
checkpoint1 | 512 |
空 | 512 |
checkpoint2 | 512 |
2KB的信息只在log group的第一个redo log file里存储,其余file留空.
当有一条记录需要更新的时候, InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时, InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是内存中的数据快照一份到磁盘,成功后之前的记录被擦除。write pos和checkpoint之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。有了redo log, InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
恢复
InnoDB存储引擎在启动时,不管上次数据库是否正常关闭,都会尝试进行恢复。重做日志是物理日志,恢复时比较快。
checkpoint 表示已经刷新到磁盘上的LSN。
例子:redo log file 记录的LSN:13000,刷新到磁盘上的LSN:10000,数据库在10000处宕机,恢复时,只需恢复10000~13000的部分。
Binlog
MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog。binlog与redo这两种日志有以下三点不同:
- redo log是InnoDB引擎特有的; binlog是MySQL的Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”; binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
- redo log是循环写的,空间固定会用完; binlog是可以追加写入的。 “追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
想了解更多Binlog请查看《mysql的复制及备份》
2PC
当mysql开启binlog的时候,会存在一个内部XA的问题:事务在存储引擎层(redo)commit的顺序和在binlog中提交的顺序不一致的问题。2PC即innodb对于事务的两阶段提交机制。
第一阶段:InnoDB prepare,持有prepare_commit_mutex,并写入到redo log中。将回滚段(undo)设置为Prepared状态,binlog不做任何操作。
第二阶段:将事务写入Binlog中,将redo log中的对应事务打上commit标记,并释放prepare_commit_mutex。
MySQL以binlog的写入与否作为事务是否成功的标记,innodb引擎的redo commit标记并不是这个事务成功与否的标记。
崩溃时:
扫描最后一个Binlog文件,提取其中所有的xid。
InnoDB维持了状态为Prepare的事务链表,将这些事务的xid与刚刚提取的xid做比较,若存在,则提交prepare的事务,若不存在,回滚。
案例分析
update语句update T set c=c+1 where ID=2;的具体流程:
1. 执行器先找引擎取ID=2这一行。 ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交( commit)状态,更新完成
redo log的写入拆成了两个步骤: prepare和commit,这就是"两阶段提交"。redo log再写入biglog后提交事务,原因是:
1. 先写redo log后写binlog。假设在redo log写完, binlog还没有写完的时候, MySQL进程异常重启。由于我们前面说过的, redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。然后你会发现,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。
2. 先写binlog后写redo log。如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
redo log用于保证crash-safe能力。 innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数我建议你设置成1,这样可以保证MySQL异常重启之后数据不丢失。sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。这个参数我也建议你设置成1,这样可以保证MySQL异常重启之后binlog不丢失。