ActiveMQ持久化机制

为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一般都会采用持久化机制,即:若MQ挂了,消息不会消失的机制 。

ActiveMQ的消息持久化机制JDBC、AMQ、KahaDB、LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。

在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或远程数据库等再试图将消息发送给接收者,成功则将消息从存储中删除,失败则继续尝试发送。消息中心启动后首先要检查指定的存储位置,若有未发送成功的消息,则需要把消息发送出去。

AMQ Message Store

AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特定,消息存储在一个个文件中,文件的默认大小为32M,当一个存储文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段这个文件被删除,AMQ适用于ActiveMQ5.3之前的版本。

KahaDB消息存储(默认)

http://activemq.apache.org/kahadb
KahaDB是基于日志文件的持久性数据库,是自ActiveMQ 5.4以来的默认存储机制,可用于任何场景,提高了性能和恢能力,它是基于文件的本地数据库存储形式。它已针对快速持久性进行了优化。KahaDB使用较少的文件描述符,并提供比其前身AMQ消息存储更快的恢复。

ActiveMQ消息持久化_持久性

消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址:(文件在apache-activemq\data\kahadb下)

ActiveMQ消息持久化_数据_02

1、db-<number>.log(主要存数据)是KahaDB存储消息到预定义大小的数据记录文件,文件命名为db-<number>.log,当数据文件已满时,一个新的文件会随之创建,number数值也会随之递增,它随之消息数量的增多,如每32M一个文件,文件名按照数字进行编号,如sb-1.log……当不再有引用到数据文件中的任何消息时,文件会被删除或归档。

2、db.data文件包含了持久化的BTree索引,索引了消息数据记录中的消息,它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-<number>.log里面存储的消息。

3、db.free文件表示当前db.data文件哪些页面是空闲的,文件具体内容是所有空闲页的ID。

4、db.redo文件是用来进行消息恢复,如果KahaDB消息存储在强制退出后启动,用于恢复BTree索引。

5、lock文件表示当前获得KahaDB读写权限的broker

使用

要使用KahaDB作为代理的持久性适配器,请按如下方式配置ActiveMQ(ActiveMQ.xml文件)(示例):

 <broker brokerName="broker">
    <persistenceAdapter>
      <kahaDB directory="activemq-data" journalMaxFileLength="32mb"/>
    </persistenceAdapter>
 </broker>

 可以更改的属性:

