目录
0.key
1.string
2.list
3.set
4.hash
5.sorted set(zset)
6.streams
7.衍生类型
7.1 bitmaps
7.2.geo
Redis 5之前一共有六个主要数据结构:key、string、list、set、hash、sorted-set(zset),还有由string衍生出来的bitmaps、由zset衍生出来的geo。key类型实际就是byte数组,因此可以将任何二进制序列作为key使用,虽然key最大可以为512MB,不过最好还是尽量小一点,一般约定redis的key形式如:user:0001:name,代表id为0001的用户的姓名,这种命名方式兼顾可读性和性能。
Redis 5新增了stream类型。
下面是一些通用操作(或者说是对于key类型的操作):
127.0.0.1:6379> scan 0
1) "15"
2) 1) "key3"
2) "key2"
3) "key6"
4) "key9"
5) "key4"
6) "key1"
7) "key10"
8) "key7"
9) "key8"
10) "key5"
127.0.0.1:6379> scan 15
1) "0"
2) (empty list or set)
可以看到,第一次执行scan 0后,返回的新cursor是15,表示这一批最多遍历了15个数据,下一次执行scan需要从15开始,不过因为数据不够,所以执行scan 15后,cursor又回到了0。
此外还有一些Redis管理命令:
以上命令只是一部分,Redis的所有命令可以查看官方文档:https://redis.io/commands
string可以用来表示3种值:字符串、正数、浮点数,可以自动进行转换,它支持的操作如下:
对于set、get,如果要设置的键非常多,最好使用mset、mget替代,因为n次set/get需要n次网络I/O和n次读写,而批量操作n个元素则只需要1次网络I/O和n次读写,分布式环境下,网络I/O是主要的延迟来源,减少网络I/O次数可以显著提升性能。
此外,对于数值,如果使用了append等操作,就会永远转换为字符串,再也无法使用数字类型的操作。
string一共有三种内部编码:int、embstr、raw,第一个可以存储8字节长整型数据,后面两个用于存储字符串,embstr可以处理39字节以下的数据,raw可存储39字节以上的数据。其中,字符串类型实际存储在sdshdr结构体中(定义于sds.h头文件),以sdshdr8为例:
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[];
};
buf数组存储了字符串的内容,实际长度会大于存储的内容的长度,以便在append时不必重新申请内存。string刚初始化时,sizeof(buf)=len+1(还要存放一个'/0'),在字符串操作完成后,sizeof(buf)=2*len+1(len<1MB)或2*len+1MB(len>=1MB)。
string的主要应用有:
Redis的list具有双端进出、有序的特点,可最多存放个元素,其操作如下:
list的内部编码有ziplist、linkedlist和quicklist三种,ziplist的结构如下:
...
zlbytes代表整个ziplist的长度,zltail指向最后一个元素,zllen代表元素个数,zlend的作用类似于'\0',标记ziplist结束。entry则是实际保存元素的结构,源码如下(位于ziplist.c):
typedef struct zlentry {
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
unsigned int prevrawlen; /* Previous entry len. */
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
这里可以划分为两部分:prevrawlensize和prerawlen是对前驱entry的描述,剩下的属性是对本entry的描述,p指针指向实际存储的数据。linkedlist编码的list结构体和listNode结构体源码如下(位于adlist.h头文件):
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;
quicklist是Redis 3.2之后提供的编码,相当于把ziplist当作linkedlist的listnode,可以结合两者的优点:
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 : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
list的一个应用就是消息队列,例如生产者使用lpush生产消息,多个消费者使用brpop消费消息。
不允许重复元素的数据结构,也可以存储个元素,但是无法通过下标获取元素,集合操作分为集合内操作和集合间操作。
首先是集合内的操作:
然后是集合间操作:
set的内部编码有intset(位于intset.h)和hashtable(位于dict.h)两种。
可以理解为嵌套的HashMap,其value的类型为string,所以它的操作基本就是string的操作加上一个h:
hash的内部编码有两个:ziplist和hashtable,在dict.h中,可以看到,hashtable为三层结构:dict - dictht - dictEntry:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
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;
dict实际就是个二维数组,通过对插入的subkey取模,就能得到桶号,然后把数据包装为dictEntry存入。这里ht之所以是个数组,是为了提供伸缩,hashtable的伸缩实际就是创建一个新hashtable,然后把桶逐步迁移,h[0]代表原来的hashtable,h[1]就是新创建的hashtable,如果迁移过程中进行访问,会先访问h[0],如果要访问的桶已经迁移到h[1],则再去h[1]访问。
ziplist类似于list的实现,区别在于,list中只存放值,而hash中则是交替存放subkey和subvalue,即:
... - subkey1 -subvalue1 -subkey2 -subvalue2 -...
zset有一个新概念:评分,将评分作为元素排序的依据,也可以理解为插入的是 score-string 对,不过score可以相同
127.0.0.1:6379> zadd test 1 "one" 2 "two" 3 "three" 4 "four" 5 "five"
(integer) 5
127.0.0.1:6379> zrangebyscore test (1 (4
1) "two"
2) "three"
zset和set一行,也支持集合间操作:
zset虽然也是set,不过内部编码和set完全不同,它有两种内部编码:ziplist、skiplist。这里主要介绍skiplist,首先上源码(位于server.h):
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
每个skiplist节点,除了有string类型的元素值、双精度浮点型的score外,还有一个zskiplistLevel类型的数组,代表同高度的元素,内部包含一个forward指针,指向同高度元素,和一个无符号长整型值span,代表两个节点间的距离。这种设计在zrange和zremrangebyrank操作中可以有效提高效率。
从整个zset的结构看,它将hashtable和skiplist结合使用,也可以提升操作效率。
streams是Redis 5新增的类型,看名字也能猜到,它的主要用处类似Kafka等MQ,可用于日志系统、消息队列等,而且也引入了Kafka的Consumer Group概念。此处是官方介绍:https://redis.io/topics/streams-intro
streams支持的命令如下:
127.0.0.1:6379> xadd mystream * name zhangsan age 18 sexual male
"1556086502267-0"
127.0.0.1:6379> xrange mystream - +
1) 1) "1556086502267-0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "18"
5) "sexual"
6) "male"
127.0.0.1:6379> xread count 2 streams mystream 0-0
1) 1) "mystream"
2) 1) 1) "1556086502267-0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "18"
5) "sexual"
6) "male"
2) 1) "1556090957152-0"
2) 1) "name"
2) "lisi"
3) "age"
4) "19"
5) "sexual"
6) "female"
127.0.0.1:6379> xread count 2 block 1000 streams mystream $
(nil)
(1.07s)
127.0.0.1:6379> xread count 2 streams mystream $
(nil)
127.0.0.1:6379> xtrim mystream maxlen 2
(integer) 3
127.0.0.1:6379> xinfo stream mystream
1) "length"
2) (integer) 2
...
maxlen和count之间的“~”表示约数,即命令执行后,流的实际大小会比指定的大小大上一些,一般会多几十条
127.0.0.1:6379> xgroup create mystream testgroup 0-0
OK
//执行7次 xadd 命令
127.0.0.1:6379> xpending mystream testgroup
1) (integer) 7
2) "1556109904690-0"
3) "1556109910400-0"
4) 1) 1) "testconsumer"
2) "7"
返回值有四部分,第一部分是待处理消息总数,第二部分和第三部分是未处理消息的开始和结束id,第四部分是当前所有的consumer及它们各自的未处理消息数。该命令有一个应用,可以用于配合xclaim命令,将离线consumer未处理的消息转交给其他consumer处理。
127.0.0.1:6379> xpending mystream testgroup - + 2
1) 1) "1556109904690-0"
2) "testconsumer"
3) (integer) 988495
4) (integer) 1
2) 1) "1556109906511-0"
2) "testconsumer"
3) (integer) 988495
4) (integer) 1
127.0.0.1:6379> xreadgroup group testgroup testconsumer2 count 2 streams mystream >
(nil)
127.0.0.1:6379> xclaim mystream testgroup testconsumer2 988495 1556109904690-0
1) 1) "1556109904690-0"
2) 1) "name"
2) "jerry"
3) "age"
4) "15"
5) "sexual"
6) "male"
127.0.0.1:6379> xpending mystream testgroup
1) (integer) 7
2) "1556109904690-0"
3) "1556109910400-0"
4) 1) 1) "testconsumer"
2) "6"
2) 1) "testconsumer2"
2) "1"
xpending key group start end count的返回值构成如下: id、所属consumer、空闲时间(根据这个设置min-idle-time)、消息传递次数,示例中将第一条消息转交给了testconsumer2,然后可以看到,消息总数不变,还是7,但是在第四部分出现了testconsumer2,它拥有一条消息。
127.0.0.1:6379> xack mystream testgroup 1556109904690-0
(integer) 1
127.0.0.1:6379> xpending mystream testgroup
1) (integer) 6
2) "1556109906511-0"
3) "1556109910400-0"
4) 1) 1) "testconsumer"
2) "6"
可以看到,对testconsumer2的消息执行xack后,消息总数变成了6条
bitmaps是由字符串衍生出来的,不过Redis官方没有将其当作一个独立类型。实际就是将每个字符当作一位进行操作,专属操作如下:
geo实际就是zset,所以可以使用zset的命令,例如zrem就可以用于删除geo中存储的地理信息。geo类型的专属命令如下: