Pulsar源码解析-延迟队列实现

本文深入解析Apache Pulsar的延迟队列功能,用于处理延迟消费场景,如订单超时。延迟队列基于堆外内存的TripleLongPriorityQueue实现,支持任意维度延迟,但当延迟时间差距大时需按时间维度分topic。添加到延迟队列涉及扩容、排序和交换操作,而从队列中取出消息则根据时间戳进行。该系统保证消息按延迟时间顺序处理,确保高效且精确的延迟消费。
摘要由CSDN通过智能技术生成

问题:

  1. 延迟队列的作用
  2. 延迟队列数据结构
  3. 如何添加到延迟队列?
  4. 如何从延迟队列取出?

问题1延迟队列的作用

延迟队列用来解决需要延迟消费的场景,例如 电商中订单超时15分钟未支付自动关闭
特点:Pulsar的延迟队列可支持任意维度的延迟
存储:堆外内存
消息数:1G堆外内存 = 1 * 1024 * 1024 * 1024 / 24 = 44,739,242条,1G支持4400万条可满足大多数业务场景,不足就加内存。(ps: 消息是持久化到磁盘,延迟消息是从磁盘读取出来放入延迟队列中,只放消息的索引:2个long类型索引+1个long类型时间=24字节,假设broker宕机,启动后消费者读取消息会先放入延迟队列校验是否到时间,然后推送给消费者)
缺点:当延迟的时间维度差距很大(有几个月的,有几分钟的)影响磁盘删除,这时需要使用者按时间维度分topic发送(分多个层级分、时、天、周、月、季度、年),为什么呢?因为pulsar的删除最小单元是Ledger,一个Ledger可以存很多消息,其中有一条未消费,整个Ledger都不会删。导致磁盘越来越大

问题2延迟队列数据结构

延迟队列的实现是TripleLongPriorityQueue,本质是ByteBuf,一次写入24字节

public class TripleLongPriorityQueue implements AutoCloseable {

    private static final int SIZE_OF_LONG = 8;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // Each item is composed of 3 longs
    private static final int ITEMS_COUNT = 3;

    private static final int TUPLE_SIZE = ITEMS_COUNT * SIZE_OF_LONG;

    private final ByteBuf buffer;

    private int capacity;
    private int size;
    public TripleLongPriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY);
    }

    public TripleLongPriorityQueue(int initialCapacity) {
        capacity = initialCapacity;
        buffer = PooledByteBufAllocator.DEFAULT.directBuffer(initialCapacity * ITEMS_COUNT * SIZE_OF_LONG);
        size = 0;
    }
    ...
}

问题3如何添加到延迟队列?

public class TripleLongPriorityQueue implements AutoCloseable {

    public void add(long n1, long n2, long n3) {
    	// 扩容
        if (size == capacity) {
            increaseCapacity();
        }
        // 添加
        put(size, n1, n2, n3);
        // 排序+交换
        siftUp(size);
        // 统计个数
        ++size;
    }
}

扩容

    private void increaseCapacity() {
        // 小于256扩一倍,超过256扩一半
        this.capacity += (capacity <= 256 ? capacity : capacity / 2);
        buffer.capacity(this.capacity * TUPLE_SIZE);
    }

添加

    private void put(int idx, long n1, long n2, long n3) {
    	// 当前size * 24 = 尾部
        int i = idx * TUPLE_SIZE;
        // 追加写,比较紧凑
        buffer.setLong(i, n1);
        buffer.setLong(i + 1 * SIZE_OF_LONG, n2);
        buffer.setLong(i + 2 * SIZE_OF_LONG, n3);
    }
    private void siftUp(int idx) {
        while (idx > 0) {
        	// 折半
            int parentIdx = (idx - 1) / 2;
            // 当前数据比中间大结束
            // 有没有可能是 2 3 4 5 1 6 7
            // 不可能,因为在插入1时比4小就会换位置,并发?也没有,外层调用时加了锁
            if (compare(idx, parentIdx) >= 0) {
                break;
            }

            swap(idx, parentIdx);
            idx = parentIdx;
        }
    }

