Redis Stream做消息队列

Redis Stream

接上篇文章,这章说说消息队列。消息队列用于应用间通信,“消息”是两个应用间传输的数据单位。今天介绍一款轻量级、用于小型项目间、相对可靠的队列——redis stream

消息队列

当我们使用消息队列时,希望消息队列具有以下优点:

  1. 支持发布/订阅模式
  2. 消息持久化,做到消息宕机不丢失
  3. 可重复消费
  4. 可阻塞式拉取
  5. 处理消息消费失败的机制

发布/订阅和Stream的区别

redis做消息队列有多种方式,其中发布/订阅就是其中一种。发布/订阅有什么缺点呢?

发布/订阅可以做到消息的广播模式,对消费者能做到有消息就通知的能力,但有个致命缺点,即发布/订阅不能持久化消息。当redis服务出现断开、宕机等突发情况,消息就会丢失。

Stream

使用Stream数据结构,需要注意redis的版本。redis从5.0开始支持Stream能力,是对消息队列(Message Queue)的完善实现

对于队列,我们要考虑的是除了发布/订阅模式的不能持久化的缺点之外,我们还需要考虑

  • 消息的生产
  • 消息的消费
    • 单播和多播
    • 阻塞和非阻塞
  • 消息的有序性
  • 消息的重复消费
  • 消息堆积等

Stream的结构

每个stream数据都有唯一的名称,那就是redis的key,不能显示创建stream,但可以在我们首次使用XADD指令时隐式创建stream以及指定key。

在这里插入图片描述

  • stream中每一个消息都有一个唯一ID,用于区分不同消息。在stream中,entry是消息的称呼,即ID也叫entry的id

    entry ID是通过XADD指令返回的,ID的结构是<millisecondsTime>-<sequenceNumber>

  • consumer group:消费者组,使用XGROUP CREATE指令创建,一个消费者组有多个消费者,组内的消费者之间是竞争关系。

  • last_delivered_id:游标。每个消费者组有一个游标,表示消费者组读取消息的位置

  • pending_ids:待确认消息id集合。属于消费者的属性,当中记录当前消费者读取但未确认(ACK)的消息id

Stream相关操作

[官方链接](XADD | Docs (redis.io))

属于Stream本身的操作:

  • XADD:添加消息到指定Stream中,如果Stream不存在,则创建
  • XDEL:从Stream中删除指定的条目,并返回删除的条目数
  • XINFO STREAM:提供Stream信息
  • XLEN:返回Stream长度,不存在返回0
  • XRANGE:范围查看Stream中的条目数据
  • XREAD:查看指定的条目数据
  • XREVRANGE:和XRANGE相反,以反方向顺序查看条目数据
  • XTRIM:通过删除就条目来修剪Stream

属于消费者组或消费者的操作:

  • XGROUP CREATE:给Stream创建一个消费者组
  • XGROUP CREATECONSUMER:给Stream中的消费者组创建一个消费者
  • XGROUP DELCONSUMER:删除一个消费者
  • XGROUP DESTROY:完全销毁消费者组,包括消费者组、消费者、消费者的pending、last_deliver_id等
  • XGROUP SETID:为消费者组设置last delivered ID
  • XINFO GROUPS:提供消费者组信息
  • XINFO CONSUMERS:提供消费者信息
  • XPENDING:查看消费者组中读取但未确认的消息集合
  • XREADGROUP:和XREAD类似,XREADGROUP支持消费者组

项目中应用

spring启动时初始化stream

/**
 * @author zhangyi
 */
@Component
@Slf4j
@Order(1)
public class MqRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 初始化stream
        MqService mqService = SpringUtils.getBean(MqService.class);
        mqService.init();
        // 添加消费者监听
        StreamConsumerService consumerService = SpringUtils.getBean(StreamConsumerService.class);
        consumerService.startListenering();
    }
}

@Override
public void init() {
    log.info("init redis stream");
    try {
        StreamInfo.XInfoStream streamInfo = redisUtils.streamInfo(BIG_MODEL_STREAM_KEY);
    } catch (Exception e) {
        // 还没创建stream时
        if (e.getMessage().contains("ERR no such key")) {
            redisUtils.sendMessageByStream(BIG_MODEL_STREAM_KEY, ImmutableMap.of("init", "init"));

            // 还没创建group和consumer时
            if (redisUtils.groupInfo(BIG_MODEL_STREAM_KEY).stream().noneMatch(group -> Objects.equals(BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, group.groupName()))) {
                redisUtils.createStreamGroup(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME);
            }
        }
    }
    log.info("init redis stream success");
}
/**
 * @author zhangyi
 */
@Slf4j
@Component
public class StreamConsumerServiceImpl implements StreamConsumerService {
    @Autowired
    private RedisUtils redisUtils;

    @Override
    public void startListenering() {
        AsyncManager.me().execute(new TimerTask() {
            @Override
            public void run() {
                while (true) {
                    try {
                        // 拉取并处理消息
                        pullFromStreamAndConsume();
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                        ThreadUtil.sleep(5, TimeUnit.MINUTES);
                    }
                }
            }
        });
    }

