Redis数据结构

Redis数据结构基础

Redis有5个基本数据结构,string、list、hash、set和zset。

string

字符串 string 是 Redis 最简单的数据结构。

Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。

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

string表示的是一个可变的字节数组,我们初始化字符串的内容、可以拿到字符串的长度,可以获取string的字串,可以覆盖string的字串内容,可以追加子串。

Redis的字符串是动态字符串,是可以修改的字符串。

内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配

初始化时,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。

当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。

需要注意的是字符串最大长度为512M。

初始化字符串

需要提供「变量名称」和「变量的内容」

> set ireader beijing.zhangyue.keji.gufen.youxian.gongsi
OK

获取字符串的内容

提供「变量名称」

> get ireader
"beijing.zhangyue.keji.gufen.youxian.gongsi"

获取字符串的长度

提供「变量名称」

> strlen ireader
(integer) 42

获取子串

提供「变量名称」以及开始和结束位置[start, end]

> getrange ireader 28 34
"youxian"

覆盖子串

提供「变量名称」以及开始位置和目标子串

> setrange ireader 28 wooxian
(integer) 42  # 返回长度
> get ireader
"beijing.zhangyue.keji.gufen.wooxian.gongsi"

追加子串

> append ireader .hao
(integer) 46 # 返回长度
> get ireader
"beijing.zhangyue.keji.gufen.wooxian.gongsi.hao"

遗憾的是字符串没有提供字串插入方法和子串删除方法。

计数器

如果字符串的内容是一个整数,那么还可以将字符串当成计数器来使用。

> set ireader 42
OK

> get ireader
"42"

> incrby ireader 100
(integer) 142

> get ireader
"142"

> decrby ireader 100
(integer) 42

> get ireader
"42"

> incr ireader  # 等价于incrby ireader 1
(integer) 143

> decr ireader  # 等价于下·decrby ireader 1
(integer) 142

计数器,简单来说是一个自增长的数值,比如用户的投票数,网站的访问次数等等。以往自增长ID,可以通过mysql的自增主键实现,但是性能却不高,而且采用分库分表策略的时候,无法获取唯一的自增ID。通过redis字符串类型,可以实现一个简单的计数器,贴上伪代码!

> incr ireader # 等价于incrby ireader 1

incr命令是增加键的整数值一次,如果键没有被设置,则设置为1,返回自增后的数值。

这里利用了redis的原子性,即同一个键只允许一个客户端操作,确保自增值准确。

计数器是有范围的,它不能超过Long.Max,不能低于Long.MIN

> set ireader 9223372036854775807
OK

> incr ireader
(error) ERR increment or decrement would overflow

> set ireader -9223372036854775808
OK

> decr ireader
(error) ERR increment or decrement would overflow

过期&删除&如果不存在插

> set name codehole 
> get name 
"codehole" 
> expire name 5 # 5s 后过期 
... # wait for 5s 
> get name (nil)

> setex name 5 codehole # 5s 后过期,等价于 set+expire 
> get name "codehole" 
... # wait for 5s 
> get name (nil)

> setnx name codehole # 如果 name 不存在就执行 set 创建 
(integer) 1 
> get name 
"codehole" 
> setnx name holycoder 
(integer) 0 
# 因为 name 已经存在,所以 set 创建不成功 > get name "codehole"
# 没有改变
> expire ireader 60    # 可以使用expire指令设置过期时间,到点会自动删除,这属于被动删除。
(integer) 1  # 1表示设置成功,0表示变量ireader不存在

> ttl ireader    # 可以使用ttl指令获取字符串的寿命。
(integer) 50  # 还有50秒的寿命,返回-2表示变量不存在,-1表示没有设置过期时间

> del ireader    # 字符串可以使用del指令进行主动删除。
(integer) 1  # 删除成功返回1

> get ireader
(nil)  # 变量ireader没有了

批量读写键值对

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

> set name1 codehole OK

>set name2 holycoder OK

>mget name1 name2 name3 # 返回一个列表 
1) "codehole" 
2) "holycoder" 
3) (nil) 
> mset name1 boy name2 girl name3 unknown 
> mget name1 name2 name3 
1) "boy" 
2) "girl"
3) "unknown"

list

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组,而且链表还是双向链表

因为它是链表,这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。如果list的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。

Redis列表只是字符串列表,按插入顺序排序。可以在列表的头部或尾部添加Redis列表中的元素。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符
串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。

列表的最大长度为2^32 - 1个元素(即4294967295,每个列表可存储超过40亿个元素)。