交换

    private void swap(int idx1, int idx2) {
    	// 小
        int i1 = idx1 * TUPLE_SIZE;
        // 大
        int i2 = idx2 * TUPLE_SIZE;

		// 获取小的值
        long tmp1 = buffer.getLong(i1);
        long tmp2 = buffer.getLong(i1 + 1 * SIZE_OF_LONG);
        long tmp3 = buffer.getLong(i1 + 2 * SIZE_OF_LONG);
		
		// 大小交换
        buffer.setLong(i1, buffer.getLong(i2));
        buffer.setLong(i1 + 1 * SIZE_OF_LONG, buffer.getLong(i2 + 1 * SIZE_OF_LONG));
        buffer.setLong(i1 + 2 * SIZE_OF_LONG, buffer.getLong(i2 + 2 * SIZE_OF_LONG));

        buffer.setLong(i2, tmp1);
        buffer.setLong(i2 + 1 * SIZE_OF_LONG, tmp2);
        buffer.setLong(i2 + 2 * SIZE_OF_LONG, tmp3);
    }

总结:存储使用netty堆外内存Bytebuf,每条消息是3个Long类型,第一个Long是延迟时间,即24字节,插入时自动扩容+排序,顺序从小到大。即快过期的是第一条。
ps:add调用之前的文章分析消费者服务端拉取的那篇有讲到,推给消费者之前会判断:如果有设置延迟时间则往延迟队列添加,添加成功是延迟消息,添加失败延迟时间已到。下面这段代码:

public class InMemoryDelayedDeliveryTracker {
    public boolean addMessage(long ledgerId, long entryId, long deliveryAt) {
        long now = clock.millis();
        if (deliveryAt < (now + tickTimeMillis)) {
            return false;
        }

        priorityQueue.add(deliveryAt, ledgerId, entryId);
        updateTimer();
        return true;
    }
}

问题4如何从延迟队列取出?

public class InMemoryDelayedDeliveryTracker {

public Set<PositionImpl> getScheduledMessages(int maxMessages) {
		// 读取数量
        int n = maxMessages;
        Set<PositionImpl> positions = new TreeSet<>();
        long now = clock.millis();
        long cutoffTime = now + tickTimeMillis;
		// 读取数量>0 && 队列不为空
        while (n > 0 && !priorityQueue.isEmpty()) {
        	// 读前8个字节的时间
            long timestamp = priorityQueue.peekN1();
            // 如果最近的延迟都大于当前时间,说明全都没到时间 结束
            if (timestamp > cutoffTime) {
                break;
            }
			
            long ledgerId = priorityQueue.peekN2();
            long entryId = priorityQueue.peekN3();
            // 保存消息索引
            positions.add(new PositionImpl(ledgerId, entryId));
			// 移除first
            priorityQueue.pop();
            --n;
        }
		// 找到最近过期时间的消息计算还有多久到时间,设置一个倒计时,到时间后触发dispatcher读数据
		// dispatcher读的时候会读重新投递的和延迟队列的
        updateTimer();
        return positions;
    }
}
pulsar-java-spring-boot-starter是一个用于在Spring Boot应用程序中集成Apache Pulsar消息队列的开源库。Apache Pulsar是一个可扩展的、低延迟分布式消息传递平台,它具有高吞吐量和高可靠性的特点。 pulsar-java-spring-boot-starter允许开发人员在Spring Boot应用程序中轻松地发送和接收Pulsar消息。它提供了一组容易使用的注解和工具类,简化了与Pulsar集群的交互。 使用pulsar-java-spring-boot-starter,开发人员可以通过添加依赖和配置一些属性来快速集成Pulsar到他们的Spring Boot应用程序中。一旦集成完成,开发人员可以使用注解来定义消息的生产者和消费者。通过生产者注解,开发人员可以将消息发送到Pulsar集群,并指定消息的主题和内容。通过消费者注解,开发人员可以订阅Pulsar主题,并定义接收和处理消息的方法。 除了基本的生产者和消费者功能,pulsar-java-spring-boot-starter还提供了一些其他特性。例如,它支持失败重试机制,当消息发送或接收出现问题时,可以自动重试。它还支持消息过滤器,可以按条件过滤接收的消息。而且,它还提供了一些监控和管理功能,可以方便地监控消息的生产和消费情况。 总之,pulsar-java-spring-boot-starter为Spring Boot开发人员提供了一种方便、快捷地集成Apache Pulsar消息队列的方法。它简化了与Pulsar集群的交互,提供了易于使用的注解和工具类,让开发人员可以更专注于业务逻辑的实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值