MySQL中支持事务的引擎有:innodb
和bdb
,主要用的引擎为innodb,接下来介绍innodb事务
回忆一下数据库原理的ACID原则
Atomicity 原子性
undo日志
用来保障原子性Consistency 一致性
Isolation 隔离性
Durability 持久性
redo日志
用来保障持久性STATRT TRANSACTION
或BEGIN
READ ONLY
:表示是一个只读事务 ,该事务内数据库操作只能读取数据,不能修改数据READ WRITE
:表示是一个读写事务 ,该事务内数据库操作既可以读也可以写WITH CONSISTENT SNAPSHOT
:启动一致性读COMMIT
或者回滚ROLLBACK
MySQL中有个系统变量autocommit
这种自动提交功能,可以用以下两种方法关闭:
START TRANSACTION
或者 BEGIN
语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能autocommit
的值设置为 OFF
:SET autocommit = OFF;
READ UNCOMMITTED 读未提交
下才会出现,一般很少出现。READ UNCOMMITTED 读未提交
和READ COMMITTED 读已提交
下出现READ UNCOMMITTED 读未提交
和READ COMMITTED 读已提交
和REPEATABLE READ 可重复读
下出现SQL标准定义了以下四种隔离级别
READ UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
隔离级别越低,事务请求的锁越少或者保持锁的时间就越短
InnoDB引擎默认支持的是
REPEATABLE READ(可重复读)
与标准SQL不同,InnoDB使用了
Next-Key Lock
锁算法避免幻读的发生所以,InnoDB引擎在默认
REPEATABLE READ
隔离级别下已经完全能保证事务的隔离性要求了
MySQL日志中比较重要的三个日志分别为:
binlog
:二进制日志,用来保证数据的一致性,数据备份,主备、主主、主从都需要binlogredo log
:重做日志,用来保证事务的持久性undo log
:回滚日志,用来保证事务的原子性binlog(二进制日志)记录了对MySQL数据库 执行更改的所有操作 (但是不包括SELECT和SHOW这类操作,因为这类操作对数据没修过),二进制日志主要有以下几个作用:
max_binlog_size: 指定单个二进制日志文件的最大值,如果超过该值就产生新的二进制日志文件,后缀名+1,并记录到 .index 文件
binlog_cache_size: InnoDB使用事务时,所有未提交
的二进制日志会先被记录到一个缓存中,等待事务提交时
直接将缓存中的二进制日志写入到二进制日志文件中。这个缓冲池大小有该参数控制,默认为32K
sync_binlog = [N]: 表示每写缓存多少次就同步到硬盘,N默认为0
只写入缓存
,由系统自行判断什么时候刷盘到二进制日志文件中
执行刷盘
,将缓存中的日志刷入二进制日志文件中binlog_fomat: 这个参数影响了记录的二进制日志格式,十分重要
now()函数
,获取系统时间,这个级别会导致与原来不一致mysqlbinlog
工具解析出来
redo日志是InnoDB引擎独有的,使MySQL拥有崩溃恢复 的功能,从而保证数据库的持久性和完整性
InnoDB引擎是以页为单位关联存储空间的,增删改查本质上说就是访问页面。在真正访问页面之前,需要把磁盘中的页加载到内存中的Buffer Pool
中才可以访问。
如果事务提交后,刚写完Buffer Pool,服务器就宕机了,这就导致内存中数据丢失,那么这个已提交的事务的修改也跟着丢失了,这是我们不能忍受的。
所以需要一些解决方案
完整的数据页太浪费IO了
。同时一个事务可能有多个语句,一个语句可能修改多个页面。这就意味着Buffer Pool刷新到硬盘需要很多的随机IO
,随机IO较顺序IO慢多了上述方案二便是redo日志的思想
redo日志优点:
设计MySQL的大叔对底层页面进行一层原子访问的过程 称为一个Mini-Transaction (MTR)
一个MTR可以包含一组redo日志,在进行崩溃恢复时,需要把这一组 redo 日志作为一个不可分割的整体 来处理。
一个事务可以包含若干条语向,每一条语句又包含若干个 MTR,每一个 MTR 又可以包含着若干条 redo 日志。
在InnoDB存储引擎中,重做日志都是以512字节存储的,这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block)
,每块大小512字节
真正的redo日志是存在496字节里的log block body
里的
和Buffer Pool同理,引入重做日志缓冲区也是为了解决磁盘速度过慢,写入redo日志时不能直接写到磁盘上,
在服务器启动时就向操作系统申请了一大片redo log buffer的连续内存空间 如图:
我们可以通过innodb_log_buffer_size
来指定log buffer的大小,MySQL 5.7.22 默认为16MB
向log buffer写入是顺序写入的,全局变量buf_free
保存偏移量,告诉mysql该往哪写
前面说了,每个mtr都会产生一组redo日志,这些redo日志是一个不可分割的组
所以并不是每生成一条redo日志就将其插入到log buffer,而是将每个mtr产生的日志暂存到一个地方;当该mtr结束的时候,再将这一组redo日志全部复制到log buffer中
InnoDB为redo log的刷盘策略提供了innodb_flush_log_at_trx_commit
参数,它支持三种策略
0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作
1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作**(默认值)**
2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache(文件系统缓存)
Innodb后台有一个线程,大约以每秒一次的频率将log buffer中的redo日志刷新到磁盘
图示:
设置为0,提交事务的时候不会刷盘,每隔一秒由后台线程进行刷盘
设置为1,提交事务的时候会刷一次盘,同时每隔一秒后台线程也会刷盘
设置为2,提交事务的时候会刷到操作系统文件缓存中,同时每隔一秒后台线程也会刷盘
MySQL数据目录下默认有两个文件:ib_logfile0
和ib_logfile1
,log buffer中的日志在默认情况下会刷到这两个文件中。如果对默认的不满意,可以通过下面几个启动选项调节:
innodb_log_group_home_dir
:指定了redo日志文件的所在目录innodb_log_file_size
:指定每个redo日志文件的大小,MySQL5.7.22 默认48MBinnodb_log_files_in_group
:指定redo日志文件的个数,默认为2,最大为100磁盘上的redo日志文件是以组的方式出现的,在redo日志写入文件组时,从0号开始写,写满了就写1号,往下一直这样写,最后一个写满了就转到0号开始写
redo日志文件大小:innodb_log_file_size × innodb_log_files_in_group
如果 write pos
追上 checkpoint
,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint
推进一下
在崩溃恢复过程中,从redo日志文件组中第一个文件的管理信息中(管理信息存在日志文件前2048个字节中,也就是前4个block)取出最近发生的那次checkpoint信息,然后从checkpoint_lsn在日志文件组中对应的偏移量开始,一直扫描日志文件中的block,直到某个block的LOG_BLOCK_HDR_DATA_LEN值不等于512为止。
在恢复过程中,使用哈希表可以加速恢复过程,并且会跳过已经刷新到磁盘的页面
我们说过,事务需要保证原子性,但是偏偏有时候事务在执行到一半的时候会出现一些问题,比如:
为了保证原子性,这时候就需要回滚来改回原来的样子。设计数据库的大叔把每一条记录的改动都记录一下,比如:
这些为了回滚而记录的东西叫做撤销日志(undo log)
在说事务id时,要回忆一下InnoDB记录行格式,如图:聚簇索引的记录除了保存完整的用户数据之外,还会自动添加名为trx_id
、roll_pointer
的隐藏列
如图,每生成一条undo日志,相关的记录的roll_pointer就会指向这个undo日志,如果该事务内有很多修改同一条记录的undo日志,就将其串成链表,最近的undo日志离记录更近
如图串成版本链,这个将会在MVCC上起到作用
事务可以是只读事务,也可以是读写事务。如果某个事务在执行过程中对表执行了增删改操作,那么InnoDB引擎就会为它分配一个独一无二的事务id
第一次对某个用户创建的临时表执行增删改操作
时才会为这个事务分配一个事务id,否则是不分配事务id的,即默认为0第一次对某个表(包括某个用户创建的临时表)执行增删改查
时才会分配,否则是不分配的,默认为0服务器内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把这个值赋给它并且该变量自增1
当这个变量值为256的倍数时,会进行刷盘,刷到系统表空间页号为5的页面中一个名为Max Trx ID的属性中
系统下次启动的时候,读取这个Max Trx ID属性加载到内存并加上256赋给之前的全局变量,因为上次该全局变量的值可能大于磁盘中Max Trx ID属性的值,所以要再加上256
在InnoDB引擎中,undo log分为
insert 操作的记录只对事务本身可见,对其他事务不可见,故undo log可以在提交事务后直接删除,不需要进行purge操作
insert的回滚日志类型为:TRX_UNDO_INSERT_REC
len
,以及真实值col
update undo log 可能需要提供MVCC机制,因此不能在事务提交的时候就删除,提交时放入undo log链表,等待purge线程进行最后的删除
12 TRX_UNDO_UPD_EXIST_REC
更新non-delete-mark 的记录13 TRX_UNDO_UPD_DEL_REC
将delete 的记录标记为not delete14 TRX_UNDO_DEL_MARK_REC
将记录标记为delete旧纪录的trx_id值
旧纪录的roll_pointer值
,记录这个是为了串成版本链举个例子,这是页面内记录一个时间段的初始状态,现在要执行一条DELETE语句
DELETE语句的操作要进行两个步骤
阶段一: 将要删除的记录的deleted_flag
标识位置为1,其他的不用修改,这个阶段称为:delete mark
,这阶段undo日志类型为:TRX_UNDO_DEL_MARK_REC
阶段二: 当删除语句所在事务真正提交后,将该记录从正常记录链表上移除,放到垃圾链表中,此处PAGE_FREE指的是可重用空间,如果有新的记录插入,如果空间够用,直接覆盖这部分的记录
对于Update语句,InnoDB对更新主键和不更新主键两种情况有截然不同的的处理方法
这种方式类型为:TRX_UNDO_UPD_EXIST_REC
就地更新: 如果更新后的列与更新前的列占用的空间一模一样大 ,就可以就地更新
先删除旧纪录,再插入新纪录: 如果更新后的列与之前的空间有一点不一样,大了或小了都不行,就用这个方法,那么就需要将这条记录移除,即直接加入到垃圾链表中 ,然后新建一条新的记录插入到页面中
在聚簇索引中,记录按照主键值大小串成单向链表,如果我们更新了某条记录的主键值,意味着该记录会改变位置
InnoDB在聚簇索引中分了两步进行处理
delete mark
操作
delete mark
,在事务提交后再由专门的线程进行purge操作表空间其实是许许多多的页面构成的,页面默认大小为16KB,而页面有很多类型,比如类型为FIL_PAGE_INDEX
的页面用于存储聚簇索引以及二级索引,类型为FIL_PAGE_TYPE_FSP_HDR
的页面用来存储表空间头部信息
而FIL_PAGE_UNDO_LOG
类型用来存储undo日志,简称undo日志页
每个undo日志页分为两大类:TRX UNDO INSERT
和TRX UNDO UPDATE
,这两大类分别存insert和update undo log,两者不能混着存
每个页面有两个属性TRX_UNDO_PAGE_START
表示页面从什么地方开始存日志,TRX_UNDO_PAGE_FREE
表示当前页面可以从这个位置开始继续写
一个事务可能包含多个语句,一个语句可能包含多个undo日志,所以一个事务执行过程中可能产生很多undo日志,并且可能一个页面放不下,就要放到多个页面,这些页面串起来就是Undo页面链表
有两个链表对应两种Undo页面类型
同时,Innodb规定普通表和临时表记录的undo日志必须分开:所以一个事务在执行过程最多可以分配4个Undo页面链表
说到段要先说区,区(Extent)是为了更好管理表空间中的页而提出来的概念,对于16KB的页,连续64个页就是一个区,一个区就默认1MB,同时每256个区算一个区组。
我们每向表中插入一条记录,本质上就是向表的聚簇索引以及二级索引代表的B+树中插入数据,而B+树中每个页虽然通过双向链表连接,但是实际上物理位置可能离的很远
,这时候对于传统机械硬盘来说,要重新定位磁头位置
即产生了随机IO
,这就会影响磁盘性能。所以,为了让页面链表相邻的页尽量相邻,引入了区的概念
,一个区就是物理位置上连续的64页,这样就能消除很多的随机IO
。
但事情还没有结束,我们在使用B+树查询时如果不区分叶子节点和非叶子节点,统统放进申请到的区中,扫描效果就大打折扣了。所以,InnoDB设计者对B+树的叶子节点和非叶子节点进行区别的对待
。即叶子节点有独立的区,非叶子结点也有独立的区,存放叶子节点的区的集合就算一个段,非叶子结点页同理。也就是说一个索引会生成两个段:一个叶子节点段
和一个非叶子节点段
设计InnoDB 的大叔规定,每一个Undo页面链表都对应着一个段,称为Undo Log Segment
。也就是说,链表中的页面都是从这个段中申请的,所以他们在Undo页面链表的第一个页面(first undo page)
中设计了一个名为Undo Log Segment Header
的部分。
Undo页面第一个页面比普通页面多了个Undo Log Segment Header,如图为其结构
我们知道,一个事务在执行过程中最多可以分配4个Undo页面链表。在同一时刻,不同事务拥有的Undo页面链表是不一样的,系统在同一时刻其实可以存在许多个Undo页面链表。
为了更好地管理这些链表,设计InnoDB的大叔又设计了一个名为Rollback Segment Header
的页面。这个页面中存放了各个Undo页面链表的first undo page的页号,这些页号称为undo slot
。
如图为Rollback Segment Header页面,InnoDB规定,每一个Rollback Segment Header页面对应一个段,这个段称为回滚段
TRX_RSEG_MAX_SIZE:这个回滚段中管理的所有Undo页面链表中的Undo页面数量之和的最大值。
TRX_RSEG_HISTORY_SIZE: History链表占用的页面数量。
TRX_RSEG_HISTORY: History链表的基节点。
TRX_RSEG_FSEG_HEADER: 这个回滚段对应的10字节大小的Segment Header结构,通过它可以找到本回滚段对应的INODE Entry。
TRX_RSEG_UNDO_SLOTS: 各个Undo页面链表的first undo page的页号集合,也就是undo slot集合。
一个页号占用
4
字节,对于大小为16KB的页面来说,这个TRX_RSEG_UNDO_SLOTS部分共存储了1,024个undo slot
,所以共需1,024×4= 4,096
字节。
服务器崩溃恢复的过程中,首先要按照redo日志
将各个页面恢复到崩溃之前的状态,从而保证持久性
。
而redo日志中那些没有提交的事务可能也以及刷盘,那么这些未提交的事务修改过的页面在服务器重启时有可能被恢复了。
为了保证原子性
,有必要在服务器重启时将这些未提交的事务回滚掉。那么,怎么找到这些未提交的事务呢?这个工作又落到了undo日志头上。
过程:
我们可以通过系统表空间的第5号页面定位到128个回滚段的位置,在每一个回滚段的 1,024个undo slot中找到那些值不为FIL_NULL
的undo slot
,每一个undo slot对应着一个Undo页面链表。
然后从Undo页面链表第一个页面的Undo Segment Header中找到TRX_UNDO_STATE属性
,该属性标识当前Undo页面链表所处的状态。
TRX_UNDO_ACTIVE
,则意味着有一个活跃的事务正在向这个Undo页面链表中写入undo日志。然后再在Undo Segment Header中找到TRX_UNDO_LAST_LOG属性
,通过该属性可以找到本Undo页面链表最后一个Undo Log Header的位置。
从该Undo Log Header中可以找到对应事务的事务id以及一些其他信息,则该事务id对应的事务就是未提交的事务。
通过undo日志中记录的信息将该事务对页面所做的更改全部回滚掉,这样就保证了事务的原子性。
简单来说,就是找undo slot中的undo页面链表,找到之前状态是TRX_UNDO_ACTIVE
的日志,然后找到其对应位置,和事务id及其他信息,将这些更改回滚掉
InnoDB 引擎使用 redo log(重做日志)
保证事务的持久性
使用 undo log(回滚日志)
来保证事务的原子性
数据备份、主备、主主、主从都离不开**binlog
,需要依靠binlog来同步数据,保证数据一致性**。