负下标

链表元素的位置使用自然数0,1,2,....n-1表示,还可以使用负数-1,-2,...-n来表示,-1表示「倒数第一」,-2表示「倒数第二」,那么-n就表示第一个元素,对应的下标为0

队列/堆栈

链表可以从表头和表尾追加和移除元素,结合使用rpush/rpop/lpush/lpop四条指令,可以将链表作为队列或堆栈使用,左向右向进行都可以

# 右进左出
> rpush ireader go
(integer) 1

> rpush ireader java python
(integer) 3

> lpop ireader
"go"

> lpop ireader
"java"

> lpop ireader
"python"

# 左进右出
> lpush ireader go java python
(integer) 3

> rpop ireader
"go"
...

# 右进右出
> rpush ireader go java python
(integer) 3

> rpop ireader 
"python"
...

# 左进左出
> lpush ireader go java python
(integer) 3

> lpop ireader
"python"
...

在日常应用中,列表常用来作为异步队列来使用。

长度

使用llen指令获取链表长度

> rpush ireader go java python
(integer) 3

> llen ireader
(integer) 3

随机读->O(n) 慎用

可以使用lindex指令访问指定位置的元素,使用lrange指令来获取链表子元素列表,提供start和end下标参数

> rpush ireader go java python
(integer) 3

> lindex ireader 1
"java"

> lrange ireader 0 2
1) "go"
2) "java"
3) "python"

> lrange ireader 0 -1  # -1表示倒数第一
1) "go"
2) "java"
3) "python"

使用lrange获取全部元素时,需要提供end_index,如果没有负下标,就需要首先通过llen指令获取长度,才可以得出end_index的值,有了负下标,使用-1代替end_index就可以达到相同的效果。

修改元素

使用lset指令在指定位置修改元素。

> rpush ireader go java python
(integer) 3

> lset ireader 1 javascript
OK

> lrange ireader 0 -1
1) "go"
2) "javascript"
3) "python"

插入元素

使用linsert指令在列表的中间位置插入元素,有经验的程序员都知道在插入元素时,我们经常搞不清楚是在指定位置的前面插入还是后面插入,所以antirez在linsert指令里增加了方向参数before/after来显示指示前置和后置插入。不过让人意想不到的是linsert指令并不是通过指定位置来插入,而是通过指定具体的值。这是因为在分布式环境下,列表的元素总是频繁变动的,意味着上一时刻计算的元素下标在下一时刻可能就不是你所期望的下标了。

> rpush ireader go java python
(integer) 3

> linsert ireader before java ruby
(integer) 4

> lrange ireader 0 -1
1) "go"
2) "ruby"
3) "java"
4) "python"

删除元素

列表的删除操作也不是通过指定下标来确定元素的,你需要指定删除的最大个数以及元素的值

> rpush ireader go java python
(integer) 3

> lrem ireader 1 java
(integer) 1

> lrange ireader 0 -1
1) "go"
2) "java"
lrem key count value 

从key对应list中删除count个和value相同的元素。

count为0时候删除全部。

count为正,则删除匹配count个元素。

如果为负数,则是从右侧扫描删除匹配count个元素。

复杂度是O(N),N是List长度,因为List的值不唯一,所以要遍历全部元素,而Set只要O(log(N))。

定长列表->O(n) 慎用

在实际应用场景中,我们有时候会遇到「定长列表」的需求。

比如要以走马灯的形式实时显示中奖用户名列表,因为中奖用户实在太多,能显示的数量一般不超过100条,那么这里就会使用到定长列表。

维持定长列表的指令是ltrim,需要提供两个参数start和end,表示需要保留列表的下标范围,范围之外的所有元素都将被移除。

> rpush ireader go java python javascript ruby erlang rust cpp
(integer) 8

> ltrim ireader -3 -1
OK

> lrange ireader 0 -1
1) "erlang"
2) "rust"
3) "cpp"

如果指定参数的end对应的真实下标小于start,其效果等价于del指令,因为这样的参数表示需要需要保留列表元素的下标范围为空。

快速列表

img

如果再深入一点,你会发现Redis底层存储的还不是一个简单的linkedlist,而是称之为快速链表quicklist的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

hash

img

哈希等价于Java语言的HashMap或者是Python语言的dict。

在实现结构上它使用二维结构,第一维是数组,第二维是链表。

hash的内容key和value存放在链表中,数组里存放的是链表的头指针。

通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值,链表的作用就是用来将产生了「hash碰撞」的元素串起来。

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

