消息队列
博主前面讲过各个消息队列的基础入门,那么处于成本考虑,Redis中也提供了三种不同的方式来实现消息队列:
- List结构:基于list结构模型模拟消息队列
- PubSub:基本的点对点消息模型
- Stream:比较完善的消息队列模型
基于list结构模型模拟消息队列
Redis的list模型是一个双向链表,很容易模拟出队列效果
我们可以通过LPUSH
结合RPOP
,或者RPUSH
结合LPOP
来实现
但是队列中没有消息的话会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息
因此这里应该使用BRPOP
或者BLPOP
来实现阻塞效果
优点:
- 利用Redis存储,不限于JVM内存上限
- 基于Redis的持久化机制,数据安全性有保证
- 可以满足消息有序性
缺点:
- 无法避免消息丢失
- 只支持单消费者(同一条消息无法被多个消费者消费)
基于PubSub的消息队列
PubSub(发布订阅),消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有的订阅者都能够收到相关信息
SUBSCRIBE channel [channel]
:订阅一个或多个频道
PUBLISH channel msg
:向一个频道发送消息
PSUBSCRIBE pattern[pattern]
:订阅与pattern格式匹配的所有频道
优点:
采用发布订阅模型,支持多生产,多消费
缺点:
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积有上限,超出时数据丢失
基于stream的消息队列
Stream是Redis 5.0 引入的一种新数据模型,可以实现一个功能非常完善的消息队列
XADD
:发送消息的命令
XREAD
:读取消息的命令
读取最新消息的命令:
XREAD COUNT 1 BLOCK 1000 STREAMS users $
在java代码中我们可以持续监听队列中最新的消息
while(true)
{
Object msg = redis.execute("XREAD COUNT 1 BLOCK 1000 STREAMS users $");
if(msg==null){
continue;
}
//处理消息
handleMessage(msg);
}
但是这种方式存在弊端,当我们指定ID为$的时候,如果我们在执行handleMessage操作时一下传过来四五条消息,这时就会出现漏读的情况,针对这个我们采用一下方案
基于Stream的消息队列-消费者组
将多个消费者划分到一个组当中,监听同一个队列
特点:
- 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
- 消息提示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕是消息者宕机重启,还会从标示之后读取消息,确保每一个消息都会被消费
消费者组会将所有信息已经消费但未确认的消息放在pending-list中
创建消费者组:XGROUP CREATE key groupName ID [MKSTREAM]
key
:队列名称
groupName
消费者组名称
ID
起始ID标示,$代表队列的最后一个消息,0则代表队列中的第一个消息
MKSTREAM
队列不存在的时候自动创建队列
其他常见命令
从消费者组读取消息:
group
:消费者组名称
consumer
:消费者名称,如果消费者不存在,会自动创建一个消费者
count
:本次查询的最大数量
NOACK
:无需手动ACK,获取消息后自动确认
STRWAMS key
:指定队列名称
ID
: >
:从下一个未消费的消息开始
其它:根据指定的id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
XACK 队列名 消费者组名 ID名称
:确认消息以完成
如何查看pending中的消息:
XPENDING 队列名 组名 - + 10
:标示从开头到结尾读10个
基于Stream消费者监听消息的基本思路:
while(true){
// 尝试监听队列,使用阻塞模式,最长等待2000毫秒
Object msg= redis.call("XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >");
if(msg == null){
continue;
}
try{
//处理消息,完成后一定要ACK
handleMessage(msg);
}catch(Exception e){
while(true){
Object msg=redis.call("XREADGROUP GROUP g1 c1 COUNT 1 STREAM s1 0");
if(msg==null){
break;
}
try{
//说明有异常消息,再次处理
handleMessage(msg);
}catch(Exception e){
//再次出现异常,记录日志,继续循环
continue;
}
}
}
}
采用消费者组的特点:
- 消息可回溯
- 可以多消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有消息漏读的风险
- 有消息确认机制,保证消息至少被消费一次