属性 默认 说明
archiveCorruptedIndex false 如果true,启动时发现的损坏索引将被归档(未删除)。
archiveDataLogs false 如果true,将邮件数据日志移动到存档目录而不是删除它。
checkForCorruptJournalFiles false 如果true,将在启动时检查损坏的日志文件并尝试恢复它们。
checkpointInterval 5000 检查日志之前的时间(ms)。
checksumJournalFiles true 为日志文件创建校验和。需要存在校验和才能使持久性适配器能够检测损坏的日志文件。在ActiveMQ 5.9.0之前:默认为false
cleanupInterval 30000 连续检查之间的间隔(以毫秒为单位),用于确定哪些日志文件(如果有)可以从邮件存储中删除。符合条件的日志文件是没有未完成引用的文件。
compactAcksAfterNoGC 10 ActiveMQ 5.14.0开始:当启用确认压缩功能时,此值控制必须完成的存储GC周期数,在压缩逻辑被触发之前不会清除其他文件,从而可能将跨日志文件的旧确认压缩到新日志中文件。值设置越低,压缩可能发生得越快,如果经常运行,可能会影响性能。
compactAcksIgnoresStoreGrowth false ActiveMQ 5.14.0开始:当启用确认压缩功能时,此值控制当存储仍在增长时是否运行压缩,或者是否仅在存储停止增长时(由于达到空闲或存储限制而发生)。如果启用,则无论商店仍有空间或处于活动状态,压缩都会运行,这会降低整体性能,但会更快地回收空间。 
concurrentStoreAndDispatchQueues true 启用将Queue消息分派给感兴趣的客户端以与消息存储同时发生。
concurrentStoreAndDispatchTopics false 允许向感兴趣的客户端分派主题消息以与消息存储同时发生。建议不要启用此属性。
directory activemq-data 用于存储消息存储数据和日志文件的目录的路径。
directoryArchive null 定义目录以将数据日志移动到它们包含的所有消息时。
enableAckCompaction true ActiveMQ 5.14.0:此设置控制商店是否将定期压缩仅包含消息确认的旧日记日志文件。通过将这些旧的确认压缩到新的日志日志文件中,可以删除较旧的文件以释放空间并允许消息存储继续操作而不会达到存储大小限制。
enableIndexWriteAsync false 如果true,索引是异步更新的。
enableJournalDiskSyncs true 确保每个日志写入后跟一个磁盘同步(JMS持久性要求)。从ActiveMQ 5.14.0开始,不推荐使用此属性。从ActiveMQ 5.14.0:看journalDiskSyncStrategy
ignoreMissingJournalfiles false 如果true,忽略日记文件丢失的报告。
indexCacheSize 10000 内存中缓存的索引页数。
indexDirectory   ActiveMQ 5.10.0开始:如果设置,则配置将存储KahaDB索引文件(db.data和 db.redo)的位置。如果未设置,索引文件将存储在directory属性指定的目录中  。
indexWriteBatchSize 1000 批量写入的索引数。
journalDiskSyncInterval 1000 何时执行磁盘同步的时间间隔(ms)  journalDiskSyncStrategy=periodic。只有在自上次磁盘同步或日志转到新日志文件后对日志进行了写入时,才会执行同步。
journalDiskSyncStrategy always ActiveMQ 5.14.0:此设置配置磁盘同步策略。可用的同步策略列表(按安全性降低和性能提高的顺序):always确保每个日志写入后跟一个磁盘同步(JMS持久性要求)。这是最安全的选项,但也是最慢的选项,因为它需要在每次写入消息后进行同步。这相当于不推荐使用的属性  enableJournalDiskSyncs=trueperiodic磁盘将以设定的时间间隔(如果发生写入)而不是在每次日志写入之后同步,这将减少磁盘上的负载并且应该提高吞吐量。滚动到新的日志文件时,磁盘也将同步。默认间隔为1秒。默认间隔提供非常好的性能,同时比更安全 never磁盘同步,因为数据丢失的最大值限制为1秒。请参阅journalDiskSyncInterval更改磁盘同步的频率。never永远不会显式调用同步,并且操作系统将刷新到磁盘。这相当于设置deprecated属性enableJournalDiskSyncs=false。这是最快的选择,但是最不安全,因为无法确保何时将数据刷新到磁盘。因此,代理失败时可能会发生消息丢失。
journalMaxFileLength 32mb 提示设置消息数据日志的最大大小。
maxAsyncJobs 10000 将等待存储排队的异步消息的最大数量(应与并发MessageProducers的数量相同)。
preallocationScope entire_journal ActiveMQ 5.14.0:此设置配置如何预分配日记帐数据文件。默认策略使用appender线程在首次使用时预先分配日志文件。 entire_journal_async将在单独的线程中提前使用preallocate。none禁用预分配。在SSD上,使用 entire_journal_async避免在首次使用时延迟写入等待预分配。注意:在HDD上,磁盘的额外线程争用会产生负面影响。因此使用默认值。
preallocationStrategy sparse_file ActiveMQ 5.12.0:此设置配置代理在需要新日志文件时尝试预分配日志文件的方式。sparse_file - 设置文件长度,但不会用任何数据填充它。os_kernel_copy - 将预分配委派给操作系统。zeros  - 每个预分配的日志文件只包含0x00整个文件。
storeOpenWireVersion 11 确定封送到KahaDB日志的OpenWire命令的版本。在ActiveMQ 5.12.0之前:默认值为6。代理的某些功能取决于较新协议修订版中存储在OpenWire命令中的信息,如果将商店版本设置为较低值,这些功能可能无法正常工作。在许多情况下,代理版本大于5.9.0的KahaDB存储仍然可以被代理读取,但会导致代理继续使用较旧的商店版本,这意味着较新的功能可能无法按预期工作。对于在ActiveMQ 5.9.0之前的版本中创建的KahaDB存储,需要手动设置storeOpenWireVersion="6"以便启动代理而不会出现错误。

多kahaDB持久性适配器

ActiveMQ 5.6:可以跨多个kahdb持久性适配器分发目标存储。你什么时候这样做?如果您有一个快速的生产者/消费者目的地和另一个具有不规则批量消耗的定期生产者目的地,那么随着未消耗的消息分布在多个日志文件中,磁盘使用量就会增长。每个单独的日志确保最少的日记使用。此外,某些目标可能很关键并且需要磁盘同步,而其他目标可能不需要。在这些情况下,您可以使用  mKahaDB通配符并使用通配符过滤目标,就像使用目标策略条目一样。

