OffserStore和信息存儲位置
實際運行系統,難免會遇到重新消費某條消息,跳過一段時間內的消息等情況。這些異常情況的處理都和offset有關。本節主要分析存儲位置以及如何根據需要調整offset的值。
首先先來明確一下offset的含義,rocketmq中一種類型的消息會放到一個Topic裏,爲了能夠並行,一般一個Topic會有多個message queue,offset是某個Topic下的一條消息在某個message queue裏的位置,通過offset 的值可以定位到這條消息或者指示Consume從這條消息開始向後繼續處理。
如下是offset的類結構,主要分爲本地文件類型和broker代存類型兩種。對於DefaultMQPushConsume 來說,默認是集羣模式,也就是同一個消費者組裏的多個消費者每個人消費一部分,各自收到的消息內容就不一樣,這種情況下,由broker端存儲和控制offset的值,使用RomoteBrokerOffsetStore 結構。
備註:(Diagrams可以展示父類,實現類可以用實現implemention或者光標移到該類上右擊Browse Type Hierarchy)
在廣播模式下,每個消費者都收到這個topic的全部消息,各個消費者之間互不干擾,rocketmq使用LocalFileOffsetStore,把offset存到本地,offsetStroe 使用json格式存儲,簡單明瞭。
在使用DefaultMQPushConsume 的時候,我們不用關心OffsetStore的事,但是如果是PullConsume,我們就要自己處理OffsetStore了。在上一篇博文中,pullConsume的示例中,代碼把offset存到了內存,沒有持久化存儲,這樣就有可能因爲程序的異常或者重啓而丟失offset,實際應用中並不推薦這麼做。爲了能讓我們看清楚OfsetStore究竟是何物?
1.首先通過廣播模式去消費某條topic 中的消息(LocalFileOffsetStore文件類型是以本地存儲的,必須要是廣播模式下,廣播模式下接受該topic所有的消息,包括歷史消息)
public class ProducerQuickStart {
public static void main(String[] args) throws MQClientException,InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
producer.setInstanceName("instance1");
producer.setRetryTimesWhenSendFailed(3);//重試次數 192.168.138.47 192.168.142.133 192.168.0.102
producer.setNamesrvAddr("192.168.139.151:9876");//多個用;分割 192.168.138.47
producer.start();
for (int i = 0; i < 1; i++) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String format = sdf.format(date);
System.out.println("準備發送:" + format);
Message message = new Message("topicName", String.valueOf(i),format.getBytes());
SendResult sendResult= new SendResult();
try {
sendResult = producer.send(message);
} catch (RemotingException |MQBrokerException | InterruptedException e) {
System.out.println("消息發送失敗:" + sendResult.getMsgId());
e.printStackTrace();
}
System.out.println("key:"+i + "消息的發送結果爲:" + sendResult.toString() + "消息ID爲:" + sendResult.getMsgId());
}
producer.shutdown();
}
}
public class ConsumeQuickStart {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("unique_consume_group_name");
consumer.setNamesrvAddr("192.168.139.151:9876");//
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setMessageModel(MessageModel.BROADCASTING);//默認是集羣模式
consumer.subscribe("topicName",null);
//如果要檢查配置信息
consumer.registerMessageListener((MessageListenerConcurrently) (listMsg, consumeConcurrentlyContext) -> {
byte[] body = listMsg.get(0).getBody();
try {
String ms = new String(body,"utf-8");
System.out.println(Thread.currentThread().getName()+"收到消息:" + ms);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
}
2. 查看LocalFileOffsetStore 本次文件存儲的位置(LocalFileOffsetStore類的源碼)
public final static String LocalOffsetStoreDir = System.getProperty(
"rocketmq.client.localOffsetStoreDir",
System.getProperty("user.home") + File.separator + ".rocketmq_offsets");///Users/humingming/.rocketmq_offsets
this.storePath = LocalOffsetStoreDir + File.separator + //
this.mQClientFactory.getClientId() + File.separator + //
this.groupName + File.separator + //
"offsets.json";//+ 192.168.0.102@DEFAULT/unique_consume_group_name/offsets.json
我這裏地址就是:/Users/humingming/.rocketmq_offsets/192.168.0.102@DEFAULT/unique_consume_group_name/offset.json
所以可以看出其大概組成就是:用戶根目錄 + .rocketmq_offsets + mq服務端IP地址@? + 消費者組名 + offset.json,因爲我消費端起了兩個服務(之前向兩個服務端發送過兩次消息,所以會有兩個json文件)
{
"offsetTable":{{
"brokerName":"humingmingdeMacBook-Pro.local",
"queueId":3,
"topic":"topicName"
}:8,{
"brokerName":"humingmingdeMacBook-Pro.local",
"queueId":1,
"topic":"topicName"
}:12,{
"brokerName":"humingmingdeMacBook-Pro.local",
"queueId":2,
"topic":"topicName"
}:8,{
"brokerName":"humingmingdeMacBook-Pro.local",
"queueId":0,
"topic":"topicName"
}:31
}
}
// 可以看出當前消費端已經消費 各個隊列最大offset {key:messagqqueue value:bigOffset}
值得注意的一點就是:因爲廣播模式下是offset存儲在本地,當有消息發送過來時,此時服務端只要沒收到消息或者消息接受失敗,自然是不會去更改本地存儲的OffsetStore 的json文件的,所以你下一次消息消費時候,會先去看該消息隊列的最大offset,然後在看本地json文件的該消息隊列的value值,如果相同則認爲沒有新的消息,如果隊列的offset > json文件該隊列的value 則認爲有新的消息過來,則會去更新或者新增此文件,所以我們可以主動去更改json文件的value值來讓消費端收不到消息或者重複消費之前消費過的消息。目前據我所知道這僅僅限定於廣播模式。
那麼問題來了,因爲我們之前有過利用pull模式去拉取消息,但是當時僅僅是將每個隊列的offset存於內存中,比如(這裏是存於map)
package rocketmq.day04;
import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.message.MessageQueue;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author heian
* @create 2020-01-10-11:14 上午
* @description
*/
public class OffsetPersistence {
private ConcurrentHashMap<MessageQueue, AtomicLong> offsetTable = new ConcurrentHashMap<>();
private long getMessageQueueOffset(MessageQueue mq) {
AtomicLong atomicLong = offsetTable.get(mq);
if (atomicLong != null)
return atomicLong.get();
return 0;
}
private void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, new AtomicLong(offset));
}
public void pullMsg() throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
consumer.setNamesrvAddr("192.168.139.151:9876");// 192.168.0.102
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
for (int j=0;j<=0;j++){//不斷輪詢 演示的話就調一次算了
for (MessageQueue mq : mqs) {
SINGLE_MQ:
while (true) {
try {
//第一次拉去將獲得隊列的所有消息寫入到數據庫,保存當時的 nextBeginOffset,
//下一次輪詢的時候,在與該隊列比較其nextBeginOffset 如果變化了,則說明有消息進來了,則返回FOUND狀態
//我這裏是100 一拉取 假設某條隊列的nextBeginOffset=12 則會去拉去兩次 一次拉取8 狀態爲FOUND,第二次拉取4 狀態爲FOUND 第三次拉去0 狀態爲NO_NEW_MSG
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 100);
System.out.println("拉取結果:"+pullResult + "隊列爲:" +mq );
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());//可以存在數據庫或者redis中 持久化
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> listMsg = pullResult.getMsgFoundList();
for (int i = 0; i < listMsg.size(); i++) {
byte[] body = listMsg.get(i).getBody();
try {
String ms = new String(body,"utf-8");
//System.out.println("收到消息:" + ms);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws MQClientException {
OffsetPersistence offsetPersistence = new OffsetPersistence();
offsetPersistence.pullMsg();
}
}
那麼我們怎麼將內存中的map持久化到硬盤中呢,其實參考源碼LocalFileOffsetStore類也不難,無非就是將map轉爲字符串,將字符串寫入到file.json中,所以改造下如下:
package rocketmq.day04;
import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.consumer.store.OffsetSerializeWrapper;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.MixAll;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.message.MessageQueue;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author heian
* @create 2020-01-10-11:14 上午
* @description
*/
public class OffsetPersistence2 {
private ConcurrentHashMap<MessageQueue, AtomicLong> offsetTable = new ConcurrentHashMap<>();
//offsetStore 持久化地址 這裏我就寫死了
private static final String groupName="pull_consume_group";
private static final String storePath = "/Users/humingming/.rocketmq_offsets/"+groupName+"/offset.json";
private long getMessageQueueOffset(MessageQueue mq) {
AtomicLong atomicLong = offsetTable.get(mq);
if (atomicLong != null)
return atomicLong.get();
return 0;
}
/**
* 通過message和offset 得到map (存在此條消息則put增加,不存在則去set值更新)
*/
public void putMessageQueueOffset(final MessageQueue mq,long offset){
if (mq != null){
AtomicLong offsetOld = this.offsetTable.get(mq);
if (offsetOld == null){
this.offsetTable.put(mq,new AtomicLong(offset));
}else {
offsetOld.set(offset);
}
}
}
//將set集合中的消息隊列與公共變量的消息隊列求並集,並存到對應的磁盤中
public void persistAll(Set<MessageQueue> mqs){
if (null == mqs || mqs.isEmpty()){
return;
}
OffsetSerializeWrapper offsetSerializeWrapper = new OffsetSerializeWrapper();
//map中的示例逐個與set集合中比較,有則將map中的消息存入 offsetSerializeWrapper(實際上就是一個可以序列化的map)
for (Map.Entry<MessageQueue,AtomicLong> entry:this.offsetTable.entrySet()){
if (mqs.contains(entry.getKey())){
AtomicLong offset = entry.getValue();
offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset);
}
}
String jsonStr = offsetSerializeWrapper.toJson();
if (jsonStr != null){
try {
MixAll.string2File(jsonStr,this.storePath);
}catch (IOException e){
e.printStackTrace();
}
}
}
public void pullMsg() throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
consumer.setNamesrvAddr("192.168.139.151:9876");// 192.168.0.102
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
for (int j=0;j<=0;j++){
for (MessageQueue mq : mqs) {
SINGLE_MQ:
while (true) {
try {
//第一次拉去將獲得隊列的所有消息寫入到數據庫,保存當時的 nextBeginOffset,
//下一次輪詢的時候,在與該隊列比較其nextBeginOffset 如果變化了,則說明有消息進來了,則返回FOUND狀態
//我這裏是100 一拉取 假設某條隊列的nextBeginOffset=12 則會去拉去兩次 一次拉取8 狀態爲FOUND,第二次拉取4 狀態爲FOUND 第三次拉去0 狀態爲NO_NEW_MSG
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 100);
System.out.println("拉取結果:"+pullResult + "隊列爲:" +mq );
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());//將隊列信息存於map
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> listMsg = pullResult.getMsgFoundList();
for (int i = 0; i < listMsg.size(); i++) {
byte[] body = listMsg.get(i).getBody();
try {
String ms = new String(body,"utf-8");
//System.out.println("收到消息:" + ms);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//持久化
persistAll(mqs);
}
}
public static void main(String[] args) throws MQClientException {
OffsetPersistence2 offsetPersistence = new OffsetPersistence2();
offsetPersistence.pullMsg();
}
}
所以會在/Users/humingming/.rocketmq_offsets/pull_consume_group 生成一個json文件 如下:
控制檯打印信息如下:
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=2]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=2]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=12]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=1]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=1]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=32, minOffset=0, maxOffset=38, msgFoundList=32]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=38, minOffset=0, maxOffset=38, msgFoundList=6]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=38, minOffset=0, maxOffset=38, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=3]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=3]