项目中一直在用着TDDL,但除了接入时简单了解了下他的用法和原理外,也没有过多的深究其背后的实现,毕竟在阿里内部用到的中间件太多,每一个都深入的学习需要的周期太长,再者项目中数据库层也没有用到分布式,没有了业务上的驱动,动力就更显不足。正是因为自己的一知半解的状态,所以当同行的朋友问起TDDL的分库分表原理时我也就没法清晰的回应,鉴于此,今天特地查找资料,对TDDL的架构和原理做个总结,既是对自我知识储备的填充,也希望能帮助有需要的同行朋友理解tddl。
一、分布式数据库的演化
Tddl是一个分布式数据库中间件,主要是为了解决分布式数据库产生的相关问题,因此在学习tddl之前我们先了解一下分布式数据库的演化历程,所谓磨刀不误砍柴工,知其然亦要知其所以然,分布式数据库与数据库中间件息息相关,了解学习是很有必要的。整个分布式数据库的演化过程如下:
1、单库单表
通常刚开始的时候,应用的数据比较少,也不会很复杂,所以应用只有一个数据库,数据库中的业务表是一张对应业务的完整的表,这也是我们刚开始接触数据库时的数据库形态。
2、读写分离
随着业务的发展,数据量与数据访问量不断增长,很多时候应用的主要业务是读多写少的,比如说一些新闻网站,运营在后台上传了一堆新闻之后,所有的用户都会去读取这些新闻资讯,因此数据库面临的读压力远大于写压力,那么这时候在原来数据库 Master 的基础上增加一个备用数据库 Slave,备库和主库存储着相同的数据,但只提供读服务,不提供写服务。以后的写操作以及事务中的读操作就走主库,其它读操作就走备库,这就是所谓的读写分离。
读写分离会直接带来两个问题:
1)数据复制问题
因为最新写入的数据只会存储在主库中,之后想要在备库中读取到新数据就必须要从主库复制过来,这会带来一定的延迟,造成短期的数据不一致性。但这个问题应该也没有什么特别好的办法,主要依赖于数据库提供的数据复制机制,常用的是根据数据库日志 binary-log 实现数据复制。
2)数据源选择问题
读写分离之后我们都知道写要找主库,读要找备库,但是程序不知道,所以我们在程序中应该根据 SQL 来判断出是读操作还是写操作,进而正确选择要访问的数据库。
3、垂直分库
数据量与访问量继续上升时,主备库的压力都在变大,这时候可以根据业务特点考虑将数据库垂直拆分,即把数据库中不同的业务单元的数据划分到不同的数据库里面。比如说,还是新闻网站,注册用户的信息与新闻是没有多大关系的,数据库访问压力大时可以尝试把用户信息相关的表放在一个数据库,新闻相关的表放在一个数据库中,这样大大减小了数据库的访问压力。
垂直分库会带来以下问题:
1)ACID 被打破
数据分到不同的数据库之后,原来的事务操作将会受很大影响,比如说注册账户的时候需要在用户表和用户信息表中插入一条数据,单机数据库利用事务可以很好地完成这个任务,但是多机将会变得比较麻烦。以下两点也是因为数据库在不同的机子上而导致简单的操作变得复杂。
2)Join 操作困难
3)外键约束受影响
4、水平分表
经过长时间积累,特别对于 ugc 的业务,数据量会达到惊人的地步,每张表存放着大量的数据,任何 CRUD 都变成了一次极其消耗性能的操作,这个时候就会考虑水平分表,把一张表内大量的数据拆分成多张子表,比如说原来一张表中存放 50000 条数据,水平分成 5 张表之后,每张表只要存放 10000 条数据。这种结构可以比较容易的存储和维护海量数据。
水平分表会在垂直分库的基础上带来更多的影响:
1)自增主键会有影响
这个影响很明显,分表中如果使用的是自增主键的话,那么就不能产生唯一的 ID 了,因为逻辑上来说多个分表其实都属于一张表,自增主键无法标识每一条数据。
2)有些单表查询会变成多表
比如说 count 操作,原来是一张表的问题,现在要从多张分表中共同查询才能得到结果。
二、TDDL介绍
1、产生背景
由一中可以看出,数据库从单机走向分布式将会面临很多很多的问题,分布式数据库中间件就是为了解决这些问题而生的。Tddl作为开源分布式数据库中间件之一,由阿里研发,其出现解决了以上分布式数据库需求带来的问题,总结起来如下:
随着业务的发展,数据量急剧增大,使用单库单表时,数据库压力过大,因此通过分库分表,读写分离的方式,减轻数据库的压力。
2)系统容灾
使用单机数据库时,如果数据库宕机,则无法对外提供服务,因此产生了主备数据库的方案,当主库宕机后,会自动切换到备库,不影响对外的服务。
3)运维管理
直接连接单机数据库时,无法动态的切换数据源,使用TDDL中间件,可以动态的切换数据源。
2、发展史
1)TDDL 2.0 (2009~2011) 第一个流行版本
2)TDDL 3.1 (2012~) 规则版本升级
3)TDDL 3.3 (2013~) 引入druid链接池
4)Andor (2012~2013) 一次全新的尝试,支持跨库查询
5)TDDL 5.0 (2013) 基于Andor + TDDL3.3的发展而来,保留各自的优点
6)TDDL 5.1 (2014~) 集成cobar,提供server模式,解决跨语言查询三、TDDL原理解析
1、整体结构
1)包结构
2)模型结构
Tddl主要部署在ibatis、mybatis或者其他ORM框架之下,JDBC Driver之上。整个中间件实现了JDBC规范,所以可以将Tddl当做普通数据源实例并且注入到各种ORM框架中使用。由下图中的结构模型图可以看出,tddl是JDBC或者持久框架层与底层JDBC驱动交互的桥梁,或者也可以称之为中转站,起到进出的加工作用。
3)分布式数据库逻辑结构
分布式数据库在经过读写分离和分库分表之后,数据库的结构变得复杂,逻辑上大致如下图所示:
除了最底层的物理DB,上两层的逻辑层是分布式数据库中间件着重要解决的问题,tddl的设计亦是以图中的三层逻辑模型为基础进行的。为了解决每一层所要应对的问题,tddl在结构上至上而下也分了三层,分别是 Matrix 层、Group层以及 Atom 层。具体来说,从逻辑上来看,最顶层是要进行分库分表才过渡到中间层的,分库分表所带来的问题是在 Matrix 层解决 ,包括SQL解释、优化和执行等;中间层是经过读写分离和主备切换才会出现最底层,因此读写分离与主备切换的工作由 Group 层解决;至于 Atom 层,它面对的是实实在在的每一个数据库,更多的工作在与对数据库的连接管理,比如说当数据库的 IP 地址发生改变时,Atom 层要动态感知,以免连接找不到地址。
2、三层数据源结构(核心组件)
TDDL作为中间件,其作用是根据路由规则,将sql路由到正确的分库、分表上去执行,再将结果进行汇总,返回给用户,对于用户,不需要了解TDDL的原理,可以像使用单库单表一样去使用分布式数据库。在解析执行和封装这些类似于单库单表的SQL语句时,tddl有专门的组件来进行处理,这些组件就是核心的三层数据源,分别是Matrix层、Group层以及Atom层,其结构如下图所示:
1)Matrix层
改成功能在于分库分表路由,SQL语句的解释、优化和执行,事务的管理规则的管理,各个子表查询出来结果集的Merge等。
如上所述,Matrix 层可以解决分库分表带来的问题,从本质上来看,分库分表带来的最直接的影响是数据访问的路由。单库单表的时候,什么都不用想,就是去这个 DB 中找到这张 Table 再进行查询,但是多库多表的时候就必须要考虑数据存到哪个数据库,为什么要存到这个数据库诸如此类的问题。这里面涉及到数据访问路由算法,它规定了数据的存储位置,同样也由它来指明该去哪里查询数据,常见的数据访问路由算法有以下几种:
a)固定哈希算法
固定哈希就再简单不过了,就是根据某个字段(如整形的 id 或者字符串的 hashcode)对分库的数量或者分表的数量进行取模,根据余数路由到对应的位置。下面图中的例子,数据库垂直拆分成 4 个,其中有一张表水平拆分成两张,利用固定哈希算法进行路由的过程如下:
b)一致性哈希算法
固定哈希算法足够简单实用,基本能保证数据均匀分布,它也是 TDDL 的默认路由算法,但是在数据库扩容的时候,固定哈希算法带来的数据迁移成本也是不得不考虑的。依然是上面的例子,数据库拆分成 4 个,当需要增加数据库的时候,假设变成 5 个,由于取模的结果发生变化,原来数据库中的绝大部分数据都要进行迁移,只有在数据库倍增的时候,数据迁移量才是最少的,但也高达 50%,而且倍增的成本比较高。
所以一致性哈希算法应景而生,它的原理就是通过该算法计算出 key 的 hashcode 之后对 2^32 取模,那么数据都会落在 0~2^32 所组成的环中;同样的,可以利用一致性哈希算法对机器的唯一属性计算所在位置,然后数据存储在顺时针方向最近的机器上。如图所示:
对于一致性哈希,增删机器的成本就降低很多了,比如说在上图 node2 与 node4 之间增加一台机器 node5,那么需要迁移的数据只分布在 node2 与 node5 之间,相比固定哈希来说迁移量小了很多。
c)虚拟节点
一致性哈希已经可以解决大部分需求了,但是对于数据集中在热点的情况,一致性哈希同样面临比较大的挑战。比如说,上图的 node2 与 node4 之间集中了整个环中的大部分数据,当加入 node5 之后,其实起到的效果比较有限,因为还是要有大量的数据进行迁移。引入虚拟节点之后,情况就不一样了,所谓虚拟节点,它就是物理节点的映射,一个物理节点可以复制出多个虚拟节点,尽可能的让它均匀分布在环上,那么即使数据再集中,其实也会存储在不同的节点上,很好地起到了负载均衡的作用。
d)自定义路由规则
这是最不常用的方法,不过 TDDL 也支持,你可以实现自己的算法进行数据访问路由,但一般要么效果很差要么成本很高。
2)Group层
改层的作用在于数据库读写分离,基本上主数据库负责读写,备份数据库只负责读;主备切换状态对调后备库变为主库,主库变为备库;权重的选择 根据权重选择要去读哪些库;数据保护,数据库down掉后的线程保护, 数据库挂掉后的线程保护,不会因为一个数据库挂掉导致所有线程卡死。
读写分离与主备切换带来的问题是 Group 层解决的核心。首先简单介绍一下主备切换,由于主库或者备库都有可能挂掉,但很小概率同时挂,所以当一方挂掉的时候,Group 层要马上把流量切到另一个库,保证挂掉一个不会让应用挂掉。
读写分离最大的问题是数据复制,通常有两种复制场景,一种是镜像复制,即主库和从库的数据结构是一模一样的,通常根据主库上的日志变化,在从库中执行相同的操作;另外一种是非对称复制,意思就是主库与备库是以不同的方式分库的,它们的结构虽然相同,但是主备库中存储的记录是不相同的,主要目的是查询条件不同时,把请求分发到更加适合的库去操作。举个例子,对于订单数据库,买家会根据自己的 ID 去查自己的交易记录,所以主库可以用买家 ID 分库,保证单个买家的记录在同一个数据库中;但是卖家如果想看交易记录的话可能就得从多个库中进行查询,这时候可以利用卖家 ID 进行分库作为备库,这样一来主备库的复制就不能简单的镜像复制了,在进行复制操作之前还需要进行路由。
3)Atom层
Atom 模块真正和物理数据库交互,提供数据库配置动态修改能力。
改层负责动态创建,添加,减少数据源。管理着底层的数据库IP,连接等信息;底层对物理数据库做了代理,对单库的JDBC做了一层封装,执行底层单库的SQL;线程数、执行次数等状态的统计等。
3、执行流程
1)执行流程
TDDL的工作流程类似上图,client发送一条SQL的执行语句,会优先传递给Matrix层。由Martix 解释 SQL语句,优化,并根据查询条件路由到各个group,转发sql进行查询,各个group根据权重选择其中一个Atom进行查询,各个Atom再将结果返回给Matrix,Matrix将结果合并返回给client。具体的工作流程的可以拆分成如下图:
d)Sql的转发。将上一步生成的各个sql语句转发到对应的Group进行执行。这里如上图,我查询的条件是where id = 2 or 3。那么转发给Group0的查询为where id=3,转发给group1的查询为where id =2 。查询的条件也会发生一定修改。
这样四个步骤可以在Matrix层就实现了分库分表的功能,对原始的Sql进行分解,将原本单库单表的查询语句,底层转发到多库多表并行的进行执行,提高了数据库读写的性能。
接下来由Group执行两个过程:
e)根据权重选择AtomDs。通常会在主节点和副节点上读取数据,只在主节点上写入数据。
f)具有重试的策略地在AtomDs上执行SQL。这个可以防止单个的AtomDs发生故障,那么会进入读重试,以确保尽可能多的数据访问可以在正常数据库中访问。
然后是Atom层执行两个过程:
g)读写数控制、线程并发数控制 。同时会统计线程数、执行次数等信息。
h)执行sql,返回结果集。Atom底层利用druid进行连接池的管理,具体查询还是对JDBC做了一定封装。执行完Sql后对将结果返回给Matrix。
最后Matrix执行最后一个过程:
i)结果集合并。Matrix将Atom层的返回的各个结果集进行合并Merge,返回给Client端。
2)路由与扩容(固定哈希算法为例)
a)数据库水平拆分路由
f(pavarotti17)= hash(pavarotti17) % 1024,然后根据该值找对应的DB,
b)扩容
固定哈希算法是常用的算法,其扩容一般推荐每次以2倍的形式扩容,这样只需要迁移一半的数据。
4、Senquence全局唯一id标识生成原理
1)背景
目前基于tddl进行分库分表后,原本一个数据库上的自增id的结果,在分库分表下并不是全局唯一的。所以,分库分表后需要有一种技术可以生成全局的唯一id。
2)工作原理
a)主要职责
生成全局唯一的id;保持高性能;保持高可用。
b)目前常见的几种全局ID的思路
方案一
oracle sequence:基于第三方oracle的SEQ.NEXTVAL来获取一个ID
优势:简单可用。
缺点:需要依赖第三方oracle数据库。
方案二
mysql id区间隔离:不同分库设置不同的起始值和步长,比如2台mysql,就可以设置一台只生成奇数,另一台生成偶数. 或者1台用0~10亿,另一台用10~20亿.。
优势:利用mysql自增id 。
缺点:运维成本比较高,数据扩容时需要重新设置步长。
方案三
基于数据库更新+内存分配:在数据库中维护一个ID,获取下一个ID时,会对数据库进行ID=ID+100 WHERE ID=XX,拿到100个ID后,在内存中进行分配 。
优势:简单高效。
缺点:无法保证自增顺序。
目前tddl sequence也是选择的方案3进行实现,但会有几点额外的要求:
a. 只要生成id的数据库不全部挂掉,均可以顺畅提供服务;
b. 生成id的数据库数量不定,按照应用对容灾的需求指定不同机架不同机房的数据库; (比如需要考虑单元化多机房的id生成)
c. 支持生成id的数据库hang住快速略过和恢复自动加入 。
总结一下:生成id的数据库可以是多机,其中的一个或者多个数据库挂了,不能影响id获取,保证严格高可用。
目前我们针对多机的id生成方案: 每个数据库只拿自己的那一段id,如下图左:
sample_group_0-sample_group_3是我们生成全局唯一id的4个数据库,那么每个数据库对于同一个id有一个起始值,比如间隔是1000。
应用真正启动的时候,可能某一台机器上去取id,随机取到了sample_group_1,那么这台机器上的应用会拿到1000-1999这一千个id(批量取,这个也就保证了应用端取id性能),而这个时候4个数据库上id起始值会变成右图所示,你也许注意到了,下次从sample_group_1上取得的id就变成了4000-4999。那么也就是这样,完全避免了多机上取id的重复。比如sample_group_1他会永远只会取到1000-1999,4000-4999,8000-8999,12000-12999…其他数据库也一样,相互不会重叠。
这种产生全局唯一id的方式相当有效,保证基本的全局唯一特性和高性能的同时,可以对生成id的数据库分机架分机房部署达到容灾的目的。
3)配置
5、tddl适用场景
面向客户端的电商、金融、O2O、零售等行业普遍存在用户基数大、营销活动频繁、核心交易系统数据库响应日益变慢的问题,制约业务发展。 TDDL 提供线性水平扩展能力,能够实时提升数据库处理能力,提高访问效率,峰值 TPS 达150万+,轻松应对高并发的实时交易场景。
2)海量数据存储访问场景
企业客户随着业务的快速发展,业务数据增长迅猛,会产生超过单机数据库存储能力极限的数据,造成数据库容量瓶颈,限制业务发展。 TDDL 可以线性扩展存储空间,提供 PB 级存储能力,可广泛应用于工业制造、智能家居、车联网等超大规模数据存储访问场景。
3)高性价比数据库解决方案
初创型企业初期发展阶段技术积累相对比较薄弱,资金投入有限,业务发展快,数据库的稳定性风险高。TDDL 继承了阿里巴巴多年的分布式数据库技术积累,能够提供简单易用的数据库运维系统,降低企业的技术运维成本,赋予企业强大的数据库支撑能力。
当业务数据和访问量增加到一定量时,如政务机构、大型企业、银行等行业为了支持大规模数据存储和高并发数据库访问,传统方案需要强依赖小型机和高端存储等高成本的商业解决方案,以达到扩展服务能力的目的。TDDL 能够利用普通服务器提供阿里巴巴双十一同等处理能力的高性价比国产数据库解决方案。
4)数据存储平滑扩容
当应用单机存储(MySQL)出现容量或性能瓶颈时,TDDL 提供在线数据扩容功能(该功能需要结合阿里其它内部中间件使用)。传统数据库容量扩展往往意味着服务中断,很难做到业务无感知或者少感知。