Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,对列,集合结构直接存取,基于内存,可持久化。
什么是Redis
Redis是一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型,如:String、list、set、zset、hash。
Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速对列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
为什么要使用Redis
读写性能优异
Redis能读的速度是110000次/s,写的速度是81000次/s。
数据类型丰富
Redis支持二进制案例的 String,Lists,Hashes,Sets及 Ordered Sets 数据类型操作。
原子性
Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
丰富的特性
Redis支持 publish/subscribe,通知 key 过期等特性。
持久化
Redis支持RDB,AOF等持久化方式
发布订阅
Redis支持发布/订阅模式
分布式
Redis Cluster
Redis的使用场景
热点数据的缓存
缓存是Redis最常见的应用场景,之所以这么使用,主要是因为Redis读写性能优异。而且,Redis内部是支持事务的,在使用时候能有效保证数据一致性。
作为缓存使用时,一般有两种方式保存数据:
方案一:实施起来简单,但是有两个需要注意的地方:
方案二:数据实时性强,但是开发时不便于统一处理。
当然,两种方式根据实际情况来适用。如:方案一适用于对于数据实时性要求不是特别高的场景。方案二适用于字典表、数据量不大的数据存储。
限时业务的运用
redis中可以适用expire命令设置一个键的生存空间,到时间后Redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
计数器相关问题
redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
分布式锁
这个主要利用redis的setnx命令进行,setnx:“set if not exists”就是如果不存在则成功设置缓存同时返回1,否则返回0,这个特性在很多后台中都有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。
延时操作
比如在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设置无效,同时还原库存。由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期,我们在后台实现一个监听器,监听key的失效,监听到key失效时将后续逻辑加上。
当然也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。
排行榜相关问题
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
比如点赞排行榜,做一个SortedSet,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score,然后针对每个用户做一个hash,通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息。
点赞、好友等相互关系的存储
Redis利用集合的一些命令,比如求交集、并集、差集等。
在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
简单队列
由于Redis有list push和list pop这样的命令,所以能很方便的执行队列操作。
Redis数据类型
Redis所有的key(键)都是字符串。我们在谈基础数据结构时,讨论的是存储值得数据类型,主要包括常见得5种数据类型,分别是:String、List、Set、Zset、Hash
Redis数据结构简介
对redis来说,所有得key(键)都是字符串。我们在谈基础数据结构时,讨论得是存储值得数据类型,主要包括常见得5种数据类型,分别是:String、List、Set、Zset、Hash。
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
String字符串 | 可以是字符串、整数或浮点数 | 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作; |
List列表 | 一个链表,链表上的每个节点都包含一个字符串 | 对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素; |
Set集合 | 包含字符串的无序集合 | 字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等 |
Hash散列 | 包含键值对的无序散列表 | 包含方法有添加、获取、删除单个元素 |
Zset有序集合 | 和散列一样,用于存储键值对 | 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素 |
String字符串
String是redis种最基本得数据类型,一个key对应一个value。
String类型是二进制安全的,意思是redis的string可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
图例
下图是一个String类型的实例,其中键为hello,值为world
命令使用
命令 | 简述 | 使用 |
---|---|---|
GET | 获取存储在给定键中的值 | GET name |
SET | 设置存储在给定键中的值 | SET name value |
DEL | 删除存储在给定键中的值 | DEL name |
INCR | 将键存储的值加1 | INCR key |
DECR | 将键存储的值减1 | DECR key |
INCRBY | 将键存储的值加上整数 | INCRBY key amount |
DECRBY | 将键存储的值减去整数 | DECRBY key amount |
命令执行
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> set counter 2
OK
127.0.0.1:6379> get counter
"2"
127.0.0.1:6379> incr counter
(integer) 3
127.0.0.1:6379> get counter
"3"
127.0.0.1:6379> incrby counter 100
(integer) 103
127.0.0.1:6379> get counter
"103"
127.0.0.1:6379> decr counter
(integer) 102
127.0.0.1:6379> get counter
"102"
实战场景
缓存:经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql作为持久化层,降低mysql的读写压力。
计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
session:常见方案spring session + redis实现session共享。
List列表
Redis中的LIst其实就是链表(Redis用双端链表实现List)。
使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务放在List中,然后工作线程再用POP操作将任务取出进行执行。
图例
命令使用
命令 | 简述 | 使用 |
---|---|---|
RPUSH | 将给定值推入到列表右端 | RPUSH key value |
LPUSH | 将给定值推入到列表左端 | LPUSH key value |
RPOP | 从列表的右端弹出一个值,并返回被弹出的值 | RPOP key |
LPOP | 从列表的左端弹出一个值,并返回被弹出的值 | LPOP key |
LRANGE | 获取列表在给定范围上的所有值 | LRANGE key 0 -1 |
LINDEX | 通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 | LINDEX key index |
使用列表的技巧
- lpush+lpop = Stack(栈)
- lpush+rpop = Queue(队列)
- lpush+ltrim = Capped Collection(有限集合)
- lpush+brpop = Message Queue(消息队列)
命令执行
127.0.0.1:6379> lpush mylist 1 2 ll ls mem
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "mem"
2) "ls"
3) "ll"
4) "2"
5) "1"
127.0.0.1:6379> lindex mylist -1
"1"
127.0.0.1:6379> lindex mylist 10 # index不在 mylist 的区间范围内
(nil)
实战场景
- 微博TimeLine:有人发布微博,用Ipush加入时间轴,展示新的列表信息。
- 消息队列。
Set集合
Redis的 Set 是 String类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis中集合是通过哈希表实现的,所以添加、删除、查找的复杂度都是O(1)。
命令使用
命令 | 简述 | 使用 |
---|---|---|
SADD | 向集合添加一个或多个成员 | SADD key value |
SCARD | 获取集合的成员数 | SCARD key |
SMEMBERS | 返回集合中的所有成员 | SMEMBERS key member |
SISMEMBER | 判断 member 元素是否是集合 key 的成员 | SISMEMBER key member |
命令执行
127.0.0.1:6379> sadd myset hao hao1 xiaohao hao
(integer) 3
127.0.0.1:6379> smembers myset
1) "xiaohao"
2) "hao1"
3) "hao"
127.0.0.1:6379> sismember myset hao
(integer) 1
实战场景
标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的的事或者关注的人。
点赞,或点踩,收藏等,可以放在set中实现。
Hash散列
Redis hash 是一个string类型的field(字段)和 value(值)的映射表,hash特别适合用于存储对象。
图例
命令使用
命令 | 简述 | 使用 |
---|---|---|
HSET | 添加键值对 | HSET hash-key sub-key1 value1 |
HGET | 获取指定散列键的值 | HGET hash-key key1 |
HGETALL | 获取散列中包含的所有键值对 | HGETALL hash-key |
HDEL | 如果给定键存在于散列中,那么就移除这个键 | HDEL hash-key sub-key1 |
命令执行
127.0.0.1:6379> hset user name1 hao
(integer) 1
127.0.0.1:6379> hset user email1 hao@163.com
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163.com"
127.0.0.1:6379> hget user user
(nil)
127.0.0.1:6379> hget user name1
"hao"
127.0.0.1:6379> hset user name2 xiaohao
(integer) 1
127.0.0.1:6379> hset user email2 xiaohao@163.com
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163.com"
5) "name2"
6) "xiaohao"
7) "email2"
8) "xiaohao@163.com"
实战场景
缓存:能直观,相比string更节省空间维护缓存信息,如用户信息,视频信息等。
Zset有序集合
Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序结合的成员是唯一的,但分数(score)却可以重复。有序集合是通过两种数据结构实现:
1.压缩列表(ziplist):ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关。
2.跳跃表(zSkiplist):跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。
图例
命令使用
命令 | 简述 | 使用 |
---|---|---|
ZADD | 将一个带有给定分值的成员添加到有序集合里面 | ZADD zset-key 178 member1 |
ZRANGE | 根据元素在有序集合中所处的位置,从有序集合中获取多个元素 | ZRANGE zset-key 0-1 withccores |
ZREM | 如果给定元素成员存在于有序集合中,那么就移除这个元素 | ZREM zset-key member1 |
命令执行
127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao
(integer) 2
127.0.0.1:6379> ZRANGE myscoreset 0 -1
1) "xiaohao"
2) "hao"
127.0.0.1:6379> ZSCORE myscoreset hao
"100"
实战场景
排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
3种特殊类型介绍
Redis除了5种基础数据类型,还有三种特殊的数据类型,分别是HyperLogLogs(基数统计),Bitmaps(位图)和 geospatial(地理位置)。
HyperLogLogs(基数统计)
Redis 2.8.9版本更新了 Hyperloglog 数据结构!
什么是基数?
举个例子,A={1,2,3,4,5},B={3,5,6,7,9};那么基数(不重复的元素)=1,2,4,6,7,9;(允许容错,即可以接受一定误差)
HyperLogLogs基数统计用来解决什么问题?
这个结构可以非常省内存的去统计各种计数,比如注册IP数、每日访问IP数、页面实时UV、在线用户数,共同好友数等。
它的优势体现在哪?
一个大型的网站,每天IP比如有100万,粗算一个IP消耗15字节,那么100万个IP就是15M。而HyperLogLog 在Redis 中每个键占用的内容都是 12k,理论存储近似接近2^64 个值,不论存储的内容是什么,它一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有0.81%标准错误的近似值(对于可以接受一定容错的业务场景,比如IP数统计,UV等,是可以忽略不计的)。
相关命令
127.0.0.1:6379> pfadd key1 a b c d e f g h i # 创建第一组元素
(integer) 1
127.0.0.1:6379> pfcount key1 # 统计元素的基数数量
(integer) 9
127.0.0.1:6379> pfadd key2 c j k l m e g a # 创建第二组元素
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 8
127.0.0.1:6379> pfmerge key3 key1 key2 # 合并两组:key1 key2 -> key3 并集
OK
127.0.0.1:6379> pfcount key3
(integer) 13
Bitmap(位存储)
Bitmap 即位图数据结构,都是操作二进制来进行记录,只有 0 和 1 两个状态。
用来解决什么问题?
比如:统计用户信息,活跃,不活跃!登录,未登录!打卡,不打卡! 两个状态的,都可以使用Bitmaps!
如果存储一年的打卡状态需要多少内存呢? 365天= 365bit 1字节=8bit 大约是46个字节左右!
相关命令使用
使用 bitmap 来记录周一到周日的打卡! 周一:1,周二:0,周三:0,周四:1......
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
查看某一天是否有打卡!
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 5
(integer) 0
统计操作,统计打卡的天数!
127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3
geopatial(地理位置)
Redis的 Geo 在Redis 3.2 版本推出。这个功能可以推算地理位置的信息:两地之间的距离,方圆几里的人
geoadd
添加地理位置
127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang
(integer) 3
127.0.0.1:6379> geoadd china:city 144.05 22.52 shengzhen 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 3
规则
两级无法直接添加,一般会下载城市数据(这个网址可以查询 GEO:http://www.jsons.cn/lngcode)!
- 有效的经度从 -180度到180度。
- 有效的纬度从 -85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
geopos
获取指定的成员的经度和纬度
127.0.0.1:6379> geopos china:city taiyuan manjing
1) 1) "112.54999905824661255"1) "37.86000073876942196"
2) 1) "118.75999957323074341"1) "32.03999960287850968"
获取当前定位,一定是一个坐标值!
geodist
如果不存在,返回空
单位如下
- m
- km
- mi 英里
- ft 英尺
127.0.0.1:6379> geodist china:city taiyuan shenyang m
"1026439.1070"
127.0.0.1:6379> geodist china:city taiyuan shenyang km
"1026.4391"
georadius
附近的人 ==> 获得所有附近的人的地址,定位,通过半径来查询
获得指定数量的人
127.0.0.1:6379> georadius china:city 110 30 1000 km 以 100,30 这个坐标为中心, 寻找半径为1000km的城市
1) "xian"
2) "hangzhou"
3) "manjing"
4) "taiyuan"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist
1) 1) "xian"2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord withdist count 2
1) 1) "xian"2) "483.8340"3) 1) "108.96000176668167114"2) "34.25999964418929977"
2) 1) "manjing"2) "864.9816"3) 1) "118.75999957323074341"2) "32.03999960287850968"
参数:key 经度 纬度 半径 单位 [显示结果的经度和纬度] [显示结果的距离] [显示结果的数量]
georadiusbymember
显示与指定成员一定半径范围内的其他成员
127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km
1) "manjing"
2) "taiyuan"
3) "xian"
127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km withcoord withdist count 2
1) 1) "taiyuan"2) "0.0000"3) 1) "112.54999905824661255"2) "37.86000073876942196"
2) 1) "xian"2) "514.2264"3) 1) "108.96000176668167114"2) "34.25999964418929977"
参数与 georadius 一样
geohash(较少使用)
该命令返回11个字符的hash字符串
127.0.0.1:6379> geohash china:city taiyuan shenyang
1) "ww8p3hhqmp0"
2) "wxrvb9qyxk0"
将二维的经纬度转换为一维的字符串,如果两个字符串越接近,则距离越近。
底层
geo底层的实现原理实际上就是Zset,我们可以通过Zset命令来操作geo
127.0.0.1:6379> type china:city
zset
查看全部元素 删除指定的元素
127.0.0.1:6379> zrange china:city 0 -1 withscores1) "xian"2) "4040115445396757"3) "hangzhou"4) "4054133997236782"5) "manjing"6) "4066006694128997"7) "taiyuan"8) "4068216047500484"9) "shenyang"
1) "4072519231994779"
2) "shengzhen"
3) "4154606886655324"
127.0.0.1:6379> zrem china:city manjing
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "xian"
2) "hangzhou"
3) "taiyuan"
4) "shenyang"
5) "shengzhen"
Stream介绍
Redis5.0中还增加了一个数据类型Stream,它借鉴了Kafka的设计,是一个新的强大的支持多播的可持久化的消息队列。
为什么会设计Stream
Redis5.0 中还增加了一个数据结构Stream,从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。
基于Redis的消息队列实现有很多种,例如:
PUB/SUB,订阅/发布模式
但是发布订阅模式是无法持久化的,如果出现网络断开、Redis宕机等,消息就会被丢弃;
基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现
支持了持久化,但是不支持多播,分组消费等。
为什么上面的结构无法满足广泛的MQ场景? 如果我们期望设计一种数据结构来实现消息队列,最重要的就是要理解一个消息队列需要考虑什么?初步比较容易想到
- 消息的生产
- 消息的消费
- 单播和多播(多对多)
- 阻塞和非阻塞读取
- 消息的有序性
- 消息的持久化
其他还需要考虑什么?
Redis考虑了哪些设计?
- 消息ID的序列化生成
- 消息遍历
- 消息的阻塞和非阻塞读取
- 消息的分组消费
- 未完成消费的处理
- 消息队列监控
这是我们需要理解Stream的点,结合上面的图,可以理解到Redis Stream也是一种超轻量MQ并没有完全实现消息队列所有设计要点,这决定着它适用的场景。
Stream的结构
每个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:确认字符)。如果客户没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。
此外还需要理解两点:
- 消息ID:消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的ID要大于前面的ID。
- 消息内容:消息内容就是键值对,形如hash结构的键值对。
增删改查
消息队列相关命令:
- XADD - 添加消息到末尾
- XTRIM - 对流进行修剪,限制长度
- XDEL - 删除消息
- XLEN - 获取流包含的元素数量,即消息长度
- XRANGE - 获取消息列表,会自动过滤已经删除的消息
- XREVRANGE - 反向获取消息列表,ID从大到小
- XREAD - 以阻塞或非阻塞方式获取消息列表
# *号表示服务器自动生成ID,后面顺序跟着一堆key/value
127.0.0.1:6379> xadd codehole * name laoqian age 30 # 名字叫laoqian,年龄30岁
1527849609889-0 # 生成的消息ID
127.0.0.1:6379> xadd codehole * name xiaoyu age 29
1527849629172-0
127.0.0.1:6379> xadd codehole * name xiaoqian age 1
1527849637634-0
127.0.0.1:6379> xlen codehole
(integer) 3
127.0.0.1:6379> xrange codehole - + # -表示最小值, +表示最大值
127.0.0.1:6379> xrange codehole - +
1) 1) 1527849609889-01) 1) "name"1) "laoqian"2) "age"3) "30"
2) 1) 1527849629172-01) 1) "name"1) "xiaoyu"2) "age"3) "29"
3) 1) 1527849637634-01) 1) "name"1) "xiaoqian"2) "age"3) "1"
127.0.0.1:6379> xrange codehole 1527849629172-0 + # 指定最小消息ID的列表
1) 1) 1527849629172-02) 1) "name"2) "xiaoyu"3) "age"4) "29"
2) 1) 1527849637634-02) 1) "name"2) "xiaoqian"3) "age"4) "1"
127.0.0.1:6379> xrange codehole - 1527849629172-0 # 指定最大消息ID的列表
1) 1) 1527849609889-02) 1) "name"2) "laoqian"3) "age"4) "30"
2) 1) 1527849629172-02) 1) "name"2) "xiaoyu"3) "age"4) "29"
127.0.0.1:6379> xdel codehole 1527849609889-0
(integer) 1
127.0.0.1:6379> xlen codehole # 长度不受影响
(integer) 3
127.0.0.1:6379> xrange codehole - + # 被删除的消息没了
1) 1) 1527849629172-02) 1) "name"2) "xiaoyu"3) "age"4) "29"
2) 1) 1527849637634-02) 1) "name"2) "xiaoqian"3) "age"4) "1"
127.0.0.1:6379> del codehole # 删除整个Stream
(integer) 1
独立消费
我们可以在不定义消费组的情况下进行Stream消息的独立消费,当Stream没有新消息时,甚至可以阻塞等待。 Redis设计了一个单独的消费指令xread,可以将Stream当成普通的消息队列(list)来使用。使用xread时,我们可以完全忽略消费组(Consumer Group)的存在,就好像Stream就是一个普通的列表。
# 从Stream头部读取两条消息
127.0.0.1:6379> xread count 2 streams codehole 0-0
1) 1) "codehole"2) 1) 1) 1527851486781-02) 1) "name"2) "laoqian"3) "age"4) "30"2) 1) 1527851493405-02) 1) "name"2) "yurui"3) "age"4) "29"
# 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息
127.0.0.1:6379> xread count 1 streams codehole $
(nil)
# 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来
127.0.0.1:6379> xread block 0 count 1 streams codehole $
# 我们从新打开一个窗口,在这个窗口往Stream里塞消息
127.0.0.1:6379> xadd codehole * name youming age 60
1527852774092-0
# 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容
# 而且还显示了一个等待时间,这里我们等待了93s
127.0.0.1:6379> xread block 0 count 1 streams codehole $
1) 1) "codehole"2) 1) 1) 1527852774092-02) 1) "name"2) "youming"3) "age"4) "60"
(93.11s)
客户端如果想要使用xread进行顺序消费,一定要记住当前消费到哪里了,也就是返回的消息ID。下次继续调用xread时,将上次返回的最后一个消息ID作为参数传递进去,就可以继续消费后续的消息。
block 0表示永远阻塞,直到消息到来,block 1000表示阻塞1s,如果1s内没有任何消息到来,就返回nil
127.0.0.1:6379> xread block 1000 count 1 streams codehole $
(nil)
(1.07s)
消费组消费
消费组消费图
相关命令:
- XGROUP CREATE - 创建消费者组
- XREADGROUP GROUP - 读取消费者组中的消息
- XACK - 将消息标记为"已处理"
- XGROUP SETID - 为消费者组设置新的最后递送消息ID
- XGROUP DELCONSUMER - 删除消费者
- XGROUP DESTROY - 删除消费者组
- XPENDING - 显示待处理消息的相关信息
- XCLAIM - 转移消息的归属权
- XINFO - 查看流和消费者组的相关信息;
- XINFO GROUPS - 打印消费者组的信息;
- XINFO STREAM - 打印流信息
创建消费组
Stream通过xgroup create指令创建消费组(Consumer Group),需要传递起始消息ID参数用来初始化last_delivered_id变量。
127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费
OK
# $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略
127.0.0.1:6379> xgroup create codehole cg2 $
OK
127.0.0.1:6379> xinfo stream codehole # 获取Stream信息1) length2) (integer) 3 # 共3个消息3) radix-tree-keys4) (integer) 15) radix-tree-nodes6) (integer) 27) groups8) (integer) 2 # 两个消费组9) first-entry # 第一个消息
10) 1) 1527851486781-02) 1) "name"2) "laoqian"3) "age"4) "30"
11) last-entry # 最后一个消息
12) 1) 1527851498956-02) 1) "name"2) "xiaoqian"3) "age"4) "1"
127.0.0.1:6379> xinfo groups codehole # 获取Stream的消费组信息
1) 1) name2) "cg1"3) consumers4) (integer) 0 # 该消费组还没有消费者5) pending6) (integer) 0 # 该消费组没有正在处理的消息
2) 1) name2) "cg2"3) consumers # 该消费组还没有消费者4) (integer) 05) pending6) (integer) 0 # 该消费组没有正在处理的消息
消费组消费
Stream提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从PEL中移除。
# >号表示从当前消费组的last_delivered_id后面开始读
# 每当消费者读取一条消息,last_delivered_id变量就会前进
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"2) 1) 1) 1527851486781-02) 1) "name"2) "laoqian"3) "age"4) "30"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"2) 1) 1) 1527851493405-02) 1) "name"2) "yurui"3) "age"4) "29"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole >
1) 1) "codehole"2) 1) 1) 1527851498956-02) 1) "name"2) "xiaoqian"3) "age"4) "1"2) 1) 1527852774092-02) 1) "name"2) "youming"3) "age"4) "60"
# 再继续读取,就没有新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
(nil)
# 那就阻塞等待吧
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
# 开启另一个窗口,往里塞消息
127.0.0.1:6379> xadd codehole * name lanying age 61
1527854062442-0
# 回到前一个窗口,发现阻塞解除,收到新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
1) 1) "codehole"2) 1) 1) 1527854062442-02) 1) "name"2) "lanying"3) "age"4) "61"
(36.54s)
127.0.0.1:6379> xinfo groups codehole # 观察消费组信息
1) 1) name2) "cg1"3) consumers4) (integer) 1 # 一个消费者5) pending6) (integer) 5 # 共5条正在处理的信息还有没有ack
2) 1) name2) "cg2"3) consumers4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg15) pending6) (integer) 0
# 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态
127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者
1) 1) name2) "c1"3) pending4) (integer) 5 # 共5条待处理消息5) idle6) (integer) 418715 # 空闲了多长时间ms没有读取消息了
# 接下来我们ack一条消息
127.0.0.1:6379> xack codehole cg1 1527851486781-0
(integer) 1
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name2) "c1"3) pending4) (integer) 4 # 变成了5条5) idle6) (integer) 668504
# 下面ack所有消息
127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0
(integer) 4
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name2) "c1"3) pending4) (integer) 0 # pel空了5) idle6) (integer) 745505
信息监控
Stream提供了XINFO来实现对服务器信息的监控,可以查询:
查看队列信息
127.0.0.1:6379> Xinfo stream mq1) "length"2) (integer) 73) "radix-tree-keys"4) (integer) 15) "radix-tree-nodes"6) (integer) 27) "groups"8) (integer) 19) "last-generated-id"
10) "1553585533795-9"
11) "first-entry"
12) 1) "1553585533795-3"2) 1) "msg"2) "4"
13) "last-entry"
14) 1) "1553585533795-9"2) 1) "msg"2) "10"
消费组信息
127.0.0.1:6379> Xinfo groups mq
1) 1) "name"2) "mqGroup"3) "consumers"4) (integer) 35) "pending"6) (integer) 37) "last-delivered-id"8) "1553585533795-4"
消费组成员信息
127.0.0.1:6379> XINFO CONSUMERS mq mqGroup
1) 1) "name"2) "consumerA"3) "pending"4) (integer) 15) "idle"6) (integer) 18949894
2) 1) "name"2) "consumerB"3) "pending"4) (integer) 15) "idle"6) (integer) 3092719
3) 1) "name"2) "consumerC"3) "pending"4) (integer) 15) "idle"6) (integer) 23683256