关于数据库设计:聊聊存储引擎的实现要素

家喻户晓,MySQL 的 InnoDB 存储引擎应用了 B+ 树作为索引实现,那么为什么不应用其余的数据结构呢?数组、链表或者哈希表。实现存储引擎到底须要什么条件呢? 咱们当初先以存储最简略的数据为例,这里的数据相似于 json 对象。有 key 和 value。 { "0": "value1", "1": "value2" }最简略的存储引擎必须实现以下三个办法: read: (key: number) => value 查找 key 并返回 valuewrite: (key: number, value) => void 查找并插入 key 以及 valuescan: (begin: number, end: number) => value[] 查找返回 key 范畴内数据简略数据结构对于开发我的项目来说,能应用最简略的数据结构实现我的项目是十分棒的,这意味着更少的 bug 和更少的工夫。 有序数组如果以后有序数组的地位和存储的 key 能够一一对应的话,也就是数组 index 对应 key(没有对应也就是稠密数组),咱们的 read 和 write 办法的工夫复杂度会是 O(1),scan 办法也是 O(1)。但数据量稍大就扛不住了。 退而求其次,不存在地位对应主键的状况下,有序数组严密存储,这样能够通过二分查找,read 和 scan 办法的工夫复杂度为 O(log2n)。但 write 办法老本会高到离谱。 综上所属,有序数组是在数据量少的状况下能够用来做存储引擎的。 哈希表不思考空间是不可能的,那么间接舍弃 scan 办法呢?在某些业务场景下是能够不应用 scan 办法的。 ...

June 27, 2023 · 1 min · jiezi

关于数据库设计:求求你不要再把ER图和数据库模型图搞混了好吗

1. 简介 对于从事数据库结构设计相干人员而言,咱们通常会在设计的不同阶段用到ER图和数据库模型图,用来形容数据之间的组成构造和数据间的关系,然而很多画图人员会把它们两者给搞混了,上面就来聊聊它们之间的区别。 1、ER图全称为实体分割模型、实体关系模型或实体分割模式图 个别用在概念构造设计阶段用来形容数据需要,比方存储在数据库中的数据范畴、数据类型、数据间的关系等等提供了示意实体型、属性和分割的办法,用来形容事实世界的概念模型侧重于概念设计,用于剖析数据间的关系,满足第几范式要求2、数据库模型图个别在数据库建模时应用,也能够从数据库逆向生成数据库模型图 用在数据库建模阶段,个别用于关系型数据库建模,这个过程蕴含了概念设计阶段跟具体的数据库实现有肯定关系侧重点是生成具体的数据库构造,表、字段、索引、主键、外键等等罕用的数据库模型图/ER图绘制工具很多是商用的,价格不菲;而往往很多收费的画图工具,功能完善没有那么欠缺,而且基本上没有将ER图和数据库模型图辨别分明,对于从事数据库设计相干工作的使用者,这无疑是非常不不便的。在应用过这么多画图软件之后,和听取了不少从事数据库设计相干工作的使用者的倡议之后,PDDON收费在线画图同时提供了绘制ER图和数据库模型图的能力,不便使用者在数据库设计的不同阶段绘制指标类型绘图。本文将带大家学习如何绘制ER图和数据库模型图。 2. ER图绘制教程 2.1 ER图的三个因素 实体 实体是具备公共性质、并能够互相辨别的事实世界的对象的汇合或者是具备雷同构造对象的汇合。在ER图中用矩形示意,将实体名写在矩形内。 属性/字段 每个实体都具备肯定的特色和性质,咱们能力依据实体的特色来辨别一个个实例。属性就是形容实体或分割的性质或特色的数据项,属于一个实体的所有实例都有雷同的属性。在ER图中属性用椭圆示意,属性名写在椭圆内,并用不带箭头的连线将属性和实体连接起来。 分割 在事实世界中,事物的外部或事物之间都有着某种分割,这种分割在信息世界中反馈为实体外部的分割和实体之间的分割。在ER图中用菱形示意,菱形框内写明分割名,并用连线别离与无关实体连接起来,同时在连线上表明分割的类型,常见的分割类型有: 1:11:nm:n 2.2 两个实体之间的分割这里咱们具体解说一下实体间的分割类型,并配上图例 一对一分割(1:1) 实体A中的每个实例在实体B中至少有有一个(或没有)实例与其关联,反之亦然,则称实体A和实体B为一对一关系。 一对多分割(1:n) 实体A中的每个实例在实体B中有n个实例(n>1)与之相关联,而实体B中的每个实例在实体A中最多只有一个实例与之关联,则称实体A和实体B为一对多关系 多为多分割(m:n) 实体A中的每个实例在实体B中有n(m>1)个实例与之关联,实体b中的每个实例在实体A中有m(m>1)个实例与之关联,则成为实体A与实体B为多对多关系。 2.3 实例演示咱们以学生选课为例,一个学生能够抉择多门课程,一门课程能够被多个学生抉择,一门课程能够被多名老师授课,一名老师同样能够传授多门课程,如下所示: 3. 数据库模型图绘制教程 3.1 数据库模型图阐明PDDON 提供的数据建模工具套件能除了能够绘制简洁好看的数据库模型图,还反对实时生成和预览代码/SQL脚本,而且反对多种编程语言和SQL方言、打包下载代码/SQL等性能。数据库模型图蕴含以下因素和性能: 表构造 TableFieldKey主键外键索引 类型索引字段规定等SQL预览和下载PDDON提供了实时生成和预览SQL,也能够打包下载SQL脚本。 右键菜单预览某个类生成的SQL 主菜单能够整体预览/下载SQL 代码预览和下载 PDDON会主动将表转换为实体类构造,主动转换为代码驼峰格调的类名、字段名,主动转换字段类型。反对实时生成、预览、下载代码。 下载ER图图片 您能够应用下载性能,下载图片到本地 导出导入绘图数据 当然PDDON不仅仅保留了绘图信息,而且会保留您的所有建模相干的数据,您能够应用导出设计稿性能对设计信息进行备份,也能够联合一些代码版本工具对齐进行版本跟踪和管控。 当您须要再次应用该建模设计稿时,从新导入到PDDON工作空间即可。快捷转换 PDDON还反对UML类图和ER图之间的疾速互转,节俭设计工夫。3.2 残缺示例 创立数据库模型图 数据库模型图模板 ER图应用示例 4. PDDON与其余画图工具不同的中央 ...

May 24, 2023 · 1 min · jiezi

关于数据库设计:从-ClickHouse-到-Apache-Doris腾讯音乐内容库数据平台架构演进实践

导读:腾讯音乐内容库数据平台旨在为应用层提供库存盘点、分群画像、指标剖析、标签圈选等内容分析服务,高效为业务赋能。目前,内容库数据平台的数据架构曾经从 1.0 演进到了 4.0 ,经验了剖析引擎从 ClickHouse 到 Apache Doris 的替换、经验了数据架构语义层的初步引入到深度利用,无效进步了数据时效性、升高了运维老本、解决了数据管理割裂等问题,收益显著。本文将为大家分享腾讯音乐内容库数据平台的数据架构演进历程与实际思考,心愿所有读者从文章中有所启发。 作者:腾讯音乐内容库数据平台 张俊、代凯 腾讯音乐娱乐团体(简称“腾讯音乐娱乐”)是中国在线音乐娱乐服务开拓者,提供在线音乐和以音乐为外围的社交娱乐两大服务。腾讯音乐娱乐在中国有着宽泛的用户根底,领有目前国内市场出名的四大挪动音乐产品:QQ音乐、酷狗音乐、酷我音乐和全民K歌,总月活用户数超过8亿。 业务需要腾讯音乐娱乐领有海量的内容曲库,包含录制音乐、现场音乐、音频和视频等多种形式。通过技术和数据的赋能,腾讯音乐娱乐继续翻新产品,为用户带来更好的产品体验,进步用户参与度,也为音乐人和合作伙伴在音乐的制作、发行和销售方面提供更大的反对。 在业务经营过程中咱们须要对包含歌曲、词曲、专辑、艺人在内的内容对象进行全方位剖析,高效为业务赋能,内容库数据平台旨在集成各数据源的数据,整合造成内容数据资产(以指标和标签体系为载体),为应用层提供库存盘点、分群画像、指标剖析、标签圈选等内容分析服务。 数据架构演进TDW 是腾讯最大的离线数据处理平台,公司内大多数业务的产品报表、经营剖析、数据挖掘等的存储和计算都是在TDW中进行,内容库数据平台的数据加工链路同样是在腾讯数据仓库 TDW 上构建的。截止目前,内容库数据平台的数据架构曾经从 1.0 演进到了 4.0 ,经验了剖析引擎从 ClickHouse 到 Apache Doris 的替换、经验了数据架构语义层的初步引入到深度利用,无效进步了数据时效性、升高了运维老本、解决了数据管理割裂等问题,收益显著。接下来将为大家分享腾讯音乐内容库数据平台的数据架构演进历程与实际思考。 数据架构 1.0 如图所示为数据架构 1.0 架构图,分为数仓层、减速层、应用层三局部,数据架构 1.0 是一个绝对支流的架构,简略介绍一下各层的作用及工作原理: 数仓层:通过 ODS-DWD-DWS 三层将数据整合为不同主题的标签和指标体系, DWM 集市层围绕内容对象构建大宽表,从不同主题域 DWS 表中抽取字段。减速层:在数仓中构建的大宽表导入到减速层中,Clickhouse 作为剖析引擎,Elasticsearch 作为搜寻/圈选引擎。应用层:依据场景创立 DataSet,作为逻辑视图从大宽表选取所需的标签与指标,同时能够二次定义衍生的标签与指标。存在的问题: 数仓层:不反对局部列更新,当上游任一起源表产生提早,均会造成大宽表提早,进而导致数据时效性降落。减速层:不同的标签跟指标个性不同、更新频率也各不相同。因为 ClickHouse 目前更善于解决宽表场景,无区别将所有数据导入大宽表生成天的分区将造成存储资源的节约,保护老本也将随之升高。应用层:ClickHouse 采纳的是计算和存储节点强耦合的架构,架构简单,组件依赖重大,牵一发而动全身,容易呈现集群稳定性问题,对于咱们来说,同时保护 ClickHouse 和 Elasticsearch 两套引擎的连贯与查问,老本和难度都比拟高。除此之外,ClickHouse 由国外开源,交换具备肯定的语言学习老本,遇到问题无奈精确反馈、无奈疾速取得解决,与社区沟通上的阻塞也是促成咱们进行架构降级的因素之一。 数据架构 2.0基于架构 1.0 存在的问题和 ClickHouse 的局限性,咱们尝试对架构进行优化降级,将剖析引擎 ClickHouse 切换为 Doris,Doris 具备以下的劣势: Apache Doris 的劣势: Doris 架构极繁难用,部署只需两个过程,不依赖其余零碎,运维简略;兼容 MySQL 协定,并且应用规范 SQL。反对丰盛的数据模型,可满足多种数据更新形式,反对局部列更新。反对对 Hive、Iceberg、Hudi 等数据湖和 MySQL、Elasticsearch 等数据库的联邦查问剖析。导入形式多样,反对从 HDFS/S3 等远端存储批量导入,也反对读取 MySQL Binlog 以及订阅音讯队列 Kafka 中的数据,还能够通过 Flink Connector 实时/批次同步数据源(MySQL,Oracle,PostgreSQL 等)到 Doris。**社区目前 Apache Doris 社区沉闷、技术交换更多,SelectDB 针对社区有专职的技术支持团队,在应用过程中遇到问题均能疾速失去响应解决。同时咱们也利用 Doris 的个性,解决了架构 1.0 中较为突出的问题。 ...

February 20, 2023 · 3 min · jiezi

关于数据库设计:万亿数据秒级响应Apache-Doris-在360-数科实时数仓中的应用

作者: 360数科中间件团队 编辑整理: SelectDB 作为以人工智能驱动的金融科技平台,360数科携手金融合作伙伴,为尚未享受到普惠金融服务的优质用户提供个性化的互联网生产金融产品,致力于成为连贯用户与金融合作伙伴的科技平台。360数科旗下产品次要有 360借条、360小微贷、360分期等,截止目前,已累计帮忙 141 家金融机构为 4300 万用户提供授信服务、为2630万用户提供借款服务、单季促成交易金额1106.75亿元。同时作为国内当先的信贷科技服务品牌,360数科在三季度累计注册用户数首次冲破 2 亿。 业务需要随着金融科技业务的一直倒退,对数据的安全性、准确性、实时性提出了更严格的要求,晚期 Clickhouse 集群用于剖析、标签业务场景,然而存在稳定性较低、运维简单和表关联查问较慢等问题,除此之外,咱们业务中有局部报表数据扩散存储在各类 DB 中,这也导致保护治理复杂度较高,亟需做出优化和重构。 零碎选型及比照基于以上需要及痛点,咱们对实时数仓的选型指标提出了明确的需要,咱们心愿新的 MPP 数据库具备以下几个特点: 数据写入性能高,查问秒级兼容规范的 SQL 协定表关联查问性能优良丰盛的数据模型运维复杂度低社区沉闷对商业敌对,无法律危险2022年3月开始,咱们对合乎以上特点的数据库 Apache Doris 开展了为期两个月的调研测试。以下是 Apache Doris 1.1.2 在各个方面的满足状况。 基于上述情况,咱们决定采纳 Apache Doris,除了能够满足上文提到的几个特点,咱们还思考以下几个方面: Clickhouse 因为 Join 查问限度、函数局限性、数据模型局限性(只插入,不更新)、以及可维护性较差等起因,更适宜日志存储以及保留以后存量业务,不满足咱们以后的业务需要。目前Apache Doris 社区沉闷、技术交换更多,SelectDB 针对社区有专职的技术支持团队,在应用过程中遇到问题均能疾速失去响应解决。Apache Doris 危险更小,对商业敌对,无法律危险。大数据畛域 Apache 基金会我的项目形成了事实标准,在 360数科外部已有广泛应用,且Apache 开源协定对商业敌对、无法律危险,不会有协定上的顾虑。平台架构360数科大数据平台(毓数)提供一站式大数据管理、开发、剖析服务,笼罩大数据资产治理、数据开发及任务调度、自助剖析及可视化、对立指标治理等多个数据生命周期流程。在整个 OLAP 中,目前 Apache Doris 次要使用离线数仓剖析减速、自助 BI 报表等业务场景。 在引入 Doris 后,思考已有数据分析业务以及数据规模,Doris 集群将先同步局部业务上优先级更高的数据。通过上述架构图能够看到,依靠 Doris 弱小的查问性能,咱们将把 Doris 架设在 Hive 数仓的下层,为特定场景进行查问减速,这样的架构建设起来老本很低,只须要实现数据从 Hive 数仓到 Doris 集群的导入适配,因为 Doris 集群并没有产生任何新表,能够间接复用曾经建设好的数据血缘关系。 ...

November 22, 2022 · 4 min · jiezi

关于数据库设计:表结构设计高并发场景微服务实战五

你好,我是程序员Alan。 这篇文章我会具体讲一下设计表构造时我会重点关注的中央,助你少走弯路。 数字类型 这里须要重点关注一下范畴,不须要记得十分分明,然而要有一个大略的印象,对边界问题要敏感。 另外不举荐应用数据库的浮点类型,否则在计算时,因为精度类型问题,会导致最终的计算结果出错,这是因为MySQL 之前的版本中的浮点类型 Float 和 Double,不是高精度。 更重要的是,从 MySQL 8.0.17 版本开始,当创立表用到类型 Float 或 Double 时,会抛出上面的正告:MySQL 揭示用户不该用上述浮点类型,甚至揭示将在之后版本中废除浮点类型。 在理论的开发中,咱们经常会应用整型类型来作为表的主键,即用来惟一标识一行数据。整型联合属性 auto_increment,能够实现自增性能,但在表结构设计时用自增做主键,心愿你特地要留神以下两点,若不留神,可能会对业务造成灾难性的打击: 用 BIGINT 做主键,而不是 INT;自增值并不长久化,可能会有回溯景象(MySQL 8.0 版本前)。INT 的范畴最大在 42 亿的级别,在实在的互联网业务场景的利用中,一些流水表、日志表,很容易达到最大值。 因而,用自增整型做主键,一律应用 BIGINT,而不是 INT。不要为了节俭 4 个字节应用 INT,当达到下限时,再进行表构造的变更,将是微小的累赘与苦楚。 我所在的公司就遇到过线上环境呈现INT类型达到上线导致服务异样的问题,过后运维同学的做法是批改数据库表构造(字段类型 INT -> BIGINT),你猜这个简略的操作会造成什么问题?表锁死!表锁死又导致了一系列相干申请全副卡死!举个和我的项目无关的例子,助你感受一下问题的严重性,设想一下当初是春运期间,检票进站的时候,检票服务出现异常,大量旅客滞留在候车室的场景 字符串类型MySQL 数据库的字符串类型我最常应用的是 CHAR、VARCHAR。 CHAR 和 VARCHAR 的定义 CHAR(N) 用来保留固定长度的字符,N 的范畴是 0 ~ 255,请牢记,N 示意的是字符,而不是字节。VARCHAR(N) 用来保留变长字符,N 的范畴为 0 ~ 65536, N 示意字符。 在超出 65536 个字符的状况下,能够思考应用更大的字符类型 TEXT 或 BLOB,两者最大存储长度为 4G,其区别是 BLOB 没有字符集属性,纯属二进制存储。 ...

November 1, 2022 · 2 min · jiezi

关于数据库设计:Database-Physical-Storage-Media-and-File-Organisation

Physical Storage MediaAccess time$$Access\ time = Seek\ time + Rotational\ latency + (Transfer\ time)$$ Disk BlockFeaturesA contiguous sequence of sectors from a single trackData is transferred between disk and main memory in blocksThe operating system stipulates that a disk block can only contain one file, so the space occupied by the file can only be an integer multiple of the disk blockBlock SizeCompareSmaller blocks: more transfers from diskLarger blocks: more space wasted due to partially filled blocksTherefore, Lareger block sizes may mean space wasted, smaller may lead to more IO times. ...

September 15, 2021 · 2 min · jiezi

关于数据库设计:数据库主键适合用UUID吗

动机为什么想到用UUID做数据库主键呢?思考如此场景,有多个生产环境各自运行,有时要从一个环境导数据到另一个环境,因而要求主键不抵触,自增整数不能满足要求。搞个地方发号器服务如何?很多互联网公司都这么做了。但对于以上场景,那就须要各个生产环境依赖同一个发号器服务了,难以跨数据中心乃至跨地区。其实这须要一种去中心化的发号策略,无论哪个服务器都能够发号,而且这些号互不抵触。 有一种现成的去中心化发号的实现,这就是UUID!UUID叫做通用惟一标识符(Universally Unique Identifier),是128bit的整数,用算法计算失去。在分布式系统中的任一服务器只有依规范创立UUID值,就能保障在全局范畴不反复。 例如Java的UUID.randomUUID()是用SecureRandom实现的齐全随机数,这是一种在密码学意义上平安的随机数,随机位越多越平安(猜不到法则,而且不容易反复)。因为128bit全是随机位,实践上认为在地球上是不会反复的。 考查就不多介绍了,来讲讲它能不能作为数据库主键吧,看看优缺点: 长处 去核心化生成->无单点危险去核心化生成->无需特意设计就能并行生成(自增主键是串行生成的,比较慢)无状态生成->数据记录在存入数据库之前就能领有主键(自增主键要在数据记录存入数据库后能力获取),编程更容易毛病 无序->不能用主键排序代替工夫排序,以防止加载数据记录(个别实现能有序,但Java内置实现齐全无序)无序->升高数据库写入性能无序->升高数据库读取性能长处不必多说了,来探讨这些毛病吧。 首先弄清楚关系型数据库系统是怎么工作的。相似于文件系统,关系型数据库系统以页为单位来存放数据,一页(个别为4KB、8KB或16KB大小)能寄存一批数据记录。读写时也是整页地读取或写入。对于有主键列的表,默认提供主键索引,索引项蕴含了主键值并且以主键排序。MySQL的主键索引采纳聚簇索引,索引与数据融为一体,数据记录依照主键程序来存储。PostgreSQL的主键索引采纳非聚簇索引,索引与数据各存一份(索引相当于"主键值->数据寄存地址"的映射表),有专门的索引页和数据页。 (1) 不能用主键排序代替工夫排序,以防止加载数据记录。当一个查问只须要返回主键但须要按工夫排序时,如果主键是工夫有序的,就能够对主键排序。这时只需拜访索引而无需拜访数据记录就能实现查问,因为索引就蕴含了主键值。 (2) 升高数据库写入性能用INSERT语句增加数据记录时,要更新主键索引。UUID主键的随机性使得MySQL的数据页、PostgreSQL的索引页被随机写入,很可能一页只增加一行,因而须要读写很多页(从磁盘读入内存,批改再写回,命中缓存时不必读磁盘但仍要写回)。有序的主键更有可能把多行增加到同一页,因而能够写更少的页(数据库系统不会每写一行就刷盘,而会略微缓冲一小会,让一页有可能多收到几行)。简而言之,随机主键不能利用数据的空间局部性。PostgreSQL只是索引页受影响,比MySQL数据页受影响要好些。但PostgreSQL WAL的full_page_writes个性引起的写入放大使它也受不小的影响。 (3) 升高数据库读取性能相邻工夫增加的数据记录会随机散布在MySQL的很多个数据页。新建的数据记录可能比拟热门,然而它们随机放在很多个数据页,没有哪一页是热门的。而且一些范畴查问,例如按创立工夫查问,须要读取很多个数据页能力拿到所有匹配的数据记录。同样,PostgreSQL只是索引页受影响,比MySQL数据页受影响要好些。 还有一个问题是UUID(128bit)比BIGINT(64bit)大,若用char(32)来保留UUID则须要256bit,存储和计算的开销都更大。 这篇是Percona首席架构师的文章,探讨MySQL应用UUID主键的性能。https://www.percona.com/blog/...这两篇是一位PostgreSQL专家的文章,探讨PostgreSQL应用UUID主键的性能,以及full_page_writes遇到的写入放大问题。https://www.2ndquadrant.com/e...https://www.2ndquadrant.com/e... URL编码再思考URL编码的问题。主键须要在REST格调URL中用来标识资源,例如/users/{id},id能够是任一主键值。此时主键要被编码为字符串,UUID的字符串模式至多也要32字节(若保留'-'分隔符,如Java UUID.toString(),则为36字节)。这个字符串模式理论是UUID的16进制写法。 如果对UUID做Base64编码,能够压缩到22字节。请留神要应用URL Safe Base64编码(Java提供这一编码模式),因为规范的Base64含有URL保留字符,使得id字符串须要被本义。 有一些版本的UUID是有序的,其字符串模式满足ASCII程序,Base64编码使其不恪守ASCII程序。可应用Firebase-style Base64编码,参见 https://firebase.googleblog.c... 。 有序ID能够同时取得工夫有序性和平安随机性的益处吗?能够。 一种工夫有序的去中心化惟一ID实现: 生成一个齐全随机的UUID,长度为128bit以以后unix time作前缀,这样总长度为192bitFirebase-style Base64编码,这样总长度为32字节,相当于一般UUID string一种更玲珑的实现是unix time配上64bit随机值,和UUID一样有32字节,Firebase-style Base64编码后长度为22字节。然而可能对碰撞率有影响,须要实践证实。还有一种实现是48bit unix time配上80bit随机数,编码前char(32),编码后char(22),这一类算法有Firebase在生产环境用了,举荐。Cassandra应用UUID v1,依赖微秒工夫和MAC地址。 有序ID的编码Firebase-style Base64有一个问题:以后的ID总是以'-'开始。用户体验不好,因为用户很容易疏忽这个字符,认为它不是ID的成分。 因而我从新设计了编码,还赠送一份Java实现代码给读者(48bit unix time配上80bit随机数,满足ASCII程序的URL Safe Base64编码)。 /** * (22 chars) 48bit milliseconds + 80bit random value (ASCII-ordered URL-safe Base64-encoded) */public final class UniqueIdUtil { private static final SecureRandom secureRandom = new SecureRandom(); private static final byte[] remapper = new byte[128]; static { byte[] oldCodes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".getBytes(StandardCharsets.US_ASCII); byte[] newCodes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~".getBytes(StandardCharsets.US_ASCII); for (int i = 0; i < oldCodes.length; i++) { remapper[oldCodes[i]] = newCodes[i]; } } private UniqueIdUtil() {} public static String newId() { ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[18]); byteBuffer.putLong(System.currentTimeMillis()); byte[] randomBytes = new byte[10]; secureRandom.nextBytes(randomBytes); byteBuffer.put(randomBytes); return newId(byteBuffer); } static String newId(ByteBuffer byteBuffer) { byte[] original = Arrays.copyOfRange(byteBuffer.array(), 2, 18); byte[] encoded = Base64.getUrlEncoder().withoutPadding().encode(original); for (int i = 0; i < encoded.length; i++) { encoded[i] = remapper[encoded[i]]; } return new String(encoded, StandardCharsets.US_ASCII); }}来比拟两种ID编码:Firebase-style Base64编码:-MPZFw-83QdUZ_vQ6UAMdF自制Base64编码:0NQ_LnK8m~Cv5uYuAOTzUG ...

July 12, 2021 · 1 min · jiezi

关于数据库设计:大佬都在用的数据库设计规范你不点进来看看嘛

建表规约表白是与否概念的字段,必须应用is_xxx命名,数据类型是unsigned tinyint(1-是,0-否) 任何字段如果是非正数,必须是unsignedPOJO类中的任何布尔型变量,都不要加is前缀须要在< resultMap >设置从is_xxx到Xxx的映射关系数据库示意是与否的值,应用tinyint类型保持is_ xxx的命名形式是为了明确取值含意和取值范畴表名,字段名必须应用小写字母(或数字),禁止呈现数字结尾,禁止两个下划线两头只呈现数字.数据库字段名的批改代价很大,因为无奈进行预公布,所以字段名称须要慎重考虑 MySQL在windows下不辨别大小写,但在Linux下默认是辨别大小写的因而,数据库名,表名,字段名,都不容许呈现任何大写字母表名不应用复数名词 表名应该仅仅示意表外面的实体内容,不应该示意实体数量对于DAO类名也是复数模式,合乎表白习惯禁止应用MySQL的官网保留字命名: descrangematchdelayed索引命名: pk_字段名: 主键primary key索引uk_字段名: 惟一unique key索引名idx_字段名: 一般index索引名小数类型为decimal, 禁止应用float,double float和double在存储的时候,存在精度损失的问题,很可能在值比拟时,失去不正确的后果如果存储的数据范畴超过decimal的范畴,倡议将数据拆分成整数和小数离开存储如果存储的字符串长度简直相等,应用char定长字符串类型varchar是可变长字符串,不事后调配存储空间,长度不要超过5000 如果长度大于此值,定义字符串类型为text, 独立进去一张表,用主键来对应,防止影响其它字段索引效率表必备的三个字段: id: 主键,类型为bigint,unsigned,单表时自增,步长为1gmt_create: 类型为datetime,当初时示意被动创立gmt_modified 类型为datetime,过去分词示意被动更新表的命名最好加上[业务名称_表的作用]库名与利用名称尽量统一如果批改字段含意或者对字段的示意状态追加时,须要及时更新字段正文字段容许适当冗余以进步查问性能,但必须思考数据统一.冗余的字段应遵循: 不是频繁批改的字段不是varchar超长字段,更不能是text字段 商品类目名称应用频率高,字段长度短,名称根本变化无穷,可在相关联的表中冗余存储类目名称,防止关联查问单表行数超过500万行或者单表容量超过2GB, 才举荐进行分库分表 如果预计三年后的数据量基本达不到这个级别,不要在创立表时就分库分表适合的字符存储长度,岂但节约数据库表空间,节约索引存储,更重要的是晋升检索速度 索引规约业务上具备惟一个性的字段,即便是多个字段的组合,也必须建成惟一索引 索引不会影响insert的速度,这个速度能够疏忽,但进步查找速度是显著的即便在应用层做了十分欠缺的校验管制,只有没有惟一索引,必然有脏数据产生超过三个表禁止join, 须要join的字段 ,数据类型必须相对统一. 多表关联查问时,保障被关联的字段须要有索引在varchar字段上建设索引时,必须指定索引长度,没必要对全字段建设索引,依据理论文本区分度决定索引长度即可 索引长度与区分度是一对矛盾体 个别对字符串类型数据,长度为20的索引,区分度会高达90%以上能够应用count(distinct left(列名, 索引长度)) / count(*) 的区分度来确定页面搜寻严禁左含糊或者全含糊,如果须要要应用搜索引擎来解决 索引文件具备B-Tree的最左前缀匹配个性,如果右边的值未确定,无奈应用此索引如果有order by的场景,要留神利用索引的有序性 .order by最初的字段是组合索引的一部分,并且放在索引组合程序的最初,避免出现file_sort的状况,影响查问性能 where a=? and b=? order by c;索引: a_b_c要是在索引中有范畴查找,那么索引有序性就无奈利用(WHERE a>10 ORDER BY b; 索引:a_b无奈排序) 利用笼罩索引来进行查问操作,防止回表 比方一本书须要晓得第11章是什么题目,只须要目录浏览一下就更好,这个目录就起到笼罩索引的作用可能建设索引的品种分为主键索引,惟一索引,一般索引三种,而笼罩索引只是一种查问的成果用explain的后果,extra列会呈现: using index利用提早关联或者子查问优化超多分页场景: MySQL不是跳过offset行,而是取offset+N行,而后返回放弃前offset行,返回N行当offset特地大的时候,效率就十分低下,要么管制返回的总页数,要么对超过特定阈值的页数进行SQL改写 先疾速定位须要获取的id字段,而后再关联:SELECT a.* FROM table1 a,(select id from table1 where condition LIMIT 100000,20) b where a.id=b.idSQL性能优化的指标: 至多要达到range级别,要求是ref级别,最好是consts级别 ...

June 30, 2021 · 1 min · jiezi

关于数据库设计:ER模型的逻辑和物理视图

概述实现了数据库的物理视图和逻辑视图的显示和切换,具体参考如下: 关上: https://www.freedgo.com/erd-i...制做E-R模型图减少表的逻辑名减少列的逻辑名进行逻辑与物理视图的切换在线ER模型生成数据库设计文件点击生成数据库文档

April 29, 2021 · 1 min · jiezi

关于数据:美团酒旅数据治理实践

数据已成为很多公司的外围资产,而在数据开发的过程中会引入各种品质、效率、平安等方面的问题,而数据治理就是要一直打消引入的这些问题,保障数据精确、全面和残缺,为业务发明价值,同时严格管理数据的权限,防止数据泄露带来的业务危险。数据治理是数字时代很多公司一项十分重要的外围能力,本文介绍了美团酒旅平台在数据治理方面的实际。一、背景1. 为什么要做数据治理随着挪动互联网的衰亡,线下商业活动逐步开始向线上化发展,数据的产生速度有了极大的晋升。越来越多的公司开始意识到数据的重要性,并将其打造成为公司的外围资产,从而驱动业务的倒退。在数据相干的畛域中,“数据治理”这个话题近两年尤为炽热,很多公司特地是大型互联网公司都在做一些数据治理的布局和动作。 为什么要做数据治理?因为在数据产生、采集、加工、存储、利用到销毁的全过程中,每个环节都可能会引入各种品质、效率或平安相干的问题。在公司晚期的倒退阶段,这些数据问题对公司倒退的影响并不是很大,公司对问题的容忍度绝对也比拟高。然而,随着业务的倒退,公司在利用数据资产发明价值的同时,对数据品质和稳定性要求也有所晋升。此外,当数据积攒得越来越多,公司对数据精细化经营水平的要求也随之进步,会逐步发现有很多问题须要治理。 同时,在数据开发的过程中也会一直引入一些问题,而数据治理就是要一直打消引入的这些问题,保障数据精确、全面和残缺,为业务发明价值,同时严格管理数据的权限,防止数据泄露带来的业务危险。因而,数据治理是数字时代很多公司一项十分重要的外围能力。 2. 须要治理哪些问题数据治理是一项须要长期被关注的简单工程,这项工程通过建设一个满足企业需要的数据决策体系,在数据资产治理过程中行使权力、管控和决策等流动,并波及到组织、流程、管理制度和技术体系等多个方面。一般而言,数据治理的治理内容次要包含上面几个局部: 品质问题:这是最重要的问题,很多公司的数据部门启动数据治理的大背景就是数据品质存在问题,比方数仓的及时性、准确性、规范性,以及数据利用指标的逻辑一致性问题等。老本问题:互联网行业数据收缩速度十分快,大型互联网公司在大数据基础设施上的老本投入占比十分高,而且随着数据量的减少,老本也将持续攀升。效率问题:在数据开发和数据管理过程中都会遇到一些影响效率的问题,很多时候是靠“自觉”地堆人力在做。平安问题:业务部门特地关注用户数据,一旦泄露,对业务的影响十分之大,甚至能左右整个业务的生死。规范问题:当公司业务部门比拟多的时候,各业务部门、开发团队的数据规范不统一,数据买通和整合过程中都会呈现很多问题。 3. 美团酒旅数据现状2014年,美团酒旅业务成为独立的业务部门,到2018年,酒旅平台曾经成为国内酒旅业务重要的在线预订平台之一。业务倒退速度较快,数据增长速度也很快。在2017到2018两年里,生产工作数以每年超过一倍的速度在增长,数据量以每年两倍多的速度在增长。如果不做治理的话,依据这种靠近指数级的数据增长趋势来预测,将来数据生产工作的复杂性及老本累赘都会变得十分之高。在2019年初,咱们面临着上面五种问题: 数据品质问题重大:一是数据冗余重大,从数据工作增长的速度来看,新上线工作多,下线工作少,对数据表生命周期的管制较少;二是在数据建设过程中,很多应用层数据都属于“烟囱式”建设,很多指标口径没有对立的治理标准,数据一致性无奈进行保障,同名不同义、同义不同名的景象频发。数据老本增长过快:某些业务线大数据存储和计算资源的机器费用占比曾经超过了35%,如果不加以控制,大数据成本费用只会变得越来越高。数据经营效率低下:数据应用和征询多,数据开发工程师须要破费大量工夫一对一解答业务用户的各种问题。然而这种形式对于用户来说,并没有晋升数据的易用性,无奈无效地积攒和积淀数据常识,还升高了研发人员的工作效率。数据安全不足管制:各业务线之间能够共用的数据比拟多,而且每个业务线没有对立的数据权限管控规范。开发标准规范缺失:晚期为疾速响应业务需要,研发人员通常采纳“烟囱式”的开发模式,因为不足相应的开发标准束缚,且数据工程师的工作思路和形式差异性都十分大,导致数据仓库内的反复数据多,规范性较差。当产生数据问题时,问题的排查难度也十分大,且耗时较长。4. 治理指标2019年,美团酒旅数据团队开始被动启动数据治理工作,对数据生命周期全链路进行体系化数据治理,冀望保障数据的长期向好,解决数据各个链路的问题,并保持数据体系的长期稳固。具体的指标蕴含以下几个方面: 建设数据开发全链路的标准规范,进步数据品质,通过系统化伎俩治理指标口径,保障数据一致性。管制大数据老本,防止大数据机器老本收缩对业务营收带来的影响,正当控制数据的生命周期,防止数据反复建设,缩小数据冗余,及时归档和清理冷数据。治理数据的应用平安,建设欠缺的数据安全审批流程和应用标准,确保数据被正当地应用,防止因用户数据泄露带来的平安危险和商业损失。进步数据工程师的开发和运维效率,缩小他们数据经营工夫的投入,进步数据经营的自动化和系统化水平。二、数据治理实际其实早在2018年以前,酒旅数据组就做过数据治理,过后只是从数仓建模、指标治理和利用上单点做了优化和流程标准。之后,基于下面提到的五个问题,咱们又做了一个体系化的数据治理工作。上面将介绍一下美团酒旅数据团队在数据治理各个方向上的具体实际。 1. 数据治理策略数据治理计划须要笼罩数据生命周期的全链路,咱们把数据治理的内容划分为几大部分:组织、标准规范、技术、掂量指标。整体数据治理的实现门路是以标准化的标准和组织保障为前提,通过做技术体系整体保证数据治理策略的实现。同时,搭建数据治理的掂量体系,随时观测和监控数据治理的成果,保障数据治理长期向好的方向倒退。 2. 标准化和组织保障咱们制订了一个全链路的数据规范,从数据采集、数仓开发、指标治理到数据生命周期治理,全链路建设规范,在标准化建设过程中联结组建了业务部门的数据管理委员会。 2.1 标准化 数据标准化包含三个方面:一是规范制订;二是规范执行;三是在规范制订和执行过程中的组织保障,比方怎么让规范能在数据技术部门、业务部门和相干商业剖析部门达成对立。 从规范制订上,咱们制订了一套笼罩数据生产到应用全链路的数据规范办法,从数据采集、数仓开发、指标治理到数据生命周期治理都建设了相应环节的标准化的研发标准,数据从接入到沦亡整个生命周期全副实现了标准化。 2.2 组织保障 依据美团数据管理扩散的现状,专门建设一个职能全面的治理组织去监督执行数据治理工作的老本有点太高,在推动和执行上,阻力也会比拟大。所以,在组织保障上,咱们建设了委员会机制,通过联结业务部门和技术部门中与数据最相干的团队成立了数据管理委员会,再通过委员会去推动相干各方去协同数据治理的相干工作。 业务部门的数据接口团队是数据产品组,数据技术体系是由数据开发组负责建设,所以咱们以这两个团队作为外围建设了业务数据管理委员会,并由这两个团队负责联结业务部门和技术部门的相干团队,一起实现数据治理各个环节工作和流程的保障。组织中各个团队的职责分工如下: 数据管理委员会:负责数据治理策略、指标、流程和规范的制订,并推动所有相干团队达成认知统一。业务数据产品组:负责数据规范、需要对接流程、指标对立治理、数据安全管制以及业务方各部门的协调推动工作。技术数据开发组:负责数据仓库、数据产品、数据品质、数据安全和数据工具的技术实现,以及技术团队各个部门的协调推动工作。 3. 技术零碎数据治理波及的范畴十分广,须要合作的团队也很多,除了须要通过组织和流程来保障治理口头失常发展,咱们也思考通过技术系统化和自动化的形式进一步提效,让零碎代替人工。上面咱们将从数据品质、数据老本、数据安全和经营效率等几个方向,来逐个介绍技术实现计划。 3.1 数据品质 数据品质是影响数据价值最重要的因素,高质量的数据给带来精确的数据分析,谬误的数据会把业务疏导到谬误的方向。数据品质波及范畴较广,在数据链路的每一个环节都有可能呈现数据品质问题,酒旅业务现阶段的次要品质问题包含: 数仓规范性差,数仓架构无对立的强制标准执行束缚,数仓历史冗余数据重大。应用层数据属于“烟囱式”建设,指标在多个工作中生产,无奈保证数据的一致性。数据上游利用的数据应用无奈把控,数据精确较差,接口稳定性无奈失去保障。业务方对多个数据产品的指标逻辑无对立的定义,各个产品中数据不能间接对标。数据组的治理数据品质计划笼罩了数据生命周期的各个环节,上面将介绍一下整体的技术架构。 对立数仓标准建模(One Model):通过对立数仓标准建模系统化保障数仓标准执行,做到业务数仓标准标准化,并及时监控和删除反复和过期的数据。对立指标逻辑治理(One Logic):通过业务内对立的指标定义和应用,并系统化治理指标逻辑,数据应用层的数据指标逻辑都从指标管理系统中获取,保障所有产品中的指标逻辑统一。对立数据服务(One Service):通过建设对立的数据服务接口层,解耦数据逻辑和接口服务,当数据逻辑发生变化后不影响接口数据准确性,同时监控接口的调用,把握数据的应用状况。对立用户产品入口(One Portal):分用户整合数据产品入口,使同一场景下数据逻辑和应用形式雷同,用户没有数据不统一的困惑。 3.1.1 对立数仓标准建模(One Model) 在业务倒退初期,数据团队集中精力在疾速建设数仓来反对业务,数仓建模标准疏于治理。随着业务的倒退,数仓中的数据急剧增多,数据产品和上游利用疾速减少,数据工程师和数据应用方也变得越来越多,数仓的问题日益突显。业务数据仓库从初期倒退到当初次要裸露了3方面的问题: 数据规范性较差,不同工夫的数仓标准不同,数仓标准的执行审核须要较多的人力。数据不统一问题多,同一指标在多个ETL中生产,数据更新同步也不及时。历史数据冗余重大,数据存储形式较多,业务方查问不晓得该用哪个数据。数据团队次要通过数仓规范化制订、数仓分层架构和数仓规范化零碎来解决上述问题,上面是咱们的具体解决方案。 制订规范-数仓标准 做好数仓规范化最根本的前提是要制订一系列标准化的标准,并推动组内同学执行。标准化的适用性、全面性和可执行性间接影响到标准的执行成果。数仓标准次要从3个方面制订数据标准化: 数仓建模标准,数仓建设最根底的标准,包含分层、命名、码值、指标定义、分层依赖等维度。主数据管理标准,数仓各个主题的数据只有一份,团队共建复用,不能反复开发。数据应用标准,在查问数据时优先查问主题层,不再提供明细层和ODS层的查问拜访入口。工具保障-数仓规范化开发零碎-Dataman 在执行数据规范化的过程中,咱们发现团队中每个人对标准的了解不统一,很可能造成数据标准不对立,审核人在审核上线工作时须要思考标准的全副规定,审批须要投入的人力较多。在这样的流程下,数据规范性无奈从本源上进行管制,因而须要建设数据规范化的工具,通过零碎保障标准的一致性。数据组应用的数据层规范化工具-Dataman,次要包含3个功能模块:标准化标准、配置化开发和规则化验证。 标准化标准:制订业务数据仓库的标准规范并配置在零碎中,包含架构分层、字段治理、词根治理、公共维度和码值治理等,在ETL开发时通过对立的数仓标准开发,通过配置化实现数仓的命名、分层和码值,保障数仓长期的规范性。 配置化开发:系统化保障工程师在开发ETL过程中恪守数仓标准,Dataman能够用配置化的形式生成XT工作模板,模板中蕴含数据模型的根底信息,研发同学只须要在工作模板中开发数据生产逻辑。 规则化验证:跟进数据仓库底层元数据和标准化配置信息,定期扫描数仓的规范性状况,判断出不合乎数仓标准的工作和高类似度的数据表。3.1.2 对立指标逻辑治理(One Logic) 业务应用数据的第一步是搭建业务指标体系,业务的指标和策略的执行状况须要通过指标来剖析,指标体系的合理性和指标数据的品质间接影响到业务决策,指标的重要性显而易见。咱们通过系统化地治理数据指标,从本源上解决指标口径一致性问题,次要从以下3个方向动手: 指标定义规范化指标治理系统化数据查问智能化指标定义规范化 此处次要从指标的生成和治理上做好标准,确保业务同学和研发人员对指标体系治理的认知统一,确保指标的新建、更改和应用都依照标准执行。咱们通过上面2个方向来实现指标定义的标准对立。 业务指标体系的规范化:咱们在业务线内对立了指标体系标准,指标分为原子指标、计算指标和复合指标,通过应用这3类指标反对业务的数据分析需要,业务将来新增指标也要依照这个规范分类。指标的治理规范化:咱们与商业剖析团队一起梳理业务指标逻辑规范和录入流程,通过制订指标的新增和变更标准SOP,解决由指标治理流程引起的品质问题,使得指标定义、零碎录入、指标认证和应用各个环节都有严格的流程管控,经由业务侧数据产品经理、业务侧数据治数据管理员和数据工程师独特审批,确保标准规范的落地执行。指标治理系统化 物理数据表治理:数据表治理的信息次要包含表的根底元数据信息、表类型(维表或事实表)、表的举荐度、形容信息和样例数据等。数据表治理次要是面向数据开发同学,通过保护数据表信息,为数据模型和指标治理提供数据根底反对。 数据模型治理:是对物理数据表的模型构建,通过一个物理模型能够查问到指标和相干的维度数据。数据模型能够是星型模型或宽表,星型模型中保护多个数据表的关联形式、关联字段、维度表蕴含字段和模型的ER图等信息。 指标治理:次要包含2局部的内容,指标的业务信息和技术信息。 业务信息:为了保障业务的指标信息精确且对立,指标的业务信息须要数据产品经理与商业剖析团队探讨确定后录入,录入后须要指标所属数据主题的负责人审批后能力上线。技术信息:技术信息次要包含指标对应的物理模型以及指标的计算逻辑,技术信息的填写须要数据工程师配置。技术信息配置后会在零碎里生成技术元数据,指标管理系统通过技术元数据生成数据查问语句,提供给上游利用。 指标查问智能化 在指标管理系统中创立指标时,咱们系统化治理了指标与数仓物理模型的关联关系和取数逻辑,通过数据物理模型取得指标对应的字段和能够关联的维度,以此把指标解析为数据查问SQL语句,通过数据查问引擎执行生产的SQL,智能化取得指标数据。 在查问解析过程中,经常出现指标绑定了多个底层数据表的状况,此时须要咱们手动的选一个物理模型作为指标生产的底层数据。但问题是,如果一个指标对应的模型太多,每次解析都须要手动指定,研发人员不确定抉择哪个模型的性能最好。另外,随着物理模型的增多,大量旧的指标配置的关联模型不是最优解,就须要手动优化更改。为了解决这个问题,指标管理系统减少了智能解析模块,在抉择智能模式查问时,零碎会依据指标治理模型的数据量、存储性能和查问次数等信息主动选取最优的物理模型。 3.1.3 对立数据服务(One Service) ...

April 16, 2021 · 1 min · jiezi

关于数据库:图查询语言的历史回顾短文

本文首发于 Nebula 公众号:图查询语言的历史回顾短文 前言最近在对图查询语言 GQL 和国际标准草案做个梳理,调研过程中找到上面这篇 mark 了没细看的旧文(毕竟珍藏就是看过)。做个简略的记录。 摘要本短文会波及到的图查询语言有 Cypher、Gremlin、PGQL 和 G-CORE。 背景本文次要摘录翻译自 [Tobias2018] (见参考文献),并未波及到 SPARQL 和 RDF,只探讨了属性图。 文章撰写的工夫是 2018 年,能够看做 GQL(Graph Query Language)的一些后期筹备。GQL 有多个相干的起源,参见上面这张图。 因为 Cypher 的历史和 Neo4j 严密相干,本文会提一些 Neo4j 晚期的历史。[Angles2008](见参考文献)和 [Wood2012](见参考文献)是两个不错的对于图模型和图查询语言的总结。 年表简述2000 年,Neo4j 的创始人产生将数据建模成网络(network)的想法。2001 年,Neo4j 开发了最早的外围局部代码。2007 年,Neo4j 以一个公司的形式运作。2009 年,Neo4j 团队借鉴 XPath 作为图查询语言,Gremlin 最后也是基于这个想法。2010 年,Neo4j 和 Marko Rodriguez 采纳术语属性图(Property Graph)来形容 Neo4j 和 Tinkerpop / Gremlin 的数据模型。[PG2010](见参考文献)2011 年,第一个公开发行版本 Neo4j 1.4 公布了第一个版本的 Cypher。2012 年,Neo4j 1.8 为 Cypher 减少写入图的能力。2012 年,Neo4j 2.0 减少了标签和索引,Cypher 成为申明式的语言。2015 年,Oracle 为 PGX 创造查询语言 PGQL。2015 年,Neo4j 将 Cypher 开源为 openCypher。2015 年,LDBC 成立图查询语言工作组。2016 年,LDBC 工作组开始设计 G-CORE。2017 年,WG3 工作组开始探讨如何将属性图查问能力引入 SQL。2017 年,LDBC 工作组实现了 G-CORE 的初始设计 [GCORE2018](见参考文献)。2018 年,Cypher 形式化语义的论文发表 [Cypher2018] (见参考文献)。Gremlin、Cypher、PGQL 和 G-CORE 的演进Neo4j 的晚期历史Neo4j 和属性图这种数据模型,最早构想于 2000 年。Neo4j 的创始人们过后在开发一个媒体管理系统,所应用的数据库的 schema 常常会产生重大变动。为了反对这种灵活性,Neo4j 的联结创始人 Peter Neubauer,受 Informix Cocoon 的启发,心愿将零碎建模为一些概念相互连接的网络。印度理工学院孟买分校的一群研究生们实现了最早的原型。Neo4j 的联结创始人 Emil Eifrém 和这些学生们花了一周的工夫,将 Peter 最后的想法扩大成为这样一个模型:节点通过关系连贯,key-value 作为节点和关系的属性。这群人开发了一个 Java API 来和这种数据模型交互,并在关系型数据库之上实现了一个形象层。 ...

April 15, 2021 · 4 min · jiezi

关于数据库设计:集群通信从心跳说起

本文首发 Nebula Graph 官网:https://nebula-graph.com.cn/posts/cluster-communication-heartbeat/在用户应用 Nebula Graph 的过程中,常常会遇到各种问题,通常咱们都会倡议先通过 show hosts 查看集群状态。能够说,整个 Nebula Graph 的集群状态都是靠心跳机制来构建的。本文将从心跳说起,帮忙你理解 Nebula Graph 集群各个节点之间通信的机制。 什么是心跳?有什么作用? Nebula Graph 集群个别蕴含三种节点,graphd 作为查问节点,storaged 作为存储节点,metad 作为元信息节点。本文说的心跳,次要是指 graphd 和 storaged 定期向 metad 上报信息的这个心跳,借助心跳,整个集群实现了以下性能。(相干参数是 heartbeat_interval_secs) 在 Nebula Graph 中常常提及的 raft 心跳则是用于领有同一个 partition 的多个 storaged 之间的心跳,和本文提的心跳并不相同。1. 服务发现当咱们启动一个 Nebula Graph 集群时,须要在对应的配置文件中填写 meta_server_addrs。graphd 和 storaged 在启动之后,就会通过这个 meta_server_addrs 地址,向对应的 metad 发送心跳。通常来说,graphd 和 storaged 在连贯上 metad 前是无奈对外进行服务的。当 metad 收到心跳后,会保留相干信息(见下文第 2 点),此时就可能通过 show hosts 看到对应的 storaged 节点,在 2.x 版本中,也可能通过 show hosts graph 看到 graphd 节点。 ...

April 1, 2021 · 2 min · jiezi

关于数据库设计:超详细-PowerDesigner-入门教学项目数据库设计标准

我的项目数据库设计标准步骤一、数据需要剖析Creates a new model 建好当前是这样的 而后咱们来建设实体,抉择左边的 Entity,间接在屏幕上点就能够,$\color{red}鼠标右键勾销$ 这里,咱们建设5个实体 这里咱们轻易建几个实体,大家跟我一起建就 ok双击进行编辑 先设置 General Name 写中文Code 写英文Comment 是形容 - 而后设置属性 - 简略说一下,第三个参数就是数据类型,咱们选 Variable char 就好,就相当于 MySQL 中的 varchar 类型 >这里,如果大家对 MySQL 有啥不懂的,能够看我的 [MySQL 教程](https://blog.csdn.net/qq_29339467/category_9715943.html) - $\color{red}留神:$前面的 P 代表主键,M 代表是否能够为空,D代表是否显示(上面的D都是有勾选的),咱们将编号设为主键,且三个属性都不可为空 - 其余几个相似,这里我就不一一介绍了,我间接贴图就好了- 学校实体![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/61bfa2833d674e4480f9692c9e1f518a~tplv-k3u1fbpfcp-zoom-1.image)![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e86c336617d0408b9027084631e35255~tplv-k3u1fbpfcp-zoom-1.image)- 院系实体 - 业余实体![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/02595651bcae47f5af897229c77fae2c~tplv-k3u1fbpfcp-zoom-1.image)![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9eff5a17be1e4403947a677a5a5c7468~tplv-k3u1fbpfcp-zoom-1.image)- 实验室成员实体![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba9397f4e8a84b45ac586adc837a693d~tplv-k3u1fbpfcp-zoom-1.image) - 最初,咱们就建设了如下几个实例![在这里插入图片形容](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/db4151d5194040c7beee9b463a0cfa21~tplv-k3u1fbpfcp-zoom-1.image)二、确定实体关系 CDM (ER模型设计、逻辑模型设计)实体曾经建设好,咱们就要确定它们之间的关系咱们拿用户和学校来举例,其余相似 确定 1-1 1-N N-N 一个用户只能对应一个学校,一个学校能够有多个用户,那么他们是 many-one的关系强制关系和非强制关系 强制与非强制就是说,一个学校必须有用户,这就是强制关系;反之,为非强制关系,这里,学院和用户之间、用户和学校之间就都是强制关系了(难不成还有没学生的学校?)既然曾经确定好关系,咱们就在软件中实现 首先点击左边的这个- 而后点击用户拖到学校即可,成果如下 而后咱们双击线段,进行批改即可,Mandatory 就是示意强制关系,设置完点确定即可 其余相似,我也就不一一解说了最初后果如下 $\color{red}留神:1. 找间接关系,不能找间接关系$            $\color{red}2. 设计逻辑模型时,不思考是什么数据库$三、物理模型设计(PDM)接下来咱们开始设计物理模型物理模型其实很简略,通过 CDM 生成即可 第一个能够抉择咱们的数据库类型,下拉能够看到支流的数据库类型都是有的 而后在 Detail 中把 Check model勾销勾选,点确定就能够生成 PDM 了 ...

February 20, 2021 · 1 min · jiezi

关于数据库设计:IT168专访|DataPipeline-合伙人CPO陈雷我们致力于成为中国的世界级数据中间件厂商

IT168:很快乐有机会采访到您,请您介绍一下本人,所在公司及主打产品? 陈雷:毕业之后去了方正,而后IBM11年,守业4年,始终从事数据畛域的产品研发,零碎交付工作。业务教训次要集中在金融、通信、能源等信息化当先行业,当初所在的公司DatePipeline是一家年老的中国外乡企业,咱们致力于成为中国的世界级数据中间件厂商,产品也叫DataPipeline,是一款数据集成畛域的下一代中间件产品,性能笼罩了实时数据采集、异构数据交融、实时数据处理等数据集成畛域的次要场景。 IT168:您是何时进入这个行业的?这其中有没有特地的起因或者契机? 陈雷:中间件行业可能和互联网行业还不太一样,还是有肯定门槛的,我置信从事软件行业的人大部分都和我一样,没有什么特地偶尔的起因或者契机,就是从小喜爱计算机,依据趣味抉择了业余而后一路走过去,如果肯定要说起因的话,我感觉可能是咱们国家近几十年信息技术的高速倒退为咱们提供了一展拳脚的空间,没有让咱们放弃本人的趣味,这也是一个很幸福的事。 IT168:国内的市场格局是怎么的?都有哪些玩家?DataPipeline处于怎么的地位? 陈雷:次要分为三大类。 第一类是传统的外企,比方IBM、Oracle、Informatica等,有很成熟的产品和服务体系,但面对中国市场的新技术要求的应答稍显迟缓,比方Informatica往年发表遣散了中国公司,IBM和Oracle对国内正在逐渐衰亡的数据库都无奈提供反对。 第二类是云厂商,特地是私有云厂商,在大规模数据管理和利用上有十分深刻的摸索和实际,比方OceanBase,也代表了将来的倒退方向,但在数据集成这个畛域还没有特地无力的产品,而且在面向重点行业企业信息化建设服务这一块还是有很多的工作要做。 第三类是一些有技术实力的行业集成商也在做相干畛域的工作,但大部分都是在我的项目施行过程中基于开源我的项目缓缓积攒,从商业产品角度来说适应性还有待验证。 DataPipeline从成立之初就保持专业化、产品化倒退的路线,保持技术驱动,深耕企业服务,精确地讲在产品的适应性上曾经超过了传统外企,但在产品成熟度上还有很多工作要做,咱们当初也宽泛的和云厂商与行业集成商单干,独特为企业客户提供更好的服务。 IT168:据您所知,数据交融市场的规模大略是多少? 陈雷:数据中间件的上下游市场正在快速增长,倒逼数据交融需要一直增长,能够说中间件和数据库及数据利用市场在同一量级,2018年寰球市场320亿美元,预计到2022年,数据交融市场大略在120亿美元以上,合乎增长率14%,数据交融是中间件增长最快的细分市场。 IT168:对于企业来讲,在搭建数据管理平台过程中都会面临哪些挑战和问题? 陈雷:这个内容就比拟多了,讲最重要的三个挑战吧。 第一,各类数据管理技术差别越来越大,全面、精确的实时数据获取艰难。随着数据技术的一直倒退,针对某些具体场景的个性在一直被加强,使得各类数据技术的差异性进一步扩充,但被纳入其中的数据自身不应该因技术栈不同而妨碍其价值开释。 1、交易系统、账务零碎、管理系统、剖析零碎、主数据、数据仓库与大数据平台采纳的数据库治理技术都不尽相同,数据交换困难重重; 2、数据价值一直凸显,业务翻新须要数据撑持,但大量数据没有纳入主数据管理系统,数据仓库与大数据平台又无奈满足时效性要求; 3、数据时效性要求越来越高,批量数据交换无奈满足需要,但针对不同数据库的增量数据实时采集须要大量的技术储备与研发老本; 4、增量识别字段等形式无奈获取精确残缺的增量数据,常常为实时数据利用造成阻碍,也晋升了实时数据的应用老本; 5、不同数据库治理技术在实例、库、模式、表等数据对象上,字段类型、精度、标度等语义模式上都有区别; 6、对上游的构造变动感知与应答都须要针对不同数据库技术区别对待; 7、传输过程中的一致性、抵触、特定类型的数据处理也须要区别对待。 第二,如何疾速响应实时数据需要,把握机会疾速建设竞争劣势。业务须要更高的敏捷性来应答外部环境的变动,这须要整个数字化组织能够体系化的进行多速、麻利的业务场景撑持,以及对突发业务流动有更多的可见性,以确保能够利用新呈现的机会并疾速建设竞争劣势。 1、端到端实时数据链路的构建,往往是以月为单位交付的,甚至更多; 2、新的数据需要须要大量的代码开发,交付周期也是以周为单位计算的; 3、数十种数据库技术,多家供应商,十几个反对电话,感觉本人也是是集成商; 4、实时数据处理技术栈门槛较高,人员流失率较高,刚刚用棘手的供应商总是换人; 5、数据组的要求无奈通过DBA的审核,利用研发对系统运维要求口碑载道; 6、资源应用与研发人员程度严密相干,无奈精确评估,遇到要害业务需要时顾此失彼。 第三,实时数据链路兼具业务经营与治理撑持要求,稳定性与容错性问题重重。从客户行为剖析到非交易类的触客业务到事件营销再到风控评分,实时数据链路逐步成为业务经营的重要撑持,但作为买通各业务零碎数据通道的中间层,受到的上下游的各类制约,对稳定性的影响尤其重大。 1、上下游节点的业务连续性和服务级别均高于实时数据链路,实时数据链路须要遵循上下游节点的认证、加密、权限、日志等管理机制; 2、上游数据对象构造变动与数据对象的解决机制对实时数据链路影响微小,例如构造变动采纳rename形式; 3、实时数据流量不仅仅须要参考业务交易量,与上游零碎的数据处理形式有很大的关系,经常出现一个语句百万行增量的状况; 4、随着企业多核心及多云策略的执行,部署在不同网域或云环境的系统配置,网络连通性乃至专线供应商与带宽都对稳定性有影响; 5、对打算、非打算的网络不可用,上下游系统维护,物理删除等非规操作及偶发的谬误数据及主键抵触数据没有相应的容错性策略配置; 6、呈现系统故障时,无奈保障各个组件的高可用,零碎复原艰难,特地是实时数据链路的数据完整性与数据一致性很难复原。 IT168:在过来一年中,DataPipeline在产品性能、技术研发,有哪些翻新和冲破? 陈雷:在过来的一年里,咱们针对产品进行了一次较为彻底的革新,次要体现在几个方面。 第一,进一步增强了基于日志的增量数据获取技术(Log-based change data capture),能够为各类数据平台和利用提供实时、精确的数据变动,从而使得客户能够依据最新数据进行经营治理与决策制定。 第二,对数据节点注册、数据链路配置、数据工作构建、零碎资源分配等各个环节进行分层治理,在无效地满足零碎运维治理需要的前提下,晋升实时数据获取与治理在各个环节的配合效率。在数据节点、数据链路、交融工作及系统资源四个根本逻辑概念中,用户只须要通过二至三项简略配置就能够定义出能够执行的交融工作,零碎提供基于最佳实际的默认选项,实时数据需要的研发交付工夫从2周缩小为5分钟。 第三,为应答简单的实时数据场景需要,零碎提供限度配置与策略配置两大类十余种高级配置。用户能够通过这些配置对上游进行限度与治理,也能够通过这些配置来对立调整上游的执行范畴与策略利用范畴。同时,优化了零碎整体的分布式引擎,实现了组件级高可用。从产品配置到零碎部署两个方面保障实时数据链路的稳固高容错。 IT168:近年来,您察看到的数据交融市场产生了哪些变动,有哪些发展趋势,DataPipeline如何符合这些趋势? 陈雷:数据交融市场产生的变动次要有以下几点变动。 第一,市场竞争和用户行为的巨大变化。 1、用户交互工夫越来越短,算法精度要求越来越高; 2、流量维度越来越多,不再局限于线上。必须适配场景来抢夺注意力; 3、曾经没有确定的价值锚点,企业必须一直放慢本身进化速度。 第二,转变经营模式要求多速IT的撑持。 1、以客户为核心的独立产品经营模式,企业逐步成为公共服务平台; 2、各个经营部门对数据的时效性、准确性、全面性要求都不雷同; 3、对作为根底公共服务的数据平台来说,不变的是对需要的疾速响应。 第三,数据需要响应从研发向配置转变。 1、数据撑持与利用开发、零碎运维的协调问题必须解决; 2、在保障数据资源可控的前提下,为数据利用提供更多的自主性与敏捷性; 3、零碎资源管理与零碎的部署扩大必须灵便不便且平滑稳固。 IT168:在国内上是否有相似数见科技数据交融的产品?相比之下有哪些差异化?国外的产品相比国内来讲有哪些借鉴意义? 陈雷:IBM的 InfoSphere Data Replication、DataStage和Streams、Oracle的Golden Gate和Informatica的PowerExchange和PowerCenter。和这类国外产品相比,DataPipeline有以下几点区别; 第一,从功能性上来讲,IBM和Oracle对各自的数据库的反对毋庸置疑是最好的,但对新兴的数据库特地是国内正在宽泛应用的数据库的反对力度就低了很多,DataPipeline通过自主研发和生态上下游的单干,不仅反对传统的Oracle等关系型数据库,也反对GaussDB、TiDB、巨杉等新兴数据库的实时数据采集。 第二,从部署架构和售卖形式上来讲,传统数据采集和数据处理工作是采纳成对部署、成对售卖的形式,对客户进行高可用部署、零碎扩容都不非常敌对,而DataPipeline是分布式集群部署,在系统资源容许的状况下不限度用户注册数据节点,采纳容器化部署形式,反对Kubernetes,反对动静扩缩容。 IT168:数见科技在做数据交融的过程中,有没有什么让您印象粗浅的故事?比方第一个客户是怎么来的?比方研发过程中如何解决一个比拟大的难题。 陈雷:应该说印象粗浅的事件切实是太多,客户上线的喜悦,排除故障的辛苦,攻克技术难关的成就感,和每个创业者都会经验的压力,但这些其实也都很平时,这些就是一个技术人员的日常。用两句短句总结一下。 但凡过往,皆为序章,十余年沐雨栉风,百万里地北天南,也平时! ...

January 25, 2021 · 1 min · jiezi

关于数据库设计:那些你不知道的表结构设计思路开源软件诞生9

ERP表构造的设计--第9篇用日志记录“开源软件”的诞生 赤龙 ERP 开源地址:点亮星标,感激反对,与开发者交换 kzca2000 码云:https://gitee.com/redragon/redragon-erp GitHub:https://github.com/redragon1985/redragon-erp 赤龙ERP官网:https://www.redragon-erp.com 前言上一篇文章说了ERP的零碎设计,数据库构造只是一笔带过,明天重点说说我在【赤龙ERP】的表构造外面的都做了哪些非凡的设计,并且为什么这么设计。 ID与编码我在每一个表简直无一例外的都减少了两个默认的字段,即ID和Code。这两个字段看似都是可标识数据的唯一性字段,但为什么要设计两个呢?它们当然各有用处。 (1)ID是一个表的主键,个别都是自增的,次要用于排序、定位、查问,因为它是数字所以更清晰、速度更快。 (2)Code是惟一键,类型多是字符。可用UUID或雪花算法等生成。当然在有具体业务场景的状况下,能够由用户输出或按逻辑生成。除了能够具备强语义外,还优先用于外键的关联。 这里做个非凡阐明:为什么要用Code做外键,ID也能够做外键啊。外键要具备两个最大的特点:惟一,不可变。ID因为多是自增或由数据库的特质生成,所以不能保障在数据迁徙时相对不变。所以应用Code更安全可靠一些。组织机构这个字段名为:org_code,示意组织机构。那什么是组织机构呢?简略说就是独立的公司或主体。作用次要是用于数据隔离,因为没有必要为不同公司建设不同的数据表,所以用一个字段将不同公司的数据隔离开。有点像财务的账套的概念。 操作记录在每个表都会减少四个字段,用来记录谁在什么工夫做了数据操作。别离为: (1)CREATED_DATE(创立工夫) (2)LAST_UPDATED_DATE(最初批改工夫) (3)CREATED_BY(创建人) (4)LAST_UPDATED_BY(最初批改人) 创建人和创立工夫,在数据新增的时候设置;最初批改人和最初批改工夫,在数据更新的时候设置数据权限信息化零碎都须要数据权限的管制,即什么人能够操作哪些数据。个别企业级信息化,数据权限的逻辑都是在组织架构的层面进行管制的。个别包含:本人操作本人的数据、不同级别部门内的数据可共享、整个公司的数据共享。 为了解决上述的数据权限管制的须要,所以减少一个字段DEPARTMENT_CODE(部门编码)。这个字段只会记录创立以后数据的人所属的部门,即这条数据的所属部门。代码层面再联合数据权限,即可实现数据权限的管控。 版本与日志表在须要记录数据版本的表中减少VERSION(版本号),常见的业务场景就是“变更性能”。上面举个例子,比方:洽购订单变更。当咱们创立了一个洽购订单,并且审批通过后,这个数据实质是不能批改的,但呈现须要批改的时候,咱们就须要用上洽购订单变更性能。当订单变更时,须要做的就是版本号+1,并且在日志表生成历史数据。 自定义字段自定义字段的作用是让用户能够依据本人的业务须要减少一个表的字段并保留数据。做法是须要在一个表中减少attribute字段,少数状况下会预留多个attribute字段,字段名attribute1、attribute2、attribute3以此类推。而后再通过可配置的性能来设置attribute字段和字段中文名的对应关系即可。 心愿您读完本文能够帮忙笔者进入【码云】或【GitHub】搜寻“赤龙ERP”点击星标。期待着您的反对!

September 16, 2020 · 1 min · jiezi

一文读懂图数据库-Nebula-Graph-访问控制实现原理

摘要:数据库权限管理对大家都很熟悉,然而怎么做好数据库权限管理呢?在本文中将详细介绍 Nebula Graph 的用户管理和权限管理。 本文首发 Nebula Graph 博客:https://nebula-graph.com.cn/posts/access-control-design-code-nebula-graph/数据库权限管理对大家来说都已经很熟悉了。Nebula Graph 本身是一个高性能的海量图数据库,数据库的安全问题更是数据库设计的重中之重。目前 Nebula Graph 已支持基于角色的权限控制功能。在这篇文章中将详细介绍 Nebula Graph 的用户管理和权限管理。 Nebula Graph 架构体系 由上图可知,Nebula Graph的主体架构分为三部分:Computation Layer、Storage Layer 和 Meta Service。Console 、API 和 Web Service 被统称为 Client API。 账户数据和权限数据将被存储在 Meta Engine中,当Query Engine 启动后,将会初始 Meta Client,Query Engine 将通过 Meta Client 与 Meta Service 进行通信。 当用户通过 Client API 连接 Query Engine 时,Query Engine 会通过 Meta Client 查询 Meta Engine 的用户数据,并判断连接账户是否存在,以及密码是否正确。当验证通过后,连接创建成功,用户可以通过这个连接执行数据操作。当用户通过 Client API 发送操作指令后,Query Engine 首先对此指令做语法解析,识别操作类型,通过操作类型、用户角色等信息进行权限判断,如果权限无效,则直接在 Query Engine 阻挡操作,并返回错误信息至 Client API。 在整个权限检查的过程中,Nebula Graph 对 Meta data 进行了缓存,将在以下章节中介绍。 ...

June 3, 2020 · 3 min · jiezi

Java秒杀系统实战系列整体业务流程介绍与数据库设计

摘要: 本篇博文是“Java秒杀系统实战系列文章”的第三篇,本篇博文将主要介绍秒杀系统的整体业务流程,并根据相应的业务流程进行数据库设计,最终采用Mybatis逆向工程生成相应的实体类Entity、操作Sql的接口Mapper以及写动态Sql的配置文件Mapper.xml。 内容: 对于该秒杀系统的整体业务流程,相信机灵的小伙伴在看完第二篇博文的时候,就已经知道个大概了!因为在提供的源码数据库下载的链接中,Debug已经跟各位小伙伴介绍了该秒杀系统整体的业务流程,而且还以视频形式给各位小伙伴进行了展示!该源码数据库的下载链接如下:https://gitee.com/steadyjack/...  在本篇博文中Debug将继续花一点篇幅介绍介绍! 一图以概之,如下图所示为该秒杀系统整体的业务流程: 从该业务流程图中,可以看出,后端接口在接收前端的秒杀请求时,其核心处理逻辑为: (1)首先判断当前用户是否已经抢购过该商品了,如果否,则代表用户没有抢购过该商品,可以进入下一步的处理逻辑 (2)判断该商品可抢的剩余数量,即库存是否充足(即是否大于0),如果是,则进入下一步的处理逻辑 (3)扣减库存,并更新数据库的中对应抢购记录的库存(一般是减一操作),判断更新库存的数据库操作是否成功了,如果是,则创建用户秒杀成功的订单,并异步发送短信或者邮件通知信息通知用户 (4)以上的操作逻辑如果有任何一步是不满足条件的,则直接结束整个秒杀的流程,即秒杀失败! 如下图所示为后端处理“秒杀请求”时的核心处理逻辑: 综合这两个业务流程,下面进入“秒杀系统”的数据库设计环节,其中,主要包含以下几个表:商品信息表item、待秒杀信息表item_kill、秒杀成功记录表item_kill_success以及用户信息表user;当然,在实际的大型网站中,其所包含的数据库表远远不止于此!本系统暂且浓缩出其中核心的几张表! 如下图所示为该“秒杀系统”的数据库设计模型: 紧接着,是采用Mybatis的逆向工程生成这几个数据库表对应的实体类Entity、操作Sql的接口Mapper以及写动态Sql的配置文件Mapper.xml。如下图所示: 下面,贴出其中一个实体类以及相对应的Mapper接口和Mapper.xml代码,其他的,各位小伙伴可以点击链接:https://gitee.com/steadyjack/... 前往下载查看!首先是实体类ItemKill的源代码: import com.fasterxml.jackson.annotation.JsonFormat;import lombok.Data;import java.util.Date;@Datapublic class ItemKill { private Integer id; private Integer itemId; private Integer total; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date startTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date endTime; private Byte isActive; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; private String itemName; //采用服务器时间控制是否可以进行抢购 private Integer canKill;}然后是ItemKillMapper接口的源代码: ...

July 16, 2019 · 2 min · jiezi

认识CoreData-基础使用

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/0ddfa35c7898 第一篇文章中并没有讲CoreData的具体用法,只是对CoreData做了一个详细的介绍,算是一个开始和总结吧。这篇文章中会主要讲CoreData的基础使用,以及在使用中需要注意的一些细节。因为文章中会插入代码和图片,内容可能会比较多,比较考验各位耐心。 文章中如有疏漏或错误,还请各位及时提出,谢谢!????` 创建自带CoreData的工程在新建一个项目时,可以勾选Use Core Data选项,这样创建出来的工程系统会默认生成一些CoreData的代码以及一个.xcdatamodeld后缀的模型文件,模型文件默认以工程名开头。这些代码在AppDelegate类中,也就是代表可以在全局使用AppDelegate.h文件中声明的CoreData方法和属性。 系统默认生成的代码是非常简单的,只是生成了基础的托管对象模型、托管对象上下文、持久化存储调度器,以及MOC的save方法。但是这些代码已经可以完成基础的CoreData操作了。 这部分代码不应该放在AppDelegate中,尤其对于大型项目来说,更应该把这部分代码单独抽离出去,放在专门的类或模块来管理CoreData相关的逻辑。所以我一般不会通过这种方式创建CoreData,我一般都是新建一个“干净”的项目,然后自己往里面添加,这样对于CoreData的完整使用流程掌握的也比较牢固。 CoreData模型文件的创建构建模型文件使用CoreData的第一步是创建后缀为.xcdatamodeld的模型文件,使用快捷键Command + N,选择Core Data -> Data Model -> Next,完成模型文件的创建。 创建完成后可以看到模型文件左侧列表,有三个选项Entities、Fetch Requests、Configurations,分别对应着实体、请求模板、配置信息。 添加实体现在可以通过长按左侧列表下方的Add Entity按钮,会弹出Add Entity、Add Fetch Request、Add Configuration选项,可以添加实体、请求模板、配置信息。这里先选择Add Entity来添加一个实体,命名为Person。 添加Person实体后,会发现一个实体对应着三部分内容,Attributes、Relationships、Fetched Properties,分别对应着属性、关联关系、获取操作。 现在对Person实体添加两个属性,添加age属性并设置type为Integer 16,添加name属性并设置type为String。 实体属性类型在模型文件的实体中,参数类型和平时创建继承自NSObject的模型类大体类似,但是还是有一些关于类型的说明,下面简单的列举了一下。 Undefined: 默认值,参与编译会报错Integer 16: 整数,表示范围 -32768 ~ 32767Integer 32: 整数,表示范围 -2147483648 ~ 2147483647Integer 64: 整数,表示范围 –9223372036854775808 ~ 9223372036854775807Float: 小数,通过MAXFLOAT宏定义来看,最大值用科学计数法表示是 0x1.fffffep+127fDouble: 小数,小数位比Float更精确,表示范围更大String: 字符串,用NSString表示Boolean: 布尔值,用NSNumber表示Date: 时间,用NSDate表示Binary Data: 二进制,用NSData表示Transformable: OC对象,用id表示。可以在创建托管对象类文件后,手动改为对应的OC类名。使用的前提是,这个OC对象必须遵守并实现NSCoding协议添加实体关联关系创建两个实体Department和Employee,并且在这两个实体中分别添加一些属性,下面将会根据这两个实体来添加关联关系。 ...

June 24, 2019 · 3 min · jiezi

认识CoreData-多线程

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/283e67ba12a3 CoreData使用相关的技术点已经讲差不多了,我所掌握的也就这么多了....在本篇文章中主要讲CoreData的多线程,其中会包括并发队列类型、线程安全等技术点。我对多线程的理解可能不是太透彻,文章中出现的问题还请各位指出。在之后公司项目使用CoreData的过程中,我会将其中遇到的多线程相关的问题更新到文章中。 在文章的最后,会根据我对CoreData多线程的学习,以及在工作中的具体使用,给出一些关于多线程结构的设计建议,各位可以当做参考。 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? MOC并发队列类型在CoreData中MOC是支持多线程的,可以在创建MOC对象时,指定其并发队列的类型。当指定队列类型后,系统会将操作都放在指定的队列中执行,如果指定的是私有队列,系统会创建一个新的队列。但这都是系统内部的行为,我们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的。 NSManagedObjectContext并发队列类型:NSConfinementConcurrencyType : 如果使用init方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。NSPrivateQueueConcurrencyType : 私有并发队列类型,操作都是在子线程中完成的。NSMainQueueConcurrencyType : 主并发队列类型,如果涉及到UI相关的操作,应该考虑使用这个枚举值初始化上下文。其中NSConfinementConcurrencyType类型在iOS9之后已经被苹果废弃,不建议使用这个API。使用此类型创建的MOC,调用某些比较新的CoreData的API可能会导致崩溃。 MOC多线程调用方式在CoreData中MOC不是线程安全的,在多线程情况下使用MOC时,不能简单的将MOC从一个线程中传递到另一个线程中使用,这并不是CoreData的多线程,而且会出问题。对于MOC多线程的使用,苹果给出了自己的解决方案。 在创建的MOC中使用多线程,无论是私有队列还是主队列,都应该采用下面两种多线程的使用方式,而不是自己手动创建线程。调用下面方法后,系统内部会将任务派发到不同的队列中执行。可以在不同的线程中调用MOC的这两个方法,这个是允许的。 - (void)performBlock:(void (^)())block 异步执行的block,调用之后会立刻返回。- (void)performBlockAndWait:(void (^)())block 同步执行的block,调用之后会等待这个任务完成,才会继续向下执行。 下面是多线程调用的示例代码,在多线程的环境下执行MOC的save方法,就是将save方法放在MOC的block体中异步执行,其他方法的调用也是一样的。 [context performBlock:^{ [context save:nil];}];但是需要注意的是,这两个block方法不能在NSConfinementConcurrencyType类型的MOC下调用,这个类型的MOC是不支持多线程的,只支持其他两种并发方式的MOC。 多线程的使用在业务比较复杂的情况下,需要进行大量数据处理,并且还需要涉及到UI的操作。对于这种复杂需求,如果都放在主队列中,对性能和界面流畅度都会有很大的影响,导致用户体验非常差,降低屏幕FPS。对于这种情况,可以采取多个MOC配合的方式。 CoreData多线程的发展中,在iOS5经历了一次比较大的变化,之后可以更方便的使用多线程。从iOS5开始,支持设置MOC的parentContext属性,通过这个属性可以设置MOC的父MOC。下面会针对iOS5之前和之后,分别讲解CoreData的多线程使用。 尽管现在的开发中早就不兼容iOS5之前的系统了,但是作为了解这里还是要讲一下,而且这种同步方式在iOS5之后也是可以正常使用的,也有很多人还在使用这种同步方式,下面其他章节也是同理。 iOS5之前使用多个MOC在iOS5之前实现MOC的多线程,可以创建多个MOC,多个MOC使用同一个PSC,并让多个MOC实现数据同步。通过这种方式不用担心PSC在调用过程中的线程问题,MOC在使用PSC进行save操作时,会对PSC进行加锁,等当前加锁的MOC执行完操作之后,其他MOC才能继续执行操作。 每一个PSC都对应着一个持久化存储区,PSC知道存储区中数据存储的数据结构,而MOC需要使用这个PSC进行save操作的实现。 这样做有一个问题,当一个MOC发生改变并持久化到本地时,系统并不会将其他MOC缓存在内存中的NSManagedObject对象改变。所以这就需要我们在MOC发生改变时,将其他MOC数据更新。 根据上面的解释,在下面例子中创建了一个主队列的mainMOC,主要用于UI操作。一个私有队列的backgroundMOC,用于除UI之外的耗时操作,两个MOC使用的同一个PSC。 // 获取PSC实例对象- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { // 创建托管对象模型,并指明加载Company模型文件 NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"]; NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath]; // 创建PSC对象,并将托管对象模型当做参数传入,其他MOC都是用这一个PSC。 NSPersistentStoreCoordinator *PSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // 根据指定的路径,创建并关联本地数据库 NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject; dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"]; [PSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil]; return PSC;}// 初始化用于本地存储的所有MOC- (void)createManagedObjectContext { // 创建PSC实例对象,其他MOC都用这一个PSC。 NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator; // 创建主队列MOC,用于执行UI操作 NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; mainMOC.persistentStoreCoordinator = PSC; // 创建私有队列MOC,用于执行其他耗时操作 NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; backgroundMOC.persistentStoreCoordinator = PSC; // 通过监听NSManagedObjectContextDidSaveNotification通知,来获取所有MOC的改变消息 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];}// MOC改变后的通知回调- (void)contextChanged:(NSNotification *)noti { NSManagedObjectContext *MOC = noti.object; // 这里需要做判断操作,判断当前改变的MOC是否我们将要做同步的MOC,如果就是当前MOC自己做的改变,那就不需要再同步自己了。 // 由于项目中可能存在多个PSC,所以下面还需要判断PSC是否当前操作的PSC,如果不是当前PSC则不需要同步,不要去同步其他本地存储的数据。 [MOC performBlock:^{ // 直接调用系统提供的同步API,系统内部会完成同步的实现细节。 [MOC mergeChangesFromContextDidSaveNotification:noti]; }];}在上面的Demo中,创建了一个PSC,并将其他MOC都关联到这个PSC上,这样所有的MOC执行本地持久化相关的操作时,都是通过同一个PSC进行操作的。并在下面添加了一个通知,这个通知是监听所有MOC执行save操作后的通知,并在通知的回调方法中进行数据的合并。 ...

June 24, 2019 · 2 min · jiezi

认识CoreData-MagicalRecord

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> http://www.jianshu.com/p/61b7a615508c 到目前为止,已经将CoreData相关的知识点都讲完了。在这篇文章中,主要讲一个CoreData第三方库-MagicalRecord。目前为止这个第三方在Github上有9500+的Star,是所有CoreData第三方库中使用最多、功能最全的。在文章的后面还会对CoreData做一个总结,以及对本系列所有文章做一个总结。 文章中如有疏漏或错误,还请各位及时提出,谢谢!???? MagicalRecordCoreData是苹果自家推出的一个持久化框架,使用起来更加面向对象。但是在使用过程中会出现大量代码,而且CoreData学习曲线比较陡峭,如果掌握不好,在使用过程中很容易造成其他问题。 国外开发者开源了一个基于CoreData封装的第三方——MagicalRecord,就像是FMDB封装SQLite一样,MagicalRecord封装的CoreData,使得原生的CoreData更加容易使用。并且MagicalRecord降低了CoreData的使用门槛,不用去手动管理之前的PSC、MOC等对象。 根据Github上MagicalRecord的官方文档,MagicalRecord的优点主要有三条: 1. 清理项目中CoreData代码2. 支持清晰、简单、一行式的查询操作3. 当需要优化请求时,可以获取NSFetchRequest进行修改 添加MagicalRecord到项目中将MagicalRecord添加到项目中,和使用其他第三方一样,可以通过下载源码和CocoaPods两种方式添加。 1. 从Github下载MagicalRecord源码,将源码直接拖到项目中,后续需要手动更新源码。 2. 也可以通过CocoaPods安装MagicalRecord,需要在Podfile中加入下面命令,后续只需要通过命令来更新。 pod "MagicalRecord"在之前创建新项目时,通过勾选"Use Core Data"的方式添加CoreData到项目中,会在AppDelegate文件中生成大量CoreData相关代码。如果是大型项目,被占用的位置是很重要的。而对于MagicalRecord来说,只需要两行代码即可。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 初始化CoreData堆栈,也可以指定初始化某个CoreData堆栈 [MagicalRecord setupCoreDataStack]; return YES; } - (void)applicationWillTerminate:(UIApplication *)application { // 在应用退出时,应该调用cleanUp方法 [MagicalRecord cleanUp]; }MagicalRecord是支持CoreData的.xcdatamodeld文件的,使得CoreData这一优点可以继续使用。建立数据结构时还是像之前使用CoreData一样,通过.xcdatamodeld文件的方式建立。 支持iCloudCoreData是支持iCloud的,MagicalRecord对iCloud相关的操作也做了封装,只需要使用MagicalRecord+iCloud.h类中提供的方法,就可以进行iCloud相关的操作。 例如下面是MagicalRecord+iCloud.h中的一个方法,需要将相关参数传入即可。 + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID localStoreNamed:(NSString *)localStore;创建上下文MagicalRecord对上下文的管理和创建也比较全面,下面是MagicalRecord提供的部分创建和获取上下文的代码。因为是给NSManagedObjectContext添加的Category,可以直接用NSManagedObjectContext类调用,使用非常方便。 但是需要注意,虽然系统帮我们管理了上下文对象,对于耗时操作仍然要放在后台线程中处理,并且在主线程中进行UI操作。 + [NSManagedObjectContext MR_context] 设置默认的上下文为它的父级上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_newMainQueueContext] 创建一个新的上下文,并发类型为NSMainQueueConcurrencyType+ [NSManagedObjectContext MR_newPrivateQueueContext] 创建一个新的上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_contextWithParent:] 创建一个新的上下文,允许自定义父级上下文,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_contextWithStoreCoordinator:] 创建一个新的上下文,并允许自定义持久化存储协调器,并发类型为NSPrivateQueueConcurrencyType+ [NSManagedObjectContext MR_defaultContext] 获取默认上下文对象,项目中最基础的上下文对象,并发类型是NSMainQueueConcurrencyType增删改查MagicalRecord对NSManagedObject添加了一个Category,将增删改查等操作放在这个Category中,使得这些操作可以直接被NSManagedObject类及其子类调用。 ...

June 24, 2019 · 1 min · jiezi

一款在线ER模型生成的工具-ER模型生成通过DDL语句生成ER模型

我是如何生成ER模型图,无脑加小白生成ER模型,还用画ER模型图吗,直接导入,数据库视图直接导入查看。 Freedgo(自由行走) Design 一款在线ER模型生成的工具,可以针对MySQL的DDL文件在线生成ER模型图表。 支持导入SQL文件创建ER模型,支持create table,alter table。支持主键、外键显示 具体操作如下: 步骤一:首先访问https://www.freedgo.com/draw_... 调整图形 -> 插入 -> From MySQL 步骤二:使用工具生成数据库表结构SQL语句,然后copy到输入框,点击Insert MySQL

June 12, 2019 · 1 min · jiezi

11知识图谱是什么

知识图谱是什么?一起先看看知识图谱的发展、定义和相关示例吧。 知识图谱的发展:知识图谱自上世纪60年代从语义网络发展起来以后,分别经历了1980年代的专家系统、1990年代的贝叶斯网络、2000年代的OWL和语义WEB,以及2010年以后的谷歌的知识图谱。2012年Google知识图谱一出激起千层浪:微软必应、搜狗、百度等搜索引擎公司在短短一年内纷纷宣布了各自的“知识图谱”产品,如百度“知心”、搜狗“知立方(现更名为‘立知’)”等。谷歌目前的知识图谱已经包含了数亿个条目,并广泛应用于搜索、推荐等领域。知识图谱的定义:在维基百科的官方词条中:知识图谱是Google用于增强其搜索引擎功能的知识库。本质上,知识图谱是一种揭示实体之间关系的语义网络,可以对现实世界的事物及其相互关系进行形式化地描述。详细的说,知识图谱是基于图的数据结构,以图的方式存储知识并向用户返回经过加工和推理的知识。它由“节点”和“边”组成,节点表示现实世界中的“实体”,边表示实体之间的“关系”。现在的知识图谱已被用来泛指各种大规模的知识库。知识图谱的一些示例:1、基础三元组表示![基础三元组表示]() 2、知识图谱在Neo4j中的可视化![知识图谱在Neo4j中的可视化]() 3、知识图谱在金融领域的使用![知识图谱在金融领域的使用]() 知识图谱技术研究内容包括:1、知识表示:研究客观世界知识的建模,以方便机器识别和理解,既要考虑知识的表示与存储,又要考虑知识的使用和计算;2、知识图谱构建:解决如何建立计算机算法从客观世界或者互联网的各种数据资源中获取客观世界的知识,主要研究使用何种数据和方法抽取何种知识;3、知识图谱应用:主要研究如何利用知识图谱建立基于知识的智能服务系统,更好地解决实际应用问题。相关资料知识图谱的技术与应用(18版)知识图谱初探

June 8, 2019 · 1 min · jiezi

12知识图谱有什么用

知识图谱经过几年的发展已经得到广泛的应用。当知识图谱遇上人工智能,更加突显出了它的优势和价值。 最先应用于搜索![用Google搜索泰姬陵]() 最典型的就是在谷歌搜索引擎里面应用。谷歌是在2012年率先提出来知识图谱的概念,提出这个概念的最主要的目的就是用于改善它的搜索引擎的体验。我们从这个图就可以看到,用户搜索的是泰姬陵,泰姬陵是印度的非常著名的,也是世界八大奇迹之一的景点。这里面不一样的地方是它在搜索引擎的右侧,会以知识卡片的形式来呈现跟泰姬陵相关的结构化的信息,包括泰姬陵的地图、图片、景点的描述、开放时间门票等等,甚至在下面会列出跟泰姬陵相类似或者相关联的景点,比如中国的万里长城同样是世界的几大奇迹,包括金字塔等等。这样的知识点,可以非常好的把知识组织和关联起来。现已广泛应用于金融风控![借款人身份信息]() 反欺诈是风控中非常重要的一道环节,也是知识图谱适合应用的场景。反欺诈的核心是人,这就要求把与借款人相关的数据源打通,然后抽取该借款人的特征标签,从而将相关的信息整合成结构化的知识图谱。其中,不仅可以处理记录借款人的基本信息,还可以把借款人日常生活中的消费记录、行为记录、关系信息、网上浏览记录等整合到知识图谱里。在此基础上,对该借款人的借贷风险进行分析和评估。 反欺诈的应用不仅体现在贷前阶段,还可以应用在贷中阶段,通过构建已知的主要欺诈要素(如手机、设备、账号和地域等)的关系图谱,全方位了解借款人风险数据的统计分析,对潜在的欺诈行为作出及时的反应。当然,这要求能够获得借款人全方位的各种类型的信息,并且利用机器学习和自然语言处理技术从数据中提取出符合图谱规格的数据。相比虚假身份的识别,组团欺诈的发现难度更大。一般来说,团体欺诈往往隐藏在非常复杂的关系网络里,很难识别。只有把其中隐含的关系网络梳理清楚,才有可能去分析出其中潜在的风险。知识图谱,因为天生用来描述关系网络,因而具备了分析组团欺诈的便捷手段。 电商营销方面大显身手![电商网站推荐商品]() 基于知识图谱的精准营销,能够知道你的客户的非常详细的信息,包括名字,住址,经常和什么样的人进行互动,还认识其它什么样的人,网上的行为习惯、行为方式是什么样子。这样就可以知识图谱挖掘出更多的用户的属性标签和兴趣标签,以及社会的属性标签,基于知识图谱就可以进行个性化的商品核心活动的推送能够实现,从而实现精准的营销。还可以借助商品知识图谱,通过用户已经购买的商品,推荐相关联的潜在需求商品。行业预测上的应用不容小觑![企业信息知识图谱]() 基于多维度的数据,从而建立起客户、企业和行业间的知识图谱,从行业关联的角度预测行业或企业面临的风险。例如,通过对行业进行细分,根据贷款信息、行业信息建立行业间的关系模型;通过机器学习,可发现各个行业间的关联度,如果某一行业发生了行业风险或高风险事件,根据关联关系可以及时预测有潜在风险的其他行业。从而可以帮助金融机构做出预判,尽早地规避风险。除此以外,通过知识图谱,也可以将行业和企业之间数据进行连接,借助对行业的潜在风险的预测,能够及时发现与该行业风险或系统性风险相关联的企业客户。例如,某地区某行业连续出现了多笔逾期贷款,通过对行业和客户的知识图谱进行分析,可以及时发现该地区相关行业存在潜在风险的客户。 还有知识搜索、智能问答方面![智能问答系统知识图谱]() 基于知识图谱,我们也可以提供智能搜索和数据可视化服务。智能搜索的功能指的是,知识图谱能够在语义上扩展用户的搜索关键词,从而返回更丰富、更全面的信息。比如,搜索某个人的身份证号,可以返回与这个人相关的所有历史借款记录、联系人关系和其他相关的标签(如黑名单等)。这些结果可以用图形网络的方式展示,从而把复杂的信息以直观明了的图像呈现出来,让使用者对隐藏信息的来龙去脉一目了然。问答系统可分为面向任务、面向知识和面向聊天三类,从关键技术上分,还可以把其分成基于搜索技术的问答系统、基于协同的问答系统、基于知识库的问答系统。面向知识的问答系统可用于闭域和开放域,通常使用以数据为驱动的信息检索模型。该类方法基于从问答知识库中查找与提问问题最匹配的知识。一份最新的研究工作尝试使用基于神经网络的方法实现问题间的匹配。最常用的一种方法是基于知识图谱与信息检索相结合的方法,检索知识图谱可给出高准确率的问答,并以信息检索为补充。目前国内有代表性的企业应用搜索方面的应用:像百度“知心”,搜狗“知立方”等智能问答方面的应用:百度度秘,阿里小蜜,搜狗汪仔等行业应用:脉脉,天眼查,企信宝,出门问问等相关资料为什么知识图谱终于火了?知识图谱正在改变金融?深度解剖知识图谱的四大应用

June 8, 2019 · 1 min · jiezi

13知识图谱怎么去做

知识图谱怎么去做,这当然不是几句话说得清楚的。首先肯定要先基于自身的业务进行思考,这里整理一些知识图谱构建的主要路径。 构建的逻辑思路1、梳理业务,构建本体:是否需要用知识图谱?成本怎么样,能达到怎么的效果?是否有能力构建知识图谱?数据、团队等情况是否能支撑?如果有必要,如何根据业务梳理一套本体框架?2、编辑本体,给出业务知识表示框架:可以利用Protege进行本体编辑,获得一个用OWL表示的知识表示文件。3、给本体补充实例数据:先找一些示例数据,便于理解。构建的不同方式自顶向下的构建方式:先定义本体和数据模式,再将实体加入知识库。利用一些现有的结构化知识库作为其基础知识库。自底向上的构建方式:从一些开放链接数据中提取出实体,选择其中置信度较高的加入到知识库,再构建顶层的本体模式。构建过程中的关键技术大体包含五个方面:知识抽取、知识表示、知识融合、知识加工、知识评估通过知识提取技术,可以从一些公开的半结构化、非结构化和第三方结构化数据库的数据中提取出实体、关系、属性等知识要素。知识表示则通过一定有效手段对知识要素表示,便于进一步处理使用。分布式的知识表示形成的综合向量对知识库的构建、推理、融合以及应用均具有重要的意义。然后通过知识融合,可消除实体、关系、属性等指称项与事实对象之间的歧义,形成高质量的知识库。知识加工则是在已有的知识库基础上进一步挖掘隐含的知识,构建新本体,补全关系,从而丰富、扩展知识库。知识评估可以对知识的可信度进行量化,保留置信度较高的,舍弃置信度较低的,有效确保知识的质量。除此之外,大规模知识图谱构建,还需要多种技术的支持:分布式存储和计算、图数据库、图推理、内存数据库等。数据的存储数据库选择知识图谱的存储和查询语言也经历了历史的洗涤,从RDF到OWL以及SPARQL查询,都逐渐因为使用上的不便及高昂的成本,而被工业界主流所遗弃。图数据库逐步成为目前主要的知识图谱存储方式。 目前应用比较广泛的图数据库包括Neo4j、graphsql、sparkgraphx(包含图计算引擎)、基于hbase的Titan、BlazeGraph等,各家的存储语言和查询语言也不尽相同。实际应用场景下,OrientDB和postgresql也有很多的应用,主要原因是其相对低廉的实现成本和性能优势。 应用推理和知识自学习在知识图谱构建过程中,还存在很多关系补全问题。虽然一个普通的知识图谱可能存在数百万的实体和数亿的关系事实,但相距补全还差很远。知识图谱的补全是通过现有知识图谱来预测实体之间的关系,是对关系抽取的重要补充。 传统方法TransE和TransH通过把关系作为从实体A到实体B的翻译来建立实体和关系嵌入,但是这些模型仅仅简单地假设实体和关系处于相同的语义空间。而事实上,一个实体是由多种属性组成的综合体,不同关系关注实体的不同属性,所以仅仅在一个空间内对他们进行建模是不够的。 相关资料大规模知识图谱的构建、推理及应用肖仰华 | 大规模知识图谱构建与应用

June 8, 2019 · 1 min · jiezi

势高则围广TiDB-的架构演进哲学

本文根据我司 CEO 刘奇在第 100 期 Infra Meetup 上的演讲整理,预计阅读时间为 30 分钟。 大家可能知道我是 PingCAP CEO,但是不知道的是,我也是 PingCAP 的产品经理,应该也是最大的产品经理,是对于产品重大特性具有一票否决权的人。中国有一类产品经理是这样的,别人有的功能我们统统都要有,别人没有的功能,我们也统统都要有,所以大家看到传统的国内好多产品就是一个超级巨无霸,功能巨多、巨难用。所以我在 PingCAP 的一个重要职责是排除掉“看起来应该需要但实际上不需要”的那些功能,保证我们的产品足够的专注、足够聚焦,同时又具有足够的弹性。一、最初的三个基本信念本次分享题目是《TiDB 的架构演进哲学》,既然讲哲学那肯定有故事和教训,否则哲学从哪儿来呢?但从另外的角度来说,一般大家来讲哲学就先得有信念。有一个内容特别扯的美剧叫做《美国众神》,里面核心的一条思路是“你相信什么你就是什么”。其实人类这么多年来,基本上也是朝这条线路在走的,人类对于未知的东西很难做一个很精确的推导,这时信念就变得非常重要了。 <center>图 1 最初的基本信念</center> 实际上,我们开始做 TiDB 这个产品的时候,第一个信念就是相信云是未来。当年 K8s 还没火,我们就坚定的拥抱了 K8s。第二是不依赖特定硬件、特定的云厂商,也就是说 TiDB 的设计方向是希望可以 Run 在所有环境上面,包括公有云私有云等等。第三是能支持多种硬件,大家都知道我们支持 X86、AMD64、ARM 等等,可能大家不清楚的是 MIPS,MIPS 典型代表是龙芯,除此之外,TiDB 未来还可以在 GPU 上跑(TiFlash 的后续工作会支持 GPU)。 二、早期用户故事2.1 Make it work有一句话大概是“眼睛里面写满了故事,脸上没有一点沧桑”,其实现实是残酷的,岁月一定会给你沧桑的。我们早期的时候,也有相对比较难的时候,这时候就有一些故事关于我们怎么去经历、怎么渡过的。   首先大家做产品之前肯定先做用户调研,这是通用的流程,我们当初也做过这个事,跟用户聊。我们通常会说:“我们要做一个分布式数据库,自动弹性伸缩,能解决分库分表的问题,你会用吗?”用户说“那肯定啊,现在的分库分表太痛苦了。”这是最初我们获取需求最普通的方式,也是我们最容易掉入陷阱的方式,就好像“我有一百万,你要不要?肯定要。”“我有一瓶水,喝了之后就健康无比,延年益寿你要不要?肯定要。”很容易就得到类似的结论。 所以这个一句话结论的代价是我们进行了长达两年的开发。在这两年的时间里,我们确定了很多的技术方向,比如最初 TiDB 就决定是分层的。很显然一个复杂的系统如果没有分层,基本上没有办法很好的控制规模和复杂度。TiDB 分两层,一层是 SQL 层,一层是 key-value 层,那么到底先从哪一个层开始写呢?其实从哪层开始都可以,但是总要有一个先后,如何选择? 这里就涉及到 TiDB 的第一条哲学。我们做一个产品的时候会不断面临选择,那每次选择的时候核心指导思想是什么?核心思想是能一直指导我们持续往前去迭代,所以我们第一条哲学就是:永远站在离用户更近的地方去考虑问题。 为什么我们会定义这样一条哲学?因为离用户越近越能更快的得到用户的反馈,更快的验证你的想法是不是可行的。显然 SQL 层离用户更近,所以我们选择从 SQL 层写起。其实一直到现在,绝大多数用户用 TiDB 的时候根本感受不到 KV 层的存在,用户写的都是 SQL,至于底层存储引擎换成了别的,或者底层的 RocksDB 做了很多优化和改进,这些变化对于用户关注的接口来说是不可见的。 ...

May 31, 2019 · 4 min · jiezi

通俗易懂如何设计能支撑百万并发的数据库架构

1、引言相信看到这个标题,很多人的第一反应就是:对数据库进行分库分表啊!但是实际上,数据库层面的分库分表到底是用来干什么的,其不同的作用如何应对不同的场景,我觉得很多同学可能都没搞清楚。 本篇文章我们一起来学习一下,对于一个支撑日活百万用户的高并发系统,数据库架构应该如何设计呢? 本文的讨论和分享,将用一个创业公司的发展作为背景引入,方便大家理解。 (本文同步发布于:http://www.52im.net/thread-25...) 2、相关文章高性能数据库方面的文章: 《优秀后端架构师必会知识:史上最全MySQL大表优化方案总结》《阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史》 《阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路》 《腾讯TEG团队原创:基于MySQL的分布式数据库TDSQL十年锻造经验分享》 分布式架构方面的入门文章: 《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》 《快速理解高性能HTTP服务端的负载均衡技术原理》 《一篇读懂分布式架构下的负载均衡技术:分类、原理、算法、常见方案等》 3、小型系统的典型数据库单机架构和明显的瓶颈假如我们现在是一个小创业公司,注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10。 天呐!就这种系统,随便找一个有几年工作经验的高级工程师,然后带几个年轻工程师,随便干干都可以做出来。 因为这样的系统,实际上主要就是在前期进行快速的业务功能开发,搞一个单块系统部署在一台服务器上,然后连接一个数据库就可以了。 接着大家就是不停地在一个工程里填充进去各种业务代码,尽快把公司的业务支撑起来。 如下图所示: 结果呢,没想到我们运气这么好,碰上个优秀的 CEO 带着我们走上了康庄大道! 公司业务发展迅猛,过了几个月,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表新增数据量达到 50 万条!高峰期每秒请求量达到 1 万! 同时公司还顺带着融资了两轮,估值达到了惊人的几亿美金!一只朝气蓬勃的幼年独角兽的节奏! 好吧,现在大家感觉压力已经有点大了,为啥呢?因为每天单表新增 50 万条数据,一个月就多 1500 万条数据,一年下来单表会达到上亿条数据。 经过一段时间的运行,现在咱们单表已经两三千万条数据了,勉强还能支撑着。 但是,眼见着系统访问数据库的性能怎么越来越差呢,单表数据量越来越大,拖垮了一些复杂查询 SQL 的性能啊! 然后高峰期请求现在是每秒 1 万,咱们的系统在线上部署了 20 台机器,平均每台机器每秒支撑 500 请求,这个还能抗住,没啥大问题。但是数据库层面呢? 如果说此时你还是一台数据库服务器在支撑每秒上万的请求,负责任的告诉你,每次高峰期会出现下述问题: 1)你的数据库服务器的磁盘 IO、网络带宽、CPU 负载、内存消耗,都会达到非常高的情况,数据库所在服务器的整体负载会非常重,甚至都快不堪重负了; 2)高峰期时,本来你单表数据量就很大,SQL 性能就不太好,这时加上你的数据库服务器负载太高导致性能下降,就会发现你的 SQL 性能更差了; 3)最明显的一个感觉,就是你的系统在高峰期各个功能都运行的很慢,用户体验很差,点一个按钮可能要几十秒才出来结果; 4)如果你运气不太好,数据库服务器的配置不是特别的高的话,弄不好你还会经历数据库宕机的情况,因为负载太高对数据库压力太大了。 4、多台服务器分库支撑高并发读写首先我们先考虑第一个问题,数据库每秒上万的并发请求应该如何来支撑呢? 要搞清楚这个问题,先得明白一般数据库部署在什么配置的服务器上。通常来说,假如你用普通配置的服务器来部署数据库,那也起码是 16 核 32G 的机器配置。 ...

May 15, 2019 · 5 min · jiezi

Whats-New-in-TiDB-300rc1

作者:段兵 2019 年 5 月 10 日,TiDB 3.0.0-rc.1 版本正式推出,该版本对系统稳定性,性能,安全性,易用性等做了较多的改进,接下来逐一介绍。 提升系统稳定性众所周知,数据库的查询计划的稳定性至关重要,此版本采用多种优化手段促进查询计划的稳定性得到进一步提升,如下: 新增 Fast Analyze 功能,使 TiDB 收集统计信息的速度有了数量级的提升,对集群资源的消耗和生产业务的影响比普通 Analyze 方式更小。新增 Incremental Analyze 功能,对于值单调增的索引能够更加方便和快速地更新其统计信息。在 CM-Sketch 中新增 TopN 的统计信息,缓解因为 CM-Sketch 哈希冲突导致估算偏大的问题,使代价估算更加准确。优化 Cost Model,利用和 RowID 列之间的相关性更加精准的估算谓词的选择率,使得索引选择更加稳定和准确。提升系统性能TableScan,IndexScan,Limit 算子,进一步提升 SQL 执行性能。TiKV 采用Iterator Key Bound Option存储结构减少内存分配及拷贝,RocksDB 的 Column Families 共享 block cache 提升 cache命中率等手段大幅提升性能。TiDB Lightning encode SQL 性能提升 50%,将数据源内容解析成 TiDB 的 types.Datum,减少 encode 过程中多余的解析工作,使得性能得到较大的提升。增强系统安全性RBAC(Role-Based Access Control)基于角色的权限访问控制是商业系统中最常见的权限管理技术之一,通过 RBAC 思想可以构建最简单”用户-角色-权限“的访问权限控制模型。RBAC 中用户与角色关联,权限与角色关联,角色与权限之间一般是多对多的关系统,用户通过成为什么样的角色获取该角色所拥有的权限,达到简化权限管理的目的,通过此版本的迭代 RBAC 功能开发完成,欢迎试用。 提升产品易用性新增 SQL 方式查询慢查询,丰富 TiDB 慢查询日志内容,如:Coprocessor 任务数,平均/最长/90% 执行/等待时间,执行/等待时间最长的 TiKV 地址,简化慢查询定位工作,提升产品易用性。新增系统配置项合法性检查,优化系统监控项等,提升产品易用性。支持对 TableReader、IndexReader 和 IndexLookupReader 算子进行内存追踪控制,对 Query 内存使用统计更加精确,可以更好地检测、处理对内存消耗较大的语句。社区贡献V3.0.0-rc.1 版本的开发过程中,开源社区贡献者给予了我们极大的支持,例如美团的同学负责开发的 SQL Plan Management 特性对于提升产品的易用性有很大的帮助,一点资讯的陈付同学与其他同学一起对 TiKV 线程池进行了重构,提高了性能并降低了延迟,掌门科技的聂殿辉同学实现 TiKV 大量 UDF 函数帮忙 TiKV 完善 Coprocessor 功能,就不再一一列举。在此对各位贡献者表示由衷的感谢。接下来我们会开展更多的专项开发活动以及一系列面向社区的培训课程,希望能对大家了解如何做分布式数据库有帮助。 ...

May 13, 2019 · 1 min · jiezi

游戏合服时如何避免主键冲突

Last-Modified: 2019年5月10日15:23:31 背景滚服类型的游戏常见于 手游、网游(包括H5), 滚服类型游戏的特点(与传统大服架构区别): 单服同时在线游戏人数少(eg. 3000人), 达到上限就开新服以下这部分内容来自: https://www.cnblogs.com/youji...滚服模式是游戏类型,技术架构和急功近利的坑钱策略等因素共同决定的,大服游戏包括绝大部分端游,以及类COC这样类型的游戏。 另外,虽然像英雄联盟,王者荣耀这样的游戏也分服架构,但是这个并不是我理解中的“滚服游戏“,首先他们虽然分服,但是每个服的人数上限也是可以高达几十万,他们并不会发生频繁的合服情况。 而滚服游戏更多是通过游戏策略设计,鼓励玩家花钱走捷径透支游戏生命周期,甚至几天即可独霸一个服务器。从而导致其他玩家望尘莫及,即使是花钱追也性价比极低,还不如进入一个新服重新开始。 这就导致了新服一开,玩家即蜂拥而至,争先恐后练级升装备,以求最快速进入排行榜前列,如果努力一番发现落后了,可能就只能坐等下一个新服。这也导致了新服人数火爆,老服慢慢变成人烟凋零的村服,甚至没人的死服。 为了能够节约服务器带宽资源,同时让少数剩余的玩家能够玩得起来,就必须要要进行频繁的合服,把若干个互不相干的服务器玩家,合并到一个服里面;这样又开启一波玩家竞争和收割。 合服处理合服时要特别注意: 防止主键冲突防止唯一(unique)键不冲突 (eg. 用户昵称)清空僵尸数据/无效玩家数据(小心数据残留, 避免数据不一致)insert into 时注意字段顺序不一致问题处理主键冲突的办法主要有2种: 合服前预处理冲突键开服时预分配好可能的冲突键, 合服时则无需额外处理(推荐)防止主键冲突合服时处理冲突如果在一开始没有设计好数据库的话, 合服时很容易遇到的普遍情况就是: 主键冲突 游戏通常有角色表, 道具表, 一般都是用数据库的自增长(AUTO_INCREMENT)特定来创建其主键, 以此保证主键的唯一,以如下表结构为例,id 只能保证在本服中唯一,A服中有个玩家id是1, B服中也有个玩家id是1, 合服前必须解决这个冲突. -- 玩家表CREATE TABLE `users`( `id` int(11) unsigned not null, `name` varchar(50) default null, primary key (`id`)) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;-- 玩家道具表CREATE TABLE `props`( `id` int(11) unsigned not null, `user_id` int(11) unsigned not null, primary key (`id`)) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;通常做法是给 B表中的所有 users.id 字段值加上一个基数(max('A.users.id'), 同时还要修改涉及到的其他表, 比如 props.user_id 字段必须相应修改, 否则无法关联到对应玩家,因为修改了 users.id 后一般要修改相应的数十张表中的 user_id, 同时若使用了外键还得额外处理外键的删除和重做. ...

May 10, 2019 · 1 min · jiezi

在线数据库关系图设计工具-dbdiagramio

前段时间,笔者在设计某个系统模块的时候,需要增加十几张表。 为了简单快速地把这十几张表设计并定义出来,我找到了一个可以在线设计数据库关系图(database relationship diagram)且可以导出DDL SQL的工具——dbdiagram.io。 dbdiagram.io是holistics.io这款商业产品的社区版。 dbdiagram.io使用DSL语言,可以简单快速地创建数据库关系图。 这款工具的操作界面也非常简约并具有设计感: 有时候我们需要在关系型数据库中设计一些表,以便实现我们的业务功能。或者我们对某个系统的表结构不是很熟悉,希望画个图表示一下这些实体之间的关系。又或者我们希望把设计好的数据库关系图直接转化为DDL SQL。而且我们不想使用复杂的工具,付出高昂的学习成本。也不想用太重的工具,占用内存。这个时候这个在线的数据库关系图工具就排上用场了。 语法下面介绍一下它的语法。 定义表的语法如下: Table users { id integer [pk] username varchar [not null, unique] full_name type [not null] .....}如果表名太长还支持取别名: Table longtablename as t_alias { .....}定义外键支持如下三种关系: < : One-to-many> : Many-to-one- : One-to-one并且提供了3种定义外键的方式: Ref name-optional { table1.field1 < table2.field2}Ref name-optional: t1.f1 < t2.f2Table posts { id integer [pk, ref: < comments.post_id] user_id integer [ref: > users.id]}例子下面以电商系统常用的几张表作为例子演示一下它的用法。 当你登录自己的Google账号以后,可以把你设计好的图形保存到线上,这样就可以通过一个唯一的链接访问 : https://dbdiagram.io/d/5cc910...。 ...

May 1, 2019 · 1 min · jiezi

TiDB 3.0.0 Beta.1 Release Notes

2019 年 03 月 26 日,TiDB 发布 3.0.0 Beta.1 版,对应的 TiDB-Ansible 版本为 3.0.0 Beta。相比 3.0.0 Beta 版本,该版本对系统稳定性、易用性、功能、优化器、统计信息以及执行引擎做了很多改进。TiDBSQL 优化器支持使用 Sort Merge Join 计算笛卡尔积支持 Skyline Pruning,用一些规则来防止执行计划过于依赖统计信息支持 Window FunctionsNTILELEAD 和 LAGPERCENT_RANKNTH_VALUECUME_DISTFIRST_VALUE 和 LAST_VALUERANK 和 DENSE_RANKRANGE FRAMEDROW FRAMEDROW NUMBER增加了一类统计信息,表示列和 handle 列之间顺序的相关性SQL 执行引擎增加内建函数JSON_QUOTEJSON_ARRAY_APPENDJSON_MERGE_PRESERVEBENCHMARKCOALESCENAME_CONST根据查询上下文优化 Chunk 大小,降低 SQL 执行时间和集群的资源消耗权限管理支持 SET ROLE 和 CURRENT_ROLE支持 DROP ROLE支持 CREATE ROLEServer新增 /debug/zip HTTP 接口,获取当前 TiDB 实例的信息支持使用 show pump status/show drainer status 语句查看 Pump/Drainer 状态支持使用 SQL 语句在线修改 Pump/Drainer 状态支持给 SQL 文本加上 HASH 指纹,方便追查慢 SQL新增 log_bin 系统变量,默认:0,管理 binlog 开启状态,当前仅支持查看状态支持通过配置文件管理发送 binlog 策略支持通过内存表 INFORMATION_SCHEMA.SLOW_QUERY 查询慢日志将 TiDB 显示的 MySQL Version 从 5.7.10 变更为 5.7.25统一日志格式规范,利于工具收集分析增加监控项 high_error_rate_feedback_total,记录实际数据量与统计信息估算数据量差距情况新增 Database 维度的 QPS 监控项 , 可以通过配置项开启DDL增加ddl_error_count_limit全局变量,默认值:512,限制 DDL 任务重试次数,超过限制次数会取消出错的 DDL支持 ALTER ALGORITHM INPLACE/INSTANT支持 SHOW CREATE VIEW 语句支持 SHOW CREATE USER 语句PD统一日志格式规范,利于工具收集分析模拟器支持不同 store 可采用不同的心跳间隔时间添加导入数据的场景热点调度可配置化增加 store 地址为维度的监控项,代替原有的 Store ID优化 GetStores 开销,加快 Region 巡检周期新增删除 Tombstone Store 的接口TiKV优化 Coprocessor 计算执行框架,完成 TableScan 算子,单 TableScan 即扫表操作性能提升 5% ~ 30%实现行 BatchRows 和列 BatchColumn 的定义- 实现 VectorLike 使得编码和解码的数据能够用统一的方式访问- 定义 BatchExecutor 接口,实现将请求转化为 BatchExecutor 的方法 - 实现将表达式树转化成 RPN 格式 - TableScan 算子实现为 Batch 方式,通过向量化计算加速计算统一日志格式规范,利于工具收集分析支持 Raw Read 接口使用 Local Reader 进行读新增配置信息的 Metrics新增 Key 越界的 Metrics新增碰到扫越界错误时 Panic 或者报错选项增加 Insert 语义,只有在 Key 不存在的时候 Prewrite 才成功,消除 Batch GetBatch System 使用更加公平的 batch 策略tikv-ctl 支持 Raw scanToolsTiDB-Binlog新增 Arbiter 工具支持从 Kafka 读取 binlog 同步到 MySQLReparo 支持过滤不需要同步的文件支持同步 generated columnLightning支持禁用 TiKV periodic Level-1 compaction,当 TiKV 集群为 2.1.4 或更高时,在导入模式下会自动执行 Level-1 compaction根据 table_concurrency 配置项限制 import engines 数量,默认值:16,防止过多占用 importer 磁盘空间支持保存中间状态的 SST 到磁盘,减少内存使用优化 TiKV-Importer 导入性能,支持将大表的数据和索引分离导入支持 CSV 文件导入数据同步对比工具 (sync-diff-inspector)支持使用 TiDB 统计信息来划分对比的 chunk支持使用多个 column 来划分对比的 chunkAnsibleN/A ...

March 27, 2019 · 2 min · jiezi

skiplist跳表--一种高性能数据结构

skiplist简介skip List是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)(大多数情况下),因为其性能匹敌红黑树且实现较为简单,因此在很多著名项目都用跳表来代替红黑树,例如LevelDB、Reddis的底层存储结构就是用的SkipList。目前常用的key-value数据结构有三种:Hash表、红黑树、SkipList,它们各自有着不同的优缺点:Hash表:插入、查找最快,为O(1);如使用链表实现则可实现无锁;数据有序化需要显式的排序操作。红黑树:插入、查找为O(logn),但常数项较小;无锁实现的复杂性很高,一般需要加锁;数据天然有序。SkipList:插入、查找为O(logn),但常数项比红黑树要大;底层结构为链表,可无锁实现;数据天然有序。SkipList基本数据结构及其实现一个跳表,应该具有以下特征:1、一个跳表应该有几个层(level)组成;通常是10-20层,leveldb中默认为12层。2、跳表的第0层包含所有的元素;且节点值是有序的。3、每一层都是一个有序的链表;层数越高应越稀疏,这样在高层次中能’跳过’许多的不符合条件的数据。4、如果元素x出现在第i层,则所有比i小的层都包含x;5、每个节点包含key及其对应的value和一个指向该节点第n层的下个节点的指针数组x->next[level]表示第level层的x的下一个节点skiplist的查询过程查询的第一个比vx大的节点的前一个值,看是否相等。相等则存在,否则查找下一层,直到层数为0。以已有数据13、22、75、80、99为例从最高层(此处为2)开始1、level2找到结点Node75小于80,且level2.Node75->next 大于80,则进入level1查找(此处已经跳过了13~75中间的结点(22), 2、level1.Node75 < 80 < level1.Node75->next,进入level03、level0.Node75->next 等于80,找到结点skiplist的插入过程假设插入一新键值key,值为84,level为当前层1、从最高层开始找到每一层比84大的节点的前一个值,存入prev[level]。这里prev[2] = leve2.Node75prev[1] = leve1.Node75prev[0] = level0.Node802、初始化一个新的节点843、为x随机选择一个高度h,这里选24、x->next[0..h-1] = prev[0..h-1]->next5、prev[0..h-1]->next[0..h-1] = x(步骤4、5为链表插入结点的操作)skiplist删除操作删除操作类似于插入操作,包含如下3步:1、查找到需要删除的结点 2、删除结点 3、调整指针总结如果要实现一个key-value结构,需求的功能有插入、查找、迭代、修改,那么首先Hash表就不是很适合了,因为迭代的时间复杂度比较高;而红黑树的插入很可能会涉及多个结点的旋转、变色操作,因此需要在外层加锁,这无形中降低了它可能的并发度。而SkipList底层是用链表实现的,可以实现为lock free,同时它还有着不错的性能(单线程下只比红黑树略慢),非常适合用来实现我们需求的那种key-value结构。

March 10, 2019 · 1 min · jiezi

字典与哈希表 | 自己实现Redis源代码(3)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的字典及其内部哈希表的实现。字典dict的实现dict的API(1)创建一个新的字典dict dictCreate(dictType type,int hashSize);(2)根据key寻找其在hashTable中对应的结点dictEntry lookup(dict d,void *key);(3)将给定的键值对添加到字典里面(如果键已经存在于字典,那么用新值取代原有的值)bool dictInsert(dict d, void key, void *val);(4)返回给定的键的值void dictFetchValue(dict d, void *key);(5)从字典中删除给定键所对应的键值对void dictDelete(dict d, void key);(6)释放给定字典,以及字典中包含的所有键值对void dictRelease(dict *d);头文件#ifndef __DICT_H#define __DICT_H//哈希表的结点使用dictEntry结构来表示//每个dictEntry结构都保存着一个key-valuetypedef struct dictEntry{ //键 void *key; //值 void *value; //指向下个哈希表结点,形成链表——避免键冲突 struct dictEntry *next;}dictEntry;//保存一组用于操作特定类型键值对的函数typedef struct dictType { //计算哈希值的函数 unsigned int (*hashFunction)(void *key,int size); //复制键的函数 void *(*keyDup)(void *key); //复制值的函数 void *(*valDup)(void *obj); //对比键的函数 int (*keyCompare)(void *key1, void *key2); //销毁键的函数 void (*keyDestructor)(void *key); //销毁值的函数 void (*valDestructor)(void *obj);} dictType;//哈希表typedef struct dictht { //哈希表数组 dictEntry **table; //哈希表大小 int size; //该哈希表已有结点的数量 int used;} dictht;//字典//其实字典就是对普通的哈希表再做一层封装//增加了一些属性typedef struct dict { //类型特定函数 dictType *type; //哈希表 dictht *ht;} dict;//创建一个新的字典//需要传入哈希表的大小dict *dictCreate(dictType type,int hashSize);//根据key寻找其在hashTable中对应的结点dictEntry lookup(dict *d,void *key);//将给定的键值对添加到字典里面//将给定的键值对添加到字典里面,如果键已经存在于字典,//那么用新值取代原有的值bool dictInsert(dict d, void key, void val);//返回给定的键的值void dictFetchValue(dict d, void key);//从字典中删除给定键所对应的键值对void dictDelete(dict d, void key);//释放给定字典,以及字典中包含的所有键值对void dictRelease(dict d);#endifdict API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “dict.h”//哈希表的大小#define HASHSIZE 10//定义对哈希表进行相关操作的函数集//计算哈希值的函数unsigned int myHashFunction(void key,int size){ char charkey=(char)key; unsigned int hash=0; for(;charkey;++charkey){ hash=hash33+charkey; } return hash%size;}//复制键的函数void myKeyDup(void key){ return key;}//复制值的函数void myValDup(void obj){ return obj;}//对比键的函数int myKeyCompare(void key1, void key2){ charcharkey1=(char)key1; charcharkey2=(char)key2; return strcmp(charkey1,charkey2);}//销毁键的函数void myKeyDestructor(void key){ //free(key);}//销毁值的函数void myValDestructor(void obj){ //free(obj);}//创建一个新的字典dict dictCreate(dictType type,int hashSize){ dict d=(dict)malloc(sizeof(dict)); //对hashTable进行相关操作的特定函数集 if(type==NULL){ printf(“PIG Redis WARNING : Type is NULL.\n”); } d->type=type; //哈希表初始化 d->ht=(dictht)malloc(sizeof(dictht)); d->ht->size=hashSize; d->ht->used=0; d->ht->table=(dictEntry)malloc(sizeof(dictEntry)hashSize); //全部结点都设为NULL for(int i=0;i<hashSize;i++){ d->ht->table[i]=NULL; } return d;}//根据key寻找其在hashTable中对应的结点dictEntry lookup(dict d,void key){ dictEntry node; //该key在hashTable中对应的下标 unsigned int index; index=d->type->hashFunction(key,d->ht->size); for(node=d->ht->table[index];node;node=node->next){ if(!(d->type->keyCompare(key,node->key))){ return node; } } return NULL;}//将给定的键值对添加到字典里面bool dictInsert(dict d, void key, void val){ unsigned int index; dictEntry node; if(!(node=lookup(d,key))){ index=d->type->hashFunction(key,d->ht->size); node=(dictEntry)malloc(sizeof(dictEntry)); if(!node)return false; node->key=d->type->keyDup(key); node->next=d->ht->table[index]; d->ht->table[index]=node; } //若存在,直接修改其对应的value值 node->value=d->type->valDup(val); return true;}//返回给定的键的值void dictFetchValue(dict d, void key){ unsigned int index; dictEntry node; //找不到这个结点 if(!(node=lookup(d,key))){ return NULL; } return node->value;}//从字典中删除给定键所对应的键值对void dictDelete(dict d, void key){ dictEntry node; dictEntry temp; //该key在hashTable中对应的下标 unsigned int index; index=d->type->hashFunction(key,d->ht->size); node=d->ht->table[index]; //key相同 if(!(d->type->keyCompare(key,node->key))){ d->ht->table[index]=node->next; d->type->keyDestructor(node->key); d->type->valDestructor(node->value); free(node); return; } temp=node; node=node->next; while(node){ if(!(d->type->keyCompare(key,node->key))){ temp->next=node->next; d->type->keyDestructor(node->key); d->type->valDestructor(node->value); free(node); return; } temp=node; node=node->next; } return;}//释放给定字典,以及字典中包含的所有键值对void dictRelease(dict d){ dictEntry node; dictEntry temp; for(int i=0;i<d->ht->size;i++){ node=d->ht->table[i]; //printf("%d\n",i); while(node){ char t=(char)node->value; //printf("%s\n",t); temp=node; node=node->next; d->type->keyDestructor(temp->key); d->type->valDestructor(temp->value); free(temp); } } free(d->ht); free(d->type); free(d);}/int main(){ dictTypetype=(dictType)malloc(sizeof(dictType)); type->hashFunction=myHashFunction; type->keyDup=myKeyDup; type->valDup=myValDup; type->keyCompare=myKeyCompare; type->keyDestructor=myKeyDestructor; type->valDestructor=myValDestructor; dict d=dictCreate(type,HASHSIZE); charkey1=“sss”; charvalue1=“111”; bool result=dictInsert(d,key1,value1); if(result){ printf(“insert1 success\n”); }else{ printf(“insert1 fail\n”); } charkey2=“3sd”; charvalue2=“ddd”; result=dictInsert(d,key2,value2); if(result){ printf(“insert2 success\n”); }else{ printf(“insert2 fail\n”); } charkey3=“ddds”; charvalue3=“1ss”; result=dictInsert(d,key3,value3); if(result){ printf(“insert3 success\n”); }else{ printf(“insert3 fail\n”); } char value4=(char)dictFetchValue(d,key3); printf("—%s\n",value4); dictDelete(d,key3); value4=(char)dictFetchValue(d,key3); printf("—%s\n",value4); dictRelease(d); system(“pause”); return 0;}/ ...

March 9, 2019 · 2 min · jiezi

双端链表list的实现 | 自己实现Redis源代码(2)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的双端链表list的实现。双端链表list的实现list的API(1)创建一个不包含任何结点的新链表list *listCreate(void);(2)释放给定链表,以及链表中的所有结点void listRelease(list *l);(3)将一个包含给定值的新节点添加到给定链表的表头list listAddNodeHead(list l, void *value);(4)将一个包含给定值的新节点添加到给定链表的表尾list listAddNodeTail(list l, void *value);(5)将一个包含给定值的新节点添加到给定结点的之前或之后list listInsertNode(list l, listNode old_node, void value, int after);(6)从链表中删除给定结点list listDelNode(list l, listNode *node);(7)复制一个给定链表的副本list listDup(list orig);(8)查找并返回链表中包含给定值的结点listNode listSearchKey(list l, void *key);(9)返回链表在给定索引上的结点listNode listIndex(list l, long index);(10)将链表结点的表位结点弹出,然后将被弹出的结点插入到链表的表头,成为新的表头结点void listRotate(list *l);头文件#ifndef ADLIST_H#define ADLIST_H//双端链表//双端链表的结点typedef struct listNode{ struct listNode *prev;//指向点一个结点的指针 struct listNode *next;//指向下一个结点的指针 void *value;//结点存放的值}listNode;//链表typedef struct list{ listNode *head;//头结点 listNode *tail;//尾结点 int len;//链表的长度 //用于实现多态链表所需的类型的特定函数 //函数指针 //用于复制链表结点所保存的值 void *(*dup)(void *ptr); //用于释放链表结点所保存的值 void (*free)(void *ptr); //用于对比 int (*match)(void *ptr, void *key);}list;//定义对链表进行操作的宏//获取链表长度#define listLength(l) ((l)->len)//获取链表的头结点#define listFirst(l) ((l)->head)//获取链表的尾结点#define listLast(l) ((l)->tail)//获取前一个结点#define listPrevNode(n) ((n)->prev)//获取下一个结点#define listNextNode(n) ((n)->next)//获取该结点的值#define listNodeValue(n) ((n)->value)//设置复制操作的函数指针#define listSetDupMethod(l,m) ((l)->dup = (m))//设置释放操作的函数指针#define listSetFreeMethod(l,m) ((l)->free = (m))//设置对比操作的函数指针#define listSetMatchMethod(l,m) ((l)->match = (m))//获取复制链表结点的函数指针#define listGetDupMethod(l) ((l)->dup)//获取释放链表结点的函数指针#define listGetFree(l) ((l)->free)//获取比较链表结点的函数指针#define listGetMatchMethod(l) ((l)->match)//创建一个不包含任何结点的新链表list *listCreate(void);//释放给定链表,以及链表中的所有结点void listRelease(list *l);//将一个包含给定值的新节点添加到给定链表的表头list *listAddNodeHead(list *l, void *value);//将一个包含给定值的新节点添加到给定链表的表尾list *listAddNodeTail(list *l, void *value);//将一个包含给定值的新节点添加到给定结点的之前或之后list *listInsertNode(list *l, listNode *old_node, void *value, int after);//从链表中删除给定结点list *listDelNode(list *l, listNode *node);//复制一个给定链表的副本list *listDup(list *orig);//查找并返回链表中包含给定值的结点listNode *listSearchKey(list *l, void *key);//返回链表在给定索引上的结点listNode listIndex(list l, long index);//将链表结点的表位结点弹出,然后将被弹出的结点插//入到链表的表头,成为新的表头结点void listRotate(list l);#endiflist API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “adlist.h”//创建一个不包含任何结点的新链表list listCreate(void){ list l=(list)malloc(sizeof(list)); //没有结点 l->head=NULL; l->tail=NULL; l->len=0; l->dup=NULL; l->free=NULL; l->match=NULL; return l;}//释放给定链表,以及链表中的所有结点void listRelease(list l){ if(l==NULL){ return ; } //没有head(没有结点) if(l->head==NULL){ return ; } //保证了链表是有结点存在的 //用来移动的指针,指向第一个结点 listNodetemp=l->head; while(temp->next!=NULL){ temp=temp->next; //使用链表对应释放value的free来释放value的值 if(l->free!=NULL){ l->free(temp->value); }else{ printf(“PIG Redis WARNING : List->free is not define.\n”); } free(temp->prev); } free(temp); l->head=NULL; l->tail=NULL; free(l); l=NULL; return;}//将一个包含给定值的新节点添加到给定链表的表头list listAddNodeHead(list l, void value){ if(l==NULL){ printf(“PIG Redis ERROR : List NULL.\n”); return NULL; } //链表中没有结点 if(l->head==NULL){ l->head=(listNode)malloc(sizeof(listNode)); l->head->next=NULL; l->head->prev=NULL; //使用链表对应复制value的dup来复制value的值 if(l->dup!=NULL){ l->head->value=l->dup(value); }else{ printf(“PIG Redis WARNING : List->dup is not define.\n”); l->head->value=value; }/ int c=(int)(l->head->value); printf("%d====\n",c);/ l->len=1; //头尾指针都指向新的结点 l->tail=l->head; return l; }else{ listNodenewone=(listNode)malloc(sizeof(listNode)); //newone->value=value; //使用链表对应复制value的dup来复制value的值 if(l->dup!=NULL){ newone->value=l->dup(value); }else{ printf(“PIG Redis WARNING : List->dup is not define.\n”); newone->value=value; }/ int cc=(int)(newone->value); printf("%d====\n",cc);/ newone->next=l->head; l->head->prev=newone; //新节点设为头结点 l->head=newone; newone->prev=NULL; l->len++; return l; }}//将一个包含给定值的新节点添加到给定链表的表尾list listAddNodeTail(list l, void value){ if(l==NULL){ printf(“PIG Redis ERROR : List NULL.\n”); return NULL; } //链表中没有结点 if(l->head==NULL){ l->head=(listNode)malloc(sizeof(listNode)); //l->head->value=value; //使用链表对应复制value的dup来复制value的值 if(l->dup!=NULL){ l->head->value=l->dup(value); }else{ printf(“PIG Redis WARNING : List->dup is not define.\n”); l->head->value=value; } l->head->next=NULL; l->head->prev=NULL; l->tail=l->head; l->len=1; return l; }else{ listNodetemp=(listNode)malloc(sizeof(listNode)); //temp->value=value; //使用链表对应复制value的dup来复制value的值 if(l->dup!=NULL){ temp->value=l->dup(value); }else{ printf(“PIG Redis WARNING : List->dup is not define.\n”); temp->value=value; } temp->next=NULL; temp->prev=l->tail; l->tail->next=temp; l->tail=temp; l->len++; return l; }}//将一个包含给定值的新节点添加到给定结点的之前或之后//after为1表示之后,after为0表示之前list *listInsertNode(list *l, listNode *old_node, void *value, int after){ listNode newone=(listNode)malloc(sizeof(listNode)); //newone->value=value; //使用链表对应复制value的dup来复制value的值 if(l->dup!=NULL){ newone->value=l->dup(value); }else{ printf(“PIG Redis WARNING : List->dup is not define.\n”); newone->value=value; } l->len++; if(after){ newone->next=old_node->next; newone->prev=old_node; old_node->next->prev=newone; old_node->next=newone; //检查原来的temp是否为tail if(l->tail==old_node){ l->tail=newone; } return l; }else{ newone->next=old_node; newone->prev=old_node->prev; old_node->prev->next=newone; old_node->prev=newone; //检查原来的temp是否为头结点 if(l->head==old_node){ l->head=newone; } return l; }} //从链表中删除给定结点list *listDelNode(list *l, listNode node){ l->len–; //使用链表对应释放value的free来释放value的值 if(l->free!=NULL){ l->free(node->value); }else{ printf(“PIG Redis WARNING : List->free is not define.\n”); } //要删除的是最后一个结点 if(l->head==node&&l->tail==node){ free(node); l->head=NULL; l->tail=NULL; return l; }else if(l->head==node){ printf(“head\n”); l->head=node->next; l->head->prev=NULL; free(node); return l; }else if(l->tail==node){ l->tail=node->prev; l->tail->next=NULL; free(node); return l; } node->prev->next=node->next; node->next->prev=node->prev; free(node); return l;}//复制一个给定链表的副本list listDup(list orig){ if(orig==NULL){ return NULL; } //该链表没有结点 if(orig->head==NULL){ listl=listCreate(); return l; }else{ listl=listCreate(); listNodetemp=orig->head; while(temp!=NULL){ //向表尾插入 l=listAddNodeTail(l,temp->value); temp=temp->next; } return l; }}//查找并返回链表中包含给定值的结点listNode *listSearchKey(list *l, void key){ if(l==NULL){ printf(“PIG Redis ERROR : List NULL.\n”); return NULL; //链表中没有结点 }else if(l->head==NULL){ printf(“PIG Redis ERROR : List does’t have nodes.\n”); return NULL; }else{ listNodetemp=l->head; //检查是否定义了比较value的函数 if(l->match==NULL){ printf(“PIG Redis ERROR : List->match is not define.\n”); return NULL; } //match函数当两者相等时返回1 while(temp!=NULL&&!(l->match(key,temp->value))){ temp=temp->next; } if(temp==NULL){ printf(“PIG Redis ERROR : List doesn’t have this node.\n”); return NULL; }else{ return temp; } }} //返回链表在给定索引上的结点,index从0开始listNode *listIndex(list l, long index){ if(l==NULL){ printf(“PIG Redis ERROR : List NULL.\n”); return NULL; }else if(l->head==NULL){ printf(“PIG Redis ERROR : List doesn’t have node.\n”); return NULL; } listNodetemp=l->head; for(int i=0;i<index&&temp!=NULL;i++){ temp=temp->next; } if(temp==NULL){ printf(“PIG Redis ERROR : Subscript out of range.\n”); return NULL; } return temp;}//将链表结点的表尾结点弹出,然后将被弹出的结点插//入到链表的表头,成为新的表头结点void listRotate(list l){ if(l==NULL){ printf(“PIG Redis ERROR : List NULL.\n”); return ; }else if(l->head==NULL){ printf(“PIG Redis ERROR : List doesn’t have node.\n”); return ; }else if(l->head==l->tail){ printf(“PIG Redis ERROR : List only have one node.\n”); return ; } listNodenode=l->tail->prev; l->tail->prev->next=NULL; l->tail->next=l->head; l->head->prev=l->tail; l->head=l->tail; l->tail=node; l->head->prev=NULL;}int intMatch(void *ptr, void *key){ int *a=(int *)ptr; int *b=(int *)key; return (*a==*b)?1:0;}void *intDup(void ptr){ return ptr;}int main(){ printf(“listCreate()\n”); listl=listCreate(); printf("%d\n",l->len); listSetDupMethod(l,&intDup); int b=111; int a=&b; l=listAddNodeHead(l,a); printf("%d\n",l->len); //使用void指针的时候需要强制转换 int *c=(int *)(l->head->value); printf("%d\n",*c); int bb=12; int aa=&bb; l=listAddNodeHead(l,aa); //listInsertNode(l,l->head,a,1); //l=listAddNodeTail(l,aa); //printf("%d\n",l->len); //l=listDelNode(l,l->head); //l=listDelNode(l,l->tail); //printf("%d\n",l->len); listRotate(l); //使用void指针的时候需要强制转换 int cc=NULL; listNodetemp=l->tail; while(temp){ cc=(int )(temp->value); printf("%d\n",cc); temp=temp->prev; } / listl2=listDup(l); temp=l2->tail; while(temp){ cc=(int )(temp->value); printf("%d\n",cc); temp=temp->prev; }/ //listSetMatchMethod(l,&intMatch); listNodenode=listIndex(l,1); int zhu=(int)node->value; printf("*zhu:%d\n",*zhu); listRelease(l); //listRelease(l2); system(“pause”); return 0;} ...

March 7, 2019 · 4 min · jiezi

动态字符串SDS的实现 | 自己实现Redis源代码(1)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的动态字符串SDS的实现。动态字符串SDS的实现SDS的API(1)创建一个包含给定c字符串的sdssds sdsnew(char *);(2)为sds(也就是buf数组)分配指定空间sds sdsnewlen(sds,int);(3)创建一个不包含任何内容的空字符串sds sdsempty(void);(4)释放给定的sdsvoid sdsfree(sds);(5)创建一个给定sds的副本sds sdsdup(sds);(6)清空sds保存的字符串内容sds sdsclear(sds);(7)将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char *);(8)将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);(9)将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char *);(10)保留sds给定区间内的数据sds sdsrange(sds,int,int);(11)从sds中移除所有在c字符串中出现过的字符sds sdstrim(sds,const char );(12)对比两个sds字符串是否相同bool sdscmp(sds,sds);头文件#ifndef SDS_H#define SDS_H//实现Redis中的动态字符串//SDS:simple dynamic stringtypedef struct sdshdr{ //记录buf数组中已使用字节的数量 //等于SDS所保存字符串的长度,不 //包括最后的’\0’; int len; //记录buf数组中未使用字节的数量 int free; //字节数组,用于保存字符串,以 //’\0’结束 char buf;}*sds;//返回sds已使用空间的字节数:lenstatic inline int sdslen(const sds sh){ return sh->len;}//返回sds未使用空间的字节数:freestatic inline int sdsavail(const sds sh){ return sh->free;}//创建一个包含给定c字符串的sdssds sdsnew(char *);//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds,int);//创建一个不包含任何内容的空字符串sds sdsempty(void);//释放给定的sdsvoid sdsfree(sds);//创建一个给定sds的副本sds sdsdup(sds);//清空sds保存的字符串内容sds sdsclear(sds);//将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char );//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);//将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char );//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds,int,int);//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//大小写不敏感sds sdstrim(sds,const char );//对比两个sds字符串是否相同bool sdscmp(sds,sds);#endifSDS API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “sds.h”//创建一个包含给定c字符串的sdssds sdsnew(char init){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=strlen(init); sh->free=0; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); //将字符串内容进行复制 int i; for(i=0;i<sh->len;i++){ (sh->buf)[i]=init[i]; } (sh->buf)[i]=’\0’; return sh;}//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds sh,int len){ int i; sh->free=len-1-sh->len; //保存之前的buf内容 char str=(char )malloc(sizeof(char)(sh->len+1)); for(i=0; i<(sh->len); i++){ str[i]=sh->buf[i]; } str[i]=’\0’; //sh->buf=(char)realloc(sh->buf,len); sh->buf=(char)malloc(sizeof(char)len); for(i=0; i<(sh->len); i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//创建一个不包含任何内容的空字符串sds sdsempty(void){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=0; sh->free=0; sh->buf=(char)malloc(sizeof(char)); sh->buf[0]=’\0’; return sh;}//释放给定的sdsvoid sdsfree(sds sh){ (sh)->free=0; (sh)->len=0; free((sh)->buf); free(sh);}//创建一个给定sds的副本sds sdsdup(sds sh01){ sds sh02=(sds)malloc(sizeof(struct sdshdr)); sh02->free=sh01->free; sh02->len=sh01->len; sh02->buf=(char)malloc(sizeof(char)(sh02->free+sh02->len+1)); int i; for(i=0;i<sh01->len;i++){ sh02->buf[i]=sh01->buf[i]; } sh02->buf[i]=’\0’; return sh02;}//清空sds保存的字符串内容sds sdsclear(sds sh){ int total=sh->len+sh->free+1; sh->len=0; sh->free=total-1; sh->buf[0]=’\0’; return sh;}//将给定c字符串拼接到另一个sds字符串的末尾//先检查sds的空间是否满足修改所需的要求,如//果不满足则自动将sds空间扩展至执行修改所需//要的大小,然后在执行实际的修改操作——防止//缓冲区溢出//扩展空间的原则:拼接后的字符串是n个字节,则//再给其分配n个字节的未使用空间,buf数组的实际长度为n+n+1//当n超过1MB的时候,则为其分配1MB的未使用空间//两个字符串cat,中间使用空格隔开sds sdscat(sds sh,char str){ int newlen=strlen(str); int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds sh,sds str){ int newlen=str->len; int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str->buf[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定的c字符串复制到sds里面,覆盖原有的字符串//需要先检查sds sdscpy(sds sh,char str){ //新来的长度 int len=strlen(str); //需要使用到的新空间长度 int newlen=len-sh->len; int total; //剩余的空间不够了需要重新分配,在copy if(newlen>=sh->free){ //新空间长度大于1M,就只多分配newlen+1M+1 //总的空间是len+newlen+1M+1 if(newlen>=1024){ total=len+newlen+1024+1; //copy后使用到的len,就是新字符串的长度 sh->len=len; //空闲的空间长度 //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); //分配newlen+newlen+1 }else{ total=len+newlen+newlen+1; sh->len=len; //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); } if(sh->buf==NULL){ printf(“PIG Redis ERROR : Realloc failed.\n”); } }else{ //剩余的空间够,不需要分配 //原来拥有的总空间 total=sh->len+sh->free; sh->len=len; sh->free=total-sh->len; } //开始copy int i; for(i=0;i<len;i++){ (sh->buf)[i]=str[i]; } sh->buf[i]=’\0’; return sh;}//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds sh,int start,int end){ int newlen=end-start+1; char str=(char)malloc(sizeof(char)(sh->len+1)); //sh1->free=sh->len-sh1->len; int i,j; for(i=start,j=0;i<=end;i++,j++){ str[j]=sh->buf[i]; } str[j]=’\0’; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); sh->free=sh->len-newlen; sh->len=newlen; for(i=0;i<strlen(str);i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//截断操作需要通过内存重分配来释放字符串中不再使用的空间,否则会造成内存泄漏//大小写不敏感//使用惰性空间释放优化字符串的缩短操作,执行缩短操作的时候,不立即使用内存重分//配来回收缩短后多出来的字节,而是使用free属性记录这些字节,等待将来使用sds sdstrim(sds s,const char chstr);//对比两个sds字符串是否相同bool sdscmp(sds sh1,sds sh2){ if(sh1->len!=sh2->len){ return false; } for(int i=0;i<sh1->len;i++){ if(sh1->buf[i]!=sh2->buf[i]){ return false; } } return true;}int main(){ printf(“sdsnew(‘sss’)\n”); sds sh=sdsnew(“sss”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdscat(sh,‘www’)\n”); sh=sdscat(sh,“www”); printf("%s\n",sh->buf); /for(int i=0;i<sh->len;i++){ printf("%c",sh->buf[i]); }/ printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh1=sdsnew(“qqqq”); sh=sdscatsds(sh,sh1); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sh=sdsrange(sh,1,5); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh3=sdsnew(“qqqq”); sds sh4=sdsnew(“qqqq”); if(sdscmp(sh3,sh4)){ printf(“same\n”); }else{ printf(“no same\n”); }/ printf(“sdscpy(sh,‘wwww’)\n”); sh=sdscpy(sh,“wwww”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsnewlen(sh,12)\n”); sh=sdsnewlen(sh,12); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsdup(sh)\n”); sds sh1=sdsdup(sh); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free); printf(“sdsclear(sh1)\n”); sh1=sdsclear(sh1); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free);/ sdsfree(&sh); sdsfree(&sh1); //sdsfree(&sh2); sdsfree(&sh3); sdsfree(&sh4); system(“pause”); return 0;} ...

March 5, 2019 · 2 min · jiezi

The Way to TiDB 3.0 and Beyond (下篇)

本文为我司 Engineering VP 申砾在 TiDB DevCon 2019 上的演讲实录。在 上篇 中,申砾老师重点回顾了 TiDB 2.1 的特性,并分享了我们对「如何做好一个数据库」的看法。本篇将继续介绍 TiDB 3.0 Beta 在稳定性、易用性、功能性上的提升,以及接下来在 Storage Layer 和 SQL Layer 的规划,enjoy~TiDB 3.0 Beta2018 年年底我们开了一次用户吐槽大会,当时我们请了三个 TiDB 的重度用户,都是在生产环境有 10 套以上 TiDB 集群的用户。那次大会规则是大家不能讲 TiDB 的优点,只能讲缺点;研发同学要直面问题,不能辩解,直接提解决方案;当然我们也保护用户的安全(开个玩笑 :D),让他们放心的来吐槽。刚刚的社区实践分享也有点像吐槽大会第二季,我们也希望用户来提问题,分享他们在使用过程遇到什么坑,因为只有直面这些问题,才有可能改进。所以我们在 TiDB 3.0 Beta 中有了很多改进,当然还有一些会在后续版本中去改进。1. Stability at ScaleTiDB 3.0 版本第一个目标就是「更稳定」,特别是在大规模集群、高负载的情况下保持稳定。稳定性压倒一切,如果你不稳定,用户担惊受怕,业务时断时续,后面的功能都是没有用的。所以我们希望「先把事情做对,再做快」。1.1 Multi-thread RaftStore首先来看 TiDB 3.0 一个比较亮眼的功能——多线程 Raft。我来给大家详细解释一下,为什么要做这个事情,为什么我们以前不做这个事情。<center>图 8 TiKV 抽象架构图</center>这是 TiKV 一个抽象的架构(图 8)。中间标红的图形是 RaftStore 模块,所有的 Raft Group 都在一个 TiKV 实例上,所有 Raft 状态机的驱动都是由一个叫做 RaftStore 的线程来做的,这个线程会驱动 Raft 状态机,并且将 Raft Log Append 到磁盘上,剩下的包括发消息给其他 TiKV 节点以及 Apply Raft Log 到状态机里面,都是由其他线程来做的。早期的时候,可能用户的数据量没那么大,或者吞吐表现不大的时候,其实是感知不到的。但是当吞吐量或者数据量大到一定程度,就会感觉到这里其实是一个瓶颈。虽然这个线程做的事情已经足够简单,但是因为 TiKV 上所有的 Raft Peer 都会通过一个线程来驱动自己的 Raft 状态机,所以当压力足够大的时候就会成为瓶颈。用户会看到整个 TiKV 的 CPU 并没有用满,但是为什么吞吐打不上去了?<center>图 9 TiDB 3.0 Multi-thread RaftStore</center>因此在 TiDB 3.0 中做了一个比较大的改进,就是将 RaftStore 这个线程,由一个线程变成一个线程池, TiKV 上所有 Raft Peer 的 Raft 状态机驱动都由线程池来做,这样就能够充分利用 CPU,充分利用多核,在 Region 特别多以及写入量特别大的时候,依然能线性的提升吞吐。<center>图 10 TiDB 3.0 Beta oltp_insert</center>通过上图大家可以看到,随着并发不断加大,写入是能够去线性扩展的。在早期版本中,并发到一定程度的时候,RaftStore 也会成为瓶颈,那么为什么我们之前没有做这个事情?这个优化效果这么明显,之所以之前没有做,是因为之前 Raft 这块很多时候不会成为瓶颈,而在其他地方会成为瓶颈,比如说 RocksDB 的写入或者 gRPC 可能会成为瓶颈,然后我们将 RaftStore 中的功能不断的向外拆,拆到其他线程中,或者是其他线程里面做多线程,做异步等等,随着我们的优化不断深入,用户场景下的数据量、吞吐量不断加大,我们发现 RaftStore 线程已经成为需要优化的一个点,所以我们在 3.0 中做了这个事情。而且之前保持单线程也是因为单线程简单,「先把事情做对,然后再做快」。1.2 Batch Message第二个改进是 Batch Message。我们的组件之间通讯选择了 gRPC,首先是因为 gRPC 是 Google 出品,有人在维护他,第二是用起来很简单,也有很多功能(如流控、加密)可以用。但其实很多人吐嘈它性能比较慢,在知乎上大家也能看到各种问题,包括讨论怎么去优化他,很多人也有各种优化经验,我们也一直想怎么去优化他。以前我们用的方法是来一个 message 就通过 gRPC 发出去,虽然性能可能没有那么好,或者说性能不是他最大的亮点,但有时候调性能不能单从一个模块去考虑,应该从架构上去想,就是架构需要为性能而设计,架构上的改进往往能带来性能的质变。所以我们在 TiDB 3.0 Beta 中设计了 Batch Message 。以前是一个一个消息的发,现在是按照消息的目标分队列,每个队列会有一个 Timer,当消息凑到一定个数,或者是你的 Timer 到了时间(现在应该设置的是 1ms,Batch 和这个 Timer 数量都可以调),才会将发给同一个目的地的一组消息,打成一个包,一起发过去。有了这个架构上的调整之后,我们就获得了性能上的提升。<center>图 11 TiDB 3.0 Beta - Batch Message</center>当然大家会想,会不会在并发比较低的时候变慢了?因为你凑不到足够的消息,那你就要等 Timer。其实是不会的,我们也做了一些设计,就是由对端先汇报「我当前是否忙」,如果对端不忙,那么选择一条一条的发,如果对端忙,那就可以一个 Batch 一个 Batch 的发,这是一个自适应的 Batch Message 的一套系统。图 11 右半部分是一个性能对比图,有了 Batch Message 之后,在高并发情况下吞吐提升非常快,在低并发情况下性能并没有下降。相信这个改进可以给大家带来很大的好处。1.3 Titan第三点改进就是 Titan。CEO 刘奇在 Opening Keynote 中提到了我们新一代存储引擎 Titan,我们计划用 Titan 替换掉 RocksDB,TiDB 3.0 中已经内置了 Titan,但没有默认打开,如果大家想体验的话,可以通过配置文件去把 RocksDB 改成 Titan。我们为什么想改进 RocksDB 呢?是因为它在存储大的 Key Value 的时候,有存储空间放大和写放大严重的问题。<center>图 12 TiDB 3.0 中内置的新存储引擎 Titan</center>所以我们尝试解决这个问题。当你写入的 Key Value 比较大的时候,我们会做一个检查,然后把大的 Value 放到一个 Blob File 里去,而不是放到 LSM-Tree。这样的分开存储会让 LSM-Tree 变得很小,避免了因为 LSM-Tree 比较高的时候,特别是数据量比较大时出现的比较严重的写放大问题。有了 Titan 之后,就可以解决「单个 TiKV 服务大量数据」的需求,因为之前建议 TiKV 一个实例不要高于 1T。我们后面计划单个 TiKV 实例能够支持 2T 甚至 4T 数据,让大家能够节省存储成本,并且能在 Key Value 比较大的时候,依然能获得比较好的性能。除了解决写放大问题之外,其实还有一个好处就是我们可以加一个新的 API,比如 KeyExist,用来检查 Key 是否存在,因为这时 Key 和 Value 是分开存储的,我们只需要检查 Key 是否在,不需要把 Value Load 进去。或者做 Unique Key 检查时,可以不需要把 Key Value 取出来,只需要加个接口,看这个 Key 是否存在就好了,这样能够很好的提升性能。1.4 Robust Access Path Selection第四点是保持查询计划稳定。这个在数据库领域其实是一个非常难的问题,我们依然没有 100% 解决这个问题,希望在 2019 年第一季度,最多到第二季度,能有一个非常好的解决方案。我们不希望当数据量变化 、写入变化、负载变化,查询计划突然变错,这个问题在线上使用过程中是灾难。那么为什么会跑着跑着变错?首先来说我们现在是一个 Cost-based optimizers,我们会参考统计信息和当前的数据的分布,来选择后面的 plan。那么数据的分布是如何获得的呢?我们是通过统计信息,比如直方图、CM Sketch来获取,这里就会出现两个问题:1. 统计信息可能是不准的。统计信息毕竟是一个采样,不是全量数据,会有一些数据压缩,也会有精度上的损失。2. 随着数据不断写入,统计信息可能会落后。因为我们很难 100% 保证统计信息和数据是 Match 的。<center>图 13 查询计划稳定性解决方案</center>一个非常通用的思路是, 除了依赖于 Cost Model 之外,我们还要依赖更多的 Hint,依赖于更多启发式规则去做 Access Path 裁减。举个例子:select * from t where a = x and b = y;idx1(a, b)idx2(b) – pruned大家通过直观印象来看,我们一定会选择第一个索引,而不是第二个索引,那么我们就可以把第二个索引裁掉,而不是因为统计信息落后了,然后估算出第二个索引的代价比较低,然后选择第二个索引。上面就是我们最近在做的一个事情,这里只举了一个简单的例子。2. UsabilityTiDB 3.0 第二个目标是可用性,是让 TiDB 简单易用。2.1 Query Tracing在 TiDB 2.0 中,大家看一个 Query 为什么慢了依赖的是 Explain,就是看查询计划,其实那个时候大家很多都看不懂,有时候看了也不知道哪有问题。后来我们在 TiDB 2.1 中支持了 Explain Analyze,这是从 PG 借鉴过来一个特性,就是我们真正的把它执行一边,然后再看看每个算子的耗时、处理的数据量,看看它到底干了一些什么事情,但其实可能还不够细,因为还没有细化到算子内部的各种操作的耗时。<center>图 14 TiDB 3.0 - Query Tracing</center>所以我们又做了一个叫 Query Tracing 的东西,其实在 TiDB 2.1 之前我们已经做了一部分,在 TiDB 3.0 Beta 中做了一个收尾,就是我们可以将 Explain 结果转成一种 Tracing 格式,再通过图形化界面,把这个 Tracing 的内容展示出来,就可以看到这个算子具体干了一些什么事,每一步的消耗到底在哪里,这样就可以知道哪里有问题了。希望大家都能在 TiDB 3.0 的版本中非常直观的定位到 Query 慢的原因。2.2 Plan Management然后第二点 Plan Management 其实也是为了 Plan 不稳定这个问题做准备的。虽然我们希望数据库能自己 100% 把 Plan 选对,但是这个是非常美好的愿望,应该还没有任何一个数据库能保证自己能 100% 的解决这个问题。那么在以前的版本中,出现问题怎么办?一种是去 Analyze 一下,很多情况下他会变好,或者说你打开自动 Analyze 这个特性,或者自动 FeedBack 这个特性,可以一定程度上变好,但是还可能过一阵统计信息又落后了,又不准了,Plan 又错了,或者由于现在 cost 模型的问题,有一些 Corner Case 处理不到,导致即使统计信息是准确的, Plan 也选不对。<center>图 15 TiDB 3.0 Beta - Plan Management</center>那么我们就需要一个兜底方案,让大家遇到这个问题时不要束手无策。一种方法是让业务去改 SQL,去加 Hint,也是可以解决的,但是跟业务去沟通可能会增加他们的使用成本或者反馈周期很长,也有可能业务本身也不愿意做这个事情。另外一种是用一种在线的方式,让数据库的使用者 DBA 也能非常简单给这个 Plan 加 Hint。具体怎么做呢?我们和美团的同学一起做了一个非常好的特性叫 Plan Management,就是我们有一个 Plan 管理的模块,我们可以通过 SQL 接口给某一条 Query,某一个 Query 绑定 Plan,绑定 Hint,这时我们会对 SQL 做指纹(把 Where 条件中的一些常量变成一个通配符,然后计算出一个 SQL 的指纹),然后把这个 Hint 绑定在指纹上。一条 Query 来了之后,先解成 AST,我们再生成指纹,拿到指纹之后,Plan Hint Manager 会解析出绑定的 Plan 和 Hint,有 Plan 和 Hint 之后,我们会把 AST 中的一部分节点替换掉,接下来这个 AST 就是一个「带 Hint 的 AST」,然后扔给 Optimizer,Optimizer 就能根据 Hint 介入查询优化器以及执行计划。如果出现慢的 Query,那么可以直接通过前面的 Query Tracing 去定位,再通过 Plan Management 机制在线的给数据库手动加 Hint,来解决慢 Query 的问题。这样下来也就不需要业务人员去改 SQL。这个特性应该在 TiDB 3.0 GA 正式对外提供,现在在内部已经跑得非常好了。在这里也非常感谢美团数据库开发同学的贡献。2.3 Join ReorderTiDB 3.0 中我们增加了 Join Reorder。以前我们有一个非常简单的 Reorder 算法,就是根据 Join 这个路径上的等值条件做了一个优先选择,现在 TiDB 3.0 Beta 已经提供了第一种 Join Reorder 算法,就是一个贪心的算法。简单来说,就是我有几个需要 Join 的表,那我先从中选择 Join 之后数据量最小的那个表(是真正根据 Join 之后的代价来选的),然后我在剩下的表中再选一个,和这个再组成一个 Join Path,这样我们就能一定程度上解决很多 Join 的问题。比如 TPC-H 上的 Q5 以前是需要手动加 Hint 才能跑出来,因为它没有选对 Join 的路径,但在 TiDB 3.0 Beta 中,已经能够自动的选择最好的 Join Path 解决这个问题了。<center>图 16 TiDB 3.0 Beta - Join Reorder</center>我们接下来还要再做一个基于动态规划的 Join Reorder 算法,很有可能会在 3.0 GA 中对外提供。 在 Join 表比较少的时候,我们用动态规划算法能保证找到最好的一个 Join 的路径,但是如果表非常多,比如大于十几个表,那可能会选择贪心的算法,因为 Join Reorder 还是比较耗时的。3. Functionality说完稳定性和易用性之外,我们再看一下功能。<center>图 17 TiDB 3.0 Beta 新增功能</center>我们现在做了一个插件系统,因为我们发现数据库能做的功能太多了,只有我们来做其实不太可能,而且每个用户有不一样的需求,比如说这家想要一个能够结合他们的监控系统的一个模块,那家想要一个能够结合他们的认证系统做一个模块,所以我们希望有一个扩展的机制,让大家都有机会能够在一个通用的数据库内核上去定制自己想要的特性。这个插件是基于 Golang 的 Plugin 系统。如果大家有 TiDB Server 的 Binary 和自己插件的 .so,就能在启动 TiDB Server 时加载自己的插件,获得自己定制的功能。图 17 还列举了一些我们正在做的功能,比如白名单,审计日志,Slow Query,还有一些在 TiDB Hackathon 中诞生的项目,我们也想拿到插件中看看是否能够做出来。4. Performance<center>图 18 TiDB 3.0 Beta - OLTP Benchmark</center>从图 18 中可以看到,我们对 TiDB 3.0 Beta 中做了这么多性能优化之后,在 OLTP 这块进步还是比较大的,比如在 SysBench 下,无论是纯读取还是写入,还是读加写,都有几倍的提升。在解决稳定性这个问题之后,我们在性能方面会投入更多的精力。因为很多时候不能把「性能」单纯的当作性能来看,很多时候慢了,可能业务就挂了,慢了就是错误。当然 TiDB 3.0 中还有其他重要特性,这里就不详细展开了。(TiDB 3.0 Beta Release Notes )Next?刚才介绍是 3.0 Beta 一些比较核心的特性,我们还在继续做更多的特性。1. Storage Layer<center>图 19 TiDB 存储引擎层未来规划</center>比如在存储引擎层,我们对 Raft 层还在改进,比如说刚才我提到了我们有 Raft Learner,我们已经能够极大的减少由于调度带来的 Raft Group 不可用的概率,但是把一个 Learner 提成 Voter 再把另一个 Voter 干掉的时间间隔虽然比较短,但时间间隔依然存在,所以也并不是一个 100% 安全的方案。因此我们做了 Raft Joint Consensus。以前成员变更只能一个一个来:先把 Learner 提成 Voter,再把另一个 Voter 干掉。但有了 Raft Joint Consensus 之后,就能在一次操作中执行多个 ConfChange,从而把因为调度导致的 Region 不可用的概率降为零。另外我们还在做跨数据中心的部署。前面社区实践分享中来自北京银行的于振华老师提到过,他们是一个两地三中心五部分的方案。现在的 TiDB 已经有一些机制能比较不错地处理这种场景,但我们能够做更多更好的东西,比如说我们可以支持 Witness 这种角色,它只做投票,不同步数据,对带宽的需求比较少,即使机房之间带宽非常低,他可以参与投票。在其他节点失效的情况下,他可以参与选举,决定谁是 Leader。另外我们支持通过 Follower 去读数据,但写入还是要走 Leader,这样对跨机房有什么好处呢? 就是可以读本地机房的副本,而不是一定要读远端机房那个 Leader,但是写入还是要走远端机房的 Leader,这就能极大的降低读的延迟。除此之外,还有支持链式复制,而不是都通过 Leader 去复制,直接通过本地机房复制数据。之后我们还可以基于 Learner 做数据的 Backup。通过 learner 去拉一个镜像,存到本地,或者通过 Learner 拉取镜像之后的增量,做增量的物理备份。所以之后要做物理备份是通过 Learner 实时的把 TiKV 中数据做一个物理备份,包括全量和增量。当需要恢复的时候,再通过这个备份直接恢复就好了,不需要通过 SQL 导出再导入,能比较快提升恢复速度。2. SQL Layer<center>图 20 TiDB 存储引擎层未来规划</center>在 SQL 层,我们还做了很多事情,比如 Optimizer 正在朝下一代做演进,它是基于最先进的 Cascades 模型。我们希望 Optimizer 能够处理任意复杂的 Query,帮大家解决从 OLTP 到 OLAP 一整套问题,甚至更复杂的问题。比如现在 TiDB 只在 TiKV 上查数据,下一步还要接入TiFlash,TiFlash 的代价或者算子其实不一样的,我们希望能够在 TiDB 上支持多个存储引擎,比如同一个 Query,可以一部分算子推到 TiFlash 上去处理,一部分算子在 TiKV 上处理,在 TiFlash 上做全表扫描,TiKV 上就做 Index 点查,最后汇总在一起再做计算。我们还计划提供一个新的工具,叫 SQL Tuning Advisor。现在用户遇到了慢 Query,或者想在上线业务之前做 SQL 审核和优化建议,很多时候是人肉来做的,之后我们希望把这个过程变成自动的。除此之外我们还将支持向量化的引擎,就是把这个引擎进一步做向量化。未来我们还要继续兼容最新的 MySQL 8.0 的特性 Common Table,目前计划以 MySQL 5.7 为兼容目标,和社区用户一起把 TiDB 过渡到 MySQL 8.0 兼容。说了这么多,我个人觉得,我们做一个好的数据库,有用的数据库,最重要一点是我们有大量的老师,可以向用户,向社区学习。不管是分享了使用 TiDB 的经验和坑也好,还是去提 Issue 报 Bug,或者是给 TiDB 提交了代码,都是在帮助我们把 TiDB 做得更好,所以在这里表示一下衷心的感谢。最后再立一个 flag,去年我们共写了 24 篇 TiDB 源码阅读文章,今年还会写 TiKV 源码系列文章。我们希望把项目背后只有开发同学才能理解的这套逻辑讲出来,让大家知道 TiDB 是怎样的工作的,希望今年能把这个事情做完,感谢大家。1 月 19 日 TiDB DevCon 2019 在北京圆满落幕,超过 750 位热情的社区伙伴参加了此次大会。会上我们首次全面展示了全新存储引擎 Titan、新生态工具 TiFlash 以及 TiDB 在云上的进展,同时宣布 TiDB-Lightning Toolset & TiDB-DM 两大生态工具开源,并分享了 TiDB 3.0 的特性与未来规划,描述了我们眼中未来数据库的模样。此外,更有 11 位来自一线的 TiDB 用户为大家分享了实践经验与踩过的「坑」。同时,我们也为新晋 TiDB Committer 授予了证书,并为 2018 年最佳社区贡献个人、最佳社区贡献团队颁发了荣誉奖杯。 ...

February 27, 2019 · 4 min · jiezi

The Way to TiDB 3.0 and Beyond (上篇)

我司 Engineering VP 申砾在 TiDB DevCon 2019 上分享了 TiDB 产品进化过程中的思考与未来规划。本文为演讲实录上篇,重点回顾了 TiDB 2.1 的特性,并分享了我们对「如何做一个好的数据库」的看法。<center>我司 Engineering VP 申砾</center>感谢这么多朋友的到场,今天我会从我们的一些思考的角度来回顾过去一段时间做了什么事情,以及未来的半年到一年时间内将会做什么事情,特别是「我们为什么要做这些事情」。TiDB 这个产品,我们从 2015 年年中开始做,做到现在,三年半,将近四年了,从最早期的 Beta 版的时候就开始上线,到后来 RC 版本,最后在 2017 年终于发了 1.0,开始铺了一部分用户,到 2.0 的时候,用户数量就开始涨的非常快。然后我们最近发了 2.1,在 2.1 之后,我们也和各种用户去聊,跟他们聊一些使用的体验,有什么样的问题,包括对我们进行吐嘈。我们就在这些实践经验基础之上,设计了 3.0 的一些特性,以及我们的一些工作的重点。现在我们正在朝 3.0 这个版本去演进,到今天早上已经发了 3.0 Beta 版本。TiDB 2.1首先我们来讲 2.1,2.1 是一个非常重要的版本,这个版本我们吸取了很多用户的使用场景中看到的问题,以及特别多用户的建议。在这里我跟大家聊一聊它有哪些比较重要的特性。<center>图 1 TiDB 2.1 新增重要功能</center>首先我们两个核心组件:存储引擎和计算引擎,在这两方面,我们做了一些非常重要的改进,当然这些改进有可能是用户看不到的。或者说这些改进其实我们是不希望用户能看到的,一旦你看到了,注意到这些改进的话,说明你的系统遇到这些问题了。1. Raft1.1 Learner大家都知道 Raft 会有 Leader 和 Follower 这两个概念,Leader 来负责读写,Follower 来作为 Backup,然后随时找机会成为新的 Leader。如果你想加一个新的节点,比如说在扩容或者故障恢复,新加了一个 Follower 进来,这个时候 Raft Group 有 4 个成员, Leader、Follower 都是 Voter,都能够在写入数据时候对日志进行投票,或者是要在成员变更的时候投票的。这时一旦发生意外情况,比如网络变更或者出现网络分区,假设 2 个被隔离掉的节点都在一个物理位置上,就会导致 4 个 Voter 中 2 个不可用,那这时这个 Raft Group 就不可用了。大家可能觉得这个场景并不常见,但是如果我们正在做负载均衡调度或者扩容时,一旦出现这种情况,就很有可能影响业务。所以我们加了 Learner 这个角色,Learner 的功能也是我们贡献给 etcd 这个项目的。有了 Learner 之后,我们在扩容时不会先去加一个 Follower(也就是一个 Voter),而是增加一个 Learner 的角色,它不是 Voter,所以它只会同步数据不会投票,所以无论在做数据写入还是成员变更的时候都不会算上它。当同步完所有数据时(因为数据量大的时候同步时间会比较长),拿到所有数据之后,再把它变成一个 Voter,同时再把另一个我们想下线的 Follower 下掉就好了。这样就能极大的缩短同时存在 4 个 Voter 的时间,整个 Raft Group 的可用性就得到了提升。<center>图 2 TiDB 2.1 - Raft Learner</center>其实增加 Learner 功能不只是出于提升 Raft Group 可用性,或者说出于安全角度考虑,实际上我们也在用 Learner 来做更多的事情。比如,我们可以随便去加 Learner,然后把 Learner 变成一个只读副本,很多很重的分析任务就可以在 Learner 上去做。TiFlash 这个项目其实就是用 Learner 这个特性来增加只读副本,同时保证不会影响线上写入的延迟,因为它并不参与写入的时候投票。这样的好处是第一不影响写入延迟,第二有 Raft 实时同步数据,第三我们还能在上面快速地做很复杂的分析,同时线上 OLTP 业务有物理上的隔离。1.2 PreVote除了 Learner 之外,我们 2.1 中默认开启了 PreVote 这个功能。我们考虑一种意外情况,就是在 Raft group 中出现了网络隔离,有 1 个节点和另外 2 个节点隔离掉了,然后它现在发现「我找不到 Leader 了,Leader 可能已经挂掉了」,然后就开始投票,不断投票,但是因为它和其他节点是隔离开的,所以没有办法选举成功。它每次失败,都会把自己的 term 加 1,每次失败加 1,网络隔离发生一段时间之后,它的 term 就会很高。当网络分区恢复之后,它的选举消息就能发出去了,并且这个选举消息里面的 term 是比较高的。根据 Raft 的协议,当遇到一个 term 比较高的时候,可能就会同意发起选举,当前的 Leader 就会下台来参与选举。但是因为发生网络隔离这段时间他是没有办法同步数据的,此时它的 Raft Log 一定是落后的,所以即使它的 term 很高,也不可能被选成新的 Leader。所以这个时候经过一次选举之后,它不会成为新 Leader,只有另外两个有机会成为新的 Leader。大家可以看到,这个选举是对整个 Raft Group 造成了危害:首先它不可能成为新的 Leader,第二它把原有的 Leader 赶下台了,并且在这个选举过程中是没有 Leader 的,这时的 Raft Group 是不能对外提供服务的。虽然这个时间会很短,但也可能会造成比较大的抖动。<center>图 3 TiDB 2.1 - Raft PreVoter</center>所以我们有了 PreVote 这个特性。具体是这样做的(如图 3):在进行选举之前,先用 PreVote 这套机制来进行预选举,每个成员把自己的信息,包括 term,Raft Log Index 放进去,发给其它成员,其它成员有这个信息之后,认为「我可以选你为 Leader」,才会发起真正的选举。有了 PreVote 之后,我们就可以避免这种大规模的一个节点上很多数据、很多 Raft Group、很多 Peer 的情况下突然出现网络分区,在恢复之后造成大量的 Region 出现选举,导致整个服务有抖动。 因此 PreVote 能极大的提升稳定性。2. Concurrent DDL Operation当然除了 Raft 这几个改进之外,TiDB 2.1 中还有一个比较大的改进,就是在 DDL 模块。这是我们 2.1 中一个比较显著的特性。<center>图 4 TiDB 2.1 之前的 DDL 机制</center>在 2.1 之前的 DDL 整套机制是这样的(如图 4):用户将 DDL 提交到任何一个 TiDB Server,发过来一个 DDL 语句,TiDB Server 经过一些初期的检查之后会打包成一个 DDL Job,扔到 TiKV 上一个封装好的队列中,整个集群只有一个 TiDB Server 会执行 DDL,而且只有一个线程在做这个事情。这个线程会去队列中拿到队列头的一个 Job,拿到之后就开始做,直到这个 Job 做完,即 DDL 操作执行完毕后,会再把这个 Job 扔到历史队列中,并且标记已经成功,这时 TiDB Sever 能感知到这个 DDL 操作是已经结束了,然后对外返回。前面的 Job 在执行完之前,后面的 DDL 操作是不会执行的,因而会造成一个情况: 假设前面有一个 AddIndex,比如在一个百亿行表上去 AddIndex,这个时间是非常长的,后面的 Create Table 是非常快的,但这时 Create Table 操作会被 AddIndex 阻塞,只有等到 AddIndex 执行完了,才会执行 Create Table,这个给有些用户造成了困扰,所以我们在 TiDB 2.1 中做了改进。<center>图 5 TiDB 2.1 - DDL 机制</center>在 TiDB 2.1 中 DDL 从执行层面分为两种(如图 5)。一种是 AddIndex 操作,即回填数据(就是把所有的数据扫出来,然后再填回去),这个操作耗时是非常长的,而且一些用户是线上场景,并发度不可能调得很高,因为在回写数据的时候,可能会对集群的写入造成压力。另外一种是所有其他 DDL 操作,因为不管是 Create Table 还是加一个 Column 都是非常快的,只会修改 metadata 剩下的交给后台来做。所以我们将 AddIndex 的操作和其他有 DDL 的操作分成两个队列,每种 DDL 语句按照分类,进到不同队列中,在 DDL 的处理节点上会启用多个线程来分别处理这些队列,将比较慢的 AddIndex 的操作交给单独的一个线程来做,这样就不会出现一个 AddIndex 操作阻塞其他所有 Create Table 语句的问题了。这样就提升了系统的易用性,当然我们下一步还会做进一步的并行, 比如在 AddIndex 时,可以在多个表上同时 AddIndex,或者一个表上同时 Add 多个 Index。我们也希望能够做成真正并行的一个 DDL 操作。3. Parallel Hash Aggregation除了刚刚提到的稳定性和易用性的提升,我们在 TiDB 2.1 中,也对分析能力做了提升。我们在聚合的算子上做了两点改进。 第一点是对整个聚合框架做了优化,就是从一行一行处理的聚合模式,变成了一批一批数据处理的聚合模式,另外我们还在哈希聚合算子上做了并行。<center>图 6 TiDB 2.1 - Parallel Hash Aggregation</center>为什么我们要优化聚合算子?因为在分析场景下,有两类算子是非常重要的,是 Join 和聚合。Join 算子我们之前已经做了并行处理,而 TiDB 2.1 中我们进一步对聚合算子做了并行处理。在哈希聚合中,我们在一个聚合算子里启用多个线程,分两个阶段进行聚合。这样就能够极大的提升聚合的速度。<center>图 7 TiDB 2.0 与 TiDB 2.1 TPC-H Benchmark 对比</center>图 7 是 TiDB 2.1 发布的时候,我们做了一个 TPC-H Benchmark。实际上所有的 Query 都有提升,其中 Q17 和 Q18 提升最大。因为在 TiDB 2.0 测试时,Q17、Q18 还是一个长尾的 Query,分析之后发现瓶颈就在于聚合算子的执行。整个机器的 CPU 并不忙,但就是时间很长,我们做了 Profile 发现就是聚合的时间太长了,所以在 TiDB 2.1 中,对聚合算子做了并行,并且这个并行度可以调节。4. Ecosystem ToolsTiDB 2.1 发布的时我们还发布了两个工具,分别叫 TiDB-DM 和 TiDB-Lightning。TiDB-DM 全称是 TiDB Data Migration,这个工具主要用来把我们之前的 Loader 和以及 Syncer 做了产品化改造,让大家更好用,它能够做分库分表的合并,能够只同步一些表中的数据,并且它还能够对数据做一些改写,因为分库分表合并的时候,数据合到一个表中可能会冲突,这时我们就需要一种非常方便、可配置的工具来操作,而不是让用户手动的去调各种参数。TiDB-Lightning 这个工具是用来做全量的数据导入。之前的 Loader 也可以做全量数据导入,但是它是走的最标准的那套 SQL 的流程,需要做 SQL 的解析优化、 两阶段提交、Raft 复制等等一系列操作。但是我们觉得这个过程可以更快。因为很多用户想迁移到 TiDB 的数据不是几十 G 或者几百 G,而是几 T、几十 T、上百 T 的数据,通过传统导入的方式会非常慢。现在 TiDB-Lightning 可以直接将本地从 MySQL 或者其他库中导出的 SQL 文本,或者是 CSV 格式的文件,直接转成 RocksDB 底层的 SST file ,然后再注入到 TiKV 中,加载进去就导入成功了,能够极大的提升导入速度。当然我们还在不断的优化,希望这个速度能不断提升,将 1TB 数据的导入,压缩到一两个小时。这两个工具,有一部分用户已经用到了(并且已经正式开源)。How to build a good database?我们有相当多的用户正在使用 TiDB,我们在很多的场景中见到了各种各样的 Case,甚至包括机器坏掉甚至连续坏掉的情况。见了很多场景之后,我们就在想之后如何去改进产品,如何去避免在各种场景中遇到的「坑」,于是我们在更深入地思考一个问题:如何做一个好的数据库。因为做一个产品其实挺容易的,一个人两三个月也能搞一套数据库,不管是分库分表,还是类似于在 KV 上做一个 SQL,甚至做一个分布式数据库,都是可能在一个季度甚至半年之内做出来的。但是要真正做一个好的数据库,做一个成熟的数据库,做一个能在生产系统中大规模使用,并且能够让用户自己玩起来的数据库,其实里面有非常多工作要做。首先数据库最基本的是要「有用」,就是能解决用户问题。而要解决用户问题,第一点就是要知道用户有什么样的问题,我们就需要跟各类用户去聊,看用户的场景,一起来分析,一起来获得使用场景中真实存在的问题。所以最近我们有大量的同事,不管是交付的同事还是研发的同事,都与用户做了比较深入的访谈,聊聊用户在使用过程中有什么的问题,有什么样的需求,用户也提供各种各样的建议。我们希望 TiDB 能够很好的解决用户场景中存在的问题,甚至是用户自己暂时还没有察觉到的问题,进一步的满足用户的各种需求。第二点是「易用性」。就好像一辆车,手动挡的大家也能开,但其实大家现在都想开自动挡。我们希望我们的数据库是一辆自动挡的车,甚至未来是一辆无人驾驶的车,让用户不需要关心这些事情,只需要专注自己的业务就好了。所以我们会不断的优化现有的解决方案,给用户更多更好的解决方案,下一步再将这些方案自动化,让用户用更低的成本使用我们的数据库。最后一点「稳定性」也非常重要,就是让用户不会用着用着担惊受怕,比如半夜报警之类的事情。而且我们希望 TiDB 能在大规模数据集上、在大流量上也能保持稳定。未完待续…下篇将于明日推送,重点介绍 TiDB 3.0 Beta 在稳定性、易用性和功能性上的提升,以及 TiDB 在 Storage Layer 和 SQL Layer 方面的规划。 ...

February 26, 2019 · 3 min · jiezi

消息通知系统模型设计

本篇主要明确消息通知系统的概念和具体实现,包括数据库设计、技术方案、逻辑关系分析等。消息通知系统是一个比较复杂的系统,这里主要分析站内消息如何设计和实现。我们常见的消息推送渠道有以下几种:设备推送站内推送短信推送邮箱推送我们常见的站内通知有以下几种类别:公告 Announcement提醒 Remind资源订阅提醒「我关注的资源有更新、评论等事件时通知我」资源发布提醒「我发布的资源有评论、收藏等事件时通知我」系统提醒「平台会根据一些算法、规则等可能会对你的资源做一些事情,这时你会收到系统通知」私信 Mailbox以上三种消息有各自特点,实现也各不相同,其中「提醒」类通知是最复杂的,下面会详细讲。数据模型设计公告公告是指平台发送一条含有具体内容的消息,站内所有用户都能收到这条消息。方案一:【适合活跃用户在5万左右】 公告表「notify_announce」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //公告编号;senderID: {type: ‘string’, required: true} //发送者编号,通常为系统管理员;title: {type: ‘string’, required: true} //公告标题;content: {type: ’text’, required: true} //公告内容;createdAt: {type: ’timestamp’, required: true} //发送时间;用户公告表「notify_announce_user」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //用户公告编号;announceID: {type: ‘integer’} //公告编号;recipientID: {type: ‘string’, required: true} //接收用户编号;createdAt:{type: ’timestamp’, required: true} //拉取公告时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;平台发布一则公告之后,当用户登录的时候去拉取站内公告并插入notify_announce_user表,这样那些很久都没登陆的用户就没必要插入了。「首次拉取,根据用户的注册时间;否则根据notify_announce_user.createdAt即上一次拉取的时间节点获取公告」方案二:【适合活跃用户在百万-千万左右】 和方案一雷同,只是需要把notify_announce_user表进行哈希分表,需事先生成表:notify_announce_<hash(uid)>。 用户公告表「notify_announce_<hash(uid)>」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //用户公告编号;announceID: {type: ‘integer’} //公告编号;recipientID: {type: ‘string’, required: true} //接收用户编号;createdAt:{type: ’timestamp’, required: true} //拉取公告时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;提醒提醒是指「我的资源」或「我关注的资源」有新的动态产生时通知我。提醒的内容无非就是: 「someone do something in someone’s something」 「谁对一样属于谁的事物做了什么操作」 常见的提醒消息例子,如: XXX 关注了你 - 「这则属于资源发布提醒」 XXX 喜欢了你的文章 《消息通知系统模型设计》 - 「这则属于资源发布提醒」 你喜欢的文章《消息通知系统模型设计》有新的评论 - 「这则属于资源订阅提醒」 你的文章《消息通知系统模型设计》已被加入专题 《系统设计》 - 「这则属于系统提醒」 小明赞同了你的回答 XXXXXXXXX -「这则属于资源发布提醒」最后一个例子中包含了消息的生产者(小明),消息记录的行为(赞同),行为的对象(你的回答内容)分析提醒类消息的句子结构: someone = 动作发起者,标记为「sender」 do something = 对资源的操作,如:评论、喜欢、关注都属于一个动作,标记为「action」 something = 被作用对象,如:一篇文章,文章的评论等,标记为「object」 someone’s = 动作的目标对象或目标资源的所有者,标记为「objectOwner」总结:sender 和 objectOwner 就是网站的用户,object 就是网站资源,可能是一篇文章,一条文章的评论等等。action 就是动作,可以是赞、评论、收藏、关注、捐款等等。提醒设置提醒通常是可以在「设置-通知」里自定义配置的,用户可以选择性地「订阅」接收和不接收某类通知。呈现在界面上是这样的:通知设置我发布的 publish 文章 被 评论 是/否 通知我 被 收藏 是/否 通知我 被 点赞 是/否 通知我 被 喜欢 是/否 通知我 被 捐款 是/否 通知我我订阅的 follow 文章 有 更新 是/否 通知我 被 评论 是/否 通知我订阅一般系统默认是订阅了所有通知的。系统在给用户推送消息的时候必须查询通知「订阅」模块,以获取某一事件提醒消息应该推送到哪些用户。也就是说「事件」和「用户」之间有一个订阅关系。那么接下来我们分析下「订阅」有哪些关键元素: 比如我发布了一篇文章,那么我会订阅文章《XXX》的评论动作,所以文章《XXX》每被人评论了,就需要发送一则提醒告知我。分析得出以下关键元素:订阅者「subscriber」订阅的对象「object」订阅的动作「action」订阅对象和订阅者的关系「objectRelationship」什么是订阅的目标关系呢? 拿知乎来说,比如我喜欢了一篇文章,我希望我订阅这篇文章的更新、评论动作。那这篇文章和我什么关系?不是所属关系,只是喜欢。objectRelationship = 我发布的,对应着 actions = [评论,收藏]objectRelationship = 我喜欢的,对应着 actions = [更新,评论]讲了那么多,现在来构建「提醒」的数据结构该吧! 提醒表「notify_remind」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //主键;remindID: {type: ‘string’, required: true} //通知提醒编号;senderID: {type: ‘string’, required: true} //操作者的ID,三个0代表是系统发送的;senderName: {type: ‘string’, required: true} //操作者用户名;senderAction: {type: ‘string’, required: true} //操作者的动作,如:赞了、评论了、喜欢了、捐款了、收藏了;objectID: {type: ‘string’, required: true}, //目标对象ID;object: {type: ‘string’, required: false}, //目标对象内容或简介,比如:文章标题;objectType: {type: ‘string’, required: true} //被操作对象类型,如:人、文章、活动、视频等;recipientID: {type: ‘string’} //消息接收者;可能是对象的所有者或订阅者;message: {type: ’text’, required: true} //消息内容,由提醒模版生成,需要提前定义;createdAt:{type: ’timestamp’, required: true} //创建时间;status:{type: ‘integer’, required: false} //是否阅读,默认未读;readAt:{type: ’timestamp’, required: false} //阅读时间;假如:特朗普关注了金正恩,以下字段的值是这样的senderID = 特朗普的IDsenderName = 特朗普senderAction = 关注objectID = 金正恩的IDobject = 金正恩objectType = 人recipientID = 金正恩的IDmessage = 特朗普关注了金正恩这种情况objectID 和 recipientID是一样的。这里需要特别说下消息模版,模版由「对象」、「动作」和「对象关系」构成唯一性。通知提醒订阅表「notify_remind_subscribe」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //订阅ID;userID: {type: ‘string’, required: true},//用户ID,对应 notify_remind 中的 recipientID;objectType: {type: ‘string’, required: true} //资源对象类型,如:文章、评论、视频、活动、用户;action: {type: ‘string’, required: true} //资源订阅动作,多个动作逗号分隔如: comment,like,post,update etc.objectRelationship: {type: ‘string’, required: true} //用户与资源的关系,用户发布的published,用户关注的followed;createdAt:{type: ’timestamp’, required: true} //创建时间;特别说下「objectRelationship」字段的作用,这个字段用来区分「消息模版」,为什么呢?因为同一个「资源对象」和「动作」会有两类订阅者,一类是该资源的Owner,另一类是该资源的Subscriber,这两类人收到的通知消息内容应该是不一样的。聚合假如我在抖音上发布了一个短视频,在我不在线的时候,被评论了1000遍,当我一上线的时候,应该是收到一千条消息,类似于:「* 评论了你的文章《XXX》」? 还是应该收到一条信息:「有1000个人评论了你的文章《XXX》」? 当然是后者更好些,要尽可能少的骚扰用户。消息推送是不是感觉有点晕了,还是先上一张消息通知的推送流程图吧: 订阅表一共有两张噢,一张是「通知订阅表」、另一张是用户对资源的「对象订阅表」。 具体实现就不多讲了,配合这张图,理解上面讲的应该不会有问题了。私信通常私信有这么几种需求:点到点:用户发给用户的站内信,系统发给用户的站内信。「1:1」点到多:系统发给多个用户的站内信,接收对象较少,而且接收对象无特殊共性。「1:N」点到面:系统发给用户组的站内信,接收对象同属于某用户组之类的共同属性。「1:N」点到全部:系统发给全站用户的站内信,接收对象为全部用户,通常为系统通知。「1:N」这里主要讲「点到点」的站内信。私信表「notify_mailbox」表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //编号;dialogueID: {type: ‘string’, required: true} //对话编号; senderID: {type: ‘string’, required: true} //发送者编号;recipientID: {type: ‘string’, required: true} //接收者编号;messageID: {type: ‘integer’, required: true} //私信内容ID;createdAt:{type: ’timestamp’, required: true} //发送时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;Inbox私信列表select * from notify_inbox where recipientID=“uid” order by createdAt desc对话列表select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (recipientID=“uid” or senderID=“uid”) order by createdAt asc私信回复时,回复的是dialogueIDOutbox私信列表select * from notify_inbox where senderID=“uid” order by createdAt desc对话列表select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (senderID=“uid” or recipientID=“uid”) order by createdAt asc私信内容表「notify_inbox_message」表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //编号;senderID: {type: ‘string’, required: true} //发送者编号;content: {type: ‘string’, required: true} //私信内容; createdAt:{type: ’timestamp’, required: true}参考消息系统设计与实现 通知系统设计 ...

February 21, 2019 · 2 min · jiezi

造个轮子 | 自己用C++实现Redis

最近学习了Redis,对其内部结构较为感兴趣,为了进一步了解其运行原理,我打算自己动手用C++写一个redis。这是我第一次造轮子,所以纪念一下 ^ _ ^。源码github链接,项目现在实现了客户端与服务器的链接与交互,以及一些Redis的基本命令,下面是测试结果:(左边是服务端,右边是客户端)为了完善其功能并且锻炼一下自己的数据结构与算法,我下一阶段打算根据《Redis设计与实现》一书优化数据结构与算法从而完善自己的项目。基本结构介绍基本流程介绍首先是对服务端的初始化,包括数据库的初始化以及命令集合的初始化。在客户端连接之后,开始创建客户端对其进行初始化,并且将其与服务端对应的数据库进行连接。在客户端发送命令之后,服务端接受命令,对命令的合法性进行判断,然后在命令集合中查找相关命令并执行,最后返回执行结果给客户端。

February 20, 2019 · 1 min · jiezi

TiKV 源码解析系列文章(一)序

作者:唐刘TiKV 是一个支持事务的分布式 Key-Value 数据库,有很多社区开发者基于 TiKV 来开发自己的应用,譬如 titan、tidis。尤其是在 TiKV 成为 CNCF 的 Sandbox 项目之后,吸引了越来越多开发者的目光,很多同学都想参与到 TiKV 的研发中来。这时候,就会遇到两个比较大的拦路虎:Rust 语言:众所周知,TiKV 是使用 Rust 语言来进行开发的,而 Rust 语言的学习难度相对较高,有些人认为其学习曲线大于 C++,所以很多同学在这一步就直接放弃了。文档:最开始 TiKV 是作为 HTAP 数据库 TiDB 的一个底层存储引擎设计并开发出来的,属于内部系统,缺乏详细的文档,以至于同学们不知道 TiKV 是怎么设计的,以及代码为什么要这么写。对于第一个问题,我们内部正在制作一系列的 Rust 培训课程,由 Rust 作者以及 Rust 社区知名的开发者亲自操刀,预计会在今年第一季度对外发布。希望通过该课程的学习,大家能快速入门 Rust,使用 Rust 开发自己的应用。而对于第二个问题,我们会启动 《TiKV 源码解析系列文章》以及 《Deep Dive TiKV 系列文章》计划,在《Deep Dive TiKV 系列文章》中,我们会详细介绍与解释 TiKV 所使用技术的基本原理,譬如 Raft 协议的说明,以及我们是如何对 Raft 做扩展和优化的。而 《TiKV 源码解析系列文章》则是会从源码层面给大家抽丝剥茧,让大家知道我们内部到底是如何实现的。我们希望,通过这两个系列,能让大家对 TiKV 有更深刻的理解,再加上 Rust 培训,能让大家很好的参与到 TiKV 的开发中来。结构本篇文章是《TiKV 源码解析系列文章》的序篇,会简单的给大家讲一下 TiKV 的基本模块,让大家对这个系统有一个整体的了解。要理解 TiKV,只是了解 https://github.com/tikv/tikv 这一个项目是远远不够的,通常,我们也需要了解很多其他的项目,包括但不限于:https://github.com/pingcap/raft-rshttps://github.com/pingcap/rust-prometheushttps://github.com/pingcap/rust-rocksdbhttps://github.com/pingcap/fail-rshttps://github.com/pingcap/rocksdbhttps://github.com/pingcap/grpc-rshttps://github.com/pingcap/pd在这个系列里面,我们首先会从 TiKV 使用的周边库开始介绍,然后介绍 TiKV,最后会介绍 PD。下面简单来说下我们的一些介绍计划。Storage EngineTiKV 现在使用 RocksDB 作为底层数据存储方案。在 pingcap/rust-rocksdb 这个库里面,我们会简单说明 Rust 是如何通过 Foreign Function Interface (FFI) 来跟 C library 进行交互,以及我们是如何将 RocksDB 的 C API 封装好给 Rust 使用的。另外,在 pingcap/rocksdb 这个库里面,我们会详细的介绍我们自己研发的 Key-Value 分离引擎 - Titan,同时也会让大家知道如何使用 RocksDB 对外提供的接口来构建自己的 engine。RaftTiKV 使用的是 Raft 一致性协议。为了保证算法的正确性,我们直接将 etcd 的 Go 实现 port 成了 Rust。在 pingcap/raft-rs,我们会详细介绍 Raft 的选举,Log 复制,snapshot 这些基本的功能是如何实现的。另外,我们还会介绍对 Raft 的一些优化,譬如 pre-vote,check quorum 机制,batch 以及 pipeline。最后,我们会说明如何去使用这个 Raft 库,这样大家就能在自己的应用里面集成 Raft 了。gRPCTiKV 使用的是 gRPC 作为通讯框架,我们直接把 Google C gRPC 库封装在 grpc-rs 这个库里面。我们会详细告诉大家如何去封装和操作 C gRPC 库,启动一个 gRPC 服务。另外,我们还会介绍如何使用 Rust 的 futures-rs 来将异步逻辑变成类似同步的方式来处理,以及如何通过解析 protobuf 文件来生成对应的 API 代码。最后,我们会介绍如何基于该库构建一个简单的 gRPC 服务。PrometheusTiKV 使用 Prometheus 作为其监控系统, rust-prometheus 这个库是 Prometheus 的 Rust client。在这个库里面,我们会介绍如果支持不同的 Prometheus 的数据类型(Coutner,Gauge,Historgram)。另外,我们会重点介绍我们是如何通过使用 Rust 的 Macro 来支持 Prometheus 的 Vector metrics 的。最后,我们会介绍如何在自己的项目里面集成 Prometheus client,将自己的 metrics 存到 Prometheus 里面,方便后续分析。FailFail 是一个错误注入的库。通过这个库,我们能很方便的在代码的某些地方加上 hook,注入错误,然后在系统运行的时候触发相关的错误,看系统是否稳定。我们会详细的介绍 Fail 是如何通过 macro 来注入错误,会告诉大家如何添加自己的 hook,以及在外面进行触发TiKVTiKV 是一个非常复杂的系统,这块我们会重点介绍,主要包括:Raftstore,该模块里面我们会介绍 TiKV 如何使用 Raft,如何支持 Multi-Raft。Storage,该模块里面我们会介绍 Multiversion concurrency control (MVCC),基于 Percolator 的分布式事务的实现,数据在 engine 里面的存储方式,engine 操作相关的 API 等。Server,该模块我们会介绍 TiKV 的 gRPC API,以及不同函数执行流程。Coprocessor,该模块我们会详细介绍 TiKV 是如何处理 TiDB 的下推请求的,如何通过不同的表达式进行数据读取以及计算的。PD,该模块我们会介绍 TiKV 是如何跟 PD 进行交互的。Import,该模块我们会介绍 TiKV 如何处理大量数据的导入,以及如何跟 TiDB 数据导入工具 lightning 交互的。Util,该模块我们会介绍一些 TiKV 使用的基本功能库。PDPD 用来负责整个 TiKV 的调度,我们会详细的介绍 PD 内部是如何使用 etcd 来进行元数据存取和高可用支持,也会介绍 PD 如何跟 TiKV 交互,如何生成全局的 ID 以及 timestamp。最后,我们会详细的介绍 PD 提供的 scheduler,以及不同的 scheudler 所负责的事情,让大家能通过配置 scheduler 来让系统更加的稳定。小结上面简单的介绍了源码解析涉及的模块,还有一些模块譬如 https://github.com/tikv/client-rust 仍在开发中,等完成之后我们也会进行源码解析。我们希望通过该源码解析系列,能让大家对 TiKV 有一个更深刻的理解。当然,TiKV 的源码也是一直在不停的演化,我们也会尽量保证文档的及时更新。最后,欢迎大家参与 TiKV 的开发。 ...

January 28, 2019 · 2 min · jiezi

Mysql事务隔离

数据库事务隔离事务的介绍事务就是一组原子性的sql查询,或者说是一个独立的工作单元。简而言之,事务内的语句要么全部执行成功,要么全部执行失败。在Mysql中,事务支持是在引擎层实现的,但并不是所有的Mysql引擎都支持事务,比如MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一。提到事务,我们肯定会想到ACID:原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)隔离级别当数据库中有多个事务同时执行时,就可能会出现脏读、不可重复读、幻读等问题,因为就有了事务隔离级别的概念。SQL标准正定义了四种隔离级别:READ UNCOMMITTED (未提交读)事务中的修改,即使还没有提交,对其他事务都是可见的。事务可以读取未提交的数据,也被称为脏读(Dirty Read)。READ COMMITTED(提交读)一个事务提交后,所做的变更才能被其他事务看到。这个级别也叫不可重复读,因为事务中执行2次相同的查询,可能得到的结果是不一样的。REPEATABLE READ(可重复读)一个事务执行的过程中,总是和这个事务在启动时看到的数据是一致的。当然在这个级别下,未提交的数据变更对其他事务也是不可见的。SERIALIZABLE(可串行化)对同一行记录,写和读都会加锁,当出现读写锁冲突时,后访问的事务必须等前一个事务执行完成才能继续执行,就会导致大量的超时和锁争用的问题。在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑为准。在可重复读这个隔离级别下,这个视图是事务开启的时候创建的,整个事务期间都用这个视图。在读提交的隔离级别下,这个视图是在sql语句开始执行的时候创建的。在读未提交的隔离级别下,直接返回记录上的最新值,没有视图概念。在串行化的隔离级别下,直接用加锁的方式避免并行访问。配置的方式是将启动参数transaction-isolation设置成想要的隔离级别。查看当前设置:mysql> show variables like ’transaction_isolation’;+———————–+—————–+| Variable_name | Value |+———————–+—————–+| transaction_isolation | REPEATABLE-READ |+———————–+—————–+1 row in set (0.00 sec)总之,存在即合理,不同的隔离级别适用于不同的场景,具体我们应该根据业务场景来决定。事务隔离的实现在Mysql中,实际上每条记录的更新同时也会记录一条回滚操作,记录上的最新值通过回滚操作,都可以得到前一个状态的值。系统会自动判断,当没有事务再需要回滚日志时,会删除回滚日志。为什么不建议使用长事务:长事务意味着系统里面会存在很老的事务视图,由于这些事务随时可以访问数据库里面的任何数据,所以这个事务提交之前,数据库里可能用到的回滚记录必须保留着,这就会占用大量的存储空间。同时长事务还占用锁资源,也可能拖垮整个库。事务启动的方式显式启动事务语句,begin或者start transaction,提交就是commit,回滚用rollback。set autocommit = 0,这个命令会将线程的自动提交关掉,意味着如果执行一个select 语句,这个事务就启动了,并且不会自动提交,直到你主动执行commit或者rollback,或者断开连接。个人建议还是通过第一种方式显式启动事务,避免长事务的发生。在 set autocommit = 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。查询长事务:下面语句是查询持续时间超过60s的事务mysql> select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60;Empty set (0.00 sec)总结下来,我们在开发过程中,尽量少用长事务,如果无法避免,保证逻辑日志空间足够大,并且支持动态日志空间增长。监控Innodb_trx表,发现长事务报警。欢迎交流。参考资料《高性能Mysql》极客时间-Mysql实战45讲

January 26, 2019 · 1 min · jiezi

Titan 的设计与实现

作者:郑志铨Titan 是由 PingCAP 研发的一个基于 RocksDB 的高性能单机 key-value 存储引擎,其主要设计灵感来源于 USENIX FAST 2016 上发表的一篇论文 WiscKey。WiscKey 提出了一种高度基于 SSD 优化的设计,利用 SSD 高效的随机读写性能,通过将 value 分离出 LSM-tree 的方法来达到降低写放大的目的。我们的基准测试结果显示,当 value 较大的时候,Titan 在写、更新和点读等场景下性能都优于 RocksDB。但是根据 RUM Conjecture,通常某些方面的提升往往是以牺牲其他方面为代价而取得的。Titan 便是以牺牲硬盘空间和范围查询的性能为代价,来取得更高的写性能。随着 SSD 价格的降低,我们认为这种取舍的意义会越来越明显。设计目标Titan 作为 TiKV 的一个子项目,首要的设计目标便是兼容 RocksDB。因为 TiKV 使用 RocksDB 作为其底层的存储引擎,而 TiKV 作为一个成熟项目已经拥有庞大的用户群体,所以我们需要考虑已有的用户也可以将已有的基于 RocksDB 的 TiKV 平滑地升级到基于 Titan 的 TiKV。因此,我们总结了四点主要的设计目标:支持将 value 从 LSM-tree 中分离出来单独存储,以降低写放大。已有 RocksDB 实例可以平滑地升级到 Titan,这意味着升级过程不需要人工干预,并且不会影响线上服务。100% 兼容目前 TiKV 所使用的所有 RocksDB 的特性。尽量减少对 RocksDB 的侵入性改动,保证 Titan 更加容易升级到新版本的 RocksDB。架构与实现Titan 的基本架构如下图所示:图 1:Titan 在 Flush 和 Compaction 的时候将 value 分离出 LSM-tree,这样做的好处是写入流程可以和 RockDB 保持一致,减少对 RocksDB 的侵入性改动。Titan 的核心组件主要包括:BlobFile、TitanTableBuilder、Version 和 GC,下面将逐一进行介绍。BlobFileBlobFile 是用来存放从 LSM-tree 中分离出来的 value 的文件,其格式如下图所示:图 2:BlobFile 主要由 blob record 、meta block、meta index block 和 footer 组成。其中每个 blob record 用于存放一个 key-value 对;meta block 支持可扩展性,可以用来存放和 BlobFile 相关的一些属性等;meta index block 用于检索 meta block。BlobFile 有几点值得关注的地方:BlobFile 中的 key-value 是有序存放的,目的是在实现 Iterator 的时候可以通过 prefetch 的方式提高顺序读取的性能。每个 blob record 都保留了 value 对应的 user key 的拷贝,这样做的目的是在进行 GC 的时候,可以通过查询 user key 是否更新来确定对应 value 是否已经过期,但同时也带来了一定的写放大。BlobFile 支持 blob record 粒度的 compression,并且支持多种 compression algorithm,包括 Snappy、LZ4 和 Zstd 等,目前 Titan 默认使用的 compression algorithm 是 LZ4 。TitanTableBuilderTitanTableBuilder 是实现分离 key-value 的关键。我们知道 RocksDB 支持使用用户自定义 table builder 创建 SST,这使得我们可以不对 build table 流程做侵入性的改动就可以将 value 从 SST 中分离出来。下面将介绍 TitanTableBuilder 的主要工作流程:图 3:TitanTableBuilder 通过判断 value size 的大小来决定是否将 value 分离到 BlobFile 中去。如果 value size 大于等于 min_blob_size 则将 value 分离到 BlobFile ,并生成 index 写入 SST;如果 value size 小于 min_blob_size 则将 value 直接写入 SST。Titan 和 Badger 的设计有很大区别。Badger 直接将 WAL 改造成 VLog,这样做的好处是减少一次 Flush 的开销。而 Titan 不这么设计的主要原因有两个:假设 LSM-tree 的 max level 是 5,放大因子为 10,则 LSM-tree 总的写放大大概为 1 + 1 + 10 + 10 + 10 + 10,其中 Flush 的写放大是 1,其比值是 42 : 1,因此 Flush 的写放大相比于整个 LSM-tree 的写放大可以忽略不计。在第一点的基础上,保留 WAL 可以使 Titan 极大地减少对 RocksDB 的侵入性改动,而这也正是我们的设计目标之一。VersionTitan 使用 Version 来代表某个时间点所有有效的 BlobFile,这是从 LevelDB 中借鉴过来的管理数据文件的方法,其核心思想便是 MVCC,好处是在新增或删除文件的同时,可以做到并发读取数据而不需要加锁。每次新增文件或者删除文件的时候,Titan 都会生成一个新的 Version ,并且每次读取数据之前都要获取一个最新的 Version。图 4:新旧 Version 按顺序首尾相连组成一个双向链表,VersionSet 用来管理所有的 Version,它持有一个 current 指针用来指向当前最新的 Version。Garbage CollectionGarbage Collection (GC) 的目的是回收空间,一个高效的 GC 算法应该在权衡写放大和空间放大的同时,用最少的周期来回收最多的空间。在设计 GC 的时候有两个主要的问题需要考虑:何时进行 GC挑选哪些文件进行 GCTitan 使用 RocksDB 提供的两个特性来解决这两个问题,这两个特性分别是 TablePropertiesCollector 和 EventListener 。下面将讲解我们是如何通过这两个特性来辅助 GC 工作的。BlobFileSizeCollectorRocksDB 允许我们使用自定义的 TablePropertiesCollector 来搜集 SST 上的 properties 并写入到对应文件中去。Titan 通过一个自定义的 TablePropertiesCollector —— BlobFileSizeCollector 来搜集每个 SST 中有多少数据是存放在哪些 BlobFile 上的,我们将它收集到的 properties 命名为 BlobFileSizeProperties,它的工作流程和数据格式如下图所示:图 5:左边 SST 中 Index 的格式为:第一列代表 BlobFile 的文件 ID,第二列代表 blob record 在 BlobFile 中的 offset,第三列代表 blob record 的 size。右边 BlobFileSizeProperties 中的每一行代表一个 BlobFile 以及 SST 中有多少数据保存在这个 BlobFile 中,第一列代表 BlobFile 的文件 ID,第二列代表数据大小。EventListener我们知道 RocksDB 是通过 Compaction 来丢弃旧版本数据以回收空间的,因此每次 Compaction 完成后 Titan 中的某些 BlobFile 中便可能有部分或全部数据过期。因此我们便可以通过监听 Compaction 事件来触发 GC,通过搜集比对 Compaction 中输入输出 SST 的 BlobFileSizeProperties 来决定挑选哪些 BlobFile 进行 GC。其流程大概如下图所示:图 6:inputs 代表参与 Compaction 的所有 SST 的 BlobFileSizeProperties,outputs 代表 Compaction 生成的所有 SST 的 BlobFileSizeProperties,discardable size 是通过计算 inputs 和 outputs 得出的每个 BlobFile 被丢弃的数据大小,第一列代表 BlobFile 的文件 ID,第二列代表被丢弃的数据大小。Titan 会为每个有效的 BlobFile 在内存中维护一个 discardable size 变量,每次 Compaction 结束之后都对相应的 BlobFile 的 discardable size 变量进行累加。每次 GC 开始时就可以通过挑选 discardable size 最大的 BlobFile 来作为作为候选的文件。Sample每次进行 GC 前我们都会挑选一系列 BlobFile 作为候选文件,挑选的方法如上一节所述。为了减小写放大,我们可以容忍一定的空间放大,所以我们只有在 BlobFile 可丢弃的数据达到一定比例之后才会对其进行 GC。我们使用 Sample 算法来获取每个候选文件中可丢弃数据的大致比例。Sample 算法的主要逻辑是随机取 BlobFile 中的一段数据 A,计其大小为 a,然后遍历 A 中的 key,累加过期的 key 所在的 blob record 的 size 计为 d,最后计算得出 d 占 a 比值 为 r,如果 r >= discardable_ratio 则对该 BlobFile 进行 GC,否则不对其进行 GC。上一节我们已经知道每个 BlobFile 都会在内存中维护一个 discardable size,如果这个 discardable size 占整个 BlobFile 数据大小的比值已经大于或等于 discardable_ratio 则不需要对其进行 Sample。基准测试我们使用 go-ycsb 测试了 TiKV 在 Txn Mode 下分别使用 RocksDB 和 Titan 的性能表现,本节我会简要说明下我们的测试方法和测试结果。由于篇幅的原因,我们只挑选两个典型的 value size 做说明,更详细的测试分析报告将会放在下一篇文章。测试环境CPU:Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz(40个核心)Memory:128GB(我们通过 Cgroup 限制 TiKV 进程使用内存不超过 32GB)Disk:SATA SSD 1.5TB(fio 测试:4KB block size 混合随机读写情况下读写 IOPS 分别为 43.8K 和 18.7K)测试计划数据集选定的基本原则是原始数据大小(不算上写放大因素)要比可用内存小,这样可以防止所有数据被缓存到内存中,减少 Cache 所带来的影响。这里我们选用的数据集大小是 64GB,进程的内存使用限制是 32GB。Value SizeNumber of Keys (Each Key = 16 Bytes)Raw Data Size1KB64M64GB16KB4M64GB我们主要测试 5 个常用的场景:Data Loading Performance:使用预先计算好的 key 数量和固定的 value 大小,以一定的速度并发写入。Update Performance:由于 Titan 在纯写入场景下不需要 GC(BlobFile 中没有可丢弃数据),因此我们还需要通过更新来测试 GC 对性能的影响。Output Size:这一步我们会测量更新场景完成后引擎所占用的硬盘空间大小,以此反映 GC 的空间回收效果。Random Key Lookup Performance:这一步主要测试点查性能,并且点查次数要远远大于 key 的数量。Sorted Range Iteration Performance:这一步主要测试范围查询的性能,每次查询 2 million 个相连的 key。测试结果图 7 Data Loading Performance:Titan 在写场景中的性能要比 RocksDB 高 70% 以上,并且随着 value size 的变大,这种性能的差异会更加明显。值得注意的是,数据在写入 KV Engine 之前会先写入 Raft Log,因此 Titan 的性能提升会被摊薄,实际上裸测 RocksDB 和 Titan 的话这种性能差异会更大。图 8 Update Performance:Titan 在更新场景中的性能要比 RocksDB 高 180% 以上,这主要得益于 Titan 优秀的读性能和良好的 GC 算法。图 9 Output Size:Titan 的空间放大相比 RocksDB 略高,这种差距会随着 Key 数量的减少有略微的缩小,这主要是因为 BlobFile 中需要存储 Key 而造成的写放大。图 10 Random Key Lookup: Titan 拥有比 RocksDB 更卓越的点读性能,这主要得益与将 value 分离出 LSM-tree 的设计使得 LSM-tree 变得更小,因此 Titan 在使用同样的内存量时可以将更多的 index 、filter 和 DataBlock 缓存到 Block Cache 中去。这使得点读操作在大多数情况下仅需要一次 IO 即可(主要是用于从 BlobFile 中读取数据)。图 11 Sorted Range Iteration:Titan 的范围查询性能目前和 RocksDB 相比还是有一定的差距,这也是我们未来优化的一个重要方向。本次测试我们对比了两个具有代表性的 value size 在 5 种不同场景下的性能差异,更多不同粒度的 value size 的测试和更详细的性能报告我们会放在下一篇文章去说明,并且我们会从更多的角度(例如 CPU 和内存的使用率等)去分析 Titan 和 RocksDB 的差异。从本次测试我们可以大致得出结论,在大 value 的场景下,Titan 会比 RocksDB 拥有更好的写、更新和点读性能。同时,Titan 的范围查询性能和空间放大都逊于 RocksDB 。兼容性一开始我们便将兼容 RocksDB 作为设计 Titan 的首要目标,因此我们保留了绝大部分 RocksDB 的 API。目前仅有两个 API 是我们明确不支持的:MergeSingleDelete除了 Open 接口以外,其他 API 的参数和返回值都和 RocksDB 一致。已有的项目只需要很小的改动即可以将 RocksDB 实例平滑地升级到 Titan。值得注意的是 Titan 并不支持回退回 RocksDB。如何使用 Titan创建 DB#include <assert>#include “rocksdb/utilities/titandb/db.h”// Open DBrocksdb::titandb::TitanDB* db;rocksdb::titandb::TitanOptions options;options.create_if_missing = true;rocksdb::Status status = rocksdb::titandb::TitanDB::Open(options, “/tmp/testdb”, &db);assert(status.ok());…或#include <assert>#include “rocksdb/utilities/titandb/db.h”// open DB with two column familiesrocksdb::titandb::TitanDB* db;std::vector<rocksdb::titandb::TitanCFDescriptor> column_families;// have to open default column familycolumn_families.push_back(rocksdb::titandb::TitanCFDescriptor( kDefaultColumnFamilyName, rocksdb::titandb::TitanCFOptions()));// open the new one, toocolumn_families.push_back(rocksdb::titandb::TitanCFDescriptor( “new_cf”, rocksdb::titandb::TitanCFOptions()));std::vector<ColumnFamilyHandle*> handles;s = rocksdb::titandb::TitanDB::Open(rocksdb::titandb::TitanDBOptions(), kDBPath, column_families, &handles, &db);assert(s.ok());Status和 RocksDB 一样,Titan 使用 rocksdb::Status 来作为绝大多数 API 的返回值,使用者可以通过它检查执行结果是否成功,也可以通过它打印错误信息:rocksdb::Status s = …;if (!s.ok()) cerr << s.ToString() << endl;销毁 DBstd::string value;rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value);if (s.ok()) s = db->Put(rocksdb::WriteOptions(), key2, value);if (s.ok()) s = db->Delete(rocksdb::WriteOptions(), key1);在 TiKV 中使用 Titan目前 Titan 在 TiKV 中是默认关闭的,我们通过 TiKV 的配置文件来决定是否开启和设置 Titan,相关的配置项包括 [rocksdb.titan] 和 [rocksdb.defaultcf.titan], 开启 Titan 只需要进行如下配置即可:[rocksdb.titan]enabled = true注意一旦开启 Titan 就不能回退回 RocksDB 了。未来的工作优化 Iterator我们通过测试发现,目前使用 Titan 做范围查询时 IO Util 很低,这也是为什么其性能会比 RocksDB 差的重要原因之一。因此我们认为 Titan 的 Iterator 还存在着巨大的优化空间,最简单的方法是可以通过更加激进的 prefetch 和并行 prefetch 等手段来达到提升 Iterator 性能的目的。GC 速度控制和自动调节通常来说,GC 的速度太慢会导致空间放大严重,过快又会对服务的 QPS 和延时带来影响。目前 Titan 支持自动 GC,虽然可以通过减小并发度和 batch size 来达到一定程度限制 GC 速度的目的,但是由于每个 BlobFile 中的 blob record 数目不定,若 BlobFile 中的 blob record 过于密集,将其有效的 key 更新回 LSM-tree 时仍然可能堵塞业务的写请求。为了达到更加精细化的控制 GC 速度的目的,后续我们将使用 Token Bucket 算法限制一段时间内 GC 能够更新的 key 数量,以降低 GC 对 QPS 和延时的影响,使服务更加稳定。另一方面,我们也正在研究自动调节 GC 速度的算法,这样我们便可以,在服务高峰期的时候降低 GC 速度来提供更高的服务质量;在服务低峰期的时候提高 GC 速度来加快空间的回收。增加用于判断 key 是否存在的 APITiKV 在某些场景下仅需要判断某个 key 是否存在,而不需要读取对应的 value。通过提供一个这样的 API 可以极大地提高性能,因为我们已经看到将 value 移出 LSM-tree 之后,LSM-tree 本身会变的非常小,以至于我们可以将更多地 index、filter 和 DataBlock 存放到内存当中去,这样去检索某个 key 的时候可以做到只需要少量甚至不需要 IO 。 ...

January 23, 2019 · 4 min · jiezi

TiDB 3.0 Beta Release Notes

2019 年 1 月 19 日,TiDB 发布 3.0 Beta 版,对应 master branch 的 TiDB-Ansible。相比 2.1 版本,该版本对系统稳定性、优化器、统计信息以及执行引擎做了很多改进。TiDB新特性支持 View支持 Window Function支持 Range Partition支持 Hash PartitionSQL 优化器重新支持聚合消除的优化规则优化 NOT EXISTS 子查询,将其转化为 Anti Semi Join添加 tidb_enable_cascades_planner 变量以支持新的 Cascades 优化器。目前 Cascades 优化器尚未实现完全,默认关闭支持在事务中使用 Index Join优化 Outer Join 上的常量传播,使得对 Join 结果里和 Outer 表相关的过滤条件能够下推过 Outer Join 到 Outer 表上,减少 Outer Join 的无用计算量,提升执行性能调整投影消除的优化规则到聚合消除之后,消除掉冗余的 Project 算子优化 IFNULL 函数,当输入参数具有非 NULL 的属性的时候,消除该函数支持对 _tidb_rowid 构造查询的 Range,避免全表扫,减轻集群压力优化 IN 子查询为先聚合后做 Inner Join 并,添加变量 tidb_opt_insubq_to_join_and_agg 以控制是否开启该优化规则并默认打开支持在 DO 语句中使用子查询添加 Outer Join 消除的优化规则,减少不必要的扫表和 Join 操作,提升执行性能修改 TIDB_INLJ 优化器 Hint 的行为,优化器将使用 Hint 中指定的表当做 Index Join 的 Inner 表更大范围的启用 PointGet,使得当 Prepare 语句的执行计划缓存生效时也能利用上它引入贪心的 Join Reorder 算法,优化多表 Join 时 Join 顺序选择的问题支持 View支持 Window Function当 TIDB_INLJ 未生效时,返回 warning 给客户端,增强易用性支持根据过滤条件和表的统计信息推导过滤后数据的统计信息的功能增强 Range Partition 的 Partition Pruning 优化规则SQL 执行引擎优化 Merge Join 算子,使其支持空的 ON 条件优化日志,打印执行 EXECUTE 语句时使用的用户变量优化日志,为 COMMIT 语句打印慢查询信息支持 EXPLAIN ANALYZE 功能,使得 SQL 调优过程更加简单优化列很多的宽表的写入性能支持 admin show next_row_id添加变量 tidb_init_chunk_size 以控制执行引擎使用的初始 Chunk 大小完善 shard_row_id_bits,对自增 ID 做越界检查Prepare 语句对包含子查询的 Prepare 语句,禁止其添加到 Prepare 语句的执行计划缓存中,确保输入不同的用户变量时执行计划的正确性优化 Prepare 语句的执行计划缓存,使得当语句中包含非确定性函数的时候,该语句的执行计划也能被缓存优化 Prepare 语句的执行计划缓存,使得 DELETE/UPDATE/INSERT 的执行计划也能被缓存优化 Prepare 语句的执行计划缓存,当执行 DEALLOCATE 语句时从缓存中剔除对应的执行计划优化 Prepare 语句的执行计划缓存,通过控制其内存使用以避免缓存过多执行计划导致 TiDB OOM 的问题优化 Prepare 语句,使得 ORDER BY/GROUP BY/LIMIT 子句中可以使用 “?” 占位符权限管理增加对 ANALYZE 语句的权限检查增加对 USE 语句的权限检查增加对 SET GLOBAL 语句的权限检查增加对 SHOW PROCESSLIST 语句的权限检查Server支持了对 SQL 语句的 Trace 功能支持了插件框架支持同时使用 unix_socket 和 TCP 两种方式连接数据库支持了系统变量 interactive_timeout支持了系统变量 wait_timeout提供了变量 tidb_batch_commit,可以按语句数将事务分解为多个事务支持 ADMIN SHOW SLOW 语句,方便查看慢日志兼容性支持了 ALLOW_INVALID_DATES 这种 SQL mode提升了 load data 对 CSV 文件的容错能力支持了 MySQL 320 握手协议支持将 unsigned bigint 列声明为自增列支持 SHOW CREATE DATABASE IF NOT EXISTS 语法当过滤条件中包含用户变量时不对其进行谓词下推的操作,更加兼容 MySQL 中使用用户变量模拟 Window Function 的行为DDL支持快速恢复误删除的表支持动态调整 ADD INDEX 的并发数支持更改表或者列的字符集到 utf8/utf8mb4默认字符集从 utf8 变为 utf8mb4支持 RANGE PARTITIONToolsTiDB-Lightning大幅优化 SQL 转 KV 的处理速度对单表支持 batch 导入,提高导入性能和稳定性PD增加 RegionStorage 单独存储 Region 元信息增加 shuffle hot region 调度增加调度参数相关 Metrics增加集群 Label 信息相关 Metrics增加导入数据场景模拟修复 Leader 选举相关的 Watch 问题TiKV支持了分布式 GC在 Apply snapshot 之前检查 RocksDB level 0 文件,避免产生 Write stall支持了逆向 raw_scan 和 raw_batch_scan更好的夏令时支持支持了使用 HTTP 方式获取监控信息支持批量方式接收和发送 Raft 消息引入了新的存储引擎 Titan升级 gRPC 到 v1.17.2支持批量方式接收客户端请求和发送回复多线程 Apply线程 Raftstore英文版 Release Noteshttps://github.com/pingcap/docs/blob/master/releases/3.0beta.md ...

January 21, 2019 · 2 min · jiezi

TiDB 源码阅读系列文章(二十四)TiDB Binlog 源码解析

作者:姚维TiDB Binlog Overview这篇文章不是讲 TiDB Binlog 组件的源码,而是讲 TiDB 在执行 DML/DDL 语句过程中,如何将 Binlog 数据 发送给 TiDB Binlog 集群的 Pump 组件。目前 TiDB 在 DML 上的 Binlog 用的类似 Row-based 的格式。具体 Binlog 具体的架构细节可以参考这篇 文章。这里只描述 TiDB 中的代码实现。DML BinlogTiDB 采用 protobuf 来编码 binlog,具体的格式可以见 binlog.proto。这里讨论 TiDB 写 Binlog 的机制,以及 Binlog 对 TiDB 写入的影响。TiDB 会在 DML 语句提交,以及 DDL 语句完成的时候,向 pump 输出 Binlog。Statement 执行阶段DML 语句包括 Insert/Replace、Update、Delete,这里挑 Insert 语句来阐述,其他的语句行为都类似。首先在 Insert 语句执行完插入(未提交)之前,会把自己新增的数据记录在 binlog.TableMutation 结构体中。// TableMutation 存储表中数据的变化message TableMutation { // 表的 id,唯一标识一个表 optional int64 table_id = 1 [(gogoproto.nullable) = false]; // 保存插入的每行数据 repeated bytes inserted_rows = 2; // 保存修改前和修改后的每行的数据 repeated bytes updated_rows = 3; // 已废弃 repeated int64 deleted_ids = 4; // 已废弃 repeated bytes deleted_pks = 5; // 删除行的数据 repeated bytes deleted_rows = 6; // 记录数据变更的顺序 repeated MutationType sequence = 7;}这个结构体保存于跟每个 Session 链接相关的事务上下文结构体中 TxnState.mutations。 一张表对应一个 TableMutation 对象,TableMutation 里面保存了这个事务对这张表的所有变更数据。Insert 会把当前语句插入的行,根据 RowID + Row-value 的格式编码之后,追加到 TableMutation.InsertedRows 中:func (t *Table) addInsertBinlog(ctx context.Context, h int64, row []types.Datum, colIDs []int64) error { mutation := t.getMutation(ctx) pk, err := codec.EncodeValue(ctx.GetSessionVars().StmtCtx, nil, types.NewIntDatum(h)) if err != nil { return errors.Trace(err) } value, err := tablecodec.EncodeRow(ctx.GetSessionVars().StmtCtx, row, colIDs, nil, nil) if err != nil { return errors.Trace(err) } bin := append(pk, value…) mutation.InsertedRows = append(mutation.InsertedRows, bin) mutation.Sequence = append(mutation.Sequence, binlog.MutationType_Insert) return nil}等到所有的语句都执行完之后,在 TxnState.mutations 中就保存了当前事务对所有表的变更数据。Commit 阶段对于 DML 而言,TiDB 的事务采用 2-phase-commit 算法,一次事务提交会分为 Prewrite 阶段,以及 Commit 阶段。这里分两个阶段来看看 TiDB 具体的行为。Prewrite Binlog在 session.doCommit 函数中,TiDB 会构造 binlog.PrewriteValue:message PrewriteValue { optional int64 schema_version = 1 [(gogoproto.nullable) = false]; repeated TableMutation mutations = 2 [(gogoproto.nullable) = false];}这个 PrewriteValue 中包含了跟这次变动相关的所有行数据,TiDB 会填充一个类型为 binlog.BinlogType_Prewrite 的 Binlog:info := &binloginfo.BinlogInfo{ Data: &binlog.Binlog{ Tp: binlog.BinlogType_Prewrite, PrewriteValue: prewriteData, }, Client: s.sessionVars.BinlogClient.(binlog.PumpClient),}TiDB 这里用一个事务的 Option kv.BinlogInfo 来把 BinlogInfo 绑定到当前要提交的 transaction 对象中:s.txn.SetOption(kv.BinlogInfo, info)在 twoPhaseCommitter.execute 中,在把数据 prewrite 到 TiKV 的同时,会调用 twoPhaseCommitter.prewriteBinlog,这里会把关联的 binloginfo.BinlogInfo 取出来,把 Binlog 的 binlog.PrewriteValue 输出到 Pump。binlogChan := c.prewriteBinlog()err := c.prewriteKeys(NewBackoffer(prewriteMaxBackoff, ctx), c.keys)if binlogChan != nil { binlogErr := <-binlogChan // 等待 write prewrite binlog 完成 if binlogErr != nil { return errors.Trace(binlogErr) }}这里值得注意的是,在 prewrite 阶段,是需要等待 write prewrite binlog 完成之后,才能继续做接下去的提交的,这里是为了保证 TiDB 成功提交的事务,Pump 至少一定能收到 Prewrite Binlog。Commit Binlog在 twoPhaseCommitter.execute 事务提交结束之后,事务可能提交成功,也可能提交失败。TiDB 需要把这个状态告知 Pump:err = committer.execute(ctx)if err != nil { committer.writeFinishBinlog(binlog.BinlogType_Rollback, 0) return errors.Trace(err)}committer.writeFinishBinlog(binlog.BinlogType_Commit, int64(committer.commitTS))如果发生了 error,那么输出的 Binlog 类型就为 binlog.BinlogType_Rollback,如果成功提交,那么输出的 Binlog 类型就为 binlog.BinlogType_Commit。func (c *twoPhaseCommitter) writeFinishBinlog(tp binlog.BinlogType, commitTS int64) { if !c.shouldWriteBinlog() { return } binInfo := c.txn.us.GetOption(kv.BinlogInfo).(*binloginfo.BinlogInfo) binInfo.Data.Tp = tp binInfo.Data.CommitTs = commitTS go func() { err := binInfo.WriteBinlog(c.store.clusterID) if err != nil { log.Errorf(“failed to write binlog: %v”, err) } }()}值得注意的是,这里 WriteBinlog 是单独启动 goroutine 异步完成的,也就是 Commit 阶段,是不再需要等待写 binlog 完成的。这里可以节省一点 commit 的等待时间,这里不需要等待是因为 Pump 即使接收不到这个 Commit Binlog,在超过 timeout 时间后,Pump 会自行根据 Prewrite Binlog 到 TiKV 中确认当条事务的提交状态。DDL Binlog一个 DDL 有如下几个状态:const ( JobStateNone JobState = 0 JobStateRunning JobState = 1 JobStateRollingback JobState = 2 JobStateRollbackDone JobState = 3 JobStateDone JobState = 4 JobStateSynced JobState = 6 JobStateCancelling JobState = 7)这些状态代表了一个 DDL 任务所处的状态:JobStateNone,代表 DDL 任务还在处理队列,TiDB 还没有开始做这个 DDL。JobStateRunning,当 DDL Owner 开始处理这个任务的时候,会把状态设置为 JobStateRunning,之后 DDL 会开始变更,TiDB 的 Schema 可能会涉及多个状态的变更,这中间不会改变 DDL job 的状态,只会变更 Schema 的状态。JobStateDone, 当 TiDB 完成自己所有的 Schema 状态变更之后,会把 Job 的状态改为 Done。JobStateSynced,当 TiDB 每做一次 schema 状态变更,就会需要跟集群中的其他 TiDB 做一次同步,但是当 Job 状态为 JobStateDone 之后,在 TiDB 等到所有的 TiDB 节点同步之后,会将状态修改为 JobStateSynced。JobStateCancelling,TiDB 提供语法 ADMIN CANCEL DDL JOBS job_ids 用于取消某个正在执行或者还未执行的 DDL 任务,当成功执行这个命令之后,DDL 任务的状态会变为 JobStateCancelling。JobStateRollingback,当 DDL Owner 发现 Job 的状态变为 JobStateCancelling 之后,它会将 job 的状态改变为 JobStateRollingback,以示已经开始处理 cancel 请求。JobStateRollbackDone,在做 cancel 的过程,也会涉及 Schema 状态的变更,也需要经历 Schema 的同步,等到状态回滚已经做完了,TiDB 会将 Job 的状态设置为 JobStateRollbackDone。对于 Binlog 而言,DDL 的 Binlog 输出机制,跟 DML 语句也是类似的,只有开始处理事务提交阶段,才会开始写 Binlog 出去。那么对于 DDL 来说,跟 DML 不一样,DML 有事务的概念,对于 DDL 来说,SQL 的事务是不影响 DDL 语句的。但是 DDL 里面,上面提到的 Job 的状态变更,是作为一个事务来提交的(保证状态一致性)。所以在每个状态变更,都会有一个事务与之对应,但是上面提到的中间状态,DDL 并不会往外写 Binlog,只有 JobStateRollbackDone 以及 JobStateDone 这两种状态,TiDB 会认为 DDL 语句已经完成,会对外发送 Binlog,发送之前,会把 Job 的状态从 JobStateDone 修改为 JobStateSynced,这次修改,也涉及一次事务提交。这块逻辑的代码如下:worker.handleDDLJobQueue():if job.IsDone() || job.IsRollbackDone() { binloginfo.SetDDLBinlog(d.binlogCli, txn, job.ID, job.Query) if !job.IsRollbackDone() { job.State = model.JobStateSynced } err = w.finishDDLJob(t, job) return errors.Trace(err)}type Binlog struct { DdlQuery []byte DdlJobId int64}DdlQuery 会设置为原始的 DDL 语句,DdlJobId 会设置为 DDL 的任务 ID。对于最后一次 Job 状态的提交,会有两条 Binlog 与之对应,这里有几种情况:如果事务提交成功,类型分别为 binlog.BinlogType_Prewrite 和 binlog.BinlogType_Commit。如果事务提交失败,类型分别为 binlog.BinlogType_Prewrite 和 binlog.BinlogType_Rollback。所以,Pumps 收到的 DDL Binlog,如果类型为 binlog.BinlogType_Rollback 应该只认为如下状态是合法的:JobStateDone (因为修改为 JobStateSynced 还未成功)JobStateRollbackDone如果类型为 binlog.BinlogType_Commit,应该只认为如下状态是合法的:JobStateSyncedJobStateRollbackDone当 TiDB 在提交最后一个 Job 状态的时候,如果事务提交失败了,那么 TiDB Owner 会尝试继续修改这个 Job,直到成功。也就是对于同一个 DdlJobId,后续还可能会有多次 Binlog,直到出现 binlog.BinlogType_Commit。 ...

January 16, 2019 · 3 min · jiezi

TBSSQL 的那些事 | TiDB Hackathon 2018 优秀项目分享

本文作者是来自 TiBoys 队的崔秋同学,他们的项目 TBSSQL 在 TiDB Hackathon 2018 中获得了一等奖。TiDB Batch and Streaming SQL(简称 TBSSQL)扩展了 TiDB 的 SQL 引擎,支持用户以类似 StreamSQL 的语法将 Kafka、Pulsar 等外部数据源以流式表的方式接入 TiDB。通过简单的 SQL 语句,用户可以实现对流式数据的过滤,流式表与普通表的 Join(比如流式事实表与多个普通维度表),甚至通过 CREATE TABLE AS SELECT 语法将处理过的流式数据写入普通表中。此外,针对流式数据的时间属性,我们实现了基于时间窗口的聚合/排序算子,使得我们可以对流式数据进行时间维度的聚合/排序。序算起来这应该是第三次参加的 Hackathon 了,第一次参加的时候还是在小西天的豌豆荚,和东旭一起,做跨平台数据传输的工具,两天一夜;第二次和奇叔一起在 3W 咖啡,又是两天一夜;这次在自己家举办 Hackathon 比赛,下定决心一定要佛性一些,本着能抱大腿就不单干的心态,迅速决定拉唐长老(唐刘)下水。接下来就计划着折腾点啥,因为我们两个前端都不怎么样,所以只能硬核一些,于是拍了两个方案。方案一:之前跟唐长老合作过很长一段时间,我们两个对于测试质量之类的事情也都非常关注,所以想着能不能在 Chaos 系统上做一些文章,把一些前沿的测试理论和经验方法结合到系统里面来,做一套通用的分布式系统测试框架,就像 Jepsen 那样,用这套系统去测试和验证主流的开源分布式项目。方案二:越接近于业务实时性的数据处理越有价值,不管是 Kafka/KSQL,Flink/Spark Streaming 都是在向着实时流计算领域方向进行未来的探索。TiDB 虽然已经能够支持类 Real Time OLAP 的场景,但是对于更实时的流式数据处理方面还没有合适的解决方案,不过 TiDB 具有非常好的 Scale 能力,天然的能存储海量的数据库表数据,所以在 Streaming Event 和 Table 关联的场景下具有非常明显的优势。如果在 TiDB 上能够实现一个 Streaming SQL 的引擎,实现 Batch/Streaming 的计算融合,那将会是一件非常有意思的事情。因为打 Hackathon 比赛主要是希望折腾一些新的东西,所以我们两个简单讨论完了之后还是倾向于方案二,当然做不做的出来另说。当我们正准备做前期调研和设计的时候,Hackathon 主办方把唐长老拉去做现场导师,参赛规则规定导师不能下场比赛,囧,于是就这样被被动放了鸽子。好在后来遇到了同样被霸哥(韩飞)当导师而放鸽子的川总(杜川),川总对于 Streaming SQL 非常感兴趣,于是难兄难弟一拍即合,迅速决定抱团取暖。随后,Robot 又介绍了同样还没有组队的社区小伙伴 GZY(高志远),这样算是凑齐了三个人,但是一想到没有前端肯定搞不定,于是就拜托娘家人(Dashbase)的交际小王子 WPH(王鹏翰)出马,帮助去召唤一个靠谱的前端小伙伴,后来交际未果直接把自己卖进了队伍,这样终于凑齐了四后端,不,应该是三后端 + 一伪前端的组合。因为马上要准备提交项目和团队名称,大家都一致觉得方案二非常有意思,所以就选定了更加儒雅的 TBSSQL(TiDB Batch and Streaming SQL)作为项目名称,TSBSQL 遗憾落选。在团队名称方面,打酱油老男孩 / Scboy / TiStream / 养生 Hackathon / 佛系 Hackathon 都因为不够符合气质被遗憾淘汰,最后代表更有青春气息的 TiBoys 入选(跟着我左手右手一个慢动作,逃……前期准备所谓 “三军未动, 粮草先行”,既然已经报名了,还是要稍作准备,虽然已经确定了大的方向,但是具体的落地方案还没有细化,而且人员的分工也不是太明确。又经过一轮简单的讨论之后,明确了大家的职责方向,我这边主要负责项目整体设计,进度管理以及和 TiDB 核心相关的代码,川总主要负责 TiDB 核心技术攻关,GZY 负责流数据源数据的采集部分,WPH 负责前端展现以及 Hackathon 当天的 Demo 演示,分工之后大家就开始分头调研动工。作为这两年来基本没怎么写过代码的退役型选手来说,心里还是非常没底的,也不知道现在 TiDB 代码结构和细节变成什么样了,不求有功,但求别太拖后腿。对于项目本身的典型应用场景,大家还是比较明确的,觉得这个方向是非常有意义的。应用层系统:实时流事件和离线数据的关联查询,比如在线广告推荐系统,在线推荐系统,在线搜索,以及实时反欺诈系统等。内部数据系统:实时数据采样统计,比如内部监控系统;时间窗口数据分析系统,比如实时的数据流数据分析(分析一段时间内异常的数据流量和系统指标),用于辅助做 AI Ops 相关的事情(比如根据数据流量做节点自动扩容/自动提供参数调优/异常流量和风险报告等等)。业界 Streaming 相关的系统很多,前期我这边快速地看了下能不能站在巨人的肩膀上做事情,有没有可借鉴或者可借用的开源项目。Apache Beam本质上 Apache Beam 还是一个批处理和流处理融合的 SDK Model,用户可以在应用层使用更简单通用的函数接口实现业务的处理,如果使用 Beam 的话,还需要实现自定义的 Runner,因为 TiDB 本身主要的架构设计非常偏重于数据库方向,内部并没有特别明确的通用型计算引擎,所以现阶段基本上没有太大的可行性。当然也可以选择用 Flink 作为 Runner 连接 TiDB 数据源,但是这就变成了 Flink&TiDB 的事情了,和 Beam 本身关系其实就不大了。Apache Flink / Spark StreamingFlink 是一个典型的流处理系统,批处理可以用流处理来模拟出来。本身 Flink 也是支持 SQL 的,但是是一种嵌入式 SQL,也就是 SQL 和应用程序代码写在一起,这种做法的好处是可以直接和应用层进行整合,但是不好的地方在于,接口不是太清晰,有业务侵入性。阿里内部有一个增强版的 Flink 项目叫 Blink,在这个领域比较活跃。如果要实现批处理和流处理融合的话,需要内部定制和修改 Flink 的代码,把 TiDB 作为数据源对接起来,还有可能需要把一些环境信息提交给 TiDB 以便得到更好的查询结果,当然或许像 TiSpark 那样,直接 Flink 对接 TiKV 的数据源应该也是可以的。因为本身团队对于 Scala/Java 代码不是很熟悉,而且 Flink 的模式会有一定的侵入性,所以就没有在这方面进行更多的探索。同理,没有选择 Spark Streaming 也是类似的原因。当然有兴趣的小伙伴可以尝试下这个方向,也是非常有意思的。Kafka SQL因为 Kafka 本身只是一个 MQ,以后会向着流处理方向演进,但是目前并没有实现批处理和流处理统一的潜力,所以更多的我们只是借鉴 Kafka SQL 的语法。目前 Streaming SQL 还没有一个统一的标准 SQL,Kafka SQL 也只是一个 SQL 方言,支持的语法还比较简单,但是非常实用,而且是偏交互式的,没有业务侵入性。非常适合在 Hackathon 上做 Demo 演示,我们在项目实现中也是主要参考了 Kafka SQL 的定义,当然,Flink 和 Calcite 也有自己定义的 Streaming 语法,这里就不再讨论了。调研准备工作讨论到这里基本上也就差不多了,于是我们开始各自备(hua)战(shui),出差的出差,加班的加班,接客户的接客户,学 Golang 的学 Golang,在这种紧(fang)张(fei)无(zi)比(wo)的节奏中,迎来了 Hackathon 比赛的到来。Hackathon 流水账具体的技术实现方面都是比较硬核的东西,细节也比较多,扔在最后面写,免的大家看到一半就点×了。至于参加 Hackathon 的感受,因为不像龙哥那么文豪,也不像马老师那么俏皮,而且本来读书也不多,所以也只能喊一句“黑客马拉松真是太好玩了”!Day 13:30 AM由于飞机晚点,川总这个点儿才辗转到酒店。睡觉之前非常担心一觉睡过头,让这趟 Hackathon 之旅还没开始就结束了,没想到躺下以后满脑子都是技术细节,怎么都睡不着。漫漫长夜,无眠。7:45 AM川总早早来到 Hackathon 现场。由于来太早,其他选手都还没到,所以他提前刺探刺探敌情的计划也泡汤了,只好在赛场瞎晃悠一番熟悉熟悉环境,顺道跟大奖合了个影。11:00 AM简单的开幕式之后,Hackathon 正式开始。我们首先搞定的是 Streaming SQL 的语法定义以及 Parser 相关改动。这一部分在之前就经过比较详细的在线讨论了,所以现场只需要根据碰头后统一的想法一顿敲敲敲就搞定了。快速搞定这一块以后,我们就有了 SQL 语法层面的 Streaming 实现。当然此时 Streaming 也仅限于语法层面,Streaming 在 SQL 引擎层面对应的其实还是普通的TiDB Table。接下来是 DDL 部分。这一块我们已经想好了要复用 TiDB Table 的 Meta 结构 TableInfo ,因此主要工作就是按照 DDL源码解析 依葫芦画瓢,难度也不大,以至于我们还有闲心纠结一下 SHOW TABLES 语法里到底要不要屏蔽掉 Streaming Table 的问题。整体上来看上午的热身活动还是进行的比较顺利的,起码 Streaming DDL 这块没有成为太大的问题。这里面有个插曲就是我在 Hackathon 之前下载编译 TiDB,结果发现 TiDB 的 parser 已经用上时髦的 go module 了(也是好久好久没看 TiDB 代码),折腾好半天,不过好处就是 Hackathon 当天的时候改起来 parser 就比较轻车熟路了,所以赛前编译一个 TiDB 还是非常有必要的。15:30 PM随着热身的结束,马上迎来了稳定的敲敲敲阶段。川总简单弄了一个 Mock 的 StreamReader 然后丢给了我,因为我之前写 TiDB 的时候,时代比较遥远,那时候都还在用周 sir 的 Datum,现在一看,为了提高内存效率和性能,已经换成了高大上的 Chunk,于是一个很常见的问题:如何用最正确的做法把一个传过来的 Json 数据格式化成 Table Row 数据放到 Chunk 里面,让彻底我懵逼了。这里面倒不是技术的问题,主要是类型太多,如果枚举所有类型,搞起来很麻烦,按道理应该有更轻快的办法,但是翻了源代码还是没找到解决方案。这个时候果断去求助现场导师,也顺便去赛场溜(ci)达(tan)一(di)圈(qing)。随便扫了一眼,惊呆了,龙哥他们竟然已经开始写 PPT 了,之前知道龙哥他们强,但是没想到强到这个地步,还让不让大家一块欢快地玩耍了。同时,也了解到了不少非常有意思的项目,比如用机器学习方法去自动调节 TiDB 的调度参数,用 Lua 给 TiKV 添加 UDF 之类的,在 TiDB 上面实现异构数据库的关联查询(简直就是 F1 的大一统,而且听小道消息,他们都已经把 Join 推到 PG 上面去了,然而我们还没开始进入到核心开发流程),在 TiKV 上面实现时序数据库和 Memcached 协议等等,甚至东旭都按捺不住自己 Hackathon 起来了(嘻嘻,可以学学我啊 ;D )。本来还想去聊聊各个项目的具体实现方案,但是一想到自己挖了一堆坑还没填,只能默默回去膜拜 TiNiuB 项目。看起来不能太佛系了,于是乎我赶紧召开了一次内部团队 sync 的 catch up,明确下分工,川总开始死磕 TBSSQL 的核心逻辑 Streaming Aggregation 的实现,我这边继续搞不带 Aggregation 的 Streaming SQL 的其他实现,GZY 已经部署起来了 Pulsar,开始准备 Mock 数据,WPH 辅助 GZY 同时也快速理解我们的 Demo 场景,着手设计实现前端展现。18:00 PM我这边和面带慈父般欣慰笑容的老师(张建)进行了一些技术方案实现上的交流后,了解到目前社区小伙伴已经在搞 CREATE TABLE AS SELECT 的重要信息(后续证明此信息值大概一千块 RMB)。此时,在解决了之前的问题之后,TBSSQL 终于能跑通简单的 SELECT 语句了。我们心里稍微有点底了,于是一鼓作气,顺路也实现了带 Where 条件的 Stream Table 的 SELECT,以及 Stream Table 和 TiDB Table 的多表 Join,到这里,此时,按照分工,我这边的主体工作除了 Streaming Position 的持久化支持以外,已经写的差不多了,剩下就是去实现一些 Nice to have 的 DDL 的语法支持。川总这里首先要搞的是基于时间窗口的 Streaming Aggregation。按照我们的如意算盘,这里基本上可以复用 TiDB 现有的 Hash Aggregation 的计算逻辑,只需要加上窗口的处理就完事儿了。不过实际下手的时候仔细一研究代码,发现 Aggregation 这一块代码在川总疏于研究这一段时间已经被重构了一把,加上了一个并发执行的分支,看起来还挺复杂。于是一不做二不休,川总把 Hash Aggregation 的代码拷了一份,删除了并发执行的逻辑,在比较简单的非并发分支加上窗口相关实现。不过这种方法意味着带时间窗口的 Aggregation 得单独出 Plan,Planner 上又得改一大圈。这一块弄完以后,还没来得及调试,就到吃晚饭的点儿了。21:00 PM吃完晚饭,因为下午死磕的比较厉害,我和张建、川总出门去园区溜达了一圈。期间张建问我们搞得咋样了,我望了一眼川总,语重心长地说主要成败已经不在我了(后续证明这句语重心长至少也得值一千块 RMB),川总果断信心满满地说问题不大,一切尽在掌握之中。没想到这个 Flag 刚立起来还是温的,就立马被打脸了。问题出在吃饭前搞的聚合那块(具体细节可以看下后面的坑系列),为了支持时间窗口,我们必须确保 Streaming 上的窗口列能透传到聚合算子当中,为此我们屏蔽了优化器中窗口聚合上的列裁剪规则。可是实际运行当中,我们的修改并没有生效???而此时,川总昨天一整晚没睡觉的副作用开始显现出来了,思路已经有点不太清醒了。于是我们把张建拖过来一起 debug。然后我这边也把用 TiDB Global Variable 控制 Streaming Position 的功能实现了,并且和 GZY 这边也实现了 Mock 数据。之后,我也顺路休息休息,毕竟川总这边搞不定,我们这边搞的再好也没啥用。除了观摩川总和张建手把手,不,肩并肩结对小黑屋编程之外,我也顺便申请了部署 Kafka 联调的机器。23:00 PM我们这边最核心的功能还没突破,亮眼的 CREATE TABLE AS SELECT Streaming 也还没影,其实中期进度还是偏慢了(或者说之前我设计实现的功能的工作量太大了,看起来今天晚上只能死磕了,囧)。我调试 Kafka 死活调不通,端口可以 Telnet 登陆,但是写入和获取数据的时候一直报超时错误,而且我这边已经开始困上来了,有点扛不动了,后来在 Kafka 老司机 WPH 一起看了下配置参数,才发现 Advertise URL 设置成了本地地址,换成对外的 IP 就好了,当然为了简单方便,我们设置了单 Partition 的 Topic,这样 collector 的 Kafka 部分就搞的差不多了,剩下就是实现一个 http 的 restful api 来提供给 TiDB 的 StreamReader 读取,整个连通工作就差不多了。Day 200:00 AM这时候川总那边也传来了好消息,终于从 Streaming Aggregation 这个大坑里面爬出来了,后面也比较顺利地搞定了时间窗口上的聚合这块。此时时间已经到了 Hackathon 的第二天,不少其他项目的小伙伴已经收摊回家了。不过我们抱着能多做一个 Feature 是一个的心态,决定挑灯夜战。首先,川总把 Sort Executor 改了一把以支持时间窗口,可能刚刚的踩坑经历为我们攒了人品,Sort 上的改动竟然一次 AC 了。借着这股劲儿,我们又回头优化了一把 SHOW CREATE STREAM 的输出。这里有个插曲就是为了近距离再回味和感受下之前的开发流程,我们特意在 TiDB 的 repo 里面开了一个 tiboys/hackathon 的分支,然后提交的时候用了标准的 Pull Request 的方式,点赞了才能 merge(后来想想打 Hackathon 不是太可取,没什么用,还挺耽误时间,不知道当时怎么想的),所以在 master 分支和 tiboys/hackathon 分支看的时候都没有任何提交记录。嘻嘻,估计龙哥也没仔细看我们的 repo,所以其实在龙哥的激励下,我们的效率还是可以的 :) 。2:30 AMGZY 和 WPH 把今天安排的工作完成的差不多了,而且第二天还靠他们主要准备 Demo Show,就去睡觉了,川总也已经困得不行了,准备打烊睡觉。我和川总合计了一下,还差一个最重要的 Feature,抱着就试一把,不行就手工的心态,我们把社区的小伙伴王聪(bb7133)提的支持 CREATE TABLE AS SELECT 语法的 PR 合到了我们的分支,冲突竟然不是太多,然后稍微改了一下来支持 Streaming,结果一运行奇迹般地发现竟然能够运行,RP 全面爆发了,于是我们就近乎免费地增加了一个 Feature。改完这个地方,川总实在坚持不住了,就回去睡了。我这边的 http restful api 也搞的差不多了,准备联调一把,StreamReader 通过 http client 从 collector 读数据,collector 通过 kafka consumer 从 kafka broker 获取数据,结果获取的 Json 数据序列化成 TiDB 自定义的 Time 类型老是出问题,于是我又花了一些时间给 Time 增加了 Marshall 和 Unmarshal 的格式化支持,到这里基本上可以 work 了,看了看时间,凌晨四点半,我也准备去睡了。期间好几次看到霸哥(韩飞)凌晨还在一直帮小(tian)伙(zi)伴(ji)查(wa)问(de)题(keng),其实霸哥认真的时候还是非常靠谱的。7:30 AM这个时候人陆陆续续地来了,我这边也进入了打酱油的角色,年纪大了确实刚不动了,吃了早餐之后,开始准备思考接下来的分工。因为大家都是临时组队,到了 Hackathon 才碰面,基本上没有太多磨合,而且普遍第二天状态都不大好。虽然大家都很努力,但是在我之前设计的宏大项目面前,还是感觉人力不太够,所以早上 10 点我们开了第二次 sync 的 catch up,讨论接下来的安排。我去负责更新代码和 GitHub 的 Readme,川总最后再简单对代码扫尾,顺便和 GZY 去录屏(罗伯特小姐姐介绍的不翻车经验),WPH 准备画图和 PPT,因为时间有限,前端展现部分打算从卖家秀直接转到买家秀。11 点敲定代码完全封板,然后安心准备 PPT 和下午的 Demo。14:00 PM因为抽签抽的比较靠后,主要事情在 WPH 这边,我和川总基本上也没什么大事了,顺手搞了几幅图,然后跟马老师还有其他项目的小伙伴们开始八卦聊天。因为正好周末,家里妹子买东西顺便过来慰问了下。下午主要听了各个 Team 的介绍,欣赏到了极尽浮夸的 LOGO 动画,Get 到了有困难找 Big Brother 的新技能,学习和了解了很有意思的 Idea,真心觉得这届 Hackathon 做的非常值得回忆。从最后的现场展示情况来看,因为 TBSSQL 内容比较多,真的展示下来,感觉 6 分钟时间还是太赶,好在 WPH Demo 的还是非常顺利的,把我们做的事情都展示出来了。因为砍掉了一些前端展现的部分(这块我们也确实不怎么擅长),其实对于 Hackathon 项目是非常吃亏的,不过有一点比较欣慰,就像某光头大佬说的,评委们都是懂技术的。因为实现完整性方面能做的也都搞差不多了,打的虽然很累但是也很开心,对于结果也就不怎么纠结了。因为川总晚上的飞机,小伙伴们简单沟通了几句,一致同意去园区找个地吃个晚饭,于是大家拉上霸哥去了“头一号”,也是第一次吃了大油条,中间小伙伴们各种黑谁谁谁写的 bug 巴拉巴拉的,后来看手机群里有人 @ 我说拿奖了。其实很多项目各方面综合实力都不错,可以说是各有特色,很难说的上哪个项目有绝对的优势。我们之前有讨论过,TBSSQL 有获奖的赢面,毕竟从完整性,实用性和生态方面都是有潜质的,但是能获得大家最高的认可还是小意外的,特别感谢各位技术大佬们,也特别感谢帮助我们领奖的满分罗伯特小姐姐。最后大家补了一张合照,算是为这次 Hackathon 画下一个句号。至此,基本上 Hackathon 的流水账就记录完了,整个项目地址在 https://github.com/qiuyesuifeng/tidb 欢迎大家关注和讨论。选读:技术实现TLDR: 文章很长,挑感兴趣的部分看看就可以了。在前期分析和准备之后,基本上就只有在 TiDB 上做 SQL Streaming 引擎一条路可选了,细化了下要实现的功能以及简单的系统架构,感觉工作量还是非常大的。下面简单介绍下系统架构和各个模块的功能:在数据源采集部分(collector),我们计划选取几种典型的数据源作为适配支持。Kafka最流行的开源 MQ 系统,很多 Streaming 系统对接的都是 Kafka。Pulsar流行的开源 MQ 系统,目前比较火爆,有赶超 Kafka 的势头。Binlog支持 MySQL/TiDB Binlog 处理,相当于是 MySQL Trigger 功能的升级加强版了。我们对之前的 MySQL -> TiDB 的数据同步工具 Syncer 也比较熟悉,所以这块工作量应该也不大。Log常见的 Log 日志,这个就没什么好解释的了。为了方便 Demo 和协作,collector 除了适配不同的数据源,还会提供一个 restful api 的接口,这样 TBSSQL 就可以通过 pull 的方式一直获取 streaming 的数据。因为 collector 主要是具体的工程实现,所以就不在这里细节展开了,感兴趣的话,可以参考下 相关代码。要在 TiDB 中实现 Streaming 的功能即 TBSSQL,就需要在 TiDB 内部深入定制和修改 TiDB 的核心代码。Streaming 有两个比较本质的特征:Streaming 具有流式特性,也就是说,其数据可以是一直增长,无穷无尽的。而在 Batch 系统(暂时把 MySQL/TIDB 这种数据在一定时间内相对稳定的系统简称 Batch 系统,下面都会沿用这种说法)当中,每个 SQL 的输入数据集是固定,静态的。Streaming 具有时序特性。每一条数据都有其内在的时间属性(比如说事件发生时间等),数据之间有先后顺序关系。而在 Batch 系统当中,一个表中的数据在时间维度上是无序的。因此,要在 TiDB SQL 引擎上支持 Streaming SQL,所涉及到的算子都需要根据 Streaming 的这两个特点做修改。以聚合函数(Aggregation)为例,按照 SQL 语义,聚合算子的实现应该分成两步:首先是 Grouping, 即对输入按照聚合列进行分组;然后是 Execute, 即在各个分组上应用聚合函数进行计算,如下图所示。对于 Streaming,因为其输入可以是无尽的,Grouping 这个阶段永远不可能结束,所以按照老套路,聚合计算就没法做了。这时,就要根据 Streaming 的时序特性对 Streaming 数据进行分组。每一个分组被称为一个 Time Window(时间窗口)。就拿最简单的 Tumbling Window 来说,可以按照固定的时间间隔把 Streaming 输入切分成一个个相互无交集的窗口,然后在每一个窗口上就可以按照之前的方式进行聚合了。聚合算子只是一个比较简单的例子,因为其只涉及一路输入。如果要修改多路输入的算子(比如说 Join 多个 Streaming),改动更复杂。此外,时间窗口的类型也是多种多样,刚刚例子中的 Tumbling Window 只是基础款,还有复杂一点的 Hopping Window 以及更复杂的 Sliding Window。在 Hackathon 的有限时间内,我们既要考虑实现难度,又要突出 Batch / Streaming 融合处理的特点,因此在技术上我们做出如下抉择:时间窗口只做最基本的 Tumbling Window。实现基于时间窗口的 Aggregation 和 Sort 作为经典流式算子的代表。实现单 Streaming Join 多 Batch Table 作为 Batch / Streaming 融合的示例, 多个 Streaming Join 太复杂,因为时间有限就先不做了。支持 Streaming 处理结果写入 Batch Table(TiDB Table)这种常见但是非常实用的功能。也就是说要支持 CREATE TABLE AS SELECT xxx FROM streaming 的类似语法。此外,既然是要支持 Streaming SQL,选择合适的 SQL 语法也是必要的,需要在 Parser 和 DDL 部分做相应的修改。单整理下,我们的 Feature List 如下图所示:下面具体聊聊我们实现方案中的一些关键选择。Streaming SQL 语法Streaming SQL 语法的核心是时间窗口的定义,Time Window 和一般 SQL 中的 Window Function 其实语义上是有区别的。在 Streaming SQL 中,Time Window 主要作用是为后续的 SQL 算子限定输入的范围,而在一般的 SQL 中,Window Funtion 本身就是一个 SQL 算子,里面的 Window 其实起到一个 Partition 的作用。在纯 Streaming 系统当中,这种语义的差别影响不大,反而还会因为语法的一致性降低用户的学习成本,但是在 TBSSQL 这种 Batch / Streaming 混合场景下,同一套语法支持两种语义,会对用户的使用造成一定困扰,特别是在 TiDB 已经被众多用户应用到生产环境这种背景下,这种语义上的差别一定要体现在语法的差异上。Sreaming DDLDDL 这一块实现难度不大,只要照着 DDL源码解析 依葫芦画瓢就行。这里值得一提的是在 Meta 层,我们直接(偷懒)复用了 TableInfo 结构(加了判断是否为 Streaming 的 Flag 和一些表示 Streaming 属性的字段)来表示 Streaming Table。这个选择主要是从实现难度上考虑的,毕竟复用现有的结构是最快最安全的。但是从设计思想上看,这个决定其实也暗示了在 TBSSQL 当中,Streaming 是 Table 的一种特殊形式,而不是一个独立的概念。理解这一点很重要,因为这是一些其他设计的依据。比如按照以上设定,那么从语义上讲,在同一个 DB 下 Streaming 和普通 Table 就不能重名,反之的话这种重名就是可以接受的。StreamReader这一块主要有两个部分,一个是适配不同的数据源(collector),另一个是将 Streaming 数据源引入 TiDB 计算引擎(StreamReader)。collector 这部分上面已经介绍过了,这里就不再过多介绍了。StreamReader 这一块,主要要修改由 LogicalPlan 生成 PhysicalPlan(具体代码),以及由 PhysicalPlan 生成 Executor Operator Tree 的过程(具体代码)。StreamReader 的 Open 方法中,会利用 Meta 中的各种元信息来初始化与 collector 之间的连接,然后在 Next 方法中通过 Pull 的方式不断拉取数据。对时间窗口的处理前面我们提到,时间窗口是 Streaming 系统中的核心概念。那么这里就有一个重要的问题,Time Window 中的 Time 如何界定?如何判断什么时候应该切换 Window?最容易想到,也是最简单粗暴的方式,就是按照系统的当前时间来进行切割。这种方式问题很大,因为:数据从生成到被 TBSSQL 系统接收到,肯定会有一定的延迟,而且这个延迟时间是没有办法精确预估的。因此在用户实际场景中,除非是要测量收发延迟,这个系统时间对用户没有太大意义。考虑到算子并发执行的可能性(虽然还没有实现),不同机器的系统时间可能会有些许偏差,这个偏差对于 Window 操作来说可能导致致命的误差,也会导致结果的不精确(因为 Streaming 源的数据 Shuffle 到不同的处理节点上,系统时间的误差可能不太一样,可能会导致 Window 划分的不一样)。因此,比较合理的方式是以 Streaming 中的某一 Timestamp 类型的列来切分窗口,这个值由用户在应用层来指定。当然 Streaming 的 Schema 中可能有多个 Timestamp 列,这里可以要求用户指定一个作为 Window 列。在实现 Demo 的时候,为了省事,我们直接限定了用户 Schema 中只能有一个时间列,并且以该列作为 Window 列(具体代码)。当然这里带来一个问题,就是 Streaming 的 Schema 中必须有 Timestamp 列,不然这里就没法玩了。为此,我们在创建 Streaming 的 DDL 中加了 检查逻辑,强制 Streaming 的 Schema 必须有 Timestamp 列(其实我们也没想明白当初 Hackathon 为啥要写的这么细,这些细节为后来通宵埋下了浓重的伏笔,只能理解为程序猿的本能,希望这些代码大家看的时候吐槽少一些)。Streaming DML这里简单 DML 指的就是不依赖时间窗口的 DML,比如说只带 Selection 和 Projection 的SELECT 语句,或者单个 Streaming Join 多个 Table。因为不依赖时间窗口,支持这类 DML 实际上不需要对计算层做任何改动,只要接入 Streaming 数据源就可以了。对于 Streaming Join Table(如上图表示的是 Stream Join User&Ads 表的示意图) 可以多说一点,如果不带 Time Window,其实这里需要修改一下Planner。因为 Streaming 的流式特性,这里可能没法获取其完整输入集,因此就没法对 Streaming 的整个输入进行排序,所以 Merge Join 算法这里就没法使用了。同理,也无法基于 Streaming 的整个输入建 Hash 表,因此在 Hash Join 算法当中也只能某个普通表 Build Hash Table。不过,在我们的 Demo 阶段,输入其实也是还是有限的,所以这里其实没有做,倒也影响不大。基于时间窗口的 Aggregation 和 Sort在 TBSSQL 当中,我们实现了基于固定时间窗的 Hash Aggregation Operator 和 Sort Operator。这里比较正规的打法其实应该是实现一个独立的 TimeWindow,各种基于时间窗口的 Operator 可以切换时间窗的逻辑,然后比如 Aggregation 和 Sort 这类算子只关心自己的计算逻辑。 但是这样一来要对 Planner 做比较大的改动,想想看难度太大了,所以我们再一次采取了直(tou)接(lan)的方法,将时间窗口直接实现分别实现在 Aggregation 和 Sort 内部,这样 Planner 这块不用做伤筋动骨的改动,只要在各个分支逻辑上修修补补就可以了。对于 Aggregation,我们还做了一些额外的修改。Aggregation 的输出 Schema 语义上来说只包括聚合列和聚合算子的输出列。但是在引入时间窗口的情况下,为了区分不同的窗口的聚合输出,我们为聚合结果显式加上了两个 Timestamp 列 window_start 和 window_end, 来表示窗口的开始时间和结束时间。为了这次这个小特性,我们踩到一个大坑,费了不少劲,这个后面再仔细聊聊。支持 Streaming 处理结果写入 Batch Table因为 TiDB 本身目前还暂时不支持 CREATE TABLE AS SELECT … 语法,而从头开始搞的话工作量又太大,因此我们一度打算放弃这个 Feature。后面经过老司机提醒,我们发现社区的小伙伴王聪(bb7133)已经提了一个 PR 在做这个事情了。本着试一把的想法我们把这个 PR 合到我们的分支上一跑,结果竟然没多少冲突,还真能 Work……稍微有点问题的是如果 SELECT 子句中有带时间窗口的聚合,输出的结果不太对。仔细研究了一下发现,CREATE TABLE AS SELECT 语句中做 LogicalPlan 的路径和直接执行 SELECT 时做 LogicalPlan 的入口不太一致,以至于对于前者,我们做 LogicalPlan 的时候遗漏了一些 Streaming 相关信息。这里稍作修改以后,也能够正常运行了。遇到的困难和坑本着前人采坑,后人尽量少踩的心态聊聊遇到的一些问题,主要的技术方案上面已经介绍的比较多了。限于篇幅,只描述遇到的最大的坑——消失的窗口列的故事。在做基于时间窗口的 Aggregation 的时候,我们要按照用户指定的窗口列来切窗口。但是根据 列裁剪 规则,如果这个窗口列没有被用作聚合列或者在聚合函数中被使用,那么这一列基本上会被优化器裁掉。这里的修改很简单(我们以为),只需要在聚合的列裁剪逻辑中,如果发现聚合带时间窗口,那么直接不做裁剪就完事儿了(代码)。三下五除二修改完代码,编译完后一运行,结果……瞬间 Panic 了……Debug 一看,发现刚刚的修改没有生效,Streaming 的窗口列还是被裁剪掉了,随后我们又把 Planner 的主要流程看了一遍,还是没有在其他地方发现有类似的裁剪逻辑。这时我们意识到事情没有这么简单了,赶忙从导师团搬来老司机(还是上面那位)。我们一起用简单粗暴的二分大法和 Print 大法,在生成 LogicalPlan,PhysicalPlan 和 Executor 前后将各个算子的 Schema 打印出来。结果发现,在 PhysicalPlan 完成后,窗口列还是存在的,也就是说我们的修改是生效了的,但是在生成 Executor 以后,这一列却神秘消失了。所以一开始我们定位的思路就错了,问题出在生成 Executor 的过程,但是我们一直在 Planner 中定位,当然找不到问题。明确了方向以后,我们很快就发现了元凶。在 Build HashAggregation 的时候,有一个不起眼的函数调用 buildProjBelowAgg,这个函数悄悄地在 Aggregation 算子下面加塞了一个 Projection 算子,顺道又做了一把列裁剪,最为头疼的是,因为这个 Projection 算子是在生成 Executor 阶段才塞进去的,而 EXPLAIN 语句是走不到这里来的,所以这个 Projection 算子在做 Explain 的时候是看不见的,想当于是一个隐形的算子,所以我们就这样华丽丽地被坑了,于是就有了罗伯特小姐姐听到的那句 “xxx,出来挨打” 的桥段。今后的计划从立项之初,我们就期望 TBSSQL 能够作为一个正式的 Feature 投入生产环境。为此,在设计和实现过程中,如果能用比较优雅的解决方案,我们都尽量不 Hack。但是由于时间紧迫和能力有限,目前 TBSSQL 还是处于 Demo 的阶段,离实现这个目标还有很长的路要走。1. Streaming 数据源在对接 Streaming 数据源这块,目前 TBSSQL 有两个问题。首先,TBSSQL 默认输入数据是按照窗口时间戳严格有序的。这一点在生产环境中并不一定成立(比如因为网络原因,某一段数据出现了乱序)。为此,我们需要引入类似 Google MillWheel 系统中 Low Watermark 的机制来保证数据的有序性。其次,为了保证有序,目前 StreamReader 只能单线程运行。在实际生产环境当中,这里很可能因为数据消费速度赶不上上游数据生产速度,导致上游数据源的堆积,这又会反过来导致产生计算结果的时间和数据生产时间之间的延迟越来越大。为了解决这个问题,我们需要将 StreamReader 并行化,而这又要求基于时间窗口的计算算子能够对多路数据进行归并排序。另外,目前采用 TiDB Global Variable 来模拟 Streaming 的位置信息,其实更好地方案是设计用一个 TiDB Table 来记录每个不同 StreamReader 读取到的数据位置,这种做法更标准。2. Planner在 Planner 这块,从前面的方案介绍可以看出,Streaming 的流式特性和时序特性决定了 Streaming SQL 的优化方式和一般 SQL 有所不同。目前 TBSSQL 的实现方式是在现有 Planner 的执行路径上加上一系列针对 Streaming SQL 的特殊分支。这种做法很不优雅,既难以理解,也难以扩展。目前,TiDB 正在基于 Cascade 重构 Planner 架构,我们希望今后 Streaming SQL 的相关优化也基于新的 Planner 框架来完成。3. 时间窗口目前,TBSSQL 只实现了最简单的固定窗口。在固定窗口上,Aggregation、Sort 等算子很大程度能复用现有逻辑。但是在滑动窗口上,Aggregation、Sort 的计算方式和在 Batch Table 上的计算方式会完全不一样。今后,我们希望 TBSSQL 能够支持完善对各种时间窗口类型的支持。4. 多 Streaming 处理目前 TBSSQL 只能处理单路 Streaming 输入,比如单个 Streaming 的聚合,排序,以及单个Streaming 和多个 Table 之间的 Join。多个 Streaming 之间的 Join 因为涉及多个 Streaming 窗口的对齐,目前 TBSSQL 暂不支持,所以 TBSSQL 目前并不是一个完整的 Streaming SQL 引擎。我们计划今后对这一块加以完善。TBSSQL 是一个复杂的工程,要实现 Batch/Streaming 的融合,除了以上提到这四点,TBSSQL 还有很有很多工作要做,这里就不一一详述了。或许,下次 Hackathon 可以再继续搞一把 TBSSQL 2.0 玩玩:) 有点遗憾的是作为选手出场,没有和所有优秀的参赛的小伙伴们畅谈交流,希望有机会可以补上。属于大家的青春不散场,TiDB Hackathon 2019,不见不散~~ ...

December 28, 2018 · 6 min · jiezi

TiPrometheus:基于 TiDB 的 TSDB | TiDB Hackathon 2018 优秀项目分享

本文作者是菜哥和他的朋友们队的于畅同学,他们的项目 TiPrometheus 已经被 Prometheus adapter 合并。该项目分两个小项目,分别解决了时序数据的存储与计算问题。存储主要兼容 Prometheus 语法和数据格式,实现了精确查询、模糊查询,完全兼容现有语法。所有数据仅存在 TiKV 中。计算主要通过 TiKV 调用 Lua 实现,通过 Lua 动态扩展实现数据计算的功能。项目简介既然你关注了 TiDB, 想必你一定是个关注 Infrastructure 的硬汉(妹)子。监控作为 Infra 不可或缺的一环,其核心便是 TSDB(time series database) 。TSDB 是一种以时间为主要索引的数据库,主要用来存储大量以时间为序列的指标数据,数据结构也比较简单,通常包括特征信息,指标数据和 timestamp。常见的 TSDB 包括 InfluxDB, OpenTSDB, Prometheus。而 Prometheus 是一整套监控系统,时序数据库是它的存储部分,下面这张架构图来自于 Prometheus 官方,简单概括了其架构和生态的组成。Prometheus 还支持一个图上没有体现的功能 Remote Storage,可以进行远程的读写,对查询是透明的。这个功能主要是用来做长存储。我们的项目就是实现了一个基于 TiKV 的 TSDB 来做 Prometheus 的 Remote Storage。核心实现Prometheus 记录的数据结构分为两部分 label, samples。label 记录了一些特征信息。samples 包含了指标数据和 timestamp。“labels”: [{ “job”: “node”, “instance”: “123.123.1.211:9090”,}]“samples”:[{ “timestamp”: 1473305798 “value”: 0.9}]label 和时间范围结合,可以查询到需要的 value。为了查询这些记录,我们需要构建两种索引 label index 和 time index,并以特殊的 key 存储 value。label index每对 label 为会以 index🏷️<name>#<latency> 为key,labelID 为 value 存入。新的记录会追加到 value 后面。这是一种搜索中常用的倒排索引。time index每个 sample 项会以 index:timeseries:<labelID>:<splitTime> 为 key,timestamp 为 value。splitTime为时间切片的起始点。新的 timestamp 会追加到 value 后面。doc 存储我们将每一条 samples 记录以 timeseries:doc:<labelID>:<timestamp> 为 key 存入 TiKV,其中 labelID 是 label 全文的散列值。下面做一个梳理写入过程生成 labelID构建 label index,index🏷️<name>#<latency> “labelID,labelID"构建 time index,index:timeseries:<labelID>:<splitTime> “ts,ts"写入时序数据,timeseries:doc:<labelID>:<timestamp> “value"查询过程根据倒排索引查出 labelID 的集合,多对 label 的查询会对 labelID 集合求交集。根据 labelID 和时间范围内的时间分片查询包含的 timestamp。根据 labelID 和 timestamp 查出所需的 value。扯完这些没用的我们来聊些正经的。我们为什么要做这样一个项目在 2018 年下半年,PingCAP 组织的 Hackathon,当时作为萌新即将参加比赛,想着一定要文体两开花,弘扬开源文化。萌生了四个想法:TiKV TSDBMachine Learning on TiSpark魔改 TiKV + Lua 做成 mapreducegeo 全文检索核心想法能做出来,符合参赛要求。确实能解决生产问题而不是一个比赛项目。摸了摸头发,觉得 ML on TiSpark 太硬核,根本做不完。TiHaoop 也太硬核,也做不完。geo 没在厂里的生产中遇到什么问题。最后辗转反侧思考一番,拍脑袋决定双线操作,做基于 TiKV 的 TSDB 和 TiKV + Lua,完成时序检索功能的同时,增加更丰富的算子(比赛前两天才想好做什么)。比赛过程周五原计划,提前看看 rust,作为 rust 萌新。于是前一天和同事借了本 rust 书,准备一天速成 rust。后来发现还是看电视剧更管用。Day1(周六)周六参加比赛的时候,原以为会有个很长的开场致辞,所以决定 10 点再去。到了现场,发现大家已经开始撸代码了???整体过程还算顺利,但其中也遇到了一些问题。Prometheus 的依赖和 TiKV 的一些依赖不兼容,于是 fork 一份 Prometheus 依赖,野路子改两行,兼容了。下午 5 点的时候,时序基本实现了,但联调发现有数据读写不一致的情况。因菜哥的一个 bug 导致,然后开始了漫长的 debug,一共历时 5 个小时(特别说明,我们组叫菜哥和他的朋友们)。晚 10 点,准备回家了,不准备再 debug 了,一个 bug 查了 5 个小时。作为娱乐队,熬夜写代码是不可能。各回各家,各找各妈。Day2(周日)开始漫长的半天精通 Lua 虚拟机 + rust。也遇到了一些问题,比如为什么 TiKV 编译这么慢???一天只有 24 次编译机会???下午 2 点,作为第一个讲的团队,我们及时生成了一个 PPT ,毕竟 PPT 工程师的基础还在。一周后的周一之前写的渣代码,简单写了个 README。抱着尝试的心态,给 Prometheus adapter 提了个 PR。然后,居然被合进去了!!!一下午写的代码居然被合进去了!!!成果彻底打通了 TiKV 和 Prometheus。为 TiKV 的时序存储和计算提供了一个思路(之前做过 TiDB 存储时序数据)。为 Prometheus 的长存储提供了一个还算好用的方案(M3 其实还可以,Thanos 是分片机制,不能算真正意义的分布式存储)。已在公司生产环境试用,需要经过大数据量的测试,如果没问题计划替代现有方案。感悟参加 Hackathon,和周末加两天班没有太大的区别。最先开始来,只是想混个奖品,比如说书包。去年参加 DevCon 给的布袋用了一年,还没坏,今年准备再领一个。见到了很多年龄比我们小,但技术又还不错的小伙伴,比如兰海他们组,udf 那个组。也见到了一些年龄稍长的参赛者。他们的存在,让我们在充满杂事的日常工作中又有了继续奋斗的动力。似乎,当时选择这个行业没有错,而不仅仅是一份工作。Just for fun。感谢感谢唐刘老师和申砾老师的指导。感谢 PingCAP 举办了这场大型网友见面活动,收获颇丰。项目地址:https://github.com/bragfoo/TiPrometheus (代码比较渣,思路供参考)打个广告:由菜哥和他的朋友们翻译的书:《Go 语言并发之道》已登陆京东、淘宝。非常棒一本 Go 语言书籍,搜索即可购买。参考资料:https://fabxc.org/tsdb/https://docs.influxdata.com/influxdb/v1.7/concepts/storage_engine/https://github.com/prometheus/prometheus/tree/release-1.8/storage ...

December 28, 2018 · 2 min · jiezi

TiDB Ecosystem Tools 原理解读系列(三)TiDB-DM 架构设计与实现原理

作者:张学程简介TiDB-DM(Data Migration)是用于将数据从 MySQL/MariaDB 迁移到 TiDB 的工具。该工具既支持以全量备份文件的方式将 MySQL/MariaDB 的数据导入到 TiDB,也支持通过解析执行 MySQL/MariaDB binlog 的方式将数据增量同步到 TiDB。特别地,对于有多个 MySQL/MariaDB 实例的分库分表需要合并后同步到同一个 TiDB 集群的场景,DM 提供了良好的支持。如果你需要从 MySQL/MariaDB 迁移到 TiDB,或者需要将 TiDB 作为 MySQL/MariaDB 的从库,DM 将是一个非常好的选择。架构设计DM 是集群模式的,其主要由 DM-master、DM-worker 与 DM-ctl 三个组件组成,能够以多对多的方式将多个上游 MySQL 实例的数据同步到多个下游 TiDB 集群,其架构图如下:DM-master:管理整个 DM 集群,维护集群的拓扑信息,监控各个 DM-worker 实例的运行状态;进行数据同步任务的拆解与分发,监控数据同步任务的执行状态;在进行合库合表的增量数据同步时,协调各 DM-worker 上 DDL 的执行或跳过;提供数据同步任务管理的统一入口。DM-worker:与上游 MySQL 实例一一对应,执行具体的全量、增量数据同步任务;将上游 MySQL 的 binlog 拉取到本地并持久化保存;根据定义的数据同步任务,将上游 MySQL 数据全量导出成 SQL 文件后导入到下游 TiDB,或解析本地持久化的 binlog 后增量同步到下游 TiDB;编排 DM-master 拆解后的数据同步子任务,监控子任务的运行状态。DM-ctl:命令行交互工具,通过连接到 DM-master 后,执行 DM 集群的管理与数据同步任务的管理。实现原理数据迁移流程单个 DM 集群可以同时运行多个数据同步任务;对于每一个同步任务,可以拆解为多个子任务同时由多个 DM-worker 节点承担,其中每个 DM-worker 节点负责同步来自对应的上游 MySQL 实例的数据。对于单个 DM-worker 节点上的单个数据同步子任务,其数据迁移流程如下,其中上部的数据流向为全量数据迁移、下部的数据流向为增量数据同步:在每个 DM-worker 节点内部,对于特定的数据同步子任务,主要由 dumper、loader、relay 与 syncer(binlog replication)等数据同步处理单元执行具体的数据同步操作。对于全量数据迁移,DM 首先使用 dumper 单元从上游 MySQL 中将表结构与数据导出成 SQL 文件;然后使用 loader 单元读取这些 SQL 文件并同步到下游 TiDB。对于增量数据同步,首先使用 relay 单元作为 slave 连接到上游 MySQL 并拉取 binlog 数据后作为 relay log 持久化存储在本地,然后使用 syncer 单元读取这些 relay log 并解析构造成 SQL 语句后同步到下游 TiDB。这个增量同步的过程与 MySQL 的主从复制类似,主要区别在于在 DM 中,本地持久化的 relay log 可以同时供多个不同子任务的 syncer 单元所共用,避免了多个任务需要重复从上游 MySQL 拉取 binlog 的问题。数据迁移并发模型为加快数据导入速度,在 DM 中不论是全量数据迁移,还是增量数据同步,都在其中部分阶段使用了并发处理。对于全量数据迁移,在导出阶段,dumper 单元调用 mydumper 导出工具执行实际的数据导出操作,对应的并发模型可以直接参考 mydumper 的源码。在使用 loader 单元执行的导入阶段,对应的并发模型结构如下:使用 mydumper 执行导出时,可以通过 –chunk-filesize 等参数将单个表拆分成多个 SQL 文件,这些 SQL 文件对应的都是上游 MySQL 某一个时刻的静态快照数据,且各 SQL 文件间的数据不存在关联。因此,在使用 loader 单元执行导入时,可以直接在一个 loader 单元内启动多个 worker 工作协程,由各 worker 协程并发、独立地每次读取一个待导入的 SQL 文件进行导入。即 loader 导入阶段,是以 SQL 文件级别粒度并发进行的。在 DM 的任务配置中,对于 loader 单元,其中的 pool-size 参数即用于控制此处 worker 协程数量。对于增量数据同步,在从上游拉取 binlog 并持久化到本地的阶段,由于上游 MySQL 上 binlog 的产生与发送是以 stream 形式进行的,因此这部分只能串行处理。在使用 syncer 单元执行的导入阶段,在一定的限制条件下,可以执行并发导入,对应的模型结构如下:当 syncer 读取与解析本地 relay log 时,与从上游拉取 binlog 类似,是以 stream 形式进行的,因此也只能串行处理。当 syncer 解析出各 binlog event 并构造成待同步的 job 后,则可以根据对应行数据的主键、索引等信息经过 hash 计算后分发到多个不同的待同步 job channel 中;在 channel 的另一端,与各个 channel 对应的 worker 协程并发地从 channel 中取出 job 后同步到下游的 TiDB。即 syncer 导入阶段,是以 binlog event 级别粒度并发进行的。在 DM 的任务配置中,对于 syncer 单元,其中的 worker-count 参数即用于控制此处 worker 协程数量。但 syncer 并发同步到下游 TiDB 时,存在一些限制,主要包括:对于 DDL,由于会变更下游的表结构,因此必须确保在旧表结构对应的 DML 都同步完成后,才能进行同步。在 DM 中,当解析 binlog event 得到 DDL 后,会向每一个 job channel 发送一个特殊的 flush job;当各 worker 协程遇到 flush job 时,会立刻向下游 TiDB 同步之前已经取出的所有 job;等各 job channel 中的 job 都同步到下游 TiDB 后,开始同步 DDL;等待 DDL 同步完成后,继续同步后续的 DML。即 DDL 不能与 DML 并发同步,且 DDL 之前与之后的 DML 也不能并发同步。sharding 场景下 DDL 的同步处理见后文。对于 DML,多条 DML 可能会修改同一行的数据,甚至是主键。如果并发地同步这些 DML,则可能造成同步后数据的不一致。DM 中对于 DML 之间的冲突检测与处理,与 TiDB-Binlog 中的处理类似,具体原理可以阅读《TiDB EcoSystem Tools 原理解读(一)TiDB-Binlog 架构演进与实现原理》中关于 Drainer 内 SQL 之间冲突检测的讨论。合库合表数据同步在使用 MySQL 支撑大量数据时,经常会选择使用分库分表的方案。但当将数据同步到 TiDB 后,通常希望逻辑上进行合库合表。DM 为支持合库合表的数据同步,主要实现了以下的一些功能。table router为说明 DM 中 table router(表名路由)功能,先看如下图所示的一个例子:在这个例子中,上游有 2 个 MySQL 实例,每个实例有 2 个逻辑库,每个库有 2 个表,总共 8 个表。当同步到下游 TiDB 后,希望所有的这 8 个表最终都合并同步到同一个表中。但为了能将 8 个来自不同实例、不同库且有不同名的表同步到同一个表中,首先要处理的,就是要能根据某些定义好的规则,将来自不同表的数据都路由到下游的同一个表中。在 DM 中,这类规则叫做 router-rules。对于上面的示例,其规则如下:name-of-router-rule: schema-pattern: “schema_” table-pattern: “table_” target-schema: “schema” target-table: “table"name-of-router-rule:规则名,用户指定。当有多个上游实例需要使用相同的规则时,可以只定义一条规则,多个不同的实例通过规则名进行引用。schema-pattern:用于匹配上游库(schema)名的模式,支持在尾部使用通配符()。这里使用 schema_ 即可匹配到示例中的两个库名。table-pattern:用于匹配上游表名的模式,与 schema-pattern 类似。这里使用 table_* 即可匹配到示例中的两个表名。target-schema:目标库名。对于库名、表名匹配的数据,将被路由到这个库中。target-table:目标表名。对于库名、表名匹配的数据,将被路由到 target-schema 库下的这个表中。在 DM 内部实现上,首先根据 schema-pattern / table-pattern 构造对应的 trie 结构,并将规则存储在 trie 节点中;当有 SQL 需要同步到下游时,通过使用上游库名、表名查询 trie 即可得到对应的规则,并根据规则替换原 SQL 中的库名、表名;通过向下游 TiDB 执行替换后的 SQL 即完成了根据表名的路由同步。有关 router-rules 规则的具体实现,可以阅读 TiDB-Tools 下的 table-router pkg 源代码。column mapping有了 table router 功能,已经可以完成基本的合库合表数据同步了。但在数据库中,我们经常会使用自增类型的列作为主键。如果多个上游分表的主键各自独立地自增,将它们合并同步到下游后,就很可能会出现主键冲突,造成数据的不一致。我们可看一个如下的例子:在这个例子中,上游 4 个需要合并同步到下游的表中,都存在 id 列值为 1 的记录。假设这个 id 列是表的主键。在同步到下游的过程中,由于相关更新操作是以 id 列作为条件来确定需要更新的记录,因此会造成后同步的数据覆盖前面已经同步过的数据,导致部分数据的丢失。在 DM 中,我们通过 column mapping 功能在数据同步的过程中依据指定规则对相关列的数据进行转换改写来避免数据冲突与丢失。对于上面的示例,其中 MySQL 实例 1 的 column mapping 规则如下:mapping-rule-of-instance-1: schema-pattern: “schema_” table-pattern: “table_” expression: “partition id” source-column: “id” target-column: “id” arguments: [“1”, “schema_”, “table_”] mapping-rule-of-instance-1:规则名,用户指定。由于不同的上游 MySQL 实例需要转换得到不同的值,因此通常每个 MySQL 实例使用一条专有的规则。schema-pattern / table-pattern:上游库名、表名匹配模式,与 router-rules 中的对应配置项一致。expression:进行数据转换的表达式名。目前常用的表达式即为 “partition id”,有关该表达式的具体说明见下文。source-column:转换表达式的输入数据对应的来源列名,“id” 表示这个表达式将作用于表中名为 id 的列。暂时只支持对单个来源列进行数据转换。target-column:转换表达式的输出数据对应的目标列名,与 source-column 类似。暂时只支持对单个目标列进行数据转换,且对应的目标列必须已经存在。arguments:转换表达式所依赖的参数。参数个数与含义依具体表达式而定。partition id 是目前主要受支持的转换表达式,其通过为 bigint 类型的值增加二进制前缀来解决来自不同表的数据合并同步后可能产生冲突的问题。partition id 的 arguments 包括 3 个参数,分别为:MySQL 实例 ID:标识数据的来源 MySQL 实例,用户自由指定。如 “1” 表示匹配该规则的数据来自于 MySQL 实例 1,且这个标识将被转换成数值后以二进制的形式作为前缀的一部分添加到转换后的值中。库名前缀:标识数据的来源逻辑库。如 “schema_” 应用于 schema_2 逻辑库时,表示去除前缀后剩下的部分(数字 2)将以二进制的形式作为前缀的一部分添加到转换后的值中。表名前缀:标识数据的来源表。如 “table_” 应用于 table_3 表时,表示去除前缀后剩下的部分(数字 3)将以二进制的形式作为前缀的一部分添加到转换后的值中。各部分在经过转换后的数值中的二进制分布如下图所示(各部分默认所占用的 bits 位数如图所示):假如转换前的原始数据为 123,且有如上的 arguments 参数设置,则转换后的值为:1<<(64-1-4) | 2<<(64-1-4-7) | 3<<(64-1-4-7-8) | 123另外,arguments 中的 3 个参数均可设置为空字符串(""),即表示该部分不被添加到转换后的值中,且不占用额外的 bits。比如将其设置为[“1”, “”, “table_"],则转换后的值为:1 << (64-1-4) | 3<< (64-1-4-8) | 123有关 column mapping 功能的具体实现,可以阅读 TiDB-Tools 下的 column-mapping pkg 源代码。sharding DDL有了 table router 和 column mapping 功能,DML 的合库合表数据同步已经可以正常进行了。但如果在增量数据同步的过程中,上游待合并的分表上执行了 DDL 操作,则可能出现问题。我们先来看一个简化后的在分表上执行 DDL 的例子。在上图的例子中,分表的合库合表简化成了上游只有两个 MySQL 实例,每个实例内只有一个表。假设在开始数据同步时,将两个分表的表结构 schema 的版本记为 schema V1,将 DDL 执行完成后的表结构 schema 的版本记为 schema V2。现在,假设数据同步过程中,从两个上游分表收到的 binlog 数据有如下的时序:开始同步时,从两个分表收到的都是 schema V1 的 DML。在 t1 时刻,收到实例 1 上分表的 DDL。从 t2 时刻开始,从实例 1 收到的是 schema V2 的 DML;但从实例 2 收到的仍是 schema V1 的 DML。在 t3 时刻,收到实例 2 上分表的 DDL。从 t4 时刻开始,从实例 2 收到的也是 schema V2 的 DML。假设在数据同步过程中,不对分表的 DDL 进行处理。当将实例 1 的 DDL 同步到下游后,下游的表结构会变更成为 schema V2。但对于实例 2,在 t2 时刻到 t3 时刻这段时间内收到的仍然是 schema V1 的 DML。当尝试把这些与 schema V1 对应的 DML 同步到下游时,就会由于 DML 与表结构的不一致而发生错误,造成数据无法正确同步。继续使用上面的例子,来看看我们在 DM 中是如何处理合库合表过程中的 DDL 同步的。在这个例子中,DM-worker-1 用于同步来自 MySQL 实例 1 的数据,DM-worker-2 用于同步来自 MySQL 实例 2 的数据,DM-master 用于协调多个 DM-worker 间的 DDL 同步。从 DM-worker-1 收到 DDL 开始,简化后的 DDL 同步流程为:DM-worker-1 在 t1 时刻收到来自 MySQL 实例 1 的 DDL,自身暂停该 DDL 对应任务的 DDL 及 DML 数据同步,并将 DDL 相关信息发送给 DM-master。DM-master 根据 DDL 信息判断需要协调该 DDL 的同步,为该 DDL 创建一个锁,并将 DDL 锁信息发回给 DM-worker-1,同时将 DM-worker-1 标记为这个锁的 owner。DM-worker-2 继续进行 DML 的同步,直到在 t3 时刻收到来自 MySQL 实例 2 的 DDL,自身暂停该 DDL 对应任务的数据同步,并将 DDL 相关信息发送给 DM-master。DM-master 根据 DDL 信息判断该 DDL 对应的锁信息已经存在,直接将对应锁信息发回给 DM-worker-2。DM-master 根据启动任务时的配置信息、上游 MySQL 实例分表信息、部署拓扑信息等,判断得知已经收到了需要合表的所有上游分表的该 DDL,请求 DDL 锁的 owner(DM-worker-1)向下游同步执行该 DDL。DM-worker-1 根据 step 2 时收到的 DDL 锁信息验证 DDL 执行请求;向下游执行 DDL,并将执行结果反馈给 DM-master;若执行 DDL 成功,则自身开始继续同步后续的(从 t2 时刻对应的 binlog 开始的)DML。DM-master 收到来自 owner 执行 DDL 成功的响应,请求在等待该 DDL 锁的所有其他 DM-worker(DM-worker-2)忽略该 DDL,直接继续同步后续的(从 t4 时刻对应的 binlog 开始的)DML。根据上面 DM 处理多个 DM-worker 间的 DDL 同步的流程,归纳一下 DM 内处理多个 DM-worker 间 sharding DDL 同步的特点:根据任务配置与 DM 集群部署拓扑信息,在 DM-master 内建立一个需要协调 DDL 同步的逻辑 sharding group,group 中的成员为处理该任务拆解后各子任务的 DM-worker。各 DM-worker 在从 binlog event 中获取到 DDL 后,会将 DDL 信息发送给 DM-master。DM-master 根据来自 DM-worker 的 DDL 信息及 sharding group 信息创建/更新 DDL 锁。如果 sharding group 的所有成员都收到了某一条 DDL,则表明上游分表在该 DDL 执行前的 DML 都已经同步完成,可以执行 DDL,并继续后续的 DML 同步。上游分表的 DDL 在经过 table router 转换后,对应需要在下游执行的 DDL 应该一致,因此仅需 DDL 锁的 owner 执行一次即可,其他 DM-worker 可直接忽略对应的 DDL。从 DM 处理 DM-worker 间 sharding DDL 同步的特点,可以看出该功能存在以下一些限制:上游的分表必须以相同的顺序执行(table router 转换后相同的)DDL,比如表 1 先增加列 a 后再增加列 b,而表 2 先增加列 b 后再增加列 a,这种不同顺序的 DDL 执行方式是不支持的。一个逻辑 sharding group 内的所有 DM-worker 对应的上游分表,都应该执行对应的 DDL,比如其中有 DM-worker-2 对应的上游分表未执行 DDL,则其他已执行 DDL 的 DM-worker 都会暂停同步任务,等待 DM-worker-2 收到对应上游的 DDL。由于已经收到的 DDL 的 DM-worker 会暂停任务以等待其他 DM-worker 收到对应的 DDL,因此数据同步延迟会增加。增量同步开始时,需要合并的所有上游分表结构必须一致,才能确保来自不同分表的 DML 可以同步到一个确定表结构的下游,也才能确保后续各分表的 DDL 能够正确匹配与同步。在上面的示例中,每个 DM-worker 对应的上游 MySQL 实例中只有一个需要进行合并的分表。但在实际场景下,一个 MySQL 实例可能有多个分库内的多个分表需要进行合并,比如前面介绍 table router 与 column mapping 功能时的例子。当一个 MySQL 实例中有多个分表需要合并时,sharding DDL 的协调同步过程增加了更多的复杂性。假设同一个 MySQL 实例中有 table_1 和 table_2 两个分表需要进行合并,如下图:由于数据来自同一个 MySQL 实例,因此所有数据都是从同一个 binlog 流中获得。在这个例子中,时序如下:开始同步时,两个分表收到的数据都是 schema V1 的 DML。在 t1 时刻,收到了 table_1 的 DDL。从 t2 时刻到 t3 时刻,收到的数据同时包含 table_1 schema V2 的 DML 及 table_2 schema V1 的 DML。在 t3 时刻,收到了 table_2 的 DDL。从 t4 时刻开始,两个分表收到的数据都是 schema V2 的 DML。假设在数据同步过程中不对 DDL 进行特殊处理,当 table_1 的 DDL 同步到下游、变更下游表结构后,table_2 schema V1 的 DML 将无法正常同步。因此,在单个 DM-worker 内部,我们也构造了与 DM-master 内类似的逻辑 sharding group,但 group 的成员是同一个上游 MySQL 实例的不同分表。但 DM-worker 内协调处理 sharding group 的同步不能完全与 DM-master 处理时一致,主要原因包括:当收到 table_1 的 DDL 时,同步不能暂停,需要继续解析 binlog 才能获得后续 table_2 的 DDL,即需要从 t2 时刻继续向前解析直到 t3 时刻。在继续解析 t2 时刻到 t3 时刻的 binlog 的过程中,table_1 的 schema V2 的 DML 不能向下游同步;但在 sharding DDL 同步并执行成功后,这些 DML 需要同步到下游。在 DM 中,简化后的 DM-worker 内 sharding DDL 同步流程为:在 t1 时刻收到 table_1 的 DDL,记录 DDL 信息及此时的 binlog 位置点信息。继续向前解析 t2 时刻到 t3 时刻的 binlog。对于属于 table_1 的 schema V2 DML,忽略;对于属于 table_2 的 schema V1 DML,正常同步到下游。在 t3 时刻收到 table_2 的 DDL,记录 DDL 信息及此时的 binlog 位置点信息。根据同步任务配置信息、上游库表信息等,判断该 MySQL 实例上所有分表的 DDL 都已经收到;将 DDL 同步到下游执行、变更下游表结构。设置新的 binlog 流的解析起始位置点为 step 1 时保存的位置点。重新开始解析从 t2 时刻到 t3 时刻的 binlog。对于属于 table_1 的 schema V2 DML,正常同步到下游;对于属于 table_2 的 shema V1 DML,忽略。解析到达 step 4 时保存的 binlog 位置点,可得知在 step 3 时被忽略的所有 DML 都已经重新同步到下游。继续从 t4 时刻对应的 binlog 位置点正常同步。从上面的分析可以知道,DM 在处理 sharding DDL 同步时,主要通过两级 sharding group 来进行协调控制,简化的流程为:各 DM-worker 独立地协调对应上游 MySQL 实例内多个分表组成的 sharding group 的 DDL 同步。当 DM-worker 内所有分表的 DDL 都收到时,向 DM-master 发送 DDL 相关信息。DM-master 根据 DM-worker 发来的 DDL 信息,协调由各 DM-worker 组成的 sharing group 的 DDL 同步。当 DM-master 收到所有 DM-worker 的 DDL 信息时,请求 DDL lock 的 owner(某个 DM-worker)执行 DDL。owner 执行 DDL,并将结果反馈给 DM-master;自身开始重新同步在内部协调 DDL 同步过程中被忽略的 DML。当 DM-master 发现 owner 执行 DDL 成功后,请求其他所有 DM-worker 开始继续同步。其他所有 DM-worker 各自开始重新同步在内部协调 DDL 同步过程中被忽略的 DML。所有 DM-worker 在重新同步完成被忽略的 DML 后,继续正常同步。数据同步过滤在进行数据同步的过程中,有时可能并不需要将上游所有的数据都同步到下游,这时一般期望能在同步过程中根据某些规则,过滤掉部分不期望同步的数据。在 DM 中,支持 2 种不同级别的同步过滤方式。库表黑白名单DM 在 dumper、loader、syncer 三个处理单元中都支持配置规则只同步/不同步部分库或表。对于 dumper 单元,其实际调用 mydumper 来 dump 上游 MySQL 的数据。比如只期望导出 test 库中的 t1、t2 两个表的数据,则可以为 dumper 单元配置如下规则:name-of-dump-rule: extra-args: “-B test -T t1,t2"name-of-dump-rule:规则名,用户指定。当有多个上游实例需要使用相同的规则时,可以只定义一条规则,多个不同的实例通过规则名进行引用。extra-args:dumper 单元额外参数。除 dumper 单元中明确定义的配置项外的其他所有 mydumper 配置项都通过此参数传入,格式与使用 mydumper 时一致。有关 mydumper 对库表黑白名单的支持,可查看 mydumper 的参数及 mydumper 的源码。对于 loader 和 syncer 单元,其对应的库表黑白名单规则为 black-white-list。假设只期望同步 test 库中的 t1、t2 两个表的数据,则可配置如下规则:name-of-bwl-rule: do-tables: - db-name: “test” tbl-name: “t1” - db-name: “test” tbl-name: “t2"示例中只使用了该规则的部分配置项,完整的配置项及各配置项的含义,可阅读该功能对应的用户文档。DM 中该规则与 MySQL 的主从同步过滤规则类似,因此也可参考 Evaluation of Database-Level Replication and Binary Logging Options 与 Evaluation of Table-Level Replication Options。对于 loader 单元,在解析 SQL 文件名获得库名表名后,会与配置的黑白名单规则进行匹配,如果匹配结果为不需要同步,则会忽略对应的整个 SQL 文件。对于 syncer 单元,在解析 binlog 获得库名表名后,会与配置的黑白名单规则进行匹配,如果匹配结果为不需要同步,则会忽略对应的(部分)binlog event 数据。binlog event 过滤在进行增量数据同步时,有时会期望过滤掉某些特定类型的 binlog event,两个典型的场景包括:上游执行 TRUNCATE TABLE 时不希望清空下游表中的数据。上游分表上执行 DROP TABLE 时不希望 DROP 下游合并后的表。在 DM 中支持根据 binlog event 的类型进行过滤,对于需要过滤 TRUNCATE TABLE 与 DROP TABLE 的场景,可配置规则如下:name-of-filter-rule: schema-pattern: “test_” table-pattern: “t_” events: [“truncate table”, “drop table”] action: Ignore规则的匹配模式与 table router、column mapping 类似,具体的配置项可阅读该功能对应的用户文档。在实现上,当解析 binlog event 获得库名、表名及 binlog event 类型后,与配置的规则进行匹配,并在匹配后依据 action 配置项来决定是否需要进行过滤。有关 binlog event 过滤功能的具体实现,可以阅读 TiDB-Tools 下的 binlog-filter pkg 源代码。 ...

December 27, 2018 · 6 min · jiezi

管理日志、IoT和事件数据的设计模式

Trafodion在IoT(物联网)空间、电信和网络安全中的一个常见应用场景是用一个非常大的单表,记录实时事件。用户希望快速摄取新数据,查询数据,并清理过时的数据。对于这种情况,我们一般建议客户使用一种设计模式。该模式包含三个要素:Salting、分块和Stripe合并。Salting第一个要素是salting,在集群中平均分布数据。通过salting 不仅平均分布全部数据,而且在集群中的所有节点均匀分布热(最新)数据。Salting基于哈希散列,运用哈希散列函数计算每一行的 region 号。一般情况下,这是基于运营型查询中使用的一列或多列,比如客户id或设备id。Trafodion自动管理salt。计算哈希散列函数,并自动对Salt列执行条件判断。SQL的Insert、Select和Delete语句不需要任何特殊的操作。数据均匀分布之后,我们面临下一个问题——选择一个恰当的行键。为了让数据查询、过时数据清理和合并更容易,我们需要在每个region的末尾添加新行,但并不是随机添加。我们还希望按日期范围对时间序列的数据进行查询,并对其他重要的列(例如,customer id或device id)进行查询。为了实现以上两个目标,我们紧接着在salt后面采用了另一个键前缀——分块标识(divisionid)。分块是一个时间范围,比如一天、一周或一个月。它将该时间范围内所有的行全部归并在一起,以便我们选择一列(例如,customer id)作为主键列。这类情景中典型的运营型查询如下所示:SELECT * FROM tWHERE cust_id = x AND transaction_timestamp BETWEEN y AND z在cust_id上做Salting可确保用户x的所有行均放在同一个HBase region 中。Trafodion通过对该salt列进行条件判断,确保查询仅访问该 region 服务器。分块按列分块确保我们只需要读取相关时间范围中的数据,而非多年的历史数据。此外,我们能够使用cust_id作为主键列,从而在数百万用户数据中迅速为我们的用户锁定所需数据。有时我们想要查询多位用户的数据和多天的数据。Trafodion采用独特的多维访问方法(MDAM)将多个复杂的条件判断分离,只扫描相关范围,略过中间不需要的数据,从而有效地实现该目标。我们已经证明,通过salting和分块,很容易在时间结构表中插入数据,并且在查询时仅需要访问特定时间范围和关键列中的所需的数据。Stripe合并还有两个容易忽视的问题:数据过时处理和HBase主合并。为此,HBase的stripe合并可以起到很大的作用。将HBase region中的数据分成多个stripe,每个Strip对应一个键的范围(比如每个stripe中存储一个月的数据)。只合并这些stripe内的文件。这意味着数月不曾改变的历史数据不需要通过压缩重复改写。这些数据保持原状,直到过时(即删除)。到那时,一些“stripe”中的数据清空,然后与更新的非空stripe合并。总之:Trafodion和HBase提供了三个强大的设计要素,使您能够在一个表中存储大量基于时间的数据。对于SQL查询,salting、分块和stripe合并是透明的。通过这三个要素可以有效地实现数据摄取、查询和过时处理。

December 26, 2018 · 1 min · jiezi

TiEye:Region 信息变迁历史可视化工具 | TiDB Hackathon 2018 优秀项目分享

本文作者是矛盾螺旋队的成员刘玮,他们的项目 TiEye 在 TiDB Hackathon 2018 中获得了三等奖。TiEye 是 Region 信息变迁历史可视化工具,通过 PD记录 Region 的Split、Merge、ConfChange、LeaderChange 等信息,可以方便的回溯 Region 某个时间的具体状态,为开发人员提供了方便的可视化展示界面及查询功能。TiKV 的 RegionRegion 是 TiKV 的一个数据调度单元,TiKV 将数据按照键值范围划分为很多个 Region,分在集群的多台机器上,通过调度 Region 来实现负载均衡以及数据存储的扩展,同时一个 Region 也是一个 Raft Group,一个 Region 分布在多个 TiKV 实例上(通常是 3 个或者 5 个),通过 Raft 算法保证多副本的强一致性。动机这个项目的灵感是之前在查一些问题的时候想到的,因为我们很多时候需要去知道 Region 在某个时间的状态,这就需要通过日志从杂乱的信息中提取出来有用的信息来复原当时的场景,但实际并不是特别方便高效,尤其是在看多个 Region 之间的关系的时候。因此通过将 Region 信息变化历史可视化,希望能为开发者们在定位问题的时候提供一个方便直观的工具,同时还能通过它来分析 PD 的调度策略,以及调度带来的写放大问题等等。实际方案一开始我们考虑的是通过一个独立的服务去解析 PD 的日志来获取 Region 信息的变化历史,后来讨论后认为这样做不仅依赖于 PD 中的日志格式,造成系统耦合,同时 PD 的 leader 变迁导致日志内容不连续,以及日志中的信息并不是特别充分等问题也增加了开发难度。因此我们最后决定直接修改 PD 的源代码,在每次 PD 变更 Region 的时候,记录下这些信息并持久化。这样既能保证在 PD 切换 leader 后的变化信息的连续性,又提供了更加丰富的历史信息。同时,PD 添加相关的 API,以供前端进行查询。Hackathon 回顾我们的团队由三个人组成,分别是我(刘玮)、周振靖和张博康,都毕业于北京邮电大学。我们在这次 Hackathon 之前就认识,因为大家都在北京,因此交流还是蛮方便的,在开赛大约一周前就确定了这个题目。其实我最初的想法是做一些有关于性能优化的事情,但是在跟队友们交流后还是决定做 Region 历史可视化,其更具有实用性,也更适合在 Hackathon 上来做。10:00 比赛正式开始。我们之前已经讨论好了项目的大体架构,因此没有再做过多的讨论就各自开始码代码了。博康负责后端框架以及 PD 相应的修改,我负责后端查询 API,振靖负责前端可视化。12:15 午餐。休息片刻,继续码代码。14:00 后端框架大体完成,已经可以在 PD 中收集 Region 相应的状态变化;前端部分已经画出简单的 Region 分裂、合并等示意图。17:30 完成了最简单的查询逻辑,进行了第一次联调,发现大家对于 Region 状态的展示方式理解不一样,于是再次讨论统一了意见。18:00 晚餐时间。19:00 ~ 次日 2:30: 我们基本完成了后端开发,而前端这时还剩比较多的工作量。同时晚上在前端展示,后端查询 API,数据持久化方面都发现了几个 bug,大家一直忙到很晚才一一解决。次日 9:00 返回赛场,抽签确定 Demo 时间,最终为第四个出场。次日 12:00 前端可视化基本完善,为界面做最后的调整。次日 12:00 ~ 12:30 午餐时间次日 13:00 ~ 14:00 准备 PPT 和展示录屏次日 14:30 ~ 18:30 Demo Time(B 站直播)TiEye 架构我们采取了前后端分离的架构。前端是 Vue.js 框架,使用 Typescript 语言开发。由于看上去现有的图表库啥的并不能很好地满足我们的需求,所以前端同学决定手撸 SVG。后端则是 PD 提供的 API。数据存储目前暂时存储在 etcd,将来会考虑其它方案来应对数据规模太大的情况。我们将 Region 的变化分成了以下四种:LeaderChange:Raft Group 选举(或者是主动移交)了新的 leaderConfChange:Raft Group 成员变更Split:当某个 Region 数据超过一定阙值时(或被手动干预时)会分裂成键值范围相邻的两个 RegionMerge:两个键值范围连续的 Region 合并成一个Bootstrap:一个新的集群中第一个 Region 产生在前端的表示方式如图所示:给 PD 添加的 API 则有如下几种:/pd/api/v1/history/list,GET 方法,返回全部历史。返回结果:[ { “timestamp”:1544286220000000, “leader_store_id”:0, “event_type”:“Bootstrap”, “Region”:{ “id”:2, “start_key”:"", “end_key”:"", “Region_epoch”:{ “conf_ver”:1, “version”:1 }, “peers”:[ { “id”:3, “store_id”:1 } ] }, “parents”:[], “children”:26 }, …]/pd/api/v1/history/Region/{RegionId},GET 方法,查询某个 Region 的变化历史,返回结果同上。/pd/api/v1/history/key/{key},GET 方法,查询某个 key 所属 Region 的变化历史,返回结果同上。以上几个 API 均可附加起止时间参数(时间戳),如:/pd/api/v1/history/list?start=0&end=1544286229000000顺便一提,前端部分原先打算作为 PD 的一部分来提供,与 PD 一起构建(于是前端的代码也放进了 PD 的一个单独的文件夹里)。但是后来觉得对于不涉及这些前端代码的开发者来说这样做不太好,所以我们之后会抽时间将这些前端代码放进一个单独的仓库里。测试过程测试的时候我们部署了1个 TiDB,6个 TiKV,3个 PD,通过 sysbench 导入少量数据,最后通过开启 random-merge-scheduler 来进行随机合并 Region。下图是我们的测试过程中的结果展示:此时通过我们的工具还意外发现了一个 bug。可以在上图看到,在第一个红框处 Region 2 合并进 Region 28,然后第二红框那里已经被 merge 进 Region 28 的 Region 2 莫名其妙地又连接了后面的 Region 40,显然这里是有问题的。经过通过日志确认,这是由于 PD 收到了一个含有过期 Region 2 信息的心跳导致的,追根溯源发现是 TiKV 中的 pd-client 的一个 Bug 导致了在与 PD 重连后会发送一个过期的心跳信息(Bug 地址在 https://github.com/tikv/tikv/…)。实际运行结果横轴表示时间,纵轴表示 Region 的存储键值的顺序(仅表示顺序,不代表实际的数据量),矩形上的数字表示 Region id,为了便于理解,所有的 Region 的最终状态都会在最后的时间点上展示出来(即使在这个时间点没有发生 Region 的改变)。点击右上角可以更改查下的时间范围。右上角可以设置按照 key 的范围对齐,效果如下图:点击任何一个节点,会展示当时 Region 的详细信息。拖动下方的框可以对局部进行缩放(你也可以通过查询更小的时间范围达到同样的效果)。Hackathon Demo我们团队的 Demo 展示是博康负责的。一开始他还担心如果演讲的时候忘词了怎么办,不过最后展示效果很不错,整个 Demo show 进行得非常顺利(P.S. 要是展示时间能多给几分钟就好了)。在展示中,我们也看见了其他团队的作品也都非常棒。这其中让我最感兴趣的是有个团队做的是以 TiKV 作为数据存储的 etcd。这个选题一开始我也考虑过,因为我在工作中实际已经遇到了这个问题,不过最后和队友商量后还是选择了现在这个题目。总结我们“矛盾螺旋”团队最终获得了三等奖,这对我来说简直是意外之喜。在演示中,很多别的团队也都做得十分优秀,我们在观看其它团队的演示时几乎都觉得获奖无望了。最后却拿到了三等奖,实在是意料之外。这次我们之所以能够获奖,一方面是选题选得恰到好处,具有一定的实际作用,同时工作量又能保证在 Hackathon 期间完成。最后感谢我的两位队友,谢谢导师,谢谢评委老师,谢谢 PingCAP 的所有工作人员为这次 Hackathon 所做的努力。视频 | TiDB Hackathon 2018(上半场)(????0:36:00 开始)矛盾螺旋队 Demo 演示TiDB Hackathon 2018 共评选出六个优秀项目,本系列文章将由这六个项目成员主笔,分享他们的参赛经验和成果。我们非常希望本届 Hackathon 诞生的优秀项目能够在社区中延续下去,感兴趣的小伙伴们可以加入进来哦。延伸阅读:1. TiDB Hackathon 2018 回顾2. 天真贝叶斯学习机 | 优秀项目分享3. TiQuery:All Diagnosis in SQL | 优秀项目分享4. 让 TiDB 访问多种数据源 | 优秀项目分享5. TiDB Lab 诞生记 | 优秀项目分享 ...

December 21, 2018 · 2 min · jiezi

TiDB EcoSystem Tools 原理解读系列(二)TiDB-Lightning Toolset 介绍

简介TiDB-Lightning Toolset 是一套快速全量导入 SQL dump 文件到 TiDB 集群的工具集,自 2.1.0 版本起随 TiDB 发布,速度可达到传统执行 SQL 导入方式的至少 3 倍、大约每小时 100 GB,适合在上线前用作迁移现有的大型数据库到全新的 TiDB 集群。设计TiDB 从 2017 年开始提供全量导入工具 Loader,它以多线程操作、错误重试、断点续传以及修改一些 TiDB 专属配置来提升数据导入速度。然而,当我们全新初始化一个 TiDB 集群时,Loader 这种逐条 INSERT 指令在线上执行的方式从根本上是无法尽用性能的。原因在于 SQL 层的操作有太强的保证了。在整个导入过程中,TiDB 需要:保证 ACID 特性,需要执行完整的事务流程。保证各个 TiKV 服务器数据量平衡及有足够的副本,在数据增长的时候需要不断的分裂、调度 Regions。这些动作确保 TiDB 整段导入的期间是稳定的,但在导入完毕前我们根本不会对外提供服务,这些保证就变成多此一举了。此外,多线程的线上导入也代表资料是乱序插入的,新的数据范围会与旧的重叠。TiKV 要求储存的数据是有序的,大量的乱序写入会令 TiKV 要不断地移动原有的数据(这称为 Compaction),这也会拖慢写入过程。TiKV 是使用 RocksDB 以 KV 对的形式储存数据,这些数据会压缩成一个个 SST 格式文件。TiDB-Lightning Toolset使用新的思路,绕过SQL层,在线下将整个 SQL dump 转化为 KV 对、生成排好序的 SST 文件,然后直接用 Ingestion 推送到 RocksDB 里面。这样批量处理的方法略过 ACID 和线上排序等耗时步骤,让我们提升最终的速度。架构TiDB-Lightning Toolset 包含两个组件:tidb-lightning 和 tikv-importer。Lightning 负责解析 SQL 成为 KV 对,而 Importer 负责将 KV 对排序与调度、上传到 TiKV 服务器。为什么要把一个流程拆分成两个程式呢?Importer 与 TiKV 密不可分、Lightning 与 TiDB 密不可分,Toolset 的两者皆引用后者为库,而这样 Lightning 与 Importer 之间就出现语言冲突:TiKV 是使用 Rust 而 TiDB 是使用 Go 的。把它们拆分为独立的程式更方便开发,而双方都需要的 KV 对可以透过 gRPC 传递。分开 Importer 和 Lightning 也使横向扩展的方式更为灵活,例如可以运行多个 Lightning,传送给同一个 Importer。以下我们会详细分析每个组件的操作原理。LightningLightning 现时只支持经 mydumper 导出的 SQL 备份。mydumper 将每个表的内容分别储存到不同的文件,与 mysqldump 不同。这样不用解析整个数据库就能平行处理每个表。首先,Lightning 会扫描 SQL 备份,区分出结构文件(包含 CREATE TABLE 语句)和数据文件(包含 INSERT 语句)。结构文件的内容会直接发送到 TiDB,用以建立数据库构型。然后 Lightning 就会并发处理每一张表的数据。这里我们只集中看一张表的流程。每个数据文件的内容都是规律的 INSERT 语句,像是:INSERT INTO tbl VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9); INSERT INTO tbl VALUES (10, 11, 12), (13, 14, 15), (16, 17, 18);INSERT INTO tbl VALUES (19, 20, 21), (22, 23, 24), (25, 26, 27);Lightning 会作初步分析,找出每行在文件的位置并分配一个行号,使得没有主键的表可以唯一的区分每一行。此外亦同时将文件分割为大小差不多的区块(默认 256 MiB)。这些区块也会并发处理,让数据量大的表也能快速导入。以下的例子把文件以 20 字节为限分割成 5 块:Lightning 会直接使用 TiDB 实例来把 SQL 转换为 KV 对,称为「KV 编码器」。与外部的 TiDB 集群不同,KV 编码器是寄存在 Lightning 进程内的,而且使用内存存储,所以每执行完一个 INSERT 之后,Lightning 可以直接读取内存获取转换后的 KV 对(这些 KV 对包含数据及索引)。得到 KV 对之后便可以发送到 Importer。Importer因异步操作的缘故,Importer 得到的原始 KV 对注定是无序的。所以,Importer 要做的第一件事就是要排序。这需要给每个表划定准备排序的储存空间,我们称之为 engine file。对大数据排序是个解决了很多遍的问题,我们在此使用现有的答案:直接使用 RocksDB。一个 engine file 就相等于本地的 RocksDB,并设置为优化大量写入操作。而「排序」就相等于将 KV 对全写入到 engine file 里,RocksDB 就会帮我们合并、排序,并得到 SST 格式的文件。这个 SST 文件包含整个表的数据和索引,比起 TiKV 的储存单位 Regions 实在太大了。所以接下来就是要切分成合适的大小(默认为 96 MiB)。Importer 会根据要导入的数据范围预先把 Region 分裂好,然后让 PD 把这些分裂出来的 Region 分散调度到不同的 TiKV 实例上。最后,Importer 将 SST 上传到对应 Region 的每个副本上。然后通过 Leader 发起 Ingest 命令,把这个 SST 文件导入到 Raft group 里,完成一个 Region 的导入过程。我们传输大量数据时,需要自动检查数据完整,避免忽略掉错误。Lightning 会在整个表的 Region 全部导入后,对比传送到 Importer 之前这个表的 Checksum,以及在 TiKV 集群里面时的 Checksum。如果两者一样,我们就有信心说这个表的数据没有问题。一个表的 Checksum 是透过计算 KV 对的哈希值(Hash)产生的。因为 KV 对分布在不同的 TiKV 实例上,这个 Checksum 函数应该具备结合性;另外,Lightning 传送 KV 对之前它们是无序的,所以 Checksum 也不应该考虑顺序,即服从交换律。也就是说 Checksum 不是简单的把整个 SST 文件计算 SHA-256 这样就了事。我们的解决办法是这样的:先计算每个 KV 对的 CRC64,然后用 XOR 结合在一起,得出一个 64 位元的校验数字。为减低 Checksum 值冲突的概率,我们目时会计算 KV 对的数量和大小。若速度允许,将来会加入更先进的 Checksum 方式。总结和下一步计划从这篇文章大家可以看到,Lightning 因为跳过了一些复杂、耗时的步骤使得整个导入进程更快,适合大数据量的初次导入,接下来我们还会做进一步的改进。提升导入速度现时 Lightning 会原封不动把整条 SQL 命令抛给 KV 编码器。所以即使我们省去执行分布式 SQL 的开销,但仍需要进行解析、规划及优化语句这些不必要或未被专门化的步骤。Lightning 可以调用更底层的 TiDB API,缩短 SQL 转 KV 的行程。并行导入另一方面,尽管我们可以不断的优化程序代码,单机的性能总是有限的。要突破这个界限就需要横向扩展:增加机器来同时导入。如前面所述,只要每套 TiDB-Lightning Toolset 操作不同的表,它们就能平行导进同一个集群。可是,现在的版本只支持读取本机文件系统上的 SQL dump,设置成多机版就显得比较麻烦了(要安装一个共享的网络盘,并且手动分配哪台机读取哪张表)。我们计划让 Lightning 能从网路获取 SQL dump(例如通过 S3 API),并提供一个工具自动分割数据库,降低设置成本。在线导入TiDB-Lightning 在导入时会把集群切换到一个专供 Lightning 写入的模式。目前来说 Lightning 主要用于在进入生产环境之前导入全量数据,所以在此期间暂停对外提供服务还可以接受。但我们希望支持更多的应用场景,例如回复备份、储存 OLAP 的大规模计算结果等等,这些都需要维持集群在线上。所以接下来的一大方向是考虑怎样降低 Lightning 对集群的影响。 ...

December 19, 2018 · 2 min · jiezi

数据库学习--范式与规范化

我们都知道数据库设计中有一个比较重要的就是范式。可以说范式这让很多人头痛,不知所云。那么范式到底是什么呢?下面一起来学习下。范式与规范化首先我们肯定要对范式有一个比较笼统的了解:范式是什么。范式是为了在设计数据库的时候,让数据表结构更加合理的一种规范。也就是说,我们依据范式设计的数据表会让我们后面对数据表的操作更加简单与合理。我们所说的范式也是分等级的,按照需要满足的要求从低到高依次是:第一范式、第二范式、第三范式、BC范式、第四范式、第五范式(又称完美范式)。但是一般来说,只需要满足第三范式就足够了,所以下面我只对前三个范式和BC范式做说明。既然范式是分等级的,而在后面的学习中我们会知道,每一个高一级的范式,都是在低一级范式的基础上完善而来的,那么就有人给我们这个完善的过程起了个名字,就叫规范化。通过规范化,我们将使我们的表结构逐步满足第三范式甚至是更高级别的范式。范式的分级第一范式第一范式:最低要求的范式。它是针对数据表中具体的某一列来说的,强调了一列的原子性。也就是说,这一列所代表的属性是不可分的。比如下面的例子:上面的数据表就是一个不符合第一范式的例子,这里讲电话认为是包含了手机和座机两种类型,所以,电话这个属性就是可分的,也就不属于第一范式。将上面的数据表进行规范化后,形成符合第一范式的表结构应该是这样的:第二范式第二范式:首先就是需要满足第一范式,然后在此的基础上, 要求除了主键之外的所有属性都必须完全依赖于主键,而不能只依赖于主键的一部分。它规定了数据的唯一性。思考上面的话,发下它需要两步:1.所有属性都依赖于主键2.所有依赖都是完全依赖依赖好说,就是强调唯一确定的关系。那么完全依赖是什么意思呢?这个用存在联合主键的表来解释更容易理解:比如有如下的表结构学生课程表:在上面的数据表中,需要使用(学号,课程号)来作为联合主键。首先来验证第一点:姓名和成绩都`依赖于主键,主键确定了,姓名和成绩也就确定了。然后来验证第二点:成绩是完全依赖于主键的,只有确定了是哪个学生的哪门课,成绩才能确定;但是姓名就不是完全依赖于主键了,只要学号确定了,就可以唯一确定姓名了,所以,姓名是部分依赖于主键的。不满足第二点,所以上面的表也就不符合第二范式。那么,这样的数据表结构会有什么问题呢?数据冗余:当一个学生有多门课的成绩的时候,就会出现每条数据中都有姓名这么一个重复的字段删除异常:当删除了一个学生所有的成绩后,这个学生也就没有了插入异常:在学期开始的时候,所有学生都还没有选课,这时,就没有办法保存学生的信息更新异常:在输入姓名的时候,错别字,写错了,需要修改,这时候就需要多条记录那么如何解决这个问题呢?正确的做法是应该将上面的表拆分成三个表:学生课程表:学生表课程表第三范式第三范式:也是在第一范式的基础上定义的,同时,任何非主属性(除主键)不依赖于其他非主属性,且不存在传递依赖(a->b,b->c,则a->c)。它解决了数据表的冗余性。简单点来说,就是每个属性都被主键直接约束。看一下下面的例子:学院编号会被学号所唯一确定,然后学院名称又被学院编号唯一确定,即:学号->学院编号->学院名称这就出现了传递依赖,所以不属于第三范式。那么这样会有什么问题呢?同第二范式解决的问题类似,会出现数据冗余、更新异常等的问题:1.添加一个学院,每个学生都需要输入学院名称这个字段2.修改学院时,每个学生的记录都需要修改,否则就会出现数据不一致解决办法就是将上面的表拆成两个子表:学生表学员表BC范式BC范式:它是在第三范式的基础上定义的,是一个用的稍微少一点的概念,在上面的讲第二范式的时候,我们提到了联合主键的概念,BC范式就是对它进行约束:主属性不能依赖于主属性(联合主键中的属性)因为这个概念用的比较少,这里就不做过多的介绍了。范式的作用通过上面对几个范式的介绍,也可以看出,范式帮助我们解决了在插入、删除、更新的操作的时候遇到的问题,使我们在操作数据库的时候方便而且准确。范式的出现,使数据表结构更加规范,也使操作数据表更加容易。但是,是不是满足级别越高范式就越好呢?其实也不然。我们在解决上面范式问题的时候,所用到的方法,无一例外,全是用增加数据表的数量,然后建立表之间的关联实现的。而在我们实际的项目种,很多时候都是为了处理查询问题,这时候,进行某个查询就很有可能是多表的关联查询,根据实际项目中的经验我们也能知道,关联查询还是比较费事的,所以查询就显得效率有点低了,而且操作的难度也更大。这时候,如果能够有个别的冗余数据,就会使查询有意向不到的效果。所以,在使用范式的时候,还是要结合考虑实际的需求,在查询的需求很高的时候,不妨添加以两个冗余的字段,来减小查询的压力;而如果删除和更新的需求较高的时候,还是按照范式的规范来进行比较好。相关参考:https://segmentfault.com/a/11…https://blog.csdn.net/Dream_a...https://coach.iteye.com/blog/...https://blog.csdn.net/zhufuyi…

December 14, 2018 · 1 min · jiezi