数据库事务性质及保障方法

数据库事务

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
为什么要有事务呢? 就是为了保证数据的最终一致性。

性质(ACID)

原子性(Atomicity)

事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行,而不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

例如,A向B转账5000元,要么A转账成功,A减少5000,B增加5000;要么转账失败,A不减少,B也不增加。但是不能够A减少了5000,但是B没收到,整个数据库操作就停止了。

一致性(Consistency)

事务的一致性是指事务必须使数据库从一个一致性状态转为为另一个一致性状态,也就是说事务执行前后数据库都得是一致性状态,是正确的不矛盾的

例如,A和B账户一共5000元,无论AB相互怎么转账,转账几次,最后A和B账户共计还得是5000元。

隔离性(Isolation)

事务的隔离性指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间,由并发事务所做的修改必须与任何其他并发事务所做的修改隔离

例如,A向B转账5000元的同时,A也向C也进行转账2000元。最后的一定是A减少了7000元,而仅仅是5000或2000,更不是其他数目。

持久性(Durability)

对于任意已成功事务,系统必须保证该事务对数据库的改变被必须永久保存下来不被丢失,即使数据库出现故障,发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

例如,A已经向B成功转账5000元,数据库哪怕崩溃、重启后,A和B的账户金额必须还是A向B转账成功后的状态,而不能将B的5000元退还给A。

保障方法

原子性:undo log日志
一致性:其他性质、程序代码
隔离性:MVCC多版本并发控制(快照读)&锁(当前读)
持久性:redo log日志 + binlog日志

原子性(Atomicity)保障

原子性是由undo log日志保证的,它记录了需要回滚的日志信息,也就是说我们的事务还没提交需要回滚,那么事务回滚就是根据undo log日志来撤销已经执行成功的SQL。

说白了,undo log其实就是SQL的反向执行,它记录了反向执行的SQL语句,把正向语句回滚回去。

可以这样认为,当delete一条记录时,undo log 中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。

undo log有什么用途呢?

  1. 事务回滚时,保证原子性和一致性。
  2. 用于MVCC快照读。

一致性(Consistency)保障

一致性是ACID的目的。也就是说,只需要保证原子性、隔离性、持久性,自然也就保证了数据的一致性。

比如说,我们的ID在数据库中是唯一的,此时插入了一个唯一ID,数据库会给我们做一个检查,告诉咱们是否发生了主键冲突,如果主键冲突数据就无法插入。

另一部分是业务数据的一致性,这需要程序代码来保证。

比如说转账这个场景,假设我要转账100元出去,实际上数据库中只有90元,那这时候就不应该转账成功,这种情况通过数据库是无法保证的,只能由程序来保证。

隔离性(Isolation)保障

隔离性保障是较为复杂,也是较为重要的一部分,这里首先需要先明确一些基本知识。

没有隔离性产生的问题

数据库多事务并发执行的。否则来一个事务处理一个请求,处理一个人请求的时候,其它事务都等着,用户体验太差。

既然事务可以并发操作,这里就有一些问题:一个事务在写数据的时候,另一个事务要读这行数据,该怎么处理?一个事务在写数据,另一个数据也要写这行数据,又该怎么处理这个冲突?

这就是并发事务所产生的一些问题。具体来说就是:脏读不可重复读幻读

脏读
脏读指的是一个事务处理过程中读到了其他 未提交 的事务中的数据。未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,即不存在的数据。
脏读

A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。

这种情况常发生于转账(存款)与取款操作中:

时间顺序转账(存款)事务取款事务
1开始事务
2开始事务
3查询账户余额为2000元
4取款1000元,余额被更改为1000元
5查询账户余额为1000元(产生脏读)
6取款操作发生未知错误,事务回滚,余额变更为2000元
7 转入2000元,余额被更改为3000元(脏读的1000+2000)
8提交事务
备注按照正确逻辑,此时账户余额应该为4000元

不可重复读
不可重复读是指对于数据库中的某个数据,一个事务范围内多次查询却返回了不同结果
多指数据的修改,数据内容不一致
不可重复读

事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历较长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

时间顺序事务A事务B
1开始事务
2 第一次查询,小明的年龄为20岁
3开始事务
4其他操作
5更改小明的年龄为30岁
6提交事务
7第二次查询,小明的年龄为30岁
备注按照正确逻辑,事务A前后两次读取到的数据应该一致

