目录
Redis,是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存、消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
Redis同时具备高性能和高并发两种特性。
如果用户第一次从MySQL中取某些数据,这个过程会比较缓慢,因为是从磁盘中读取的;而如果用户访问的数据缓存在Redis中,则下次访问会直接从直接会从缓存中读取数据,操作Redis缓存就是直接操作内存,因此速度较快。
Redis的QPS(每秒钟处理请求速度)是MySQL的10倍,Redis单机的QPS能轻松突破10w,而MySQL的单击QPS很难突破1w。所以直接访问Redis的承受能力大于MySQL的,因此可以考虑将MySQL中部分数据转移到Redis中,这样用户的一部分请求会直接到Redis而不会经过MySQL。
Redis提供了丰富的数据结构,Redis的十大数据类型有:String(字符串)、List(集合)、Hash(哈希)、Set(无序集合)、Zset(有序集合)、GEO(地理空间)、HyperLogLog(基数统计)、BitMap(位图)、Stream(流)
String是Redis最基本的数据类型,一个key对应一个value。
String类型底层数据结构主要实现是int和SDS(简单动态字符串)
在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。
SDS 和我们认识的 C 字符串不太一样,SDS相比于C的原生字符串:
1)SDS不仅可以保存文本数据,还可以保存二进制数据。SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。
2)SDS获取字符串长度的时间复杂度为O(1)。SDS是通过len属性纪律字符串长度。C语言并不记录自身长度。
3)Redis的SDS API是安全的,拼接字符串不会造成缓冲区溢出。SDS在拼接字符串之前会检查SDS空间是否满足要求,如果空间不够会自动扩容,不会导致缓冲区溢出的问题。

字符串对象内部编码有三种:int、raw和embstr。
当一个字符串对象保存的是整数值,并且这个整数值可以用long类型转换,那么字符串对象会将整数值保存在字符串对象结构的ptr属性中,并将字符串对象设置为int类型。
只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。
embstr 与 raw 类型底层的数据结构其实都是 SDS。
两者区别如下图:(Redis7版)

- # 设置 key-value 类型的值
- > SET name lin
- OK
- # 根据 key 获得对应的 value
- > GET name
- "lin"
- # 判断某个 key 是否存在
- > EXISTS name
- (integer) 1
- # 返回 key 所储存的字符串值的长度
- > STRLEN name
- (integer) 3
- # 删除某个 key 对应的值
- > DEL name
- (integer) 1
- # 批量设置 key-value 类型的值
- > MSET key1 value1 key2 value2
- OK
- # 批量获取多个 key 对应的 value
- > MGET key1 key2
- 1) "value1"
- 2) "value2"
- # 设置 key-value 类型的值
- > SET number 0
- OK
- # 将 key 中储存的数字值增一
- > INCR number
- (integer) 1
- # 将key中存储的数字值加 10
- > INCRBY number 10
- (integer) 11
- # 将 key 中储存的数字值减一
- > DECR number
- (integer) 10
- # 将key中存储的数字值键 10
- > DECRBY number 10
- (integer) 0
使用String来缓存对象有两种方式:
1.直接缓存整个对象的JSON

2.采用将key进行分离为user:ID属性,采用MSET存储,用MGET获取各属性值

因为Redis处理命令是单线程,所以执行命令过程是原子的,所以可以用于一些计数场景:计算机访问次数,点赞数、转发数、收藏数、库存数等等
如:计算文章阅读量

set命令有个nx参数,可以实现【key不存在时才插入】,可以用它来实现分布式锁
1.如果key存在,则显示插入成功,可以用来表示加锁成功
2.如果key不存在,则显示插入失败,可以用来表示加锁失败
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:
set lock_key unique_value nx px 10000
lock_key就是key键;
unique_value就是客户端生成的唯一标识;
nx代表只在lock_key的过期时间不存在时,才能对lock_key进行设置操作;
px 10000表示设置lock_key的过期时间为10秒,这是为了避免客户端异常而无法释放锁
解锁过程就是将lock_key删掉,但要确保执行删除的客户端是原本加锁的客户端。所以在删除时需要先进行判断。可以使用lua脚本将多个命令作为一个原子操作执行。
- // 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
- if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("del",KEYS[1])
- else
- return 0
- end
通常在开发后台管理系统时,会使用Session来保存用户的登录状态,这些session信息会被保存到服务器端,但这只适用于单系统,如果是分布式系统此模式将不再适用。
例如如果将用户1的session信息保存到服务器1中,第二次访问时用户1被分配到服务器2,服务器2并没有用户的session,因此可能会出现重复登录问题,问题在于分布式系统每次会把请求分配到不同服务器上。
因此可以借助Redis对这些session信息进行统一的存储和管理,这样无论请求发送到哪台服务器上,都可以去访问Redis获取相同的session信息,这样就解决了分布式session系统下session信息不共享问题。

