当ActiveMQ接收到PERSISTENT Message消息后就需要借助持久化方案来完成PERSISTENT Message的存储。这个介质可以是磁盘文件系统、可以是ActiveMQ的内置数据库,还可以是某种外部提供的关系型数据库。
- 所有PERSISTENT Message都要执行持久化存储操作,持久化存储操作方案的性能直接影响着整个MQ服务端的PERSISTENT Message吞吐性能。另外NON_PERSISTENT Message虽然不会进行持久化存储,但是NON_PERSISTENT Message也不是永远都只存在与内存区域。
- Topic模式的工作队列在没有任何活动订阅者的情况下也会对PERSISTENT Message进行持久化存储吗?当然会,因为Topic模式的工作队列还要考虑“Durable Topic Subscribers”形式的订阅者。即使没有“Durable Topic Subscribers”形式的订阅者,先存储再标记的过程也不会改变(只是不一定真正进入物理磁盘)。
- 在客户端启动事务的情况下,如果没有事务的commit操作,PERSISTENT Message也会进行持久化存储吗?当然还是会,没有做事务的commit只是说这些事务中的消息不会进行确认操作,不会分发到某个指定的具体队列中;但是只要使用了send方法,PERSISTENT Message就会被发送到服务端,就会进行持久化存储操作。
- 在ActiveMQ设置的持久化方案完成某条消息的持久化后,会在ActiveMQ服务节点的内部发出一个“完成”信号。这是为了告诉ActiveMQ服务节点自己,是否可以进行下一步操作。但是为了加快ActiveMQ服务节点内部的处理效率,这个过程可以设置为“异步”。
- 那么进行了持久化存储的PERSISTENT Message什么时候被删除呢?就如同之前我们提到的一样,ActiveMQ服务端只有在收到消费者端某一条消息或某一组消息的ACK标示后,才会认为消息被消费者端正确处理了。就是在这个时候,ActiveMQ会通知持久化方案,进行删除这一条或者这一组消息的操作,并空闲出相应的存储空间。
存储方案配置
- ActiveMQ的内核是Java编写的,也就是说如果服务端没有Java运行环境ActiveMQ是无法运行的。ActiveMQ启动时,启动脚本使用wrapper包装器来启动JVM。JVM相关的配置信息在启动目录的“wrapper.conf”配置文件中。设置JVM的初始内存大小和最大内存大小,如下:
# Initial Java Heap Size (in MB)
wrapper.java.initmemory=2048
# Maximum Java Heap Size (in MB)
wrapper.java.maxmemory=2048
- 明确了ActiveMQ的内存区域来源,才好理解后续的设置。ActiveMQ每一个服务节点都是一个独立的进程。在ActiveMQ主配置文件中,读者可以找到一个“systemUsage”标记,类似定义如下:
<systemUsage>
<systemUsage>
<memoryUsage>
<memoryUsage percentOfJvmHeap="70" />
</memoryUsage>
<storeUsage>
<storeUsage limit="100 gb"/>
</storeUsage>
<tempUsage>
<tempUsage limit="50 gb"/>
</tempUsage>
</systemUsage>
</systemUsage>
systemUsage:该标记用于设置整个ActiveMQ节点在进程级别的各种“容量”的设置情况。其中可设置的属性包括:
- sendFailIfNoSpaceAfterTimeout,当ActiveMQ收到一条消息时,如果ActiveMQ这时已经没有多余“容量”了,那么就会等待一段时间(这里设置的毫秒数),如果超过这个等待时间ActiveMQ仍然没有可用的容量,那么就拒绝接收这条消息并在消息的发送端抛出javax.jms.ResourceAllocationException异常。
- sendFailIfNoSpace,当ActiveMQ收到一条消息时,如果ActiveMQ这时已经没有多余“容量”了,就直接拒绝这条消息(不用等待一段时间),并在消息的发送端抛出javax.jms.ResourceAllocationException异常。
memoryUsage:该子标记设置整个ActiveMQ节点的“可用内存限制”。这个值不能超过上文中您设置的JVM maxmemory的值。其中:
- percentOfJvmHeap属性表示使用“百分数值”进行设置。
- limit属性进行固定容量授权,例如:limit=”1000 mb”。这些内存容量将供所有队列使用。
storeUsage:该标记设置整个ActiveMQ节点,用于存储“持久化消息”的“可用磁盘空间”。该子标记的limit属性必须要进行设置。在使用后续介绍的KahaDB方案或者LevelDB方案进行PERSISTENT Message持久化存储时,这个storeUsage属性都会起作用;但是如果使用其他数据库存储方案,这个属性就不会起作用了。
tempUsage:在ActiveMQ 5.X+ 版本中,一旦ActiveMQ服务节点存储的消息达到了memoryUsage的限制,NON_PERSISTENT Message就会被转储到 temp store区域。虽然我们说过NON_PERSISTENT Message不进行持久化存储,但是ActiveMQ为了防止“数据洪峰”出现时NON_PERSISTENT Message大量堆积致使内存耗尽的情况出现,还是会将NON_PERSISTENT Message写入到磁盘的临时区域——temp store。这个子标记就是为了设置这个temp store区域的“可用磁盘空间限制”。最后提醒各位读者storeUsage和tempUsage并不是“最大可用空间”,而是一个阀值。
ActiveMQ中持久化存储方案的演化
说到ActiveMQ中持久化存储方案的演化问题,如果您仔细阅读ActiveMQ官方文档中关于持久化部分的描述,您就不难发现ActiveMQ的开发团队在针对持久化性能问题的优化上可谓与时俱进。这也符合一款健壮软件的生命周期特征:任何功能特性都在进行不断累计完善:
从最初的AMQ Message Store方案,到ActiveMQ V4版本中推出的High performance journal(高性能事务支持)附件 ,并且同步推出了关于关系型数据库的存储方案。ActiveMQ 5.3版本中又推出了对KahaDB的支持(V5.4版本后称为ActiveMQ默认的持久化方案),后来ActiveMQ V5.8版本开始支持LevelDB,到现在,V5.9+版本提供了标准的Zookeeper+LevelDB集群化方案。
对于最初的AMQ Message Store方案,ActiveMQ官方已不再推荐使用(实际上在笔者的实际工作中,也不会使用AMQ Message Store)。如果各位读者想进行了解可以自行搜索相关资料,这里不再进行介绍。
KahaDB存储方案
KahaDB基本结构
KahaDB is a file based persistence database that is local to the message broker that is using it. It has been optimised for fast persistence and is the the default storage mechanism from ActiveMQ 5.4 onwards. KahaDB uses less file descriptors and provides faster recovery than its predecessor, the AMQ Message Store.
以上引用自Apache ActiveMQ 官方对KahaDB的定义。首先KahaDB基于文件系统,其次KahaDB支持事务。在ActiveMQ V5.4版本及后续版本KahaDB都是ActiveMQ的默认持久化存储方案。最后Apache ActiveMQ官方表示它用来替换之前的AMQ Message Store存储方案。
KahaDB主要元素包括:一个内存Metadata Cache用来在内存中检索消息的存储位置、若干用于记录消息内容的Data log文件、一个在磁盘上检索消息存储位置的Metadata Store、还有一个用于在系统异常关闭后恢复Btree结构的redo文件。如下图所示(官网引用):
以下是KahaDB在磁盘文件上的现实展示。注意,可能您查看自己测试实例中所运行的KahaDB,看到的效果和本文中给出的效果不完全一致。例如您的data log文件可能叫db-1.log,也有可能会多出一个db.free的文件,但是这些都不影响我们对文件结构的分析:
[root@localhost KahaDB]# ll -h
总用量 29M
-rw-r--r--. 1 root root 32M 4月 7 04:53 db-3.log
-rw-r--r--. 1 root root 7.6M 4月 7 04:53 db.data
-rw-r--r--. 1 root root 2.8M 4月 7 04:53 db.redo
-rw-r--r--. 1 root root 8 4月 7 04:50 lock
- db-3.log:这个文件就是我们上文提到的Data log文件。一个KahaDB中,可能同时存在多个Data log文件,他们存储了每一条持久化消息的真正内容。这些Data log文件统一采用db-*.log的格式进行命名,并且每个Data log文件默认的大小都是32M(当然是可以进行设置的)。当一个Data log文件中的所有消息全部被成功消息后,这个Data log文件会在Metadata Cache中被标记为删除,并在下个checkpoint周期进行删除操作。
- 各位读者可能已经注意到一个现象:为什么db-3.log的默认占用大小就是32M,但是目录显示的“总用量”却只有29M呢?在这个文件夹中,除了db-3.log文件本身,加上其他几个文件所占用的大小,已经远远超过了32M!这是因为,为了加快写文件的性能,Data log文件采用顺序写的方式进行操作,为了保证文件使用的扇区在物理上是连续的,所以Data log文件需要预占这些扇区(这个和Hadoop中每一个block大小都是固定的原因相似)。虽然您看到Data log文件占用的32M的磁盘空间,但是这些磁盘空间并没有全部使用。另外,关于随机读写和连续读写的巨大性能差异,我会在今年下半年新的“数据存储专栏”中,进行详细介绍。
- 为了更快的找到某个具体消息在Data log文件中的具体位置。消息的位置索引采用BTree的结构被存储在内存中,这个内存区域就是上文提到的Metadata Cache(大小也是可以设置的)。要知道MySQL的Innodb 存储引擎也是采用BTree结构构造索引结构(用了都说快哦~~)。所以一般情况下,只要某个队列有活动的消费者存在,消息的定位、读取操作是可以很快完成的。
- 内存中没有被处理的消息索引会以一定的周期(或者一定的数量规模)为依据,同步(checkpoint)到Metadata Store中,这就是我们在上文中看到的“db.data”文件。当然db.redo文件也会被更新,以便在ActiveMQ服务节点在重启后对Metadata Cache进行恢复。最后,消息同步(checkpoint)依据,可以在ActiveMQ的主配置文件中进行设置。
在ActiveMQ中配置KahaDB
由于在ActiveMQ V5.4+的版本中,KahaDB是默认的持久化存储方案。所以即使您不配置任何的KahaDB参数信息,ActiveMQ也会启动KahaDB。这种情况下,KahaDB文件所在位置是您的ActiveMQ安装路径下的/data/${broker.Name}/KahaDB子目录。其中${broker.Name}代表这个ActiveMQ服务节点的名称。
正式的生产环境还是建议您在主配置文件中明确设置KahaDB的工作参数。如下所示:
......
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}">
......
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
......
</broker>
......
以上配置项设置使用kahaDB为持久化存储方法,并且设置kahaDB的工作目录为ActiveMQ安装路劲下/data/kahadb目录。如果您需要Data log文件默认的32M的大小,可以使用journalMaxFileLength属性进行设置,如下所示:
......
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}">
......
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb" journalMaxFileLength="64mb"/>
</persistenceAdapter>
......
</broker>
......
您还可以设置为:当Metadata Cache中和Metadata Store中不同的索引条数达到500条时,就进行checkpoint同步。如下所示:
......
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}">
......
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb" journalMaxFileLength="64mb" indexWriteBatchSize="500"/>
</persistenceAdapter>
......
</broker>
......
以下表格为读者示例了KahaDB中所有的配置选项和其含义(引用自网络,加“*”部分是笔者认为重要的配置选项):
property name | default value | Comments |
*directory | activemq-data | 消息文件和日志的存储目录 |
*indexWriteBatchSize | 1000 | 当Metadata cache区域和Metadata store区域不同的索引数量达到这个值后,Metadata cache将会发起checkpoint同步 |
*indexCacheSize | 10000 | 内存中,索引的页大小。超过这个大小Metadata cache将会发起checkpoint同步 |
*enableIndexWriteAsync | false | 索引是否异步写到消息文件中,将以不要设置为true |
*journalMaxFileLength | 32mb | 一个消息文件的大小 |
*enableJournalDiskSyncs | true | 如果为true,保证使用同步写入的方式持久化消息到journal文件中 |
*cleanupInterval | 30000 | 清除(清除或归档)不再使用的db-*.log文件的时间周期(毫秒)。 |
*checkpointInterval | 5000 | 写入索引信息到metadata store中的时间周期(毫秒) |
ignoreMissingJournalfiles | false | 是否忽略丢失的journal文件。如果为false,当丢失了journal文件时,broker启动时会抛异常并关闭 |
checkForCorruptJournalFiles | false | 检查消息文件是否损坏,true,检查发现损坏会尝试修复 |
checksumJournalFiles | false | 产生一个checksum,以便能够检测journal文件是否损坏。 |
- 5.4版本之后有效的属性:
property name | default value | Comments |
*archiveDataLogs | false | 当为true时,归档的消息文件被移到directoryArchive,而不是直接删除 |
*directoryArchive | null | 存储被归档的消息文件目录 |
databaseLockedWaitDelay | 10000 | 在使用负载时,等待获得文件锁的延迟时间,单位ms |
maxAsyncJobs | 10000 | 等待写入journal文件的任务队列的最大数量。应该大于或等于最大并发producer的数量。配合并行存储转发属性使用。 |
concurrentStoreAndDispatchTopics | false | 如果为true,转发消息的时候同时提交事务 |
concurrentStoreAndDispatchQueues | true | 如果为true,转发Topic消息的时候同时存储消息的message store中 |
- 5.6版本之后有效的属性:
property name | default value | Comments |
archiveCorruptedIndex | false | 是否归档错误的索引到Archive文件夹下 |
- 5.10版本之后有效的属性:
property name | default value | Comments |
IndexDirectory | | 单独设置KahaDB中,db.data文件的存储位置。如果不进行设置,db.data文件的存储位置还是将以directory属性设置的值为准 |
LevelDB存储方案
LevelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库,由Google发起并开源。LevelDB只能由本操作系统的其他进程调用,所以它不具有网络性。如果您需要网络上的远程进程操作LevelDB,那么就要自行封装服务层。
LevelDB基本结构
LevelDB中的核心设计算法是跳跃表(Skip List),核心操作策略是对磁盘上的数据日志结构进行归并(LSM)。跳跃表实际上是二叉平衡树的一种变形结构,它通过将一个有序链表进行“升维”操作,从而减少每一层上需要遍历的数据数量,达到快速查找的目的。下图示意了一个跳跃表结构(在实际工作中,跳跃表的层级和“升维”策略的不同,跳跃表的结构也不一样):
您可以将上图中的每个元素节点,想象成每一条消息的key值。为了讲解方便,上图中我将拥有全部数据的元素的跳跃层称为Level 2(最高层),但实际上规范的跳跃表结构中,拥有全部元素的层次称为Level 0(最底层)。跳跃表的结构并非一成不变,当有一条新的记录需要插入到结构时,可能会引起表中的多个Level都发生变化。
那么LevelDB是如何应用跳跃表结构的?又是如何进行归并操的?我们首先来看看LevelDB的简要结构:
- Log文件
当LevelDB收到新的消息是会同步写两个地方:内存中的MemTable区域和磁盘上的Log文件。直接写Log文件是为了在系统异常退出并重启时,能够将LevelDB恢复到退出前的结构;那么有的读者会问,由于是直接写磁盘会不会成为性能瓶颈呢?答案是,LevelDB的log文件操作采用预占磁盘空间(默认为100MB),进行顺序写的方式。并且这个过程可以设置为异步的(当然如果设置成异步的,可能需要接受异常情况下数据丢失的风险)。
- MemTable和Immutable
LevelDB还写将消息写入内存的MemTable区域,MemTable区域的的数据组织结构就是跳跃表(Skip List),这样的数据组织结构可以在读取内存中信息的时候,快速完成信息定位。当MemTable区域的数据量达到indexWriteBufferSize属性设置的大小时(默认为6MB),LevelDB就会把这个MemTable区域标记为Immutable,并开启一个新的MemTable区域。一定注意,是标记为Immutable,而不是把MemTable区域的数据拷贝到某一个Immutable区域。
新标记的Immutable区域中的数据会被执行Compact操作,从而写入到磁盘上的.sst文件中。所谓Compact操作是指:LevelDB会剔除Immutable区域中那些已经被标示为“删除”的数据(成功消费的数据就会被标记为“删除”),排除那些格式错误的数据,并可能进行数据压缩。
- SSTable文件
SSTable文件是指存在于硬盘上,后缀名为.sst的文件。这些文件是LevelDB磁盘上最重要的数据记录文件,每一个SSTable文件的默认大小为2MB,也就是说LevelDB的文件夹下会有很多的.sst文件。SSTable文件并不是顺序写的,而是按照数据的key排序进行随机写,所以SSTable文件无需预占存储磁盘存储空间。
借鉴于跳跃表的设计思想,SSTable文件也是分层次的。每一层可存储的数据量是上一层的的10倍。举个例子,第Level 2层可存储的数据量80MB,那么第Level 3层可存储的数据量就是800MB。当某一层可存储的数据量达到最大值,LevelDB就会从当层选取一个.sst文件,向下层做Compact操作,由于来自于上层的新数据,所以下层的.sst文件内容将产生变化(上文说过,.sst文件中的内容是按照数据的key排序的)。
每一个SSTable文件,由多个Block块构成(默认大小为4KB),block块是LevelDB读写磁盘上SSTable文件的最小单元。每一个SSTable文件最后一个Block块称为Index Block,它指明了SSTable文件中每一个Data block的起始位置。
但是每次读取某个Block块时,如果都在磁盘上先去找Index Block,然后再根据其中记录的index,找到Block在文件的起始位置的话,查找效率显然不高。所以LevelDB的内存区域中,有一个称为Block Cache的区域。这个区域存储着众多的Index Block,这样就不需要到磁盘上查找Index Block了。
- Manifest
那么众多的.sst文件是如何被管理的呢?要知道如果在众多.sst文件中进行某条消息的查找时,如果将某一层的.sst文件全部进行遍历,那么性能肯定是不能接受的。在LevelDB中有一类文件被称为Manifest,这些Manifest文件记录了sst文件的关键信息,包括(但不限于):某个.sst文件属于哪一个Level、这个.sst文件中最小的key值、这个.sst文件中最大的key值。
在ActiveMQ中配置LevelDB
在ActiveMQ中配置使用LevelDB作为持久化存储方案实际上很简单,使用主配置文件中的persistenceAdapter标记就可以完成。最简配置如下所示:
......
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}">
......
<persistenceAdapter>
<levelDB directory="${activemq.data}/levelDB"/>
</persistenceAdapter>
......
</broker>
......
以上示例配置中,directory属性表示LevelDB的结构文件所放置的目录位置。请注意,由于log文件是顺序写的机制,所以log文件也会预占磁盘空间,并且log文件默认的大小就是100MB。那么只要生成一个log文件,就至少会占据100MB的存储空间(但这不代表总的已使用量)。也就是说,如果您将主配置文件中storeUsage标记的limit属性设置为200mb,那么透过ActiveMQ管理界面看到的现象就是:只要有任何一条PERSISTENT Message被接受,Store percent used立刻就会变成50%。如果您将storeUsage标记的limit属性为100mb,那么只要有任何一条PERSISTENT Message被接受,ActiveMQ服务端的Producer Flow Control策略就会立刻开始工作。
所以一定不要吝啬分配memoryUsage、storeUsage。依据您的团队在生产环境下的存储方案,也可以通过logSize属性改变LevelDB中单个log文件的大小。如下示例:
......
<!-- 限制成50mb -->
<persistenceAdapter>
<levelDB directory="${activemq.data}/levelDB" logSize="52428800"/>
</persistenceAdapter>
......
上一小节我们介绍到,默认的LevelDB存储策略中,当ActiveMQ接收到一条消息后,就会同步将这条消息写入到log文件中,并且同时在内存区域向Memtable写入位置索引。通过配置您也可以将这个过程改为“异步”:
......
<!-- 改为异步写log文件 -->
<persistenceAdapter>
<levelDB directory="${activemq.data}/levelDB" logSize="52428800" sync="false"/>
</persistenceAdapter>
......
以下列表展示了您可以使用的LevelDB的配置属性,使用“*”标识出来的属性是笔者认为重要的配置项:
property name | default value | Comments |
*directory | “LevelDB” | 数据文件的存储目录 |
sync | true | 是否进行磁盘的同步写操作 |
*logSize | 104857600 (100 MB) | log日志文件的最大值 |
verifyChecksums | false | 是否对从文件系统中读取的数据进行强制校验校验 |
paranoidChecks | false | 如果LevelDB检测到数据错误,则尽快将错误在存储位置进行标记 |
indexFactory | org.fusesource.leveldbjni.JniDBFactory, org.iq80.leveldb.impl.Iq80DBFactory | 创建LevelDB时使用的工厂类,由于LevelDB的本质是C++程序库,所以Java是通过Jni进行底层调用的 |
*indexMaxOpenFiles | 1000 | 可供索引使用的打开文件的数量,这是因为Level内部使用了多线程进行文件读写操作 |
indexWriteBufferSize | 6291456 (6 MB) | 内存MemTable的最大值,如果MemTable达到这个值,就会被标记为Immutable |
indexBlockSize | 4096 (4 K) | 每个读取到内存的SSTable——Index Block数据的大小 |
*indexCacheSize | 268435456 (256 MB) | 使用一个内存区域记录多个Level中,SSTable——Index Block数据,以便读操作时,不经过遍历就可直接定位数据在某个level中的位置,建议增大该区域 |
indexCompression | snappy | 适用于索引块的压缩类型,影响Compression策略 |
logCompression | none | 适用于日志记录的压缩类型,影响Compression策略 |
关系型数据库存储方案
从ActiveMQ V4版本开始,ActiveMQ就支持使用关系型数据库进行持久化存储——通过JDBC实现的数据库连接。可以支持的关系型数据库包括(但不限于):Apache Derby、DB2、HSQL、Informix、MySQL、Oracle、Postgresql、SQLServer、Sybase。
下面向各位读者演示如何为ActiveMQ配置Mysql数据库服务。前提是您已经某个网络位置准备好了Mysql服务,并可以成功进行远程登录。
......
<broker>
......
<!-- 配置ActiveMQ连接到Mysql服务 -->
<!-- 记得去掉原来的KahaDB或者LevelDB的配置 -->
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql_datasource" createTablesOnStartup="true"/>
</persistenceAdapter>
......
</broker>
<!-- 演示使用的是C3p0连接池,当然您也可以使用DBCP连接池 -->
<!-- 就是spring的配置文件结构 -->
<bean id="mysql_datasource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/activemqdb?relaxAutoCommit=true&useUnicode=true&characterEncoding=utf-8"/>
<property name="user" value="root"/>
<property name="password" value="123456"/>
<property name="minPoolSize" value="10"/>
<property name="maxPoolSize" value="30"/>
<property name="initialPoolSize" value="10"/>
</bean>
......
在配置关系型数据库作为ActiveMQ的持久化存储方案时,要注意几个事项:
- 配置信息建议放置在您的jetty.xml配置文件中,也可以放置在activemq.xml配置文件中。除此之外,还要记得需要使用到的相关jar文件放置到ActiveMQ安装路径下的./lib目录。例如使用mysql + c3p0的配置中,需要的jar包至少包括:mysql-jdbc驱动的jar包和c3p0的jar包。
- 在jdbcPersistenceAdapter标签中,我们设置了createTablesOnStartup属性为true,这是为了在第一次启动ActiveMQ时,ActiveMQ服务节点会自动创建所需要的数据表。启动完成后,可以去掉这个属性,或者更改createTablesOnStartup属性为false。
- 在配置和测试的过程中,您可以会遇到这样的问题:“Java.lang.IllegalStateException: BeanFactory not initialized or already closed”这是因为您的操作系统的机器名中有“_”符号。更改机器名并且重启后,即可解决问题。
在同样的硬件资源条件下,相比KahaDB和LevelDB这样的“内存+存储介质”这样的持久化方案而言,使用关系型数据库作为ActiveMQ的持久化方案绝对不能说“性能”最好,但是在大多数情况下这个持久化方案也不会成为整个顶层架构的设计瓶颈(因为关系型数据库一般都有自己的热备和负载方案)。所以很多团队还是会使用这样的持久化方案,很大一部分原因就是这些团队对关系型数据库有更丰富的使用经验,且有专门的数据库管理人员。