其实你应该也感觉到了,不可重复读好像不是什么问题,至少不像“脏读”一眼就能看出那是不正确的。甚至比较难以找出不可重复读会造成严重错误的情景。

因为这取决于你自己想要数据库是什么样子的,如果你希望看到的场景就是是不可重复读的,也就是事务 A 在执行期间多次查询一条数据,每次都可以查到其它已经提交的事务修改过的值,那么就是不可重复读,如果你希望这样子,那也没问题。

如果你期望的是可重复读,但是数据库表现的是不可重复读,让你事务 A 执行期间多次查到的值都不一样,都的问题是别的提交过的事务修改过的,那么此时你就可以认为,数据库有问题,这个问题就是「不可重复读」。

幻读
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。(重点在于新增和删除,数据集的对比)

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

时间顺序事务A事务B
1开始事务
2第一次查询,数据总量为100条
3开始事务
4其他操作
5新增100条数据
6提交事务
7第二次查询,数据总量为200条
备注按照正确逻辑,事务A前后两次读取到的数据总量应该一致

同理,你应该也发现了,跟不可重复读一样,幻读是可以分情况确定是否需要避免的。

那么,不可重复读和幻读的区别在哪呢?
不可重复读侧重的是某一个数据的修改,针对数据的。
幻读则侧重的是数据的增删,针对数据集的。

最后,你会发现,这三个问题的重要性或者说需求场景是逐渐降低的。
很明显,几乎所有的数据库应用场景都得解决脏读,其中的一小部分场可能景需要在解决脏读问题后还需要解决不可重复读问题,而在这一小部分场景中可能还有一小部分甚至需要解决幻读的问题。
诶,那么我们是不是可以把这些问题划分等级,脏读最低,不可重复读居中,幻读最高。
既然连问题都划分等级了,那么我们解决问题的策略是不是也可以划分等级,不同的策略应对不同等级的场景需求。
这就引出了“隔离等级”这个概念。

隔离等级

隔离等级分为四种:

  1. Read uncommitted(读未提交):最低级别,任何情况都无法保证。
  2. Read committed(读已提交):可避免脏读的发生。
  3. Repeatable read(可重复读):可避免脏读、不可重复读的发生。
  4. Serializable(串行化):可避免脏读、不可重复读、幻读的发生。

以上四种隔离等级中,串行化级别隔离等级最高,读未提交隔离等级最低。当然,隔离等级越高,执行效率也就越低。
数据库的事务隔离越严格,并发副作用越小,但付出的代价越大;因为事务隔离本质就是使事务在一定程度上处于串行状态,这本身就是和并发相矛盾的。
同时,不同的应用对读一致性和事务隔离级别是不一样的,比如许多应用对数据的一致性没那么个高要求,相反,对并发有一定要求。

读未提交:它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

串行化:串行化就相当于上面所说的,处理一个人请求的时候,别的人都等着。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。

读已提交可重复读:这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。MySQL默认事务隔离级别为可重复读(RR),Oracle默认事务隔离级别为读已提交(RC)。

因为MySQL和Oracle默认的隔离等级不同,我们这里主要根据MySQL讲解隔离性的保障。

隔离性保障

数据库是通过加锁,来实现事务的隔离性的。这就好像,如果你想一个人静静,不被别人打扰,你就可以在房门上加上一把锁。

加锁确实好使,可以保证隔离性。比如串行化隔离级别就是加锁实现的。但是频繁的加锁,导致读数据时,没办法修改,修改数据时,没办法读取,大大降低了数据库性能

那么,如何解决加锁后的性能问题的

答案就是:MVCC多版本并发控制!它实现读取数据不用加锁,可以让读取数据同时修改。修改数据时同时可读取。

什么是 MVCC

MVCC,即Multi-Version Concurrency Control (多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

多版本控制指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
InnoDB是在undo log中实现的,通过undo log可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。
在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

通俗的讲,数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事物隔离级别去判断读取哪个版本的数据。

数据库隔离级别读 已提交、可重复读 都是基于MVCC实现的,相对于加锁简单粗暴的方式,它用更好的方式去处理读写冲突,能有效提高数据库并发性能。

什么是当前读和快照读

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?

  • 当前读
    像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

有关更多锁知识:MySQL锁总结

MVCC能解决什么问题,好处是?

数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

备注:
第一类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了;
第二类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:

  • MVCC + 悲观锁
    MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁
    MVCC解决读写冲突,乐观锁解决写写冲突

这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题

MVCC实现原理

​ MVCC的实现,是基于3个隐式字段、undo log和Read View。

隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  • DB_ROW_ID
    6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

undo log
undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

purge:

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

显然,与保证原子性而使用undo log不同,对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
undo log在MVCC中的应用图1

一、现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁

undo log在MVCC中的应用图2

二、又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 在事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交,释放锁
    undo log在MVCC中的应用图3

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)

