c++中两个key确定一个value_如何实现一个支持海量数据存储的「Redis」

v2-e99343dd272d04dfa4537615d849b0b1_1440w.jpg?source=172ae18b

Redis 作为一个内存数据库,在拥有极高读写性能的同时,提供了丰富的数据结构:string、hash、list、set、zset,成为了构建高并发网站不可或缺的一部分。

但redis并非是完美的,譬如:

  1. 内存很贵,而且并不是无限容量的,所以我们不可能将大量的数据存放到一台机器。
  2. 异步复制并不能保证 Redis 的数据安全。
  3. Redis 提供了 transaction mode,但其实并不满足 ACID 特性。
  4. Redis 提供了集群支持,但也不能支持跨多个节点的分布式事务。

理想情况下的 KV 存储需要满足以下条件:

  • 兼容 redis 现有协议(使用方可以无感知迁移)
  • 将数据存储在多台机器上(基于 hash / range 对数据进行分片),并且拥有多重备份
  • 单个存储节点的宕机,不影响整个系统的正常运转(或者可以基于备份自动恢复)
  • 支持集群成员变更(添加/删除存储节点)
  • 相对于 redis,性能损失(延迟,吞吐量 ...等)要在可接受范围内

1. ConfigServer + DataServer 架构

v2-917e1a49c5f339e10539739c32b1c1e3_b.jpg

上述架构,由一个中心控制节点(config server)和一系列的服务节点(data server)组成。

  • config server 负责管理所有的data server,并维护data server的状态信息;为了保证高可用(High Available),config server可通过hearbeat 以一主一备形式提供服务;
  • data server 对外提供各种数据服务,并以心跳的形式将自身状况汇报给config server;所有的 data server 地位都是等价的。

在读写数据的时候,client首先连接config server获取整个集群的数据分布表,确定将要读写的key位于哪一个data server实例。数据分布表具有version的概念,client在读写数据的时候,必须要带上version信息。

当整个集群的分片信息发生变化的时候(有新data server的加入或者hash节点的重新分配),config server就会更新该version信息,由于data server与config server之间维护着heartbeat,data server可以直接感知到version的变化。当client端下次请求data server的时候,data server发现version不匹配,就会拒绝该请求,client端就会重新去config server获取新的数据分布表。

这种(无proxy)架构,直接读写数据节点,可以提供较好的性能。但由于数据分布表version机制的存在,这种架构无法兼容redis协议。

2. TiKV + Proxy 架构

v2-7bc0f8d9545319f105692dad58d2d04f_b.jpg

TiKV 是一个高性能,支持分布式事务的 key-value 数据库。虽然它仅仅提供了简单的 key-value API,但基于 key-value,我们可以构造自己的逻辑去创建更强大的应用。譬如,TiDB 通过将 database 的 schema 映射到 key-value 来支持了相关 SQL 特性。所以对于 Redis,我们也可以采用同样的办法 - 构建一个支持 Redis 协议的服务,将 Redis 的数据结构映射到 key-value 上面。

那么如何基于TiKV构建一个兼容redis协议的分布式kv存储呢?

  • Redis Protocol Layer: 实现对redis协议的支持
  • GMKV Logic Layer:将redis的数据结构转化为底层存储引擎支持的kv形式
  • Transactional KV API Layer:存储引擎抽象层,与底层存储引擎解耦,方便支持不同的存储引擎

3. Redis 协议实现层

redisClient与redisServer之间的通信协议叫:RESP (REdis Serialization Protocol)。

RESP遵循Request-Response模型,具体实现如下:

  • Clients send commands to a Redis server as a RESP Array of Bulk Strings.
  • The server replies with one of the RESP types according to the command implementation.

在RESP中,数据的类型取决于该数据的第一个字节:

  • For Simple Strings the first byte of the reply is "+"
  • For Errors the first byte of the reply is "-"
  • For Integers the first byte of the reply is ":"
  • For Bulk Strings the first byte of the reply is "$"
  • For Arrays the first byte of the reply is "*"

