Redis与KV存储(RocksDB)融合之编码方式

本文涉及的产品
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 MongoDB,通用型 2核4GB
简介: Redis与KV存储的融合可以在性能可接受的情况下以Redis协议提供巨量数据的存储服务。本文以ArDB为例介绍了Redis与KV存储(RocksDB)融合之编码方式。

Redis与KV存储(RocksDB)融合之编码方式

简介

Redis 是目前 NoSQL 领域的当红炸子鸡,它象一把瑞士军刀,小巧、锋利、实用,特别适合解决一些使用传统关系数据库难以解决的问题。Redis 作为内存数据库,所有的数据全部都存在内存中,特别适合处理少量的热数据。当有巨量数据超过内存大小需要落盘保存时,就需要使用 Redis + KV存储的方案了。

本文涉及的Ardb就是一个完全兼容Redis协议的NoSQL的存储服务。其存储基于现有成熟的KV存储引擎实现,理论上任何类似B-Tree/LSM Tree实现的KV存储实现均可作为Ardb的底层存储实现,目前Ardb支持LevelDB/RocksDB/LMDB.

本文以Ardb为例,介绍Redis与KV存储之间融合时编解码层的实现。

编码方式

Redis与KV存储的融合方案中, 编解码层是一个很重要的环节。通过编解码层,我们可以屏蔽了各种kv存储实现的不同,可以在任意一个简单的kv存储引擎上,封装实现Redis中string,hash,list,set,sorted set等复杂类型的数据结构。

对于String类型,很显然可以与KV存储中的一个KV对一一对应;

对于其它的容器类型,我们需要

  • 一个KV来存储其整个Key的元信息(比如List的成员个数,过期时间等);
  • 每一个成员需要一个KV来保存成员的名称和值;

对于sorted set,其每个成员有score和rank两个属性,所以需要:

  • 一个KV保存整个Key的元信息
  • 每一个成员需要一个KV保存 score信息
  • 每一个成员需要一个KV保存每个成员对应 rank 信息

Key的编码格式

对于所有的Key, 包含同样的前缀,编码格式定义如下:

[<namespace>] <key> <type> <element...>

namespace用于支持类似redis中的库概念, 可以为任意字符串, 不限制必须为数字;
key则是一个变长二进制字符串
type用于定义一个简单key-value的类型,此类型隐含表明key的数据结构类型;一个字节
meta信息的key中type固定为KEY_META;具体类型将在value中定义(参考下一节)
除以上三部分外,不同类型的key可能有附加字段;如Hash的key可能需要附加field字段

Value的编码格式

内部Value则比较复杂,编码均以type开始, type取值即上节定义的KeyType

<type> <element...>

后续格式根据各种类型定义不同.

各类型数据编码方式

各类型数据的编码方式如下: ns代表namespace

            KeyObject                             ValueObject
String      [<ns>] <key> KEY_META                 KEY_STRING <MetaObject>
Hash        [<ns>] <key> KEY_META                 KEY_HASH <MetaObject>
            [<ns>] <key> KEY_HASH_FIELD <field>   KEY_HASH_FIELD <field-value>
Set         [<ns>] <key> KEY_META                 KEY_SET <MetaObject>
            [<ns>] <key> KEY_SET_MEMBER <member>  KEY_SET_MEMBER
List        [<ns>] <key> KEY_META                 KEY_LIST <MetaObject>
            [<ns>] <key> KEY_LIST_ELEMENT <index> KEY_LIST_ELEMENT <element-value>
Sorted Set  [<ns>] <key> KEY_META                 KEY_ZSET <MetaObject>
            [<ns>] <key> KEY_ZSET_SCORE <member>  KEY_ZSET_SCORE <score>
            [<ns>] <key> KEY_ZSET_SORT <score> <member> KEY_ZSET_SORT

ZSet编码实例

这里以最复杂的Sorted Set来做实例。假设有个Sorted Set为 A: {member=frist, score=1}, {member=second, score=2}。其在Ardb中的存储方式如下:
Key A的存储编码为:

// 伪代码中的|代表域的分割,不代表实际存储为"|"。实际序列化的时候每个域是按照特定位置序列化的.
键为:ns|1|A(1代表是KEY_META元信息类型) 
值为:元信息编码(redis数据类型/zset,过期时间,成员个数,最大最小score等)

成员first的score信息存储编码为:

