mysql隔离级别及实现原理

一.事务隔离级别

  • 读未提交(READ UNCOMMITTED)
  • 读提交 (READ COMMITTED)
  • 可重复读 (REPEATABLE READ)
  • 串行化 (SERIALIZABLE)

在这里插入图片描述

二.事务隔离级别效果演示

创建如下一张简单的表来辅助分析几种隔离级别,表结构如下。

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) DEFAULT NULL,
  `age` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

初始只有一条记录:

mysql> SELECT * FROM user;
+----+-----------------+------+
| id | name            | age  |
+----+-----------------+------+
|  1 | 古时的风筝        |    1 |
+----+-----------------+------+
1 row in set (0.00 sec)

2.1 读未提交-脏读

启动两个事务,分别为事务 A 和事务 B,在事务 A 中使用 update 语句,修改 age 的值为 10,初始是 1,在执行完 update 语句之后,在事务 B 中查询 user 表,会看到 age 的值已经是 10 了,这时候事务 A 还没有提交,而此时事务 B 有可能拿着已经修改过的 age =10 去进行其他操作了。在事务 B 进行操作的过程中,很有可能事务 A 由于某些原因,进行了事务回滚操作,那其实事务 B 得到的就是脏数据了,拿着脏数据去进行其他的计算,那结果肯定是有问题的。
在这里插入图片描述

2.2 读已提交-不可重复读

同样开启事务 A 和事务 B 两个事务,在事务 A 中使用 update 语句将 id = 1 的记录的 age 字段改为 10。此时,在事务 B 中使用 select 语句进行查询,我们发现在事务 A 提交之前,事务 B 中查询到的记录 age 一直是 1,直到事务 A 提交,此时在事务 B 中 select 查询,发现 age 的值已经是 10 了。

这就出现了一个问题,在同一事务中(本例中的事务 B ),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务 A 的提交影响了事务 B 的查询结果,这就是不可重复读,也就是读提交隔离级别。
在这里插入图片描述

2.3 可重复读-幻读(可重复读部分解决了幻读问题)

首先看一下可重复读的效果,事务 A 启动后修改了数据,并且在事务 B 之前提交,事务 B 在事务开始和事务 A 提交之后两个时间节点所读取的数据相同,已经可以看出可重复读的效果。
在这里插入图片描述
做到了可重复读,这只是针对已有行的更改操作有效,但是对于新插入的记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:
  事务 A 开始后,执行 update 操作,将 age = 1 的记录的 name 改为“风筝2号”;
  事务 B 开始后,在事务 A 执行完 update 后,执行 insert 操作,插入记录 age = 1,name = 古时的风筝,这和事务 A 修改的那条记录值相同,然后提交。
  事务 B 提交后,事务 A 中执行 select,查询 age = 1 的数据,这时会发现多了一行,并且发现还有一条 name = 古时的风筝,age = 1 的记录,这其实就是事务 B 刚刚插入的,这就是幻读。
在这里插入图片描述
要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别部分解决了幻读问题,这会在后面的内容说明。

2.34串行化

串行化是 4 种事务隔离级别中隔离效果最好的,解决了脏读、不可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。

三.MVCC (多版本并发控制) 、当前读、快照读

3.1MVCC (多版本并发控制),是由undo log实现的

我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
在这里插入图片描述
按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录着使其产生的事务 ID,比如事务 A 的 transaction id 是 100,那么版本 1 的 row trx_id 就是 100,同理版本 2 和版本 3 的 row trx_id 分别为 200 和 300。

3.2当前读、快照读

快照读就是读取数据的时候会根据一定规则读取事务可见版本的数据。 而当前读就是读取最新版本的数据。
对于一个快照来说,它能够读到哪些版本数据,要遵循以下规则:
A.当前事务内的更新,可以读到;
B.版本未提交,不能读到;
C.版本已提交,但是却在快照创建后提交的,不能读到;
D.版本已提交,且是在快照创建前提交的,可以读到。

什么情况下使用的是快照读(快照读,一般不会加锁)?一般的 select * from … where … 语句都是快照读。
什么情况下使用的是当前读(当前读,会在搜索的时候加锁)?一般的 select * from … where … for update; select * from … where … lock in share mode; update … set … where … ; delete from. . where … 语句都是当前读。

四.MySQL 如何实现事务隔离

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

再来说串行化,读的时候加共享锁,也就是其他事务可以并发读,但是不能写;写的时候加排它锁,其他事务不能并发写也不能并发读。