Read View(读视图)
什么是Read View?
说白了Read View就是事务进行 快照读操作 的时候产生的一个视图——读视图(Read View)。
它主要是用来做 可见性判断 的,即判断当前事务可见哪个版本的数据,通过Read View我们才知道自己能够读取哪个版本

Read View是如何保证可见性判断的呢?我们先看看Read view 的几个重要属性:

属性名含义
m_ids表示在生成Read View时,当前系统中活跃(未提交)的读写事务的id列表,数据结构为一个List
min_limit_id表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值
max_limit_id表示生成Read View时,系统中应该分配给下一个事务的id值
creator_trx_id表示生成当前Read View的事务的事务id

因为这是读视图(Read View),只是读操作,用到的SQL语句可能是select。creator_trx_id就表示产生当前Read View对应的读事务(select)的事务id。

Read View如何判断版本链中的哪个版本可用呢?
Read View如何判断版本链中的哪个版本可用
trx_id表示要读取的事务id

  • 如果 trx_id == creator_trx_id,说明是我读取我自己创建的记录,那么为什么不可以呢。
  • 如果 trx_id < min_limit_id,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
  • 如果trx_id >= max_limit_id,表明生成该版本的事务在生成Read View后才生成,所以该版本不可以被当前事务访问。
  • 如果 min_limit_id <= trx_id < max_limit_id,需要分3种情况讨论
    • 如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。(第一种情况)
    • 如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,当前事务处于活跃(未提交)的读写事务的id列表,即事务未提交,所以该版本不可以被当前事务访问。
    • 如果m_ids不包含trx_id,则说明你这个事务的事务id不在活跃列表(在Read View生成之前就已经提交commit了),所以该版本可以被当前事务访问。
MVCC如何实现读已提交(RC)和可重复读(RR)的隔离级别
  • 读已提交(RC)的隔离级别下,每个快照读都会生成并获取最新的readview。

  • 可重复读(RR)的隔离级别下,只有在同一个事务第一个快照读才会创建readview,之后的每次快照读都使用的同一个readview,所以每次的查询结果都是一样的。

持久性(Durability)保障

持久性意味着事务操作最终要持久化到数据库中。事务一旦提交,则其所有的修改将会保存到数据库当做。即使此时系统崩溃,修改的数据也不会丢失。

所以通常情况下,我们对数据库做的任何修改,只要事务提交都可以确保数据不会丢失。

在MySQL中完美的支持事务的存储引擎只有InnoDB,所以以下所有内容都是在InnoDB存储引擎下的故不会再做特别声明。

日志系统

如果要解释清楚持久性,就绕不开日志系统。同时MySQL的日志系统非常重要,如果不理解日志系统,后面所有的东西将无法理解

redo log

我们知道数据是存储在磁盘当中的,如果每一次数据修改操作都要写进磁盘,然后磁盘找到对应的那一条记录,然后再去更新。整个过程看下来IO成本、查询成本都非常高。

为了解决这个问题,MySQL采用了一种叫WAL(Write Ahead Logging)提前写日志的技术。意思就是说,发生了数据修改操作先写日志记录下来,等不忙的时候再持久化到磁盘。这里提到的日志就是redo log。

redo log称为重做日志,当有一条记录需要修改的时候,InnoDB引擎会先把这条记录写到redo log里面。redo log是物理格式日志,它记录的是对于每个页的修改。

redo log由两部分组成:

  1. 内存中的重做日志缓冲 (redo log buffer) , 是易失的;
  2. 重做日志文件 (redo log file) , 是持久的。

所以为了消耗不必要的IO操作,事务再执行过程中产生的redo log首先会写入redo log buffer中,之后再统一存入redo log file刷盘进行持久化,这个动作成为fsync

至于什么时候从redo log buffer写入redo log file,可以通过InnoDB提供的innodb_flush_log_at_trx_commit参数来配置。

