文章目录
- 1 Redis
- 1.1 概述
- 1.2 查看内部编码
- 1.3 String字符串
- 1.3.1 简介
- 1.3.2 应用常景
- 1.3.3 String内部编码
- 1.4 Hash散列
- 1.4.1 简介
- 1.4.2 应用常景
- 1.4.3 Hash内部编码
- 1.4.4 rehash和渐进式rehash操作
- 1.4.4.1 过程
- 1.4.4.2 rehash触发条件
- 1.4.5 跟JDK的HashMap的区别
- 1.5 List列表
- 1.5.1 简介
- 1.5.2 命令和应用
- 1.5.3 List内部编码
- 1.6 Set集合
- 1.6.1 简介
- 1.6.2 命令和应用
- 1.6.3 Set内部编码
- 1.7 ZSet有序集合
- 1.7.1 简介
- 1.7.2 命令和应用
- 1.7.3 ZSet内部编码
- 1.8 Bitmap位图
- 1.8.1 简介
- 1.8.2 应用常景
- 1.8.3 底层原理
- 1.8.4 命令
- 1.9 HyperLogLog基数统计
- 1.9.1 简介
- 1.9.2 命令和场景
- 1.9.3 内部编码和原理
- 1.10 GEO地理位置
- 1.10.1 简介
- 1.10.2 命令和场景
- 1.10.3 内部编码
- 1.11 Stream流
- 1.11.1 简介
- 1.11.2 命令
- 1.11.3 内部编码
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作为哈希的内部实现。
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-valueskiplist(跳跃表):当上述条件不满足时,Redis会采用skiplist作为内部编码。
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:添加指定元素到HyperLogLogPFCOUNT:返回给定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]
参数说明同GEORADIUSgeohash:返回一个或多个位置对象的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 [~] countkey:队列名称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 结构中




















