前言
《软件架构设计》通俗的讲,事务就是一个“代码块”,这个代码块要么不执行,要么全部执行。事务要操作数据(数据库里面的表),事务与事务之间会存在并发冲突,就好比在多线程编程中,多个线程操作同一份儿数据,存在线程间的并发冲突是一个道理。
一个线上SQL死锁异常分析:深入了解事务和锁我们在业务实现时,经常需要保证某一批SQL能够具备ACID特性,如果没有事务,在应用里自己保证将会变得非常复杂,InnoDB引擎引入事务机制,极大简化了我们在此方面的编程模型。PS:事务支持是在引擎层实现的。
ACID的实现机制是什么?
- 原子性(Atomicity):事务内SQL要么同时成功要么同时失败 ,基于UndoLog实现。
- 一致性(Consistency):系统从一个正确态转移到另一个正确态,由应用通过AID来保证,并非数据库的责任。
- 隔离性(Isolation):控制事务并发执行时数据的可见性,基于锁和MVCC实现。
- 持久性(Durability):提交后一定存储成功不会丢失,基于RedoLog实现。PS: 数据持久性(Data Durability)意味着数据一旦被成功存储就可以一直继续使用,即使系统中的节点下线、宕机或数据损坏也是如此。不同的分布式数据库拥有不同级别的持久性。有些系统支持机器 / 节点级别的持久性,有些做到了集群级别,而有些系统压根没有持久性。
事务的最终目的是实现一致性,即确保事务正确地将数据从一个一致性的状态,变换到另一个一致性的状态。为了达成这个目标,除了需要应用层的逻辑保证外,在事务层面还需要通过原子性、隔离性和持久性这三个特性一起协作。
几个特性的实现原理
为了实现原子性,需要通过日志:将所有对数据的更新操作都写入日志,如果一个事务中的一部分操作已经操作,但以后的操作由于断电等原因无法继续,则通过回溯日志,将已经执行成功的操作撤销,从而达到全部操作失败的目的。(原子性要求的“要么全成功,要么全失败”,在实现上其实就是提供“一部分失败则撤销已经成功的操作”的能力)
最常见的场景是,数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash recovery的过程:读取日志进行REDO(重新执行所有已经执行成功,但尚未写入到磁盘的操作,保证持久性),再对所有崩溃时尚未成功提交的事务进行进行undo(撤销所有执行一部分但尚未提交的操作,保证原子性)。crash recovery结束后,数据库恢复到一致性状态,可以继续被使用。(原来REDO和UNDO是以crash recovery的视角来命名的)
Undo日志记录某数据被修改前的值,可以用来在事务失败时进行rollback;Redo日志记录某数据块被修改后的值,可以用来恢复未写入data file的已成功事务更新的数据。例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Undo日志为<T1, X, 5>
,Redo日志为<T1, X, 15>
。
日志的管理和重演是数据库实现中最复杂的部分之一,如果涉及到并行处理和分布式系统(日志的复制和重演是数据库高可用性的基础),会比上述场景还要复杂的多。
在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。
所谓一致性,指的是数据处于一种有意义的状态,这种状态是语义上的,而不是语法上的,比如常见的转账的例子。
从转账的例子可以看到,一致性的前提是原子性,但原子性并不能完全保证一致性。在多个事务并行进行的情况下,即使保证了每一个事务的原子性,仍然可能导致数据不一致的结果。例如,事务1将100元转给A,先读取账号A的值,然后在这个值上加上100.但是在这两个操作之间,另一个事务2修改了账号A的值,为它增加了100元,那么最后的结果应该是A增加了200元。但事实上,事务1最终完成后,账号A只增加了100元,因为事务2的修改结果被事务1覆盖掉了。说白了,还是并发读写问题
为了保证并发情况下的一致性,引入了隔离性,TiKV 的 MVCC 机制事务隔离在数据库系统中有着非常重要的作用,因为对于用户来说数据库必须提供这样一个“假象”:当前只有这么一个用户连接到了数据库中,这样可以减轻应用层的开发难度。但是,对于数据库系统来说,因为同一时间可能会存在很多用户连接,那么许多并发问题,比如数据竞争(data race),就必须解决。在这样的背景下,数据库管理系统(简称 DBMS)就必须保证并发操作产生的结果是安全的,通过可串行化(serializability)来保证。注意此处说的是 可串行化 不是 串行化,即不要求形式上串行执行,只要求结果上多个事务并发执行后的状态和它们串行执行后的状态是等价的。MVCC In TiKV可串行性是并发事务正确性的准则。按这个准则规定,一个给定的并发调度,当且仅当它是可串行化的,才认为是正确调度。
事务的原子性和持久性——redo/undo log
宕机恢复后(redo log undo log 貌似都是从宕机恢复的视角来说的)
- 针对已经提交的数据还未写入到磁盘:InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。并不关心事务性,提交的事务和未提交的事务都被重放了,从而让数据库”原封不动“的回到宕机前的状态。
- 针对还未提交的数据已经写入到磁盘:重放完成后,再把未完成的事务找出来,逐一利用undo log进行逻辑上的“回滚”。 undo log 记录了sql 的反操作,所谓回滚即 执行反操作sql
redo log 不保证事务原子性, 只是保证了持久性, 不管提交未提交的事务都会进入redo log。
redo log和undo log所做的一切都是为了提高 数据本身的IO效率,已提交事务和未提交事务的数据 可以随意立即/延迟写入磁盘。代价是,事务提交时,redo log必须写入到磁盘,数据随机写转换为日志数据顺序写。PS,随机写优化为顺序写,也是一种重要的架构优化方法。
redolog
- 同步写改为异步写:数据写磁盘一般是随机的,单次较慢,也不允许频繁写入。数据写入一般先保存在内存中,然后定期将内存数据写入到磁盘
- 用Write-Ahead log/redo log 解决异步写在宕机场景下的数据丢失问题
庖丁解InnoDB之REDO LOG 写的非常好,细节没有copy过来,看redo log 就这一篇
- 为什么需要redo log? 为了取得更好的读写性能,InnoDB会将数据缓存在内存中(InnoDB Buffer Pool),对磁盘数据的修改也会落后于内存,这时如果进程或机器崩溃,会导致内存数据丢失,为了保证数据库本身的一致性和持久性,InnoDB维护了REDO LOG。修改Page之前需要先将修改的内容记录到REDO中,并保证REDO LOG早于对应的Page落盘,也就是常说的WAL,Write Ahead Log。当故障发生导致内存数据丢失后,InnoDB会在重启时,通过重放REDO,将Page恢复到崩溃前的状态。
- 需要什么样的REDO?
- 首先,REDO的维护增加了一份写盘数据,同时为了保证数据正确,事务只有在他的REDO全部落盘才能返回用户成功,REDO的写盘时间会直接影响系统吞吐,显而易见,REDO的数据量要尽量少。
- 其次,系统崩溃总是发生在始料未及的时候,当重启重放REDO时,系统并不知道哪些REDO对应的Page已经落盘,因此REDO的重放必须可重入,即REDO操作要保证幂等。
- 最后,为了便于通过并发重放的方式加快重启恢复速度,REDO应该是基于Page的,即一个REDO只涉及一个Page的修改。
从逻辑上来说,日志就是一个无限延长的字节流,从数据库启动开始,日志便源源不断的追加,直到结束。但从物理上来看,日志不可能是一个永不结束的字节流, 磁盘是块设备,磁盘的读取和写入都不是按照一个个字节来处理的,日志文件不可能无限膨胀,过了一定时间,之前的历史日志就不需要了。
在支付业务中,有一个用户账户表,还会有一个用户账户临时表,更新用户账户的金额数据时,经常先在临时表中先插入一条日志,因为只有插入操作,自然没有并发问题,然后再去更新用户账户。此时,临时表的作用就类似于redo日志。
undo log
庖丁解InnoDB之Undo LOG写的非常好,细节没有copy过来
- 在设计数据库时,我们假设数据库可能在任何时刻,由于如硬件故障,软件Bug,运维操作等原因突然崩溃。这个时候尚未完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对Atomic的保证,也就是任何事务的修改要么全部提交,要么全部取消。针对这个问题,直观的想法是等到事务真正提交时,才能允许这个事务的任何修改落盘,也就是No-Steal策略。显而易见,这种做法一方面造成很大的内存空间压力,另一方面提交时的大量随机IO会极大的影响性能。因此,数据库实现中通常会在正常事务进行中,就不断的连续写入Undo Log,来记录本次修改之前的历史值。当Crash真正发生时,可以在Recovery过程中通过回放Undo Log将未提交事务的修改抹掉。InnoDB采用的就是这种方式。
- 既然已经有了在Crash Recovery时支持事务回滚的Undo Log,自然地,在正常运行过程中,死锁处理或用户请求的事务回滚也可以利用这部分数据来完成。
- 为了避免只读事务与写事务之间的冲突,避免写操作等待读操作,几乎所有的主流数据库都采用了多版本并发控制(MVCC)的方式,也就是为每条记录保存多份历史数据供读事务访问,新的写入只需要添加新的版本即可,无需等待。InnoDB在这里复用了Undo Log中已经记录的历史版本数据来满足MVCC的需求。
Undo Log的设计思路不同于Redo Log,Undo Log需要的是事务之间的并发,以及方便的多版本数据维护,其重放逻辑不希望因DB的物理存储变化而变化。因此,InnoDB中的Undo Log采用了基于事务的Logical Logging的方式。
undo log 亦log亦数据,每个事务在修改记录之前,都会先把该记录拷贝出来一份,存在undo log里,也就是copyOnWrite。也正因为每条记录都有多个版本,才很容易实现隔离性。事务提交后,没用其它事务引用的“历史版本/undo log”就可以删除了。PS:跟cpu 缓存导致一条内存数据多个cpu 副本异曲同工
InnoDB将Undo Log看作数据,因此记录Undo Log的操作也会记录到redo log中,包含Undo Log操作的Redo Log,看起来是这样的:
记录1: <trx1, Undo log insert <undo_insert …>>
记录2: <trx1, insert …>
记录3: <trx2, Undo log insert <undo_update …>>
记录4: <trx2, update …>
记录5: <trx3, Undo log insert <undo_delete …>>
记录6: <trx3, delete …>
更多的责任意味着更复杂的管理逻辑,InnoDB中其实是把Undo当做一种数据来维护和使用的,也就是说,Undo Log日志本身也像其他的数据库数据一样,会写自己对应的Redo Log,通过Redo Log来保证自己的原子性。因此,更合适的称呼应该是Undo Data。
一致性
理解事务 - MySQL 事务处理机制在事务T开始时,此时数据库有一种状态,这个状态是所有的MySQL对象处于一致的状态,例如数据库完整性约束正确,日志状态一致等,当事务T提交后,这时数据库又有了一个新的状态,不同的数据,不同的索引,不同的日志等,但此时,约束,数据,索引,日志(binlog/redo/undo log)等MySQL各种对象还是要保持一致性(正确性)。 这就是 从一个一致性的状态,变到另一个一致性的状态。也就是事务执行后,并没有破坏数据库的完整性约束。有分布式一致性,其实一致性问题分布式和单机都有。
条分缕析分布式:到底什么是一致性?ACID中的一致性,是个很偏应用层的概念。原子性、隔离性和持久性,都是数据库本身所提供的技术特性;而一致性,则是由特定的业务场景规定的。要真正做到ACID中的一致性,它是要依赖数据库的原子性和隔离性的(应对错误和并发)。但是,就算数据库提供了所有你所需要的技术特性,也不一定能保证ACID的一致性。这还取决于你在应用层对于事务本身的实现逻辑是否正确无误。ACID中的一致性,甚至跟分布式都没什么直接关系。它跟分布式的唯一关联在于,在分布式环境下,它所依赖的数据库原子性和隔离性更难实现。
日志落盘
应用层所说的事务都是”逻辑事务“,以上图为例,在逻辑层面事务是三条sql语句,涉及两张表。在物理层面,可能是修改了两个Page,修改每个page 产生一部分日志,生成一个LSN,存储到Redo log 的Block 里。不同事务的日志在 redo log 中是交叉存在的。
redo log buffer 是一块内存,用来暂存 redo 日志,事务commit时真正把日志写到 redo log 文件(文件名是 ib_logfile+ 数字)
MySQL checkpoint深入分析MySQL · 引擎特性 · InnoDB redo log漫游
为了防止数据丢失,采用WAL,事务(具体应该是数据增删改操作)提交时,先写重做日志,再修改页。LSN(log sequence number) 用于记录日志序号,它是一个不断递增的 unsigned long类型整数。因为写redo log是第一个要做的事儿,因此可以用lsn来做一些标记。在 InnoDB 的日志系统中,LSN 无处不在,它既用于表示修改脏页时的日志序号,也用于记录checkpoint,通过LSN,可以具体的定位到其在redo log文件中的位置。
为了管理脏页,在 Buffer Pool 的每个instance上都维持了一个flush list,flush list 上的 page 按照修改这些 page 的LSN号进行排序。猜测:脏页刷新到磁盘时,应该也是按lsn顺序来的,不会存在较大lsn已经刷盘,而较小lsn未刷盘的情况。
编号 | lsn的某个状态值 | 说明 | 本阶段的lsn redo log所在位置 | 本阶段的lsn对应页的内存和硬盘一致性状态 | 备注 |
---|---|---|---|---|---|
1 | Log sequence number | 最新日志号 | |||
2 | Log flushed up to | 日志刷盘量 | 2~1:内存 | 2~1:不一致 | |
3 | Pages flushed up to | 脏页刷盘量 | 3~2:硬盘 | 3~2:不一致 | 没找到地方显式存在 |
4 | Last checkpoint at | 上一次检查点的位置 | 4~3:硬盘 | 4~3:一致,此时5~3对应的redo日志已失效,可以被覆盖 | |
5 | 0 | 起始lsn | 5~4:硬盘 | 5~4:一致 |
我们来回顾一下:
-
为了保证宕机时数据不丢失,采用WAL,为了减少恢复的时间,使用了checkpoint,为了加快日志的写入速度使用了redo log buffer。磁盘上的redo log容量有限,在两个checkpoint之间,发现redo log快不够时,则刷新一定量的脏页,其对应范围的lsn redo log可以被覆盖(释放)。
-
为了加快增删改查数据的速度,使用了缓冲池。缓冲池的容量有限,所以使用了lru。lru决定将某页从缓冲池中移除,该页恰好是脏页时,需要将数据同步到内存,连带更新Pages flushed up to。
各个环节环环相扣,像艺术品。
[转]MySQL日志——Undo Redo中有一种非常贴切的描述:将redo log成为新数据(还未同步到磁盘)的备份儿,重做的时候好知道怎么做。将undo log称为老数据的备份儿,恢复的时候好知道怎么恢复。
MySQL之Undo Log和Redo LogUndo + Redo的设计主要考虑的是提升IO性能,将随机读写磁盘转换为顺序读写。虽说通过缓存数据,减少了写数据的IO。 但是却引入了新的IO,即写Redo Log的IO。