设置为0的时候,表示事物提交的时候不写入重做日志文件持久化。
设置为1的时候,表示每次事务提交都将redo log直接持久化到磁盘
设置为2的时候,表示每次事务提交时将重做日志写入重做日志文件,但是写入的仅仅是文件系统的缓存page cache不进行fsync。

InnoDB有一个后台线程master thread,每隔一秒就会把redo log buffer中的日志文件调用write写到文件系统缓存page cache,然后调用fsync持久化磁盘。

虽然设置成0或者2可以提升效率,但是也丧失了事务持久性的特性。

如果设置为0,事务提交之后master thread还没有来得及持久化MySQL就宕机了,那么这部分数据将会丢失。
如果设置成为1,MySQL发生宕机并不会导致数据丢失,但是当操作系统宕机时,重启数据库将会丢失文件系统缓存page cache中那部分数据。

redo log file也并不是无限大,而是固定大小的,默认是2个一组在我们MySQL安装路径下面就会找到两个文件“ib_logfile0”和“ib_logfile1”。它是从头开始写,写到末尾后回到头开始循环写,如下图所示。

redo log file

withe pos表示当前位置坐标,一边写一边往后移动。当write pos写到ib_logfile2末尾的时候,就会回到最开始的ib_logfile0文件。

check point表示已经刷新到磁盘上的位置,write pos到check point之间的位置表示安全可写的位置。如果write pos快要追上check point了,那么此时就暂停工作将一部分数据刷回磁盘。

Binlog

从MySQL架构上来看,主要分为Server层存储引擎层。我们上面提到的redo log它是InnoDB引擎特有的日志,而Server层也有自己的日志,成为binlog二进制日志用于归档。

binlog记录了mysql执行所有的所有操作,但不包含select和show这类本对数据本身没有更改的操作。
然后,若操作本身并没有导致数据库发生变化,那么该操作也会写入二进制日志,并不是说对数据本身没有修改就不会记录binlog日志。
例如update t set a = 1 where a = 2,数据库中可能没有a=2的项,这条语句对数据库没有做任何修改,但是通过命令show binlog event也可以看到在二进制日志中做了记录。

所以,是否记录binlog主要是看语句类型,而不是看结果。

binlog日志的作用
恢复(recover):数据恢复
复制(replication):和恢复类似,用做主从复制

为什么有了redo log还要再来一个binlog?

因为binlog是MySQL自带的,而redo log是InnoDB特有的。
最开始的时候MySQL里没有InnoDB引擎,自带的MyISAM又没有crash-safe能力,binlog日志只能用于归档。为了让MySQL具有crash-safe能力所以就引入InnoDB,而InnoDB的crash-safe能力依靠redo log,所以就有了两套日志。

binlog 和 redo log都是记录事务日志,他们有什么区别?
  1. 记录范围不同:
    binlog是MySQL自带的,它会记录 所有 存储引擎的日志文件。
    redo log是InnoDB特有的,它只记录 InnoDB 存储引擎产生的日志文件
  2. 记录形式不同:
    binlog是逻辑日志,记录这个语句具体操作了什么内容,是对应的 SQL语句
    redo log是物理日志,记录的是每个页的更改情况,是 数据库中的值
  3. 写入方式不同:
    binlog采用追加写入,当一个binlog文件写到一定大小后会切换到下一个文件。
    redo log是循环写入,只有那么大的空间。
  4. 写入时间点不同:
    binlog只在事务提交完成后进行一次写入
    redo log在事务进行中不断地被写入(不断地写入redo log buffer,不断地刷新)。
  5. 写入顺序不同:
    binglog仅在事务提交时记录,顺序按照事务提交顺序
    redo log事务的重做日志写入是并发的,并非在事务提交时写入,其在文件中记录的顺序并非是事务提交顺序
  6. 事务对应日志数不同:
    binglog对于每一个事务,仅包含对应事务的一个日志。
    redo log其记录是物理操作日志,因此每个事务对应多个日志条目。
  7. 用途不同:
    binlog可以作为恢复数据使用,主从复制搭建
    redo log作为异常宕机或者介质故障后的数据恢复使用。
日志写入流程