如果目的地是分布式的,则交易可以跨越多个期刊。这意味着需要两个阶段完成,这会产生性能(额外的磁盘同步)惩罚以记录提交结果。只有在交易涉及多个期刊时才会受到处罚。

每个实例  kahaDB都可以独立配置。如果没有向a提供目标filteredKahaDB,则隐式默认值将匹配任何目标,队列或主题。这是一个方便的抓住所有。如果找不到匹配的持久性适配器,则目标创建将失败并出现异常。该filteredKahaDB股份公司的通配符匹配的规则每个目的地政策。

从ActiveMQ 5.15,filteredKahaDB 支持名为的StoreUsage属性usage。这允许对匹配的队列施加单独的磁盘限制。

<broker brokerName="broker">

 <persistenceAdapter>
  <mKahaDB directory="${activemq.base}/data/kahadb">
    <filteredPersistenceAdapters>
      <!-- match all queues -->
      <filteredKahaDB queue=">">
        <usage>
         <storeUsage limit="1g" />
        </usage>
        <persistenceAdapter>
          <kahaDB journalMaxFileLength="32mb"/>
        </persistenceAdapter>
      </filteredKahaDB>
      
      <!-- match all destinations -->
      <filteredKahaDB>
        <persistenceAdapter>
          <kahaDB enableJournalDiskSyncs="false"/>
        </persistenceAdapter>
      </filteredKahaDB>
    </filteredPersistenceAdapters>
  </mKahaDB>
 </persistenceAdapter>

</broker>

自动每目标持久性适配器

设置perDestination="true"catch all,即,当没有设置显式目的地时,filteredKahaDB输入。将为每个匹配目标分配其自己的kahaDB实例。

<broker brokerName="broker">

 <persistenceAdapter>
  <mKahaDB directory="${activemq.base}/data/kahadb">
    <filteredPersistenceAdapters>
      <!-- kahaDB per destinations -->
      <filteredKahaDB perDestination="true">
        <persistenceAdapter>
          <kahaDB journalMaxFileLength="32mb"/>
        </persistenceAdapter>
      </filteredKahaDB>
    </filteredPersistenceAdapters>
  </mKahaDB>
 </persistenceAdapter>

</broker>

指定两者perDestination="true"  queue=">"同一  filteredKahaDB条目尚未经过测试。它可能导致引发以下异常:

