赞
踩
一致性是事务的最终追求的目标,隔离性、原子性、持久性是达成一致性目标的手段。
脏读读取的是其他事务未提交的数据,不可重复读读取的是其他事务提交了的数据
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)
不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。
读提交事务隔离级别是大多数流行数据库的默认事务隔离界别,比如 Oracle,但是不是 MySQL 的默认隔离级别。
MySQL的RR级别是可以防止幻读发生的。
在@Transactional 注解里是有一个 isolation 参数的用于设置事务隔离级别的,如 @Transactional(isolation=Isolation.DEFAULT) ,默认的就是 DEFAULT 值,即 MySQL 默认支持什么隔离级别就是什么隔离级别。
想要了解MySQL事务的实现原理,我们首先就需要先明白在MySQL中语句的执行流程。
从字面上看,不难看出buffer pool其实就是缓存池的意思,它是MySQL中一个至关重要的主组件,MySQL中的增删改查操作基本都是在这里执行。之所以要引进缓存池这样一个主键,也不难想到无非就是为了提高性能,毕竟数据存储在磁盘中,如果每次操作都是直接操作磁盘,涉及到磁盘随机读写性能自然不高。所以MySQL会把数据取出,放入内存中,每次操作就直接在内存中执行即可,这样一个专门处理数据的区域就是buffer pool。
由于引入了缓存池,内存中的数据是使用异步线程随机时间刷盘,那么就会存在一定的问题,如果内存中的数据还未来得及更新到磁盘中,系统就宕机了,那么此时缓存中的数据便丢失了。
为了解决这样的问题,我们需要引入日志来将更改的操作记录下来。InnoDB中引入了undo log和redo log,在buffer pool内存数据修改之前会先保存旧数据到undolog,在修改之后会将在某个页上面做了什么修改写入redo log,那么即使MySQL中途挂了,也可以根据undolog进行回滚(事务未完成提交时)或redo log对数据进行恢复(事务完成提交后)
可能有人会说写日志不也是磁盘操作吗,将直接在磁盘中修改数据换成了在磁盘中写日志有区别吗?
这里其实redo log是顺序写的(磁盘顺序写的效率接近内存写,远超过磁盘随机写),记录的是物理修改,文件的体积很小,恢复速度也很快(因为不管对什么表的修改都是记录到一个日志文件中,所以可以采用顺序写,而数据保存在ibd文件,每个表对应一个文件,且会有删除操作,对数据的修改显然无法使用顺序写)
所以引入 redo log 机制是十分必要的。因为写 redo log 时,我们将 redo log 日志追加到文件末尾,虽然也是一次磁盘 IO,但是这是顺序写操作(不需要移动磁头);而对于直接将数据更新到磁盘,涉及到的操作是将 buffer pool 中缓存页写入到磁盘上的数据页上,由于涉及到寻找数据页在磁盘的哪个地方,这个操作发生的是随机写操作(需要移动磁头),相比于顺序写操作,磁盘的随机写操作性能消耗更大,花费的时间更长,因此 redo log 机制更优,能提升 MySQL 的性能。
从另一方面来讲,通常一次更新操作,我们往往只会涉及到修改几个字节的数据,而如果因为仅仅修改几个字节的数据,就将整个数据页写入到磁盘(无论是磁盘还是 buffer pool,他们管理数据的单位都是以页为单位),这个代价未免也太了(每个数据页默认是 16KB),而一条 redo log 日志的大小可能就只有几个字节,因此每次磁盘 IO 写入的数据量更小,那么耗时也会更短。 综合来看,redo log 机制的引入,在提高 MySQL 性能的同时,也保证了数据的可靠性。
具体执行流程图:
正如前面所说,当我们查询数据的时候,会先去Buffer Pool中查询。如果Buffer Pool中不存在,存储引擎会先将数据从磁盘加载到Buffer Pool中,然后将数据返回给客户端;同理,当我们更新某个数据的时候,如果这个数据不存在于Buffer Pool,同样会先数据加载进来,然后修改修改内存的数据,被修改过的数据会在之后统一刷入磁盘。但是这个过程存在一定问题,如果还未来得及刷库就宕机了那么数据就永久丢失了。所以MySQL使用日志来保障系统崩溃时的恢复。
MySQL日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中比较重要的就是二进制日志binlog(归档日志)、事务日志redo log(重做日志)和undo log(回滚日志)。 这里面binlog是server层的日志,而redo log和undo log都是引擎层(innodb)的日志,要换其他数据引擎那么就未必有redo log和undo log了。
MySQL的二进制日志binlog可以说是MySQL最重要的日志,它记录了所有的DDL和DML语句(除了数据查询语句select),以事件形式记录,还包含语句所执行的消耗的时间。
使用场景:
记录格式:
这三种模式需要注意的是:使用 row 格式的 binlog 时,在进行数据同步或恢复的时候不一致的问题更容易被发现,因为它是基于数据行记录的。而使用 mixed 或者 statement 格式的 binlog 时,很多事务操作都是基于SQL逻辑记录,我们都知道一个SQL在不同的时间点执行它们产生的数据变化和影响是不一样的,所以这种情况下,数据同步或恢复的时候就容易出现不一致的情况。
写入策略:
在进行事务的过程中,首先会把binlog 写入到binlog cache中(因为写入到cache中会比较快,一个事务通常会有多个操作,避免每个操作都直接写磁盘导致性能降低),事务最终提交的时候再吧binlog 写入到磁盘中。当然事务在最终commit的时候binlog是否马上写入到磁盘中是由参数 sync_binlog 配置来决定的。
很显然三种模式下,sync_binlog=1是强一致的选择,选择0或者N的情况下在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。
一个事务的binlog是有完整格式的,statement格式(记录sql语句),最后会有个COMMIT;row格式(记录行的内容,记两条,更新前和更新后都有),最后会有个XID event
redo log 是属于引擎层(InnoDB)的日志,它的设计目标是支持InnoDB的事务特性中的持久性。
MySQL为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Buffer Pool缓冲池里,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步,这就存在了系统故障时数据未同步的情况。而redo log能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做。即一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决了异常、宕机而可能造成数据错误或丢失问题,这就是redo log的核心职责。
写入策略:
redo log占用的空间是一定的,并不会无限增大(可以通过参数设置),写入的时候是进顺序写的,所以写入的性能比较高。当redo log空间满了之后又会从头开始以循环的方式进行覆盖式的写入。在写入redo log的时候也有一个redo log buffer,日志什么时候会刷到磁盘(指的是redo log buffer刷到redo log file)是通过innodb_flush_log_at_trx_commit 参数决定。
除了上面几种机制外,还有其它两种情况会把redo log buffer中的日志刷到磁盘。
只有将innodb_flush_log_at_trx_commit设置为1才能够严格的保证数据不丢失,不然仍然存在宕机时redo log buffer未刷盘而造成数据无法重做的可能。
以上是redo log buffer在内存中的结构,可以看到这里有两个指针,write_pos表示当前写的位置,只要有记录更新了,write_pos就会往后移动。而check_point表示检查点,只要InnoDB将check_point指向的修改记录更新到了磁盘中,check_point将会往后移动。redo log buffer采用的是循环写,check point顺时针到write pos之间的数据是待刷盘数据,如果不刷盘数据则会被覆盖,write pos顺时针到check point之间则是可用的空间。
如果事务提交成功之后buffer pool中的数据还没有刷盘,这时MySQL宕机了,那么在重启的时通可以从redo log file中恢复数据,redo log file根据innodb_flush_log_at_trx_commit参数配置,通常最多丢失一秒的数据。
重做日志被设计成可循环使用,当日志文件写满时,重做日志中对应数据已经被刷新到磁盘的那部分不再需要的日志可以被覆盖重用。而判断哪部分日志可以覆盖重用就需要有一个标记。事实上,当数据库宕机时,数据库不需要重做所有的日志,只需要执行上次刷入点之后的日志。这个点就叫做Checkpoint,它解决了以下的问题:
- 缩短数据库恢复时间
- 缓冲池不够用时,将脏页刷新到磁盘
- 重做日志不可用时,刷新脏页
InnoDB引擎通过LSN(Log Sequence Number)来标记版本,LSN是日志空间中每条日志的结束点,用字节偏移量来表示。每个page有LSN,redo log也有LSN,Checkpoint也有LSN。可以通过命令show engine innodb status来观察
- Log sequence number(LSN1):当前系统LSN最大值,新的事务日志LSN将在此基础上生成(LSN1+新日志的大小)
- Log flushed up to(LSN2):当前已经写入日志文件的LSN
- Pages flushed up to(LSN3):当前最旧的脏页数据对应的LSN,写Checkpoint的时候直接将此LSN写入到日志文件
- Last checkpoint at(LSN4):当前已经写入Checkpoint的LSN;
InnoDB实现了Fuzzy Checkpoint的机制,每次取到最老的脏页,然后确保此脏页对应的LSN之前的LSN都已经写入日志文件,再将此脏页的LSN作为Checkpoint点记录到日志文件,意思就是“此LSN之前的LSN对应的日志和数据都已经写入磁盘文件”。恢复数据文件的时候,InnoDB扫描日志文件,当发现LSN小于Checkpoint对应的LSN,就认为恢复已经完成。
- 创建阶段:事务创建一条日志;
- 日志刷盘:日志写入到磁盘上的日志文件;
- 数据刷盘:日志对应的脏页数据写入到磁盘上的数据文件;
- 写CKP:日志被当作Checkpoint写入日志文件;
Checkpoint写入的位置在日志文件开头固定的偏移量处,即每次写Checkpoint都覆盖之前的Checkpoint信息。
虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经刷盘,哪些数据还没有。而 redo log 每次刷盘会更新日志文件中的Check Point根据对应的LSN来判断该条操作是否已经落盘。所以redo log具有crash-safe能力
为了保证两份日志的逻辑一致性(redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致)MySQL采用了两阶段提交,即将redo log日志的写入拆分为两个步骤:prepare和commit
如果不采用两阶段提交,那么有可能在写入redo log后准备写入binlog时发生crash,那么就会导致两个日志的数据不一致。而采用两阶段提交,即使在写入binlog系统崩溃,那么在进行数据恢复时,发现redo log处于prepare阶段,并且没有对应的binlog日志,就会进行事务的回滚,从而保证了一致性。即redo log 在进行数据重做时,只有读到了 commit 标识,才会认为这条 redo log 日志是完整的,才会进行数据重做,否则会认为这个 redo log 日志不完整,不会进行数据重做。而 commit 标识是在 binlog 日志写完之后才标志上的,所以保证了两个日志的一致性。
redo log保证的是持久性,成功修改后一定持久化到数据库中,故日志中主要记录的是事务执行过程中的修改情况。而想要保证事务的原子性,就需要在发生异常时,对已经执行的操作进行回滚,所以它主要记录的是修改前的值。即每次修改之前,都会将修改前的数据作为历史保存一份到undo日志中,数据里面还会记录操作该数据的事务ID,之后可以使用该事务ID进行回滚。
对于如下数据表(其中DB_TRX_ID和DB_ROLL_PTR是两个隐式字段,分别表示最近进行修改的事务id,而回滚指针):
如执行update user_info set name = “李四” where id=1
时,就会将原本的数据先保存到undo log,随后将表中的数据值进行修改,并更改最近修改的事务id为当前事务id,以及让回滚指针指向刚保存到undo log的记录
undo log没有buffer,不然系统崩溃时还未将buffer刷到文件中则无法进行回滚
MySQL实现最高事务隔离级别串行化时,使用的就是锁技术。在MySQL中使用的是读写锁,即在读时加共享锁,写时加互斥锁。允许读读并行,读写以及写写都不能并行。
MVCC(Multi-Version Concurrency Control)多版本并发控制是一种并发控制机制,指维护一个数据的多个版本,使得读写操作没有冲突。具体实现就是采用快照读,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的隐式字段、undo log和read view。
当前读和快照读
当前读:读取的数据总是最新的,实现的原理是对正在读的记录加锁,使得读写互斥,保证每次读的都是数据库中最新的数据
快照读:每次读取到的数据不一定是最新的数据,而是这条数据的快照版本,这样可以保证读写不互斥(读写分离),能够并发执行
在MySQL中有两个隐藏字段:
undo log 版本链是基于 undo log 实现的。undo log 中主要保存了数据的基本信息,比如说日志开始的位置、结束的位置,主键的长度、表id,日志编号、日志类型,记录了每个事务对数据行的修改的情况。
此外,undo log 还包含两个隐藏字段 trx_id 和 roll_pointer。trx_id 表示当前这个事务的 id,MySQL 会为每个事务分配一个 id,这个 id 是递增的。roll_pointer 是一个指针,指向这个事务之前的 undo log。
当事务并发执行修改某条记录的时候,不同的事务对该数据的修改会产生不同的版本,每次修改都会记录下这条记录之前的数据,并且在隐式字段中设置上本次操作的事务的id,并让回滚指针指向上一个版本,这样就会形成一条链表,即所谓的版本链。
ReadView其实就是一个保存事务id的list列表,记录的是本事务执行时有哪些事务在执行,且还没有提交(即当前系统还有哪些活跃的读写事务),用于判断事务是否可以读取某个数据行的版本。
它主要包含:
在访问某条记录时,只需要按照下面的步骤判断该记录在版本链中某个版本是否可见:
即当事务id在m_ids中,或者大于m_ids中最大的事务id的时候,这个版本就不能被访问
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。查询到便使用undo log来恢复该数据行的历史版本。
RC级别时,在一个事务中每一次查询都会生成一次读视图,使得每次读取都有了不同的ReadView,就可以读到已经提交了的数据,造成不可重复度的问题
RR级别时,在一个事务中只有**第一次查询(查询时才生成)**会生成一次视图,后面的查询都是用的这个读视图,保证了可重复读,因为数据对于当前事务的可见性和第一次是一样的,从unlog中读到的数据快照和第一次是一样的,中途即使有事务修改也读取不到。
MySQL的RR不止防止了不可重复读问题,同时也避免了幻读的发生,因为在事务执行过程中如果其他事务插入了一些新的数据,由于版本号的问题,当前事务仍然是读不到的。
由于MVCC使用的是快照读(普通的select则就是快照读),所以再查询出数据后如果需要进行修改可能会存在脏写的情况,即别的事务已经将数据修改了,而当前事务并没有读取到新的数据就进行修改,则会出现写覆盖,即脏写的问题。
解决脏写有两种方式:
insert、update、delete、select xxx for update
都是当前读(且会加写锁),那么修改时可以采用update xxx set xxx = xxx - 1 where id = 1
,此时就会读取到最新的xxx值去进行修改MySQL有以下两种锁
而串行化的实现非常简单,其实就是对默认的select语句加上lock in share mode,即加上共享锁。那么由于修改语句都会加写锁,因此实现了阻塞
参考文章:
Mysql buffer pool详解 - 奕锋博客 - 博客园 (cnblogs.com)
Mysql 核心日志(redolog、undolog、binlog)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。