怎么设计?我们需要从一个用户的角度去看问题,什么叫用户的角度,说白了就是一个key-value对象时如何存储到数据库中,然后如何从一个数据库中读取出来,最后这个数据库要实现一定的容灾能力。
整个流程就是按照一个key-value从客户端发送过来。第一,这个key-value对象的逻辑数据结构是怎么设计的,这就用到了龙珠one:key-value的逻辑数据结构设计,个人觉得列族模型相对灵活。第二,该对象是如何找到对应的服务节点的,用到了龙珠two:分发策略设计,主要是一致性哈希。第三,数据到了服务节点后在数据库内部是如何存储的并且当要查询某个key对应的value值时是如何快速实现的,这个用到了龙珠tree:存储和查找key-value设计,哈希函数的应用是关键。第四,这份数据是怎样落地到磁盘的,这就用到了龙珠four:持久化策略设计,绕不开的是重做日志。第五,由于整个集群是去中心化的,保持节点信息和生产数据的一致性就成为个难题,这就用到了龙珠five:节点间一致性设计,核心就是Gossip协议实现最终的一致性。第六,是不管是不是分布式数据库都配置的那就是备份和容灾策略,用到了龙珠six:备份和负载均衡设计,相对于多份副本都在一个集群上,我更倾向于通过主从集群进行备份。
龙珠one:key-value的逻辑数据结构设计(列族模型)
大家都知道分布式数据库的基本的逻辑数据结构都是key-value键值对设计,区别只在于value值的数据结构不同,我们看看三个标本数据库是怎么设计的。
Cassandra是列族模型,简单来说就是在key下面还存在另一层维度,那就是列,一个key下面有多个列(而且数量可以不固定),每个列对应一个value值,所以你可以吧多个列和这些列对应的value值作为一个整体当做是这个key对应的value值。
Redis呢?redis的value值的数据结构是可变的,可以直接是个string,也可以是个容器比如list,set等,当value是个容器时,跟cassandra的列族的概念差不多,都是key对应有多条记录,每条记录还有自己的唯一标识。
Hbase跟cassandra很类似,都是列族模型。就不再细说了。
个人觉得从实际情况来看列族模型更灵活。
龙珠two:分发策略设计(一致性哈希算法或者它的变种)
所谓分布式,那就是以为着数据要分散存储在不同主机不同节点上,所以任何分布式数据库首先要解决的就是如何把客户端发送过来的key-value对象按照一定的算法存储到相应的节点上。我们先看目前比较流行的3种分布式数据库是怎么做的,没接触过hbase,早上冲忙看了一眼,说的不对的地方请各位见谅。cassandra:一致性哈希。Redis:一致性哈希的变种,hbase:由master节点控制着key的分片机制。注意,hase是分布式数据库但不是去中心化,所以对这篇文章而言也就是个对比例子,没太多借鉴的理论,某种程度而言hbase数据库的理论体系是相对落后的,当然落后不一定不好,落后意味着相对简单,从而相对稳健。从这三种比较来看,一致性哈希是比较好的选择,什么是一致性哈希呢?(具体的请看上篇文章《Redis分布式集群机制原理》,那里有详细而通俗的介绍,在这里就简单说一下:简单来说就是定义一个大数组,先把集群节点也映射到对应的数组下标中,等客户端发送过来的key-value对象发过来时,哈希映射到对应的数组下标中,最后根据就近原则,把离某个节点下标近的key-value对象按照顺时针的方向分发给该节点管理。一致性哈希的优点在于当节点进行增删时,数据迁移量会较小,只影响被增删的节点相领的节点,不影响其他节点的数据。而hbase使用的由master节点管理key-value对象的分发路由表,如果某个节点被移除或增加了,这张路由表就要全局更新,至于数据的迁移量有多大要由这个路由表的生成算法决定。但无论如何hbase必须依靠master节点进行分发管理,不符合去中心化的设计。所以龙珠one的选择就是“一致性哈希”。
龙珠tree:存储和查找key-value设计(哈希函数)
分布式数据库基本上都以key-value的形式作为逻辑上的数据结构存储,那么如何设计出根据key值快速访问到对应的value值就对数据库的性能就显得格外重要,以三个标本数据库来看看是怎么实现的。
Cassandra是通过Bloom过滤器和索引文件,data文件共同形成快速查找的能力。每个data文件对应着一个Bloom过滤器和一个索引文件,什么是Bloom过滤器呢?又得用到我们的杀手锏了,哈希算法。所谓的Bloom过滤器简单来说就是先定义一个大的数组,然后数组的每个元素都取0和1两种值,然后定义若干个比如8个哈希函数,然后对要存储的key值做8个哈希计算,最终得到8个结果值(指纹),然后把八个值对应下标的数组都取1,当下次这个key要被访问时,直接用这个key作为输入计算出八个指纹值,然后查看8个下标对应的数组元素是否都为1,如果都为1的话,那么说明可能存在于这个布隆器对应的存储文件中(注意,只是可能不是一定,为什么呢?因为存在哈希冲突),否则说明一定不存在。然后确定了这个存储文件可能存在这个key的数据时,下一步进入这个存储文件的索引文件中,由于data文件中的key-value对象时按照key值顺序存储的,所以可以通过以key为主键建立的索引文件进行二分查找进行快速定位。根据索引文件找到该key对应的value值在data文件 中的偏移量,最后找到相应的value值。
Redis因为是因为全量的生产数据都存放在内存中,所以快速查找设计相对简单,具体详细内容见上篇文章《对redis数据结构进一步探析》,简单来说就是用了哈希表来实现,首先创建了一个一定长度的空的数组hash[n](注意是数组,不是链表,因为只有数据能做到随机访问而不需遍历,也就能达到时间要求是n(1)),由于数组是在内存中定长并且连续排列的,所以每个元素的偏移量都是可以根据下标直接计算得来,而不需遍历。然后对要存储的key-value对象的key做哈希计算,根据结果值存放到对应的数组下标中,如果出现哈希冲突的话就在加到同个数组下标的短链表后面,由于一般而言哈希函数选用得当的话出现哈希冲突的概率不是很大,所以可以认为所有的key—value们都均匀得放置在数组中,并且每个数组下标只放一个key-value对象。所以当我们根据key=7去访问对应的value值时,实际上的操作步骤1.7%4=3 步骤2:获取到hash[3],所以整个流程的速度非常快,效率几乎达到完美的n(1)。
Hbase草草看了下,感觉跟cassandra的设计里面相似。所以对于分布式的物理上存放的数据结构基本上就上面两种分类,但是无论那种,哈希函数的应用都是至关重要的一点。
数据库不可避免的问题是如何持久化,不管是所谓的内存数据库还是偏向于磁盘存储的数据库,持久化是个设计要点。看看三个标本数据库是怎样做持久化的。
Cassandra的持久化是通过写缓存和commitlog来实现,什么意思呢?就是当客户端往数据库中写数据时,集群服务端接收到数据后,先写commitlog日志记录下这条数据记录,然后往写缓存中写该条数据,当写缓存满后就冲刷到磁盘上以文件的形式存储。这样即使节点挂了,写缓存中的数据丢失了,也能根据commitlog来恢复,所谓的commitlog跟oracle的重做日志一样的原理。当写缓存中的数据已经冲刷到磁盘后,就可以清理掉commitlog里面相关的记录。
Redis作为一个内存数据库,是通过全量快照写磁盘和追加操作命令到重做日志的形式实现的。流程是这样的,一条数据过来,redis服务端拿到后先写到重做日志中,然后再修改内存中对应的key-value对象。当需要全量从内存中dump一份全量的数据就执行命令全量备份一份快照。
hbase貌似跟cassandra类似,也是通过写缓存和commitlog来实现。
通过分析三个标本数据库来看,持久化第一步就是得写重做日志,这个步骤是不能省的。然后再根据数据库的特性,设计写缓存以提高数据库的写速度。最后生成数据要全量下载一份到磁盘上保存起来。
就我个人而言,cassandra的持久化设计思想相对要好,虽然也要保存一份重做日志,但是可以根据已经持久化到磁盘上的数据的情况来清理掉没用的重做日志。因为重做日志的作用只是为了防止写缓存丢失的发生。所以如果要设计内存数据库,持久化策略可以参考redis的,如果是非内存数据库的话,持久化策略可以参考cassandra。
说到这里,好像所谓的去中心化分布式数据库的主要设计点就说完了,说实在的这个标题有点吓人,我个人也觉得诚惶诚恐,其实是标题党,吸引大家来看为的文章,我觉得这篇文章还真不可能指导你去从零开始设计一个去中心化的分布式数据库,虽然对设计一个去中心化的分布式数据库的所有要点都有涉及到,真要实现一个超简单的分布式数据库不是不可能,但是更重要的是从设计的角度去理顺了整个去中心化分布式数据库的难点和要点,希望对于大家更深入地理解去中心化分布式数据库有所帮助。文章主要目的是加深我个人的学习印象,如有偏颇错误的地方请大家见谅。有机会再继续修正。
题外话,我现在只找到了6颗龙珠,还差一颗,希望大家有什么想法可以文明留言交流。