00 – Undo Log
Undo Log 是为了实现事务的原子性,在MySQL数据库InnoDB存储引擎中,还用Undo Log来实现多版本并发控制(简称:MVCC)。
- 事务的原子性(Atomicity)
事务中的所有 *** 作,要么全部完成,要么不做任何 *** 作,不能只做部分 *** 作。如果在执行的过程中发生了错误,要回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过。
- 原理
Undo Log的原理很简单,为了满足事务的原子性,在 *** 作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。除了可以保证事务的原子性,Undo Log也可以用来辅助完成事务的持久化。
- 事务的持久性(Durability)
事务一旦完成,该事务对数据库所做的所有修改都会持久的保存到数据库中。不能因为错误/重启/宕机而丢失已经COMMIT的数据。为了保证持久性,数据库系统需要将修改后的数据完全的记录到持久的存储上。
- 用Undo Log实现原子性和持久化的事务的简化过程
假设有A、B两个数据,值分别为1,2。
A.事务开始.
B.记录A=1到undo log的内存buffer.
C.在内存中修改A=3.
D.记录B=2到undo log的内存buffer.
E.在内存中修改B=4.
F.将undo log的buffer写到磁盘。
G.将内存中修改后的数据写到磁盘。
H.事务提交
这里有一个前提条件:‘数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘’。以上过程之所以能同时保证原子性和持久化,是因为以下特点:
A. 更新数据前记录Undo log。
B. 为了保证持久性,必须将数据在事务提交前写到磁盘。只要事务成功提交,数据必然已经持久化。
C. Undo log必须先于数据持久化到磁盘。如果在G,H之间系统崩溃,undo log是完整的,可以用来回滚事务。
D. 如果在A-F之间系统崩溃,因为数据没有持久化到磁盘。所以磁盘上的数据还是保持在事务开始前的状态。
缺陷:每个事务提交前将数据和Undo Log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。如果能够将数据缓存一段时间,就能减少IO提高性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即Redo Log.
01 – Redo Log
- 原理
和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交时,只要将Redo Log持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是Redo Log已经持久化。系统可以根据Redo Log的内容,将所有数据恢复到最新的状态。
- Undo + Redo事务的简化过程
假设有A、B两个数据,值分别为1,2.
A.事务开始.
B.记录A=1到undo log的内存buffer.
C.内存中修改A=3.
D.记录A=3到redo log的内存buffer.
E.记录B=2到undo log的内存buffer.
F..内存中修改B=4.
G.记录B=4到redo log的内存buffer.
H.将redo log的内存buffer写入磁盘。
I.事务提交
- Undo + Redo事务的特点
A. 为了保证持久性,必须在事务提交时将Redo Log持久化。
B. 数据不需要在事务提交前写入磁盘,而是缓存在内存中。
C. Redo Log 保证事务的持久性。
D. Undo Log 保证事务的原子性。
E. 有一个隐含的特点,数据必须要晚于redo log写入持久存储。这是因为Recovery要依赖redo log. 如果redo log丢失了,系统需要保持事务的数据也没有被更新。
- IO性能
Undo + Redo的设计主要考虑的是提升IO性能。虽说通过缓存数据,减少了写数据的IO. 但是却引入了新的IO,即写Redo Log的IO。如果Redo Log的IO性能不好,就不能起到提高性能的目的。为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:
A. 尽量保持Redo Log存储在一段连续的空间上。以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。因此在系统第一次启动时就会将日志文件的空间完全分配,从而保证Redo Log文件在存储上的空间有更好的连续性。
B. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时 (如事务提交),才将许多日志一起写入磁盘,这样可以减少IO次数。
C. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,以减少Redo Log的IO次数。例如,Redo Log中的记录内容可能是这样的:
记录1: <trx1, insert …>
记录2: <trx2, update …>
记录3: <trx1, delete …>
记录4: <trx3, update …>
记录5: <trx2, insert …>
D. 因为C的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
E. Redo Log上只进行顺序追加的 *** 作,当一个事务需要回滚时,它的Redo Log记录也不会从Redo Log中删除掉。InnoDB的做法时将回滚 *** 作也记入Redo Log(具体做法看下一节).
今天我们来看看多个事务对缓存页里的同一条数据同时进行更新或者查询,此时会产生哪些问题?这里实际会涉及到 脏写、脏读、不可重复读、幻读, 四中问题。
这个脏写的话,它的意思是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把它更新为A值,事务B紧接着就把它更新为B值。事务A是先更新的,它在更新之前,这行数据的值为NULL,所以此时事务A的undo log日志大概是这样的:更新之前这行数据的值为NULL,主键为XX 。
那么此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么就会用它的undo log日志去回滚。此时事务A一回滚,直接就会把那行数据的值更新回之前的NULL值。所以对于事务B看到的场景,就是自己明明更新了,结果值却没了,这就是 脏写。
假设事务A更新了一行数据的值为A,此时事务B去查询了一些这行数据的值,看到的值是A,然后事务B拿着刚查询到的A值去处理各种业务。但是此时不幸的事情发生了,事务A突然回滚了,导致它刚才更新的A值没了,此时那行数据的值回滚为NULL值。这就是所谓的 脏读。 它的本质是事务B去查询了事务A修改过的数据,但是此时事务A还没有提交,事务A随时会回滚导致事务B查询了一个不存在的值。
接着我们来看一下 的问题,假设我们有一个事务A开启了,在这个事务A里会多次对一条数据进行查询。然后另外有两个事务,一个是事务B,一个是事务C,它们都是对一条数据进行更新的。假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的是A值。接着事务B更新了那行数据的值为B,同时提交事务,然后事务A第二次查询该行数据,此时查到的是事务B修改过的值B 。接着事务C更新了那行数据的值为C,同时提交事务,然后事务A第三次查询该行数据,此时查到的是事务C修改过的值C值。
那么上面的场景有什么问题呢?其实要说没问题也是可以的,毕竟事务B和C都提交事务了。但是要说有问题也是可以的,就是事务A可能第一次查询到的是A值,那么它可能希望的是在事务执行期间,如果多次查询数据,都是同样的一个A值。但是该场景下,A值明显不是可重复读的。
这种情况算不算一个问题呢?其实这是根据你的业务决定的。有的业务要的是可重复读,而有的业务却需要不可重复读。
假设一个事务A先发送一条SQL语句,里面有一个条件,要查询一批数据出来,比如“select * from table where id >10”,类似这种SQL,它一开始查询出了10条数据。然后事务B往表里插入了几条数据,而且事务B还提交了。此时事务A再次查询,由于事务B插入了几条数据,导致这次它查询出来了12条数据。同样的SQL语句,两次的查询结果却不一样,所以开始怀疑自己是不是出现了幻觉?导致刚才幻读了?这就是幻读一词的由来。
在SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,互相是如何隔离的,从而避免一些事务并发问题。这4种级别包括了: read uncommitted(读未提交)、read committed(读已提交)、repeatable read(可重复读)、serializable(串行化) 。
第一个read uncommitted隔离级别是不允许发生脏写的。也就是说,不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读、不可重复读、幻读。所以一般来说,是没有人做系统开发的时候把事务隔离级别设置为读未提交这个级别的。
第二个是read committed隔离级别,也就是俗称的RC级别,这个级别不会发生脏写和脏读。也就是说,别的事务没提交的情况下修改的值,你是绝对读不到的。但是,可能会发生不可重复读和幻读问题。
第三个是repeatable read隔离级别,也就是俗称的RR级别,就是可重复读级别。这个级别下,不会发生脏写、脏读、不可重复读的问题。事务一旦开启,多次查询一个值,会一直读到同一个值。但是它会发生幻读的问题。
最后一个隔离级别,就是serializable级别,这种级别,根本不允许多个事务并发执行,只能串行执行,所以不可能有幻读问题。但是这种级别一般除非脑子坏了,否则不可能设置这种级别。
MySQL默认设置的事务隔离级别都是RR级别的,而且MySQL的RR级别是可以避免幻读发生的。
下面的命令可以修改MySQL的默认事务隔离级别:
另外,给大家一个彩蛋,假设你在开发业务系统的时候,比如用spring里的@Transaction注解来做事务这块,假设某个事务你就是有点手痒,想搞成RC级别,那么没问题,在@Transaction注解里是有一个isolation参数的,里面是可以设置事务隔离级别的,具体的设置方式如下:
@Transaction(isolation=Isolation.DEFAULT),默认的就是DEFAULT值,这个就是MySQL默认支持什么隔离就是什么隔离级别。但是你可以手动改成其它的隔离级别,比如,isolation = Isolation.READ_COMMITTED级别,此时你就可以读取到其它事务已提交的数据。
简单来说,我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向了你更新这个事务之前生成的undo log,关于undo log之前都讲过了。
举个例子,假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示:
插入的这条数据的值是A,因为事务A的id是50,所以这条数据的trx_id就是50,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。接着有一个事务B修改了一下这条数据,把值改成了B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示:
undolog日志:回滚日志,用于处理回滚 *** 作,也就是做在数据变化之前,就要记录,方便因事务,进行的数据回滚。
redolog:重写日志,用来记录事务 *** 作的变化,记录的是数据修改之后的值,不管事务提交是否成功。可以提升数据落盘的速度,易于系统奔溃恢复。
总结:先undolog 后 redolog,下面是数据库数据加载变化的过程示意图。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)