【Redis实战篇】Redis三种方法实现消息队列优化秒杀优惠券

消息队列概念

消息队列Message Queue),字面意思就是存放消息的队列,是一种在不同组件或服务间进行异步通信的中间件。最简单的消息队列模型包括3个角色:

消息队列:存储和管理消息,也被称为消息代理(Message Broker)

生产者:发送消息到消息队列

消费者:从消息队列获取消息并处理消息在秒杀场景中的应用

场景分析

在秒杀场景中,消息队列可用于处理高并发的下单请求,避免系统因瞬间流量过大而崩溃。具体流程如下:

生产者:用户发起秒杀请求,系统将请求封装成消息发送到消息队列。
消息队列:缓存大量秒杀请求,按顺序处理。
消费者:从消息队列中获取消息,进行库存检查、扣减库存、创建订单等操作。

Redis三种方式来实现消息队列

list结构:基于List结构模拟消息队列

PubSub:基本的点对点消息模型

Stream:比较完善的消息队列模型

基于list实现

Redis 的 List 数据结构可以用来实现简单的消息队列,它提供了 LPUSHRPOP 等命令,能够轻松实现生产者 - 消费者模式。

实现原理

生产者:使用 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),订阅该频道的客户端(订阅者)可以接收到这些消息。

实现思路
  1. 发布消息:在用户成功参与秒杀后,将订单信息通过 Redis 的 PUBLISH 命令发布到指定频道。
  2. 订阅消息:启动一个线程订阅该频道,接收消息并处理订单。
代码实现
定义秒杀消息频道
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);
    }
}
  1. 初始化订阅:在 init 方法里,使用 RedisMessageListenerContainer 订阅指定频道,当收到消息时调用 handleVoucherOrder 处理。
  2. 发布消息:在 seckillVoucher 方法中,通过 sendMessage 方法将订单信息发布到频道。
  3. 处理消息:在 MessageListeneronMessage 方法中,接收消息并转换为 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 ...]
  1. key:必需参数,代表 Redis Stream 的键名,用于指定要添加消息的目标 Stream。

  2. MAXLEN [~] count

    :可选参数,用于限制 Stream 的长度,防止其无限增长。

    • MAXLEN:指定 Stream 最大长度。
    • ~:可选修饰符,使用近似裁剪策略,能提升性能。Redis 不会严格保证 Stream 长度恰好为 count,而是在接近该长度时进行裁剪。
    • count:具体的长度限制值。
  3. *|id

    • *:表示让 Redis 自动生成唯一的消息 ID。生成的 ID 格式为 <millisecondsTime>-<sequenceNumber>,例如 1672531200000-0
    • id:用户自定义消息 ID,格式必须符合 <millisecondsTime>-<sequenceNumber>,且要大于当前 Stream 中最大的消息 ID,否则会报错。
  4. field value [field value ...]:必需参数,用于指定消息的字段和对应的值,可添加多个键值对。

XREADGROUP
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  1. GROUP group consumer
    • 必需参数。group 是消费者组的名称,若该消费者组不存在,需先使用 XGROUP CREATE 命令创建。consumer 是当前消费者的名称,用于标识当前读取消息的消费者实例。
  2. COUNT count
    • 可选参数。count 是一个整数,用于指定每次最多读取的消息数量。若不指定,Redis 会返回尽可能多的消息。
  3. BLOCK milliseconds
    • 可选参数。milliseconds 表示阻塞的毫秒数,用于实现阻塞式读取。若指定该参数,当 Stream 中没有新消息时,客户端会阻塞等待,直到有新消息到来或者超时。设置为 0 表示无限期阻塞。
  4. NOACK
    • 可选参数。若指定该参数,读取消息后不会自动将消息标记为已处理,即不会将消息从 pending 列表移除。默认情况下,读取消息后会自动确认。
  5. STREAMS key [key ...]
    • 必需参数。key 是要读取消息的 Redis Stream 的键名,可同时指定多个 Stream 键名,以实现从多个 Stream 中读取消息。
  6. ID [ID ...]
    • 必需参数。ID是消息 ID,用于指定从哪个消息 ID 开始读取。常见取值如下:
      • >:表示从 Stream 中从未被该消费者组处理过的最新消息开始读取。
      • 0-0:表示从 Stream 的第一条消息开始读取,常用于处理历史消息或处理 pending 列表中的消息。
  • 使用 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 方法)确认消息处理完成。

优缺点分析
优点
  1. 消息持久化

Redis Stream 支持消息持久化存储,即使 Redis 服务重启,消息也不会丢失。因为 Redis 可以将 Stream 中的数据持久化到磁盘,当服务恢复后,能从磁盘重新加载数据,保证消息的可靠性。

  1. 消费者组支持

Redis Stream 提供了消费者组的概念,允许多个消费者共同处理同一个 Stream 中的消息,实现负载均衡。不同消费者可以从 Stream 中获取不同的消息,提高消息处理的并发能力。同时,消费者组还能记录每个消费者的消费进度,当消费者故障恢复后,可以从上次中断的位置继续消费消息。

  1. 消息确认机制

消费者从 Stream 中读取消息后,需要显式调用 XACK 命令确认消息处理完成。若消费者在处理消息过程中发生故障,未确认的消息会保留在 pending 列表中,待消费者恢复后可以重新处理这些消息,避免消息丢失。

  1. 消息索引和范围查询

Redis Stream 为每条消息分配一个唯一的 ID,该 ID 包含时间戳和序列号信息。通过消息 ID,可以方便地进行范围查询,例如获取指定时间段内的消息,或者从某个消息 ID 开始继续读取消息。

5. 高性能

Redis 基于内存操作,读写速度极快,能够处理高并发的消息生产和消费请求。同时,Redis Stream 的数据结构设计经过优化,保证了消息的高效存储和读取。

ListPubSubStream
消息持久化支持不支持支持
阻塞读取支持支持支持
消息堆积处理受限于内存空间,可以利用多消费者加快处理受限于消费者缓冲区受限于队列长度,可以利用消费者组提高消费速度,减少堆积
消息确认机制不支持不支持支持
消息回溯不支持不支持支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MonKingWD

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值