img

不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。

Redis为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。

增加元素

可以使用hset一次增加一个键值对,也可以使用hmset一次增加多个键值对

ps : 把每创建的hash看作java中使用Map时的HashMap对象就很简单了~

> hset ireader go fast
(integer) 1

> hmset ireader java fast python slow
OK

获取元素

> hmset ireader go fast java fast python slowOK
# 可以通过hget定位具体key对应的value。
> hget ireader go
"fast"

# 可以通过hmget获取多个key对应的value。
> hmget ireader go python
1) "fast"
2) "slow"

# 可以使用hgetall获取所有的键值对。
> hgetall ireader
1) "go"
2) "fast"
3) "java"
4) "fast"
5) "python"
6) "slow"

# 可以使用hkeys和hvals分别获取所有的key列表和value列表。
> hkeys ireader
1) "go"
2) "java"
3) "python"

> hvals ireader
1) "fast"
2) "fast"
3) "slow"

这些操作和Java语言的Map接口是类似的。

删除元素

当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

可以使用hdel删除指定key,hdel支持同时删除多个key

> hmset ireader go fast java fast python slow
OK

> hdel ireader go
(integer) 1

> hdel ireader java python
(integer) 2

判断元素是否存在

通常我们使用hget获得key对应的value是否为空就直到对应的元素是否存在了,不过如果value的字符串长度特别大,通过这种方式来判断元素存在与否就略显浪费,这时可以使用hexists指令。

> hmset ireader go fast java fast python slow
OK

# 如果哈希表含有给定域,返回 1 。
# 如果哈希表不含有给定域,或 key 不存在,返回 0 。
> hexists ireader go
(integer) 1

计数器

hash结构还可以当成计数器来使用,对于内部的每一个key都可以作为独立的计数器。

为哈希表 key 中的域 val 的值加上**增量 increment **。

增量也可以为负数,相当于对给定域进行减法操作。

如果 key 不存在,一个新的哈希表被创建并执行HINCRBY命令。

如果域 field 不存在,那么在执行命令前,域的值被初始化为 0

如果value值不是整数,调用hincrby指令会出错。

> hincrby ireader go 1
(integer) 1
> hincrby ireader python 4
(integer) 4
> hincrby ireader java 4
(integer) 4

> hgetall ireader
1) "go"
2) "1"
3) "python"
4) "4"
5) "java"
6) "4"

> hset ireader rust good    # 创建一个新的hash并且val不赋值为int类型
(integer) 1
127.0.0.1:6379> hincrby ireader rust 1    
(error) ERR hash value is not an integer    # val不是int类型计数器会报错

扩容问题

当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。

扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。

如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。所以Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

缩容

Redis的hash结构不但有扩容还有缩容,从这一点出发,它要比Java的HashMap要厉害一些,Java的HashMap只有扩容。

缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。

set

Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL

当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。

set用来做什么?

set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

增加元素

可以一次增加多个元素

> sadd ireader go java python
(integer) 3

读取元素

使用smembers列出所有元素。

使用scard获取集合长度。

使用srandmember获取随机count个元素,如果不提供count参数,默认为1

> sadd ireader go java python
(integer) 3

> smembers ireader
1) "java"
2) "python"
3) "go"

> scard ireader
(integer) 3

> srandmember ireader
"java"

删除元素

使用srem删除一到多个元素,使用spop删除随机一个元素

> sadd ireader go java python rust erlang
(integer) 5

> srem ireader go java
(integer) 2

> spop ireader
"erlang"

判断元素是否存在

使用sismember指令,只能接收单个元素

> sadd ireader go java python rust erlang
(integer) 5

> sismember ireader rust
(integer) 1

> sismember ireader javascript
(integer) 0

sortedset

SortedSet(zset)是Redis提供的一个非常特别的数据结构。

它类似于 Java 的 SortedSet 和 HashMap 的结合体。

一方面它是一个 set,保证了内部 value 的唯一性。

另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

其实zset就是一个Map,把Map中的key看作储存value的地方,把Map中的value看作储存score的地方。由于hash结构是采用key的hashcode取模的方式来决定集合顺序的,所以在hash数据结构中是无序的。但zset是引入了跳跃链表的方式来模仿数组的平衡数来达到对hash结构排序的目的。

zset底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表

hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。

zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

zset用来做什么?

zset 可以用来存 粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间 进行排序。

zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们
可以对成绩按分数进行排序就可以得到他的名次。

zset 可以通过排序的名次找到前几名或后几名

增加元素

