在日常开发中会遇到这样一种需求,比如订单超时未支付,则取消订单并返还库存,或者定时发送邮件等信息。这时可以采用定时任务去扫描数据库,但是这种情况下就会存在一定的时间误差。那么还有一种方式,就是使用消息中间件rocketmq的延时队列来实现,但是如果项目中使用的是kafka话怎么实现延时队列呢。
由于kafka并没有像rocketmq那样提供延时队列等丰富的高级特性,只能自己想办法解决延时效果。这里仅提供个人的想法和实现(当然也参考了rocketmq😏):
- 约定延迟时间等级,如10s,20s,60s等
- 每个延迟时间等级绑定不同的延时队列和topic
- 将消息体存储到延迟队列
- 每个队列分别绑定一个线程,用于循环获取消息并推送消息
首先定义延迟的时间等级以及对应的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));
消费端
由于kafka的应用场景主要还是大数据领域,如果普通业务中使用mq的话,还是推荐使用rocketmq。rocketmq的读写能力和底层架构都和kafka差不多,而且rocketmq提供的功能比kafka实在一些,不过使用哪种还是要看自己的选择。