本文转载自微信公众号“咖啡拿铁”,作者咖啡拿铁。转载请联系CaféLatte公众号。背景很多同学都或多或少听说过tidb这个专业名词,但是很多同学认为是分布式数据库,业务用的是mysql。他们基本上不能使用这项技术,而且他们可能不知道。最近有业务需要用到tidb,于是研究了一下基本原理,发现这些原理并不局限于分布式数据库,很多技术都是通用的,所以写在这里分享一些学习tidb的心得。先说说为什么我们选择TiDB。一般来说,我们业务中使用mysql,但是单机数据库的容量和并发性能有限。对于一些大容量或者高并发的场景,我们会选择sharding-jdbc来做。使用sharding-jdbc确实解决了问题但是增加了开发难度。我需要为我的每个表设置一个分表键,每次查询都必须带入这个键的值,这样就增加了查询的限制。如果不是有key的value,所有的数据库表都要查询一次,效率极低,所以我们异构的发一份数据给es,满足其他条件。如何解决这个问题呢?就在最近,公司内部正在推广TiDB。看到tidb基本兼容mysql,存储无限扩展,开发成本相对较低,整体性能不错,所以决定使用tidb。数据库发展历史关系型单机数据库关系型数据库的开端是EdgarF.Codd在1970年提出的关系模型。在数据库发展的早期,出现了许多优秀的商业数据库产品,如Oracle/DB2。1990年后,出现了开源数据库MySQL和PostgreSQL。这些数据库不断提升单机实例的性能,加上遵循摩尔定律的硬件提升速度,往往能很好地支撑业务发展。分布式数据库随着摩尔定律的失效,单一数据库的发展难以应对更高层次的挑战,于是分布式数据库应运而生。分布式数据库具有处理海量并发和海量存储的能力,因此可以应对更困难的。挑战。nosql:HBase就是一个典型的代表。HBase是Hadoop生态中的重要产品,GoogleBigTable的开源实现,当然还有我们熟悉的redis。Nosql自身有一些特殊的使用场景,所以也有一些缺点。BigTable不支持跨行事务,Java开发性能也跟不上,redis使用内存存储,不能保证事务。而且nosql不再依赖于关系模型。sharding:我们还是可以通过单机数据库来完成我们分布式数据库的功能。我们通过某个组件来实现将SQL分发到不同分片的功能。比如比较有名的开源的有sharing-jdbc、mycat,还有阿里云上的商用的。有drds。共享对于运维来说是比较困难的。如果需要扩容,需要手动不断的迁移数据,需要自己指定一个shardkey。newsql:在newsql中,可以保证acid事务,也维护关系模型,也支持sql。比较出名的有goole的F1和Spanner,阿里的OceanBase,pingCap的tidb。学前题我们在学习某种知识的时候,通常是带着一些问题来学习的。有目的的学习会让你上手更快。对于TiDB或者分布式数据库,我在使用的时候有这样的疑问:如何保证无限扩展?因为平时用的技术大多是sharding-jdbc加上sharding-key技术,无限扩容其实比较麻烦,所以一开始就质疑tidb如何保证无限扩容。?如何保证唯一标识?分布式数据库往往是碎片化的。独立数据库中的自增id无效。TiDB是怎么保证的?如何保证交易?前面我们说过newsql需要支持acid事务。那么我们的TiDB是怎么保证的呢?我们如何通过索引查询数据呢?单机数据库使用索引加速查询,而TiDB如何使用索引加速查询?在TiDB架构回答上述问题之前首先,我们来看一下TiDB的整体架构。TiDB其实就是一个典型的计算分离架构。如果你对计算分离架构不熟悉,可以看我之前的文章:浅谈计算与分离TiDBServer:计算层,对外暴露协议的连接端口,负责管理客户端的连接。主要进行SQL解析和优化,生成分布式执行计划。由于计算层是无状态的,可以无限扩展。PDServer:PD是整个集群的大脑。它负责存储每个TiKV节点的实时数据分布和集群的整体拓扑结构。提供TiDBDashboard管控接口,需要保持高可用。TiKV:k-v存储引擎。在TiKV内部,存储数据的基本单位是Region。Tiflash:这是一个列式存储引擎TSpark:这是tidb对spark的支持,所以tidb是一个HTAP数据库。如何无限扩张?先来我们的第一个问题,TiDB是如何做到无限扩容的?首先我们来看一下计算层:tidb-server刚才说了,在计算层是无状态的,所以才可以无限扩展。如果你的场景并发度很高或者数据库连接很多,可以考虑扩展tidb-server。那么我们再来看看存储层。有一种数据云数据库经常被误认为是分布式数据库,就是aws的auroradb和阿里云的polardb。这两个数据库也采用了计算和存储分离的架构。计算层也可以无限扩展,但是在存储层他们用的是一个数据,就是共享存储架构。这两个数据库都依赖于这块大容量的磁盘来支持更高容量的数据。在TiDB中,有一个Shared-Nothing的架构,存储层也是分离的:多个Region被划分到每个TiKV中,这是我们的基本存储单元。看到这张图是不是觉得这个架构很眼熟?从上面看,region对应的是Kafka下的partition。Kafka中partition的作用也是将topic的压力分散到不同的broker上。TiDB的Region也是如此。我们使用区域作为存储的最小单位。在详细介绍Region之前,先说说存储引擎为什么叫TiKV?原因是这个存储引擎保存了一个key-value,可以理解为java中的hashmap。在TiKV中,你不会选择开发自己的地图数据。要落地,还是要用一个非常好的kv存储引擎——rocksdb来进行磁盘落地。RocksDB是Facebook开源的KV高性能单机数据库。很多公司基于rocksdb做了很多优秀的存储产品,后面会写一篇文章详细介绍rocksdb。rocksdb是一个独立的存储引擎,所以我们需要保证数据在分布式环境下不丢失。在kafka中,有其他分区的副本会不断拉取leader副本,通过ISR机制维护。在TiKV中,直接使用Raft协议进行数据复制,每一次数据变更都会实现为一条Raft日志。通过Raft的日志复制功能,将数据安全可靠地同步到复制组中的各个节点。但在实际写入时,根据Raft协议,只需要同步复制到大部分节点即可安全认为数据写入成功。可以发现这里其实是写raft的,rocksdb是通过raft接口写的。让我们回到这里的区域。区域和分区的区别在于分区一般不会自动扩容。在业务发展中,往往是一个常数值,只是地域不同而已。region的默认大小是96MB,然后在实际业务中,我们的region的个数会随着我们的数据量的增加而增加。当然如果我们的数据量变小了,也会自动合并。如何判断某个数据在哪个区域?一般来说,有散列(key)和范围(key)两种方案。tikv中选择了rangekey,因为这样更方便regionsplitting。每个region其实就是[StartKey,EndKey]的一个表示:当一个region分裂的时候,只需要添加一个新的region,将旧region的部分数据转移到新region中,比如[a,b)->[a,ab)+[ab,b),如果是hash的话,会重新hash所有region的数据,所以tikv中选择了range(key)方式,合并也是一样。所以对于TiDB来说,无论是存储层还是计算层,我们都可以无限扩展。如何保证id在mysql中是唯一的?我们可以直接给主键设置AUTO_INCREMENT来达到自增的效果。mysql是如何实现自增的?MySQL5.7及之前版本:InnoDB引擎自增,自增存储在内存中,没有持久化。每次重启后,第一次打开表时,会找到自增max(id)的最大值,然后用max(id)+stepsize作为表的当前自增值。MySQL8.0版本中:自增变化记录在redolog中,重启时依赖redolog恢复重启前的值。这些在单机中很容易做到,但是在分布式数据库中,我们无法保证id的唯一性。之前写过一篇相关文章:如果有人问你分布式ID,就把这篇文章扔给他。我们在使用sharding-jdbc时,使用文中介绍的叶子ID生成中间件来完成ID的生成。Tidb也支持AUTO_INCREMENT。实现原理与leaf中的数段模式相同。不能保证严格增量,只能保证趋势增量。具体原则是:对于每一个自增列,使用一个全局可见的键值对来记录当前分配的最高ID。由于分布式环境中节点通信的开销,为了避免写请求放大的问题,每个TiDB节点在分配ID时申请一段ID作为缓存,用完后再取下一段up,而不是每次都分配它。都适用于存储节点。TiDB还支持AUTO_RANDOM,可以用来解决大批量写入TiDB时,包含整型自增主键列的表导致的热点问题。因为region是有序的,如果一段时间内产生大量的有序数据,可能是在同一个region,所以我们可以使用AUTO_RANDOM来打散我们的主键数据。如何保证事务这里我们先回顾一下事务ACID的四大特性,我们来想一想在mysql的innodb中这是怎么做到的?A:原子性是指一个事务中的所有操作,或者全部完成,或者全部没有完成,不会在中间某个环节结束。在mysql中,我们依靠redolog和undolog来完成C:一致性,即事务开始前和结束后数据库的完整性没有被破坏。依赖于其他几个属性来保证一致性。I:隔离性是指数据库允许多个并发事务同时读取、写入和修改其数据的能力。隔离可以防止多个事务并发执行时交叉执行导致的数据不一致,主要用于处理并发场景。mysql隔离依赖锁和mvcc。mysql中的锁有很多种,mysql支持多重隔离。D:坚持。事务处理完成后,对数据的修改是永久性的。即使系统出现故障,也不会丢失。持久化依赖于redolog和mysql的磁盘刷新机制。ACID在TiDB中有什么作用?A:分布式事务的原子性是通过PrimaryKey所在Region的原子性来保证的。C:写入数据前,TiDB会校验数据的一致性。验证通过后才会写入内存,返回成功。I:隔离也是通过锁和mvcc来实现的,但是tidb只支持RR(RepeatableRead)级别,4.0之后optimistic模式也可以支持RC隔离级别。D:一旦事务提交成功,所有的数据都会持久化存储在TiKV中,而且还有多副本机制,宕机数据不会丢失。mysql中的事务模型都是悲观事务模型,而tidb中的事务模型提供了乐观和悲观两种模型。如何理解悲观和乐观两种模型:总是认为你修改的每一条数据都有很大的概率被其他事务修改(悲观观点)。在mysql中,如果你在一个事务中修改了一行,就会给你加一个行锁。如果此时有其他事务要修改这个数据,其他事务就会被阻塞等待。可以简单理解为在执行时检测冲突。乐观模型:我们认为我们修改的数据大概率不会和其他事务发生冲突,所以不需要在执行的时候进行冲突检测,而是在最后提交的时候进行冲突检测。如果冲突较少,可以获得更高的性能。这两种模式在TiDB中是如何实现的?因为我们是分布式数据库,所以两阶段提交一般是分布式事务的通用解决方案。之前写过很多分布式事务相关的文章,大家可以自行查看。乐观模式TiDB也使用两阶段提交来保证分布式事务的原子性,分为Prewrite和Commit两个阶段:Prewrite:对一个事务修改的每个Key检测冲突,并写锁防止被其他事务修改。对于每一个事务,TiDB都会选择所有参与修改的键中的一个作为当前事务的主键。提交或回滚一个事务,首先需要修改主键,其提交与否将作为整个事务执行结果的标识。Commit:所有预写成功后,先同步提交PrimaryKey。交易成功后,交易提交成功,其他SecondaryKey会异步提交。整个事务步骤如下:第一步:客户端启动事务,类似于我们在mysql中的begintrasaction;Step2:TiDB从PD获取全局时间,这样就可以知道这个事务的全局顺序,以便后续的mvcc处理Step3:发起DML,比如updatexxx;此时不会进行冲突检测,只会保存在tidb内存中;第四步:提交事务,类似于我们在mysql中的commit。这个时候TiDB会在commit阶段完成两阶段的commit,先prewrite各种锁检测之后,如果没有问题,再commit。下面是一个例子:begin;//step1insertintoxx;//step3updatexx;//step3updatexx;//step3commit;//step4上面的例子中,如果是悲观模式step3,就会进行锁检测。在乐观模式下,所有的工作都放在commit中,所以commit中会出现异常状态,所以我们在使用乐观模式的时候需要更好的处理commit阶段的异常行为,这和我们的不一样一般编程。但是如果数据竞争不是太激烈,可以使用乐观模式来提升性能。悲观模式悲观模式推进锁。每个DML都会加一个悲观锁,锁会写入TiKV。也会通过raft同步。添加悲观锁时,检查各种约束,如写冲突和键唯一性约束。在悲观事务下,我们可以保证我们的提交成功。这种模式比较符合我们的编程模式,所以TiDB默认的模式也是悲观模式。如何做索引查询为什么我会想到这个索引查询呢?当时看到rocksdb是tidb的底层存储介质后,想到我们innodb中的索引是B+树。如果tidb的索引是一个b+树的话,那么rocksdb应该怎么构造呢?其实TiDB中的索引也是以k-v的形式来做的。我们先来看看每一行的数据是如何存储的:为了保证同一张表的数据放在一起,方便查找,TiDB会给每张表分配一个表ID,记为TableID。表ID是一个整数,在集群中是唯一的。TiDB会为表中的每一行数据分配一个行ID,记为RowID。行ID也是一个整数,在表中是唯一的。对于行ID,TiDB做了一个小的优化。如果一张表有整型主键,TiDB会使用主键的值作为这行数据的行ID。每行数据按照如下规则编码成(Key,Value)键值对:Key:tablePrefix{TableID}_recordPrefixSep{RowID}Value:[col1,col2,col3,col4]假设我们的tablePrefix是一个常量字符t,而recordPrefixSep是常量字符r,我们的tableId是1,rowID是我们这里的主键,假设是100,如果有用户表数据,如下:Key:t1_r100Value:[100,"zhangsan"】如果我们的主键是一个整数,那么上面也可以看做是我们的主键索引。如果我们的主键不是整数或者是唯一索引的情况下,规则编码如下:上面的例子,一个indexedColumnsValue只能对应一条数据来满足唯一性。如果是非唯一索引,我们可以有:Key:tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowIdValue:null这样一个indexedColumnsValue可以是多行数据,所以其实我们区域数据的索引不会和region的数据合并,而是有自己的region分片。同样,我们在查询数据的时候,需要依赖我们的tidb-server来分析我们应该使用什么样的索引,先根据索引数据查询rowId,然后根据rowId查询我们对应的数据。总结一下,不管是TiDB还是分布式数据库,要学的知识还是很多的。以上只是对TiDB的粗略分析。想学的话可以看看下面的资料:pingcap文档:https://docs.pingcap.com,pingcap的文档是我见过的最顶级的文档。可以说它不叫文档,实际上是文章的知识库。我文章的很多图片和内容都是借用的。极客时间?:极客时间有一个类叫分布式数据库,不局限于tidb,主要讲解分布式数据库的各种知识,会列出市面上的分布式数据库进行对比。《数据库系统内幕》:豆瓣评分8.5。本书讲解了很多数据库理论的基础知识。它可以用于分布式数据库或独立数据库。有点难懂,不过还是有不少收获的。
