为什么要是用redis消息队列?
1.之前使用JDK的阻塞队列完成异步下单,阻塞队列使用的时JDK内存,为队列长度设置了上限,若出现高并发的情况,无数的数据存入JDK内存,可能会导致内存溢出,就引发了内存限制问题
2.因为数据都存在与内存中,若内存宕机,则所有内存都会受到影响,用户已经完成了抢单操作,但内存中没有相关的订单信息。还有当从队列中取出数据,要完成相应的下单任务,出现了严重事故,导致任务丢失,队列中没有记录,就引发了数据安全问题。
消息队列(Message Queue)
字面上就是存放消息的队列,包含三个角色,最直白的理解:就是菜鸟驿站,决出了耦合,提升了消息传递、工作的效率
那么就有一个疑问,消息队列和阻塞队列有什么不一样吗,都是起到临时存放的作用:
1.消息队列不受JVM内存的限制
2.消息队列不仅仅起到消息临时存储的作用,还未数据提供了安全性保障,提供持久化管理,不用担心出现重大事故时,数据丢失的问题。同时,在传递给消费者数据时,还会提供确认机制,确保消费者接收到消息,否则数据仍存在于队列中,直至至少一次的确定。
redis中的消息队列
list:模仿链表的阻塞队列
pubsub:发布订阅模式
stream: 更加完善
基于list结构模仿消息队列
基本结构:是一个双向链表,容易模拟出队列效果,链表的特点先进先出
队列时入口出口不再同一侧的,因此利用LPUSH和RPOP等来实现
注意的是,若队列中没有消息时,POP操作会返回null,而我们希望的是可以是阻塞式的,在有消息的时候再返回消息,因此可以使用B(block)RPOP和B(block)LPOP实现阻塞效果
创建两个redis连接,一个生产者,一个消费者
生产者:再l1中存入两个数据e1,e2
消费者:监听l1,先后拿到e1,e2,在输入BRPOP输出数据时,就处于阻塞状态直至有新的消息输入
总结
1.无法避免消息丢失:因为采取POP操作是直接移除remove消息
2.一对一:因为一个消费者拿走消息后,其他消费者就无法或者该消息
基于PubSub的消息队列
消费者订阅一或多个channel,生产者相对应channel发布消息后,所有订阅者都能收到消息,即一对多
相关语法
PubSub消息队列实现敏捷,虽然有多个消费者同时订阅一个生产者,但彼此订阅的队列不同。如,A订阅order.queue,B订阅一个order.*,那么生产者发送,publish order.q1 meg1,那就只有B能收到
总结
1.因为List队列本身就是数据结构,具有存储功能,所以可以做到数据持久化,而PubSub本身就只是一个发送消息模型,无法做到数据持久化
2.无法做到持久化,就无法避免数据的丢失
3.消息发送时需要生产者和消费者都同时在线,否则一方不再就无法传递消息,
4.安全问题较大
基于Stream的消息队列
stream是redis5.0新引入数据类型,注意是数据类型,所以本身就可以做到数据持久化,保障数据的安全,所以是功能非常完善的消息队列
发送消息命令;XADD,格式如下,发送成功后,返回的信息格式是”时间戳-递增数字“
读取消息之一:XREAD
可以发现,在不同的消费者上读取消息,消息并不会读一次就消失,证明了持久化管理
XREAD COUNT 1 BLOCK 10 STREAM s1 $:这代表了永久阻塞的阻塞模式,读取消息最大数量为1,s1队列的最新消息,可以看到下图进入了阻塞模式,等待新消息的出现
XADD s1 * k2 v2:表示创建名为s1的队列,冰箱其中发送消息内容为{k2=v2},并且使用redis自动生成id
这时,上面这个消费者在等待了14.53s之后收到了{k2=v2}的消息
Stream类型消息队列的XREAD命令特点
1.因为持久化管理,消息永久保存在队列中,所以消息可以回溯查询
2.可以设置BLOCK 0,表示永久阻塞进行阻塞读取,等待新消息的传递
3.因为$表示从最新消息开始,但只局限于最后一个的消息,如果一次发送了三条新消息,技能拿到最后一个消息,存在消息漏读的风险
基于Stream的消息队列-消费者组
消费者组:将多个消费者划分到一个组中,监听一个队列,有以下四个特点
1.消息分流:队列中的消息会分流给组内不同的消费者,即不同的消费者要去抢队列中大哥消息,而不是重复消费,从而加快处理消息的速度,避免了消息堆积
2.消息表示:消费者组会为维护一个表示,记录最后一个被处理的消息,即使发生了宕机等情况,还可以从标识之后读取消息,避免了消息漏读的情况
3.消息确认:消费者收到消息后,消息会维持pending(待处理)状态,并且存入pending-list,等待消费者处理完消息后,通过XACK来确认,标记消息已处理,才会从pending-list中移除,避免了消息丢失的情况
命令方法如下:
创建消费者组
下述命令表示:创建了队列名为s1,消费者组名称为g1,并且从队列中第一个消息开始读取
XGROUP CREATE s1 g1 0
从消费者组中读取消息下面这个命令表示:指定消费者组g1中消费者c1(若c1不存在,则自动创建)从队列s1中读取开始于下一个未消费,最大数量为1的消息,并且阻塞时间设置为2s,即监听2s以内最新的消息,没有退出监听
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
可以看见,组g1中的消费者c1从队列s1中读取了{k1 = v1}的消息,并且将其标记成已消费,同样当组g1中的消费者c1再次从队列s1中读取消息是,只能读取下一个标识为未消费的消息,即{k2 = v2},并设置为已消费
{k3 = v3}与{k4 = v4}同理
但上述消息只是被标记为已消费,还没有用XACK确认消费,所以这5个消息仍存在在pending-list中 ,所以当用XACK确认队列s1中被g1消费者组消费的5个消息的ID,才能将5个消息从pending-list中移除
当读取完消息{k6 = v6}时,可以在pending-list中查看,下面命令表示查看pending-list中由消费者组g1消费的队列s1中的消息从最小到最大的10条消息,但结果只显示了消息{k6 = v6}的ID,因为前五条消息都被XACK确认移除了
XPENDING s1 g1 - + 10
最后,当队列s1中的消息都被XACK确认移除了,再去读取消息,会返回empty
java代码实现