键为:ns|11|A|first (11代表类型为KEY_ZSET_SCORE)
值为:11|1 (11代表类型KEY_ZSET_SCORE,1为该成员first的score)

成员first的rank信息存储编码为:

键为:ns|10|A|1|first (10代表类型为KEY_ZSET_SORT, 1为score)
值为:10 (代表类型KEY_ZSET_SORT,无意义。rocksdb中自动按key大小排序,所以很容易算出rank,不需要存储和更新)

成员second的score信息存储编码略。
当用户使用zcard A命令时,直接访问namespace_1_A即可得到元信息中该有序集合的数目;
当用户使用zscore A first时,直接访问namespace_A_first即可得到first成员的score;
当用户使用zrank A first时,先用zscore得到score,再查找namespace_10_A_1_first的序号;

具体的存储方式代码如下:

KeyObject meta_key(ctx.ns, KEY_META, key);
ValueObject meta_value;
for (each_member) {
  // KEY_ZSET_SORT 存储rank信息
  KeyObject zsort(ctx.ns, KEY_ZSET_SORT, key);
  zsort.SetZSetMember(str);
  zsort.SetZSetScore(score);
  ValueObject zsort_value;
  zsort_value.SetType(KEY_ZSET_SORT);
  GetDBWriter().Put(ctx, zsort, zsort_value); 
  
  // 存储score信息
  KeyObject zscore(ctx.ns, KEY_ZSET_SCORE, key);
  zscore.SetZSetMember(str);
  ValueObject zscore_value;
  zscore_value.SetType(KEY_ZSET_SCORE);
  zscore_value.SetZSetScore(score);
  GetDBWriter().Put(ctx, zscore, zscore_value);
}
if (expiretime > 0)
{
    meta_value.SetTTL(expiretime);
}
// 元信息
GetDBWriter().Put(ctx, meta_key, meta_value);

Del的实现

所有的数据结构都有保存meta的一个key-value,而meta信息的key编码格式是统一的,因此不可能出现不同数据结构有相同名字的情况。(这就是为什么保存Key的KV对中,K固定为KEY_META类型,而对应redis类型信息存在META类型数据的Value中的原因)。
Del实现中会先查询meta的key-value,得到具体数据结构类型,然后执行对应的删除工作, 类似如下的步骤:

  • 查询指定key的meta信息,得到数据结构类型
  • 根据具体类型,执行删除工作
  • 所以一次del至少需要 一次读 + 后续删除写操作

具体代码如下:

int Ardb::DelKey(Context& ctx, const KeyObject& meta_key, Iterator*& iter)
{
  ValueObject meta_obj;
  if (0 == m_engine->Get(ctx, meta_key, meta_obj))
  {
    // 如果是string类型直接删除即可
    if (meta_obj.GetType() == KEY_STRING)
    {
      int err = RemoveKey(ctx, meta_key);
      return err == 0 ? 1 : 0;
    }
  }
  else
  {
    return 0;
  }

  if (NULL == iter)
  {
    // 如果是复杂类型,需要按照namespace,key,类型前缀遍历库
    // 搜索出所有前缀为 namespace|类型|Key的成员
    iter = m_engine->Find(ctx, meta_key);
  }
  else
  {
    iter->Jump(meta_key);
  }
  
  while (NULL != iter && iter->Valid())
  {
    KeyObject& k = iter->Key();
    ...
    iter->Del();
    iter->Next();
  }
}

前缀搜索代码如下:

Iterator* RocksDBEngine::Find(Context& ctx, const KeyObject& key) {
  ...
  opt.prefix_same_as_start = true;
  if (!ctx.flags.iterate_no_upperbound)
  {
    KeyObject& upperbound_key = iter->IterateUpperBoundKey();
    upperbound_key.SetNameSpace(key.GetNameSpace());
    if (key.GetType() == KEY_META)
    {
      upperbound_key.SetType(KEY_END);
    }
    else
    {
      upperbound_key.SetType(key.GetType() + 1);
    }
    upperbound_key.SetKey(key.GetKey());
    upperbound_key.CloneStringPart();
  }
  ...
}

Expire的实现

