Redis 及其数据类型、编码格式

Redis,全称为远程字典服务(Remote Dictionary Server)。

Redis 印象

Redis 为什么叫 Redis(远程字典服务)?

从形式上,作为开源的 kv 存储系统,使用了字典 dict 结构来管理数据,内部定义了数据库对象 server.h/redisDb 负责存储数据。

typedef struct redisDb {
    dict *dict;     // 数据库字典,该redisDb所有的数据都存储在这里
    dict *expires;    // 过期字典,存储了 Redis 中所有设置了过期时间的 key及其对应的过期时间
    dict *blocking_keys;    // 处于阻塞状态的 key 和相应的客户端
    dict *ready_keys;        // 准备好数据后可以解除阻塞状态的 key 和相应的客户端
    dict *watched_keys;        // 被 watch 命令监控的 key 和相应的客户端
    int  id;        // 数据库 ID 标识
    ...
} redisDb;

redisDb.dict 字典中的 key 都是 sds(简单动态string),用 redisObject 来装载以供存储,值 v 也都是 redisObject(将所有的数据结构(比如字符串、列表、散列、集合等)都封装为 redisObject 结构,作为 redisDb 字典的值 v)。当需要操作 Redis 数据时,都需要从 redisDb 中找到该数据。可以说 Redis 中万物皆是字符串,像列表、散列、集合、有序集合这样的结构也由字符串组成。

其中,Redis 中的数据对象 server.h/redisObject 是 Redis 对内部存储的数据定义的抽象类型,它负责装载 Redis 中的所有键和值,定义如下,

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;    // 指向实际的数据结构,如sds、quicklist等,真正的数据存储在该数据结构中
} robj;

sds,Simple Dynamic String,可看作 C 字符串的扩展,C 中将空字符 '\0' 结尾的字符数组作为字符串,在 Redis 的 sds.h/sds.c 中对不同长度的字符串定义了不同的 sds 结构体,

typedef char *sds;
struct __attribute__((__packed__)) sdshdr5 {
    unsigned char flags;
    char buf[];
};

struct __attribute__((__packed__)) sdshdr8 {  // sdshdr16、sdshdr32、sdshdr64 和这个类似
    uint8_t len;
    uint8_t alloc;
    unsigned char flags;
    char buf[];    // 使用柔性数组存储字符串内容,sds遵循C字符串的规范,保留一个空字符作为buf的结尾,且不计入 len、alloc属性
}

注意,Redis 中的 key 都是字符串类型sds,Redis 中最简单的值类型也是sds,复杂的值类型有很多,比如quicklist等,Redis 存储时会把键和值装载到 redisObject 变量中存入数据库的 dict 结构中,也可以说 Redis 中所有键和值在存储层面上都是 redisObject 变量。一个字典 dict 中,键、值可以是不同的类型,但键必须类型相同,值也必须类型相同。

与 memcache 直接使用物理内存、mongodb直接使用内存映射不同,Redis 直接实现了自己的虚拟内存子系统,理论上能存储比物理内存更多的数据,当一个值被换出到磁盘上时,一个指向那个磁盘页的指针会和键一起存储,具体内容参见Google_Redis_VirtualMemorySpecificationRedis Virtual Memory: the story and the code也讨论了 Redis的虚拟内存系统。

Redis 不依赖操作系统交换,主要出于以下原因,

  1. Redis 对象并不与内存页一一映射,一页是 4096 字节,而一个 Redis对象可以跨越多个页。多个 Redis对象也可以放在一页上。因此,即使只访问少量 Redis 对象也可能触及大量页,操作系统会跟踪对页的访问。因此,即使一页里只有一字节被访问过,它也会被排除到交换系统之外。
  2. 和 mongodb 不同,Redis 数据在 RAM 中和在磁盘上的格式不同。在磁盘上的数据经过压缩,远小于在 RAM 中的大小,使用自定义的交换技术能减少磁盘IO。
  3. 如果像 mongodb 一样使用内存映射,没有分离开操作系统缓存和数据库缓存,那么虚拟内存映射将会受操作系统的限制,不同的操作系统,其内存映射方式也会有所不同,比如 System V、POSIX等。

Redis 中的数据类型及数据编码

基本数据类型有 字符串string列表list散列hash集合set有序集合sorted-set 五种,如果你是 Redis 中高级用户,还要加上 Bitmaps、HyperLogLog、Geo、Pub/Sub。

