消息队列学习-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方式的首选数据复制方案。