<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; NoSQL</title>
	<atom:link href="https://blog.gmem.cc/tag/nosql/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>KeyDB学习笔记</title>
		<link>https://blog.gmem.cc/keydb-study-note</link>
		<comments>https://blog.gmem.cc/keydb-study-note#comments</comments>
		<pubDate>Tue, 21 Jan 2020 02:58:48 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35505</guid>
		<description><![CDATA[<p>简介 KeyDB是Redis的替代品，宣称是世界上最快的NoSQL数据库，比Redis快5倍。KeyDB完全遵循Redis的协议，可以无缝的从Redis切换。 KeyDB支持多主复制、跨Region的水平扩容，支持垂直扩容。 多主复制 KeyDB支持多个保持同步的主节点，这些主节点都可以接受读、写请求。主节点可以括Region分布。不需要哨兵节点进行监控。 MVCC支持 KeyDB通过MVCC实现无锁（不需等待）的并发操作、后台保存。 ACID兼容 得益于KeyDB的MVCC支持： 事务、查询是非阻塞（不会有锁）的，同时具有原子性保证 未来将支持事务回滚 多线程 KeyDB是完全的多线程的，不像Redis那样仅仅在I/O上支持并发，这意味着它可以充分利用多核心。 这个特性让KeyDB的吞吐量比Redis 5大5倍，Redis 6大3倍。再启用TLS的情况下，吞吐量可以比Redis大7倍。 垂直扩容 多线程特性让KeyDB能最大化利用单机能力，这意味着通过配置能实现水平扩容。 单个KeyDB能够充分利用10（不使用TLS）-16（使用TLS）核心。 其它特性 更好的过期处理 支持将一个set中的所有键一起过期，过期时的删除操作是接近实时完成的，不会有延迟。 <a class="read-more" href="https://blog.gmem.cc/keydb-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/keydb-study-note">KeyDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>KeyDB是<a href="/redis-study-note">Redis</a>的替代品，宣称是世界上最快的NoSQL数据库，比Redis快5倍。KeyDB完全遵循Redis的协议，可以无缝的从Redis切换。</p>
<p>KeyDB支持多主复制、跨Region的水平扩容，支持垂直扩容。</p>
<div class="blog_h2"><span class="graybg">多主复制</span></div>
<p>KeyDB支持多个保持同步的主节点，这些主节点<span style="background-color: #c0c0c0;">都可以接受读、写请求</span>。主节点可以括Region分布。<span style="background-color: #c0c0c0;">不需要哨兵</span>节点进行监控。</p>
<div class="blog_h2"><span class="graybg">MVCC支持</span></div>
<p>KeyDB通过MVCC实现无锁（不需等待）的并发操作、后台保存。</p>
<div class="blog_h3"><span class="graybg">ACID兼容</span></div>
<p>得益于KeyDB的MVCC支持：</p>
<ol>
<li><span style="background-color: #c0c0c0;">事务、查询是非阻塞（不会有锁）</span>的，同时具有原子性保证</li>
<li>未来将支持事务回滚</li>
</ol>
<div class="blog_h2"><span class="graybg">多线程</span></div>
<p>KeyDB是<span style="background-color: #c0c0c0;">完全的多线程</span>的，不像Redis那样仅仅在I/O上支持并发，这意味着它可以充分利用多核心。</p>
<p>这个特性让KeyDB的吞吐量比Redis 5大5倍，Redis 6大3倍。再启用TLS的情况下，吞吐量可以比Redis大7倍。</p>
<div class="blog_h2"><span class="graybg">垂直扩容</span></div>
<p>多线程特性让KeyDB能最大化利用单机能力，这意味着通过配置能实现水平扩容。</p>
<p>单个KeyDB能够充分利用10（不使用TLS）-16（使用TLS）核心。</p>
<div class="blog_h2"><span class="graybg">其它特性</span></div>
<div class="blog_h3"><span class="graybg">更好的过期处理</span></div>
<p>支持将一个set中的所有键一起过期，过期时的删除操作是接近实时完成的，不会有延迟。</p>
<div class="blog_h3"><span class="graybg">ARM友好</span></div>
<p>可以很好的在ARM体系结构下运行。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<div class="blog_h3"><span class="graybg">通过PPA</span></div>
<pre class="crayon-plain-tag">sudo curl -s --compressed -o /etc/apt/trusted.gpg.d/keydb.gpg https://download.keydb.dev/keydb-ppa/keydb.gpg
sudo curl -s --compressed -o /etc/apt/sources.list.d/keydb.list https://download.keydb.dev/keydb-ppa/keydb.list
sudo apt update
sudo apt install keydb</pre>
<div class="blog_h3"><span class="graybg">仅安装工具</span></div>
<pre class="crayon-plain-tag">wget https://download.keydb.dev/packages/deb/ubuntu16.04_xenial/amd64/keydb-latest/keydb-tools_6.0.16-1~xenial1_amd64.deb
sudo dpkg -i keydb-tools_6.0.16-1~xenial1_amd64.deb </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<div class="blog_h2"><span class="graybg">启用复制</span></div>
<div class="blog_h3"><span class="graybg">主主模式</span></div>
<p>要启用Active Replica模式，遵循如下步骤：</p>
<ol>
<li>两个服务器A/B都需要配置<pre class="crayon-plain-tag">active-replica yes</pre></li>
<li>在B上执行命令<pre class="crayon-plain-tag">replicaof [A address] [A port]</pre>，丢弃自己的数据，加载A的数据</li>
<li>在A上执行命令<pre class="crayon-plain-tag">replicaof [B address] [B port]</pre>，丢弃自己的数据，加载B的数据</li>
<li>现在两个服务器会传播自己的写操作到对方</li>
</ol>
<p>或者完全通过配置文件：</p>
<pre class="crayon-plain-tag"># 节点A的配置
port 6379
# 当前节点的密码
requirepass mypassword123
# 连接到Master的密码
masterauth mypassword123
# 增加下面的
active-replica yes
# 从Redis 5.0开始，slaveof改成replicaof
replicaof 10.0.0.3 6379


# 节点B的配置
port 6379
requirepass mypassword123
masterauth mypassword123
# 增加下面的
active-replica yes
replicaof 10.0.0.2 6379</pre>
<div class="blog_h3"><span class="graybg">多主模式</span></div>
<p>在每个节点上增加多个replicaof配置项：</p>
<pre class="crayon-plain-tag"># 一共有3个节点 A/B/C 10.0.0.2/3/4

# 节点A
multi-master yes
active-replica yes
replicaof 10.0.0.3 6379
replicaof 10.0.0.4 6379

# 节点B
multi-master yes
active-replica yes
replicaof 10.0.0.2 6379
replicaof 10.0.0.4 6379

# 节点C
multi-master yes
active-replica yes
replicaof 10.0.0.2 6379
replicaof 10.0.0.3 6379</pre>
<div class="blog_h2"><span class="graybg">创建集群</span></div>
<p>创建配置文件，参考：</p>
<pre class="crayon-plain-tag">port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

dir ./
loglevel notice
logfile keydb.log

save 900 1
save 300 10
save 60 10000</pre>
<p>启动实例，参考：</p>
<pre class="crayon-plain-tag">keydb-server keydb.conf</pre>
<p>所有实例启动后，使用如下命令创建集群：</p>
<pre class="crayon-plain-tag">keydb-cli --cluster create 10.0.0.1:6379   10.0.0.2:6379 ... --cluster-replicas 1</pre>
<div class="blog_h3"><span class="graybg">增加节点</span></div>
<p>后续可以使用如下命令添加新的节点：</p>
<pre class="crayon-plain-tag"># 作为新的Master加入
#                            新节点           通过谁连接到既有集群
keydb-cli --cluster add-node 10.0.0.11:6379  10.0.0.1:6379

# 自动作为副本少的Master的Slave
keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave

# 强制指定Master
keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e </pre>
<div class="blog_h3"><span class="graybg">再分片</span></div>
<p>通过如下命令可以进行再分片： </p>
<pre class="crayon-plain-tag">keydb-cli reshard 10.0.0.11:6379 --cluster-from &lt;node-id&gt; --cluster-to &lt;node-id&gt; \
  --cluster-slots &lt;number of slots&gt; --cluster-yes</pre>
<div class="blog_h2"><span class="graybg">配置项说明</span></div>
<p>大部分配置项和Redis是相通的，参考<a href="/redis-study-note#config">Redis学习笔记</a>。这里仅仅列出Redis新版本增加 / KeyDB特有的配置项。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>supervised no</td>
<td>
<p>如果KeyDB由Systemd/Upstart管理，此配置项用于和supervision tree交互</p>
</td>
</tr>
<tr>
<td>always-show-logo yes</td>
<td>总是为交互式会话显示KeyDB的Logo</td>
</tr>
<tr>
<td>server-threads</td>
<td>
<p>建议4，默认1</p>
<p>处理请求的线程数量，应该取决于你的网络接口的队列数量（而不是CPU核心数量）</p>
<p>由于KeyDB使用自旋锁来减少延迟，将此值设置的过大会降低性能</p>
</td>
</tr>
<tr>
<td>server-thread-affinity</td>
<td>true/false，是否启用亲和性</td>
</tr>
<tr>
<td>db-s3-object</td>
<td>
<p>添加S3的桶路径，需要在本地具有S3客户端，并且已经配置好</p>
<p>如果配置该项，则保存RDB时，首先保存在本地，然后上传到S3。尝试加载时，首先尝试从本地加载，如果失败从S3加载</p>
</td>
</tr>
<tr>
<td>active-replica yes</td>
<td>启用Active Active复制。主从<span style="background-color: #c0c0c0;">都可以接受读写请求</span></td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">复制</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>通过复制（Replication），一个KeyDB节点可以保证自己的数据集和和Master(s)实例一致：</p>
<ol>
<li>当Slave和Master之间的连接良好时，Master通过发送写、键过期、键驱除等操作的数据流给Slave来保证Slave的数据和自己一致</li>
<li>当网络断开后，Slave会尝试重新连接到Master，并进行增量同步</li>
<li>如果<span style="background-color: #c0c0c0;">增量同步无法实现（落后太远），Slave会请求进行全量同步</span>。Master会创建它的数据集的快照，发送给Slave，然后继续发送增量的数据流</li>
</ol>
<p>关于KeyDB的复制机制，你需要知道以下事实：</p>
<ol>
<li>KeyDB默认使用异步的同步方式，这种方式的优势时延迟低、性能高。Slave会在接收了一定量的复制数据之后，向Master发送Ack。类似于Redis，使用WAIT命令可以确保特定的数据被多个Slave所Ack。WAIT命令并不能保证CAP中的CP —— 已经Ack的写操作，仍然可能在故障转移时丢失，尽管几率很小</li>
<li>每个Master可以对应多个Slaves</li>
<li><span style="background-color: #c0c0c0;">Slave可以有自己的Slaves</span>，参与复制的KeyDB节点可以形成树状拓扑</li>
<li>在Master侧，复制是非阻塞的。不管是增量/全量同步，都不影响Master继续处理查询</li>
<li>在Slave侧，复制很大程度上也是非阻塞的：
<ol>
<li>通过配置，KeyDB可以在接收初始同步时使用旧数据集提供服务。你也可以<span style="background-color: #c0c0c0;">配置，当复制流宕掉的情况下，让Slave返回错误</span>给客户端</li>
<li>在初始同步完毕后，有个阻塞的时间窗口，在此窗口中Slave替换旧的数据集</li>
<li>从4.0开始，可以配置KeyDB让删除数据集的操作在其它线程执行。但是，加载新数据集的操作仍然在主线程执行，会阻塞Slave</li>
</ol>
</li>
<li>通过复制，可以：
<ol>
<li>提升可扩容性：将缓慢的O(N)操作offload给Slave</li>
<li>提升数据安全性、高可用性</li>
</ol>
</li>
<li>使用复制，可以<span style="background-color: #c0c0c0;">免于Master需要将数据集写入到磁盘的开销</span>。这需要在Slave上配置高频的存盘，或者启用AOF。需要注意，这种用法下<span style="background-color: #c0c0c0;">Master重启时是空数据集</span>，如果不进行适当处理，它会导致Slave的数据集立刻变空</li>
</ol>
<div class="blog_h2"><span class="graybg">主主模式</span></div>
<p>KeyDB支持Active Replicas（Active Active）模式，这大大简化了故障转移的处理 —— 不需要确定副本何时应该晋升。</p>
<p>默认情况下，KeyDB以类似Redis的方式运行，仅仅允许从Master到Replicas的单向数据复制，副本仅仅支持只读处理。启用Active Replica模式后，即使Replica到Master的连接中断，它也能够处理请求。</p>
<div class="blog_h2"><span class="graybg">多主模式</span></div>
<p>KeyDB在复制时，支持多主模式。 使用配置：<pre class="crayon-plain-tag">multi-master yes</pre>即可启用。</p>
<p>当KeyDB连接到多个Master进行复制时，其行为和传统复制不同：</p>
<ol>
<li>多次调用replicaof命令，会为本节点添加额外的Master，而非替换掉Master</li>
<li>同步到Masters之前，<span style="background-color: #c0c0c0;">不会丢弃自己的数据</span></li>
<li>将<span style="background-color: #c0c0c0;">合并来自多个Masters的读写</span>到自己的内部数据库中</li>
<li><span style="background-color: #c0c0c0;">最后一个操作生效，也就是说，有两个Masters同时修改一个键，那么后修改的那个是实际值</span></li>
</ol>
<p>到目前为止，多主特性仍然是试验性的，在偶然情况下该特性可能触发流量封包。</p>
<p>如果仅仅有两个实例，建议使用主主模式。</p>
<div class="blog_h2"><span class="graybg">脑裂处理</span></div>
<p>KeyDB能够处理脑裂（两个节点之间网络断开）的场景。脑裂发生时，每个节点各自接受写入操作，每个写操作被加上时间戳，当连接恢复后，主节点们合并数据，以后写入的数据为准。</p>
<p>这种last-win原则在KeyDB里很常见，其它的例子如下文会提到的configEpoch冲突处理。</p>
<div class="blog_h2"><span class="graybg">只读Slave</span></div>
<p>从2.6版本开始KeyDB支持Slave的只读模式，并且默认开启。对应的选项是<pre class="crayon-plain-tag">slave-read-only</pre>，可以在运行时使用<pre class="crayon-plain-tag">CONFIG SET</pre>动态开关。</p>
<div class="blog_h2"><span class="graybg">最小Slave数量</span></div>
<p>从2.8版本开始KeyDB支持设置：<span style="background-color: #c0c0c0;">仅仅当至少有N个Slave连接到Master，此Master才允许写操作</span>。这可以增强数据安全性，再次强调一下，任何时候都不能保证数据绝不丢失。</p>
<p>该特性的工作方式是：</p>
<ol>
<li>KeyDB Slaves每秒PING一次Master，Ack自己处理的复制流的量</li>
<li>KeyDB Master记住每个Slave的最后一次PING的时间</li>
<li>如果PING正常的Slave数量小于配置的值，Master停止写入</li>
</ol>
<p>相关配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">min-slaves-to-write</pre>：正常PING的Slave的最小数量</li>
<li><pre class="crayon-plain-tag">min-slaves-max-lag</pre>：如果PING延迟大于此参数指定的秒数，认为Slave不正常</li>
</ol>
<div class="blog_h1"><span class="graybg">集群</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<div class="blog_h3"><span class="graybg">设计目标</span></div>
<p>KeyDB集群的设计目标，依据重要性排列如下：</p>
<ol>
<li>高性能，支持最多1000节点规模。不使用代理、异步复制，不需要对键值进行merge操作</li>
<li>可接受的数据安全性：集群尽可能保证连接到大部分Master节点的客户端的写操作。最小化Ack后的写操作丢失的可能性，连接到少数Master节点的客户端的写操作丢失的可能性更大</li>
<li>可用性：当出现网络分区后，如果大部分Master节点可以连接，并且对于无法连接的Master节点们至少各有一个Slave节点可以连接，则可用性得到保证。此外，如果使用副本迁移（replicas migration），没有Slave的节点可以从Slave过多的节点夺取Slave</li>
</ol>
<div class="blog_h3"><span class="graybg">写安全性</span></div>
<p>KeyDB集群使用异步复制，Last Failover  Wins原则意味着最后晋升的主的数据集会覆盖其它副本的数据集。由于异步复制的天然特征，这意味着总是存在丢失数据的窗口。</p>
<p>如果出现网络分区，连接到大部分Master所在分区的客户端，丢失数据的时间窗口比连接到少数Master所在分区的客户端要小。<span style="background-color: #c0c0c0;">发生故障转移的必要条件是集群中大多数Master到某个Master的通信不可达时间超过NODE_TIMEOUT</span>，如果网络分区在超时前恢复，不会丢失数据（被新的Master覆盖）。<span style="background-color: #c0c0c0;">当连接断开超过NODE_TIMEOUT之后，如果网络分区中Master数量少于一半，则所有Master停止接受写入</span>。</p>
<p>下面是连接到大多数Master的客户端丢失已经Ack数据的场景：</p>
<ol>
<li>写操作已经到达Master，应答客户端时，异步的复制尚未传播到Slave</li>
<li>Master宕机</li>
<li>过了较长时间后，Slave晋升，它的数据集成为权威</li>
</ol>
<p>另外一个场景：</p>
<ol>
<li>Master由于网络分区不可达</li>
<li>发生故障转移，Slave晋升</li>
<li>旧Master网络恢复</li>
<li>某个客户端使用过期的路由表，将数据写到Master</li>
</ol>
<p>第二种场景的发生概率很小，需要多种巧合同时发生。</p>
<p><span style="background-color: #c0c0c0;">对于连接到少数Master的客户端，数据丢失的时间窗口可能较大，直到NODE_TIMEOUT之前的写入都可能丢失。</span></p>
<div class="blog_h3"><span class="graybg">可用性</span></div>
<p>出现网络分区后，少数Master所在的分区失去可用性。 </p>
<p>在多数Master所在的分区，如果对于不可达的Master，至少有一个Slave也在该分区，则可用性在NODE_TIMEOUT+若干秒（Slave晋升为Master，通常需要1-2秒）后恢复。</p>
<p><span style="background-color: #c0c0c0;">副本迁移（replicas migration）可以提升可用性</span>，在大多数Master分区中，如果某个Master有多余的Replica，可以迁移给（在当前分区）没有Slave的（不在当前分区的）Master。</p>
<div class="blog_h3"><span class="graybg">性能</span></div>
<p>KeyDB集群不会代理请求，只会向客户端发起重定向，让它访问匹配的键空间分片的节点。</p>
<p>最终客户端会得到up-to-date的集群、键空间分片-节点映射关系的信息，并且直接访问正确的节点，避免重定向。</p>
<p><span style="background-color: #ffffff;">由于</span><span style="background-color: #c0c0c0;">通常的操作仅仅发生在单个节点上</span>，因此可以认为N主节点的KeyDB集群的性能和单个KeyDB实例是相似的。</p>
<div class="blog_h2"><span class="graybg">集群总线</span></div>
<div class="blog_h3"><span class="graybg">总线功能</span></div>
<p>KeyDB集群节点提供总线进行通信，总线使用客户端端口+10000（默认16379），协议是KeyDB Cluster protocol。</p>
<p>在KeyDB Cluster protocol中，节点负责存储数据、集群状态（包括键到节点的映射）、发现其它节点、检测无效节点、晋升Slave节点为Master。</p>
<p>节点之间使用一种gossip协议来传播关于集群的信息，从而发现新节点、发送PING包来检测节点是否正常工作、同步关于集群特定状态的信号。</p>
<p>KeyDB Cluster Bus还用于在集群范围内传播Pub/Sub消息，协调用户发起的手工故障转移。</p>
<div class="blog_h3"><span class="graybg">全互联</span></div>
<p>为了实现总线的功能，所有节点通过二进制协议KeyDB Cluster Bus连接在一起，支持1000节点需要维持百万级的TCP连接。</p>
<p>为了避免在节点之间交换过多的信息，KeyDB使用gossip协议 + 一种配置更新机制。这保证了消息量不会随着集群规模而指数级增长。</p>
<div class="blog_h3"><span class="graybg">不转发客户端请求</span></div>
<p>KeyDB集群不会代理（转发）客户端请求，它可能给出响应并触发重定向。理论上客户端可以发送请求给任何节点，并在必要时获得重定向，因此它不需要持有集群的状态信息。但是，客户端可以缓存键-节点映射，来改善性能。</p>
<div class="blog_h3"><span class="graybg">节点握手</span></div>
<p>节点总是接受总线端口上的连接，并且应答PONG给PING请求，即使源节点不受信任。但是对于<span style="background-color: #c0c0c0;">其它消息，如果源节点不是集群成员，将全部丢弃</span>。</p>
<p>节点在以下情况下，接受某个源节点作为集群成员：</p>
<ol>
<li>新节点发送<pre class="crayon-plain-tag">MEET</pre>消息，此消息类似于PING，但是会强迫消息接受者，接纳源节点作为集群成员。仅当系统管理员执行：<br />
<pre class="crayon-plain-tag">CLUSTER MEET ip port</pre></p>
<p>节点才会发送MEET消息</p>
</li>
<li>通过gossip协议传递的，关于节点被加入集群的事实。如果A、B已经是集群成员，现在C新加入并且MEET了A，那么A会gossip给B，这样A、B都认可C作为成员了</li>
</ol>
<div class="blog_h3"><span class="graybg">不支持NAT</span></div>
<p>总线不支持在NAT环境下，或者任何TCP被映射的环境下工作。对于Docker来说，你可能需要设置<pre class="crayon-plain-tag">--net=host</pre>来避免NAT。</p>
<div class="blog_h2"><span class="graybg">键空间分片</span></div>
<p>整个键空间被划分为16384个Hash Slots，Slot是分片的最小单位，意味着理论上集群最多有16384个主节点（实际上建议的上限是1000左右）。</p>
<p>集群中每个Master处理这些Slots的子集。如果没有进行中的集群再配置（reconfiguration）—— 没有正在移动的Hash Slots ——则我们称集群为stable的。稳定集群中每个Slot被单个Master节点所处理</p>
<div class="blog_h3"><span class="graybg">映射算法</span></div>
<p>键到Slot的映射算法是：<pre class="crayon-plain-tag">HASH_Slot = CRC16(key) mod 16384</pre></p>
<div class="blog_h3"><span class="graybg">Hash Tag</span></div>
<p>使用Hash tag可以确保多个键被分配到同一个Slot，以便KeyDB集群支持multi-key操作。</p>
<p>如果键中包含 <pre class="crayon-plain-tag">{...}</pre>则仅仅花括号中的部分用来计算Hash Slot，这可以保证{user1000}.following、{user1000}.followers这两个键被分到同一个Slot。</p>
<div class="blog_h2"><span class="graybg">节点属性</span></div>
<div class="blog_h3"><span class="graybg">节点ID</span></div>
<p>集群中的每个节点具有唯一的、自动生成的ID，该名称是160bit的随机数。节点会将此名称存放在配置文件中，并且一直使用相同的名称，除非：</p>
<ol>
<li>配置文件被删除</li>
<li>执行硬重置命令<pre class="crayon-plain-tag">CLUSTER RESET</pre></li>
</ol>
<p>节点ID用于在集群范围内唯一的标识节点，节点<span style="background-color: #c0c0c0;">可以改变自己的IP地址，但是它的ID不需要改变</span>。集群可以感知节点的IP地质变更，并使用gossip协议，通过集群总线进行重配置。</p>
<div class="blog_h3"><span class="graybg">其它属性</span></div>
<p>在任何节点执行<pre class="crayon-plain-tag">CLUSTER NODES</pre>可以获得节点列表，并列出其关键属性。除了上面的节点ID之外，其他属性包括：</p>
<ol>
<li>节点IP地址:端口</li>
<li>一系列标记位</li>
<li>如果节点被标记为Slave，则它的Master是谁</li>
</ol>
<pre class="crayon-plain-tag">keydb-cli cluster nodes

d1861060fe 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
# 节点ID   地质:端口       标记      最后PING/PONG时间   配置epoch  连接状态   Slots
3886e65cc9 127.0.0.1:6380 master - 1318428930 1318428931 2        connected 1365-2729
d289c575dc 127.0.0.1:6381 master - 1318428931 1318428931 3        connected 2730-4095</pre>
<div class="blog_h2"><span class="graybg">重新分片</span></div>
<p>KeyDB集群允许在运行时添加、删除节点。这都潜在的意味着Hash Slot的重新平衡。和调整Hash Slot有关的命令有：</p>
<pre class="crayon-plain-tag"># 添加Slot，通常加入新节点时使用
CLUSTER ADDSlotS Slot1 [Slot2] ... [SlotN]
# 很少使用
CLUSTER DELSlotS Slot1 [Slot2] ... [SlotN]

# 将Slot分配给特定节点
CLUSTER SETSlot Slot NODE node</pre>
<p>Hash Slot重新分配之后，将通过gossip协议进行配置传播（ configuration propagation ）。</p>
<p>SETSlot子命令还可以拆分为两条。假设我们希望将Slot 8从A迁移到B：</p>
<ol>
<li>向B发送：<pre class="crayon-plain-tag">CLUSTER SETSlot Slot MIGRATING A</pre>。这样，B会接受所有关于Slot 8的查询，但是如果本地没有Key，则发送<pre class="crayon-plain-tag">ASK</pre>重定向，让客户度询问A</li>
<li>向A发送：<pre class="crayon-plain-tag">CLUSTER SETSlot Slot IMPORTING B</pre>。这样，A会接受所有关于Slot 8的查询，但是请求必须以<pre class="crayon-plain-tag">ASKING</pre>命令作为前导，否则MOVE重定向给Slot的负责人B</li>
</ol>
<div class="blog_h3"><span class="graybg">关于多键操作</span></div>
<p>重新分片牵涉到键的迁移，这个迁移是逐步进行的 —— 不是原子的移动Slot。</p>
<p>这意味着，在迁移过程中，多键操作的Key，可能一部分位于节点A，一部分以及迁移到节点B。由于KeyDB集群限制多键操作仅仅牵涉单个节点（所有键位于一个Hash Slot），因此这种情况下会应答客户端以<pre class="crayon-plain-tag">TRYAGAIN</pre>错误。</p>
<div class="blog_h2"><span class="graybg">重定向</span></div>
<div class="blog_h3"><span class="graybg">MOVED</span></div>
<p>KeyDB客户端可以自由的向任何节点发送查询请求，节点会进行以下处理：</p>
<ol>
<li>分析查询请求是否是<span style="background-color: #c0c0c0;">acceptable的 —— 要么查询仅仅牵涉一个键，要么牵涉到在同一Hash Slot中的multikey</span></li>
<li>查找其内部的Slot-node映射，找到负责目标Hash Slot的节点：
<ol>
<li>如果是当前节点负责，直接处理请求</li>
<li>如果是其它节点负责，应答客户端以MOVED错误：<br />
<pre class="crayon-plain-tag">GET x
#      键所属Slot   负责处理此Slot的节点
-MOVED 3999        127.0.0.1:6381</pre>
</li>
</ol>
</li>
</ol>
<p>重定向中已经给出正确节点的信息，客户端应该（但不必须）缓存并且向正确节点发请求。</p>
<p>一旦重定向发生，可能潜在的Slot-node映射发生很大变化，客户端可以通过<pre class="crayon-plain-tag">CLUSTER NODES</pre>或<pre class="crayon-plain-tag">CLUSTER SlotS</pre>命令获取最新映射关系，并缓存。</p>
<div class="blog_h3"><span class="graybg">ASK</span></div>
<p>MOVED用于提示客户端，很明确Hash Slot永久的被另外一个服务器所处理了。</p>
<p>ASK重定向则是提示客户端，Hash Slot可能处于迁移过程中，我虽然负责此Slot，但是没有找到你需要的Key，你去问问原来的所有者吧。</p>
<div class="blog_h2"><span class="graybg">在Slave上读</span></div>
<p>默认情况下，Slave节点仅仅会重定向请求给处理Hash Slot的Master节点。</p>
<p>客户端可以使用<pre class="crayon-plain-tag">READONLY</pre>命令，提示Slave节点，我可以读取（可能是陈旧的）你的数据，我不进行写查询。</p>
<p>当一个连接处于只读默认下时，仅当查询牵涉不被Slave的Master负责的Key（可以由于重新分片导致）时才会发送重定向。</p>
<p>使用命令<pre class="crayon-plain-tag">READWRITE</pre>可以清除连接的只读状态。</p>
<div class="blog_h2"><span class="graybg">容错</span></div>
<div class="blog_h3"><span class="graybg">关于心跳</span></div>
<p>KeyDB节点持续的交换PING/PONG封包，这两种消息的差别仅仅是消息类型。PING/PONG消息也叫心跳。</p>
<p>通常情况下，PING会触发接受者回复PONG。但是<span style="background-color: #c0c0c0;">PONG也可以用来携带重要的配置信息，并且不需要应答</span>。这种用法可以尽快广播新的配置。</p>
<p>通常情况下，发送心跳时，节点会<span style="background-color: #c0c0c0;">随机选择几个节点作为目标，而不是发给所有节点</span>。这避免了随着集群规模的增大而出现消息风暴。不过，作为一个前提，节点<span style="background-color: #c0c0c0;">确保在NODE_TIMEOUT / 2的时间内，至少发送一个心跳给从未沟通过（不管谁主动发送PING都可以）的</span>节点。</p>
<p><span style="background-color: #c0c0c0;">即使NODE_TIMEOUT到达了，节点也会重新发起一次TCP连接，防止因为连接的问题导致误判</span>。注意NODE_TIMEOUT必须要大于RTT。</p>
<p>需要注意根据集群规模来配置NODE_TIMEOUT，如果集群规模很大，而NODE_TIMEOUT又偏小，会导致大量的心跳包。</p>
<div class="blog_h3"><span class="graybg">心跳包内容</span></div>
<p>PING/PONG包和其他消息具有相同的头，该头的内容：</p>
<ol>
<li>Node ID，发送节点的标识符</li>
<li>发送节点的currentEpoch、configEpoch，这两个字段和KeyDB集群使用的分布式算法有关。Slave的configEpoch是它的Master的最后一个已知的configEpoch</li>
<li>节点标记，提示节点是Slave还是Master，以及其他的单比特节点属性</li>
<li>发送节点所负责的Hash Slot的位图信息，如果发送节点是Slave则发送它的Master的信息</li>
<li>发送节点的客户端TCP端口</li>
<li>从发送节点的角度来看，集群的状态（down/ok）</li>
<li>对于Slave，其Master的Node ID</li>
</ol>
<p>PING/PONG包还包括一个gossip段，其中包含<span style="background-color: #c0c0c0;">发送者所认为的，集群（一部分随机的，否则消息太大，具体数量取决于集群规模）其它节点的状态</span>。每个节点的信息包括：Node ID、IP:PORT、节点标记。这个<span style="background-color: #c0c0c0;">gossip段可以用来进行故障发现、发现新节点</span>。</p>
<div class="blog_h3"><span class="graybg">故障发现</span></div>
<p>故障发现（Failure Detection）用于发现这样的异常：Master/Slave不再被集群中的大部分节点可见，进而引起的Slave晋升为Master的过程。</p>
<p><span style="background-color: #c0c0c0;">如果无法通过晋升来解决故障，则集群进入错误状态，不再接受客户端查询请求</span>。</p>
<p>通过前面的心跳机制我们了解到，每个节点都维护自己所认为的，其它节点的状态。状态表现为一系列标记，其中和故障发现有关的是：</p>
<ol>
<li><pre class="crayon-plain-tag">PFAIL</pre>，P表示Possible，即某个节点可能出故障了，但是尚未确认。如果目标节点在NODE_TIMEOUT时间范围内，从当前节点不可达，则标注为PFAIL。Master/Slave都有权标记某个节点为PFAIL</li>
<li><pre class="crayon-plain-tag">FAIL</pre>，节点故障在<span style="background-color: #c0c0c0;">固定的时间范围内被大多数Master</span>所认可，该状态从PFAIL升级而来</li>
</ol>
<p>前面我们了解到心跳的gossip段包含当前节点所认为的，随机的其它几个节点的状态。经过若干次心跳后，每个节点的的认知会传播到所有其它节点。当满足以下条件后，PFAIL变为FAIL：</p>
<ol>
<li>某个节点A，将节点B标记为PFAIL</li>
<li>节点A通过心跳接收到的gossip分析集群中大部分节点关于B状态的声明</li>
<li>如果大部分节点在<pre class="crayon-plain-tag">NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT</pre>的时间区间内，标记节点B为PFAIL或FAIL。<span style="background-color: #c0c0c0;">那么A标记B为FAIL，并发送<pre class="crayon-plain-tag">FAIL</pre>消息</span>给所有可达节点</li>
<li>FAIL消息会导致所有接收节点都将节点B标记为FAIL，不管它是不是认为B处于PFAIL状态</li>
</ol>
<p>当前实现将FAIL_REPORT_VALIDITY_MULT设置为2。这意味着，在两倍NODE_TIMEOUT时间内，大多数Master将某个节点标记为故障，则集群认为该节点宕掉了。</p>
<p>需要了解PFAIL到FAIL的转换，依赖于一种弱（一致）的协议：</p>
<ol>
<li>节点在一段时间范围内，收集其它节点的（关于节点状态）视图。由于时间窗口的存在，我们<span style="background-color: #c0c0c0;">无法确认（也不需要）在某个时间点</span>是否大多数Master达成一致</li>
<li>FAIL消息会被传播，但是无法保证所有节点都能收到并修改故障节点状态，这是因为故障常常伴随网络分区</li>
</ol>
<div class="blog_h3"><span class="graybg">故障恢复</span></div>
<p>节点状态可以从PFAIL变成FAIL，但是FAIL状态仅仅在以下情况下才能清除掉：</p>
<ol>
<li>节点变得可达，并且成为Slave。由于Slave不能参与故障转移，因此FAIL状态清除</li>
<li>节点变得可达，成为Master，但是不负责任何Slot。这种情况下FAIL状态可以被清除，因为该节点并没有正常参与到集群中</li>
<li>节点变得可达，成为Master，过了很长时间（NODE_TIMEOUT的N倍）仍然没有可觉察的Slave晋升。这种情况下，最好是让该节点重新加入集群</li>
</ol>
<div class="blog_h2"><span class="graybg">配置传播 </span></div>
<p>KeyDB使用类似于Raft算法中的Term的概念，叫Epoch。当多个节点提供了冲突的信息时，Epoch可用于确定谁的状态是up-to-date的：</p>
<ol>
<li>currentEpoch，可以认为是集群状态的版本号，<span style="background-color: #c0c0c0;">最终所有节点应该具有相同的currentEpoch</span></li>
<li>configEpoch，每个Master节点具有独特的<span style="background-color: #c0c0c0;">configEpoch，主要包含它负责的Hash Slots列表</span></li>
</ol>
<p>每个新创建的节点，它的 currentEpoch（64bit无符号整数）都是0。每当从其它节点接收到一个消息，<span style="background-color: #c0c0c0;">如果消息中的epoch大于本地的currentEpoch，则更新currentEpoch为接收到的epoch。</span></p>
<p>过了一段时间后，集群中所有节点都会使用最大的configEpoch作为自己的currentEpoch。</p>
<p>currentEpoch在集群节点需要协商，以决定执行某操作的时候用到，<span style="background-color: #c0c0c0;">currentEpoch大的节点具有话语权</span>。目前只支持Slave晋升这一种操作。</p>
<div class="blog_h3"><span class="graybg">configEpoch</span></div>
<p>所有Master在PING/PONG的时候，都会通告自己的configEpoch（连同自己负责的Hash Slots）。</p>
<p>当一个新节点加入集群后，Master将configEpoch设置为0。<span style="background-color: #c0c0c0;">Slave尝试晋升自己的时候，会增加configEpoch的值</span>，并尝试获得大多数Master的授权。一旦Slave获得授权，则它成为<span style="background-color: #c0c0c0;">使用此新configEpoch的Master</span>。</p>
<p>Slave在PING/PONG中也会通告configEpoch，通告的是它（通过最后一次消息得到）的Master的configEpoch。如果它通告的configEpoch小于Master的真实configEpoch，这意味着它需要更新，选举时<span style="background-color: #c0c0c0;">Master不会投票给持有过期configEpoch的Slave</span>。</p>
<p>节点的configEpoch变更，会被所有接收到相关心跳的节点fsync到nodes.config中。如果引起currentEpoch的变更，也会fsync到nodes.config。</p>
<p>configEpoch值的递增有一个简单算法负责，此算法确保它是新的、递增的、唯一的。configEpoch的变化由于故障转移、再分片导致。</p>
<div class="blog_h3"><span class="graybg">configEpoch冲突</span></div>
<p>如果由于Slave<span style="background-color: #c0c0c0;">晋升导致configEpoch增加，集群会确保它是唯一</span>的。</p>
<p>以下两种操作，仅仅简单的更新本地的configEpoch，可能导致configEpoch冲突：</p>
<ol>
<li>使用带<pre class="crayon-plain-tag">TAKEOVER</pre>选项的<pre class="crayon-plain-tag">CLUSTER FAILOVER</pre>命令，可以强制晋升一个Slave节点，不需要大多数Master可用</li>
<li>手工迁移Slot来进行再分片，也仅仅在本地节点生成新的epoch</li>
</ol>
<p>手工再分片时，如果Slot从A迁移到B，分片程序会强制B增加自己的epoch为可发现集群范围内的最大值 + 1，除非节点的epoch已经是最大。由于再分片常常牵涉到大量Slot的迁移，为了每个Slot，进行协商并获得更高的configEpoch是很低效的，因此增加epoch仅在本地进行，并且仅仅在迁移第一个Slot时进行。</p>
<p>尽管可能性较低，还是会出现多个节点声明相同configEpoch的情况 —— 如果手工再分片和自动故障转移发生外加运气差的话。KeyDB使用下面的冲突解决算法：</p>
<ol>
<li>如果Master A发现Master B正在通告和自己相同的configEpoch</li>
<li>并且根据字典序比较，A的Node ID比B大</li>
<li>那么A将currentEpoch+1，并且将其作为自己的configEpoch</li>
</ol>
<p>也就是说，如果集群中有若干节点具有相同configEpoch，那么其中ID最大的取胜，出现Hash Slot冲突时以它为准。</p>
<div class="blog_h3"><span class="graybg">Slots配置传播</span></div>
<p>Hash Slots - 节点映射关系的传播，不管对于新集群，还是由于故障转移/手工重分片导致的Slot负责节点变更，都是关键的。</p>
<p>Hash Slots配置信息可以通过两种方式传播：</p>
<ol>
<li>心跳消息：节点发送PING/PONG时总是携带它（或者它的Master）所负责的Slot的信息</li>
<li>UPDATE消息：由于心跳中还携带了自己的epoch，如果接受者发现此epoch过期，则会发送UPDATE消息，强制过期节点更新配置</li>
</ol>
<p>当一个新的节点加入到集群后，它的Hash Slot映射（16383 个key）简单的置空（表示未分配）。当接收到心跳/UPDATE消息后，根据以下规则来更新映射：</p>
<ol>
<li>如果Slot为空，并且某个已知的节点claim该Slot，分配Slot给节点</li>
<li>如果Slot不为空，并且它映射给Master A，如果现在接收到Master B的消息，声明Slot归它管。这种情况下，<span style="background-color: #c0c0c0;">如果B的configEpoch大于A，则重新绑定Slot给B</span></li>
</ol>
<p>由于规则2的存在，最终集群中所有的节点都认可configEpoch最大的节点通告的映射关系，这就是所谓last failover wins机制。</p>
<div class="blog_h2"><span class="graybg">故障转移</span></div>
<p>故障转移：</p>
<ol>
<li>依赖于上面两节提到的故障发现、配置传播机制</li>
<li>由出问题的Master的Slave主导，其它Master节点参与投票完成</li>
<li>触发时机为：
<ol>
<li>Master进入FAIL状态，并且经过了选举时延</li>
<li>Master至少负责1个Slot</li>
<li>Slave和Master的复制连接断开时间小于，这是为了保证Slave的数据不过于陈旧</li>
</ol>
</li>
</ol>
<p>故障转移过程如下：</p>
<ol>
<li>Slave增加自己的configEpoch，并同步currentEpoch</li>
<li>请求Masters来投票，具体做法是广播<pre class="crayon-plain-tag">FAILOVER_AUTH_REQUEST</pre>消息给所有Master节点</li>
<li>等待Master的应答，最长<pre class="crayon-plain-tag">NODE_TIMEOUT * 2</pre>，最短2秒</li>
<li>如果应答的Master的epoch小于发起晋升时Slave的currentEpoch，则应答被丢弃。这个可以防止接收到关于上一次选举的投票</li>
<li>如果Slave在限定时间内获得大多数Master的投票，则晋升成功。否则，等待<pre class="crayon-plain-tag">NODE_TIMEOUT * 4</pre>再次进行选举</li>
</ol>
<p>Master参与投票时遵循以下规则：</p>
<ol>
<li>对于一个给定的epoch，每个Master仅会投票一次：如果Master投票给Slave，它应答FAILOVER_AUTH_ACK，同时将lastVoteEpoch字段持久化到配置文件。Master在NODE_TIMEOUT * 2的时间内，不再能投票给故障Master的其它Slave，这可以防止同时选出多个新Master。</li>
<li>只有Master任何被故障转移的Master处于FAIL状态，才会进行投票</li>
<li>如果FAILOVER_AUTH_REQUEST中的currentEpoch小于Master的currentEpoch，请求被忽略 —— 这意味着投票应答的epoch总是和请求的相同</li>
</ol>
<div class="blog_h3"><span class="graybg">选举时延</span></div>
<p>Master进入FAIL状态后，Slave还需要等待一个选举延迟：</p>
<p style="padding-left: 30px;">DELAY = 500 毫秒 + 0 -500 毫秒随机延迟 + SLAVE_RANK * 1000 毫秒</p>
<p>才会发起选举投票。固定延迟让FAIL消息有机会传播到整个集群，随机延迟可以防止两个SLAVE_RANK相同的Slave同时尝试晋升。</p>
<p>SLAVE_RANK的大小取决于复制的进度。当Master故障时，Slave会相互通信，交换自己的复制进度，并进行排名。进度最快的Slave的SLAVE_RANK为0，依次递增。</p>
<div class="blog_h3"><span class="graybg">后处理</span></div>
<p>一旦Slave赢得选举，它将获得大于所有其它Master的configEpoch，并且通过心跳通告自己的configEpoch、负责的Slots。</p>
<p>为了让集群尽快完成重新配置，Slave将发送一个PONG包给整个集群。当前不可达的节点可能通过下面两种方式之一获得更新：</p>
<ol>
<li>接收到其它节点的PING/PONG包</li>
<li>其自己发送的心跳，由于信息过期，被其它节点应答以UPDATE</li>
</ol>
<p>重新配置的内容包括：</p>
<ol>
<li>对于故障的Master的原有其它Slave，需要更新复制源</li>
<li>对于所有节点，需要更新Hash Slot - 节点映射关系 </li>
</ol>
<div class="blog_h3"><span class="graybg">旧Master重新加入</span></div>
<p>如果一个Master节点因为网络分区，脱离集群，并且时间足够长，发生了故障转移。那么网络恢复后，旧的Master如何处理？</p>
<p>在KeyDB中，旧Master<span style="background-color: #c0c0c0;">将配置为偷取了它的Slot的那个节点的Slave</span>。</p>
<div class="blog_h2"><span class="graybg">节点重置</span></div>
<p>重置节点，可以让它以另外一个角色加入集群，或者加入其它集群。</p>
<p>执行<pre class="crayon-plain-tag">CLUSTER RESET</pre>可以软重置一个节点，如果不指定选项，默认为SOFT。制定HARD则进行硬重置。在重置时会进行如下处理：</p>
<ol>
<li>如果节点是Slave，则角色切换为Master，丢弃数据集；如果节点是Master并且持有Key，则中止重置</li>
<li>释放所有Hash Slot，重置手工故障转移状态</li>
<li>移除节点表中所有其它节点，这样节点对原先集群一无所知</li>
<li>对于硬重置：currentEpoch、configEpoch、lastVoteEpoch均置零</li>
<li>对于硬重置：Node ID修改为新的随机值</li>
</ol>
<div class="blog_h2"><span class="graybg">移除节点</span></div>
<p>通过重新分片，将Master的Slot全部迁移走，就可以关闭它了。</p>
<p>由于其它节点仍然记着移除节点的Node ID和地址，还会尝试连接它，因此，你应当调用<pre class="crayon-plain-tag">CLUSTER FORGET &lt;node-id&gt;</pre>，该命令的作用：</p>
<ol>
<li>从所有节点的nodes table中移除指定节点</li>
<li>60s内禁止被移除节点（根据ID）再次加入集群。防止因为gossip导致重新加入</li>
</ol>
<div class="blog_h2"><span class="graybg">订阅/发布</span></div>
<p>集群的客户端可以在任何节点上订阅，在任何节点上发布。KeyDB会保证消息被正确的转发 —— 目前的实现仅仅是简单的广播。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/keydb-study-note">KeyDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/keydb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Grafana展示时间序列数据</title>
		<link>https://blog.gmem.cc/time-series-data-renderering-with-grafana</link>
		<comments>https://blog.gmem.cc/time-series-data-renderering-with-grafana#comments</comments>
		<pubDate>Tue, 09 Aug 2016 03:18:54 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[K8S]]></category>
		<category><![CDATA[Monitoring]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[TSDB]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=12721</guid>
		<description><![CDATA[<p>简介 Grafana是一个独立运行的系统，内置了Web服务器。它可以基于仪表盘的方式来展示、分析时间序列数据。 Grafana支持多种数据源，例如：Graphite、OpenTSDB、InfluxDB、Elasticsearch。你可以混合使用多种数据源。它对Graphite有以下增强的支持： 点击修改Metrics路径的每一个片断 快速的添加函数，支持点击函数参数以修改之 修改函数顺序 丰富的模板支持 在UI方面，Grafana具有以下特性： 丰富的、基于客户端的图表组件：Bar图、区域图、线图。支持多Y轴 支持点击/选择以缩放（Zoom）时间区间 支持混合多种图表组件，一起展示或者堆叠展示 支持定制图表的配色，支持黑白两种主题 支持拖放仪表盘面板（Panel），支持多种面板类型 支持脚本化仪表盘、仪表盘模板 通过来自数据源的事件（例如Graphite的Events），可以对仪表进行标注 核心概念 术语 说明 数据源（Data Source） Grafana支持多种存储后端，这些后端称为数据源Grafana为每种数据源开发了查询编辑器（ Query <a class="read-more" href="https://blog.gmem.cc/time-series-data-renderering-with-grafana">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/time-series-data-renderering-with-grafana">使用Grafana展示时间序列数据</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Grafana是一个独立运行的系统，内置了Web服务器。它可以基于仪表盘的方式来展示、分析时间序列数据。</p>
<p>Grafana支持多种数据源，例如：Graphite、OpenTSDB、InfluxDB、Elasticsearch。你可以混合使用多种数据源。它对Graphite有以下增强的支持：</p>
<ol>
<li>点击修改Metrics路径的每一个片断</li>
<li>快速的添加函数，支持点击函数参数以修改之</li>
<li>修改函数顺序</li>
<li>丰富的模板支持</li>
</ol>
<p>在UI方面，Grafana具有以下特性：</p>
<ol>
<li>丰富的、基于客户端的图表组件：Bar图、区域图、线图。支持多Y轴</li>
<li>支持点击/选择以缩放（Zoom）时间区间</li>
<li>支持混合多种图表组件，一起展示或者堆叠展示</li>
<li>支持定制图表的配色，支持黑白两种主题</li>
<li>支持拖放仪表盘面板（Panel），支持多种面板类型</li>
<li>支持脚本化仪表盘、仪表盘模板</li>
<li>通过来自数据源的事件（例如Graphite的Events），可以对仪表进行标注</li>
</ol>
<div class="blog_h2"><span class="graybg">核心概念</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>数据源（Data Source）</td>
<td>Grafana支持多种存储后端，这些后端称为数据源<br />Grafana为每种数据源开发了查询编辑器（ Query Editor）以匹配后端的特点</td>
</tr>
<tr>
<td>组织（Organization）</td>
<td>引入组织机构的概念，以便一个Grafana实例可以被多个可能不受信任的组织使用</td>
</tr>
<tr>
<td>用户（User）</td>
<td>Grafana用户可以归属于1个或者多个组织</td>
</tr>
<tr>
<td>行（Row）</td>
<td>仪表盘中的逻辑分段</td>
</tr>
<tr>
<td>面板（Panel）</td>
<td>
<p>仪表盘中最基本的可视化单元，每个面板提供查询编辑器。Grafana目前内置了Graph, Singlestat, Dashlist和Text这几种面板</p>
<p>每种面板都具有一些样式、格式化选项，你可以利用它们设计出漂亮的图表</p>
<p>可以通过拖拽，在仪表盘中重新放置面板，也可以缩放其大小</p>
</td>
</tr>
<tr>
<td>查询编辑器（Query Editor）</td>
<td>依据特定数据源设计的表单，用以查询需要的Metrics</td>
</tr>
<tr>
<td>仪表盘（Dashboard）</td>
<td>一系列面板的集合，这些面板被组织在一个或者多个行中</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装与配置</span></div>
<div class="blog_h2"><span class="graybg">安装Grafana</span></div>
<div class="blog_h3"><span class="graybg">独立安装</span></div>
<p>依次执行下面的命令完成安装：</p>
<pre class="crayon-plain-tag"># CentOS
yum install initscripts fontconfig
wget https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm
rpm -Uvh grafana-3.1.1-1470047149.x86_64.rpm</pre>
<p>设置为开机启动：</p>
<pre class="crayon-plain-tag">/bin/systemctl daemon-reload
/bin/systemctl enable grafana-server.service</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl start grafana-server.service</pre>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<pre class="crayon-plain-tag">helm repo update gmem
helm del --purge grafana
helm install gmem/grafana --name grafana --namespace kube-system

# 执行下面的命令获得初始密码
kubectl get secret --namespace kube-system grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo</pre>
<p>值覆盖情况参考：<a href="https://github.com/gmemcc/charts/tree/master/grafana">https://github.com/gmemcc/charts/tree/master/grafana</a></p>
<div class="blog_h3"><span class="graybg">常用插件</span></div>
<pre class="crayon-plain-tag">grafana-cli plugins install grafana-kubernetes-app
grafana-cli plugins install natel-plotly-panel
grafana-cli plugins install kentik-app
grafana-cli plugins install alexanderzobnin-zabbix-app
grafana-cli plugins install grafana-worldmap-panel
grafana-cli plugins install grafana-clock-panel
grafana-cli plugins install grafana-piechart-panel
grafana-cli plugins install percona-percona-app
grafana-cli plugins install digrich-bubblechart-panel
grafana-cli plugins install digiapulssi-breadcrumb-panel
grafana-cli plugins install petrslavotinek-carpetplot-panel
grafana-cli plugins install neocat-cal-heatmap-panel
grafana-cli plugins install briangann-gauge-panel
grafana-cli plugins install jdbranham-diagram-panel</pre>
<p>安装插件后，需要重新启动Grafana服务：<pre class="crayon-plain-tag">service grafana-server restart</pre></p>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<div class="blog_h3"><span class="graybg">环境变量配置</span></div>
<p>当Grafana服务启动时，会读取<pre class="crayon-plain-tag">/etc/sysconfig/grafana-server</pre> 中的环境变量。你可以修改日志目录、数据存储目录等变量。</p>
<p>默认的，日志文件存储目录为/var/log/grafana，数据存储目录为/var/lib/grafana。默认使用SQLite数据库/var/lib/grafana/grafana.db。</p>
<div class="blog_h3"><span class="graybg">主配置文件</span></div>
<p>如果基于deb/rpm包进行安装，则主配置文件的位置为：<pre class="crayon-plain-tag">/etc/grafana/grafana.ini</pre> 。使用命令行参数 --config可以覆盖此位置。主配置文件说明如下：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 90px; text-align: center;">段</td>
<td style="width: 25%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td> </td>
<td>instance_name</td>
<td>Grafana服务的名称，默认为${HOSTNAME}，即环境变量HOMENAME的值</td>
</tr>
<tr>
<td rowspan="2">[paths]</td>
<td>data</td>
<td>在何处存放SQLite3数据库文件、基于文件的Session，以及其它数据</td>
</tr>
<tr>
<td>logs</td>
<td>在何处存放日志</td>
</tr>
<tr>
<td rowspan="5">[server]</td>
<td>http_addr</td>
<td>内置Web服务的监听地址，默认绑定所有地址</td>
</tr>
<tr>
<td>http_port</td>
<td>内置Web服务的监听端口，默认3000</td>
</tr>
<tr>
<td>protocol</td>
<td>http或者https</td>
</tr>
<tr>
<td>cert_file</td>
<td>https证书文件</td>
</tr>
<tr>
<td>cert_key</td>
<td>https证书密钥</td>
</tr>
<tr>
<td rowspan="6">[database]</td>
<td>type</td>
<td>数据库类型：mysql、postgres或者sqlite3</td>
</tr>
<tr>
<td>path</td>
<td>sqlite3数据库文件路径</td>
</tr>
<tr>
<td>host</td>
<td>mysql或postgres的主机:端口，例如<pre class="crayon-plain-tag">127.0.0.1:3306</pre> </td>
</tr>
<tr>
<td>name</td>
<td>数据库名称默认grafana</td>
</tr>
<tr>
<td>user</td>
<td>数据库用户</td>
</tr>
<tr>
<td>password</td>
<td>数据库密码</td>
</tr>
<tr>
<td rowspan="5">[security]</td>
<td>admin_user</td>
<td>Grafana管理员用户，默认admin</td>
</tr>
<tr>
<td>admin_password</td>
<td>Grafana管理员密码，默认admin</td>
</tr>
<tr>
<td>login_remember_days</td>
<td>记住登陆的最大天数</td>
</tr>
<tr>
<td>secret_key</td>
<td>用于签名记住登陆的Cookie</td>
</tr>
<tr>
<td>disable_gravatar</td>
<td>禁用头像</td>
</tr>
<tr>
<td>[log]</td>
<td>mode</td>
<td>日志记录方式，console、file或者syslog。可以用空格分开多个方式</td>
</tr>
<tr>
<td> </td>
<td>level</td>
<td>日志记录级别，debug、info、warn、error、critical</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用Grafana</span></div>
<p>使用Grafana的日常工作包括：用户管理、系统管理、仪表盘设计、数据源管理，等等。所有这些工作都在Web界面<pre class="crayon-plain-tag">http://GRAFANA_HOST:3000</pre>  完成。该界面比较友好，适合最终用户。</p>
<div class="blog_h2"><span class="graybg">配置Graphite数据源</span></div>
<p>点击Web界面左上角的图标，下拉列表中选择Data Sources，即可管理数据源。</p>
<p>点击Add data source按钮，添加新的数据源，参考下图：</p>
<p><img class="aligncenter size-full wp-image-12739" src="https://blog.gmem.cc/wp-content/uploads/2016/08/grafana-ds-cfg.png" alt="grafana-ds-cfg" width="478" height="440" /></p>
<p>注意Access设置成proxy，则数据通过Grafana间接获取，否则，数据直接通过客户端获取。添加Graphite数据源后，可以点击Dashboards选项卡，获得其预置的仪表盘Graphite Carbon Metrics并导入到Grafana中。</p>
<div class="blog_h2"><span class="graybg">设计仪表盘</span></div>
<p>点击Web界面左上角的图标，下拉列表中选择Dashboards  ⇨ New，可以新建仪表盘。在新仪表盘中，点击左侧的绿条，可以添加新的面板；点击右下侧ADD ROW按钮则可以新建一行。</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/time-series-data-renderering-with-grafana">使用Grafana展示时间序列数据</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/time-series-data-renderering-with-grafana/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MongoDB学习笔记</title>
		<link>https://blog.gmem.cc/mongodb-study-note</link>
		<comments>https://blog.gmem.cc/mongodb-study-note#comments</comments>
		<pubDate>Sat, 23 May 2015 03:25:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14820</guid>
		<description><![CDATA[<p>简介 MongoDB是一个开源的文档数据库（Document Database），具有高性能、高可用性、自动化的可扩容性。 高性能的持久化能力，主要体现在： 对内嵌数据模型的支持，减少了数据库系统的I/O活动 支持索引，加快了查询速度。可以包含来自内嵌文档、数组的索引键 高可用性由MongoDB的数据复制机制 —— 复制集（Replica set，一系列持有相同数据集的MongoDB实例）提供，主要体现在： 自动的故障转移 数据冗余 水平可扩容性主要体现在： 分片（Sharding）机制，在集群中分布数据 从3.4开始，支持基于分片键（Shard key）来划分数据区域（Zone）。在平衡化的集群中，MongoDB把读写操作定向到包含目标数据的区域中 所谓文档数据库，是指数据库中的每一条记录是一个“文档”。MongoDB的记录（文档）格式类似于JSON，由一系列字段组成，每个字段的值可以是文档、文档的数组、简单的值。文档格式的优势在于： 文档很自然的对应了主流编程语言中的原生数据结构（对象） 内嵌的文档、数组避免了关系型数据库昂贵的Join操作 由于支持动态Schema（动态字段），因此对动态的支持很轻松 MongoDB提供了富查询语言（Rich Query <a class="read-more" href="https://blog.gmem.cc/mongodb-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/mongodb-study-note">MongoDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>MongoDB是一个开源的文档数据库（Document Database），具有高性能、高可用性、自动化的可扩容性。</p>
<p>高性能的持久化能力，主要体现在：</p>
<ol>
<li>对内嵌数据模型的支持，减少了数据库系统的I/O活动</li>
<li>支持索引，加快了查询速度。可以包含来自内嵌文档、数组的索引键</li>
</ol>
<p>高可用性由MongoDB的数据复制机制 —— 复制集（Replica set，一系列持有相同数据集的MongoDB实例）提供，主要体现在：</p>
<ol>
<li>自动的故障转移</li>
<li>数据冗余</li>
</ol>
<p>水平可扩容性主要体现在：</p>
<ol>
<li>分片（Sharding）机制，在集群中分布数据</li>
<li>从3.4开始，支持基于分片键（Shard key）来划分数据区域（Zone）。在平衡化的集群中，MongoDB把读写操作定向到包含目标数据的区域中</li>
</ol>
<p>所谓文档数据库，是指数据库中的每一条记录是一个“文档”。MongoDB的记录（文档）格式类似于JSON，由一系列字段组成，每个字段的值可以是文档、文档的数组、简单的值。文档格式的优势在于：</p>
<ol>
<li>文档很自然的对应了主流编程语言中的原生数据结构（对象）</li>
<li>内嵌的文档、数组避免了关系型数据库昂贵的Join操作</li>
<li>由于支持动态Schema（动态字段），因此对动态的支持很轻松</li>
</ol>
<p>MongoDB提供了富查询语言（Rich Query Language），除了支持读写操作以外，还支持数据聚合、文本搜索、地理空间查询。</p>
<p>MongoDB支持可拔插的存储引擎，你可以自己依据API开发存储引擎，或者使用开箱即用的WiredTiger、MMAPv1</p>
<div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">数据库-集合-文档</span></div>
<p>MongoDB存储记录 —— <span style="background-color: #c0c0c0;"><a href="http://bsonspec.org">BSON</a>文档（JSON的二进制形式）</span>到集合（Collection）中，数据库中包含多个集合。集合的概念类似于RDBMS的表。</p>
<p>要指定使用的数据库，可以通过use语句：</p>
<pre class="crayon-plain-tag">use newdb
# 下面的命令列出现有的数据库 
show dbs</pre>
<p>如果newdb不存在，MongoDB会在你<span style="background-color: #c0c0c0;">第一次向其中存储数据时自动创建</span>。类似的，使用不存在的集合时，也会自动的创建：</p>
<pre class="crayon-plain-tag">// 执行下面的语句时，会自动创建数据库newdb和集合newcoll
db.newcoll.insertOne({x:1});

// 你也可以显式的创建集合，并提供参数，例如最大尺寸、文档验证规则
db.createCollection()</pre>
<div class="blog_h3"><span class="graybg">文档验证</span></div>
<p>在3.2版本之前，集合中存放的文档的Schema是任意的。3.2开始，你可以提供文档验证规则（Document Validation Rules）来限制某个集合中文档的结构。插入/更新文档时，必须满足规则。</p>
<div class="blog_h3"><span class="graybg">文档的其它用途</span></div>
<p>除了定义数据记录之外，MongoDB在很多地方使用文档结构，包括但是不限于：</p>
<ol>
<li>查询过滤器</li>
<li>更新规格文档（update specifications documents）</li>
<li>索引规格文档（update specifications documents）</li>
</ol>
<div class="blog_h2"><span class="graybg">BSON</span></div>
<p>BSON类似于JSON，但是它的字段具有更丰富的类型，例如：</p>
<pre class="crayon-plain-tag">var mydoc = {
    // 类似于UUID，但是支持快速生成，且有序
    _id: ObjectId( "5099803df3f4948bd2f98391" ),
    // 内嵌文档
    name: { first: "Alan", last: "Turing" },
    // 日期
    birth: new Date( 'Jun 23, 1912' ),
    death: new Date( 'Jun 07, 1954' ),
    // 数组
    contribs: [ "Turing machine", "Turing test", "Turingery" ],
    views: NumberLong( 1250000 )
}</pre>
<p>BSON字段的命名限制：</p>
<ol>
<li>名称<span style="background-color: #c0c0c0;">_id保留用作主键</span>，即指必须在集合中唯一、不可变。其类型可以是除了数组之外的任何类型</li>
<li><span style="background-color: #c0c0c0;">字段名不得以 $开头</span></li>
<li>字段名不得包含 . 号</li>
<li>字段名不得包含空字符（\0）</li>
</ol>
<p><span style="background-color: #c0c0c0;">BSON支持重名字段</span>，但是大部分MongoDB客户端使用哈希呈现文档，哈希不支持重名key。MongoDB进程产生的某些文档会使用重名字段，但是绝不会向用户定义的文档中添加重名字段。</p>
<p>对于被索引集合（Indexed collections），被索引的字段的值的长度，受到最大索引键长度的约束。</p>
<p>要访问内嵌文档、数组的元素，需要使用点号标记：</p>
<pre class="crayon-plain-tag">mydoc.name.first
mydoc.contribs.2</pre>
<p><span style="background-color: #c0c0c0;">BSON文档的最大尺寸是16MB</span>。要存储更大的文档，可以使用GridFS API。</p>
<p>文档中字段的顺序，<span style="background-color: #c0c0c0;">和插入文档时指定的顺序一致</span>，除了两点例外：</p>
<ol>
<li>_id总是作为第一个字段</li>
<li>重命名字段名的操作可能导致文档中的字段重排序。从2.6开始，MongoDB积极的尝试保持字段顺序</li>
</ol>
<div class="blog_h3"><span class="graybg">BSON数据类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">类型</td>
<td style="width: 10%; text-align: center;">序号</td>
<td style="width: 15%; text-align: center;">别名</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Double</td>
<td>1</td>
<td>double</td>
<td> </td>
</tr>
<tr>
<td>String</td>
<td>2</td>
<td>string</td>
<td>
<p>BSON字符串以UTF-8格式编码。各语言的驱动将语言内部字符串编码为UTF-8再进行存储</p>
<p>MongoDB的$regex查询支持在正则式中使用UTF-8字符</p>
<p>由于sort()操作使用C的strcmp函数，某些字符的排序处理可能不正确</p>
</td>
</tr>
<tr>
<td>Object</td>
<td>3</td>
<td>object</td>
<td> </td>
</tr>
<tr>
<td>Array</td>
<td>4 </td>
<td>array</td>
<td> </td>
</tr>
<tr>
<td>Binary data</td>
<td>5 </td>
<td>binData</td>
<td> </td>
</tr>
<tr>
<td>Undefined</td>
<td>6 </td>
<td>undefined</td>
<td>废弃</td>
</tr>
<tr>
<td>ObjectId</td>
<td>7 </td>
<td>objectId</td>
<td>
<p>较小、很可能唯一、快速生成、有序的标识符类型。由12字节组成：</p>
<ol>
<li>4个字节为UNIX时间戳（秒）。可以使用ObjectId.getTimestamp()获得</li>
<li>3个字节的机器识别号</li>
<li>2字节的进程标识符</li>
<li>3字节的计数器，从随机数开始</li>
</ol>
<p>对此类字段进行排序，粗略的等同于按创建时间排序</p>
</td>
</tr>
<tr>
<td>Boolean</td>
<td>8 </td>
<td>bool</td>
<td> </td>
</tr>
<tr>
<td>Date</td>
<td>9 </td>
<td>date</td>
<td>
<p>BSON日期类型是一个64位整数，表示UNIX时间戳（毫秒），支持表示最近29000万年时间范围</p>
</td>
</tr>
<tr>
<td>Null</td>
<td>10 </td>
<td>null</td>
<td> </td>
</tr>
<tr>
<td>Regular Expr</td>
<td>11 </td>
<td>regex</td>
<td> </td>
</tr>
<tr>
<td>DBPointer</td>
<td>12 </td>
<td>dbPointer</td>
<td>废弃 </td>
</tr>
<tr>
<td>JavaScript</td>
<td>13 </td>
<td>javascript</td>
<td> </td>
</tr>
<tr>
<td>Symbol</td>
<td>14 </td>
<td>symbol</td>
<td>废弃</td>
</tr>
<tr>
<td>JavaScript (with scope)</td>
<td>15 </td>
<td>javascriptWithScope</td>
<td> </td>
</tr>
<tr>
<td>32-bit integer</td>
<td>16 </td>
<td>int</td>
<td> </td>
</tr>
<tr>
<td>Timestamp</td>
<td>17 </td>
<td>timestamp</td>
<td>
<p>特殊的类型，供MongoDB内部使用，与Date类型不同，它包含64个位：</p>
<ol>
<li>前32位是UNIX时间戳（秒）</li>
<li>后32位是秒内递增的计数器</li>
</ol>
<p>在单个mongod实例内，每个时间戳都是唯一的</p>
<p>在主从复制时，oplog包含一个ts字段，其类型是timestamp </p>
<p>当你插入空的Timestamp字段到顶级文档中时，MongoDB服务器自动将其替换为当前时间戳：</p>
<pre class="crayon-plain-tag">db.test.insertOne( { ts: new Timestamp() } );
db.test.find()
# { "_id" : ObjectId("542c2b97bac0595474108b48"), 
#   "ts" : Timestamp(1412180887, 1) }</pre>
</td>
</tr>
<tr>
<td>64-bit integer</td>
<td>18 </td>
<td>long</td>
<td> </td>
</tr>
<tr>
<td>Decimal128</td>
<td>19 </td>
<td>decimal</td>
<td>3.4引入</td>
</tr>
<tr>
<td>Min key</td>
<td>-1 </td>
<td>minKey</td>
<td> </td>
</tr>
<tr>
<td>Max key</td>
<td>127 </td>
<td>maxKey </td>
<td> </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">_id</span></div>
<p>集合中的每一个文档，都必须具有作为主键的_id字段。
<p>如果用户没有指定该字段，MongoDB驱动会自动生成一个ObjectId类型的_id，如果驱动没有生成_id则mongod会生成ObjectId类型的_id。此规则适用于插入操作，以及指定了upsert: true的更新操作。</p>
<p>在创建集合的过程中，MongoDB在_id上创建唯一性索引。</p>
<p>你可以考虑使用以下数据类型作为_id：</p>
<ol>
<li>ObjectId类型</li>
<li>可以考虑使用自然键，可以节省空间并减少索引</li>
<li>使用自增长数字</li>
<li>在应用程序中生成UUID，可以保存为BinData以节省空间</li>
</ol>
<div class="blog_h3"><span class="graybg">比较和排序</span></div>
<p>当比较不同BSON类型的值时，MongoDB使用如下（从低到高）比较顺序：MinKey、Null、Numbers (ints, longs, doubles, decimals)、Symbol, String、Object、Array、BinData、ObjectId、Boolean、Date、Timestamp、Regular Expression、MaxKey (internal type)</p>
<p>在比较时，MongoDB把某些BSON类型当做一种类型看待，例如各种数字类型。</p>
<p>比较字符串时，默认使用简单的二进制比较的方式。从3.4开始，支持所谓排序规则（Collation） —— 用于指定语言相关（是否大小写敏感、是否有声调）的排序算法。排序规则的规格如下：</p>
<pre class="crayon-plain-tag">{
   locale: &lt;string&gt;,  // 该字段必须
   caseLevel: &lt;boolean&gt;,
   caseFirst: &lt;string&gt;,
   strength: &lt;int&gt;,
   numericOrdering: &lt;boolean&gt;,
   alternate: &lt;string&gt;,
   maxVariable: &lt;string&gt;,
   backwards: &lt;boolean&gt;
}</pre>
<p>比较数组时，小于比较/升序排序基于数组的最小元素进行，大于比较/降序排序基于数组的最大元素进行。</p>
<p>比较BinData时，按照以下顺序进行：</p>
<ol>
<li>比较数据的长度</li>
<li>根据BSON单字节子类型比较</li>
<li>执行逐字节比较 </li>
</ol>
<div class="blog_h2"><span class="graybg">视图</span></div>
<p>从3.4版本开始，MongoDB支持在现有的集合、其它视图之上，创建只读的视图（View）。</p>
<p>要创建视图，你可以使用create命令，指定viewOn、pipeline选项，可选的，指定一个collation选项：</p>
<pre class="crayon-plain-tag">db.runCommand( { 
    create: &lt;view&gt;, viewOn: &lt;source&gt;, pipeline: &lt;pipeline&gt;, collation: &lt;collation&gt; 
} )

// 也可以使用新引入的Shell助手：
db.createView(&lt;view&gt;, &lt;source&gt;, &lt;pipeline&gt;, &lt;collation&gt; )</pre>
<p>那些用于列出集合列表的操作，例如db.getCollectionInfos()、db.getCollectionNames()，它们的输出包含视图。</p>
<p>要删除视图，可以使用命令：<pre class="crayon-plain-tag">db.collection.drop()</pre> </p>
<div class="blog_h3"><span class="graybg">基本特性</span></div>
<ol>
<li>只读，对视图进行写操作导致错误</li>
<li>索引与排序：视图使用其底层的集合上的索引，你不能在视图上使用$natural排序</li>
<li>投影操作限制：在视图上进行find()操作，不支持以下投影操作：$、$elemMatch、$slice、$meta</li>
<li>视图的名字不可改变 </li>
</ol>
<p>视图在读操作发生时，按需的进行计算。在视图上执行的读操作，是底层聚合管道（ aggregation pipeline）的一部分，因此，视图不支持：</p>
<ol>
<li>db.collection.mapReduce()</li>
<li>$text操作符，因为聚合中的$text仅仅对于第一阶段（first stage）有效</li>
<li>geoNear命令以及$geoNear管道阶段（pipeline stage） </li>
</ol>
<div class="blog_h3"><span class="graybg">关于分片</span></div>
<p>如果底层集合是分片的，则视图也是分片的。因此不能为$lookup、$graphlookup操作的from字段指定一个分片的视图。</p>
<div class="blog_h3"><span class="graybg">排序规则</span></div>
<ol>
<li>在创建视图时，可以指定一个默认的排序规则（collation，判断两个比较项谁大谁小的准则）。如果不指定，视图的默认排序规则是简单的二进制比较排序规则。视图不会从底层集合继承排序规则</li>
<li> 在视图上进行字符串比较，使用视图默认排序规则。尝试修改视图默认排序规则的操作会失败</li>
<li>如果基于其它视图创建新视图，你不能指定不同于源视图的排序规则</li>
<li>当执行牵涉到多个视图的聚合操作时，例如$lookup、$graphlookup，所有牵涉到的视图必须具有一致的排序规则</li>
</ol>
<div class="blog_h2"><span class="graybg">定长集合</span></div>
<p>定长集合（Capped Collections）是用于支持高吞吐量操作（插入、按插入顺序读取）的、具有固定尺寸的集合。定长集合的工作方式类似于环形缓冲，一旦空间占满，最老的文档会被覆盖。</p>
<p>创建定长集合的方法：</p>
<pre class="crayon-plain-tag">// size 集合的最大字节数，小于4096则MongoDB自动设置为4096。否则，自动舍入到最接近的256的倍数
db.createCollection( "log", { capped: true, size: 100000 } )
// 可以指定max，集合包含的文档的最大数量
db.createCollection("log", { capped : true, size : 5242880, max : 5000 } )</pre>
<p>当你使用find()来操作定长集合，并且没有指定顺序时，MongoDB保证结果的顺序和文档插入的顺序一致。要逆插入序获得结果，可以：</p>
<pre class="crayon-plain-tag">db.cappedCollection.find().sort( { $natural: -1 } )</pre>
<p>可以使用集合的isCapped方法检测定长集合：</p>
<pre class="crayon-plain-tag">db.collection.isCapped()</pre>
<p>普通集合可以被转换为定长集合：</p>
<pre class="crayon-plain-tag">db.runCommand({"convertToCapped": "mycoll", size: 100000});</pre>
<div class="blog_h3"><span class="graybg">基本特性</span></div>
<ol>
<li>定长集合记住文档的插入顺序， 查询时不需要基于索引以得到文档。没有了维护索引的负担，定长集合支持更高的插入吞吐量</li>
<li>定长集合具有 _id 字段，并在其上默认建立了索引</li>
<li>如果你需要对定长集合中的文档进行update操作，应当建立索引，避免全集合扫描</li>
<li>定长集合不支持删除文档，如果更新/替换操作改变了文档的尺寸，会失败</li>
<li>定长集合不支持分片</li>
<li>聚合管道操作符$out不能把结果输出到定长集合</li>
</ol>
<div class="blog_h1"><span class="graybg">安装与配置</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">Docker</span></div>
<pre class="crayon-plain-tag"># 在容器中运行MongoDB
docker run --name mongodb -p 27017:27017 -v ~/Docker/volumes/mongodb/data/db:/data/db -d mongo:3.4
# 登录到MongoDB管理Shell
docker exec -it mongodb mongo admin</pre>
<div class="blog_h1"><span class="graybg">Mongo Shell</span></div>
<p>Mongo Shell是交互式的、基于JavaScript语言的MongoDB接口。使用此Shell你可以查询、插入数据，或者执行管理任务。</p>
<p>要启动Mongo Shell，cd到安装目录，执行mongo命令。不加任何参数时，Shell尝试连接到localhost:27017。Shell启动时默认会读取~/.mongorc.js文件，并执行其中的JavaScript脚本。</p>
<div class="blog_h2"><span class="graybg">基本用法</span></div>
<div class="blog_h3"><span class="graybg">连接到服务器</span></div>
<pre class="crayon-plain-tag"># 连接到本地 27017
mongo
# 连接到复制集
mongo --host rs1/mongo-11.gmem.cc,mongo-12.gmem.cc,mongo13.gmem.cc
# 使用指定的用户登陆到服务器，连接到bais数据库，基于admin数据库进行身份验证
mongo -u root -p root --host  mongo-11.gmem.cc --authenticationDatabase admin bais</pre>
<div class="blog_h3"><span class="graybg">常用代码</span></div>
<pre class="crayon-plain-tag">// 显示当前正在使用的数据库
db
// 切换数据库
use mydb
// Shell不支持使用名字中包含空格或者以数字开始的集合，需要改变语法：
mydb.3test.find()    // 错误
mydb["3test"].find()
mydb.getCollection("3test").find()

// 美化输出
db.myCollection.find().pretty()

// 打印输出
print()
// 转换为JSON并打印，等价于printjson() 
print(tojson(obj))

// 退出Shell
quit()</pre>
<div class="blog_h3"><span class="graybg">多行文本</span></div>
<p>当输入行以花、圆、方括号结尾时，下面的行自动以...开头，直到所有开括号的匹配关闭括号都输入了，再回车，输入才会被估算。</p>
<div class="blog_h2"><span class="graybg">配置Shell</span></div>
<div class="blog_h3"><span class="graybg">设置命令提示符</span></div>
<p>默认的命令提示符是 <pre class="crayon-plain-tag">&gt;</pre>，你可以使用如下代码定制：</p>
<pre class="crayon-plain-tag">// 显示计数器
cmdCount = 1;
prompt = function() {
    return (cmdCount++) + "&gt; ";
}

// 显示数据库和主机信息
host = db.serverStatus().host;
prompt = function() {
   return db+"@"+host+"$ ";
}</pre>
<div class="blog_h3"><span class="graybg">使用外部编辑器 </span></div>
<p>你可以在MongoDB Shell中使用自己喜欢的编辑器，只需要在启动Shell之前设置环境变量： <pre class="crayon-plain-tag">export EDITOR=vim</pre>即可。</p>
<p>这样，你就可以编辑变量或者函数了：<pre class="crayon-plain-tag">edit myFunc</pre></p>
<div class="blog_h3"><span class="graybg">设置批量尺寸</span></div>
<p>db.collection.find()会返回一个游标，但是，在Shell中没有把游标赋值给一个变量的情况下，Shell会自动迭代游标20次，并打印结果到屏幕上。这个迭代的次数是可以定制的：</p>
<pre class="crayon-plain-tag">DBQuery.shellBatchSize = 10;</pre>
<div class="blog_h2"><span class="graybg">为Shell编写脚本</span></div>
<p> 你可以使用JavaScript语言编写一段脚本，交由Shell执行，以完整数据操控、管理工作：</p>
<pre class="crayon-plain-tag"># 命令行中提供脚本
mongo test --eval "printjson(db.getCollectionNames())"
# 文件方式提供脚本
mongo localhost:27017/test myjsfile.js
# 在Shell中加载JS
load("myjstest.js")</pre>
<div class="blog_h3"><span class="graybg">打开连接</span></div>
<pre class="crayon-plain-tag">conn = new Mongo();
db = conn.getDB("myDatabase");

// 也可以使用connect函数
db = connect("localhost:27020/myDatabase");</pre>
<div class="blog_h3"><span class="graybg">Shell助手等价代码</span></div>
<p>不能在JavaScript中使用任何Shell助手，因为不是合法的JavaScript表达式。等价写法如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Shell助手</td>
<td style="text-align: center;">等价JS代码</td>
</tr>
</thead>
<tbody>
<tr>
<td>show dbs, show databases</td>
<td>db.adminCommand('listDatabases')</td>
</tr>
<tr>
<td>use &lt;db&gt;</td>
<td>db = db.getSiblingDB('&lt;db&gt;')</td>
</tr>
<tr>
<td>show collections</td>
<td>db.getCollectionNames()</td>
</tr>
<tr>
<td>show users</td>
<td>db.getUsers()</td>
</tr>
<tr>
<td>show roles</td>
<td>db.getRoles({showBuiltinRoles: true})</td>
</tr>
<tr>
<td>show log &lt;logname&gt;</td>
<td>db.adminCommand({ 'getLog' : '&lt;logname&gt;' })</td>
</tr>
<tr>
<td>show logs</td>
<td>db.adminCommand({ 'getLog' : '*' })</td>
</tr>
<tr>
<td>it</td>
<td>cursor = db.collection.find()<br />if ( cursor.hasNext() ){<br /> cursor.next();<br />}</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Shell中的数据类型</span></div>
<p>BSON提供了很多数据类型，不同驱动都根据其宿主编程语言，提供了Native的类型映射。Shell类似，提供了助手类以支持这些数据类型。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">BSON类型</td>
<td style="text-align: center;">Shell助手类说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Date</td>
<td>
<p>Shell提供多种方法来返回日期对象：</p>
<ol>
<li>Date()方法，返回当前日期的<span style="background-color: #c0c0c0;">字符串</span>形式</li>
<li>new Date()构造函数，使用ISODate()包装器返回一个日期对象</li>
<li>ISODate()构造函数，使用ISODate()包装器返回一个日期对象</li>
</ol>
<p>示例：<pre class="crayon-plain-tag">ISODate('2008-08-08T08:08:08.888Z')</pre></p>
</td>
</tr>
<tr>
<td>ObjectId</td>
<td>Shell提供ObjectId包装器类，要生成一个新的ObjectId，你可以<pre class="crayon-plain-tag">new ObjectId</pre>或者<pre class="crayon-plain-tag">ObjectId()</pre></td>
</tr>
<tr>
<td>NumberLong</td>
<td>Shell提供NumberLong包装器类，你可以：<pre class="crayon-plain-tag">NumberLong("1111")</pre></td>
</tr>
<tr>
<td>NumberInt</td>
<td>Shell提供NumberInt包装器类</td>
</tr>
<tr>
<td>NumberDecimal</td>
<td>
<p>3.4版本引入。默认情况下Shell把所有数字作为64bit双精度浮点数看待，使用NumberDecimal()可以明确的构造128bit浮点数，在牵涉到货币的领域使</p>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<p> 要检测对象的数据类型，可以使用：</p>
<pre class="crayon-plain-tag">mydoc._id instanceof ObjectId
// 或者
typeof mydoc._id</pre>
<div class="blog_h1"><span class="graybg">读写特性</span></div>
<div class="blog_h2"><span class="graybg">原子性/事务</span></div>
<p>在MongoDB中，写操作在<span style="background-color: #c0c0c0;">单个文档上</span>是原子的，即使在修改多个内嵌的文档的情况下。</p>
<p>相反的，当一个写操作操控了多个文档的情况下，整个操作默认不是原子的。多个写操作可能会发生交叉（interleave）、而且不会在中途出错后回滚</p>
<div class="blog_h3"><span class="graybg">$isolated</span></div>
<p>如果要为影响了多个文档的单个写操作建立隔离性语义，可以使用<pre class="crayon-plain-tag">$isolated</pre>操作符，该操作符可以避免其它写操作交叉进来。<span style="background-color: #c0c0c0;">直到写操作完成后，其它进程不能看到结果</span>。</p>
<p>注意：</p>
<ol>
<li>该操作符<span style="background-color: #c0c0c0;">不能与分片集群（Sharded Clusters）</span>协同工作</li>
<li>该操作符<span style="background-color: #c0c0c0;">不能提供all-or-nothing的原子性语义</span>。如果中途发生错误，已经发生的修改不会回滚</li>
<li>该操作符符导致当前操作获得<span style="background-color: #c0c0c0;">目标集合上的独占锁</span>，即使在使用支持文档级锁的引擎（WiredTiger）的情况下</li>
</ol>
<p>$isolated产生的隔离效果，在修改了第一个文档后显现。</p>
<div class="blog_h3"><span class="graybg">类事务语义</span></div>
<p>由于MongoDB支持内嵌文档，因此在很多应用场景下，单文档级别的原子性足够应付。</p>
<p>如果多个写操作需要在单个事务中执行，可以考虑在代码中实现两阶段提交（two-phase commit ）模式 。注意这只能提供类似于事务的语义，用于保证数据一致性，却不能避免中间结果被其它操作看到</p>
<div class="blog_h3"><span class="graybg">一致性控制</span></div>
<p>一致性控制允许多个程序并发的执行，而不导致数据不一致或者冲突。</p>
<p>实现一致性控制的可选方式：</p>
<ol>
<li>在应当具有唯一性值的（一个或者多个）字段上创建一个唯一性索引，以阻止重复的数据插入</li>
<li>使用查询断言（Query predicate）指定某个字段的当前期望值</li>
</ol>
<div class="blog_h2"><span class="graybg">并发性</span></div>
<p>为保证数据一致性，MongoDB使用锁机制来管理多客户端的并发读写。</p>
<div class="blog_h3"><span class="graybg">锁类型</span></div>
<p>MongoDB使用多粒度的锁，操作可能在全局、数据库、集合级别进行锁定。同时允许存储引擎实现更加细粒度的锁定，例如<span style="background-color: #c0c0c0;">WiredTiger支持文档级别的锁定</span>。</p>
<p>MongoDB使用读写锁（共享锁S，独占锁X），允许多个读操作共享同一资源。对于MMAPv1，写操作权限仅仅能赋予单个写操作。</p>
<p>除了S、X锁以外，MongoDB还支持读意向锁（IS）、写意向锁（IX），表示操作将要对（比被意向锁定的资源）更加细粒度的资源进行读写操作，IX是可以共享的，<span style="background-color: #c0c0c0;">意向锁可以减少不必要的锁检查</span>。当在某一级别进行锁定时，更高级别（粗粒度）资源被意向锁定。举例来说，当X锁一个集合进行写操作时，对应的数据库、全局必须上IX锁。</p>
<p>单个数据库可以同时被IS、IX锁。X锁不能与其它锁共存。S锁仅仅能和IS锁共存。</p>
<p>MongoDB的锁是公平的，操作依据排队的顺序被授予锁。但是为了吞吐量的考虑，兼容的锁可能被一并授予，而不考虑排队情况。</p>
<div class="blog_h3"><span class="graybg">锁粒度</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">存储引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>WiredTiger</td>
<td>
<p>从3.0开始，MongoDB引入该引擎</p>
<p>对于大部分的读写操作，该引擎使用乐观并发控制。WiredTiger仅仅在全局、数据库、集合级别使用意向锁。当检测到两个操作之间存在冲突时，其中一个操作会透明的重试</p>
<p>某些全局性的（通常是短暂的牵涉到多数据库的）操作，仍然要求全局（实例级别）的锁。drop之类的操作仍然要求数据库级别的独占锁</p>
</td>
</tr>
<tr>
<td>MMAPv1</td>
<td>从3.0开始，MMAPv1引擎使用集合级别的锁</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">让出锁</span></div>
<p>某些情况下，读写操作可能让出它们占有的锁。长时间运行的读写操作，可能在多种情况下让出锁。对于影响到多个文档的操作，锁让出可能在修改每个文档前后发生。</p>
<p>MMAPv1引擎会在认为需要读取的数据不在物理内存的时候，让出锁，等MongoDB把目标数据加载到内存之后，再重新获得锁。</p>
<div class="blog_h3"><span class="graybg">操作和锁</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">操作</td>
<td style="text-align: center;">锁定</td>
</tr>
</thead>
<tbody>
<tr>
<td>发起查询</td>
<td>S</td>
</tr>
<tr>
<td>从游标抓取数据</td>
<td>S</td>
</tr>
<tr>
<td>插入数据</td>
<td>X</td>
</tr>
<tr>
<td>删除数据</td>
<td>X</td>
</tr>
<tr>
<td>更新数据</td>
<td>X</td>
</tr>
<tr>
<td>MapReduce</td>
<td>S、X锁，除非操作被指定为非原子性的。Map Reduce任务的某些部分可以并发执行</td>
</tr>
<tr>
<td>创建索引</td>
<td>在前端（默认）创建索引，导致数据库级别的锁</td>
</tr>
<tr>
<td>aggregate()</td>
<td>S</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">读隔离/一致性/Recency</span></div>
<div class="blog_h3"><span class="graybg">隔离性保证</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 20%;">读取未提交</td>
<td>
<p>在MongoDB中，客户端可以在写操作持久化（durable）之前，即看到其结果：</p>
<ol>
<li>不论指定什么样的写关注选项。其它使用读关注选项local的客户端都可以在发起写操作获得确认（acknowledged）之前，就看到写的结果</li>
<li>使用local读关注的客户端，可能读取到之后被回滚的数据</li>
</ol>
<p>注意：local是默认的读关注</p>
<p>读取未提交是单实例mongod、复制集，以及分片集群的默认隔离级别</p>
</td>
</tr>
<tr>
<td>读取未提交&amp;单文档原子性</td>
<td>
<p>写操作对于单个文档是原子性的，因而任何客户端都不会读取到只更新了一部分字段的文档</p>
<p>对于单实例mongod，针对某个特定文档的一系列读写操作，是可串行化的（serializable）</p>
<p>对于复制集，仅仅在不存在回滚的情况下，针对某个特定文档的一系列读写操作，是可串行化的（serializable）</p>
</td>
</tr>
<tr>
<td>读取未提交&amp;多文档写操作</td>
<td>
<p>对于操作了多个文档的单个写操作，作为整体来说它不是原子性的，其它写操作可能与之产生交错。你可以使用$isolated操作符改变此行为，使用此操作符时MongoDB的行为如下：</p>
<ol>
<li>没有时间点快照：假设一个读操作在t1时刻开始读取一系列文档，一个写操作在随后的t2提交了针对其中一个文档的写入，则该写入可能被读操作读取到（不可重复读）</li>
<li>不可串行化操作：假设一个读操作在t1读取文档d1，一个写操作在后续的t3更新了d1。这一场景引入了读-写依赖 —— 如果操作能够被串行化，读操作必须发生在写操作之前。再假设写操作在t2更新了文档d2，读操作又在t4读取了d2，这会引入一个写-读依赖 —— 要求读操作发生在写操作之后。读-写、写-读依赖导致一个环形，因而是不可串行化的</li>
<li>读操作可能获取到不匹配查询条件的文档，因为在读处理过程中，文档可能已经被更新过</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">游标快照</span></div>
<p>MongoDB游标可能出现反常行为 —— 迭代同一个文档超过一次。发生这种现象的原因是，在游标迭代期间，某个写操作交叉进来并修改了某个文档。如果：</p>
<ol>
<li>被修改文档存储位置发生移动（例如文档大小增长导致）</li>
<li>或者查询所使用的索引对应的字段值被修改</li>
</ol>
<p>则可能出现重复迭代。</p>
<p>要防止此情形的出现，你可以使用<pre class="crayon-plain-tag">cursor.snapshot()</pre>调用来隔离游标。注意：</p>
<ol>
<li>该调用不能保证查询返回的数据是某个时间点的快照，不能和其它写操作隔离</li>
<li>不支持分片集群</li>
<li>不能和游标方法sort()、hint()联用</li>
</ol>
<p>另一个防止此情形的方法是，在不会修改的字段上建立唯一索引，然后使用hint()强制使用该索引，可以产生类似于snapshot()的效果。</p>
<div class="blog_h3"><span class="graybg">单调写</span></div>
<p>所为单调写（Monotonic Writes），是指<span style="background-color: #c0c0c0;">写操作扩散的顺序和它们逻辑上的顺序一致</span>。MongoDB针对单实例mongod、复制集、分片集群提供单调写保证。</p>
<div class="blog_h3"><span class="graybg">实时顺序</span></div>
<p>3.4引入的新特性。</p>
<p>针对在主节点上执行的读、写操作。使用linearizable读关注发起的读操作，和使用m:majority发起的写操作，如果这些读写操作针对单个文档进行操作，其效果就好像单个线程在执行这些读写一样，也就是说它们的实时顺序获得保证。</p>
<div class="blog_h2"><span class="graybg">分布式读</span></div>
<div class="blog_h3"><span class="graybg">针对分片集群的读</span></div>
<p>使用分片集群，你可以在多个mongod组成的服务器群中进行数据的分区，该分区对于应用程序来说几乎是透明的。</p>
<p>在分片集群中，客户端向关联到集群的某个mongos（路由器）实例发起操作请求，后者将请求转发给适当的分片：</p>
<p><img class=" wp-image-14982 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster.bakedsvg.png" alt="sharded-cluster-bakedsvg" width="466" height="373" /></p>
<p>当被转发到单个特定的分片上时，针对分片集群的读操作效率最高。</p>
<p>针对分片集合的查询，应当包含集合的分片键（Shard key），这样mongos可以基于集群元数据（来自配置服务器）进行准确的路由。如果不包含分片键，则必须向所有mongod实例转发请求并聚合响应，这种分散 - 聚集（scatter gather ）模式通常效率不高。</p>
<p>对于复制集分片，发送给从节点的读操作可能和主节点的最新状态不一致（不确定的延迟）。这种行为可能导致牵涉到多个从节点的非单调读 —— 后入库的数据比先入库的数据先被读到。 </p>
<div class="blog_h3"><span class="graybg">针对复制集的读</span></div>
<p>默认情况下，客户端<span style="background-color: #c0c0c0;">针对复制集的主节点进行读</span>操作。但是客户端可以使用<a href="https://docs.mongodb.com/manual/core/read-preference/">Read perference</a>把读请求定向到复制集的其它成员。例如，客户端可以配置以便从最近的节点进行读取以达成以下目标：</p>
<ol>
<li>在跨数据中心部署的情况下，减少延迟</li>
<li>可以分散大量读请求以增大吞吐量</li>
<li>执行备份操作</li>
<li>在新的主节点被选举出来前，仍然可以执行读操作</li>
</ol>
<p>针对从节点进行读操作，得到的可能不是主节点的最新状态，重定向到不同从节点的读操作可能导致非单调读。</p>
<div class="blog_h2"><span class="graybg">分布式写</span></div>
<div class="blog_h3"><span class="graybg">针对分片集群的写</span></div>
<p>对于分片集群中的分片集合，mongos负责把写操作分发到负责对应数据集的那些分片，路由时mongos从配置数据库（config database，位于config server）中查询元数据信息，并判断如何分发写操作。</p>
<p>MongoDB依据<span style="background-color: #c0c0c0;">分片键的值范围</span><span style="color: #c0c0c0;"><em>（支持Hash方式么）</em></span>来进行数据分区，之后把这些分区（chunks）发布到某些分片上（Shard，即mongod实例）。</p>
<p>对于update类操作来说，如果：</p>
<ol>
<li>针对单个文档，更新操作必须包含分片键或者_id字段</li>
<li>针对多个文档，如果更新操作包含分片键性能在某些情况下会更好，但是有时仍然会广播操作到所有分片</li>
</ol>
<p>如果分片键的值在每次插入时总是递增/递减，那么连续的很多插入操作会落到同一个分片中，产生性能瓶颈。 </p>
<div class="blog_h3"><span class="graybg">针对复制集的写</span></div>
<p>在复制集中，<span style="background-color: #c0c0c0;">所有写操作都针对主节点</span>。 主节点应用写操作，然后把操作记录在自己的操作日志（oplog）中。oplog是可重做的写操作流水，所有从节点都会复制该日志并应用，从节点的操作均是异步的，因此不能假设何时主从节点状态完全一致。</p>
<div class="blog_h2"><span class="graybg">两阶段提交</span></div>
<p>这是一种编程模式，当你进行多文档“事务”操作时，你可以实现类似关系型数据库的回滚功能。</p>
<div class="blog_h3"><span class="graybg">背景</span></div>
<p>在MongoDB中单文档的写操作总是原子的，但是写操作牵涉到多个文档时（通常称为多文档事务）则没有原子性保证。由于MongoDB支持任意复杂度的内嵌文档，因此很多应用场景下不需要多文档操作。</p>
<p>尽管如此，某些情况下你不得不进行多文档写操作。当执行由一系列写操作序列构成的事务时，可能面临以下需求：</p>
<ol>
<li>原子性：如果操作中途失败，前面已经完成的操作需要回滚</li>
<li>一致性：如果重大错误（网络、硬件）中断了事务，数据库必须有能力恢复到一个一致性的状态</li>
</ol>
<div class="blog_h3"><span class="graybg">模式</span></div>
<p>考虑转账的应用场景：你需要从账户A转账到账户B。使用关系型数据库时，你需要在单个事务中，减去A账户的余额并加到B账户中去。使用MongoDB时，你可以手工实现两阶段提交，模拟一个类似的结果。</p>
<p>在该场景中，我们使用集合accounts来存储账户信息，transactions存储交易信息。</p>
<p>正常交易流程的代码如下：</p>
<pre class="crayon-plain-tag">// 初始化账户信息，下面的调用返回一个BulkWriteResult对象
db.accounts.insert(
    [
        { _id: "A", balance: 1000, pendingTransactions: [] },
        { _id: "B", balance: 1000, pendingTransactions: [] }
    ]
)
// 初始化交易（转账）信息，state反应交易的状态，可以取值initial, pending, applied, done, canceling,canceled
db.transactions.insert(
    { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)

// 使用两阶段提交来转账
// 1、找到一个初始状态的交易记录
var t = db.transactions.findOne( { state: "initial" } )
// 2、更新交易状态，如果返回值的nMatched、nModified为零，说明被上一步找到的记录，被其它客户端处理过，
//    返回第1步，获取下一条记录
db.transactions.update(
    { _id: t._id, state: "initial" },
    {
        $set: { state: "pending" },
        $currentDate: { lastModified: true }
    }
)
// 3、把交易关联到两个账户，过滤条件pendingTransactions可以防止重复转账
db.accounts.update(
    { _id: t.source, pendingTransactions: { $ne: t._id } },
    { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
db.accounts.update(
    { _id: t.destination, pendingTransactions: { $ne: t._id } },
    { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
// 4、更新交易状态为applied
db.transactions.update(
    { _id: t._id, state: "pending" },
    {
        $set: { state: "applied" },
        $currentDate: { lastModified: true }
    }
)
// 5、更新账户第pending交易列表
db.accounts.update(
    { _id: t.source, pendingTransactions: t._id },
    { $pull: { pendingTransactions: t._id } }
)
db.accounts.update(
    { _id: t.destination, pendingTransactions: t._id },
    { $pull: { pendingTransactions: t._id } }
)
// 6、设置交易状态为done
db.transactions.update(
    { _id: t._id, state: "applied" },
    {
        $set: { state: "done" },
        $currentDate: { lastModified: true }
    }
)</pre>
<div class="blog_h3"><span class="graybg">从错误中恢复</span></div>
<p>交易的关键之处在于，能够从各种错误场景中恢复，而不是简单完成上述6个步骤。</p>
<p>两阶段提交模式允许应用程序应用程序执行一个操作序列，以便恢复事务到一致性的状态 —— 这种能力由代码中刻意安排的filter保证。你可以在应用程序启动时，或者周期性的执行恢复操作，以便捕获并处理那些未完成的事务。</p>
<p>事务经历多就没有完成，则需要恢复，取决于应用程序的需要，我们刻意使用lastModified判断事务的最后操作时间。</p>
<p>找到需要恢复的事务后，根据state确定尚未完毕的后续步骤，并执行。</p>
<div class="blog_h3"><span class="graybg">回滚操作</span></div>
<p>某些时候（例如交易被撤销，或者目标账户在交易过程中被关闭），你需要回滚（撤销，传统意义的回滚，在MongoDB中rollback这个术语通常情况下不是这个含义）一个事务操作。 </p>
<p>两阶段提交模式中，你需要手工实现“补偿行为”来进行回滚。</p>
<div class="blog_h1"><span class="graybg">CRUD操作</span></div>
<div class="blog_h2"><span class="graybg">连接到MongoDB</span></div>
<p>各类驱动连接MongoDB时，均需要提供一个连接字符串，其格式为：</p>
<pre class="crayon-plain-tag">mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
# 示例
mongodb://172.21.3.1:27017,172.21.3.2:27017/?replicaSet=rs3&amp;connectTimeoutMS=300000</pre>
<p>其中：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mongodb://</td>
<td>固定的前缀</td>
</tr>
<tr>
<td>username:password@</td>
<td>使用指定的密码来登录</td>
</tr>
<tr>
<td>host*</td>
<td>
<p>服务器的主机名、IP地址或者UNIX Domain Socket</p>
<p>对于复制集，指定复制集成员的信息</p>
<p>对于分片集群，指定mongos的信息</p>
</td>
</tr>
<tr>
<td>port*</td>
<td>服务器的监听端口，默认27017</td>
</tr>
<tr>
<td>/database</td>
<td>可选，当提供用户名密码时，针对哪个数据库进行验证，默认admin</td>
</tr>
<tr>
<td>options</td>
<td>
<p>连接选项：</p>
<p>replicaSet，指定连接到的复制集，连接到复制集时应该至少指定两个host:port并且指定复制集名称，如果不指定，客户端创建的是针对Standalone的mongod的连接</p>
<p>ssl，如果为true，以SSL协议发起连接，不是所有驱动都支持</p>
<p>connectTimeoutMS，连接超时的毫秒数</p>
<p>maxPoolSize，连接池中最大的连接数，默认100<br />minPoolSize，连接池中最小的连接数，默认0<br />maxIdleTimeMS，多余连接被移除之前，最大空闲时间<br />waitQueueMultiple，等待获取连接的调用者排队的最大数量<br />waitQueueTimeoutMS，等待超时时间</p>
<p>w，指定默认写关注的w选项值，可以指定数字、majority、标签集名称<br />wtimeoutMS，指定默认写关注的wtimeoutMS选项值<br />journal，指定默认写关注的j选项值</p>
<p>readConcernLevel，指定默认读隔离级别，可选local、majority<br />readPreference，指定如何对复制集进行读操作，可选值：primary、primaryPreferred、secondary、<br />                                   secondaryPreferred、nearest<br />maxStalenessSeconds，从从节点读取数据时，最大容忍数据有多旧（复制延迟于主节点的秒数）<br />readPreferenceTags，可以指定多次，从具有那些标签的节点读，示例：</p>
<pre class="crayon-plain-tag"># 指定两类tags和一个空tag
readPreferenceTags=dc:ny,rack:1&amp;readPreferenceTags=dc:ny&amp;readPreferenceTags= </pre>
<p>authSource，指定存放用户认证信息的数据库的名称，默认来自连接串的database项<br />authMechanism，认证方式，SCRAM-SHA-1、MONGODB-CR、MONGODB-X509、GSSAPI、PLAIN</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">插入文档</span></div>
<p>这类操作添加新的文档到集合中，如果目标集合不存在，会自动创建。MongoDB提供以下插入文档的方法：</p>
<pre class="crayon-plain-tag"># 插入一个文档
db.collection.insertOne()
# 插入多个文档
db.collection.insertMany()</pre>
<p> 从单个文档级别上来看，所有MongoDB的写操作都是原子的。</p>
<div class="blog_h3"><span class="graybg">Shell示例</span></div>
<pre class="crayon-plain-tag">use local
db.users.insertOne({ name : 'Alex', age : 29, gender : 'M'})
db.users.insertMany([
    { name: 'Meng', age: 26, gender : 'F'},
    { name: 'Cai', age: 2, gender : 'F'},
    { name: 'Dang', age: 0, gender : 'M'},
])</pre>
<div class="blog_h3"><span class="graybg">Python示例</span></div>
<pre class="crayon-plain-tag">from pymongo import MongoClient
if __name__ == '__main__':
    client = MongoClient('mongodb://localhost:27017/')
    db = client.local  # 或者 client['local']
    db.users.insert_one({
        'name': 'FengYu',
        'age': 59,
        'gender': 'F'
    })</pre>
<div class="blog_h3"><span class="graybg">Java示例</span></div>
<p>引入依赖：</p>
<pre class="crayon-plain-tag">&lt;!-- 同步驱动 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
    &lt;artifactId&gt;mongodb-driver&lt;/artifactId&gt;
    &lt;version&gt;3.4.2&lt;/version&gt;
&lt;/dependency&gt;

&lt;!-- 异步驱动，支持更快的非阻塞的IO --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
    &lt;artifactId&gt;mongodb-driver-async&lt;/artifactId&gt;
    &lt;version&gt;3.4.2&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>MongoDB的Java驱动提供了同步、异步两套接口。同步代码示例：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.junit.Test;

public class CRUDTest {

    @Test
    public void insertDoc() {
        MongoClient client = new MongoClient( "localhost", 27017 );
        MongoDatabase db = client.getDatabase( "local" );
        MongoCollection&lt;Document&gt; coll = db.getCollection( "users" );
        Document doc = new Document( "name", "CongHua" ).append( "age", 55 ).append( "gender", "M" );
        coll.insertOne( doc );
    }
}</pre>
<p>异步代码示例： </p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClients;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.async.client.MongoDatabase;
import org.bson.Document;
import org.junit.Test;

import java.util.concurrent.CountDownLatch;

public class CRUDTest {

    @Test
    public void insertDoc() throws InterruptedException {
        // 这个客户端相当于连接池，即使你有很多并发操作，也不需要第二个实例
        MongoClient client = MongoClients.create( "mongodb://localhost" );
        MongoDatabase db = client.getDatabase( "local" );
        MongoCollection&lt;Document&gt; coll = db.getCollection( "users" );
        Document doc = new Document( "name", "GuangFang" ).append( "age", 55 ).append( "gender", "F" );
        // 这里采用同步机制等待异步操作完成
        final CountDownLatch latch = new CountDownLatch( 1 );
        coll.insertOne( doc, ( result, t ) -&gt; {
            System.out.println( "OK" );
            latch.countDown();
        } );
        latch.await();
    }
}</pre>
<div class="blog_h3"><span class="graybg">Node.js示例</span></div>
<p>安装MongoDB的Node.js驱动：</p>
<pre class="crayon-plain-tag">npm install mongodb</pre><br />
<pre class="crayon-plain-tag">const mongo = require( 'mongodb' );
let client = mongo.MongoClient;
client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    return db.collection( 'users' ).insertOne( {
        name: 'GuangLiang',
        age: 55,
        gender: 'M'
    } );
} ).then( result =&gt; console.log( result.insertedId ) );</pre>
<div class="blog_h2"><span class="graybg">查询文档</span></div>
<p>这类操作从集合中检索并返回若干文档。MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag">db.collection.find(&lt;query&gt;, &lt;projection&gt;)
# 可以指定一个参数作为查询文档（Query Document），查询文档提供查询条件
db.collection.find( { 'name' : 'Alex' } )</pre>
<div class="blog_h3"><span class="graybg">Python示例</span></div>
<pre class="crayon-plain-tag">client = MongoClient('mongodb://localhost:27017/')
db = client['local']
# 传入空文档作为查询过滤器，得到所有文档，返回值是一个游标
cursor = db.users.find({})
# { &lt;field1&gt;: &lt;value1&gt; } 表示相等过滤，例如
cursor = db.users.find({'gender': 'F'})
# { &lt;field1&gt;: { &lt;operator1&gt;: &lt;value1&gt; } } 使用查询操作符，例如
cursor = db.users.find({'age': {'$gt': 30}})
# 逻辑与
cursor = db.users.find({'gender': 'F', 'age': {'$gt': 30}})
# 逻辑或
cursor = db.users.find({
    '$or': [{'gender': 'F'}, {'age': {'$gt': 30}}]
})</pre>
<div class="blog_h3"><span class="graybg">游标</span></div>
<p>查询操作的返回值是一个<a href="https://docs.mongodb.com/manual/reference/method/js-cursor/">游标</a>。你可以对其进行遍历操作：</p>
<pre class="crayon-plain-tag">cursor = db.trades.find({'state': 'A'})
for trade in cursor:
    print(trade)

# 对于PyPy、Jython以及其它不使用引用计数垃圾回收的Python实现，需要调用
cursor.close()</pre>
<div class="blog_h3"><span class="graybg">查询操作符</span></div>
<p>上面代码中，$or、$gt等以$开头的键，属于查询操作符（Query Operator），是操作符的一种。常用的查询操作符如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 17%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$eq</td>
<td>匹配等于指定值的字段值</td>
</tr>
<tr>
<td>$gt</td>
<td>匹配大于指定值的字段值</td>
</tr>
<tr>
<td>$gte</td>
<td>匹配大于等于指定值的字段值</td>
</tr>
<tr>
<td>$lt</td>
<td>匹配小于指定值的字段值</td>
</tr>
<tr>
<td>$lte</td>
<td>匹配小于等于指定值的字段值</td>
</tr>
<tr>
<td>$ne</td>
<td>匹配不等于指定值的字段值</td>
</tr>
<tr>
<td>$in</td>
<td>匹配等于数组中元素之一的字段值</td>
</tr>
<tr>
<td>$nin</td>
<td>匹配不等于数组中任何元素的字段值</td>
</tr>
<tr>
<td>$or</td>
<td>逻辑或</td>
</tr>
<tr>
<td>$and</td>
<td>逻辑与</td>
</tr>
<tr>
<td>$not</td>
<td>逻辑非</td>
</tr>
<tr>
<td>$nor</td>
<td>逻辑非或，逻辑或取反</td>
</tr>
<tr>
<td>$exists</td>
<td>
<p>匹配具有指定字段的文档，示例：</p>
<pre class="crayon-plain-tag"># 存在qty字段，且该字段的值不是5或者15
{ qty: { $exists: true, $nin: [ 5, 15 ] } }</pre>
</td>
</tr>
<tr>
<td>$type</td>
<td>匹配字段类型是指定类型的文档，类型使用数字或者别名表示</td>
</tr>
<tr>
<td>$mod</td>
<td>
<p>执行取模操作，示例：
<pre class="crayon-plain-tag"># 匹配qty为12的文档，因为12%4 = 0
{ qty: { $mod: [ 4, 0 ] } }</pre>
</td>
</tr>
<tr>
<td>$regex</td>
<td>
<p>匹配其值匹配指定正则式的字段，格式：
<pre class="crayon-plain-tag">{ &lt;field&gt;: { $regex: /pattern/, $options: '&lt;options&gt;' } }
{ &lt;field&gt;: { $regex: 'pattern', $options: '&lt;options&gt;' } }
{ &lt;field&gt;: { $regex: /pattern/&lt;options&gt; } }</pre>
</td>
</tr>
<tr>
<td>$text</td>
<td>
<p>对建立了文本索引（text index）的字段进行文本搜索与匹配，格式：
<pre class="crayon-plain-tag">{
  $text:
    {
      # 搜索内容，一个字符串，包含多个搜索关键词时进行逻辑或操作，除非指定为短语
      $search: &lt;string&gt;,
      # 可选，用于确定搜索的停用词（stop word）列表，以及词干分析器（Stemmer）
      # 和分词器（tokenizer）使用的规则，默认使用索引的语言
      $language: &lt;string&gt;,
      # 是否大小写敏感
      $caseSensitive: &lt;boolean&gt;,
      # 是否声调敏感
      $diacriticSensitive: &lt;boolean&gt;
    }
}</pre>
</td>
</tr>
<tr>
<td>$where</td>
<td>
<p>传递一个包含JavaScript表达式或者完整JavaScript函数的字符串给查询系统。尽管提供了很强的灵活性，这种操作符需要遍历所有文档，需要注意
<p>要引用当前正在处理的文档，可以使用变量<pre class="crayon-plain-tag">this</pre>或者<pre class="crayon-plain-tag">obj</pre></p>
</td>
</tr>
<tr>
<td>$geoWithin</td>
<td>匹配某个地理位置字段的值，在指定的多边形范围之内，2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$geoIntersects</td>
<td>选择其地理空间数据与指定的GeoJSON对象交叉的文档，2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$near</td>
<td>选择其地理空间数据接近指定的点的文档，需要地理空间索引（geospatial index），2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$nearSphere</td>
<td>选择其地理空间数据接近指定的点（在球面上）的文档，需要地理空间索引（geospatial index），2dsphere /2d 索引支持该查询操作符</td>
</tr>
<tr>
<td>$all</td>
<td>匹配包含此操作符指定的数组中所有元素的数组字段</td>
</tr>
<tr>
<td>$elemMatch</td>
<td>
<p>匹配这样的数组字段：至少有一个元素满足该操作符指定的查询条件，例如：</p>
<pre class="crayon-plain-tag"># results字段中，至少有一个元素的值在80-85之间
{ results: { $elemMatch: { $gte: 80, $lt: 85 } } }</pre>
</td>
</tr>
<tr>
<td>$size</td>
<td>匹配数组字段的尺寸</td>
</tr>
<tr>
<td>$bitsAllSet</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值均为1</td>
</tr>
<tr>
<td>$bitsAnySet</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值至少一个为1</td>
</tr>
<tr>
<td>$bitsAllClear</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值均为0</td>
</tr>
<tr>
<td>$bitsAnyClear</td>
<td>匹配这样的数字/二进制字段：该操作符指定的那些位的值至少一个为0</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">查询内嵌文档</span></div>
<p>要根据内嵌文档进行查询过滤，只需要依次指出内嵌文档各字段的值：
<pre class="crayon-plain-tag"># bson.son.SON类似于Python的字典，但是保持字段的顺序
# 匹配宽高为21/14cm的货物：
{"size": SON([("h", 14), ("w", 21), ("uom", "cm")])}
# 这种匹配，要求内嵌文档与查询条件完全相同，包括字段的顺序</pre>
<p>要根据内嵌文档的某个字段进行查询过滤，可以使用点号导航：</p>
<pre class="crayon-plain-tag">db.inventory.find({"size.uom": "cm"})
# 和简单字段一样，可以使用查询操作符
db.inventory.find({"size.h": {"$lt": 15}})</pre>
<div class="blog_h3"><span class="graybg">查询数组</span></div>
<pre class="crayon-plain-tag"># 匹配标签字段等于["red", "blank"]的存货
db.inventory.find({"tags": ["red", "blank"]})
# 匹配标签字段包含"red", "blank"这两个元素的存货
db.inventory.find({"tags": {"$all": ["red", "blank"]}})
# 匹配标签字段包含"red"元素的存货
db.inventory.find({"tags": "red"})
# 匹配size数组中至少有一个元素大于25的存货
db.inventory.find({"size": {"$gt": 25}})
# 匹配dim_cm数组中至少有一个元素大于15的存货、一个元素小于20的存货。可以单个元素同时满足连个条件
db.inventory.find({"dim_cm": {"$gt": 15, "$lt": 20}})
# 匹配dim_cm数组中至少有一个元素同时满足多个条件的存货
db.inventory.find({"dim_cm": {"$elemMatch": {"$gt": 22, "$lt": 30}}})
# 限定dim_cm的第2个元素的最小值
db.inventory.find({"dim_cm.1": {"$gt": 25}})
# 限定数组的大小
db.inventory.find({"tags": {"$size": 3}})</pre>
<div class="blog_h3"><span class="graybg">投影操作</span></div>
<p>默认情况下，查询操作<span style="background-color: #c0c0c0;">返回匹配文档的全部字段</span>。为了减少传递给应用程序的数据量，你可以指定一个投影文档（Projection document）以限制返回的字段：</p>
<pre class="crayon-plain-tag"># 仅仅返回name字段
# 投影文档（第二个参数）的字段值，1表示结果包含此字段，0表示不包含，默认的_id被包含
db.users.find( { gender: 'F' }, { name: 1, _id: 0 } )</pre>
<p>注意，除了_id字段之外，所有投影文档字段的值必须相等，当值：</p>
<ol>
<li>都为0的时候，表示结果排除这些字段</li>
<li>都为1的时候 ，表示结果仅包含这些字段</li>
</ol>
<p>要包含/排除内嵌文档中字段到查询结果，可以使用点号导航：</p>
<pre class="crayon-plain-tag"># 包含地址的邮编字段
db.users.find( {}, { name: 1, address.zip: 1 } )
# 包含最后一个孩子
db.users.find( {}, { name: 1, children: { "$slice": -1 } } )</pre>
<div class="blog_h3"><span class="graybg">投影操作符</span></div>
<p>上例中的$slice也是操作符，它属于投影操作符的一种：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$</td>
<td>
<p>投影并得到数组的<span style="background-color: #c0c0c0;">第一个元素</span>，该操作符根据查询文档（find第一个参数）中某些查询条件进行投影</p>
<p>语法格式：</p>
<pre class="crayon-plain-tag">db.collection.find( { array: value ... }, { "array.$": 1 } )
db.collection.find( { array.field: value ...}, { "array.$": 1 } )</pre>
<p>注意，被限制的数组字段，必须存在于查询文档之中，value可以是查询操作符表达式</p>
<p>针对某个数组字段进行投影时，有如下限制：</p>
<ol>
<li>投影文档中仅能包含一个投影操作符$</li>
<li>查询文档中仅能包含被投影操作符$限定的数组字段， 包含其它数组字段可能导致未定义行为，下面的查询：<br />
<pre class="crayon-plain-tag">db.collection.find( { array: value, someOtherArray: value2 }, { "array.$": 1 } )</pre></p>
<p>是不正确的</p>
</li>
<li>查询文档针对被投影数组字段的查询条件只能有一个</li>
</ol>
<p>示例代码：</p>
<pre class="crayon-plain-tag">client = MongoClient('mongodb://localhost:27017/')
db = client['local']
db.users.drop()
db.users.insert_many([
    {
        'name': 'Alex',
        'age': 30,
        'children': [{'name': 'Dang', 'age': 0}, {'name': 'Cai', 'age': 2}]
    },
    {
        'name': 'Meng',
        'age': 26,
        'children': [{'name': 'Dang', 'age': 0}, {'name': 'Cai', 'age': 2}]
    },
    {
        'name': 'FengYu',
        'age': 59,
        'children': [
            {'name': 'Alex', 'age': 30}, {'name': 'Meng', 'age': 26}, {'name': 'WenJun', 'age': 26}
        ]
    }
])
db.users.find({'children': {'$elemMatch': {'age': {'$lt': 1}}}}, {'name': 1, '_id': 0, 'children': 1})
# { "name" : "Alex", "children" : [ { "name" : "Dang", "age" : 0 }, { "name" : "Cai", "age" : 2 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Dang", "age" : 0 }, { "name" : "Cai", "age" : 2 } ] }
db.users.find({'children': {'$elemMatch': {'age': {'$lt': 1}}}}, {'name': 1, '_id': 0, 'children.$': 1})
# { "name" : "Alex", "children" : [ { "name" : "Dang", "age" : 0 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Dang", "age" : 0 } ] }</pre>
<p>可以看到，增加了$操作符后，满足查询文档的查询结果中，对应数组字段，仅返回了第一个元素 </p>
<p>注意，数组元素是简单值的情况下，此操作符同样适用</p>
</td>
</tr>
<tr>
<td>$elemMatch</td>
<td>
<p>投影并得到数组的<span style="background-color: #c0c0c0;">第一个满足额外条件的元素</span>，该操作符明确指定投影条件，你可以基于<span style="background-color: #c0c0c0;">不存在于查询文档中的条件</span>进行投影、或者基于数组元素（内嵌文档）的字段进行投影。该操作符对处理结果进行二次过滤</p>
<p>示例代码：</p>
<pre class="crayon-plain-tag">db.users.find(
    {'children': {'$elemMatch': {'age': {'$lt': 1}}}}, 
    {'name': 1, '_id': 0, 'children' : {'$elemMatch': {'age': {'$gt': 1}}}})
# { "name" : "Alex", "children" : [ { "name" : "Cai", "age" : 2 } ] }
# { "name" : "Meng", "children" : [ { "name" : "Cai", "age" : 2 } ] }</pre>
</td>
</tr>
<tr>
<td>$meta</td>
<td>投影文档在$text操作期间被分配的分数</td>
</tr>
<tr>
<td>$slice</td>
<td>
<p>限制匹配数组所返回的元素的数量，示例：
<pre class="crayon-plain-tag"># 返回最前面5个元素
db.posts.find( {}, { comments: { $slice: 5 } } )
# 返回最后面5个元素
db.posts.find( {}, { comments: { $slice: -5 } } )
# 先跳过前20个元素，然后返回接着的10个元素
db.posts.find( {}, { comments: { $slice: [ 20, 10 ] } } )
#从倒数第20个元素开始，返回10个元素
db.posts.find( {}, { comments: { $slice: [ -20, 10 ] } } )</pre>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<p>注意，针对视图进行的find()操作不支持上表中的投影操作符。</p>
<div class="blog_h3"><span class="graybg">查询空/缺失字段</span></div>
<p>不同查询操作符处理null值的方式是不一样的，需要注意：</p>
<ol>
<li>等于操作符：<pre class="crayon-plain-tag">{ item : null }</pre>匹配item<span style="background-color: #c0c0c0;">为null或者不包含</span>item字段的文档</li>
<li>类型检查操作符：<pre class="crayon-plain-tag">{ item : { $type: 10 } }</pre>仅仅匹配item值为null的文档</li>
<li>存在性检查：<pre class="crayon-plain-tag">{ item : { $exists: false } }</pre>仅仅匹配不包含item字段的文档</li>
</ol>
<div class="blog_h2"><span class="graybg">更新文档</span></div>
<p>这类操作修改既有的文档，MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag"># 更新文档
db.collection.updateOne(&lt;filter&gt;, &lt;update&gt;, &lt;options&gt;)
db.collection.updateMany(&lt;filter&gt;, &lt;update&gt;, &lt;options&gt;)
# 替换掉文档
db.collection.replaceOne(&lt;filter&gt;, &lt;replacement&gt;, &lt;options&gt;)</pre>
<p>参数filter类似于查询文档，update表示更新文档 —— 使用更新操作符来指定哪些字段需要怎么样被更新，replacement则是替换文档，直接替换掉原有的文档。</p>
<p>更新文档的格式：</p>
<pre class="crayon-plain-tag">{
  &lt;update operator&gt;: { &lt;field1&gt;: &lt;value1&gt;, ... },
  &lt;update operator&gt;: { &lt;field2&gt;: &lt;value2&gt;, ... },
  ...
}</pre>
<p>更新代码示例：</p>
<pre class="crayon-plain-tag">db.inventory.update(
    # 过滤器，指定查询条件：item为paper
    {'item': 'paper'},
    # 更新文档：
    {
        # 可以同时设置多个字段
        '$set': {'size.uom': 'cm', 'status': 'P'},
        '$currentDate': {'lastModified': True}
    }
)

# 更新多个满足条件的文档
db.inventory.update_many(
    {'qty': {'$lt': 50}},
    {
        '$set': {'size.uom': 'in', 'status': 'P'},
        '$currentDate': {'lastModified': True}
    }
)</pre>
<div class="blog_h3"><span class="graybg">更新操作特性</span></div>
<ol>
<li>原子性：对于单个文档来说，更新操作总是原子性的</li>
<li>_id字段：该字段不能被更新</li>
<li>文档大小：当更新后，文档的尺寸大于先前分配的空间，则另外在磁盘上分配空间给它</li>
<li>字段顺序：_id总是保持在最前面。包含重命名操作的更新操作可能会改变字段的顺序</li>
</ol>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>upsert</td>
<td>如果设置为true，则当满足filter的文档不存在时，新的文档会依据更新文档来创建，并插入</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">更新操作符</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><em>更新字段的操作符</em></td>
</tr>
<tr>
<td>$inc</td>
<td>增加目标字段的值到给定的增量</td>
</tr>
<tr>
<td>$mul</td>
<td>将目标字段的值乘以一定的倍数</td>
</tr>
<tr>
<td>$rename</td>
<td>重命名字段</td>
</tr>
<tr>
<td>$setOnInsert</td>
<td>
<p>当一个更新操作导致了文档的插入时，设置以一个字段的值。当更新操作是对既有文档进行修改时，该操作符不起任何作用</p>
</td>
</tr>
<tr>
<td>$set</td>
<td>设置一个字段的值</td>
</tr>
<tr>
<td>$unset</td>
<td>移除某个字段</td>
</tr>
<tr>
<td>$min</td>
<td>仅当指定的值比目标字段当前值大的时候，进行更新</td>
</tr>
<tr>
<td>$max</td>
<td>仅当指定的值比目标字段当前值小的时候，进行更新</td>
</tr>
<tr>
<td>$currentDate</td>
<td>
<p>设置目标字段的值为当前日期：</p>
<pre class="crayon-plain-tag">{ $currentDate: { &lt;field1&gt;: &lt;typeSpecification1&gt;, ... } }
# 其中 typeSpecification格式：
# true，表示设置为当前日期
# { $type: "timestamp" }或者 { $type: "date" }明确的设置为当前时间戳或者日期</pre>
</td>
</tr>
<tr>
<td>$bit</td>
<td>
<p>对目标字段进行按位操作：
<pre class="crayon-plain-tag">{ $bit: { &lt;field&gt;: { &lt;and|or|xor&gt;: &lt;int&gt; } } } </pre>
</td>
</tr>
<tr>
<td>$isolated</td>
<td>
<p>阻止影响到多个文档的写操作，在彻底完成之前，中间结果被其它客户端看到。示例：
<pre class="crayon-plain-tag">db.foo.update(
    { status : "A" , $isolated : 1 },
    { $inc : { count : 1 } },
    { multi: true }
) </pre>
</td>
</tr>
<tr>
<td colspan="2"><em>更新数组的操作符</em></td>
</tr>
<tr>
<td>$</td>
<td>
<p>更新数组的第一个元素的值：
<pre class="crayon-plain-tag">{ "&lt;arrayField&gt;.$" : value }</pre>
</td>
</tr>
<tr>
<td>$addToSet</td>
<td>如果指定的元素不存在数组中，则将其加入</td>
</tr>
<tr>
<td>$pop</td>
<td>移除数组的第一个或者最后一个元素</td>
</tr>
<tr>
<td>$pullAll</td>
<td>移除数组中所有匹配的元素</td>
</tr>
<tr>
<td>$pull</td>
<td>移除所有匹配查询的数组元素</td>
</tr>
<tr>
<td>$push</td>
<td>添加一个元素</td>
</tr>
<tr>
<td>$each </td>
<td>
<p>修改$push、$addToSet 的行为，使之能够同时添加多个元素：
<pre class="crayon-plain-tag">{ $addToSet: { &lt;field&gt;: { $each: [ &lt;value1&gt;, &lt;value2&gt; ... ] } } }
{ $push: { &lt;field&gt;: { $each: [ &lt;value1&gt;, &lt;value2&gt; ... ] } } } </pre>
</td>
</tr>
<tr>
<td>$slice</td>
<td>
<p>修改$push的行为，限制被更新数组的长度：
<pre class="crayon-plain-tag">{
  $push: {
     &lt;field&gt;: {
       $each: [ &lt;value1&gt;, &lt;value2&gt;, ... ],
       $slice: &lt;num&gt;
     }
  }
}</pre>
<p>num的含义：</p>
<ol>
<li>取值0，表示把目标数组（field字段的值）设置为空数组</li>
<li>取值负数，表示仅保留数组的最后num个元素</li>
<li>取值正数，表示仅保留数组的最前num个元素</li>
</ol>
</td>
</tr>
<tr>
<td> $sort</td>
<td>修改$push的行为，对数组元素进行排序 </td>
</tr>
<tr>
<td>$position </td>
<td>修改$push的行为，指定新元素的插入位置 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">替换文档</span></div>
<p>所谓替换，就是指替换掉文档的所有字段 —— _id除外。</p>
<div class="blog_h2"><span class="graybg">删除文档</span></div>
<p>这类操作删除既有的文档。MongoDB提供以下方法：</p>
<pre class="crayon-plain-tag">db.collection.deleteOne(&lt;filter&gt;)
db.collection.deleteMany(&lt;filter&gt;)  # 如果过滤文档为空文档，则集合中所有文档被删除

db.collection.remove()              # 删除匹配过滤文档的文档，有多少删除多少</pre>
<p>过滤文档的格式参考查询文档。</p>
<div class="blog_h3"><span class="graybg">删除操作特性</span></div>
<ol>
<li>原子性：对于单个文档来说，删除操作是原子的</li>
<li>对索引的影响：删除操作不会drop掉索引，即使集合中所有文档都被删除</li>
</ol>
<div class="blog_h2"><span class="graybg">批量写操作</span></div>
<p>MongoDB为客户端提供了批量写操作能力。批量写操作<span style="background-color: #c0c0c0;">影响单个集合</span>。 应用程序可以为批量写操作指定一个可接受的确认级别（acknowledgement level）。</p>
<p>下面的方法用于执行批量插入、更新或者删除操作：</p>
<pre class="crayon-plain-tag">db.collection.bulkWrite(&lt;bulkOperationArray&gt;, &lt;options&gt;)</pre>
<p>bulkWrite支持insertOne、updateOne、updateMany、replaceOne、deleteOne、deleteMany这些写操作：</p>
<pre class="crayon-plain-tag">client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    // 批量写仍然是针对单个集合的操作
    return db.collection( 'users' ).bulkWrite( [
        { insertOne: { document: { _id: 1, name: 'Alex', age: 30 } } },
        { insertOne: { document: { _id: 2, name: 'Meng', age: 27, gender: 'F' } } },
        {
            updateOne: {
                filter: { name: 'Alex' },
                update: {
                    $set: { gender: 'M' }
                }
            }
        }
    ] );
} ).then( result =&gt; console.log( result ) );</pre>
<div class="blog_h3"><span class="graybg">有序和无序</span></div>
<p>批量写操作支持有序、无序两种方式：</p>
<ol>
<li>有序：串行化的执行，如果其中某个操作失败，则MongoDB会返回，不处理后续的操作</li>
<li>无序：并发的执行，如果其中某个操作失败，别的操作不受其影响</li>
</ol>
<p>对于分片集合，无序批量写操作通常比有序的快。</p>
<p>默认的，MongoDB进行有序批量写，除非你指定选项：<pre class="crayon-plain-tag">ordered : false</pre></p>
<div class="blog_h3"><span class="graybg">分片集合的批量插入</span></div>
<p>大批量的数据插入操作可能影响分片集群（Sharded cluster）的性能，对于批量插入，考虑以下策略：</p>
<ol>
<li>集合预切分（Pre-split）：如果分片集合当前是空的，则集合仅仅包含一个位于单个分片中的初始块（initial chunk）。MongoDB必须花费时间来接收数据、切分数据，然后把切分好的数据块发送到可用的分片中。要避免此性能损失，可以考虑集合的与切分</li>
<li>使用无序写：使用无序写选项可以提高性能，MongoDB会尝试把数据同时发送给多个分片</li>
<li>避免单调递增瓶颈（Monotonic Throttling）：如果你的分片键（ shard key ）在插入过程中单调的递增，则所有插入的数据都会进入集合的最后一个块 —— 总是在单个分片上</li>
</ol>
<div class="blog_h2"><span class="graybg">读隔离</span></div>
<p>查询选项<pre class="crayon-plain-tag">readConcern</pre>用于复制集、复制集分片，决定为查询返回什么数据：</p>
<pre class="crayon-plain-tag">readConcern: { level: &lt;"majority"|"local"|"linearizable"&gt; }</pre>
<p>支持该选项的操作包括：find、aggregate 、distinct、count 、parallelCollectionScan 、geoNear 、geoSearch。</p>
<p>注意：节点上的最新数据，不代表是复制集系统中的最新数据</p>
<div class="blog_h3"><span class="graybg">读关注级别（Concern Levels）</span></div>
<p>该选项可以取以下值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">级别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>local</td>
<td>默认值。查询返回实例最新的数据，不保证这些数据已经写入到复制集中大部分节点或者已经持久化到磁盘。类似于读取未提交</td>
</tr>
<tr>
<td>majority</td>
<td>
<p>MMAPv1引擎不支持</p>
<p>查询返回实例最新的、已经确认被复制集中大部分节点写入的数据。要使用该级别，你需要：</p>
<ol>
<li>使用--enableMajorityReadConcern选项启动mongod实例，或者在配置文件中设置 replication.enableMajorityReadConcern 为true</li>
<li>复制集必须使用 WiredTiger 引擎，且使用推举协议版本1</li>
</ol>
</td>
</tr>
<tr>
<td>linearizable</td>
<td>
<p>3.4版本新加入，用于确保读取到最新鲜（任何发生在之前的写操作都可以读到）、持久化的数据（不会被回滚）</p>
<p>查询返回尽可能新的数据，数据由这样的写操作产生：</p>
<ol>
<li>基于w:majority级别的、成功的写操作</li>
<li>这些写操作在当前读操作开始之前，已经被确认（acknowledged，即发起操作的节点认为写操作已经完成）</li>
</ol>
<p>对于 writeConcernMajorityJournalDefault=true的复制集，该级别返回绝不会被回滚的数据</p>
<p>对于 writeConcernMajorityJournalDefault=false的复制集，MongoDB不等待majority级别写操作持久化到磁盘，即确认相应的写操作。因此，在复制集成员丢失的情况下，majority级别写操作可能回滚</p>
<p>你<span style="background-color: #c0c0c0;">只能在复制集的主节点</span>上指定该读级别。并且，上文所述保证，仅仅在查询的过滤器<span style="background-color: #c0c0c0;">精确的匹配单个文档</span>时有效</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">写确认</span></div>
<p>写关注（Write concern）描述写操作请求的确认级别。这些写操作可以应用在单独的mongod、复制集或者分片集群。 对于分片集群，mongos实例会把写关注级别传递给分片。</p>
<p>从2.6开始，写操作的新协议集成了写关注，你不再需要在写操作之后紧跟着一个getLastError调用来指定写关注级别。</p>
<div class="blog_h3"><span class="graybg">写关注选项</span></div>
<p>写关注相关的选项包括： </p>
<pre class="crayon-plain-tag">{ w: &lt;value&gt;, j: &lt;boolean&gt;, wtimeout: &lt;number&gt; }</pre>
<p>其中：</p>
<ol>
<li>w选项：要求当前写操作已经传播到指定数量的mongod实例，或者具有指定tag的mongod实例</li>
<li>j选项：要求当前写操作已经写入到磁盘日志</li>
<li> wtimeout：等到上述两个条件达成的超时，防止无限阻塞</li>
</ol>
<div class="blog_h3"><span class="graybg">w选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;number&gt;</td>
<td>
<p>要求写操作请求已经传播到指定数量的mongod实例：</p>
<ol>
<li><pre class="crayon-plain-tag">w: 1</pre>要求确认写操作已经传播到单独的mongod实例，或者复制集中的主节点。这是默认取值</li>
<li><pre class="crayon-plain-tag">w: 0</pre>不要求写操作的确认，尽管如此，使用该选项时客户端可以收到套接字异常、网络错误等信息。与<pre class="crayon-plain-tag">j: true</pre>联用时，后者优先，因此需要确认写操作已经传播到单独mongod实例或者复制集中的主节点</li>
</ol>
<p>大于1的取值，仅仅针对复制集有意义。即要求确认写操作传播到包括主节点在内的N个复制集成员</p>
</td>
</tr>
<tr>
<td>majority</td>
<td>
<p>要求确认写操作已经传播到大部分的投票节点，包括主节点</p>
<p>当基于此取值的写操作调用返回后，使用 readConcern:majority的客户端可以读取到其写入的数据</p>
</td>
</tr>
<tr>
<td>&lt;tag set&gt;</td>
<td>要求确认写操作已经传播到复制集中具有指定标签（tag）的节点</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">j选项</span> </div>
<p><pre class="crayon-plain-tag">j: true</pre>从MongoDB获得确认：写操作已经被写到日志（journal）中。该选项本身不保证写操作不被回滚，回滚的原因可能是复制集的主节点发生故障转移。</p>
<p>从3.2开始，该选项导致写操作仅仅在：指定数量（w选项）的复制集节点都写了日志后才返回，之前仅仅要求主节点写了日志就返回（因而就不存在主节点故障转移导致的回滚问题？）</p>
<div class="blog_h3"><span class="graybg">wtimeout</span></div>
<p>指定一个毫秒的限制，但是仅仅用于w取值大于1的情况。如果不指定此选项或者指定为0，可能导致永久的阻塞。</p>
<p>当超时到达后，调用立即以一个错误返回，然而后续所要求的写关注可能成功。 当返回后，MongoDB不会撤销已经执行的数据修改。</p>
<div class="blog_h3"><span class="graybg">确认行为</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">w取值</td>
<td style="text-align: center;">不指定j</td>
<td style="text-align: center;">j:true</td>
<td style="text-align: center;">j:false</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4"><em>单独实例（Standalone）</em></td>
</tr>
<tr>
<td><strong>w: 1</strong></td>
<td>确认写入到内存</td>
<td>确认写入到磁盘日志</td>
<td>确认写入到内存 </td>
</tr>
<tr>
<td><strong>w: "majority"</strong></td>
<td>如果启用了日志，确认写入到日志 </td>
<td>确认写入到磁盘日志</td>
<td>确认写入到内存</td>
</tr>
<tr>
<td colspan="4"><em>复制集（Replica Sets）    </em></td>
</tr>
<tr>
<td><strong>w: "majority"</strong></td>
<td>
<p>行为取决于writeConcernMajorityJournalDefault：</p>
<ol>
<li>true 确认写入磁盘</li>
<li>false 确认写入到内存</li>
</ol>
</td>
<td> 确认写入到磁盘日志</td>
<td>确认写入到内存  </td>
</tr>
<tr>
<td><strong>w: &lt;number&gt; </strong></td>
<td> 确认写入到内存</td>
<td>确认写入到磁盘日志 </td>
<td>确认写入到内存</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">可追加游标</span></div>
<p>默认情况下，当客户端消费了游标中所有结果集后，MongoDB会自动关闭游标。</p>
<p>但是，对于定长（Capped）集合来说，你可以使用可追加游标（Tailable Cursor）。该游标在客户端耗尽所有结果集后仍然保持打开状态。从概念上来说，这种游标类似于UNIX命令tail -f。当客户端插入新数据到集合中后，可追加游标会继续取回文档。</p>
<p>在高写入量、不适用索引的定长集合上，可以使用这种游标。例如，MongoDB本身的复制机制，就是在主节点的oplog这个定长集合上使用可追加游标。</p>
<p>注意可追加游标的以下特性：</p>
<ol>
<li>该游标不使用索引，以自然顺序—— 在磁盘上的存储顺序 ——返回文档</li>
<li>由于不使用索引，可追加游标的最初扫描代价很高，但是一旦最初扫描的结果被耗尽后，再取回新的文档，成本很低</li>
<li>可追加游标可能变得不可用，可能的情况包括：
<ol>
<li>查询返回结果为空</li>
<li>游标返回位于集合尾部的文档，而应用程序随后删除了此文档</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">性能优化</span></div>
<div class="blog_h2"><span class="graybg">查询计划</span></div>
<p>MongoDB的查询优化器会分析查询，然后选择最高效的执行计划。后续执行相同查询时，会使用同样的执行计划。</p>
<p>查询优化器会缓存执行计划，但是仅仅缓存那些可以有多个执行路径的查询形状（Query shape，指查询断言、排序、投影的组合）。</p>
<p>查询规划器（query planner ）针对每个查询，搜索执行计划缓存，寻找匹配查询形状的计划。如果匹配的计划不存在，查询规划器会生成候选的计划，在“试用期间”评估它们，然后选取其中最高效的，并缓存该执行计划。</p>
<p>如果匹配的计划存在，查询规划器则会通过replanning机制重新评估其性能，如果评估不通过对应的缓存条目被清除。在发生清除的情况下，查询规划器会按照正常流程重新选择执行计划并缓存。</p>
<p>上述逻辑的流程图如下：</p>
<p><img class="aligncenter  wp-image-14990" src="https://blog.gmem.cc/wp-content/uploads/2015/05/query-planner-diagram.baked_-544x1024.png" alt="query-planner-diagram-baked" width="417" height="785" /></p>
<div class="blog_h3"><span class="graybg">相关API</span></div>
<p>你可以使用<pre class="crayon-plain-tag">db.collection.explain()</pre>或者<pre class="crayon-plain-tag">cursor.explain()</pre>调用获得目标查询的执行计划的统计信息。这些信息有助于帮助你分析如何建立索引。</p>
<div class="blog_h3"><span class="graybg">缓存的清除</span></div>
<p>当mongod重新启动或者关闭后，所有执行计划的缓存都被清除。</p>
<p>从2.6开始，提供了一些方法来控制缓存：<pre class="crayon-plain-tag">PlanCache.clear()</pre>清除所有缓存，<pre class="crayon-plain-tag">PlanCache.clearPlansByQuery()</pre>清除特定缓存。</p>
<div class="blog_h3"><span class="graybg">索引过滤器</span></div>
<p>索引过滤器决定查询优化器使用哪些索引来评估查询形状。当为查询形状指定了索引过滤器时，仅过滤器中包含的索引会被用来优化查询。</p>
<p>索引过滤器存在时，MongoDB会忽略 <pre class="crayon-plain-tag">hint()</pre>调用。鉴于此，谨慎的使用之。</p>
<p>要检查索引过滤器是否存在，获取db.collection.explain()或者cursor.explain()返回值的<pre class="crayon-plain-tag">indexFilterSet</pre>字段。</p>
<p>在服务器关闭后，索引过滤器不会持久化。MongoDB也提供了手工移除索引过滤器的命令。</p>
<div class="blog_h2"><span class="graybg">查询优化</span></div>
<p>本节内容简单的介绍一些优化查询的方向。</p>
<div class="blog_h3"><span class="graybg">使用索引</span></div>
<p>通过减少查询操作需要处理的数据的量，索引可以提升读操作、更新操作以及聚合管线部分阶段的性能。</p>
<p>如果你的业务通常基于某个、某些字段对集合进行查询，你可以考虑在字段上创建索引、复合索引。这可以避免查询操作进行全集合扫描。创建索引的示例代码：</p>
<pre class="crayon-plain-tag">db.inventory.createIndex( { type: 1 } )</pre>
<p>除了优化读操作外，索引还可以用来支持排序操作、优化存储空间利用。</p>
<p>由于MongoDB支持升序、降序读取索引，因此对于<span style="background-color: #c0c0c0;">单键索引来说，其方向不重要</span>。</p>
<p>在大部分情况下，查询优化器都会选择合适的索引。如果你需要强制指定一个索引，可以调用<pre class="crayon-plain-tag">hint()</pre></p>
<div class="blog_h3"><span class="graybg">使用$inc</span></div>
<p>该操作符用于增加或者减少字段的值。它在服务器端工作，不需要把原先的值取到客户端。</p>
<p>该操作符也避免了多个客户端同时get-and-set时的竞态条件。</p>
<div class="blog_h3"><span class="graybg">减少网络流量</span></div>
<p>如果知道需要返回的数据的量，可以使用limit()：</p>
<pre class="crayon-plain-tag">db.posts.find().sort( { timestamp : -1 } ).limit(10)</pre>
<p>另外，可以使用投影，仅仅返回需要的字段。 </p>
<div class="blog_h3"><span class="graybg">查询选择性</span></div>
<p>Query Selectivity用来度量查询断言（条件）过滤掉集合中文档的强度，针对主键的唯一性查询的选择性最高 —— 因为它只会匹配单个文档。查询选择性决定了是否能高效的使用索引，甚至是能否使用索引。</p>
<p>低选择性的常见例子是$nin、$ne查询操作符，它们通常都会匹配索引值域的很大一部分。这导致使用索引有时还不如直接全文档扫描快，因此索引不被使用。</p>
<p>如果使用正则式来指定字段值（条件），查询的选择性取决于正则式本身</p>
<div class="blog_h3"><span class="graybg">覆盖查询</span></div>
<p>所谓Covered Query是指索引即可满足查询所需的全部字段，不需要执行文档扫描的情况，需要配合投影使用：</p>
<pre class="crayon-plain-tag"># 复合索引
db.inventory.createIndex( { type: 1, item: 1 } )
# 覆盖查询
db.inventory.find(
    { type: "food", item:/^c/ },
    { item: 1, _id: 0 }  # 被覆盖，注意指定_id:0排除了主键字段，确保了覆盖
)</pre>
<p>覆盖查询通常具有很高的性能（相对那些需要检索文档的查询），原因包括：</p>
<ol>
<li>索引键值通常要比对应的文档小</li>
<li>索引常常驻留内存，或者在磁盘上顺序的分布 </li>
</ol>
<p>以下情况下，索引无法覆盖查询：</p>
<ol>
<li>任意索引字段，在任意一个文档中存储了数组时。当索引字段存储了数组后，索引称为多键索引（Multi-key Index），这种索引不支持覆盖查询</li>
<li>任何断言字段（条件）、投影字段（返回）是位于嵌入文档中的字段时</li>
</ol>
<p>针对分片集群的限制：当索引不包含分片键的时候，它不能覆盖针对分片集合的查询。一个例外是，查询断言仅针对_id且投影仅仅返回_id，即使_id不是分片键，也可以做到覆盖查询。</p>
<p>使用<pre class="crayon-plain-tag">db.collection.explain()</pre>调用可以查看目标查询是否是覆盖查询。</p>
<div class="blog_h2"><span class="graybg">写操作优化</span></div>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>集合上的每个索引，都增加了写入操作的成本。</p>
<p>对于插入/删除操作，MongoDB需要插入/删除集合上所有索引中的文档键。更新操作可能导致索引的一个子集的变更。</p>
<p>对于使用MMAPv1引擎的mongod，更新操作可能导致文档增长，超过为其分配的空间。这时MMAPv1需要把文档移动到一个新的地方，并更新索引索引，指向文档的新位置。这些成本较高，但是发生频率较低。</p>
<p>通常来说，索引带来的读性能提升，值得损耗写性能。尽管如此，不要盲目的创建索引，要评估既有索引是否真的有用。</p>
<div class="blog_h3"><span class="graybg">MMAPv1引擎相关</span></div>
<p>更新操作可能会改变文档的尺寸，例如添加新的字段。</p>
<p>对于MMAPv1引擎来说，如果更新操作导致文档尺寸超过当前分配尺寸，MongoDB会在磁盘上重新分配文档，确保有足够的连续空间可以存放文档。需要重新分配空间的更新操作，其效率更加低，特别是结合使用索引的情况下，因为需要修改文档的位置信息。</p>
<p>默认的，从3.0开始MongoDB总是分配2的N次方大小的空间。这可以尽量减少重新分配、高效的重用删除操作回收的空间，但是不能消除重新分配。</p>
<div class="blog_h3"><span class="graybg">存储性能</span></div>
<p>存储系统的硬件因子——随机存取能力、磁盘预读取、RAID等——对MongoDB写操作的性能影响很大。对于随机性的工作负载，SSD能够提供比HDD高100倍的性能。</p>
<p>为了防止意外宕机导致数据丢失，MongoDB使用预写式日志（write ahead logging）—— 变更首先发生在内存中，然后首先写入到日志。不直接写入存储引擎是因为日志文件是顺序写，速度快。如果MongoDB需要终止服务进程或者遭遇错误，可以使用日志文件恢复，把尚未完成的操作应用到存储引擎的数据文件中。</p>
<p>日志和数据文件的写入，存在对存储能力的争用，特别是二者保存在同一物理设备上时。</p>
<p>如果应用程序指定包含j选项的写关注，则mogod会减少日志写操作之间的间隔，从而增大总体的写负载。</p>
<p>日志写间隔可以通过运行时配置<pre class="crayon-plain-tag">commitIntervalMs</pre>来设置，减少此参数会增加写操作的数量，从而降低MongoDB的写容量。反之减少量写操作数量，却有更大概率在宕机时丢失数据。</p>
<div class="blog_h2"><span class="graybg">读懂执行计划</span></div>
<p>使用db.collection.explain()、cursor.explain()方法以及explain命令都可以获得执行计划相关的信息（包括执行统计信息）。</p>
<p>执行计划（上述方法或命令的结果）以阶段（stage）树的形式呈现。每个stage把自身的结果（例如文档或者索引键）传递给父节点。叶子节点访问文档或者索引，中间节点操控文档或者索引键，根节点是最终的stage，MongoDB从中获得结果集。</p>
<p>Stage是操作的描述，例如：</p>
<ol>
<li>COLLSCAN 全集合扫描</li>
<li>IXSCAN 索引键扫描</li>
<li>FETCH  读取文档</li>
<li>SHARD_MERGE 合并来自分片的结果</li>
<li>AND_SORTED 可在索引交叉时出现</li>
<li>AND_HASH 可在索引交叉时出现</li>
</ol>
<div class="blog_h3"><span class="graybg">输出内容</span></div>
<p>执行计划以JSON格式输出，重要的字段包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2">
<p><em><strong>queryPlanner 被查询优化器选取的执行计划的详细信息</strong></em></p>
<p>示例输出：</p>
<pre class="crayon-plain-tag">{
   "queryPlanner" : {
      "plannerVersion" : &lt;int&gt;,
      "namespace" : &lt;string&gt;,
      "indexFilterSet" : &lt;boolean&gt;,
      "parsedQuery" : {
         ...
      },
      "winningPlan" : {
         "stage" : &lt;STAGE1&gt;,
         ...
         "inputStage" : {
            "stage" : &lt;STAGE2&gt;,
            ...
            "inputStage" : {
               ...
            }
         }
      },
      "rejectedPlans" : [
         &lt;candidate plan 1&gt;,
         ...
      ]
  }
}</pre>
</td>
</tr>
<tr>
<td>namespace</td>
<td>查询在什么名字空间上运行，例如&lt;database&gt;.&lt;collection&gt;</td>
</tr>
<tr>
<td>indexFilterSet</td>
<td>应用到此查询形状的索引过滤器</td>
</tr>
<tr>
<td>winningPlan</td>
<td>
<p>被查询优化器选中的执行计划的细节信息，以Stage的树的形式呈现
</td>
</tr>
<tr>
<td>w***P.stage</td>
<td>Stage的名称</td>
</tr>
<tr>
<td>w***P.inputStage</td>
<td>Stage的输入（单个子节点）</td>
</tr>
<tr>
<td>w***P.inputStages</td>
<td>Stage的输入（多个子节点）</td>
</tr>
<tr>
<td>w***P.shards</td>
<td>针对每个分片的信息</td>
</tr>
<tr>
<td>rejectedPlans</td>
<td>被拒绝的候选计划的列表</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>executionStats 被选中计划的执行情况</em></strong></p>
<p>示例输出：</p>
<pre class="crayon-plain-tag">"executionStats" : {
   "executionSuccess" : &lt;boolean&gt;,
   "nReturned" : &lt;int&gt;,
   "executionTimeMillis" : &lt;int&gt;,
   "totalKeysExamined" : &lt;int&gt;,
   "totalDocsExamined" : &lt;int&gt;,
   "executionStages" : {
      "stage" : &lt;STAGE1&gt;
      "nReturned" : &lt;int&gt;,
      "executionTimeMillisEstimate" : &lt;int&gt;,
      "works" : &lt;int&gt;,
      "advanced" : &lt;int&gt;,
      "needTime" : &lt;int&gt;,
      "needYield" : &lt;int&gt;,
      "isEOF" : &lt;boolean&gt;,
      ...
      "inputStage" : {
         "stage" : &lt;STAGE2&gt;,
         ...
         "nReturned" : &lt;int&gt;,
         "executionTimeMillisEstimate" : &lt;int&gt;,
         "keysExamined" : &lt;int&gt;,
         "docsExamined" : &lt;int&gt;,
         ...
         "inputStage" : {
            ...
         }
      }
   },
   "allPlansExecution" : [
      { &lt;partial executionStats1&gt; },
      { &lt;partial executionStats2&gt; },
      ...
   ]
}</pre>
</td>
</tr>
<tr>
<td> nReturned</td>
<td>匹配查询的文档数量</td>
</tr>
<tr>
<td>executionTimeMillis</td>
<td>包含查询计划选择、计划执行在内的总计消耗时间</td>
</tr>
<tr>
<td>totalKeysExamined</td>
<td>总计扫描的索引条目数量</td>
</tr>
<tr>
<td>totalDocsExamined</td>
<td>总计扫描的文档的数量</td>
</tr>
<tr>
<td>executionStages</td>
<td>被选中计划各阶段的执行细节统计信息zbook g4</td>
</tr>
<tr>
<td>e***S.works</td>
<td>
<p>该阶段执行的工作单元数量，查询执行过程把整个工作划分为细小的单元。一个单元可能包括：
<ol>
<li>检查单个索引键</li>
<li>取回单个文档</li>
<li>对单个文档进行投影</li>
</ol>
</td>
</tr>
<tr>
<td>e***S.advanced</td>
<td>返回（或者提升，advance）到父Stage的中间结果数量</td>
</tr>
<tr>
<td>e***S.needTime</td>
<td>不是用来返回中间结果到父Stage的工作周期数。例如一个索引扫描Stage可能花费一个工作周期来定位索引的下一个位置，不是把所有工作周期都用来向父节点返回索引键</td>
</tr>
<tr>
<td>e***S.needYield</td>
<td>存储层请求查询系统让出它的锁的次数</td>
</tr>
<tr>
<td>e***S.isEOF</td>
<td>指出Stage是否到达的流的尾部</td>
</tr>
<tr>
<td>e***S.shards</td>
<td>针对每个分片的信息</td>
</tr>
<tr>
<td>e***S.inputStage.keysExamined</td>
<td>
<p>对于扫描索引的Stage（例如IXSCAN），该字段表示被检查的键的总数：</p>
<ol>
<li>对于单个连续范围的索引扫描，总数仅仅包含界内（in-bounds）键</li>
<li>对于多个非连续范围的索引扫描，总是还包括界外键，这些键虽然对结果无用，但是可能还是要读取（以便找到下一范围的起点）</li>
</ol>
</td>
</tr>
<tr>
<td>e***S.inputStage.docsExamined</td>
<td>
<p>该字段出现在文档扫描（COLLSCAN）阶段，或者FETCH之类取回文档的阶段</p>
<p>总计扫描的文档数量</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>serverInfo 返回MongoDB实例的信息</em></strong></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">应用示例</span></div>
<p>准备数据：</p>
<pre class="crayon-plain-tag">db.collection( 'inventory' ).insertMany(
    [
        { "_id": 1, "item": "f1", type: "food", quantity: 500 },
        { "_id": 2, "item": "f2", type: "food", quantity: 100 },
        { "_id": 3, "item": "p1", type: "paper", quantity: 200 },
        { "_id": 4, "item": "p2", type: "paper", quantity: 150 },
        { "_id": 5, "item": "f3", type: "food", quantity: 300 },
        { "_id": 6, "item": "t1", type: "toys", quantity: 500 },
        { "_id": 7, "item": "a1", type: "apparel", quantity: 250 },
        { "_id": 8, "item": "a2", type: "apparel", quantity: 400 },
        { "_id": 9, "item": "t2", type: "toys", quantity: 50 },
        { "_id": 10, "item": "f4", type: "food", quantity: 75 }
    ]
)</pre>
<p>获取不使用索引时的执行计划： </p>
<pre class="crayon-plain-tag">require( 'promise.prototype.finally' ).shim();
client.connect( 'mongodb://localhost:27017/local' ).then( db =&gt; {
    db.collection( 'inventory' ).find(
        { quantity: { $gte: 100, $lte: 200 } }
    ).explain( "executionStats" ).then( r =&gt; {
        console.log( r.queryPlanner.winningPlan.stage );    // COLLSCAN 表示全集合扫描
        console.log( r.executionStats.nReturned );          // 匹配并返回3条数据
        console.log( r.executionStats.totalDocsExamined );  // 总计检查10条（全部）数据
    } ).catch( e =&gt; console.log( e ) ).finally( () =&gt; process.exit() );
} );</pre>
<p>创建一个索引：</p>
<pre class="crayon-plain-tag">db.collection( 'inventory' ).createIndex( { quantity: 1 } )</pre>
<p>执行计划现在为：</p>
<pre class="crayon-plain-tag">console.log( r.queryPlanner.winningPlan.inputStage.stage );  // IXSCAN 表示索引扫描（在子Stage完成）
console.log( r.queryPlanner.winningPlan.stage );             // FETCH 直接抓取数据
console.log( r.executionStats.nReturned );                   // 匹配并返回3条数据
console.log( r.executionStats.totalDocsExamined );           // 总计检查3条数据

// 打印整个计划：
const util = require( 'util' );&lt;br&gt;console.log( util.inspect( r, { depth: null, colors: true } ) );
// 输出：
{ queryPlanner: 
   { plannerVersion: 1,
     namespace: 'local.inventory',
     indexFilterSet: false,
     parsedQuery: { '$and': [ { quantity: { '$lte': 200 } }, { quantity: { '$gte': 100 } } ] },
     winningPlan: 
      { stage: 'FETCH',
        inputStage: 
         { stage: 'IXSCAN',
           keyPattern: { quantity: 1 },
           indexName: 'quantity_1',
           isMultiKey: false,
           multiKeyPaths: { quantity: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { quantity: [ '[100, 200]' ] } } },
     rejectedPlans: [] },
  executionStats: 
   { executionSuccess: true,
     nReturned: 3,
     executionTimeMillis: 0,
     totalKeysExamined: 3,
     totalDocsExamined: 3,
     executionStages: 
      { stage: 'FETCH',
        nReturned: 3,
        executionTimeMillisEstimate: 0,
        works: 4,
        advanced: 3,
        needTime: 0,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        invalidates: 0,
        docsExamined: 3,
        alreadyHasObj: 0,
        inputStage: 
         { stage: 'IXSCAN',
           nReturned: 3,
           executionTimeMillisEstimate: 0,
           works: 4,
           advanced: 3,
           needTime: 0,
           needYield: 0,
           saveState: 0,
           restoreState: 0,
           isEOF: 1,
           invalidates: 0,
           keyPattern: { quantity: 1 },
           indexName: 'quantity_1',
           isMultiKey: false,
           multiKeyPaths: { quantity: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { quantity: [ '[100, 200]' ] },
           keysExamined: 3,
           seeks: 1,
           dupsTested: 0,
           dupsDropped: 0,
           seenInvalidated: 0 } },
     allPlansExecution: [] },
  serverInfo: 
   { host: '226ce0b60d62',
     port: 27017,
     version: '3.4.5',
     gitVersion: '520b8f3092c48d934f0cd78ab5f40fe594f96863' },
  ok: 1 
}</pre>
<div class="blog_h2"><span class="graybg">评估操作性能</span></div>
<p>本节介绍几种评估MongoDB操作性能的技术。</p>
<div class="blog_h3"><span class="graybg">数据库剖析器</span></div>
<p>MongoDB提供了一个数据库剖析器（database profiler ），它能够显示数据库中每个查询的性能特征。使用该剖析器可以定位运行缓慢的读写操作。</p>
<div class="blog_h3"><span class="graybg">db.currentOp()</span></div>
<p>该调用可以显示当前mongod实例上正在执行的操作的各项参数。调用方式：</p>
<pre class="crayon-plain-tag">db.currentOp({filterDocument})
// 示例
db.currentOp( { query: { $exists: true } , ns: 'bais.corps' } ).inprog</pre>
<p>可以传递一个过滤文档作为参数，该文档可以包含以下字段：</p>
<ol>
<li>$ownOps，如果设置为true，仅仅显示当前用户的操作</li>
<li>$all，如果设置为true，返回所有操作的信息，包括那些空闲连接上的操作、系统操作</li>
<li>任何输出字段都可以作为过滤条件使用 </li>
</ol>
<p>在单个mongod实例、复制集上，该调用的输出格式为：</p>
<pre class="crayon-plain-tag">{
    "inprog" : [/* 正在执行的操作列表 */], 
    "ok" : 1.0
}</pre>
<p>在分片集群的mongos上，该调用的输出格式为：</p>
<pre class="crayon-plain-tag">{ 
    // 每个分片的情况
    "raw" : {
        // 分片名称为键
        "rs1/mongo-11.gmem.cc:27017,mongo-12.gmem.cc:27017,mongo-13.gmem.cc:27017" : {
            "inprog" : [/* 正在执行的操作列表 */], 
            "ok" : 1.0, 
            "$gleStats" : {
                "lastOpTime" : Timestamp(0, 0), 
                "electionId" : ObjectId("7fffffff000000000000000e")
            }
        }
    }, 
    // 当前mongos上的情况
    "inprog" : [/* 正在执行的操作列表 */], 
    "ok" : 1.0
}</pre>
<p>上述输出的核心是inprog属性，它包括以下字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>desc</td>
<td>客户端的描述，其中包含了connectionId</td>
</tr>
<tr>
<td>threadId</td>
<td>用于处理此数据库连接的线程标识 </td>
</tr>
<tr>
<td>connectionId</td>
<td>发起操作的连接的标识符 </td>
</tr>
<tr>
<td>client</td>
<td>客户端的地址和端口，例如<pre class="crayon-plain-tag">"client" : "172.21.1.1:44938" </pre></td>
</tr>
<tr>
<td>appName</td>
<td>客户端提供的应用程序名称</td>
</tr>
<tr>
<td>opid</td>
<td>操作的标识符，可以传递给<pre class="crayon-plain-tag">db.killOp()</pre></td>
</tr>
<tr>
<td>active</td>
<td>
<p>提示该操作是否已经启动的布尔值：true表示操作已经启动；false表示操作空闲（idle）。当一个操作让出锁（yielded）给其它操作的情况下，此字段仍然为true </p>
</td>
</tr>
<tr>
<td>secs_running</td>
<td>操作已经持续的时间 。仅仅active=true时存在 </td>
</tr>
<tr>
<td>microsecs_running</td>
<td>操作已经持续的时间，以微秒计算。仅仅active=true时存在 </td>
</tr>
<tr>
<td>op</td>
<td>
<p>该操作的类型：none、update、insert、query、command、getmore、remove、killcursors</p>
<p>其中：</p>
<ol>
<li>query包含了读操作，不包含其它类型的CUD操作</li>
<li>command包含了大部分<a href="https://docs.mongodb.com/manual/reference/command/">数据库命令</a></li>
<li>insert, update,  delete分别对应插入、更新、删除</li>
<li>getmore 游标抓取操作</li>
</ol>
</td>
</tr>
<tr>
<td>ns</td>
<td>操作针对的名字空间，<pre class="crayon-plain-tag">&lt;database&gt;.&lt;collection&gt;</pre>形式</td>
</tr>
<tr>
<td>insert</td>
<td>如果op为insert则存在，包含正在被插入的文档</td>
</tr>
<tr>
<td>query</td>
<td>如果op不为insert则存在，包含查询、删除、更新的过滤文档。对于getmore操作，包含了对应的find的过滤文档或者aggregate的Stages文档</td>
</tr>
<tr>
<td>planSummary</td>
<td>执行计划的概要信息，便于调试缓慢查询</td>
</tr>
<tr>
<td>locks</td>
<td>
<p>当前操作持有的锁的类型：</p>
<ol>
<li>Global 全局锁</li>
<li>MMAPV1Journal MMAPv1的日志锁，用于同步日志写</li>
<li>Database 数据库级别锁</li>
<li>Collection 文档级别锁</li>
<li>Metadata 元数据锁</li>
<li>oplog oplog锁 </li>
</ol>
<p>以及锁定模式：</p>
<ol>
<li>R 共享锁</li>
<li>W 独占锁</li>
<li>r 共享意向锁</li>
<li>w 独占意向锁</li>
</ol>
</td>
</tr>
<tr>
<td>waitingForLock</td>
<td>当前操作是否在等待锁</td>
</tr>
<tr>
<td>msg</td>
<td>描述操作状态、进度的字符串 </td>
</tr>
<tr>
<td>progress</td>
<td>描述mapReduce或者索引构建的进度</td>
</tr>
<tr>
<td>killPending</td>
<td>当前操作是否被标记为要杀死。当操作进入下一个安全点后会终结</td>
</tr>
<tr>
<td>numYields</td>
<td>当前操作让出锁以便其它操作进行的次数 </td>
</tr>
<tr>
<td>fsyncLock</td>
<td>当前数据库是否被db.fsyncLock()锁定 </td>
</tr>
<tr>
<td>info</td>
<td>仅仅fsyncLock为true时存在，描述如何解锁数据库</td>
</tr>
<tr>
<td>lockStats</td>
<td>
<p>对于每类锁（类型+模式组合），报告以下统计信息：</p>
<ol>
<li>acquireCount 获取到锁的次数</li>
<li>acquireWaitCount 尝试获取锁时，进行的等待次数</li>
<li>timeAcquiringMicros  累计等到锁的时间</li>
<li>deadlockCount 等待锁的时候一共遭遇的死锁次数</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">mongotop</span></div>
<p>这是一个命令行工具，查看读写最繁忙的集合。该命令不能在mongos上执行。示例：</p>
<pre class="crayon-plain-tag">mongotop -u root -p root --authenticationDatabase admin

#                   ns    total    read    write    2017-08-25T15:33:53+08:00
#       local.oplog.rs      1ms     1ms      0ms                             
#   admin.system.roles      0ms     0ms      0ms                             
#   admin.system.users      0ms     0ms      0ms                             
# admin.system.version      0ms     0ms      0ms                             
#           bais.corps      0ms     0ms      0ms                             
#       bais.corptypes      0ms     0ms      0ms                             
#            bais.orgs      0ms     0ms      0ms                             
#  bais.system.indexes      0ms     0ms      0ms                             
#          bais.trades      0ms     0ms      0ms                             
#             local.me      0ms     0ms      0ms</pre>
<div class="blog_h3"><span class="graybg">mongostat</span></div>
<p>这是一个命令行工具，可以快速的查看当前mongos/mongod的概览信息，类似于vmstat：</p>
<pre class="crayon-plain-tag">mongostat -u root -p root --authenticationDatabase admin
# insert query update delete getmore command flushes mapped vsize   res faults qrw arw net_in net_out conn                time
#    *0    *0     *0     *0       0     2|0       0     0B  300M 19.0M      0 0|0 0|0   288b   16.8k    8 Aug 25 15:15:14.827</pre>
<p>该命令的输出，反映每秒的平均统计指标。字段列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>inserts</td>
<td>插入文档数量</td>
</tr>
<tr>
<td>query</td>
<td>查询操作的数量</td>
</tr>
<tr>
<td>update</td>
<td>更新操作的数量</td>
</tr>
<tr>
<td>update</td>
<td>删除操作的数量</td>
</tr>
<tr>
<td>getmore</td>
<td>游标抓取操作的数量</td>
</tr>
<tr>
<td>command</td>
<td>执行命令的数量，在复制集的从节点上，输出为local|replicated格式</td>
</tr>
<tr>
<td>flushes</td>
<td>对于 WiredTiger，为触发的检查点数量；对于 MMAPv1，为fsync操作数量</td>
</tr>
<tr>
<td>dirty</td>
<td>仅WiredTiger，WiredTiger缓存中脏字节占比</td>
</tr>
<tr>
<td>used</td>
<td>仅WiredTiger，WiredTiger缓存当前正被使用的占比</td>
</tr>
<tr>
<td>mapped</td>
<td>仅 MMAPv1，从上次mongostat调用以后，累计映射到内存的数据量（MB）</td>
</tr>
<tr>
<td>vsize</td>
<td>从上次mongostat调用以后，虚拟内存用量（MB）</td>
</tr>
<tr>
<td>non-mapped</td>
<td>仅 MMAPv1，从上次mongostat调用以后，累计没有映射到内存的数据量（MB）。仅仅使用--all 选项时出现</td>
</tr>
<tr>
<td>res</td>
<td>从上次mongostat调用以后，MongoDB进程使用的驻留内存累计数量</td>
</tr>
<tr>
<td>faults</td>
<td>仅 MMAPv1，页面错误次数</td>
</tr>
<tr>
<td>lr</td>
<td>仅 MMAPv1，多少百分比的读操作必须等待锁</td>
</tr>
<tr>
<td>lw</td>
<td>仅 MMAPv1，多少百分比的写操作必须等待锁</td>
</tr>
<tr>
<td>lrt</td>
<td>仅 MMAPv1，等待读锁消耗的平均时间</td>
</tr>
<tr>
<td>lwt</td>
<td>仅 MMAPv1，等待写锁消耗的平均时间</td>
</tr>
<tr>
<td>idx miss</td>
<td>仅 MMAPv1，导致页面错误的索引访问操作的占比</td>
</tr>
<tr>
<td>qr</td>
<td>等待读操作的队列深度</td>
</tr>
<tr>
<td>qw</td>
<td>等待写操作的队列深度</td>
</tr>
<tr>
<td>ar</td>
<td>活动的、正在执行读操作的客户端数量</td>
</tr>
<tr>
<td>aw</td>
<td>活动的、正在执行写操作的客户端数量</td>
</tr>
<tr>
<td>netIn</td>
<td>入站网络流量，单位字节</td>
</tr>
<tr>
<td>netOut</td>
<td>出站网络流量，单位字节</td>
</tr>
<tr>
<td>conn</td>
<td>打开的连接数</td>
</tr>
<tr>
<td>repl</td>
<td>成员的复制状态：<br />M 主节点<br />SEC 从节点<br />REC 正在恢复<br />UNK 未知<br />SLV 主从复制模式的从节点<br />RTR mongos节点<br />ARB 仲裁节点</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">解释计划</span></div>
<p>方法<pre class="crayon-plain-tag">cursor.explain()</pre>和<pre class="crayon-plain-tag">db.collection.explain()</pre>可以返回关于查询的执行计划，例如选取什么索引、执行的统计信息。</p>
<p>你可以在queryPlanner、executionStats 、allPlansExecution三种模式下运行这些方法，获取多或少的信息。</p>
<div class="blog_h1"><span class="graybg">聚合</span></div>
<p>聚合操作处理数据记录，并返回计算过后的结果。这类操作将多个文档中的数值进行分组，通过多种数学计算将其合并为单个数值。</p>
<p>MongoDB提供三种聚合操作途径：聚合管线（aggregation pipeline）、Map-Reduce函数（map-reduce function）、单意图聚合方法（single purpose aggregation methods） 。</p>
<div class="blog_h2"><span class="graybg">聚合管线</span></div>
<p>MongoDB的聚合框架，以数据处理管线的概念进行建模 —— 多个文档进入由多个阶段（stage）构成的管道，并被管道转换为单个聚合后的结果。每个阶段都会对文档进行某种转换。每个阶段的输入、输出文档没有一一对应关系，一个文档可以产生多个新文档，多个文档也可能被合并为单个文档。</p>
<p>最基本管线stage，提供<span style="background-color: #c0c0c0;">过滤器功能</span>，工作方式类似于查询和文档转换，修改输出文档的形式。</p>
<p>其它管线stage，提供依据指定字段来<span style="background-color: #c0c0c0;">分组、排序文档的工具</span>，以及<span style="background-color: #c0c0c0;">聚合数组（包括文档的数据）内容的工具</span>。此外，管线stage可以<span style="background-color: #c0c0c0;">使用操作符</span>来计算平均值、连接字符串…等操作。</p>
<p>通过MongoDB内置的native操作，管线可以提供高效的数据聚合能力。管线是最优选的聚合途径。</p>
<p>聚合管线可以支持分片集合。</p>
<p>在某些stage，聚合管线会利用索引来改善性能。聚合管线声明周期中，具有内部的优化阶段。</p>
<p>要使用聚合管线，可以调用<pre class="crayon-plain-tag">db.collection.aggregate( arrayOfStages )</pre>或者<pre class="crayon-plain-tag">aggregate</pre>命令。</p>
<div class="blog_h3"><span class="graybg">管线表达式</span></div>
<p>某些stage需要一个管线表达式来作为操作数，管线表达式说明如何来转换输入文档。表达式的格式类似于文档，并且可以包含其它表达式。</p>
<p>管线表达式仅可以操作管线中的当前文档，无法引用其它文档中的数据。通常来说，表达式都是无状态的，例外是那些累加器表达式。累加器用在<pre class="crayon-plain-tag">$group</pre>阶段，需要维护自身状态（例如总数、最大值）。</p>
<p>从3.2开始，某些累加器可以用在<pre class="crayon-plain-tag">$project</pre>阶段。 但是用在此阶段时不能跨文档的维护自身的状态。</p>
<div class="blog_h3"><span class="graybg">管线的行为</span></div>
<p>聚合管线操控单个集合，在逻辑上，是把整个集合推到管线上进行处理。为了优化性能，仅可能的使用以下策略避免全集合扫描：</p>
<ol>
<li>管线操作符和索引：当出现在管线的开头时，$match、$sort等管线操作可以使用索引。$geoNear 可以使用地理空间索引。从3.2开始，索引可以覆盖聚合管线，而避免扫描集合</li>
<li>尽早的过滤：如果聚合操作仅仅需要集合的一个子集，可以使用$match、$limit、$skip等stage来限制进入管线开头的文档数量</li>
<li>内部优化阶段（optimization phase）</li>
</ol>
<div class="blog_h3"><span class="graybg">管线优化</span></div>
<p>聚合管线内部有一个优化阶段，会尝试对管线进行塑形，以改善性能。要了解此优化阶段是如何工作的，以explain选项来调用aggregate()方法。随着MongoDB的版本发布，管线优化的实现可能会变化。</p>
<div class="blog_h3"><span class="graybg">管线的限制</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>结果集大小的限制</em></strong></p>
<p>从2.6开始，aggregate命令可以返回一个游标，或者把结果存储在一个集合中。每个文档的大小限制当前为16MB（BSON文档尺寸限制），如果某个文档超过此限制，命令会报错。注意这个限制仅仅针对作为结果的文档，在管线中间流转的文档不受限制。</p>
<p>从2.6开始aggregate()方法默认返回游标。对于aggregate命令来说，如果不指定cursor选项，也不在集合中存储结果，结果集会存放在一个大的文档中，该文档可能超过16MB限制而报错</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>内存限制</em></strong></p>
<p>管线的stage使用内存的限制是100MB，如果某个stage超过此限制，MongoDB会报错。为了处理大型数据集，应该使用allowDiskUse选项，以便stage的临时结果可以存储在磁盘上</p>
<p>从3.4开始，$graphLookup 阶段必须受限于100MB内存，allowDiskUse: true对该stage无效</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">关于分片集合</span></div>
<p>聚合管线支持针对分片集合进行操作，但是具有一些特殊的行为。</p>
<p>如果管线以精确匹配针对某个分片键的$match开头，则整个管线在匹配的分片上运行。在3.2之前的行为是，管线分拆在所有管线上运行，并且由主分片负责合并最后结果。</p>
<p>对于必须运行在多个分片上的聚合操作，如果不是必须在主分片上运行，这些操作会把结果路由到随机的分片上，由该分片负责合并结果，避免增加主分片的负担。$out、$lookup操作必须在主分片上运行。</p>
<div class="blog_h3"><span class="graybg">Stages</span></div>
<p>db.collection.aggregate()方法的参数是一个数组，每个数组元素表示一个阶段（Stage）。阶段是单键对象，键的名称以$开头，列于下面的表格中。</p>
<p>除了<pre class="crayon-plain-tag">$out</pre>、<pre class="crayon-plain-tag">$geoNear</pre>之外的所有Stage都<span style="background-color: #c0c0c0;">可以出现多次</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 16%; text-align: center;">Stage</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$collStats</td>
<td>
<p>依据集合或者视图来返回统计信息，3.4新增：</p>
<pre class="crayon-plain-tag">{
  $collStats:
    {
      // 在输出文档中添加延迟统计信息，histogram:true表示添加延迟直方图信息
      latencyStats: { histograms: &lt;boolean&gt; },
      // 在输出文档中添加存储统计信息
      storageStats: {}
    }
}</pre>
<p>输出文档包含以下字段：<br />ns  请求视图/集合的名字空间<br />localTime  服务器当前时间<br />latencyStats  和目标视图/集合有关的请求延迟信息集合<br />storageStats  和目标视图/集合有关的存储引擎统计信息</p>
</td>
</tr>
<tr>
<td>$project</td>
<td>
<p>对输入文档进行重新构形：<pre class="crayon-plain-tag">{ $project: { &lt;specification(s)&gt; } }</pre> </p>
<p>规格可以包含以下形式的字段：<br /><pre class="crayon-plain-tag">&lt;field&gt;: &lt;1 or true&gt;</pre>  包含输入文档的某些字段<br /><pre class="crayon-plain-tag">_id: &lt;0 or false&gt;</pre>  禁用输入文档的_id字段<br /><pre class="crayon-plain-tag">&lt;field&gt;: &lt;expression&gt;</pre>  以表达式的结果，添加/覆盖一个字段<br /><pre class="crayon-plain-tag">&lt;field&gt;:&lt;0 or false&gt;</pre>  排除一个除了_id之外的字段，一旦使用该形式，前面所有形式不得使用</p>
</td>
</tr>
<tr>
<td>$match</td>
<td>
<p>对输入文档集进行过滤，仅仅允许满足条件的文档进入下一Stage：</p>
<pre class="crayon-plain-tag">{ $match: { &lt;query&gt; } }</pre>
<p>查询条件的规格，与读操作的过滤条件语法一致 </p>
</td>
</tr>
<tr>
<td>$redact</td>
<td>
<p>根据文档本身存储的内容，来限制文档的内容：<pre class="crayon-plain-tag">{ $redact: &lt;expression&gt; }</pre> </p>
</td>
</tr>
<tr>
<td>$limit</td>
<td>限制传递到下一Stage的文档数量：<pre class="crayon-plain-tag">{ $limit: &lt;positive integer&gt; }</pre></td>
</tr>
<tr>
<td>$skip</td>
<td>跳过指定数量的文档，然后把剩下的传递到下一Stage：<pre class="crayon-plain-tag">{ $skip: &lt;positive integer&gt; }</pre></td>
</tr>
<tr>
<td>$unwind</td>
<td>展开某个数组字段，每个数组元素替换该字段形成一个输出文档，对于N元素的数组字段，形成N个输出文档</td>
</tr>
<tr>
<td>$group</td>
<td>
<p>根据指定的_id表达式来分组文档，可选的，应用一个或者多个累加器表达式</p>
<p>对于每个特定的_id表达式组合，输出一个文档 </p>
</td>
</tr>
<tr>
<td>$sample</td>
<td>从输入中随机选择指定数量的文档 </td>
</tr>
<tr>
<td>$sort</td>
<td>
<p>根据指定的key重新排序文档流，改变的仅仅是顺序，每个文档不会改变：</p>
<pre class="crayon-plain-tag"># 1表示升序，-1表示降序。前面的字段优先排序
{ $sort: { &lt;field1&gt;: &lt;sort order&gt;, &lt;field2&gt;: &lt;sort order&gt; ... } }</pre>
</td>
</tr>
<tr>
<td>$geoNear</td>
<td>
<p>根据距离指定地理空间点的远近对文档流进行排序，输出文档包括额外的distance字段，还可以包含地理位置标识符字段：
<pre class="crayon-plain-tag">{
    $geoNear: {
        spherical: 'boolean = false，使用2dsphere索引时应设为true，决定如何计算距离',
        limit: 'number = 100，返回的最大文档数',
        num: '功能和limit相同，优先级高',
        maxDistance: 'number，可返回文档距离中心点的最远多少',
        query: 'document，对输入文档进行过滤',
        distanceMultiplier: 'number，对查询结果的距离字段进行倍乘',
        near: '中心点，使用2dsphere索引时类型为GeoJSON点或者坐标对，使用2d索引时类型为坐标对',
        distanceField: 'string，输出文档字段，该字段包含计算出的距离，可以使用点号来指定为嵌入文档字段',
        minDistance: 'number，可返回文档距离中心点的最近多少',
        includeLocs:'string，可选，存放用来计算距离的那个点的坐标的输出文档字段'
    }

}</pre>
<p>注意：</p>
<ol>
<li>只能作为第一个Stage</li>
<li>必须指定distanceField</li>
<li>该Stage要求目标集合上建立地理空间索引（geospatial index）</li>
<li>该Stage要求目标集合最多具有一个2d或者2dsphere索引</li>
<li>你不需要指定集合中什么字段是存放了GeoJSON点或者坐标对（coordinate pair ），因为MongoDB可以从唯一地理空间索引推导出该字段</li>
<li>在query中不能指定$near操作符</li>
<li>不支持针对视图进行操作</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">db.places.aggregate([
   {
     $geoNear: {
        near: { type: "Point", coordinates: [ -73.99279 , 40.719296 ] },
        // 距离字段
        distanceField: "dist.calculated",
        maxDistance: 2,
        query: { type: "public" },
        // 存放用来计算到中心点距离的输入值的字段
        includeLocs: "dist.location", 
        num: 5,
        spherical: true
     }
   }
])

// 输出文档示例：
{
   "_id" : 8,
   "name" : "Sara D. Roosevelt Park",
   "type" : "public",
   "location" : {
      "type" : "Point",
      "coordinates" : [ -73.9928, 40.7193 ]
   },
   "dist" : {
      "calculated" : 0.9539931676365992,
      "location" : {
         "type" : "Point",
         "coordinates" : [ -73.9928, 40.7193 ]
      }
   }
}</pre>
</td>
</tr>
<tr>
<td>$lookup</td>
<td>
<p>对同一数据库中的另外一个非分片集合执行左外连接操作：
<pre class="crayon-plain-tag">{
   $lookup:
     {
       from: '被左外连接的集合',
       localField: '输入文档中用于匹配（等于）的字段',
       foreignField: '被连接集合中用于匹配的字段',
       as: '被连接文档在输出中对应的字段名'
     }
}</pre>
<p>示例：</p>
<p><pre class="crayon-plain-tag">// 订单左外连接库存
db.orders.aggregate([
    {
      $lookup:
        {
          from: "inventory",
          localField: "item",
          foreignField: "sku",
          as: "inventory_docs"
        }
   }
])
// 输出文档示例
{
  "_id" : 1,
   "item" : "abc",
  "price" : 12,
  "quantity" : 2,
  // 匹配的被连接文档，数组形式
  "inventory_docs" : [
    { "_id" : 1, "sku" : "abc", description: "product 1", "instock" : 120 }
  ]
}</pre>
</td>
</tr>
<tr>
<td>$out</td>
<td>必须作为最后一个Stage，把输入文档写到目标集合中：<pre class="crayon-plain-tag">{ $out: "&lt;output-collection&gt;" }</pre> </td>
</tr>
<tr>
<td>$indexStats</td>
<td>返回集合中每个索引使用情况的统计信息</td>
</tr>
<tr>
<td>$facet</td>
<td> 在单个Stage中处理多个聚合管线，这些管线针对同一组输入文档进行</td>
</tr>
<tr>
<td>$bucket</td>
<td>
<p>基于指定的表达式和桶边界，将输入文档划分到称为桶（bucket）的组中：</p>
<pre class="crayon-plain-tag">{
  $bucket: {
      // 分组的依据，针对每个输入文档进行评估
      groupBy: &lt;expression&gt;,
      // 桶的划分边界，[)区间，3个值确定2个区间，起始值作为桶的_id
      boundaries: [ &lt;lowerbound1&gt;, &lt;lowerbound2&gt;, ... ],
      // 如果groupBy的值没有落到boundaries声明的任何区间，则归入此_id为此值的桶
      default: &lt;literal&gt;,
      // 每个桶的输出字段列表，_id不需要指定
      output: {
         &lt;output1&gt;: { &lt;$accumulator expression&gt; },
         ...
         &lt;outputN&gt;: { &lt;$accumulator expression&gt; }
      }
   }
}

// 油画拍卖的例子
{ "_id" : 1, "title" : "社会的支柱", "artist" : "格罗斯", "year" : 1926,"price" : 199 }
// 聚合管线
db.artwork.aggregate( [
  {
    $bucket: {
      // 根据价格区间分组
      groupBy: "$price",
      boundaries: [ 0, 200, 400 ],
      default: "Other",
      output: {
        // 输出油画总数
        "count": { $sum: 1 },
        // 油画名称的数组
        "titles" : { $push: "$title" }
      }
    }
  }
] )
// 输出
{
  "_id" : 0,
  "count" : 1,
  "titles" : [
    "The Pillars of Society"
  ]
} </pre>
</td>
</tr>
<tr>
<td>$bucketAuto</td>
<td>
<p>与$bucket，只是boundaries不需要指定，根据需要的桶的数量自动划分：
<pre class="crayon-plain-tag">{
  $bucketAuto: {
      groupBy: &lt;expression&gt;,
      // 期望的分组的数量
      buckets: &lt;number&gt;,
      output: {
         &lt;output1&gt;: { &lt;$accumulator expression&gt; },
         ...
      }
      granularity: &lt;string&gt;
  }
} </pre>
</td>
</tr>
<tr>
<td>$sortByCount</td>
<td>
<p>根据指定表达式的值对输入文档进行分组，并且计算每个分组中文档的数量：
<pre class="crayon-plain-tag">{ $sortByCount:  &lt;expression&gt; }
// 等价于以下两个Stage的组合：
{ $group: { _id: &lt;expression&gt;, count: { $sum: 1 } } },
{ $sort: { count: -1 } }</pre>
</td>
</tr>
<tr>
<td>$addFields</td>
<td>
<p>为每个输入文档添加额外的字段：
<p><pre class="crayon-plain-tag">{ $addFields: { &lt;newField&gt;: &lt;expression&gt;, ... } } </pre>
</td>
</tr>
<tr>
<td>$replaceRoot</td>
<td>提升输入文档中的一个内嵌文档，将其作为根文档，代替原有文档 </td>
</tr>
<tr>
<td>$count</td>
<td>计算输入文档的总数：<pre class="crayon-plain-tag">{ $count: &lt;field_name&gt; }</pre>  </td>
</tr>
<tr>
<td>$graphLookup</td>
<td>
<p>在集合上执行递归的检索：</p>
<pre class="crayon-plain-tag">{
   $graphLookup: {
      // 被搜索的集合的名称
      from: &lt;collection&gt;,
      // 指定connectFromField的初始值
      startWith: &lt;expression&gt;,
      // 指定起始文档中用于匹配的字段名
      connectFromField: &lt;string&gt;,
      // 指定与起始文档进行匹配的，目标文档的字段名
      connectToField: &lt;string&gt;,
      // 保存被匹配文档链条的字段名
      as: &lt;string&gt;,
      // 递归匹配的最大深度
      maxDepth: &lt;number&gt;,
      // 添加到匹配文档链条中每个元素的“深度”字段的名字
      depthField: &lt;string&gt;,
      // 查询文档，为匹配指定额外的条件
      restrictSearchWithMatch: &lt;document&gt;
   }
}

// 示例：
{ "_id" : 1, "name" : "Dev" }
{ "_id" : 2, "name" : "Eliot", "reportsTo" : "Dev" }
{ "_id" : 3, "name" : "Ron", "reportsTo" : "Eliot" }
// 聚合管线：
db.employees.aggregate( [
   {
      $graphLookup: {
         from: "employees",
         startWith: "$reportsTo",
         connectFromField: "reportsTo",
         connectToField: "name",
         as: "reportingHierarchy"
      }
   }
] )
// 输出
{
   "_id" : 1,
   "name" : "Dev",
   "reportingHierarchy" : [ ]
}
{
   "_id" : 2,
   "name" : "Eliot",
   "reportsTo" : "Dev",
   "reportingHierarchy" : [
      { "_id" : 1, "name" : "Dev" }
   ]
}
{
   "_id" : 3,
   "name" : "Ron",
   "reportsTo" : "Eliot",
   "reportingHierarchy" : [
      { "_id" : 1, "name" : "Dev" },
      { "_id" : 2, "name" : "Eliot", "reportsTo" : "Dev" }
   ]
}</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">表达式</span></div>
<p>使用聚合管线时，很多的值可以使用表达式。表达式包括：
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">表达式类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>字段路径</td>
<td>用于读取输入文档中的字段，包括内嵌文档字段，以前缀<pre class="crayon-plain-tag">$</pre>开始，例如$user、$user.name</td>
</tr>
<tr>
<td>系统变量</td>
<td>一些预置的特殊对象，以前缀<pre class="crayon-plain-tag">$$</pre>开始。例如<pre class="crayon-plain-tag">$$CURRENT</pre>通常表示当前正在被处理的输入文档，因此$$CURRENT.&lt;field&gt;通常等价于$&lt;field&gt;</td>
</tr>
<tr>
<td>字面值</td>
<td>
<p>可以使用<pre class="crayon-plain-tag">{ $literal: &lt;value&gt; }</pre>形式指定，例如：</p>
<pre class="crayon-plain-tag">{ $literal: { $add: [ 2, 3 ] } }	  // { "$add" : [ 2, 3 ] }
{ $literal:  { $literal: 1 } }	 	  // { "$literal" : 1 }</pre>
</td>
</tr>
<tr>
<td>操作符表达式</td>
<td>
<p>类似于接受参数的函数：
<pre class="crayon-plain-tag">// 接受多参数的操作符
{ &lt;operator&gt;: [ &lt;argument1&gt;, &lt;argument2&gt; ... ] }
// 接受单参数的操作符
{ &lt;operator&gt;: &lt;argument&gt; } </pre>
<p>MongoDB提供了很多聚合管线专用操作符，对应了各类操作符表达式，主要包括：</p>
<ol>
<li>布尔表达式：$and、$or、$not</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-set/">集合表达式</a>：对数组进行集合操作，把数组当作集合处理——忽略重复元素</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-comparison/">比较表达式</a>：进行比较操作，除了$cmp之外均返回boolean</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-arithmetic/">算术表达式</a>：进行算术操作，某些操作符支持日期</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-string/">字符串表达式</a>：进行字符串查找、子串提取、分割等操作</li>
<li>文本搜索表达式</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-array/">数组表达式</a>：处理数组</li>
<li>变量表达式：<pre class="crayon-plain-tag">$let</pre>，定义在子表达式范围内可见的变量，并返回子表达式的值</li>
<li><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-date/">日期表达式</a>：处理日期和时间</li>
</ol>
</td>
</tr>
<tr>
<td><a href="https://docs.mongodb.com/manual/reference/operator/aggregation-group/">累加器表达式</a></td>
<td>
<p>聚合管线专用操作符中有一类特殊的累加器操作符，只能用于$group阶段，从3.2开始，部分可用在$project阶段（stage）。主要包括：$sum、$avg、$first、$last、$max、$min、$push、$addToSet等</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">例子：邮编数据</span></div>
<pre class="crayon-plain-tag">// 每条数据的结构
{
    "_id": "10280",        // 邮编
    "city": "NEW YORK",    // 市
    "state": "NY",         // 州
    "pop": 5574,           // 人口
    "loc": [               // 经纬
        -74.016323,
        40.710537
    ]
}
// 人口大于1000万的州
db.zipcodes.aggregate( [
    // 输出文档的_id作为分组的依据
    // 该stage决定的输出文档的结构，使用$xxx可以引用输入文档的字段
    // totalPop字段通过$sum操作符（聚合表达式）计算出
    { $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
    // 该stage针对上一stage的结果进行匹配
    { $match: { totalPop: { $gte: 10 * 1000 * 1000 } } }
] );

// 返回每州的平均城市人口
db.zipcodes.aggregate( [
    // 支持依多字段分组。先获得 州 - 市 - 人口总和
    { $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
    // 再次分组，州总人口除以城市数量。注意点号导航
    { $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }
] );

// 返回每个州里面最大和最小的城市
db.zipcodes.aggregate( [
    {
        // 按照州、市进行分组，统计人口总数
        $group: {
            _id: { state: "$state", city: "$city" },
            pop: { $sum: "$pop" }
        }
    },
    // 根据人口升序排列
    { $sort: { pop: 1 } },
    // 第二次分组，$last、$first分别取最后、最前的组内文档中的值
    {
        $group: {
            _id: "$_id.state",
            biggestCity: { $last: "$_id.city" },
            biggestPop: { $last: "$pop" },
            smallestCity: { $first: "$_id.city" },
            smallestPop: { $first: "$pop" }
        }
    },
    // 投影stage，重命名_id字段为state字段
    // 把城市名称、人口合并为文档
    // 注意投影是针对输入文档每个条目执行的
    {
        $project: {
            _id: 0,  // 设置为0表示明确的在输出文档中禁用该字段
            state: "$_id",
            biggestCity: { name: "$biggestCity", pop: "$biggestPop" },
            smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
        }
    }
] );</pre>
<div class="blog_h3"><span class="graybg">例子：用户参数</span></div>
<pre class="crayon-plain-tag">20// 每条数据的结构
{
    _id : "jane",
    joined : ISODate("2011-03-02"),
    likes : ["golf", "racquetball"]
}
{
    _id : "joe",
    joined : ISODate("2012-07-02"),
    likes : ["tennis", "golf", "swimming"]
}

// 规范化数据结构，_id转换为大写，命名为name，然后根据name升序排列
db.users.aggregate(
    [
        { $project: { name: { $toUpper: "$_id" }, _id: 0 } },
        { $sort: { name: 1 } }
    ]
);


// 根据加入的月份升序排列
db.users.aggregate(
    [
        {
            $project: {
                // 取得月份
                month_joined: { $month: "$joined" },
                name: "$_id",
                _id: 0 // 明确禁用_id字段，该字段是默认包含的
            }
        },
        { $sort: { month_joined: 1 } }
    ]
);
// 获得每月加入的总数，根据月份升序排列
db.users.aggregate(
    [
        { $project: { month_joined: { $month: "$joined" } } },
        { $group: { _id: { month_joined: "$month_joined" }, number: { $sum: 1 } } },
        { $sort: { "_id.month_joined": 1 } }
    ]
);
// 获得五个最被喜爱的体育运动
db.users.aggregate(
    [
        // 展开likes数组，意味着对于likes.lengt = 5的输入文档，将展开为5个输出文档
        { $unwind: "$likes" },
        // 根据喜爱的单项体育运动分组，统计总数（每个人只能喜爱一次，因此sum:1）
        { $group: { _id: "$likes", number: { $sum: 1 } } },
        // 逆序排列
        { $sort: { number: -1 } },
        // 取前五条
        { $limit: 5 }
    ]
);</pre>
<div class="blog_h2"><span class="graybg">MapReduce</span></div>
<p>Map-reduce是一种数据处理范式，用于浓缩海量的数据为有意义的聚合数据。</p>
<p>MongoDB支持map-reduce风格的聚合操作。总体上说，map-reduce操作由以下阶段组成：</p>
<ol>
<li>map阶段，处理多个文档，并针对每个输入文档产生一个或者多个键值对。此阶段由map函数中的emit语句完成</li>
<li>reduce阶段，处理map阶段的输出，把这些输出合并起来。对于具有相同键的所有值，应用reduce函数，聚合（累积）为一个值</li>
<li>可选的finalize阶段，对结果进行最终的修改</li>
</ol>
<p>类似于其它聚合操作，map-reduce也允许指定查询条件、排序方式、限制输入文档数量。</p>
<p>你需要自定义JavaScript函数来提供map、reduce、finalize逻辑。和<span style="background-color: #c0c0c0;">聚合管线比起来，JavaScript函数可以提供巨大的灵活性</span>，但是更加低效和复杂。</p>
<p>在Shell中，你可以调用<pre class="crayon-plain-tag">mapReduce</pre>命令，编程时则调用<pre class="crayon-plain-tag">db.collection.mapReduce()</pre></p>
<div class="blog_h3"><span class="graybg">mapReduce函数</span></div>
<pre class="crayon-plain-tag">db.collection.mapReduce(
    // map函数关联一个value到key，在map阶段之后产生若干key:[values]形式的键值对，作为reduce阶段的输入
    // map函数要求：
    // 1、使用this引用当前文档
    // 2、不得出于任何目的访问数据库
    // 3、必须是纯函数，不得有任何副作用
    // 4、单个emit调用可产生半数于BSON最大文档尺寸的键值对
    // 5、可以调用多次emit(key,value)以产生键值对
    &lt;map&gt;,
    // reduce整个[values]为单个对象，可以作为finalize阶段的输入
    // reduce函数要求：
    // 1、不得出于任何目的访问数据库
    // 2、不得影响外部系统
    // 3、对于只包含一个值的key，MongoDB不会调用reduce函数
    // 4、可以访问定义在scope中的全局变量
    // 5、由于针对同一key，reduce函数可能被调用多次，因此reduce函数的返回值的类型必须和map函数emit的value类型一致
    // 6、以下表达式必须成立：
    //    associative    reduce(key, [ C, reduce(key, [ A, B ]) ] ) == reduce( key, [ C, A, B ] )
    //    idempotent     reduce(key, [ reduce(key, valuesArray) ] ) == reduce( key, valuesArray )
    //    commutative    reduce( key, [ A, B ] ) == reduce( key, [ B, A ] )
    &lt;reduce&gt;,
    // 选项：
    {
        // 指定mapReduce结果的输出目的地，你可以指定
        // 1、一个集合的名字
        // 2、action:collection_name，输出到集合前应用指定的action
        // 3、inline，针对主节点进行mapReduce时可以输出到集合，针对从节点则只能inline
        out: {
            // action取值：
            // replace 如果collectionName存在，则替换其内容
            // merge  合并当前结果到collectionName，如果存在相同的key则覆盖之
            // reduce reduce当前结果到collectionName，对于相同的key（_id），使用此mapReduce的reduce函数进行reduce
            // collectoinName中文档的原型：{ "_id" : key, "value" : reducedValue }
            &lt;action&gt;: &lt;collectionName&gt;,
            // 在内存中完成mapReduce操作，并返回结果，指定此字段，则不能指定任何其它out字段
            inline : 1                    //
            [, db: &lt;dbName&gt;]              // 可以选择其它数据库
            [, sharded: &lt;boolean&gt; ]       // 设置为true则对输出集合启用分片，使用_id（即map的key）字段对输出集合进行分片
            [, nonAtomic: &lt;boolean&gt; ]     // 如果设置为true，则mapReduce的后处理阶段不会全局锁
        },
        // 用于过滤输入文档
        query: &lt;document&gt;,
        // 用于排序输入文档，可以优化性能。如果sort和emit的键一致，则可以减少reduce操作的次数。必须是集合的某个索引字段
        sort: &lt;document&gt;,
        // 限制进入map阶段的最大文档数量
        limit: &lt;number&gt;,
        // 早reduce完成之后，对最终结果进行修改，例如求平均
        finalize: &lt;function&gt;,
        // 定义map、reduce、finalize函数中可以访问的全局变量
        scope: &lt;document&gt;,
        jsMode: &lt;boolean&gt;,
        // 如果为true，则在result信息中包含耗时统计信息
        verbose: &lt;boolean&gt;
    }
)</pre>
<div class="blog_h3"><span class="graybg">分片集合</span></div>
<p>map-reduce支持将分片集合作为输入，并且其结果可以输出到分片集合上。</p>
<p>当作为输入时，MongoDB会自动把map-reduce任务派发到各分片上来并行执行，并等到所有分片上的任务全部完成。</p>
<p>当作为输出时，如果mapReduce的out字段具有sharded值，则MongoDB自动以_id为分片键，输出到分片集合中。</p>
<div class="blog_h3"><span class="graybg">并发性</span></div>
<p>Map-reduce操作由一系列的任务组成，这些任务的职责包括：从输入集合读取数据、执行map函数、执行reduce函数、在处理过程中输出到临时集合、写入到输出集合等。</p>
<p>在处理过程中，Map-reduce持有以下锁：</p>
<ol>
<li>在读阶段，持有读锁，每次锁100个文档</li>
<li>写入临时集合时，持有写锁，每次写操作持有一次</li>
<li>如果输出集合不存在，创建输出集合时持有写锁</li>
<li>如果输出集合存在，则输出操作（merge、replace、reduce等）持有写锁。<span style="background-color: #c0c0c0;">此写锁是全局的，会阻塞mongod实例上所有其它操作</span></li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>考虑一个订单集合，其文档原型如下：</p>
<pre class="crayon-plain-tag">{
    cust_id: "1000",   // 客户号
    ord_date: new Date( "2015-01-05" ),  // 订单日期
    status: 'A',
    price: 25,  // 订单金额
    // 订单详情
    items: [ { sku: "mmm", qty: 5, price: 3 }, { sku: "nnn", qty: 5, price: 2 } ] 
}</pre>
<p>下面的map-reduce操作，可以获得每个客户的订单总额：</p>
<pre class="crayon-plain-tag">db.collection( 'order' ).mapReduce(
    // 注意map函数必须是纯函数
    function () {
        // this为当前正在处理的文档
        // 调用emit可以生成输出文档
        emit( this.cust_id, this.price );
    },
    // 按照客户ID分组，所有价格构成数组。这样一个键值对送给reduce函数处理
    function ( keyCustId, valuesPrices ) {
        return Array.sum( valuesPrices );
    },
    // 选项，指定结果的输出集合
    { out: "order_prices" }
)</pre>
<div class="blog_h3"><span class="graybg">增量mapReduce</span></div>
<p>如果需要被处理的数据集持续增长，你可能需要进行增量mapReduce，而不是每次都针对完整数据集进行mapReduce。</p>
<p>要进行增量mapReduce，你需要：</p>
<ol>
<li>针对当前集合执行mapReduce，把结果存放到另外一个集合中</li>
<li>当更多的数据入库时，执行后续的mapReduce操作。使用query参数来过滤，仅仅匹配新的文档</li>
<li>使用finalize处理reduce之后的数据，例如求平均值</li>
</ol>
<p>统计用户会话时长信息的例子：</p>
<p>文档原型：</p>
<pre class="crayon-plain-tag">// ts为登录时间戳，是增量mapReduce的关键
{ userid: "a", ts: ISODate('2011-11-03 14:17:00'), length: 95 }</pre>
<p>增量mapReduce：</p>
<pre class="crayon-plain-tag">db.collection( 'sessions' ).mapReduce(
    // map，注意平均时间仅仅是占位符
    function () {
        var key = this.userid;
        var value = {
            userid: this.userid,
            total_time: this.length,
            count: 1,
            avg_time: 0
        };
        emit( key, value );
    },
    // reduce，总计在线时间、登录次数累加，平均登录时间再此无法计算
    function ( key, values ) {

        var reducedObject = {
            userid: key,
            total_time: 0,
            count: 0,
            avg_time: 0
        };

        values.forEach( function ( value ) {
                reducedObject.total_time += value.total_time;
                reducedObject.count += value.count;
            }
        );
        return reducedObject;
    },
    {
        // 增量：仅仅处理新增数据
        query: { ts: { $gt: ISODate( '2011-01-01 00:00:00' ) } },
        // reduce到统计集合，此集合必须最初的全量mapReduce初始化
        out: { reduce: "session_stat" },
        // 本次增量数据reduce到统计集合之后，才能计算平均时间
        finalize: function ( key, reducedValue ) {
            if ( reducedValue.count &gt; 0 )
                reducedValue.avg_time = reducedValue.total_time / reducedValue.count;
            return reducedValue;
        }
    }
) </pre>
<div class="blog_h2"><span class="graybg">单意图聚合操作</span></div>
<p><pre class="crayon-plain-tag">db.collection.count()</pre>、<pre class="crayon-plain-tag">db.collection.distinct()</pre>这些函数用于特殊目的的聚合操作，比较简单。</p>
<div class="blog_h1"><span class="graybg">全文检索</span></div>
<p>MongoDB支持对字符串内容进行检索查询。为了使用这种文本搜索功能，你需要使用文本索引（text index）以及<pre class="crayon-plain-tag">$text</pre>操作符。注意视图不支持文本搜索。</p>
<p>全文检索不是MongoDB的核心功能，如果有复杂的搜索需求，建议配合使用全文搜索引擎，例如<a href="http://lucene.apache.org/solr/">Solr</a>。</p>
<div class="blog_h2"><span class="graybg">行为</span></div>
<div class="blog_h3"><span class="graybg">停用词</span></div>
<p>MongoDB的全文搜索会忽略一些语言相关的单词，例如英语中的there、and、the等单词，这些词作为搜索关键字，没有意义。</p>
<div class="blog_h3"><span class="graybg">词根</span></div>
<p>如果某个文档包含单词blueberry，则你搜索blue不会有结果，但是搜索blueberries则匹配。即全文搜索支持复数形式的识别。</p>
<div class="blog_h2"><span class="graybg">文本索引</span></div>
<p>这类索引用于支持对字符串内容的检索。文本索引可以包含任何类型为字符串、字符串数组的字段。要支持文本搜索，你必须首先为集合创建文本索引：</p>
<pre class="crayon-plain-tag">db.stores.insert(
    [
        { _id: 1, name: "Java Hut", description: "Coffee and cakes" }
    ]
)
# 创建文本索引，该索引将针对name、description两个字段
db.stores.createIndex( { name: "text", description: "text" } )

# 删除文本索引，需要先查询到索引的名字
db.collection.getIndexes()
# 传入名字以删除
db.trades.dropIndex("name_text")</pre>
<p>注意：每个<span style="background-color: #c0c0c0;">集合只能拥有一个文本索引</span>，但是该索引可以覆盖多个字段。</p>
<div class="blog_h2"><span class="graybg">$text操作符</span></div>
<p>使用该操作符，可以对启用了文本索引的集合进行文本搜索：</p>
<pre class="crayon-plain-tag">// 返回包含 java、coffee shop两个词语的文档
db.stores.find( { $text: { $search: "java \"coffee shop\"" } } )
// 返回包含java、shop，但是不包含coffee的文档
db.stores.find( { $text: { $search: "java shop -coffee" } } )</pre>
<p>$search用于指定关键字，多个关键字使用空格分隔，如果某个关键字内部包含空格，必须以双引号包围整个关键字。MongoDB对所有关键字进行逻辑或操作。 </p>
<div class="blog_h2"><span class="graybg">相关性</span></div>
<p>文本搜索的结果默认是无序的。文本搜索会为每个匹配的文档计算一个相关性分数（relevance score），你可以访问该分数用来排序：</p>
<pre class="crayon-plain-tag">db.stores.find(
   { $text: { $search: "java coffee shop" } },
   // 把相关性分数投影为score字段
   { score: { $meta: "textScore" } }
).sort( { score: { $meta: "textScore" } } )</pre>
<div class="blog_h2"><span class="graybg">聚合管线</span></div>
<p>在聚合框架中使用文本搜索时，你需要在$match阶段使用$text操作符。类似的，要进行排序，你需要在$sort阶段使用$meta投影操作符。</p>
<p>在聚合管线中使用文本搜索，受到以下限制：</p>
<ol>
<li>包含$text操作符的$match必须是管线的第一个Stage</li>
<li>$text操作符仅能够在管线中出现一次</li>
<li>$text操作符不可出现在$and或者$or内部</li>
<li>默认搜索结构没有按照相关性排序，请使用$sort 阶段实现</li>
</ol>
<div class="blog_h2"><span class="graybg">中文支持</span></div>
<p>文本搜索支持多种语言，从3.2开始，<span style="background-color: #c0c0c0;">MongoDB Enterprise可以支持中文全文检索</span>。</p>
<p>为了支持中文和阿拉伯文等语言，MongoDB Enterprise集成了RLP（ Basis Technology Rosette Linguistics Platform ），来完成正规化、分词、分句、词干提取（stemming）、符号化等全文检索领域的职责。</p>
<div class="blog_h1"><span class="graybg">地理空间查询</span></div>
<div class="blog_h2"><span class="graybg">地理空间数据</span></div>
<p>在MongoDB中，你可以<span style="background-color: #c0c0c0;">GeoJSON对象</span>的形式或者<span style="background-color: #c0c0c0;">遗留的坐标对</span>形式，来存储地理空间数据。</p>
<div class="blog_h3"><span class="graybg">GeoJSON</span></div>
<p>要沿着类似于地球的球面，进行几何计算，你应当使用GeoJSON来存储位置数据。即使用这样的内嵌文档：</p>
<ol>
<li>包含一个名为type的字段，指定此GeoJSON对象的类型，例如Point、LineString、Polygon等</li>
<li>包含一个名为coordinates的字段，指定构成此GeoJSON对象的所有坐标值。如果指定经纬度坐标，则经度在前、纬度在后</li>
</ol>
<p>GeoJSON对象示例：</p>
<pre class="crayon-plain-tag">// 格式：
&lt;field&gt;: { type: &lt;GeoJSON type&gt; , coordinates: &lt;coordinates&gt; }
// 点：
location: {
    type: "Point",
    coordinates: [-73.856077, 40.848447]
}
// 线
scope: {
    type: "LineString", 
    coordinates: [ [ 40, 5 ], [ 41, 6 ] ]
}</pre>
<p>针对GeoJSON类型字段上的查询，计算行为是在球面上进行的，使用WGS84坐标参考系统。 </p>
<div class="blog_h3"><span class="graybg">坐标对</span></div>
<p>要在欧几里得平面上计算距离，在坐标对中存储位置信息同时使用2d索引。</p>
<p>通过 2dsphere索引并且把坐标对转换为GeoJSON对象，MongoDB支持遗留坐标对的球面计算。</p>
<p>坐标对的示例：</p>
<pre class="crayon-plain-tag">// 数组形式，推荐
&lt;field&gt;: [ &lt;x&gt;, &lt;y&gt; ]
&lt;field&gt;: [&lt;longitude&gt;, &lt;latitude&gt; ]

// 内嵌文档形式
&lt;field&gt;: { &lt;field1&gt;: &lt;x&gt;, &lt;field2&gt;: &lt;y&gt; }</pre>
<div class="blog_h2"><span class="graybg">地理空间索引</span></div>
<div class="blog_h3"><span class="graybg">2dsphere</span></div>
<p>这种索引用于支持在类似于地球的球面上执行几何计算。创建这类索引的方法为：</p>
<pre class="crayon-plain-tag">// location field必须为GeoJSON或者坐标对字段
db.collection.createIndex( { &lt;location field&gt; : "2dsphere" } )</pre>
<div class="blog_h3"><span class="graybg">2d</span></div>
<p>这种索引用于支持在二维平面上进行的几何运算。尽管这种索引可以支持 $nearSphere以实现球面几何运算，最好还是使用2dsphere索引。创建这类索引的方法为：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( { &lt;location field&gt; : "2d" } )</pre>
<div class="blog_h3"><span class="graybg">关于分片集合</span></div>
<p>地理空间索引不能作为分片键使用，但是分片集合的普通字段上可以存在地理空间索引。</p>
<p>对于分片集合，$near、$nearSphere等查询操作符不被支持，你可以考虑使用$geoNear聚合管线。</p>
<div class="blog_h3"><span class="graybg">关于覆盖查询</span></div>
<p>地理空间索引不能够覆盖任何查询。</p>
<div class="blog_h2"><span class="graybg">地理空间查询</span></div>
<p>相关的查询操作符包括：$geoWithin、$geoIntersects、$near、$nearSphere。</p>
<p>相关的查询命令为：geoNear。</p>
<p>相关的聚合管线Stage为：$geoNear。 </p>
<div class="blog_h1"><span class="graybg">数据建模</span></div>
<p>关系型数据库具有严格的Schema —— 例如表结构定义，但是MongoDB则允许你使用非常灵活的Schema，集合中文档的结构不被限制。这种灵活性可以让文档很方便的对应到一个复杂的实体。</p>
<p>实践中，一个集合中的文档的结构都是相似的。</p>
<p>对实体进行建模的关键挑战是在应用程序需求、数据库引擎的性能特征、数据检索图式之间寻求平衡。 </p>
<div class="blog_h2"><span class="graybg">文档结构</span></div>
<p>为MongoDB设计数据模型的关键决策是，如何表示数据之间的关系。关系的表达有两种风格：引用、内嵌文档。</p>
<div class="blog_h3"><span class="graybg">引用</span></div>
<p>规范化数据模型：使用一个字段来引用目标对象的_id，来建立两者的关系。</p>
<p>以下情况下，考虑使用引用：</p>
<ol>
<li>当内嵌文档会导致数据冗余，但是却不能提供足够高效的读性能时（相对其数据冗余的代价）</li>
<li>表示复杂的M2M关系时</li>
<li>对大型的层次化结构进行建模时</li>
</ol>
<p>引用的缺点是，客户端可能需要发起更多的查询请求。</p>
<div class="blog_h3"><span class="graybg">内嵌文档</span></div>
<p>非规范化数据模型：把目标对象的全部/部分数据以内嵌文档的形式，直接存放在当前对象中。这样，访问关联对象的时候不需要发出额外的查询。</p>
<p>以下情况下，考虑使用内嵌文档：</p>
<ol>
<li>两个实体之间是合成关系</li>
<li>O2M关系中，M总是通过O来访问时</li>
</ol>
<p>需要注意，内嵌文档/数组在MMAPv1引擎下，可能导致文档增长而产生磁盘数据块移动、碎片化，影响性能。</p>
<p>从3.0开始MongoDB使用2^N尺寸的空间分配，以最小化数据碎片化的可能性。</p>
<div class="blog_h2"><span class="graybg">影响因素</span></div>
<p>多个方面的因素影响你如何建模。</p>
<div class="blog_h3"><span class="graybg">文档增长</span></div>
<p>某些update操作会导致即有文档尺寸增长，例如添加数组元素、增加字段。</p>
<p>使用MMAPv1引擎时，文档增长是影响建模的考虑因素。因为文档尺寸超过预分配的大小时，引擎会重新在磁盘上分配空间，则意味着数据移动和可能的碎片化。</p>
<p>如果需要进行<span style="background-color: #c0c0c0;">频繁的改变文档大小的update操作，考虑使用引用而非内嵌文档</span>。你也可以使用预分配（pre-allocation）策略来避免文档增长。</p>
<p>从3.0开始的2^N尺寸分配可以减少重新分配空间的几率，并有效的重用因为移动文档而释放的空间。</p>
<div class="blog_h3"><span class="graybg">原子性</span></div>
<p>在MongoDB中，针对单个文档的写操作是原子的。因此内嵌文档风格可以用来保证原子性需求。</p>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>如果以读操作为主，考虑增加索引。索引可以提升查询性能，MongoDB自动为_id创建唯一索引。注意索引的以下行为：</p>
<ol>
<li>每个索引至少要求8KB的空间</li>
<li>索引对写操作有一定的负面影响。对于写读比很高的集合，索引的代价相对较高</li>
<li>每一个活动的索引都消耗磁盘、内存空间。</li>
</ol>
<div class="blog_h3"><span class="graybg">大量集合</span></div>
<p>通常情况下，使用大量的集合对性能没有太大影响，但是可能改善性能。创建大量集合时，要注意：</p>
<ol>
<li>每个集合需要至少占用若干KB的空间</li>
<li>每个集合至少有一个索引，因此需要占用至少8KB空间</li>
<li>对于每个数据库，一个名字空间文件&lt;database&gt;.ns存储了数据库的所有元数据。这些元数据包含每个集合及其索引的信息</li>
<li>MMAPv1限制了名字空间的数量，可以<pre class="crayon-plain-tag">db.system.namespaces.count()</pre>查询</li>
</ol>
<div class="blog_h3"><span class="graybg">大量小文档</span></div>
<p>如果某个集合中存放了大量的小文档（例如GPS轨迹记录），你可能需要考虑使用内嵌文档或数组，以改善性能。小文档本身表示了独立实体时例外。</p>
<p>如果小文档仅仅包含几个字节，需要考虑存储空间的优化：</p>
<ol>
<li>明确指定_id字段。如果不指定，MongoDB默认生成12字节的ObjectId，可能太长了</li>
<li>使用简短的字段名，MongoDB会把<span style="background-color: #c0c0c0;">字段名附带在每一个文档</span>中</li>
</ol>
<div class="blog_h3"><span class="graybg">文档生命周期</span></div>
<p>集合可以具有TTL特性，让文档在一定时间后自动过期而被删除。</p>
<p>如果应用程序仅仅需要使用最近插入的文档，考虑定长集合。</p>
<div class="blog_h1"><span class="graybg">索引</span></div>
<p>索引用于提升查询性能，没有索引，MongoDB查询必须进行全集合扫描，性能低下。</p>
<p>索引是一种特殊的数据结构，它有序的保存了集合的一小部分信息。索引存储的是特定字段、字段集的值，以值的大小顺序存储 —— 因而能够支持高效的相等性查找、范围查找。 从算法上讲，MongoDB和RDBMS的索引类似。</p>
<p>索引可以在后台构建，这样可以避免影响运行中的应用程序。</p>
<p>要创建索引，可以调用：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( &lt;key and index type specification&gt;, &lt;options&gt; )</pre>
<div class="blog_h2"><span class="graybg">默认_id索引</span></div>
<p>在集合最初被创建时，MongoDB在_id字段上创建唯一性索引。</p>
<p>使用分片集合时，如果_id不作为分片键，则<span style="background-color: #c0c0c0;">应用程序负责确保在集群范围内_id的唯一性</span>。</p>
<div class="blog_h2"><span class="graybg">单字段索引</span></div>
<p>顾名思义，就是针对单个字段创建的索引。</p>
<p>针对直接字段创建索引的示例：</p>
<pre class="crayon-plain-tag">// 升序索引
db.records.createIndex( { score: 1 } )
// 降序索引
db.records.createIndex( { score: -1 } )</pre>
<p>对于单字段索引，升序/降序并不重要，因为MongoDB可以两端访问索引。</p>
<p>类似的，还可以在内嵌文档字段上创建索引：</p>
<pre class="crayon-plain-tag">// 使用点号导航
db.records.createIndex( { "location.state": 1 } )</pre>
<div class="blog_h2"><span class="graybg">复合索引</span></div>
<p>MongoDB支持复合索引，即持有多个字段引用的单个索引。复合索引最多引用31个字段。要创建复合索引，调用：</p>
<pre class="crayon-plain-tag">// type取值1或者-1
db.collection.createIndex( { &lt;field1&gt;: &lt;type&gt;, &lt;field2&gt;: &lt;type2&gt;, ... } )</pre>
<p>注意：</p>
<ol>
<li>已经创建了哈希索引的字段不能包含在复合索引中</li>
<li>字段的顺序非常重要，第一个字段最优先排序，该字段值相同的记录，按照第二字段排序。注意索引按此排序存储在磁盘上</li>
<li>复合索引支持<span style="background-color: #c0c0c0;">针对索引前缀（Prefix）的查询</span>，例如字段一、字段二或者字段一。如果查询条件不包含第一个字段，则索引肯定无法使用</li>
</ol>
<div class="blog_h3"><span class="graybg">升序/降序</span></div>
<p>复合索引中，字段的索引类型（升降）决定了索引能否支持排序操作。例如：</p>
<pre class="crayon-plain-tag">db.events.createIndex( { "username" : 1, "date" : -1 } )
// 上述索引可以支持下面两种排序操作
db.events.find().sort( { username: 1, date: -1 } )  // 正序访问索引，读出的就是排序好的
db.events.find().sort( { username: -1, date: 1 } )  // 逆序访问索引，读出的就是排序好的
// 而不能支持下面的排序操作
db.events.find().sort( { username: 1, date: 1 } )   // 无论以什么方向读取索引，都需要额外的处理才能获得正确的顺序</pre>
<div class="blog_h2"><span class="graybg">多键索引</span></div>
<p>为了能够索引数组字段，MongoDB需要为每个数组元素创建索引键。这种多键（每元素）索引可以很好的支持针对数组字段的查询。</p>
<p>创建多键索引不需要特殊的API，MongoDB会自动发现目标字段是数组，进而自动创建多键索引。</p>
<div class="blog_h2"><span class="graybg">全文索引</span></div>
<p>从3.2开始，MongoDB引入了第3版的全文索引，关键特性包括：</p>
<ol>
<li>改善大小写不敏感性</li>
<li>支持音调不敏感</li>
<li>额外的分界符</li>
</ol>
<p>要创建全文索引，以text作为索引类型：</p>
<pre class="crayon-plain-tag">// 在comments字段上创建全文索引
db.reviews.createIndex( { comments: "text" } )
// 针对多个字段的全文索引
db.reviews.createIndex( { subject: "text", comments: "text" } )
// 针对所有包含字符串数据的字段进行索引
db.collection.createIndex( { "$**": "text" } )</pre>
<div class="blog_h2"><span class="graybg">哈希索引</span></div>
<p>基于Hash的分片需要在分片键上使用这种索引。使用哈希分片可以更加随机的分布数据。</p>
<p>要创建哈希索引，以hashed为索引类型：</p>
<pre class="crayon-plain-tag">db.collection.createIndex( { _id: "hashed" } )</pre>
<p>创建了哈希索引的字段，不能包含在复合索引中。 </p>
<div class="blog_h2"><span class="graybg">索引选项</span></div>
<p>创建索引时，第二个参数文档可以指定以下选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 17%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>background</td>
<td>
<p>默认情况下，创建索引会导致<span style="background-color: #c0c0c0;">当前数据库（而不仅仅是当前集合）的所有读写操作被阻</span>塞</p>
<p>设置该选项为true，则可以避免阻塞其它操作（但是发起索引创建的客户端总是被阻塞）。从2.4开始，支持在后台同时构建多个索引</p>
<p>没有完全构建好的索引，不会影响查询。在索引构建完毕之前，你不能执行影响索引所在集合的任何管理性操作，例如repairDatabase、drop目标集合</p>
<p>后台索引构建使用一种增量的、较为缓慢的方式（比起前台索引构建）</p>
<p>如果索引构建被中断（例如宕机），下次mongod启动时会以前台线程构建索引，这种情况下索引构建失败mongod会继续宕机</p>
</td>
</tr>
<tr>
<td>unique</td>
<td>默认false。是否创建唯一性索引，可以防止重复数据的插入 </td>
</tr>
<tr>
<td>name</td>
<td>为索引指定一个名称 </td>
</tr>
<tr>
<td>partialFilterExpression</td>
<td>
<p>用于创建部分索引 —— 仅仅对集合中部分文档进行索引。可以降低存储空间占用和性能影响</p>
<p>如果指定，则索引仅仅引用匹配过滤表达式的文档。过滤表达式包括$eq、$exist:true、$gt, $gte, $lt, $lte、$type以及位于顶级的$and 。示例：</p>
<pre class="crayon-plain-tag">// 针对烹调风格、餐馆名称创建索引，指对排名大于5的餐馆进行索引
db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)</pre>
<p>所有索引类型均支持该选项</p>
<p>如果会<span style="background-color: #c0c0c0;">导致不完整的结果集，则MongoDB不会使用部分索引</span>。这意味着，要使用部分索引，你的查询条件必须包含过滤表达式（或者其子集）。例如：</p>
<pre class="crayon-plain-tag">// 该查询可以用到上面的部分索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
// 下面的两个查询都不能使用上面的部分索引
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )</pre>
<p>比起稀疏索引，部分索引更加灵活，可以满足特殊需求</p>
</td>
</tr>
<tr>
<td>sparse</td>
<td>
<p>是否创建稀疏索引，如果是则占用较少的空间，但是行为会发生变化 </p>
<p>稀疏索引仅仅针对包含了索引字段的那些文档进行索引，即使字段的值为null文档也会被索引</p>
<p>如果会导致不完整的结果集，则MongoDB不会使用稀疏索引，除非你使用hint明确指定使用稀疏索引</p>
<p>2dsphere v2、geoHaystack、text这几种索引总是稀疏的</p>
</td>
</tr>
<tr>
<td>expireAfterSeconds</td>
<td>
<p>TTL索引是一种特殊的单字段索引，控制MongoDB保存文档的时间。适用于事件记录、日志类数据，例如：</p>
<pre class="crayon-plain-tag">db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )</pre>
<p>后台线程负责读取索引中的值，并删除过期的文档。对于复制集成员，后台线程仅仅在当前是主节点的情况下才执行删除操作。此操作会通过Replication机制传播到从节点 </p>
<p>注意：</p>
<ol>
<li>不能针对复合索引指定该选项</li>
<li>不能针对定长集合使用该选项</li>
</ol>
</td>
</tr>
<tr>
<td>storageEngine</td>
<td>指定使用的存储引擎：<pre class="crayon-plain-tag">{ &lt;storage-engine-name&gt;: &lt;options&gt; }</pre></td>
</tr>
<tr>
<td colspan="2"><strong><em>排序规则相关选项</em></strong></td>
</tr>
<tr>
<td>collation</td>
<td>
<p>设置索引的排序规则，示例：</p>
<pre class="crayon-plain-tag">collation: {
   locale: &lt;string&gt;,
   caseLevel: &lt;boolean&gt;,
   caseFirst: &lt;string&gt;,
   // 用于指定索引是否大小写敏感
   strength: &lt;int&gt;,     
   numericOrdering: &lt;boolean&gt;,
   alternate: &lt;string&gt;,
   maxVariable: &lt;string&gt;,
   backwards: &lt;boolean&gt;
} </pre>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>全文索引相关选项</em></strong></td>
</tr>
<tr>
<td>weights</td>
<td>以{ &lt;field&gt;: &lt;weight&gt; }的形式设置字段的权重，权重值范围1-99999</td>
</tr>
<tr>
<td>default_language</td>
<td>使用的默认语言</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">为复制集构建索引</span></div>
<p>从2.6开始，复制集的从节点支持在后台构建索引，而之前的版本从节点必须在前台构建。
<p>当主节点在后台构建好索引后，从节点自动在后台开始构建索引。对于分片集群，mongos会发送createIndex命令到每个分片上的复制集的主节点。</p>
<p>推荐使用本节的步骤来为复制集构建索引，避免影响数据库性能。</p>
<p>注意：</p>
<ol>
<li>确保oplog足够大，这样当索引创建操作完毕后，从节点不会太过落后而无法赶上主节点的节奏</li>
<li>该推荐步骤每次把一个成员移出复制集，因此不会影响所有从节点</li>
</ol>
<div class="blog_h3"><span class="graybg">操作步骤</span></div>
<p>要为从节点构建索引，针对所有从节点执行：</p>
<ol>
<li>停止一个从节点，并以独立模式启动（不带 --replSet选项）：<br />
<pre class="crayon-plain-tag">// 不使用默认的27017端口，防止构建索引期间，复制集的成员、客户端连接到该节点
mongod --port 47017</pre>
</li>
<li>使用createIndex命令创建索引</li>
<li>重新启动mongod，并添加到复制集中：<br />
<pre class="crayon-plain-tag">mongod --port 27017 --replSet rs0</pre></p>
<p>该节点将和主节点同步 </p>
</li>
</ol>
<p>要为主节点构建索引，你可以：</p>
<ol>
<li>在主节点上触发后台索引构建</li>
<li>或者，调用<pre class="crayon-plain-tag">rs.stepDown()</pre>让主节点优雅的变成从节点，集群会重新选择主节点。然后，对其执行从节点构建索引的推荐步骤</li>
</ol>
<div class="blog_h2"><span class="graybg">索引交叉</span></div>
<p>从2.6开始，MongoDB能够使用多个索引的交叉来满足查询。典型的索引交叉牵涉到两个索引，但是MongoDB支持多个索引、内嵌索引的交叉。</p>
<p>假设集合orders在qty和item上有索引，则查询：</p>
<pre class="crayon-plain-tag">db.orders.find( { item: "abc123", qty: { $gt: 15 } } )</pre>
<p>会用到索引交叉 —— 即使用item、qty两个索引的扫描结果进行逻辑与操作。查看执行计划，你可以看到AND_SORTED或者AND_HASH这样的Stage。</p>
<div class="blog_h3"><span class="graybg">关于索引前缀</span></div>
<p>复合索引的前缀可以参与到索引交叉中。</p>
<div class="blog_h3"><span class="graybg">关于复合索引</span></div>
<p>由于字段顺序、升序/降序都对复合索引有影响，复合索引可能无法满足某些查询：</p>
<ol>
<li>没有指定合理前缀作为查询条件</li>
<li>不匹配的升/降序排序规则</li>
</ol>
<p>这些情况下，索引交叉能够代替复合索引提升查询性能。</p>
<div class="blog_h2"><span class="graybg">索引策略</span></div>
<p>创建哪些索引最合适，取决于很多因素，例如：</p>
<ol>
<li>期望的查询的形状</li>
<li>读写比</li>
<li>系统的可用内存</li>
</ol>
<div class="blog_h3"><span class="graybg">载入内存</span></div>
<p>如果能够让整个索引位于内存之中，则可以避免磁盘扫描，获得很快的处理速度。执行：<pre class="crayon-plain-tag">db.collection.totalIndexSize()</pre> 可以获得某个集合的全部索引的尺寸。你的内存必须足够大，才能保证索引完整载入内存。</p>
<p>注意：索引并非总是需要完全载入内存。假设索引字段的值随着insert不断增长，而且查询总是针对最新添加的文档，则MongoDB仅仅需要在内存中载入部分索引内容。</p>
<div class="blog_h2"><span class="graybg">管理索引</span></div>
<p>查看集合上现存的索引：</p>
<pre class="crayon-plain-tag">db.collection.getIndexes()</pre>
<p>移除单个索引：</p>
<pre class="crayon-plain-tag">db.collection.dropIndex()</pre>
<p>移除除了_id之外的全部索引：</p>
<pre class="crayon-plain-tag">db.collection.dropIndexes()</pre>
<p>重新构建集合上的全部索引：</p>
<pre class="crayon-plain-tag">// 对于复制集，该操作不会传播到从节点
// 如果索引在创建时，指定了background:true，则索引会在后台重新创建
// 但是，_id的索引总是在前台创建，导致数据库写锁定
db.collection.reIndex()</pre>
<div class="blog_h2"><span class="graybg">度量索引</span></div>
<div class="blog_h3"><span class="graybg">$indexStats</span></div>
<p>你可以使用这一聚合管线Stage来获得索引的使用情况统计信息。</p>
<div class="blog_h3"><span class="graybg">explain()</span></div>
<p>在 executionStats模式下获得执行计划，可以获得查询的统计信息，包括使用的索引、扫描的文档数量、消耗的时间。</p>
<div class="blog_h2"><span class="graybg">hint()</span></div>
<p>要强制查询使用某个索引，可以调用此方法：</p>
<pre class="crayon-plain-tag">db.people.find(
   { name: "John Doe", zipcode: { $gt: "63000" } }
)
// 强制使用zipcode索引
.hint( { zipcode: 1 } )
// 阻止使用任何索引
.hint( { $natural: 1 } )</pre>
<div class="blog_h1"><span class="graybg">存储</span></div>
<div class="blog_h2"><span class="graybg">存储引擎</span></div>
<p>存储引擎是MongoDB的一个核心组件，它决定了数据如何被存储（内存和磁盘）。MongoDB支持多存储引擎，以便你根据不同的工作负载进行选择。</p>
<div class="blog_h3"><span class="graybg">WiredTiger</span></div>
<p>从3.2开始，这是默认的存储引擎。适用于大部分工作负载，推荐新项目使用该引擎。 </p>
<p>WiredTiger提供文档级的并发模型、检查点、压缩以及其它特性。</p>
<div class="blog_h3"><span class="graybg">MMAPv1</span></div>
<p>最初的存储引擎，3.2之前的默认存储引擎。适用于大量读写的工作负载，以及in-place更新。</p>
<div class="blog_h3"><span class="graybg">内存引擎</span></div>
<p>仅在MongoDB Enterprise中可用，在内存中保存数据，速度快。 </p>
<div class="blog_h2"><span class="graybg">WiredTiger</span></div>
<div class="blog_h3"><span class="graybg">文档级并发控制</span></div>
<p>WiredTiger使用文档级并发来控制写操作。因此，多个客户端可以在<span style="background-color: #c0c0c0;">同时修改某个集合中的不同文档</span>。 </p>
<p>对于大部分的读写操作，WiredTiger使用乐观并发控制。它仅仅在全局、数据库、集合级别使用意向锁。当引擎检测到两个操作之间存在冲突时，会透明的重试其中会引起写冲突的操作。 </p>
<p>某些全局操作，通常是牵涉多个数据库的短暂操作，仍然使用全局（实例级别）的锁。某些其它操作，例如drop集合，仍然需要数据库级别的独占锁。</p>
<div class="blog_h3"><span class="graybg">快照和检查点</span></div>
<p>WiredTiger支持多版本并发控制（ MultiVersion Concurrency Control ，MVCC）。在操作开始时，引擎提供数据集在那个时间点的精确快照 —— 位于内存中的一致性视图。</p>
<p>当写入到磁盘时，引擎以一种一致性的方式，把快照中的数据写入到所有相关的数据文件中。快照中的数据集，在数据文件中可以作为检查点（checkpoint），数据库可以恢复到检查点之前的状态。</p>
<p>默认的，WiredTiger每60秒、或者每产生2GB日志文件后，创建检查点。</p>
<p>在写入新检查点的过程中，以前的检查点仍然可用。因此，如果正在写入检查点时MongoDB崩溃，重启后它能够恢复到上一个检查点。</p>
<p>当WiredTiger原子的把对检查点的引用写入到元数据表（metadata table）之后，新的检查点变得可访问、永久化。一旦新检查点变得可访问，旧检查点的页被释放。</p>
<p>使用WiredTiger时，即使没有启用日志（journaling）。MongoDB仍然能够从上一个检查点恢复。但是要想恢复上一检查点后面的变动，需要日志的配合。</p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>WiredTiger使用预写式（Write-ahead）日志，联合检查点，来确保数据的安全性。日志（Journal）对上一个检查点以来的所有数据修改都进行了持久化。如果MongoDB宕机，可以重放（Replay）从上一检查点依赖的所有数据修改。日志文件存放在<pre class="crayon-plain-tag">$dbPath/journal/WiredTigerLog.&lt;sequence&gt;</pre>，其中sequence时从0000000001开始的序号。</p>
<p>WiredTiger为客户端发起的每一个操作，创建一条日志记录（journal record），该记录操作触发Mongod对数据库进行修改（包括集合、索引）的全部信息。</p>
<p>WiredTiger使用内存来作为日志记录的缓冲，多个线程协作以分配、拷贝自己的那部分缓冲。在以下情况下，日志记录缓冲被刷入磁盘：</p>
<ol>
<li>从3.2开始，每50ms</li>
<li>一旦检查点被创建</li>
<li>使用写关注j:true执行了写操作后</li>
<li>每当新的日志文件被创建后。MongoDB限制日志文件大小为100MB，因此每超过此大小，就同步日志缓冲到磁盘</li>
</ol>
<p>WiredTiger日志利用snappy库进行压缩，你可以通过<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.journalCompressor</pre>来指定其它压缩算法或者禁用压缩。日志条目最小128字节，如果条目内容小于128字节，则不会进行压缩。</p>
<p>设置<pre class="crayon-plain-tag">storage.journal.enabled</pre>为false可以禁用日志，以减少日志维护的成本。在独立MongoDB服务器上，禁用日志可能导致上一个检查点以来的数据丢失。在复制集上，数据丢失的可能性较小。</p>
<div class="blog_h3"><span class="graybg">压缩</span></div>
<p>使用WiredTiger引擎时，MongoDB支持所有集合、索引的压缩。压缩通过CPU时间来换取存储空间的节省。</p>
<p>默认的，WiredTiger利用snappy库对集合、索引前缀进行压缩。对于集合来说，还可选zlib进行压缩。你可以设置：</p>
<ol>
<li><pre class="crayon-plain-tag">storage.wiredTiger.collectionConfig.blockCompressor</pre>来改变集合的压缩算法</li>
<li><pre class="crayon-plain-tag">storage.wiredTiger.indexConfig.prefixCompression</pre>来禁用索引前缀压缩</li>
</ol>
<p>压缩可以在集合、索引级别设置，你可以在创建集合、索引的时候指定对应的选项。</p>
<p>在大部分的工作负载下，默认压缩算法在时间和空间之间保持了一个平衡。</p>
<div class="blog_h3"><span class="graybg">内存使用</span></div>
<p>使用WiredTiger时，MongoDB同时利用WiredTiger内部缓存、文件系统缓存。</p>
<p>从3.4开始，WiredTiger 内部缓存的内存用量，默认取内存总量/2 -1GB 和256MB两个值中较大的那个。参数<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.cacheSizeGB</pre>用于自定义内存用量。</p>
<p>通过文件系统缓存，MongoDB可以利用所有空闲内存，文件系统缓存中的数据是被压缩的。</p>
<div class="blog_h2"><span class="graybg">MMAPv1</span></div>
<p>这是MongoDB最初的，基于内存映射文件的引擎。当面临<span style="background-color: #c0c0c0;">大量插入、读取、原地（in-place，不需要移动文档）更新</span>时，性能比WiredTiger更好。从3.2开始，该引擎不再是MongoDB的默认存储引擎。</p>
<p>当写操作发生时，MMAPv1更新内存视图。如果启用了日志内存中的改变首先被写到日志中而不是直接写到数据文件中。</p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>为了确保所有数据集能够正确的持久化。MMAPv1使用磁盘日志，写磁盘日志比数据文件更加频繁，因为其效率较高。</p>
<p>使用默认配置的情况下，MMAPv1每60秒写一次磁盘（可以通过<pre class="crayon-plain-tag">storage.syncPeriodSecs</pre>修改），每100ms左右写一次日志文件（可以通过<pre class="crayon-plain-tag">storage.journal.commitIntervalMs</pre> 修改）。</p>
<div class="blog_h3"><span class="graybg">文档存储特性</span></div>
<p>MMAPv1的所有文档都是连续的存储在磁盘上的。如果文档update后变得过大，则必须重新分配磁盘空间。这意味着<span style="background-color: #c0c0c0;">文档本身的移动、全部索引的更新</span>，会影响性能并引起<span style="background-color: #c0c0c0;">存储碎片化</span>。</p>
<p>从3.0开始，MongoDB使用2^N尺寸的空间分配策略，多余的空间作为补白（padding），可以降低文档移动的几率。原来的精确适合（exact fit）空间分配策略，使用不包含update/delete操作的场景。</p>
<div class="blog_h3"><span class="graybg">内存用量</span></div>
<p>MMAPv1自动使用机器的全部空闲内存作为它的缓存，但是，一旦其它进程需要使用内存，MongoDB占据的内存会立即释放。</p>
<p>典型情况下，操作系统的虚拟内存系统管理MongoDB的内存使用，如果内存不够，MongoDB缓存会被交换到磁盘中。配备足够的内存，可以很大程度的提高性能。</p>
<div class="blog_h2"><span class="graybg">GridFS</span></div>
<p>GridFS是一套规范，用于存放那些大于BSON文档尺寸限制（16MB）的文件。</p>
<p>GridFS不是把文件存放在文档中，而是将其切分成多个部分（chunks），然后把这些部分分别存放在文档中。默认情况下，GridFS使用255KB的chunk，因此除了最后一个之外的chunk具有一致的大小。</p>
<p>GridFS使用两个集合来存放文件：</p>
<ol>
<li><pre class="crayon-plain-tag">fs.chunks</pre>，用于存放文件的chunks，文档原型如下：<br />
<pre class="crayon-plain-tag">{
    "_id" : &lt;ObjectId&gt;,         // 块的唯一标识
    "files_id" : &lt;ObjectId&gt;,    // 所属的文件的唯一标识  
    "n" : &lt;num&gt;,                // 从0开始的序号
    "data" : &lt;binary&gt;           // BinData类型的数据块
} </pre>
</li>
<li><pre class="crayon-plain-tag">fs.files</pre>，用于存放文件的元数据，文档原型如下：<br />
<pre class="crayon-plain-tag">{
    "_id" : &lt;ObjectId&gt;,            // 文件的唯一标识
    "length" : &lt;num&gt;,              // 文件的长度
    "chunkSize" : &lt;num&gt;,           // 块的数量
    "uploadDate" : &lt;timestamp&gt;,    // 存放到GridFS中的时间
    "md5" : &lt;hash&gt;,                // 散列值
    "filename" : &lt;string&gt;,         // 人类可读的可选的文件名
    "contentType" : &lt;string&gt;,      // 可选的MIME类型
    "aliases" : &lt;string array&gt;,    // 别名数组
    "metadata" : &lt;dataObject&gt;,     // 任意额外的自定义元数据
} </pre>
</li>
</ol>
<p>当你查询GridFS以取回文件时，驱动程序负责把chunks装配成文件。GridFS支持针对文件进行范围查询，或者skip一定的长度。</p>
<div class="blog_h3"><span class="graybg">适用场景</span></div>
<p>如果你需要存放大于16MB的文件时，应当使用GridFS。否则，考虑使用BinData类型存储在集合中。</p>
<p>某些情况下，在MongoDB中存储大文件比在文件系统中直接存放更加高效：</p>
<ol>
<li>如果文件系统限制目录中最大文件数，MongoDB没有此限制</li>
<li>如果你想访问大文件的一小部分，而不希望把整个文件加载到内存</li>
<li>如果你想获得复制集带来的文件安全性</li>
</ol>
<div class="blog_h3"><span class="graybg">GridFS索引</span></div>
<p>chunks、files两个集合自带一部分索引（由驱动创建），以提升性能。你也可以创建自己的索引，以满足需要。</p>
<p>chunks上具有唯一性的复合索引：files_id + n，这让chunk的检索非常高效，示例查询：</p>
<pre class="crayon-plain-tag">db.fs.chunks.find( { files_id: myFileID } ).sort( { n: 1 } )</pre>
<p>如果驱动没有创建此索引（不满足规范），可以手工创建：</p>
<pre class="crayon-plain-tag">db.fs.chunks.createIndex( { files_id: 1, n: 1 }, { unique: true } );</pre>
<p>files上具有filename、uploadDate两个索引，你可以很方便的根据名称、日志进行文件检索。</p>
<p>如果驱动没有创建此索引（不满足规范），可以手工创建：</p>
<pre class="crayon-plain-tag">db.fs.files.createIndex( { filename: 1, uploadDate: 1 } );</pre>
<div class="blog_h3"><span class="graybg">GridFS分片</span></div>
<p>如果需要分片chunks，考虑以<pre class="crayon-plain-tag">{ files_id : 1, n : 1 }</pre>或者<pre class="crayon-plain-tag">{ files_id : 1 }</pre>作为分片键。 files_id是单调递增字段。对chunks进行分片时，不能使用哈希分片。</p>
<p>files集合仅仅包含元数据，通常不需要分片。 </p>
<div class="blog_h1"><span class="graybg">复制Replication</span></div>
<p>复制集（replica set ）是指一组mongod进程，它们维护相同的数据集。复制集提供了数据冗余、高可用性，对于生产环境来说复制集基本是标配。</p>
<p>在某些情况下，复制集可以提供额外的读容量，因为客户端可以把请求分发给复制集中的任意成员。对于跨地域部署的应用程序来说，分别在不同数据中心的复制集节点可以大大降低网络延迟。</p>
<div class="blog_h2"><span class="graybg">复制集成员</span></div>
<p>每个复制集可以具有N个数据存储节点，和一个可选的仲裁（arbiter）节点。存储节点中有且仅有一个是主（primary）节点，其它均为从（secondary）节点。</p>
<div class="blog_h3"><span class="graybg">主节点</span></div>
<p><span style="background-color: #c0c0c0;">主节点接收所有写操作</span>请求，此节点负责确认{ w: "majority" }写关注。主节点把自己对<span style="background-color: #c0c0c0;">数据集的全部变更存放在日志 —— oplog中</span>。这些oplog随后被<span style="background-color: #c0c0c0;">异步的传播</span>到所有从节点，确保所有节点的数据集一致：</p>
<p><img class="aligncenter  wp-image-15115" src="https://blog.gmem.cc/wp-content/uploads/2015/05/replica-set-read-write-operations-primary.png" alt="replica-set-read-write-operations-primary" width="438" height="358" /></p>
<p>当主节点不可用时（和其它成员超过10s不进行通信），一个从节点会被选举为新的主节点。你可以额外配置一个mongod实例作为仲裁节点，这类节点不维护数据集，它的职责仅仅是响应心跳、应答其它节点的选举（投票）请求。当存储节点数量为偶数时，添加一个仲裁节点，可以满足投票的大多数（majority）原则。仲裁节点不需要专用的硬件，可以和某个存储节点部署在一起。</p>
<p>尽管所有成员都可以接受读请求，但是<span style="background-color: #c0c0c0;">默认的，应用程序会把读请求重定向给主节点</span>。</p>
<div class="blog_h3"><span class="graybg">priority0从节点</span></div>
<p>这类从节点<span style="background-color: #c0c0c0;">没有称为主节点的资格</span>，没有资格触发选举（从节点总是自荐为主节点以触发选举），但是可以进行投票。在多数据中心部署的情况下，要避免某个数据中心产生主节点，则将其中的节点均配置为priority0从节点。</p>
<p>默认的，没有任何节点是priority0从节点。你需要手工设置节点的priority为0。</p>
<p>注意当前主节点不支持设置priority为0，因此，如果你想把主节点变为priority，先需要将其变为从节点。<pre class="crayon-plain-tag">rs.reconfig()</pre>可以导致当前主节点立即优雅的关闭（ step down）从而导致重新选举主节点。在step down期间mongod会关闭所有客户端，通常会花费10-20秒，最好在例行维护期间进行这种操作。</p>
<p>调用<pre class="crayon-plain-tag">cfg = rs.conf()</pre>获得复制集的配置文档，该文档有一个<pre class="crayon-plain-tag">members</pre>字段，它是一个数组，包含复制集每个成员的配置信息。要设置第N个节点为priority0，执行<pre class="crayon-plain-tag">members[n].priority = 0</pre>。直到你重新配置复制集位置，priority设置不会生效。执行<pre class="crayon-plain-tag">rs.reconfig(cfg)</pre>可以重新配置复制集。</p>
<div class="blog_h3"><span class="graybg">隐藏从节点</span></div>
<p>之类从节点维持主节点数据集的拷贝，但是对客户端不可见。隐藏从节点必须是priority0节点。</p>
<p>由于对客户端不可见，隐藏从节点的流量仅仅包括来自主节点的数据复制流。你可以使用<span style="background-color: #c0c0c0;">隐藏从节点执行专门任务，你如报表分析、备份</span>。在分片集群中，mongos不会和隐藏从节点交互。</p>
<p>当使用隐藏从节点执行备份时，需要注意：</p>
<ol>
<li>如果使用MMAPv1引擎，使用<pre class="crayon-plain-tag">db.fsyncLock()</pre>和<pre class="crayon-plain-tag">db.fsyncUnlock()</pre>操作可以在备份期间flush所有写操作并且锁定mongod，这样可以不必为了备份而停止隐藏节点</li>
<li>从3.2开始db.fsyncLock()可以保证数据文件不改变，不管使用MMAPv1还是WiredTiger 引擎，因此可以保证备份期间的数据一致性。在之前的版本，不保证WiredTiger下的数据一致性</li>
</ol>
<div class="blog_h3"><span class="graybg">延迟从节点</span></div>
<p>延迟从节点也维持主节点数据集的拷贝，但是其状态对应了主节点一个早先的状态，例如一小时前。</p>
<p>由于此特性，延迟从节点可以用做滚动备份（rolling backup），当出现人为操作错误时，可以恢复到先前的数据状态。</p>
<p>延迟节点必须是priority0节点，而且应当是隐藏节点。配置文档示例： </p>
<pre class="crayon-plain-tag">{
   "_id" : &lt;num&gt;,
   "host" : &lt;hostname:port&gt;,
   "priority" : 0,
   "slaveDelay" : &lt;seconds&gt;,
   "hidden" : true
}</pre>
<div class="blog_h3"><span class="graybg">仲裁节点</span></div>
<p>仲裁者节点不维护数据拷贝，不能变为主节点，它仅仅在选举主节点时起作用。 仲裁者总是具有1张选票，因此它可用于确保总是有奇数张选票，以满足大多数原则。</p>
<p>注意：不要在部署了复制集主节点、从节点的机器上部署仲裁节点。</p>
<p>一个节点被作为仲裁者加入到复制集之前，行为与普通mongod无异，它会创建一系列的数据文件、全尺寸的日志文件。为了最小化默认文件的创建，可以配置仲裁节点的配置文件：</p>
<ol>
<li>设置<pre class="crayon-plain-tag">storage.journal.enabled</pre>为false</li>
<li>对于MMAPv1 引擎，设置<pre class="crayon-plain-tag">storage.mmapv1.smallFiles</pre>为true</li>
</ol>
<p>为复制集添加仲裁节点的参考步骤如下：</p>
<ol>
<li>指定复制集名称、数据库路径，启动mongod：<br />
<pre class="crayon-plain-tag">mongod --port 30000 --dbpath /data/arb --replSet rs</pre>
</li>
<li> 在复制集主节点中执行先的命令，把上一步的mongod添加为仲裁者：<br />
<pre class="crayon-plain-tag">rs.addArb("HOST:30000") </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">Oplog</span></div>
<p>操作日志（Oplog）是一个特殊的定长集合，保存了所有修改了数据集的操作的滚动记录。MongoDB会在主节点上应用写操作，并将这些操作记录到主节点的Oplog。 复制集中的从节点会在之后异步的读取此定长集合，并将其中的操作应用到自己本地的数据库。</p>
<p>所有复制集成员都在<pre class="crayon-plain-tag">local.oplog.rs</pre>包含一个Oplog副本，便于本地数据库的当前状态。为了进行复制，复制集成员之间相互发送心跳，任意成员可以从其它任意成员那里读取到Oplog条目。</p>
<p>Oplog中的条目是幂等的，也就是说，这些条目应用一次还是多次，其产生的效果是一致的。</p>
<div class="blog_h3"><span class="graybg">Oplog的尺寸</span></div>
<p>当以第一次复制集成员身份启动mongod时，会自动创建默认尺寸的Oplog。默认尺寸对于In-Memory引擎来说是5%内存大小，对于其它引擎默认5%磁盘空闲空间大小。</p>
<p>在第一次启动前，你可以指定<pre class="crayon-plain-tag">oplogSizeMB</pre>参数，来设置Oplog的大小。之后如果需要改变此大小，需要特殊的<a href="https://docs.mongodb.com/manual/tutorial/change-oplog-size/">操作步骤</a>。</p>
<p>某些可能需要大尺寸Oplog的场景：</p>
<ol>
<li>同时更新很多文档：为了确保幂等性，Oplog必须把批量更新操作拆分成很多单条操作</li>
<li>删除的文档和更新的文档数量相近：这种情况下，数据库文件可能增长不大，但是Oplog会较大</li>
<li>还量的In-place更新</li>
</ol>
<div class="blog_h3"><span class="graybg">Oplog状态</span></div>
<p>调用<pre class="crayon-plain-tag">rs.printReplicationInfo()</pre>可以查看Oplog的状态，包括尺寸和操作的时间范围。</p>
<p>在某些异常情况下，从节点的Oplog更新可能过于延迟，达不到性能要求。在从节点调用<pre class="crayon-plain-tag">db.getReplicationInfo()</pre>可以查看复制状态、延迟。</p>
<div class="blog_h2"><span class="graybg">数据的同步</span></div>
<p>为了维护最新的共享数据集，从节点需要从其它节点同步（sync）或者拷贝数据。MongoDB使用两种形式的数据同步：</p>
<ol>
<li><span style="background-color: #c0c0c0;">初始同步</span>（initial sync）：为新的从节点提供完整的数据集</li>
<li><span style="background-color: #c0c0c0;">复制</span>（replication）：在初始数据集之上同步增量数据</li>
</ol>
<div class="blog_h3"><span class="graybg">初始同步</span></div>
<p>MongoDB执行初始同步的流程如下：</p>
<ol>
<li>克隆除了local之外的所有数据库，注意：
<ol>
<li>从3.4开始，所有的索引也被构建，之前的版本仅仅克隆_id索引</li>
<li>从3.4开始，在克隆过程中新产生的Oplog也被拉取过来。你需要确保新的从节点的local数据库有足够的磁盘空间</li>
</ol>
</li>
<li>新从节点应用所有克隆来的数据，并应用Oplog</li>
</ol>
<p>初始同步完毕后，新从节点的复制集成员状态从STARTUP2变为SECONDARY</p>
<p>为了防止偶发的网络错误，<span style="background-color: #c0c0c0;">初始同步内置了重试逻辑</span>作为容错措施。</p>
<div class="blog_h3"><span class="graybg">复制</span></div>
<p>在初始同步之后，从节点会不断的复制增量数据 —— 异步的拷贝Oplog并应用到数据库中。</p>
<p style="text-align: left;">从节点会根据PING延迟、其它节点的复制状态，<span style="background-color: #c0c0c0;">自动切换拷贝Oplog的源节点</span>。 但是：</p>
<ol>
<li>从3.2开始，投票权1的节点不得把投票权0的节点作为源</li>
<li>会避免将延迟从节点、因从从节点作为源</li>
<li>如果当前节点的复制集配置<pre class="crayon-plain-tag">members[n].buildIndexes</pre>为true，则仅此配置也为true的其它节点，才能作为源。此配置默认true</li>
</ol>
<p>为了增强性能，MongoDB会使用多线程来应用Oplog，日志中的条目被按照名字空间（MMAPv1）、文档标识（WiredTiger）分组，每个组一个线程，同时入库。日志的入库顺序总是和主节点上相同，在分组<span style="background-color: #c0c0c0;">批量入库期间，从节点阻塞所有读请求</span>，因此不会读到主节点上不存在的数据</p>
<div class="blog_h3"><span class="graybg">索引预抓取</span></div>
<p>对于MMAPv1引擎来说，MongoDB会抓取包含了受到影响的数据、索引的内存页，用于辅助增强应用Oplog条目的性能。</p>
<p>此索引预抓取可以最小化应用Oplog时持有写锁的时间，默认的从节点预抓取所有索引。配置项<pre class="crayon-plain-tag">secondaryIndexPrefetch</pre>与该特性有关。</p>
<div class="blog_h2"><span class="graybg">部署架构</span></div>
<p>复制集的架构影响其容量和能力。</p>
<p>通常情况下，生产环境中使用<span style="background-color: #c0c0c0;">3成员的复制集</span>，这保证了数据冗余和容错，同时也避免过于复杂的部署。请结合应用程序需要，并注意：</p>
<ol>
<li>复制集最多具有50个成员，但是能投票的最多7个成员。因此，如果复制集已经有7个成员，再添加节点必须是非投票成员</li>
<li>投票成员数量应为奇数，必要时添加一个仲裁节点，确保此规则</li>
<li>为了满足特殊的需求，考虑添加延迟、隐藏从节点</li>
<li>负载均衡与大量读负载：如果应用面临很高的读负载，可以将负载分发到从节点以提升性能。如果应用跨地域分布，可以考虑在不同数据中心部署从节点</li>
<li>异地灾备：为了防止数据中心出现重大灾难而导致数据丢失，至少应当部署一个异地的从节点。为了<span style="background-color: #c0c0c0;">确保主数据中心的的节点优先被选举</span>为主节点，配置异地节点的<pre class="crayon-plain-tag">members[n].priority</pre>为更小的值</li>
<li>为从节点应用标签集（Tag set）：你可以为节点应用一系列标签，从而把读操作引向特定的节点，或者定制写关注——从其它节点获得请求确认</li>
<li>使用日志防止断电导致的数据丢失：MongoDB默认启用了写前日志（journaling ），可以有效的防止断电、宕机导致的数据丢失</li>
<li>网络、计算资源受限的节点，应该避免称为主节点，使用priority0</li>
</ol>
<p>如果你的应用程序连接到多个复制集，则每个复制集需要具有唯一的名称，某些驱动根据此名称对复制集进行分组。</p>
<div class="blog_h3"><span class="graybg">三成员模式</span></div>
<p>这是典型的、最小化的复制集架构。三个成员中，可以有一个作为仲裁节点。</p>
<p>使用三个数据节点时，数据具有三份拷贝，提供了容错和HA。如果主节点宕机，一个从节点会被选举为新主节点。宕机主节点恢复后，自动重新加入复制集。</p>
<div class="blog_h3"><span class="graybg">多数据中心模式</span></div>
<p>在异地数据中心部署节点，可以抵御断电、网络中断甚至自然灾害。</p>
<p>要实现异地灾备，至少在其它数据中心部署一个从节点。可能的话，使用奇数个数的数据中心。要尽可能保证，即使一个数据中心完全不可用，复制集仍然能保证majority原则。</p>
<p>部署示例：</p>
<ol>
<li>三成员复制集：
<ol>
<li>两数据中心：DC1两成员，DC2一成员，如果有仲裁者，部署在DC1。当DC1不可用时复制集变为只读；当DC2不可用时DC1继续运行（大多数原则）</li>
<li>三数据中心：每个数据中心一个节点。不管哪个数据中心不可用，复制集仍然可读写</li>
</ol>
</li>
<li>五成员复制集：
<ol>
<li>两数据中心：DC1三成员，DC2两成员。当DC1不可用时复制集变为只读；当DC2不可用时DC1继续运行（大多数原则）</li>
<li>三数据中心：DC1两成员，DC2两成员，DC3一成员。不管哪个数据中心不可用，复制集仍然可读写（大多数原则）</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">高可用</span></div>
<p>利用<span style="background-color: #c0c0c0;">自动的故障转移机制</span>，复制集提供高可用性。故障转移即当主节点不可用时，某个从节点被选举为新主节点的过程。</p>
<p>从3.2开始，MongoDB引入版本1（(protocolVersion: 1）的复制协议，<span style="background-color: #c0c0c0;">降低了故障转移消耗的时间，加速重复主节点检测速度</span>。新创建的复制集会自动使用版本1的协议。</p>
<div class="blog_h3"><span class="graybg">主节点选举</span></div>
<p>复制集使用选举机制来确定谁作为主节点，在以下情况下，发生选举：</p>
<ol>
<li>初始化一个复制集时</li>
<li>当主节点变得不可用时</li>
</ol>
<p>主节点的选举需要时间，在选举完成之前，复制集不能接受写请求，所有节点呈只读状态。</p>
<p>如果复制集的大部分节点对于主节点不可访问，则主节点会setp down并变成从节点。这样复制集就不能接受写请求了，但是如果配置了在从节点上执行查询，则剩余节点仍然支持读操作。</p>
<p>影响主节点选举的因素包括：</p>
<ol>
<li>复制协议：从3.2引入的版本1提高了性能</li>
<li>心跳：复制集成员每2秒向各成员发送心跳，如果心跳应答<span style="background-color: #c0c0c0;">10秒没有返回</span>，标记为不可达（inaccessible）</li>
<li>成员优先级：选举算法会仅可能的让高优先级的从节点触发选举。优先级影响选举的耗时和结果，因为高优先级的节点会更快的触发选举并很可能赢得选举。但是，低优先级的成员也可能临时的被选举为主节点，此时选举会继续，直到最高优先级的成员当选</li>
<li>数据中心不可用：多数据中心部署的情况下，某个数据中心整体不可用可能导致无法选举</li>
<li>否决：版本1协议取消了否决机制。但是版本0中任何成员均具有否决权</li>
</ol>
<div class="blog_h3"><span class="graybg">投票成员</span></div>
<p>复制集成员的配置<pre class="crayon-plain-tag">members[n].votes</pre>以及其状态（state）决定了它是否具有投票资格：</p>
<ol>
<li><pre class="crayon-plain-tag">members[n].votes=1</pre>的节点具有投票资格，要禁止某个节点投票，可以设置为0。从3.2开始，非投票节点的priority必须为0</li>
<li>仅仅以下状态的节点可以参与投票：PRIMARY、SECONDARY、RECOVERING、ARBITER、ROLLBACK</li>
</ol>
<div class="blog_h3"><span class="graybg">故障转移导致的回滚</span></div>
<p>回滚导致前主节点上的写操作被撤销，当此“前主节点”重新加入到复制时。回滚仅仅在主节点已经应用了写操作，但是此<span style="background-color: #c0c0c0;">写操作没有来得及在step down之前被成功复制</span>的情况下。回滚保证了数据一致性。</p>
<p>如果写操作被任意从节点复制，并且此从节点可以被大多数节点访问，则回滚不会发生。</p>
<p>对于复制集，默认写关注<pre class="crayon-plain-tag">{w: 1}</pre>仅仅要求主节点的确认，这样存在回滚的可能，但是客户端认为数据已经持久化。要避免这种回滚，使用 <pre class="crayon-plain-tag">w: "majority"</pre>。另外：</p>
<ol>
<li>如果writeConcernMajorityJournalDefault设置为false，即使使用前面的写关注，仍然不能保证不会回滚，因为写操作可能没有持久化到磁盘</li>
<li>不管写关注设置为什么，local读关注都可能看到甚至没有被写者确认的数据。local可能读到之后被回滚的数据</li>
</ol>
<p>一个mongod<span style="background-color: #c0c0c0;">不支持回滚超过300MB的数据</span>。你可以在mongod日志中看到：</p>
<pre class="crayon-plain-tag">[replica set sync] replSet syncThread: 13410 replSet too much data to roll back </pre>
<p>如果系统需要回滚超过300MB的数据，你必须手工介入，强制初始同步 —— 删除前主节点的dbPath目录。</p>
<div class="blog_h2"><span class="graybg">复制集的读写语义</span></div>
<p>从客户端角度来说，mongod以独立方式运行，还是作为复制集成员，是透明的。但是，MongoDB提供了额外的读写选项，提供数据一致性保证。</p>
<div class="blog_h3"><span class="graybg">写关注</span></div>
<p>对于复制集，默认写关注仅要求来自主节点的确认。你可以改变此选项，要求来自指定数量/大多数节点的确认。例如，使用w:2时写操作的处理流程如下：<img class="aligncenter  wp-image-15133" src="https://blog.gmem.cc/wp-content/uploads/2015/05/crud-write-concern-w2.png" alt="crud-write-concern-w2" width="435" height="452" /></p>
<p>要修改默认写关注选项，可以为复制集配置<pre class="crayon-plain-tag">settings.getLastErrorDefaults</pre>选项，示例代码：</p>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">读设置</span></div>
<p>读设置描述MongoDB如何把读操作请求路由到某个复制集成员。<span style="background-color: #c0c0c0;">默认</span>情况下，应用程序把<span style="background-color: #c0c0c0;">读请求</span>发送给复制集中的<span style="background-color: #c0c0c0;">主节点</span>。读设置与直接连接到单个mongod的那些客户端没有关系。修改读设置时需要小心，从节点可能返回陈旧的数据，原因是复制的异步性。</p>
<p>使用Shell时，可以调用游标的<pre class="crayon-plain-tag">cursor.readPref(mode,tagset)</pre>方法进行读设置，执行查询时，可以这样进行读设置：</p>
<pre class="crayon-plain-tag">// 从最近的节点读取数据，仅仅从东部数据中心读取（根据节点的标签集）
db.collection.find().readPref('nearest', [ { 'dc': 'east' } ])</pre>
<p>读设置的mode可以取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>primary</td>
<td>默认模式，所有读请求发送给主节点</td>
</tr>
<tr>
<td>primaryPreferred</td>
<td>使用主节点，主节点不可用时才使用从节点</td>
</tr>
<tr>
<td>secondary</td>
<td>所有读请求分发给从节点</td>
</tr>
<tr>
<td>secondaryPreferred</td>
<td>使用从节点，从节点不可用时才使用主节点</td>
</tr>
<tr>
<td>nearest</td>
<td>从网络延迟最低的节点读取，不管其类型</td>
</tr>
</tbody>
</table>
<p>标签集用于指定仅具有特定标签的复制集成员才可以接收读操作请求。除了primary模式外，都可以指定该选项。</p>
<p>3.4引入的<pre class="crayon-plain-tag">maxStalenessSeconds</pre>，用于指定一个最大的复制延迟值，如果某个节点的延迟超过此值，则不用它进行读操作。除了primary模式外，都可以指定该选项。</p>
<p>读设置适用于通过mongos连接到分片集群的客户端。mongos连接到某个以复制集形式出现的分片时，遵守此读设置。</p>
<p>使用非主读设置的应用场景：</p>
<ol>
<li>运行不影响前端应用程序的系统操作</li>
<li>对跨地域分布的应用程序提供本地读功能，考虑使用读设置<pre class="crayon-plain-tag">nearest</pre>，以获得低延迟</li>
<li>在故障转移期间维持可用性，考虑使用读设置<pre class="crayon-plain-tag">primaryPreferred</pre>，这样正常情况下读取主节点，如果主节点不可用则允许读取（只读）可能陈旧的数据</li>
</ol>
<p>在一般场景下，不要使用<pre class="crayon-plain-tag">secondary</pre>或者<pre class="crayon-plain-tag">secondaryPreferred</pre>来提供额外的读容量，因为：</p>
<ol>
<li>复制集中的大部分成员的写流量是差不多的，因而，从节点不会比主节点具有更大的读能力</li>
<li>复制是异步进行的，你可能读取到过时的数据，从不同的从节点读取数据，可能导致非单调读</li>
<li>对于针对分片集合的查询，对于启用负载均衡器的集群，由于不完整的chunk迁移，从节点可能返回重复的、丢失的数据</li>
</ol>
<p>要扩容，分片通常是更加优先的选择，因为它利用了多台机器的计算资源。</p>
<div class="blog_h2"><span class="graybg">local数据库</span></div>
<p>每个mongod都拥有一个名为local的数据库，其中存放有关工作集复制处理过程用到的数据、以及其它实例相关的数据，该数据库<span style="background-color: #c0c0c0;">对Replication是不可见</span>的。</p>
<p>该数据库包含以下集合：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">集合</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>所有mongod实例都有的集合</em></strong></td>
</tr>
<tr>
<td>startup_log</td>
<td>
<p>在启动时，monod向此集合插入一条诊断信息：</p>
<p>_id  由主机名和时间戳组成<br />hostname  主机名<br />startTime  服务启动时间<br />startTimeLocal 服务启动时间（本地）<br />cmdLine  启动使用的mogod命令行选项<br />pid  mongod进程的ID<br />buildinfo  mongod的构建信息</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>复制集成员拥有的集合</em></strong></td>
</tr>
<tr>
<td>system.replset</td>
<td>保存复制集配置对象，单个文档，调用<pre class="crayon-plain-tag">rs.conf()</pre>可以查看该对象</td>
</tr>
<tr>
<td>oplog.rs</td>
<td>保存oplog的定长集合，复制集配置项oplogSizeMB决定其大小</td>
</tr>
<tr>
<td>replset.minvalid</td>
<td>内部使用，追踪复制状态</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="rs-member-state"></a>复制集成员状态</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30px; text-align: center;">代码</td>
<td style="width: 15%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>STARTUP</td>
<td>尚不是某个复制集的活动成员，每个成员在启动时最初处于这一状态。在此状态时成员会读取复制集配置文档</td>
</tr>
<tr>
<td>1</td>
<td>PRIMARY</td>
<td>作为主节点存在于复制集</td>
</tr>
<tr>
<td>2</td>
<td>SECONDARY</td>
<td>作为从节点存在于复制集</td>
</tr>
<tr>
<td>3</td>
<td>RECOVERING</td>
<td>正在进行启动时自检，或者正在进行回滚、resync。具有投票权</td>
</tr>
<tr>
<td>5</td>
<td>STARTUP2</td>
<td>加入到了工作集，并在进行初始同步</td>
</tr>
<tr>
<td>6</td>
<td>UNKNOWN</td>
<td>不知道该节点的状态</td>
</tr>
<tr>
<td>7</td>
<td>ARBITER</td>
<td>作为仲裁者存在于复制集</td>
</tr>
<tr>
<td>8</td>
<td>DOWN</td>
<td>该节点已经关闭</td>
</tr>
<tr>
<td>9</td>
<td>ROLLBACK</td>
<td>正在进行回滚，不支持读操作</td>
</tr>
<tr>
<td>10</td>
<td>REMOVED</td>
<td>曾经是某个复制集的成员，但是现在被移除了</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">复制集Shell方法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">方法</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>rs.add()</td>
<td>为复制集添加一个新成员</td>
</tr>
<tr>
<td>rs.addArb()</td>
<td>为复制集添加一个仲裁者成员</td>
</tr>
<tr>
<td>rs.conf()</td>
<td>
<p>获得复制集的配置文档</p>
<p>对应数据库命令replSetGetConfig</p>
</td>
</tr>
<tr>
<td>rs.freeze()</td>
<td>
<p>在一段时间内，阻止当前成员提起选举自己为主节点</p>
<p>对应数据库命令replSetFreeze</p>
</td>
</tr>
<tr>
<td>rs.help()</td>
<td>打印复制集相关方法的简短帮助</td>
</tr>
<tr>
<td>rs.initiate()</td>
<td>
<p>初始化一个复制集</p>
<p>对应数据库命令replSetInitiate</p>
</td>
</tr>
<tr>
<td>rs.printReplicationInfo()</td>
<td>从主节点的角度，打印复制集的状态</td>
</tr>
<tr>
<td>rs.printSlaveReplicationInfo()</td>
<td>从从节点的角度，打印复制集的状态</td>
</tr>
<tr>
<td>rs.reconfig()</td>
<td>
<p>根据传入的配置文档，对复制集进行重新配置</p>
<p>对应数据库命令replSetReconfig</p>
</td>
</tr>
<tr>
<td>rs.remove()</td>
<td>移除一个复制集成员</td>
</tr>
<tr>
<td>rs.slaveOk()</td>
<td>设置当前连接的slaveOk属性，一般不再使用</td>
</tr>
<tr>
<td>rs.status()</td>
<td>
<p>获得复制集状态</p>
<p>对应数据库命令replSetGetStatus</p>
</td>
</tr>
<tr>
<td>rs.stepDown()</td>
<td>
<p>优雅的关闭当前主节点，让其成为从节点并触发选举</p>
<p>对应数据库命令replSetStepDown</p>
</td>
</tr>
<tr>
<td>rs.syncFrom()</td>
<td>
<p>指定当前从节点的复制源节点</p>
<p>对应数据库命令replSetSyncFrom</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">复制集数据库命令</span></div>
<p>下表仅仅列出没有对应Shell方法的数据库命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>replSetMaintenance</td>
<td>启用/禁用维护模式，可以将一个从节点置于RECOVERING状态</td>
</tr>
<tr>
<td>resync</td>
<td>强制当前节点从主节点重新同步，仅仅用于主从复制</td>
</tr>
<tr>
<td>isMaster</td>
<td>显示当前成员在复制集中的角色</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">分片</span></div>
<p>解决扩容性问题的两种手段，主要是：</p>
<ol>
<li>垂直扩容（Vertical Scaling）：增加单机的能力，例如更换更强的CPU、增加内存条、增加硬盘等。这种扩容方式有极限，而且成本是指数级增加的。使用云服务时，则不能进行垂直扩容</li>
<li>水平扩容（Horizontal Scaling）：将数据集切分到多个廉价服务器上，通过增加服务器即可提升系统容量</li>
</ol>
<p>分片（Sharding）是跨越多态机器分发、存储数据的手段，属于水平扩容。MongoDB使用分片来支持海量数据集和高吞吐量需求。MongoDB<span style="background-color: #c0c0c0;">在集合的级别进行分片</span>。</p>
<p>分片的优势包括：</p>
<ol>
<li>读/写：进行分片后，MongoDB的读写操作均可以被很好的扩容。当查询包含分片键（前缀）条件时，mongos仅仅发送查询请求给相关的Shard</li>
<li>存储容量：可以使用廉价硬件无限扩展</li>
<li>高可用性：在部分Shard不可用的情况下，仍然能够进行部分的读写操作。配置服务器可以作为复制集部署，保证高可用性</li>
</ol>
<p>执行分片前，应当考虑：</p>
<ol>
<li>分片增加了调试、运维的复杂度</li>
<li>小心的选择分片键，不合理的选择会影响性能</li>
<li>某些需要scatter/gather，因而消耗较多时间</li>
</ol>
<div class="blog_h2"><span class="graybg">分片集群</span></div>
<p>一个MongoDB分片集群（sharded cluster ）由以下成员组成：</p>
<ol>
<li>分片（Shard）：由单个mongod或者一个复制集组成，维护一个数据分片。在生产环境下，每个分片可以定义为3成员复制集</li>
<li>路由器（mongos）：作为查询路由器存在，作为客户端应用和分片集群之间的接口。你可以在每个应用服务器上都部署mongos，或者定义一组mongos并提供前置负载均衡器/代理给应用程序使用</li>
<li>配置服务器：保存分片集群的元数据、配置信息。从3.4开始，复制服务器必须作为复制集部署（CSRS）。在生产环境下，可以定义为3成员复制集</li>
</ol>
<p>下图示意了这些成员之间的关系：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster-production-architecture.png"><img class=" wp-image-15181 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharded-cluster-production-architecture.png" alt="sharded-cluster-production-architecture" width="620" height="440" /></a></p>
<p>&nbsp;</p>
<p>应用程序（MongoDB客户端）<span style="background-color: #c0c0c0;">要想和集群进行交互，必须连接到mongos</span> —— 不管你要使用的是分片还是非分片集合，客户端绝不该直接连接到某个Shard，除了执行分片本地管理、维护工作的时候。</p>
<div class="blog_h3"><span class="graybg">主分片</span></div>
<p>分片集群中的每个数据库都有一个主分片（Primary Shard），该分片存放数据库上所有未分片的集合。</p>
<p>当创建新数据库时，mongos会选取集群中保存数据最少的分片，作为数据库的主分片。要改变主分片，可以调用<pre class="crayon-plain-tag">movePrimary</pre>命令，迁移主分片的操作可能消耗很长时间，完毕前不应访问其中的集合。</p>
<div class="blog_h3"><span class="graybg">分片状态</span></div>
<p>执行<pre class="crayon-plain-tag">sh.status()</pre>可以看到分片集群的整体状态信息，包括：</p>
<ol>
<li>数据库的主分片信息</li>
<li>Chunk在Shard中的分布情况</li>
</ol>
<div class="blog_h3"><span class="graybg">配置服务器</span></div>
<p>这类服务器存放分片集群的元数据，元数据记录了：</p>
<ol>
<li>分片集群中所有数据、组件的状态和组织</li>
<li>Chunk对应的值范围</li>
<li>每个分片包含的Chunks</li>
</ol>
<p>此外，配置服务器还：</p>
<ol>
<li>存储身份验证配置信息，例如基于角色的访问控制、内部验证设置</li>
<li>管理分布式锁</li>
</ol>
<p>mongos会缓存以上信息，以便对读写操作进行路由。当元数据变更后mogos的缓存会自动更新。</p>
<p>从3.4开始，配置服务器必须使用满足以下要求的复制集：</p>
<ol>
<li>具有0个仲裁者</li>
<li>不存在延迟节点</li>
<li>任何节点的buildIndexes设置不得为false</li>
</ol>
<div class="blog_h3"><span class="graybg">配置服务器上的读写</span></div>
<p>配置服务器具有config、admin数据库。</p>
<p>admin数据库包含与认证/授权相关的集合，以及 system.*集合（内部使用）。</p>
<p>config数据库包含了分片集群的元数据，当元数据变更（例如chunk迁移、chunk分裂）时MongoDB会写此数据库，而mongos会读取该数据库。客户端不应该直接写此数据库。读写此数据库时，均使用关注级别majority。</p>
<div class="blog_h3"><span class="graybg">配置服务器可用性</span></div>
<p>如果配置服务器复制集的主节点宕机，并且不能选举出新的主节点，则集群的元数据变为只读状态。你仍然可以对分片进行读写操作，但是不能进行Chunk迁移、分裂。</p>
<p>如果配置服务器复制集完全不可用，则分片集群不支持任何操作。但是mongos会缓存元数据，因而在重新启动mongos之前，仍然能进行分片的读写。</p>
<p>由于配置服务器非常重要，并且数据量很小，应该重视对其进行备份。</p>
<div class="blog_h3"><span class="graybg">路由器</span></div>
<p>路由器（mongos）这样路由查询：</p>
<ol>
<li>确定需要接收查询请求的Shard列表</li>
<li>在所有目标分片上建立一个游标</li>
<li>合并各Shard返回的查询结果，返回结果文档给客户端</li>
</ol>
<p>要确保客户端连接到的是MongoDB实例是路由器，你可以执行isMaster命令，其返回值：</p>
<pre class="crayon-plain-tag">{
   "ismaster" : true,
   "msg" : "isdbgrid",
   "maxBsonObjectSize" : 16777216,
   "ok" : 1
}</pre>
<p>中的msg字段如果为isdbgrid，则说明他是mongos。</p>
<div class="blog_h2"><span class="graybg">分片键</span></div>
<p>进行分片时，需要决定每一条数据应该存放到哪个Shard上，这依赖于分片键。分片键由集合中<span style="background-color: #c0c0c0;">每个文档都具有的、不变的（immutable）字段</span>构成。一旦选择好分片键，以后就不能再修改，每个分片集合有且只有一个分片键。分片键的选择影响性能、可扩容性。</p>
<p>要对某个集合进行分片，执行：</p>
<pre class="crayon-plain-tag"># namespace：&lt;database&gt;.&lt;collection&gt;
# key 索引规格文档，用于指定分片键
# unique 对分片键进行唯一性约束，哈希分片键不支持。非空集合的唯一性索引必须提前手工创建好
# options.numInitialChunks 使用哈希分片键来创建空的分片集群时，最初创建的Chunk的数量
sh.shardCollection( namespace, key, unique, options )</pre>
<div class="blog_h3"><span class="graybg">索引</span></div>
<p>分片集合必须有一个支持分片键的索引：此索引要么在分片键上创建，要么以分片键开头。如果：</p>
<ol>
<li>集合是空的，并且分片键上没有索引，则sh.shardCollection()自动在分片键上创建索引</li>
<li>集合是非空，你必须在调用sh.shardCollection()之前，<span style="background-color: #c0c0c0;">手工创建索引</span></li>
</ol>
<div class="blog_h3"><span class="graybg">唯一性索引</span></div>
<p>对于分片集合，仅仅<span style="background-color: #c0c0c0;">_id、位于分片键字段的索引、以分片键字段开头的索引</span>可以是唯一性索引。不满足此条件的集合无法分片，分片后你无法在不满足此条件的字段上新建唯一性索引。</p>
<p>如果对分片键使用唯一性索引（sh.shardCollection指定unique为true），则MongoDB可以保证分片键取值的唯一性。如果分片键包含多个字段，则保证其整体上的唯一性而非某个字段。</p>
<div class="blog_h3"><span class="graybg">分片键的限制</span></div>
<p>分片键长度不能超过512字节。要么对索引键进行索引，要么以其为索引前缀。可以对分片键建立哈希索引。多键索引、文本索引、地理空间索引均不支持。</p>
<p>分片键是不可变的，如果你必须改变分片键的值，按以下步骤：</p>
<ol>
<li>把索引数据Dump出来</li>
<li>Drop原先的分片集合</li>
<li>使用新的分片键进行分片</li>
<li>Pre-split分片键范围，确保均匀的数据分布</li>
<li>把Dump出的数据导入到数据库</li>
</ol>
<p>分片键的值是不可变的：你不能修改分片键字段的值。</p>
<div class="blog_h3"><span class="graybg">选择分片键</span></div>
<p>理想的分片键，应该让MongoDB仅可能均匀的把文档分发到各分片上。</p>
<p>分片键的<span style="background-color: #c0c0c0;">基数</span>（cardinality，候选取值数量）决定了负载均衡器能够创建的<span style="background-color: #c0c0c0;">Chunks的最大数量</span>，进而影响水平扩容的有效性。一个特定的分片键值，任何时刻仅仅能存在于一个Chunk上，因此如果分片键的基数为4，则最多创建4个Chunk，因而最多使用4个分片（服务器），添加额外的服务器得不到任何好处。</p>
<p>如果数据模型要求在低基数字段上分片，可以考虑联合某个高基数字段建立联合索引。</p>
<p>尽管具有<span style="background-color: #c0c0c0;">高基数</span>的分片键可以更好的支持水平扩容，但是它<span style="background-color: #c0c0c0;">不能保证数据均匀分布</span>在各分片，因为各键值对应的记录数量（Frequency）差异可能很大。分片键频率（Shard Key Frequency）用来描述一个特定的分片键值出现的频率。如果某些分片键取值具有非常高的Frequency，那么存储高Frequency的Chunks将出现性能瓶颈，并且这些Chunk可能无法再次分裂（单个键值无法再分裂）。</p>
<p>如果数据模型要求在高Frequency字段上分片，可以考虑联合某个低Frequency/Unique字段建立联合索引。</p>
<p>单调变化的分片键可能限制集群的插入吞吐量。因为一段时间内索引的插入可能都被分发到同一个分片（开区间的第一个、最后一个Chunk）上：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/05/monotonically-changing-shard-key.png"><img class="aligncenter size-full wp-image-15186" src="https://blog.gmem.cc/wp-content/uploads/2015/05/monotonically-changing-shard-key.png" alt="monotonically-changing-shard-key" width="687" height="348" /></a></p>
<p>&nbsp;</p>
<p>在上图中，如果分片键单调递增，则后续所有insert都分发到Chunk C；如果单调递减，则都分发到Chunk A。</p>
<p>如果数据模型要求在单调递增字段上分片，可以考虑使用哈希分片。</p>
<div class="blog_h2"><span class="graybg">分片策略</span></div>
<p>MongoDB支持两种分片策略。</p>
<div class="blog_h3"><span class="graybg">哈希分片</span></div>
<p>所谓哈希分片，是指使用<span style="background-color: #c0c0c0;">单字段的哈希索引来作为分片键</span>的分片策略。哈希分片导致<span style="background-color: #c0c0c0;">基于分片键的范围查询难以匹配单个/少量分片</span>（查询隔离，Query Isolation），往往需要广播到所有分片。但是，哈希分片让数据<span style="background-color: #c0c0c0;">更加均匀的分布在各Shard</span>中，特别是在分片键单调递增/减的情况下（例如自增长）。</p>
<p>这种分片策略需要对每一个分片键字段的值计算哈希，每个Chunk对应一个哈希值区间。</p>
<p>如果对空集合进行哈希分片，MongoDB默认为每个Shard创建两个空的Chunk，所有这些Chunks覆盖哈希值域。在执行分片时，可以指定numInitialChunks来定制Chunk的数量。</p>
<p>用作哈希分片的字段，应当具有高基数（大量不同的取值），哈希分片适用于<span style="background-color: #c0c0c0;">时间戳、ObjectId之类的单调变化</span>的值类型，你可以基于自动生成的_id进行哈希分片。</p>
<p>要执行哈希分片，调用命令：</p>
<pre class="crayon-plain-tag">sh.shardCollection( "database.collection", { &lt;field&gt; : "hashed" } )</pre>
<p> 注意：MongoDB的哈希索引会在计算哈希值之前，<span style="background-color: #c0c0c0;">把浮点数截断为64-bit的整数</span>，并且不支持大于2^53的浮点数。为防止哈希冲突，你可能需要将分片键乘以10^N后存储。</p>
<div class="blog_h3"><span class="graybg">范围分片</span></div>
<p>这是MongoDB默认的分片策略，它直接根据分片键字段的值来划分Chunk，分片键值相近的文档，可能分布在同一个Shard/Chunk中。这样，进行范围查询时往往能够实现查询隔离。</p>
<p>选择范围分片键时，要注意前文所述的：高基数、低Frequency、非单调变化原则。</p>
<p>要执行范围分片，调用命令：</p>
<pre class="crayon-plain-tag">sh.shardCollection( "database.collection", { &lt;shard key&gt; } )</pre>
<div class="blog_h2"><span class="graybg">块（Chunks）</span></div>
<p>MongoDB把数据划分为Chunk。 MongoDB可以使用分片集群负载均衡器进行跨Shard的Chunk迁移，以保证每个Shard被合理利用。</p>
<p>每个Shard上存储0-N个Chunk，每个Chunk对应了一个分片键取值区间，区间总是[ 闭，开 ) 形式的。所有Chunk正好覆盖分片键的键空间。</p>
<p>将数据集和Chunk而不是Shard进行关联，其好处是：</p>
<ol>
<li>可以按需添加新的机器（Shard），然后轻易的再平衡数据集（迁移Chunk）</li>
<li>在必要时（假设某个Chunk过大），可以进行Chunk分裂，然后进行再平衡</li>
</ol>
<div class="blog_h3"><span class="graybg">块分裂</span></div>
<p>如果块增长超过配置的尺寸，MongoDB会执行块分裂（Split），插入、更新操作都可能引发块分裂。只要块包含<span style="background-color: #c0c0c0;">超过1个的分片键值，就可以被分裂</span>。</p>
<p>MongoDB默认的Chunk尺寸是64MB。修改此默认值时，要注意：</p>
<ol>
<li>小尺寸的Chunk可以让数据分布更加均匀，代价是更加频繁的数据迁移。性能影响产生在mongos层</li>
<li>大尺寸的Chunk导致的迁移较少，因而从网络、mongos角度是高效的。但是可能导致数据不均匀分布</li>
<li>Chunk尺寸影响Chunk能够包含的最大文档数量（一旦超过此数量就会发生分裂）</li>
<li>分片既有集合时，Chunk尺寸影响集合的最大尺寸</li>
<li>实际的分裂行为只能被插入、更新操作触发</li>
<li>分裂不能被撤销，如果你增大Chunk尺寸，那些分裂产生的Chunk不会合并回去</li>
</ol>
<p>在实际应用中，不要为了追求一点点更加平均的分布，而盲目设置过小的Chunk尺寸。</p>
<p><span style="background-color: #c0c0c0;">块分裂本身是高效</span>的元数据级别的操作，不牵涉到数据迁移（跨Shard）。但是，当数据分布不均匀时，集群的负载均衡器可能会自动重新分布Chunks。</p>
<div class="blog_h3"><span class="graybg">块迁移</span></div>
<p>为了在Shard之间更加均匀的分布数据，MongoDB会执行块的迁移 —— 将一个Chunk移动到另外一个Shard上并更新元数据。</p>
<p>块迁移可以：</p>
<ol>
<li>手工执行，你仅仅在一些特殊情况下才需要进行手工迁移</li>
<li>自动执行，当超过迁移阈值时，负载均衡器自动执行</li>
</ol>
<p>负载均衡器（balancer）是专门负责Chunk迁移的后台进程。如果拥有最大、最小数量Chunk的Shards的<span style="background-color: #c0c0c0;">Chunk数量差超过迁移阈值</span>，负载均衡器启动迁移。Zone的配置影响负载均衡器的行为。</p>
<p>如果一个Chunk仅仅包含一个分片键值，则它可能超过Chunk尺寸而继续增大，成为巨块（jumbo chunk）。因为近包含一个分片键值，这种块无法再分裂，可能形成性能瓶颈。</p>
<div class="blog_h3"><span class="graybg">archiveMovedChunks</span></div>
<p>2.6/3.0版本的 <pre class="crayon-plain-tag">sharding.archiveMovedChunks</pre>参数默认开启，迁移源Shard会自动把被迁移的Chunks归档到storage.dbPath/moveChunk目录下。</p>
<p>如果迁移过程中出现错误，这些归档文件可以用于数据恢复。一旦迁移完成，这些文件不再有用，你可以将其删除。要判断迁移是否已经完成，连接到mongos并执行命令<pre class="crayon-plain-tag">sh.isBalancerRunning()</pre></p>
<div class="blog_h3"><span class="graybg">预创建块</span></div>
<p>某些场景下，MongoDB自动创建的Chunk不足以满足吞吐量需求。这些场景例如：</p>
<ol>
<li>当你希望对位于单个Shard上的大集合进行分片时</li>
<li>当你希望导入大量数据到一个负载不均衡的集群，或者导入可能导致负载不均衡时 —— 例如使用范围分片的情况下进行单调递增数据导入</li>
</ol>
<p>上面的场景对资源敏感，原因是：</p>
<ol>
<li>块迁移要求将Chunk中所有数据进行跨Shard的移动</li>
<li>任何Shard在同一时刻仅仅能参与一次迁移活动，负载均衡器会串行化的迁移Chunk。从3.4开始限制解除，变为：具有n个分片的集群，负载均衡器最多同时进行n/2个迁移活动</li>
<li>块分裂仅仅会在数据插入/更新后发生</li>
</ol>
<p>要进行预分裂（Pre-split，创建满足需要的Chunks数量），你可以执行<pre class="crayon-plain-tag">split</pre>命令。</p>
<p>警告：你只能在空集合上执行预分裂操作。如果集合已经包含数据，MongoDB在你执行集合分片时自动创建Chunks。后续再进行手工创建Chunks可能导致不可预测的Chunk范围/尺寸，以及低效的负载均衡行为。</p>
<div class="blog_h3"><span class="graybg">手工分裂</span></div>
<p>当Chunk尺寸到达限制后，MongoDB会自动分裂Chunk。但是某些场景下你可能期望手工执行分裂，例如：</p>
<ol>
<li>你有少量Chunks，但是要部署大量的数据到集群中</li>
<li>你需要部署大量可能落到单个Chunk/Shard中的数据，例如分片键值200-500归属于一个Chunk，现在你要插入海量分片键值300-400之间的数据</li>
</ol>
<p>根据需要，负载均衡器可能立即迁移刚刚分裂出来的Chunk，不考虑它是自动还是手工创建的。</p>
<p>调用<pre class="crayon-plain-tag">sh.status()</pre>可以了解当前Chunk对应的值区间。要手工分裂，调用split命令，或者Shell助手：<pre class="crayon-plain-tag">sh.splitFind()</pre>、<pre class="crayon-plain-tag">sh.splitAt()</pre>。</p>
<div class="blog_h3"><span class="graybg">手工合并</span></div>
<p>调用<pre class="crayon-plain-tag">mergeChunks</pre>命令，可以把空Chunk合并到同一Shard上的相邻Chunk上。所谓空Chunk，是值其键值范围内没有文档。</p>
<p>以下场景下，你可能需要手工合并：</p>
<ol>
<li>预创建（Pre-split）了过多的块</li>
<li>你删除了很多文档，导致某些Chunk变空</li>
</ol>
<div class="blog_h3"><span class="graybg">无法迁移</span></div>
<p>如果Chunk包含的文档数量大于：</p>
<ol>
<li>250000，或者</li>
<li>1.3 * Chunk尺寸 / 平均文档尺寸。平均文档尺寸通过db.collection.stats().avgObjSize得到</li>
</ol>
<p>则此Chunk无法被移动。</p>
<div class="blog_h2"><span class="graybg">区域（Zones）</span></div>
<p>在分片集群中，你可以依据分片键来创建分片数据的区域（zone）。<span style="background-color: #c0c0c0;">一个Zone可以关联1-N个Shard，每个Shard可以关联到任意个不冲突的Zone（多对多）</span>。在启用负载均衡的集群里，<span style="background-color: #c0c0c0;">Chunk仅仅在Zone内部迁移</span>。</p>
<p>需要使用Zone的场景包括：</p>
<ol>
<li>在一个Shard集上，隔离某个数据子集</li>
<li>确保相关的数据分布于地理位置相近的Shard上</li>
<li>根据Shard的硬件性能来路由</li>
</ol>
<p>考虑下面这个Zone配置：</p>
<p><img class="aligncenter size-full wp-image-15190" src="https://blog.gmem.cc/wp-content/uploads/2015/05/sharding-zone.png" alt="sharding-zone" width="692" height="330" /></p>
<p>分片键值位于1-10之间的，归属于Zone A；10-20之间的归属于Zone B。Shard A属于Zone A，Shard B属于Zone A和B。这样，键值位于1-10之间的数据子集仅仅能在Shard A、B之间迁移。</p>
<div class="blog_h3"><span class="graybg">键值范围</span></div>
<p>每个Zone可以<span style="background-color: #c0c0c0;">覆盖1-N个键值区间</span>。这些区间总是[ 闭，开 ) 形式的。所有<span style="background-color: #c0c0c0;">Zone的键值区间不得重叠</span>，一个Zone的每个键值区间也不能重叠。</p>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>负载均衡器会尝试把分片集合的Chunks均匀分发在所有Shard上。</p>
<p>对于标记为待迁移的Chunk，负载均衡器根据Zone配置来确定其可选的目标Shard。如果Chunk的值范围没有落到任何Zone里，则可能被迁移到任意Shard上。</p>
<p>如果负载均衡器发现任何Chunk违反了Zone定义（例如进行了Zone配置），它会将其进行迁移。</p>
<p>进行了Zone配置后，集群可能需要一定时间来执行Chunk迁移，迁移由负载均衡器在下一次 balancing rounds执行。</p>
<div class="blog_h3"><span class="graybg">分片键</span></div>
<p>定义Zone覆盖的值范围时，必须使用分片键或者分片键前缀字段。</p>
<div class="blog_h2"><span class="graybg">负载均衡器</span></div>
<p>MongoDB的集群负载均衡器是一个后台进程，它监控每个Shard上Chunk的数量。当最多 - 最少Chunk数量（按Shard）打到迁移阈值后，负载均衡器尝试进行Chunk迁移，使所有Shard持有仅可能平均数量的Chunks。</p>
<p>负载均衡器的行为对于用户、应用程序是完全透明的，但是迁移过程可能对性能产生影响。</p>
<p>从3.4开始，<span style="background-color: #c0c0c0;">负载均衡器在配置服务器的主节点上运行</span>。一旦其进程激活，就会修改配置服务器的lock集合上的一个文档，获得一个“锁”，这个锁一直不会释放。</p>
<p>从2.6开始，负载均衡器可能影响磁盘使用，因为迁移会导致源Shard对被迁移chunk进行归档。</p>
<p>负载均衡器会带来带宽、工作负载方面的成本，从而影响数据库的整体性能。它通过下面的措施使影响最小化：</p>
<ol>
<li>任何时刻仅能执行一个Chunk的迁移。从3.4开始，允许N个Shard的集群同时进行N/2个块迁移</li>
<li>仅仅当到达迁移阈值（migration threshold）时才启动负载均衡周期（balancing round）</li>
</ol>
<p>你可以临时的禁用负载均衡器，以执行维护任务。你也可以限制负载均衡器运行的时间窗口，防止对生产环境产生不利影响。</p>
<div class="blog_h3"><span class="graybg">增减Shard</span></div>
<p>添加Shard到集群时，会导致负载不均衡的出现，因为新的Shard不持有任何Chunk，达到再平衡需要一定的时间。</p>
<p>从集群中移除Shard时，其中的Chunk必须被重新分发，达到再平衡需要一定的时间，切勿再迁移完成前关闭被移除Shard对应的服务器。</p>
<div class="blog_h3"><span class="graybg">迁移工作流</span></div>
<p>所有块迁移均遵循以下流程：</p>
<ol>
<li>负载均衡器向源Shard发送moveChunk命令</li>
<li>源Shard使用内部moveChunk命令启动。在<span style="background-color: #c0c0c0;">迁移完成之前，路由到被移动Chunk的读写请求仍然由源Shard负责</span></li>
<li>目标Shard构建接纳Chunk所需要的、尚不存在的索引</li>
<li>目标Shard开始请求Chunk中的文档，接收数据拷贝并入库</li>
<li>接收完最后一个文档后，目标Shard启动一个同步进程，确保在迁移过程中，对被迁移Chunk的写操作被应用</li>
<li>当完全同步后，源Shard连接到配置数据库，并更新集群元数据，写入Chunk的新位置</li>
<li>元数据写入完毕后，一旦没有针对打开的被迁移Chunk的游标，源Shard删除本地的Chunk数据拷贝。如果负载均衡器需要针对源Shard进行下一个Chunk迁移，它不会等待删除操作的完成。从2.6开始，被迁移Chunk会在源Shard归档</li>
</ol>
<div class="blog_h3"><span class="graybg">迁移阈值</span></div>
<p>此阈值考虑Shard上的Chunk数量的最大差。默认取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Chunk数量</td>
<td style="text-align: center;">阈值</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt; 20</td>
<td>2</td>
</tr>
<tr>
<td>20-79</td>
<td>4</td>
</tr>
<tr>
<td>&gt;=80</td>
<td>8</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">迁移与复制</span></div>
<p>配置项_secondaryThrottle的值，决定了负载均衡器何时处理Chunk中下一个被迁移的文档（在目标Shard？）：</p>
<ol>
<li>如果取值为true，则当前文档必须至少被复制到一个从节点，才处理下一个文档，相当于写关注{ w :2 }</li>
<li>如果取值为false，则不等待复制到从节点，直接处理下一个文档</li>
</ol>
<p>从3.4开始，如果使用 WiredTiger引擎，该配置的默认值false， MMAPv1引擎的默认值仍然为true。</p>
<p>此外，不管_secondaryThrottle如何取值，迁移工作流的某些阶段，遵行如下复制策略：</p>
<ol>
<li>在更新集群元数据（第6步）之前，MongoDB会短暂的暂停针对源Shard上被迁移集合的读写操作。在更新元数据的前后，要求移动Chunk的写操作被复制集大多数节点确认</li>
<li>在执行源Shard清理（第7步）或者新的Chunk迁移之前，写操作必须被目标Shard复制集的大多数节点确认</li>
</ol>
<div class="blog_h2"><span class="graybg">分片集群Shell方法</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>sh._adminCommand()</td>
<td>针对admin数据库执行一个数据库命令</td>
</tr>
<tr>
<td>sh.getBalancerLockDetails()</td>
<td>报告负载均衡器锁的详细信息</td>
</tr>
<tr>
<td>sh._checkMongos()</td>
<td>检查当前Shell是否连接到mongos</td>
</tr>
<tr>
<td>sh._lastMigration()</td>
<td>报告最后一次发生的块迁移</td>
</tr>
<tr>
<td>sh.addShard()</td>
<td>
<p>添加一个分片到集群中</p>
<p>对应数据库命令addShard</p>
</td>
</tr>
<tr>
<td>sh.addShardTag()</td>
<td>sh.addShardToZone()的别名</td>
</tr>
<tr>
<td>sh.addShardToZone()</td>
<td>
<p>关联一个Shard到一个Zone</p>
<p>对应数据库命令addShardToZone</p>
</td>
</tr>
<tr>
<td>sh.addTagRange()</td>
<td>sh.updateZoneKeyRange()的别名</td>
</tr>
<tr>
<td>sh.updateZoneKeyRange()</td>
<td>关联一个分片键值范围到一个Zone。每个Zone可以关联多个值范围区间</td>
</tr>
<tr>
<td>sh.removeTagRange()</td>
<td>sh.removeRangeFromZone()的别名</td>
</tr>
<tr>
<td>sh.removeRangeFromZone()</td>
<td>解除一个分片键值范围与Zone的关联</td>
</tr>
<tr>
<td>sh.disableBalancing()</td>
<td>针对单个分片集合禁用负载均衡功能</td>
</tr>
<tr>
<td>sh.enableBalancing()</td>
<td>针对单个分片集合启用负载均衡功能</td>
</tr>
<tr>
<td>sh.enableSharding()</td>
<td>
<p>针对某个特定的数据库启用分片功能</p>
<p>对应数据库命令enableSharding</p>
</td>
</tr>
<tr>
<td>sh.getBalancerState()</td>
<td>返回一个布尔值，说明负载均衡器是否启用（全局的）</td>
</tr>
<tr>
<td>sh.help()</td>
<td>显示分片集群的Shell方法的简短帮助</td>
</tr>
<tr>
<td>sh.isBalancerRunning()</td>
<td>负载均衡器是否正在迁移Chunk</td>
</tr>
<tr>
<td>sh.moveChunk()</td>
<td>在分片集群中移动一个Chunk</td>
</tr>
<tr>
<td>sh.removeShardTag()</td>
<td>sh.removeShardFromZone()的别名</td>
</tr>
<tr>
<td>sh.removeShardFromZone()</td>
<td>
<p>从Zone中移除一个Shard</p>
<p>对应数据库命令removeShardFromZone</p>
</td>
</tr>
<tr>
<td>sh.setBalancerState()</td>
<td>启用或者禁用负载均衡器（全局的）</td>
</tr>
<tr>
<td>sh.shardCollection()</td>
<td>
<p>对一个集合进行分片</p>
<p>对应数据库命令shardCollection</p>
</td>
</tr>
<tr>
<td>sh.splitAt()</td>
<td>以分片键的某个值为分割点，把一个Chunk分割为两个</td>
</tr>
<tr>
<td>sh.splitFind()</td>
<td>将包含满足查询条件的Chunk分割为两个近似相等的新Chunks</td>
</tr>
<tr>
<td>sh.startBalancer()</td>
<td>
<p>启用负载均衡器并等待负载均衡触发</p>
<p>对应数据库命令balancerStart</p>
</td>
</tr>
<tr>
<td>sh.status()</td>
<td>报告分片集群的状态</td>
</tr>
<tr>
<td>sh.stopBalancer()</td>
<td>
<p>禁用负载均衡器并等待可能正在进行的负载均衡操作完毕</p>
<p>对应数据库命令balancerStop</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">分片集群数据库命令</span></div>
<p>下表仅仅列出没有对应Shell方法的数据库命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>flushRouterConfig</td>
<td>强制清空mongos的分片集群配置元数据缓存</td>
</tr>
<tr>
<td>balancerStatus</td>
<td>报告负载均衡器的状态信息</td>
</tr>
<tr>
<td>cleanupOrphaned</td>
<td>清理孤儿数据，所谓孤儿数据，是指其分片键值超越了某个Shard拥有的Chunk范围的数据</td>
</tr>
<tr>
<td>listShards</td>
<td>列出集群中的分片</td>
</tr>
<tr>
<td>removeShard</td>
<td>从集群中移除一个分片。集群将把该分片的Chunks迁移到其它分片上，这需要时间 </td>
</tr>
<tr>
<td>mergeChunks</td>
<td>合并单个分片上的多个Chunk</td>
</tr>
<tr>
<td>shardingState</td>
<td>报告当前mongod是否某个分片集群的成员 </td>
</tr>
<tr>
<td> split</td>
<td>创建一个新的Chunk</td>
</tr>
<tr>
<td> movePrimary</td>
<td>当从集群移除分片时，用于重新分片某个数据库的主分片</td>
</tr>
<tr>
<td> isdbgrid</td>
<td> 检查进程是否是mongos</td>
</tr>
<tr>
<td>updateZoneKeyRange</td>
<td> 添加/删除一个Zone关联的分片键值区间</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">管理MongoDB</span></div>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>启动mongos、mongod实例时，可以指定一个配置文件，提供必要的选项。MongoDB的配置文件是YAML-based格式的，不支持tab，要使用space代替。</p>
<p>要使用配置文件，指定命令行选项：</p>
<pre class="crayon-plain-tag">#  -f 是 --config 的别名
mongod --config /etc/mongod.conf
mongos --config /etc/mongos.conf</pre>
<div class="blog_h3"><span class="graybg">选项详解</span></div>
<pre class="crayon-plain-tag"># 日志选项
systemLog:
   # 默认日志级别，0-5之间，默认0，越大日志越详细
   # 要指定某个MongoDB组件的日志级别，使用 systemLog.component.&lt;name&gt;.verbosity
   verbosity: &lt;int&gt;
   # 安静模式，布尔，可以减少日志输出
   quiet: &lt;boolean&gt;
   # 截断异常信息，布尔
   traceAllExceptions: &lt;boolean&gt;
   # 记录到syslog使用的facility级别，默认user
   syslogFacility: &lt;string&gt;
   # 输出到指定的文件，而不是syslog中
   path: &lt;string&gt;
   # 默认false，是否追加内容到日志文件中
   logAppend: &lt;boolean&gt;
   # 日志轮转方式，rename / reopen
   logRotate: &lt;string&gt;
   # 日志记录目标，syslog / file ，选择file则必须设置systemLog.path
   destination: &lt;string&gt;
   # 日志时间戳格式，ctime / iso8601-utc / iso8601-local
   # 默认iso8601-local，示例 1969-12-31T19:00:00.000-0500
   timeStampFormat: &lt;string&gt;
   # 配置特定MongoDB组件的日志选项
   # 组件：accessControl / command / control / ftdc / geo / index / network / query /
   #       replication / sharding / storage / storage.journal / write 
   component:
      accessControl:
         verbosity: &lt;int&gt;
      command:
         verbosity: &lt;int&gt;

# 进程管理选项
processManagement:
   # 默认false，是否以daemon方式来启动mongos/mongod，默认不是daemon方式
   # MongoDB的Linux包期望此选项为默认值
   fork: &lt;boolean&gt;
   # PID文件的路径
   pidFilePath: &lt;string&gt;

# 网络选项
net:
   # 监听连接的端口，默认27017
   port: &lt;int&gt;
   # 监听的网络接口，默认0.0.0.0
   bindIp: &lt;string&gt;
   # mongos/mongod允许的最大连接数，默认65536
   maxIncomingConnections: &lt;int&gt;
   # 默认true，对每个客户端请求进行校验，防止插入无效、恶意的BSON到数据库
   wireObjectCheck: &lt;boolean&gt;
   # 默认false，是否启用IPv6支持
   ipv6: &lt;boolean&gt;
   # 在UNIX Domain套接字上监听
   unixDomainSocket:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 套接字路径前缀，默认/tmp
      pathPrefix: &lt;string&gt;
      # 套接字文件权限，默认 0700
      filePermissions: &lt;int&gt;
   # 提供RESTful API的HTTP接口，不要在生产环境使用
   http:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 是否支持JSONP
      JSONPEnabled: &lt;boolean&gt;
      # 是否提供RESTful接口
      RESTInterfaceEnabled: &lt;boolean&gt;
   ssl:
      # SSL的运作模式：disabled禁用；allowSSL服务器互联不使用，允许客户端连接使用；
      #              preferSSL服务器互联默认使用，允许客户端不适用；requireSSL 强制所有连接使用SSL
      mode: &lt;string&gt;
      # 同时包含证书和密钥的.pem文件
      # 如果连接时没有指定--sslCAFile则使用系统CA来验证服务器证书
      PEMKeyFile: &lt;string&gt;
      # 如果.pem加密，这里提供密码
      PEMKeyPassword: &lt;string&gt;
      # 用于集群成员、复制集成员相互认证的pem文件
      clusterFile: &lt;string&gt;
      # 如果.pem加密，这里提供密码
      clusterPassword: &lt;string&gt;
      # 包含了根证书链的.pem文件
      CAFile: &lt;string&gt;
      # 包含了证书吊销列表的.pem文件
      CRLFile: &lt;string&gt;
      # 是否允许客户端不提供证书（单向认证）
      allowConnectionsWithoutCertificates: &lt;boolean&gt;
      # 是否允许使用无效证书（集群中其它服务器）
      allowInvalidCertificates: &lt;boolean&gt;
      # 是否允许 TLS/SSL 证书和主机名不匹配
      allowInvalidHostnames: &lt;boolean&gt;
      # 禁用的入站协议类型TLS1_0、TLS1_1、TLS1_2
      disabledProtocols: &lt;string&gt;
      # 是否启用OpenSSL的FIPS模式
      FIPSMode: &lt;boolean&gt;
   # 是否启用服务器之间，服务器与Shell之间的数据压缩
   compression:
      compressors: &lt;string&gt;  # 压缩器，例如snappy

# 安全选项
security:
   # 对分片集群、复制集中其它成员进行认知的共享密钥所在的文件
   keyFile: &lt;string&gt;
   # 集群的认证方式：keyFile共享密钥文件；sendKeyFile发送共享密钥但是也允许其它节点以X509方式请求本节点
   #              sendX509发送X509但是也允许其它节点以共享密钥方式请求本节点；x509 推荐，仅X509
   clusterAuthMode: &lt;string&gt;
   # 默认disabled，可选disabled。是否启用基于角色的访问控制（RBAC），对应命令行选项 --auth
   authorization: &lt;string&gt;
   # 默认false，3.4新增。允许mongos/mongd接受/创建到其它mongos/mongd的认证/非认证连接
   transitionToAuth: &lt;boolean&gt;
   # 是否启用服务器端的JS执行，默认true。如果禁用，则无法使用$where、db.collection.mapReduce()、db.collection.group()等
   javascriptEnabled:  &lt;boolean&gt;

# 设置MongoDB服务器参数
setParameter:
   &lt;parameter1&gt;: &lt;value1&gt;
   &lt;parameter2&gt;: &lt;value2&gt;

# 存储选项
storage:
   # mongod实例存储数据的位置，默认/data/db 
   dbPath: &lt;string&gt;
   # 默认true，是否在下一次启动时构建未完成的索引
   indexBuildRetry: &lt;boolean&gt;
   # 当mongod以--repair进行修复启动时，使用的工作目录
   repairPath: &lt;string&gt;
   # 是否启用日志，64bit默认true，32bit默认false
   journal:
      # 是否启用
      enabled: &lt;boolean&gt;
      # 允许写入日志的最小间隔，默认100ms
      commitIntervalMs: &lt;num&gt;
   # 是否为每个数据库新建子目录，默认false
   directoryPerDB: &lt;boolean&gt;
   # 每隔多久把数据刷入数据文件中，默认60s，通常不需要修改。如果设置为0从不把内存映射文件刷入磁盘
   syncPeriodSecs: &lt;int&gt;
   # 使用的存储引擎，mmapv1 / wiredTiger / inMemory
   engine: &lt;string&gt;
   # 引擎特定选项
   mmapv1:
      # 是否预分配数据文件的磁盘空间
      preallocDataFiles: &lt;boolean&gt;
      # 名字空间文件的大小，每个集合、索引都被认为是名字空间
      # 默认16MB，允许大概24000个名字空间
      nsSize: &lt;int&gt;
      quota:
         # 是否限制每个数据库能够拥有的数据文件的数量
         enforced: &lt;boolean&gt;
         # 每个数据库的最大数据文件数量，默认8
         maxFilesPerDB: &lt;int&gt;
      # 使用小数据文件，设置为true则减小数据文件的初始大小，且不允许超过512MB
      # 设置为true，同时导致日志文件的最大尺寸从1G变为128MB
      # 可能导致创建大量的数据文件，从而影响性能
      smallFiles: &lt;boolean&gt;
      journal:
         debugFlags: &lt;int&gt;
         commitIntervalMs: &lt;num&gt;
   # 引擎特定选项
   wiredTiger:
      engineConfig:
         # 为所有数据准备的内部缓存的大小，最小256MB，默认RAM /2 - 1GB
         # 避免修改此配置，因为该引擎可以利用OS所有空闲内存作为缓存
         # 使用容器时，配合容器可用内存设置该选项
         cacheSizeGB: &lt;number&gt;
         # 日志的压缩算法 snappy / zlib
         journalCompressor: &lt;string&gt;
         # 是否在不同子目录存放索引、数据
         directoryForIndexes: &lt;boolean&gt;
      collectionConfig:
         # 集合的默认压缩算法 none / snappy / zlib
         # 你可以在创建集合的时候覆盖
         blockCompressor: &lt;string&gt;
      indexConfig:
         # 是否启用索引前缀压缩
         prefixCompression: &lt;boolean&gt;
   # 引擎特定选项
   inMemory:
      engineConfig:
         # 使用的内存量
         inMemorySizeGB: &lt;number&gt;

# 性能剖析选项
operationProfiling: 
   # 超过多少ms，操作被剖析器认为是缓慢的
   slowOpThresholdMs: &lt;int&gt;
   # 剖析模式： off / slowOp / all
   mode: &lt;string&gt;

# 复制集选项
replication:
   # Oplog的大小，对于64bit系统，默认5%可用磁盘空间
   oplogSizeMB: &lt;int&gt;
   # 该实例所属的复制集的名称
   replSetName: &lt;string&gt;
   # 仅mmapv1引擎，是否预抓取索引
   secondaryIndexPrefetch: &lt;string&gt;
   # 设置读关注为majority
   enableMajorityReadConcern: &lt;boolean&gt;
   # 以下子项仅仅用于mongos
   localPingThresholdMs: &lt;int&gt;

# 分片选项
sharding:
   # 当前分片在集群中的角色，configsvr / shardsvr
   clusterRole: &lt;string&gt;
   # 是否对被迁移的Chunk进行归档保存
   archiveMovedChunks: &lt;boolean&gt;
   # 以下子项仅仅用于mongos
   configDB: &lt;string&gt;

# 审计选项
auditLog:
   # 审计日志的目的地 syslog / console / file
   destination: &lt;string&gt;
   # 审计日志格式 JSON / BSON
   format: &lt;string&gt;
   # 审计日志存放路径
   path: &lt;string&gt;
   # 过滤器文档，不匹配的不记录
   filter: &lt;string&gt;

# SNMP监控选项
snmp:
   # 是否作为subagent运行
   subagent: &lt;boolean&gt;
   # 是否作为master运行
   master: &lt;boolean&gt;

# 全文搜索选项
basisTech
   # Basis Technology Rosette Linguistics Platform 安装目录
   basisTech.rootDirectory</pre>
<div class="blog_h2"><span class="graybg">管理mongod进程</span></div>
<pre class="crayon-plain-tag"># 启动mongod进程，默认数据目录/data/db，端口27017
mongod
# 以守护进程的方式启动，并把日志记录到指定位置
mongod --fork --logpath /var/log/mongodb.log

# 停止服务
use admin
db.shutdownServer()

# 停止服务，方式二
mongod --shutdown</pre>
<div class="blog_h3"><span class="graybg">停止复制集</span></div>
<p>主节点的mongod的停止流程如下：</p>
<ol>
<li>检查从节点的复制进度</li>
<li>如果没有任何从节点延迟小于10s，mongod会返回一个信息，提示不能停止。要强行停止，可以为shutdown命令提供force参数：<br />
<pre class="crayon-plain-tag">db.adminCommand({shutdown : 1, force : true})</pre></p>
<p>如果要持续检查指定的时间，在此时间内有从节点跟上复制进度则关闭，跟不上则不关闭，可以执行：</p>
<pre class="crayon-plain-tag">db.adminCommand({shutdown : 1, timeoutSecs : 5})
# 或者
db.shutdownServer({timeoutSecs : 5})</pre>
</li>
<li>如果由节点延迟小于10s，主节点会Stepdown，并等待从节点跟上复制进度</li>
<li>从节点跟上复制进度，或者60s之后，主节点关闭 </li>
</ol>
<div class="blog_h3"><span class="graybg">停止操作</span></div>
<p>可以指定查询、命令的最长执行耗时：
<pre class="crayon-plain-tag">db.collection.find(...).maxTimeMS(30)
db.runCommand( { maxTimeMS: 45 } )

# 如果查询、命令因为超时而被终止，可以通过db.getLastError() 或者db.getLastErrorObj()得到相关信息</pre>
<p>你可以调用<pre class="crayon-plain-tag">db.killOp(&lt;opId&gt;)</pre>来强制终止某个操作，注意，不要对任何数据库内部操作执行此调用。 执行<pre class="crayon-plain-tag">db.currentOp()</pre>得到正在运行的操作的列表。</p>
<div class="blog_h2"><span class="graybg">生产环境注意事项</span></div>
<div class="blog_h3"><span class="graybg">并发能力</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MMAPv1</td>
<td>
<p>2.2以前的版本：实例级别的全局锁，同一时刻只能有一个客户端执行写操作</p>
<p>2.2 - 2.6版本：每个数据库有一个读写锁，允许针对数据库的并发读，但是不允许针对数据库的并发写。也就是说，同一时刻只能有一个客户端在写某个数据库</p>
<p>3.0以后的版本：每个集合有一个读写锁，允许多个客户端同时对多个不同的集合进行写操作</p>
</td>
</tr>
<tr>
<td>WiredTiger</td>
<td>
<p>支持文档级别的并发读写，一个线程可以修改集合C的文档A，同时另外一个线程可以修改集合C的文档B</p>
<p>在写操作进行的过程中，读操作不受限制</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">数据一致性</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">数据库特性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>日志</td>
<td>
<p>MongoDB使用预写式磁盘日志，这种日志写入很快，只要写入了该日志，宕机后可以恢复数据，即使没有写到数据文件中</p>
<p>该日志默认开启，要保证宕机后不影响数据一致性，就不要关闭它</p>
</td>
</tr>
<tr>
<td>读关注</td>
<td>
<p>当使用majority、linearizable两种读关注时，必须配合写关注{ w: "majority" }，这样可以确保线程可以读到它自己先前的写操作</p>
<p>当使用读关注majority时：</p>
<ol>
<li>必须配置replication.enableMajorityReadConcern = true</li>
<li>复制集必须使用WiredTiger引擎，且使用协议版本1</li>
</ol>
</td>
</tr>
<tr>
<td>写关注</td>
<td>写关注影响写操作的返回数据，强写关注导致返回更慢，但是可以更好的确保数据一致性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络</span></div>
<p>你应当总是在受信任的网络中运行MongoDB，使用合理的防火墙策略来阻止非受信任的机器、系统、网络。通常，仅仅应用服务器、监控服务、其它MongoDB组件需要MongoDB的访问权限。</p>
<p>默认情况下，MongoDB的授权系统是关闭的，任何人都可以访问，需要注意。</p>
<p>MongoDB提供了一个用于检查服务器状态、执行查询的HTTP接口，此接口默认是关闭的，不要在生产环境中开启此接口。</p>
<p>避免盲目调整mongos/mongod的连接池大小，以满足你的需要。<pre class="crayon-plain-tag">connPoolStats</pre>命令显示当前数据库中打开的连接数。</p>
<div class="blog_h3"><span class="graybg">CPU和RAM</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">存储引擎</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MMAPv1</td>
<td>
<p>由于其并发性，你不需要为MMAPv1分配过多的CPU资源，但是应该保证mongos/mongod能够访问两个真实CPU</p>
<p>增加MongoDB可访问的内存量可以减少页错误发生的频率</p>
<p>注意MMAPv1不会使用Swap</p>
</td>
</tr>
<tr>
<td>WiredTiger</td>
<td>
<p>该引擎是多线程的，能够利用另外的CPU。特别是，活动线程数（并发操作数）与可用CPU的比值，可以影响性能：</p>
<ol>
<li>当比值小于1时，随着并发数增加，吞吐量增加</li>
<li>当比值大于1时，随着并发数增加，吞吐量减小</li>
</ol>
<p>mongostat输出的ar/aw列可以显示活动读/写数量</p>
<p>使用该引擎时，MongoDB同时利用WiredTiger内部缓存、OS缓存。</p>
<p>从3.4开始，WiredTiger缓存默认使用256MB、总RAM/2 - 1GB之中较大的内存量。可以通过<pre class="crayon-plain-tag">storage.wiredTiger.engineConfig.cacheSizeGB</pre>配置。在容器（例如Docker）中运行MongoDB时，应该设置此配置，使之小于容器可使用内存的量</p>
<p>要了解缓存的统计信息、清除率，运行<pre class="crayon-plain-tag">serverStatus</pre>命令并查看wiredTiger.cache字段</p>
<p>MongoDB会使用所有空闲内存作为OS缓存（文件系统缓存），OS缓存中的数据是被压缩的，存放MongoDB数据文件的映射</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">存储</span></div>
<p>使用SSD，MongoDB可以获得更好的性能、更高的性价比。 SSD的随机I/O能力可以很好的适应MMAPv1的更新模型。</p>
<p>注意为系统分配Swap，避免内存争用，或者因为OOM导致mongod被杀死。当使用MMAPv1时，其映射数据文件到内存的行为，导致永远不会使用Swap空间。</p>
<div class="blog_h2"><span class="graybg">数据库性能</span></div>
<p>当面向MongoDB性能低下时，其原因往往和数据库访问策略、硬件、并发操作数有关、错误/不当索引、低效的Schema设计相关。排除了前述可能性之后，数据库可能已经满载运行了，或许需要进行水平扩容。</p>
<p>你应该保证应用的工作集（Working set，最常使用的数据）能够全部装入内存，这样对性能提升有很大帮助，特别是使用MMAPv1引擎时。</p>
<p>某些情况下性能问题是临时的，通常由于突发的高并发导致。</p>
<div class="blog_h3"><span class="graybg">锁性能</span></div>
<p>为了确保数据一致性，MongoDB采用了一套锁系统。长时间运行、队列形式的操作可能导致性能降低，因为它们持有锁导致后续请求被迫等待。</p>
<p>要查看锁是否影响了系统性能，执行命令：<pre class="crayon-plain-tag">db.runCommand( { serverStatus: 1 } )</pre>并查看输出的locks、globalLock段。</p>
<p>locks.timeAcquiringMicros /  locks.acquireWaitCount的结果近似的反映了等待某种特定锁模式所消耗的平均时间。</p>
<p>locks.deadlockCount表示锁请求导致死锁的次数。</p>
<p>如果globalLock.currentQueue.total计数总是很高，意味着可能大量的操作在等待一个锁，提示存在影响性能的并发问题。</p>
<p>如果globalLock.totalTime / uptime的比值较高，意味着数据库很长时间处于一个锁状态。</p>
<p>耗时漫长的操作可能由于：</p>
<ol>
<li>低效的索引</li>
<li>低效的Schema设计</li>
<li>低效的查询语句</li>
<li>内存不足，导致页错误而产生磁盘读操作</li>
</ol>
<div class="blog_h3"><span class="graybg">MMAPv1和内存</span></div>
<p>MMAPv1引擎使用内存映射文件来存储数据。对于一个尺寸足够大的数据集，MMAPv1会分配尽可能多的系统内存供其使用。</p>
<p>判断内存是否对于数据集来说是足够的并不容易，查看 serverStatus的mem段可以查看MongoDB的内存使用情况。mem.resident记录MongoDB驻留工作集使用的内存，如果该值超过系统可用内存，并且还有很多数据没有映射到内存中，说明达到了系统的最大容量。</p>
<p>mem.mapped字段记录了MongoDB使用的内存映射文件大小，如果此值操作系统内存总大小，意味着某些读操作会触发页面错误而导致磁盘读。</p>
<p>serverStatus.extra_info.page_faults记录了MongoDB发生页面错误的数量。如果此计数快速增加，则可能是：</p>
<ol>
<li>系统内存过小</li>
<li>正在访问大量数据、扫描整个集合</li>
</ol>
<p>导致。偶尔发生的页面错误不要紧，但是大量累积的页面错误意味着存在I/O性能问题。</p>
<p>当遇到页面错误的时候，MongoDB可能放弃读锁，这样在换页期间其它数据库进场可以进行读写操作，这种行为提升了吞吐量、并发能力。</p>
<p>处理内存不足的方法是，水平或者垂直扩容。</p>
<div class="blog_h3"><span class="graybg">连接数</span></div>
<p>某些情况下，客户端和数据库之间的连接数会超过服务器的处理能力。serverStatus输出的以下字段可以提供相关信息：</p>
<ol>
<li>globalLock.activeClients，当前正在执行操作、或者排队等待操作的客户端数量</li>
<li>connections.current，连接到服务器的客户端数量</li>
<li>connections.available，空闲的可以供新客户端使用的连接数</li>
</ol>
<p>如果并发连接数持续的高，可能系统需要扩容。对于读过载的应用考虑复制集，对于写过载的应用考虑分片集群。</p>
<p>MongoDB本身没有对并发连接数的限制，操作系统本身的限制，例如UNIX，考虑设置ulimit。</p>
<div class="blog_h3"><span class="graybg">数据库剖析</span></div>
<p>MongoDB自带了一个剖析器，用于识别和分析低效的查询、操作。要为某个数据库启用剖析器，执行：</p>
<pre class="crayon-plain-tag"># 0 不启用剖析
# 1 仅仅剖析缓慢操作，设置项slowOpThresholdMs决定了缓慢查询的判断标准
# 2 剖析所有操作
db.setProfilingLevel(1)
# 输出示例：
{"was" : 0, "slowms" : 100, "ok" : 1 }

# 先的命令检查剖析设置
db.getProfilingStatus()</pre>
<p>要为整个mongod启用剖析器，使用选项：<pre class="crayon-plain-tag">mongod --profile 1 --slowms 15</pre>。</p>
<p>注意，剖析对数据库性能具有负面影响，因而默认是关闭的。你可以针对单个mongod实例、单个数据库进行设置，这些设置不会传播到整个复制集或者分片集群。不支持对整个分片集群设置剖析，你必须针对单个mongod进行设置。</p>
<p>剖析器收集MongoDB写操作、游标、数据库命令的细粒度信息。收集的信息被存放到定长集合 <pre class="crayon-plain-tag">system.profile</pre>中。执行<pre class="crayon-plain-tag">show profile</pre>或者：<pre class="crayon-plain-tag">db.system.profile.find( { millis : { $gt : 100 } } )</pre> 可以查看剖析器的输出。输出示例：</p>
<pre class="crayon-plain-tag">{
   // 操作类型
   "op" : "query",
   // 操作针对的对象
   "ns" : "test.c",
   // 使用的查询文档，对于insert操作来说，则是插入的文档
   "query" : {
      "find" : "c",
      "filter" : {
         "a" : 1
      }
   },
   // 执行update操作时，使用的更新文档
   "updateobj" :..,
   // query、getmore操作使用的游标ID
   "cursorid" :...,
   // 执行的命令
   "command" : ..,
   // 别名system.profile.nscanned，为了完成操作MongoDB扫描的索引值数量
   "keysExamined" : 2,
   // 别名system.profile.nscannedObjects，为了完成操作MongoDB扫描的文档数量
   "docsExamined" : 2,
   // 仅MMAPv1引擎，为了完成操作，在磁盘上移动文档的数量
   "nmoved" :...,
   // 操作删除的文档数量
   "ndeleted" :...,
   // 操作插入的文档数量
   "ninserted" :...,
   // update操作修改文档的数量
   "nModified" :...,
   // 是否为upsert操作
   "upsert" :...,
   // 如果通过索引不能得到需要的排序，则出现此Stage
   "hasSortStage" : ...,
   "cursorExhausted" : true,
   // 写操作插入的索引键数量
   "keysInserted" : 0,
   // 删除的索引键数量
   "keysDeleted" : 0,
   // 写操作冲突文档数量，所谓冲突即多个update操作尝试修改同一文档的情况
   "writeConflicts" : 0,
   // 让出锁以便其它操作能够进行的次数，这意味着当前操作需要读取不在内存中的数据
   "numYield" : 0,
   // 原来该字段叫lockStats
   "locks" : {
      // 锁类型：{剖析信息}
      // Global 全局锁
      // MMAPV1Journal 该引擎特定的，用于同步日志写操作的锁
      // Database 数据库锁
      // Collection 集合锁
      // Metadata 元数据锁
      // oplog Oplog锁
      "Global" : {
         // 请求锁的次数
         "acquireCount" : {
            // 锁模式：
            // R 共享锁，W 独占锁，r 意向共享锁，w 意向独占锁
            "r" : NumberLong(2)
         }
         "acquireWaitCount" :...,      // 等待获得锁的次数
         "timeAcquiringMicros" :...,   // 累积等待锁消耗的微秒数
         "deadlockCount" :...,         // 等待锁时遭遇死锁的次数
      }
   },
   // 操作返回文档的数量
   "nreturned" : 2,
   // 返回文档的字节长度
   "responseLength" : 108,
   // 从mongod角度来看，操作完整消耗的时间
   "millis" : 0,
   // 执行统计信息，参考执行计划
   "execStats" : {},
   // 操作的时间戳
   "ts" : ISODate("2015-09-03T15:26:14.948Z"),
   // 发起操作的客户端地址
   "client" : "127.0.0.1",
   // 发起操作的应用名称，驱动支持设置应用名称
   "appName" : "MongoDB Shell",
   // 当前会话认证的所有用户的数组
   "allUsers" : [ ],
   // 执行此操作的用户
   "user" : ""
}</pre>
<p>需要修改剖析日志的容量时，参考以下步骤：</p>
<pre class="crayon-plain-tag">db.setProfilingLevel(0)
db.system.profile.drop()
db.createCollection( "system.profile", { capped: true, size:4000000 } )
db.setProfilingLevel(1)

# 如果要修改从节点的剖析日志容量，首先需要以standalone模式启动之，然后执行上述修改</pre>
<div class="blog_h3"><span class="graybg">禁用透明巨页</span></div>
<p>透明巨页（Transparent Huge Pages）是一种Linux的内存管理系统，用于减少转译后备缓冲区（Translation Lookaside Buffer ，TLB）的查询成本。由于THP倾向于导致稀疏而非连续的内存访问模式，因而不适合数据库工作负载。</p>
<p>要禁用透明巨页特性，参考：</p>
<pre class="crayon-plain-tag"># Ubuntu
sudo update-rc.d disable-transparent-hugepages defaults
# CentOS
sudo chkconfig --add disable-transparent-hugepages</pre>
<div class="blog_h3"><span class="graybg">ulimit设置</span></div>
<p> 大部分类UNIX系统提供了按用户/进程来控制文件、线程、网络连接等系统资源使用数量的手段 —— ulimits。</p>
<p>ulimit的默认值对于MongoDB可能太低了，需要调整。</p>
<div class="blog_h2"><span class="graybg">备份</span></div>
<div class="blog_h3"><span class="graybg">基于文件系统快照</span></div>
<p>可以基于Linux下的LVM进行文件系统级的备份/恢复。创建快照的命令示意：</p>
<pre class="crayon-plain-tag"># 针对卷组vg0中的mongodb卷创建一个名为mdb-snap01的快照
# 快照容量100M，存放此快照与文件系统最终状态的diff
lvcreate --size 100M --snapshot --name mdb-snap01 /dev/vg0/mongodb

# 归档压缩
umount /dev/vg0/mdb-snap01
dd if=/dev/vg0/mdb-snap01 | gzip &gt; mdb-snap01.gz</pre>
<p>要基于快照恢复，可以执行：</p>
<pre class="crayon-plain-tag"># 在卷组vg0中创建一个名为mdb-new的逻辑卷，大小1G（根据实际MongoDB占用磁盘空间确定）
lvcreate --size 1G --name mdb-new vg0
# 把之前的快照导入到逻辑卷
gzip -d -c mdb-snap01.gz | dd of=/dev/vg0/mdb-new
# 挂载逻辑卷，挂载点直线
mount /dev/vg0/mdb-new /srv/mongodb</pre>
<div class="blog_h3"><span class="graybg">基于MongoDB工具</span></div>
<p>命令mongodump/mongorestore实现基于BSON格式的备份与恢复，适合小型数据库。要实现弹性、无缝的备份，建议使用文件系统、块设备级别的快照功能进行备份。</p>
<p>mongodump/mongorestore需要和运行中的mongod进行交互，因而会影响数据库性能。除了网络流量方面的开销外，这两个工具还需要通过内存来读取数据。MongoDB会因此载入很少使用的数据并把常用数据清除出内存。</p>
<p>备份命令示例：</p>
<pre class="crayon-plain-tag">mongodump --host 172.21.2.1 --port 27017   --out /data/backup/
          # 可以限制导出的数据库，甚至集合
          --db cluster --collection corps
          # 如果服务器启用了身份验证，可以指定密码
          --username user --password "passwd" 
          # 在dump期间收集oplog，形成指向特定时间点的备份
          --oplog</pre>
<p>恢复命令示例：</p>
<pre class="crayon-plain-tag">mongorestore --host 172.21.2.1 --port 27017 
             # 重做oplog
             --oplogReplay
             # 备份所在位置
             /data/backup/</pre>
<div class="blog_h3"><span class="graybg">宕机后修复</span></div>
<p>如果单机mongod禁用了日志，则意外宕机可能导致数据处于不一致状态，启动时报错： Detected unclean shutdown - mongod.lock is not empty。在数据文件目录下会存在非空的 mongod.lock 文件。这种情况下，你需要修复数据库：</p>
<ol>
<li>备份数据文件目录</li>
<li>以修复模式启动数据库：<pre class="crayon-plain-tag">mongod --dbpath /data/db --repair</pre> </li>
</ol>
<div class="blog_h2"><span class="graybg">管理复制集</span></div>
<div class="blog_h3"><span class="graybg">配置管理</span></div>
<p>要读取复制集的配置对象，调用<pre class="crayon-plain-tag">rs.conf()</pre>方法或者在admin数据库上调用：</p>
<pre class="crayon-plain-tag">db.runCommand( { replSetGetConfig: 1 } )</pre>
<p>要修改复制集配置，调用<pre class="crayon-plain-tag">rs.reconfig()</pre>并传入一个配置文档：</p>
<pre class="crayon-plain-tag">{
  // 复制集的名称，一旦设置，不可更改。必须和配置replication.replSetName或者命令行参数--replSet一致
  _id: &lt;string&gt;,
  // 递增的配置版本号，用于区分复制集配置的修订版
  version: &lt;int&gt;,
  // 选举协议的版本，从3.2开始默认1
  protocolVersion: &lt;number&gt;,
  // 3.4新增，protocolVersion为1默认true，否则默认false。指示{ w: "majority" }是否隐含{ j: true }
  writeConcernMajorityJournalDefault: &lt;boolean&gt;,
  // 指示当前复制集是否用作分片集群的配置服务器
  configsvr: &lt;boolean&gt;,
  // 成员配置文档的数组
  members: [
    {
      // 成员标识符，0-255，一旦设置不得修改
      _id: &lt;int&gt;,
      // 成员主机名，或者host:port，主机名必须可以从任何复制集成员进行DNS解析
      host: &lt;string&gt;,
      // 布尔，用于指示该成员是否为仲裁者
      arbiterOnly: &lt;boolean&gt;,
      // 默认true，mongod是否在此成员上构建索引。仅在添加复制集成员时可以设置，不得改变
      // 在下列条件全部满足时，设置为false可能有用：
      // 1、仅仅使用该成员执行mongodump
      // 2、该成员从不接受查询
      // 3、维护索引的成本让硬件受不了
      // 即使设置为false，也会构建_id索引
      // 设置为false时必须同时设置priority为0
      // 其它成员不能从buildIndexes = false的成员进行复制
      buildIndexes: &lt;boolean&gt;,
      // 如果设置为true，db.isMaster()的输出不包含此成员，阻止读操作转发给该成员
      hidden: &lt;boolean&gt;,
      // 默认1.0，0-100之间，优先级权重，值越高，越有资格被选举为主节点
      priority: &lt;number&gt;,
      // 标签集文档，可以包含任意键值对映射。用于定制读写关注，使其感知数据中心
      tags: &lt;document&gt;,
      // 单位秒，默认0，用于配置延迟从节点
      slaveDelay: &lt;int&gt;,
      // 投票权，默认1，可选0。仲裁者总是1
      votes: &lt;number&gt;
    },
    ...
  ],
  settings: {
    // 默认true，如果true，则允许从节点从其它从节点复制，而不仅仅时主节点
    chainingAllowed : &lt;boolean&gt;,
    // 内部使用，复制集成员需要不断的相互发送心跳，确认可达性
    heartbeatIntervalMillis : &lt;int&gt;,
    // 心跳超时时间，默认10s
    heartbeatTimeoutSecs: &lt;int&gt;,
    // 默认10000ms，用于检测主节点不可用的延迟时间。高取值延长故障转移时间但是降低对不稳定网络的敏感性
    electionTimeoutMillis : &lt;int&gt;,
    // 新选举的主节点，其数据可能不是最新的，此时需要从最新的从节点同步
    // 高取值避免从节点回滚数据的可能，但是延长了故障转移时间
    // 同步完成之前，主节点不接受写请求
    catchUpTimeoutMillis : &lt;int&gt;,
    // 用于定义扩展的写关注值，可以提供数据中心感知，例如：
    // { getLastErrorModes: { eastCoast: { "east": 1 } } } 允许使用写关注：
    // { w: "eastCoast" }，表示要求写操作传播到具有east:1标签的节点上
    getLastErrorModes : &lt;document&gt;,
    // 用于指定默认的写关注
    getLastErrorDefaults : &lt;document&gt;,
    // 此复制集的内部唯一标识，自动创建不可更改
    replicaSetId: &lt;ObjectId&gt;
  }
}</pre>
<div class="blog_h3"><span class="graybg">查看复制集状态</span></div>
<p>调用<pre class="crayon-plain-tag">rs.status()</pre>或者在admin数据库执行replSetGetStatus命令，查看当前复制集的状态：</p>
<pre class="crayon-plain-tag">{
    // 复制集名称
    "set" : "replset",
    // 当前时间
    "date" : ISODate("2016-11-02T20:02:16.543Z"),
    // 当前节点的复制状态
    "myState" : 1,
    // 选举发生的次数（本节点知晓的），使用协议版本0时总是返回-1
    "term" : NumberLong(1),
    // 心跳频率
    "heartbeatIntervalMillis" : NumberLong(2000),
    // 用于了解复制信息细节的时间信息
    "optimes" : {
          // 从当前节点角度看，最近一次已经传播到大部分复制集成员的写操作的发生时间
          "lastCommittedOpTime" : {
             "ts" : Timestamp(1478116934, 1),  // 操作发生的时间
             "t" : NumberLong(1)               // 操作发生在那次term
          },
          // 从当前节点角度看，最近一次能满足majority读关注的操作的发生时间
          "readConcernMajorityOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          },
          // 从当前节点角度看，最近一次应用到当前节点的操作的发生时间
          "appliedOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          },
          // 从当前节点角度看，最近一次应用到当前节点、且已经写入日志的操作的发生时间
          "durableOpTime" : {
             "ts" : Timestamp(1478116934, 1),
             "t" : NumberLong(1)
          }
       },
    // 复制集成员状态
    "members" : [
        {
            "_id" : 0,
            "name" : "m1.example.net:27017",
            // 仅当前节点出现：用于指示当前节点
            "self" : true,
            // 仅非当前节点出现：是否宕机
            "health" : 1,
            // 复制集成员状态
            "state" : 1,
            "stateStr" : "PRIMARY",
            // 此成员已经在线的时间
            "uptime" : 269,
            // 该成员最后一次从oplog应用写操作的时间
            "optime" : {
                        "ts" : Timestamp(1478116934, 1),
                        "t" : NumberLong(1)
            },
            "optimeDate" : ISODate("2016-11-02T20:02:14Z"),
            "infoMessage" : "could not find member to sync from",
            // 选举发生的时间
            "electionTime" : Timestamp(1478116933, 1),
            "electionDate" : ISODate("2016-11-02T20:02:13Z"),
            // 该成员最后一次从oplog应用写操作、且已经写入日志的时间
            "optimeDurable" : {
               "ts" : Timestamp(1478116934, 1),
               "t" : NumberLong(1)
            },
            "optimeDurableDate" : ISODate("2016-11-02T20:02:14Z"),
            // 最后一次该成员发送心跳的时间
            "lastHeartbeat" : ISODate("2016-11-02T20:02:15.619Z"),
            // 最后一次从该成员收到心跳的时间，与上一字段的差值体现了两节点之间的网络延迟
            "lastHeartbeatRecv" : ISODate("2016-11-02T20:02:14.787Z"),
            // PING该节点消耗的时间
            "pingMs" : NumberLong(0),
            // 该节点的复制源
            "syncingTo" : "m1.example.net:27018",
            // 该节点使用的复制集配置版本号
            "configVersion" : 1
        }
    ],
    "ok" : 1
}</pre>
<p>成员状态的含义参考<a href="#rs-member-state">复制集成员状态</a>。</p>
<div class="blog_h3"><span class="graybg">新建复制集</span></div>
<p>在生产环境下部署复制集，应当保证以下前置条件：</p>
<ol>
<li>仅可能把mongod部署在不同的物理机器上。如果使用虚拟机，应当使虚拟机对应不同的物理机器。应当确保物理机器之间有冗余电路、冗余网络路径</li>
<li>保证所有节点可以相互通信</li>
</ol>
<p>配置文件添加复制集配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rs0</pre>
<p>创建三成员复制集：</p>
<pre class="crayon-plain-tag"># 创建三个mongod实例
docker run --name mongo-00 --network local --ip 172.21.0.100 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-01 --network local --ip 172.21.0.101 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-02 --network local --ip 172.21.0.102 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>启动所有实例，然后在其中一个实例上，打开MongoDB Shell，执行：</p>
<pre class="crayon-plain-tag">rs.initiate( {
   _id : "rs0",
   members: [ { _id : 0, host : "172.21.0.100:27017" } ]
})</pre>
<p>现在可以执行 <pre class="crayon-plain-tag">rs.conf()</pre> 查看复制集的配置信息，可以看到当前复制集有一个成员。接着把另外两个成员加入到复制集中：</p>
<pre class="crayon-plain-tag">rs.add("172.21.0.101:27017")
rs.add("172.21.0.102:27017")</pre>
<p>现在调用<pre class="crayon-plain-tag">rs.status()</pre>可以看到复制集及其三个成员的状态。 </p>
<div class="blog_h3"><span class="graybg">添加成员</span></div>
<p>注意以下几点：</p>
<ol>
<li>每个复制集最多7个投票成员，如果已经有7个成员，再添加成员必须设置votes=0，或者移除既有成员的投票权</li>
<li>可以添加先前被移除的成员，如果该成员的数据没有被删除且足够新，它有可能跟得上oplog的节奏而不需要完全同步</li>
<li>如果你拥有既有成员的足够新的数据备份，拷贝备份到新成员对应dbPath目录下，可以快速添加新成员</li>
</ol>
<div class="blog_h3"><span class="graybg">添加仲裁者</span></div>
<p>以类似创建上面三成员的方式，再创建一个成员，启动后，在既有成员节点上执行：</p>
<pre class="crayon-plain-tag">rs.addArb("172.21.0.103:27017")</pre>
<div class="blog_h3"><span class="graybg">移除成员</span></div>
<p>可以执行命令：</p>
<pre class="crayon-plain-tag">rs.remove("172.21.0.102:27017")</pre>
<p>或者：</p>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.members.splice(2,1)
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">替换成员</span> </div>
<pre class="crayon-plain-tag">cfg = rs.conf()
cfg.members[0].host = "172.21.0.105"
rs.reconfig(cfg) </pre>
<div class="blog_h3"><span class="graybg">成员管理</span></div>
<pre class="crayon-plain-tag">cfg = rs.conf()

# 设置优先级，范围0-1000，非投票节点的优先级必须是0
cfg.members[0].priority = 0.5
# 设置优先级为0可以阻止它成为主节点
cfg.members[1].priority = 0

# 隐藏成员对客户端不可见， isMaster的输出中不包含
cfg.members[0].hidden = true

# 配置复制延迟
cfg.members[0].slaveDelay = 3600

# 禁用投票权
cfg.members[4].votes = 0

# 设置标签集
cfg.members[1].tags = { "dc": "east", "use": "reporting" }

# 必须重新配置才能生效
# 注意，下面的方法会导致主节点step down然后触发选举，step down期间MongoDB会关闭所有客户端连接
# 这可能耗时10-20秒
rs.reconfig(cfg)</pre>
<div class="blog_h3"><span class="graybg">开发测试环境</span></div>
<p>这类环境下，你可能没有足够多的机器可用，只需要在命令行指定合适的端口、数据库路径、参与的复制集名称，就可以在一台机器上部署属于多个复制集的mongod：</p>
<pre class="crayon-plain-tag">mongod --port 27010 --dbpath /data/rs0-0 --replSet rs0 --smallfiles --oplogSize 128
mongod --port 27011 --dbpath /data/rs0-1 --replSet rs0 --smallfiles --oplogSize 128
mongod --port 27012 --dbpath /data/rs0-2 --replSet rs0 --smallfiles --oplogSize 128</pre>
<p>通过MongoDB Shell连接时，指定端口即可连接到不同实例：</p>
<pre class="crayon-plain-tag">mongo --port 27010</pre>
<div class="blog_h3"><span class="graybg">异地部署环境</span></div>
<p>注意以下几点：</p>
<ol>
<li>网络安全性：保证成员之间、客户端与复制集之间流量的安全性。手段包括VPN、防火墙配置、启用MongoDB认证和授权机制</li>
<li>使用标签集实现数据中心感知 </li>
<li>最好能保证某个数据中心完全不可用时，复制集仍然能够工作</li>
<li>不要超过7个投票成员</li>
</ol>
<div class="blog_h3"><span class="graybg">成员维护流程</span></div>
<p>要对复制集成员进行维护，一般性流程如下：</p>
<ol>
<li>首先对各从节点执行维护，最后对主节点进行维护</li>
<li>对于每一个节点，执行：
<ol>
<li>以独立模式重启mongod，对于主节点要stepDown</li>
<li>在独立模式运行的mongod下执行维护任务</li>
<li>以复制集成员方式重新启动mongod </li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">修改Oplog尺寸</span></div>
<p>oplog本质上是一个定长集合，默认的尺寸通常够用。可能需要增大oplog的场景例如：执行影响大量数据的update操作。</p>
<p>如果要修改复制集的oplog，你必须轮流对每个成员进行操作：</p>
<ol>
<li>将被操作成员切换到独立运行模式：
<ol>
<li>如果该成员是主节点，一定要通过<pre class="crayon-plain-tag">rs.stepDown()</pre>将其优雅的变为从节点</li>
<li>调用<pre class="crayon-plain-tag">db.shutdownServer()</pre>关闭从节点</li>
<li>不带--replSet参数、以不同端口重新启动该成员</li>
</ol>
</li>
<li> 可选的，创建oplog的备份：<pre class="crayon-plain-tag">mongodump --db local --collection 'oplog.rs' --port 37017</pre> </li>
<li>以新尺寸创建oplog集合：<br />
<pre class="crayon-plain-tag"># 或者 db = db.getSiblingDB('local')
use local
# 创建一个临时集合，存放oplog集合的内容
db.temp.drop()
# 备份oplog，以自然反序排列（最新条目排在最前）
db.temp.save( db.oplog.rs.find( { }, { ts: 1, h: 1 } ).sort( {$natural : -1} ).limit(1).next() )

# 删除oplog
db.oplog.rs.drop()

# 重建oplog
db.runCommand( { create: "oplog.rs", capped: true, size: (2 * 1024 * 1024 * 1024) } )

# 导入临时集合中最后（最新）的oplog条目
db.oplog.rs.save( db.temp.findOne() ) </pre>
</li>
<li>以复制集成员的方式重新启动该节点</li>
</ol>
<div class="blog_h3"><span class="graybg">强制主节点</span></div>
<p>要强制某个从节点成为主节点，你可以将其优先级调为最高；类似的，优先级设置为0则可以让从节点永远不能成为主节点。</p>
<p>通过设置高优先级来强制主节点： </p>
<pre class="crayon-plain-tag"># 假设复制集成员 0 1 2 目前0是主节点，现在希望强制2为主节点
# 执行：
cfg = rs.conf()
cfg.members[0].priority = 0.5
cfg.members[1].priority = 0.5
cfg.members[2].priority = 1
rs.reconfig(cfg)
# 上面的语句调用后，会发生以下时间序列：
# 1、成员1/2和0进行同步（通常10秒内完成）
# 2、主节点发现自己的优先级不是最高，通常会stepDown
#    如果2的同步进度远远落后，则不会stepDown，等待2的optime差距在10s以后再stepDown
# 3、选举发生，2当选</pre>
<div class="blog_h3"><span class="graybg">重新同步</span></div>
<p>如果某个从节点复制进度远远落后，则oplog中尚未被该从节点应用到本地的条目可能已经被覆盖（定长集合循环覆盖），则该从节点变得Stale，必须进行完整的重新同步 —— 移除数据，重新执行初始同步。</p>
<p>执行重新同步时，应该选择一个带宽比较空闲的时机。</p>
<p>重新同步有两种方式：</p>
<ol>
<li>将Stale节点的数据目录清空，启动后会自动执行initial sync</li>
<li>从其它复制集成员拷贝数据目录，启动后可以增量同步。注意要一并拷贝local数据库的内容 </li>
</ol>
<div class="blog_h3"><span class="graybg">链式复制</span></div>
<p>启用链式复制的情况下，从节点可以从其它从节点复制，不一定非要主节点。</p>
<p>启用或者禁用链式复制：</p>
<pre class="crayon-plain-tag">cfg = rs.config()
cfg.settings.chainingAllowed = true | false
rs.reconfig(cfg)</pre>
<p>要修改某个从节点的复制源，可以：</p>
<pre class="crayon-plain-tag">rs.syncFrom("hostname&lt;:port&gt;");</pre>
<div class="blog_h3"><span class="graybg">复制集排错</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>查看状态</em></strong></p>
<p>调用<pre class="crayon-plain-tag">rs.status()</pre>可以查看复制集、复制集成员的当前状态</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>检查复制日志</em></strong></p>
<p>复制延迟是指从节点应用来自主节点的oplog的落后时间，复制延迟是复制集部署中较为严重的问题：</p>
<ol>
<li>过慢的节点难以快速的切换为主节点，为了保证数据不丢失，新主节点必须复制全部oplog（除非老主节点宕机 + 不适当的写关注，导致oplog没有传播到任何从节点）</li>
<li>数据不一致性的可能性增加 </li>
</ol>
<p>要查看复制延迟，执行<pre class="crayon-plain-tag">rs.printSlaveReplicationInfo()</pre>，输出如下：</p>
<pre class="crayon-plain-tag"># 从节点地址端口
source: 172.21.0.100:27017
	syncedTo: Thu Aug 03 2015 07:51:58 GMT+0000 (UTC)
        # 延迟时间
	0 secs (0 hrs) behind the primary 
source: 172.21.0.101:27017
	syncedTo: Thu Aug 03 2015 07:51:58 GMT+0000 (UTC)
	0 secs (0 hrs) behind the primary 

# 注意：如果主节点长期不活动，延迟从节点的延迟时间也可能小至0</pre>
<p>复制延迟的原因可能是：</p>
<ol>
<li>网络延迟，使用ping/traceroute等工具检查网络</li>
<li>磁盘吞吐量，从节点的磁盘性能可能太差，使用iostat/vmstat等工具检查</li>
<li>高并发，某些情况下，主节点上运行的长时操作可能阻塞从节点的复制，应当要求写操作得到从节点的确认，避免主节点太快而从节点跟不上</li>
<li>不适当的写关注，例如第三点的情况</li>
</ol>
</td>
</tr>
<tr>
<td>
<p><strong><em>无法选举</em></strong></p>
<p>同时重启多个从节点时，要确保可投票节点的大部分在线，否则主节点会stepDown并变为从节点，客户端会被断开连接</p>
</td>
</tr>
<tr>
<td>
<p><strong><em>查看oplog信息</em></strong></p>
<p>调用<pre class="crayon-plain-tag">rs.printReplicationInfo()</pre>可以查看oplog的尺寸和条目信息，输出如下：</p>
<pre class="crayon-plain-tag"># 配置的oplog尺寸，必须足够大，能够保证宕机时间最长的从节点启动后仍然能够跟得上
configured oplog size:   2988.1271476745605MB
log length start to end: 81850secs (22.74hrs)
oplog first event time:  Wed Aug 02 2015 09:15:58 GMT+0000 (UTC)
oplog last event time:   Thu Aug 03 2015 08:00:08 GMT+0000 (UTC)
now:                     Thu Aug 03 2015 08:00:13 GMT+0000 (UTC)</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">管理分片集群</span></div>
<div class="blog_h3"><span class="graybg">部署分片集群</span></div>
<p>首先，<em><strong>①部署配置服务复制集</strong></em>，生产环境下应该部署最少3个成员。
<p>配置文件添加分片集群角色配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rsc

sharding:
  clusterRole: configsvr</pre>
<p>创建复制集的三成员：</p>
<pre class="crayon-plain-tag">docker run --name mongo-c1 --network local --ip 172.21.1.1 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-c2 --network local --ip 172.21.1.2 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-c3 --network local --ip 172.21.1.3 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>连接到其中一个成员，进行复制集的初始化：</p>
<pre class="crayon-plain-tag">rs.initiate(
  {
    _id: "rsc",
    configsvr: true,
    members: [
      { _id : 0, host : "172.21.1.1:27017" },
      { _id : 1, host : "172.21.1.2:27017" },
      { _id : 2, host : "172.21.1.3:27017" }
    ]
  }
)</pre>
<p>一旦配置服务复制集（CSRS）创建完毕，即可<strong><em>②创建分片复制集</em></strong>，生产环境下，每个分片复制集因该部署最少3个成员。</p>
<p>第一个分片复制集的配置：</p>
<pre class="crayon-plain-tag">replication:
  replSetName: rs0
  
sharding:
  clusterRole: shardsvr</pre>
<p>创建复制集的三成员：</p>
<pre class="crayon-plain-tag">docker run --name mongo-51 --network local --ip 172.21.5.1 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-52 --network local --ip 172.21.5.2 -d  docker.gmem.cc/mongo --config /etc/mongod.conf
docker run --name mongo-53 --network local --ip 172.21.5.3 -d  docker.gmem.cc/mongo --config /etc/mongod.conf</pre>
<p>连接到其中一个成员，进行复制集的初始化：</p>
<pre class="crayon-plain-tag">rs.initiate(
  {
    _id : "rs5",
    members: [
      { _id : 0, host : "172.21.5.1:27017" },
      { _id : 1, host : "172.21.5.2:27017" },
      { _id : 2, host : "172.21.5.3:27017" }
    ]
  }
)</pre>
<p>参考以上方法，创建更多的分片复制集。 </p>
<p><em><strong>③创建一个路由器（mongos）</strong></em>并连接到配置复制集，使用如下配置文件：</p>
<pre class="crayon-plain-tag">net:
  port: 27017
  bindIp: 0.0.0.0

sharding:
  configDB: rsc/172.21.1.1:27017,172.21.1.2:27017,172.21.1.3:27017
  # 至少需要指定一个配置复制集成员的地址</pre>
<p>创建mongos：</p>
<pre class="crayon-plain-tag">docker run --name mongo-s1 --network local --ip 172.21.99.1 -d  docker.gmem.cc/mongo mongos --config /etc/mongos.conf</pre>
<p><em><strong>④连接到mongos</strong></em>：<pre class="crayon-plain-tag">mongo --host 172.21.99.1 </pre> </p>
<p><em><strong>⑤将分片复制集添加到集群</strong></em>中： </p>
<pre class="crayon-plain-tag"># 添加一个复制集
sh.addShard( "rs2/172.21.2.1:27017")
# 你也可以添加独立MongoDB实例
sh.addShard( "172.21.0.1:27017")</pre>
<p><em><strong>⑥为数据库启用分片支持</strong></em>，只有这样，该数据库中才可以有分片集合： </p>
<pre class="crayon-plain-tag">sh.enableSharding("cluster")</pre>
<p>现在，你可以查看分片集群的状态，在mongos上执行<pre class="crayon-plain-tag">sh.status()</pre>，输出如下：</p>
<pre class="crayon-plain-tag">--- Sharding Status --- 
  # 这一段显示配置数据库的基本信息
  sharding version: {
        # 配置元数据的唯一标识
	"_id" : 1,
        # 配置服务器最小的兼容版本号
	"minCompatibleVersion" : 5,
        # 当前配置元数据的版本
	"currentVersion" : 6,
        # 分片集群的唯一标识
	"clusterId" : ObjectId("5982e147eb7a3c5622dd9bc3")
}
  # 此集群中包含的分片
  shards:
        # _id 分片的唯一标识
        # host 分片的主机，如果复制集，显示所有成员的地址
        # state 分片的状态
        # tags，分片的标签集
	{  "_id" : "rs2",  "host" : "rs2/172.21.2.1:27017,172.21.2.2:27017,172.21.2.3:27017",  "state" : 1 }
	{  "_id" : "rs3",  "host" : "rs3/172.21.3.1:27017,172.21.3.2:27017,172.21.3.3:27017",  "state" : 1 }
	{  "_id" : "rs4",  "host" : "rs4/172.21.4.1:27017,172.21.4.2:27017,172.21.4.3:27017",  "state" : 1 }
	{  "_id" : "rs5",  "host" : "rs5/172.21.5.1:27017,172.21.5.2:27017,172.21.5.3:27017",  "state" : 1 }
  # 活动的mongos的版本和数量
  active mongoses:
        # 版本:数量
        "3.4.5" : 1
  # 是否启用了Chunk自动分裂
  autosplit:
        Currently enabled: yes
  # 负载均衡器的状态
  balancer:
        # 目前集群是否启用了负载均衡器
        Currently enabled: yes
        # 负载均衡器是否正在工作（迁移Chunk）
        Currently running: no
              Balancer lock taken at Thu Aug 03 2017 08:39:36 GMT+0000 (UTC) by ConfigServer:Balancer
        # 最近五次负载均衡尝试，失败的次数，如果Chunk迁移失败，则负载均衡失败
        Failed balancer rounds in last 5 attempts: 0
        # 最近24小时迁移的数量
        Migration Results for the last 24 hours: 
              No recent migrations
  # 此集群中包含的数据库
  databases:
        # _id 数据库的名称
        # primary 主分片所在
        # partitioned 该数据库是否支持分片
	{  "_id" : "cluster",  "primary" : "rs2",  "partitioned" : true }</pre>
<p>创建范围分片的集合示例：</p>
<pre class="crayon-plain-tag">sh.shardCollection("cluster.corps", { regNo : 1 }, true )</pre>
<p>再次查看分片集群状态，可以看到databases段有新增内容：</p>
<pre class="crayon-plain-tag"># 集合的名称
cluster.corps
    # 分片键: { &lt;shard key&gt; : &lt;1 or hashed&gt; }
    shard key: { "regNo" : 1 }
    # 是否对分片键应用了唯一性约束
    unique: true
    # 是否对该集合启用了负载均衡
    balancing: true
    # 块的详细信息
    chunks:
        # 下面是一个列表，以Shard名称:拥有块的数量显示
        rs2  1
    # 分片键范围对应的Shard，以及最后修改的时间
    # { &lt;shard key&gt;: &lt;min range1&gt; } --&gt;&gt; { &lt;shard key&gt; : &lt;max range1&gt; } on : &lt;shard name&gt; &lt;last modified timestamp&gt;
    { "regNo" : { "$minKey" : 1 } } --&gt;&gt; { "regNo" : { "$maxKey" : 1 } } on : rs2 Timestamp(1, 0)</pre>
<div class="blog_h3"><span class="graybg">管理配置服务器</span></div>
<p>如果配置管理服务器复制集变为只读状态（无主节点）则分片集群不支持对元数据的写操作，因而Chunk分裂、迁移无法执行。这种情况下，你应该尽快修复或者替换损坏的配置管理复制集成员。替换成员的步骤：</p>
<ol>
<li>以选项--configsvr --replSet启动新成员</li>
<li>在成员节点上，将新成员添加到复制集rs.add()</li>
<li>关闭被替换的成员</li>
<li>在成员节点上，移除被替换成员rs.remove()</li>
<li>可选的，更新mongos的--configdb选项</li>
</ol>
<div class="blog_h3"><span class="graybg">查看集群配置</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong><em>列出支持分片的数据库</em></strong></p>
<pre class="crayon-plain-tag">use config
db.databases.find( { "partitioned": true } )

# 输出如下：
{ "_id" : "cluster", "primary" : "rs2", "partitioned" : true }</pre>
</td>
</tr>
<tr>
<td>
<p><strong><em>列出所有分片</em></strong>
<pre class="crayon-plain-tag">use admin
db.runCommand( { listShards : 1 } )
# 输出如下
{
	"shards" : [
		{
			"_id" : "rs2",
			"host" : "rs2/172.21.2.1:27017,172.21.2.2:27017,172.21.2.3:27017",
			"state" : 1
		}
                ...
	],
	"ok" : 1
} </pre>
</td>
</tr>
<tr>
<td>
<p><strong><em>查看集群详细信息</em></strong>
<p>调用db.printShardingStatus()或者sh.status() </p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">硬件迁移</span></div>
<p>如果要把整个分片集群迁移到新的硬件上，可以参考以下步骤：</p>
<ol>
<li>禁用负载均衡器<pre class="crayon-plain-tag">sh.stopBalancer()</pre>，这样可以防止Chunk迁移、元数据写。如果当前正在Chunk迁移，负载均衡器会等待其终止</li>
<li>单独的迁移各个配置服务器。从3.4开始，配置服务器应以复制集的方式部署，使用 WiredTiger引擎。另外注意，此复制集必须：没有仲裁者、没有延迟成员、启用buildIndexes。具体迁移步骤：
<ol>
<li>在新硬件上开启一个配置服务实例（使用适当的选项启动mongod，例如--configsvr --replSet rsc）</li>
<li>添加到配置服务器复制集中</li>
<li>移除被替换的旧硬件成员，主节点要先StepDown</li>
<li>从复制集中移除被替换成员</li>
<li>循环上面4步，直到所有成员都迁移</li>
</ol>
</li>
<li>重新启动mongos，指向新的配置服务器</li>
<li>进行分片的迁移，一个个分片的迁移，先迁移从节点，最后主节点。具体迁移步骤：
<ol>
<li>调用shutdown命令，关闭一个成员，主节点要先StepDown</li>
<li>移动数据目录（dbPath）到新硬件</li>
<li>在新硬件上启动mongod，并连接到当前主节点</li>
<li>如果主机名/IP变化了，要执行rs.reconfig()来重新配置复制集</li>
<li>等待此节点恢复正常，调用rs.status()查看节点状态</li>
<li>循环上面5步，直到所有成员都迁移</li>
</ol>
</li>
<li>重新启用负载均衡器</li>
</ol>
<div class="blog_h3"><span class="graybg">增减分片</span></div>
<p>从分片集群中添加、删除分片，可能会导致负载再平衡（Chunk迁移）。估算一下总计的迁移数据量，抽生产环境空闲的时段执行增减。</p>
<p>增加分片的方式：</p>
<ol>
<li>配置好分片复制集</li>
<li>调用 sh.addShard()增加分片</li>
</ol>
<p>删除分片的方式：</p>
<ol>
<li>确保负载均衡器被启用，因为移除分片必然面临Chunk迁移</li>
<li>执行<pre class="crayon-plain-tag">db.adminCommand( { listShards: 1 } )</pre>来决定要移除的分片</li>
<li>在admin数据库上执行移除命令：<br />
<pre class="crayon-plain-tag">use admin
db.runCommand( { removeShard: "mongodb0" } ) </pre>
</li>
<li>验证Chunk迁移完成，再次调用上述命令，系统会报告迁移进度：<br />
<pre class="crayon-plain-tag">{
    // 正在迁移被移除节点上的Chunk
    "msg" : "draining ongoing",
    "state" : "ongoing",
    "remaining" : {
        // 剩余的Chunk数量
        "chunks" : 42,
        // 剩余的主分片位于被移除Shard的数据库数量
        "dbs" : 1
    },
    "ok" : 1
}</pre></p>
<p> 反复检查此命令，直到remaining为0</p>
</li>
<li>移动未分片集合。如果被删除分片是某个数据库的主分片，则需要转移主分片：
<ol>
<li>检查主分片分布情况<pre class="crayon-plain-tag">sh.status()</pre>：<br />
<pre class="crayon-plain-tag"># 数据库products的主分片为mongodb0（被删除分片）
{  "_id" : "products",  "partitioned" : true,  "primary" : "mongodb0" } </pre>
</li>
<li>
<p>移动主分片，执行命令：</p>
<pre class="crayon-plain-tag"># 移动数据库products的主分片到mongodb1
db.runCommand( { movePrimary: "products", to: "mongodb1" })</pre>
<p>该命令会阻塞，直到移动完成 </p>
</li>
</ol>
</li>
<li>再次执行命令以清除所有元数据：<br />
<pre class="crayon-plain-tag">use admin
db.runCommand( { removeShard: "mongodb0" } )</pre></p>
<p>理想的输出应该是：</p>
<pre class="crayon-plain-tag">{
    "msg" : "removeshard completed successfully",
    "state" : "completed",   // 删除分片成功
    "shard" : "mongodb0",
    "ok" : 1
} </pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">清除jumbo标记</span></div>
<p>如果某个Chunk超过指定的大小，或者包含的文档数量超过限制，则MongoDB将其标记为 jumbo。如果元数据变更后，jumbo不再超过限制，则MongoDB自动取消其标记。
<p>如果要手工清除标记，执行：</p>
<ol>
<li>对于可分裂（Divisible）Chunk，最好的方式是将其split，分裂成功后，MongoDB会清除jumbo标记：
<ol>
<li>通过客户端连接到mongos</li>
<li>执行<pre class="crayon-plain-tag">sh.status(true)</pre>找到jumbo，例如：<br />
<pre class="crayon-plain-tag">test.foo
     shard key: { "x" : 1 }
...
{ "x" : 2 } --&gt;&gt; { "x" : 4 } on : shard-a Timestamp(2, 2) jumbo</pre>
</li>
<li>对于上面这个分片键值范围为[2,4)，因此可以从分片键值3一分为二。分裂成功后jumbo标记自动清除：<br />
<pre class="crayon-plain-tag">sh.splitAt( "test.foo", { x: 3 }) </pre>
</li>
</ol>
</li>
<li>对于不可见（Indivisible）Chunks，某些情况下jumbo不能再次分裂，比如它仅仅包含一个分片键值，此时的清除步骤如下：
<ol>
<li>临时的停止负载均衡器</li>
<li>备份config数据库：<pre class="crayon-plain-tag">mongodump --db config --port &lt;config server port&gt; --out &lt;output file&gt;</pre> </li>
<li>连接到mongos</li>
<li>执行sh.status(true)找到不可再分的Chunk，例如：<br />
<pre class="crayon-plain-tag">{ "x" : 2 } --&gt;&gt; { "x" : 3 } on : shard-a Timestamp(2, 2) jumbo</pre></p>
<p> 可以看到，该jumbo仅仅一个分片键值2，因此无法再分裂</p>
</li>
<li>手工修改配置数据库：<br />
<pre class="crayon-plain-tag">db.getSiblingDB("config").chunks.update(
   { ns: "test.foo", min: { x: 2 }, jumbo: true },
   # 清除标记
   { $unset: { jumbo: "" } }
)</pre>
</li>
<li>重新启动负载均衡器</li>
<li>刷空元数据缓存：<pre class="crayon-plain-tag">db.adminCommand({ flushRouterConfig: 1 } )</pre> </li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">备份元数据</span></div>
<p>集群的配置数据库包含了集群的所有元数据，特别是Chunk如何映射到Shard，应该定期备份防止丢失。</p>
<p>备份步骤：</p>
<ol>
<li>停止负载均衡器</li>
<li>关闭一个配置服务器复制集的成员</li>
<li>拷贝其dbPath下所有数据文件，备份起来</li>
<li>启动关闭的配置服务器复制集成员</li>
<li>重新启用负载均衡器</li>
</ol>
<div class="blog_h2"><span class="graybg">管理脚本样例</span></div>
<div class="blog_h3"><span class="graybg">一键式集群创建脚本</span></div>
<pre class="crayon-plain-tag">#!/bin/bash

run_docker(){
type=$1
ipfx=$type
shrole=shardsvr
if [[ $type = "c" ]]; then :  
	ipfx=
	shrole=configsvr
fi 
num=$2
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
(( num++ ))
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
(( num++ ))
docker run --name mongo-$type$num --network local --ip 172.21.1.$ipfx$num -d  docker.gmem.cc/mongo --auth \
  --replSet=rs$type --$shrole --config /etc/mongod.conf
}

init_rs(){
type=$1
ipfx=$type
cfgsvr=
if [[ $type = "c" ]]; then :  
	ipfx=
	cfgsvr="configsvr: true,"
fi 
num=$2
num1=$num
(( num++ ))
num2=$num
(( num++ ))
num3=$num
read -d '' scr &lt;&lt;EOF
rs.initiate(
  {
    _id: "rs$type",
    $cfgsvr
    members: [
      { _id : 0, host : "172.21.1.$ipfx$num1:27017" },
      { _id : 1, host : "172.21.1.$ipfx$num2:27017" },
      { _id : 2, host : "172.21.1.$ipfx$num3:27017" }
    ]
  }
)
EOF
echo Prepare to execute Replica Set init script on mongo-$type$2: 
echo  "$scr"
docker exec mongo-$type$2 mongo --eval "$scr"
}

docker stop mongo-11  mongo-13  mongo-22  mongo-31  mongo-33  mongo-42  mongo-51  mongo-53  mongo-c7  \
  mongo-s1  mongo-12  mongo-21  mongo-23  mongo-32  mongo-41  mongo-43  mongo-52  mongo-c6  mongo-c8 

docker rm mongo-11  mongo-13  mongo-22  mongo-31  mongo-33  mongo-42  mongo-51  mongo-53  mongo-c7  mongo-s1  \
  mongo-12  mongo-21  mongo-23  mongo-32  mongo-41  mongo-43  mongo-52  mongo-c6  mongo-c8 

run_docker c 6
run_docker 1 1
run_docker 2 1
run_docker 3 1
run_docker 4 1
run_docker 5 1

sleep 3

init_rs c 6
init_rs 1 1
init_rs 2 1 
init_rs 3 1 
init_rs 4 1
init_rs 5 1


docker run --name mongo-s1 --network local --ip 172.21.1.1 -d  docker.gmem.cc/mongo mongos \
  --configdb=rsc/172.21.1.6:27017,172.21.1.7:27017,172.21.1.8:27017 --config /etc/mongos.conf


read -d '' scr &lt;&lt;EOF
sh.addShard( "rs1/172.21.1.11:27017")
sh.addShard( "rs2/172.21.1.21:27017")
sh.addShard( "rs3/172.21.1.31:27017")
sh.addShard( "rs4/172.21.1.41:27017")
sh.addShard( "rs5/172.21.1.51:27017")

db.getSiblingDB('admin').createUser(
 {
 user: "root",
 pwd: "root",
 roles: [ { role: "root", db: "admin" } ]
 }
)
EOF

docker exec mongo-s1 mongo --eval "$scr" </pre>
<div class="blog_h1"><span class="graybg">安全性</span></div>
<div class="blog_h2"><span class="graybg">身份验证</span></div>
<div class="blog_h3"><span class="graybg">认证机制</span></div>
<p>要改变使用的认证机制，设置mongod/mongos的参数<pre class="crayon-plain-tag">authenticationMechanisms</pre>。</p>
<p>支持的认证机制列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">机制</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SCRAM-SHA-1</td>
<td rowspan="2">3.0之前的版本，MongoDB使用MONGODB-CR作为默认的，质询/响应式的身份验证机制。之后的版本默认使用SCRAM-SHA-1 </td>
</tr>
<tr>
<td>MONGODB-CR</td>
</tr>
<tr>
<td>x.509</td>
<td>基于数字证书的身份验证，该机制支持外部验证（客户端）、内部验证（复制集/分片集群）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">X.509</span></div>
<p>这种认证机制要求使用TLS/SSL连接，X509可以实现客户端验证和内部验证。在生产环境中使用时，你需要具有单个CA签发的有效证书。可以选择自己维护CA。</p>
<p>客户端可以提供X.509证书来代替用户名/密码，进行登录验证。复制集/分片集群成员可以用X.509来代替keyfile进行相互验证。</p>
<div class="blog_h3"><span class="graybg">基于密码的外部验证</span></div>
<p>所谓外部验证，是指针对MongoDB客户端的身份验证和访问控制。</p>
<p>注意，当没有创建任何用户时，通过localhost匿名登录不受访问限制（Localhost Exception）。但是，一旦创建了任意用户，就不能再用匿名登录。你创建的第一个用户，必须具有创建新用户的权限 —— 针对admin数据库具有 userAdmin 或者userAdminAnyDatabase 权限的用户，后续使用该用户管理普通的MongoDB用户，例如创建、授权、角色管理。</p>
<p>启用步骤：</p>
<ol>
<li>不启用身份验证的情况下，启动mongod</li>
<li>通过Shell连接到mongod</li>
<li>创建管理员用户（注意，用户一旦创建，以后你就不能通过localhost匿名登录了）：<br />
<pre class="crayon-plain-tag">use admin
db.createUser(
  {
    user: "root",
    pwd: "root",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  }
)</pre>
</li>
<li>启用身份验证，重启mongod：<pre class="crayon-plain-tag">mongod --auth</pre></li>
<li>通过Shell连接，你可以：
<ol>
<li>提供身份信息连接：<pre class="crayon-plain-tag">mongo --port 27017 -u "root" -p "root" admin</pre> </li>
<li>不提供身份信息，连接到Shell后，调用 <pre class="crayon-plain-tag">db.auth("root","root")</pre>进行验证</li>
</ol>
</li>
<li>添加普通用户：<br />
<pre class="crayon-plain-tag">use cluster
db.createUser(
  {
    user: "cluster",
    pwd: "cluster",
    roles: [ { role: "readWrite", db: "cluster" },
             { role: "read", db: "reporting" } ]
  }
)</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">基于Keyfiles的内部验证</span></div>
<p>keyfiles基于SCRAM-SHA-1认证机制。keyfile的内容作为集群成员的共享密钥使用。密钥的长度在6-1024字节之间，仅仅支持base64字符，空白符被自动去除。在UNIX系统中，keyfile不得授予组、全局访问权限。</p>
<p>对于相互连接的所有mongos/mongod实例，它们必须具有相同的keyfile，否则无权加入到复制集或者连接到分片集群。keyfile通过配置项<pre class="crayon-plain-tag">security.keyFile</pre>指定。</p>
<p>为复制集启用keyfiles认证的步骤：</p>
<ol>
<li>创建keyfile，可以使用openssl生成随机密钥：<br />
<pre class="crayon-plain-tag">openssl rand -base64 756 &gt; &lt;path-to-keyfile&gt;
chmod 400 &lt;path-to-keyfile&gt;</pre>
</li>
<li>拷贝keyfile到所有复制集成员</li>
<li>关闭复制集，连接到每一个mongod，并执行：<br />
<pre class="crayon-plain-tag">use admin
db.shutdownServer()</pre>
</li>
<li>在启用访问控制的情况下启动复制集：<br />
<pre class="crayon-plain-tag">security:
  keyFile: &lt;path-to-keyfile&gt;</pre>
</li>
<li>使用Shell连接到主节点</li>
<li>创建管理员用户：<br />
<pre class="crayon-plain-tag">db.getSiblingDB('admin').createUser(
  {
    user: "root",
    pwd: "root",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
  }
) </pre>
</li>
<li>以管理员身份登录：<pre class="crayon-plain-tag">db.getSiblingDB("admin").auth("root", "root" )</pre></li>
<li>可选的，创建具有clusterAdmin角色的集群（能管理整个复制集、分片集群）管理用户：<br />
<pre class="crayon-plain-tag">db.getSiblingDB("admin").createUser(
  {
    "user" : "ca",
    "pwd" : "ca",
    roles: [ { "role" : "clusterAdmin", "db" : "admin" } ]
  }
)</pre>
</li>
</ol>
<p>为分片集群启用keyfiles认知的步骤：</p>
<ol>
<li>创建keyfile</li>
<li>拷贝keyfile到所有分片集群成员</li>
<li>禁用负载均衡器：<br />
<pre class="crayon-plain-tag">sh.stopBalancer()
# 查看负载均衡器状态，确保停止后进行下一步
sh.getBalancerState()</pre>
</li>
<li>关闭所有mongos实例，通过Shell连接到mongos，执行<br />
<pre class="crayon-plain-tag">db.getSiblingDB("admin").shutdownServer()</pre>
</li>
<li>关闭所有配置服务器实例，类似步骤4</li>
<li>关闭所有分片复制集的所有成员，类似步骤4</li>
<li>修改所有mongos、mongod的配置文件，启用security.keyFile选项</li>
<li>启动分片，可选的，创建分片本地管理员用户。通过Shell连接到分片复制集的主节点进行创建</li>
<li>启动mongos，通过localhost匿名登录，创建管理员，至少需要userAdminAnyDatabase角色</li>
<li>以管理员身份登录mongos，创建具有clusterAdmin角色的集群管理用户</li>
<li>使用集群管理用户登录</li>
<li>启动负载均衡器 </li>
</ol>
<div class="blog_h2"><span class="graybg">访问控制</span></div>
<p>MongoDB使用基于角色的访问控制（RBAC）。一个用户被授予1-N个角色，这些角色决定了用户能够对数据库进行哪些操作。</p>
<div class="blog_h3"><span class="graybg">启用访问控制</span></div>
<p>在配置文件中启用<pre class="crayon-plain-tag">security.authorization</pre>设置，或者使用<pre class="crayon-plain-tag">--auth</pre>命令行选项，即可启用访问控制。</p>
<div class="blog_h3"><span class="graybg">角色</span></div>
<p>所谓角色，即特权（privileges）的组合，每个特权可以针对某个资源进行某种操作。角色可以存在继承层次，子角色继承父角色的所有特权。</p>
<p>要查看角色具有哪些特权，执行：</p>
<pre class="crayon-plain-tag">db.runCommand({
  rolesInfo: { role: &lt;name&gt;, db: &lt;db&gt; },
  # 设置为true则显示角色的特权
  showPrivileges: &lt;Boolean&gt;,
  # 如果设置为true，则db.runCommand({rolesInfo:1})的输出包含内部角色
  showBuiltinRoles: &lt;Boolean&gt;
})</pre>
<p>内置角色列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">角色</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2">
<p><strong><em>数据库用户角色</em></strong></p>
<p>每个数据库都具有以下两个角色</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>read</td>
<td>
<p>读取所有非系统集合、system.indexes、system.js、system.namespaces上的数据</p>
</td>
</tr>
<tr>
<td>readWrite</td>
<td>读写所有非系统集合、system.js上的数据</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>数据库管理角色<br /></em></strong></p>
<p>每个数据库都具有以下角色</p>
</td>
</tr>
<tr>
<td>dbAdmin</td>
<td>执行Schema相关的管理，管理索引，收集统计信息</td>
</tr>
<tr>
<td>dbOwner</td>
<td>readWrite, dbAdmin,userAdmin的组合</td>
</tr>
<tr>
<td>userAdmin</td>
<td>管理数据库上的角色和用户</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>集群管理角色</em></strong></p>
<p>admin数据库具有以下角色</p>
</td>
</tr>
<tr>
<td>clusterAdmin</td>
<td>对集群有很宽泛的管理权，合并clusterManager, clusterMonitor,hostManager，且支持dropDatabase操作</td>
</tr>
<tr>
<td>clusterManager</td>
<td>对集群进行管理监控，可以访问config、local数据库</td>
</tr>
<tr>
<td>clusterMonitor</td>
<td>只读权限</td>
</tr>
<tr>
<td>hostManager</td>
<td>监控和管理服务器</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>备份还原角色</em></strong></p>
<p>admin数据库具有以下角色</p>
</td>
</tr>
<tr>
<td>backup</td>
<td>支持数据库备份操作</td>
</tr>
<tr>
<td>restore</td>
<td>支持数据库还原操作</td>
</tr>
<tr>
<td colspan="2">
<p><strong><em>全数据库角色</em></strong></p>
<p>admin数据库具有以下角色，自动应用到除了local、config之外的所有数据库</p>
</td>
</tr>
<tr>
<td>readAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行读操作</td>
</tr>
<tr>
<td>readWriteAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行读写操作</td>
</tr>
<tr>
<td>userAdminAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）进行用户管理操作</td>
</tr>
<tr>
<td>dbAdminAnyDatabase</td>
<td>对除了local、config之外的所有数据库（包括集群中）具有类似于dbAdmin的特权</td>
</tr>
<tr>
<td colspan="2"><strong><em>超级用户角色</em></strong></td>
</tr>
<tr>
<td>root</td>
<td>readWriteAnyDatabase, dbAdminAnyDatabase, userAdminAnyDatabase,clusterAdmin, restore,backup的组合</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">特权</span></div>
<p>特权即： 资源和针对该资源可以进行的操作。资源可以是：数据库、集合、一系列集合、集群。</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">复制集</span></div>
<div class="blog_h3"><span class="graybg">not master and slaveOk=false</span></div>
<p>默认情况下，复制集仅仅允许在主节点上执行读操作，要允许在从节点上进行读，在主节点上执行：<pre class="crayon-plain-tag">rs.slaveOk()</pre></p>
<div class="blog_h3"><span class="graybg">增加Shell中it返回的数据量</span></div>
<p>在MongoDB Shell中执行命令：<pre class="crayon-plain-tag">DBQuery.shellBatchSize = 100</pre>   </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/mongodb-study-note">MongoDB学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/mongodb-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Redis学习笔记</title>
		<link>https://blog.gmem.cc/redis-study-note</link>
		<comments>https://blog.gmem.cc/redis-study-note#comments</comments>
		<pubDate>Fri, 27 Mar 2015 12:43:13 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Database]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[Redis]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14710</guid>
		<description><![CDATA[<p>简介 Redis（REmote DIctionary Server）是一个开源的、基于内存的数据结构存储。可以作为数据库、缓存、消息代理使用。它支持字符串、散列、列表、集合、有序集合等多种数据结构，并提供范围查询。它支持位图、hyperloglogs（一种用于解决count-distinct问题的算法，可以估算Bag中的distinct元素的近似个数）、地理空间索引，并支持径向查询。Redis内置了复制（replication）机制，支持Lua脚本、LRU清除算法。Redis支持事务，以及不同级别的磁盘持久化。基于Redis Sentinel和Redis Cluster的自动分区（automatic partitioning）机制，它能够提供HA保障。 Redis可以认为是NoSQL数据库的一种，它是目前最流行的键值对存储引擎。这类存储允许你基于键来保存数据，在之后，你必须知道键才能取回数据。 几乎所有的主流语言都有Redis的客户端。 对比Memcached Redis使用场景 由于Redis更加新，特性更加丰富，相比Memcached它通常总是正确的选择。 Redis的最根本优势在于数据结构的支持。它支持长达250MB的键、值大小，支持字符串、哈希、列表、有序集合、集合等数据类型；Memcached仅仅支持250字节的键，值仅仅支持字符串。 Redis作为缓存使用时，数据清除算法更加丰富，相比之下Memcached仅支持LRU、随机清除。Redis提供主动清除（生存期）、被动（延迟）清除，Memcached仅支持被动清除。 Memcached使用场景 Memcached更加适合缓存相对小的、静态数据，例如HTML代码片段。这是因为Memcached的内部内存管理不像Redis那样精巧，在元数据方面消耗较少的内存。但是，如果数据尺寸是动态的，Memcached的上述优势很快消失，因为它存在内存碎片问题。 Scaling是选择Memcached的另外一个场景，因为它是多线程的。你可以很容易的Scaling up来使用更多的计算资源。Redis则基本是单线程的，你需要通过集群来水平Scale，比Memcached复杂。 安装 Ubuntu 参考以下步骤进行安装： [crayon-69d58cd0edf03585399636/] 容器化 <a class="read-more" href="https://blog.gmem.cc/redis-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/redis-study-note">Redis学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Redis（REmote DIctionary Server）是一个开源的、基于内存的数据结构存储。可以作为数据库、缓存、消息代理使用。它支持字符串、散列、列表、集合、有序集合等多种数据结构，并提供范围查询。它支持位图、hyperloglogs（一种用于解决count-distinct问题的算法，可以估算Bag中的distinct元素的近似个数）、地理空间索引，并支持径向查询。Redis内置了复制（replication）机制，支持Lua脚本、LRU清除算法。Redis支持事务，以及不同级别的磁盘持久化。基于Redis Sentinel和Redis Cluster的自动分区（automatic partitioning）机制，它能够提供HA保障。</p>
<p>Redis可以认为是NoSQL数据库的一种，它是目前最流行的键值对存储引擎。这类存储允许你基于键来保存数据，在之后，你必须知道键才能取回数据。</p>
<p>几乎所有的主流语言都有Redis的客户端。</p>
<div class="blog_h2"><span class="graybg">对比Memcached</span></div>
<div class="blog_h3"><span class="graybg">Redis使用场景</span></div>
<p>由于Redis更加新，特性更加丰富，相比Memcached它通常总是正确的选择。</p>
<p>Redis的<span style="background-color: #c0c0c0;">最根本优势在于数据结构的支持</span>。它支持长达250MB的键、值大小，支持字符串、哈希、列表、有序集合、集合等数据类型；Memcached仅仅支持250字节的键，值仅仅支持字符串。</p>
<p>Redis作为缓存使用时，数据清除算法更加丰富，相比之下Memcached仅支持LRU、随机清除。Redis提供主动清除（生存期）、被动（延迟）清除，Memcached仅支持被动清除。</p>
<div class="blog_h3"><span class="graybg">Memcached使用场景</span></div>
<p>Memcached更加<span style="background-color: #c0c0c0;">适合缓存相对小的、静态数据</span>，例如HTML代码片段。这是因为Memcached的内部内存管理不像Redis那样精巧，在元数据方面消耗较少的内存。但是，如果数据尺寸是动态的，Memcached的上述优势很快消失，因为它存在内存碎片问题。</p>
<p>Scaling是选择Memcached的另外一个场景，因为它是多线程的。你可以很容易的Scaling up来使用更多的计算资源。Redis则基本是单线程的，你需要通过集群来水平Scale，比Memcached复杂。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<p>参考以下步骤进行安装：</p>
<pre class="crayon-plain-tag"># 安装Redis服务器
sudo apt-get install redis-server
# 查看版本
redis-server --version
# 禁用服务自动启动
sudo update-rc.d redis-server disable</pre>
<div class="blog_h2"><span class="graybg">容器化</span></div>
<p>使用下面的命令可以运行基于Docker的Redis：</p>
<pre class="crayon-plain-tag"># 拉取镜像
docker pull redis:3

# 在后台运行Redis容器
docker run --name redis -d redis

# 启用持久化存储，存储目录默认/data
docker run 
    -v ~/Docker/volumes/redis/data:/data -v ~/Docker/volumes/redis/conf:/conf 
    -p 6379:6379 --name redis -d redis redis-server /conf/redis.conf</pre>
<p>要定制Redis配置文件，可以扩展镜像： </p>
<pre class="crayon-plain-tag">FROM redis:3

COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]</pre>
<div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">通信协议</span></div>
<p>Redis是一个TCP服务器，使用请求/应答通信模型。这意味着一个请求通常按如下步骤完成：</p>
<ol>
<li>客户端发送一个请求给服务器，通常以阻塞的方式等待读套接字，以获取服务器响应</li>
<li>服务器处理命令，并把结果发送给客户端</li>
</ol>
<p>客户端/服务器基于网络连接，这个网络可能很快——例如环回网卡，也可能很慢。从客户端发送请求，到接收到服务器响应的这段时间，称为RTT（Round Trip Time）。</p>
<div class="blog_h3"><span class="graybg">管道</span></div>
<p>Redis支持在客户端尚未读取旧的响应之前就处理新的请求，客户端可以连续发送多个命令给服务器，而<span style="background-color: #c0c0c0;">在最后一起处理所有应答</span>。Redis API提供了管道相关的接口</p>
<div class="blog_h2"><span class="graybg">发布/订阅</span></div>
<p>Redis支持这种交互模型，并提供了相关的命令。使用这种模型，消息发送者不需要显式的发消息发送给特定的接受者，而仅仅需要把消息发布到频道（Channel）上，从而实现解耦。</p>
<p>订阅了频道的客户端，不应该发送不相关的命令，仅可以发送：SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING, QUIT这几个命令。</p>
<div class="blog_h3"><span class="graybg">推送消息格式</span></div>
<p>每个推送给订阅客户端的消息，是三元素的数组。第一个元素是消息的类型，值可以是：</p>
<ol>
<li>subscribe，表示成功订阅了通道。目标通道名称作为第二个元素，第三个元素是当前订阅的通道数</li>
<li>unsubscribe，表示成功取消了订阅。目标通道名称作为第二个元素，第三个元素是当前订阅的通道数</li>
<li>message，表示接收到其它客户端PUBLISH的消息。第二个元素是消息来自的通道，第三个元素是消息载荷</li>
</ol>
<div class="blog_h3"><span class="graybg">关于键空间</span></div>
<p>发布/订阅与Redis数据库、键空间没有任何关系，你在数据库0上发布，客户端可以在数据库10上订阅。</p>
<div class="blog_h3"><span class="graybg">基于模式匹配的订阅</span></div>
<p>Redis支持基于通配符的订阅，示例：</p>
<pre class="crayon-plain-tag">PSUBSCRIBE news.*
PSUBSCRIBE f*</pre>
<div class="blog_h2"><span class="graybg">内存优化</span></div>
<div class="blog_h3"><span class="graybg">容器类型的特殊编码</span></div>
<p>从2.2开始，Redis优化了很多数据类型，以占用更少的内存空间。仅仅由整数构成的哈希、集合、列表，以及有序集合，在编码后占用内存大小可能小十倍。从用户和API的角度来说，这一编码是完全透明的。 </p>
<p>特殊编码是一种CPU消耗 - 内存占用的权衡。Redis提供一些参数，来调整编码行为：</p>
<pre class="crayon-plain-tag"># 进行压缩的容器，其元素个数的限制
hash-max-zipmap-entries 512  
# 进行压缩的集合，其元素长度的限制
hash-max-zipmap-value 64 
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512</pre>
<p>当目标容器元素个数或者元素长度超过限制，则自动使用正常编码。</p>
<div class="blog_h3"><span class="graybg">使用32位实例</span></div>
<p>由于指针长度短，32位的Redis使用更少的内存，但是缺点是，它最多使用4GB的内存。对于32/64位Redis，RDB、AOF文件的格式是兼容的。</p>
<div class="blog_h3"><span class="graybg">位与字节操作</span></div>
<p>从Redis 2.2引入的GETRANGE, SETRANGE, GETBIT,SETBIT命令，允许你将字符串作为随机访问数组看待。前面两个命令可以按字节操作，后面两个命令更是按位操作。合理使用这些命令可以减少内存消耗。</p>
<div class="blog_h3"><span class="graybg">尽可能使用哈希</span></div>
<p>小的哈希被Redis很好的编码，很节约空间，因此你可以尽可能考虑以哈希的方式存储数据，而不是单独存储每个字段。</p>
<div class="blog_h3"><span class="graybg">内存分配</span></div>
<p>为了存储用户的键值，Redis可以分配<span style="background-color: #c0c0c0;">不超过maxmemory设置</span>的内存。</p>
<p>关于Redis管理内存的方式，你需要知道：</p>
<ol>
<li>当键被移除后，Redis<span style="background-color: #c0c0c0;">不总会把内存归还给OS</span>。你需要<span style="background-color: #c0c0c0;">根据内存需要量的峰值</span>来规划配置</li>
<li>尽管Redis可能不归还内存，但是<span style="background-color: #c0c0c0;">新插入的键会智能的使用这些未归还、但是已经空闲的内存</span></li>
<li>碎片率（ fragmentation ratio）在当前内存用量远小于峰值用量时，可能不准确。<span style="background-color: #c0c0c0;">碎片率 = 当前内存用量 / RSS</span>。RSS即驻留工作集尺寸——占据物理内存的尺寸</li>
</ol>
<p>如果你不设置maxmemory参数，Redis会尽可能多的占用内存，从而影响OS性能。</p>
<div class="blog_h2"><span class="graybg">作为LRU缓存</span></div>
<p>当把Redis作为缓存使用时，让其自动的清除陈旧数据，通常符合缓存需求。</p>
<p>LRU是最常见的一种数据清除算法，Redis也支持其它算法。</p>
<div class="blog_h3"><span class="graybg">限制最大内存</span></div>
<p>使用配置指令可以限制Redis使用内存的峰值：</p>
<pre class="crayon-plain-tag">maxmemory 100mb</pre>
<p>如果设置为0，表示无限制。这是64位系统的默认值，32位系统隐含限制3GB。</p>
<div class="blog_h3"><span class="graybg">清除策略</span></div>
<p>当Redis到达最大内存限制后，可以依据设置的策略（policies）来决定如何处理 —— 例如返回错误，或者清除旧数据。支持的策略如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>noeviction</td>
<td>在内存不足的时候，返回错误</td>
</tr>
<tr>
<td>allkeys-lru</td>
<td>针对所有键执行LRU算法，最久未被使用的键被移除</td>
</tr>
<tr>
<td>volatile-lru</td>
<td>针对设置了超时的键执行LRU算法</td>
</tr>
<tr>
<td>allkeys-random</td>
<td>针对所有键进行随机清除</td>
</tr>
<tr>
<td>volatile-random</td>
<td>针对设置了超时的键进行随机清除</td>
</tr>
<tr>
<td>volatile-ttl</td>
<td>尝试移除TTL最短（过期时间最近）的键</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">LFU算法</span></div>
<p>从4.0版本开始，Redis支持新的LFU（ Least Frequently Used）算法，在某些情况下该算法工作的更好。该算法会尝试跟踪每个键被使用的频率，使用频率最少的键会被清除。LRU算法可能会保留最近访问了一次，但是实际上基本不会被访问的键，LFU不存在此问题。</p>
<div class="blog_h2"><span class="graybg">Redis事务</span></div>
<p>Redis支持把多个命令分为一组，然后作为单个事务来执行。</p>
<p>MULTI, EXEC, DISCARD,WATCH这几个命令是Redis事务控制的基础。通过组合使用这些命令，你可以单步执行多个命令，并确保：</p>
<ol>
<li>事务中<span style="background-color: #c0c0c0;">所有命令被顺序的执行</span>。从效果上说，<span style="background-color: #c0c0c0;">不会发生其他客户端发起的命令，插入上述事务中间执行的情况</span>。所有命令就像是单个操作似的</li>
<li><span style="background-color: #c0c0c0;">要么所有命令执行，要么都不执行</span>。EXEC命令触发事务中所有命令的执行。当使用AOF文件时，Redis确保使用单个write(2)系统调用把事务操作写入磁盘，除非Redis崩溃或者被强制杀死 —— 可能仅有部分命令的结果被写入磁盘。如果发生这种意外，在Redis下一次启动时，它会提示错误并退出。你需要使用<pre class="crayon-plain-tag">redis-check-aof</pre>来修复AOF文件，把不完整执行的事务移除，然后再启动Redis</li>
</ol>
<p><span style="background-color: #c0c0c0;">从2.2开始，Redis为事务提供额外保证 —— 使用乐观锁</span>。</p>
<div class="blog_h3"><span class="graybg">用法</span></div>
<p>Redis事务从MULTI命令开始，该命令的应答总是OK。发起该命令后，用户可以继续发起多个命令。后续的命令不会立即执行，而是<span style="background-color: #c0c0c0;">排队，直到你调用EXEC</span>命令。</p>
<p>如果调用DISCARD而不是EXEC，会清除命令队列并且关闭事务。</p>
<div class="blog_h3"><span class="graybg">事务中的错误</span></div>
<p>在事务中，你可能遇到两类命令错误：</p>
<ol>
<li>命令可能无法进入队列，原因例如命令格式错误，或者出现极端情况，例如内存不够</li>
<li>命令可以在EXEC调用之后失败</li>
</ol>
<p>对于第一类错误，用户可以检查入队命令的返回值，如果显示QUEUED意味着入队成功。否则意味着入队失败，这种情况下通常需要DISCARD事务。从2.6.5开始，只要存在入队失败，EXEC一定会返回一个错误并自动DISCARD事务。</p>
<p>对于第二类错误，没有特殊的处理。如果<span style="background-color: #c0c0c0;">某个命令执行失败，其它任务仍然会继续执行</span>。</p>
<div class="blog_h3"><span class="graybg">不支持回滚</span></div>
<p>前面我们提到过，事务过程中某条命令执行失败，并不会中断后续命令的执行，或者回滚之前已经执行的命令，这和关系型数据库很不一样。Redis的这种行为的原因是：</p>
<ol>
<li>Redis<span style="background-color: #c0c0c0;">命令仅仅会在语法错误、键持有不匹配的数据类型的情况下，才会失败</span>。这意味着<span style="background-color: #c0c0c0;">失败通常由于编程错误</span>，应该在开发阶段就发现</li>
<li>由于不去支持回滚，这让Redis更简单、更快</li>
</ol>
<div class="blog_h3"><span class="graybg">乐观锁</span></div>
<p><span style="background-color: #c0c0c0;">WATCH命令</span>提供了一种检查并设置（CAS，check-and-set），为Redis事务提供<span style="background-color: #c0c0c0;">乐观锁</span>。</p>
<p>被监控（WATCHed）的键的修改会被发现，如果存在一个或者更多的键，在EXEC之前修改了，则整个事务被中止，EXEC命令返回一个Null Reply。</p>
<p>WATCH命令可以调用多次，其监控的时间范围是，从调用的那一刻起，到EXEC被调用时为止。</p>
<p>调用UNWATCH，可以撤销之前所有WATCH命令。</p>
<div class="blog_h2"><span class="graybg">持久化</span></div>
<p>Redis提供了两种持久化模式：<span style="background-color: #c0c0c0;">RDB、AOF。这两种模式可以同时启用</span>，你也可以完全禁用持久化。当同时启用时，在Redis启动时会读取AOF文件，因为它更能保证数据的完整性。</p>
<p>Redis官方建议同时使用两种持久化模式，并且在远期未来将其合并为一种。</p>
<p>RDB相关的配置项：save；AOF相关的配置项：appendonly。</p>
<div class="blog_h3"><span class="graybg">RDB模式</span></div>
<p>该模式在磁盘上保存<span style="background-color: #c0c0c0;">某个瞬间的数据集快照</span>，可以定期的保存。Redis异步的把数据写到磁盘上，形成.rdb文件。该模式对于很多应用程序足够好用。</p>
<p>RDB的优势：</p>
<ol>
<li>作为Redis的瞬时快照，RDB文件<span style="background-color: #c0c0c0;">格式紧凑，很适合作备份用途</span>，例如远程灾备</li>
<li>RDB<span style="background-color: #c0c0c0;">性能较好</span>，因为Redis主线程仅仅需要Fork出子线程负责磁盘I/O，<span style="background-color: #c0c0c0;">主线程基本不需要执行磁盘I/O</span></li>
<li>如果数据集很大，RDB的启动速度高于AOF</li>
</ol>
<p>RDB的劣势：</p>
<ol>
<li>如果Redis进程崩溃或者电力中断，你可能<span style="background-color: #c0c0c0;">丢失数分钟的数据，具体时间范围取决于你如何配置保存点（Save point）</span></li>
</ol>
<div class="blog_h3"><span class="graybg">AOF模式</span></div>
<p>另一种持久化模式是仅附加文件（Append Only File），它实际上是服务器<span style="background-color: #c0c0c0;">接收到的每个写命令的日志</span>，且命令日志的格式与Redis协议相同。当服务器重新启动时，该日志可以被<span style="background-color: #c0c0c0;">回放，以还原先前的数据集</span>。</p>
<p>该模式提供了好得多的durability：</p>
<ol>
<li>使用<span style="background-color: #c0c0c0;">默认的数据fsync策略</span>你最多在断电这种<span style="background-color: #c0c0c0;">极端情况下丢失1秒的数据</span></li>
<li><span style="background-color: #c0c0c0;">Redis进程本身出现问题（但OS正常）时，最多丢失最后一次写操作</span></li>
</ol>
<p>AOF的额外优势：</p>
<ol>
<li>AOF日志仅仅需要进行追加操作，因而不需要Seek。AOF不存在文件损坏的问题，即使日志结尾是写入一半的命令，也可以用 redis-check-aof进行修复</li>
<li>AOF文件变大后，Redis可以在后台进行重写。这个重写操作是完全安全的，因为重写时Redis还会在旧文件上追加，如果重写失败，旧文件仍然可用</li>
<li>AOF日志格式容易解析</li>
</ol>
<p>AOF的劣势：</p>
<ol>
<li><span style="background-color: #c0c0c0;">相同数据集下AOF比RDB大</span></li>
<li><span style="background-color: #c0c0c0;">基于使用的fsync策略，AOF可能比RDB慢</span>。禁止fsync在高并发情况下，性能与RDB一样快，如果<span style="background-color: #c0c0c0;">每秒fsync一次，也还算很快</span></li>
</ol>
<div class="blog_h2"><span class="graybg">主从复制</span></div>
<p>Redis中进行主从复制的配置很简单，关于主从复制，你需要知道：</p>
<ol>
<li>Redis使用<span style="background-color: #c0c0c0;">异步的主从复制</span>，从2.8开始，Slave定期的确认（acknowledge）从复制流中处理的数据量</li>
<li>一个Master可以拥有<span style="background-color: #c0c0c0;">多个Slave</span></li>
<li>Slave允许来自其它Slave的连接。并形成<span style="background-color: #c0c0c0;">Master - Slave - Slave的树形结构</span></li>
<li>在Master节点，<span style="background-color: #c0c0c0;">复制是非阻塞的</span>。当一个或者多个Slave执行初始同步时，Master仍然接受查询请求</li>
<li>在Slave节点，复制也是非阻塞的。当Slave执行初始同步时，它仍然能够（如果你进行适当的配置）使用原来的数据集对外提供查询服务</li>
<li>复制可以提供扩容性，可以<span style="background-color: #c0c0c0;">把只读的缓慢查询分配给Slave执行</span></li>
<li>可以<span style="background-color: #c0c0c0;">完全避免Master的磁盘I/O开销</span>，你可以配置一个Slave，并启用AOF。这种技巧需要注意处理Master宕机，因为重启后它的数据集为空，不应该作为Master</li>
</ol>
<div class="blog_h3"><span class="graybg">不持久化Master的安全性</span></div>
<p>上面我们提到过，可以处于性能的考虑禁用Master的持久化。Redis不建议这样，<span style="background-color: #c0c0c0;">如果禁用Master的持久化，一定要同时禁止Redis开机启动</span>。</p>
<div class="blog_h3"><span class="graybg">主从复制工作方式</span></div>
<p>当你配置好一个Slave后，在连接建立后它发送一个<span style="background-color: #c0c0c0;">PSYNC命令</span>。如果这次连接属于<span style="background-color: #c0c0c0;">“重新连接”并且Master的backlog足够大，则Master把增量数据集</span>发送给Slave。否则，触发一次<span style="background-color: #c0c0c0;">完全同步（full resynchronization ）</span>。</p>
<p><span style="background-color: #c0c0c0;">完全同步触发后，Master在后台启动一个保存线程，产生RDB文件</span>。与此同时，<span style="background-color: #c0c0c0;">Master对新的写命令进行缓存</span>。RDB文件准备好之后，发送给Slave加载到内存，然后写命令缓存也被发送给Slave进行回放。</p>
<p>当与Master的<span style="background-color: #c0c0c0;">连接断掉后，Slave会自动进行重连</span>。</p>
<p>如果<span style="background-color: #c0c0c0;">多个Slave需要同步，Master只会产生一个RDB保存进程</span>。</p>
<div class="blog_h2"><span class="graybg">批量插入</span></div>
<p>某些场景下你需要在短时间内完成大量数据的插入，例如添加百万个新的Redis键。本节内容介绍如何尽快的完成数据的插入。</p>
<p>使用普通的Redis客户端执行海量数据插入通常不是好主意，因为：</p>
<ol>
<li>最简单的方式：一个一个的发送命令，大量时间浪费在了RRT上</li>
<li>使用管道（Pipelining）可以缓解上一条，但是，它限制了在最后一起处理响应。在大量数据插入的场景下，最好能一边插入新条目，一边处理旧条目的响应</li>
</ol>
<p>仅仅少量的客户端支持非阻塞I/O，而且，并非所有客户度能够高效的解析响应以最大化吞吐量。因此，在Redis中完成海量数据的最好方式是，生成包含原始数据、Redis协议的文本文件，然后通过Redis客户端的Pipe模式发送给服务器处理。</p>
<div class="blog_h2"><span class="graybg">分区</span></div>
<p>所谓<span style="background-color: #c0c0c0;">分区（Partitioning）</span>是指把你的数据分散到多个Redis实例的处理过程，<span style="background-color: #c0c0c0;">每个实例仅持有键空间的子集</span>。分区的意义在于：</p>
<ol>
<li>通过利用多台计算机，突破单机内存限制，支持更大的数据集</li>
<li>把计算能力Scale到多台计算机的多个CPU；把网络带宽Scale到多台计算机的多个网络接口</li>
</ol>
<div class="blog_h3"><span class="graybg">分区方式</span></div>
<p>最简单的是范围分区（range partitioning），它要求键的格式为<pre class="crayon-plain-tag">key-name:id</pre>。它还需要一张表来记录id范围和实例的映射关系。在实际中很少使用这种方式</p>
<p>另外一种是哈希分区（hash partitioning），它通过计算键的散列值来决定其应当由哪个实例持有，对键格式没有要求，也不需要额外的表。</p>
<div class="blog_h3"><span class="graybg">实现方式</span></div>
<p>分区的职责可以划分给软件栈中的不同组件：</p>
<ol>
<li>客户端分区：客户端直接决定该把键发送给哪个节点处理。很多Redis客户端实现了此功能</li>
<li>代理辅助分区：客户端把请求发送给一个理解Redis协议的代理，此代理负责转发请求给适当的实例。Redis实现了这种方式，Memcached的Twemproxy类似</li>
<li>查询路由：客户端随机的把请求发送给一个实例。由该实例将其转发给正确的实例。<span style="background-color: #c0c0c0;">Redis集群利用客户端的辅助，实现了混合形式的查询路由 —— 请求不在节点间转发</span>，<span style="background-color: #c0c0c0;">而是客户端被重定向到正确的节点</span>，并由客户端直接发送请求给正确的节点</li>
</ol>
<div class="blog_h3"><span class="graybg">分区的缺点</span></div>
<p>Redis分区的某些特性做的不是很好：</p>
<ol>
<li><span style="background-color: #c0c0c0;">牵涉到多个键的操作常常不被支持</span>。例如，你不能（直接）对两个分布在不同实例上的两个集合进行交叉操作</li>
<li>牵涉到<span style="background-color: #c0c0c0;">多个键的事务无法支持</span></li>
<li>分区的粒度是键，<span style="background-color: #c0c0c0;">无法对一个键下的巨大数据集进行分区</span></li>
<li>使用分区时，数据的处理更加复杂。例如需要处理多个RDB/AOF文件。在备份时，你需要从多个机器上收集持久化文件</li>
<li>增减容量可能比较复杂。<span style="background-color: #c0c0c0;">Redis集群支持几乎透明的在运行时添加/移除节点</span>，实现数据的Rebalance。<span style="background-color: #c0c0c0;">其它分区实现，例如客户端分区、代理辅助分区则不支持这种Rebalance</span>，需要使用Pre-sharding技术</li>
</ol>
<div class="blog_h3"><span class="graybg">数据存储还是缓存</span></div>
<p>尽管从概念上说，Redis分区用在数据存储还是缓存场景下没有什么区别。但是，用作数据存储时，有一个重要的限制——<span style="background-color: #c0c0c0;">一个键必须总是映射到相同的Redis实例</span>。</p>
<p>一致性哈希（Consistent hashing）实现通常能够在某个键的最优节点不可用时，自动切换到其它节点。类似的，当添加新节点后，一部分新的键可以自动分配到新节点上。</p>
<p>当<span style="background-color: #c0c0c0;">作为缓存使用时，Redis可以基于一致性哈希很容易的Scale up/down</span>。</p>
<p>当<span style="background-color: #c0c0c0;">用作数据存储时，需要基于固定的键-节点映射表，节点的数量必须是固定的，不能改变</span> —— 否则必须在增减节点时实现Rebalance，当前只有Redis Cluster支持这种Rebalance。</p>
<div class="blog_h3"><span class="graybg">Presharding</span></div>
<p>前面我们提到过，作为数据存储的分区，要添加/删除节点并不容易。但是，随着时间的推移，数据存储肯定是要不断变化的，今天需要10个节点就满足需要，明天可能需要增加到15个。</p>
<p>要解决这个问题，第一个方案是，<span style="background-color: #c0c0c0;">从开始就准备足够多（32或者64个满足绝大部分场景）的实例</span>。可以这样做的原因是Redis实例本身占用很少的资源，它仅仅需要1MB的内存。计算在数据很少的情况下，你也可以在单台机器上使用这种分区方式。当数据增多，单台机器计算资源不够时，可以增加一台服务器，并把一半的实例迁移到新的机器上。利用Redis的复制机制（Replication），可以在免宕机/最小化宕机时间的前提下，实现这种迁移：</p>
<ol>
<li>在新服务器上启动新实例</li>
<li>把这些实例<span style="background-color: #c0c0c0;">作为需要迁移的旧实例的Slave</span></li>
<li>停止客户端</li>
<li>更新迁移实例的IP地址配置</li>
<li>在新服务器上，对实例发送<pre class="crayon-plain-tag">SLAVEOF NO ONE</pre>命令</li>
<li>基于新的配置启动客户端</li>
<li>关闭旧服务器上不再需要的那些实例</li>
</ol>
<div class="blog_h3"><span class="graybg">Redis集群</span></div>
<p>从2015年4月开始，Redis Cluster已经可以胜任生产环境了，<span style="background-color: #c0c0c0;">Redis Cluster的实现方式类似于查询路由、客户端分区的混合</span>。</p>
<p>Redis Cluster是最优的分区方案，因为它可以自动分区并提供高可用性。一旦基于你熟悉编程语言的Redis Cluster客户端可用，Redis集群将作为分区实现的标准。</p>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<p>当多个进程需要互斥的操作同一共享资源时，分布式锁是一种有用的原语。</p>
<p>很多第三方库实现了可以配合Redis的分布式锁管理器（Distributed Lock Manager，DLM），它们的实现方式各有不同。Redis推荐基于红锁（Redlock）算法的实现，主流语言有<a href="https://redis.io/topics/distlock">实现</a>。</p>
<div class="blog_h3"><span class="graybg">安全性和活动性保证</span></div>
<p>Redlock提供以下最小化保证：</p>
<ol>
<li>安全性保证：互斥性，在任意时刻，仅一个客户端能够持有锁</li>
<li>活动性A：不会发生死锁，请求者最终一定会获得锁，甚至是在持有锁的客户端崩溃、分区发生的情况下</li>
<li>活动性B：容错，只要大部分Redis节点可用，客户端总是能获得、释放锁</li>
</ol>
<div class="blog_h2"><span class="graybg">键空间通知</span></div>
<p>这是2.8.0开始引入的功能，它允许客户端订阅特定的频道，并在影响了Redis数据集的事件发生时，获得通知。这些事件例如：</p>
<ol>
<li>针对某个特定键的命令被执行</li>
<li>针对所有键的LPUSH命令被执行</li>
<li>数据库0中的任意键过期</li>
</ol>
<p>事件通过普通的Redis Pub/Sub机制完成推送。但是，由于Pub/Sub没有提供持久化机制（因为它是fire and forget的），如果你的应用程序需要可靠的事件通知（reliable notification），键空间通知目前是不满足需求的。</p>
<div class="blog_h2"><span class="graybg">辅助索引</span></div>
<p>由于值可以是结构化的，Redis并不是严格意义上的键-值存储。由于值可以是结构化的，因此支持不同类别的辅助索引（secondary indexes），包括组合（多列）索引就很有意义了。</p>
<p>Redis支持创建以下类型的索引：</p>
<ol>
<li>对于有序集合，可以根据ID或者其它数字字段创建索引</li>
<li>基于词法范围（lexicographical ranges）的有序集合，可以创建更加复杂的辅助索引，组合索引，或者图遍历（graph traversal ）索引</li>
<li>对于集合，可以创建随机索引</li>
<li>对于列表，可以创建简单的迭代索引以及最后N条目的索引</li>
</ol>
<p>索引的实现和维护，是Redis服务器的高级主题。对于大部分使用复杂查询的用户来说，应该考虑是否利用关系型数据库更加合适。</p>
<div class="blog_h3"><span class="graybg">有序集合的简单数字索引</span></div>
<p>在Redis中<span style="background-color: #c0c0c0;">使用辅助索引的最简单方式是，使用有序集合</span> —— 基于浮点数Score来排序元素的数据结构。使用有序集合索引的两个基础命令是ZADD、ZRANGEBYSCORE，前者添加元素，后者根据Score进行范围扫描。</p>
<p>举例来说，你可以根据年龄来索引一系列的用户名：</p>
<pre class="crayon-plain-tag"># 添加元素
ZADD myindex 18 Anna
ZADD myindex 35 Jon
ZADD myindex 67 Helen

# 索引范围查找
ZRANGEBYSCORE myindex 20 40</pre>
<p>通常情况下，用户（User） 实体包含多个字段，而不仅仅是名字，这种情况下，可以在有序集合中存储用户的ID：</p>
<pre class="crayon-plain-tag"># 有序集合中存储实体标识符
ZADD user.age.index 38 1
ZADD user.age.index 42 2

# 实体以哈希方式另外存放
HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42</pre>
<p>这样，可以针对多个字段，分别建立索引。</p>
<p>ZADD还可以用来更新索引值，例如一年后，用户的年龄需要增加1（当然使用出身日期更简单），这时候，可以：</p>
<pre class="crayon-plain-tag"># 设置实体哈希的年龄字段
HSET user:1 age 39
# 更新索引
ZADD user.age.index 39 1

# 注意，上面的两个操作，可以使用MULTI/EXEC事务确保原子性</pre>
<div class="blog_h3"><span class="graybg">字典序索引</span></div>
<p>有序集合具有一个重要特性，当两个元素的Score相同时，它们将根据元素值进行字典排序（底层调用memcmp函数）。Redis的这种索引的内部<span style="background-color: #c0c0c0;">实现方式和性能类似于关系型数据库的B树索引</span>。使用字典序索引时，常常把Scoure一律设置为0。</p>
<p>使用 <a href="#zrangbylex">ZRANGEBYLEX</a>、 ZLEXCOUNT 命令，可以使用字典序索引。</p>
<p><strong>自动完成的例子</strong></p>
<p> 字典序索引的一个常见应用是，表单的快速自动完成提示。当用户键入bit时，可以在后端执行以下Redis命令：</p>
<pre class="crayon-plain-tag"># \xff表示最后一个字节的最大值可以是255
ZRANGEBYLEX myindex "[bit" "[bit\xff"</pre>
<p>如果需要对自动提示根据使用频率进行排序，我们可以扩展元素的值：</p>
<pre class="crayon-plain-tag">ZREM myindex 0 banana:1
# banana第二次被搜索的时候，设置频率为2
ZADD myindex 0 banana:2</pre>
<p>这里用冒号来分隔自动完成关键字和搜索频率，由于字典序范围搜索在Redis中是二进制安全的，所以你可以使用任何分隔符，例如\0\0。</p>
<p><strong>添加辅助信息</strong></p>
<p>使用上述的分隔机制，我们可以在字典元素值中附加任意的内容，以满足应用需要。<span style="background-color: #c0c0c0;">总之记住，字典序比较是以前缀为基准的</span>。</p>
<div class="blog_h3"><span class="graybg">复合索引</span></div>
<p>前面我们介绍了索引单个字段的方式，那么，能不能像关系型数据库那样，索引多个字段呢。</p>
<p>考虑一个场景，我们需要对一个巨大仓库中的产品进行查询，依据是房间号、价格。这依赖于这两个字段的复合索引，实现起来其实很简单，关键之处还是字典序的前缀机制。我们需要<span style="background-color: #c0c0c0;">对数字类型的索引字段进行前缀补齐</span>，<span style="background-color: #c0c0c0;">确保它们的长度总是一致</span>，这样才能正确的进行字典序排序：</p>
<pre class="crayon-plain-tag"># room:price:product_id
# 索引字段必须补齐
# 辅助信息产品ID不需要补齐 

ZADD myindex 0 0056:0028.44:90
ZADD myindex 0 0034:0011.00:832

# 查询56号房间中，价格在10-30之间的产品
ZRANGEBYLEX myindex [0056:0010.00 [0056:0030.00</pre>
<div class="blog_h1"><span class="graybg">集群</span></div>
<p>Redis集群（Cluster）提供了自动的把数据分区（Shard）到多个Redis节点（实例）的机制。</p>
<p>此外，集群也提供了某种程度的高可用性，当<span style="background-color: #c0c0c0;">部分节点不可达的情况下，集群仍然可以运行</span>。但是某些极端情况下，例如<span style="background-color: #c0c0c0;">大多数Master不可达，则不行</span>。</p>
<p>Redis集群中的节点需要两个端口，一个用于<span style="background-color: #c0c0c0;">服务客户端，默认6379</span>，另外一个端口比此端口<span style="background-color: #c0c0c0;">大10000，用于集群总线</span>。集群总线使用二进制协议，进行节点-节点之间的通信，用于<span style="background-color: #c0c0c0;">故障检测（failure detection）、配置更新、故障转移授权等操作</span>。</p>
<p>注意Redis集群与Docker的端口映射不兼容（也不兼容一般性的NAT环境、IP或者端口被重映射的环境），你可能需要使用host networking mode模式。</p>
<div class="blog_h2"><span class="graybg">集群基础</span></div>
<div class="blog_h3"><span class="graybg">数据分片</span></div>
<p>Redis集群不使用一致性哈希（consistent hashing），而是基于所谓Hash Slot进行分片，从概念上说，每个键都是Hash Slot的一部分。</p>
<p>集群中一共有16384个Hash Slot，<span style="background-color: #c0c0c0;">计算键所属的Slot时，仅需要获得键的CRC16，然后针对16384进行取模操作</span>。</p>
<p>集群中的每个节点负责所有Hash Slot的一个子集，当<span style="background-color: #c0c0c0;">增减节点时，可以很方便的重新分配Hash Slot</span>。把Hash Slot从一个节点转移到另外一个，不会引入downtime。</p>
<p><span style="background-color: #c0c0c0;">如果所有键都属于同一个Hash  Slot，Redis集群支持针对这些键的multiple key操作</span>。你可以<span style="background-color: #c0c0c0;">使用Hash Tag强制一批键归属于同一个Slot</span>。</p>
<div class="blog_h3"><span class="graybg">集群的主从模型</span></div>
<p>为了避免因为Master子集故障，或者无法连接到大部分的Master节点导致的集群不可用，Redis使用主从节点模型，<span style="background-color: #c0c0c0;">每个Hash Slot具有N份复制，其中一份位于Master节点，N-1份位于N-1个Slave节点上</span>。当Master节点失败后，集群会自动推举它的一个Slave成为新的Master。</p>
<div class="blog_h3"><span class="graybg">一致性保证</span></div>
<p>Redis集群<span style="background-color: #c0c0c0;">不保证强一致性</span>，这意味着某些情况下，服务器已经向客户端确认的写操作，其数据可能丢失。</p>
<p>导致写丢失的一个原因是Redis使用异步复制，考虑以下场景：</p>
<ol>
<li>客户端向Master B发送写操作</li>
<li>Master B向客户端应答OK</li>
<li>Master B向它的Slave  B1 B2 B3传播此写操作</li>
</ol>
<p>可以看到，在<span style="background-color: #c0c0c0;">同步到Slave之前，Master已经应答了客户端OK</span>，Redis这样做是出于性能考虑。如果在第2、3步之间Master B宕机，Bx晋升为Master，则写操作就永远的丢失了。</p>
<p>上述场景和那些配置为每秒执行Flush操作的数据库类似，也就是说你以前可能也面临这种数据丢失的风险。此外，你可以在<span style="background-color: #c0c0c0;">应答客户端之前强制Redis Flush数据，这可能会影响性能，但是可以改善一致性</span>。</p>
<p>Redis集群可以提供同步写操作，如果你的确需要。<span style="background-color: #c0c0c0;">使用WAIT命令，丢失写的可能性大大减小</span>。注意，即使使用WAIT也不能保证强一致性，特殊的情况下，一个不能接收来自Master的写入操作的Slave可能晋升为Master。</p>
<div class="blog_h2"><span class="graybg">创建并使用集群</span></div>
<div class="blog_h3"><span class="graybg">手工建立</span></div>
<p>首先，你需要若干个<span style="background-color: #c0c0c0;">运行在集群模式</span>的Redis实例。节点的最小化配置文件示例如下：</p>
<pre class="crayon-plain-tag">port 7000
cluster-enabled yes
# 节点的集群配置文件，集群内部使用，不应该手工编辑
cluster-require-full-coverage no
cluster-node-timeout 15000
cluster-config-file nodes.conf
cluster-migration-barrier 1
appendonly yes</pre>
<p>节点的数量<span style="background-color: #c0c0c0;">至少需要3个Maste</span>r。本节我们使用6个节点，其余三个为Slave。依次分配端口7000 - 7005。 你可以依据端口分别创建目录，在目录中存放上面的配置文件和Redis服务器二进制文件。然后，启动Redis服务器，你就拥有了6个Redis实例。</p>
<p>在节点第一次启动时，nodes.conf尚不存在，redis会自动初始化此文件，并得到自己的唯一标识（Node  ID）：</p>
<pre class="crayon-plain-tag">[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1</pre>
<p>要建立集群，可以使用Redis提供的命令行工具redis-trib，该工具包含在Redis 的源码中，依赖于：</p>
<pre class="crayon-plain-tag">gem install redis</pre>
<p>执行下面的命令即可建立集群：</p>
<pre class="crayon-plain-tag"># create表示建立集群
# --replicas 指定每个Master的Slave数量
# 后面的参数为每个节点的地址和端口
./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005</pre>
<p>上述命令执行后，如果一起正常，屏幕上最终会打印： </p>
<pre class="crayon-plain-tag">[OK] All 16384 slots covered</pre>
<div class="blog_h3"><span class="graybg">检查状态</span></div>
<p>你可以随时执行下面的命令，判断集群状态是否正常：</p>
<pre class="crayon-plain-tag">redis-trib.rb check  redis-node-ip:6379
# 理想输出
# [OK] All nodes agree about slots configuration.
# &gt;&gt;&gt; Check for open slots...
# &gt;&gt;&gt; Check slots coverage...
# [OK] All 16384 slots covered.</pre>
<div class="blog_h3"><span class="graybg">使用create-cluster脚本</span></div>
<p>如果你不想如上面那样，手工的配置、执行每个Redis实例， 可以使用Redis的utils目录下的create-cluster，这是一个Bash脚本，在底层它还是调用redis-trib完成集群创建的。示例：</p>
<pre class="crayon-plain-tag">create-cluster create
create-cluster start
create-cluster stop</pre>
<div class="blog_h2"><span class="graybg">重分区（Resharding）</span></div>
<p><span style="background-color: #c0c0c0;">重分区就是把一些Hash Slots从一批节点转移到另外一批节点的过程</span>，我们同样需要使用redis-trib完成此工作：</p>
<pre class="crayon-plain-tag"># 只需要提供一个集群节点，其它节点会自动找到
./redis-trib.rb reshard 127.0.0.1:7000
# 回答问题，需要转移多少个Hash Slots
How many slots do you want to move (from 1 to 16384)?
# 根据提示，提供接收这些Slots的节点的ID
# 最后，可以检查集群的健康状态
./redis-trib.rb check 127.0.0.1:7000

# 非交互式的：
./redis-trib.rb reshard 
    --from &lt;node-id&gt; --to &lt;node-id&gt; --slots &lt;number of slots&gt; 
    --yes &lt;host&gt;:&lt;port&gt;</pre>
<div class="blog_h2"><span class="graybg">手动故障转移</span></div>
<p>某些时候，在Master没有宕机的时候，你就需要进行“故障转移”，例如Master机器需要进行硬件维护。<span style="background-color: #c0c0c0;">手工故障转移时，数据安全性比被迫故障转移要高</span>，数据不会在处理过程中丢失。</p>
<p>要实现手工的故障转移，需要使用命令<pre class="crayon-plain-tag">CLUSTER FAILOVER</pre>，注意必须<span style="background-color: #c0c0c0;">在被转移的Master的某个Slave上执行该命令</span>。 </p>
<div class="blog_h2"><span class="graybg">增减节点</span></div>
<div class="blog_h3"><span class="graybg">添加节点</span></div>
<p>向集群中添加节点，基本上就是建立一个Redis实例，然后：</p>
<ol>
<li><span style="background-color: #c0c0c0;">分配一些Slot给它，这种情况下新节点作为Master</span></li>
<li>作为一个<span style="background-color: #c0c0c0;">已知节点的Replica，这种情况下新节点作为Slave</span></li>
</ol>
<p>添加节点命令：</p>
<pre class="crayon-plain-tag"># 添加一个新节点 7006到集群
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000</pre>
<p>新添加的节点，与其它Master的不同之处：</p>
<ol>
<li>由于没有分配Slots，<span style="background-color: #c0c0c0;">因此它不持有数据</span></li>
<li>由于没有分配Slots，<span style="background-color: #c0c0c0;">它不参与Slave晋升的选举处理 </span></li>
</ol>
<p>通过重分区，可以为新节点分配Slots。</p>
<p>如果要作为Slave添加，可以参考如下脚本：</p>
<pre class="crayon-plain-tag">./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000</pre>
<p>另外，你也可以使用CLUSTER REPLICATE命令完成上面的操作。 </p>
<div class="blog_h3"><span class="graybg">移除节点</span></div>
<p>从集群中移除节点，可以执行命令：<pre class="crayon-plain-tag">./redis-trib del-node 127.0.0.1:7000 `&lt;node-id&gt;`</pre>  </p>
<p>如果要<span style="background-color: #c0c0c0;">移除的是Master节点，它必须是空的</span>，如果不是，你需要Reshard它的数据到其它节点。</p>
<div class="blog_h2"><span class="graybg">Slave迁移</span></div>
<p>Redis集群支持重新配置Slave节点，让它<span style="background-color: #c0c0c0;">成为其它Master的Slave</span>。你只需要在Slave上执行：</p>
<pre class="crayon-plain-tag">CLUSTER REPLICATE &lt;master-node-id&gt;</pre>
<p>另外，Redis集群支持<span style="background-color: #c0c0c0;">自动化的重新配置Slave节点以增强集群的可用性</span>，不需要管理员手工介入。这种自动化的重新配置称为replicas migration。</p>
<div class="blog_h1"><span class="graybg">Sentinel</span></div>
<p>Redis Sentinel为Redis提供高可用性（HA），利用Sentinel，你可以创建无人值守的高可用系统，自动处理某些故障。此外，Sentinel还负责一些附属任务，包括监控、通知，并作为客户端的配置提供者（configuration provider）：</p>
<ol>
<li>监控：不断的检查你的Master、Slave是否正常工作</li>
<li>通知：可以通过API通知系统管理员或者其他应用程序，某个Redis实例出现问题</li>
<li>自动故障转移：如果一个Master不能正常工作，Sentinel启动一个故障转移，把Master的某个Slave提升为新Master，并让其兄弟Slave重新配置为新Master的Slave。此外，使用Redis的客户端可以得到Master变更的通知</li>
<li>配置提供者：可以作为客户端服务发现的源，客户端连接到Sentinel，后者提供某个服务的Master地址，故障转移发生后，Sentinel会报告新的地址</li>
</ol>
<p>Sentinel是一个分布式的系统，其本身设计为<span style="background-color: #c0c0c0;">多个Sentinel进程相互协作的运作模式</span>。多个Sentinel进程协作模式的优势：</p>
<ol>
<li>当多个Sentinel进程一致认为某个Master不可用，则启动故障检测。这可以降低误报（ false positives）的几率</li>
<li>如果部分Sentinel进程不可用，Sentinel作为一个整体仍然能够正常工作，这增强了系统的健壮性</li>
</ol>
<p>当前版本为Sentinel 2，比起上个版本使用了更强大、简单的预测算法（predict algorithms），作为Redis 2.8+的一部分发布。</p>
<div class="blog_h2"><span class="graybg">快速起步</span></div>
<div class="blog_h3"><span class="graybg">运行Sentinel</span></div>
<p>启动启用了Sentinel的Redis服务器，有两种方式：</p>
<pre class="crayon-plain-tag">redis-sentinel /path/to/sentinel.conf
# 或者
redis-server /path/to/sentinel.conf --sentinel</pre>
<p>提供Sentinel配置文件是必须的，因为配置文件会被用来保存系统的状态，如果不提供配置文件则无法启动。<span style="background-color: #c0c0c0;">Sentinel默认监听26379端口</span>。</p>
<div class="blog_h3"><span class="graybg">重要的事情</span></div>
<p>在部署Sentinel之前，你需要知道一些重要的事情：</p>
<ol>
<li>你需要至少3个Sentinel实例，以确保健壮性</li>
<li>这三个Sentinel相互独立，不会同时发生故障。例如将它们部署在不同的物理服务器上，或者独立的几个虚拟机上</li>
<li>Sentinel + Redis的分布式系统，不能保证已经确认的写操作在故障发生后不丢失，这是由于Redis异步复制机制导致的。但是，可以使用更安全的方式部署Sentinel，是数据丢失的窗口更小</li>
<li>客户端必须支持Sentinel，流行的客户端大多支持</li>
<li>注意测试，甚至是在生产环境下</li>
<li>Sentinel、Docker或者其它形式的NAT/端口映射机制需要小心的共存。Docker的<span style="background-color: #c0c0c0;">端口重映射破坏了Sentinel自动发现其它Sentinel、发现Master的Slave列表的能力</span></li>
</ol>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>最小化的配置示例：</p>
<pre class="crayon-plain-tag"># 指定每个需要监控的Master，为每个Master提供一个唯一性的名称
# 不需要指定Slave，因为可以自动发现。自动发现后，该配置文件会被Sentinel自动更小以反映Slave信息
# 每当故障转移发生的时候，该配置文件也会被自动更新

# 第一组Redis实例
# 定义Master组的选项：
# sentinel monitor &lt;master-group-name&gt; &lt;ip&gt; &lt;port&gt; &lt;quorum&gt;
# quorum的含义是，如果认定当前Master不可达，需要几个Sentinel进程同意
# quorum仅仅用于检测故障，要执行故障转移，某个Sentinel进程需要被推举为故障转移的Leader然后被授权执行故障转移
# 推举是由Sentinel进程内部进行的，如果大部分Sentinel同意推举则OK，这显然要求大部分Sentinel进程的相互可达
sentinel monitor mymaster 127.0.0.1 6379 2
# 其它选项的格式都是：
# sentinel &lt;option_name&gt; &lt;master_name&gt; &lt;option_value&gt;
# 至少多少mm不可达，Sentinel才开始考虑目标Master是否宕机
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
# 在新的Master产生后，同时与之重新同步的兄弟Slave的最大数量。数字越低，故障转移消耗的时间越多
# 但是，如果Slave被配置为，继续使用旧数据对外服务，你可能不希望所有兄弟Slave同时重新同步
sentinel parallel-syncs mymaster 1

# 第二组Redis实例
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5</pre>
<p>所有配置项在运行时，都可以基于<pre class="crayon-plain-tag">SENTINEL SET</pre>命令来配置。 </p>
<div class="blog_h1"><span class="graybg"><a id="config"></a>配置</span></div>
<p>如果处于测试和开发的目的，可以零配置的启动Redis服务器。但是在生产环境下，你通常需要自定义配置。</p>
<p>提供自定义配置，可以编写配置文件redis.conf。该文件由一系列的指令组成，每个指令的格式如下：</p>
<pre class="crayon-plain-tag">keyword argument1 argument2 ... argumentN
# 示例：
slaveof 127.0.0.1 6380
# 如果某个参数包含空格，可以使用引号：
requirepass "hello world"</pre>
<p>除了提供配置文件以外，还可以直接从命令行传递配置项：</p>
<pre class="crayon-plain-tag"># 格式与配置文件一样，只是每个配置项都具有 -- 前导标记
./redis-server --port 6380 --slaveof 127.0.0.1 6379</pre>
<p>要指定Redis使用的配置文件，可以这样启动服务：</p>
<pre class="crayon-plain-tag">./redis-server /path/to/redis.conf</pre>
<div class="blog_h2"><span class="graybg">运行时修改配置</span></div>
<p>Redis支持运行时按需修改配置，而不需要重启服务器。Redis提供在运行时读取、写入配置的命令：<pre class="crayon-plain-tag">CONFIG GET</pre>、  <pre class="crayon-plain-tag">CONFIG SET</pre>，大部分指令支持基于这两个命令进行读写。</p>
<p>需要注意，CONFIG SET命令对服务器配置的修改不会持久化，因此服务重启后配置信息即丢失。从2.8开始，你可以调用 <pre class="crayon-plain-tag">CONFIG REWRITE</pre>自动更新redis.conf，使其反映当前运行时的配置信息。</p>
<div class="blog_h2"><span class="graybg">配置为缓存</span></div>
<p>将Redis作为缓存服务使用时，你可以配置：</p>
<pre class="crayon-plain-tag">maxmemory 2mb
maxmemory-policy allkeys-lru</pre>
<p>这样，你不需要为键设置过期信息，所有键均会基于LRU算法清除。 </p>
<div class="blog_h2"><span class="graybg">常用配置项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3" colspan="2"><strong><em>基本配置</em></strong></td>
</tr>
<tr>
<td>include path-to-file</td>
<td>包含其它的配置文件 </td>
</tr>
<tr>
<td>protected-mode</td>
<td>保护模式，如果on则仅仅本地连接可以访问</td>
</tr>
<tr>
<td>daemonize  bool</td>
<td>
<p>默认情况下Redis不是以守护程序的形式运行的</p>
<p>配置为daemonize yes则作为守护程序运行，Redis会在/var/run/redis.pid写PID文件</p>
</td>
</tr>
<tr>
<td>pidfile path-to-file</td>
<td>定制PID文件的位置</td>
</tr>
<tr>
<td>port  number</td>
<td>监听端口，默认6379。如果设置为0则不监听TCP端口</td>
</tr>
<tr>
<td>tcp-backlog number</td>
<td>TCP连接建立请求最大排队数</td>
</tr>
<tr>
<td>bind ipaddr [ipaddr...]</td>
<td>监听的IP地址，默认0.0.0.0</td>
</tr>
<tr>
<td>timeout number</td>
<td>客户端空闲多久后，断开连接，默认0表示不断开</td>
</tr>
<tr>
<td>tcp-keepalive number</td>
<td>如果不为0，启用TCP保活。默认0，如果启用，建议的值可以是60，表示每60秒使用SO_KEEPALIVE发送TCP ACKs</td>
</tr>
<tr>
<td>loglevel  level</td>
<td>设置服务器日志级别，默认notice。依据日志冗长程度，可选debug、verbose、notice、warning</td>
</tr>
<tr>
<td>logfile path-to-file</td>
<td>日志文件的名称</td>
</tr>
<tr>
<td>syslog-enabled bool</td>
<td>是否记录到操作系统日志</td>
</tr>
<tr>
<td>syslog-ident identity</td>
<td>系统日志标识符，默认redis</td>
</tr>
<tr>
<td>syslog-facility</td>
<td>可选值USER，或者LOCAL0-LOCAL7。默认local0</td>
</tr>
<tr>
<td>databases number</td>
<td>指定可以使用的数据库数量， 默认16。每个连接都可以选择不同的数据库</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><strong><em>快照相关配置</em></strong></td>
</tr>
<tr>
<td>save delay keynum</td>
<td>
<p>启用RDB快照。在delay秒内，如果有keynum或者更多的键被修改，执行持久化</p>
<p>该指令可以出现多次，可以使用<pre class="crayon-plain-tag">save ""</pre>取消前面所有save指令</p>
</td>
</tr>
<tr>
<td>stop-writes-on-bgsave-error bool</td>
<td>
<p>默认取值yes，意味着，如果启用了RDB快照（至少一个savepoint），并且上一次发起的后台保存操作失败，则不再接受写入命令。加入后台保存线程恢复工作，则Redis会自动允许保存</p>
<p>设置为no，则在保存失败的情况下继续支持写操作</p>
</td>
</tr>
<tr>
<td>rdbcompression bool</td>
<td>
<p>是否在dmup出.rdb文件时，启用压缩。默认yes</p>
</td>
</tr>
<tr>
<td>rdbchecksum bool</td>
<td>从版本5的RDB格式开始，文件尾部放置校验和。这可以检测文件破坏，但是会在保存、加载RDB时，损失10%左右的性能。默认yes</td>
</tr>
<tr>
<td>dbfilename filename</td>
<td>Dump出RDB时，使用的文件名，默认dump.rdb</td>
</tr>
<tr>
<td>dir path-to-dir</td>
<td>
<p>Dump出RDB时的工作目录，默认./</p>
<p>AOF也存放在此目录</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>主从复制相关配置</strong></em></td>
</tr>
<tr>
<td>slaveof  masterip masterport </td>
<td>设置当前实例为某个实例的Slave</td>
</tr>
<tr>
<td>masterauth master-password</td>
<td>如果Master启用了密码保护（基于requirepass配置项），则这里需要提供此密码</td>
</tr>
<tr>
<td>slave-serve-stale-data bool</td>
<td>
<p>当Slave失去到Master的网络连接，或者复制操作正在进行中，该选项可以设置为：</p>
<ol>
<li>yes（默认值）：Slave仍然响应客户端请求，但是客户端获得的数据可能是陈旧的</li>
<li>no：lave会拒绝执行INFO、SLAVEOF之外的所有命令，并回复错误SYNC with master in progress</li>
</ol>
</td>
</tr>
<tr>
<td>slave-read-only bool</td>
<td>
<p>设置Slave是否是只读的，从2.6开始默认yes</p>
<p>允许Slave可写，可以用它存放一些生存期短暂的数据。因为每次和Slave重新同步时，这些数据都被清除 </p>
</td>
</tr>
<tr>
<td>repl-diskless-sync bool</td>
<td>
<p>是否启用无盘复制，默认no</p>
<p>对于新的或者重新连接到Master的Slave，不能进行增量同步，必须首先进行完整同步——把RDB文件传输给Slave。传输有两种方式：</p>
<ol>
<li>磁盘后备（ Disk-backed）的：Master创建一个新的进程，把RDB文件写到磁盘上。稍后该文件被父进程增量的传递给Slave。使用该方式时，多个Slave可以排队，只要RDB写入完毕，就可以一起接受RDB传输</li>
<li>无盘的：<span style="background-color: #c0c0c0;">Master创建一个新进程，直接把RDB写到Slave的套接字</span>。这种情况下，一旦RDB传输开始，后续到达的Slave需要排队，等RDB传输结束后，在一起接受传输。为了增大并行量，可以配置一个等待时间，超过此时间后，下一次RDB传输才会触发</li>
</ol>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>repl-diskless-sync-delay sec</td>
<td>
<p>上面提到的无盘同步方式，产生传输RDB的子进程的延迟，默认5秒</p>
</td>
</tr>
<tr>
<td>repl-ping-slave-period sec</td>
<td>Slave会定期发送PING给Master，默认10秒</td>
</tr>
<tr>
<td>repl-timeout sec</td>
<td>
<p>主从复制的超时时间，超时用于：</p>
<ol>
<li>从Slave的角度，同步时的块I/O传输</li>
<li>从Slave的角度，Master的超时（数据、ping）</li>
<li>从Master的角度，Slave的超时（REPLCONF ACK pings）</li>
</ol>
<p>该配置的值必须比repl-ping-slave-period大，否则总是会超时</p>
</td>
</tr>
<tr>
<td>repl-disable-tcp-nodelay bool</td>
<td>
<p>同步完成后，是否在Slave套接字上禁用TCP_NODELAY，默认否</p>
<p>在禁用TCP_NODELAY的情况下，Redis会使用更小的TCP数据包、更小的带宽峰值来发送数据到Slave，但是会导致数据延迟。在Linux内核默认配置下，Slave可能在40ms后才看到Master上的数据</p>
</td>
</tr>
<tr>
<td>repl-backlog-size number</td>
<td>
<p>主从复制排队（Backlog）大小，默认值1mb。Backlog是一个数据缓冲区，当Slave断开并重连后，如果使用此缓冲区就足以进行增量同步，则不进行完整同步</p>
<p>仅当至少一个Slave连接到本Master的情况下，才分配此缓冲区</p>
<p><span style="background-color: #c0c0c0;">在很多环境下这个默认值显得太小，可能需要调整到100MB</span></p>
</td>
</tr>
<tr>
<td>repl-backlog-ttl  sec</td>
<td>默认600，表示当最后一个Slave断开到Master的连接之后，再过多少秒，清空Backlog</td>
</tr>
<tr>
<td>slave-priority number</td>
<td>Slave优先级，默认100。数值越低，有有资格晋升为Master。数值0表示永远不能晋升</td>
</tr>
<tr>
<td>min-slaves-to-write number</td>
<td>如果连接的Slave的数量少于指定的数字，Master拒绝写入操作，默认0</td>
</tr>
<tr>
<td>min-slaves-max-lag sec</td>
<td>如果最慢的Slave，其延迟超过指定的秒数，Master拒绝写入操作，默认10</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>安全性配置</strong></em></td>
</tr>
<tr>
<td>requirepass passwd</td>
<td>要求客户端在发起其它命令之前，先发起AUTH进行身份认证</td>
</tr>
<tr>
<td>
<p>rename-command cmd newname</p>
</td>
<td>
<p>可以执行命令的重命名，从而避免客户端调用某些重要命令</p>
<p>newname为""则禁用</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>资源限制配置</strong></em></td>
</tr>
<tr>
<td>maxclients num</td>
<td>同时连接的最大客户端数，默认10000</td>
</tr>
<tr>
<td>maxmemory num</td>
<td>
<p>允许Redis使用的最大内存，如果超过此限制，Redis会根据清除策略来移除键</p>
<p>如果清除策略为noeviction，则那些可能导致内存占用增加的命令，都会收到错误</p>
</td>
</tr>
<tr>
<td>maxmemory-policy policy</td>
<td>
<p>当最大内存限制到达时，Redis选择什么键执行移除：<br />volatile-lru、allkeys-lru、volatile-random、allkeys-random、volatile-ttl、noeviction</p>
<p>默认noeviction</p>
</td>
</tr>
<tr>
<td>maxmemory-samples</td>
<td>为了节省资源，Redis使用的LRU和最小化TTL算法是近似算法。该配置项用于微调，数字越大，CPU消耗越大，但是算法越精确。默认值5，较为适合生产环境，如果取值10则足够精确，但是较慢，如果取值3则足够快，但是不怎么精确</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>APPEND ONLY 配置</strong></em></td>
</tr>
<tr>
<td>appendonly bool</td>
<td>是否启用AOF模式的持久化，默认no</td>
</tr>
<tr>
<td>appendfilename  filename </td>
<td>AOF文件的名字，默认appendonly.aof</td>
</tr>
<tr>
<td>appendfsync when</td>
<td>
<p>设置Redis调用fsync的频率。fsync调用导致操作系统立即同步写操作到磁盘，而不是在OS缓存中等待更多的输出。可选值：</p>
<ol>
<li>no，不主动调用fsync()，由操作系统决定何时flush数据，速度最快</li>
<li>everysec，每秒钟调用一次</li>
<li>always，每次写入Append log后都调用，速度最慢，但是安全性最高</li>
</ol>
</td>
</tr>
<tr>
<td>no-appendfsync-on-rewrite bool</td>
<td>
<p>当AOF策略设置为everysec或者always时，如果一个后台保存线程（一个后台保存或者AOF日志后台重写线程）正在执行很多磁盘IO操作，此时Redis主线程调用fsync可能（在某些Linux配置下）阻塞过长时间，此现象目前无法避免</p>
<p>为了缓和此问题，可以设置此选项的值为yes（默认值no），当BGSAVE、BGREWRITEAOF正在进行时，防止主线程调用fsync</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>auto-aof-rewrite-percentage  100</td>
<td rowspan="2">
<p>配置自动触发的AOF重写，通过重写，AOF文件的尺寸可以得到优化。AOF重写会隐含的触发BGREWRITEAOF命令</p>
<p>当AOF日志文件增长了一定比例（以上一次重写后AOF文件，如果启动后从来没有进行过重写，则以启动时的AOF文件为基准）后，可以触发重写。第一个配置项设置此比例，设置为0则禁用自动的AOF重写</p>
<p>当AOF文件小于某个尺寸时，可以总不触发重写，第二个配置项设置此尺寸</p>
</td>
</tr>
<tr>
<td>auto-aof-rewrite-min-size  64mb</td>
</tr>
<tr>
<td>aof-load-truncated yes</td>
<td>
<p>当操作系统崩溃后，特别是挂在Ext4文件系统时没有指定data=ordered选项的情况下，你可能发现在Redis启动时，AOF文件被截断了</p>
<p>AOF截断的情况发生后，Redis要么：</p>
<ol>
<li>退出 ，提示错误。用户需要使用redis-check-aof来修复AOF文件</li>
<li>尽可能多的加载数据，这是默认行为</li>
</ol>
<p>该配置项就是控制这个行为的</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>集群配置</strong></em></td>
</tr>
<tr>
<td>cluster-enabled yes</td>
<td>是否启用集群，如果设置为yes则以集群节点的方式启动当前实例</td>
</tr>
<tr>
<td>cluster-config-file name</td>
<td>
<p>集群的每个节点具有一个集群配置文件。该文件通常不应该手工编辑，它由节点自己创建和更新</p>
<p>该配置用于手工指定集群配置文件的名称，注意，集群中每个节点必须拥有唯一性名称 </p>
</td>
</tr>
<tr>
<td>cluster-node-timeout 15000 </td>
<td>在认为一个节点进入失败状态（Failure state）之前，它必须处于不可达（unreachable）状态的毫秒数</td>
</tr>
<tr>
<td>cluster-slave-validity-factor 10 </td>
<td>
<p>如果一个Slave的数据貌似过于陈旧，它不会在Master宕机后作为故障转移的目标。陈旧的判断依据：</p>
<ol>
<li>如果有多个候选进行故障转移的Slave，它们会相互通信，<span style="background-color: #c0c0c0;">看谁具有最佳的复制偏移量</span>（replication offset），这样，偏移量最大的Slave数据最新</li>
<li>每个Slave计算与<span style="background-color: #c0c0c0;">Master的最后交互时间</span>——例如最后一个PING或者命令；如果Master已经断开，则计算断开至今的时间。如果最后交互时间过于久远，则此Slave绝不进行故障转移</li>
</ol>
<p>上述第二条的“久远”的阈值，计算公式为：</p>
<p style="padding-left: 30px;">(node-timeout * slave-validity-factor) + repl-ping-slave-period</p>
<p>公式中的slave-validity-factor即为当前所述的配置项</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>cluster-migration-barrier 1</td>
<td>
<p>Slave可以迁移到孤立Master —— 即那些没有工作中的Slave的Master。该特性增强了Redis集群的健壮性，避免孤立Master无法故障转移</p>
<p>为了避免迁移后，Slave原先的Master又变成孤立Master，可以设置一个阈值，即<span style="background-color: #c0c0c0;">迁移后，原先Master至少拥有的工作中的Slave数量</span></p>
</td>
</tr>
<tr>
<td>cluster-require-full-coverage yes</td>
<td>
<p>默认的，如果有<span style="background-color: #c0c0c0;">任何一个Hash Slot没有被覆盖（分配给某个Master），则集群拒绝接受查询请求</span>。这样，如果集群的一部分节点关闭，则整个集群不再可用</p>
<p>这个默认行为可以改变，设置为no即可</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>缓慢日志</strong></em></td>
</tr>
<tr>
<td>slowlog-log-slower-than 10000</td>
<td>判定查询为缓慢的阈值，单位微秒。设置为0则记录所有命令</td>
</tr>
<tr>
<td>slowlog-max-len 128</td>
<td>缓慢日志的最大长度</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>延迟监控</strong></em></td>
</tr>
<tr>
<td>latency-monitor-threshold 00</td>
<td>
<p>Redis的延迟监控子系统在运行时分析不同操作的样本，以分析Redis实例高延迟的可能原因</p>
<p>该系统仅仅记录消耗时间大于latency-monitor-threshold的操作。如果latency-monitor-threshold设置为0，则延迟监控系统被关闭</p>
</td>
</tr>
<tr>
<td class="blog_h3" colspan="2"><em><strong>事件通知</strong></em></td>
</tr>
<tr>
<td>notify-keyspace-events ""</td>
<td>Redis可以通知Pub/Sub客户端键空间中发生的事件，该配置项指定启用哪些通知</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">redis-cli</span></div>
<p>该工具是Redis提供的命令行接口，它具有两种工作模式：交互式（REPL - 读取、估算、打印循环）、脚本式（命令作为redis-cli的参数提供）。</p>
<p>在交互式环境下，键入clear可以清屏，键入help可以获取帮助，键入exit可以退出交互模式。</p>
<div class="blog_h2"><span class="graybg">用法示例</span></div>
<pre class="crayon-plain-tag"># 执行一个命令，把键mycounter的值增加1
redis-cli incr mycounter
# 输出如下，括号内是值的类型：
# (integer) 7

# 强制输出原始数据
redis-cli --raw incr mycounter
# 强制输出易读数据
redis-cli --no-raw mycounter

# 默认情况下，redis-cli链接到本机的6379端口，你可以定制主机、端口
redis-cli -h hostname -p 6390 ping
# PONG

# 默认情况下，redis-cli操控数据库序号0，你可以指定操控的数据库序号：
redis-cli flushall
redis-cli -n 1 incr a  # 操控数据库1

# 从文本中读取foo的值，并写入到Redis
redis-cli -x set foo &lt; fooval.txt
# 从文本中读取一系列redis-cli命令，并交付执行
# cat /tmp/commands.txt | redis-cli
# commands.txt的内容如下：
# set foo 100
# incr foo
# append foo xxx
# get foo
# ...

# 连续执行、延迟执行：每隔100ms执行命令，连续5次
redis-cli -r 5 -i 100 incr foo


# 在交互式环境下，可以使用如下命令连接到其它Redis实例
connect host 6379
# 在连接断开后，CLI会自动尝试重新连接，如果连接失败，会给出提示信息
# 执行下面的命令手工重新连接
ping


# 连续打印Redis的统计信息，例如内存用量
redis-cli --stat


# 扫描整个键空间，来获得最大的键
redis-cli --bigkeys
# 扫描键空间
redis-cli --scan
# 扫描键空间，打印匹配指定正则式的键
redis-cli --scan --pattern '*-11*'


# 持续监控在Redis中执行的命令
redis-cli monitor

# 监控Redis实例的延迟，不停的发送PING命令
redis-cli --latency


# RDB远程备份：在Redis复制模式第一次同步的时候，Master/Slave以RDB文件的形式交换整个数据集
# 你可以通过CLI利用这一特性，实现远程备份
redis-cli --rdb /tmp/dump.rdb    #该命令获取Master的Dump，保存到本地</pre>
<div class="blog_h2"><span class="graybg">屏蔽危险命令</span></div>
<p>某些命令是管理性的，甚至能立即清空数据库，这类命令可以重命名，甚至禁用，以防客户端误用：</p>
<pre class="crayon-plain-tag">rename-command FLUSHALL "" </pre>
<div class="blog_h2"><span class="graybg">所有命令</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">APPEND</td>
<td>附加值到指定的键，类似于字符串连接操作，返回此命令结束后，值的长度</td>
</tr>
<tr>
<td class=" blog_h3">AUTH</td>
<td>请求身份验证</td>
</tr>
<tr>
<td class=" blog_h3">BGREWRITEAOF</td>
<td>
<p>指示服务器启动一个AOF（仅附加文件，Append only file）重写进程，此进程<span style="background-color: #c0c0c0;">将当前AOF重写为一个小的、优化的版本</span></p>
<p>如果此命令执行失败，数据不会丢失，因为当前AOF文件没有改变</p>
<p>仅不存在执行持久化操作的后台线程时，重写操作才被Redis触发：</p>
<ol>
<li>如果一个Redis子进程正在磁盘上建立快照，AOF重写仅被调度，直到创建RDB文件的子进程终结之后才启动</li>
<li>如果当前AOF重写操作正在进行，则此命令返回错误，且不调度AOF重写</li>
</ol>
<p>从2.4开始，AOF重写由Redis自动触发，但是你仍然可以使用该命令，随时触发重写</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BGSAVE</td>
<td>
<p>在后台保存DB，立即返回OK。Redis进程将Fork出子进程来执行DB保存任务并在完毕后退出</p>
<p>执行LASTSAVE 命令可以查看上一次BGSAVE的执行状态</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BITCOUNT</td>
<td>
<p>统计值中设置为1的位的数量：<pre class="crayon-plain-tag">BITCOUNT key [start end]</pre> 。start为起始字节索引，end为终止字节索引，闭区间</p>
<p>示例：</p>
<p><pre class="crayon-plain-tag">SETBIT BS 7 1
SETBIT BS 6 1
SETBIT BS 15 1
GET BS             # "\x03\x01"
BITCOUNT BS 0 0    # 2</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">BITFIELD</td>
<td>将Redis字符串看作位的数组，可以处理任意位宽的整数位、任意非对齐的偏移。该命令可以一次性操控多个比特位</td>
</tr>
<tr>
<td class=" blog_h3">BITOP</td>
<td>
<p>对多个键的值进行按位的操作：<pre class="crayon-plain-tag">BITOP operation destkey key [key ...]</pre> </p>
<p>支持的operation包括AND, OR, XOR,NOT</p>
<p>如果某个参数字符串的长度不足，自动补零</p>
<p>返回值：destkey的值的长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BITPOS</td>
<td>
<p>返回字符串中第一个被设置为1或者0的位的偏移量：<pre class="crayon-plain-tag">BITPOS key bit [start] [end]</pre> </p>
</td>
</tr>
<tr>
<td class=" blog_h3">BLPOP</td>
<td>
<p>阻塞性的列表弹出操作，LPOP的阻塞版本。如果列表中没有可用元素，则当前连接被阻塞，直到超时或元素可用</p>
<p>格式：<pre class="crayon-plain-tag">BLPOP listkey [listkey ...] timeout</pre> </p>
<p>第一个非空列表的第一个元素将被弹出，返回一个数组，如果成功弹出，则第一个元素是被操作列表的key，第二个元素是弹出的值；如果弹出失败，则元素为nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">BRPOP</td>
<td>与上面类似，但是弹出的是尾部（R）元素而不是头部（L）</td>
</tr>
<tr>
<td class=" blog_h3">BRPOPLPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">BRPOPLPUSH source destination timeout</pre> </p>
<p>RPOPLPUSH的阻塞版本，如果source包含元素或者在MULTI/EXEC块中使用，此命令的行为与RPOPLPUSH一致</p>
<p>如果source为空，则在超时前或者其它客户端向source压入数据前，连接会一直阻塞。timeout设置为0则一直阻塞</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT KILL</td>
<td>
<p>格式：</p>
<pre class="crayon-plain-tag">CLIENT KILL [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] 
            [ADDR ip:port] [SKIPME yes/no]</pre>
<p> 关闭指定的客户端连接，ip:port参数必须和CLIENT LIST命令输出的addr列匹配</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT LIST</td>
<td>
<p>以可读格式输出客户端连接的基本、统计信息。输出列：<br />id，客户端的标识符<br />addr，地址:端口<br />fd，与套接字对应的文件描述符<br />age，连接总计持续的秒数<br />idle，连接已经空闲的秒数<br />flags，客户端标记<br />db，当前数据库ID<br />sub，频道订阅数量<br />psub，基于模式匹配的订阅数量<br />multi，在MULTI/EXEC 上下文中的命令数量<br />qbuf，查询缓冲长度，0表示没有悬挂的查询<br />qbuf-free，查询缓冲中空闲的长度，0表示查询缓冲已满<br />obl，输出缓冲的长度<br />oll，输出列表的长度（当缓冲满了的时候，答复在此列表排队）<br />omem，输出缓冲的内存用量<br />events，文件描述符事件<br />cmd，最后一次执行的命令</p>
<p>客户端标记可以是以下值的组合：<br />O，客户端是MONITOR模式下的slave<br />S，客户端是一个正常模式的slave服务器<br />M，客户端是一个master<br />x，客户端处于MULTI/EXEC上下文中<br />b，客户端正在等待阻塞的操作完成<br />d，一个watched的键已经被修改，EXEC将失败<br />c，在写入完整的答复后，连接将被关闭<br />u，客户端是非阻塞的<br />U，客户端通过UNIX域套接字连接<br />r，客户端基于集群节点处于只读模式<br />A，客户端将被尽快关闭<br />N，没有特别的标记被设置</p>
<p>文件描述符事件：<br />r，客户端套接字可读<br />w，客户端套接字可写</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT GETNAME</td>
<td>获得当前连接的名称，该名称由CLIENT SETNAME设定</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT PAUSE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLIENT PAUSE timeout</pre> ，在指定的毫秒数内，暂停所有客户端。具体行为：</p>
<ol>
<li>暂停普通客户端、Pub/Sub客户端的所有悬挂命令。但是与Slave的交互仍然会正常进行</li>
<li>该命令会尽快的返回OK，因而此命令不会导致调用者客户端被自己暂停</li>
<li>当指定的时间过去后，所有客户端不再被阻塞，所有客户端的查询缓冲中累积的命令会被处理</li>
</ol>
<p>该命令可以让客户端受控的切换其所连接到的服务器，例如管理员可以：</p>
<ol>
<li>暂停所有客户端</li>
<li>等待数秒，确保Slave处理了来自Master的最后的复制流（replication stream）</li>
<li>把一个Slave切换为Master</li>
<li>重新配置clients，让其连接到新的Master</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT REPLY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLIENT REPLY ON|OFF|SKIP</pre> </p>
<p>可以用来禁止来自Redis服务器的任何答复信息，在客户端发送fire and forget命令时、载入大量数据时，该命令可以提升效能。取值ON表示启用客户端回复，OFF表示关闭，SKIP表示跳过当前命令之后所有命令的回复</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLIENT SETNAME</td>
<td>设置连接的名称</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER ADDSLOTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER ADDSLOTS slot [slot ...]</pre> </p>
<p>此命令用于修改节点（Node）的集群配置视图。它<span style="background-color: #c0c0c0;">为接受命令的Redis节点添加若干Hash slot</span>。如果操作成功，节点会把Hash slots映射给自己，并开始<span style="background-color: #c0c0c0;">广播新的配置</span>。需要注意：</p>
<ol>
<li>仅在从接受命令的节点的角度来看，所有Slot目前尚未分配时，命令才会成功。如果Slot当前已经分配给其它节点了，则节点拒绝获得Slot的所有权</li>
<li>如果同一个Slot在命令参数中多次指定，命令失败</li>
<li>如果某个Slot被设置为importing，则节点分配了该Slot后，importing状态被清除</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">CLUSTER ADDSLOTS 1 2 3
# OK
CLUSTER ADDSLOTS 1 2 3
# ERR Slot 1 is already busy</pre>
<p>该命令仅在集群模式下（cluster mode ）可用，用来实现：</p>
<ol>
<li>创建一个新集群时，使用该命令来初始化Master nodes，在这些节点之间分配Hash Slots</li>
<li>修复一个被破坏的集群，此集群中某些Slots没有被分配</li>
</ol>
<p>注意，当节点给自己分配一系列Slot时，它会在心跳包的头中传播这一信息。然而，其它节点仅仅在：</p>
<ol>
<li>没有认为Slot已经分配给其它节点</li>
<li>或者，传播信息的节点的配置信息更加新的时候</li>
</ol>
<p>才接受这些信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNT-FAILURE-REPORTS</td>
<td>
<p>返回指定节点未过期的错误报告（failure reports ）的数量。<span style="background-color: #c0c0c0;">Redis集群基于错误报告来把节点的状态从PFAIL（目标节点不可达）升级到FAIL（集群中的大部分Master同意在一个时间窗内，目标节点不可达）</span></p>
<p>关于PFAIL/FAIL的一些细节：</p>
<ol>
<li>当超过配置项<span style="background-color: #c0c0c0;">node timeout</span>（这是Redis集群的基础配置项）之后，节点A到节点B不可达，<span style="background-color: #c0c0c0;">则A标记B为PFAIL</span></li>
<li><span style="background-color: #c0c0c0;">处于PFAIL状态的节点，会写入心跳报文的gossip段</span></li>
<li>每当一个<span style="background-color: #c0c0c0;">节点C接收来自节点A的gossip后，它创建一个错误报告（并在需要时更新TTL），记住节点A认为节点B为PFAIL状态</span></li>
<li>每个错误报告的<span style="background-color: #c0c0c0;">TTL为node timeout的两倍大小</span></li>
<li>在一个特定的时间点，当前节点认为节点B为PFAIL，并且从收集的错误报告中看，<span style="background-color: #c0c0c0;">大多数Master节点（如果当前节点是Master，包含在内）也认为节点B为PFAIL，则当前节点标记B为FAIL，并且广播一个消息，迫使所有可达节点都标记B为FAIL</span></li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNTKEYSINSLOT</td>
<td>返回指定的Redis集群Hash Slot中存储的键的数量。该命<span style="background-color: #c0c0c0;">令仅仅查询本地数据集</span>，因此在没有分配目标Slot的节点上运行此命令总是返回0</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER DELSLOTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER DELSLOTS slot [slot ...]</pre> </p>
<p>在Redis集群中，每个节点都跟踪哪个Master负责Serving某个特定的Slot。该命令让目标节点<span style="background-color: #c0c0c0;">忘记谁负责Serving参数指定的Slots，这些Slots变为Unbound</span>。另外，<span style="background-color: #c0c0c0;">尚未被分配给任何Master节点的Slot天然处于Unbound状态</span>。如果Slot已经被分配，在那些尚未接收到通知（心跳或者更新报文）的节点来看，Slot也处于Unbound状态。</p>
<p>认为某个Slot为Unbound的节点，一旦<span style="background-color: #c0c0c0;">收到其它节点声明自己是该Slot所有者</span>的心跳报文，就会立即<span style="background-color: #c0c0c0;">建立节点与Slot之间的关联</span></p>
<p>如果某个节点收到包含配置信息的心跳/更新报文，且此报文的时间比<span style="background-color: #c0c0c0;">该节点自身的配置时间更加新，则节点-Slot关联会被重新建立</span></p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER FAILOVER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER FAILOVER [FORCE|TAKEOVER]</pre> </p>
<p>该命令<span style="background-color: #c0c0c0;">仅可以在Redis集群的Slave节点上执行</span>。可以强制一个Slave启动手工的故障转移（针对它的Master节点）</p>
<p>手工故障转移是一种特殊的故障转移，通常在未发生实际错误但期望安全的（不招致丢失数据的窗口期）把Master与它的某个Slave（接收命令的节点）进行Swap时执行。工作方式如下：</p>
<ol>
<li>Slave通知Master，停止处理来自客户端的查询请求</li>
<li>Master回复Slave，告知当前的复制偏移量（replication offset）</li>
<li>Slave等待自己的复制偏移量和Master匹配</li>
<li>Slave触发一个故障转移，从大多数Master获得一个新的配置纪元（configuration epoch），并广播新的配置信息</li>
<li>旧的Master接收配置更新，Unblock客户端连接，回复重定向信息，导致客户端与新的Master进行交互</li>
</ol>
<p>这样，客户端可以原子的从旧的Master切换到新的Master</p>
<p>FORCE选项：可以在Master宕机的情况下手工故障转移（failover）。使用此选项时，Slave不会与Master握手，而是直接从上面的第4步开始执行。尽管如此，Slave还是需要联系上大部分Master节点，以确保故障转移被授权，并未Slave生成新的配置时间点</p>
<p>TAKEOVER选项：不经过集群共识（cluster consensus）的手工故障转移。某些情况下，我们希望不经过其它Master同意的情况下就执行故障转移。</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER FORGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER FORGET node-id</pre> </p>
<p><span style="background-color: #c0c0c0;">从集群中移除某个节点</span>。接收此命令的节点的Node table中的某个节点将被移除</p>
<p>集群中的其它节点都需要知晓节点被移除的事实，因而<span style="background-color: #c0c0c0;">该命令需要被发送给所有其它节点</span>。仅仅从Node table中移除节点是不够的，<span style="background-color: #c0c0c0;">Redis还维护一个禁止清单（banlist）</span>，<span style="background-color: #c0c0c0;">防止心跳信息中的Gossip段把被移除的节点重新加入</span></p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER COUNTKEYSINSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER COUNTKEYSINSLOT slo</pre> </p>
<p>返回指定Slot中本地键的数量，注意该命令仅仅查询本地数据集，因此针对非关联到目标Slot的Node执行此命令，总是返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER INFO</td>
<td>获取Redis集群的关键参数：<br />cluster_state，如果节点可以接收查询，为ok；如果至少一个Hash Slot没有关联到Node（Unbound）或者处于错误状态（关联的Node被标记为FAIL），为fail；如果当前节点不能抵达大部分的Master，为fail<br />cluster_slots_assigned，已经关联到Node的Slot数量<br />cluster_slots_ok，已经关联到Node，且Node不处于PFAIL、FAIL状态到Slot数量<br />cluster_slots_pfail，关联到PFAIL节点的Slot数量<br />cluster_slots_fail，关联到FAIL节点的Slot数量<br />cluster_known_nodes，集群中已知节点的数量，包括正处于HANDSHAKE状态的节点<br />cluster_size，至少关联一个Hash Slot的Master节点的数量<br />cluster_current_epoch，本地的Current Epoch变量的值，此变量用于在故障转移时创建唯一的递增的版本号<br />cluster_my_epoch，本地的Config Epoch变量的值，当前节点的配置的版本<br />cluster_stats_messages_sent，通过集群节点-节点总线发送的消息数量<br />cluster_stats_messages_received，通过集群节点-节点总线接收的消息总量</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER KEYSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER KEYSLOT key</pre> </p>
<p>返回指定的键被散列到的Slot的整数编号</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER GETKEYSINSLOT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER GETKEYSINSLOT slot count</pre> </p>
<p>从指定的Slot中获得一定数量的键</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER MEET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER MEET ip port</pre> </p>
<p>用于<span style="background-color: #c0c0c0;">连接某个启用了集群支持的节点，将其加入到当前集群</span>中</p>
<p>基本的思想是，<span style="background-color: #c0c0c0;">节点之间默认是相互不信任的，被看作是未知（Unknown）节点</span>。分属于不同集群的节点通常不会因为管理员的误操作、网络地址改变而混在一起</p>
<p>因此，为了让一个节点接受另外一个节点作为集群的成员，有两种方法：</p>
<ol>
<li>系统管理员发起CLUSTER MEET命令，强迫一个节点接受另一个</li>
<li>一个已知节点通过gossip段发送节点列表，如果<span style="background-color: #c0c0c0;">接收报文的节点信任发送节点</span>，则会处理gossip中的节点，然后与其中未知的节点进行握手</li>
</ol>
<p>尽管Redis集群需要构成节点的完整网络（Mesh），但是<span style="background-color: #c0c0c0;">并非你需要向每对节点发送MEET命令</span>，只需要保证任意两个节点可以通过<span style="background-color: #c0c0c0;">已知节点链（Known Nodes Chain）可达</span>。此外，<span style="background-color: #c0c0c0;">MEET命令只需要单向的发送</span></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class=" blog_h3">
<p>CLUSTER NODES</p>
</td>
<td>
<p>集群中每个节点，<span style="background-color: #c0c0c0;">对于集群的配置有自己的视图</span>。配置信息包括：<span style="background-color: #c0c0c0;">已知节点的集合、当前节点与已知节点的连接状态、已知节点的标记／属性／分配的Slot，等等</span></p>
<p>该命令的输出包括上述配置信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER REPLICATE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER REPLICATE node-id</pre> </p>
<p>配置<span style="background-color: #c0c0c0;">当前节点，将其作为Master节点（node-id指定）的Slave</span>。如果当前节点是空的Master，其角色从Master变为Slave</p>
<p>当一个节点变成某个Master的Slave后，不需要通知集群中的其它节点，因为节点之间交换的心跳报文会自动传播这一配置变更</p>
<p>如果当前节点是Slave，当满足以下条件时，命令总是被接受：</p>
<ol>
<li>node-id存在于节点表中</li>
<li>node-id不是当前节点的标识符</li>
<li>node-id对应了一个Master的标识符</li>
</ol>
<p>如果当前节点是Master，则仅当满足以下条件时，节点才被转化为Slave并成功返回：</p>
<ol>
<li>node-id没有关联到任何Hash Slot</li>
<li>node-id是空白的，其键空间中没有存放任何键</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER RESET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER RESET [HARD|SOFT]</pre> </p>
<p>重置一个集群节点，HARD|SOFT决定重置动作的激烈程度，默认SOFT</p>
<p>该命令不能针对存放了键的Master节点使用，如果要重置Master节点，需要先清空其中的键，可以先使用FLUSH ALL命令清空</p>
<p>该命令对节点的作用：</p>
<ol>
<li><span style="background-color: #c0c0c0;">忘记集群中所有其它节点</span></li>
<li>所有<span style="background-color: #c0c0c0;">Slot分配信息被清空</span></li>
<li>如果节点是<span style="background-color: #c0c0c0;">Slave，它变成一个空白的Master</span></li>
<li>如果指定HARD，则节点的标识符被重置</li>
<li>如果指定HARD，则currentEpoch、configEpoch变量重置为0</li>
<li>新的配置信息持久化到该节点的集群配置文件中</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SAVECONFIG</td>
<td>
<p>强制一个节点在磁盘上存储nodes.conf文件。命令返回前fsync(2)系统调用被执行以确保持久化成功</p>
<p>如果nodes.conf文件因为某种原因丢失，可以使用该命令重新生成 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SET-CONFIG-EPOCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER SET-CONFIG-EPOCH config-epoch</pre> </p>
<p>设置一个新节点的config epoch，仅当满足以下条件时生效：</p>
<ol>
<li>目标节点的节点表（node table）为空</li>
<li>目标节点当前的config epoch为零</li>
</ol>
<p>之所以要求这两个条件，是由于手工修改config epoch是不安全的。此变量的值更大的节点，在声明Slot所有权时，具有更高的优先级</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SETSLOT</td>
<td>
<p>格式：</p>
<pre class="crayon-plain-tag">CLUSTER SETSLOT slot IMPORTING|MIGRATING|STABLE|NODE [node-id]
CLUSTER SETSLOT &lt;slot&gt; MIGRATING &lt;destination-node-id&gt;
CLUSTER SETSLOT &lt;slot&gt; IMPORTING &lt;source-node-id&gt;
CLUSTER SETSLOT &lt;slot&gt; STABLE
CLUSTER SETSLOT &lt;slot&gt; NODE &lt;node-id&gt;  </pre>
<p><span style="background-color: #c0c0c0;">改变</span>接收命令节点<span style="background-color: #c0c0c0;">所看到的Slot的状态</span>，包括以下子命令：</p>
<ol>
<li>MIGRATING，设置Slot状态为migrating</li>
<li>IMPORTING，设置Slot状态为importing</li>
<li>STABLE，清空Slot的migrating或者importing状态</li>
<li>NODE，绑定Slot到其它节点</li>
</ol>
<p>该命令在集群的在线重分区（live resharding）操作中有用，重分区可以把某个Slot完整的迁移到另外一个节点上。通常你会在源节点执行MIGRATING子命令，然后在目标节点执行IMPORTING子命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SLAVES</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CLUSTER SLAVES node-id</pre> </p>
<p>列出指定Master节点的Slave。如果接收命令节点的节点表中不存在node-id或者node-id不是Master，命令失败</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CLUSTER SLOTS</td>
<td>列出Hash Slot与Redis实例映射的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">COMMAND</td>
<td>列出所有Redis命令的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">COMMAND COUNT</td>
<td>统计命令的数量 </td>
</tr>
<tr>
<td class=" blog_h3">COMMAND INFO</td>
<td>显示一个或者多个命令的详细信息</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG GET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CONFIG GET parameter</pre> </p>
<p>该命令用于读取运行中Redis服务器的配置参数，可以使用通配符* </p>
</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG REWRITE </td>
<td>重写当前Redis服务使用的redis.conf配置文件，进行最小化的修订以反映当前运行中的服务正在使用的配置信息</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG SET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">CONFIG SET parameter value</pre>  </p>
<p>在运行时重新配置Redis服务</p>
</td>
</tr>
<tr>
<td class=" blog_h3">CONFIG RESETSTAT</td>
<td>重置INFO命令所报告的统计信息</td>
</tr>
<tr>
<td class=" blog_h3">DBSIZE</td>
<td>返回当前选中的数据库中键的数量</td>
</tr>
<tr>
<td class=" blog_h3">DECR</td>
<td>递减指定键的值，如果指定的键不存在，在操作前将其值设置为0 </td>
</tr>
<tr>
<td class=" blog_h3">DECRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">DECRBY key decrement</pre> </p>
<p>减少指定键的值，如果指定的键不存在，在操作前将其值设置为0 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">DEL</td>
<td>删除一个或者多个键</td>
</tr>
<tr>
<td class=" blog_h3">DISCARD</td>
<td>刷出（Flush）事务中排队的所有命令，并且恢复连接状态为Normal</td>
</tr>
<tr>
<td class=" blog_h3">DUMP</td>
<td>串行化指定键的值，以Redis特有的格式返回给用户</td>
</tr>
<tr>
<td class=" blog_h3">ECHO</td>
<td>让服务器回响</td>
</tr>
<tr>
<td class=" blog_h3">EVAL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EVAL script numkeys key [key ...] arg [arg ...]</pre></p>
<p>基于内置的脚本解释器来估算脚本的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EVALSHA</td>
<td>基于SHA1摘要检索缓存在服务器上的脚本，并估算其值</td>
</tr>
<tr>
<td class=" blog_h3">EXEC</td>
<td>
<p>在事务中执行先前排队的命令，然后恢复连接为正常状态</p>
<p>使用WATCH命令时，仅在被监控键未被修改时EXEC才会执行命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXISTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXISTS key [key ...]</pre></p>
<p>判断指定的键是否存在，返回存在的键的数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXPIRE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXPIRE key seconds</pre></p>
<p>设置指定键的超时，时间到达后，目标键被自动删除。在Redis的术语体系中，带有超时的键被成为易变键（volatile）</p>
<p>超时仅可以被删除/覆盖键内容的命令清除，包括DEL、SET、GETSET等。<span style="background-color: #c0c0c0;">修改键的值，而不是替换它</span>，不会改变超时设置，因此INCR、LPUSH等操作不会改变超时设置</p>
<p>PERSIST命令用于清除超时设置</p>
<p>RENAME命令修改一个键值时，旧有的超时设置被重命名后的键继承</p>
</td>
</tr>
<tr>
<td class=" blog_h3">EXPIREAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">EXPIREAT key timestamp</pre></p>
<p>在指定的时间让键超时，时间到达后，目标键被自动删除。timestamp是秒为单位的UNIX时间戳</p>
</td>
</tr>
<tr>
<td class=" blog_h3">FLUSHALL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">FLUSHALL [ASYNC]</pre></p>
<p>从现存的数据库中删除所有的键，包括当前选择的数据库</p>
<p>ASYNC表示异步执行，不会阻塞服务器</p>
<p>在集群模式下仅仅会处理当前分片的数据</p>
</td>
</tr>
<tr>
<td class=" blog_h3">FLUSHDB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">FLUSHDB [ASYNC]</pre></p>
<p>从当前选中的数据库中删除所有的键</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOADD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOADD key longitude latitude member [longitude latitude member ...]</pre></p>
<p>添加指定的地理信息条目（经度、纬度、名称）到指定的键，值的存储类型为有序集合（sorted set），便于之后基于径向查询命令取回</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOHASH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOHASH key member [member ...]</pre></p>
<p>返回代表位置的HASH，key为先前基于GEOADD添加的地理信息条目，member则为某个条目的名称</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEOPOS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEOPOS key member [member ...]</pre></p>
<p>返回位置信息，经度、纬度组成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEODIST</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEODIST key member1 member2 [unit]</pre></p>
<p>返回两个地理信息条目之间的距离</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEORADIUS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]</pre></p>
<p>径向查询，返回以指定经纬度为原点，radius半径范围内的地理信息条目</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GEORADIUSBYMEMBER</td>
<td>格式：<pre class="crayon-plain-tag">GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]</pre></td>
</tr>
<tr>
<td class=" blog_h3">GET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GET key</pre></p>
<p>返回指定键对应的值，如果键不存在，返回nil这个特殊值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETBIT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETBIT key offset</pre></p>
<p>返回值在指定偏移量处的二进制位，如果offset超过长度，则一律返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETRANGE key start end</pre></p>
<p>在2.0之前的版本中命令名为SUBSTR，用于获得一个字符串的子串</p>
</td>
</tr>
<tr>
<td class=" blog_h3">GETSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">GETSET key value</pre></p>
<p>设置key的值为value，并返回旧的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HDEL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HDEL key field [field ...]</pre></p>
<p>从指定的HASH中删除字段，此HASH以key为键存储在Redis中</p>
<p>在2.4版本之前，一次只能删除一个字段</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HEXISTS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HEXISTS key field</pre></p>
<p>查询指定的HASH是否存在字段，返回0/1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HGET key field</pre></p>
<p>从指定的HASH中获取一个字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HGETALL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HGETALL key</pre></p>
<p>以数组的形式返回一个HASH的所有字段值，如果key不存在则返回一个空数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HINCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HINCRBY key field increment</pre></p>
<p>增加一个HASH字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HINCRBYFLOAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HINCRBYFLOAT key field increment</pre></p>
<p>增加一个HASH字段的值，支持浮点数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HKEYS </td>
<td>
<p>格式：<pre class="crayon-plain-tag">HKEYS key</pre></p>
<p>以数组的形式返回HASH的所有键，如果key不存在则返回空数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HLEN key</pre></p>
<p>返回HASH字段的数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HMGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HMGET key field [field ...] </pre></p>
<p>同时得到多个HASH字段的值，返回数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HMSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HMSET key field value [field value ...]</pre></p>
<p>同时设置多个HASH字段的值，如果HASH不存在自动创建，现有的字段值会被覆盖</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSET key field value</pre></p>
<p>设置一个HASH字段的值，如果目标字段已经存在则返回0，否则返回1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSETNX key field value</pre></p>
<p>设置HASH字段的值，仅仅在field 不存在的时候执行</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HSTRLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HSTRLEN key field</pre></p>
<p>返回HASH字段的字符串长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">HVALS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">HVALS key</pre></p>
<p>以数组的形式返回一个HASH中所有字段的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCR</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCR key</pre></p>
<p>增加一个键的值，如果键不存在，则在操作前将其值设置为0</p>
<p>注意：这是一个字符串操作，因为Redis不提供专门的整数类型，存在key下的字符串被看做是64位无符号整数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCRBY key increment</pre></p>
<p>增加一个键的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INCRBYFLOAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INCRBYFLOAT key increment</pre></p>
<p>增加一个键的值，支持浮点数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">INFO</td>
<td>
<p>格式：<pre class="crayon-plain-tag">INFO [section]</pre></p>
<p>返回服务器的基本信息、统计信息，以可读并易于机器解析的格式输出</p>
<p>section用于指定需要获取的信息的段：<br />server  Redis服务器的基本信息<br />clients  客户端连接信息<br />memory  内存消耗相关的信息<br />persistence  RDB和AOF相关的信息<br />stats  一般性的统计信息<br />replication  主从复制相关信息<br />cpu  CPU消耗统计信息<br />commandstats  Redis命令的统计信息<br />cluster  Redis集群相关信息<br />keyspace  数据库相关信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">KEYS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">KEYS pattern</pre></p>
<p>返回所有匹配指定pattern的键的数组，此操作的效率较高，但是在生产环境下仍然要小心使用以防止对性能造成影响。如果希望在键空间的子集中查找键，最好考虑使用SCAN、SETS</p>
<p>pattern支持有限的几种统配符：</p>
<ol>
<li>?l，匹配单个字符</li>
<li>*l，匹配0-N个字符</li>
<li>[ae]，匹配a或e</li>
<li>[^e]，匹配非e字符</li>
<li>[a-b]，匹配a到b之间的字符</li>
</ol>
<p>此命令和集群模式不兼容，仅仅能返回当前节点上的数据</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LASTSAVE</td>
<td>返回上次数据保存命令执行的UNIX时间。客户端可以发起BGSAVE命令进行数据库保存</td>
</tr>
<tr>
<td class=" blog_h3">LINDEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LINDEX key index</pre></p>
<p>返回指定索引上的元素值，索引以0开始。如果index超出索引范围则返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LINSERT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LINSERT key BEFORE|AFTER pivot value</pre></p>
<p>在参考值pivot之前/后插入指定的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LLEN key</pre></p>
<p>返回指定列表的长度，如果key不存在则当做空列表，如果key存储的不是列表则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPOP</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPOP key</pre></p>
<p>移除列表的第一个元素并返回之。如果key不存在则返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPUSH key value [value ...]</pre></p>
<p>在列表的头部插入指定的元素，如果key不存在则自动创建列表。指定多个value则最后一个在操作后成为第一个元素。返回操作后列表的长度</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LPUSHX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LPUSHX key value</pre></p>
<p>与上面的命令类似，但是仅仅在key存在且其值是一个列表时才只需插入操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LRANGE key start stop</pre></p>
<p>从列表中返回一个子列表，从start开始，到stop结束（包含）。如果索引值为负数，则表示倒数，-1表示倒数第一个元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LREM key count value</pre></p>
<p>从key对应的列表中移除一定数量的值等于value的元素，如果：<br />count为正数，从列表头开始查找<br />count为负数，从列表尾开始查找<br />count为0，移除所有值为value的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LSET key index value</pre></p>
<p>设置列表指定索引上的元素值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">LTRIM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">LTRIM key start stop</pre></p>
<p>修剪列表，仅仅保留从start到stop的元素，索引可以为负数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MGET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MGET key [key ...]</pre></p>
<p>同时返回多个键的值构成的数组，key不存在的，对应数组元素为nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MIGRATE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MIGRATE host port key|"" destination-db timeout [COPY] [REPLACE] [KEYS key [key ...]]</pre></p>
<p>从源Redis实例传输一个或者多个键到目标Redis实例，如果操作成功则键从源实例删除并存在于目标实例上</p>
<p>该命令是原子的，在操作完成之前，源、目的实例都被阻塞除非超时。在任意时刻，目标键仅仅存在于源或者目的实例上</p>
<p>从3.2开始，可以设置key 为""并通过KEYS子句来指定多个需要迁移的键</p>
<p>该命令在内部，先使用DUMP来串行化目标键，然后在目的实例上执行RESTORE命令，如果RESTORE命令返回OK则源实例执行DEL命令删除键</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MONITOR</td>
<td>调试命令，流式的显示在Redis服务器上执行的所有命令</td>
</tr>
<tr>
<td class=" blog_h3">MOVE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MOVE key db</pre></p>
<p>把键从当前选中的数据库转移到指定数据库db。如果key在目目的数据库上存在、或者在源数据库上不存在，什么都不做</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MSET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MSET key value [key value ...]</pre></p>
<p>同时设置多个键的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MSETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">MSETNX key value [key value ...]</pre></p>
<p>同时设置多个键的值，仅仅在所有键都不存在的情况下才会执行设置</p>
</td>
</tr>
<tr>
<td class=" blog_h3">MULTI</td>
<td>标记事务块的起点，后续命令会自动排队，直到遇到EXEC命令</td>
</tr>
<tr>
<td class=" blog_h3">OBJECT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">OBJECT subcommand [arguments [arguments ...]]</pre></p>
<p>查看关联到某个键的Redis对象的内部信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PERSIST</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PERSIST key</pre></p>
<p>移除键上的超时设置</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PEXPIRE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PEXPIRE key milliseconds</pre></p>
<p>与EXPIRE命令类似，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PEXPIREAT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PEXPIREAT key milliseconds-timestamp</pre></p>
<p>与EXPIREAT类似，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PING</td>
<td>用于测试连接状态、测量延迟</td>
</tr>
<tr>
<td class=" blog_h3">PSETEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PSETEX key milliseconds value</pre></p>
<p>类似SETEX，但是时间以毫秒为单位</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PSUBSCRIBE pattern [pattern ...]</pre></p>
<p>让客户端订阅指定的模式，pattern示例：<br />? 匹配单个字符<br />* 匹配任意个数字符<br />[abc] 匹配abc其中之一</p>
</td>
</tr>
<tr>
<td>PSYNC</td>
<td>向Master申请一条复制流，由Slave调用</td>
</tr>
<tr>
<td class=" blog_h3">PUBSUB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUBSUB subcommand [argument [argument ...]]</pre></p>
<p>一个自省命令，用于测试订阅/发布系统的状态。包括子命令：</p>
<p><pre class="crayon-plain-tag">PUBSUB CHANNELS [pattern]</pre></p>
<p>列出当前活动的订阅/发布频道的列表。所谓活动，是指至少具有一个订阅者。如果不指定pattern则所有频道被列出</p>
<p><pre class="crayon-plain-tag">PUBSUB NUMSUB [channel-1 ... channel-N]</pre></p>
<p>返回指定频道的订阅者数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PTTL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PTTL key</pre></p>
<p>以毫秒返回目标键的TTL</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PUBLISH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUBLISH channel message</pre></p>
<p>向指定频道发布一个消息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">PUNSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">PUNSUBSCRIBE [pattern [pattern ...]]</pre></p>
<p>格局pattern来取消订阅，如果不指定pattern则取消订阅该客户端先前订阅的所有频道</p>
</td>
</tr>
<tr>
<td class=" blog_h3">QUIT</td>
<td>请求服务器关闭连接，当服务器输出所有未决的命令答复后，连接被关闭</td>
</tr>
<tr>
<td class=" blog_h3">RANDOMKEY</td>
<td>从当前选中的数据库中随机返回一个键</td>
</tr>
<tr>
<td class=" blog_h3">READONLY</td>
<td>
<p>对于连接到集群Slave节点的连接，该命令启动只读查询</p>
<p>通常情况下，Slave节点会依据命令牵涉到的Hash Slot来重定向客户端到对应的Master节点。使用该命令后，可能读取到陈旧的数据</p>
<p>如果连接处于readonly模式，仅在命令牵涉到Slave的Master节点所不管理的Hash Slot时，服务器才会向客户端发送重定向信息</p>
</td>
</tr>
<tr>
<td class=" blog_h3">READWRITE</td>
<td>恢复读写模式</td>
</tr>
<tr>
<td class=" blog_h3">RENAME </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RENAME key newkey</pre></p>
<p>重命名指定的键，如果源键不存在返回错误。该命令隐含一个DEL，因此值很大的时候可能消耗较长时间</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RENAMENX </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RENAMENX key newkey</pre></p>
<p>如果newkey不存在，执行RENAME操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RESTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RESTORE key ttl serialized-value [REPLACE]</pre></p>
<p>根据串行化的值来恢复一个key，如果key已经存在且没有指定REPLACE则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ROLE</td>
<td>获得当前Redis实例在复制（Replication）上下文中的角色</td>
</tr>
<tr>
<td class=" blog_h3">RPOP</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPOP key</pre></p>
<p>弹出列表的最后一个元素并返回之</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPOPLPUSH </td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPOPLPUSH source destination</pre></p>
<p>从source列表的尾部弹出一个元素，并把该元素插入到destination列表的头部。如果源列表不存在则返回nil，并不执行LPUSH操作，否则返回弹出的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPUSH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPUSH key value [value ...]</pre></p>
<p>插入多个元素到列表的尾部</p>
</td>
</tr>
<tr>
<td class=" blog_h3">RPUSHX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">RPUSHX key value</pre></p>
<p>如果key存在且存储了列表，则执行RPUSH命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SADD </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SADD key member [member ...]</pre></p>
<p>添加多个元素到key所存储的集合（Set）中，返回实际插入的元素数量</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SAVE</td>
<td>
<p>对所有数据集进行同步的保存，产生RDB文件</p>
<p>在生产环境下通常不会使用该命令，因为它会阻塞所有其他客户端，可以使用BGSAVE代替</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SCARD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SCARD key</pre></p>
<p>返回集合的基数（cardinality，元素的个数）</p>
</td>
</tr>
<tr>
<td class=" blog_h3"> SDIFF</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SDIFF key [key ...] </pre></p>
<p>返回第一个集合与后续所有集合的差集，以数组形式</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SDIFFSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SDIFFSTORE destination key [key ...]</pre></p>
<p>类似于SDIFF，但是差集不是返回客户端，而是存储到destination</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SELECT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SELECT index</pre></p>
<p>根据索引选择使用的数据库，新连接默认选择0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SET</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SET key value [EX seconds] [PX milliseconds] [NX|XX]</pre></p>
<p>让键存储一个字符串值，如果key已经存在，不管它当前存储的值类型，都被覆盖<br />EX  以秒为单位指定生存期<br />PX  以毫秒为单位指定生存期<br />NX  仅在键不存在的情况下才设置值<br />XX  仅在键已存在的情况下才设置值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETBIT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETBIT key offset value</pre></p>
<p>设置或者清除offset位，如果key不存在，则自动设置为字符串值。字符串长度会自动增长以满足offset，但是最大支持offset为2^32 - 1，也就是说最大支持512MB的位图</p>
<p>注意性能位图：在2010年产的Macbook上设置第2^32-1位需要耗时接近300毫秒，设置第2^30-1位需要80毫秒</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETEX </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETEX key seconds value</pre></p>
<p>设置值，并以秒为单位设置键的生存期</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETNX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETNX key value</pre></p>
<p>如果键不存在，则设置其值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SETRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SETRANGE key offset value</pre></p>
<p>从指定的偏移处开始用value覆盖key的值</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SHUTDOWN </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SHUTDOWN [NOSAVE|SAVE]</pre></p>
<p>该命令的行为：</p>
<ol>
<li>停止所有客户端</li>
<li>如果至少一个保存点被配置，执行阻塞的SAVE命令</li>
<li>如果AOF被启用，刷出Append Only File</li>
<li>退出服务器</li>
</ol>
<p>如果启用了持久化，该命令确保不丢失数据。相对的，SAVE + QUIT的组合无法保证不丢失数据，因为其它客户端可能在两个命令之间修改了数据</p>
<p>对于配置了不持久化到磁盘的Redis实例（没有配置AOF，也没有使用save指令），该命令不会在磁盘上DUMP出RDB文件</p>
<p>SAVE，强制执行数据库保存操作，即使在没有配置保存点（save points）的情况下<br />NOSAVE，即使在配置了一个或多个保存点的情况下，也阻止数据库保存操作</p>
<p><strong>SHUTDOWN命令失败的情况</strong></p>
<p>如果启用了Append Only File，命令可能失败，因为系统可能处于一个不能立即安全的持久化到磁盘上的状态</p>
<p>通常的，如果一个AOF子进程正在执行AOF重写（Rewrite）操作，Redis会简单的杀掉这些子进程并退出。但是下面两个条件下这样杀死是不安全的，SHUTDOWN命令会被拒绝，并返回错误：</p>
<ol>
<li>用户刚刚启用AOF，并且服务器触发第一次AOF重写以创建初始的AOF文件。这种情况下，杀死AOF子进程会导致数据集完全丢失</li>
<li>一个Slave启用了AOF，重连到它的Master执行一次完整的同步操作，然后重新初始化AOF文件。这种情况下，不让AOF子进程完成工作是危险的，因为从Master接收到的最后的数据集会丢失</li>
</ol>
<p>某些情况下，我们只是想立即关闭服务器，而不管它存储了什么东西。此时可以联用： CONFIG appendonly no、SHUTDOWN NOSAVE。前一个命令会关闭AOF并且杀死AOF子进程（如果存在）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SINTER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SINTER key [key ...]</pre></p>
<p>以数组形式返回多个集合的交集</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SINTERSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SINTERSTORE destination key [key ...]</pre></p>
<p>存储多个集合的交集到destination，返回交集包含的元素个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SISMEMBER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SISMEMBER key member</pre></p>
<p>判断member是不是key存储的集合的成员</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SLAVEOF</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SLAVEOF host port</pre></p>
<p>即时的设置一个Slave的复制设置（replication settings）</p>
<p>如果一个Redis已经是Slave，那么执行<pre class="crayon-plain-tag">SLAVEOF NO ONE</pre>会关闭复制功能，并把此实例变为Master。当指定host port时，会让此实例成为目标实例的Slave，如果当前实例已经是其它某个实例的Slave，则解除其关系</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SLOWLOG</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SLOWLOG subcommand [argument]</pre></p>
<p>读取或者重置Redis缓慢查询日志</p>
<p><strong>缓慢日志概述</strong></p>
<p>这是Redis记录缓慢查询（执行时间超过一定的阈值）的机制。注意执行时间不包括握手、发送答复等网络I/O消耗的时间</p>
<p>与缓慢查询相关的配置参数：</p>
<ol>
<li>slowlog-log-slower-than：慢于多少ms的查询被认为是缓慢的。设置为负数禁用缓慢查询日志，设置为0则记录所有查询</li>
<li>slowlog-max-len：缓慢日志的最大长度，超过此长度后，最老的日志被丢弃</li>
</ol>
<p>你可以编辑redis.conf或者在运行时执行CONFIG SET/GET命令还读写这些参数</p>
<p><strong>读取缓慢日志</strong></p>
<p>缓慢日志驻留内存，因此它才能支持对所有命令进行记录而不影响性能。调用SLOWLOG GET可以读取所有缓慢日志，SLOWLOG GET 10则读取最近的10条日志。输出字段包括：流水号、目标命令执行时的UNIX时间戳、执行消耗的微秒数、命令及其参数</p>
<p><strong>重置缓慢日志</strong></p>
<p>执行SLOWLOG RESET可以清空缓慢日志</p>
<p><strong>示例</strong></p>
<p><pre class="crayon-plain-tag">1) 1) (integer) 32                   # 慢查询条目的标识符
   2) (integer) 1611661425           # 命令开始执行时的时间戳                  
   3) (integer) 24959                # 耗时，单位微秒
   4) 1) "PSYNC"                     # 命令及其参数   
      2) "?"                                           
      3) "-1"                                          
   5) "172.29.0.81:38265"            # 客户端的IP和端口                  
   6) ""                             # 通过CLIENT SETNAME设置的客户端名称</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">SMEMBERS</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SMEMBERS key</pre></p>
<p>以数组的形式返回集合的全部元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SMOVE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SMOVE source destination member</pre></p>
<p>以原子操作的方式，把元素member从集合source移动到集合destination中</p>
<p>如果source不存在或者元素不属于soruce，返回0；否则，从source中移除元素，添加到destination中，如果destination已经包含该元素，则不添加，完成这些操作后返回1；如果source或者destination持有的值不是集合，则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SORT </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]</pre></p>
<p>对列表、集合或者有序集合进行排序，排序结果返回或者存储到destination</p>
<p>默认排序方式是升序，可以指定DESC来降序</p>
<p>如果元素是字符串，可以指定ALPHA，进行字典序排列。注意Redis支持UTF-8，但是必须正确设置LC_COLLATE环境变量</p>
<p>要限制返回元素的偏移量和数量，可以指定<pre class="crayon-plain-tag">LIMIT 0 10</pre>，表示从0开始，10结束（不包含）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SPOP </td>
<td>
<p>格式：<pre class="crayon-plain-tag">SPOP key [count]</pre></p>
<p>从集合中随机弹出指定数量的元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SRANDMEMBER</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SRANDMEMBER key [count]</pre></p>
<p>从集合中随机的返回1个元素，如果指定count为正数，则返回指定个数随机distinct元素的数组 </p>
</td>
</tr>
<tr>
<td class=" blog_h3">SREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SREM key member [member ...]</pre></p>
<p>从集合中移除指定的元素，返回实际移除元素的个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">STRLEN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">STRLEN key</pre></p>
<p>返回字符串的长度，如果key存储的不是字符串则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUBSCRIBE channel [channel ...]</pre></p>
<p>让当前客户端订阅某些频道。一旦客户端进入已订阅状态（ subscribed state），它不应再发起任何除 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE之外的任何命令</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUNION</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUNION key [key ...]</pre></p>
<p>以数组的形式返回并集</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SUNIONSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SUNIONSTORE destination key [key ...]</pre></p>
<p>求并集并存储到destination，如果destination已经存在，它将被覆盖</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SWAPDB</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SWAPDB index index</pre></p>
<p>立即交换两个数据库，所有客户端将立即看到交换后的数据库</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SYNC</td>
<td>用于同步Master/Slave的内部命令</td>
</tr>
<tr>
<td class=" blog_h3">TIME</td>
<td>返回当前的服务器时间，返回值是两个元素的列表，第一个元素为UNIX时间戳，第二个是在当前秒内已经流逝的微秒数</td>
</tr>
<tr>
<td class=" blog_h3">TOUCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TOUCH key [key ...]</pre></p>
<p>修改键的最后访问时间，如果键不存在则不做任何操作</p>
</td>
</tr>
<tr>
<td class=" blog_h3">TTL</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TTL key</pre></p>
<p>返回一个键剩余的生存期，如果key不存在返回-2，如果key没有设置过期时间返回-1</p>
</td>
</tr>
<tr>
<td class=" blog_h3">TYPE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">TYPE key</pre></p>
<p>返回键所存储值的类型</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNSUBSCRIBE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">UNSUBSCRIBE [channel [channel ...]]</pre></p>
<p>取消客户端对频道的订阅，如果没有指定频道，则所有频道都被取消订阅</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNLINK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">UNLINK key [key ...]</pre></p>
<p>行为类似于DEL：移除指定的key，如果key不存在则忽略</p>
<p>不同之处是，内存回收工作由其他线程完成，因而该命令是非阻塞的</p>
</td>
</tr>
<tr>
<td class=" blog_h3">UNWATCH</td>
<td>为事务刷出所有先前监控的键（watched keys），如果你调用了EXEC、DISCARD则不需要手工调用该命令</td>
</tr>
<tr>
<td class=" blog_h3">WAIT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">WAIT numslaves timeout</pre></p>
<p>阻塞客户端，直到先前所有的写命令被成功传输、并被至少numslaves个Slave所确认。如果timeout到达，则命令立即返回。该命令返回实际确认了先前写命令的Slave的个数</p>
<p>一些备注：</p>
<ol>
<li>当WAIT命令返回时，在当前连接上下文下发起的所有写命令都确保，被WAIT返回值数量的Slave接收</li>
<li>如果该命令作为MULTI事务的一部分发起，不会引起阻塞，而仅仅会尽快的返回确认了先前写命令的Slave数量</li>
<li>指定timeout为0 则永远阻塞</li>
<li>调用者应该检查返回值，确认确认的Slave数量是否满足复制级别（replication level ，份数）的需求</li>
</ol>
<p><strong>WAIT命令和数据一致性</strong></p>
<p>WAIT命令并不能把Redis变成强一致性的数据库。尽管同步化的复制是复制状态机（replicated state machine）的一部分，但它不是唯一需要的事情</p>
<p>WAIT在集群故障转移的上下文中，的确增强了数据安全性。当一个写命令被传递给一个或者多个Slave时，假设Master宕机，Redis会尽可能推举最好（数据最完整）的Slave成为新的Master</p>
</td>
</tr>
<tr>
<td class=" blog_h3">WATCH</td>
<td>
<p>格式：<pre class="crayon-plain-tag">WATCH key [key ...]</pre></p>
<p>标记指定的键被监控，如果其值没有变化则执行MULTI/EXEC块</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZADD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZADD key [NX|XX] [CH] [INCR] score member [score member ...]</pre></p>
<p>将指定的member附带score（影响其排序）存放到有序集合中。如果member已经是该集合的成员，则仅仅更新它的score</p>
<p>score必须是字符串形式的双精度浮点数。+inf、-inf 是有效的值</p>
<p>XX，仅仅更新既有元素的score，绝不插入新元素<br />NX，不会更新既有元素，仅仅会插入新元素<br />CH，默认情况下，该命令返回添加的元素个数，使用该选项后，返回的是改变（添加、更新scoure）的元素个数<br />INCR，让该命令的行为类似于 ZINCRBY，仅接受一个score/member对</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZCARD</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZCARD key</pre></p>
<p>返回有序集合的基数（元素个数），如果key不存在则返回0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZCOUNT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZCOUNT key min max</pre></p>
<p>在min、max指定区间的score的元素的个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZINCRBY</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZINCRBY key increment member</pre></p>
<p>增加key对应的有序集合中member元素的score，增加的数值为increment</p>
<p>如果member不是key的成员，则在增加score前，将其添加到集合中并设置初始score为0.0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZINTERSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</pre></p>
<p>取numkeys个有序集合的交集，这些集合由key指定。交集被保存到destination，如果destination已经存在则覆盖之</p>
<p>默认情况下，destination集合中每个元素的score，为其来源集合中score的求和（作为交集成员的元素必须在每个输入集合中存在）</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZLEXCOUNT</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZLEXCOUNT key min max</pre></p>
<p>返回值在min、max之间的元素个数</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGE key start stop [WITHSCORES]</pre></p>
<p>有序集合中的元素依据score升序排列，score相同的元素以字典序排列。该命令返回指定索引范围内的元素，以数组形式返回</p>
<p>start、stop是基于0的索引，构成闭区间。索引值可以指定负数，表示倒数第几个元素（-1表示倒数第一个）</p>
<p>WITHSCORES，附带获得Score</p>
</td>
</tr>
<tr>
<td class=" blog_h3"><a id="zrangbylex"></a>ZRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGEBYLEX key min max [LIMIT offset count]</pre></p>
<p>当有序集合中所有元素的score一致时，可以使用该命令强制进行字典序排序。该命令返回有序集合中值位于min max之间的元素构成的数组</p>
<p>示例：</p>
<p><pre class="crayon-plain-tag">ZADD myindex 0 baaa
ZADD myindex 0 abbb
ZADD myindex 0 aaaa
ZADD myindex 0 bbbb

# 注意，前缀[或者(是必须的，表示包含或者不包含后面的值（开闭区间）
# 取得以a开头的条目
ZRANGEBYLEX myindex [a (b
1) "aaaa"
2) "abbb"

# 特殊符号 + 表示正无穷大， - 则表示负无穷大
ZRANGEBYLEX myindex [b +</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGEBYLEX key max min [LIMIT offset count]</pre></p>
<p>与上面的命令类似，但是倒序</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]</pre></p>
<p>返回有序集合中所有score位于min max之间（包含）的元素构成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZRANK key member</pre></p>
<p>获得有序集合中元素的排名，排名根据score从低到高产生，最低score排名为0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREM</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREM key member [member ...]</pre></p>
<p>从有序集合中移除指定的元素，不存在的元素被忽略。返回实际移除的元素个数，如果key存储的不是有序集合则返回错误</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYLEX</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYLEX key min max</pre></p>
<p>从有序集合中移除值在min max之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYRANK key start stop</pre></p>
<p>从有序集合中移除排名在start stop之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREMRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREMRANGEBYSCORE key min max</pre></p>
<p>从有序集合中移除score在start stop之间的所有元素</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGE key start stop [WITHSCORES]</pre></p>
<p>以倒序返回指定索引范围的元素构成的数组，元素先按score降序排列，score相同的则按字典序降序排列</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANGEBYSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]</pre></p>
<p>以倒序返回指定score范围的元素构成的数组</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZREVRANK</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZREVRANK key member</pre></p>
<p>返回一个元素的倒序排名，score最高的元素的排名为0</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZSCORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZSCORE key member</pre></p>
<p>返回一个元素的score。如果key不存在或者member不是其成员返回nil</p>
</td>
</tr>
<tr>
<td class=" blog_h3">ZUNIONSTORE</td>
<td>
<p>格式：<pre class="crayon-plain-tag">ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</pre></p>
<p>取由key...指定的numkeys个有序集合的并集，存储到destination。destination中元素的score为其来源集合的score的求和</p>
<p>WEIGHTS指定各输入集合的权重，此权重会乘以score以产生destination集合中元素的score的因素</p>
<p>AGGREGATE指定结果score的聚合方式，默认是SUM</p>
</td>
</tr>
<tr>
<td class=" blog_h3">SCAN<br />SSCAN<br />HSCAN<br />ZSCAN</td>
<td>
<p>格式：<pre class="crayon-plain-tag">SCAN cursor [MATCH pattern] [COUNT count]</pre></p>
<p>该命令以及SSCAN、HSCAN、ZSCAN用于遍历对应类型的容器：</p>
<ol>
<li>SCAN：遍历当前选中数据库的所有键，不支持集群</li>
<li>SSCAN：遍历某个集合的元素</li>
<li>HSCAN：遍历哈希的字段，及其关联的值</li>
<li>ZSCAN：遍历有序集合，及其关联的score</li>
</ol>
<p>由于这些命令支持增量迭代（incremental iteration），在每次调用中返回少量的元素，你可以在生产环境下使用它们而不会遭致KEYS、SMEMBERS等命令带来的服务器阻塞</p>
<p>尽管如此，SMEMBERS这样的阻塞命令能够得到容器在一个瞬间所包含的所有元素。SCAN类则没有一致性保证，因为在迭代期间，容器内容可能发生变化</p>
<p><strong>基本用法</strong></p>
<p>SCAN是基于游标的迭代器，这意味着，每次调用该命令时，服务器都会返回一个更新后的游标。此游标在下一次SCAN调用中需要。一次迭代从游标为0时开始，当服务器返回游标0时结束。示例：</p>
<pre class="crayon-plain-tag">redis 127.0.0.1:6379&gt; scan 0
1) "6"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"

redis 127.0.0.1:6379&gt; scan 6
1) "0"
2) 1) "key:5"
   2) "key:18"

# 在这个例子中，第一次迭代调用以0为游标，服务器返回6。下一次调用客户端需要传递6
# 在第二次迭代调用后，服务器返回0，表示没有更多的元素可以迭代

# 从0开始迭代，一直调用SCAN直到服务器返回0的迭代过程，称为完全迭代（full iteration）</pre>
<p><strong>SCAN特性</strong></p>
<p>SCAN以及其它*SCAN命令，在用户进行完全迭代时，能够保证：</p>
<ol>
<li>返回所有在迭代期间，位于容器中的元素。这意味着当迭代开始时元素E位于容器，迭代结束时E仍然在容器中，则E必然被迭代</li>
<li>类似的，如果一个元素在迭代开始前被移除，或者迭代结束后才加入，则它绝不会被迭代 </li>
</ol>
<p>尽管如此，由于游标的特性，*SCAN命令具有以下缺点：</p>
<ol>
<li style="text-align: left;">一个元素可能被返回多次，应用程序需要处理这种重复</li>
<li style="text-align: left;">在迭代期间，不是一直存在于容器中的元素，有可能被迭代，也可能不，这是无法确定的</li>
</ol>
<p><strong>返回元素的数量</strong></p>
<p>*SCAN命令不保证每次迭代返回元素的个数，返回0个元素也是允许的——这并不代表已经完成迭代</p>
<p>尽管如此，Redis会返回“合理”个数的元素：</p>
<ol>
<li>对于很大的容器，可能返回数十个元素</li>
<li>对于较小的容器，可能一次性返回所有元素</li>
</ol>
<p>你可以指定COUNT选项，来提示返回元素的“数量级”。COUNT的默认值是10，它只是一个提示性参数，Redis不作出精确保证</p>
<p><strong>MATCH选项</strong></p>
<p>使用该选项，你可以提供通配符 *，示例：</p>
<pre class="crayon-plain-tag">redis 127.0.0.1:6379&gt; scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379&gt; scan 228 MATCH *11*
1) "176"
2) (empty list or set)
redis 127.0.0.1:6379&gt; scan 176 MATCH *11* COUNT 1000  # 增大了count提示
1) "0"
2)  1) "key:611"
    2) "key:711"</pre>
<p><strong>并行迭代支持</strong></p>
<p>由于迭代的所有状态都在游标之中，而游标是每次调用产生的，不保存在服务器上，因此很多客户端对同一容器同时进行迭代是可以的</p>
<p><strong>中止迭代</strong></p>
<p>同样由于服务器端不保存迭代状态，你可以随时中途停止迭代，而不需要通知服务器</p>
<p><strong>损坏游标</strong></p>
<p>以损坏的游标调用*SCAN命令，例如负游标，其行为是未定义的，但是不会导致崩溃</p>
<p><strong>返回值</strong></p>
<p>*SCAN命令返回两元素的数组，第一个元素是下一次调用需要的游标，第二个元素：</p>
<ol>
<li>对于SCAN，是键的列表</li>
<li>对于SSCAN，是集合元素的列表</li>
<li>对于HSCAN，是[field,value]构成的数组</li>
<li>对于ZSCAN，是[element,score]构成的数组</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">客户端</span></div>
<div class="blog_h2"><span class="graybg">Java客户端</span></div>
<div class="blog_h3"><span class="graybg">Jedis</span></div>
<p>这是一个非常轻量易用的Java客户端，完全兼容Redis 2.8.x和3.x.x。</p>
<p>Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;redis.clients&lt;/groupId&gt;
    &lt;artifactId&gt;jedis&lt;/artifactId&gt;
    &lt;version&gt;2.9.0&lt;/version&gt;
    &lt;type&gt;jar&lt;/type&gt;
    &lt;scope&gt;compile&lt;/scope&gt;
&lt;/dependency&gt; </pre>
<p>简单的客户端代码：</p>
<pre class="crayon-plain-tag">Jedis jedis = new Jedis( "localhost", 6379 );
final String HELLO_KEY = "Hello";
final String NUMS_KEY = "Numbers";

jedis.set( HELLO_KEY, "World" );
jedis.get( HELLO_KEY );
jedis.lpush( NUMS_KEY, "1", "2", "3" );
jedis.lpop( NUMS_KEY );
// 使用Redis事务
Transaction multi = jedis.multi();
jedis.set( HELLO_KEY, "Alex" );
jedis.expire( HELLO_KEY, 10 );
multi.exec();

jedis.close();</pre>
<p>使用连接池：</p>
<pre class="crayon-plain-tag">JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxTotal( 100 );
JedisPool pool = new JedisPool( cfg, "localhost", 6379 );
Jedis jedis = pool.getResource();
jedis.auth( "foobared" );
jedis.set( "foo", "0" );
jedis.close();

jedis = pool.getResource();
jedis.auth( "foobared" );
jedis.incr( "foo" );

jedis.close();
pool.destroy();</pre>
<div class="blog_h3"><span class="graybg">spring-data</span></div>
<p>定义RestTemplate：</p>
<pre class="crayon-plain-tag">@Bean
JedisConnectionFactory jedisConnectionFactory() {
    JedisConnectionFactory jcf = new JedisConnectionFactory();
    jcf.setHostName( "10.4.37.22" );
    jcf.setPort( 6079 );
    jcf.setDatabase( 0 );
    jcf.setUsePool( true );
    JedisPoolConfig cfg = new JedisPoolConfig();
    cfg.setMaxTotal( 256 );
    cfg.setMaxIdle( 16 );
    jcf.setPoolConfig( cfg );
    return jcf;
}

@Bean
public RedisTemplate&lt;String, Object&gt; redisTemplateNoCompress() {
    final RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;String, Object&gt;();
    template.setConnectionFactory( jedisConnectionFactory() );
    template.setValueSerializer( new GenericToStringSerializer&lt;Object&gt;( Object.class ) );
    return template;
}</pre>
<p>使用RestTemplate： </p>
<pre class="crayon-plain-tag">// SET/GET操作
ValueOperations&lt;String, Object&gt; ops = template.opsForValue();
ops.set( key, value );
ops.get( key );</pre>
<p>定制串行化器，示例：</p>
<pre class="crayon-plain-tag">package com.dangdang.digital.spring.data.redis;

import net.jpountz.lz4.LZ4Compressor;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * 基于LZ4算法的RedisSerializer装饰器
 *
 * @param &lt;T&gt; 串行化处理的目标类型
 */
public class LZ4RedisSerializeDecorator&lt;T&gt; implements RedisSerializer&lt;T&gt; {

    private final int compressThreshold;

    private final RedisSerializer&lt;T&gt; serializer;

    private LZ4Factory factory;

    private LZ4Compressor compressor;

    private LZ4FastDecompressor decompressor;

    private static final int COMPRESS_HEAD_SIZE = Integer.SIZE / Byte.SIZE;

    private static final int NOCOMPRESS_HEAD_SIZE = 1;

    /**
     * 构造函数
     *
     * @param serializer        被装饰的串行化器
     * @param fastMode          是否使用快速模式，快速模式压缩比低但是压缩的速度快
     *                          快速模式不会让解压缩的速度更快
     * @param compressThreshold 启用压缩的最小字节数
     */
    public LZ4RedisSerializeDecorator( RedisSerializer&lt;T&gt; serializer, boolean fastMode, int compressThreshold ) {
        this.serializer = serializer;
        this.compressThreshold = compressThreshold;
        factory = LZ4Factory.fastestInstance();
        if ( fastMode ) {
            compressor = factory.fastCompressor();
        } else {
            compressor = factory.highCompressor();
        }
        decompressor = factory.fastDecompressor();
    }

    public byte[] serialize( T src ) throws SerializationException {
        byte[] srcBytes = serializer.serialize( src );
        int srcLen = srcBytes.length;
        ByteBuffer destBuf;
        if ( srcLen &lt; compressThreshold ) {
            destBuf = ByteBuffer.allocate( NOCOMPRESS_HEAD_SIZE + srcLen );
            destBuf.put( (byte) -1 );
            destBuf.put( srcBytes );
            onSerialize( false, srcLen, srcLen );
            return destBuf.array();
        } else {
            int maxDestLen = compressor.maxCompressedLength( srcLen );
            destBuf = ByteBuffer.allocate( COMPRESS_HEAD_SIZE + maxDestLen );
            destBuf.putInt( srcLen );
            byte[] destArray = destBuf.array();
            int compressedLength = compressor.compress( srcBytes, 0, srcLen, destArray, destBuf.position(), maxDestLen );
            onSerialize( true, srcLen, compressedLength );
            return Arrays.copyOfRange( destArray, 0, COMPRESS_HEAD_SIZE + compressedLength );
        }
    }

    public T deserialize( byte[] src ) throws SerializationException {
        if ( src == null ) return serializer.deserialize( src );
        ByteBuffer srcBuf = ByteBuffer.wrap( src );
        boolean noCompressed = srcBuf.array()[0] &lt; 0;
        if ( noCompressed ) {
            byte[] destBytes = Arrays.copyOfRange( src, 1, src.length );
            return serializer.deserialize( destBytes );
        } else {
            int destLen = srcBuf.getInt();
            byte[] destBytes = new byte[destLen];
            decompressor.decompress( src, srcBuf.position(), destBytes, 0, destLen );
            return serializer.deserialize( destBytes );
        }
    }

    protected void onSerialize( boolean compressed, int srcLen, int compressedLen ) {
    }
}</pre>
<div class="blog_h3"><span class="graybg">Lettuce</span></div>
<p>Lettuce是一个可扩容的、线程安全的Redis客户端，提供同步、异步、响应式（reactive）的API。</p>
<p>在不使用阻塞性、事务性操作 —— 例如BLPOP、MULTI/EXEC ——的情况下，多个Java线程可以共享一个连接。到Redis服务器的连接由netty框架管理。Letttuce支持Sentinel、Cluster等高级特性。</p>
<p>Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;biz.paluch.redis&lt;/groupId&gt;
    &lt;artifactId&gt;lettuce&lt;/artifactId&gt;
    &lt;version&gt;4.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>开启连接： </p>
<pre class="crayon-plain-tag">// 使用URI连接到本机的Redis实例，使用数据库0
// redis://[password@]host[:port][/databaseNumber]
// 如果是使用SSL的连接，协议部分改为rediss
RedisClient redisClient = RedisClient.create( "redis://passwd@localhost:6379/0" );
// 显式创建连接
StatefulRedisConnection&lt;String, String&gt; connection = redisClient.connect();</pre>
<p>资源清理：</p>
<pre class="crayon-plain-tag">// 资源清理
connection.close();
redisClient.shutdown();</pre>
<p>通过Sentinel连接：</p>
<pre class="crayon-plain-tag">// redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId
RedisClient redisClient = RedisClient.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster");</pre>
<p>连接到集群：</p>
<pre class="crayon-plain-tag">// Syntax: redis://[password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7379");
StatefulRedisClusterConnection&lt;String, String&gt; connection = redisClient.connect();</pre>
<p>基于Spring：</p>
<pre class="crayon-plain-tag">&lt;bean id="RedisClient" class="com.lambdaworks.redis.support.RedisClientFactoryBean"&gt;
    &lt;property name="uri" value="redis://localhost:6379"/&gt;
&lt;/bean&gt;</pre>
<p>同步API：</p>
<pre class="crayon-plain-tag">RedisCommands&lt;String, String&gt; syncCommands = connection.sync();
syncCommands.set( "Hello", "World" );</pre>
<p>异步API：</p>
<pre class="crayon-plain-tag">RedisAsyncCommands&lt;String, String&gt; asyncCommands = connection.async();
RedisFuture&lt;String&gt; future = asyncCommands.get( "Hello" );
String value = future.get( 1, TimeUnit.MINUTES );</pre>
<p>响应式API：</p>
<pre class="crayon-plain-tag">RedisStringReactiveCommands&lt;String, String&gt; reactiveCommands = connection.reactive();
reactiveCommands.get( "Hello" ).subscribe( new Action1&lt;String&gt;() {
    public void call( String value ) {
        System.out.println( value );
    }
} );</pre>
<div class="blog_h2"><span class="graybg">Python客户端</span></div>
<div class="blog_h3"><span class="graybg">redis-py</span></div>
<p>首先需要安装包：<pre class="crayon-plain-tag">sudo pip install redis</pre> </p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">import redis

if __name__ == '__main__':
    client = redis.StrictRedis(host='localhost', port=6379, db=0, password='passwd', encoding='utf-8')
    client.set('Hello', '世界')
    print(client.get('Hello').decode())

    # 管道，在一个请求中发送多个命令给服务区
    pipeline = client.pipeline()
    pipeline.set('Name', 'Alex')
    pipeline.get('Name')
    # 批量执行命令，返回结果的列表
    # transaction默认为true，能确保命令被原子的执行
    resp = pipeline.execute(transaction=True)
    print(resp)  # [True, b'Alex']</pre>
<div class="blog_h1"><span class="graybg">基于K8S</span></div>
<div class="blog_h2"><span class="graybg">集群搭建</span></div>
<p>下面是搭配Calico CNI的Redis集群样例。</p>
<div class="blog_h3"><span class="graybg">ConfigMap</span></div>
<p>配置文件内容：</p>
<pre class="crayon-plain-tag">port 6379
cluster-enabled yes
cluster-require-full-coverage no
cluster-node-timeout 5000
cluster-config-file nodes.conf
cluster-migration-barrier 1
appendonly yes
dir /data</pre>
<p>创建ConfigMap：</p>
<pre class="crayon-plain-tag">kubectl -n dev create configmap redis-cluster-conf --from-file=redis.conf</pre>
<div class="blog_h3"><span class="graybg">Pods</span></div>
<p>模板如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-cluster-8-pvc
  namespace: dev
spec:
  storageClassName: rook-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 256Mi

---

apiVersion: v1
kind: Pod
metadata:
  namespace: dev
  name: redis-cluster-8
  labels:
    app: redis-cluster
  annotations:
    "cni.projectcalico.org/ipAddrs": "[\"172.27.0.18\"]"
spec:
  terminationGracePeriodSeconds: 10
  containers:
  - name: redis
    image: docker.gmem.cc/redis-cluster
    ports:
    - containerPort: 6379
      name: client
    - containerPort: 16379
      name: gossip
    command:
    - /usr/local/bin/redis-server
    args:
    - /usr/local/etc/redis/redis.conf
    readinessProbe:
      exec:
        command:
        - sh
        - -c
        - "/usr/local/bin/redis-cli -h $(hostname) ping"
      initialDelaySeconds: 15
      timeoutSeconds: 5
    livenessProbe:
      exec:
        command:
        - sh
        - -c
        - "redis-cli -h $(hostname) ping"
      initialDelaySeconds: 20
      periodSeconds: 3
    resources:
      limits:
        cpu: 100m
        memory: 256Mi
    volumeMounts:
      - name: redis-cluster-conf-volume
        mountPath: /usr/local/etc/redis
      - name: redis-cluster-8-pv
        mountPath: /data
  volumes:
    - name: redis-cluster-conf-volume
      configMap:
        name: redis-cluster-conf
    - name: redis-cluster-8-pv
      persistentVolumeClaim:
        claimName: redis-cluster-8-pvc</pre>
<div class="blog_h3"><span class="graybg">创建集群</span></div>
<p>执行下面的命令创建集群：</p>
<pre class="crayon-plain-tag">./redis-trib.rb create --replicas 1 172.27.0.10:6379 172.27.0.11:6379 172.27.0.12:6379 \
    172.27.0.13:6379 172.27.0.14:6379 172.27.0.15:6379 172.27.0.16:6379 172.27.0.17:6379 \
    172.27.0.18:6379 172.27.0.19:6379

# &gt;&gt;&gt; Creating cluster                                                                                                                                                                
# &gt;&gt;&gt; Performing hash slots allocation on 10 nodes...                                                                                                                                 
# Using 5 masters:                                                                                                                                                                    
# 172.27.0.10:6379                                                                                                                                                                    
# 172.27.0.11:6379                                                                                                                                                                    
# 172.27.0.12:6379                                                                                                                                                                    
# 172.27.0.13:6379                                                                                                                                                                    
# 172.27.0.14:6379                                                                                                                                                                    
# Adding replica 172.27.0.15:6379 to 172.27.0.10:6379                                                                                                                                 
# Adding replica 172.27.0.16:6379 to 172.27.0.11:6379                                                                                                                                 
# Adding replica 172.27.0.17:6379 to 172.27.0.12:6379                                                                                                                                 
# Adding replica 172.27.0.18:6379 to 172.27.0.13:6379                                                                                                                                 
# Adding replica 172.27.0.19:6379 to 172.27.0.14:6379</pre>
<p>根据提示操作，默认前面5个节点作为Master，后面5个作为Slave。</p>
<div class="blog_h3"><span class="graybg">集群命令</span></div>
<p>执行如下命令连接到集群节点（Pod）：</p>
<pre class="crayon-plain-tag">kubectl -n dev exec redis-cluster-0 -it bash</pre>
<p>运行Redis命令行工具redis-cli，查看集群信息：</p>
<pre class="crayon-plain-tag"># CLUSTER INFO
cluster_state:ok
# 全部Slot已经分配
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:10
# 集群的大小取决于Master
cluster_size:5
cluster_current_epoch:10
cluster_my_epoch:1
cluster_stats_messages_sent:2301
cluster_stats_messages_received:2301</pre>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">3.0</span></div>
<ol style="list-style-type: undefined;">
<li>实现了Redis Cluster</li>
<li>多种性能提升</li>
</ol>
<div class="blog_h2"><span class="graybg">4.0</span></div>
<ol style="list-style-type: undefined;">
<li>PSYNC 2.0，增强复制性能，避免不必要的全量复制</li>
<li>UNLINK命令，异步删除，提升性能</li>
<li>SWAPDB，交换两个数据库</li>
<li>混合持久化，一种新的RDB-AOF混合持久化模式。开启后，AOF重写产生的文件将同时包含RDB格式的内容以及AOF格式的内容。其中RDB格式的部分记录已有数据，AOF则记录最新的增量数据。利用选项aof-use-rdb-preamble开启</li>
<li>集群模式兼容NAT和Docker</li>
<li>支持自动碎片整理activedefrag，相关配置：<br />
<pre class="crayon-plain-tag"># 开启自动内存碎片整理(总开关)
activedefrag yes
# 当碎片达到 100mb 时，开启内存碎片整理
active-defrag-ignore-bytes 100mb
# 当碎片超过 10% 时，开启内存碎片整理
active-defrag-threshold-lower 10
# 内存碎片超过 100%，则尽最大努力整理
active-defrag-threshold-upper 100
# 内存自动整理占用资源最小百分比
active-defrag-cycle-min 25
# 内存自动整理占用资源最大百分比
active-defrag-cycle-max 75
 </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">5.0</span></div>
<ol style="list-style-type: undefined;">
<li> 增加Stream类型，相比起List、Zset等获取元数据更加高效。作为订阅发布模型，比Pub/Sub起来支持：消息持久化、消息分组、ACK等</li>
<li>不再使用redis-trib脚本，直接用redis-cli --cluster来进行集群管理</li>
<li>自动碎片整理V2，配合Jemalloc，更快更智能</li>
<li>HyperLogLog算法改进，计数时内存效率更高</li>
</ol>
<div class="blog_h2"><span class="graybg">6.0</span></div>
<ol style="list-style-type: undefined;">
<li>引入多线程IO，需要设置<pre class="crayon-plain-tag">io-threads-do-reads yes</pre>以启用。注意： Redis的<span style="background-color: #c0c0c0;">多线程部分只是用来处理网络数据的读写和协议解析，执行命令仍然是单线程顺序执行</span>，这意味着<span style="background-color: #c0c0c0;">不需要去考虑</span>控制 key、lua、事务，LPUSH/LPOP 等等的<span style="background-color: #c0c0c0;">并发及线程安全问题</span>。参数<pre class="crayon-plain-tag">io-threads</pre>控制IO线程数量，4核的机器建议设置为2或3个线程，8核的建议设置为6个线程，线程数一定要小于机器核数。线程数不是越多越好，一般认为超过8没有意义</li>
<li>支持SSL协议连接</li>
<li>支持对连接进行ACL控制</li>
<li>集群代理。因为 Redis Cluster 内部使用的是P2P中的Gossip协议，每个节点既可以从其他节点得到服务，也可以向其他节点提供服务，没有中心的概念，通过一个节点可以获取到整个集群的所有信息。所以如果<span style="background-color: #c0c0c0;">应用连接Redis Cluster可以配置一个节点地址，也可以配置多个节点地址</span>。但需要注意<span style="background-color: #c0c0c0;">如果集群进行了增减节点的的操作，其应用也需要进行修改，这样会导致需要重启应用，非常的不友好。</span>通过使用 <pre class="crayon-plain-tag">redis-cluster-proxy</pre>可以与组成Redis集群的一组实例进行通讯，就像是单个实例一样</li>
</ol>
<div class="blog_h2"><span class="graybg">7.0</span></div>
<ol style="list-style-type: undefined;">
<li>新增Function自定义函数库，函数库支持持久化与可复制</li>
<li>Lua脚本（脚本本身代码）不再支持持久化和复制，仅对命令执行结果进行持久化和复制。</li>
<li>ACL支持对Pub/Sub channel的权限控制</li>
<li>支持Multi-Part AOF</li>
<li>支持Client-Eviction</li>
<li>支持Sharded-Pub/Sub</li>
<li>支持命令执行耗时直方图</li>
<li>支持子命令级别的性能统计</li>
<li>Ziplist编码替换为Listpack编码</li>
<li>支持Global Replication Buffer</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">不同之处</span></div>
<p>和其它键值存储方案相比，Redis具有以下特点：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>复杂数据类型支持</strong></p>
<p>Redis支持多种数据类型，并在其上定义了一些原子操作。这些数据类型和常用的基础数据结构很类似并且直接暴露给程序员，没有添加额外的抽象层</p>
</td>
</tr>
<tr>
<td>
<p><strong>驻留内存而又持久化到磁盘</strong></p>
<p>为了支持可能大于内存总量的数据集，同时保证高速的读写，Redis在内存、磁盘存储之间进行了权衡。作为磁盘存储格式的RDB、AOF不需要考虑随机访问的支持，因而它们格式很紧凑，并且总是以append-only的方式生成</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">内存占用</span></div>
<p>在64bit机器上，Redis内存用量大概如下：</p>
<ol>
<li>实例本身占用 1MB左右内存</li>
<li>100万简短的键 - 字符串值，大概占用100MB内存</li>
<li>100万键 - 哈希值，每个哈希有5个字段，大概占用200MB内存 </li>
</ol>
<p>你可以使用redis-benchmark来自己测试内存占用。</p>
<p>64bit机器会占用比32位机器更多的内存，特别是键值都比较小的时候，这是因为前者指针长度为8字节。</p>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">redis-cli报错(error) MOVED </span></div>
<p>命令行客户端默认不处理重定向指令，加上 -c 参数即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/redis-study-note">Redis学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/redis-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