Redis 基础数据类型及其编码格式
数据类型说明编码使用的数据结构
OBJ_STRING字符串OBJ_ENCODING_INTlong long、long
OBJ_ENCODING_EMBSTRstring
OBJ_ENCODING_RAWstring
OBJ_LIST列表OBJ_ENCODING_QUICKLISTquicklist
OBJ_SET集合OBJ_ENCODING_HTdict
OBJ_ENCODING_INTSETintset
OBJ_ZSET有序集合OBJ_ENCODING_ZIPLISTziplist
OBJ_ENCODING_SKIPLISTskiplist
OBJ_HASH散列OBJ_ENCODING_HTdict
OBJ_ENCODING_ZIPLISTziplist
OBJ_STREAM消息流OBJ_ENCODING_STREAMrax
OBJ_MODULEModule 自定义类型OBJ_ENCODING_RAWModule 自定义

数据类型的编码格式,即数据的存储格式,数据库中的数据存储格式非常重要,如 RDBMS 的行式存储和列式存储。Redis 作为内存数据库,对于数据编码的设计思想是,最大限度地“以时间换空间”,从而最大限度地节省内存。

数据底层存储结构
数据结构说明
sds可变长数组实现的动态字符串,C99标准(定义在 sds.h/sds.c)
ziplist

一种类似数组的紧凑型链表格式,申请一整块内存,在这个内存上存放该链表所有数据,不过这种结构对插入、删除不太友好,数组类型你懂的。ziplist整体内存布局使用小端字节序进行字节顺序存储,CPU 处理指令通常是按照内存地址增长方向执行的,使用小端字节序,CPU可以先读取并处理低位字节,执行计算的借位、进位操作效率更高(定义在 ziplist.h/ziplist.c )

quicklist  

将一个长 ziplist 拆分为多个短 ziplist,避免插入、删除元素时导致大量内存拷贝。

ziplist 存储数据的形式更类似与数组,quicklist是链表结构,它由quicklistNode节点链接而成,在quicklistNode中使用ziplist存储数据,而且会使用LZF无损压缩访问频率低的中间节点数据(定义在 quicklist.h/quicklist.c)

dict使用 hash表实现 dict 结构,Redis 字典用 SipHash 算法进行散列,使用链表法解决Hash冲突,并实现了自己渐进式的扩缩容(定义在 dict.h/dict.c)
intset如果一个集合全是整数,使用 dict 太浪费内存,会使用 intset 数据结构专门存放整数集合数据(定义在 intset.h/intset.c)
skiplist一个多层级的链表结构,通过概率平衡实现近似平衡p叉树的数据存取效率,高层查找时,每向后移动一个节点,实际上会跨越低层多个节点,这样便大大提升了查找效率,最终达到二叉查找的效率(定义在 server.h/zskiplistNode)

字符串 string

Redis 定义的字符串类型是 sds,支持二进制安全和扩容,可以在常数时间内获取字符串长度,并使用预分配内存机制减少内存拷贝次数。

eg.

SET msg "Hello"

列表 list

存储一组按照插入顺序排序的字符串,支持两端插入、弹出数据,可充当栈和队列的角色。底层数据存储使用 quicklist。

eg.

LPUSH fruit apple
RPUSH fruit banana
RPOP fruit
LPOP fruit

列表 list 为什么不使用一般链表实现保存用户列表数据?

因为一般链表对内存管理不够友好,链表中每一个节点都占用独立的一块内存,导致内存碎片过多。链表节点中的前后节点指针占用过多的额外内存。

散列 hash

存储一组无序的键值对,适用于存储一个对象数据。底层数据存储,散列会优先使用 ziplist,使用一个 ziplist 节点存储 key,后驱节点存放 val,查找时需要遍历ziplist。使用 dict 存储,字典的 kv 都是 sds 类型。

hash 使用 ziplist,需要满足下面两个条件,

  1. 散列中所有 kv 的长度小于或等于 server.hash_max_ziplist_value;
  2. 散列中 kv对 的数量小于 server.hash_max_ziplist_entries。

eg.

HSET fruit name apple price 8.9 origin china-henan
HGET fruit price

集合 set

存储一组不重复的数据。底层数据存储,如果是 intset,需满足集合元素全是整数。dict 存储集合数据,key 存储集合元素,val 为 nil。

eg.

SADD fruits apple banana grape
SMEMBERS fruits

有序集合 sorted set

通过每个元素关联的分数排序,数据是有序的。底层数据存储,使用 ziplist、skiplist。

zset 使用 ziplist,需要满足下面两个条件,

  1. zset中元素数量小于或等于 server.zset_max_ziplist_entries;
  2. zset中 所有元素长度小于或等于 server.zset_max_ziplist_value。

eg.

ZADD fruitsWithPrice 8.8 apple 4.98 banana 6.8 grape
ZRANGE fruitsWithPrice 0 1

Have Fun

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值