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_VirtualMemorySpecification。Redis Virtual Memory: the story and the code也讨论了 Redis的虚拟内存系统。
Redis 不依赖操作系统交换,主要出于以下原因,
- Redis 对象并不与内存页一一映射,一页是 4096 字节,而一个 Redis对象可以跨越多个页。多个 Redis对象也可以放在一页上。因此,即使只访问少量 Redis 对象也可能触及大量页,操作系统会跟踪对页的访问。因此,即使一页里只有一字节被访问过,它也会被排除到交换系统之外。
- 和 mongodb 不同,Redis 数据在 RAM 中和在磁盘上的格式不同。在磁盘上的数据经过压缩,远小于在 RAM 中的大小,使用自定义的交换技术能减少磁盘IO。
- 如果像 mongodb 一样使用内存映射,没有分离开操作系统缓存和数据库缓存,那么虚拟内存映射将会受操作系统的限制,不同的操作系统,其内存映射方式也会有所不同,比如 System V、POSIX等。
Redis 中的数据类型及数据编码
基本数据类型有 字符串string、列表list、散列hash、集合set、有序集合sorted-set 五种,如果你是 Redis 中高级用户,还要加上 Bitmaps、HyperLogLog、Geo、Pub/Sub。
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_STRING | 字符串 | OBJ_ENCODING_INT | long long、long |
OBJ_ENCODING_EMBSTR | string | ||
OBJ_ENCODING_RAW | string | ||
OBJ_LIST | 列表 | OBJ_ENCODING_QUICKLIST | quicklist |
OBJ_SET | 集合 | OBJ_ENCODING_HT | dict |
OBJ_ENCODING_INTSET | intset | ||
OBJ_ZSET | 有序集合 | OBJ_ENCODING_ZIPLIST | ziplist |
OBJ_ENCODING_SKIPLIST | skiplist | ||
OBJ_HASH | 散列 | OBJ_ENCODING_HT | dict |
OBJ_ENCODING_ZIPLIST | ziplist | ||
OBJ_STREAM | 消息流 | OBJ_ENCODING_STREAM | rax |
OBJ_MODULE | Module 自定义类型 | OBJ_ENCODING_RAW | Module 自定义 |
数据类型的编码格式,即数据的存储格式,数据库中的数据存储格式非常重要,如 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,需要满足下面两个条件,
- 散列中所有 kv 的长度小于或等于 server.hash_max_ziplist_value;
- 散列中 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,需要满足下面两个条件,
- zset中元素数量小于或等于 server.zset_max_ziplist_entries;
- zset中 所有元素长度小于或等于 server.zset_max_ziplist_value。
eg.
ZADD fruitsWithPrice 8.8 apple 4.98 banana 6.8 grape
ZRANGE fruitsWithPrice 0 1
Have Fun