前言
Redis(Remote Dictionary Server)是一个开源的高性能键值对(key-value)数据库,也称为远程字典服务。它通常被用作数据结构服务器,因为它支持各种类型的数据结构。Redis 以其卓越的性能和低延迟特性而闻名,非常适合用作数据库、缓存系统和消息代理。 Redis作为一种服务,使用TCP连接通信,每个命令都是TCP请求,然后Redis再进行TCP回应。
Redis特点:
- 内存数据库:Redis 将所有数据存储在内存中,这使得它能够提供极快的读写速度。
- 键值对数据库:使用key操作value,所有对Redis的数据操作都要用到与之对应的key。
- 数据结构数据库:Redis的value又可以是多种数据结构,如string、list、hash、zset、set等。
数据结构与应用
命令不需要刻意去记,而是要多使用,干记是没有用的,可以参考命令手册或者问AI,因为我如果写命令,这篇博客就是操作手册了,画蛇添足罢了。五种常用数据结构如下:
- string:与c++中的std::string类似的二进制安全字符串,不同于c字符串的以\0作为分隔符,而是以长度作为分隔符,这样就能存储二进制字符串,图片视频等。
- hash:哈希表,最外层是哈希表,value本身也是哈希表,也就是Key-<key,value>,但是value都是string类型,存int也是string。
- list:双向循环列表,本身有序,按照插入先后顺序存储,不去重。
- set:集合,去重,无序。
- zset:有序集合,用score字段进行排序。
大家可以直接搜索redis源码,redis是开源的,而且设计非常巧妙,值得去细品。我们接下来讨论redis常用的几种数据结构的底层及应用。
string
string底层是动态数组。对于字符串内存空间申请,redis会选择性使用几种不同长度的结构体(pow(2,16),pow(2,32),pow(2,64)),以便合理分配内存,每个结构内部有一个柔性数组,这样只会free一次,如果是char*就需要free外部的结构体,并且free char*数组。
/* 针对不同的字符串设置了不同的结构体,主要差别在于len和alloc的数据类型,不同长度使用
* 不同的数据类型,以达到节省内存的目的。
*
* 注意:sdshdr5从未被使用过,我们只是直接访问flag。但是,这里记录下sdshdr5的结构。 */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用空间大小 */
uint8_t alloc; /* 总共可用的字符空间大小,应该是实际buf的大小减1(因为c字符串末尾必须是\0,不计算在内) */
unsigned char flags; /* 标志位,主要是识别这是sdshdr几,目前只用了3位,还有5位空余 */
char buf[]; /* 真正存储字符串的地方 */
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
string虽然功能单一,但是玩法却sao的很。
- 存储对象:string可以用于存储对象,比如MySQL的row、JSON等等,但是如果value经常会改变,不适合用string存储。
- 累加器:可以用incr或incrby命令对value进行累加操作。
- 分布式锁:用redis实现非公平锁,setnx lock 1占用锁,不存在才能设置,返回1加锁成功,还可以设置过期时间。
- 位运算:使用setbit可进行位运算,比如实现签到功能,按日历每个月签到,可以使用位运算标记。用bitcount可统计签到次数。
list
list用双向链表实现,首尾操作时间复杂度为O(1),查询中间元素时间复杂度为O(n)。内部可能会进行压缩操作,元素长度小于48则不压缩,否则会压缩,但是压缩后长度与压缩前长度差不超过8就不会压缩。
/* quicklistNode 中用了32个字节存储一个节点,三个指针总计24字节,加sz和后面按位用的几个int,总计32字节
* 用了32个bit来保存ziplist的信息。
* count: 16位,最大65536(最大ziplist的大小是65k,所以实际上count小于32k)
* encoding: 2位,RAW=1, LZF=2.
* container: 2位,NONE=1, ZIPLIST=2.
* recompress: 1位,bool类型,如果是true表示当前节点是临时使用的解压后的节点.
* attempted_compress: 1位, boolean类型,基本是在测试中使用
* extra: 用剩下的10位,暂时没有用到,留给之后的feature */
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl; /* quicklist节点对应的ziplist */
unsigned int sz; /* ziplist的字节数 */
unsigned int count : 16; /* ziplist的item数*/
unsigned int encoding : 2; /* 数据类型,RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* 这个节点以前压缩过吗? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* 未使用到的10位 */
} quicklistNode;
/* quicklist是一个40字节的结构体(在64位系统中),具体字段如下:
* count: quicklist中所有数据项的个数
* len:quicklist中的节点数
* compress: quicklist的压缩深度,0表示不压缩,否则就表示从两端开始有多少个节点不压缩
* bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间 */
typedef struct quicklist {
quicklistNode *head; /* 头结点 */
quicklistNode *tail; /* 尾结点 */
unsigned long count; /* 在所有的ziplist中的entry总数 */
unsigned long len; /* quicklist节点总数 */
int fill : QL_FILL_BITS; /* 16位,每个节点的最大容量*/
unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
unsigned int bookmark_count: QL_BM_BITS; /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
quicklistBookmark bookmarks[];
} quicklist;
list的用法:
- 命令组合:可以通过命令组合实现栈、队列等数据结构,比如LPUSH+LPOP实现栈,LPUSH+RPOP实现队列,LPUSH+BRPOP实现阻塞队列(队列为空时则阻塞到有元素可用,用于生产者消费者场景)。
- 获取固定窗口记录:使用lpush+ltrim可确保list只保留指定长度的最近记录,可用于实现最近评论,最近战绩等。
hash
在redis中最多存在两层hash,也就是外部的key接hash数据结构,内部hash的value就不能是hash了。如果值经常会改变就需要使用hash,因为string会将值加密解密。Redis 的 Hash 数据类型用于存储键值对集合,它可以被视为包含多个字段的单一值。在 Redis 中,Hash 数据类型是一个非常灵活的结构,因为它允许你存储和管理对象属性,而不需要将每个属性作为独立的键存储。
-
ziplist(压缩列表):这是早期 Redis 版本中 Hash 的默认内部编码方式。ziplist 是一种紧凑的列表编码方式,它将所有元素存储在一块连续的内存空间中。这种方式适合存储小的 Hash 结构,因为它可以极大地节省内存。但是,当 Hash 中的字段或值较大时,ziplist 的性能会下降,因为它需要在内存中移动元素。
-
hashtable(哈希表):随着 Redis 版本的更新,当 Hash 中的元素数量或元素大小超过一定阈值时,Redis 会将 ziplist 转换为 hashtable。这种结构由数组和链表组成,提供了更好的性能,尤其是在处理大量数据时。hashtable 内部使用多个散列表来减少哈希冲突,并动态调整大小以优化性能。
-
quicklist:在 Redis 3.0 及以后的版本中,List 数据类型开始使用 quicklist 作为内部编码,这是一种结合了压缩列表(ziplist)和普通链表(linkedlist)的数据结构。虽然这是 List 数据类型的内部编码,但值得注意的是,Hash 数据类型在超过一定阈值后,也会从 ziplist 转换为基于 quicklist 的结构,以提高性能和内存利用率。
typedef struct {
/* When string is used, it is provided with the length (slen). */
unsigned char *sval;
unsigned int slen;
/* When integer is used, 'sval' is NULL, and lval holds the value. */
long long lval;
} ziplistEntry;
hash一般与其他结构组合使用:
- 购物车:物品信息是会经常改变的,就不能用string,物品按加入的先后顺序排序,就需要使用list,这时候可以list+hash组合使用。
- 游戏玩家信息:在线玩家可以使用set存储在线的玩家,而要知道用户属性,可以用hash+set。
set
set本身是无序集合,但底层确是有序的,这样集合做交并差操作更方便。它类似于数学中的集合,可以存储一组不重复的值。Set 数据类型在 Redis 中的内部实现也采用了不同的数据结构,以优化内存使用和操作性能。
-
intset(整数集合):这是 Set 数据类型的早期内部编码方式,适用于存储整数元素的集合。intset 是一个有序数组,它可以存储整数值,并且保持有序,这使得范围查询变得非常高效。intset 适合存储小的、包含整数值的集合,因为它在内存使用上非常高效。但是,当集合中的元素数量增加时,intset 的性能会下降,因为它需要在数组中移动元素。
-
hashtable(哈希表):当 Set 集合中的元素数量或元素的类型超过一定阈值时,Redis 会将 intset 转换为 hashtable。这种结构使用散列表来存储元素,提供了更好的性能,尤其是在处理大量数据或非整数值时。hashtable 内部使用多个散列表来减少哈希冲突,并动态调整大小以优化性能。
-
quicklist:虽然 quicklist 主要是 List 数据类型的内部编码,但在某些情况下,Redis 的 Set 数据类型也可能使用类似 quicklist 的结构,尤其是当集合中的元素数量非常大时。这种结构结合了压缩列表(ziplist)和普通链表(linkedlist)的优点,以提高性能和内存利用率。
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
set用法:
- 抽奖:set表面是无序唯一的,就可以用来作抽奖。
- 共同关注:使用集合计算操作选出共同关注。
zset
Zset(有序集合)是一个复杂的数据结构,它结合了 Set(集合)的特性,并为每个元素增加了一个排序属性 score(分值)。Zset 允许你存储不重复的成员,并且可以根据 score 进行排序。Zset 的内部实现使用了两种不同的数据结构:ziplist(压缩列表)和 skiplist(跳跃表)。
-
ziplist(压缩列表):当 Zset 满足以下条件时,Redis 使用 ziplist 作为底层实现:
- 有序集合保存的元素个数要小于 128 个;
- 有序集合保存的所有元素成员的长度都必须小于 64 字节。 ziplist 是一种紧凑的存储方式,它将所有的元素和分数紧密地存储在一起,形成了一个有序的列表。这种方式在元素数量较少且元素长度较短时非常高效,因为它减少了内存的使用。
-
skiplist(跳跃表):当 Zset 不满足使用 ziplist 的条件时,Redis 会使用 skiplist 作为底层结构。Skiplist 是一种随机化的数据结构,它通过在有序链表上增加多级索引来提高查找效率。Skiplist 允许快速的查找、插入和删除操作,时间复杂度为 O(logN)。在 Redis 的实现中,skiplist 通常由多层链表组成,每一层都是一个有序的链表,上层的链表是对下层的索引。这样可以在高层进行快速查找,然后迅速定位到下层的具体元素。
zset用法:
- 热搜:有序集合唯一有序的特性可以实现热搜榜。
- 延时队列:可以将消息队列当做一个字符串作为zset的member,消息的到期处理时间作为score,多线程轮询zset获取到期任务进行处理。