消息隊列學習-ActiveMQ(五)
10 ActiveMQ的消息存儲和持久化
10.1 官網
http://activemq.apache.org/persistence
10.2 是什麼
面試題:ActiveMQ持久化機制?
爲了避免意外宕機以後丟失信息,需要做到重啓後可以恢復消息隊列,消息系統一半都會採用持久化機制。
ActiveMQ的消息持久化機制有JDBC,AMQ,KahaDB和LevelDB,無論使用哪種持久化方式,消息的存儲邏輯都是一致的。
就是在發送者將消息發送出去後,消息中心首先將消息存儲到本地數據文件、內存數據庫或者遠程數據庫等。再試圖將消息發給接收者,成功則將消息從存儲中刪除,失敗則繼續嘗試嘗試發送。
消息中心啓動以後,要先檢查指定的存儲位置是否有未成功發送的消息,如果有,則會先把存儲位置中的消息發出去。
10.3 有哪些
10.3.1 AMQ Mesage Store(瞭解)
基於文件的存儲方式,是以前的默認消息存儲,現在不用了。
AMQ是一種文件存儲形式,它具有寫入速度快和容易恢復的特點。消息存儲再一個個文件中文件的默認大小爲32M,當一個文件中的消息已經全部被消費,那麼這個文件將被標識爲可刪除,在下一個清除階段,這個文件被刪除。AMQ適用於ActiveMQ5.3之前的版本
10.3.2 KahaDB消息存儲(默認)
基於日誌文件,從ActiveMQ5.4開始默認的持久化插件,查看詳情
KahaDB是目前默認的存儲方式,可用於任何場景,提高了性能和恢復能力。
消息存儲使用一個事務日誌和僅僅用一個索引文件來存儲它所有的地址。
KahaDB是一個專門針對消息持久化的解決方案,它對典型的消息使用模型進行了優化。
數據被追加到data logs中。當不再需要log文件中的數據的時候,log文件會被丟棄。
vim activemq.xml
KahaDB的存儲原理
KahaDB在消息保存的目錄中有4類文件和一個lock,跟ActiveMQ的其他幾種文件存儲引擎相比,這就非常簡潔了。
- db-<Number>.log
KahaDB存儲消息到預定大小的數據記錄文件中,文件名爲db-<Number>.log。當數據文件已滿時,一個新的文件會隨之創建,number數值也會隨之遞增,它隨着消息數量的增多,如每32M一個文件,文件名按照數字進行編號,如db-1.log,db-2.log····。當不再有引用到數據文件中的任何消息時,文件會被刪除或者歸檔。
- db.data
該文件包含了持久化的BTree索引,索引了消息數據記錄中的消息,它是消息的索引文件,本質上是B-Tree(B樹),使用B-Tree作爲索引指向db-<Number>.log裏面存儲的消息。 - db.free
當前db.data文件裏面哪些頁面是空閒的,文件具體內容是所有空閒頁的ID - db.redo
用來進行消息恢復,如果KahaDB消息存儲再強制退出後啓動,用於恢復BTree索引。 - lock
文件鎖,表示當前kahadb讀寫權限的broker。
10.3.3 JDBC消息存儲
消息基於JDBC存儲的
10.3.4 LevelDB消息存儲(瞭解)
這種文件系統是從ActiveMQ5.8之後引進的,它和KahaDB非常相似,也是基於文件的本地數據庫存儲形式,但是它提供比KahaDB更快的持久性。
但它不使用自定義B-Tree實現來索引獨寫日誌,而是使用基於LevelDB的索引
默認配置如下:
<persistenceAdapter>
<levelDB directory="activemq-data"/>
</persistenceAdapter>
10.4 JDBC存儲消息
- 添加mysql數據庫的驅動包到lib文件夾
使用rz -E
上傳到/myactiveMQ/apache-activemq-5.15.12/lib
- jdbcPersistenceAdapter配置
修改activemq.xml配置文件
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds"/>
</persistenceAdapter>
dataSource是指定將要引用的持久化數據庫的bean名稱。createTableOnStartup是否在啓動的時候創建數據庫表,默認是true,這樣每次啓動都會去創建表了,一般是第一次啓動的時候設置爲true,然後再去改成false。一般是第一次啓動的時候設置爲true,然後再去改成false。
- 數據庫連接池配置
vim activemq.xml
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.0.102:3306/activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxTotal" value="200"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
- 建庫SQL和創表說明
建一個名爲activemq的數據庫
CREATE DATABASE activemq
三張表的說明
- ACTIVEMQ_MSGS:
ID:自增的數據庫主鍵
CONTAINER:消息的Destination
MSGID_PROD:消息發送者的主鍵
MSG_SEQ:是發送消息的順序,MSGID_PROD+MSG_SEQ可以組成JMS的MessageID
EXPIRATION:消息的過期時間,存儲的是從1970-01-01到現在的毫秒數
MSG:消息本體的Java序列化對象的二進制數據
PRIORITY:優先級,從0-9,數值越大優先級越高
消息表,缺省表名ACTIVEMQ_MSGS,Queue和Topic都存在裏面,結構如下:
- ACTIVEMQ_ACKS:
- ACTIVEMQ_LOCK
表ACTIVEMQ_LOCK在集羣環境下才有用,只有一個Broker可以獲取消息,稱爲Master Broker,其他的只能作爲備份等待Master Broker不可用,纔可能成爲下一個Master Broker。這個表用於記錄哪個Broker是當前的Master Broker
如果新建數據庫ok,上述配置ok,代碼運行ok,3張表會自動生成
如果表沒生成,可能需要自己創建
- 代碼運行驗證
注意:一定要開啓持久化:messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
(1)先以隊列爲例
生產者代碼:
public class JmsProduce {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String QUEUE_NAME = "jdbc";
public static void main(String[] args) throws JMSException {
// 1. 創建連接工廠,按照給定的URL地址,採用默認用戶名和密碼
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
// 2. 通過連接工廠,獲得連接connection並啓動
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3. 創建會話session
// 兩個參數,第一個叫事務/第二個叫簽收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4. 創建目的地(具體是隊列還是主題)
Queue queue = session.createQueue(QUEUE_NAME);
// 5. 創建消息的生產者
MessageProducer messageProducer = session.createProducer(queue);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 6. 通過使用messageProducer生產3條消息發送到MQ隊列裏
for (int i = 1; i <= 3; i++) {
// 7. 創建消息
TextMessage textMessage = session.createTextMessage("jdbc msg--->" + i);
// 8. 通過messageProducer發送給mq
messageProducer.send(textMessage);
}
// 9. 關閉資源
messageProducer.close();
session.close();
connection.close();
System.out.println("***********消息發佈到MQ完成");
}
}
消費者代碼:
public class JmsConsumer {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String QUEUE_NAME = "jdbc";
public static void main(String[] args) throws JMSException, IOException {
// 1. 創建連接工廠,按照給定的URL地址,採用默認用戶名和密碼
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
// 2. 通過連接工廠,獲得連接connection並啓動
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3. 創建會話session
// 兩個參數,第一個叫事務/第二個叫簽收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4. 創建目的地(具體是隊列還是主題)
Queue queue = session.createQueue(QUEUE_NAME);
// 5.創建消費者
MessageConsumer messageConsumer = session.createConsumer(queue);
// 通過監聽的方式來消費消息
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("****消費者收到消息:ListenerMsg---" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
先執行生產者代碼:
在執行消費者代碼:
(1)先以隊列爲例
消費者代碼
public class JmsConsumer_Topic_Persist {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String TOPIC_NAME = "Topic-jdbc-Persist";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("****我是李四");
// 1. 創建連接工廠,按照給定的URL地址,採用默認用戶名和密碼
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
// 2. 通過連接工廠,獲得連接connection並啓動
Connection connection = activeMQConnectionFactory.createConnection();
connection.setClientID("李四");
// 3. 創建會話session
// 兩個參數,第一個叫事務/第二個叫簽收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4. 創建目的地(具體是隊列還是主題)
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "jdbc-topic");
connection.start();
Message message = topicSubscriber.receive();
while (null != message) {
TextMessage textMessage = (TextMessage) message;
System.out.println("****收到持久化topic"+textMessage.getText());
message = topicSubscriber.receive();
}
session.close();
connection.close();
}
}
生產者代碼:
public class JmsProduce_Topic_Persist {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String TOPIC_NAME = "Topic-jdbc-Persist";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start();
for (int i = 1; i <= 3; i++) {
TextMessage textMessage = session.createTextMessage("TOPIC_NAME--->" + i);
messageProducer.send(textMessage);
}
messageProducer.close();
session.close();
connection.close();
System.out.println("***********TOPIC_NAME消息發佈到MQ完成");
}
}
先啓動訂閱者,查看ack的那張表,其記錄了訂閱者:
再啓動生產者,查看msg那張表:
發現,對於topic,消費完後不會像queue那樣消失,而是會記錄下來。
- 小總結
- 如果是queue
在沒有消費者消費的情況下會將消息保存到activemq_msgs表中,只要有任意一個消費者消費了,就會刪除消費過的消息刪除消費過的消息 - 如果是topic
一般是先啓動消費訂閱者然後再生產的情況下會將持久訂閱者永久保存到activemq_acks,而消息則永久保存在activemq_msgs,會將持久訂閱者永久保存到activemq_acks一般是先啓動消費訂閱者然後再生產的情況下會將持久訂閱者永久保存到qctivemq_acks,而消息則永久保存在activemq_msgs,消息則永久保存在activemq_msgs
訂閱者最後收到的消息是哪一條在acks表中的訂閱者有一個last_ack_id對應了activemq_msgs中的id字段,這樣就知道訂閱者最後收到的消息是哪一條。
- 開發有坑
在配置關係型數據庫作爲ActiveMQ的持久化存儲方案時,有坑
數據庫jar包
注意把對應版本的數據庫jar或者你自己使用的非自帶的數據庫連接池jar包
createTablesOnStartup屬性
默認爲true,每次啓動activemq都會自動創建表,在第一次啓動後,應改爲false,避免不必要的損失。
java.lang.IllegalStateException: LifecycleProcessor not initialized
確認計算機主機名名稱沒有下劃線
10.5 JDBC Message store with ActiveMQ Journal
10.5.1 是什麼
10.5.2 說明
這種方式克服了JDBC Store的不足,JDBC每次消息過來,都需要去寫庫讀庫。
ActiveMQ Journal,使用高速緩存寫入技術,大大提高了性能。
當消費者的速度能夠及時跟上生產者消息的生產速度時,journal文件能夠大大減少需要寫入到DB中的消息。
舉個例子:
生產者生產了1000條消息,這1000條消息會保存到journal文件,如果消費者的消費速度很快的情況下,在journal文件還沒有同步到DB之前,消費者已經消費了90%的以上消息,那麼這個時候只需要同步剩餘的10%的消息到DB。如果消費者的速度很慢,這個時候journal文件可以使消息以批量方式寫到DB。
10.5.3 配置
vim ../conf/activemq.xml
<persistenceFactory>
<journalPersistenceAdapterFactory
journalLogFiles="4"
journalLogFileSize="32768"
useJournal="true"
useQuickJournal="true"
dataSource="#mysql-ds"
dataDirectory="activemq-data" />
</persistenceFactory>
./activemq restart
,重啓activeMQ,激活配置文件。
啓動上一節同樣的生產者代碼:
但是數據庫中並未立即有記錄:
現在是11點49,等等吧…有了有了,不到10分鐘吧。
10.6 ActiveMQ持久化機制小總結
持久化消息主要指的是:
MQ所在服務器宕機了消息不會丟試的機制。
持久化機制演變的過程:
從最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高性能事務支持)附件,並且同步推出了關於關係型數據庫的存儲方案。ActiveMQ5.3版本又推出了對KahaDB的支持(5.4版本後被作爲默認的持久化方案),後來ActiveMQ 5.8版本開始支持LevelDB,到現在5.9提供了標準Zookeeper+LevelDB集羣化方案。
ActiveMQ消息持久化機制有:
- AMQ:基於日誌文件
- KahaDB:基於日誌文件,從ActiveMQ5.4開始默認使用
- JDBC:於第三方數據庫
- Replicated LevelDB Store:從5.9開始提供了LevelDB和Zookeeper的數據複製方法,用於Master-slave方式的首選數據複製方案。