使用 Kotlin+RocketMQ 實現延時訊息
一. 延時訊息
延時訊息是指訊息被髮送以後,並不想讓消費者立即拿到訊息,而是等待指定時間後,消費者才拿到這個訊息進行消費。
使用延時訊息的典型場景,例如:
- 在電商系統中,使用者下完訂單30分鐘內沒支付,則訂單可能會被取消。
- 在電商系統中,使用者七天內沒有評價商品,則預設好評。
這些場景對應的解決方案,包括:
- 輪詢遍歷資料庫記錄
- JDK 的 DelayQueue
- ScheduledExecutorService
- 基於 Quartz 的定時任務
- 基於 Redis 的 zset 實現延時佇列。
除此之外,還可以使用訊息佇列來實現延時訊息,例如 RocketMQ。
二. RocketMQ
RocketMQ 是一個分散式訊息和流資料平臺,具有低延遲、高效能、高可靠性、萬億級容量和靈活的可擴充套件性。RocketMQ 是2012年阿里巴巴開源的第三代分散式訊息中介軟體。
三. RocketMQ 實現延時訊息
3.1 業務背景
我們的系統完成某項操作之後,會推送事件訊息到業務方的介面。當我們呼叫業務方的通知介面返回值為成功時,表示本次推送訊息成功;當返回值為失敗時,則會多次推送訊息,直到返回成功為止(保證至少成功一次)。
當我們推送失敗後,雖然會進行多次推送訊息,但並不是立即進行。會有一定的延遲,並按照一定的規則進行推送訊息。
例如:1小時後嘗試推送、3小時後嘗試推送、1天后嘗試推送、3天后嘗試推送等等。因此,考慮使用延時訊息實現該功能。
3.2 生產者(Producer)
生產者負責產生訊息,生產者向訊息伺服器傳送由業務應用程式系統生成的訊息。
首先,定義一個支援延時傳送的 AbstractProducer。
abstract class AbstractProducer :ProducerBean() {
var producerId: String? = null
var topic: String? = null
var tag: String?=null
var timeoutMillis: Int? = null
var delaySendTimeMills: Long? = null
val log = LogFactory.getLog(this.javaClass)
open fun sendMessage(messageBody: Any, tag: String) {
val msgBody = JSON.toJSONString(messageBody)
val message = Message(topic, tag, msgBody.toByteArray())
if (delaySendTimeMills != null) {
val startDeliverTime = System.currentTimeMillis() + delaySendTimeMills!!
message.startDeliverTime = startDeliverTime
log.info( "send delay message producer startDeliverTime:${startDeliverTime}currentTime :${System.currentTimeMillis()}")
}
val logMessageId = buildLogMessageId(message)
try {
val sendResult = send(message)
log.info(logMessageId + "producer messageId: " + sendResult.getMessageId() + "n" + "messageBody: " + msgBody)
} catch (e: Exception) {
log.error(logMessageId + "messageBody: " + msgBody + "n" + " error: " + e.message, e)
}
}
fun buildLogMessageId(message: Message): String {
return "topic: " + message.topic + "n" +
"producer: " + producerId + "n" +
"tag: " + message.tag + "n" +
"key: " + message.key + "n"
}
}
根據業務需要,增加一個支援重試機制的 Producer
@Component
@ConfigurationProperties("mqs.ons.producers.xxx-producer")
@Configuration
@Data
class CleanReportPushEventProducer :AbstractProducer() {
lateinit var delaySecondList:List<Long>
fun sendMessage(messageBody: CleanReportPushEventMessage){
//重試超過次數之後不再發事件
if (delaySecondList!=null) {
if(messageBody.times>=delaySecondList.size){
return
}
val msgBody = JSON.toJSONString(messageBody)
val message = Message(topic, tag, msgBody.toByteArray())
val delayTimeMills = delaySecondList[messageBody.times]*1000L
message.startDeliverTime = System.currentTimeMillis() + delayTimeMills
log.info( "messageBody: " + msgBody+ "startDeliverTime: "+message.startDeliverTime )
val logMessageId = buildLogMessageId(message)
try {
val sendResult = send(message)
log.info(logMessageId + "producer messageId: " + sendResult.getMessageId() + "n" + "messageBody: " + msgBody)
} catch (e: Exception) {
log.error(logMessageId + "messageBody: " + msgBody + "n" + " error: " + e.message, e)
}
}
}
}
在 CleanReportPushEventProducer 中,超過了重試的次數就不會再傳送訊息了。
每一次延時訊息的時間也會不同,因此需要根據重試的次數來獲取這個delayTimeMills 。
透過 System.currentTimeMillis() + delayTimeMills 可以設定 message 的 startDeliverTime。然後呼叫 send(message) 即可傳送延時訊息。
我們使用商用版的 RocketMQ,因此支援精度為秒級別的延遲訊息。在開源版本中,RocketMQ 只支援18個特定級別的延遲訊息。:(
3.3 消費者(Consumer)
消費者負責消費訊息,消費者從訊息伺服器拉取資訊並將其輸入使用者應用程式。
定義 Push 型別的 AbstractConsumer:
@Data
abstract class AbstractConsumer ():MessageListener{
var consumerId: String? = null
lateinit var subscribeOptions: List<SubscribeOptions>
var threadNums: Int? = null
val log = LogFactory.getLog(this.javaClass)
override fun consume(message: Message, context: ConsumeContext): Action {
val logMessageId = buildLogMessageId(message)
val body = String(message.body)
try {
log.info(logMessageId + " body: " + body)
val result = consumeInternal(message, context, JSON.parseObject(body, getMessageBodyType(message.tag)))
log.info(logMessageId + " result: " + result.name)
return result
} catch (e: Exception) {
if (message.reconsumeTimes >= 3) {
log.error(logMessageId + " error: " + e.message, e)
}
return Action.ReconsumeLater
}
}
abstract fun getMessageBodyType(tag: String): Type?
abstract fun consumeInternal(message: Message, context: ConsumeContext, obj: Any): Action
protected fun buildLogMessageId(message: Message): String {
return "topic: " + message.topic + "n" +
"consumer: " + consumerId + "n" +
"tag: " + message.tag + "n" +
"key: " + message.key + "n" +
"MsgId:" + message.msgID + "n" +
"BornTimestamp" + message.bornTimestamp + "n" +
"StartDeliverTime:" + message.startDeliverTime + "n" +
"ReconsumeTimes:" + message.reconsumeTimes + "n"
}
}
再定義具體的消費者,並且在消費失敗之後能夠再傳送一次訊息。
@Configuration
@ConfigurationProperties("mqs.ons.consumers.clean-report-push-event-consumer")
@Data
class CleanReportPushEventConsumer(val cleanReportService: CleanReportService,val eventProducer:CleanReportPushEventProducer):AbstractConsumer() {
val logger: Logger = LoggerFactory.getLogger(this.javaClass)
override fun consumeInternal(message: Message, context: ConsumeContext, obj: Any): Action {
if(obj is CleanReportPushEventMessage){
//清除事件
logger.info("consumer clean-report event report_id:${obj.id} ")
//消費失敗之後再傳送一次訊息
if(!cleanReportService.sendCleanReportEvent(obj.id)){
val times = obj.times+1
eventProducer.sendMessage(CleanReportPushEventMessage(obj.id,times))
}
}
return Action.CommitMessage
}
override fun getMessageBodyType(tag: String): Type? {
return CleanReportPushEventMessage::class.java
}
}
其中,cleanReportService 的 sendCleanReportEvent() 會透過 http 的方式呼叫業務方提供的介面,進行事件訊息的推送。如果推送失敗了,則會進行下一次的推送。(這裡使用了 eventProducer 的 sendMessage() 方法再次投遞訊息,是因為要根據呼叫的http介面返回的內容來判斷訊息是否傳送成功。)
最後,定義 ConsumerFactory
@Component
class ConsumerFactory(val consumers: List<AbstractConsumer>,val aliyunOnsOptions: AliyunOnsOptions) {
val logger: Logger = LoggerFactory.getLogger(this.javaClass)
@PostConstruct
fun start() {
CompletableFuture.runAsync{
consumers.stream().forEach {
val properties = buildProperties(it.consumerId!!, it.threadNums)
val consumer = ONSFactory.createConsumer(properties)
if (it.subscribeOptions != null && !it.subscribeOptions!!.isEmpty()) {
for (options in it.subscribeOptions!!) {
consumer.subscribe(options.topic, options.tag, it)
}
consumer.start()
val message = "n".plus(
it.subscribeOptions!!.stream().map{ a -> String.format("topic: %s, tag: %s has been started", a.topic, a.tag)}
.collect(Collectors.toList<Any>()))
logger.info(String.format("consumer: %sn", message))
}
}
}
}
private fun buildProperties(consumerId: String,threadNums: Int?): Properties {
val properties = Properties()
properties.put(PropertyKeyConst.ConsumerId, consumerId)
properties.put(PropertyKeyConst.AccessKey, aliyunOnsOptions.accessKey)
properties.put(PropertyKeyConst.SecretKey, aliyunOnsOptions.secretKey)
if (StringUtils.isNotEmpty(aliyunOnsOptions.onsAddr)) {
properties.put(PropertyKeyConst.ONSAddr, aliyunOnsOptions.onsAddr)
} else {
// 測試環境接入RocketMQ
properties.put(PropertyKeyConst.NAMESRV_ADDR, aliyunOnsOptions.nameServerAddress)
}
properties.put(PropertyKeyConst.ConsumeThreadNums, threadNums!!)
return properties
}
}
總結
正如本文開頭曾介紹過,可以使用多種方式來實現延時訊息。然而,我們的系統本身就大量使用了 RocketMQ,藉助成熟的 RocketMQ 實現延時訊息不失為一種可靠而又方便的方式。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/964/viewspace-2823181/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 直播電商原始碼,利用Kotlin+RocketMQ 實現延時訊息原始碼KotlinMQ
- 延時訊息常見實現方案
- 簡單延時訊息替代改造JOB實現
- SpringBoot整合rabbitMq實現訊息延時傳送Spring BootMQ
- RabbitMQ實現延時訊息的兩種方法MQ
- RocketMQ定時/延時訊息MQ
- 延遲訊息的五種實現方案
- SpringCloud 2020.0.4 系列之 Stream 延遲訊息 的實現SpringGCCloud
- 使用 RabbitMQ 實現延時佇列MQ佇列
- 基於訊息佇列(RabbitMQ)實現延遲任務佇列MQ
- Spring Boot 整合 RabbitMQ 傳送延時訊息Spring BootMQ
- 使用 NSProxy 實現訊息轉發
- C# 使用SignalR實現訊息通知C#SignalR
- 使用Spring Boot實現訊息佇列Spring Boot佇列
- RabbitMQ高階之訊息限流與延時佇列MQ佇列
- golang 封裝 rabbitmq,正常訊息,延時訊息,非炫技,僅記錄(golang新人)Golang封裝MQ
- 短影片電商系統,編寫延遲訊息實現程式碼
- Redis 使用 List 實現訊息佇列能保證訊息可靠麼?Redis佇列
- 實時訊息推送整理
- [Redis]延遲訊息佇列Redis佇列
- 訊息的即時推送——net實現、websocket實現以及socket.io實現Web
- redis應用系列三:延遲訊息佇列正確實現姿勢Redis佇列
- 如果有人再問你怎麼實現分散式延時訊息,這篇文章丟給他分散式
- 使用swoole作為MQTT客戶端並接收實現即時訊息推送MQQT客戶端
- workerman 實現訊息推送
- Go中使用Redis實現訊息佇列教程GoRedis佇列
- 微信小程式+mqtt.js實現實時接收訊息微信小程式MQQTJS
- PHP與反ajax推送,實現的訊息實時推送功能PHP
- StompJS+SpeechSynthesis實現前端訊息實時語音播報JS前端
- 7種 實現web實時訊息推送的方案,7種!Web
- 實時訊息推送方案-SSE
- Redis使用ZSET實現訊息佇列使用總結一Redis佇列
- Redis使用ZSET實現訊息佇列使用總結二Redis佇列
- Redis 應用-非同步訊息佇列與延時佇列Redis非同步佇列
- Java如何實現延時訪問Java
- RabbitMQ延遲訊息的延遲極限是多少?MQ
- RabbitMQ使用 prefetch_count優化佇列的消費,使用死信佇列和延遲佇列實現訊息的定時重試,golang版本MQ優化佇列Golang
- 用redis實現訊息佇列(實時消費+ack機制)Redis佇列