Reason: java.io.IOException: File '/opt/java/apache-activemq-5.9.0/data/mKahaDB/lock'
 could not be locked as lock is already held for this jvm`

 LevelDB(适用ActiveMQ 5.8及更高版本

该文件系统是从ActiveMQ5.8之后引进的,它和KahaDB很相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性,但它不再使用自定义B-Tree实现来索引预写日志,而是使用基于LevelDB的索引。其索引具有几个不错的属性:

  • 快速更新(无需进行随机磁盘更新)
  • 并发读取
  • 使用硬链接快速索引快照

KahaDB和LevelDB存储都必须定期执行垃圾收集周期,以确定可以删除哪些日志文件。KahaDB由于增加了存储的数据量并且在收集发生时可能导致读/写停顿,因此可能非常慢。LevelDB存储使用更快的算法来确定何时收集日志文件并避免这些停顿。

可以将ActiveMQ配置为使用LevelDB作为其持久性适配器(ActiveMQ.xml文件) - 如下所示

 <broker brokerName="broker" ... >
    ...
    <persistenceAdapter>
      <levelDB directory="activemq-data"/>
    </persistenceAdapter>
    ...
  </broker>

LevelDB的属性:

财产名称 默认值 说明
directory LevelDB 商店用于保存数据文件的目录。如果目录尚不存在,商店将创建该目录。
sync true 如果设置为false,则存储不会将日志记录操作同步到磁盘
logSize 104857600 (100 MB) 发生日志文件旋转之前每个数据日志文件的最大大小(以字节为单位)。
verifyChecksums false 设置为true以强制对从文件系统读取的所有数据进行校验和验证。
paranoidChecks false 如果检测到内部损坏,请尽快将存储错误输出。
indexFactory org.fusesource.leveldbjni.JniDBFactoryorg.iq80.leveldb.impl.Iq80DBFactory 创建LevelDB索引时要使用的工厂类
indexMaxOpenFiles 1000 索引可以使用的打开文件数。
indexBlockRestartInterval 16 键的delta编码的重启点之间的数字键。
indexWriteBufferSize 6291456 (6 MB) 在转换为已排序的磁盘文件之前在内存中构建的索引数据量。
indexBlockSize 4096 (4 K) 每个块打包的索引数据的大小。
indexCacheSize 268435456 (256 MB) 用于缓存索引块的最大堆外内存量。
indexCompression snappy 要应用于索引块的压缩类型。可以是活泼的或没有。
logCompression none 要应用于日志记录的压缩类型。可以是活泼的或没有。

JDBC消息存储

1、将原来的kshadb的持久化数据的方式更改为jdbc:

        <persistenceAdapter>
            <!--<kahaDB directory="${activemq.data}/kahadb"/>-->
            <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false"/>
        </persistenceAdapter>

注意:dataSource指定将要引用的持久化数据库的bean名称,createTablesOnStartup表示是否在启动时创建数据表,默认值为true,这样每次启动都会创建数据表,一般是第一次启动的时候设置为true之后改为false

2、添加mysql数据库的驱动jar包到activwmq/lib的文件夹下
注意:若使用第三方连接池或是连接器(例如c3p0、druid),应同时将其jar包添加到lib文件夹下

ActiveMQ消息持久化_日志文件_03

3、数据库连接池配置(使用官方自带的连接池),将其配置到activemq.xml配置文件中的</broker>标签之后,<import>标签之前

ActiveMQ消息持久化_持久性_04

官方连接池配置示例: 

<bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>
  <property name="username" value="activemq"/>
  <property name="password" value="activemq"/>
  <property name="poolPreparedStatements" value="true"/>
</bean>

4、自动创建的三张数据表说明

  • activemq_msgs用于存储消息,Queue和Topic都存储在这个表中:
    ID:自增的数据库主键
    CONTAINER:消息的Destination
    MSGID_PROD:消息发送者客户端的主键
    MSG_SEQ:是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
    EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒数
    MSG:消息本体的Java序列化对象的二进制数据
    PRIORITY:优先级,从0-9,数值越大优先级越高
  • activemq_acks用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存:
    主要的数据库字段如下:
    CONTAINER:消息的Destination
    SUB_DEST:如果是使用Static集群,这个字段会有集群其他系统的信息
    CLIENT_ID:每个订阅者都必须有一个唯一的客户端ID用以区分
    SUB_NAME:订阅者名称
    SELECTOR:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作
    LAST_ACKED_ID:记录消费过的消息的ID。
  • activemq_lock在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,
    其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。
    这个表用于记录哪个Broker是当前的Master Broker。

5、队列发送者示例代码(生产者)

package com.hern.avtivemq;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.jms.*;

/*
* 消息生产者
* */
@Service
public class SpringMQ_Produce {

    private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    private static final String QUEUE_NAME = "queue-123";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);
        MessageProducer messageProducer = session.createProducer(queue);
        messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
        for (int i = 0; i < 10; i++) {
            TextMessage textMessage = session.createTextMessage("jdbc  msg---" + i);
            messageProducer.send(textMessage);
        }

        messageProducer.close();
        session.close();
        connection.close();

        System.out.println("发送消息结束");
    }
}

6、队列接收者示例代码(接收者) 

package com.hern.avtivemq;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;

import javax.jms.*;

/*
* 消息消费者
* */
@Service
public class SpringMQ_Consume {

    private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    private static final String QUEUE_NAME = "queue-123";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);
        MessageConsumer messageConsumer = session.createConsumer(queue);
        while (true){
            TextMessage textMessage = (TextMessage) messageConsumer.receive(4000L);
            if (textMessage != null){
                System.out.println("消费者接收到的消息是:" + textMessage.getText());
            }else {
                break;
            }
        }

        messageConsumer.close();
        session.close();
        connection.close();

        System.out.println("接收消息结束");
    }
}

注意:

  • 如果是queue模式,在没有消费者的情况下会将消息保存到activemq_msgs表中,只要有任意一个消费者已经消费过,相应的消息将会立即被删除。
  • 如果是topic模式,一般是先启动消费订阅然后再生产的情况下会将消息保存到activemq_acks表中。
  • 若报错“java.lang.lllegalStateException:BeanFactory not initialized or already closed”这是因为操作系统的机器名中有“_”符号,更改机器名并且重启后即可解决。