基于redis stream实现一个可靠的消息队列

我们使用的库为redisson。
添加元素到队列很简单,用RStream.add方法即可。

如何从队列获取元素?由于我们打算实现kafka那样的consumer group机制,所以,读操作要用RStream.readGroup函数(XREADGROUP命令),该命令有阻塞和非阻塞版本,简单起见,我们使用非阻塞版本(不带BLOCK参数),由应用层来定时轮询。Id参数我们设置为StreamReadGroupArgs.neverDelivered(),相当于redis命令里的>,每次只取最新的消息。相关的代码样例如下:

public List<Record> poll(String groupName, String consumerName) {
    return toConsumerRecords(redisMsgQueue.readGroup(groupName, consumerName, StreamReadGroupArgs.neverDelivered()));
}

追求可靠性,则须封装XACK命令。引入ACK之后,自然要考虑对那些ACK超时的消息如何补救。为此,我们要结合XPENDING和XCLAIM两条命令,利用前者查出目前尚未ACK的消息集M(术语叫PEL,pending entry list),利用后者将M中已超时的那部分(通过min-idle-time参数过滤)分配给其它存活的consumer去做补救处理。为何XPENDING出来之后要做XCLAIM呢?主要是我们希望做补救处理的consumer只能有一位,XCLAIM命令在多consumer竞争时能保证独占性,从而避免消息的多次处理。XPENDING遍历PEL也是有技巧的,一是要分批获取,二是由于XPENDING的startId和endId是闭区间, 需将用于遍历的cursor Id转成开区间(参考redis的streamId说明,具体做法是将streamId的sequence部分加1)。参考实现如下:

public List<Record> fetchNotAck(String groupName, String consumerName, long ackTimeout) {
    PendingResult pr = redisMsgQueue.getPendingInfo(groupName);
    if (pr.getTotal() == 0) {
        return Collections.emptyList();	
    }
    StreamMessageId curId = pr.getLowestId();
    StreamMessageId maxId = pr.getHighestId();
    List<Record> totalRecs = new ArrayList<>();
    while (true) {
        List<PendingEntry> entries = redisMsgQueue.listPending(groupName, curId, maxId, 100);
        if (entries.isEmpty()) {
            break;
        }
        curId = makeExclusive(entries.get(entries.size() - 1).getId());
        Map<StreamMessageId, Map<String, T> > rawRes = redisMsgQueue.claim(groupName, consumerName, ackTimeout, TimeUnit.SECONDS, entries.stream.map(PendingEntry::getId).toArray(StreamMessageId[]::new));
        totalRecs.addAll(toConsumerRecords(rawRes));
    }
    return totalRecs;
}

private StreamMessageId makeExclusive(StreamMessageId id) {
    return new StreamMessageId(id.getId0(), id.getId1() + 1);
}

最后,stream的定时清理动作,解决内存占用过大的问题,使用lua脚本包装XTRIM命令,根据上一次清理时的stream size,计算本次要保留的元素个数。这里还有一个特殊点,如果一条未ACK消息被XTRIM或XDEL从stream里删除,其并不会从PEL里同步删除,用XCLAIM也获取不了它,这就成了一条PEL垃圾数据,据说redis7.0会解决该问题。stream清理算法如下:

private static final String SHRINK_SCRIPT = "local nowSz = redis.call('XLEN', KEYS[1]);redis.call('XTRIM', KEYS[1], 'MAXLEN', nowSz - ARGV[1]); local leftSz = redis.call('XLEN', KEYS[1]); redis.call('HSET', KEYS[2], KEYS[1], leftSz);return leftSz;";

public long shrinkByLastSize(long lastSize, String queueSizeMapName) {
    RScript  rScript = getRedissonClient().getScript(LongCodec.INSTANCE);
    return rScript.eval(RScript.Mode.READ_WRITE, SHRINK_SCRIPT, RScript.ReturnType.INTEGER, 
        Arrays.asList(this.streamName, queueSizeMapName), lastSize);
}

其中,queueSizeMap是一个redis hash,用于存储每个stream上次清理后的size。size的设置和stream的XTRIM操作必须是原子操作,所以放一个lua脚本里。另外,stream 和queueSizeMap有可能不在一个slot里(redis cluster下),要考虑在名字里加{}确保这俩在一起,否则lua脚本会报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Redis StreamRedis 5.0版本中新增的一种数据结构,它是一个高性能、持久化的消息队列,可以用于实现消息的发布和订阅。Java Redis Stream可以看作是一个有序的消息队列,每个消息都有一个唯一的ID,可以根据ID进行消息的查找、删除和确认。在Java Redis Stream中,消息以键值对的形式存储,可以存储任意类型的数据。Java Redis Stream还支持多个消费者组,每个消费者组可以独立消费消息,避免消息重复消费。Java Redis Stream的引入使得Redis消息队列领域更具竞争力,同时也为开发者提供了一种高效、可靠的消息处理方式。 以下是Java Redis Stream的一些基本操作: 1.连接Redis服务器 ```java Jedis jedis = new Jedis("localhost", 6379); ``` 2.发布消息 ```java String key = "mystream"; String message = "Hello, Redis Stream!"; String id = jedis.xadd(key, null, ImmutableMap.of("message", message)); System.out.println("Message ID: " + id); ``` 3.消费消息 ```java String groupname = "mygroup"; String consumername = "myconsumer"; String key = "mystream"; String lastseen = "0"; jedis.xgroupCreate(key, groupname, lastseen, true); List<StreamEntry> entries = jedis.xreadGroup(groupname, consumername, 1, 0, false, new AbstractMap.SimpleEntry<>(key, lastseen)); for (StreamEntry entry : entries) { System.out.println("Message ID: " + entry.getID() + ", Message: " + entry.getFields().get("message")); jedis.xack(key, groupname, entry.getID()); } ``` 4.删除消息 ```java String key = "mystream"; String id = "1234567890-0"; jedis.xdel(key, id); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值