0x01 定义
是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务要同时满足ACID这四个特性,使用事务可以把数据库从一种一致状态转换为另一种一致状态。
0x02 ACID
A原子性:整个事务是不可分割的单元,任何一个sql语句执行的失败都会导致事务回滚。
C一致性:将数据库从一种一致性状态转变为下一种一致性状态。
I隔离性:事务之间相互分离,在一个事务提交之前对另一个事务屏蔽操作。
D持久性:事务一旦提交就是永久性的。
0x03 分类
扁平事务:操作是原子的要么都提交要么都回滚,最简单也是最常用的。
带有保存点的扁平事务:可以在一个扁平事务中创建保存点,这样可以回滚到该事务中的任意一个保存点的状态。
链事务:在事务提交的时候,将必要的上下文信息隐式的传递给下一个事务,链事务只能回滚到最近的一个状态。
嵌套事务:由若干事务组成的一棵树,叶子节点是扁平事务,非页节点是一个嵌套事务,任何一个事务的回滚都将引起它的子事务一同回滚,子事务的提交要在顶层事务提交后才生效。
分布式事务:在分布式环境下运行的扁平事务。
0x04 mini-transaction
一次逻辑事务可以包含多个物理事务。eg.逻辑事务内一条记录的插入,可能对应一个物理事务(undo log的写入,页面数据的修改)。
虽然Redo Log将数据的操作细分到了页面级别。但是有些在多个页面上的操作是逻辑上不可分裂的。innodb使用mtr来保证对多个页面操作的原子性和一致性。mtr_commit进行的操作就是将日志写入到innodb的日志缓冲区中,等待master_thread落盘。
日志缓冲区的大小通过innodb_log_buffer_size控制。
为了保证物理日志的完整,每次在物理日志持久化到磁盘后,在物理日志的后面加上一些特殊日志。如果这些特殊日志在,则代表物理事务完整,否则不完整,跳过这个物理事务。
struct mtr_struct {
dyn_array_t memo; // 动态数组空间,存储这个事务访问到的所有页面。
ib_uint64_t start_lsn; // mtr开始之前的lsn
ib_uint64_t end_lsn; // mtr提交后产生新的lsn
dyn_array_t log; // 动态数组空间,存储这个mtr修改数据页过程中产生的所有日志(redo log)。
ulint n_log_recs; // mtr产生的日志量,单位记录条数。
ulint log_mode; // 日志模式(MTR_LOG_ALL、MTR_LOG_NONE)
}
0x05 redo log
重做日志通过Force Log at Commit来实现原子性和持久性,分为两部分,一部分是内存中的重做日志缓冲,另一部分是磁盘上的重做日志文件。
Force Log at Commit
innodb_flush_log_at_trx_commit设置为1的时候会保证事务的持久性。
取值 | 描述 |
---|---|
0 | 等待master thread每秒写入重做日志文件并且fsync。 |
1 | 每个事务提交的时候都写入重做日志文件并且调用fsync。 |
2 | 每次事务提交都将重做日志文件写入操作系统文件缓冲中,不调用fsync。 |
和binlog的对比
- binlog用于主从复制,而redo log用于数据安全性。
- binlog是mysql上层生成的,redo log是存储层innodb生成的。
- binlog记录的是sql语句,redo log记录的是对物理页的修改。
- binlog只在事务提交后写入文件一次,redo log在事务进行的过程中不断的写入。
redo log格式
log block
重做日志块大小为512字节,和磁盘扇区大小一致,可保证写入的原子性,不需要double write。
重做日志块分为三部分,日志块头、日志、日志块尾。块头12字节,块尾8字节,一条日志可以跨多个block。
log block格式:
- 4字节:存储log block在log buffer中的位置,第一个bit表示flush bit,所以能最大值是2^31字节。
- 2字节:当前block占用的大小(0 <= x <= 512-4-2-2-4)。
- 2字节:如果这个block中有MTR(mini-transaction),则这两个字节存block中第一个redo日志的偏移量。
- 4字节:存有checkpoint的序号。
log group
逻辑上的概念,重做日志组,有多个重做日志文件。在innodb1.2版本之前重做日志总大小为4G,innodb1.2之后提高到512G。对log block的写入追加到redo log file的最后,但是写入并不是顺序的,因为redo log file前4个块中会存储2个checkpoint的信息,每次写入的时候除了追加redo log,还需要修改这两个block中的内容。
重做日志格式
redo log type | space | page_no | redo log body
redo log type:重做日志类型。
space:表空间的id。
page_no:页的偏移量。
LSN
日志序列号,占8字节,单调递增。表示日志写入的总量(redo log的LSN),checkpoint的位置(Checkpoint的LSN),页的版本(page的LSN)。
Log sequence number:当前的LSN。
Log flushed up to:刷新到重做日志的LSN。
Last checkpoint at:刷新到磁盘的LSN。
Checkpoint
Checkpoint的定义是在某这一时刻将部分日志对应的数据页落地。
Checkpoint是解决以下几个问题:1、缩短数据库的恢复时间;2、缓冲池不够用时,将脏页刷新到磁盘;3、重做日志不可用时,刷新脏页。
如果日志空间不够,要从LSN最小的日志开始,不断让日志失效。也就是让最小LSN的日志所对应的数据页,写入到磁盘中,这样最小LSN的日志就可以被回收重新利用了。
恢复
innodb在起服的时候会去尝试根据redo log恢复数据,首先回去读取redo log中checkpoint的LSN,然后恢复超过这个LSN的redo log。
具体的过程就是以block为单位解析redo log,将block掐头去尾找出MTR。然后将MTR以key<表空间,页>放到hashmap中,这样就保证了MTR对某个页面修改的顺序,而且这样的操作是可重入的。
0x06 undo log
undo log用来实现事务的一致性,如果事务在执行的过程中需要回滚,那就需要undo日志来讲数据库的状态恢复到事务执行之前。undo日志放在共享表空间中的一个segment中,称为回滚段。undo log也可以用来实现mysql中的MVCC。undo log也会生成redo log,因为undo log也需要持久化。
存储管理
innodb1.1之前中有1个rollback segment,每个回滚段记录了1024个undo log segment,innodb1.1开始支持128个回滚段,所以undo段就有128*1024个了。从innodb1.2开始,可以通过参数对回滚段做一些设置。
innodb_undo_directory:回滚段可以不放在共享表空间中,设置回滚段存放的路径,默认值.。
innodb_undo_logs:设置回滚段的个数,默认值128。
innodb_undo_tablespaces:设置构成回滚段的文件数量,文件名前缀为undo。
事务在undo段分配页写undo log的时候也是需要写redo log的。事务提交的时候首先将undo log放入列表中,供之后的purge操作(涉及磁盘的离散读,会很慢),然后判断该页是否可以重用,若可以则分配给其他事务使用(每个事务一个undo页很浪费,所以一般使用率不超过3/4的都会被重用)。
undo log类型
insert undo log:innodb在执行insert操作时产生的undo log,因为只对事务本身可见,对其他事务不可见,所以undo log可以在事务提交后直接删除。
update undo log:innodb在执行update或delete操作时产生的undo log,因为涉及到MVCC,所以在事务提交后可能要保留一段时间,等待master thread在执行purge操作时删除。
undo log查看
这个sql用来查询回滚段所在的页
select * segment_id, space, page_no from INNODB_TRX_ROLLBACK_SEGMENT;
查询结果字段:
segment_id:回滚段id。
insert_undo_list:insert undo list上有多少个元素。
insert_undo_cached:insert undo list cache上有多少个元素。
update_undo_list:update undo list上有多少个元素。
update_undo_cached:update undo list cache上有多少个元素。
这个sql用来查询事务对应的undo log
select * from information_schema.INNODB_TRX_UNDO\G
查询结果字段:
trx_id:事务id。
rseg_id:回滚段id
undo_rec_no:事务中操作序号。
undo_rec_type:事务中的操作类型(TRX_UNDO_INSERT_REC、TRX_UNDO_DEL_MARK_REC、TRX_UNDO_UPD_EXIST_REC、TRX_UNDO_UPD_DEL_REC)。
size:undo log的大小。
space:表空间。
page_no:page序号。
offset:undo log的开始在页中的偏移量。
delete操作不直接删除字段而是标记为删除,等待purge操作真正删除记录。update操作分为两步,第一步将原主键删除,生成一条类型为TRX_UNDO_DEL_MARK_REC的undo log,然后生成一个类型为TRX_UNDO_INSERT_REC的undo log。
0x07 purge
purge用于完成最终的delete和update操作。innodb存在着一个history列表,逆序排列,purge的过程首先从尾部找到第一个undo页,清理所有可以清理的记录,然后第二个页,如此往复。这样做的优点是可以避免大量的随机读,提高purge效率。
innodb_purge_batch_size用来设置每次purge需要清理的undo page数量,在innodb1.2之前默认值为20,从1.2开始该参数的默认值是300。这个参数设置的越大purge的吞吐量越高,减少了磁盘的分配和占用,但是可能导致磁盘IO和CPU过于集中处理purge,使性能下降。
innodb引擎压力很大的时候,purge不能高效的进行,导致history list越来越长,innodb_max_purge_lag用来控制history list的长度,方法是history list长度大于该参数的时候会延缓DML操作,默认值为0(不延缓)。innodb1.2开始引入innodb_max_purge_lag_delay来控制最大延时的毫秒数。
0x08 group commit
为了减少fsync操作的次数,提高效率,当前主流的数据库都提供了group commit,一次fsync可以写入多个事务。对于开启了binlog的innodb来讲,事务提交时启用了XA事务两阶段提交,过程如下:
1、innodb引擎prepare操作。
2、mysql上层写入binlog。
3、innodb将日志写入redo log缓冲。
4、调用fsync,将redo log缓冲写入磁盘。
为了保证binlog和innodb引擎中事务提交的顺序一致(为了备份和恢复),mysql数据库内部使用了prepare_commit_mutex锁,将group commit硬生生的变为单条提交。mysql5.6中开启了BLGC(Binary Log Group Commit),首先mysql上层将事务提交放入一个队列(队列中第一个事务是leader,其他的是flower,leader控制flower的行为),BLGC分为三个阶段。
Flush:将每个事务的binlog写入缓存。
Sync:将内存中的binlog刷新到磁盘,若队列中有多个事务,一次fsync就完成了binlog的磁盘写入。
Commit:leader根据顺序调用存储引擎层事务的提交,innodb本身就支持group commit。
0x09 事务控制语句
START TRANSACTION/BEGIN:开启一个事务。
COMMIT:提交一个事务。
ROLLBACK:回滚一个事务。
SAVEPOINT xxx:创建一个保存点。
RELEASE SAVEPOINT xxx:删除指定保存点(未存在该保存点抛异常)。
ROLLBACK TO xxx:回滚到指定保存点的状态。
SET TRANSACTION:设置事务的隔离级别。
completion_type控制提交后的操作,默认值为0,表示无操作。设置为1的时候COMMIT WORK等同于COMMIT AND CHAIN表示开启一个相同隔离级别的事务。设置为2的时候COMMIT WORK等同于COMMIT AND RELEASE,在事务提交后自动断开连接。
ROLLBACK TO xxx并不是真正结束一个事务,需要显示的运行COMMIT或ROLLBACK命令。
mysql中很多sql语句会产生一个隐式的提交操作。
0x0A 事务的统计
show global status like 'com_commit'\G
show global status like 'com_rollback'\G
tps=(com_commit+com_rollback)/time,这种计算tps的前提是所有的事务都必须是显示的提交。
0x0B 隔离级别
ANIS SQL提供了四种隔离级别,READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。innodb默认的隔离级别是REPEATABLE READ(达到SQL标准的SERIALIZABLE)。隔离级别越低,锁保持的时间约短。SERIALIZABLE会对每个SELECT语句后面加上LOCK IN SHARE MODE。在mysql5.1中READ COMMITTED隔离级别只能使用ROW格式的binlog,使用Statement的binlog会报错。