通过zadd指令可以增加一到多个value/score对,score放在前面

> zadd ireader 4.0 python
(integer) 1

> zadd ireader 4.0 java 1.0 go
(integer) 2

长度

通过指令zcard可以得到zset的元素个数

> zcard ireader
(integer) 3

删除元素

通过指令zrem可以删除zset中的元素,可以一次删除多个

> zrem ireader go python
(integer) 2

计数器

同hash结构一样,zset也可以作为计数器使用。

> zadd ireader 4.0 python 4.0 java 1.0 go
(integer) 3
> zincrby ireader 1.0 python
"5"

获取排名和分数

# 通过zscore指令获取指定元素的权重。
> zscore ireader python
"5"

# 通过zrank指令获取指定元素的正向排名。# 分数低的排名考前,rank值小
> zrank ireader go  
(integer) 0

# 通过zrank指令获取指定元素的正向排名。
> zrank ireader java
(integer) 1

# 通过zrank指令获取指定元素的正向排名。
> zrank ireader python
(integer) 2

# 通过zrevrank指令获取指定元素的反向排名[倒数第一名]。正向是由小到大,负向是由大到小。
> zrevrank ireader python
(integer) 0

根据排名范围获取元素列表

# 通过zrange指令指定排名范围参数获取对应的元素列表,携带withscores参数可以一并获取元素的权重
> zrange ireader 0 -1  # 获取所有元素
1) "go"
2) "java"
3) "python"

# 通过zrange指令指定排名范围参数获取对应的元素列表,携带withscores参数可以一并获取元素的权重
127.0.0.1:6379> zrange ireader 0 -1 withscores
1) "go"
2) "1"
3) "java"
4) "4"
5) "python"
6) "5"

# 通过zrevrange指令按负向排名获取元素列表[倒数]。正向是由小到大,负向是由大到小。
> zrevrange ireader 0 -1 withscores
1) "python"
2) "5"
3) "java"
4) "4"
5) "go"
6) "1"

根据score范围获取列表

# 通过zrangebyscore指令指定score范围获取对应的元素列表。
> zrangebyscore ireader 0 5
1) "go"
2) "java"
3) "python"

# 通过zrangebyscore指令指定score范围获取对应的元素列表。参数-inf表示负无穷,+inf表示正无穷。
> zrangebyscore ireader -inf +inf withscores
1) "go"
2) "1"
3) "java"
4) "4"
5) "python"
6) "5"

# 通过zrevrangebyscore指令获取倒排元素列表。正向是由小到大,负向是由大到小。
> zrevrangebyscore ireader +inf -inf withscores  # 注意正负反过来了
1) "python"                                      # 参数-inf表示负无穷,+inf表示正无穷。
2) "5"
3) "java"
4) "4"
5) "go"
6) "1"

根据范围移除元素列表

可以通过排名范围,也可以通过score范围来一次性移除多个元素

> zremrangebyrank ireader 0 1	# 根据排名删除第0个,第1个元素
(integer) 2  # 删掉了2个元素

> zadd ireader 4.0 java 1.0 go
(integer) 2

> zremrangebyscore ireader -inf 4	# 根据score删除从无穷小到4包括4的元素
(integer) 2

> zrange ireader 0 -1
1) "python"

跳跃列表

zset内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂。

因为zset要支持随机的插入和删除,所以它不好使用数组来表示。我们先看一个普通的链表结构。

44b184164e5352301fefd397c0208b4a99c.jpg

我们需要这个链表按照score值进行排序。这意味着当有新元素需要插入时,需要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到,那该怎么办?

想想一个创业公司,刚开始只有几个人,团队成员之间人人平等,都是联合创始人。随着公司的成长,人数渐渐变多,团队沟通成本随之增加。这时候就会引入组长制,对团队进行划分。每个团队会有一个组长。开会的时候分团队进行,多个组长之间还会有自己的会议安排。公司规模进一步扩展,需要再增加一个层级——部门,每个部门会从组长列表中推选出一个代表来作为部长。部长们之间还会有自己的高层会议安排。

跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。

想想你老家在世界地图中的位置:亚洲-->中国->安徽省->安庆市->枞阳县->汤沟镇->田间村->xxxx号,也是这样一个类似的结构。

img

「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于L0、L1和L2层,可以快速在不同层次之间进行「跳跃」。

img

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问那新插入的元素如何才有机会「身兼数职」呢?

跳跃列表采取一个随机策略来决定新元素可以兼职到第几层,首先L0层肯定是100%了,L1层只有50%的概率,L2层只有25%的概率,L3层只有12.5%的概率,一直随机到最顶层L31层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。

