多租户体系架构,使得oracle 数据库成为了一个多租户的容器数据库,也就是container database,也就是CDB。而一个CDB可以包含0个、一个或者多个用户创建的可插入的数据库,也就是pluggable database,也就是PDB,也就是所谓的“租户”。这就像合租房一样,每个租户可以获得其所需的各种资源。也可以说CDB就是各PDB的母体。
由于故障导致Instance异常关闭或由于执行了shutdown abort/startup force都会导致数据库实例在重启时执行实例恢复,具体就是SMON来完成这个任务了。实例可以理解为内存级别,不需要人工干预,需要mttr参数控制时间。
oracle中存在bufer cache缓冲区,正常运行期间,里面存在很多脏块,如果实例异常,导致脏块未写入磁盘,如果没有什么手段把崩溃后的脏块找回来,肯定会出现数据不一致的情况。当然关系数据库设计之初,这种情况就已经考虑了,通过重做日志,完全可以实现实例crash不丢数据。 总而言之,所有已提交的事务,都是可以恢复的,这样才能保证数据的一致性。
1、系统检查点scn
每次检查点完成都会记录到控制文件中,由于控制文件是全局的,多库结构下系统只有cdb有全局scn。
SQL> select con_id,checkpoint_change# from v$database;
CON_ID CHECKPOINT_CHANGE#
---------- ------------------
0 4332329
2、数据文件检查点scn(CHECKPOINT_CHANGE#)
每当发生检查点动作时,oracle把每个数据文件SCN保存在控制文件中。
SQL> select CON_ID,CHECKPOINT_CHANGE#,LAST_CHANGE# from v$datafile order by 1;
CON_ID CHECKPOINT_CHANGE# LAST_CHANGE#
---------- ------------------ ------------
1 4332329
1 4332329
1 4332329
1 4332329
2 2145459 2145459
2 2145459 2145459
2 2145459 2145459
3 4332329
3 4332329
3 4332329
3 4332329
CON_ID CHECKPOINT_CHANGE# LAST_CHANGE#
---------- ------------------ ------------
4 4332329
4 4332329
4 4332329
5 4332329
5 4332329
5 4332329
8 4332329
8 4332329
8 4332329
20 rows selected.
3、数据文件启动SCN(数据文件头)和终止SCN(LAST_CHANGE#)
每个数据文件头会记录启动SCN,而控制文件会记录每个文件的终止SCN 这两个SCN 来确定文件是否需要恢复。
select CON_ID,CHECKPOINT_CHANGE#,LAST_CHANGE# from v$datafile order by 1;
如果12C以后多库结构,以abort的方式停止pdb和cdb的区别,需要注意。
这里不讨论media recover的场景,该场景不是实例异常宕机导致的
检查点队列可以确认数据库恢复的起点,因为检查点队列,是根据数据块第一次脏的时间进行连接,检查点队列最前面的块,就是最早脏的块了,增量检查点会把这个块地址记录到控制文件中,当实例恢复时,就可以确认恢复的起点了(LRBA),**检查点的触发频率,不仅影响数据库读写效率,还会影响数据的崩溃恢复时间,检查点的间隔越大,需要前滚的日志越多,恢复越慢,但是如果间隔越小,刷脏频率增高,IO读写压力会变大,此时增量检查点变的尤为重要**。 恢复的终点比较容易确认,就是on disk RBA,最后一条写到磁盘的redo地址。
回滚,有些未提交的事务也会构造出来,但是在undo记录了未提交的事务,只需根据undo记录记性回滚即可。
由于RAC是多节点共享访问一份数据,如果一个节点异常关闭,不会等到该节点恢复正常再进行崩溃恢复。因为活着的节点可以完成恢复的过程,同样也是先前滚再回滚,当然在开始之前,必须禁止故障节点访问共享数据,需要对故障节点进行IO Fencing。 这个过程是也是oracle内部机制处理,RAC架构恢复过程和单实例区别在于,故障实例的脏数据恢复过程需要用到PCM-lock来判断,哪些是需要恢复的脏数据,因为正常节点还是会访问共享数据,这个恢复过程更复杂。
无论是单节点还是RAC架构,DBA都无法干预这个过程,但是可以通过一些参数来缩短这个恢复过程,以减少数据库启动的时间。
PostgreSQL常用容灾架构主从流复制,本文从从库切入,说明流复制的运行机制及原理。
声明:该系列文章为Hironobu SUZUKI的私人项目,为促进PostgreSQL的技术分享,现翻译出来供大家学习。
版权:所有版权归 Hironobu SUZUKI所有,本系列文章只是负责翻译和分享工作,再次感谢大神。
概述:本系列文章,旨在分享PostgreSQL技术,推广PostgreSQL。由于该系列文章原作者还在更新中,中文版将会同步更新
本章和下一章总结了PostgreSQL的基本知识,以帮助阅读后续章节。 在本章中,描述了以下主题:
如果您已经熟悉它们,可以跳过本章。
数据库集群是PostgreSQL服务器管理的数据库集合。 如果你是第一次听到这个定义,你可能会对此感到疑惑,但PostgreSQL中的术语“数据库集群” 并不意味着“一组数据库服务器”。 PostgreSQL服务器在单个主机上运行并管理单个数据库集群。
图1.1显示了数据库集群的逻辑结构。 数据库是数据库对象的集合。 在关系数据库理论中, 数据库对象是用于存储或引用数据的数据结构。 (堆) 表是它的典型示例,还有更多像索引,序列,视图,函数等。 在PostgreSQL中,数据库本身也是数据库对象,并且在逻辑上彼此分离。 所有其他数据库对象(例如,表,索引等)属于它们各自的数据库。
图1.1。 数据库集群的逻辑结构。
PostgreSQL中的所有数据库对象都由相应的对象标识符(OID)进行内部管理,这些标识符是无符号的4字节整数。 数据库对象与相应OID之间的关系存储在适当的系统目录中 ,具体取决于对象的类型。 例如,数据库和堆表的OID分别存储在pg_database和pg_class中 ,因此您可以通过发出以下查询来查找您想要知道的OID:
sampledb =#SELECT datname,oid FROM pg_database WHERE datname ='sampledb';
datname | OID
---------- + -------
sampledb | 16384
(1排)
sampledb = #SELECT relname,oid FROM pg_class WHERE relname ='sampletbl';
relname | OID
----------- + -------
sampletbl | 18740
(1排)
数据库集群基本上是一个称为基本目录的目录 ,它包含一些子目录和大量文件。 如果执行initdb实用程序以初始化新数据库集群,则将在指定目录下创建基目录。 虽然它不是必须的,但基本目录的路径通常设置为环境变量PGDATA 。
图1.2显示了PostgreSQL中数据库集群的一个示例。 数据库是base子目录下的子目录,每个表和索引(至少)一个文件存储在它所属的数据库的子目录下。 还有几个包含特定数据和配置文件的子目录。 虽然PostgreSQL支持表空间 ,但该术语的含义与其他RDBMS不同。 PostgreSQL中的表空间是一个包含基本目录之外的数据的目录。
图1.2。 数据库集群的一个示例。
在以下小节中,描述了数据库集群的布局,数据库,与表和索引关联的文件以及PostgreSQL中的表空间。
数据库集群的布局已在官方文档中描述。 表1.1中列出了文档一部分中的主要文件和子目录: 表1.1:基本目录下的文件和子目录的布局(来自官方文档)
文件 | 描述 |
---|---|
PG_VERSION | 包含PostgreSQL主版本号的文件 |
pg_hba.conf | 用于控制PosgreSQL客户端身份验证的文件 |
pg_ident.conf | 用于控制PostgreSQL用户名映射的文件 |
postgresql.conf | 用于设置配置参数的文件 |
postgresql.auto.conf | 用于存储在ALTER SYSTEM(版本9.4或更高版本)中设置的配置参数的文件 |
postmaster.opts | 记录服务器上次启动的命令行选项的文件 |
子目录 | 描述 |
---|---|
base/ | 包含每个数据库子目录的子目录。 |
global/ | 包含群集范围表的子目录,例如pg_database和pg_control。 |
pg_commit_ts/ | 包含事务提交时间戳数据的子目录。 9.5或更高版本 |
pg_clog/ | (版本9.6或更早版本 包含事务提交状态数据的子目录。 它在版本10中重命名为pg_xact.CLOG将在5.4节中描述。 |
pg_dynshmem/ | 子目录,包含动态共享内存子系统使用的文件。 版本9.4或更高版本。 |
pg_logical/ | 子目录,包含逻辑解码的状态数据。 版本9.4或更高版本。 |
pg_multixact/ | 包含多次事务状态数据的子目录(用于共享行锁) |
pg_notify/ | 包含LISTEN / NOTIFY状态数据的子目录 |
pg_repslot/ | 包含复制槽数据的子目录。 版本9.4或更高版本。 |
pg_serial/ | 包含有关已提交的可序列化事务(版本9.1或更高版本)的信息的子目录 |
pg_snapshots/ | 包含导出快照的子目录(版本9.2或更高版本)。 PostgreSQL的函数pg_export_snapshot在此子目录中创建快照信息文件。 |
pg_stat/ | 包含统计子系统永久文件的子目录。 |
pg_stat_tmp/ | 子目录,包含统计子系统的临时文件。 |
pg_subtrans/ | 包含子事务状态数据的子目录 |
pg_tblspc关联/ | 包含指向表空间的符号链接的子目录 |
pg_twophase/ | 子目录,包含准备好的事务的状态文件 |
pg_wal/ | (版本10或更高版本) 包含WAL(Write Ahead Logging)段文件的子目录。 它在版本10中从pg_xlog重命名。 |
pg_xact/ | (版本10或更高版本) 包含事务提交状态数据的子目录。 它在版本10中从pg_clog重命名.CLOG将在5.4节中描述。 |
pg_xlog/ | (版本9.6或更早版本) 包含WAL(Write Ahead Logging)段文件的子目录。 它在版本10中重命名为pg_wal 。 |
数据库是base子目录下的子目录; 并且数据库目录名称与相应的OID相同。 例如,当数据库sampledb的OID为16384时,其子目录名称为16384。
$ cd $ PGDATA
$ ls -ld base / 16384
drwx ------ 213 postgres postgres 7242 8 26 16:33 16384
大小小于1GB的每个表或索引是存储在其所属的数据库目录下的单个文件。 作为数据库对象的表和索引由各个OID在内部管理,而这些数据文件由变量relfilenode管理 。 表和索引的relfilenode值基本上但不总是与相应的OID匹配,详细信息如下所述。
让我们展示表sampletbl的OID和relfilenode :
sampledb = #SELECT relname,oid,relfilenode FROM pg_class WHERE relname ='sampletbl';
relname | oid | relfilenode
----------- + ------- + -------------
sampletbl | 18740 | 18740
(1排)
从上面的结果中,您可以看到oid和relfilenode值都相等。 您还可以看到表sampletbl的数据文件路径是’base / 16384/18740’ 。
cd $ PGDATA
$ ls -la base / 16384/18740
-rw ------- 1 postgres postgres 8192 Apr 21 10:21 base / 16384/18740
通过发出一些命令(例如,TRUNCATE,REINDEX,CLUSTER)来更改表和索引的relfilenode值。 例如,如果我们截断表sampletbl ,PostgreSQL会为表分配一个新的relfilenode(18812),删除旧的数据文件(18740),并创建一个新的(18812)。
sampledb =#TRUNCATE sampletbl;
TRUNCATE TABLE
sampledb = #SELECT relname,oid,relfilenode FROM pg_class WHERE relname ='sampletbl';
relname | oid | relfilenode
----------- + ------- + -------------
sampletbl | 18740 | 18812
(1排)
在9.0或更高版本中,内置函数pg_relation_filepath非常有用,因为此函数返回具有指定OID或名称的关系的文件路径名。
sampledb =#SELECT pg_relation_filepath('sampletbl');
pg_relation_filepath
----------------------
碱/一万八千八百十二分之一万六千三百八十四
(1排)
当表和索引的文件大小超过1GB时,PostgreSQL会创建一个名为relfilenode.1的新文件并使用它。 如果新文件已填满,则将创建名为relfilenode.2的下一个新文件,依此类推。
$ cd $ PGDATA
$ ls -la -h base / 16384/19427 *
-rw ------- 1 postgres postgres 1.0G Apr 21 11:16 data / base / 16384/19427
-rw ------- 1 postgres postgres 45M Apr 21 11:20 data / base / 16384 / 19427.1
在构建PostgreSQL时,可以使用配置选项–with-segsize更改表和索引的最大文件大小。
仔细查看数据库子目录,您会发现每个表都有两个相关文件,后缀分别为’_fsm’和’_vm’。 这些被称为自由空间映射和可见性映射 ,分别存储表文件中每个页面上的可用空间容量和可见性的信息(参见第5.3.4 节和第6.2 节中的更多细节)。 索引仅具有单独的可用空间映射,并且没有可见性映射。
具体示例如下所示:
$ cd $ PGDATA
$ ls -la base / 16384/18751 *
-rw ------- 1 postgres postgres 8192 Apr 21 10:21 base / 16384/18751
-rw ------- 1 postgres postgres 24576 Apr 21 10:18 base / 16384 / 18751_fsm
-rw ------- 1 postgres postgres 8192 Apr 21 10:18 base / 16384 / 18751_vm
它们也可以在内部被称为每种关系的叉子 ; 可用空间映射是表/索引数据文件的第一个分支(fork编号为1),可见性映射表的数据文件的第二个分支(fork编号为2)。 数据文件的分叉号为0。
PostgreSQL中的表空间是基本目录之外的附加数据区域。 此功能已在8.0版中实现。
图1.3显示了表空间的内部布局,以及与主数据区的关系。
图1.3。 数据库群集中的表空间。
在发出CREATE TABLESPACE语句时指定的目录下创建表空间,并在该目录下创建特定于版本的子目录(例如,PG_9.4_201409291)。 版本特定的命名方法如下所示。
PG ‘主要版本’‘目录版本号’
例如,如果在’/ home / postgres / tblspc’中创建一个表空间’new_tblspc’ ,其oid为16386,则会在表空间下创建一个子目录,例如’PG_9.4_201409291’ 。
$ ls -l / home / postgres / tblspc /
总共4
drwx ------ 2 postgres postgres 4096 Apr 21 10:08 PG_9.4_201409291
表空间目录由pg_tblspc子目录中的符号链接寻址 ,链接名称与表空间的OID值相同。
$ ls -l $ PGDATA / pg_tblspc /
总共0
lrwxrwxrwx 1 postgres postgres 21 Apr 21 10:08 16386 - > / home / postgres / tblspc
如果在表空间下创建新数据库(OID为16387),则会在特定于版本的子目录下创建其目录。
$ ls -l /home/postgres/tblspc/PG_9.4_201409291/
总共4
drwx ------ 2 postgres postgres 4096 Apr 21 10:10 16387
如果创建属于在基本目录下创建的数据库的新表,首先,在特定于版本的子目录下创建名称与现有数据库OID相同的新目录,然后放置新表文件在创建的目录下。
sampledb = #CREATE TABLE newtbl(.....)TABLESPACE new_tblspc;
sampledb =#SELECT pg_relation_filepath('newtbl');
pg_relation_filepath
----------------------------------------------
pg_tblspc关联/ 16386 / PG_9.4_201409291 /18894分之16384
在数据文件(堆表和索引,以及可用空间映射和可见性映射)内部,它被分为固定长度的页 (或块 ),默认为8192字节(8 KB)。 每个文件中的那些页面从0开始按顺序编号,这些数字称为块编号 。 如果文件已填满,PostgreSQL会在文件末尾添加一个新的空页以增加文件大小。
页面的内部布局取决于数据文件类型。 在本节中,将描述表格布局,以下章节将要求提供信息。
图1.4。 堆表文件的页面布局。
图1.4。堆表文件的页面布局。
表中的页面包含如下所述的三种数据:
1、heap tuple(s) - 堆元组本身就是一个记录数据。 它们从页面底部按顺序堆叠。 元组的内部结构在第5.2节和第9章中描述,因为需要知道PostgreSQL中的并发控制(CC)和WAL。 2、行指针 - 行指针长4个字节,并保存指向每个堆元组的指针。 它也被称为项目指针 。 行指针形成一个简单的数组,它扮演元组索引的角色。 每个索引从1开始按顺序编号,并称为偏移号 。 当向页面添加新元组时,新的行指针也会被推到数组上以指向新的元组。 3、标头数据 - 由结构PageHeaderData定义的标头数据在页面的开头分配。 它长24个字节,包含有关页面的一般信息。 该结构的主要变量如下所述。
行指针末尾和最新元组开头之间的空白空间称为空闲空间或空洞 。
为了识别表中的元组,内部使用元组标识符(TID) 。 TID包括一对值:包含元组的页面的块编号 ,以及指向元组的行指针的偏移编号 。 其用法的典型示例是索引。 请参见第1.4.2节中的更多细节。
PageHeaderData在src / include / storage / bufpage.h中定义 。
另外,使用称为TOAST (超大属性存储技术)的方法来存储和管理其大小大于约2KB(约为8KB的1/4)的堆元组。 有关详细信息,请参阅PostgreSQL文档 。
在本章的最后,描述了编写和读取堆元组的方法。
假设一个表由一个页面组成,该页面只包含一个堆元组。 此页面的pd_lower指向第一行指针,行指针和pd_upper都指向第一个堆元组。 见图1.5(a)。
插入第二个元组时,将其放在第一个元组之后。 第二行指针被推到第一行,它指向第二个元组。 pd_lower更改为指向第二行指针,pd_upper更改为第二个堆元组。 见图1.5(b)。 此页面中的其他标题数据(例如,pd_lsn,pg_checksum,pg_flag)也被重写为适当的值; 更多细节在第5.3节和第9章中描述。
图1.5。 编写堆元组。
图1.5。编写堆元组。
###1.4.2。 阅读堆元组
这里概述了两种典型的访问方法,顺序扫描和B树索引扫描:
图1.6。 顺序扫描和索引扫描。
PostgreSQL还支持TID-Scan, Bitmap-Scan和Index-Only-Scan。
TID-Scan是一种通过使用所需元组的TID直接访问元组的方法。 例如,要在表中找到第0个页面中的第一个元组,请发出以下查询:
sampledb = #SELECT ctid,data FROM sampletbl WHERE ctid ='(0,1)';
ctid | 数据
------- + -----------
(0,1)| AAAAAAAAA
(1排)
Index-Only-Scan将在第7章中详细介绍。
©版权所有 2015-2018 Hironobu SUZUKI版权所有。
本文介绍TiDB的一些基本概念、基本实现原理以及运用的一些开源技术,当然需要说明的是,TiDB不是平常理解的数据库,它是一套架构,通过几个组件的互相调用和一些分布式原理实现。
(http://www.oschina.net/p/grpc-framework) gRPC() 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。
目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
(https://blog.csdn.net/zhufenglonglove/article/details/54286068) RocksDB项目起源于Facebook的一个实验项目,该项目旨在开发一个与快速存储器(尤其是闪存)存储数据性能相当的数据库软件,以应对高负载服务。
这是一个c++库,可用于存储键和值,可以是任意大小的字节流。它支持原子读和写。
RocksDB具有高度灵活的配置功能,可以通过配置使其运行在各种各样的生产环境,包括纯内存,Flash,硬盘或HDFS。它支持各种压缩算法,并提供了便捷的生产环境维护和调试工具。
RocksDB借鉴了开源项目LevelDB的重要代码和Apache HBase项目的重要思想。最初的代码来源于开源项目leveldb 1.5分叉。它借鉴了了Facebook的代码和思想。
Raft是一种共识算法,旨在替代Paxos。 它通过逻辑分离比Paxos更容易理解,但它也被正式证明是安全的,并提供了一些额外的功能。 Raft提供了一种在计算系统集群中分布状态机的通用方法,确保集群中的每个节点都同意一系列相同的状态转换。
要深入了解 TiDB 的水平扩展和高可用特点,首先需要了解 TiDB 的整体架构。TiDB 集群主要包括三个核心组件:TiDB Server,PD Server 和 TiKV Server。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件。
TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。
Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。
PD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署 3 个节点。
TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。
TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,并融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。
TiKV 的事务采用的是 Percolator 模型,并且做了大量的优化。事务的细节这里不详述,大家可以参考论文以及我们的其他文章。这里只提一点,TiKV 的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方会尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,就是计数器,多个客户端同时修改少量行,导致冲突严重的,造成大量的无效重试。
该端引用网址为:http://www.itpub.net/thread-2059684-1-1.html
总体来说,TiKV 的读写事务分为两个阶段:1、Prewrite 阶段;2、Commit 阶段。
客户端会缓存本地的写操作,在客户端调用 client.Commit() 时,开始进入分布式事务 prewrite 和 commit 流程。
Prewrite 对应传统 2PC 的第一阶段:
1、首先在所有行的写操作中选出一个作为 primary row,其他的为 secondary rows 2、PrewritePrimary: 对 primaryRow 写入锁(修改 meta key 加入一个标记),锁中记录本次事务的开始时间戳。上锁前会检查:
i.该行是否已经有别的客户端已经上锁 (Locking)
ii.是否在本次事务开始时间之后,检查versions ,是否有更新 [startTs, +Inf) 的写操作已经提交 (Conflict)
在这两种种情况下会返回事务冲突。否则,就成功上锁。将行的内容写入 row 中,版本设置为 startTs 3、将 primaryRow 的锁上好了以后,进行 secondaries 的 prewrite 流程:
i.类似 primaryRow 的上锁流程,只不过锁的内容为事务开始时间 startTs 及 primaryRow 的信息
ii.检查的事项同 primaryRow 的一致
iii.当锁成功写入后,写入 row,时间戳设置为 startTs
以上 Prewrite 流程任何一步发生错误,都会进行回滚:删除 meta 中的 Lock 标记 , 删除版本为 startTs 的数据。
当 Prewrite 阶段完成以后,进入 Commit 阶段,当前时间戳为 commitTs,TSO会保证 commitTs> startTs
Commit 的流程是,对应 2PC 的第二阶段:
1、commit primary: 写入 meta 添加一个新版本,时间戳为 commitTs,内容为 startTs, 表明数据的最新版本是 startTs 对应的数据
2、删除 Lock 标记
值得注意的是,如果 primary row 提交失败的话,全事务回滚,回滚逻辑同 prewrite 失败的回滚逻辑。
如果 commit primary 成功,则可以异步的 commit secondaries,流程和 commit primary 一致, 失败了也无所谓。Primary row 提交的成功与否标志着整个事务是否提交成功。
事务中的读操作: 1、检查该行是否有 Lock 标记,如果有,表示目前有其他事务正占用此行,如果这个锁已经超时则尝试清除,否则等待超时或者其他事务主动解锁。注意此时不能直接返回老版本的数据,否则会发生幻读的问题。
2、读取至 startTs 时该行最新的数据,方法是:读取 meta ,找出时间戳为 [0, startTs], 获取最大的时间戳 t,然后读取为于 t 版本的数据内容。
由于锁是分两级的,Primary 和 Seconary row,只要 Primary row 的锁去掉,就表示该事务已经成功提交,这样的好处是 Secondary 的 commit 是可以异步进行的,只是在异步提交进行的过程中,如果此时有读请求,可能会需要做一下锁的清理工作。因为即使 Secondary row 提交失败,也可以通过 Secondary row 中的锁,找到 Primary row,根据检查 Primary row 的 meta,确定这个事务到底是被客户端回滚还是已经成功提交。
1、小事务,事务不跨region,如何保证事务强一致性。
该情况较为简单,和单实例一样,事务保证性也好,无需复杂的确认过程。多个副本之间的数据一致性,通过raft协议和事务日志应用保证leader和follower region之间的数据一致性。 一旦leader 和 follower 之间事务日志传输量较大,传输到副本超时(180ms),或者多个副本超时,会引发节点踢出,虽然可以通过大多数确认提交,就认为成功,但是还是存在一定问题。
2、小事务,事务跨region,涉及分布式事务,如何保证数据强一致性。
通过二阶段提交实现,primary row 和seconary row 异步提交,只要primary row 提交,全事务提交成功。 如果异步提交的seconary row失败,会重新尝试,笔者个人认为这点是primary row成功之后,会记录seconary row的信息,但是官方解释和一些分享比没有说明primary row提交记录的版本信息具体会记录seconary row的哪些信息,数据强一致性有待确认。
3、小事务,多会话同时更新同一条记录,如何实现数据强一致性。
该情况涉及分布式事务,不同rockdb实例之间分布式事务实现,由于通过乐观锁解决锁冲突,如果失败会话会重试该事务。如果业务层逻辑没有控制好,多个会话更新相同记录,造成锁冲突,引起多会话重试事务,严重影响性能。
4、大事务执行时间长,提交前,其他会话已经更新大事务操作的数据,如何实现数据强一致性。
由于乐观锁定,后提交的事务会回滚,大事务回滚后然后重新尝试,中间在有小事务更新其中的数据且优先提交,大事务还是会失败。
本文模拟的灾难环境
环境:MySQL 5.7.21 模拟故障:实例无法启动,数据文件还在。 恢复前的表结构和数据
mysql> show create table t1 \G;
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
ERROR:
No query specified
mysql> select * from t1;
+----+------+
| id | name |
+----+------+
| 1 | a |
| 4 | c |
| 7 | b |
| 11 | e |
| 20 | d |
| 30 | b |
+----+------+
6 rows in set (0.00 sec)
开启innodb_file_per_table=on。
该参数表示表有独立的表空间,5.6.6默认开启
1.2恢复*.frm文件
- 1、新的实例
- 2、创建新的数据库
mysql> create database db1; mysql> use db1;
- 3、创建同名表
mysql> create table t1(col1 int);
- 4、复制*.frm 覆盖当前实例的。t1.frm文件
- 5、添加参数innodb_force_recovery=6到my.cnf文件 innodb_force_recovery默认值是0,6代表实例启动不校验idb文件
重复1.2的操作 注意:需要注释掉#innodb_force_recovery=6 第二次建表语句 create table t1(col1 int,col2 int); 获得建表语句
Create Table: CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB row_format=compact
该情况下mysql被删除的数据页会很快被覆盖,所以先要把表的数据文件拷贝出来,保护现场,此种情况无法保证完全恢复,如果删除的数据被覆盖,只能恢复部分数据。
2.1安装恢复工具
tar -zxf percona-data-recovery-tool-for-innodb-0.5.tar.gz
cd percona-data-recovery-tool-for-innodb-0.5/mysql-source/
./configure
cd ..
make
# cd /data/
# mkdir -p /data/mysql/recover
# cd /data/mysql/recover
# cd db3/
# cp t.* /data/mysql/recover
cd /usr/local/
cd percona-data-recovery-tool/
# ./page_parser -5 -f /data/db3/t.ibd
# cd pages-1517613235/
# ./create_defs.pl --user root --password mysql --db db3 --table t1 > include/table_defs.h
# cat include/table_defs.h
[root@node1 percona-data-recovery-tool]# make
[root@node1 percona-data-recovery-tool]# make
# ./constraints_parser -5 -f pages-1517613235/FIL_PAGE_INDEX/0-42/ > /data/mysql/recover/t1.sql
# cat /data/mysql/recover/t.sql | wc -l
# head -10 /data/mysql/recover/t1.sql