目录
5.Redis消息队列-基于Stream的消息队列-消费者组
6.基于Redis的Stream结构作为消息队列,实现异步秒杀下单
1.Redis消息队列-认识消息队列(MQ)
消息队列的定义:是基于 Redis 实现的消息异步传输、暂存与消费的中间件方案,主要用于存放消息,管理消息
消息队列的基础模型包含三个部分:
-
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
-
生产者:发送消息到消息队列
-
消费者:从消息队列获取消息并处理消息

消息队列的好处:让消息生产者 (发送消息的服务)和消息消费者 (处理消息的服务)解耦,生产者只管发消息,消费者只管取消息处理,无需同时在线、无需直接通信,可以异步进行操作
举例:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间,所以这种思想在我们日常开发中,是非常有必要的。
redis中有三种实现消息队列的方式:
1.list队列
2.PubSub发布订阅
3.Stream(常用)
2.Redis消息队列-基于List实现消息队列
因为队列需要出口和入口不在同一边,所以可以用list的LPUSH 和 BRPOP来从左端口进右端口出,来模拟消息队列的效果

从redis-cli控制台使用help @list中也可以看到每一个的效果


注意BRPOP是需要控制阻塞时间的
基于List的消息队列有哪些优缺点?
优点:
利用Redis存储,不受限于JVM内存上限
基于Redis的持久化机制,数据安全性有保证
可以满足消息有序性
缺点:
无法避免消息丢失
只支持单消费者
3.Redis消息队列-基于PubSub的消息队列
PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel(频道)发送消息后,所有订阅者都能收到相关消息。
SUBSCRIBE channel [channel] :订阅一个或多个频道
PUBLISH channel msg :向一个频道发送消息
PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道

只要执行了SUBSCRIBE命令订阅频道就会开始监听这个频道,直到这个频道发布消息

但是有可能会出现消息丢失,如果在给频道发送消息时,没有监听就会丢失消息
下面返回0就是没有消费者监听频道,消息就丢失了,因为这个消息不会存储在redis里,所以不能像list一样持久化

基于PubSub的消息队列有哪些优缺点?
优点:
- 采用发布订阅模型,支持多生产、多消费
缺点:
不支持数据持久化
无法避免消息丢失
消息堆积有上限,超出时数据丢失
4.Redis消息队列-基于Stream的消息队列
Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
因为是一种数据类型,所以可以做到数据持久化
写命令和读命令
xadd


xread
注意如果需要写count参数一定要写count,block同理


但如果我们一次性发送多条消息,只读最新的一条的话可能会漏读消息


如图,当我们往stream里添加了4个消息时,只读到了一个
在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下

5.Redis消息队列-基于Stream的消息队列-消费者组
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:

创建消费组组:XGROUP CREATE key groupname id|$ [MKSTREAM]
key:队列名称
groupName:消费者组名称ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
MKSTREAM:队列不存在时自动创建队列
删除指定的消费者组 :XGROUP DESTORY key groupName
给指定的消费者组添加消费者 :XGROUP CREATECONSUMER key groupname consumername
删除消费者组中的指定消费者 :XGROUP DELCONSUMER key groupname consumername
从消费者组读取消息: XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
在xread的基础上多了消费者组
group:消费组名称
consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
count:本次查询的最大数量
BLOCK milliseconds:当没有消息时最长等待时间
NOACK:无需手动ACK,获取到消息后自动确认,必须手动写 NOACK,才会自动确认!
STREAMS key:指定队列名称
ID:获取消息的起始ID:
">":从下一个未消费的消息开始其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
消费者监听消息的基本思路:
先监听队列,如果为null说明没有消息则继续下一次,有消息就处理消息并且给消息ACK,如果没有成功处理消息就会抛出异常进入pending List队列,然后再catch中读取消费者的pending List把> 改成 0,即把从读最新消息改成第一个没有ACK的,因为此时是已经读过但没处理所以需要改成0,再次处理,直到ACK
Pending List中的一定是没有ACK的消息但不一定是没有处理的消息

6.基于Redis的Stream结构作为消息队列,实现异步秒杀下单
需求:
创建一个Stream类型的消息队列,名为stream.orders
修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId
项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单
java
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
//处理异常消息
handlePendingList();
}
}
}
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理pendding订单异常", e);
try{
Thread.sleep(20);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
}