容器型数据结构的通用规则

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

1、create if not exists

如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。

2、drop if no elements

如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一 个元素,列表就消失了。

3、过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。

需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期, 而不是其中的某个子 key。

还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了set 方法修改了它,它的过期时间会消失。

set codehole yoyo OK

expire codehole 600 (integer) 1 

ttl codehole (integer) 597 

set codehole yoyo OK

ttl codehole
(integer) -1

3个特殊的高级数据结构

Bitmaps

bitmaps不是一个真实的数据结构。而是String类型上的一组面向bit操作的集合。由于strings是二进制安全的blob,并且它们的最大长度是512m,所以bitmaps能最大设置2^32个不同的bit。

bit操作被分为两组:

  • 恒定时间的单个bit操作,例如把某个bit设置为0或者1。或者获取某bit的值。
  • 一组bit的操作。例如给定范围内bit统计(例如人口统计)。

Bitmaps的最大优点就是存储信息时可以节省大量的空间

例如在一个系统中,不同的用户被一个增长的用户ID表示。

40亿(2^32=4*1024*1024*1024≈40亿)用户只需要512M内存就能记住某种信息,例如用户是否登录过。

Bits设置和获取通过SETBIT 和GETBIT 命令,用法如下:

SETBIT key offset value
GETBIT key offset

使用实例:

127.0.0.1:6380> setbit dupcheck 10 1
(integer) 0
127.0.0.1:6380> getbit dupcheck 10 
(integer) 1

SETBIT命令第一个参数是位编号,第二个参数是这个位的值,只能是0或者1。

如果bit地址超过当前string长度,会自动增大string。

4dd8254367af0111104e706895e1853ec32.jpg

Bitmaps示意图

GETBIT命令指示返回指定位置bit的值。超过范围(寻址地址在目标key的string长度以外的位)的GETBIT总是返回0。三个操作bits组的命令如下:

  • BITOP 执行两个不同string的位操作.,包括AND,OR,XOR和NOT.
  • BITCOUNT 统计位的值为1的数量。
  • BITPOS 寻址第一个为0或者1的bit的位置(寻址第一个为1的bit的位置:bitpos dupcheck 1; 寻址第一个为0的bit的位置:bitpos dupcheck 0).

bitmaps一般的使用场景:

  • 各种实时分析.
  • 存储与对象ID关联的节省空间并且高性能的布尔信息.

例如,想象一下你想知道访问你的网站的用户的最长连续时间。你开始计算从0开始的天数,就是你的网站公开的那天,每次用户访问网站时通过SETBIT命令设置bit为1,可以简单的用当前时间减去初始时间并除以3600*24(结果就是你的网站公开的第几天)当做这个bit的位置。

这种方法对于每个用户,都有存储每天的访问信息的一个很小的string字符串。通过BITCOUN就能轻易统计某个用户历史访问网站的天数。另外通过调用BITPOS命令,或者客户端获取并分析这个bitmap,就能计算出最长停留时间。

HyperLogLogs

HyperLogLog是用于计算唯一事物的概率数据结构(从技术上讲,这被称为估计集合的基数)。如果统计唯一项,项目越多,需要的内存就越多。因为需要记住过去已经看过的项,从而避免多次统计这些项。

然而,有一组算法可以交换内存以获得精确度:在redis的实现中,您使用标准错误小于1%的估计度量结束。这个算法的神奇在于不再需要与需要统计的项相对应的内存,取而代之,使用的内存一直恒定不变。最坏的情况下只需要12k,就可以计算接近2^64个不同元素的基数。或者如果您的HyperLogLog(我们从现在开始简称它为HLL)已经看到的元素非常少,则需要的内存要要少得多。

在redis中HLL是一个不同的数据结构,它被编码成Redis字符串。因此可以通过调用GET命令序列化一个HLL,也可以通过调用SET命令将其反序列化到redis服务器。

HLL的API类似使用SETS数据结构做相同的任务,SETS结构中,通过SADD命令把每一个观察的元素添加到一个SET集合,用SCARD命令检查SET集合中元素的数量,集合里的元素都是唯一的,已经存在的元素不会被重复添加。

而使用HLL时并不是真正添加项到HLL中(这一点和SETS结构差异很大),因为HLL的数据结构只包含一个不包含实际元素的状态,API是一样的:

  • PFADD命令用于添加一个新元素到统计中。
  • PFCOUNT命令用于获取到目前为止通过PFADD命令添加的唯一元素个数近似值。
  • PFMERGE命令执行多个HLL之间的联合操作。
