kafka实现延迟消息发送

1,511 阅读3分钟

在日常开发中会遇到这样一种需求,比如订单超时未支付,则取消订单并返还库存,或者定时发送邮件等信息。这时可以采用定时任务去扫描数据库,但是这种情况下就会存在一定的时间误差。那么还有一种方式,就是使用消息中间件rocketmq的延时队列来实现,但是如果项目中使用的是kafka话怎么实现延时队列呢。

由于kafka并没有像rocketmq那样提供延时队列等丰富的高级特性,只能自己想办法解决延时效果。这里仅提供个人的想法和实现(当然也参考了rocketmq😏):

  1. 约定延迟时间等级,如10s,20s,60s等
  2. 每个延迟时间等级绑定不同的延时队列和topic
  3. 将消息体存储到延迟队列
  4. 每个队列分别绑定一个线程,用于循环获取消息并推送消息

首先定义延迟的时间等级以及对应的topic。

public enum DelayEnum {

    TEN_S(10, "delay_queue_10s"),
    TWENTY_S(20, "delay_queue_20s"),
    SIXTY_S(60, "delay_queue_60s"),
    FIVE_M(5 * 60, "delay_queue_5m");

    public long delayTime;

    public String topic;

    DelayEnum(long delayTime, String topic) {
        this.delayTime = delayTime;
        this.topic = topic;
    }
}

定义发送消息的消息体,包括消息内容,延时等级枚举,像一些key,partition啥的可以根据实际情况自己定义。这里的延时效果是使用的DelayQueue,所以消息类需要实现Delayed接口。


public class KafkaDelayMsg implements Delayed { 

    private String msg; //消息内容

    private DelayEnum delayTime; // 延时等级

    private long time; // 实际的发送时间

    public KafkaDelayMsg() {
    }

    public KafkaDelayMsg(String msg, DelayEnum delayTime) {
        this.delayTime = delayTime;
        this.time = delayTime.delayTime*1000 + System.currentTimeMillis();
        this.msg = msg;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return Long.compare(time, ((KafkaDelayMsg) o).time);
    }
}

创建对应的队列,用于存储消息,并且提供一个方法用于发送消息到kafka,这里也可以只创建一个队列,但是为了方便分类和提高效率,所以创建的多个。

@Slf4j
public class DelayQueues {

    // 延迟10s的队列
    public static DelayQueue<KafkaDelayMsg> TEN_S = new DelayQueue<KafkaDelayMsg>();

    // 延迟20s的队列
    public static DelayQueue<KafkaDelayMsg> TWENTY_S = new DelayQueue<KafkaDelayMsg>();

    // 延迟60s的队列
    public static DelayQueue<KafkaDelayMsg> SIXTY_S = new DelayQueue<KafkaDelayMsg>();

    // 延迟5min的队列
    public static DelayQueue<KafkaDelayMsg> FIVE_M = new DelayQueue<KafkaDelayMsg>();


    /*
    * 实际发送消息的方法
    */
    public static void run(KafkaProducer<String, String> producer) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        log.info("开始执行=======");
        executorService.execute(() -> {
            while (true) {
                try {
                    KafkaDelayMsg msg = TEN_S.take(); // 获取消息
                     // 向broker推送消息
                    RecordMetadata recordMetadata = producer.send(new ProducerRecord<>(msg.getDelayTime().topic, msg.getMsg())).get(); 
                    log.info("[kafka-delay-10s]:=====" + msg.toString()+"==="+recordMetadata.topic()+"==="+recordMetadata.partition());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        executorService.execute(() -> {
            while (true) {
                try {
                    KafkaDelayMsg msg = TWENTY_S.take();
                    RecordMetadata recordMetadata = producer.send(new ProducerRecord<>(msg.getDelayTime().topic, msg.getMsg())).get();
                    log.info("[kafka-delay-20s]:=====" + msg.toString()+"==="+recordMetadata.topic()+"==="+recordMetadata.partition());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        // ......
    }

}

创建发送工具类,并在无参构造方法中实例化发送对象,如果在SpringBoot或Spring项目中,可以设置为Bean对象,需要使用时在注入Bean对象。

public class KafkaPushUtil {

    KafkaProducer<String, String> producer = null;

    public KafkaPushUtil() {
       // 配置kafka信息
        Map<String, Object> config = new HashMap<>();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.36.128:9092");
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 实例化生产者对象
        producer = new KafkaProducer<>(config);
        // 启动队列线程
        DelayQueues.run(producer);
    }

    /*
    * 发送消息的方法,根据不同的延迟等级,存入不同的队列
    */
    public void send(KafkaDelayMsg msg) {
        if (msg.getDelayTime() == DelayEnum.TEN_S) {
            DelayQueues.TEN_S.add(msg);
        } else if (msg.getDelayTime() == DelayEnum.TWENTY_S) {
            DelayQueues.TWENTY_S.add(msg);
        } else if (msg.getDelayTime() == DelayEnum.SIXTY_S) {
            DelayQueues.SIXTY_S.add(msg);
        } else if (msg.getDelayTime() == DelayEnum.FIVE_M) {
            DelayQueues.FIVE_M.add(msg);
        }
    }


}

测试消息发送

KafkaPushUtil KafkaPushUtil = new KafkaPushUtil();
KafkaPushUtil.send(new KafkaDelayMsg("延时测试10s", DelayEnum.TEN_S));
KafkaPushUtil.send(new KafkaDelayMsg("延时测试20s", DelayEnum.TWENTY_S));

image.png 消费端 image.png

由于kafka的应用场景主要还是大数据领域,如果普通业务中使用mq的话,还是推荐使用rocketmq。rocketmq的读写能力和底层架构都和kafka差不多,而且rocketmq提供的功能比kafka实在一些,不过使用哪种还是要看自己的选择。