Redis之数据类型详解分析
1 Redis
Redis
官网英文版:https://redis.io/
Redis
官网中文版:http://redis.cn/
1.1 概述
Redis
是以key-value
存储的数据结构服务器,所有的key(键)
是字符串,而value
可以包含:
- 字符串类型(
string
):最基本的数据类型,二进制安全的字符串,最大512M
- 列表类型(
list
):按照添加顺序保持顺序的字符串列表 - 集合类型(
set
):无序的字符串集合,不存在重复的元素 - 有序集合类型(
sorted set
或Zset
):已排序的字符串集合 - 散列类型(
hash
):key-value对的一种集合 - 位操作(
bitmap
):更细化的一种操作,以bit
为单位。 - 基数统计(
hyperloglog
):基于概率的数据结构,2.8.9新增 - 地理位置(
Geo
):地理位置信息储存起来, 并对这些信息进行操作 3.2新增 - 流(
Stream
) 5.0新增
1.2 查看内部编码
Redis
查看内部编码使用OBJECT ENCODING
命令
该命令用来返回数据结构的内部编码
对象所使用的底层数据结构 | 编码常量 | object encoding 命令输出 |
---|---|---|
整数 | REDIS_ENCODING_INT | "int" |
embstr编码简单动态字符串(SDS) | REDIS_ENCODING_EMBSTR | "embstr" |
简单动态字符串 | REDIS_ENCODING_RAW | "raw" |
字典 | REDIS_ENCODING_HT | "hashtable" |
双端链表 | REDIS_ENCODING_LINKEDLIST | "linkedlist" |
压缩列表 | REDIS_ENCODING_ZIPLiST | "ziplist" |
整数集合 | REDIS_ENCODING_INTSET | "intset" |
跳跃表和字典 | REDIS_ENCODING_SKIPLIST | "skiplist" |
1.3 String字符串
1.3.1 简介
String
是redis中最基本的数据类型,一个key对应一个value。
redis的key和string类型value限制均为512MB
虽然Key的大小上限为512M
,但是一般建议key的大小不要超过1KB
,这样既可以节约存储空间,又有利于Redis
进行检索
1.3.2 应用常景
String
类型是二进制安全的,意思是 redis
的 string
可以包含任何数据。如数字
,字符串
,jpg图片
或者序列化
的对象。字符串类型实际上可以是字符串(简单的字符串、复杂的字符串(xml、json)、数字(整数、浮点数)、二进制(图片、音频、视频))
缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
1.3.3 String内部编码
String
内部编码:
int
:8个字节的长整型(long,2^63-1)embstr
:小于等于44
个字节的字符串,embstr
格式的SDS(简单动态字符串:Simple Dynamic String)
raw
:SDS大于 44
个字节的字符串
Redis
为什么要自己写一个SDS
的数据类型,主要是为了解决C语言 char[]
的四个问题:
字符数组
必须先给目标变量分配足够的空间,否则可能会溢出- 查询字符数组长度,时间复杂度
O(n)
- 长度变化,需要重新分配内存
- 通过从字符串开始到结尾碰到的第一个
\0
来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes
)保存的内容,二进制不安全
Redis SDS
的优势:
- 不用担心内存溢出问题,如果需要会对
SDS
进行扩容 - 因为定义了
len
属性,查询数组长度时间复杂度O(1)
固定长度 - 空间预分配,惰性空间释放
- 根据长度
len
来判断是否结束,而不是\0
为什么要有embstr
编码呢?比raw
的优势在哪里?
embstr
编码将创建字符串对象所需的空间分配的次数从raw
编码的两次降低为一次。
因为emstr
编码字符串的素有对象保持在一块连续的内存里面,所以那个编码的字符串对象比起raw
编码的字符串对象能更好的利用缓存。并且释放embstr
编码的字符串对象只需要调用一次内存释放函数,而释放raw
编码对象的字符串对象需要调用两次内存释放函数
1.4 Hash散列
1.4.1 简介
常用命令:hget,hsetnx,hset,hvals,hgetall,hmset,hmget 等
Redis
中每个 hash
可以存储 2^32 - 1
键值对(40多亿)
1.4.2 应用常景
我们简单举个实例来描述下 Hash 的应用场景:
比如我们要存储一个用户信息对象数据,包含以下信息:用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用普通的 key/value 结构来存储,主要有以下2种存储方式:
- 第一种方式将用户 ID 作为查找 key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
- 第二种方法是这个用户信息对象有多少成员就存成多少个 key-value 对儿,用用户 ID +对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户 ID 为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么 Redis
提供的 Hash
很好的解决了这个问题,Redis
的 Hash
实际是内部存储的 Value
为一个 HashMap
,并提供了直接存取这个 Map
成员的接口
也就是说,Key 仍然是用户 ID,value 是一个 Map,这个 Map 的 key 是成员的属性名,value 是属性值,这样对数据的修改和存取都可以直接通过其内部 Map 的 Key(Redis 里称内部 Map 的 key 为 field),也就是通过 key(用户 ID) + field(属性标签)就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
很好的解决了问题。这里同时需要注意,Redis
提供了接口(hgetall
)可以直接取到全部的属性数据,但是如果内部 Map 的成员很多,那么涉及到遍历整个内部 Map
的操作,由于 Redis
单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
1.4.3 Hash内部编码
内部编码:
ziplist
(压缩列表):当哈希类型中元素个数小于hash-max-ziplist-entries
配置(默认512
个),同时所有值都小于hash-max-ziplist-value
配置(默认64 字节
)时,Redis
会使用ziplist
作为哈希的内部实现。hashtable
(哈希表):当上述条件不满足时,Redis
则会采用hashtable
作为哈希的内部实现。
在 Redis 7.0
中,压缩列表
数据结构已经废弃了,交由 listpack
数据结构来实现了
1.4.4 rehash和渐进式rehash操作
扩容和缩容都会通过rehash
来实现,所谓渐进式rehash
是指我们的大字典的扩容是比较消耗时间的,需要重新申请新的数组,然后将旧字典所有链表的元素重新挂接到新的数组下面,是一个O(n)
的操作。但是因为我们的redis
是单线程的,无法承受这样的耗时过程,所以采用了渐进式rehash
小步搬迁,虽然慢一点,但是可以搬迁完毕
redis
会在内部扩容时新建一个长度为原始长度2倍的空哈希表,然后原哈希表上的元素重新rehash
到新的哈希表中去,然后我们再使用新的哈希表即可。
那么,这样还是有个问题要解决呀
要知道
redis
中存储的数据可能是成百万上千万的,我们重新rehash
一次未免太耗时了吧,因为redis中操作大部分是单线程的。
这个过程可能会阻断其他操作很长时间,这是不能忍受的,那要怎么处理呢
1.4.4.1 过程
首先redis
是采用了渐进式rehash
的操作,就是会有一个变量,指向第一个哈希桶,然后redis
每执行一个添加key
,删除key
的类似命令,就顺便copy
一个哈希桶中的数据到新的哈希表中去,这样细水长流的操作,是不会影响什么性能,就会所有的数据都被重新hash
到新的哈希表中。
那么在这个过程中,当然再有写的操作,会直接把数据放到新的哈希表中,保证旧的肯定有copy完的时候,如果这段时间对数据库的操作比较少,也没有关系,redis
内部也有定时任务,每隔一段时间也会copy
一次
redis
通过链式哈希解决冲突,也就是同一个桶里面的元素使用链表
保存。但是当链表过长就会导致查找性能变差可能。所以redis
为了追求块,使用了两个全局哈希表
。用于rehash
操作,增加现有的哈希桶数量,减少哈希冲突
开始默认使用hash表1
保存键值对数据,hash表2
此刻没有分配空间。当数据越来越多的触发rehash
操作,则执行以下操作:
- 给
hash表2
分配更大的空间 - 将
hash表1
的数据重新映射拷贝到hash表2
中
将hash表1的数据重新映射到hash表2的过程并不是一次性的,这样会造成redis阻塞,无法提供服务 - 释放
hash表1
的空间
详细步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
- 在rehash进行期间,每次对字典执行添加,删除,查找或者更新操作时,程序除了执行特定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增1
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成
- 将ht[0]释放,然后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表
1.4.4.2 rehash触发条件
rehash
触发条件:
- 扩容
我们的扩容一般会在Hash
表中的元素个数等于第一维数组的长度的时候,就会开始扩容。扩容的大小是原数组的两倍。不过在redis
在做bgsave
(RDB持久化操作的过程)时,为了减少内存页的过多分离(Copy On Write),redis
不会去扩容。
但是如果hash表的元素个数已经到达了第一维数组长度的5倍的时候,就会强制扩容,不管你是否在持久化。 - 缩容
当我们的hash
表元素逐渐删除的越来越少的时候。redis
就会对hash
表进行缩容来减少第一维数组长度的空间占用。缩容的条件是元素个数低于数组长度的10%
,并且缩容不考虑是否在做redis持久化
。
不用考虑bgsave
主要原因是因为我们的缩容的内存都是已经使用过的,缩容的时候可以直接置空,而且由于申请的内存比较小,同时会释放掉一些已经使用的内存,不会增大系统的压力。
1.4.5 跟JDK的HashMap的区别
数据结构上,采用了两个数组保存数据,发生hash冲突时,只采用了链地址法
解决hash
冲突,并没有跟jdk1.8
一样当链表超过8时优化成红黑树,因此插入元素时跟jdk1.7
的hashmap
一样采用的是头插法
。
在发生扩容时,跟jdk的hashmap一次性、集中式进行扩容不一样,采取的是渐进式的rehash,每次操作只会操作当前的元素,在当前数组中移除或者存放到新的数组中,直到老数组的元素彻底变成空表。
当负载因子小于0.1
时,会自动进行缩容
。jdk的hashmap出于性能考虑,不提供缩容的操作。
redis
使用MurmurHash
来计算哈希表的键的hash值,而jdk
的hashmap
使用key.hashcode()的高十六位跟低十六位做与运算获得键的hash值。
1.5 List列表
1.5.1 简介
Redis
中的List
其实就是链表
(Redis
用双端链表
实现List
)
使用List
结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
列表(List
)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1
个元素。Redis
中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等
1.5.2 命令和应用
常用命令:lpush,rpush,lpop,rpop,lrange
等。
应用场景
比如 twitter 的关注列表,粉丝列表等都可以用 Redis 的 list 结构来实现,可以利用lrange命令,做基于Redis的分页功能,性能极佳,用户体验好
消息队列:Redis 的 list 是有序的列表结构,可以实现阻塞队列,使用左进右出的方式。Lpush 用来生产 从左侧插入数据,Brpop 用来消费,用来从右侧 阻塞的消费数据。
数据的分页展示: lrange 命令需要两个索引来获取数据,这个就可以用来实现分页,可以在代码中计算两个索引值,然后来 redis 中取数据。
可以用来实现粉丝列表以及最新消息排行等功能
使用列表的技巧:
lpush+lpop=Stack
(栈)lpush+rpop=Queue
(队列)lpush+ltrim=Capped Collection
(有限集合)lpush+brpop=Message Queue
(消息队列)
1.5.3 List内部编码
内部编码:
ziplist
(压缩列表):当列表中元素个数小于512
(默认)个,并且列表中每个元素的值都小于64
(默认)个字节时,Redis
会选择用ziplist
来作为列表的内部实现以减少内存的使用。当然上述默认值也可以通过相关参数修改:list-max-ziplist-entried
(元素个数)、list-max-ziplist-value
(元素值)。linkedlist
(链表):当列表类型无法满足ziplist
条件时,Redis
会选择用linkedlist
作为列表的内部实现。
因为双向链表
占用的内存比压缩列表
要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现quicklist
(快速列表)就是linkedlist
和ziplist
的结合。quicklist
中的每个节点ziplist
都能够存储多个数据元素。Redis3.2开始,列表采quicklist进编码
1.6 Set集合
1.6.1 简介
Redis
的 Set
是 String
类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis
中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)
集合中最大的成员数为 2^32 - 1
(每个集合可存储40多亿个成员)
1.6.2 命令和应用
常用命令:sadd,spop,smembers,sunion,scard,sscan,sismember等。
应用场景:
Redis set
对外提供的功能与 list
类似是一个列表的功能,特殊之处在于 set
是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set
是一个很好的选择,并且 set
提供了判断某个成员是否在一个 set
集合内的重要接口,这个也是 list 所不能提供的。
标签(tag):集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及曾强用户粘度比较重要。
点赞,或点踩,收藏等,可以放到set中实现
1.6.3 Set内部编码
Set
内部编码:
intset
(整数集合):当集合中的元素都是整数,并且集合中的元素个数小于set-max-intset-entries
参数时,默认512
,Redis
会选用intset
作为底层内部实现。hashtable
(哈希表):当上述条件不满足时,Redis
会采用hashtable
作为底层实现。
1.7 ZSet有序集合
1.7.1 简介
Redis
有序集合和集合一样也是 string
类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double
类型的分数
。redis
正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score
)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 2^32 - 1
(每个集合可存储40多亿个成员)
1.7.2 命令和应用
常用命令:zadd,zrange,zrem,zcard,zscore,zcount,zlexcount等
应用常景:
- 排行榜:有序集合经典使用场景
例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行,
如新闻网站对热点新闻排序,比如根据点击量、点赞量等。 - 带权重的消息队列:重要的消息
score
大一些,普通消息score
小一些,可以实现优先级高的任务先执行
1.7.3 ZSet内部编码
内部编码:
ziplist
(压缩列表):当有序集合的元素个数小于128
个(默认设置),同时每个元素的值都小于64
字节(默认设置),Redis
会采用ziplist
作为有序集合的内部实现。也可以通过以下参数设置:zset-max-ziplist-entries
和zset-max-ziplist-value
skiplist
(跳跃表):当上述条件不满足时,Redis
会采用skiplist
作为内部编码。
在 Redis 7.0
中,压缩列表数据结构已经废弃了,交由 listpack
数据结构来实现了
1.7.4 跳表数据结构
链表
在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N)
,于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种 多层
的有序链表,这样的好处是能快读定位数据。
那跳表长什么样呢?这里举个例子,下图展示了一个层级为 3 的跳表。
图中头节点有 L0~L2
三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:
- L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
- L1 层级共有 3 个节点,分别是节点 2、3、5;
- L2 层级只有 1 个节点,也就是节点 3 。
如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。
可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)
。
跳表是怎么设置层高的?
跳表在创建节点时候,会生成范围为
[0-1]
的一个随机数,如果这个随机数小于0.25
(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
1.8 Bitmap位图
1.8.1 简介
Bitmap
(也称为位数组或者位向量等)是一种实现对位的操作的'数据结构',在数据结构加引号主要因为:Bitmap
本身不是一种数据结构,底层实际上是字符串,可以借助字符串进行位操作。
Bitmap
单独提供了一套命令,所以与使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0
和 1
,数组的下标在 Bitmap
中叫做偏移量 offset
。
bitmap
的出现是为了大数据量而来的,但是前提是统计的这个大数据量每个的状态只能有两种
,因为每一个bit
位只能表示两种状态。
1.8.2 应用常景
假如我们现在有几亿个数据,数据状态都是1或者0两个状态,比如用户签到次数、或者登录次数等。
- 场景一:用户签到
很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况
Redis
为我们提供了bitmap
(位图)这一数据结构,每个用户每天的登录记录只占据一位,365
天就是365
位,仅仅需要46字节就可存储,极大地节约了存储空间。 - 场景二:统计活跃用户
使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令 - 场景三:用户在线状态
前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间
1.8.3 底层原理
我们知道 Bitmap
本身不是一种数据结构,底层实际上使用字符串
来存储。只不过操作的粒度变成了位,即bit。
由于 Redis
中字符串的最大长度是 512 MB
字节,所以 BitMap
的偏移量 offset
值也是有上限的,其最大值是:8 * 1024 * 1024 * 512 = 2^32
。由于 C 语言中字符串的末尾都要存储一位分隔符,所以实际上 BitMap
的偏移量 offset 值上限是:2^32-1
Bitmap
本身是用 String
类型作为底层数据结构实现的一种统计二值状态的数据类型。String
类型是会保存为二进制的字节数组,所以,Redis
就把字节数组的每个 bit
位利用起来,用来表示一个元素的二值状态。可以把 Bitmap
看作是一个 bit
数组。
1.8.4 命令
- SETBIT
SETBIE
用来设置或清除存储在键处的字符串值的偏移位,其返回值是原来位上存储的值。key 在初始状态下所有的位都为 0
基本格式:SETBIT key offset value
- GETBIT
GETBIT
用来获取存储在键处的字符串值中偏移位置的位值。
基本格式:GETBIT key offset
- BITCOUNT
BITCOUNT
用来统计指定区间内,值为1的个数。选择特定的byte
范围计数,具体如下
基本格式:BITCOUNT key [start end]
(注意start和end指的是字节,不是位)start
:设置位索引起始位置(包含该位置计数),第一个位置以 0 开始,start
参数需和end
参数同时设置才合法end
:设置位索引结束位置(包含该位置计数),end
参数需和start
参数同时设置才合法
- BITOP
对一个或多个保存二进制位的字符串key
进行位元操作,并将结果保存到destkey
上
语法格式:BITOP operation destkey key [key ...]
语法:operation
可以是AND(与) 、 OR (或)、 NOT(非) 、 XOR(异或)
除了NOT
操作之外,其他操作都可以接受一个或多个 key 作为输入 - BITPOS
用来计算指定key
对应字符串中,第一位为1
或者0
的offset
位置。除此之外,BITPOS
也有两个选项start
和end
,跟BITCOUNT
一样。
语法格式:BITPOS key bit [ start [ end [ BYTE | BIT]]]
BYTE、BIT 这两个选项从 7.0.0 版本开始才能使用。
1.9 HyperLogLog基数统计
1.9.1 简介
Redis 2.8.9
版本更新了 Hyperloglog
数据结构,Redis HyperLogLog
是用来做基数统计的算法,所谓基数,也就是不重复的元素
- 优点:
在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且是很小的。在Redis
里面,每个HyperLogLog
键只需要花费12 KB
内存,就可以计算接近2^64
个不同元素的基数。 - 缺点:
因为HyperLogLog
只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog
不能像集合那样,返回输入的各个元素。
估算的值,可能存在误差,带有 0.81% 标准错误的近似值
1.9.2 命令和场景
这个数据结构的命令有三个:
PFADD
:添加指定元素到HyperLogLog
PFCOUNT
:返回给定HyperLogLog
的基数估算值PFMERGE
:将多个HyperLogLog
合并为一个HyperLogLog
应用场景:
- 网页统计UV (浏览用户数量,同一天同一个ip多次访问算一次访问,目的是计数,而不是保存用户)
传统的方式是使用set保存用户的id,可以统计set中元素数量作为标准判断。
但如果这种方式保存大量用户id,会占用大量内存,我们的目的是为了计数,而不是去保存id。 - 注册 IP 数、每日访问 IP 数、页面实时UV)、在线用户数等
1.9.3 内部编码和原理
HyperLogLog
算法时一种非常巧妙的近似统计大量去重元素数量的算法,它内部维护了16384
个桶来记录各自桶的元素数量,当一个元素过来,它会散列到其中一个桶。当元素到来时,通过 hash
算法将这个元素分派到其中的一个小集合存储,同样的元素总是会散列到同样的小集合。这样总的计数就是所有小集合大小的总和。使用这种方式精确计数除了可以增加元素外,还可以减少元素
一个HyperLogLog
实际占用的空间大约是 12k
字节。但是在计数比较小的时候,大多数桶的计数值都是零。如果 12k
字节里面太多的字节都是零,那么这个空间是可以适当节约一下的。
Redis
在计数值比较小的情况下采用了稀疏存储
,稀疏存储
的空间占用远远小于 12k
字节。相对于稀疏存储
的就是密集存储
,密集存储
会恒定占用 12k 字节。
内部编码
HyperLogLog
整体的内部结构就是 HLL
对象头 加上 16384
个桶的计数值位图。它在 Redis
的内部结构表现就是一个字符串位图。你可以把 HyperLogLog
对象当成普通的字符串来进行处理。
1.10 GEO地理位置
1.10.1 简介
Redis
的 Geo
在 Redis 3.2
版本就推出了,这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人,GEO使用的是国际通用坐标系WGS-84
1.10.2 命令和场景
命令:
help @geo 查看geo分组下所有的命令
help geoadd 用于查看单个具体命令
主要操作方法有:
geoadd
:添加地理位置的坐标
geoadd 语法格式:GEOADD key longitude latitude member [longitude latitude member ...]
geopos
:用于从给定的key
里返回所有指定名称(member
)的位置(经度和纬度),不存在的返回nil
geopos
语法格式:GEOPOS key member [member ...]
geodist
:用于返回两个给定位置之间的距离
geodist 语法格式:GEODIST key member1 member2 [m|km|ft|mi]
member1 member2 为两个地理位置
最后一个距离单位参数说明:m :米,默认单位;km :千米;mi :英里;ft :英尺georadius
:以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素
语法格式:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明:m
:米,默认单位。km
:千米。mi
:英里。ft
:英尺。WITHDIST
: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。WITHCOORD
: 将位置元素的经度和纬度也一并返回。WITHHASH
: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。COUNT
: 限定返回的记录数ASC
: 查找结果根据距离从近到远排序。DESC
: 查找结果根据从远到近排序。
georadiusbymember
: 和GEORADIUS
命令一样, 都可以找出位于指定范围内的元素, 但是georadiusbymember
的中心点是由给定的位置元素
决定的, 而不是使用经度和纬度
来决定中心点
语法格式:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明同GEORADIUS
geohash
:返回一个或多个位置对象的geohash
值。
Redis GEO
使用geohash
来保存地理位置的坐标。geohash
用于获取一个或多个位置元素的geohash
值。
geohash
语法格式如下:GEOHASH key member [member ...]
应用场景:
用于存储地理信息以及对地理信息作操作的场景
- 查看附近的人
- 微信位置共享
- 地图上直线距离的展示
- 比如检索附近的主播
1.10.3 内部编码
需要说明的是,Geo
本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET)
,并且使用GeoHash
技术进行填充。Redis
中将经纬度使用52位
的整数进行编码,放进zset
中,score
就是GeoHash
的52位整数值。在使用Redis
进行Geo
查询时,其内部对应的操作其实就是zset(skiplist)
的操作。
通过zset
的score
进行排序就可以得到坐标附近的其它元素,通过将score
还原成坐标值就可以得到元素的原始坐标。
总之,Redis
中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score
(整数编码值)反解坐标点 --> 附近点的地理位置坐标
1.11 Stream流
1.11.1 简介
Redis Stream
是 Redis 5.0
版本新增加的数据结构。
Redis Stream
主要用于消息队列(MQ,Message Queue),Redis
本身是有一个 Redis
发布订阅 (pub/sub
) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
点击此处了解为什么Redis不适合用作MQ
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。
Redis Stream
提供了消息的持久化
和主备复制
功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
用一句话概括Stream
就是Redis
实现的内存版kafka
,支持多播的可持久化的消息队列,用于实现发布订阅功能,借鉴了 kafka
的设计。Redis Stream
的结构有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis
重启后,内容还在。
1.11.2 命令
Redis Stream
的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容
每个 Stream
都有唯一的名称,它就是 Redis
的 key
,在我们首次使用 xadd
指令追加消息时自动创建。
上图解析:
Consumer Group
:消费组,使用XGROUP CREATE
命令创建,一个消费组有多个消费者(Consumer
)。last_delivered_id
:游标,每个消费组会有个游标last_delivered_id
,任意一个消费者读取了消息都会使游标last_delivered_id
往前移动。pending_ids
:消费者(Consumer
)的状态变量,作用是维护消费者的未确认的 id。pending_ids
记录了当前已经被客户端读取的消息,但是还没有ack
(Acknowledge character
:确认字符)。
消息队列相关命令:
XADD
:添加消息到末尾
使用XADD
向队列添加消息,如果指定的队列不存在,则创建一个队列
语法格式:XADD key ID field value [field value ...]
key
:队列名称,如果不存在就创建ID
:消息 id,我们使用*
表示由 redis 生成,可以自定义,但是要自己保证递增性。field value
: 记录
XTRIM
:对流进行修剪,限制长度
语法格式:XTRIM key MAXLEN [~] count
key
:队列名称MAXLEN
:长度count
:数量
XDEL
:删除消息
语法格式:XDEL key ID [ID ...]
key
:队列名称ID
:消息 ID
XLEN
:获取流包含的元素数量,即消息长度
语法格式:XLEN key
,key
:队列名称XRANGE
:获取消息列表,会自动过滤已经删除的消息
语法格式:XRANGE key start end [COUNT count]
key
:队列名start
:开始值,-
表示最小值end
:结束值,+
表示最大值count
:数量
XREVRANGE
:反向获取消息列表,ID 从大到小
语法格式:XREVRANGE key end start [COUNT count]
key
:队列名end
:结束值, + 表示最大值start
:开始值, - 表示最小值count
:数量
XREAD
:以阻塞或非阻塞方式获取消息列表
语法格式:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
count
:数量milliseconds
:可选,阻塞毫秒数,没有设置就是非阻塞模式key
:队列名id
:消息 ID
消费者组相关命令:
XGROUP CREATE
:创建消费者组
语法格式:XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
key
:队列名称,如果不存在就创建groupname
:组名。$
: 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。- 从头开始消费:
XGROUP CREATE mystream consumer-group-name 0-0
- 从尾部开始消费:
XGROUP CREATE mystream consumer-group-name $
XREADGROUP GROUP
:读取消费者组中的消息
语法格式:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
group
:消费组名consumer
:消费者名count
: 读取数量milliseconds
: 阻塞毫秒数key
: 队列名ID
: 消息 ID
XACK
:将消息标记为"已处理"XGROUP SETID
:为消费者组设置新的最后递送消息IDXGROUP DELCONSUMER
:删除消费者XGROUP DESTROY
:删除消费者组XPENDING
:显示待处理消息的相关信息XCLAIM
:转移消息的归属权XINFO
:查看流和消费者组的相关信息;XINFO GROUPS
:打印消费者组的信息;XINFO STREAM
:打印流信息
1.11.3 内部编码
stream
底层的数据结构是radix tree
:Radix Tree
(基数树) 事实上就是几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix
树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。
如下是一个保存了7个单词的Radix Tree:
127.0.0.1:6379> xadd mystream * key1 128
"1576480551233-0"
127.0.0.1:6379> object encoding mystream
"unknown"
mystream 总共由 3 部分构成:
- 第一部分是
robj
, 每个redis
对象实例都会有一个最基本的结构来存储它实际的类型, 编码和对应的结构的位置 - 第二部分是一个
rax
, 用作存储 stream ID - 第三部分是
listpack
,rax
下的每一个 key 节点都会把对应的 keys 和 values 的值存在这个 listpack 结构中