List列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向list列表添加元素
Redis6版本前:list用quicklist来存储,quicklist存储了一个双向链表,每个结点都是一个ziplist。
quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

Redis7版本:使用listpack紧凑列表,用来代替ziplist
list用quicklist来存储,quicklist存储了一个双向链表,每一个节点都是一个listpack
- # 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
- LPUSH key value [value ...]
- # 将一个或多个值value插入到key列表的表尾(最右边)
- RPUSH key value [value ...]
- # 移除并返回key列表的头元素
- LPOP key
- # 移除并返回key列表的尾元素
- RPOP key
-
- # 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
- LRANGE key start stop
-
- # 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
- BLPOP key [key ...] timeout
- # 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
- BRPOP key [key ...] timeout
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性
1.消息保序
List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。
List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。

在消费者读取数据时,有一个潜在的性能风险点。
在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用 RPOP 命令(比如使用一个while(1)循环)。如果有新消息写入,RPOP命令就会返回结果,否则,RPOP命令返回空值,再继续循环。
所以,即使没有新消息写入List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。
为了解决这个问题,Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用RPOP命令相比,这种方式能节省CPU开销。

2.处理重复的消息
消费者要实现重复消息的判断,需满足两方面要求
1.每条消息都有一个全局ID
2.消费者要记录已经处理过的消息的ID,收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。
3.保证消息可靠
当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。
为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。
list作为消息队列的缺陷:list不支持多个消费之读取同一条消息,当一个消费者读取一条消息后,这条消息就从list中删除了无法被其他消费之再次读取。
Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。
.哈希对象保存的键字段数量小于512个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于64字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表(ziplist)作为 Hash 类型的底层数据结构;否则会采用哈希表(Hashtable)作为 Hash 类型的 底层数据结构。

哈希对象保存的键字段个数小于512个(默认值,可由hash-max-listpack-entries 配置),所有值小于64字节(默认值,可由hash-max-listpack-value 配置)的话,Redis 会使用紧凑列表(listpack)作为 Hash 类型的底层数据结构;否则会采用哈希表(Hashtable)作为 Hash 类型的 底层数据结构。

压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。


和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项
不再像ziplist列表项那样保存其前一个列表项的长度。


- # 存储一个哈希表key的键值
- HSET key field value
- # 获取哈希表key对应的field键值
- HGET key field
-
- # 在一个哈希表key中存储多个键值对
- HMSET key field value [field value...]
- # 批量获取哈希表key中多个field键值
- HMGET key field [field ...]
- # 删除哈希表key中的field键值
- HDEL key field [field ...]
-
- # 返回哈希表key中field的数量
- HLEN key
- # 返回哈希表key中所有的键值
- HGETALL key
-
- # 为哈希表key中field键的值加上增量n
- HINCRBY key field n
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
我们以用户信息为例,它在关系型数据库中的结构是这样的:

我们可以使用如下命令,将用户对象的信息存储到 Hash 类型:
- # 存储一个哈希表uid:1的键值
- > HMSET uid:1 name Tom age 15
- 2
- # 存储一个哈希表uid:2的键值
- > HMSET uid:2 name Jerry age 13
- 2
- # 获取哈希表用户id为1中所有的键值
- > HGETALL uid:1
- 1) "name"
- 2) "Tom"
- 3) "age"
- 4) "15"
以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素,如下图所示。

涉及的命令如下:
HSET cart:{用户id} {商品id} 1HINCRBY cart:{用户id} {商品id} 1HLEN cart:{用户id}HDEL cart:{用户id} {商品id}HGETALL cart:{用户id}当前仅仅是将商品ID存储到了Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
Redis用intset或hashtable存储set。如果元素都是整数类型且元素个数小于 512 (默认值,set-maxintset-entries配置)个,就用intset存储。
如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。

