目录
Hash(MAP)
Redis学习总结:Redis数据结构
引言
Redis是使用键值(Key-Value)存储数据的非关系型存储系统,Redis不同于Memcached将Value视作黑盒,Redis的value本身具有结构化的特点,用结构化的value满足业务多样性的需求。
如下图所示,Redis的value包含6种类型:string,list,set,hash,sorted-set,hyperLogLog。
String
在Redis中,string型value可以表示字节串,整数和浮点数三种值的类型。根据具体场景Redis自动完成互相间转换,并根据需要的选取底层的承载方式。例如,设置键k的值为“110”,可以把它当成数型运行incr命令,也可以当成字节串运行append命令。
127.0.0.1:6379> set k 110
OK
127.0.0.1:6379> incr k
(integer) 111
127.0.0.1:6379> append k redis
(integer) 8
127.0.0.1:6379> get k
"111redis"
1)内存数据结构
在Redis内部,作为字节串承载的string型value内部以int、SDS(simple dynamic string)作为结构存储。int用于存放整形数据,sds存放字节串、字符串和浮点型数据。结构如下:
typedef struct sdshdr{
unsigned int len;
unsigned int free;
char buf[];
};
simple dynamic string(sds)结构体主要属性如有
len:buf数组的长度,通过此属性可以在O(1)的复杂度内得出数组的长度。
free: 余量内存,初始化value时多请求的内存,可以提升sds对字节串处理的性能(典型的空间换时间),减少处理过程中可能遇到的内存申请和内存释放次数。
buf[ ]: 存储了字节串的内容,以'\0'结尾作为字符串的定界符(通过转义字符,业务数据内容也可以包含'\0'字符)。buf数组的长度通常大于所存储内容的长度,即SizeOf(buf) = 1 + len + free.
buf 扩容与缩容的策略:为了合理分配内存,串的大小超过buf现有容量的时候,sds会对buf经行扩容。buf以业务操作完成后串的预期长度的两倍+1(‘\0’定界符)来扩容,但最大不超过1MB空间。即:
buf = len*2 + 1
对于len大于1MB的长串,最大保留出1MB空间。
例如内存中存储了一个string类型的值“redis”,它的结构如图所示:
r | e | d | i | s | \0 |
它的len为5,buf实际容量为8,而free为2。如果该值增长大小小于2,则无需申请新的内存,否则将触发扩容,变成下图所示的结构(命令:append rediskey "demo"):
r | e | d | i | s | d | e | m | o | \0 |
2)使用实例
某系统使用不大于7位的纯数字字符串作为用户账号,可以满足百万级的用户需求。用户注册新的账号,假如已经注册的号码已有百万级,且它们之间是无序的,如何快速生成一个未注册的号码,如何快速求出该系统的有多少用户?
解决方案:从数据库中读取这100万个号码缓存到内存中,使用bitmap存储这些号码,将取出来的数的实际值作为offset,并将该offset设置为1。举个例子,读取到8888888这个号码,则执行命令 “SETBIT account 8888888 1”,如下所示:
127.0.0.1:6379> setbit account 8888888 1
(integer) 0
127.0.0.1:6379> setbit account 7777777 1
(integer) 0
127.0.0.1:6379> setbit account 66666 1
(integer) 0
127.0.0.1:6379> bitcount account
(integer) 3
遍历account字节串,当在偏移量x处的值不为1时,说明号码x未被注册,时间复杂度为O(1)。使用命令bitcount account得出的结果就是系统的用户总数;对于整个系统而言,即使是缓存百万数量的账号总数,需要申请的内存为百万字节(不超过10M)。
本例涉及到的命令有:
setbit:对 key 所储存的字符串值,设置或清除指定偏移量上的位(setbit key offset value)。
bitcount:计算给定字符串中,被设置为 1 的比特位的数量(bitcount key [start end])。
注:更多命令和具体使用方法可以查阅官方文档,目前网上已有完整的中文翻译版本(点击链接)。后面实例也不可能将命令全部演示,读者根据需要自己翻阅文档。
List
list是stiring序列,按照插入顺序排序,可以添加一个元素到序列的头部(rpush)尾部(lpush),一个list类型数据最多可以包含-1个元素。
1)内存数据结构
list类型的value对象内部以linkedlist或者ziplist承载。
linkedlist内部实现是双向链表,ziplist的内部实现是顺序表。从数据结构可以看出,相对likedlist,ziplist对于rpush,rpop这样的操作,复杂度一致,都是O(1)。但是lpush/pop操作由于涉及到全列表元素的移动,复杂度较高O(N)。
因此当List的元素个数和单个元素的长度较小时,Redis采用ziplist实现以减少内存占用,否则采用linkedlist结构。这样的话,即便是复杂度较高的O(N),由于N本身不大,对性能的影响就是可控范围内的,且ziplist元素结构采用了可变长度的压缩方法,针对元素长度较小的string具有较好的压缩效果。
2)使用实例
用LPUSH插入六个元素到名为list的序列中,用LRANGE并返回集合的元素
127.0.0.1:6379> lpush list 1 2 a b c
(integer) 5
127.0.0.1:6379> lpush list hello
(integer) 6
127.0.0.1:6379> lrange list 0 5
1) "hello"
2) "c"
3) "b"
4) "a"
5) "2"
6) "1"
本例涉及到的命令有:
LPUSH:将一个或多个值 value 插入到列表 key 的表头(LPUSH key value [value ...])。
LRANGE:返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定(lrange key start stop)。
Map(Hash)
map又叫hash,由于它的value还可以是hash,为了不产生混淆,我就叫他map。map包含若干个键值对(key-value),其中key不重复。Redis本身就是key-value结构,它的value还可以是hash型,可以满足许多丰富的业务场景。但是,hash内部的无法再嵌套hash了,只能是string型。
1)内存数据结构
map的value内部使用hashtable和ziplist两种承载方式实现。对于数量较小的map,采用ziplist实现,实现方式与list的ziplist相似,采用类似顺序表的数据结构实现。
ziplist实现的map大多数操作的复杂度不再是O(1)了,变成了O(N)。但是由于ziplist的map大小通常偏小,所以性能的损失可控,通常情况下,只有很少的几个kv对的map,采用ziplist效率反而更高,省去了hash计算、内存寻址等操作。尤其对于长字符串key,其hash值计算本身的开销甚至远大于顺序遍历时字符串比较的开销。
2)使用实例
在名为ipAdr的hash对象中存入地址和地址名,然后查出ipAdr中value的总数和所有的key
127.0.0.1:6379> hmset ipAdr localhost 127.0.0.1 testServer 192.111.12.6 remoteServer www.redis.com
OK
127.0.0.1:6379> hlen ipAdr
(integer) 3
127.0.0.1:6379> hkeys ipAdr
1) "localhost"
2) "testServer"
3) "remoteServer"
本例涉及到的命令有:
HMSET:同时将多个 field-value (域-值)对设置到哈希表 key 中(HMSET key field value [field value ...])。
HLEN :获取哈希表中字段的数量(HLEN key)。
HKEYS :获取所有哈希表中的字段(HKEYS key),
Set
set类似list都是string的集合,不同的是,它是一个无序集合,其中元素不重复。
1)内存数据结构
set在redis内部以intset或者hashtable存储,其中,hashtable的value永远为null,由于hashtable的key是不重复的,因此可以满足set的需求。当set中只包含整数型元素时,采用intset作为实现。
2)使用实例
往名为num的set对象中存入10个数字,并全部取出。
127.0.0.1:6379> sadd num 1 3 4 1
(integer) 3
127.0.0.1:6379> sadd num 6 4 5 2 4 8
(integer) 4
127.0.0.1:6379> smembers num
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "8"
本例涉及到的命令有:
SADD :将一个或多个 member 元素(在Redis2.4版本以前, sadd 只接受单个 member 值。)加入到集合 key 当中,已经存在于集合的 member 元素将被忽略(SADD key member [member ...])。
SMEMBERS :返回集合 key 中的所有成员(SMEMBERS key )
Sorted-Set
顾名思义,有序的set。sorted-set也是string的集合,不允许重复的元素,不同的是它的每一个元素都会关联一个浮点数score,sorted-set内部按照score从小到大排序。
1)内存数据结构
sorted-set内部以ziplist或skiplist+hashtable来实现。ziplist适用于元素个数不多,元素内容不大的场景。
其中,redis的skiplist和通用的跳表实现不同,redis为每一个level对象增加了span字段,表示该level指向forward节点和当前节点的距离,使得getByRank类的操作效率提升。
2)使用实例
往名为znum的sorted-set对象中存入4个元素,并全部取出。观察结果和set的区别
127.0.0.1:6379> zadd znum 1 redis
(integer) 1
127.0.0.1:6379> zadd znum 2 hello
(integer) 1
127.0.0.1:6379> zadd znum 4 hi
(integer) 1
127.0.0.1:6379> zadd znum 3 xxx
(integer) 1
127.0.0.1:6379> zrange znum 0 4
1) "redis"
2) "hello"
3) "xxx"
4) "hi"
127.0.0.1:6379>
从结果可以看出,取出来的顺序是按照score排序的。而不是插入顺序。
本例涉及到的命令有:
ZADD:将一个或多个 member 元素及其 score 值加入到有序集 key 当中(ZADD key score member [[score member] [score member] ...])。
ZRANGE:返回有序集 key 中,指定区间内的成员(ZRANGE key start stop [WITHSCORES])。
HyperLogLog
Redis HyperLogLog 是在 2.8.9 版本添加的结构,用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
1)内存数据结构
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
什么是基数?比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
2)使用实例
添加四个元素{"hello","hi","redis","hello"}到hyperloglog类型的hhlkey中。
本例涉及到的命令有:
PFADD:添加指定元素到HyperLogLog中(pfadd key element [element ...])。
PFCOUNT:返回给定HyperLogLog的基数估计值( pfcount key [key ...])。
127.0.0.1:6379> pfadd hhlkey hello
(integer) 1
127.0.0.1:6379> pfadd hhlkey hi
(integer) 1
127.0.0.1:6379> pfadd hhlkey redis
(integer) 1
127.0.0.1:6379> pfadd hhlkey hello
(integer) 0
127.0.0.1:6379> pfcount hhlkey
(integer) 3