在一个key-value存储引擎上支持复杂数据结构的expire过期数据的实现比较困难,ardb中则用几个特殊技巧实现了对所有数据结构的过期(expire)的支持。
具体实现如下:

  • meta的value中保存expire信息, 用绝对unix时间(ms)保存;
  • 基于以上设计,ttl/pttl等查询ttl的操作只需要一次读meta即可完成;
  • 基于以上设计,任何对meta信息的读取,都会触发expire的判断,由于对meta信息的读操作是必须的步骤,这里无需额外的读操作(和Redis一样访问时会触发)
  • 创建一个namespace TTL_DB专门存放TTL排序信息。
  • 保存设置expire时间到meta时, 当expire时间非0时,额外保存一个key-value, type为KEY_TTL_SORT; key的编码格式为 [TTL_DB] "" KEY_TTL_SORT , value为空;所以类似expire 等设置过期时间的操作,在ardb的实现中将会多一次写操作;
  • 在自定义的comparator中,对KEY_TTL_SORT类型的key比较规则为先比较,这样KEY_TTL_SORT数据将会以过期时间远近保存在一起
  • ardb中独立启动一个线程,每隔一定时间(100ms)顺序扫描KEY_TTL_SORT类型数据;当过期时间小于当前时间,即可触发删除操作;当过期时间大于当前时间,即可终止本次扫描。(相当于是Redis中的定时任务serverCron中处理过期Key)。

结语

通过编码层的转换,我们可以很好的对KV存储进行封装从而和Redis进行融合。所有对Redis数据的操作,经过编码层的转换,最终会转化为对KV存储的n次读写(N>=1)。在符合Redis命令语义的情况下,编码曾设计应当尽量的减少n的次数。
最重要的一点是,Redis与KV存储的融合并不是为了替换Redis,而是寻求一种在性能可接受的情况下使得单机能支持远超内存限制的数据量。在特定的场景中,也可以作为冷数据的存储方案与Redis热数据之间互联互通。

硬广来了,不要急:
即日起购买阿里云数据库Redis版(首台)3年5折、1年6折、按月7折啦,赶快点击购买吧:https://m.aliyun.com/markets/aliyun/act/redissaleoff?spm=5176.8112568.483450.6.o85c4v

阿里云数据库Redis团队诚邀加入:
https://job.alibaba.com/zhaopin/position_detail.htm?spm=0.0.0.0.l7roxQ&positionId=26437

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
27天前
|
存储 NoSQL 算法
09- Redis分片集群中数据是怎么存储和读取的 ?
Redis分片集群使用哈希槽分区算法,包含16384个槽(0-16383)。数据存储时,通过CRC16算法对key计算并模16383,确定槽位,进而分配至对应节点。读取时,根据槽位找到相应节点直接操作。
59 12
|
29天前
|
存储 缓存 NoSQL
深入解析Redis:一种快速、高效的键值存储系统
**Redis** 是一款高性能的键值存储系统,以其内存数据、高效数据结构、持久化机制和丰富的功能在现代应用中占有一席之地。支持字符串、哈希、列表、集合和有序集合等多种数据结构,适用于缓存、计数、分布式锁和消息队列等场景。安装Redis涉及下载、编译和配置`redis.conf`。基本操作包括键值对的设置与获取,以及哈希、列表、集合和有序集合的操作。高级特性涵盖发布/订阅、事务处理和Lua脚本。优化策略包括选择合适数据结构、配置缓存和使用Pipeline。注意安全、监控和备份策略,以确保系统稳定和数据安全。
304 1
|
3月前
|
存储 NoSQL 5G
redis优化编码之字符串
Redis数据结构之字符串
42 2
redis优化编码之字符串
|
4月前
|
存储 NoSQL Redis
redis存储原理和数据模型
redis存储原理和数据模型
29 1
|
4月前
|
存储 NoSQL 前端开发
jwt与redis,把生成的token放入redis中进行临时存储
jwt与redis,把生成的token放入redis中进行临时存储
81 0
|
2月前
|
存储 缓存 NoSQL
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(一)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
41 0
|
2月前
|
存储 NoSQL 算法
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(二)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
54 0
|
2月前
|
存储 机器学习/深度学习 NoSQL
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(二)
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)
24 0
|
2月前
|
存储 缓存 NoSQL
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(一)
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)
29 0
|
2月前
|
存储 NoSQL Redis
作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)(三)
作者推荐 |【Redis技术进阶之路】「原理系列开篇」揭秘高效存储模型与数据结构底层实现(SDS)
39 0

相关产品

  • 云数据库 Redis 版