- # 往集合key中存入元素,元素存在则忽略,若key不存在则新建
- SADD key member [member ...]
- # 从集合key中删除元素
- SREM key member [member ...]
- # 获取集合key中所有元素
- SMEMBERS key
- # 获取集合key中的元素个数
- SCARD key
-
- # 判断member元素是否存在于集合key中
- SISMEMBER key member
-
- # 从集合key中随机选出count个元素,元素不从key中删除
- SRANDMEMBER key [count]
- # 从集合key中随机选出count个元素,元素从key中删除
- SPOP key [count]
- # 交集运算
- SINTER key [key ...]
- # 将交集结果存入新集合destination中
- SINTERSTORE destination key [key ...]
-
- # 并集运算
- SUNION key [key ...]
- # 将并集结果存入新集合destination中
- SUNIONSTORE destination key [key ...]
-
- # 差集运算
- SDIFF key [key ...]
- # 将差集结果存入新集合destination中
- SDIFFSTORE destination key [key ...]
Set类型可以保证一个用户只能点一次赞,这里举例子一个场景,key 是文章id,value 是用户id。
uid:1 、uid:2、uid:3 三个用户分别对 article:1 文章点赞了。
- # uid:1 用户对文章 article:1 点赞
- > SADD article:1 uid:1
- (integer) 1
- # uid:2 用户对文章 article:1 点赞
- > SADD article:1 uid:2
- (integer) 1
- # uid:3 用户对文章 article:1 点赞
- > SADD article:1 uid:3
- (integer) 1
Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。
key 可以是用户id,value 则是已关注的公众号的id。
uid:1 用户关注公众号 id 为 5、6、7、8、9,uid:2 用户关注公众号 id 为 7、8、9、10、11。
- # uid:1 用户关注公众号 id 为 5、6、7、8、9
- > SADD uid:1 5 6 7 8 9
- (integer) 5
- # uid:2 用户关注公众号 id 为 7、8、9、10、11
- > SADD uid:2 7 8 9 10 11
- (integer) 5
uid:1 和 uid:2 共同关注的公众号:
- # 获取共同关注
- > SINTER uid:1 uid:2
- 1) "7"
- 2) "8"
- 3) "9"
采用差集
给 uid:2 推荐 uid:1 关注的公众号:
- > SDIFF uid:1 uid:2
- 1) "5"
- 2) "6"


Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。
有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现。否则会使用ziplist作为有序集合的底层实现。

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。
这样查找效率就会很低,时间复杂度会很高O(N)

优化:空间换时间
加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。
跳表是什么?跳表是可以实现二分查找的有序链表。
跳表是一种以空间换时间的结构,由于链表无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表。
由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多
跳表查询的时间复杂度分析,如果链表里有N个结点,会有多少级索引呢?
按照我们前面讲的,两两取首。每两个结点会抽出一个结点作为上一级索引的结点,以此估算:
第一级索引的结点个数大约就是n/2,
第二级索引的结点个数大约就是n/4,
第三级索引的结点个数大约就是n/8,依次类推......
也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)

第一步:首先原始链表长度为n
第二步:两两取首,每层索引的结点数:n/2, n/4, n/8 ... , 8, 4, 2 每上升一级就减少一半,直到剩下2个结点,以此类推;如果我们把每层索引的结点数写出来,就是一个等比数列。

这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是O(n) 。也就是说,如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。
第三步:思考三三取首,每层索引的结点数:n/3, n/9, n/27 ... , 9, 3, 1 以此类推;
第一级索引需要大约n/3个结点,第二级索引需要大约n/9个结点。每往上一级,索引结点个数都除以3。为了方便计算,我们假设最高一级的索
引结点个数是1。我们把每级索引的结点个数都写下来,也是一个等比数列

通过等比数列求和公式,总的索引结点大约就是n/3+n/9+n/27+…+9+3+1=n/2。尽管空间复杂度还是O(n) ,但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。所以空间复杂度是O(n);
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
维护成本相对较高,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1) ,但是新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n)
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。


