redis数据结构 redis全名(Remote Dictionary Server),即远程字典服务
redis的值的数据结构类型有String、List、Set、Hash、zset(sorted set,有序集合)、Bitmaps(位图)、HyperLogLogs
注意:我使用的版本是6.0.10,不同版本可能略有差别
redis中key的最大长度为512M
对象
server.h
任何value对象都会被包装成一个redisObject,redisObject能指定value类型,编码方式等数据属性
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct redisObject { unsigned type:4 ; unsigned encoding:4 ; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;
类型总览 redis中的数据类型,即为redisObject的type属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define OBJ_STRING 0 #define OBJ_LIST 1 #define OBJ_SET 2 #define OBJ_ZSET 3 #define OBJ_HASH 4 #define OBJ_MODULE 5 #define OBJ_STREAM 6
1 2 # 可以使用type 命令来查看该键所对应值的类型 type key
redis的编码分类 对应于redisObject的ecoding属性,encoding是决定对象底层实现数据结构的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define OBJ_ENCODING_RAW 0 #define OBJ_ENCODING_INT 1 #define OBJ_ENCODING_HT 2 #define OBJ_ENCODING_ZIPMAP 3 #define OBJ_ENCODING_LINKEDLIST 4 #define OBJ_ENCODING_ZIPLIST 5 #define OBJ_ENCODING_INTSET 6 #define OBJ_ENCODING_SKIPLIST 7 #define OBJ_ENCODING_EMBSTR 8 #define OBJ_ENCODING_QUICKLIST 9 #define OBJ_ENCODING_STREAM 10
可以使用命令来查看存储方式
来查看该key使用的是哪种编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 127.0 .0 .1 :6379 > set test_int 1 OK 127.0 .0 .1 :6379 > object encoding test_int"int" 127.0 .0 .1 :6379 > set test_raw zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzOK 127.0 .0 .1 :6379 > strlen test_raw(integer) 44 127.0 .0 .1 :6379 > object encoding test_raw"embstr" 127.0 .0 .1 :6379 > set test_raw zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzOK 127.0 .0 .1 :6379 > object encoding test_raw"raw" 127.0 .0 .1 :6379 > strlen test_raw(integer) 45 # 查看key的内部结构和编码等信息 >debug object k4 Value at:0x7ffc97c044f0 refcount:1 encoding:embstr serializedlength:3 lru:11351195 lru_seconds_idle:5363
String类型 string类型属于单值单value,是一个可变的字节数组,就像StringBuilder似的,可以追加、获取长度、截取等操作。使用场景很多,如缓存数据信息、分布式锁、计数器等。
扩容:当字符串长度小于1M时,扩容是现有空间进行加倍;当字符串长度超过1M,扩容只会多扩1M的空间。且字符串最长为512M
源码结构 sds.h其内部使用的是SDS简单动态字符串,保存了长度信息,获取字符串长度只需要O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 struct __attribute__ ((__packed__ )) sdshdr5 { unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr32 { uint32_t len; uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr64 { uint64_t len; uint64_t alloc; unsigned char flags; char buf[]; };
编码格式
编码
对象
OBJ_ENCODING_INT
使用整数值实现的字符串对象
OBJ_ENCODING_EMBSTR
使用embstr编码的简单动态字符串实现的字符串对象
OBJ_ENCODING_RAW
使用简单动态字符串实现的字符串对象
string有三种编码格式
embstr和raw两种编码的区别 embstr编码用于保存短字符串,与raw编码一样,都是使用redisObject结构和sdshdr结构来表示字符串对象,但是raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,两个对象在内存地址上是不连续的;而embstr编码则通过调用一次内存分配函数来分配一块连续的内存空间,空间中依次包含redisObject和sdshdr两个结构
操作命令 值拼接
1 2 3 4 # 给key对应的value进行拼接,返回的是拼接之后的字符长度 append k1 qwe (integer) 5
值长度
数字运算操作
1 2 3 4 5 6 7 8 # 加一 INCR k2 # 减一 DECR k2 # 加3,第二个参数是步长 INCRBY k2 3 # 减2,第二个参数是步长 DECRBY k2 2
注意:这几个命令只能对数字进行操作,范围是Long.MIN~Long.MAX
获取值
1 2 3 4 get k1 # 只获取该key所对应value的部分字符 getrange k1 0 2
判断值是否存在
设置值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # set key value set test 123 # setrange覆盖部分值 其中0为从下标为0的字符开始替换 setrange k1 0 zx (integer) 5 # 设置值时设置过期时间 # setex key seconds value setex k3 20 b # 如果key不存在设置值,防止覆盖(set if not exists) # setnx key value setnx k1 1 # 设置key的值,并返回key的旧值 getset k1 vv1
多值操作
1 2 3 4 5 6 7 8 9 # 同时set 多个键值 # mset key value [key value ...] mset k4 v4 k5 v5 # 同时获取多个键的值 # mget key [key ...] mget k4 k5 # 同时set 多个键值(全部不存在时才会设置值) # msetnx key value [key value ...] msetnx k6 v6 k7 k8
List类型 List属于单值多value,链表用的是双向链表 结构,list支持pop、push来操作头部和尾部,既可以用做栈也可以用做队列
在3.2版本之前使用的是ziplist+linkedlist,在3.2版本之后使用的是quicklist
源码结构 quicklist结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; unsigned long len; int fill : QL_FILL_BITS; unsigned int compress : QL_COMP_BITS; unsigned int bookmark_count: QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist; typedef struct quicklistNode { struct quicklistNode *prev ; struct quicklistNode *next ; unsigned char *zl; unsigned int sz; unsigned int count : 16 ; unsigned int encoding : 2 ; unsigned int container : 2 ; unsigned int recompress : 1 ; unsigned int attempted_compress : 1 ; unsigned int extra : 10 ; } quicklistNode;
ziplist结构 压缩的双链表,以连续的空间来表示双链表,节省前驱和后继指针的空间,在小的list上,压缩效率明显,因为在普通的双链表中,前驱和后继指针在64位机器上分别占用8B,在ziplist.h和ziplist.c中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct zlentry { unsigned int prevrawlensize; unsigned int prevrawlen; unsigned int lensize; unsigned int len; unsigned int headersize; unsigned char encoding; unsigned char *p; } zlentry; typedef struct { unsigned char *sval; unsigned int slen; long long lval; } ziplistEntry;
而对于quicklist编码的定义是
1 OBJ_ENCODING_QUICKLIST 9
操作命令 设置值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # lpush(指从左边进,头部,先进后出,像栈) # lpush key element [element ...] lpush list1 1 2 3 4 5 # rpush(指从右边进,尾部,先进先出,像队列) # rpush key element [element ...] rpush list2 1 2 3 4 5 # 给某个索引位置赋值 # lset key index element lset list1 0 q # 在某个元素之前或之后插入一个元素 # linsert key before|after pivot element linsert list1 before q zz # 获取指定索引的元素,如果使用负数,则从右边开始计算 lindex list1 0
取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 弹出栈顶元素(从左边开始弹出,弹出之后,list中就不存在该元素了) # lpop key lpop list1 # 弹出栈顶元素(从右边开始弹出,弹出之后,list中就不存在该元素了) # rpop key rpop list1 # 阻塞读 可以使用blpop、brpop,如果队列没有数据的时候就会进入休眠状态(这里要注意阻塞的话,客户端连接就变成了闲置连接,闲置过长服务器会断开连接的,所以使用的时候客户端要捕获异常进行重试) # 按照索引位置取值 # lindex key index # 获取头元素 lindex list1 0 # 获取尾部元素 lindex list1 -1 # 取值,从头部到尾部 # lrange key start end (负下标表示的是从后往前,如-1表示倒数第一) lrange list1 0 -1 1) "5" 2) "4" 3) "3" 4) "2" 5) "1" # blpop和brpop是阻塞版本 # blpop key timeout # timeout表示 列表为空时会等3秒后返回,如果timeout=0,则会一直阻塞下去,直到有数据为止 # 如果多个客户端对同一个键执行blpop,那么先执行的blpop命令的客户端可以获取到弹出的值 blpop list1 3
列表长度
删除值
1 2 3 4 5 6 7 8 9 10 11 12 # 删除几个某个元素 count>0时,从左边开始删除;count=0时,删除所有值为element的元素;count<0时,从右边开始删除 # lrem key count element lrem list1 1 "hi" # 截取指定范围的值赋给key(范围是索引) # ltrim key start stop ltrim list1 0 0 # 从头部删除元素 出栈 lpop list1 # 从尾部删除元素 rpop list1
出栈入栈
1 2 3 # 从源列表右出栈压入左目标列表,将一个列表移至另一个列表 # rpoplpush source dest rpoplpush list2 list1
由于使用的链表,所以在使用index取值的时候需要对链表进行遍历,性能会随着index增大而变差
Set类型 set类型是单值多value,与List的区别在于不可重复,且没有顺序, 类似于HashSet,通过hash table实现的
set除了基本的增删改查操作之外,还可以使用集合来取并集、交集、差集,可以用来实现好友推荐等功能
编码格式
编码
对象
OBJ_ENCODING_INTSET
使用整数集合实现的集合对象
OBJ_ENCODING_HT
使用的hash table
set有两种编码格式
intset 保存的元素全都是整数,且元素数量不超过512
hash table 保存的不是整数
1 2 set-max-intset-entries 512
源码结构 intset结构 整数集合
1 2 3 4 5 6 7 8 typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
操作命令 设置值
1 2 3 4 # 设置值 # sadd key member [member ...] sadd set1 vv vc vb vv
取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 取值,集合中的所有元素 # smembers key smembers set1 1) "vc" 2) "vb" 3) "vv" # 判断该元素是否在set 中 # sismember key member sismember set1 vv # 获取随机的元素,不会删除元素(count表示获取的数量,默认为1) # srandmember key [count] srandmember set1 2
查看集合元素个数
删除
1 2 3 4 5 6 # srem key member [member ...] srem set1 vb # 随机出栈(默认数量为1) # spop key [count] spop set1
随机抽取
1 2 3 # 随机从集合中抽取2个(默认1个) # srandmember key [count] srandmember set1 2
1 2 3 # 将源集合中的值移至目标集合 # smove source dest member smove set1 set2 test
差集 交集 并集
1 2 3 4 5 6 7 8 9 10 # 差集 返回的是其他key与第一个key的差集,属于A且不属于B的(A - B) # sdiff key [key ...] sdiff set1 set2 # 交集 属于A且属于B的(A ∩ B) # sinter key [key ...] sinter set1 set2 # 并集 属于A或属于B的(A ∪ B) # sunion key [key ...] sunion set1 set2
Hash类型 value是键值对,相当于HashMap,对于hash碰撞也是采用的HashMap的处理方式,数组+链表
更适合存储对象,将一个对象存储在hash类型中会占用更少的内存,且可以更方便的存取整个对象
编码格式
编码
对象
OBJ_ENCODING_ZIPLIST
使用ziplist
OBJ_ENCODING_HT
使用的hash table
hash有两种编码格式,当键值对数据量比较小时,使用紧凑的数组格式来节省内存空间
ziplist 一开始存储使用的ziplist,但是当满足一定条件时会转换为hash table
hash table
1 2 3 4 5 hash-max -ziplist-entries 512 hash-max -ziplist-value 64
源码结构 dict.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht; typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next ; } dictEntry; typedef struct dict { dictType *type; void *privdata; dictht ht[2 ]; long rehashidx; int16_t pauserehash; } dict; 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); int (*expandAllowed)(size_t moreMem, double usedRatio); } dictType;
哈希冲突 redis哈希冲突是使用链地址法进行解决的,将新节点添加在链表的表头
扩容 redis的字典源码中的dictht是一个容量为2的数组,其作用就是用于进行扩容的
最初始的数据是存放在ht[0]中的,当要进行扩展或收缩时,ht[1]会根据ht[0]的容量来计算容量,ht[1]扩展的大小是比当前ht[0].used值的两倍大的第一个2的整数幂;收缩的大小是ht[0].used的第一个大于等于的2的整数幂。将ht[0]的所有键值重新散列到ht[1]中,重新计算所有的数组下标,当数据迁移完后ht[0]就会释放,将ht[1]改为ht[0],为下一次扩展或收缩做准备
由于有时候redis中的数据比较多,不能瞬间完成扩容操作,会将rehash操作进行分步进行,其字段rehashidx就用于标识rehash过程,如果是-1表示当前没有进行rehash操作,当进行rehash操作时会将该值改为0,在进行更新、删除、查询操作时会在ht[0]和ht[1]中都进行,先更新ht[0],再更新ht[1],而进行新增操作就直接新增到ht[1]中,保证ht[0]只减不增,直到rehash完成
操作命令 设置值
1 2 3 4 5 6 7 8 9 10 11 12 # Redis4.0之后可以设置多个值,与hmset功能相同 # hset key field value [field value ...] # 不区分插入和更新,插入时返回1,更新时返回0 hset user id 123 # 设置多个字段 # hmset key field value [field value ...] hmset user name zhangsan age 18 # 不存在则设置值 # hsetnx key field value hsetnx user id 12
获取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # hget key field hget user id # 获取多个字段的值 # hmget key field [field ...] hmget user id name sex age # 可以获取该key所对应的map # hgetall key hgetall user # 获取key对应的map有几个字段 # hlen key hlen user # 判断该key的某个字段是否存在 # hexists key field hexists user id # 获取该key下所有的字段 # hkeys key hkeys user # 获取该key下所有的字段值 # hvals key hvals user
删除字段值
1 2 3 # 支持删除多个字段 # hdel key field [field ...] hdel user age
数值操作
1 2 3 4 5 6 7 # 加2 # hincrby key field increment hincrby user id 2 # 减2 # hdecrby key field increment hdecrby user id 2
Zset类型 zset是sorted set,即有序的集合,在set的基础上加了一个score ,类似于TreeSet,使用score来进行排序,也可以根据score的范围来获取元素的列表
原本set值为k1 v1 k2 v2,而zset是k1 score1 v1 k2 score2 v2
底层使用的是dict哈希表和skiplist跳表,dict用来关联value和score,保证value的唯一性,同时通过value可以获取到score。skiplist的作用在于给value排序以及根据score的范围来获取元素列表
编码格式
编码
对象
OBJ_ENCODING_ZIPLIST
使用ziplist
OBJ_ENCODING_SKIPLIST
使用的skiplist
zset有两种编码格式
ziplist 满足条件使用ziplist,否则使用skiplist
skiplist
1 2 3 zset-max -ziplist-entries 128 zset-max -ziplist-value 64
源码结构 跳表skiplist相比一般的链表,有更高的查找效率,效率可以比拟二叉查找树
在存储时会预先间隔地保存有序链表的节点,从而在查找时能达到类似于二分搜索的效果,可以节省查找时间,但是浪费了空间,数据只存储了一次,只是指针多次存储了
由多层结构组成
每一层都是一个有序链表
最底层的链表包含了所有元素
如果一个元素出现在leveli的链表中,则它在leveli之下的链表也都会出现
每个节点包含两个指针,一个指向同链表中的下一个元素,一个指向下面一层的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 typedef struct zset { dict *dict; zskiplist *zsl; } zset; 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;
操作命令 设置值
1 2 3 # 向zset中添加元素,并且会根据score进行排序,如果元素存在,会更新score # zadd key score member [score member ...] zadd zset1 1 q 2 w
取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # WITHSCORES表示查询结果是否带着score,score从小到大 # zrange key start stop [WITHSCORES] zrange zset1 0 -1 # WITHSCORES表示查询结果是否带着score,score从大到小 # zrevrange key start stop [WITHSCORES] zrevrange zset1 0 -1 # 根据score范围查询 # zrangebyscore key min max [WITHSCORES] [LIMIT offset count] zrangebyscore zset2 80 100 # 根据score的范围来计数 # zcount key min max zcount zset2 0 10 # 根据值来获取下标(根据score从小到大来排的序) # zrank key member zrank zset2 d # 根据值来获取下标(根据score从大到小来排的序) # zrevrank key member zrevrank zset2 d # 获取元素的score # zscore key member zscore zset2 c
查看集合元素个数
删除
1 2 3 4 5 6 7 8 9 10 # zrem key member zrem zset2 s # 根据下标区间来删除(根据score从小到大来排的序) # zremrangebyrank key start stop zremrangebyrank zset1 5 8 # 根据score区间来删除 # zremrangebyscore key min max zremrangebyscore zset1 7 10
数值操作
1 2 3 # 如果key中存在该元素,则该元素的score增加increment,否则新增该元素 # zincrby key increment member zincrby zset1 2 b
Bitmap类型 位图不是一个真实的数据类型,是定义在字符串类型之上的面向位操作的集合,每个单元只能存储0和1。位图的最大优势在于节省空间
1 2 127.0.0.1:6379> type bittest string
设置值
1 2 3 # setbit key offset value # value只能是0或者1 setbit bittest 10 1
取值
1 2 3 4 5 6 7 # getbit key offset # 如果offset超出范围的话返回0 getbit bittest 10 # bitcount key [start] [end] # 获取指定范围为1的个数 bitcount bittest 0 1000
bitmap适用于大量数据,少量数据的话会导致大部分位都是0,占用的内存数比集合还多
HyperLogLog类型 HyperLogLog是一种基于字符串类型的基数算法。通过HyperLogLog可以利用极小的内存空间完成独立总数的统计
1 2 127.0.0.1:6379> type hpl string
1 2 3 4 5 6 7 8 9 10 11 # 添加 # pfadd key element [element ...] pfadd hpl 123 # 统计 # pfcount key [key ...] pfcount hpl # 合并 # pfmerge destkey sourcekey [sourcekey ...] pfmerge destkey sourcekey1 sourcekey2
HyperLogLog非常的节省空间,但是HyperLogLog统计的数据是不准确的
GEO类型 地理信息定位功能,支持存储地理位置信息。其底层是zset,通过zset的score排序就可以得到坐标附近的其他元素
1 2 127.0.0.1:6379> type key zset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 增加地理位置信息 # longitude经度 # latitude纬度 # member成员 # geoadd key longitude latitude member [longitude latitude member ...] geoadd key 116.28 39.55 bj # 获取地理位置信息 返回经纬度 # geopos key member [member ...] geopos key bj # 获取两个地理位置的距离 unit为单位 包含 m 米、km 千米、mi 英里、ft 尺 # geodist key member1 member2 [unit] geodist key bj sjz km # 获取指定位置范围内的地理信息集合 # georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key] # longitude latitude也可以使用成员来替换,此时使用georadiusbymember key member # radius m|km|ft|mi 表示半径+单位 # withcoord 返回结果中包含经纬度 # withdist 返回结果中包含里节点位置的距离 # withhash 返回结果中包含geohash # COUNT count 指定返回的数量 # asc|desc 按照距离做升序或者降序 # store key 将返回结果的地理位置信息保存到指定键 # storedist key 将返回结果距离节点距离保存到指定键 georadiusbymember key bj 250 km # 删除地理位置信息 # zrem key member
各个类型的场景