RESP Arrays 的格式如下:

  • A * character as the first byte, followed by the number of elements in the array as a decimal number, followed by CRLF.
  • An additional RESP type for every element of the Array.

v2-d03f1b8e1d467b343422f350223c4629_b.jpg

redis协议官方文档:

Redis Protocol specification - Redis​redis.io

redcon是一个redis协议的go实现:

tidwall/redcon​github.com
v2-f43eaa13c7c99bb0fd9669e18bcf3744_ipico.jpg

4. GMKV 逻辑处理层

GMKV Logic Layer作为整个GMKV的核心,负责将redis的各种数据结构(string, list, hash, set, zset) 映射为底层存储引擎(TiKV)支持的KV结构,通过Transactional KV API层对存储引擎进行读写。

核心问题在于,如何将redis的各种数据结构以KV的形式存储,并且提供较高的读写性能。一种可行的办法是对key进行拼接,为了防止和业务的key冲突,这里需要制定一个特殊的规则来避免冲突。

这个规则就是经典的tlv(type-length-value)规则,当然为了节省存储空间,在不发生冲突的时候,会使用tv(type-value)规则。

4.1、 redis的string结构

string结构的前缀为:k。

比如redis有一个string类型,key是hello,value时world。

在GMKV中的key储存为 khello,value是world。 其中前面的k是为string数据结构加的前缀,GMKV为每种数据结构定义了唯一的前缀。

4.2、 redis的hash结构

hash结构的前缀为:h。

假如我们要将一部视频的信息存在redis中,视频的ID为:941215,视频包含title,url,state等字段。依照上面的tlv规则,可以拼出三个key:h{6}941215-title, h{6}941215-url, h{6}941215-state。

"h"作为hash结构的前缀,"6"作为hash结构key的长度,"941215"作为hash结构的key,"-"作为hash结构key与field的间隔符(可省略),"title"、"url"、 "state"作为hash结构的field,三个拼出的key所对应的value即为hash结构各个field的value。

redis的hash结构的有些命令需要获取hash结构的所有字段,该如何实现?

前缀遍历:只要遍历以h{6}941215为前缀的所有kv就可以了。

4.3、 redis的zset结构

zset结构的前缀有两个:z和s。

  • field到score的映射:前缀为s
  • score到field的映射:前缀为z

redis的zset结构是一种很强大的数据结构,名字叫做有序集合。zset和hash很类似,不过hash里面field的值变成了zset里面的权重或者评分。有了评分,就涉及基于评分的排序了。

zset内部会维护field的值以及评分的排序,很多排行榜都是基于zset实现的。

假设要把某个班级所有学生的数学成绩存在一个zset中,以math作为key,以a,b,c代替学生的名字作为field,对应的score作为学生的数学成绩,abc的成绩分别是:75,95,-80

对于field到score的映射,和hash结构类似:s{4}math{1}a、s{4}math{1}b、s{4}math{1}c

对于score到field的映射,则变成:z{4}math=75,z{4}math=95,z{4}math-80。

那么为什么分隔符不是+/-,而是=/-呢?

因为zset的zrangebyscore需要用前缀遍历实现,而前缀遍历是按照key的字节序遍历的。
而+的字节序小于-,而=的字节序大于-。为了保证遍历的顺序与score的排序保持一致,将+替换为=。

4.4、 redis的list结构

list结构的前缀为:q。

对于list结构,如果在内存中使用链表实现的话,就会很简单。但是如果用kv的方式实现,我们需要一个双向队列。

双向队列可以为头部和尾部增删数据,我们给头部一个递增的序号,给尾部一个递减的序号,则队列中的编号一定是递增的。

根据以上结论以及tlv规则,可以使用kv实现一个list。假设有一个key为students的list,list的前向索引和后向索引分别为:q{8}students{minseqflag}、q{8}students{maxseqflag}。

list中item的key格式为:q{8}students{seq}

