Redis五种基本数据类型底层实现
1、Redis是什么
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 **字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) **与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。
2、RedisObject
Redis是一个k-v数据库,默认有16个数据库,每一个db 会有两个dict, 一个dict存储数据内容,一个dict 来记录失效时间。存储数据的dict,key是String,value就是RedisObject。RedisObject 有五种对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。redisDb和ReidsObject的结构如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
typedef struct redisObject {
unsigned type:4;/* 类型 */
unsigned encoding:4;/* 编码 */
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). 用于记录缓存对象最后被访问的时间*/
int refcount;/* 引用计数 */
void *ptr;/* 指向底层数据结构的指针 */
} robj;
RedisDB结构如下图所示:
RedisObject各字端作用:
- type: 对象类型 ,即五种基本类型:字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets
/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
- encoding:编码方式,同一个类型的 type 会有不同的编码方式,
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
#define OBJ_SHARED_REFCOUNT INT_MAX /* Global object never destroyed. */
#define OBJ_STATIC_REFCOUNT (INT_MAX-1) /* Object allocated in the stack. */
#define OBJ_FIRST_SPECIAL_REFCOUNT OBJ_STATIC_REFCOUNT
- lru:LRU_BITS: LRU 信息,用于记录缓存对象最后被访问的时间
- refcount:引用计数,每个对象都有个引用计数 refcount,当引用计数为零时,对象就会被销毁,内存被回收。
- ptr 指针:将指向对象内容 的具体存储位置。
- type和encoding的对应关系如下图:
3、基本的数据结构
String
- String的底层是动态字符串SDS(Simple Dynamic String)
sds结构一共有五种Header定义,其目的是为了满足不同长度的字符串可以使用不同大小的Header,从而节省内存。 Header部分主要包含以下几个部分:
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
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; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
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[];
};
- len:表示字符串真正的长度,不包含空终止字符 字符串的长度(实际使用的长度)
- alloc:表示字符串的最大容量,不包含Header和最后的空终止字符
- flags:标志位,低三位表示类型,其余五位未使用
- buf:字符数组
- encoding可能是int、raw、embstr
查看内部编码格式:
127.0.0.1:6379> set name 1234567890123456789 #小于(2^63)-1 9223372036854775807 则是int
OK
127.0.0.1:6379> debug object name
Value at:0x7fccbec160b0 refcount:1 encoding:int serializedlength:20 lru:6679383 lru_seconds_idle:2
127.0.0.1:6379> set name 12345678901234567890 #大于(2^63)-1 是embstr
OK
127.0.0.1:6379> debug object name
Value at:0x7fccbec16060 refcount:1 encoding:embstr serializedlength:21 lru:6679391 lru_seconds_idle:3
127.0.0.1:6379> set name 123456789012345678w #19位是embstr
OK
127.0.0.1:6379> debug object name
Value at:0x7fccbec160b0 refcount:1 encoding:embstr serializedlength:20 lru:6680701 lru_seconds_idle:3
127.0.0.1:6379> set name 12345678901234567890123456789012345678901234
OK
127.0.0.1:6379> debug object name #44位是embstr
Value at:0x7fccc0010f40 refcount:1 encoding:embstr serializedlength:21 lru:6669496 lru_seconds_idle:1
127.0.0.1:6379> set name 123456789012345678901234567890123456789012345
OK
127.0.0.1:6379> debug object name #45位是raw
Value at:0x7fccc000f020 refcount:1 encoding:raw serializedlength:21 lru:6669503 lru_seconds_idle:3
- int: 可以用long类型的整数表示 Redis会将键值转化为 long型来进行存储,此时即对应
OBJ_ENCODING_INT
编码类型。 - raw:长度大于44字节的字符串,使用SDS(简单动态字符串)保存
- embstr:长度小于等于44字节的字符串,效率比较高,且数据都保存在一块内存区域
- 常用命令
################################### String #######################################
AmydeMacBook-Pro:bin zhengamy$ redis-cli -h 127.0.0.1 -p 6379 #连接redis
127.0.0.1:6379> select 0 #切换数据库,默认有16个数据库
127.0.0.1:6379> keys * #查看数据库所有的key
127.0.0.1:6379> expire name 10 #设置过期时间单位是秒
127.0.0.1:6379> ttl name #查看剩余时间
127.0.0.1:6379> ttl name #查看剩余时间
127.0.0.1:6379> type name #查看类型
127.0.0.1:6379> exists name #是否存在key
127.0.0.1:6379> append name "hello" #追加字符串,如果当前key不存在,就相当于setkey
127.0.0.1:6379> strlen name #获取字符串的长度!
127.0.0.1:6379> incr count #自增1
127.0.0.1:6379> decr count #自减1
127.0.0.1:6379> incrby count 10 #可以设置步长,指定增量
127.0.0.1:6379> decrby count 5 #可以设置步长,指定减量
127.0.0.1:6379> getrange name 0 2 #截取字符串 [0,2]
127.0.0.1:6379> getrange name 0 -1 #获取全部字符串,相当于getkey
127.0.0.1:6379> set age 13 ex 10
127.0.0.1:6379> setex age 10 13 #设置过期时间,将age的值设置为13,10秒过期
127.0.0.1:6379> setnx name amy #不存在才会设置值,存在不会设置
127.0.0.1:6379> mset name saber age 22 sex 女 #同时设置多个值
127.0.0.1:6379> mget name age sex #同时获取多个值
127.0.0.1:6379> msetnx score 99 age 22 #msetnx 是一个原子性的操作,要么一起成功,要么一起失败
127.0.0.1:6379> getset name amy #先获取结果然后在修改值
127.0.0.1:6379> object encoding name #查看底层数据结构
"embstr"
127.0.0.1:6379> object encoding age #查看底层数据结构
"int"
List
- List的底层是链表
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
- encoding早期使用ziplist或linkedlist,Redis 3.2版本后list使用quicklist
- ziplist:压缩列表,适用于长度较小的值,其是由连续空间组成(会保存每个值的长度信息,因此可依次找到各个值)
- 存取效率高,内存占用小,但是由于是连续内存,修改操作需要重新分配内存
- linkedlist:双向链表,修改效率高,但是由于需要保存前后指针,占内存比较多
- quicklist:3.2之后引入的,可以理解成是一种混合结构,quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余**。
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
- 常用命令
################################### list #######################################
127.0.0.1:6379> lpush list 3 #将一个值或者多个值,插入到列表头部(左)
127.0.0.1:6379> lrange list 0 -1 #获取list中值
127.0.0.1:6379> lpop list #从列表头部(左)获取值
127.0.0.1:6379> rpop list #从列表尾部(右)获取值
127.0.0.1:6379> lrange list 0 1 #指定区间获取list中值
127.0.0.1:6379> rpush list -1 #将一个值或者多个值,插入到列表尾部(右)
127.0.0.1:6379> lindex list 0 #获取指定下表的值
127.0.0.1:6379> lrem list 1 2 #移除1个list中的值为2的元素
127.0.0.1:6379> llen list #返回列表成都
127.0.0.1:6379> ltrim list 1 2 #按照下标截取list
127.0.0.1:6379> rpoplpush list otherlist #移除列表的最后一个元素,将他移动到新的列表中
127.0.0.1:6379> lset list 1 -1 #将列表中指定下标的值替换为另外一个值,更新操作,不存在会报错(将下标为1的元素设置为-1)
127.0.0.1:6379> linsert list before -1 0 # 将某个具体的value插入到列把你中某个元素的前面或者后面!(将下标为0插入到1的前面)
127.0.0.1:6379> object encoding list #查看底层数据结构
"quicklist"
Hash
- Hash的底层是dict
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
- encoding使用ziplist或者hashtable
- ziplist: 当键和值的长度和数量都比较少时(64B和512个),默认使用ziplist,hash过程是通过直接遍历得到的,由于数据量小,且都在内存中,效率仍然很高
- hashtable: 否则,则采用hash表提高效率
- 常用命令
################################### hash #######################################
127.0.0.1:6379> hset map field1 val1 #设置一个具体 [key, key-vlaue]
127.0.0.1:6379> hget map field1 #获取一个字段值
127.0.0.1:6379> hgetall map #获取全部的数据
127.0.0.1:6379> hexists map field4 #判断hash中指定字段是否存在
127.0.0.1:6379> hsetnx map field1 val1 #不存在才会设置值,存在不会设置
127.0.0.1:6379> hmset map field2 val2 field3 val3 #设置多个
127.0.0.1:6379> hkeys map #只获得所有field
127.0.0.1:6379> hvals map #只获得所有val
127.0.0.1:6379> hlen map #获取hash表的字段数量
127.0.0.1:6379> hdel map field3 #删除hash指定key字段,对应的value值也就消失了
127.0.0.1:6379> hincrby map field3 4 #可以设置步长,指定增量
127.0.0.1:6379> hset map 1234567890123456789012345678901234567890123456789012345678901234 1234567890123456789012345678901234567890123456789012345678901234
(integer) 1
127.0.0.1:6379> object encoding map #64位 ziplist
"ziplist"
127.0.0.1:6379> hset map 12345678901234567890123456789012345678901234567890123456789012345 12345678901234567890123456789012345678901234567890123456789012345
(integer) 1
127.0.0.1:6379> object encoding map #65位 hashtable
"hashtable"
Set
- encoding使用intset或者hashtable
- intset: 集合中的数都是整数时,且数据量不超过512个,使用intset(有序不重复连续空间)
- 节约内存,但是由于是连续空间,修改效率不高
- 整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
- insert结构
```c++
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;++
```
- hashtable:其中value使用NULL填充
- 常用命令
################################### set #######################################
127.0.0.1:6379> sadd set hi #添加元素
127.0.0.1:6379> scard #获取set集合中的内容元素个数
127.0.0.1:6379> sismember hello #判断是否存在值
127.0.0.1:6379> smembers set #查看所有元素
127.0.0.1:6379> srandmember set #随机获取一个元素
127.0.0.1:6379> srandmember set 2 #随机获取指定个素元素
127.0.0.1:6379> spop myset #随机删除set集合中的元素
127.0.0.1:6379> srem set hellow #移除指定元素
127.0.0.1:6379> smove set myset hello #将一个指定的值,移动到另外一个set集合
127.0.0.1:6379> sunion set myset #获取两个集合的并集
127.0.0.1:6379> sdiff set myset #获取两个集合的差集,移除set中和myset一样的元素
127.0.0.1:6379> sinter set myset #获取两个集合的交集
127.0.0.1:6379[1]> object encoding set #所有元素是数值 intset
"intset"
127.0.0.1:6379[1]> sadd set a
(integer) 1
127.0.0.1:6379[1]> object encoding set #包含非数字 hashtable
"hashtable"
ZSet
- encoding使用ziplist或者skiplist
- ziplist:连续存放值以及score(排序的标准,double),当元素个数及长度都比较小时使用
- skiplist:跳表(具有层次结构的链表),因此可支持范围查询
- 跳表结构
typedef struct zskiplistNode {
sds ele;//成员对象
double score;//分值
struct zskiplistNode *backward;//后退指针
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned long span;//跨度 用来计算排位(rank)的
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;//表头节点,表尾节点
unsigned long length;//跳跃表的长度
int level;//层数最大的那个节点的层数
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
- 查找和插入的时间复杂度都是logn
按照层次组织,上面层的step大,因此查找快,从上往下依次确定范围。
- 查找时,从最上面一层开始查找,直到找到大于或null,然后指向前一个节点的下一层
- 新增时,为了保证每层的数量能够满足要求(第i层的某个数出现的概率是P,则上一层该数的概率是1/(1 - p),一般取p=0.5),因此需要随机产生该数的层数,并保证概率。实现上需要找到所有前驱
- redis实现上,每一层数的随机的实现:
randomLevel()
level := 1
// random()返回一个[0...1)的随机数 p = 1/4 maxlevel=32
while random() < p and level < MaxLevel do
level := level + 1
return level
- 删除时,需要考虑前驱的next节点改变,同时考虑最大level是否变化
- 同时使用一个dict保存每个值对应的score
- 为什么使用跳表:
- 其占用的内存开销可控(通过控制概率P)
- 支持范围查询,比如zrange
- 跳跃表优点是有序,但是查询分值复杂度为O(logn);字典查询分值复杂度为O(1) ,但是无序,所以结合连个结构的有点进行实现
- 虽然采用两个结构但是集合的元素成员和分值是共享的,两种结构通过指针指向同一地址,不会浪费内存。
- 常用命令
################################### sorted set #######################################
127.0.0.1:6379> zadd salary 2800 amy #添加一个值
127.0.0.1:6379> zrem salart saber #删除一个值
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #按照score升序排序,并附带score
127.0.0.1:6379> zrevrangebyscore salary +inf -inf withscores #按照score降序排序,并附带score
127.0.0.1:6379> zcard salary #获取有序集合的个数
127.0.0.1:6379> zcount salary 1000 3000 #获取指定区间的元素
127.0.0.1:6379> zadd zset 1234567890123456789012345678901234567890123456789012345678901234 1234567890123456789012345678901234567890123456789012345678901234
(integer) 1
127.0.0.1:6379> object encoding zset #64位 ziplist
"ziplist"
127.0.0.1:6379> zadd zset 12345678901234567890123456789012345678901234567890123456789012345 12345678901234567890123456789012345678901234567890123456789012345
(integer) 1
127.0.0.1:6379> object encoding zset #65位 skiplist
"skiplist"