4.1.实现读已提交和可重复读

  • 个人理解:读已提交和可重复读的实现都是通过MVCC+快照读实现的,不同的是读已提交是每次执行语句的时候都要重新创建一次快照,可重复读仅在事务开启的时候创建一次快照。

4.2.并发写问题(行锁)

update … set … where …属于当前读,会在搜索的时候加锁;
假设事务 A 执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务 A 提交之前,事务 B 也想 update 这行数据,于是申请行锁,但是由于已经被事务 A 占有,事务 B 是申请不到的,此时,事务 B 就会一直处于等待状态,直到事务 A 提交,事务 B 才能继续执行,如果事务 A 的时间太长,那么事务 B 很有可能出现超时异常。如下图所示。
在这里插入图片描述
 加锁的过程要分有索引和无索引两种情况,比如下面这条语句 update user set age = 11 where id = 1,id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落地加上行锁就可以了。

而下面这条语句 update user set age = 11 where age = 10,表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据,那怎么办呢?当然也不是加表锁了,MySQL 会为这张表中所有行加行锁,没错,是所有行。但是呢,在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。

4.3. 部分解决幻读(行锁+间隙锁)

4.3.1 无幻读示例

上面介绍可重复读的时候,那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现,MySQL 已经在可重复读隔离级别下部分解决了幻读的问题。

前面刚说了并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,部分解决了并发写和幻读的问题,这个锁叫做 Next-Key 锁。

假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。

mysql> select * from user;
+----+-----------------+------+
| id | name            | age  |
+----+-----------------+------+
|  1 | 古时的风筝        |   10 |
|  2 | 风筝2号          |   30 |
+----+-----------------+------+
2 rows in set (0.00 sec)

此时,在数据库中会为索引维护一套 B+ 树,用来快速定位记录。B+ 索引树是有序的,所以会把这张表的索引分割成几个区间。
在这里插入图片描述
如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。

