消息队列概念
消息队列(Message Queue),字面意思就是存放消息的队列,是一种在不同组件或服务间进行异步通信的中间件。最简单的消息队列模型包括3个角色:
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
生产者:发送消息到消息队列
消费者:从消息队列获取消息并处理消息在秒杀场景中的应用
场景分析
在秒杀场景中,消息队列可用于处理高并发的下单请求,避免系统因瞬间流量过大而崩溃。具体流程如下:
生产者:用户发起秒杀请求,系统将请求封装成消息发送到消息队列。
消息队列:缓存大量秒杀请求,按顺序处理。
消费者:从消息队列中获取消息,进行库存检查、扣减库存、创建订单等操作。
Redis三种方式来实现消息队列
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型
基于list实现
Redis 的 List
数据结构可以用来实现简单的消息队列,它提供了 LPUSH
、RPOP
等命令,能够轻松实现生产者 - 消费者模式。
实现原理
生产者:使用 LPUSH 命令将消息添加到列表的左侧,模拟消息的发布。
消费者:使用 RPOP 命令从列表的右侧取出消息,模拟消息的消费。另外,为了避免消费者在列表为空时频繁轮询,可以使用 BRPOP 命令,该命令在列表为空时会阻塞,直到有新消息加入。
代码实现
生产者代码
public void sendMessage(String message) {
stringRedisTemplate.opsForList().leftPush(MESSAGE_QUEUE_KEY, message);
}
消费者代码
public String receiveMessage(long timeout) {
return stringRedisTemplate.opsForList().rightPop(MESSAGE_QUEUE_KEY, timeout, TimeUnit.SECONDS);
}
逻辑实现
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
IdWorker idWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private IVoucherOrderService proxy;
public static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("./lua/seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
public BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
public static final ExecutorService SECKILL_ORDER_EXECUTER = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTER.submit(new voucherOrderHandler());
}
// 启动工作线程(使用局部内部类)
private class voucherOrderHandler implements Runnable {
@Override
public void run() {
while(true){
try {
//VoucherOrder order = orderTasks.take();
String json = receiveMessage(5L);
handleVoucherOrder(JSONUtil.toBean(json, VoucherOrder.class));
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
// 处理订单的逻辑
private void handleVoucherOrder(VoucherOrder order) throws InterruptedException {
// 提取一人一单,扣减库存,创建订单的代码加锁
Long userId = order.getUserId();
// 创建锁对象
RLock lock = redissonClient.getLock("shop:" + userId.toString());
// 尝试获取锁
boolean isLock = lock.tryLock(5, 10, TimeUnit.SECONDS);
if(!isLock){
return;
}
try {
proxy.createVoucherOrder(order);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
// 生产者
private static final String MESSAGE_QUEUE_KEY = "seckill_message_queue";
@Override
public Result seckillVoucher(Long voucherId) {
// 1.执行Lua脚本
long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
UserHolder.getUser().getId().toString()
);
// 2.结果为1,库存不足
if(result == 1){
return Result.fail("库存不足");
}
// 3.结果为2,重复下单
if(result == 2){
return Result.fail("不允许重复下单");
}
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(UserHolder.getUser().getId());
long id = idWorker.nextId("seckillVoucherOrder");
voucherOrder.setId(id);
// 4.库存为0,将订单信息保存到消息队列
sendMessage(JSONUtil.toJsonStr(voucherOrder));
// 5.返回订单id
return Result.ok(id);
}
public void sendMessage(String message) {
stringRedisTemplate.opsForList().leftPush(MESSAGE_QUEUE_KEY, message);
}
public String receiveMessage(long timeout) {
return stringRedisTemplate.opsForList().rightPop(MESSAGE_QUEUE_KEY, timeout, TimeUnit.SECONDS);
}
@Transactional
@Override
public void createVoucherOrder(VoucherOrder order) {
// 扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", order.getVoucherId()).gt("stock", 0).update();
if (!isSuccess) {
return;
}
// 创建订单
save(order);
}
}
优缺点分析
优点
- 性能高:Redis 基于内存操作,读写速度快,能满足高并发场景需求。
- 有序性:基于list数据结构,保证消息的有序性。
- 数据安全:基于Redis的持久化机制,数据安全性有所保证。
- 存储空间:不受JVM内存上限影响。
缺点
-
消息丢失:读取过的消息会直接消失。
-
单消费者:一条消息只支持一个消费者获取。
-
消息确认机制:Redis List 本身没有消息确认机制,需要业务代码自行实现。
基于PubSub实现
Redis 的发布订阅(Pub/Sub)机制可以实现简单的消息队列功能。Pub/Sub 是一种消息通信模式,发送者(发布者)发送消息到特定的频道(channel),订阅该频道的客户端(订阅者)可以接收到这些消息。
实现思路
- 发布消息:在用户成功参与秒杀后,将订单信息通过 Redis 的
PUBLISH
命令发布到指定频道。 - 订阅消息:启动一个线程订阅该频道,接收消息并处理订单。
代码实现
定义秒杀消息频道
private static final String MESSAGE_CHANNEL = "seckill_message_channel";
发布消息代码
public void sendMessage(String message) {
// 使用 PUBLISH 命令发布消息
stringRedisTemplate.convertAndSend(MESSAGE_CHANNEL, message);
}
订阅消息代码
@PostConstruct
private void init() {
// 订阅消息频道
redisMessageListenerContainer.addMessageListener(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
String json = new String(message.getBody());
handleVoucherOrder(JSONUtil.toBean(json, VoucherOrder.class));
}
}, new ChannelTopic(MESSAGE_CHANNEL));
}
功能逻辑实现
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private IdWorker idWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private IVoucherOrderService proxy;
@Autowired
private RedisMessageListenerContainer redisMessageListenerContainer;
public static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("./lua/seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
// 定义秒杀消息频道
private static final String MESSAGE_CHANNEL = "seckill_message_channel";
@PostConstruct
private void init() {
// 订阅消息频道
redisMessageListenerContainer.addMessageListener(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
String json = new String(message.getBody());
handleVoucherOrder(JSONUtil.toBean(json, VoucherOrder.class));
}
}, new ChannelTopic(MESSAGE_CHANNEL));
}
// 处理订单的逻辑
private void handleVoucherOrder(VoucherOrder order) {
Long userId = order.getUserId();
RLock lock = redissonClient.getLock("shop:" + userId.toString());
boolean isLock = lock.tryLock();
if (!isLock) {
return;
}
try {
proxy.createVoucherOrder(order);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
@Override
public Result seckillVoucher(Long voucherId) {
// 1.执行Lua脚本
long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
UserHolder.getUser().getId().toString()
);
// 2.结果为1,库存不足
if (result == 1) {
return Result.fail("库存不足");
}
// 3.结果为2,重复下单
if (result == 2) {
return Result.fail("不允许重复下单");
}
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(UserHolder.getUser().getId());
long id = idWorker.nextId("seckillVoucherOrder");
voucherOrder.setId(id);
// 4.库存充足,将订单信息发布到消息频道
sendMessage(JSONUtil.toJsonStr(voucherOrder));
// 5.返回订单id
return Result.ok(id);
}
public void sendMessage(String message) {
// 使用 PUBLISH 命令发布消息
stringRedisTemplate.convertAndSend(MESSAGE_CHANNEL, message);
}
// 创建订单的逻辑
@Transactional
@Override
public void createVoucherOrder(VoucherOrder order) {
// 扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", order.getVoucherId()).gt("stock", 0).update();
if (!isSuccess) {
return;
}
// 创建订单
save(order);
}
}
- 初始化订阅:在
init
方法里,使用RedisMessageListenerContainer
订阅指定频道,当收到消息时调用handleVoucherOrder
处理。 - 发布消息:在
seckillVoucher
方法中,通过sendMessage
方法将订单信息发布到频道。 - 处理消息:在
MessageListener
的onMessage
方法中,接收消息并转换为VoucherOrder
对象,再调用handleVoucherOrder
处理。
优缺点分析
优点
- 实时性高:消息发布后,订阅者能立即收到消息,适合实时性要求高的场景。
- 多对多通信:支持一个发布者向多个订阅者发送消息,也支持多个订阅者订阅同一个频道。
缺点
- 消息可靠性低:如果订阅者在消息发布时未在线,消息会丢失,Redis Pub/Sub 不保证消息的持久化。
- 没有消息确认机制:发布者无法得知订阅者是否成功接收和处理消息。
- 不支持消息回溯:订阅者无法获取历史消息,只能接收订阅之后发布的消息。
- 数据安全问题:不支持数据持久化,网络断开,Redis服务宕机数据立即消失。
- 消息堆积上限:消息堆积有上限,超出时数据丢失。
基于Stream实现
Stream概述
Redis Stream 是 Redis 5.0 版本引入的新数据结构,主要用于实现消息队列(MQ,Message Queue)。Redis 原本的发布订阅(pub/sub)虽能实现消息队列功能,但存在消息无法持久化的问题,一旦出现网络断开、Redis 宕机等情况,消息就会被丢弃。而 Redis Stream 提供了消息的持久化和主备复制功能,可让任何客户端访问任何时刻的数据,能记住每个客户端的访问位置,还能保证消息不丢失。
主要特性
-
消息持久化:即使 Redis 重启,消息内容依然存在,不会因网络问题或 Redis 宕机导致消息丢失。
-
多生产者和多消费者组:可以接收多个生产者发送的消息,同时支持多个消费者组,每个消费者组内的消费者相互竞争消费消息,一条消息在消费者组只会被一个消费者消费,不同消费者组之间相互独立,都能消费到 Stream 内的所有消息。如消费者g1中的消费者c1,c2与消费者组g2中的c3,c1,c3获取则c2不能获取,c2,c3获取则c1不能获取。
-
消息唯一 ID:Stream 中的每条消息都有唯一的 ID,可被多个消费者独立消费。
-
可记录历史消息:与 Redis 的发布订阅不同,Redis Stream 可以记录历史消息,客户端可以访问任何时刻的数据。
结构相关概念
Stream
每个 Stream 都有一个唯一的名称,它本质上就是 Redis 的 key,在首次使用 XADD 指令追加消息时会自动创建。Stream 内部有一个消息链表,将所有加入的消息串起来,每个消息都有唯一的 ID 和对应的内容。
ConsumerGroup(消费者组)
使用 XGROUPCREATE 命令创建,一个消费组包含多个消费者(Consumer)。消费组内的消费者共同维护一个 last_delivered_id 变量,用于向前推进消费消息。每个消费者内部有一个状态数组变量 pending_ids,用于记录当前已经被客户端读取但还没有 ack(确认)的消息。
last_delivered_id(游标)
每个消费者组都有一个游标 last_delivered_id,任意一个消费者读取了消息都会使该游标往前移动。
pending_ids
它是消费者的状态变量,作用是维护消费者未确认的消息 ID,记录了当前已被客户端读取但还未进行 ack 操作的消息。
常用命令及用法
XADD
用于向队列添加消息,如果指定的队列不存在,则会创建一个队列。
XADD key [MAXLEN [~] count] *|id field value [field value ...]
-
key
:必需参数,代表 Redis Stream 的键名,用于指定要添加消息的目标 Stream。 -
MAXLEN [~] count
:可选参数,用于限制 Stream 的长度,防止其无限增长。
MAXLEN
:指定 Stream 最大长度。~
:可选修饰符,使用近似裁剪策略,能提升性能。Redis 不会严格保证 Stream 长度恰好为count
,而是在接近该长度时进行裁剪。count
:具体的长度限制值。
-
*|id
:
*
:表示让 Redis 自动生成唯一的消息 ID。生成的 ID 格式为<millisecondsTime>-<sequenceNumber>
,例如1672531200000-0
。id
:用户自定义消息 ID,格式必须符合<millisecondsTime>-<sequenceNumber>
,且要大于当前 Stream 中最大的消息 ID,否则会报错。
-
field value [field value ...]
:必需参数,用于指定消息的字段和对应的值,可添加多个键值对。
XREADGROUP
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
GROUP group consumer
- 必需参数。
group
是消费者组的名称,若该消费者组不存在,需先使用XGROUP CREATE
命令创建。consumer
是当前消费者的名称,用于标识当前读取消息的消费者实例。
- 必需参数。
COUNT count
- 可选参数。
count
是一个整数,用于指定每次最多读取的消息数量。若不指定,Redis 会返回尽可能多的消息。
- 可选参数。
BLOCK milliseconds
- 可选参数。
milliseconds
表示阻塞的毫秒数,用于实现阻塞式读取。若指定该参数,当 Stream 中没有新消息时,客户端会阻塞等待,直到有新消息到来或者超时。设置为0
表示无限期阻塞。
- 可选参数。
NOACK
- 可选参数。若指定该参数,读取消息后不会自动将消息标记为已处理,即不会将消息从
pending
列表移除。默认情况下,读取消息后会自动确认。
- 可选参数。若指定该参数,读取消息后不会自动将消息标记为已处理,即不会将消息从
STREAMS key [key ...]
- 必需参数。
key
是要读取消息的 Redis Stream 的键名,可同时指定多个 Stream 键名,以实现从多个 Stream 中读取消息。
- 必需参数。
ID [ID ...]
- 必需参数。ID是消息 ID,用于指定从哪个消息 ID 开始读取。常见取值如下:
>
:表示从 Stream 中从未被该消费者组处理过的最新消息开始读取。0-0
:表示从 Stream 的第一条消息开始读取,常用于处理历史消息或处理pending
列表中的消息。
- 必需参数。ID是消息 ID,用于指定从哪个消息 ID 开始读取。常见取值如下:
- 使用
XREADGROUP
配合0-0
读取:从0-0
开始读取时,会读取该消费者组里所有未确认(pending
列表里)的消息,读取完pending
列表后,若后续还有消息,会继续读取 Stream 里的新消息。pending
列表存储的是已经被消费者读取,但还未通过XACK
命令确认的消息。
示例命令
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream 0-0
此命令使用 mygroup
消费者组中的 consumer1
消费者,从 mystream
的第一条消息开始读取,先处理 pending
列表里的消息,然后读取新消息,最多读取 10 条。
- 使用
XREADGROUP
配合>
读取:使用>
作为消息 ID 时,消费者会从 Stream 里从未被该消费者组处理过的最新消息开始读取,会忽略pending
列表里的消息。
XREADBLOCK
用于消费消息,支持阻塞读取功能。
XACK
用于消费者确认消息,当消费者处理完消息后,使用该命令进行 ack 操作,将消息从 pending_ids 中移除。
XGROUPCREATE
用于创建消费组。
XGROUP CREATE key groupname id [MKSTREAM]
key
:Redis Stream 的键名。groupname
:要创建的消费者组名称。id
:指定消费者组从哪个消息 ID 开始消费。常见取值:$
:从 Stream 中最新的消息开始消费,即只消费后续新增的消息。0-0
:从 Stream 的第一条消息开始消费,会处理历史消息。
MKSTREAM
:可选参数,若指定该参数,当指定的 Stream 不存在时,会自动创建该 Stream。
实现流程
1.创建消费者组
XGROUP CREATE stream.order g1 0 MKSTREAM
2.修改Lua脚本
判断具有下单资格后发送消息到队列中
--1.参数列表
--1.1优惠圈id
local voucherId = ARGV[1]
--1.2订单id
local userId = ARGV[2]
--1.3订单id
local id = ARGV[3]
--2.key
--2.1库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2订单key
local orderKey = 'seckill:order:' .. voucherId
--2.3订单key
local MQName ='seckill.order'
--3.脚本业务
--3.1判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0) then
--库存不足,直接返回1
return 1
end
--3.2判断用户是否下单
if (redis.call('sismember', orderKey, userId) == 1) then
--订单已存在,重复下单,直接返回2
return 2
end
--3.3扣库存
redis.call('incrby', stockKey, '-1')
--3.4下单
redis.call('sadd', orderKey, userId)
--3.5发送消息到队列中
redis.call('xadd', MQName, '*', 'userId', userId, 'voucherId', voucherId, 'id', id)
return 0
3.修改Lua脚本执行代码
@Override
public Result seckillVoucher(Long voucherId) {
// 1.执行Lua脚本
long id = idWorker.nextId("seckillVoucherOrder");
long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
UserHolder.getUser().getId().toString(),
String.valueOf(id)
);
// 2.结果为1,库存不足
if(result == 1){
return Result.fail("库存不足");
}
// 3.结果为2,重复下单
if(result == 2){
return Result.fail("不允许重复下单");
}
// 4.返回订单id
return Result.ok(id);
}
4.完成消费者对消息队列的处理
// 启动工作线程(使用局部内部类)
private class voucherOrderHandler implements Runnable {
public static final String MQ_NAME = "streams.order";
@Override
public void run() {
while(true){
try {
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(MQ_NAME, ReadOffset.lastConsumed())
);
// 2.判断是否获取到消息队列中的订单信息
if(list == null || list.isEmpty()){
continue;
}
// 3.如果获取到,下单
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
handleVoucherOrder(voucherOrder);
// 4.ack确认
stringRedisTemplate.opsForStream().acknowledge(MQ_NAME, "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
// 处理pending-list中的订单信息,避免消息丢失
handlePendingList();
}
}
}
// 处理pending-list中的订单
private void handlePendingList(){
while(true){
try {
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(MQ_NAME, ReadOffset.from("0"))
);
// pending-list中没有订单信息
if(list == null || list.isEmpty()){
continue;
}
// 处理订单
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
handleVoucherOrder(voucherOrder);
// ack确认订单信息
stringRedisTemplate.opsForStream().acknowledge(MQ_NAME, "g1", record.getId());
}catch (Exception e){
// 处理pending-list中的订单信息异常,继续循环处理未确认的pending-list中的订单信息,避免消息丢失
log.error("处理pending-list异常", e);
}
}
}
// 处理订单的逻辑
private void handleVoucherOrder(VoucherOrder order) throws InterruptedException {...}
}
问题总结
XREADGROUPID
传0为什么从 pending
列表中读取而非 Stream 所有消息的第一个
在 Redis Stream 的消费者组机制里,当使用 XREADGROUP
命令(对应 Java 代码里 stringRedisTemplate.opsForStream().read
方法),并把消息 ID 指定为 0
或者 0-0
时,会先从 pending
列表读取消息,而不是从 Stream 所有消息的第一个开始读取,这是由消费者组的设计机制决定的。
消费者组用于让多个消费者协作处理同一个 Stream 里的消息。当一个消费者从 Stream 读取消息后,这些消息会被放入该消费者所属消费者组的 pending
列表,直到消费者使用 XACK
命令确认消息处理完成。pending
列表的作用是记录已经被消费者获取但还未确认的消息,以此保证消息不会丢失。
所以,当指定从 0
位置读取时,Redis 会先检查 pending
列表,把其中未确认的消息返回给消费者,处理完 pending
列表后,若有需要才会继续读取 Stream 里的新消息。
读取后消息是否会从 pending
列表中移除
仅读取消息并不会让消息从 pending
列表中移除。当消费者使用 XREADGROUP
命令读取消息时,消息会被标记为已被该消费者获取,同时添加到 pending
列表。要把消息从 pending
列表移除,需要消费者显式地使用 XACK
命令(对应 Java 代码里 stringRedisTemplate.opsForStream().acknowledge
方法)确认消息处理完成。
优缺点分析
优点
- 消息持久化
Redis Stream 支持消息持久化存储,即使 Redis 服务重启,消息也不会丢失。因为 Redis 可以将 Stream 中的数据持久化到磁盘,当服务恢复后,能从磁盘重新加载数据,保证消息的可靠性。
- 消费者组支持
Redis Stream 提供了消费者组的概念,允许多个消费者共同处理同一个 Stream 中的消息,实现负载均衡。不同消费者可以从 Stream 中获取不同的消息,提高消息处理的并发能力。同时,消费者组还能记录每个消费者的消费进度,当消费者故障恢复后,可以从上次中断的位置继续消费消息。
- 消息确认机制
消费者从 Stream 中读取消息后,需要显式调用 XACK
命令确认消息处理完成。若消费者在处理消息过程中发生故障,未确认的消息会保留在 pending
列表中,待消费者恢复后可以重新处理这些消息,避免消息丢失。
- 消息索引和范围查询
Redis Stream 为每条消息分配一个唯一的 ID,该 ID 包含时间戳和序列号信息。通过消息 ID,可以方便地进行范围查询,例如获取指定时间段内的消息,或者从某个消息 ID 开始继续读取消息。
5. 高性能
Redis 基于内存操作,读写速度极快,能够处理高并发的消息生产和消费请求。同时,Redis Stream 的数据结构设计经过优化,保证了消息的高效存储和读取。
List | PubSub | Stream | |
---|---|---|---|
消息持久化 | 支持 | 不支持 | 支持 |
阻塞读取 | 支持 | 支持 | 支持 |
消息堆积处理 | 受限于内存空间,可以利用多消费者加快处理 | 受限于消费者缓冲区 | 受限于队列长度,可以利用消费者组提高消费速度,减少堆积 |
消息确认机制 | 不支持 | 不支持 | 支持 |
消息回溯 | 不支持 | 不支持 | 支持 |