    private void pullFromStreamAndConsume() {
        List<MapRecord<String, String, String>> records = redisUtils.xReadGroup(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, BIG_MODEL_STREAM_CONSUMER_1_NAME);
        for (MapRecord<String, String, String> record : records) {
            RecordId recordId = record.getId();
            Map<String, String> recordMap = record.getValue();
            for (Map.Entry<String, String> entry : recordMap.entrySet()) {
                String value = JsonUtils.fromJson(entry.getValue(), String.class);
                if (Objects.equals("init", entry.getKey()) && Objects.equals("init", value)) {
                    redisUtils.xAck(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, recordId.getValue());
                    log.info("消费者消费了一条消息,recordId:{}", recordId.getValue());
                    log.info("field:{}", entry.getKey());
                    log.info("value:{}", value);
                    continue;
                }

                try {
                    // 处理消息
                    doSomething();
                    redisUtils.xAck(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, recordId.getValue());
                    log.info("消费者消费了一条消息,recordId:{}", recordId.getValue());
                    log.info("field:{}", entry.getKey());
                    log.info("value:{}", value);
                } catch (Exception e) {
                    log.error("无法消费消息,recordId:{}", recordId.getValue());
                    log.error(e.getMessage(), e);
                }
            }
        }

        // records为空,休眠30s
        if (records.isEmpty()) {
            ThreadUtil.sleep(30, TimeUnit.SECONDS);
        }
    }
}
/**
 * spring redis 工具类
 *
 * @author zhangyi
 **/
@Component
public class RedisUtils {
    @Autowired
    public RedisTemplate redisTemplate;

    /*
     * ------------------------------------------------------redis stream------------------------------------------------------
     */

    /**
     * 获取名为key的stream相关信息
     *
     * @param key stream key
     * @return info
     */
    public StreamInfo.XInfoStream streamInfo(String key) {
        return redisTemplate.opsForStream().info(key);
    }

    /**
     * 获取名为key的stream上的消费者组
     *
     * @param key stream key
     * @return XInfoGroups
     */
    public StreamInfo.XInfoGroups groupInfo(String key) {
        return redisTemplate.opsForStream().groups(key);
    }

    /**
     * 给消息队列发送消息,相当于生产者
     *
     * @param key     stream key
     * @param message field value 对
     * @return RecordId 消息id
     */
    public RecordId sendMessageByStream(String key, Map<String, Object> message) {
        return sendMessageByStream(key, message, 1000);
    }

    public RecordId sendMessageByStream(String key, Map<String, Object> message, int maxLength) {
        Map<byte[], byte[]> messageMap = new HashMap<>();
        for (Map.Entry<String, Object> entry : message.entrySet()) {
            messageMap.put(entry.getKey().getBytes(), JsonUtils.toJson(entry.getValue()).getBytes());
        }
        MapRecord<byte[], byte[], byte[]> entries = StreamRecords.newRecord().in(key.getBytes()).ofMap(messageMap);
        RedisStreamCommands.XAddOptions options = RedisStreamCommands.XAddOptions.maxlen(maxLength);
        return (RecordId) redisTemplate.execute((RedisConnection connection) -> connection.xAdd(entries, options));
    }

    /**
     * 完全销毁消费者组
     *
     * @param key       stream key
     * @param groupName 消费者组名称
     */
    public void destroyGroup(String key, String groupName) {
        redisTemplate.opsForStream().destroyGroup(key, groupName);
    }

    /**
     * 创建名为key的stream的groupName的消费者组
     *
     * @param key       stream key
     * @param groupName 消费者组名称
     */
    public void createStreamGroup(String key, String groupName) {
        redisTemplate.opsForStream().createGroup(key, groupName);
    }

    /**
     * 读取消息
     *
     * @param key          stream key
     * @param groupName    消费者组名称
     * @param consumerName 消费者名称
     * @return list
     */
    public List<MapRecord<String, String, String>> xReadGroup(String key, String groupName, String consumerName) {
        List<ByteRecord> records = (List<ByteRecord>) redisTemplate.execute((RedisConnection connection) ->
                connection.xReadGroup(Consumer.from(groupName, consumerName), StreamReadOptions.empty().count(5),
                        StreamOffset.create(key.getBytes(), ReadOffset.lastConsumed())));

        ArrayList<MapRecord<String, String, String>> list = new ArrayList<>(records.size());
        for (ByteRecord record : records) {
            StringRedisSerializer serializer = new StringRedisSerializer();
            MapRecord<String, String, String> mapRecord = record.deserialize(serializer, serializer, serializer);
            list.add(mapRecord);
        }
        return list;
    }

    /**
     * XACK操作
     *
     * @param key       stream key
     * @param groupName 消费者组名称
     * @param recordId  recordId
     */
    public void xAck(String key, String groupName, String recordId) {
        redisTemplate.opsForStream().acknowledge(key, groupName, recordId);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雨路行人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值