- # 往有序集合key中加入带分值元素
- ZADD key score member [[score member]...]
- # 往有序集合key中删除元素
- ZREM key member [member...]
- # 返回有序集合key中元素member的分值
- ZSCORE key member
- # 返回有序集合key中元素个数
- ZCARD key
-
- # 为有序集合key中元素member的分值加上increment
- ZINCRBY key increment member
-
- # 正序获取有序集合key从start下标到stop下标的元素
- ZRANGE key start stop [WITHSCORES]
- # 倒序获取有序集合key从start下标到stop下标的元素
- ZREVRANGE key start stop [WITHSCORES]
-
- # 返回有序集合中指定分数区间内的成员,分数由低到高排序。
- ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
-
- # 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
- ZRANGEBYLEX key min max [LIMIT offset count]
- # 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
- ZREVRANGEBYLEX key max min [LIMIT offset count]
- # 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
- ZUNIONSTORE destkey numberkeys key [key...]
- # 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
- ZINTERSTORE destkey numberkeys key [key...]
有序集合比较典型的使用场景就是排行榜。例如学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。
思路:定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量。

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
- # 设置值,其中value只能是 0 和 1
- SETBIT key offset value
-
- # 获取值
- GETBIT key offset
-
- # 获取指定范围内值为 1 的个数
- # start 和 end 以字节为单位
- BITCOUNT key start end
- # BitMap间的运算
- # operations 位移操作符,枚举值
- AND 与运算 &
- OR 或运算 |
- XOR 异或 ^
- NOT 取反 ~
- # result 计算的结果,会存储在该key中
- # key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
- # 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
- BITOP [operations] [result] [key1] [keyn…]
-
- # 返回指定key中第一次出现指定value(0/1)的位置
- BITPOS [key] [value]
Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。
签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。
Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
- # 添加指定元素到 HyperLogLog 中
- PFADD key element [element ...]
-
- # 返回给定 HyperLogLog 的基数估算值。
- PFCOUNT key [key ...]
-
- # 将多个 HyperLogLog 合并为一个 HyperLogLog
- PFMERGE destkey sourcekey [sourcekey ...]
Redis HyperLogLog 优势在于只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
所以,非常适合统计百万级以上的网页 UV 的场景。
unique visitor 独立访客,一般理解为客户端IP-------需要考虑去重
在统计 UV 时,你可以用 PFADD 命令(用于向 HyperLogLog 中添加新元素)把访问页面的每个用户都添加到 HyperLogLog 中。
PFADD page1:uv user1 user2 user3 user4 user5
接下来,就可以用 PFCOUNT 命令直接获得 page1 的 UV 值了,这个命令的作用就是返回 HyperLogLog 的统计结果。
PFCOUNT page1:uv
Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。
- # 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
- GEOADD key longitude latitude member [longitude latitude member ...]
-
- # 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
- GEOPOS key member [member ...]
-
- # 返回两个给定位置之间的距离。
- GEODIST key member1 member2 [m|km|ft|mi]
-
- # 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
- GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
这里以滴滴叫车的场景为例,介绍下具体如何使用 GEO 命令:GEOADD 和 GEORADIUS 这两个命令。
假设车辆 ID 是 33,经纬度位置是(116.034579,39.030452),我们可以用一个 GEO 集合保存所有车辆的经纬度,集合 key 是 cars:locations。
执行下面的这个命令,就可以把 ID 号为 33 的车辆的当前经纬度位置存入 GEO 集合中:
GEOADD cars:locations 116.034579 39.030452 33
当用户想要寻找自己附近的网约车时,LBS 应用就可以使用 GEORADIUS 命令。
例如,LBS 应用执行下面的命令时,Redis 会根据输入的用户的经纬度信息(116.054579,39.030452 ),查找以这个经纬度为中心的 5 公里内的车辆信息,并返回给 LBS 应用。
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:



基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

常见命令
- XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
- XLEN :查询消息长度;
- XREAD:用于读取消息,可以按 ID 读取数据;
- XDEL : 根据消息 ID 删除消息;
- DEL :删除整个 Stream;
- XRANGE :读取区间消息
- XREADGROUP:按消费组形式读取消息;
- XPENDING 和 XACK:
- XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
- XACK 命令用于向消息队列确认消息处理已完成;

生产者通过 XADD 命令插入一条消息:
- # * 表示让 Redis 为插入的数据自动生成一个全局唯一的 ID
- # 往名称为 mymq 的消息队列中插入一条消息,消息的键是 name,值是 xiaolin
- > XADD mymq * name xiaolin
- "1654254953808-0"
插入成功后会返回全局唯一的 ID:"1654254953808-0"。消息的全局唯一 ID 由两部分组成:
消费者通过 XREAD 命令从消息队列中读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取(注意是输入消息 ID 的下一条信息开始读取,不是查询输入ID的消息)。
- # 从 ID 号为 1654254953807-0 的消息开始,读取后续的所有消息(示例中一共 1 条)。
- > XREAD STREAMS mymq 1654254953807-0
- 1) 1) "mymq"
- 2) 1) 1) "1654254953808-0"
- 2) 1) "name"
- 2) "xiaolin"
文章参考:
1.小林coding:Redis 常见数据类型和应用场景 | 小林coding (xiaolincoding.com)
2. 尚硅谷Redis7实战:尚硅谷Redis零基础到进阶,最强redis7教程,阳哥亲自带练(附redis面试题)_哔哩哔哩_bilibili