当前位置: 首页 > 科技观察

说说一致性读的实现原理?

时间:2023-03-20 21:32:17 科技观察

本文转载自微信公众号“三太子敖丙”,可关注下方二维码。转载本文请联系三太子敖丙公众号。这个问题是我第一次接触天猫时,二面的面试官问的。之前写过mvcc的文章,但是在笔记中看到这个问题,本来打算单独处理的。于是有了这篇文章。现在主流的关系型数据库产品基本上都实现了MVCC的特性。快照在MVCC中扮演着重要的角色,代表数据在某一时刻的版本,是一致性读取的基础。更新操作提交前,数据的前像存储在Undo中,可用于实现一致性读、事务回滚、异常恢复。下面说说MySQL事务、MVCC、快照、一致读的原理和实现。.MySQL中事务的概念在RDBMS系统中基本相同。它是由一组DML语句组成的一个工作单元,要么全部成功,要么全部失败。在开发过程中,我比较关注长事务,也就是包含很多DML语句的工作单元。如果事务太长,会导致一些错误。例如,程序可能会因为交易数据包的大小超过了参数max_allowed_pa??cket而报错,或者交易中可能存在某个SQL。对应接口报错,导致整个服务调用失败。在编程时,应考虑避免长事务对业务造成的影响。ACIDimage-20201114221841801事务的原子性是事务隔离的基础,隔离和持久化是手段,最终目的是保持数据的一致性。事务的并发问题脏读:事务A从事务B中读取未提交的数据不可重复性:事务A多次读取同一个数据,事务B在这个过程中修改并提交了数据,导致事务A读取同一个数据的结果不一致多次。幻读:在事务A修改数据的同时,事务B插入了一条数据。事务A提交的时候,发现还有数据没有修改,造成错觉。不可重复读侧重于更新操作,幻读侧重于插入或删除操作。解决不可重复读只需要对满足条件的行进行加锁,解决幻读需要对表进行加锁。事务隔离级别事务隔离是数据库处理的基础之一。当多个事务同时进行更改和执行查询时,隔离级别调整了性能与结果的可靠性、一致性和可再现性之间的平衡。InnoDB使用不同的锁定策略支持不同的隔离级别。MySQL中有四种隔离级别,分别是READUNCOMMITTED、READCOMMITTED、REPEATABLEREAD和SERIALIZABLE。隔离级别脏读不可重复读幻读READUNCOMMITTED是是是READCOMMITTED否是是REPEATABLEREAD否是对于并发,使用非锁读,不需要等待访问数据上的锁释放,而是读取行的快照,这是通过InnonDBMVCC特性实现的。MVCC是Multi-VersionConcurrencyControl的缩写,即多版本并发控制。它的作用是在事务并行发生时,保证在一定隔离级别的前提下,在某个事务中可以实现一致性读,即事务开始时按照一定条件读取数据,直到结束事务,再次执行同样的条件,还是读取同样的数据,不会有任何变化。MVCC的好处是读无锁,读写无冲突。在读多写少的OLTP应用中,读写不冲突非常重要,可以提高系统的并发性能。在MVCC中,有两种读取操作:快照读取和当前读取。MVCC快照MVCC内部使用的一致性读取快照称为ReadView。在不同的隔离级别下,当一个事务启动或SQL语句启动时,看到的数据快照的版本可能不同。它将在RR和RC隔离级别下使用。转到阅读视图。InnoDB中的每一个事务都有一个唯一的事务ID,称为TransactionID,它在事务开始时就被应用到InnoDB的事务系统中,并严格按照应用的先后顺序递增。并且每一行数据都有多个版本。事务每更新一次数据,都会产生一个新的数据版本ReadView,将TransactionID赋值给这个数据版本的事务ID,标记为row_trx_id。同时要保留旧的数据版本,在新的数据版本中可以直接获取信息。数据表中的一行记录实际上可能有多个数据版本,每个版本都有自己的row_trx_id。InnoDB行格式目前InnoDB默认的行格式是Dynamic,它是Compat格式的增强版。记录头结构信息占用5个字节,事务ID和回滚指针分别占用6个和7个字节。行格式如下:RecordheaderstructureItemsize(bit)Description()1Unknown()1Unknowndeleted_flag1数据行删除标志min_rec_flag1=1如果记录被预定义为最小记录n_owned4拥有的数量recordsheap_no13索引堆中的记录排序位置record_type3记录类型;000:normal,001:B+树叶节点,010:伪列Infinum,011:Supernum,1xx:保留下一条记录在next_record16页TransactionID48transactionID在记录中的相对位置,Fixed6-byteRollbackPointer56回滚指针,固定7字节数据行存储#建表mysql>createtablestore_users(idintnotnullauto_incrementprimarykeycomment'主键id',namevarchar(20)notnulldefault''comment'name');#查看表状态信息mysql>showtablestatuslike'store_users'\GRow_format:Dynamic#默认行格式为DynamicRows:0#行数Avg_row_length:0#平均行长data_length:16384#初始化段大小16K#开启事务,插入数据mysql>begin;mysql>insertintostore_usersvalues(null,'aaaaa'),(null,'bbbbb');#查看InnoDB分配事务IDmysql>selecttrx_idfrominformation_schema.innodb_trx\Gtrx_id:8407246#事务ID分析表行头信息和隐藏事务ID及回滚指针#用linux下下工具hexdump进行$hexdump-c-v/usr/local/local/var/mysql/test/test/stest/stest/stest/store_users.ibd>store_users.txt$vistore_users.txt00011006002002002001b696e666966666666666696d75666d7560001007100710071007750001009,000150001009009000.4009.4009.4000.400|...|.........|0001008000010000008048ce83000001d8011061|......H.......a|#RecordHeader信息000100906161616105000018ffd6800000020000|aaaa......|000100a60ce818d.260......bbbbb|000100b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010表示变长字段长度,只有一个varchar(20)不超过256字节,并且没有NULL值。00代表NULL标志,第一行没有NULL数据。字符a的十六进制为61,即6161616161表示字段值aaaaa0000008048ce6字节为TransactionID,转为十进制为8407246,也就是上面的information_schema.innodb_trx.trx_idcolumntrx_id的值:8407246。83000001d801107个字节是RollbackPointer。1c80000001是5个字节,代表RecordHeader信息。隔离级别和快照REPEATABLEREAD默认隔离级别,一致性读快照(ReadView)在第一次SELECT发起时建立,以后不会改变。如果在同一个事务中发出多条非锁定SELECT语句,那么这些SELECT语句返回的结果在事务提交之前是一致的。在RR下,快照ReadView并不是在事务发起时创建的,而是在第一个SELECT发起后创建的。READCOMMITTEDREADCOMMITTED下,一致性读快照(ReadView)会在每次SELECT后生成最新的ReadView,即每次SELECT都能读取到已经COMMIT的数据,会出现不可重复读,幻读现象.Undorollbacksegment当开启事务执行更新语句(insert/update/deeldte)时,会通过server层的处理生成执行计划,然后调用存储引擎层接口读写数据.在用户触发COMMIT或ROLLBACK之前,这些UncommittedData数据称为后映像(PostImage),这些数据存储在UndoLog中,用于用户回滚或MySQLServerCrash恢复,UndoLog用于循环覆盖.#开启交易,更新账户余额,不提交交易。mysql>starttransaction;mysql>updateaccountsetbalance=100000whereaccount_no=10001;Rowsmatched:1Changed:1Warnings:0以上,在RR隔离级别下,启动一个事务,做update更新操作,不提交事务,通过showengine查看undo状态Innodb状态\G。Trxidcounter8407258Purgedonefortrx'sn:o<8407257undon:o<0state:runningbutidleHistorylistlength33......---TRANSACTION8407257,ACTIVE154sec2lockstruct(s),heapsize1136,4rowlock(s),undologentries1Trriesidcounters8407258当前事务ID,undoIVEundo条目154秒事务时长,事务提交后,会调用PurgeThread清理undo中的旧数据。回滚记录insert:逆向操作是delete,undo记录与delete相关的信息,只存储主键id。udpate:逆向操作update,undo记录update前的相关数据。delete:逆向操作是insert,在undo中记录与insertvalues(...)相关的记录。从这里可以看出update操作占用的Undo空间大小排序为:delete>update>insert,所以不建议物理删除数据,会产生大量的UndoLog,并且撤消将在快满时切换。大量的IO操作会导致业务的DML变得很慢。一致读MySQL官方文档是这样描述一致性读的:基于某个时间点的读操作,得到的是当时数据的快照,而不管其他事务是否同时修改数据。在查询过程中,如果其他事务修改了数据,则需要从undolog中获取旧版本的数据。这样做可以有效避免因为需要加锁(防止其他事务同时修改这些数据)而导致事务并行度下降的问题。在可重复读(REPEATABLEREAD,简称RR)隔离级别下,数据快照版本在第一次发起读请求时创建。在READCOMMITTED(RC)隔离级别下,每次发出读取请求时都会重新创建快照。一致性读取是InnoDB在RR和RC下处理SELECT请求的默认模式。由于一致性读不会锁定它请求的表,其他事务可以同时修改数据而不会受到影响。一行数据有多个版本,每个数据版本都有自己的trx_id,每个事务或查询通过trx_id生成自己的一致视图。普通的select语句是一致性读,会根据行trx_id和一致性视图来决定数据版本的可见性。图中UR1和UR2为undo,存放在UndoLog中。每次查询都是根据当前数据页和Undo页构造一个一致的数据页(ConsistentReadPage),通过读取CRPage将数据返回给用户。总结介绍MySQL事务、快照、MVCC和Undo。虽然这些东西比较抽象,但是弄清楚这些东西还是很有意义的。可以帮助我们更好的理解和使用MySQL,我们也可以将这种设计思想用在自己的业务系统中。其中,Undo在MySQL中扮演着重要的角色。它是MVCC快速创建快照,支持系统高并发的基础。好了,以上就是本期的全部内容。我是敖丙你知道的越多,你不知道的就越多。下期见。