之后,我用下面的两个事务演示一下加锁过程。
![在这里插入图片描述](https://img-blog.csdnimg.cn/02d738a397ca4231b0ee320fa7d918df.png

在事务 A 提交之前,事务 B 的插入操作只能等待,这就是间隙锁起的作用。当事务 A 执行 update user set name = '风筝2号’ where age = 10; 的时候,由于条件 where age = 10,age 字段是索引列,数据库不仅在 age = 10 的行上添加了行锁,而且在这条记录的两边,也就是 (负无穷,10]、(10,30] 这两个区间加了间隙锁,从而导致事务 B 插入操作无法完成,只能等待事务 A 提交。不仅插入 age = 10 的记录需要等待事务 A 提交,age < 10、10 < age < 30 的记录也无法完成,而大于等于30的记录则不受影响,这种效果看上去解决了幻读问题。

这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。

4.3.1 无幻读示例

在这里插入图片描述
 来分析下情形:

A.T1时刻:读取年龄为 10 的数据, Session1 拿到了 1 条记录。
B.T2时刻:另一个进程 Session2 插入了一条新的记录,年龄也为 10,并提交了事务。
C.T3时刻:Session1 再次读取年龄为 10 的数据,发现还是 1 条数据,貌似 Session2 新插入的数据并未影响到 Session1 的事务读取。
  对于 T1 — T3 时刻的情形,从结果来看,在可重复读隔离级别下似乎解决了幻读的问题。

D.T4时刻:Session1 修改年龄为 10 的数据,发现影响行数为 2 条。 为什么 T3 时候只能查到 1 条数据,但现在修改数据时却修改了 2 条呢?
E.T5时刻:Session1 再次读取年龄为 10 的数据,发现结果变成了 2 条,我们知道新增的第二条记录就是 Session2 在 T2 时刻新增的那条数据,并在 T4 时刻被修改 name 字段。
  T4,T5 的结果来看,Session1 读到了 Session2 新插入的数据,产生了幻读现象。

如果事务中都使用快照读,那么就不会产生幻读现象,但是快照读和当前读混用就会产生幻读。当我们在事务中每次读取都使用当前读,也就是人工把 InnoDb 变成了串行化,一定程度上降低了并发性,但是也同样避免了幻读的情况。可重复读隔离级别下,一个事务中只使用当前读,或者只使用快照读都能避免幻读。

五.总结

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

  • 串行化,读的时候加共享锁,也就是其他事务可以并发读,但是不能写;写的时候加排它锁,其他事务不能并发写也不能并发读。

  • 读已提交是通过MVCC+快照读实现的,每次执行语句的时候都要重新创建一次快照。

  • 可重复读是通过MVCC+快照读实现的,仅在事务开启的时候创建一次快照。

  • 并发更新:行锁解决了并发更新的问题。

  • MySQL 在可重复读级别部分解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。

六.数据库锁

虽然数据库的隔离级别可以解决大多数问题,但是灵活度较差,为此又提出了悲观锁和乐观锁的概念。

如下图,如果开启2个事务A和B,在mysql默认隔离级别可重复读下,如果A和B开启事务的时间几乎相同,步骤1.查询出商品信息为快照读,则查出来的status都等于1,都会执行以下步骤的操作,虽然在步骤3时,会加行锁,等待执行,但是其结果还是事务AB重复下单了。

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_items where id=1;
//2.如果status=1,根据商品信息生成订单
//status=2,则表时商品已被下单,代码逻辑不往下继续
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_items set status=2 where id=1;
//4.提交事务
commit;/commit work;

6.1. 悲观锁

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_items where id=1  for update;;
//2.如果status=1,根据商品信息生成订单
//status=2,则表时商品已被下单,代码逻辑不往下继续
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_items set status=2 where id=1;
//4.提交事务
commit;/commit work;

select status from t_items where id=1 for update;与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_items表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改;

悲观锁:排它锁(for update)和共享锁(lock in share mode) 是悲观锁的实现
排他锁:禁止其他事务并发读写;
共享锁:禁止其他事务并发写,允许并发读;

排他锁和共享锁会导致数据库产生行锁和表锁;

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键或者索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。举例如下:
1、select * from t_items where id=1 for update;
这条语句明确指定主键(id=1),并且有此数据(id=1的数据存在),则采用row lock。只锁定当前这条数据。
2、select * from t_items where id=3 for update;
这条语句明确指定主键,但是却查无此数据,此时不会产生lock(没有元数据,又去lock谁呢?)。
3、select * from t_items where name=‘手机’ for update;
这条语句没有指定数据的主键,那么此时产生table lock,即在当前事务提交前整张数据表的所有字段将无法被查询。
4、select * from t_items where id>0 for update; 或者select * from t_items where id<>1 for update;(注:<>在SQL中表示不等于)
上述两条语句的主键都不明确,也会产生table lock。
5、select * from t_items where status=1 for update;(假设为status字段添加了索引)
这条语句明确指定了索引,并且有此数据,则产生row lock。
6、select * from t_items where status=3 for update;(假设为status字段添加了索引)
这条语句明确指定索引,但是根据索引查无此数据,也就不会产生lock。

6.2. 乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

使用版本号
使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
使用时间戳
乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

七.数据库日志

7.1 undo log

undo log 是也属于引擎层(innodb)的日志,从上面的redo log介绍中我们就已经知道了,redo log 和undo log的核心是为了保证innodb事务机制中的持久性和原子性,事务提交成功由redo log保证数据持久性,而事务可以进行回滚从而保证事务操作原子性则是通过undo log 来保证的。

要对事务数据回滚到历史的数据状态,所以我们也能猜到undo log是保存的是数据的历史版本,通过历史版本让数据在任何时候都可以回滚到某一个事务开始之前的状态。undo log除了进行事务回滚的日志外还有一个作用,就是为数据库提供MVCC多版本数据读的功能。

undo log记录内容
在Mysql里数据每次修改前,都首先会把修改之前的数据作为历史保存一份到undo log里面的,数据里面会记录操作该数据的事务ID,然后我们可以通过事务ID来对数据进行回滚。

比如我们执行 update user_info set name =“李四”where id=1的时候。整个undo log的记录形式会如下。

1、首先准备一张原始原始数据表
在这里插入图片描述

2、开启一个事务A: 对user_info表执行 update user_info set name =“李四”where id=1 会进行如下流程操作

1、首先获得一个事务编号 104

2、把user_info表修改前的数据拷贝到undo log

3、修改user_info表 id=1的数据

4、把修改后的数据事务版本号改成 当前事务版本号,并把DB_ROLL_PTR 地址指向undo log数据地址。

3、最后执行完结果如图:

在这里插入图片描述

7.2 redo log

redo log 是属于引擎层(innodb)的日志,它的设计目标是支持innodb的“事务”的特性,事务ACID特性分别是原子性、一致性、隔离性、持久性, 一致性是事务的最终追求的目标,隔离性、原子性、持久性是达成一致性目标的手段,根据的文章我们已经知道隔离性是通过锁机制来实现的。 而事务的原子性和持久性则是通过redo log 和undo log来保障的。

redo log 能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的一致性,这也就是事务持久性的特征,一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决异常、宕机而可能造成数据错误或丢是redo log的核心职责。

redo log记录的内容
redo log记录的是操作数据变更的日志,听起来好像和binlog有类似的地方,有时候我都会想有了binlog为什么还要redo log,当然从其它地方可以找到很多的理由,但是我认为最核心的一点就是redo log记录的数据变更粒度和binlog的数据变更粒度是不一样的,也正因为这个binlog是没有进行崩溃恢复事务数据的能力的。

以修改数据为例,binlog 是以表为记录主体,在ROW模式下,binlog保存的表的每行变更记录。

比如update tb_user set age =18 where name =‘赵白’ ,如果这条语句修改了三条记录的话,那么binlog记录就是

UPDATE `db_test`.`tb_user` WHERE @1=5 @2='赵白' @3=91 @4='1543571201' SET  @1=5 @2='赵白' @3=18 @4='1543571201'
 UPDATE `db_test`.`tb_user` WHERE @1=6 @2='赵白' @3=91 @4='1543571201' SET  @1=5 @2='赵白' @3=18 @4='1543571201'
 UPDATE `db_test`.`tb_user` WHERE @1=7 @2='赵白' @3=91 @4='1543571201' SET  @1=5 @2='赵白' @3=18 @4='1543571201'

redo log则是记录着磁盘数据的变更日志,以磁盘的最小单位“页”来进行记录。上面的修改语句,在redo log里面记录得可能就是下面的形式。

 把表空间10、页号5、偏移量为10处的值更新为18。
 把表空间11、页号1、偏移量为2处的值更新为18。
 把表空间12、页号2、偏移量为9处的值更新为18。

当我们把数据从内存保存到磁盘的过程中,Mysql是以页为单位进行刷盘的,这里的页并不是磁盘的页,而是Mysql自己的单位,Mysql里的一页数据单位为16K,所以在刷盘的过程中需要把数据刷新到磁盘的多个扇区中去。 而把16K数据刷到磁盘的每个扇区里这个过程是无法保证原子性的,也就意味着Mysql把数据从内存刷到磁盘的过程中,如果数据库宕机,那么就可能会造成一步分数据成功,一部分数据失败的结果。而这个时候通过binlog这种级别的日志是无法恢复的,一个update可能更改了多个磁盘区域的数据,如果根据SQL语句回滚,那么势必会让那些已经刷盘成功的数据造成数据不一致。所以这个时候还是得需要通过redo log这种记录到磁盘数据级别的日志进行数据恢复。

redo log写入策略
redo lo占用的空间是一定的,并不会无线增大(可以通过参数设置),写入的时候是进顺序写的,所以写入的性能比较高。当redo log空间满了之后又会从头开始以循环的方式进行覆盖式的写入。

在写入redo log的时候也有一个redo log buffer,日志什么时候会刷到磁盘是通过innodb_flush_log_at_trx_commit 参数决定。

innodb_flush_log_at_trx_commit=0 ,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;

innodb_flush_log_at_trx_commit=1,表示每次事务提交时都将 redo log 直接持久化到磁盘;

innodb_flush_log_at_trx_commit=2,表示每次事务提交时都只是把 redo log 写到 page cache。

除了上面几种机制外,还有其它两种情况会把redo log buffer中的日志刷到磁盘。

1、定时处理:有线程会定时(每隔 1 秒)把redo log buffer中的数据刷盘。

2、根据空间处理:redo log buffer 占用到了一定程度( innodb_log_buffer_size 设置的值一半)占,这个时候也会把redo log buffer中的数据刷盘。

7.3 binlog

binlog 是作为mysql操作记录归档的日志,这个日志记录了所有对数据库的数据、表结构、索引等等变更的操作。也就是说只要是对数据库有变更的操作都会记录到binlog里面来, 可以把数据库的数据当成我们银行账户里的余额,而binlog就相当于我们银行卡的流水。账户余额只是一个结果,至于这个结果怎么来的,那就必须得看流水了。而同样在mysql里我们就是通过binlog来归档、验证、恢复、同步数据。

binlog 记录内容
binlog应该说是Mysql里最核心的日志, 它记录了除了查询语句(select、show)之外的所有的 DDL 和 DML 语句,也就意味着我们基本上所有对数据库的操作变更都会记录到binlog里面。binlog以事件形式记录,不仅记录了操作的语句,同时还记录了语句所执行的消耗的时间。 binlog 有三种记录格式,分别是ROW、STATEMENT、MIXED。

1、ROW: 基于变更的数据行进行记录,如果一个update语句修改一百行数据,那么这种模式下就会记录100行对应的记录日志。

2、STATEMENT:基于SQL语句级别的记录日志,相对于ROW模式,STATEMENT模式下只会记录这个update 的语句。所以此模式下会非常节省日志空间,也避免着大量的IO操作。

3、MIXED: 混合模式,此模式是ROW模式和STATEMENT模式的混合体,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。

这三种模式需要注意的是:使用 row 格式的 binlog 时,在进行数据同步或恢复的时候不一致的问题更容易被发现,因为它是基于数据行记录的。而使用 mixed 或者 statement 格式的 binlog 时,很多事务操作都是基于SQL逻辑记录,我们都知道一个SQL在不同的时间点执行它们产生的数据变化和影响是不一样的,所以这种情况下,数据同步或恢复的时候就容易出现不一致的情况。

binlog 写入策略
在进行事务的过程中,首先会把binlog 写入到binlog cache中(因为写入到cache中会比较快,一个事务通常会有多个操作,避免每个操作都直接写磁盘导致性能降低),事务最终提交的时候再吧binlog 写入到磁盘中。当然事务在最终commit的时候binlog是否马上写入到磁盘中是由参数 sync_binlog 配置来决定的。

1、sync_binlog=0 的时候,表示每次提交事务binlog不会马上写入到磁盘,而是先写到page cache,相对于磁盘写入来说写page cache要快得多,不过在Mysql 崩溃的时候会有丢失日志的风险。

2、sync_binlog=1 的时候,表示每次提交事务都会执行 fsync 写入到磁盘 ;

3、sync_binlog的值大于1 的时候,表示每次提交事务都 先写到page cach,只有等到积累了N个事务之后才fsync 写入到磁盘,同样在此设置下Mysql 崩溃的时候会有丢失N个事务日志的风险。

很显然三种模式下,sync_binlog=1 是强一致的选择,选择0或者N的情况下在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。

7.4 redo、undo、binlog的生成流程与崩溃恢复

当我们执行update user_info set name =“李四”where id=1 的时候大致流程如下:

1、从磁盘读取到id=1的记录,放到内存。

2、记录undo log 日志。

3、记录redo log (预提交状态)

4、修改内存中的记录。

5、记录binlog

6、提交事务,写入redo log (commit状态)

在这里插入图片描述
我们根据上面的流程来看,如果在上面的某一个阶段数据库崩溃,如何恢复数据。

1、在第一步、第二步、第三步执行时据库崩溃:因为这个时候数据还没有发生任何变化,所以没有任何影响,不需要做任何操作。

2、在第四步修改内存中的记录时数据库崩溃:因为此时事务没有commit,所以这里要进行数据回滚,所以这里会通过undo log进行数据回滚。

3、第五步写入binlog时数据库崩溃:这里和第四步一样的逻辑,此时事务没有commit,所以这里要进行数据回滚,会通过undo log进行数据回滚。

4、执行第六步事务提交时数据库崩溃:如果数据库在这个阶段崩溃,那其实事务还是没有提交成功,但是这里并不能像之前一样对数据进行回滚,因为在提交事务前,binlog可能成功写入磁盘了,所以这里要根据两种情况来做决定。

如果binlog存在事务记录:那么就"认为"事务已经提交了,这里可以根据redo log对数据进行重做。其实你应该有疑问,其实这个阶段发生崩溃了,最终的事务是没提交成功的,这里应该对数据进行回滚。 这里主要的一个考虑是因为binlog已经成功写入了,而binlog写入后,那么依赖于binlog的其它扩展业务(比如:从库已经同步了日志进行数据的变更)数据就已经产生了,如果这里进行数据回滚,那么势必就会造成主从数据的不一致。

另外一种情况就 是binlog不存在事务记录,那么这种情况事务还未提交成功,所以会对数据进行回滚。

7.5 binlog 和 redo log 区别

在这里插入图片描述
binlog 日志也是事务提交前的日志,为啥不直接用 binlog 日志做恢复?
这个问题网上有诸多答案,归纳几点:

从日志格式来看,STATEMENT 格式的日志因为动态函数的原因,肯定不能用作数据恢复;
从恢复效率来看,首先,binlog 日志是逻辑日志,而 redo log 日志是物理日志,恢复更快;其次,binlog 日志是追加写文件,而 redo log 日志是循环写的,在脏页刷新回磁盘后即可被覆盖,因此在做数据恢复时,redo log 记录的内容本身就会比 binlog 更少

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值