对于list的大小,通常使用一个单独的key存储:Q{8}students。

对于lpush或者rpush时,我们先计算对应的seq,同时写入数据然后更新seq即可。对于lpop或者rpop时,我们计算出对应的seq,同时删除数据然后更新seq即可。对于lindex和lset操作,含义是取出或设置对应下标位置的列表值,这里也可以计算出对应的seq,然后得到对应值或者设置值。

REDIS的有一些操作比较反人类,比如linsert和lrem操作。
lrem含义为删除某些指定元素,linsert含义是在某些位置插入一些元素。
这两个操作不符合队列的定义,一旦操作后,seq就不满足连续的特征,所以ssdb不支持这个操作。假设非要实现这要一个操作,那操作后就需要重新调整整个队列,使其seq保持连续,那复杂度就是O(n*log(m))了。

4.5、如何支持ttl

在GMKV中,我们使用了多个kv来存储redis的某个数据结构。那么支持redis的ttl就变成了一件麻烦的事。

具体的实现方案是:把所有设置了过期的key放在一个固定的zset下面,zset的field就是设置了过期时间的key,而field的value则是过期的时间点(以毫秒时间展示).

根据zset按照score排序的特性,GMKV使用一个单独的线程,从该zset中不断的取score最小的数据,检查如果已过期,就删除对应的数据结构。

5. 存储引擎抽象层

为了实现底层存储引擎的可插拔,我们需要Transaction KV API Layer实现对底层存储引擎(TiKV)的抽象,给上层(GMKV)读写提供一致的事务接口。

这样的话,在特定场景下我们可以提供其它存储引擎给上层使用。譬如,阿里巴巴的tair就支持mdb(缓存), rdb(缓存), ldb(存储,基于leveldb) 等多种存储引擎。

6. 工程实践优化

6.1 对SSD磁盘的使用

TiKV底层采用了RocksDB作为存储引擎,RocksDB衍生自Google的LevelDB,LevelDB采用了采用Log-Structured Merge-Tree数据结构,RocksDB/LevelDB 凭借其优异的写性能及不俗的读性能成为众多分布式组件的存储基石。

如需更多了解Log-Structured Merge-Tree,可以参考之前的一篇文章:

gushitong:lsm-tree​zhuanlan.zhihu.com
v2-55163a1f2c2b1752baf6f236f4879d43_180x120.jpg

RocksDB针对SSD磁盘做了诸多优化,所以在性能测试的时候务必采用SSD磁盘。

6.2 zset的score支持double类型

在对zset的介绍过程中,我们默认score为int64类型,而在redis中,score是支持浮点型的。

如果我们把int64转换为浮点型,能否让浮点型的字节序与score的排序也保持一致呢,这里可以参考PingCap-唐刘大大的一篇文章:

浮点数字节序比较​www.jianshu.com
v2-5e108fa71c8bb159294b8a7e2187518c_180x120.jpg

5.3 大对象的延迟删除

在删除list/hash/set/zset的时候,如果该list/hash/set/zset中的元素过多,整个删除操作可能会变慢。

一个可行的方案是引入ObjectID机制:

v2-61739faf885303da1d33a684b8d86668_b.jpg
  • 每个对象指向一个唯一的ObjectID
  • 删除时RawKey时,将ObjectID及其关联的items放入GC队列,延迟删除;
  • 新建时RawKey时,RawKey指向新的ObjectID;

参考链接:

Redis Protocol specification - Redis​redis.io gushitong:lsm-tree​zhuanlan.zhihu.com
v2-55163a1f2c2b1752baf6f236f4879d43_180x120.jpg
三篇文章了解 TiDB 技术内幕 - 说存储​pingcap.com
v2-60ab5bd867c2434d70c957a02a2169e1_ipico.jpg
淘宝分布式NOSQL框架:Tair - 如果的事 - 博客园​www.cnblogs.com
v2-337188cb9587e1313c346eb0c120a56b_180x120.jpg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值