【MySQL速通篇004】这可能最详细的关于MySQL基础知识点的文章了
🍁一、索引种类🍀1.1、普通索引create index index_name on table(column);作用:加速查找🍀1.2、主键索引一般是在建表时指定primary key(column)作用:加速查找,不能重复,不能为空主键索引是数据库中的一种索引类型,它用于唯一标识每个表中的记录。主键索引通常由一个或多个列组成,这些列具有唯一性和非空性约束,因此可以确保表中每个记录都有唯一的标识。
在数据库中,主键索引的作用是提高数据查询和检索的效率。如果没有主键索引,查询数据库中的记录需要逐个扫描整个表,这样会导致查询时间变得极其缓慢。而使用主键索引,数据库可以快速定位到特定的记录,大大提高了查询效率和速度。
在设计数据库时,选择合适的列作为主键索引非常重要。通常情况下,主键应该是短小、简单且稳定的,并且不应该随着时间而改变。同时,为了最大程度地提高查询效率,主键列的数据类型应该尽可能小,以便在查询时占用更少的存储空间。
虽然主键索引可以显著提高数据库查询性能,但在某些情况下也会带来额外的开销。例如,在更新主键列值或插入新记录时,主键索引需要更新索引结构,这可能会导致性能损失。因此,在设计数据库时,需要综合考虑数据的访问模式、数据量、应用程序需求等因素来决定是否需要使用主键索引。🍀1.3、唯一索引create unique index index_name on table(column);或者创建表时指定unique index_name column作用:加速查找,与主键类似,但是可以为空,不能重复🍀1.4、联合索引(包括联合唯一索引)在多个字段(列)上创建索引(也就是由多列组成索引),遵循最左前缀原则alter table t add index index_name(a,b,c);作用:加速查找。联合索引可以有(联合主键索引,联合唯一索引,联合普通索引)当然括号内的叫法有些不是很官方🍀1.5、覆盖索引表中的id和email列创建了索引select email from info_test where email = 'BIN11@qq.com'
覆盖索引(Covering Index)是指,当数据库查询语句可以完全从索引中获取所需的数据结果时,就称为使用了覆盖索引。
通常来说,对于一个包含多个列的表,在执行查询操作时,需要搜索整个表,并根据查询条件过滤出符合条件的记录。这种搜索可能会非常耗时,特别是在处理大量数据时。
而如果使用了覆盖索引,即建立一个包含所有需要返回结果列的复合索引,那么查询操作只需要在这个索引上进行,而不用再去搜索整个表。这样可以极大地提高查询速度和性能。
使用覆盖索引的好处不仅在于它减少了查询操作所需的时间,还减轻了系统I/O的负担。因为索引通常比表小得多,所以一旦找到了匹配的索引值,就可以直接从磁盘读取索引所包含的所有列,而无需再访问原始数据表,从而节省了大量的磁盘I/O操作。
需要注意的是,覆盖索引只有在查询的 SELECT 列都在索引中时才能发挥作用。如果 SELECT 列中包含了不在索引中的列,则无法使用覆盖索引,查询操作还是需要访问原始数据表,这样会降低查询效率。🍀1.6、索引合并【把多个单列索引合并使用】索引合并是指一个查询语句需要多个索引才能满足查询条件,这时数据库会使用索引合并算法来优化查询性能。索引合并可以将多个索引的结果合并成一个结果集,从而避免多次扫描索引和数据行。通过索引合并,可以提高查询性能并降低系统开销。例子:select email from info_test where email = 'BIN11@qq.com' and id=620783;
🍀1.7、删除索引drop index 索引名称 on 表名🍁二、索引实现方式🍀2.1、hash索引哈希索引就是基于哈希算法,对于每一行数据,数据库存储引擎会对所有索引列都通过哈希算法去计算一个哈希码,然后将这个哈希码存储在哈希索引中,由于使用的是哈希算法,所以使用哈希索引就会存在两个弊端:1、哈希算法计算出来的哈希值可能存在哈希冲突2、由于计算出来的是个值所以无法进行范围查询使用hash索引时会自动创建一张hash索引表,索引表与数据表里的数据位置是不一致的特别注意:对取单个词的话比较快,但是对范围查找比较慢🍀2.2、btree索引搜索速度快:btree索引具有非常高效的搜索速度,能够快速地查找并返回相关数据。空间利用率高:btree索引能够在占用很少存储空间的情况下,存储大量数据。范围查询效率高:btree索引在进行范围查询时能够非常高效地处理,大大提高了数据查询的效率。支持数据排序:btree索引能够对存储在其中的数据进行排序,提供了更加灵活的查询和排序功能。并发控制优秀:btree索引的并发控制是非常优秀的,能够避免并发对索引的影响,确保数据的完整性和一致性。适合多种数据类型:btree索引能够适应多种数据类型,不同类型的数据都能够使用btree索引进行索引和查询🍀2.3、创建索引的不利方面空间占用:索引需要占用额外的磁盘空间,当表数据增加时,索引也会相应增加,增加了存储需求。维护开销:索引需要维护,每次增删改查都会更新索引,增加了维护开销。性能下降:虽然索引可以加速某些查询,但是对于表的其他操作,如数据的增加、删除和更新等,由于需要维护索引,所以性能反而会下降。不必要的索引:有些索引并不是必须的,因为它们可能永远不会被查询,但是却会影响到数据的插入和更新操作的速度。索引失效:当数据分布不均时,某些索引可能会失效,即使查询中使用了这些索引,也不能达到加速查询的目的。此时,需要重新评估索引的使用情况。从上面有一点值得思考,我们创建索引就是用来使用的,我们要懂得如何命中索引,让索引为我们工作🍁三、无法命中索引情况🍀3.1、模糊查询如果查询中使用LIKE语句以通配符(%或_)开头,则无法使用索引但是实际项目中一般会使用第三方模块进行模糊查询🍀3.2、组合索引的最左前缀匹配CREATE UNIQUE INDEX 组合索引名称 ON 表名(列名1, 列名2, ...)组合索引的最左前缀原则表示,在多个列组成的索引中,如果要使用索引,必须使用索引的最左前缀列,也就是说,只有在满足最左前缀列的搜索条件下,索引才会被使用。举例来说,如果有如下表格:idnameagegender1Tom21Male2Jack22Male3Lily23Female4Amy22Female5Bob21Male如果创建一个组合索引(name, age, gender),那么满足以下查询条件时,索引会被使用:WHERE name = 'Tom'WHERE name = 'Tom' AND age = 21WHERE name = 'Tom' AND age = 21 AND gender = 'Male'但是如果只使用其中部分列作为搜索条件时,索引不会被使用,例如:WHERE age = 21 (没有使用最左前缀列name)WHERE gender = 'Male' (没有使用最左前缀列name和age)WHERE age = 21 AND gender = 'Male'注意:组合索引的效率 > 索引合并🍀3.3、使用了函数或表达式如果查询条件中使用了函数或表达式,MySQL就无法直接使用索引来匹配记录。例如,在一个查询中,如果使用了以下条件:WHERE YEAR(date)=2019,MySQL将无法使用date列上的索引来优化查询。一种可能的解决方法是创建一个计算列,将YEAR(date)的结果存储在该列中,并在查询中使用该计算列作为条件。🍀3.4、列类型不匹配【隐式查询】如果查询条件中的列类型与索引列的类型不匹配,MySQL将无法使用该索引。例如,如果在一个索引上查询一个字符串类型的列,但查询条件中使用了一个数字值,MySQL将无法使用索引来优化查询。一种解决方法是将查询条件中的值转换为与索引列类型相同的类型。如:Select?* from` `tb1?where?name?= 999;名字列的话很明显是字符串类型,这里写个999类型肯定不一致,查询的速度肯定很慢,如果你用个引号引起来的话速度就会非常快了。🍀3.5、or的错误使用or可以命中索引的几类情况:OR操作符需要将多个条件拆分成单个条件,然后对每个条件进行查询,并将结果进行组合。如果多个条件中只有一个条件能够命中索引,那么查询的效率会受到影响。OR操作符不能用于对同一个列的多个条件查询。如果要对同一个列进行多个条件查询,应该使用IN操作符。如果要使用OR操作符进行多个条件查询,并且所有条件都能够命中索引,那么查询的效率会非常高。但是如果其中一个条件不能命中索引,那么查询的效率会受到影响。例如:select * ``from tb1 ``where nid = 1 ``or email = ``'seven@live.com'``;如果只有email列设置了索引那么这个语句整体的查询速度也是不快的但是如果是下面这种情况:``select * ``from tb1 ``where nid = 1 ``or email = ``'seven@live.com' and name = ``'alex'其中只有nid和name列为索引列的话查询的速度也是很快的,因为mysql会自动忽略email 这不是索引的一列🍀3.6、其他的情况- 使用函数
select * from tb1 where reverse(name) = 'wupeiqi';
- !=
select * from tb1 where name != 'alex'
特别的:如果是主键,则还是会走索引
select * from tb1 where nid != 123
- >
select * from tb1 where name > 'alex'
特别的:如果是主键或索引是整数类型,则还是会走索引
select * from tb1 where nid > 123
select * from tb1 where num > 123
- order by
select email from tb1 order by name desc;
当根据索引排序时候,选择的映射如果不是索引,则不走索引
特别的:如果对主键排序,则还是走索引:
select * from tb1 order by nid desc;
- 索引失效:如果索引被损坏或者过期,MySQL无法使用该索引,进行全表扫描。、
- 数据量过小:如果表中只有很少的数据,使用索引可能会比全表扫描更慢,因为耗费在索引扫描上的时间比全表扫描的时间还要长。
- 范围查询:使用BETWEEN和IN等范围查询语句,MySQL会先执行索引扫描,然后再进行分类筛选,如果筛选后的结果占比过高,MySQL会放弃使用索引,进行全表扫描。🍁四、执行计划🍀4.1、概念在 MySQL 中可以通过 explain 关键字模拟优化器执行 SQL语句,从而知道 MySQL 是如何处理 SQL 语句的。具体地说,MySQL 的执行计划包含以下信息:查询语句的类型,如 SELECT、INSERT、UPDATE 和 DELETE 等。查询涉及的表和它们之间的连接方式。查询采用的索引类型和具体使用的索引。每个表的访问方式,如全表扫描和索引扫描等。执行计划的优先级和执行顺序。估算和实际行的数量、使用的时区和排序规则等。4.2、创建方法执行计划可以通过使用 EXPLAIN 命令来获取,语法如下:explain + 查询SQL - 用于显示SQL执行信息参数,根据参考信息可以进行SQL优化;当使用 EXPLAIN 命令时,MySQL 会返回一个查询结果集,包含以上信息。具体而言,返回结果包括以下列:- id:标识查询的标识符,一般情况下为随机生成的整数。- select_type:查询类型,包括 SIMPLE、PRIMARY、UNION、SUBQUERY、DEPENDENT SUBQUERY 等。- table:</查询涉及的表及其别名。 - partitions:查询涉及的分区。- type:访问的类型,包括 ALL、INDEX、RANGE、REF、EQ_REF 和 CONST 等。- possible_keys:可能使用的索引,其值由逗号分隔的索引列表组成。- key:实际使用的索引。- key_len:索引字段的长度。- ref:此列显示哪个列或常量与 key 列一起被用于查找索引值。- rows:估算结果集中的行数。- filtered:结果集中符合条件的行数与结果集总行数的比例。- Extra:额外的信息,如使用了临时表、使用了 filesort 等。“Using index”
此值表示mysql将使用覆盖索引,以避免访问表。不要把覆盖索引和index访问类型弄混了。
“Using where”
这意味着mysql服务器将在存储引擎检索行后再进行过滤,许多where条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带where子句的查询都会显示“Using where”。有时“Using where”的出现就是一个暗示:查询可受益于不同的索引。
“Using temporary”
这意味着mysql在对查询结果排序时会使用一个临时表。
“Using filesort”
这意味着mysql会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。mysql有两种文件排序算法,这两种排序方式都可以在内存或者磁盘上完成,explain不会告诉你mysql将使用哪一种文件排序,也不会告诉你排序会在内存里还是磁盘上完成。
“Range checked for each record(index map: N)”
这个意味着没有好用的索引,新的索引将在联接的每一行上重新估算,N是显示在possible_keys列中索引的位图,并且是冗余的。重点需要注意的是,执行计划只是查询优化的一个阶段,实际执行的结果可能会受到多种因素的影响,包括数据分布、硬件性能等。因此,执行计划不能完全反映查询在实际执行中的表现,但是它可以帮助开发人员分析查询语句的性能问题,从而进行相应的优化。🍀4.3、表的访问方式执行计划中的 type 字段显示了 MySQL 查询数据表的方法,同时也是最需要关注的字段之一。MySQL 中查询数据表的方式越简便,执行速度就越快。以下是一些常见的 type 类型:- ALL:全表扫描,数据表没有使用任何索引,这种方式的效率最低。- INDEX:全索引扫描,需要扫描数据表所有的索引,对于查询的需要和原本数据表的分布情况关联比较密切。- RANGE:索引区间扫描,仅仅扫描匹配条件的数据,而不是全表扫描,大多适合一些有序数据类型。- REF:朴素索引反查,创建索引时只是索引主键、外键或唯一属性,要根据需要查询的属性从数据表中查询出结果。如果数据非常庞大,一般不会采用这种方式。- EQ_REF:联合索引扫描,按最小数量查找结果集,并针对联合索引中每个匹配的值进行搜索。-CONST:索引匹配到一个已知的常量时,直接查找结果集,是最快的查询类型。- SYSTEM 系统 表仅有一行(=系统表)。这是const联接类型的一个特例。- INDEX_MERGE 合并索引,使用多个单列索引搜索 select * from tb1 where name = 'alex' or nid in (11,22,33);🍀4.4、索引的使用执行计划中的 possible_keys 字段表示可以使用的索引,key 字段表示实际使用的索引。对于一些大型数据表和查询量较大的应用程序,索引的使用就显得尤为重要。索引的使用可以在一定程度上减少数据表数据扫描的时间,减小查询时间。MySQL 用生成的解释计划告诉开发人员在查询中哪些索引可以用上,如果没有使用到索引需要优化查询语句或者添加合适的索引。🍀4.5、 SQL 的优化执行计划可以告诉你问题是数据库结构、索引还是 SQL 语句的问题。因此,分析执行计划有助于开发人员进行 SQL 的优化,从而提高查询性能。优化 SQL 的一些技巧包括:- 将复杂查询拆分为简单查询。 - 减少使用内部查询或子查询。- 优化 WHERE 子句,减少全表扫描。 - 对 SELECT 和 FROM 子句进行优化,避免不必要的计算和 JOIN 操作。- 尽量减少使用 ORDER BY 和 GROUP BY 子句。总之,执行计划是 MySQL 中一个非常重要的概念,通过分析执行计划可以有效地优化查询语句,提高数据库查询性能。同时,也要注意实际执行中的其他因素,如数据分布、硬件性能等。🍁五、慢日志记录🍀5.1、作用对于那些执行时间比较长的语句等,如果我们是想要mysql自动帮我们记录下来,就要使用到慢日志MySQL慢日志主要有以下几个作用:找出慢查询。慢查询指的是执行时间较长的SQL语句,这些语句可能会导致数据库性能下降。通过MySQL慢日志,我们可以找出这些慢查询,及时进行优化。优化SQL语句。通过分析MySQL慢查询日志,我们可以找到SQL语句的瓶颈所在,进行优化,从而提高数据库性能。统计查询频率。通过统计MySQL慢日志中的查询语句,我们可以了解哪些查询语句最常被执行,从而可以根据查询频率优化数据库索引等。应用于性能测试。MySQL慢日志提供了比较详细的查询日志信息,是进行数据库性能测试的重要工具之一。总之,MySQL慢日志是数据库性能优化的必备工具之一,它可以帮助我们找出SQL语句中的瓶颈,从而提高数据库性能🍀5.2、查询a、配置MySQL自动记录慢日志slow_query_log = OFF 是否开启慢日志记录 long_query_time = 2 时间限制,超过此时间,则记录 slow_query_log_file = /usr/slow.log 日志文件 log_queries_not_using_indexes = OFF 为使用索引的搜索是否记录注:查看当前配置信息: ? ?show variables like '%query%' 修改当前配置: ? ? set global 变量名 = 值b、查看MySQL慢日志mysqldumpslow -s at -a /usr/local/var/mysql/MacBook-Pro-3-slow.log🍀5.3、详细步骤MySQL慢日志是记录MySQL查询日志中的长时间查询的一种机制。他可以帮助我们找出执行时间较长的SQL语句,并对其进行优化。查询MySQL慢日志的步骤如下:打开MySQL慢查询日志的开关。在MySQL配置文件my.cnf中找到slow_query_log这个参数,将其设置为on即可开启慢查询日志指定MySQL慢查询日志文件。在MySQL配置文件my.cnf中找到slow_query_log_file这个参数,将其设置为您想要的路径及文件名即可。如果没有指定,默认为MySQL数据目录下的主机名-slow.log。设置MySQL慢查询日志的阈值。在MySQL配置文件my.cnf中找到long_query_time这个参数,将其设置为您想要的阈值,即高于或等于该时长的SQL语句被记录到慢日志。默认值为10秒。重启MySQL。执行命令systemctl restart mysqld或service mysqld restart,以使配置生效。5.查询MySQL慢查询日志。可以通过命令查看MySQL慢查询日志:mysqldumpslow -s t /var/lib/mysql/hostname-slow.log ?# 查询所有慢日志 mysqldumpslow /var/lib/mysql/hostname-slow.log ?# 查询慢日志中执行时间大于5秒的SQL语句</ mysqldumpslow -t 5 /var/lib/mysql/hostname-slow.log6.以上命令中的-s参数表示按执行时间排序,-t参数表示指定执行时间阈值。7.分析MySQL慢查询语句。MySQL慢查询日志查出来的SQL语句并不是最优的,需要进行分析和优化。可以用EXPLAIN命令查看执行计划,找到瓶颈所在,并优化SQL语句。
EMR OLAP、一次最多插入不能超过10000行吗?在批量插入的时候报以下错误?
问题1:EMR OLAP、EMR ClickHouse、EMR StarRocks中StarRocks 2.5.3 一次最多插入不能超过10000行吗?在批量插入的时候报以下错误
Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Expression child number 29037 exceeded the maximum 10000
; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Expression child number 29037 exceeded the maximum 10000 at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:235) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) at com.sun.proxy.$Proxy140.insert(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:278) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93) at com.sun.proxy.$Proxy146.batchInsert(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) 问题2;expr_children_limit是这个参数控制的对吧?
flink的有类似于hive的炸裂函数吗?
flink的有类似于hive的炸裂函数吗?
大佬们有见过flink 1.17.0写hive报错hdfs路劲不存在的问题吗,多跑几次就可以成功?
问题1:大佬们有见过flink 1.17.0写hive报错hdfs路劲不存在的问题吗,多跑几次就可以成功? SYNC 问题2:不会有其他作业操作这个表
您好请问个问题 ,Hologres中我的sql 经常报内存溢出错误,是什么情况?
您好请问个问题 ,Hologres中我的sql 经常报内存溢出错误,是什么情况?> ERROR: Total memory used by all existing queries exceeded memory limitation. DETAIL: memory usage for existing queries=(30240521483095839,15562965619)(50070500037371924,6276568636)(50080517946852967,280766384); Used/Limit: 22129147904/22126235648 quota/sum_quota: 100/100. CONTEXT: [query_id:30240521483095839]
Hologres中这是什么问题?
问题1:Hologres中这是什么问题?[20:20:57] [SQL 1/1]: select * from fd_ads.tmp_ads_fd_itm_top20_lack_rate_02_001 where ds = '20190101' [20:20:57] 执行结束 [20:20:57] NOTICE: QueryID: 20432057018766398 [20:20:57] [执行失败! 0 row 耗时:323.69ms] [20:20:57] 错误原因:ERROR: status { code: SERVER_INTERNAL_ERROR message: "query next from foreign table executor failed, address:10.32.104.16:61657: GetRecordBatch() is not implemented.[query_id:20432057018766398]" } Where: [query_id:20432057018766398] 问题2:extermal_data_set odps location is illegal 又报这个错,能执行回去吗?
Flink中tableEnv执行第二条sql总是会失败 这个是什么情况 执行一个sql是没问题的?
Flink中tableEnv执行第二条sql总是会失败 这个是什么情况 执行一个sql是没问题的?
【数据库设计与实现】第二章:数据前像与回滚
数据前像与回滚
设计原则
事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:
本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的undo
区域;
异地更新方案:不在原记录上更新,而是克隆1
条新记录出来,然后在新记录上进行更新,原记录就是前像;
Oracle和MySQL采用的都是本地更新方案,本章加入PostgreSQL,从而能够深入地了解异地更新方案的优缺点。在设计数据前像和回滚方案时,有如下几点需要考虑:
记录前像对正常写操作的影响有多大;
根据前像进行回滚,回滚的效率如何;
前像占用的空间如何,回收效率如何;
前像是高价值数据,可以基于前像提供一致性读(MVCC
)、闪回等功能,前像的设计是否能高效地实现这些功能;
Oracle设计原理
开启一个事务
图2.2-1 undo segment header block结构
在开启事务之前,我们先了解一下undo segment header block的总体结构,如图2.2-1所示,undo segment header block的组成情况如下:
undo segment header block
既是1
个block
,又是1
个segment header
,所以cache layer
、footer
、segment control header
、extent map
也都是存在的(图中未标出),详细情况可以参考“空间管理与数据布局章节”;
retention table
:记录各extent
最近一次提交的时间,从而在extent
到期时回收空间给后继事务使用,详细情况见表2.2-1
;
transaction control
:记录本undo segment
中事务的总体信息,详细情况见表2.2-2
;
free block pool
:记录当前使用过且有空闲空间的undo block
,优先使用这些block
(前面的事务未用满),详细情况见表2.2-3
;
transaction table
:记录本undo segment
中各个事务的详细信息,详细情况见表2.2-4
;
表2.2-1 retention table部分关键信息
域
含义
Extent number
extent号
Commit time
最近的提交时间,1970.1.1起始的秒数
表2.2-2 transaction control部分关键信息
域
长度
含义
seq
2
新事务第1条undo记录所在block的seq,与uba中的seq相对应
chd
2
transaction table中的index,指向最老的事务,即优先被替换的事务
ctl
2
transaction table中的index,指向最新的事务,即最后被替换的事务
nfb
2
free block pool中的undo block数量,0表示没有空闲的undo block
uba
7
新事务的第1条undo记录的地址
scn
6
transaction table中最近1次被替换事务的提交scn,用于上界提交
表2.2-3 free block pool部分关键信息
一级域
二级域
含义
Array(*n)
uba
Undo block的地址
ext
Undo block所在的extent
spc
Undo block的空闲空间,单位字节
表2.2-4 transaction table部分关键信息
域
长度
含义
index
0
事务的槽位号,对应于事务表的下标,不占用实际空间,事务id的组成部分之一
state
9 unactive;10 active
cflag
1
0x00 无事务;0x10 死事务;0x80 active事务;0x90 正在被回滚的死事务
wrap
2
本槽位每被事务复用1次加1,事务id的组成部分之一
uel
2
指向下1个事务的槽位
scn
6
事务提交时的scn,部分oracle版本在事务未提交时记录事务开始时的scn
dba
4
指向事务最近1条undo记录对应的data block
nub
4
事务使用的undo block数量
cmt
事务提交时的时间
开启1个事务时,首先要到undo segment header block的transaction table中申请1个位置,该位置的下标就称为该事务id的组成部分之一。xid由segment no、transaction table index、wrap这3个部分组成。transaction table实际上就是1个大数组,该数组中元素每被复用1次,wrap就会加1。
开启的事务在transaction table中申请到位置后,会一直占用这个位置,直到该事务提交或回滚才有可能被其它事务覆盖。1个undo segment只有1个undo segment header block,受限于block的大小,transaction table一般只能提供34个槽位(随block大小而变化),即同时只能存放34个活跃事务。为此Oracle默认会创建10个undo segment,并根据并发事务的数量以及retention参数动态伸缩undo segment数量。
transaction control和transaction table是undo segment header block中最重要的信息,下面结合实例来讲解事务并发运行过程中,transaction control和transaction table是如何记录undo日志的。假设当前的transaction control和transaction table分别如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index0 ctl=index34 uba=uba99 scn=scn32
TRN TBL:
index0: uel=index1 scn=scn78 dba=dba19 wrap=1
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=0xFF scn=scn899 dba=dba31 wrap=1
从上例我们可以看出:
通过chd
、ctl
、uel
可以将transaction table
中的事务链在一起,这是我们见到第1
个链表,为了说明方便,我们在本章节将其称为链表A
;
chd
指向链表头,ctl
指向链表尾;
整个链的顺序表达了事务提交的顺序,上例中的顺序是人为构造的,过于完美,实际上随着事务的频繁生成和消亡,指向顺序会比较杂乱;
针对上例,新开始的事务需要在此基础上申请1个位置,假设复用后的结果如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index1 ctl=index0 uba=uba342 scn=scn78
TRN TBL:
index0: uel=0xFF scn=0 dba=dba26 wrap=2
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=index0 scn=scn899 dba=dba31 wrap=1
假设新事务生成的第1条undo记录(该undo记录的地址未uda342,仅保留说明原理的关键信息)如下:
uda=uda99 ctl max scn=scn32 prev tx scn=scn78 prev brb=dba19
新事务复用transaction table的过程大致如下:
从transaction control
中找到chd
指针,遍历链表,直至找到第1
个处于提交状态的事务,即scn
最小的提交事务,上例中假设找到的即将被覆盖的位置为index0
;
将index0
从链表A
中摘除,本例是从链首摘除,所以只需要修改chd
,如果是中部或者尾部摘除,情况略有不同;
针对新事务生成该事务的第1
条undo
记录,记录被替换事务的scn
和dba
(分别对应第1
条undo
记录中的prev tx scn
和prev brb
),以及transaction control
中的uda
和scn
(分别对应第1
条undo
记录中的uba
和ctl max scn
);
将transaction control
中的scn
更新为被替换事务的提交scn
,用于将来的“上界提交”,将transaction control
中的uda
更新为新事务的第1
条undo
记录的地址,至此完成链B
的组建,即本undo segment
中的所有事务通过事务的第1
条undo
记录链在一起,根据链表B
可以回溯任何时候的transaction control
和transaction table
(当然前提是undo
日志还没有被清理掉);
根据新事务的信息更新index0
,并在事务提交后将index0
加入到链表A
的尾部;
在上述过程中,已开始生成undo记录,那么就需要申请undo block来存放,undo block的申请规则如下:
查看free block pool
中是否有空闲undo block
,有则使用,否则转下一步;
当前extent
有空闲undo block
,有则使用,否则转下一步;
当前segment
存在过期extent
,重置该extent
(wrap
加1
),然后使用该extent
,否则转下一步;
如果undo tablespace
有空闲空间,从中分配1
个新的extent
,并加入到本segment
中并使用,否则转下一步;
从offline
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
从online
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
如果undo tablespace
可以扩展,扩展undo tablespace
,将新extent
加入当前segment
中并使用,否则转下一步;
如果undo segment
中的retention
时间设置为非担保,自动调整undo segment
的保留时间,每次降低10%
,寻找过期extent
,如此循环直至找到;
Undo Block与Data Block
图2.2-2 undo block结构
通过上节介绍的方法找到了空闲的undo block,并向该undo block写入了本事务的第1条undo记录。对于单个undo block来说,同时只能属于1个活跃事务,所以从单个事务来看,其undo记录是相对集中的。当然如果事务已经提交,而该事务并没有用满某个undo block,该undo block的剩余空间会被其它事务使用。
undo block的结构如图2.2-2所示(忽略了cache layer和footer,这对所有block来说都是一样的):
control section
:记录本undo block
的总体信息,详细情况见表2.2-5
;
record directory
:undo
记录的目录,array
型,下标是uba
的地址,指向undo
记录在record heap
中的位置,空间从上向下增长(uba
由3
个部分组成,undo dba. seq no. record no
,通过undo dba
定位到undo block
,通过record no
定位到目录,从而找到在record heap
中的undo
记录);
record heap
:存放具体的undo
记录,空间从下向上增长,详细情况见表2.2-6
;
表2.2-5 control section部分关键信息
域
长度
含义
xid
8
最近1次操作本undo block的事务id
cnt
1
本undo block中的undo记录数
irb
1
指向事务(对应于xid)的最近1条undo记录,回滚的起始点
表2.2-6 record heap部分关键信息
一级域
二级域
含义
record header
rec
本undo记录在record directory中的下标,不占用实际空间
undo type
组合标志位:
regular undo
:正常的
undo
记录;
begin trans
:含事务开始信息;
user undo applied
:
undo
记录已用于回滚,防止重复回滚;
objn
本undo记录所涉data block对应的data object id
objd
object id
tblspc
tablespace no
rci
指向同1个事务的上1条undo记录,0x00表示最后1条undo记录
rdba
用于单个undo block无法容纳事务的所有undo记录的场景,rdba指向本事务的下1个undo block此情况一般仅出现在undo block的第1条undo记录中,其他undo记录该值都为0x00000000
undo segment header
uba
用于恢复transaction control中被覆盖的uba
ctl max scn
用于恢复transaction control中被覆盖的scn
prev tx scn
用于恢复transaction table中被覆盖的scn
txn start scn
记录新事务开始时的scn
prev brb
用于恢复transaction table中被覆盖的dba
data block ITL
xid
同data block中的ITL相对应,用于恢复某条ITL
uba
flg
lck
scn
data block record
kdo op code
IRP
插入单行;
DRP
删除单行;
LKR
锁定行;
URP
更新行;
ORP
行链接;
CKI
簇键索引
SKL
设置簇主键指针;
QMI
插入多行
QMD
删除多行
bdba
undo记录对应的data block
hdba
undo记录对应的data block归属的segment header block
slot
undo记录所涉记录在data block中row directory的下标
ncol
记录前像数据,用于恢复UPDATE和DELTE所涉列
nnew
size
cols
如表2.2-6所示,单条undo记录包含如下信息:
record header
:记录的总体信息,如所涉data block
对应的dba
、object_id
、tablespace
,并通过rci
将属于同一个事务的所有undo
记录链在一起;
undo segment header
:可选,仅存在于事务的第1
条undo
记录中,用于恢复undo segment
中的transaction control
和transaction table
;
data block ITL
:可选,用于恢复data block
中被本事务覆盖的事务槽位上的事务信息;
data block record
:记录data block
上被修改数据的前像:
update
:仅记录被修改列的前像;
delete
:记录被删除行的所有列的前像;
insert
:仅记录插入行的RowID
(通过objn+bdba+slot
计算得到);
表2.2-7 transaction layer:fixed部分关键信息
域
长度
含义
typ
1
1 数据,2 索引
seg/obj
4
segment, object_id
csn
6
最近一次清除时的scn
itc
1
本data block中ITL的数量,即事务槽的个数,受参数INITRANS和MAXTRANS影响
flg
1
segment空间的管理方式:
E
:
ASSM
;
O
:
Free List
;
fsl
1
本data block的ITL中,第1个空闲的位置
fnx
4
用于缓存管理
表2.2-8 transaction layer:ITL部分关键信息
域
长度
含义
xid
8
事务id,4个字节的undo segment no+2个字节的transaction table slot no+2个字节的transaction table slot wrap
uba
8
本事务的最近1条undo记录的地址,4个字节的undo block地址+2个字节的seq number+1个字节的目录下标+1个字节的预留
flg
1
事务状态,组合标志位:---- 事务处于活跃态
--
事务已提交,锁已被清除
--U- 事务已上界提交---T 相对于csn时刻,事务仍然处于活跃态
lck
1
本事务在本data block中锁定的记录数(行数)
scn/fsc
6
事务已提交,记录提交或上界提交时的
scn
;
事务尚未提交,前
2
个字节记录本
data block
对本事务来说的剩余空间
(free space credit)
现在我们再从data block的角度审视事务的运行过程,在“数据布局与空间管理”章节中,我们知道data block中与事务、数据强相关的部分为transaction layer:fixed+ITL,table directory,row directory,row data。当事务修改某data block中的数据时,首先要在data block的transaction layer:ITL中占用1个新的或者复用1个已处于提交状态的事务槽位。如果是复用槽位,新事务生成的undo记录需要记录被覆盖事务在ITL中的相关(详细情况见2.2-6的data block ITL,ITL槽位数量由initrans指定,普通场景默认2个,create table as select语句默认3个)。至此,我们接触到undo相关的第3个链表,用于按照data block维度将某个data block相关的所有事务的undo链在一起,我们将其称为链C。事务在修改data block中的实际数据时,row data中的lb会记录对应事务的槽位号,这样记录、事务、undo整个1张大网已经组建完成。
图2.2-3 undo链表关系全景图
如图2.2-3所示,我们得到了整个和undo相关的全景图:
链A
:存在于undo segment header block
中,可以将transaction control
和transaction table
中的事务链接在一起,方便事务申请;
链B
:存在于undo segment
内,将本undo segment
内的所有事务通过第1
条undo
记录链接在一起,这样即使因为transaction control
和transaction table
中的事务信息发生了覆盖,所有的历史事务信息都可以通过链B
进行回溯;
链C
:存在于undo
记录中,用于将某个data block
相关的所有事务的undo
链接在一起,这样即使因为transaction layer:ITL
数量有限,事务被频繁覆盖,所有修改过本data block
的所有事务的undo
记录都可以通过链C
进行回溯(为一致性读提供了条件);
对于仍然保留在transaction layer:ITL
中的事务(活跃或已提交)则非常简单,通过xid
可以找到undo segment header block
中的transaction table
中的详细信息,也可以直接通过uba
访问到本事务的undo
记录;
事务提交与Block清除
从undo的角度来看,事务的提交需要做下列工作:
工作1
:找到undo segment header block
,定位到transaction table
中的对应位置,修改事务状态和提交scn
,并记录redo
日志;
工作2
:遍历本事务修改过的所有data block
,在其中的transaction layer:ITL
找到本事务,修改事务状态和提交scn
;
工作3
:遍历本事务修改过的所有data block
,在其中的row data
部分,找到相关行并清理锁信息,记录redo
日志;
工作1是一个常量,和事务修改的data block数量无关,且事务提交时工作1是必须实施的,这也是我们通常说的“事务提交”日志(redo日志)。
工作2的工作仅涉及transaction layer:ITL,位置相对固定,且不用记录redo日志,但又和修改的data block数量强相关,所以情况比较复杂:
Oracle
会在会话内存中跟踪本事务修改过的data block
(有说最多1000
个block
,有说10%
左右的缓存大小),事务提交时这些data block
如果还在内存中,直接修改这些data block
中的ITL
,将flg
打上C
标志,表示事务已经提交,更新scn
为事务的提交scn
;
对于超出跟踪数量的data block
,或者已经被换出缓存的data block
,Oracle
为了提高commit
速度,不再处理这些data block
,而是采用延迟块清除策略,即将更新事务状态和scn
的工作延迟到将来再做。当将来某个事务读到该data block
时,发现ITL
中事务处于活跃状态:
读取对应的undo segment header block
,发现该事务还处于transaction table
中,且已经提交,将ITL
中该事务的flg
打上C
标志,并更新事务的提交scn
;
读取对应的undo segment header block
时,发现该事务所在的transaction table
已经被覆盖,将ITL
中该事务的flg
打上CU
标志,并事务的提交scn
更新为transaction control
中的scn
,这就是“上界提交”,即在该scn
时刻事务一定已经提交了,但也可能在这之间已经提交。Oracle
通过上界提交在效率和准确度之间做了一个平衡,上界提交通过最小的代价找到尽可能老的scn
,将来一致性读构建CR
时,如果scn
已经满足要求那最好,如果不满足要求再通过undo
(链B
)进一步回溯,找到更新精准的提交scn
(实际上找到满足一致性判断要求的scn
即可);
可见对于工作2,Oracle采取了综合措施,在commit效率和ITL准确度之间做了平衡。延时块清除将大量工作分解到将来的各个data block的读过程中。工作3是最耗时的,既要找到data block中的行记录(行记录会在data block内部移动),又要记录redo日志,Oracle的策略是将工作3延期分解到将来的data block读上,这也是为什么读操作有时也产生redo日志的原因。
用户回滚与重启回滚
用户回滚时,其中一个线索是通过undo segment header block中的transaction table找到该事务,再根据其中记录的dba获取本事修改的最近的data block,通过该data block中的ITL:uda获取该事务的最后1条undo记录,而undo记录是链在一起的,至此可以顺利实施回滚。实际执行用户回滚更加容易,因为内存中拥有完备的跟踪信息,可以加速上述过程。
重启回滚和上述类似,不同的是不存在内存加速(重启了),而是通过扫描所有undo segment header block获得所有待回滚事务及其入口信息。
另外需要说明的是实施回滚时除了data block的修改要记录redo日志外,undo block中的undo记录也要打上“user undo applied”标签,也需要记录redo日志,防止重复回滚。
一致性读
本节仅讲述Oracle基于undo记录构建一致性读的过程(构建CR),至于背后的理论基础请参考“事务”章节。假设事务A的启动scn为scn_a,事务A读取某个data block时,首先对该data block做延迟块清除。如果ITL中的事务都处于提交状态,且scn都小于scn_a则不需要构建CR,直接读取即可,否则需要构建CR:
步骤1
:在缓存区中对当前data block
做一个拷贝,即CR
;
步骤2
:反向更改所有未提交事务;
步骤3
:如果有事务提交scn
大于scn_a
,应用该事务的undo
日志;
重复上述步骤3
,直至所有事务的scn
小于等于scn_a
;
因为构建CR并不是实际更新数据,所以不需要记录undo和redo。至于CR版本的生命周期,Oracle综合应用了数量控制(有说1个data block至多3个CR)和缓存控制策略。
闪回
既然数据库内部可以通过undo记录构建CR版本实现读取历史数据的功能,该功能自然可以开放给用户直接使用,这就是闪回查询。当然还可以更进一步,对表进行闪回,即将数据回退到过去某个时间,解决用户误操作问题。闪回drop和闪回database有所不同,前者是将recyclebin中的对象重新可见,后者除了将前像放在undo tablespace中,还想日志归档一样,将前像归档到闪回日志中,用于更大规模的闪回,但基本原理是一致的。
MySQL设计原理
开启一个事务
事务的开启与undo segment强相关,但MySQL的特殊地方在于发明了rollback segment和undo log segment这2个概念。rollback segment用于存放总体信息,而undo log segment用于存放具体的undo日志记录。
图2.3-1 rollback segment page结构
表2.3-1 ROLLBACK PAGE HEADER结构
域
长度
含义
TRX_RSEG_MAX_SIZE
4
rollback segment允许使用的最大page数,当前值为ULINT_MAX
TRX_RSEG_HISTORY_SIZE
4
history list上undo page数量
TRX_RSEG_HISTORY
16
链表指针头,指向history list链表
TRX_RSEG_FSEG_HEADER
10
本rollback segment对应的INODE Entry的地址
rollback segment只有1个page(type为FIL_PAGE_TYPE_SYS),如图2.3-1所示,rollback segment page由如下关键部分组成:
TRX_RSEG_UNDO_SLOTS
:每个slot
对应1
个undo log segment
,1
个rollback segment
可以管理1024
个undo log segment
。slot
本身仅占用4
个字节,记录是undo log segment header page
的page no
,没有space id
,所以rollback segment
及其管理的undo log segment
必须在同一个undo tablespace
中;
ROLLBACK PAGE HEADER
:存放了rollback segment
的总体信息,详细情况如表2.3-1
所示,其中最重要的是TRX_RSEG_HISTORY
,其为链首指针,通过和UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
组成history list
链表,将所有归属于本rollback segment
且已经提交的undo log
链接在一起;
图2.3-2 rollback segment 总体布局
开始事务需要在rollback segment中找到1个slot,那又是如何找到rollback segment的呢?如图2.3-2所示,在“空间管理与数据布局”章节中我们知道系统tablespace中有1个sys trx segment,该segment中的sys trx segment page中存放了128个RSEG地址,其中:
第1
个rollback segment
预留在系统tablespace
中;
第2~33
个rollback segment
存放在临时tablespace
中;
第34~128
个rollback segment
存放在独立的undo tablesapce
中;
如此就可以获得所有rollback segment的入口。开始事务时,MySQL默认为只读事务,当后继发生写操作时,转换为读写模式,为事务分配事务id和rollback segment,分配方式如下:
如果事务操作了临时表,为其分配临时表rollback segment
(2-33
,临时表的undo
无需记录redo
);
采用round-robin
的方式分配rollback segment
(34~128
),不过在遍历的过程中,如果某个rollback segment
的history list
过长,意味着该rollback segment
已经非常大了,会跳过该rollback segment
,继续遍历下一个rollback segment
;
Undo Log Segment
在上一节我们知道MySQL定义了rollback segment和undo log segment两种segment,rollback segment负责管理作用,undo log segment则负责记录具体的undo日志。
图2.3-3 undo log segment header page & normal undo log page
undo log segment包含1个header page和若干个normal page,rollback segment中的TRX_RSEG_UNDO_SLOTS记录的就是header page的page no。如图2.3-3所示,两种page基本类似,只是header page多了1个UNDO LOG SEGMENT HEADER,而normal page对应的区域为空。下面详细了解一下各个组成部分之间的关系:
UNDO LOG PAGE HEADER
用于描述本page
的总体信息,详细情况见表2.3-3
,而UNDO LOG SEGMENT HEADER
则描述本segment
的总体信息,详细情况见表2.3-2
;
MySQL
出于效率考虑,将undo log segment
分为TRX_UNDO_INSERT
和TRX_UNDO_UPDATE
。因为一旦事务提交完成,insert
的undo
日志就可以释放,不需要用于多版本并发控制,将insert
区分出来,可以集中管理,高效释放;
1
个undo log segment
同时只能被1
个事务使用,1
个事务可以使用多个undo log segment
(例如事务既有delete
,也有insert
,就需要申请2
个undo log segment
,分别记录delete
和insert
的undo
日志)。这样在极限情况下,系统最多支持((128-32)*1024)/2
个并发事务(此数据是假设所有事务都包含insert
和非insert
类写操作);
SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
和PAGE HEADER
中的TRX_UNDO_PAGE_NODE
将本undo log segment
中所有的undo page
链接在一起,方便管理;
表2.3-2 UNDO LOG SEGMENT HEADER结构
域
长度
含义
TRX_UNDO_STAT
2
undo log segment的当前状态:
TRX_UNDO_ACTIVE
:本
segment
上的事务处于活跃态;
TRX_UNDO_CACHED
:本
segment
上的事务已提交,本
segment
可以被复用;
TRX_UNDO_TO_FREE
:本
segment
上的事务已提交,本
segment
可以被立刻释放;
TRX_UNDO_TO_PURGE
:本
segment
上的事务已提交,本
segment
等待被
purge
;
TRX_UNDO_PREPARED
:本
segment
上的事务提交过程中,当前处于
prepare
阶段(
MySQL
需要协调
MySQL
上层和
InnoDB
,所以事务提交采用的是二阶段提交);
TRX_UNDO_LAST_LOG
2
最近1条undo log的header在page内的偏移
TRX_UNDO_FSEG_HEADER
10
记录本segment对应的INODE Entry的地址
TRX_UNDO_PAGE_LIST
16
链表头,将所有属于本undo log segment的page链接在一起
表2.3-3 UNDO LOG PAGE HEADER结构
域
长度
含义
TRX_UNDO_PAGE_TYPE
2
undo日志的类型:
TRX_UNDO_INSERT
:针对
insert
;
TRX_UNDO_UPDATE
:针对非
insert
;
TRX_UNDO_PAGE_START
2
本page中最近1条undo日志的位置
TRX_UNDO_PAGE_FREE
2
本page中空闲空间的偏移量
TRX_UNDO_PAGE_NODE
12
链表的双向指针,用于将本undo log segment中的所有page链接在一起
图2.3-4 UNDO LOG结构
表2.3-4 UNDO LOG HEADER结构
域
长度
含义
TRX_UNDO_TRX_ID
8
产生本条undo log的事务id
TRX_UNDO_TRX_NO
8
仅对update undo日志有效,加入history list的UNDO LOG RECORD数量
TRX_UNDO_DEL_MARKS
2
是否有delete mark操作
TRX_UNDO_LOG_START
2
UNDO LOG RECORD开始的位置
TRX_UNDO_DICT_OPERATION
2
是否是DDL
TRX_UNDO_TABLE_ID
8
若是DDL,记录table id
TRX_UNDO_NEXT_LOG
2
下一个undo log header的位置
TRX_UNDO_PREV_LOG
2
上一个undo log header的位置
TRX_UNDO_HISTORY_NODE
12
双向指针,用于构建history list链表
表2.3-5 INSERT型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型,取值TRX_UNDO_INSERT_REC表示INSERT型
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
START
2
本条undo log record的起始位置
表2.3-6 UPDATE型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型及标志位标志位:
UPD_NODE_NO_ORD_CHANGE
:操作是否修改了二级索引的列;
TRX_UNDO_UPD_EXTERN
:是否涉及
extern
列;
日志类型:
TRX_UNDO_DEL_MARK_REC
:删除操作,并不实际删除,而是在原记录上打上删除标签;
TRX_UNDO_UPD_EXIST_REC
:更新操作;
TRX_UNDO_UPD_DEL_REC
:更新已经被删除的记录,即记录已经打上删除标签,这时再插入相同主键的记录;
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
INFO_BITS
1
标志位
DATA_TRX_ID
compact
修改前,原记录上的事务id(隐藏列)
DATA_ROLL_PTR
compact
修改前,原记录上的回滚指针(隐藏列)
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
UPDATE VECTOR
var
记录被更新列的前像(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
N_BYTES_BELOW
compact
INDEX_COLUMS部分的总长度
INDEX_COLUMS
var
所有二级索引的前像(包括所有主键列和索引列)(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
START
2
本条undo log record的起始位置
如图2.3-4所示,每条undo log由1个undo log header和若干条undo log record组成,每条undo log record保持数据的前像,和data block中的一条row data相对应,具体情况如下:
undo log header
:存放本条undo log
的总体信息,详细情况如表2.3-4
所示,其中TRX_UNDO_NEXT_LOG
和TRX_UNDO_PREV_LOG
用于将本undo log segment header page
中的所有undo log header
链接在一起(undo log segment
可以被事务复用),而TRX_UNDO_HISTORY_NODE
是history list
链表的组成部分,用于将所有提交事务的undo log header
(不含insert
型)链接在一起,为将来purge
打好基础;
undo log record
:分为insert
型和update
型,存放在不同的undo log segment
中,insert
型不用于构造MVCC
,事务提交后对应的undo log segment
就可以立刻释放;
delete和update将来都需要用于构造MVCC,所以对应的undo log record不能立刻释放。原始data page中的每条记录保存有当前事务id和指向undo log record的回滚指针,这就要求删除操作不能真正的实施,否则隐藏列无处存放。undo log record还保存了该条数据记录隐藏列上的前事务id和回滚指针,这样就可以回溯到该记录的任意历史版本(只要undo log record还在)。对于二级索引来说,二级索引上的update,MySQL都是转换为delete+insert。有了这些背景之后,我们再来看MySQL在undo log record记录了哪些内容:
delete
:生成类型为TRX_UNDO_DEL_MARK_REC
的undo log record
,主键索引和二级索引都不实施真正的删除,都是打上删除标签,undo log record
中需要记录主键值(PRIMARY KEY
)和二级索引列值(INDEX_COLUMS
);
update
:生成类型为TRX_UNDO_UPD_EXIST_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
)。二级索引上的update
实际上是delete+insert
,所以需要记录二级索引的全部前像;
insert
:生成类型为TRX_UNDO_INSERT_REC
的undo log record
,记录主键值(PRIMARY KEY
)。还有一种特殊情况,某条记录已经被打上删除标签,这时再次插入相同主键的行,这时生成类型为TRX_UNDO_UPD_DEL_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
);
图2.3-5 UNDO全景图
最后我再回顾总结一下undo的全貌,如图2.3-5所示:
每个rollback segment
有1
个header page
,每个undo log segment
有1
个header page
和若干个normal page
;
每个事务可以有多个undo log
,但单个undo log
只能属于1
个事务。对于某个具体的undo log segment
来说,1
个事务只有1
个undo log
;
1
个undo log segment
同时只能属于1
个事务,但当undo log segment
仅使用了header page
,且该header page
至少有1/4
空闲空间时,该undo log segment
在上一个事务提交后可以被下一个事务复用,所以有时header page
中有多个undo log header
;
链A
:每个rollback segment
通过1024
个slots
可同时管理1024
个活跃态的undo log segment
;
链B
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
结合UNDO LOG PAGE HEADER
中的TRX_UNDO_PAGE_NODE
,将本undo log segment
中的所有undo page
链接在一起;
链C
:UNDO LOG HEADER
中的TRX_UNDO_LOG_START
结合UNDO LOG RECORD
中的NEXT
和START
,将本undo log
中的所有undo log record
链接在一起;
链D
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_LAST_LOG
结合UNDO LOG HEADER
中的TRX_UNDO_NEXT_LOG
、TRX_UNDO_PREV_LOG
,将本undo log segment
中的所有undo log
链接在一起;
链E
:ROLLBACK PAGE HEADER
中的TRX_RSEG_HISTORY
结合UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
,将所有已经提交的undo log
链接在一起(不含insert
和cached
);
链F
:user records
中的DATA_ROLL_PTR
隐藏列结合undo log record
中的DATA_ROLL_PTR
,将和某条记录相关的所有undo log record
链接在一起,为MVCC
打下基础;
事务提交
MySQL和InnoDB是松耦合结构,为了协调上下层的提交一致性,采用了2阶段提交。不过从undo的角度来看,prepare阶段的工作不多,但还是要将UNDO LOG SEGMENT HEADER中的TRX_UNDO_STAT从TRX_UNDO_ACTIVE改为TRX_UNDO_PREPARED,并更新其它一些信息。重点工作发生在commit阶段:
如果是insert
型undo log segment
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_FREE
,后继直接释放;
如果是update
型undo log segment
,只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_CACHED
,并将本undo log segment
加入到undo cache list
中,以供将来复用(undo cache list
是内存结构);
如果是update
型undo log segment
,不满足只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_PURGE
,同时将rollback segment
中对应的slot
置为FIL_NULL
,并将本undo log segment
(undo log header
)加入到TRX_UNDO_HISTORY_NODE
中(链E
),等待被清理(不再用于MVCC
后即可被清理);
用户回滚与重启回滚
当用户主动或者数据库内部原因对事务发起回滚时,内存中保存有相关对象和指针,直接进行逆向操作即可,主要包括如下动作:
二级索引对主键索引有依赖关系,所以先回滚二级索引,再回滚主键索引;
对于打上删除标记的记录,清理删除标记;
对于更新操作,将数据回退到前像版本;
对于插入操作,直接删除记录;
重启回滚涉及的动作和用户回滚类似,但重启内存丢失,需要根据以下原则找到回滚记录:
遍历所有rollback segment
,根据每个rollback segment
中的slot
遍历undo log segment
;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_ACTIVE
,表示需要回滚,按照类似用户回滚的动作进行回滚;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_PREPARED
,比较事务的XID
与binlog
,如果binlog
已经完成,则走提交流程,否则走回滚流程;
一致性读
RR隔离级别下一致性读的原则:事务开始时所有已经提交的事务对本事务来说都是可见的,所有未提交以及将来启动的事务对本事务来说都是不可见的。为此,开启事务时需要根据当前系统的活跃事务情况构建一个属于本事务的ReadView,包含如下要素:
up_limit_id
:最小活跃事务id
,比该id
小的事务对本事务来说都是可见的;
low_limit_id
:最大事务id
,比该事务id
大的事务都是在本事务开启之后生成的,所以对本事务来说都是不可见的;
trx_ids
:trx_ids
为本事务开启时系统活跃事务列表的快照,对于up_limit_id
和low_limit_id
之间的所有事务,如果在trx_ids
中表示本事务开启时该事务处于活跃态,尚未提交,所以不可见,否则可见;
图2.3-6 可见性判断
数据记录中有事务id和回滚指针的隐藏列,而每条undo log record中也有上1个事务id和指向上1个undo log record的指针,通过这些事务id和回滚指针,按照图2.3-6中的原则就可以进行无锁的一致性读。在RC场景下,原则和RR场景基本一致,不同的是RR在事务开始时建立ReadView,而RC在每条SQL语句开始前都要建立一个新的ReadView。
Purge
一致性读机制不再读到的这些历史版本就需要及时被清理掉,以便及时释放出undo空间。为此,获得系统中最老的ReadView,比该ReadView中up_limit_id小的事务对应的undo log都可以被清理掉。方法是遍历各个rollback segment的TRX_RSEG_HISTORY链表,链表中事务id小于up_limit_id的undo log都可以被清理掉。清理的动作主要包括2个方面:
清理记录:清理二级索引和主键索引中删除标记为真的记录;
清理undo
:清理undo log
和undo log segment
;
PostgreSQL设计原理
Page与前像设计
图2.4-1 page结构
表2.4-1 PageHeaderData结构
域
长度
含义
pd_lsn
8
本page被修改的lsn
pd_checksum
2
校验值,检查page异常
pd_flags
2
Page状态,组合标志位:
PD_HAS_FREE_LINES(0x0001)
:
ItemIdData
中有未使用的指针;
PD_PAGE_FULL(0x0002)
:
page
满;
PD_ALL_VISIBLE(0x0004)
:本
page
中所有记录都可见;
pd_lower
2
Free space的起始位置
pd_upper
2
Free space的结束位置
pd_special
2
Special space的位置
pd_pagesize_version
2
高位字节表示page大小,低位字节表示版本号
pd_prune_xid
4
本page最老的事务id,vacuum时使用
表2.4-2 ItemIdData结构
域
长度
含义
lp_off
15bit
记录在Items中的位置
lp_flag
2bit
Item状态:
LP_UNUSED(0)
:未使用;
LP_NORMAL(1)
:已使用;
LP_REDIRECT(2)
:
HOT
重定向;
LP_DEAD(3)
:
Dead
记录;
lp_len
15bit
在Items中的长度
PostgreSQL采用异地更新方案,前像没有存放在独立的undo区域中,而是和数据一起存放在data page中。因此,在研究PostgreSQL回滚前,有必要先了解一下page结构。如图2.4-1所示,page由如下部分组成:
PageHeaderData
:头部,定义了整个page
的总体信息,详细情况见表2.4-1
;
ItemIdData
:目录信息,每行记录占1
个位置,其下标是后继定位该记录的重要组成部分,详细情况见表2.4-2
,随着记录的增加,目录从上向下增加;
Items
:用户数据,随着记录的增加,数据从下向上增加;
Special Space
:存放和访问方法相关的数据,如对于BTree
索引存放左右兄弟节点的指针,普通表page
为空;
图2.4-2 Item结构
表2.4-3 HeapTupleHeaderData结构
域
长度
含义
t_xmin
4
insert本记录的事务id
t_xmax
4
delete/update本记录的事务id
t_cid/t_xvac
4
Insert/delete时的事务id和命令id
t_ctid
6
本记录下一个版本的地址,如果自己是最新版本则指向自己
t_infomask2
2
低11位表示当前记录的列数;其它位为标志位:
HEAP_KEYS_UPDATED(0x2000)
:被更新或删除;
HEAP_HOT_UPDATED(0x4000)
:
HOT-updated
;
HEAP_ONLY_TUPLE(0x8000)
:
heap only tuple
;
HEAP_XACT_MASK(0xE000)
:用于可见性判断;
t_infomask
2
组合标志位:
HEAP_HASNULL(0x0001)
:有
NullBitmap
域;
HEAP_HASVARWIDTH(0x0002)
:有变长列;
HEAP_HASEXTERNAL(0x0004)
:有外部存储列;
HEAP_HASOID_OLD(0x0008)
:有
object id
域;
HEAP_XMAX_KEYSHR_LOCK(0x0010)
:
t_xmax
状态;
HEAP_COMBOCID(0x0020)
:
t_cid
是
combo id
;
HEAP_XMAX_EXCL_LOCK(0x0040)
:
t_xmax
状态;
HEAP_XMAX_LOCK_ONLY(0x0080)
:
t_xmax
状态;
HEAP_XMIN_COMMITTED(0x0100)
:
t_xmin
状态;
HEAP_XMIN_INVALID(0x0200)
:
t_xmin
状态;
HEAP_XMAX_COMMITTED(0x0400)
:
t_xmax
状态;
HEAP_XMAX_INVALID(0x0800)
:
t_xmax
状态;
HEAP_XMAX_IS_MULTI(0x1000)
:
MultiXactId
;
HEAP_UPDATED(0x2000)
:
Updated Version
;
t_hoff
1
记录头大小
Item存放一条具体的记录,如图2.4-2所示,Item由HeapTupleHeaderData、NullBitmap、ObjectId、UserData 4个部分组成:
HeapTupleHeaderData
:记录头,固定大小,t_xmin
和t_xmax
用于判断记录在事务间的可见性判断,t_cid
用于记录在本事务内的可见性判断,t_ctid
用于将记录的多个版本链接在一起,详细情况见表2.4-3
;
NullBitmap
:可选,t_infomask
中HEAP_HASNULL
标志位为真时有此字段,用于表达相关列是否为null
;
ObjectId
:可选,t_infomask
中HEAP_HASOID_OLD
标志位为真时有此字段;
UserData
:真正的用户数据;
图2.4-3 前像设计示例
了解了PostgreSQL的page和item设计,下面来看PostgreSQL是如何进行异地更新的,如图2.4-3所示,DML主要包括下面3种情况:
Insert
:insert
直接插入数据,不需要记录前像,t_xmin
记录插入本记录的事务id
;
Delete
:delete
不进行实际删除,记录本身就是前像,t_xmax
记录删除本记录的事务id
;
Update
:不进行本地更新,而是克隆1
个新记录,然后在新纪录上更新(实际上是delete+insert
),旧记录就是前像,并将旧记录的t_ctid
指向新记录的位置,从而形成一个旧记录指向新记录的链表;
可见PostgreSQL通过上述方法保存了前像,同时通过clog、记录中的ItemIdData.lp_flag、记录中的t_infomask保存相关事务的状态,这样就可以合理地应用保存的前像。当然记录中的ItemIdData.lp_flag和t_infomask的状态可能不是最新的,需要后台根据clog中的真实状态进行刷新。
用户回滚
PostgreSQL采用异地更新的最大优势就是回滚。由于前像仍然以记录的方式存在于数据当中,所以用户回滚只需要记录事务的状态,释放相关资源即可,非常高效。至于遗留下来的垃圾数据,通过后台进程逐步清理即可。
一致性读
和MySQL类似,PostgreSQL也采用基于事务id的一致性读,该方法需要获取当前的活跃事务id列表,PostgreSQL将其称为transaction snapshot。RR或者SERIALIZABLE隔离级别下,事务在启动时会生成本事务的transaction snapshot。RC隔离级别下,每条SQL语句执行前生成最新的transaction snapshot。
由于PostgreSQL采用的是异地更新,一致性判断的规则更加复杂,需要综合判断事务状态、t_xmin、t_xmax和transaction snapshot,具体规则如下:
类别1
:t_xmin
的事务状态为ABORTED
插入此条记录的事务已经回滚,所以本记录对任何事务都不可见。
类别2
:t_xmin
的事务状态为IN_PROGRESS
R1: t_xmin=current_txid & t_xmax=INVALID -> visible
R2: t_xmin=current_txid & t_xmax!=INVALID -> invisible
R3: t_xmin!=current_txid -> invisible
R1表示当前事务读取本事务之前插入的数据,所以可见。R2表示该记录虽然是本事务插入的,但已被本事务删除,所以不可见。R3表示当前事务读取其它事务插入的记录,且其它事务尚未提交,所以不可见。
类别3
:t_xmin
的事务状态为COMMITTED
R4: snapshot(t_xmin)=active -> invisible
R5: t_xmax=INVALID | status(t_xmax)=ABORTED -> visible
R6: status(t_xmax)=IN_PROGRESS & t_xmax=current_txid -> invisible
R7: status(t_xmax)=IN_PROGRESS & t_xmax!=current_txid -> visible
R8: status(t_xmax)=COMMITTED & snapshot(t_xmax)=active -> visible
R9: status(t_xmax)=COMMITTED & snapshot(t_xmax)!=active -> invisible
判断规则是R4到R9依次判断,一旦满足条件就以此结果为依据。R4表示插入这条记录的事务虽然已经提交了,但该事务仍然在snapshot中,所以不可见。R5表示没有其它事务更新过该记录或者更新过该记录的事务已经回滚,所以可见。R6表示本事务已经更新过该记录,所以该记录的上一个版本不可见。R7表示其它事务正在更新该记录,所以该记录的上一个版本可见。R8表示更新或删除该记录的事务已经提交,但该事务在snapshot中,所以可见。R9表示更新或删除该记录的事务已经提交,但该事务不在snapshot中,需要看更新的版本,所以不可见。
HOT与Index-Only Scan
图2.4-4 索引与记录关系示意图(非HOT)
如图2.4-4所示,由于记录采用了异地更新,当记录生成一个新的版本时,为了保持同步,索引项不管有没有发生变化,都需要生成一个对应的新版本,和相应的数据记录对应起来。这导致了索引不必要的浪费,并为后继的vacuum带来了额外的负担。
图2.4-5 索引与记录关系示意图(HOT)
图2.4-6 索引与记录关系示意图(HOT, After Prune)
图2.4-7 索引与记录关系示意图(HOT, After Defragmentation)
HOT指当更新同时满足:1)新版本的记录和旧版本的据在同一个page内;2)更新的列不涉及索引列时,索引不再生成新版本,从而解决索引成本过高的问题。上述条件2很容易理解,条件1的主要目的是平衡读性能,否则通过索引访问很可能需要2次page调度,即先通过索引定位到旧版本,然后再通过旧版本的c_ctid或者ItemIdData中重定向定位到新版本。
如图2.4-5所示,HOT生效时索引仍然指向旧记录,新记录通过旧记录的t_ctid访问,同时旧记录的t_infomask2打上HEAP_HOT_UPDATED标志,新记录的t_infomask2打上HEAP_ONLY_TUPLE标志。当旧记录的状态为dead后,图2.4-6和图2.4-7分别给出修剪(prune)和去碎片化(defragmentation)实施后的情况,旧记录的ItemIdData保留,并重定向到记录的ItemIdData上。
Index-Only Scan指索引中已包含查询所需所有列,原则上直接扫描或定位索引page,无需扫描或定位数据page即可完成本次查询。PostgreSQL的一致性判断规则复杂,且依赖于数据page中各记录的t_xmin、t_xmax及其状态信息,从而导致Index-Only Scan无法实施。为此,PostgreSQL采用了一个简单直接的办法,在后台维护一个visibility map,一旦确定某个数据page中的所有记录都是可见的,就将该page加入到visibility map中,直接对外可见。这样扫描索引时,如果发现对应的数据page在visibility map中就仅扫描索引,不再访问数据page,否则仍然访问数据page,根据记录的t_xmin和t_xmax及其相应状态进行可见性判断。
Vacuum
从前面几个小节我们知道数据page和索引page都会有过期数据(dead tuple),vacuum的工作主要就是清理这些过期数据。vacuum分为concurrent vacuum和full vacuum,前者允许vacuum期间并发执行其它只读事务,后者不允许vacuum期间执行其它事务,但两者的实现原理是一致的。
concurrent vacuum的主要工作是针对某个特定的table或所有table执行如下任务:
清理dead tuple
,即删除无用的数据前像:
删除数据page
中的dead tuple
,并对数据page
做去碎片化整理;
删除索引page
中指向dead tuple
的索引记录;
冻结过期txids
,PostgreSQL
的txid
只有4
个字节,需要及时清理,以解决循环使用的问题:
冻结过期的txids
;
更新系统catalogs
;
清理不必要的clog
;
其它任务:
更新FSM
和VM
;
更新系统统计项;
concurrent vacuum的大致实现过程如下:
step1
:从table list
中获取某个待实施的table
;
step2
:针对该table
申请UpdateExclusiveLock
,该锁仍然允许其它事务读本table
;
step3
:扫描本table
中所有数据page
,识别出所有dead tuple
,必要时冻结过期txids
;
step4
:删除指向dead tuple
的所有索引记录;
step5
:针对本table
的每个数据page
,重复step6
和step7
;
step6
:删除dead tuple
,并做去碎片化工作;
step7
:更新FSM
和VM
;
step8
:如果数据page
中不再含有任何tuple
,释放该数据page
;
step9
:更新统计信息和catalogs
;
step10
:清理不必要的clog page
和clog
文件;
其中第3步是将扫描的结果存放在内存中(maintenance_work_mem),如果内存已满则开始实施第4步到第8步,然后清理内存,并回到第3步从上次断点开始继续扫描。
总结与分析
首先来看记录前像对正常写操作的影响,下面从DML操作类型和索引的角度来分析:
insert
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
无需做额外处理;
delete
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
只需设置相关状态;
update
:Oracle
和MySQL
会生成1
条undo
记录,仅记录更改列的前像,PostgreSQL
需要完整克隆1
条新记录,并更改旧记录的状态;
索引:Oracle
将索引当数据,数据与索引的处理方式一致。MySQL
的索引不记undo
,而是通过数据的undo
间接记录索引的变化。PostgreSQL
的索引页采用异地更新,但不记录事务状态,同时HOT
方式进行一定程度的优化。当然对于索引来说,索引列的更新涉及索引记录在BTree
中位置的调整,所以Oracle
和MySQL
都是将索引的update
转换为delete+insert
,即异地更新方案;
总的来说Oracle、MySQL、PostgreSQL相差不大,insert、delete操作PostgreSQL更加轻量,而update操作Oracle、MySQL相对较为轻量。但Oracle在设计上相对更加简洁,索引、数据的处理原则基本一致,没有太多的特殊情况要考虑。
其次看回滚效率,PostgreSQL是最高效的。PostgreSQL的前像就再数据之中,只需要记录事务的状态,不需要实施回滚动作。Oracle和MySQL则厚重得多,需要从undo日志中读出前像,并应用到实际数据和索引上,同时还要记录重做日志。
再次看存储效率,我们还是从DML操作类型、数据和索引这几个角度来看:
insert
:Oracle
仅记录RowID
,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
delete
:Oracle
需要记录整条记录的前像,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
update
:Oracle
需要记录被更新的列,MySQL
记录主键值和被更新的列,PostgreSQL
需要拷贝一个完整的新行(所有列);
记录:Oracle
的记录不需要做太多特殊处理,MySQL
需要引入13
个字节的事务id
和回滚指针,PostgreSQL
需要引入22
个字节的t_xmin
、t_xmax
、t_cid
、t_ctid
、t_infomask
、t_infomask2
;
索引:Oracle
的索引处理等同于数据,MySQL
需要将索引的所有列记录在数据的undo
中,并将update
转换为insert+delete
,PostgreSQL
需要生成新的索引记录;
从数据的前像来看,对于insert和delete操作,PostgreSQL基本不消耗存储空间;对于update操作,Oracle和MySQL优于PostgreSQL,因为前者仅需要记录被更新列的前像。对于delete操作,Oracle和MySQL都是在原记录上打上删除标签,但Oracle会在undo中记录整条记录的前像,而MySQL仅记录主键值。虽然MySQL更省空间,但Oracle将undo和数据block清理的工作完全解耦,没有依赖关系,且为将来的闪回提供了可能。从对记录的影响来看,Oracle是最优的,MySQL其次,PostgreSQL最后,虽然单条记录占用的字节数不是太多,但对于大数据量的窄表(表的列数很少,但记录非常庞大)来说,占用的空间会比较可观。
再再次看回收效率。Oracle在undo segment中维护retention table,直接按照时间将相关extent失效即可,不需要做太多的回收动作,block上的事务状态主要集中在ITL上,调整面也比较小。MySQL相对复杂一点,通过rollback segment和undo log segment进行日志管理,1个undo log segment同时只能服务于1个事务,segment的管理成本比Oracle高,但通过history list进行回收,回收涉及的undo page也相对比较集中。Page上基本不涉及事务状态,调整面最小。PostgreSQL的前像和数据存放在一起,需要通过扫描所有page确认dead tuple,成本非常高,事务状态涉及每条记录,调整面也非常大。
最后看一致性读和闪回。Oracle通过undo日志和scn进行一致性判断,简洁高效。MySQL和PostgreSQL分别需要维护read view和transaction snapshot,高并发时事务列表会很长,活跃事务列表的比较效率也较低,且每个事务都需要维护一个read view或transaction snapshot,RC隔离级别下每执行一次SQL还需要更新一次。虽然MySQL和PostgreSQL原理相似,但由于PostgreSQL涉及t_xmin、t_xmax、t_maskinfo、transaction snapshot、clog,判断规则极其复杂,效率最低。在扫描数据和索引时,涉及多个版本(关键看vacuum的频率),虽然VM可以有一定程度的缓解,但效率要低于Oracle和MySQL。
原则上只要前像存在,就可以提供闪回功能。Oracle在系统中维护有逻辑时间(scn),且可以和墙上时间转换,事务提交时记录当时的scn,这样就可以基于scn和墙上时间进行闪回查询和闪回恢复。MySQL和PostgreSQL当前还没有提供闪回功能,提供该功能的挑战在于:1)可以基于LSN进行闪回回退,但需要对当前的前像回收机制进行较大幅度的调整;2)当前的可见性判断机制是基于read view和transaction snapshot的,该机制无法复用到闪回查询,因为用户并不知道历史某个时刻的read view和transaction snapshot。
PDF下载地址:http://blog.itpub.net/69912723/viewspace-2717309/
【数据库设计与实现】第二章:数据前像与回滚
数据前像与回滚
设计原则
事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:
本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的undo
区域;
异地更新方案:不在原记录上更新,而是克隆1
条新记录出来,然后在新记录上进行更新,原记录就是前像;
Oracle和MySQL采用的都是本地更新方案,本章加入PostgreSQL,从而能够深入地了解异地更新方案的优缺点。在设计数据前像和回滚方案时,有如下几点需要考虑:
记录前像对正常写操作的影响有多大;
根据前像进行回滚,回滚的效率如何;
前像占用的空间如何,回收效率如何;
前像是高价值数据,可以基于前像提供一致性读(MVCC
)、闪回等功能,前像的设计是否能高效地实现这些功能;
Oracle设计原理
开启一个事务
图2.2-1 undo segment header block结构
在开启事务之前,我们先了解一下undo segment header block的总体结构,如图2.2-1所示,undo segment header block的组成情况如下:
undo segment header block
既是1
个block
,又是1
个segment header
,所以cache layer
、footer
、segment control header
、extent map
也都是存在的(图中未标出),详细情况可以参考“空间管理与数据布局章节”;
retention table
:记录各extent
最近一次提交的时间,从而在extent
到期时回收空间给后继事务使用,详细情况见表2.2-1
;
transaction control
:记录本undo segment
中事务的总体信息,详细情况见表2.2-2
;
free block pool
:记录当前使用过且有空闲空间的undo block
,优先使用这些block
(前面的事务未用满),详细情况见表2.2-3
;
transaction table
:记录本undo segment
中各个事务的详细信息,详细情况见表2.2-4
;
表2.2-1 retention table部分关键信息
域
含义
Extent number
extent号
Commit time
最近的提交时间,1970.1.1起始的秒数
表2.2-2 transaction control部分关键信息
域
长度
含义
seq
2
新事务第1条undo记录所在block的seq,与uba中的seq相对应
chd
2
transaction table中的index,指向最老的事务,即优先被替换的事务
ctl
2
transaction table中的index,指向最新的事务,即最后被替换的事务
nfb
2
free block pool中的undo block数量,0表示没有空闲的undo block
uba
7
新事务的第1条undo记录的地址
scn
6
transaction table中最近1次被替换事务的提交scn,用于上界提交
表2.2-3 free block pool部分关键信息
一级域
二级域
含义
Array(*n)
uba
Undo block的地址
ext
Undo block所在的extent
spc
Undo block的空闲空间,单位字节
表2.2-4 transaction table部分关键信息
域
长度
含义
index
0
事务的槽位号,对应于事务表的下标,不占用实际空间,事务id的组成部分之一
state
9 unactive;10 active
cflag
1
0x00 无事务;0x10 死事务;0x80 active事务;0x90 正在被回滚的死事务
wrap
2
本槽位每被事务复用1次加1,事务id的组成部分之一
uel
2
指向下1个事务的槽位
scn
6
事务提交时的scn,部分oracle版本在事务未提交时记录事务开始时的scn
dba
4
指向事务最近1条undo记录对应的data block
nub
4
事务使用的undo block数量
cmt
事务提交时的时间
开启1个事务时,首先要到undo segment header block的transaction table中申请1个位置,该位置的下标就称为该事务id的组成部分之一。xid由segment no、transaction table index、wrap这3个部分组成。transaction table实际上就是1个大数组,该数组中元素每被复用1次,wrap就会加1。
开启的事务在transaction table中申请到位置后,会一直占用这个位置,直到该事务提交或回滚才有可能被其它事务覆盖。1个undo segment只有1个undo segment header block,受限于block的大小,transaction table一般只能提供34个槽位(随block大小而变化),即同时只能存放34个活跃事务。为此Oracle默认会创建10个undo segment,并根据并发事务的数量以及retention参数动态伸缩undo segment数量。
transaction control和transaction table是undo segment header block中最重要的信息,下面结合实例来讲解事务并发运行过程中,transaction control和transaction table是如何记录undo日志的。假设当前的transaction control和transaction table分别如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index0 ctl=index34 uba=uba99 scn=scn32
TRN TBL:
index0: uel=index1 scn=scn78 dba=dba19 wrap=1
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=0xFF scn=scn899 dba=dba31 wrap=1
从上例我们可以看出:
通过chd
、ctl
、uel
可以将transaction table
中的事务链在一起,这是我们见到第1
个链表,为了说明方便,我们在本章节将其称为链表A
;
chd
指向链表头,ctl
指向链表尾;
整个链的顺序表达了事务提交的顺序,上例中的顺序是人为构造的,过于完美,实际上随着事务的频繁生成和消亡,指向顺序会比较杂乱;
针对上例,新开始的事务需要在此基础上申请1个位置,假设复用后的结果如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index1 ctl=index0 uba=uba342 scn=scn78
TRN TBL:
index0: uel=0xFF scn=0 dba=dba26 wrap=2
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=index0 scn=scn899 dba=dba31 wrap=1
假设新事务生成的第1条undo记录(该undo记录的地址未uda342,仅保留说明原理的关键信息)如下:
uda=uda99 ctl max scn=scn32 prev tx scn=scn78 prev brb=dba19
新事务复用transaction table的过程大致如下:
从transaction control
中找到chd
指针,遍历链表,直至找到第1
个处于提交状态的事务,即scn
最小的提交事务,上例中假设找到的即将被覆盖的位置为index0
;
将index0
从链表A
中摘除,本例是从链首摘除,所以只需要修改chd
,如果是中部或者尾部摘除,情况略有不同;
针对新事务生成该事务的第1
条undo
记录,记录被替换事务的scn
和dba
(分别对应第1
条undo
记录中的prev tx scn
和prev brb
),以及transaction control
中的uda
和scn
(分别对应第1
条undo
记录中的uba
和ctl max scn
);
将transaction control
中的scn
更新为被替换事务的提交scn
,用于将来的“上界提交”,将transaction control
中的uda
更新为新事务的第1
条undo
记录的地址,至此完成链B
的组建,即本undo segment
中的所有事务通过事务的第1
条undo
记录链在一起,根据链表B
可以回溯任何时候的transaction control
和transaction table
(当然前提是undo
日志还没有被清理掉);
根据新事务的信息更新index0
,并在事务提交后将index0
加入到链表A
的尾部;
在上述过程中,已开始生成undo记录,那么就需要申请undo block来存放,undo block的申请规则如下:
查看free block pool
中是否有空闲undo block
,有则使用,否则转下一步;
当前extent
有空闲undo block
,有则使用,否则转下一步;
当前segment
存在过期extent
,重置该extent
(wrap
加1
),然后使用该extent
,否则转下一步;
如果undo tablespace
有空闲空间,从中分配1
个新的extent
,并加入到本segment
中并使用,否则转下一步;
从offline
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
从online
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
如果undo tablespace
可以扩展,扩展undo tablespace
,将新extent
加入当前segment
中并使用,否则转下一步;
如果undo segment
中的retention
时间设置为非担保,自动调整undo segment
的保留时间,每次降低10%
,寻找过期extent
,如此循环直至找到;
Undo Block与Data Block
图2.2-2 undo block结构
通过上节介绍的方法找到了空闲的undo block,并向该undo block写入了本事务的第1条undo记录。对于单个undo block来说,同时只能属于1个活跃事务,所以从单个事务来看,其undo记录是相对集中的。当然如果事务已经提交,而该事务并没有用满某个undo block,该undo block的剩余空间会被其它事务使用。
undo block的结构如图2.2-2所示(忽略了cache layer和footer,这对所有block来说都是一样的):
control section
:记录本undo block
的总体信息,详细情况见表2.2-5
;
record directory
:undo
记录的目录,array
型,下标是uba
的地址,指向undo
记录在record heap
中的位置,空间从上向下增长(uba
由3
个部分组成,undo dba. seq no. record no
,通过undo dba
定位到undo block
,通过record no
定位到目录,从而找到在record heap
中的undo
记录);
record heap
:存放具体的undo
记录,空间从下向上增长,详细情况见表2.2-6
;
表2.2-5 control section部分关键信息
域
长度
含义
xid
8
最近1次操作本undo block的事务id
cnt
1
本undo block中的undo记录数
irb
1
指向事务(对应于xid)的最近1条undo记录,回滚的起始点
表2.2-6 record heap部分关键信息
一级域
二级域
含义
record header
rec
本undo记录在record directory中的下标,不占用实际空间
undo type
组合标志位:
regular undo
:正常的
undo
记录;
begin trans
:含事务开始信息;
user undo applied
:
undo
记录已用于回滚,防止重复回滚;
objn
本undo记录所涉data block对应的data object id
objd
object id
tblspc
tablespace no
rci
指向同1个事务的上1条undo记录,0x00表示最后1条undo记录
rdba
用于单个undo block无法容纳事务的所有undo记录的场景,rdba指向本事务的下1个undo block此情况一般仅出现在undo block的第1条undo记录中,其他undo记录该值都为0x00000000
undo segment header
uba
用于恢复transaction control中被覆盖的uba
ctl max scn
用于恢复transaction control中被覆盖的scn
prev tx scn
用于恢复transaction table中被覆盖的scn
txn start scn
记录新事务开始时的scn
prev brb
用于恢复transaction table中被覆盖的dba
data block ITL
xid
同data block中的ITL相对应,用于恢复某条ITL
uba
flg
lck
scn
data block record
kdo op code
IRP
插入单行;
DRP
删除单行;
LKR
锁定行;
URP
更新行;
ORP
行链接;
CKI
簇键索引
SKL
设置簇主键指针;
QMI
插入多行
QMD
删除多行
bdba
undo记录对应的data block
hdba
undo记录对应的data block归属的segment header block
slot
undo记录所涉记录在data block中row directory的下标
ncol
记录前像数据,用于恢复UPDATE和DELTE所涉列
nnew
size
cols
如表2.2-6所示,单条undo记录包含如下信息:
record header
:记录的总体信息,如所涉data block
对应的dba
、object_id
、tablespace
,并通过rci
将属于同一个事务的所有undo
记录链在一起;
undo segment header
:可选,仅存在于事务的第1
条undo
记录中,用于恢复undo segment
中的transaction control
和transaction table
;
data block ITL
:可选,用于恢复data block
中被本事务覆盖的事务槽位上的事务信息;
data block record
:记录data block
上被修改数据的前像:
update
:仅记录被修改列的前像;
delete
:记录被删除行的所有列的前像;
insert
:仅记录插入行的RowID
(通过objn+bdba+slot
计算得到);
表2.2-7 transaction layer:fixed部分关键信息
域
长度
含义
typ
1
1 数据,2 索引
seg/obj
4
segment, object_id
csn
6
最近一次清除时的scn
itc
1
本data block中ITL的数量,即事务槽的个数,受参数INITRANS和MAXTRANS影响
flg
1
segment空间的管理方式:
E
:
ASSM
;
O
:
Free List
;
fsl
1
本data block的ITL中,第1个空闲的位置
fnx
4
用于缓存管理
表2.2-8 transaction layer:ITL部分关键信息
域
长度
含义
xid
8
事务id,4个字节的undo segment no+2个字节的transaction table slot no+2个字节的transaction table slot wrap
uba
8
本事务的最近1条undo记录的地址,4个字节的undo block地址+2个字节的seq number+1个字节的目录下标+1个字节的预留
flg
1
事务状态,组合标志位:---- 事务处于活跃态
--
事务已提交,锁已被清除
--U- 事务已上界提交---T 相对于csn时刻,事务仍然处于活跃态
lck
1
本事务在本data block中锁定的记录数(行数)
scn/fsc
6
事务已提交,记录提交或上界提交时的
scn
;
事务尚未提交,前
2
个字节记录本
data block
对本事务来说的剩余空间
(free space credit)
现在我们再从data block的角度审视事务的运行过程,在“数据布局与空间管理”章节中,我们知道data block中与事务、数据强相关的部分为transaction layer:fixed+ITL,table directory,row directory,row data。当事务修改某data block中的数据时,首先要在data block的transaction layer:ITL中占用1个新的或者复用1个已处于提交状态的事务槽位。如果是复用槽位,新事务生成的undo记录需要记录被覆盖事务在ITL中的相关(详细情况见2.2-6的data block ITL,ITL槽位数量由initrans指定,普通场景默认2个,create table as select语句默认3个)。至此,我们接触到undo相关的第3个链表,用于按照data block维度将某个data block相关的所有事务的undo链在一起,我们将其称为链C。事务在修改data block中的实际数据时,row data中的lb会记录对应事务的槽位号,这样记录、事务、undo整个1张大网已经组建完成。
图2.2-3 undo链表关系全景图
如图2.2-3所示,我们得到了整个和undo相关的全景图:
链A
:存在于undo segment header block
中,可以将transaction control
和transaction table
中的事务链接在一起,方便事务申请;
链B
:存在于undo segment
内,将本undo segment
内的所有事务通过第1
条undo
记录链接在一起,这样即使因为transaction control
和transaction table
中的事务信息发生了覆盖,所有的历史事务信息都可以通过链B
进行回溯;
链C
:存在于undo
记录中,用于将某个data block
相关的所有事务的undo
链接在一起,这样即使因为transaction layer:ITL
数量有限,事务被频繁覆盖,所有修改过本data block
的所有事务的undo
记录都可以通过链C
进行回溯(为一致性读提供了条件);
对于仍然保留在transaction layer:ITL
中的事务(活跃或已提交)则非常简单,通过xid
可以找到undo segment header block
中的transaction table
中的详细信息,也可以直接通过uba
访问到本事务的undo
记录;
事务提交与Block清除
从undo的角度来看,事务的提交需要做下列工作:
工作1
:找到undo segment header block
,定位到transaction table
中的对应位置,修改事务状态和提交scn
,并记录redo
日志;
工作2
:遍历本事务修改过的所有data block
,在其中的transaction layer:ITL
找到本事务,修改事务状态和提交scn
;
工作3
:遍历本事务修改过的所有data block
,在其中的row data
部分,找到相关行并清理锁信息,记录redo
日志;
工作1是一个常量,和事务修改的data block数量无关,且事务提交时工作1是必须实施的,这也是我们通常说的“事务提交”日志(redo日志)。
工作2的工作仅涉及transaction layer:ITL,位置相对固定,且不用记录redo日志,但又和修改的data block数量强相关,所以情况比较复杂:
Oracle
会在会话内存中跟踪本事务修改过的data block
(有说最多1000
个block
,有说10%
左右的缓存大小),事务提交时这些data block
如果还在内存中,直接修改这些data block
中的ITL
,将flg
打上C
标志,表示事务已经提交,更新scn
为事务的提交scn
;
对于超出跟踪数量的data block
,或者已经被换出缓存的data block
,Oracle
为了提高commit
速度,不再处理这些data block
,而是采用延迟块清除策略,即将更新事务状态和scn
的工作延迟到将来再做。当将来某个事务读到该data block
时,发现ITL
中事务处于活跃状态:
读取对应的undo segment header block
,发现该事务还处于transaction table
中,且已经提交,将ITL
中该事务的flg
打上C
标志,并更新事务的提交scn
;
读取对应的undo segment header block
时,发现该事务所在的transaction table
已经被覆盖,将ITL
中该事务的flg
打上CU
标志,并事务的提交scn
更新为transaction control
中的scn
,这就是“上界提交”,即在该scn
时刻事务一定已经提交了,但也可能在这之间已经提交。Oracle
通过上界提交在效率和准确度之间做了一个平衡,上界提交通过最小的代价找到尽可能老的scn
,将来一致性读构建CR
时,如果scn
已经满足要求那最好,如果不满足要求再通过undo
(链B
)进一步回溯,找到更新精准的提交scn
(实际上找到满足一致性判断要求的scn
即可);
可见对于工作2,Oracle采取了综合措施,在commit效率和ITL准确度之间做了平衡。延时块清除将大量工作分解到将来的各个data block的读过程中。工作3是最耗时的,既要找到data block中的行记录(行记录会在data block内部移动),又要记录redo日志,Oracle的策略是将工作3延期分解到将来的data block读上,这也是为什么读操作有时也产生redo日志的原因。
用户回滚与重启回滚
用户回滚时,其中一个线索是通过undo segment header block中的transaction table找到该事务,再根据其中记录的dba获取本事修改的最近的data block,通过该data block中的ITL:uda获取该事务的最后1条undo记录,而undo记录是链在一起的,至此可以顺利实施回滚。实际执行用户回滚更加容易,因为内存中拥有完备的跟踪信息,可以加速上述过程。
重启回滚和上述类似,不同的是不存在内存加速(重启了),而是通过扫描所有undo segment header block获得所有待回滚事务及其入口信息。
另外需要说明的是实施回滚时除了data block的修改要记录redo日志外,undo block中的undo记录也要打上“user undo applied”标签,也需要记录redo日志,防止重复回滚。
一致性读
本节仅讲述Oracle基于undo记录构建一致性读的过程(构建CR),至于背后的理论基础请参考“事务”章节。假设事务A的启动scn为scn_a,事务A读取某个data block时,首先对该data block做延迟块清除。如果ITL中的事务都处于提交状态,且scn都小于scn_a则不需要构建CR,直接读取即可,否则需要构建CR:
步骤1
:在缓存区中对当前data block
做一个拷贝,即CR
;
步骤2
:反向更改所有未提交事务;
步骤3
:如果有事务提交scn
大于scn_a
,应用该事务的undo
日志;
重复上述步骤3
,直至所有事务的scn
小于等于scn_a
;
因为构建CR并不是实际更新数据,所以不需要记录undo和redo。至于CR版本的生命周期,Oracle综合应用了数量控制(有说1个data block至多3个CR)和缓存控制策略。
闪回
既然数据库内部可以通过undo记录构建CR版本实现读取历史数据的功能,该功能自然可以开放给用户直接使用,这就是闪回查询。当然还可以更进一步,对表进行闪回,即将数据回退到过去某个时间,解决用户误操作问题。闪回drop和闪回database有所不同,前者是将recyclebin中的对象重新可见,后者除了将前像放在undo tablespace中,还想日志归档一样,将前像归档到闪回日志中,用于更大规模的闪回,但基本原理是一致的。
MySQL设计原理
开启一个事务
事务的开启与undo segment强相关,但MySQL的特殊地方在于发明了rollback segment和undo log segment这2个概念。rollback segment用于存放总体信息,而undo log segment用于存放具体的undo日志记录。
图2.3-1 rollback segment page结构
表2.3-1 ROLLBACK PAGE HEADER结构
域
长度
含义
TRX_RSEG_MAX_SIZE
4
rollback segment允许使用的最大page数,当前值为ULINT_MAX
TRX_RSEG_HISTORY_SIZE
4
history list上undo page数量
TRX_RSEG_HISTORY
16
链表指针头,指向history list链表
TRX_RSEG_FSEG_HEADER
10
本rollback segment对应的INODE Entry的地址
rollback segment只有1个page(type为FIL_PAGE_TYPE_SYS),如图2.3-1所示,rollback segment page由如下关键部分组成:
TRX_RSEG_UNDO_SLOTS
:每个slot
对应1
个undo log segment
,1
个rollback segment
可以管理1024
个undo log segment
。slot
本身仅占用4
个字节,记录是undo log segment header page
的page no
,没有space id
,所以rollback segment
及其管理的undo log segment
必须在同一个undo tablespace
中;
ROLLBACK PAGE HEADER
:存放了rollback segment
的总体信息,详细情况如表2.3-1
所示,其中最重要的是TRX_RSEG_HISTORY
,其为链首指针,通过和UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
组成history list
链表,将所有归属于本rollback segment
且已经提交的undo log
链接在一起;
图2.3-2 rollback segment 总体布局
开始事务需要在rollback segment中找到1个slot,那又是如何找到rollback segment的呢?如图2.3-2所示,在“空间管理与数据布局”章节中我们知道系统tablespace中有1个sys trx segment,该segment中的sys trx segment page中存放了128个RSEG地址,其中:
第1
个rollback segment
预留在系统tablespace
中;
第2~33
个rollback segment
存放在临时tablespace
中;
第34~128
个rollback segment
存放在独立的undo tablesapce
中;
如此就可以获得所有rollback segment的入口。开始事务时,MySQL默认为只读事务,当后继发生写操作时,转换为读写模式,为事务分配事务id和rollback segment,分配方式如下:
如果事务操作了临时表,为其分配临时表rollback segment
(2-33
,临时表的undo
无需记录redo
);
采用round-robin
的方式分配rollback segment
(34~128
),不过在遍历的过程中,如果某个rollback segment
的history list
过长,意味着该rollback segment
已经非常大了,会跳过该rollback segment
,继续遍历下一个rollback segment
;
Undo Log Segment
在上一节我们知道MySQL定义了rollback segment和undo log segment两种segment,rollback segment负责管理作用,undo log segment则负责记录具体的undo日志。
图2.3-3 undo log segment header page & normal undo log page
undo log segment包含1个header page和若干个normal page,rollback segment中的TRX_RSEG_UNDO_SLOTS记录的就是header page的page no。如图2.3-3所示,两种page基本类似,只是header page多了1个UNDO LOG SEGMENT HEADER,而normal page对应的区域为空。下面详细了解一下各个组成部分之间的关系:
UNDO LOG PAGE HEADER
用于描述本page
的总体信息,详细情况见表2.3-3
,而UNDO LOG SEGMENT HEADER
则描述本segment
的总体信息,详细情况见表2.3-2
;
MySQL
出于效率考虑,将undo log segment
分为TRX_UNDO_INSERT
和TRX_UNDO_UPDATE
。因为一旦事务提交完成,insert
的undo
日志就可以释放,不需要用于多版本并发控制,将insert
区分出来,可以集中管理,高效释放;
1
个undo log segment
同时只能被1
个事务使用,1
个事务可以使用多个undo log segment
(例如事务既有delete
,也有insert
,就需要申请2
个undo log segment
,分别记录delete
和insert
的undo
日志)。这样在极限情况下,系统最多支持((128-32)*1024)/2
个并发事务(此数据是假设所有事务都包含insert
和非insert
类写操作);
SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
和PAGE HEADER
中的TRX_UNDO_PAGE_NODE
将本undo log segment
中所有的undo page
链接在一起,方便管理;
表2.3-2 UNDO LOG SEGMENT HEADER结构
域
长度
含义
TRX_UNDO_STAT
2
undo log segment的当前状态:
TRX_UNDO_ACTIVE
:本
segment
上的事务处于活跃态;
TRX_UNDO_CACHED
:本
segment
上的事务已提交,本
segment
可以被复用;
TRX_UNDO_TO_FREE
:本
segment
上的事务已提交,本
segment
可以被立刻释放;
TRX_UNDO_TO_PURGE
:本
segment
上的事务已提交,本
segment
等待被
purge
;
TRX_UNDO_PREPARED
:本
segment
上的事务提交过程中,当前处于
prepare
阶段(
MySQL
需要协调
MySQL
上层和
InnoDB
,所以事务提交采用的是二阶段提交);
TRX_UNDO_LAST_LOG
2
最近1条undo log的header在page内的偏移
TRX_UNDO_FSEG_HEADER
10
记录本segment对应的INODE Entry的地址
TRX_UNDO_PAGE_LIST
16
链表头,将所有属于本undo log segment的page链接在一起
表2.3-3 UNDO LOG PAGE HEADER结构
域
长度
含义
TRX_UNDO_PAGE_TYPE
2
undo日志的类型:
TRX_UNDO_INSERT
:针对
insert
;
TRX_UNDO_UPDATE
:针对非
insert
;
TRX_UNDO_PAGE_START
2
本page中最近1条undo日志的位置
TRX_UNDO_PAGE_FREE
2
本page中空闲空间的偏移量
TRX_UNDO_PAGE_NODE
12
链表的双向指针,用于将本undo log segment中的所有page链接在一起
图2.3-4 UNDO LOG结构
表2.3-4 UNDO LOG HEADER结构
域
长度
含义
TRX_UNDO_TRX_ID
8
产生本条undo log的事务id
TRX_UNDO_TRX_NO
8
仅对update undo日志有效,加入history list的UNDO LOG RECORD数量
TRX_UNDO_DEL_MARKS
2
是否有delete mark操作
TRX_UNDO_LOG_START
2
UNDO LOG RECORD开始的位置
TRX_UNDO_DICT_OPERATION
2
是否是DDL
TRX_UNDO_TABLE_ID
8
若是DDL,记录table id
TRX_UNDO_NEXT_LOG
2
下一个undo log header的位置
TRX_UNDO_PREV_LOG
2
上一个undo log header的位置
TRX_UNDO_HISTORY_NODE
12
双向指针,用于构建history list链表
表2.3-5 INSERT型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型,取值TRX_UNDO_INSERT_REC表示INSERT型
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
START
2
本条undo log record的起始位置
表2.3-6 UPDATE型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型及标志位标志位:
UPD_NODE_NO_ORD_CHANGE
:操作是否修改了二级索引的列;
TRX_UNDO_UPD_EXTERN
:是否涉及
extern
列;
日志类型:
TRX_UNDO_DEL_MARK_REC
:删除操作,并不实际删除,而是在原记录上打上删除标签;
TRX_UNDO_UPD_EXIST_REC
:更新操作;
TRX_UNDO_UPD_DEL_REC
:更新已经被删除的记录,即记录已经打上删除标签,这时再插入相同主键的记录;
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
INFO_BITS
1
标志位
DATA_TRX_ID
compact
修改前,原记录上的事务id(隐藏列)
DATA_ROLL_PTR
compact
修改前,原记录上的回滚指针(隐藏列)
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
UPDATE VECTOR
var
记录被更新列的前像(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
N_BYTES_BELOW
compact
INDEX_COLUMS部分的总长度
INDEX_COLUMS
var
所有二级索引的前像(包括所有主键列和索引列)(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
START
2
本条undo log record的起始位置
如图2.3-4所示,每条undo log由1个undo log header和若干条undo log record组成,每条undo log record保持数据的前像,和data block中的一条row data相对应,具体情况如下:
undo log header
:存放本条undo log
的总体信息,详细情况如表2.3-4
所示,其中TRX_UNDO_NEXT_LOG
和TRX_UNDO_PREV_LOG
用于将本undo log segment header page
中的所有undo log header
链接在一起(undo log segment
可以被事务复用),而TRX_UNDO_HISTORY_NODE
是history list
链表的组成部分,用于将所有提交事务的undo log header
(不含insert
型)链接在一起,为将来purge
打好基础;
undo log record
:分为insert
型和update
型,存放在不同的undo log segment
中,insert
型不用于构造MVCC
,事务提交后对应的undo log segment
就可以立刻释放;
delete和update将来都需要用于构造MVCC,所以对应的undo log record不能立刻释放。原始data page中的每条记录保存有当前事务id和指向undo log record的回滚指针,这就要求删除操作不能真正的实施,否则隐藏列无处存放。undo log record还保存了该条数据记录隐藏列上的前事务id和回滚指针,这样就可以回溯到该记录的任意历史版本(只要undo log record还在)。对于二级索引来说,二级索引上的update,MySQL都是转换为delete+insert。有了这些背景之后,我们再来看MySQL在undo log record记录了哪些内容:
delete
:生成类型为TRX_UNDO_DEL_MARK_REC
的undo log record
,主键索引和二级索引都不实施真正的删除,都是打上删除标签,undo log record
中需要记录主键值(PRIMARY KEY
)和二级索引列值(INDEX_COLUMS
);
update
:生成类型为TRX_UNDO_UPD_EXIST_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
)。二级索引上的update
实际上是delete+insert
,所以需要记录二级索引的全部前像;
insert
:生成类型为TRX_UNDO_INSERT_REC
的undo log record
,记录主键值(PRIMARY KEY
)。还有一种特殊情况,某条记录已经被打上删除标签,这时再次插入相同主键的行,这时生成类型为TRX_UNDO_UPD_DEL_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
);
图2.3-5 UNDO全景图
最后我再回顾总结一下undo的全貌,如图2.3-5所示:
每个rollback segment
有1
个header page
,每个undo log segment
有1
个header page
和若干个normal page
;
每个事务可以有多个undo log
,但单个undo log
只能属于1
个事务。对于某个具体的undo log segment
来说,1
个事务只有1
个undo log
;
1
个undo log segment
同时只能属于1
个事务,但当undo log segment
仅使用了header page
,且该header page
至少有1/4
空闲空间时,该undo log segment
在上一个事务提交后可以被下一个事务复用,所以有时header page
中有多个undo log header
;
链A
:每个rollback segment
通过1024
个slots
可同时管理1024
个活跃态的undo log segment
;
链B
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
结合UNDO LOG PAGE HEADER
中的TRX_UNDO_PAGE_NODE
,将本undo log segment
中的所有undo page
链接在一起;
链C
:UNDO LOG HEADER
中的TRX_UNDO_LOG_START
结合UNDO LOG RECORD
中的NEXT
和START
,将本undo log
中的所有undo log record
链接在一起;
链D
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_LAST_LOG
结合UNDO LOG HEADER
中的TRX_UNDO_NEXT_LOG
、TRX_UNDO_PREV_LOG
,将本undo log segment
中的所有undo log
链接在一起;
链E
:ROLLBACK PAGE HEADER
中的TRX_RSEG_HISTORY
结合UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
,将所有已经提交的undo log
链接在一起(不含insert
和cached
);
链F
:user records
中的DATA_ROLL_PTR
隐藏列结合undo log record
中的DATA_ROLL_PTR
,将和某条记录相关的所有undo log record
链接在一起,为MVCC
打下基础;
事务提交
MySQL和InnoDB是松耦合结构,为了协调上下层的提交一致性,采用了2阶段提交。不过从undo的角度来看,prepare阶段的工作不多,但还是要将UNDO LOG SEGMENT HEADER中的TRX_UNDO_STAT从TRX_UNDO_ACTIVE改为TRX_UNDO_PREPARED,并更新其它一些信息。重点工作发生在commit阶段:
如果是insert
型undo log segment
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_FREE
,后继直接释放;
如果是update
型undo log segment
,只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_CACHED
,并将本undo log segment
加入到undo cache list
中,以供将来复用(undo cache list
是内存结构);
如果是update
型undo log segment
,不满足只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_PURGE
,同时将rollback segment
中对应的slot
置为FIL_NULL
,并将本undo log segment
(undo log header
)加入到TRX_UNDO_HISTORY_NODE
中(链E
),等待被清理(不再用于MVCC
后即可被清理);
用户回滚与重启回滚
当用户主动或者数据库内部原因对事务发起回滚时,内存中保存有相关对象和指针,直接进行逆向操作即可,主要包括如下动作:
二级索引对主键索引有依赖关系,所以先回滚二级索引,再回滚主键索引;
对于打上删除标记的记录,清理删除标记;
对于更新操作,将数据回退到前像版本;
对于插入操作,直接删除记录;
重启回滚涉及的动作和用户回滚类似,但重启内存丢失,需要根据以下原则找到回滚记录:
遍历所有rollback segment
,根据每个rollback segment
中的slot
遍历undo log segment
;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_ACTIVE
,表示需要回滚,按照类似用户回滚的动作进行回滚;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_PREPARED
,比较事务的XID
与binlog
,如果binlog
已经完成,则走提交流程,否则走回滚流程;
一致性读
RR隔离级别下一致性读的原则:事务开始时所有已经提交的事务对本事务来说都是可见的,所有未提交以及将来启动的事务对本事务来说都是不可见的。为此,开启事务时需要根据当前系统的活跃事务情况构建一个属于本事务的ReadView,包含如下要素:
up_limit_id
:最小活跃事务id
,比该id
小的事务对本事务来说都是可见的;
low_limit_id
:最大事务id
,比该事务id
大的事务都是在本事务开启之后生成的,所以对本事务来说都是不可见的;
trx_ids
:trx_ids
为本事务开启时系统活跃事务列表的快照,对于up_limit_id
和low_limit_id
之间的所有事务,如果在trx_ids
中表示本事务开启时该事务处于活跃态,尚未提交,所以不可见,否则可见;
图2.3-6 可见性判断
数据记录中有事务id和回滚指针的隐藏列,而每条undo log record中也有上1个事务id和指向上1个undo log record的指针,通过这些事务id和回滚指针,按照图2.3-6中的原则就可以进行无锁的一致性读。在RC场景下,原则和RR场景基本一致,不同的是RR在事务开始时建立ReadView,而RC在每条SQL语句开始前都要建立一个新的ReadView。
Purge
一致性读机制不再读到的这些历史版本就需要及时被清理掉,以便及时释放出undo空间。为此,获得系统中最老的ReadView,比该ReadView中up_limit_id小的事务对应的undo log都可以被清理掉。方法是遍历各个rollback segment的TRX_RSEG_HISTORY链表,链表中事务id小于up_limit_id的undo log都可以被清理掉。清理的动作主要包括2个方面:
清理记录:清理二级索引和主键索引中删除标记为真的记录;
清理undo
:清理undo log
和undo log segment
;
PostgreSQL设计原理
Page与前像设计
图2.4-1 page结构
表2.4-1 PageHeaderData结构
域
长度
含义
pd_lsn
8
本page被修改的lsn
pd_checksum
2
校验值,检查page异常
pd_flags
2
Page状态,组合标志位:
PD_HAS_FREE_LINES(0x0001)
:
ItemIdData
中有未使用的指针;
PD_PAGE_FULL(0x0002)
:
page
满;
PD_ALL_VISIBLE(0x0004)
:本
page
中所有记录都可见;
pd_lower
2
Free space的起始位置
pd_upper
2
Free space的结束位置
pd_special
2
Special space的位置
pd_pagesize_version
2
高位字节表示page大小,低位字节表示版本号
pd_prune_xid
4
本page最老的事务id,vacuum时使用
表2.4-2 ItemIdData结构
域
长度
含义
lp_off
15bit
记录在Items中的位置
lp_flag
2bit
Item状态:
LP_UNUSED(0)
:未使用;
LP_NORMAL(1)
:已使用;
LP_REDIRECT(2)
:
HOT
重定向;
LP_DEAD(3)
:
Dead
记录;
lp_len
15bit
在Items中的长度
PostgreSQL采用异地更新方案,前像没有存放在独立的undo区域中,而是和数据一起存放在data page中。因此,在研究PostgreSQL回滚前,有必要先了解一下page结构。如图2.4-1所示,page由如下部分组成:
PageHeaderData
:头部,定义了整个page
的总体信息,详细情况见表2.4-1
;
ItemIdData
:目录信息,每行记录占1
个位置,其下标是后继定位该记录的重要组成部分,详细情况见表2.4-2
,随着记录的增加,目录从上向下增加;
Items
:用户数据,随着记录的增加,数据从下向上增加;
Special Space
:存放和访问方法相关的数据,如对于BTree
索引存放左右兄弟节点的指针,普通表page
为空;
图2.4-2 Item结构
表2.4-3 HeapTupleHeaderData结构
域
长度
含义
t_xmin
4
insert本记录的事务id
t_xmax
4
delete/update本记录的事务id
t_cid/t_xvac
4
Insert/delete时的事务id和命令id
t_ctid
6
本记录下一个版本的地址,如果自己是最新版本则指向自己
t_infomask2
2
低11位表示当前记录的列数;其它位为标志位:
HEAP_KEYS_UPDATED(0x2000)
:被更新或删除;
HEAP_HOT_UPDATED(0x4000)
:
HOT-updated
;
HEAP_ONLY_TUPLE(0x8000)
:
heap only tuple
;
HEAP_XACT_MASK(0xE000)
:用于可见性判断;
t_infomask
2
组合标志位:
HEAP_HASNULL(0x0001)
:有
NullBitmap
域;
HEAP_HASVARWIDTH(0x0002)
:有变长列;
HEAP_HASEXTERNAL(0x0004)
:有外部存储列;
HEAP_HASOID_OLD(0x0008)
:有
object id
域;
HEAP_XMAX_KEYSHR_LOCK(0x0010)
:
t_xmax
状态;
HEAP_COMBOCID(0x0020)
:
t_cid
是
combo id
;
HEAP_XMAX_EXCL_LOCK(0x0040)
:
t_xmax
状态;
HEAP_XMAX_LOCK_ONLY(0x0080)
:
t_xmax
状态;
HEAP_XMIN_COMMITTED(0x0100)
:
t_xmin
状态;
HEAP_XMIN_INVALID(0x0200)
:
t_xmin
状态;
HEAP_XMAX_COMMITTED(0x0400)
:
t_xmax
状态;
HEAP_XMAX_INVALID(0x0800)
:
t_xmax
状态;
HEAP_XMAX_IS_MULTI(0x1000)
:
MultiXactId
;
HEAP_UPDATED(0x2000)
:
Updated Version
;
t_hoff
1
记录头大小
Item存放一条具体的记录,如图2.4-2所示,Item由HeapTupleHeaderData、NullBitmap、ObjectId、UserData 4个部分组成:
HeapTupleHeaderData
:记录头,固定大小,t_xmin
和t_xmax
用于判断记录在事务间的可见性判断,t_cid
用于记录在本事务内的可见性判断,t_ctid
用于将记录的多个版本链接在一起,详细情况见表2.4-3
;
NullBitmap
:可选,t_infomask
中HEAP_HASNULL
标志位为真时有此字段,用于表达相关列是否为null
;
ObjectId
:可选,t_infomask
中HEAP_HASOID_OLD
标志位为真时有此字段;
UserData
:真正的用户数据;
图2.4-3 前像设计示例
了解了PostgreSQL的page和item设计,下面来看PostgreSQL是如何进行异地更新的,如图2.4-3所示,DML主要包括下面3种情况:
Insert
:insert
直接插入数据,不需要记录前像,t_xmin
记录插入本记录的事务id
;
Delete
:delete
不进行实际删除,记录本身就是前像,t_xmax
记录删除本记录的事务id
;
Update
:不进行本地更新,而是克隆1
个新记录,然后在新纪录上更新(实际上是delete+insert
),旧记录就是前像,并将旧记录的t_ctid
指向新记录的位置,从而形成一个旧记录指向新记录的链表;
可见PostgreSQL通过上述方法保存了前像,同时通过clog、记录中的ItemIdData.lp_flag、记录中的t_infomask保存相关事务的状态,这样就可以合理地应用保存的前像。当然记录中的ItemIdData.lp_flag和t_infomask的状态可能不是最新的,需要后台根据clog中的真实状态进行刷新。
用户回滚
PostgreSQL采用异地更新的最大优势就是回滚。由于前像仍然以记录的方式存在于数据当中,所以用户回滚只需要记录事务的状态,释放相关资源即可,非常高效。至于遗留下来的垃圾数据,通过后台进程逐步清理即可。
一致性读
和MySQL类似,PostgreSQL也采用基于事务id的一致性读,该方法需要获取当前的活跃事务id列表,PostgreSQL将其称为transaction snapshot。RR或者SERIALIZABLE隔离级别下,事务在启动时会生成本事务的transaction snapshot。RC隔离级别下,每条SQL语句执行前生成最新的transaction snapshot。
由于PostgreSQL采用的是异地更新,一致性判断的规则更加复杂,需要综合判断事务状态、t_xmin、t_xmax和transaction snapshot,具体规则如下:
类别1
:t_xmin
的事务状态为ABORTED
插入此条记录的事务已经回滚,所以本记录对任何事务都不可见。
类别2
:t_xmin
的事务状态为IN_PROGRESS
R1: t_xmin=current_txid & t_xmax=INVALID -> visible
R2: t_xmin=current_txid & t_xmax!=INVALID -> invisible
R3: t_xmin!=current_txid -> invisible
R1表示当前事务读取本事务之前插入的数据,所以可见。R2表示该记录虽然是本事务插入的,但已被本事务删除,所以不可见。R3表示当前事务读取其它事务插入的记录,且其它事务尚未提交,所以不可见。
类别3
:t_xmin
的事务状态为COMMITTED
R4: snapshot(t_xmin)=active -> invisible
R5: t_xmax=INVALID | status(t_xmax)=ABORTED -> visible
R6: status(t_xmax)=IN_PROGRESS & t_xmax=current_txid -> invisible
R7: status(t_xmax)=IN_PROGRESS & t_xmax!=current_txid -> visible
R8: status(t_xmax)=COMMITTED & snapshot(t_xmax)=active -> visible
R9: status(t_xmax)=COMMITTED & snapshot(t_xmax)!=active -> invisible
判断规则是R4到R9依次判断,一旦满足条件就以此结果为依据。R4表示插入这条记录的事务虽然已经提交了,但该事务仍然在snapshot中,所以不可见。R5表示没有其它事务更新过该记录或者更新过该记录的事务已经回滚,所以可见。R6表示本事务已经更新过该记录,所以该记录的上一个版本不可见。R7表示其它事务正在更新该记录,所以该记录的上一个版本可见。R8表示更新或删除该记录的事务已经提交,但该事务在snapshot中,所以可见。R9表示更新或删除该记录的事务已经提交,但该事务不在snapshot中,需要看更新的版本,所以不可见。
HOT与Index-Only Scan
图2.4-4 索引与记录关系示意图(非HOT)
如图2.4-4所示,由于记录采用了异地更新,当记录生成一个新的版本时,为了保持同步,索引项不管有没有发生变化,都需要生成一个对应的新版本,和相应的数据记录对应起来。这导致了索引不必要的浪费,并为后继的vacuum带来了额外的负担。
图2.4-5 索引与记录关系示意图(HOT)
图2.4-6 索引与记录关系示意图(HOT, After Prune)
图2.4-7 索引与记录关系示意图(HOT, After Defragmentation)
HOT指当更新同时满足:1)新版本的记录和旧版本的据在同一个page内;2)更新的列不涉及索引列时,索引不再生成新版本,从而解决索引成本过高的问题。上述条件2很容易理解,条件1的主要目的是平衡读性能,否则通过索引访问很可能需要2次page调度,即先通过索引定位到旧版本,然后再通过旧版本的c_ctid或者ItemIdData中重定向定位到新版本。
如图2.4-5所示,HOT生效时索引仍然指向旧记录,新记录通过旧记录的t_ctid访问,同时旧记录的t_infomask2打上HEAP_HOT_UPDATED标志,新记录的t_infomask2打上HEAP_ONLY_TUPLE标志。当旧记录的状态为dead后,图2.4-6和图2.4-7分别给出修剪(prune)和去碎片化(defragmentation)实施后的情况,旧记录的ItemIdData保留,并重定向到记录的ItemIdData上。
Index-Only Scan指索引中已包含查询所需所有列,原则上直接扫描或定位索引page,无需扫描或定位数据page即可完成本次查询。PostgreSQL的一致性判断规则复杂,且依赖于数据page中各记录的t_xmin、t_xmax及其状态信息,从而导致Index-Only Scan无法实施。为此,PostgreSQL采用了一个简单直接的办法,在后台维护一个visibility map,一旦确定某个数据page中的所有记录都是可见的,就将该page加入到visibility map中,直接对外可见。这样扫描索引时,如果发现对应的数据page在visibility map中就仅扫描索引,不再访问数据page,否则仍然访问数据page,根据记录的t_xmin和t_xmax及其相应状态进行可见性判断。
Vacuum
从前面几个小节我们知道数据page和索引page都会有过期数据(dead tuple),vacuum的工作主要就是清理这些过期数据。vacuum分为concurrent vacuum和full vacuum,前者允许vacuum期间并发执行其它只读事务,后者不允许vacuum期间执行其它事务,但两者的实现原理是一致的。
concurrent vacuum的主要工作是针对某个特定的table或所有table执行如下任务:
清理dead tuple
,即删除无用的数据前像:
删除数据page
中的dead tuple
,并对数据page
做去碎片化整理;
删除索引page
中指向dead tuple
的索引记录;
冻结过期txids
,PostgreSQL
的txid
只有4
个字节,需要及时清理,以解决循环使用的问题:
冻结过期的txids
;
更新系统catalogs
;
清理不必要的clog
;
其它任务:
更新FSM
和VM
;
更新系统统计项;
concurrent vacuum的大致实现过程如下:
step1
:从table list
中获取某个待实施的table
;
step2
:针对该table
申请UpdateExclusiveLock
,该锁仍然允许其它事务读本table
;
step3
:扫描本table
中所有数据page
,识别出所有dead tuple
,必要时冻结过期txids
;
step4
:删除指向dead tuple
的所有索引记录;
step5
:针对本table
的每个数据page
,重复step6
和step7
;
step6
:删除dead tuple
,并做去碎片化工作;
step7
:更新FSM
和VM
;
step8
:如果数据page
中不再含有任何tuple
,释放该数据page
;
step9
:更新统计信息和catalogs
;
step10
:清理不必要的clog page
和clog
文件;
其中第3步是将扫描的结果存放在内存中(maintenance_work_mem),如果内存已满则开始实施第4步到第8步,然后清理内存,并回到第3步从上次断点开始继续扫描。
总结与分析
首先来看记录前像对正常写操作的影响,下面从DML操作类型和索引的角度来分析:
insert
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
无需做额外处理;
delete
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
只需设置相关状态;
update
:Oracle
和MySQL
会生成1
条undo
记录,仅记录更改列的前像,PostgreSQL
需要完整克隆1
条新记录,并更改旧记录的状态;
索引:Oracle
将索引当数据,数据与索引的处理方式一致。MySQL
的索引不记undo
,而是通过数据的undo
间接记录索引的变化。PostgreSQL
的索引页采用异地更新,但不记录事务状态,同时HOT
方式进行一定程度的优化。当然对于索引来说,索引列的更新涉及索引记录在BTree
中位置的调整,所以Oracle
和MySQL
都是将索引的update
转换为delete+insert
,即异地更新方案;
总的来说Oracle、MySQL、PostgreSQL相差不大,insert、delete操作PostgreSQL更加轻量,而update操作Oracle、MySQL相对较为轻量。但Oracle在设计上相对更加简洁,索引、数据的处理原则基本一致,没有太多的特殊情况要考虑。
其次看回滚效率,PostgreSQL是最高效的。PostgreSQL的前像就再数据之中,只需要记录事务的状态,不需要实施回滚动作。Oracle和MySQL则厚重得多,需要从undo日志中读出前像,并应用到实际数据和索引上,同时还要记录重做日志。
再次看存储效率,我们还是从DML操作类型、数据和索引这几个角度来看:
insert
:Oracle
仅记录RowID
,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
delete
:Oracle
需要记录整条记录的前像,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
update
:Oracle
需要记录被更新的列,MySQL
记录主键值和被更新的列,PostgreSQL
需要拷贝一个完整的新行(所有列);
记录:Oracle
的记录不需要做太多特殊处理,MySQL
需要引入13
个字节的事务id
和回滚指针,PostgreSQL
需要引入22
个字节的t_xmin
、t_xmax
、t_cid
、t_ctid
、t_infomask
、t_infomask2
;
索引:Oracle
的索引处理等同于数据,MySQL
需要将索引的所有列记录在数据的undo
中,并将update
转换为insert+delete
,PostgreSQL
需要生成新的索引记录;
从数据的前像来看,对于insert和delete操作,PostgreSQL基本不消耗存储空间;对于update操作,Oracle和MySQL优于PostgreSQL,因为前者仅需要记录被更新列的前像。对于delete操作,Oracle和MySQL都是在原记录上打上删除标签,但Oracle会在undo中记录整条记录的前像,而MySQL仅记录主键值。虽然MySQL更省空间,但Oracle将undo和数据block清理的工作完全解耦,没有依赖关系,且为将来的闪回提供了可能。从对记录的影响来看,Oracle是最优的,MySQL其次,PostgreSQL最后,虽然单条记录占用的字节数不是太多,但对于大数据量的窄表(表的列数很少,但记录非常庞大)来说,占用的空间会比较可观。
再再次看回收效率。Oracle在undo segment中维护retention table,直接按照时间将相关extent失效即可,不需要做太多的回收动作,block上的事务状态主要集中在ITL上,调整面也比较小。MySQL相对复杂一点,通过rollback segment和undo log segment进行日志管理,1个undo log segment同时只能服务于1个事务,segment的管理成本比Oracle高,但通过history list进行回收,回收涉及的undo page也相对比较集中。Page上基本不涉及事务状态,调整面最小。PostgreSQL的前像和数据存放在一起,需要通过扫描所有page确认dead tuple,成本非常高,事务状态涉及每条记录,调整面也非常大。
最后看一致性读和闪回。Oracle通过undo日志和scn进行一致性判断,简洁高效。MySQL和PostgreSQL分别需要维护read view和transaction snapshot,高并发时事务列表会很长,活跃事务列表的比较效率也较低,且每个事务都需要维护一个read view或transaction snapshot,RC隔离级别下每执行一次SQL还需要更新一次。虽然MySQL和PostgreSQL原理相似,但由于PostgreSQL涉及t_xmin、t_xmax、t_maskinfo、transaction snapshot、clog,判断规则极其复杂,效率最低。在扫描数据和索引时,涉及多个版本(关键看vacuum的频率),虽然VM可以有一定程度的缓解,但效率要低于Oracle和MySQL。
原则上只要前像存在,就可以提供闪回功能。Oracle在系统中维护有逻辑时间(scn),且可以和墙上时间转换,事务提交时记录当时的scn,这样就可以基于scn和墙上时间进行闪回查询和闪回恢复。MySQL和PostgreSQL当前还没有提供闪回功能,提供该功能的挑战在于:1)可以基于LSN进行闪回回退,但需要对当前的前像回收机制进行较大幅度的调整;2)当前的可见性判断机制是基于read view和transaction snapshot的,该机制无法复用到闪回查询,因为用户并不知道历史某个时刻的read view和transaction snapshot。
PDF下载地址:http://blog.itpub.net/69912723/viewspace-2717309/
【数据库设计与实现】第二章:数据前像与回滚
数据前像与回滚
设计原则
事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:
本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的undo
区域;
异地更新方案:不在原记录上更新,而是克隆1
条新记录出来,然后在新记录上进行更新,原记录就是前像;
Oracle和MySQL采用的都是本地更新方案,本章加入PostgreSQL,从而能够深入地了解异地更新方案的优缺点。在设计数据前像和回滚方案时,有如下几点需要考虑:
记录前像对正常写操作的影响有多大;
根据前像进行回滚,回滚的效率如何;
前像占用的空间如何,回收效率如何;
前像是高价值数据,可以基于前像提供一致性读(MVCC
)、闪回等功能,前像的设计是否能高效地实现这些功能;
Oracle设计原理
开启一个事务
图2.2-1 undo segment header block结构
在开启事务之前,我们先了解一下undo segment header block的总体结构,如图2.2-1所示,undo segment header block的组成情况如下:
undo segment header block
既是1
个block
,又是1
个segment header
,所以cache layer
、footer
、segment control header
、extent map
也都是存在的(图中未标出),详细情况可以参考“空间管理与数据布局章节”;
retention table
:记录各extent
最近一次提交的时间,从而在extent
到期时回收空间给后继事务使用,详细情况见表2.2-1
;
transaction control
:记录本undo segment
中事务的总体信息,详细情况见表2.2-2
;
free block pool
:记录当前使用过且有空闲空间的undo block
,优先使用这些block
(前面的事务未用满),详细情况见表2.2-3
;
transaction table
:记录本undo segment
中各个事务的详细信息,详细情况见表2.2-4
;
表2.2-1 retention table部分关键信息
域
含义
Extent number
extent号
Commit time
最近的提交时间,1970.1.1起始的秒数
表2.2-2 transaction control部分关键信息
域
长度
含义
seq
2
新事务第1条undo记录所在block的seq,与uba中的seq相对应
chd
2
transaction table中的index,指向最老的事务,即优先被替换的事务
ctl
2
transaction table中的index,指向最新的事务,即最后被替换的事务
nfb
2
free block pool中的undo block数量,0表示没有空闲的undo block
uba
7
新事务的第1条undo记录的地址
scn
6
transaction table中最近1次被替换事务的提交scn,用于上界提交
表2.2-3 free block pool部分关键信息
一级域
二级域
含义
Array(*n)
uba
Undo block的地址
ext
Undo block所在的extent
spc
Undo block的空闲空间,单位字节
表2.2-4 transaction table部分关键信息
域
长度
含义
index
0
事务的槽位号,对应于事务表的下标,不占用实际空间,事务id的组成部分之一
state
9 unactive;10 active
cflag
1
0x00 无事务;0x10 死事务;0x80 active事务;0x90 正在被回滚的死事务
wrap
2
本槽位每被事务复用1次加1,事务id的组成部分之一
uel
2
指向下1个事务的槽位
scn
6
事务提交时的scn,部分oracle版本在事务未提交时记录事务开始时的scn
dba
4
指向事务最近1条undo记录对应的data block
nub
4
事务使用的undo block数量
cmt
事务提交时的时间
开启1个事务时,首先要到undo segment header block的transaction table中申请1个位置,该位置的下标就称为该事务id的组成部分之一。xid由segment no、transaction table index、wrap这3个部分组成。transaction table实际上就是1个大数组,该数组中元素每被复用1次,wrap就会加1。
开启的事务在transaction table中申请到位置后,会一直占用这个位置,直到该事务提交或回滚才有可能被其它事务覆盖。1个undo segment只有1个undo segment header block,受限于block的大小,transaction table一般只能提供34个槽位(随block大小而变化),即同时只能存放34个活跃事务。为此Oracle默认会创建10个undo segment,并根据并发事务的数量以及retention参数动态伸缩undo segment数量。
transaction control和transaction table是undo segment header block中最重要的信息,下面结合实例来讲解事务并发运行过程中,transaction control和transaction table是如何记录undo日志的。假设当前的transaction control和transaction table分别如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index0 ctl=index34 uba=uba99 scn=scn32
TRN TBL:
index0: uel=index1 scn=scn78 dba=dba19 wrap=1
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=0xFF scn=scn899 dba=dba31 wrap=1
从上例我们可以看出:
通过chd
、ctl
、uel
可以将transaction table
中的事务链在一起,这是我们见到第1
个链表,为了说明方便,我们在本章节将其称为链表A
;
chd
指向链表头,ctl
指向链表尾;
整个链的顺序表达了事务提交的顺序,上例中的顺序是人为构造的,过于完美,实际上随着事务的频繁生成和消亡,指向顺序会比较杂乱;
针对上例,新开始的事务需要在此基础上申请1个位置,假设复用后的结果如下(仅保留说明原理的关键信息):
TRN CTL:
chd=index1 ctl=index0 uba=uba342 scn=scn78
TRN TBL:
index0: uel=0xFF scn=0 dba=dba26 wrap=2
index1: uel=index2 scn=scn98 dba=dba12 wrap=1
index2: uel=index3 scn=scn107 dba=dba99 wrap=1
... ...
index33: uel=index33 scn=scn765 dba=dba72 wrap=1
index34: uel=index0 scn=scn899 dba=dba31 wrap=1
假设新事务生成的第1条undo记录(该undo记录的地址未uda342,仅保留说明原理的关键信息)如下:
uda=uda99 ctl max scn=scn32 prev tx scn=scn78 prev brb=dba19
新事务复用transaction table的过程大致如下:
从transaction control
中找到chd
指针,遍历链表,直至找到第1
个处于提交状态的事务,即scn
最小的提交事务,上例中假设找到的即将被覆盖的位置为index0
;
将index0
从链表A
中摘除,本例是从链首摘除,所以只需要修改chd
,如果是中部或者尾部摘除,情况略有不同;
针对新事务生成该事务的第1
条undo
记录,记录被替换事务的scn
和dba
(分别对应第1
条undo
记录中的prev tx scn
和prev brb
),以及transaction control
中的uda
和scn
(分别对应第1
条undo
记录中的uba
和ctl max scn
);
将transaction control
中的scn
更新为被替换事务的提交scn
,用于将来的“上界提交”,将transaction control
中的uda
更新为新事务的第1
条undo
记录的地址,至此完成链B
的组建,即本undo segment
中的所有事务通过事务的第1
条undo
记录链在一起,根据链表B
可以回溯任何时候的transaction control
和transaction table
(当然前提是undo
日志还没有被清理掉);
根据新事务的信息更新index0
,并在事务提交后将index0
加入到链表A
的尾部;
在上述过程中,已开始生成undo记录,那么就需要申请undo block来存放,undo block的申请规则如下:
查看free block pool
中是否有空闲undo block
,有则使用,否则转下一步;
当前extent
有空闲undo block
,有则使用,否则转下一步;
当前segment
存在过期extent
,重置该extent
(wrap
加1
),然后使用该extent
,否则转下一步;
如果undo tablespace
有空闲空间,从中分配1
个新的extent
,并加入到本segment
中并使用,否则转下一步;
从offline
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
从online
状态的segment
中窃取1
个过期的extent
,加入到本segment
中并使用,否则转下一步;
如果undo tablespace
可以扩展,扩展undo tablespace
,将新extent
加入当前segment
中并使用,否则转下一步;
如果undo segment
中的retention
时间设置为非担保,自动调整undo segment
的保留时间,每次降低10%
,寻找过期extent
,如此循环直至找到;
Undo Block与Data Block
图2.2-2 undo block结构
通过上节介绍的方法找到了空闲的undo block,并向该undo block写入了本事务的第1条undo记录。对于单个undo block来说,同时只能属于1个活跃事务,所以从单个事务来看,其undo记录是相对集中的。当然如果事务已经提交,而该事务并没有用满某个undo block,该undo block的剩余空间会被其它事务使用。
undo block的结构如图2.2-2所示(忽略了cache layer和footer,这对所有block来说都是一样的):
control section
:记录本undo block
的总体信息,详细情况见表2.2-5
;
record directory
:undo
记录的目录,array
型,下标是uba
的地址,指向undo
记录在record heap
中的位置,空间从上向下增长(uba
由3
个部分组成,undo dba. seq no. record no
,通过undo dba
定位到undo block
,通过record no
定位到目录,从而找到在record heap
中的undo
记录);
record heap
:存放具体的undo
记录,空间从下向上增长,详细情况见表2.2-6
;
表2.2-5 control section部分关键信息
域
长度
含义
xid
8
最近1次操作本undo block的事务id
cnt
1
本undo block中的undo记录数
irb
1
指向事务(对应于xid)的最近1条undo记录,回滚的起始点
表2.2-6 record heap部分关键信息
一级域
二级域
含义
record header
rec
本undo记录在record directory中的下标,不占用实际空间
undo type
组合标志位:
regular undo
:正常的
undo
记录;
begin trans
:含事务开始信息;
user undo applied
:
undo
记录已用于回滚,防止重复回滚;
objn
本undo记录所涉data block对应的data object id
objd
object id
tblspc
tablespace no
rci
指向同1个事务的上1条undo记录,0x00表示最后1条undo记录
rdba
用于单个undo block无法容纳事务的所有undo记录的场景,rdba指向本事务的下1个undo block此情况一般仅出现在undo block的第1条undo记录中,其他undo记录该值都为0x00000000
undo segment header
uba
用于恢复transaction control中被覆盖的uba
ctl max scn
用于恢复transaction control中被覆盖的scn
prev tx scn
用于恢复transaction table中被覆盖的scn
txn start scn
记录新事务开始时的scn
prev brb
用于恢复transaction table中被覆盖的dba
data block ITL
xid
同data block中的ITL相对应,用于恢复某条ITL
uba
flg
lck
scn
data block record
kdo op code
IRP
插入单行;
DRP
删除单行;
LKR
锁定行;
URP
更新行;
ORP
行链接;
CKI
簇键索引
SKL
设置簇主键指针;
QMI
插入多行
QMD
删除多行
bdba
undo记录对应的data block
hdba
undo记录对应的data block归属的segment header block
slot
undo记录所涉记录在data block中row directory的下标
ncol
记录前像数据,用于恢复UPDATE和DELTE所涉列
nnew
size
cols
如表2.2-6所示,单条undo记录包含如下信息:
record header
:记录的总体信息,如所涉data block
对应的dba
、object_id
、tablespace
,并通过rci
将属于同一个事务的所有undo
记录链在一起;
undo segment header
:可选,仅存在于事务的第1
条undo
记录中,用于恢复undo segment
中的transaction control
和transaction table
;
data block ITL
:可选,用于恢复data block
中被本事务覆盖的事务槽位上的事务信息;
data block record
:记录data block
上被修改数据的前像:
update
:仅记录被修改列的前像;
delete
:记录被删除行的所有列的前像;
insert
:仅记录插入行的RowID
(通过objn+bdba+slot
计算得到);
表2.2-7 transaction layer:fixed部分关键信息
域
长度
含义
typ
1
1 数据,2 索引
seg/obj
4
segment, object_id
csn
6
最近一次清除时的scn
itc
1
本data block中ITL的数量,即事务槽的个数,受参数INITRANS和MAXTRANS影响
flg
1
segment空间的管理方式:
E
:
ASSM
;
O
:
Free List
;
fsl
1
本data block的ITL中,第1个空闲的位置
fnx
4
用于缓存管理
表2.2-8 transaction layer:ITL部分关键信息
域
长度
含义
xid
8
事务id,4个字节的undo segment no+2个字节的transaction table slot no+2个字节的transaction table slot wrap
uba
8
本事务的最近1条undo记录的地址,4个字节的undo block地址+2个字节的seq number+1个字节的目录下标+1个字节的预留
flg
1
事务状态,组合标志位:---- 事务处于活跃态
--
事务已提交,锁已被清除
--U- 事务已上界提交---T 相对于csn时刻,事务仍然处于活跃态
lck
1
本事务在本data block中锁定的记录数(行数)
scn/fsc
6
事务已提交,记录提交或上界提交时的
scn
;
事务尚未提交,前
2
个字节记录本
data block
对本事务来说的剩余空间
(free space credit)
现在我们再从data block的角度审视事务的运行过程,在“数据布局与空间管理”章节中,我们知道data block中与事务、数据强相关的部分为transaction layer:fixed+ITL,table directory,row directory,row data。当事务修改某data block中的数据时,首先要在data block的transaction layer:ITL中占用1个新的或者复用1个已处于提交状态的事务槽位。如果是复用槽位,新事务生成的undo记录需要记录被覆盖事务在ITL中的相关(详细情况见2.2-6的data block ITL,ITL槽位数量由initrans指定,普通场景默认2个,create table as select语句默认3个)。至此,我们接触到undo相关的第3个链表,用于按照data block维度将某个data block相关的所有事务的undo链在一起,我们将其称为链C。事务在修改data block中的实际数据时,row data中的lb会记录对应事务的槽位号,这样记录、事务、undo整个1张大网已经组建完成。
图2.2-3 undo链表关系全景图
如图2.2-3所示,我们得到了整个和undo相关的全景图:
链A
:存在于undo segment header block
中,可以将transaction control
和transaction table
中的事务链接在一起,方便事务申请;
链B
:存在于undo segment
内,将本undo segment
内的所有事务通过第1
条undo
记录链接在一起,这样即使因为transaction control
和transaction table
中的事务信息发生了覆盖,所有的历史事务信息都可以通过链B
进行回溯;
链C
:存在于undo
记录中,用于将某个data block
相关的所有事务的undo
链接在一起,这样即使因为transaction layer:ITL
数量有限,事务被频繁覆盖,所有修改过本data block
的所有事务的undo
记录都可以通过链C
进行回溯(为一致性读提供了条件);
对于仍然保留在transaction layer:ITL
中的事务(活跃或已提交)则非常简单,通过xid
可以找到undo segment header block
中的transaction table
中的详细信息,也可以直接通过uba
访问到本事务的undo
记录;
事务提交与Block清除
从undo的角度来看,事务的提交需要做下列工作:
工作1
:找到undo segment header block
,定位到transaction table
中的对应位置,修改事务状态和提交scn
,并记录redo
日志;
工作2
:遍历本事务修改过的所有data block
,在其中的transaction layer:ITL
找到本事务,修改事务状态和提交scn
;
工作3
:遍历本事务修改过的所有data block
,在其中的row data
部分,找到相关行并清理锁信息,记录redo
日志;
工作1是一个常量,和事务修改的data block数量无关,且事务提交时工作1是必须实施的,这也是我们通常说的“事务提交”日志(redo日志)。
工作2的工作仅涉及transaction layer:ITL,位置相对固定,且不用记录redo日志,但又和修改的data block数量强相关,所以情况比较复杂:
Oracle
会在会话内存中跟踪本事务修改过的data block
(有说最多1000
个block
,有说10%
左右的缓存大小),事务提交时这些data block
如果还在内存中,直接修改这些data block
中的ITL
,将flg
打上C
标志,表示事务已经提交,更新scn
为事务的提交scn
;
对于超出跟踪数量的data block
,或者已经被换出缓存的data block
,Oracle
为了提高commit
速度,不再处理这些data block
,而是采用延迟块清除策略,即将更新事务状态和scn
的工作延迟到将来再做。当将来某个事务读到该data block
时,发现ITL
中事务处于活跃状态:
读取对应的undo segment header block
,发现该事务还处于transaction table
中,且已经提交,将ITL
中该事务的flg
打上C
标志,并更新事务的提交scn
;
读取对应的undo segment header block
时,发现该事务所在的transaction table
已经被覆盖,将ITL
中该事务的flg
打上CU
标志,并事务的提交scn
更新为transaction control
中的scn
,这就是“上界提交”,即在该scn
时刻事务一定已经提交了,但也可能在这之间已经提交。Oracle
通过上界提交在效率和准确度之间做了一个平衡,上界提交通过最小的代价找到尽可能老的scn
,将来一致性读构建CR
时,如果scn
已经满足要求那最好,如果不满足要求再通过undo
(链B
)进一步回溯,找到更新精准的提交scn
(实际上找到满足一致性判断要求的scn
即可);
可见对于工作2,Oracle采取了综合措施,在commit效率和ITL准确度之间做了平衡。延时块清除将大量工作分解到将来的各个data block的读过程中。工作3是最耗时的,既要找到data block中的行记录(行记录会在data block内部移动),又要记录redo日志,Oracle的策略是将工作3延期分解到将来的data block读上,这也是为什么读操作有时也产生redo日志的原因。
用户回滚与重启回滚
用户回滚时,其中一个线索是通过undo segment header block中的transaction table找到该事务,再根据其中记录的dba获取本事修改的最近的data block,通过该data block中的ITL:uda获取该事务的最后1条undo记录,而undo记录是链在一起的,至此可以顺利实施回滚。实际执行用户回滚更加容易,因为内存中拥有完备的跟踪信息,可以加速上述过程。
重启回滚和上述类似,不同的是不存在内存加速(重启了),而是通过扫描所有undo segment header block获得所有待回滚事务及其入口信息。
另外需要说明的是实施回滚时除了data block的修改要记录redo日志外,undo block中的undo记录也要打上“user undo applied”标签,也需要记录redo日志,防止重复回滚。
一致性读
本节仅讲述Oracle基于undo记录构建一致性读的过程(构建CR),至于背后的理论基础请参考“事务”章节。假设事务A的启动scn为scn_a,事务A读取某个data block时,首先对该data block做延迟块清除。如果ITL中的事务都处于提交状态,且scn都小于scn_a则不需要构建CR,直接读取即可,否则需要构建CR:
步骤1
:在缓存区中对当前data block
做一个拷贝,即CR
;
步骤2
:反向更改所有未提交事务;
步骤3
:如果有事务提交scn
大于scn_a
,应用该事务的undo
日志;
重复上述步骤3
,直至所有事务的scn
小于等于scn_a
;
因为构建CR并不是实际更新数据,所以不需要记录undo和redo。至于CR版本的生命周期,Oracle综合应用了数量控制(有说1个data block至多3个CR)和缓存控制策略。
闪回
既然数据库内部可以通过undo记录构建CR版本实现读取历史数据的功能,该功能自然可以开放给用户直接使用,这就是闪回查询。当然还可以更进一步,对表进行闪回,即将数据回退到过去某个时间,解决用户误操作问题。闪回drop和闪回database有所不同,前者是将recyclebin中的对象重新可见,后者除了将前像放在undo tablespace中,还想日志归档一样,将前像归档到闪回日志中,用于更大规模的闪回,但基本原理是一致的。
MySQL设计原理
开启一个事务
事务的开启与undo segment强相关,但MySQL的特殊地方在于发明了rollback segment和undo log segment这2个概念。rollback segment用于存放总体信息,而undo log segment用于存放具体的undo日志记录。
图2.3-1 rollback segment page结构
表2.3-1 ROLLBACK PAGE HEADER结构
域
长度
含义
TRX_RSEG_MAX_SIZE
4
rollback segment允许使用的最大page数,当前值为ULINT_MAX
TRX_RSEG_HISTORY_SIZE
4
history list上undo page数量
TRX_RSEG_HISTORY
16
链表指针头,指向history list链表
TRX_RSEG_FSEG_HEADER
10
本rollback segment对应的INODE Entry的地址
rollback segment只有1个page(type为FIL_PAGE_TYPE_SYS),如图2.3-1所示,rollback segment page由如下关键部分组成:
TRX_RSEG_UNDO_SLOTS
:每个slot
对应1
个undo log segment
,1
个rollback segment
可以管理1024
个undo log segment
。slot
本身仅占用4
个字节,记录是undo log segment header page
的page no
,没有space id
,所以rollback segment
及其管理的undo log segment
必须在同一个undo tablespace
中;
ROLLBACK PAGE HEADER
:存放了rollback segment
的总体信息,详细情况如表2.3-1
所示,其中最重要的是TRX_RSEG_HISTORY
,其为链首指针,通过和UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
组成history list
链表,将所有归属于本rollback segment
且已经提交的undo log
链接在一起;
图2.3-2 rollback segment 总体布局
开始事务需要在rollback segment中找到1个slot,那又是如何找到rollback segment的呢?如图2.3-2所示,在“空间管理与数据布局”章节中我们知道系统tablespace中有1个sys trx segment,该segment中的sys trx segment page中存放了128个RSEG地址,其中:
第1
个rollback segment
预留在系统tablespace
中;
第2~33
个rollback segment
存放在临时tablespace
中;
第34~128
个rollback segment
存放在独立的undo tablesapce
中;
如此就可以获得所有rollback segment的入口。开始事务时,MySQL默认为只读事务,当后继发生写操作时,转换为读写模式,为事务分配事务id和rollback segment,分配方式如下:
如果事务操作了临时表,为其分配临时表rollback segment
(2-33
,临时表的undo
无需记录redo
);
采用round-robin
的方式分配rollback segment
(34~128
),不过在遍历的过程中,如果某个rollback segment
的history list
过长,意味着该rollback segment
已经非常大了,会跳过该rollback segment
,继续遍历下一个rollback segment
;
Undo Log Segment
在上一节我们知道MySQL定义了rollback segment和undo log segment两种segment,rollback segment负责管理作用,undo log segment则负责记录具体的undo日志。
图2.3-3 undo log segment header page & normal undo log page
undo log segment包含1个header page和若干个normal page,rollback segment中的TRX_RSEG_UNDO_SLOTS记录的就是header page的page no。如图2.3-3所示,两种page基本类似,只是header page多了1个UNDO LOG SEGMENT HEADER,而normal page对应的区域为空。下面详细了解一下各个组成部分之间的关系:
UNDO LOG PAGE HEADER
用于描述本page
的总体信息,详细情况见表2.3-3
,而UNDO LOG SEGMENT HEADER
则描述本segment
的总体信息,详细情况见表2.3-2
;
MySQL
出于效率考虑,将undo log segment
分为TRX_UNDO_INSERT
和TRX_UNDO_UPDATE
。因为一旦事务提交完成,insert
的undo
日志就可以释放,不需要用于多版本并发控制,将insert
区分出来,可以集中管理,高效释放;
1
个undo log segment
同时只能被1
个事务使用,1
个事务可以使用多个undo log segment
(例如事务既有delete
,也有insert
,就需要申请2
个undo log segment
,分别记录delete
和insert
的undo
日志)。这样在极限情况下,系统最多支持((128-32)*1024)/2
个并发事务(此数据是假设所有事务都包含insert
和非insert
类写操作);
SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
和PAGE HEADER
中的TRX_UNDO_PAGE_NODE
将本undo log segment
中所有的undo page
链接在一起,方便管理;
表2.3-2 UNDO LOG SEGMENT HEADER结构
域
长度
含义
TRX_UNDO_STAT
2
undo log segment的当前状态:
TRX_UNDO_ACTIVE
:本
segment
上的事务处于活跃态;
TRX_UNDO_CACHED
:本
segment
上的事务已提交,本
segment
可以被复用;
TRX_UNDO_TO_FREE
:本
segment
上的事务已提交,本
segment
可以被立刻释放;
TRX_UNDO_TO_PURGE
:本
segment
上的事务已提交,本
segment
等待被
purge
;
TRX_UNDO_PREPARED
:本
segment
上的事务提交过程中,当前处于
prepare
阶段(
MySQL
需要协调
MySQL
上层和
InnoDB
,所以事务提交采用的是二阶段提交);
TRX_UNDO_LAST_LOG
2
最近1条undo log的header在page内的偏移
TRX_UNDO_FSEG_HEADER
10
记录本segment对应的INODE Entry的地址
TRX_UNDO_PAGE_LIST
16
链表头,将所有属于本undo log segment的page链接在一起
表2.3-3 UNDO LOG PAGE HEADER结构
域
长度
含义
TRX_UNDO_PAGE_TYPE
2
undo日志的类型:
TRX_UNDO_INSERT
:针对
insert
;
TRX_UNDO_UPDATE
:针对非
insert
;
TRX_UNDO_PAGE_START
2
本page中最近1条undo日志的位置
TRX_UNDO_PAGE_FREE
2
本page中空闲空间的偏移量
TRX_UNDO_PAGE_NODE
12
链表的双向指针,用于将本undo log segment中的所有page链接在一起
图2.3-4 UNDO LOG结构
表2.3-4 UNDO LOG HEADER结构
域
长度
含义
TRX_UNDO_TRX_ID
8
产生本条undo log的事务id
TRX_UNDO_TRX_NO
8
仅对update undo日志有效,加入history list的UNDO LOG RECORD数量
TRX_UNDO_DEL_MARKS
2
是否有delete mark操作
TRX_UNDO_LOG_START
2
UNDO LOG RECORD开始的位置
TRX_UNDO_DICT_OPERATION
2
是否是DDL
TRX_UNDO_TABLE_ID
8
若是DDL,记录table id
TRX_UNDO_NEXT_LOG
2
下一个undo log header的位置
TRX_UNDO_PREV_LOG
2
上一个undo log header的位置
TRX_UNDO_HISTORY_NODE
12
双向指针,用于构建history list链表
表2.3-5 INSERT型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型,取值TRX_UNDO_INSERT_REC表示INSERT型
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
START
2
本条undo log record的起始位置
表2.3-6 UPDATE型UNDO LOG RECORD结构
域
长度
含义
NEXT
2
本条undo log record结束的位置
TYPE_CMPL
1
日志类型及标志位标志位:
UPD_NODE_NO_ORD_CHANGE
:操作是否修改了二级索引的列;
TRX_UNDO_UPD_EXTERN
:是否涉及
extern
列;
日志类型:
TRX_UNDO_DEL_MARK_REC
:删除操作,并不实际删除,而是在原记录上打上删除标签;
TRX_UNDO_UPD_EXIST_REC
:更新操作;
TRX_UNDO_UPD_DEL_REC
:更新已经被删除的记录,即记录已经打上删除标签,这时再插入相同主键的记录;
UNDO_NO
compact
同1个undo log header下各个undo log record顺序编号
TABLE_ID
compact
对应的表id
INFO_BITS
1
标志位
DATA_TRX_ID
compact
修改前,原记录上的事务id(隐藏列)
DATA_ROLL_PTR
compact
修改前,原记录上的回滚指针(隐藏列)
PRIMARY KEY
var
主键列的长度和取值:primary_col1_lenprimary_col1_valprimary_col2_lenprimary_col2_val... ...primary_colN_lenprimary_colN_val
UPDATE VECTOR
var
记录被更新列的前像(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
N_BYTES_BELOW
compact
INDEX_COLUMS部分的总长度
INDEX_COLUMS
var
所有二级索引的前像(包括所有主键列和索引列)(列位置、长度、前像):upd_col1_posupd_col1_lenupd_col1_valupd_col2_posupd_col2_lenupd_col2_val.. ...upd_colN_posupd_colN_lenupd_colN_val
START
2
本条undo log record的起始位置
如图2.3-4所示,每条undo log由1个undo log header和若干条undo log record组成,每条undo log record保持数据的前像,和data block中的一条row data相对应,具体情况如下:
undo log header
:存放本条undo log
的总体信息,详细情况如表2.3-4
所示,其中TRX_UNDO_NEXT_LOG
和TRX_UNDO_PREV_LOG
用于将本undo log segment header page
中的所有undo log header
链接在一起(undo log segment
可以被事务复用),而TRX_UNDO_HISTORY_NODE
是history list
链表的组成部分,用于将所有提交事务的undo log header
(不含insert
型)链接在一起,为将来purge
打好基础;
undo log record
:分为insert
型和update
型,存放在不同的undo log segment
中,insert
型不用于构造MVCC
,事务提交后对应的undo log segment
就可以立刻释放;
delete和update将来都需要用于构造MVCC,所以对应的undo log record不能立刻释放。原始data page中的每条记录保存有当前事务id和指向undo log record的回滚指针,这就要求删除操作不能真正的实施,否则隐藏列无处存放。undo log record还保存了该条数据记录隐藏列上的前事务id和回滚指针,这样就可以回溯到该记录的任意历史版本(只要undo log record还在)。对于二级索引来说,二级索引上的update,MySQL都是转换为delete+insert。有了这些背景之后,我们再来看MySQL在undo log record记录了哪些内容:
delete
:生成类型为TRX_UNDO_DEL_MARK_REC
的undo log record
,主键索引和二级索引都不实施真正的删除,都是打上删除标签,undo log record
中需要记录主键值(PRIMARY KEY
)和二级索引列值(INDEX_COLUMS
);
update
:生成类型为TRX_UNDO_UPD_EXIST_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
)。二级索引上的update
实际上是delete+insert
,所以需要记录二级索引的全部前像;
insert
:生成类型为TRX_UNDO_INSERT_REC
的undo log record
,记录主键值(PRIMARY KEY
)。还有一种特殊情况,某条记录已经被打上删除标签,这时再次插入相同主键的行,这时生成类型为TRX_UNDO_UPD_DEL_REC
的undo log record
,记录主键值(PRIMARY KEY
)和更新列的前像(UPDATE VECTOR
),以及二级索引相关列的前像(INDEX_COLUMS
);
图2.3-5 UNDO全景图
最后我再回顾总结一下undo的全貌,如图2.3-5所示:
每个rollback segment
有1
个header page
,每个undo log segment
有1
个header page
和若干个normal page
;
每个事务可以有多个undo log
,但单个undo log
只能属于1
个事务。对于某个具体的undo log segment
来说,1
个事务只有1
个undo log
;
1
个undo log segment
同时只能属于1
个事务,但当undo log segment
仅使用了header page
,且该header page
至少有1/4
空闲空间时,该undo log segment
在上一个事务提交后可以被下一个事务复用,所以有时header page
中有多个undo log header
;
链A
:每个rollback segment
通过1024
个slots
可同时管理1024
个活跃态的undo log segment
;
链B
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_PAGE_LIST
结合UNDO LOG PAGE HEADER
中的TRX_UNDO_PAGE_NODE
,将本undo log segment
中的所有undo page
链接在一起;
链C
:UNDO LOG HEADER
中的TRX_UNDO_LOG_START
结合UNDO LOG RECORD
中的NEXT
和START
,将本undo log
中的所有undo log record
链接在一起;
链D
:UNDO LOG SEGMENT HEADER
中的TRX_UNDO_LAST_LOG
结合UNDO LOG HEADER
中的TRX_UNDO_NEXT_LOG
、TRX_UNDO_PREV_LOG
,将本undo log segment
中的所有undo log
链接在一起;
链E
:ROLLBACK PAGE HEADER
中的TRX_RSEG_HISTORY
结合UNDO LOG HEADER
中的TRX_UNDO_HISTORY_NODE
,将所有已经提交的undo log
链接在一起(不含insert
和cached
);
链F
:user records
中的DATA_ROLL_PTR
隐藏列结合undo log record
中的DATA_ROLL_PTR
,将和某条记录相关的所有undo log record
链接在一起,为MVCC
打下基础;
事务提交
MySQL和InnoDB是松耦合结构,为了协调上下层的提交一致性,采用了2阶段提交。不过从undo的角度来看,prepare阶段的工作不多,但还是要将UNDO LOG SEGMENT HEADER中的TRX_UNDO_STAT从TRX_UNDO_ACTIVE改为TRX_UNDO_PREPARED,并更新其它一些信息。重点工作发生在commit阶段:
如果是insert
型undo log segment
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_FREE
,后继直接释放;
如果是update
型undo log segment
,只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_CACHED
,并将本undo log segment
加入到undo cache list
中,以供将来复用(undo cache list
是内存结构);
如果是update
型undo log segment
,不满足只有1
个page
且page
的空闲空间至少有1/4
,将TRX_UNDO_STAT
设置为TRX_UNDO_TO_PURGE
,同时将rollback segment
中对应的slot
置为FIL_NULL
,并将本undo log segment
(undo log header
)加入到TRX_UNDO_HISTORY_NODE
中(链E
),等待被清理(不再用于MVCC
后即可被清理);
用户回滚与重启回滚
当用户主动或者数据库内部原因对事务发起回滚时,内存中保存有相关对象和指针,直接进行逆向操作即可,主要包括如下动作:
二级索引对主键索引有依赖关系,所以先回滚二级索引,再回滚主键索引;
对于打上删除标记的记录,清理删除标记;
对于更新操作,将数据回退到前像版本;
对于插入操作,直接删除记录;
重启回滚涉及的动作和用户回滚类似,但重启内存丢失,需要根据以下原则找到回滚记录:
遍历所有rollback segment
,根据每个rollback segment
中的slot
遍历undo log segment
;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_ACTIVE
,表示需要回滚,按照类似用户回滚的动作进行回滚;
如果undo log segment
的TRX_UNDO_STAT
为TRX_UNDO_PREPARED
,比较事务的XID
与binlog
,如果binlog
已经完成,则走提交流程,否则走回滚流程;
一致性读
RR隔离级别下一致性读的原则:事务开始时所有已经提交的事务对本事务来说都是可见的,所有未提交以及将来启动的事务对本事务来说都是不可见的。为此,开启事务时需要根据当前系统的活跃事务情况构建一个属于本事务的ReadView,包含如下要素:
up_limit_id
:最小活跃事务id
,比该id
小的事务对本事务来说都是可见的;
low_limit_id
:最大事务id
,比该事务id
大的事务都是在本事务开启之后生成的,所以对本事务来说都是不可见的;
trx_ids
:trx_ids
为本事务开启时系统活跃事务列表的快照,对于up_limit_id
和low_limit_id
之间的所有事务,如果在trx_ids
中表示本事务开启时该事务处于活跃态,尚未提交,所以不可见,否则可见;
图2.3-6 可见性判断
数据记录中有事务id和回滚指针的隐藏列,而每条undo log record中也有上1个事务id和指向上1个undo log record的指针,通过这些事务id和回滚指针,按照图2.3-6中的原则就可以进行无锁的一致性读。在RC场景下,原则和RR场景基本一致,不同的是RR在事务开始时建立ReadView,而RC在每条SQL语句开始前都要建立一个新的ReadView。
Purge
一致性读机制不再读到的这些历史版本就需要及时被清理掉,以便及时释放出undo空间。为此,获得系统中最老的ReadView,比该ReadView中up_limit_id小的事务对应的undo log都可以被清理掉。方法是遍历各个rollback segment的TRX_RSEG_HISTORY链表,链表中事务id小于up_limit_id的undo log都可以被清理掉。清理的动作主要包括2个方面:
清理记录:清理二级索引和主键索引中删除标记为真的记录;
清理undo
:清理undo log
和undo log segment
;
PostgreSQL设计原理
Page与前像设计
图2.4-1 page结构
表2.4-1 PageHeaderData结构
域
长度
含义
pd_lsn
8
本page被修改的lsn
pd_checksum
2
校验值,检查page异常
pd_flags
2
Page状态,组合标志位:
PD_HAS_FREE_LINES(0x0001)
:
ItemIdData
中有未使用的指针;
PD_PAGE_FULL(0x0002)
:
page
满;
PD_ALL_VISIBLE(0x0004)
:本
page
中所有记录都可见;
pd_lower
2
Free space的起始位置
pd_upper
2
Free space的结束位置
pd_special
2
Special space的位置
pd_pagesize_version
2
高位字节表示page大小,低位字节表示版本号
pd_prune_xid
4
本page最老的事务id,vacuum时使用
表2.4-2 ItemIdData结构
域
长度
含义
lp_off
15bit
记录在Items中的位置
lp_flag
2bit
Item状态:
LP_UNUSED(0)
:未使用;
LP_NORMAL(1)
:已使用;
LP_REDIRECT(2)
:
HOT
重定向;
LP_DEAD(3)
:
Dead
记录;
lp_len
15bit
在Items中的长度
PostgreSQL采用异地更新方案,前像没有存放在独立的undo区域中,而是和数据一起存放在data page中。因此,在研究PostgreSQL回滚前,有必要先了解一下page结构。如图2.4-1所示,page由如下部分组成:
PageHeaderData
:头部,定义了整个page
的总体信息,详细情况见表2.4-1
;
ItemIdData
:目录信息,每行记录占1
个位置,其下标是后继定位该记录的重要组成部分,详细情况见表2.4-2
,随着记录的增加,目录从上向下增加;
Items
:用户数据,随着记录的增加,数据从下向上增加;
Special Space
:存放和访问方法相关的数据,如对于BTree
索引存放左右兄弟节点的指针,普通表page
为空;
图2.4-2 Item结构
表2.4-3 HeapTupleHeaderData结构
域
长度
含义
t_xmin
4
insert本记录的事务id
t_xmax
4
delete/update本记录的事务id
t_cid/t_xvac
4
Insert/delete时的事务id和命令id
t_ctid
6
本记录下一个版本的地址,如果自己是最新版本则指向自己
t_infomask2
2
低11位表示当前记录的列数;其它位为标志位:
HEAP_KEYS_UPDATED(0x2000)
:被更新或删除;
HEAP_HOT_UPDATED(0x4000)
:
HOT-updated
;
HEAP_ONLY_TUPLE(0x8000)
:
heap only tuple
;
HEAP_XACT_MASK(0xE000)
:用于可见性判断;
t_infomask
2
组合标志位:
HEAP_HASNULL(0x0001)
:有
NullBitmap
域;
HEAP_HASVARWIDTH(0x0002)
:有变长列;
HEAP_HASEXTERNAL(0x0004)
:有外部存储列;
HEAP_HASOID_OLD(0x0008)
:有
object id
域;
HEAP_XMAX_KEYSHR_LOCK(0x0010)
:
t_xmax
状态;
HEAP_COMBOCID(0x0020)
:
t_cid
是
combo id
;
HEAP_XMAX_EXCL_LOCK(0x0040)
:
t_xmax
状态;
HEAP_XMAX_LOCK_ONLY(0x0080)
:
t_xmax
状态;
HEAP_XMIN_COMMITTED(0x0100)
:
t_xmin
状态;
HEAP_XMIN_INVALID(0x0200)
:
t_xmin
状态;
HEAP_XMAX_COMMITTED(0x0400)
:
t_xmax
状态;
HEAP_XMAX_INVALID(0x0800)
:
t_xmax
状态;
HEAP_XMAX_IS_MULTI(0x1000)
:
MultiXactId
;
HEAP_UPDATED(0x2000)
:
Updated Version
;
t_hoff
1
记录头大小
Item存放一条具体的记录,如图2.4-2所示,Item由HeapTupleHeaderData、NullBitmap、ObjectId、UserData 4个部分组成:
HeapTupleHeaderData
:记录头,固定大小,t_xmin
和t_xmax
用于判断记录在事务间的可见性判断,t_cid
用于记录在本事务内的可见性判断,t_ctid
用于将记录的多个版本链接在一起,详细情况见表2.4-3
;
NullBitmap
:可选,t_infomask
中HEAP_HASNULL
标志位为真时有此字段,用于表达相关列是否为null
;
ObjectId
:可选,t_infomask
中HEAP_HASOID_OLD
标志位为真时有此字段;
UserData
:真正的用户数据;
图2.4-3 前像设计示例
了解了PostgreSQL的page和item设计,下面来看PostgreSQL是如何进行异地更新的,如图2.4-3所示,DML主要包括下面3种情况:
Insert
:insert
直接插入数据,不需要记录前像,t_xmin
记录插入本记录的事务id
;
Delete
:delete
不进行实际删除,记录本身就是前像,t_xmax
记录删除本记录的事务id
;
Update
:不进行本地更新,而是克隆1
个新记录,然后在新纪录上更新(实际上是delete+insert
),旧记录就是前像,并将旧记录的t_ctid
指向新记录的位置,从而形成一个旧记录指向新记录的链表;
可见PostgreSQL通过上述方法保存了前像,同时通过clog、记录中的ItemIdData.lp_flag、记录中的t_infomask保存相关事务的状态,这样就可以合理地应用保存的前像。当然记录中的ItemIdData.lp_flag和t_infomask的状态可能不是最新的,需要后台根据clog中的真实状态进行刷新。
用户回滚
PostgreSQL采用异地更新的最大优势就是回滚。由于前像仍然以记录的方式存在于数据当中,所以用户回滚只需要记录事务的状态,释放相关资源即可,非常高效。至于遗留下来的垃圾数据,通过后台进程逐步清理即可。
一致性读
和MySQL类似,PostgreSQL也采用基于事务id的一致性读,该方法需要获取当前的活跃事务id列表,PostgreSQL将其称为transaction snapshot。RR或者SERIALIZABLE隔离级别下,事务在启动时会生成本事务的transaction snapshot。RC隔离级别下,每条SQL语句执行前生成最新的transaction snapshot。
由于PostgreSQL采用的是异地更新,一致性判断的规则更加复杂,需要综合判断事务状态、t_xmin、t_xmax和transaction snapshot,具体规则如下:
类别1
:t_xmin
的事务状态为ABORTED
插入此条记录的事务已经回滚,所以本记录对任何事务都不可见。
类别2
:t_xmin
的事务状态为IN_PROGRESS
R1: t_xmin=current_txid & t_xmax=INVALID -> visible
R2: t_xmin=current_txid & t_xmax!=INVALID -> invisible
R3: t_xmin!=current_txid -> invisible
R1表示当前事务读取本事务之前插入的数据,所以可见。R2表示该记录虽然是本事务插入的,但已被本事务删除,所以不可见。R3表示当前事务读取其它事务插入的记录,且其它事务尚未提交,所以不可见。
类别3
:t_xmin
的事务状态为COMMITTED
R4: snapshot(t_xmin)=active -> invisible
R5: t_xmax=INVALID | status(t_xmax)=ABORTED -> visible
R6: status(t_xmax)=IN_PROGRESS & t_xmax=current_txid -> invisible
R7: status(t_xmax)=IN_PROGRESS & t_xmax!=current_txid -> visible
R8: status(t_xmax)=COMMITTED & snapshot(t_xmax)=active -> visible
R9: status(t_xmax)=COMMITTED & snapshot(t_xmax)!=active -> invisible
判断规则是R4到R9依次判断,一旦满足条件就以此结果为依据。R4表示插入这条记录的事务虽然已经提交了,但该事务仍然在snapshot中,所以不可见。R5表示没有其它事务更新过该记录或者更新过该记录的事务已经回滚,所以可见。R6表示本事务已经更新过该记录,所以该记录的上一个版本不可见。R7表示其它事务正在更新该记录,所以该记录的上一个版本可见。R8表示更新或删除该记录的事务已经提交,但该事务在snapshot中,所以可见。R9表示更新或删除该记录的事务已经提交,但该事务不在snapshot中,需要看更新的版本,所以不可见。
HOT与Index-Only Scan
图2.4-4 索引与记录关系示意图(非HOT)
如图2.4-4所示,由于记录采用了异地更新,当记录生成一个新的版本时,为了保持同步,索引项不管有没有发生变化,都需要生成一个对应的新版本,和相应的数据记录对应起来。这导致了索引不必要的浪费,并为后继的vacuum带来了额外的负担。
图2.4-5 索引与记录关系示意图(HOT)
图2.4-6 索引与记录关系示意图(HOT, After Prune)
图2.4-7 索引与记录关系示意图(HOT, After Defragmentation)
HOT指当更新同时满足:1)新版本的记录和旧版本的据在同一个page内;2)更新的列不涉及索引列时,索引不再生成新版本,从而解决索引成本过高的问题。上述条件2很容易理解,条件1的主要目的是平衡读性能,否则通过索引访问很可能需要2次page调度,即先通过索引定位到旧版本,然后再通过旧版本的c_ctid或者ItemIdData中重定向定位到新版本。
如图2.4-5所示,HOT生效时索引仍然指向旧记录,新记录通过旧记录的t_ctid访问,同时旧记录的t_infomask2打上HEAP_HOT_UPDATED标志,新记录的t_infomask2打上HEAP_ONLY_TUPLE标志。当旧记录的状态为dead后,图2.4-6和图2.4-7分别给出修剪(prune)和去碎片化(defragmentation)实施后的情况,旧记录的ItemIdData保留,并重定向到记录的ItemIdData上。
Index-Only Scan指索引中已包含查询所需所有列,原则上直接扫描或定位索引page,无需扫描或定位数据page即可完成本次查询。PostgreSQL的一致性判断规则复杂,且依赖于数据page中各记录的t_xmin、t_xmax及其状态信息,从而导致Index-Only Scan无法实施。为此,PostgreSQL采用了一个简单直接的办法,在后台维护一个visibility map,一旦确定某个数据page中的所有记录都是可见的,就将该page加入到visibility map中,直接对外可见。这样扫描索引时,如果发现对应的数据page在visibility map中就仅扫描索引,不再访问数据page,否则仍然访问数据page,根据记录的t_xmin和t_xmax及其相应状态进行可见性判断。
Vacuum
从前面几个小节我们知道数据page和索引page都会有过期数据(dead tuple),vacuum的工作主要就是清理这些过期数据。vacuum分为concurrent vacuum和full vacuum,前者允许vacuum期间并发执行其它只读事务,后者不允许vacuum期间执行其它事务,但两者的实现原理是一致的。
concurrent vacuum的主要工作是针对某个特定的table或所有table执行如下任务:
清理dead tuple
,即删除无用的数据前像:
删除数据page
中的dead tuple
,并对数据page
做去碎片化整理;
删除索引page
中指向dead tuple
的索引记录;
冻结过期txids
,PostgreSQL
的txid
只有4
个字节,需要及时清理,以解决循环使用的问题:
冻结过期的txids
;
更新系统catalogs
;
清理不必要的clog
;
其它任务:
更新FSM
和VM
;
更新系统统计项;
concurrent vacuum的大致实现过程如下:
step1
:从table list
中获取某个待实施的table
;
step2
:针对该table
申请UpdateExclusiveLock
,该锁仍然允许其它事务读本table
;
step3
:扫描本table
中所有数据page
,识别出所有dead tuple
,必要时冻结过期txids
;
step4
:删除指向dead tuple
的所有索引记录;
step5
:针对本table
的每个数据page
,重复step6
和step7
;
step6
:删除dead tuple
,并做去碎片化工作;
step7
:更新FSM
和VM
;
step8
:如果数据page
中不再含有任何tuple
,释放该数据page
;
step9
:更新统计信息和catalogs
;
step10
:清理不必要的clog page
和clog
文件;
其中第3步是将扫描的结果存放在内存中(maintenance_work_mem),如果内存已满则开始实施第4步到第8步,然后清理内存,并回到第3步从上次断点开始继续扫描。
总结与分析
首先来看记录前像对正常写操作的影响,下面从DML操作类型和索引的角度来分析:
insert
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
无需做额外处理;
delete
:Oracle
和MySQL
都会生成1
条undo
记录,PostgreSQL
只需设置相关状态;
update
:Oracle
和MySQL
会生成1
条undo
记录,仅记录更改列的前像,PostgreSQL
需要完整克隆1
条新记录,并更改旧记录的状态;
索引:Oracle
将索引当数据,数据与索引的处理方式一致。MySQL
的索引不记undo
,而是通过数据的undo
间接记录索引的变化。PostgreSQL
的索引页采用异地更新,但不记录事务状态,同时HOT
方式进行一定程度的优化。当然对于索引来说,索引列的更新涉及索引记录在BTree
中位置的调整,所以Oracle
和MySQL
都是将索引的update
转换为delete+insert
,即异地更新方案;
总的来说Oracle、MySQL、PostgreSQL相差不大,insert、delete操作PostgreSQL更加轻量,而update操作Oracle、MySQL相对较为轻量。但Oracle在设计上相对更加简洁,索引、数据的处理原则基本一致,没有太多的特殊情况要考虑。
其次看回滚效率,PostgreSQL是最高效的。PostgreSQL的前像就再数据之中,只需要记录事务的状态,不需要实施回滚动作。Oracle和MySQL则厚重得多,需要从undo日志中读出前像,并应用到实际数据和索引上,同时还要记录重做日志。
再次看存储效率,我们还是从DML操作类型、数据和索引这几个角度来看:
insert
:Oracle
仅记录RowID
,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
delete
:Oracle
需要记录整条记录的前像,MySQL
记录主键值,PostgreSQL
不需要做额外记录;
update
:Oracle
需要记录被更新的列,MySQL
记录主键值和被更新的列,PostgreSQL
需要拷贝一个完整的新行(所有列);
记录:Oracle
的记录不需要做太多特殊处理,MySQL
需要引入13
个字节的事务id
和回滚指针,PostgreSQL
需要引入22
个字节的t_xmin
、t_xmax
、t_cid
、t_ctid
、t_infomask
、t_infomask2
;
索引:Oracle
的索引处理等同于数据,MySQL
需要将索引的所有列记录在数据的undo
中,并将update
转换为insert+delete
,PostgreSQL
需要生成新的索引记录;
从数据的前像来看,对于insert和delete操作,PostgreSQL基本不消耗存储空间;对于update操作,Oracle和MySQL优于PostgreSQL,因为前者仅需要记录被更新列的前像。对于delete操作,Oracle和MySQL都是在原记录上打上删除标签,但Oracle会在undo中记录整条记录的前像,而MySQL仅记录主键值。虽然MySQL更省空间,但Oracle将undo和数据block清理的工作完全解耦,没有依赖关系,且为将来的闪回提供了可能。从对记录的影响来看,Oracle是最优的,MySQL其次,PostgreSQL最后,虽然单条记录占用的字节数不是太多,但对于大数据量的窄表(表的列数很少,但记录非常庞大)来说,占用的空间会比较可观。
再再次看回收效率。Oracle在undo segment中维护retention table,直接按照时间将相关extent失效即可,不需要做太多的回收动作,block上的事务状态主要集中在ITL上,调整面也比较小。MySQL相对复杂一点,通过rollback segment和undo log segment进行日志管理,1个undo log segment同时只能服务于1个事务,segment的管理成本比Oracle高,但通过history list进行回收,回收涉及的undo page也相对比较集中。Page上基本不涉及事务状态,调整面最小。PostgreSQL的前像和数据存放在一起,需要通过扫描所有page确认dead tuple,成本非常高,事务状态涉及每条记录,调整面也非常大。
最后看一致性读和闪回。Oracle通过undo日志和scn进行一致性判断,简洁高效。MySQL和PostgreSQL分别需要维护read view和transaction snapshot,高并发时事务列表会很长,活跃事务列表的比较效率也较低,且每个事务都需要维护一个read view或transaction snapshot,RC隔离级别下每执行一次SQL还需要更新一次。虽然MySQL和PostgreSQL原理相似,但由于PostgreSQL涉及t_xmin、t_xmax、t_maskinfo、transaction snapshot、clog,判断规则极其复杂,效率最低。在扫描数据和索引时,涉及多个版本(关键看vacuum的频率),虽然VM可以有一定程度的缓解,但效率要低于Oracle和MySQL。
原则上只要前像存在,就可以提供闪回功能。Oracle在系统中维护有逻辑时间(scn),且可以和墙上时间转换,事务提交时记录当时的scn,这样就可以基于scn和墙上时间进行闪回查询和闪回恢复。MySQL和PostgreSQL当前还没有提供闪回功能,提供该功能的挑战在于:1)可以基于LSN进行闪回回退,但需要对当前的前像回收机制进行较大幅度的调整;2)当前的可见性判断机制是基于read view和transaction snapshot的,该机制无法复用到闪回查询,因为用户并不知道历史某个时刻的read view和transaction snapshot。
PDF下载地址:http://blog.itpub.net/69912723/viewspace-2717309/