Redis笔记(2):Redis基础数据结构

Redis有5种基础数据结构,分别为:string(字符串)、list(列表)、set(集合)、hash(哈希)和zset(有序集合)。Redis所有的数据结构都是以唯一的key字符串作为名称,key的类型可以是整型、浮点型、字符串,然后通过这个唯一key值来获取相应的value数据。不同类型的数据结构的差异就在于value的结构不一样。

1.string(字符串)

字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。我们将用户信息结构体使用JSON序列化成字符串,然后将序列化后的字符串塞进Redis来缓存。同样,取用户信息会经过一次反序列化的过程。

Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,当前字符串预分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间,字符串最大长度为512M。

img

相关操作:

批量读写键值对,对多个字符串进行读写,节省网络耗时开销。

mset name1 boy name2 girl name3 unknown
mget name1 name2 name3

查看是否存在键值对。

exists name

设置过期时间,对key设置过期时间,到点自动删除。

# 5s 后过期 
expire key 5

set命令扩展

# 5s后过期,等价于set+expire
setex name 5 codehole
# 如果name不存在就执行set创建
set not exist setnx name codehole
# 如果name已经存在,setnx会创建不成功
setnx name holycoder

数值自增,自增是有范围的,它的范围是signed long的最大、最小值,超过了这个值,Redis会报错。

set age 30 incr age incrby age -5
2.list(列表)

Redis的列表相当于Java语言里面的LinkedList,它的结构是链表,不是数组。这意味着list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。Redis的链表结构常用来做异步队列使用,将需要延后处理的任务结构体序列化成字符串塞进Redis的列表,另一个线程从这个列表中轮询进行处理。

Redis底层存储的还不是一个简单的linkedlist,,在列表元素较少的情况下会使用一块连续的内存存储,这个结构是压缩列表ziplist,它将所有的元素紧挨着一起存储,分配的是一块连续的内存,当数据量比较多的时候才会改成quicklist。

img

因为普通的链表需要的附加指针空间太大,会比较浪费空间,且会加重内存的碎片化。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

相关操作:

右边进左边出:队列

# 从右边加入数据
rpush books python java golang
# 查看list的长度(list len) 
llen books
# 从左边弹出数据(left pop) 
lpop books

右边进右边出:栈

rpush books python java golang rpop books

设置指定位置的值

lset key index value

定位与查找

lindex相当于Java链表的get(intindex)方法,它需要对链表进行遍历,性能随着参数index增大而变差。ltrim跟的两个参数start_index和end_index定义了一个区间,在这个区间内的值,ltrim要留,区间之外统统砍掉,可以通过ltrim来实现一个定长的链表。index可以为负数,index=-1表示倒数第一个元素,同样index=-2表示倒数第二个元素。

rpush books python java golang
# 查找index为1的元素,时间复杂度为O(n),慎用
lindex books 1  
# 获取所有元素,O(n)慎用
lrange books 0 -1 
# 截取部分元素,O(n)慎用
ltrim books 1 -1  
# 这其实是清空了整个列表,因为区间范围长度为负
ltrim books 1 0 
3.hash(字典)

Redis的字典相当于Java语言里面的HashMap,它是无序字典。内部实现结构上同Java的HashMap也是一致的,同样的数组+链表二维结构。出现hash冲突时,就会将冲突的元素使用链表串接起来。

不同的是,Redis的字典的值只能是字符串,另外它们rehash的方式不一样,因为Java的HashMap在字典很大时,rehash是个耗时的操作,需要一次性全部rehash。Redis为了高性能,不能阻塞服务,所以采用了渐进式rehash策略。渐进式rehash会在rehash的同时,保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务中以及hash的子指令中,循序渐进地将旧hash的内容一点点迁移到新的hash结构中。当hash移除了最后一个元素之后,该数据结构自动被删除,内被回收。

img

相关操作:

# 新建或更新kv,命令行的字符串如果包含空格,要用引号括起来
hset books java "think in java"
hset books golang "concurrency in go"
# 根据key获取value
hget books java
# 获取全部kv,key和value间隔出现,例如
hgetall books
1) "java"
2) "think in java"
# 查看hash表的长度
hlen books
# 批量添加或更新
hmset books java "effective java" python "learning python"
4.set(集合)

Redis的集合相当于Java语言里面的HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。set结构可以用来存储活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中奖两次。

相关操作:

sadd books python
# 批量添加
sadd books java golang javascript
# 查看所有的成员,显示顺序和插入的并不一致,因为set是无序的
smembers books
# 查询某个value是否存在,相当于contains(o)
sismember books java
# 获取长度相当于count()
scard books
# 弹出一个元素
spop books 
5.zset(有序列表)

zset是Redis提供的最为特色的数据结构,它类似于Java的SortedSet和HashMap的结合体,一方面它是一个set,保证了内部元素的唯一性,另一方面可以给每个元素赋予一个权重值,进行权重排序,它的内部实现使用的是跳跃列表的数据结构。

zset中最后一个value被移除后,数据结构自动删除,内存被回收。zset可以用来存储学生的成绩,value值是学生的ID,score是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。

相关操作:

# "think in java"是k,9.0是权重值。
zadd books 9.0 "think in java"
zadd books 8.9 "java concurrency"
zadd books 8.6 "java cookbook"
# 按 score 升序列出,参数区间为排名范围
zrange books 0 -1
1) "java cookbook"
2) "java concurrency"
3) "think in java" 
# 按 score 逆序列出,参数区间为排名范围
zrevrange books 0 -1
1) "think in java"
2) "java concurrency"
3) "java cookbook"
# 获取统计数量,相当于count()
zcard books
# 获取指定 value 的 score,内部 score 使用 double 类型进行存储,所以存在小数点精度问题
zscore books "java concurrency"
"8.9000000000000004"
# 获取元素的排名
zrank books "java concurrency"
# 根据分值区间遍历
zrangebyscore books 0 8.91
1) "java cookbook"
2) "java concurrency"
# 根据分值区间(-∞,8.91]遍历zset,同时返回分值。 inf代表infinite,无穷大的意思。
zrangebyscore books -inf 8.91 withscores
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
# 删除 value
zrem books "java concurrency"
# 根据分值区间遍历
zrangebyscore books 0 8.91
1) "java cookbook"
2) "java concurrency"
# 根据分值区间(-∞,8.91]遍历zset,同时返回分值。 inf代表infinite,无穷大的意思。
zrangebyscore books -inf 8.91 withscores
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
# 删除 value
zrem books "java concurrency"
6.Bitmaps

Bitmaps的结构

许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。Redis提供了Bitmaps这个“数据结构”可以实现对位的操作,把数据结构加上引号主要因为:

  • Bitmaps本身不是一种数据结构,实际上它是字符串,此外,还可以对字符串进行位操作。
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。

Redis使用字符串对象来表示位数组,因为字符串对象使用的SDS数据结构是二进制安全的,所以程序可以直接使用SDS结构来保存位数组,并使用SDS结构的操作函数来处理位数组。如图所示:

  • sdshdr.len属性的值为3,表示这个SDS保存了一个三字节长的位数组;
  • 位数组由buf数组中的buf[0]、buf[1]、buf[2]三个字节保存;

img

相关操作:

(1)设置值

setbit key offset value

设置键的第offset个位的值(从0算起),向客户端返回设置之前的值。如果offset的值超出了当前范围,则会发生扩容,在第一次初始化Bitmaps时,如果offset非常大,那么整个初始化过程执行会比较慢,可能会造成阻塞。如果不发生扩容,时间复杂度为O(1)。

(2)获取值

getbit key offset

获取bitmaps中offset位置的值,如果offset不存在,所以返回结果也是0。所有操作的时间复杂度为O(1);

(3)获取Bitmaps指定范围值为1的个数

bitcount key [start] [end]

[start]和[end]代表起始和结束字节数,如果不填,则返回整个Bitmaps中值为1的个数。实现方法包括:遍历算法、查表算法、SWAR算法。Redis使用了查表算法和SWAR算法,算法的时间复杂度为O(N)。

(4)Bitmaps间的运算

bitop op destkey key [key....]

bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。

(5)计算Bitmaps中第一个值为targetBit的偏移量

bitpos key targetBit [start] [end]
7.HyperLogLog

基数估算就是为了估算在一批数据中,它的不重复元素有多少个。比如数据集 {1, 3, 5, 7, 5, 7, 8},它的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素的个数)为5。基数估算的目的就是在误差可接受的范围内,快速计算基数,基数估算适用于一个热点页面的去重访问次数。

HyperLogLog并不是一种单独的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计。因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

HyperLogLog提供了3个命令:

  • pfadd用于向HyperLogLog添加元素,如果添加成功返回1。
  • pfcount用于计算一个或多个HyperLogLog的独立总数。
  • pfmerge可以求出多个HyperLogLog的并集并赋值给destkey。

HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:

  • 只为了计算独立总数,不需要获取单条数据。
  • 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。
8.GEO

Redis3.2版本提供了GEO功能,支持存储地理位置信息,用来实现诸如附近位置、 摇一摇这类依赖于地理位置信息的功能。

GeoHash算法:

GeoHash算法是业界比较通用的地理位置距离排序算法,Redis也使用GeoHash算法。GeoHash算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算附近的人时,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。

以二刀法为例,一张地图两刀下去均分分成四块小正方形,这四个小正方形可以分别标记为{00,01,10,11}四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用4bit的二进制整数予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。

GeoHash算法会对上面的编码做一次base32编码(0-9,a-z去掉a,i,l,o四个字母)变成一个字符串。在Redis里面,经纬度使用52位的整数进行编码,放进了zset里面,zset的value是元素的key,score是GeoHash的52位整数值。zset的score虽然是浮点数,但是对于52位的整数值,它可以无损存储。

在使用Redis进行Geo查询时,它的内部结构实际上只是一个zset(skiplist)。通过zset的score排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。获取的经纬度坐标和geoadd进去的坐标有轻微的误差,原因是GeoHash对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。

geohash有如下特点:

  • GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中;
  • 字符串越长,表示的位置更精确;
  • 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关命令。
  • geohash编码和经纬度是可以相互转换的。

相关操作:

(1)增加/更新地理位置信息

geoadd key longitude latitude member [longitude latitude member ...]

geoadd指令携带集合名称以及多个三元组(经度、纬度、元素名称),可以同时添加多个地理位置信息,longitude、latitude、member分别是该地理位置的经度、纬度、成员,如果需要更新地理位置信息,仍然使用geoadd命令,返回结果为0。

(2)获取地理位置信息

geopos key member [member ...]

获取的经纬度坐标和geoadd进去的坐标有轻微的误差,原因是geohash对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。

(3)获取两个地理位置的距离

geodist cities:locations tianjin beijing km

geodist指令可以用来计算两个元素之间的距离,携带集合名称、2个名称和距离单位等参数。

(4)获取指定位置范围内的地理信息位置集合

 georadiusbymember cities:locations beijing 150 km

georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令需要给出具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,

(5)获取geohash

geohash key member [member ...]

Redis使用有序集合并结合geohash的特性实现了GEO的若干命令。

(6)删除地理位置信息

zrem key member

GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对成员进行删除。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值