127.0.0.1:6380> PFADD hll a b c d d c
(integer) 1
127.0.0.1:6380> PFCOUNT hll
(integer) 4
127.0.0.1:6380> PFADD hll e
(integer) 1
127.0.0.1:6380> PFCOUNT hll
(integer) 5

PFMERGE命令说明:bashbash

PFMERGE destkey sourcekey [sourcekey ...]
Merge N different HyperLogLogs into a single one.


用法(把hll1和hll2合并到hlls中):bashbash

127.0.0.1:6380> PFADD hll1 1 2 3
(integer) 1
127.0.0.1:6380> PFADD hll2 3 4 5
(integer) 1
127.0.0.1:6380> PFMERGE hlls hll1 hll2
OK
127.0.0.1:6380> PFCOUNT hlls

HLL数据结构的一个使用场景就是计算用户每天在搜索框中执行的唯一查询,即搜索页面UV统计。而Bitmaps则用于判断某个用户是否访问过搜索页面。这是它们用法的不同。

GEO

Redis的GEO特性在 Redis3.2版本中推出,这个功能可以将用户给定的地理位置(经度和纬度)信息储存起来,并对这些信息进行操作。GEO相关命令只有6个:

GEOADD

GEOADD key longitude latitude member [longitude latitude member …],将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。例如:GEOADD city 113.501389 22.405556 shenzhen

经纬度具体的限制,由EPSG:900913/EPSG:3785/OSGEO:41001规定如下:
有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。

GEOHASH

GEOHASH key member [member …],返回一个或多个位置元素的标准Geohash值,它可以在http://geohash.org/使用。查询例子:http://geohash.org/sqdtr74hyu0.(可以通过谷歌了解Geohash原理,或者戳Geohash基本原理:https://www.cnblogs.com/tgzhu/p/6204173.html)。

GEOPOS

GEOPOS key member [member …],从key里返回所有给定位置元素的位置(经度和纬度)。

GEODIST

GEODIST key member1 member2 [unit],返回两个给定位置之间的距离。GEODIST命令在计算距离时会假设地球为完美的球形。在极限情况下,这一假设最大会造成0.5%的误差。

指定单位的参数unit必须是以下单位的其中一个:
m 表示单位为米(默认)。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。

GEORADIUS

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD][WITHDIST] [WITHHASH][COUNT count],以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素这个命令可以查询某城市的周边城市群。

GEORADIUSBYMEMBER

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD][WITHDIST] [WITHHASH][COUNT count],这个命令和GEORADIUS命令一样,都可以找出位于指定范围内的元素,但是GEORADIUSBYMEMBER的中心点是由给定的位置元素决定的,而不是像 GEORADIUS那样,使用输入的经度和纬度来决定中心点。

指定成员的位置被用作查询的中心。

GEO的6个命令用法示例如下:

redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2

redis> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"

redis> GEOPOS Sicily Palermo Catania NonExisting
1) 1) "13.361389338970184"
   2) "38.115556395496299"
2) 1) "15.087267458438873"
   2) "37.50266842333162"
3) (nil)

redis> GEODIST Sicily Palermo Catania
"166274.15156960039"

redis> GEORADIUS Sicily 15 37 100 km
1) "Catania"
redis> GEORADIUS Sicily 15 37 200 km
1) "Palermo"
2) "Catania"

redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km
1) "Agrigento"
2) "Palermo"

其他玩法参考

由于把所有Redis玩法全写在一张博客中太长了,即便加了目录看着也费劲,所以我就分成了2部分。
这一张是讲Redis的几种数据结构的,还有一张是讲Redis的各种花里胡哨的玩法的。
例如:redis分布式锁,redis集群之replication一主多从的主从复制读写分离集群与哨兵的配合工作实现高可用,redis集群之cluster多主多从主从复集群,redis的hashsolt,集群间的元信息交换过程,主从间增量同步,全量同步过程。redis的持久化:aof,aof中的rewrite操作,rdb。

基本操作文档

http://redisdoc.com/ 官网

https://www.yiibai.com/redis

https://www.w3cschool.cn/redis_all_about/

博客内容来源:https://my.oschina.net/xsh1208/blog/2218494

存储数据到底用什么?

《存结构体信息到底该使用 hash 还是 string?》

posted @ 2019-01-14 21:38  yabushier  阅读(702)  评论(0编辑  收藏  举报