InnoDB存储引擎中一条简单的更新语句就如下:
InnoDB存储引擎中简单更新语句

  1. 首先执行器调用引擎获取数据,如果数据在内存中就直接返回;否则先从磁盘中读取数据,写入内存后再返回。
  2. 修改数据后再调用引擎接口写入这行数据
  3. 引擎层将这行数据更新到内存中,然后将更新操作写入redo log,这时候redo log标记为prepare状态。然后告诉执行器我处理完了,可以提交事务了。
  4. 执行器生成这个操作的binlog,并把binlog写入磁盘,然后调用引擎提交事务
  5. 引擎收到commit命令后,把刚才写入的redo log改成commit状态

至此我们的一条更新语句就算基本完成了,这里面运用了一个方法两阶段提交prepare阶段commit阶段

为什么需要两阶段提交?

之前我们也清楚了,binlog是MySQL中Server层的日志,redo log是InnoDB存储引擎特有的。我们为了一致性就需要把这两个日志很好的持久化下来。而上面的redo log经历prepare和commit两个阶段才算提交。要解释为redo log什么需要两阶段提交,那么我们就用反证法说一说如果没有两阶段提交会发生什么问题吧?

假设一:先写redo log再写binlog(从库未更新)
想象一下,如果数据库系统在写完一个事务的redo log时发生crash,而此时这个事务的binlog还没有持久化。在数据库恢复后,主库会根据redo log中去完成此事务的重做,主库中就有这个事务的数据。
但是,由于此事务并没有产生binlog,即使主库恢复后,关于此事务的数据修改也不会同步到从库(临时库)上,这样就产生了主从不一致的错误。

假设二:先写binlog再写redo log(主库未更新)
想象一下,如果数据库系统在写完一个事务的binlog时发生crash,而此时这个事务的redo log还没有持久化,或者说此事务的redo log还没记录完(至少没有记录commit log)。在数据库恢复后,从库(临时库)会根据主库中记录的binlog去回放此事务的数据修改。
但是,由于此事务并没有产生完整提交的redo log,主库在恢复后会回滚该事务,这样也会产生主从不一致的错误。

两阶段提交

第一阶段: InnoDB prepare阶段
此时SQL已经成功执行(事务并未提交),并生成事务ID(xid)信息及redo和undo的内存日志。此阶段InnoDB会写事务的redo log,但要注意的是,此时redo log 只是记录 了事务的所有操作日志,并 没有记录提交 (commit)日志,因此事务此时的状态为Prepare。
此阶段对binlog不会有任何操作

第二阶段:commit 阶段,这个阶段又分成两个步骤。
第一步): 写binlog:先调用write()将binlog内存日志数据写入文件系统缓存,再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘;
第二步):完成事务的提交(commit),此时在redo log中记录此事务的提交日志(增加commit 标签)。 还要注意的是,在这个过程中是以第二阶段中binlog的写入与否作为事务是否成功提交的标志。第一步写binlog完成 ,第二步redo log的commit未提交未完成,也算完成。

两阶段提交中发送异常重启如何解决?

我们了解到了为什么需要两阶段提交,接下来我们分析一下两阶段提交中发生异常需要怎么处理的情况。

两阶段提交
我们把两阶段提交过程中会发生问题的两个时刻分别标记为时刻A和时刻B,分别来分析这两个时刻发生故障会出现的问题。

时刻A发生故障
这时候发生故障的话,redo log处于prepare阶段,此时redo log还没有提交所以崩溃恢复的时候这个事务就会回滚本次提交。因为binlog还没有写,所以恢复数据的时候也不会执行此次事务

时刻B发生了故障
如果时刻B说明redo log处于prepare阶段,并且binlog已经写完了。但是执行器调用存储引擎提交事务时候,redo log还没来记得修改为commit的时候发生崩溃。那么此时就需要分情况。

重启过后InnoDB引擎发现redo log是prepare阶段,那么就会根据自己的事务ID(xid)去寻找对应的binlog(事务ID(xid)是他们共同的数据字段)。
如果binlog是完整的,这时候极有可能binlog已经被备库同步了,那么此时直接提交事务。
如果binlog不是完整的,那么此时回滚事务。

资料参考

数据库事务
数据库事务的四大特性以及事务的隔离级别
面试官:数据库事务的ACID靠什么来保证?
一文详解脏读、不可重复读、幻读
大白话讲解脏写、脏读、不可重复读和幻读
【数据库】快速理解脏读、不可重复读、幻读
MVCC详解,深入浅出简单易懂
看一遍就理解:MVCC原理详解
MVCC详解
MySQL事务原理—持久性
redo log与binlog的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值