<?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; Redis</title>
	<atom:link href="https://blog.gmem.cc/tag/redis/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 03 Apr 2026 04:13:36 +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/debugging-slow-keydb</link>
		<comments>https://blog.gmem.cc/debugging-slow-keydb#comments</comments>
		<pubDate>Thu, 28 Jan 2021 07:04:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35755</guid>
		<description><![CDATA[<p>环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点： [crayon-69d0d59964222338806245/] KeyDB配置 KeyDB通过StatefulSet管理，一共有三个实例：  [crayon-69d0d59964229037408755/] 这三个实例： 由于反亲和设置，会在每个节点上各运行一个实例 启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写 故障描述 触发条件 出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现GET/PUT或者任何其它请求处理缓慢的情况。 故障特征 此故障有两个明显的特征： 故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间 请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种周期性的、长时间占用的锁有关。在锁被释放的间隙，请求可以被快速处理 故障分析 <a class="read-more" href="https://blog.gmem.cc/debugging-slow-keydb">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次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>
<div class="blog_h2"><span class="graybg">运行环境</span></div>
<p>这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点：</p>
<pre class="crayon-plain-tag"># kubectl get node -o wide
NAME              STATUS   VERSION   INTERNAL-IP      OS-IMAGE                KERNEL-VERSION              CONTAINER-RUNTIME
192.168.104.51    Ready    v1.18.3   192.168.104.51   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.72    Ready    v1.18.3   192.168.104.72   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.108   Ready    v1.18.3   192.168.104.108  CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9</pre>
<div class="blog_h2"><span class="graybg">KeyDB配置</span></div>
<p><a href="/keydb-study-note">KeyDB</a>通过StatefulSet管理，一共有三个实例： </p>
<pre class="crayon-plain-tag"># kubectl -n default get pod -o wide -l app.kubernetes.io/name=keydb
NAME             READY   STATUS    RESTARTS     IP             NODE            
keydb-0   1/1     Running   0            172.29.2.63     192.168.104.108 
keydb-1   1/1     Running   0            172.29.1.69     192.168.104.72  
keydb-2   1/1     Running   0            172.29.1.121    192.168.104.51</pre>
<p>这三个实例：</p>
<ol>
<li>由于反亲和设置，会在每个节点上各运行一个实例</li>
<li>启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写</li>
</ol>
<div class="blog_h1"><span class="graybg">故障描述</span></div>
<div class="blog_h2"><span class="graybg">触发条件</span></div>
<p>出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现<span style="background-color: #c0c0c0;">GET/PUT或者任何其它请求处理缓慢的情况</span>。</p>
<div class="blog_h2"><span class="graybg">故障特征</span></div>
<p>此故障有两个明显的特征：</p>
<ol>
<li>故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间</li>
<li>请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种<span style="background-color: #c0c0c0;">周期性的、长时间占用的锁</span>有关。在锁被释放的间隙，请求可以被快速处理</li>
</ol>
<div class="blog_h1"><span class="graybg">故障分析</span></div>
<div class="blog_h2"><span class="graybg">触发故障</span></div>
<p>我们将节点192.168.104.108强制关闭，这样实例keydb-0无法访问，另外两个节点无法和它进行Replication。</p>
<p>分别登录另外两个节点，监控GET/SET操作的性能：</p>
<pre class="crayon-plain-tag">kubectl -n default exec -it keydb-1 -- bash -c  \
  'while true; do key=keydb-1-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'

kubectl -n default exec -it keydb-2 -- bash -c \
 'while true; do key=keydb-2-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'</pre>
<p>监控Replication相关信息：</p>
<pre class="crayon-plain-tag">watch -- kubectl -n default exec -i keydb-1 -- keydb-cli info replication

watch -- kubectl -n default exec -i keydb-2 -- keydb-cli info replication</pre>
<p>监控KeyDB日志： </p>
<pre class="crayon-plain-tag">kubectl -n default logs  keydb-1 -f

kubectl -n default logs  keydb-2 -f</pre>
<p>经过一段时间，keydb-1请求处理随机延缓的情况出现：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; set hello world
OK
(1.24s)
127.0.0.1:6379&gt; set hello world
OK
(8.96s)
127.0.0.1:6379&gt; get hello
"world"
(5.99s)
127.0.0.1:6379&gt; get hello
"world"
(9.44s) </pre>
<p>此时keydb-2仍然正常运行，请求处理速度正常</p>
<div class="blog_h2"><span class="graybg">缓慢查询</span></div>
<p>获取keydb-1的慢查询，没有发现有价值的信息。而且延缓的时间没有计算在内：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; slowlog get 10                                         
1) 1) (integer) 7                                                      
   2) (integer) 1611833042                                             
   3) (integer) 14431               # 最慢的查询才耗时14ms                                            
   4) 1) "set"                                                         
      2) "keydb-1-1611833042"                                          
      3) "keydb-1-1611833042-val"                                      
   5) "127.0.0.1:38488"                                                
   6) ""                                                               
2) 1) (integer) 6                                                      
   2) (integer) 1611831322                                             
   3) (integer) 14486                                                  
   4) 1) "get"                                                         
      2) "keydb-1-1611831312"                                          
   5) "127.0.0.1:51680"                                                
   6) ""  </pre>
<div class="blog_h2"><span class="graybg">日志分析</span></div>
<p>部署KeyDB已经设置<pre class="crayon-plain-tag">--loglevel debug</pre>，以获得尽可能详尽的日志。</p>
<p>由于正在运行不间断执行SET/GET操作的脚本，因此日志量很大而刷屏，但是每隔一段时间就会出现卡顿。下面是keydb-1的日志片段：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:57:51.233 - Client closed connection
7:11:S 28 Jan 2021 08:57:51.251 - Accepted 127.0.0.1:44224
7:11:S 28 Jan 2021 08:57:51.252 - Client closed connection
7:12:S 28 Jan 2021 08:57:51.276 - Accepted 127.0.0.1:44226
7:11:S 28 Jan 2021 08:57:51.277 - Client closed connection
# 这一行日志之后，卡顿了10s。没有任何日志输出
7:11:S 28 Jan 2021 08:57:51.279 * Connecting to MASTER keydb-0.keydb:6379
7:11:S 28 Jan 2021 08:58:01.290 * Unable to connect to MASTER: Resource temporarily unavailable
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44228
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44264</pre>
<p>从日志信息上可以看到，卡顿前keydb-1正在尝试连接到已经宕机的keydb-0，这个连接尝试被阻塞10秒后报<pre class="crayon-plain-tag">EAGAIN</pre>错误。</p>
<p>阻塞期间SET/GET请求得不到处理，猜测原因包括：</p>
<ol>
<li>连接keydb-0的时候，占用了某种全局的锁，SET/GET请求也需要持有该锁</li>
<li>连接keydb-0、处理SET/GET请求，由同一线程负责</li>
</ol>
<p>第2种猜测应该不大可能，因为KeyDB宣称的优势之一就是，支持多线程处理请求。并且我们设置了参数<pre class="crayon-plain-tag">--server-threads 2</pre>，也就是有两个线程用于处理请求。</p>
<p>EAGAIN这个报错也没有参考价值，因为目前不卡顿的实例keydb-2输出的日志是一样的，只是没有任何卡顿：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:19:22.624 * Connecting to MASTER keydb-0.keydb:6379
# 仅仅耗时5ms即检测到连接失败
7:11:S 28 Jan 2021 08:19:22.629 * Unable to connect to MASTER: Resource temporarily unavailable</pre>
<div class="blog_h2"><span class="graybg">源码分析</span></div>
<div class="blog_h3"><span class="graybg">复制定时任务</span></div>
<p>我们使用的KeyDB版本是5.3.3，尝试用关键字“Connecting to MASTER”搜索，发现只有一个匹配，位于<pre class="crayon-plain-tag">replicationCron</pre>函数中。从函数名称上就可以看到，它是和复制（Replication）有关的定时任务。</p>
<p>KeyDB启动时会调用<pre class="crayon-plain-tag">initServer</pre>进行初始化，后者会在事件循环中每1ms调度一次<pre class="crayon-plain-tag">serverCron</pre>。serverCron负责后台任务的总体调度，它的一个职责就是，每1s调度一次replicationCron函数。</p>
<p>下面看一下replicationCron的源码：</p>
<pre class="crayon-plain-tag">/* Replication cron function, called 1 time per second. */
void replicationCron(void) {
    static long long replication_cron_loops = 0;
    serverAssert(GlobalLocksAcquired());
    listIter liMaster;
    listNode *lnMaster;
    listRewind(g_pserver-&gt;masters, &amp;liMaster);
    // 遍历当前实例的每一个Master
    while ((lnMaster = listNext(&amp;liMaster)))
    {
        redisMaster *mi = (redisMaster*)listNodeValue(lnMaster);
        std::unique_lock&lt;decltype(mi-&gt;master-&gt;lock)&gt; ulock;
        // 获得              Master的 客户端的 锁
        if (mi-&gt;master != nullptr)
            ulock = decltype(ulock)(mi-&gt;master-&gt;lock);

        /* Non blocking connection timeout? */
        // 如果当前复制状态为：正在连接到Master
        // 或者复制状态处于握手阶段（包含多个状态）且超时了
        if (mi-&gt;masterhost &amp;&amp;
            (mi-&gt;repl_state == REPL_STATE_CONNECTING ||
            slaveIsInHandshakeState(mi)) &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么取消握手 —— 取消进行中的非阻塞连接尝试，或者取消进行中的RDB传输
            serverLog(LL_WARNING,"Timeout connecting to the MASTER...");
            cancelReplicationHandshake(mi);
        }

        /* Bulk transfer I/O timeout? */
        // 如果当前正在接收来自Master的RDB文件且超时了
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;repl_state == REPL_STATE_TRANSFER &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in keydb.conf to a larger value.");
            // 那么取消握手
            cancelReplicationHandshake(mi);
        }

        /* Timed out master when we are an already connected replica? */
        // 如果当前复制状态为：已连接。而且超时之前没有活动（正常情况下有心跳维持）
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp; mi-&gt;repl_state == REPL_STATE_CONNECTED &amp;&amp;
            (time(NULL)-mi-&gt;master-&gt;lastinteraction) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么释放掉客户端资源
            serverLog(LL_WARNING,"MASTER timeout: no data nor PING received...");
            if (FCorrectThread(mi-&gt;master))
                freeClient(mi-&gt;master);
            else
                freeClientAsync(mi-&gt;master);
        }

        /* Check if we should connect to a MASTER */
        // 上面几个分支都不会匹配我们的场景，因为keydb-0已经宕机，因此
        // 状态必然是REPL_STATE_CONNECT
        if (mi-&gt;repl_state == REPL_STATE_CONNECT) {
            // 这一行就是卡顿前的日志
            serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
                mi-&gt;masterhost, mi-&gt;masterport);
            // 发起连接
            if (connectWithMaster(mi) == C_OK) {
                serverLog(LL_NOTICE,"MASTER &lt;-&gt; REPLICA sync started");
            }
        }

        // 每秒钟发送心跳给Master
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp;
            !(mi-&gt;master-&gt;flags &amp; CLIENT_PRE_PSYNC))
            replicationSendAck(mi);
    }

    // 后面处理和本实例的Slave有关的逻辑，例如发送心跳。和我们的场景无关，略...
}</pre>
<p>很明显，卡顿是因为调用<pre class="crayon-plain-tag">connectWithMaster</pre>导致的。从代码注释也可以看到，KeyDB期望这个连接操作是非阻塞的，但是不知道为何，在我们的场景中严重的阻塞了。</p>
<p>进一步查看connectWithMaster的代码：</p>
<pre class="crayon-plain-tag">int connectWithMaster(redisMaster *mi) {
    int fd;

    fd = anetTcpNonBlockBestEffortBindConnect(NULL,
        mi-&gt;masterhost,mi-&gt;masterport,NET_FIRST_BIND_ADDR);
    if (fd == -1) {
        int sev = g_pserver-&gt;enable_multimaster ? LL_NOTICE : LL_WARNING;
        // 这一行是卡顿10s后的日志，因此阻塞发生在anetTcpNonBlockBestEffortBindConnect函数中
        serverLog(sev,"Unable to connect to MASTER: %s", strerror(errno));
        return C_ERR;
    }
    // ...
}

int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port,
                                         char *source_addr)
{
    return anetTcpGenericConnect(err,addr,port,source_addr,
            // 非阻塞 + BestEffort绑定
            ANET_CONNECT_NONBLOCK|ANET_CONNECT_BE_BINDING);
}


static int anetTcpGenericConnect(char *err, char *addr, int port,
                                 char *source_addr, int flags)
{
    int s = ANET_ERR, rv;
    char portstr[6];  /* strlen("65535") + 1; */
    struct addrinfo hints, *servinfo, *bservinfo, *p, *b;

    snprintf(portstr,sizeof(portstr),"%d",port);
    memset(&amp;hints,0,sizeof(hints));
    // 不指定地址族，这会触发getaddrinfo同时进行A/AAAA查询
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    // 根据Master的主机名查找得到IP地址信息（addrinfo）列表
    if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
        anetSetError(err, "%s", gai_strerror(rv));
        return ANET_ERR;
    }
    // 遍历Master的IP地址列表
    for (p = servinfo; p != NULL; p = p-&gt;ai_next) {
        // 创建套接字，如果socket/connect调用失败，则尝试下一个
        if ((s = socket(p-&gt;ai_family,p-&gt;ai_socktype,p-&gt;ai_protocol)) == -1)
            continue;
        // 设置套接字选项SO_REUSEADDR
        if (anetSetReuseAddr(err,s) == ANET_ERR) 
            goto error;
        // 设置套接字选项 SO_REUSEPORT
        if (flags &amp; ANET_CONNECT_REUSEPORT &amp;&amp; anetSetReusePort(err, s) != ANET_OK)
            goto error;
        // 调用fcntl设置 O_NONBLOCK
        if (flags &amp; ANET_CONNECT_NONBLOCK &amp;&amp; anetNonBlock(err,s) != ANET_OK)
            goto error;
        if (source_addr) {
            int bound = 0;
            /* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
            // 解析源地址
            if ((rv = getaddrinfo(source_addr, NULL, &amp;hints, &amp;bservinfo)) != 0)
            {
                anetSetError(err, "%s", gai_strerror(rv));
                goto error;
            }
            for (b = bservinfo; b != NULL; b = b-&gt;ai_next) {
                // 绑定到第一个源地址
                if (bind(s,b-&gt;ai_addr,b-&gt;ai_addrlen) != -1) {
                    bound = 1;
                    break;
                }
            }
            freeaddrinfo(bservinfo);
            if (!bound) {
                // 绑定源地址失败，跳转到Best Effort绑定
                anetSetError(err, "bind: %s", strerror(errno));
                goto error;
            }
        }
        // 发起连接
        if (connect(s,p-&gt;ai_addr,p-&gt;ai_addrlen) == -1) {
            // 我们的场景下套接字是非阻塞的，因此这里会立即返回EINPROGRESS，属于预期行为
            if (errno == EINPROGRESS &amp;&amp; flags &amp; ANET_CONNECT_NONBLOCK)
                goto end;
            // 其它错误均认为失败，尝试连接下一个Master地址
            close(s);
            s = ANET_ERR;
            continue;
        }

        goto end;
    }
    if (p == NULL)
        anetSetError(err, "creating socket: %s", strerror(errno));

error:
    if (s != ANET_ERR) {
        close(s);
        s = ANET_ERR;
    }

end:
    freeaddrinfo(servinfo);

    // 上面指定源地址，绑定失败时跳转到此处。尝试不指定源地址来连接
    if (s == ANET_ERR &amp;&amp; source_addr &amp;&amp; (flags &amp; ANET_CONNECT_BE_BINDING)) {
        return anetTcpGenericConnect(err,addr,port,NULL,flags);
    } else {
        return s;
    }
}</pre>
<p>尽管可以确定connectWithMaster调用的anetTcpGenericConnect就是发生阻塞的地方，但是从代码上看不出什么问题，就是简单的socket、bind，外加一个非阻塞的connect操作。</p>
<div class="blog_h3"><span class="graybg">请求处理逻辑</span></div>
<p>从现象上我们已经看到了，复制定时器卡顿的时候，请求处理也无法进行。通过代码分析，也明确了卡顿期间，复制定时器持有Master的客户端的锁。</p>
<p>那么，关于请求处理（线程？）会和复制定时器产生锁争用的猜测是否正确呢？</p>
<div class="blog_h2"><span class="graybg">单步跟踪</span></div>
<p>为了精确定位阻塞的代码，我们使用GDB进行单步跟踪：</p>
<pre class="crayon-plain-tag">#              需要特权模式，否则无法加载符号表
docker run -it --rm --name gdb --privileged --net=host --pid=host --entrypoint gdb docker.gmem.cc/debug

(gdb) attach 449
(gdb) break replication.cpp:3084
# 连续执行s，以step into anet.c
(gdb) s
# 连续执行n命令
(gdb) n
# 卡顿后，查看变量
# 解析的地址
(gdb) p addr
$2 = 0x7f2f31411281 "keydb-0.keydb"
# getaddrinfo的返回值
(gdb) p rv
$3 = -3</pre>
<p>进入anetTcpGenericConnect后，逐行执行，多次测试，均在<pre class="crayon-plain-tag">anet.c</pre>的291行出现卡顿：</p>
<pre class="crayon-plain-tag">if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
    anetSetError(err, "%s", gai_strerror(rv));
    return ANET_ERR;
}</pre>
<p>也就是说，调用getaddrinfo函数耗时可能长达数秒。这是来自glibc的标准函数，用于将主机名解析为IP地址。</p>
<p>调试过程中发现此函数的返回值是-3，我们的场景中，需要解析的地址是keydb-0.keydb，卡顿时函数的返回值是-3，<pre class="crayon-plain-tag">man getaddrinfo</pre>可以了解到此返回值的意义：</p>
<p style="padding-left: 30px;">EAI_AGAIN  The name server returned a temporary failure indication. Try again later.</p>
<p>乍看起来，好像是<a href="/tcp-ip-study-note#dns">DNS</a>服务器，也就是K8S的<a href="/coredns-study-note">CoreDNS</a>存在问题。但无法解释此时keydb-2.keydb没有受到影响？</p>
<div class="blog_h2"><span class="graybg">检查CoreDNS</span></div>
<p>为了确认CoreDNS是否存在问题，我们分别在宿主机上、两个实例的网络命名空间中进行验证：</p>
<pre class="crayon-plain-tag"># nslookup keydb-0.keydb.default.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.default.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb: SERVFAIL</pre>
<p>反复测试循环测试，没有任何解析缓慢的现象。此外，查看CoreDNS的日志，我们也发现了来自keydb-1.keydb和keydb-2.keydb的查询请求，请求都是通过UDP协议发送的，处理耗时都是亚毫秒级别。</p>
<p>也就是说，从KeyDB实例所在宿主机/命名空间到CoreDNS的网络链路、CoreDNS服务器自身，都没有问题。</p>
<p>这就让人头疼了……难道问题出在getaddrinfo函数内部？或者在单步跟踪时判断错误，问题和DNS无关？为了确认，我们在CoreDNS上动了点手脚，强制将keydb-0.keydb解析到一个不存在的IP地址：</p>
<pre class="crayon-plain-tag">.:53 {
    # ...
    hosts {
        192.168.144.51  keydb-1.keydb
    }
    # ...
}</pre>
<p>结果很快，卡顿的问题就消失了。所以，我们更加怀疑问题出在getaddrinfo函数上了。</p>
<div class="blog_h2"><span class="graybg">调试getaddrinfo</span></div>
<p>查看文件/etc/lsb-release，可以看到KeyDB镜像是基于Ubuntu 18.04.4 LTS构建的，使用的libc6版本是2.27-3ubuntu1。</p>
<p>在launchpad.net找到了它的<a href="http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb">调试文件</a>和<a href="http://launchpadlibrarian.net/365856911/glibc-source_2.27-3ubuntu1_all.deb">源码</a>。下载deb包，解压后复制到GDB容器，然后设置一下调试文件目录，就可以step into到glibc的代码进行跟踪了：</p>
<pre class="crayon-plain-tag">ar x libc6-dbg_2.27-3ubuntu1_amd64.deb
tar -xf data.tar.xz 

# 拷贝到我们正在运行GDB的容器
docker cp usr gdb:/root

# 修改调试文件搜索目录
(gdb) set debug-file-directory /root/usr/lib/debug
# 打断点，下面是缓慢的执行路径
(gdb) b anet.c:291
(gdb) b getaddrinfo.c:342
(gdb) b getaddrinfo.c:786  
# (gdb) print fct4
# $2 = (nss_gethostbyname4_r) 0x7f32f97e9a70 &lt;_nss_dns_gethostbyname4_r&gt;
(gdb) b dns-host.c:317
(gdb) b res_query.c:336
(gdb) b res_query.c:495                       # invoke __res_context_querydomain
(gdb) b res_query.c:601                       # invoke __res_context_query    
(gdb) b res_query.c:216                       # invoke __res_context_send
(gdb) b res_send.c:1066 if buflen==45         # send_dg</pre>
<p>通过调试，我们发现getaddrinfo会依次对4个名字进行DNS查询：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>CoreDNS的日志显示，所有请求都快速的处理完毕：</p>
<pre class="crayon-plain-tag">4242 "A IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000215337s
38046 "AAAA IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000203934s

23194 "A IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000301011s
23722 "AAAA IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000125386s

36552 "A IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000281247s
217 "AAAA IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000150689s

6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000196686s
6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000157011s </pre>
<p>最后一个名字，也就是传递给getaddrinfo的原始请求keydb-0.keydb.的处理过程有以下值得注意的点：</p>
<ol>
<li>从GDB角度来看，<span style="background-color: #c0c0c0;">卡顿就是在解析该名字时出现</span></li>
<li>从CoreDNS日志上看，没有AAAA请求。由于KeyDB<span style="background-color: #c0c0c0;">指定了AF_UNSPEC，getaddrinfo会同时发送并等待A/AAAA应答</span>。可能<span style="background-color: #c0c0c0;">因为某种原因，该名字的AAAA解析过程没有完成，导致getaddrinfo一直等待到超时</span>。作为对比，没有卡顿的keydb-2的A/AAAA查询处理过程都是正常的</li>
<li>其它名字是一次A请求，一次AAAA请求。该名字却是两次A请求，而且，<span style="background-color: #c0c0c0;">第一次A请求日志出现了数秒后，第二次日志才出现</span>。有可能第二次是getaddrinfo没有收到应答而进行的重试</li>
<li>前三个名字分别的错误码是NXDOMAIN，该名字的错误码却是<a href="/tcp-ip-study-note#dns-rtnmsg">NOERROR</a>。通过nslookup/dig查询，错误码却是SERVFAIL，难道是CoreDNS日志有BUG？尽管如此，是否不同的错误码影响了getaddrinfo的行为</li>
</ol>
<div class="blog_h2"><span class="graybg">抓包分析</span></div>
<p>glibc的代码是优化过（<a href="https://stackoverflow.com/questions/30089652/glibc-optimizations-required">也必须优化</a>）的，GDB跟踪起来相当耗时，因此我们打算换一个角度来定位问题。基于上一节的分析，我们相信实例keydb-1.keydb在发送DNS请求的时候存在超时或丢包的情况，可以抓包来证实：</p>
<pre class="crayon-plain-tag"># 进入keydb-1.keydb的网络命名空间
nsenter -t 449 --net
# 抓包
tcpdump -i any -vv -nn udp port 53</pre>
<p>抓包的结果如下： </p>
<p style="padding-left: 30px;"><span style="background-color: #c0c0c0;">对 keydb-0.keydb.default.svc.cluster.local.  的A请求</span><br /><em> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x95aa!] 22719+ A? keydb-0.keydb.default.svc.cluster.local. (68)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0x3f57!] 22719 NXDomain*- q: A? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.default.svc.cluster.local.  的AAAA请求，注意，仍然使用之前的UDP套接字</span><br /> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x4d76!] 41176+ AAAA? keydb-0.keydb.default.svc.cluster.local. (68)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0xf722!] 41176 NXDomain*- q: AAAA? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.svc.cluster.local. 的A请求，注意，这里使用了新的UDP套接字</span><br /> <em>172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x3b5e!] 45156+ A? keydb-0.keydb.svc.cluster.local. (63)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x21ce!] 45156 NXDomain*- q: A? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.svc.cluster.local. 的AAAA请求</span><br /> 172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x8a4e!] 18036+ AAAA? keydb-0.keydb.svc.cluster.local. (63)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x70be!] 18036 NXDomain*- q: AAAA? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的A请求</span><br /><em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5054!] 2718+ A? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x36c4!] 2718 NXDomain*- q: A? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的AAAA请求</span><br /> <em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5147!] 61098+ AAAA? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain，这里开始，我们保留时间戳那一行日志</span><br /><em>14:42:15.028168 IP (tos 0x0, ttl 63, id 44636, offset 0, flags [DF], proto UDP (17), length 180)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x37b7!] 61098 NXDomain*- q: AAAA? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb的A请求，这里还没有出现卡顿</span><br /><em>14:42:15.028328 IP (tos 0x0, ttl 64, id 30583, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答，抓包和我们nslookup/dig的错误码一致，CoreDNS日志显示的应该不正常</span><br /><span style="background-color: #c0c0c0;">猜测“有可能第二次是getaddrinfo没有收到应答而进行的重试”被排除，至少说没收到应答不是网络层面的原因</span><br /><em>14:42:15.028651 IP (tos 0x0, ttl 63, id 44637, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em><br /><span style="background-color: #c0c0c0;">再一次对keydb-0.keydb.的A请求，<strong>注意时间戳，刚好5秒之后</strong>，这是默认DNS请求超时。<strong>还是使用之前的套接字</strong></span><br /><em>14:42:20.029271 IP (tos 0x0, ttl 64, id 33006, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答</span><br /><em>14:42:20.029812 IP (tos 0x0, ttl 63, id 46397, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em></p>
<p>通过上述分析我们可以相信，keydb-1.keydb容器到CoreDNS之间的DNS通信是没有问题的。但是，getaddrinfo似乎没有收到keydb-0.keydb的第一次应答，并且在超时（5s）之后进行重试</p>
<div class="blog_h2"><span class="graybg">Conntrack竞态条件</span></div>
<p>tcpdump和应用程序之间，还有个netfilter框架。回想起之前阅读过的文章：<a href="/dns-problems-on-k8s">Kubernetes上和DNS相关的问题</a>，conntrack相关的竞态条件可能导致DNS查询5秒超时。遗憾的是，这里的故障和此竞态条件无关：</p>
<ol>
<li>通过<pre class="crayon-plain-tag">conntrack -S</pre>看到的<pre class="crayon-plain-tag">insert_failed</pre>是0</li>
<li>故障一旦出现，就每次都会超时5s，没有竞态条件的随机性</li>
<li>如果是conntrack竞态条件导致，无法解释为什么前面3个名字解析正常，也无法解释为什么CoreDNS中配置一个静态解析故障就消失</li>
</ol>
<div class="blog_h1"><span class="graybg">深入理解</span></div>
<div class="blog_h2"><span class="graybg">getaddrinfo</span></div>
<p>在IPv4中，我们使用<pre class="crayon-plain-tag">gethostbyname</pre>实现主机名到地址的解析。<pre class="crayon-plain-tag">getaddrinfo</pre>也用于地址解析，而且它是协议无关的，既可用于IPv4也可用于IPv6。它的原型如下：</p>
<pre class="crayon-plain-tag">int getaddrinfo(const char* hostname,  // 主机名，可以使用IP地址或者DNS名称
                const char* service,   // 服务名，可以使用端口号或者/etc/services中的服务名
                const struct addrinfo* hints, // 可以NULL，或者一个addrinfo，提示调用者想得到的信息类型
                struct addrinfo** res);  // 解析得到的addrinfo，地址的链表</pre>
<p>此函数返回的是套接字地址信息的链表，地址信息存储在下面的addrinfo结构中。参数<pre class="crayon-plain-tag">hints</pre>会影响getaddrinfo的行为，提示信息同样存放在addrinfo结构中：</p>
<pre class="crayon-plain-tag">struct addrinfo
{
  // 额外的提示标记
  int ai_flags;	
  // 提示需要查询哪些地址族，默认AF_UNSPEC，这意味着同时查询IPv4和IPv6地址
  // 也就是同时发起A/AAAA查询
  int ai_family;
  // 提示偏好的套接字类型，例如SOCK_STREAM|SOCK_DGRAM，默认可以返回任何套接字类型
  int ai_socktype;
  // 提示返回的套接字地址的协议类型
  int ai_protocol;

  // 套接字地址
  socklen_t ai_addrlen;
  struct sockaddr *ai_addr;
  // ...
  // 指向链表的下一条目
  struct addrinfo *ai_next;
};</pre>
<p>很多软件调用getaddrinfo的时候，都会指定AF_UNSPEC（或者不提供hints，效果一样），例如KeyDB。但是，很多运行环境根本没有IPv6支持，这就凭白的给DNS服务器增加了负担。这也是在K8S中查看CoreDNS日志，总是会发现很多AAAA记录的原因。</p>
<div class="blog_h3"><span class="graybg">解析流程概览</span></div>
<p>KeyDB 5.3.3使用的glibc版本是2.27。函数getaddrinfo过于冗长，这里就不贴出来了，大概梳理一下：</p>
<ol>
<li>如果可能，它会通过/var/run/nscd/socket访问DNS缓存服务，我们没有这个服务</li>
<li>初始化NSS的hosts数据库，如果没有在文件中配置，则默认使用<pre class="crayon-plain-tag">hosts: dns [!UNAVAIL=return] files</pre>，我们的环境下配置是<pre class="crayon-plain-tag">hosts: files dns</pre></li>
<li>通过<a href="/linux-faq#nss">NSS</a>进行名字查询，实际上是调用<pre class="crayon-plain-tag">gethostbyname4_r</pre>函数：
<ol>
<li>查找files源，调用<pre class="crayon-plain-tag">_nss_files_gethostbyname4_r</pre>函数，也就是打开/etc/hosts查找。K8S容器中，/etc/hosts中仅仅存在当前Pod的条目，因此files源不会匹配</li>
<li>查找dns源，调用<pre class="crayon-plain-tag">_nss_dns_gethostbyname4_r</pre>函数：
<ol>
<li>读取/etc/resolv.conf构建<pre class="crayon-plain-tag">resolv_context</pre>。我们的环境下，配置文件内容为：<br />
<pre class="crayon-plain-tag">nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5</pre>
</li>
<li>
<p>调用<pre class="crayon-plain-tag">__res_context_search</pre>， 执行DNS查找逻辑。它会<span style="background-color: #c0c0c0;">将上面的search domain作为域名后缀，产生多个名字，逐个尝试。每个名字查询失败时都会重试，重试时尽可能选择不同的DNS服务器。可能同时发起A/AAA查询</span></p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">DNS搜索逻辑</span></div>
<p>__res_context_search()首先会计算一下，待查找名字中的dot的数量。如果<span style="background-color: #c0c0c0;">名字以dot结尾，或者dot数量大于等于ndots</span>，则直接调用<pre class="crayon-plain-tag">__res_context_querydomain</pre>向DNS服务器发请求，该函数会同时发起A/AAAA查询。</p>
<p>否则，它会根据/etc/resolv.conf中的search domain列表，给待查找的名字加后缀，然后多次向DNS服务器发请求。我们的环境下，待查找名字为keydb-0.keydb，getaddrinfo函数会依次尝试：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>需要注意：</p>
<ol>
<li>向DNS服务器发请求，仍然是由__res_context_querydomain()负责</li>
<li>一旦查找成功，就立即返回不再尝试其它search domain</li>
<li><span style="background-color: #c0c0c0;">不加修饰的原始名字，会放在最后尝试</span></li>
</ol>
<p>在K8S中，*.cluster.local一般都由CoreDNS自身负责，处理速度会很快。至于keydb-0.keydb.的处理速度，如果为CoreDNS配置了上游DNS，则处理速度依赖于外部环境。</p>
<p>__res_context_querydomain仅仅是在domain参数不为空的时候，将name和domain连接起来，然后调用<pre class="crayon-plain-tag">__res_context_query</pre>函数。</p>
<div class="blog_h3"><span class="graybg">DNS查询过程</span></div>
<p>__res_context_query负责和DNS服务器的交互，完成<span style="background-color: #c0c0c0;">单个名字的DNS查</span>询。它会调用<pre class="crayon-plain-tag">__res_context_mkquery</pre><span style="background-color: #c0c0c0;">构建一个查询请求（对应DNS报文），然后发送</span>，然后等待应答。这是一个阻塞的过程，KeyDB在期望非阻塞的代码路径下调用getaddrinfo且没有任何缓存机制，同时还加了锁，我觉得是不妥的。这导致DNS缓慢/不可用会极大的影响KeyDB的服务质量。</p>
<p>发送DNS请求的代码在<pre class="crayon-plain-tag">__res_context_send</pre>中，<span style="background-color: #c0c0c0;">重试逻辑</span>发生在该函数中，我们的环境下重试次数为2，这解释了两次keydb-0.keydb. A查询：</p>
<pre class="crayon-plain-tag">//                  重试次数，statp-&gt;retry为2
for (try = 0; try &lt; statp-&gt;retry; try++) {
    // 如果有多个DNS服务器，重试时会轮询它们
    for (unsigned ns_shift = 0; ns_shift &lt; statp-&gt;nscount; ns_shift++)
    {
    unsigned int ns = ns_shift + ns_offset;
    if (ns &gt;= statp-&gt;nscount)
        ns -= statp-&gt;nscount;

    same_ns:
    if (__glibc_unlikely (v_circuit)) {
        // ...
    } else {
        // 使用UDP方式发送请求
        n = send_dg(statp, buf, buflen, buf2, buflen2,
                &amp;ans, &amp;anssiz, &amp;terrno,
                ns, &amp;v_circuit, &amp;gotsomewhere, ansp,
                ansp2, nansp2, resplen2, ansp2_malloced);
        if (n &lt; 0)
            return (-1);
        if (n == 0 &amp;&amp; (buf2 == NULL || *resplen2 == 0))
            // 如果有多个DNS服务器的时候，会尝试下一个
            goto next_ns;
        // ...
    }
    return (resplen);
next_ns: ;
   } /*foreach ns*/
} /*foreach retry*/</pre>
<p>通常情况下，都是通过UDP协议进行DNS查询的，因此会调用<pre class="crayon-plain-tag">send_dg</pre>函数。在我们的场景中，两次尝试均5秒超时（尽管抓包显示应答报文很快就收到），__res_context_send设置错误码ETIMEDOUT，返回-1：</p>
<pre class="crayon-plain-tag">__res_iclose(statp, false);
if (!v_circuit) {
    if (!gotsomewhere)
        __set_errno (ECONNREFUSED);	/* no nameservers found */
    else
        __set_errno (ETIMEDOUT);	/* no answer obtained */
} else
    __set_errno (terrno);
return (-1);</pre>
<p>而它的调用者__res_context_query则在返回值是-1的时候，设置错误码TRY_AGAIN，这就是我们从KeyDB日志上看到报错The name server returned a temporary failure indication的原因：</p>
<pre class="crayon-plain-tag">if (n &lt; 0) {
    RES_SET_H_ERRNO(statp, TRY_AGAIN);
    return (n);
}</pre>
<div class="blog_h3"><span class="graybg">缓慢之源</span></div>
<p>缓慢的根源是send_dg函数，它阻塞了5秒。该函数的原型如下：</p>
<pre class="crayon-plain-tag">// 如果没有错误，返回第一个应答的字节数
// 对于可恢复错误，返回0；对于不可恢复错误，返回负数
static int send_dg(
    // 各种选项、DNS服务器列表、指向DNS服务器的套接字（文件描述符）
    res_state statp,
    // 查询请求1的缓冲区 和 长度
	const u_char *buf, int buflen, 
    // 查询请求2的缓冲区 和 长度
    const u_char *buf2, int buflen2,
    // 收到的第1个应答   和 最大长度
	u_char **ansp,      int *anssizp,
    // 出现错误时，将errno设置到此字段
	int *terrno, 
    // 使用的DNS服务器的序号
    int ns, 
    // 如果由于UDP数据报的限制而导致截断，则v_circuit设置为1，提示调用者使用TCP方式重试
    int *v_circuit, 
    // 提示访问DNS服务器时，是拒绝服务还是超时。如果是超时则设置为1
    int *gotsomewhere,
    // 提示遇到超长应答的时候，是否重新分配缓冲区
    u_char **anscp,
    // 收到的第2个应答 和 最大长度
	u_char **ansp2, int *anssizp2, 
    // 第2个应答的实际长度 是否为第2个应答重新分配了缓冲区
    int *resplen2, int *ansp2_malloced);</pre>
<p>该函数会向指定序号的DNS服务器发送DNS查询。它同时支持IPv4/IPv6查询，你可以传递两个查询请求，分别放在buf和buf2参数中。<span style="background-color: #c0c0c0;">如果提供了两个查询请求，默认使用并行方式发送查询</span>。设置选项<pre class="crayon-plain-tag">RES_SINGLKUP</pre>可以强制串行发送；设置选项<pre class="crayon-plain-tag">RES_SNGLKUPREOP</pre>可以<span style="background-color: #c0c0c0;">强制串行发送，同时总是关闭并重新打开套接字</span>，这样可以和某些行为异常的DNS服务器一起工作。</p>
<p>由于请求可以并行发送，因此应答到达的顺序是不确定的。<span style="background-color: #c0c0c0;">先收到的</span>应答会存放在ansp中，入参最大长度anssizp。入参anscp用于提示，应答过长的时候的处理方式：</p>
<ol>
<li>如果anscp不为空：则自动分配新的缓冲区，并且ansp、anscp都被修改为指向该缓冲区</li>
<li>如果anscp为空：则过长的部分被截断，DNS包头的TC字段被设置为1</li>
</ol>
<p>glibc的2.27-3ubuntu1版本中send_dg的完整实现如下：</p>
<pre class="crayon-plain-tag">static int
send_dg(res_state statp,
	const u_char *buf, int buflen, const u_char *buf2, int buflen2,
	u_char **ansp, int *anssizp,
	int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp,
	u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced)
{
	const HEADER *hp = (HEADER *) buf;
	const HEADER *hp2 = (HEADER *) buf2;
	struct timespec now, timeout, finish;
	struct pollfd pfd[1];
	int ptimeout;
	struct sockaddr_in6 from;
	int resplen = 0;
	int n;

	/*
	 * Compute time for the total operation.
	 */
	int seconds = (statp-&gt;retrans &lt;&lt; ns); // 0. 计算超时
	if (ns &gt; 0)
		seconds /= statp-&gt;nscount;
	if (seconds &lt;= 0)
		seconds = 1;
	bool single_request_reopen = (statp-&gt;options &amp; RES_SNGLKUPREOP) != 0; // 0. 确定是否并行请求
	bool single_request = (((statp-&gt;options &amp; RES_SNGLKUP) != 0)
			       | single_request_reopen);
	int save_gotsomewhere = *gotsomewhere;

	int retval;
 retry_reopen: // tx1. 如果套接字没有创建，则创建， SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC，非阻塞
	retval = reopen (statp, terrno, ns); // tx1. 然后调用一下connect操作，不发数据
	if (retval &lt;= 0)
	  {
	    if (resplen2 != NULL)
	      *resplen2 = 0;
	    return retval;
	  }
 retry:
	evNowTime(&amp;now);
	evConsTime(&amp;timeout, seconds, 0);
	evAddTime(&amp;finish, &amp;now, &amp;timeout);
	int need_recompute = 0;
	int nwritten = 0;
	int recvresp1 = 0;  // 用于标记请求1的应答是否接收到
	/* Skip the second response if there is no second query.
	   To do that we mark the second response as received.  */
	int recvresp2 = buf2 == NULL; // 用于标记请求2的应答是否接收到，如果buf2为空则立即标记为1
	pfd[0].fd = EXT(statp).nssocks[ns];
	pfd[0].events = POLLOUT; // tx2. 准备监听可写事件
 wait:
	if (need_recompute) {
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out:
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0); // tx2. 等待套接字可写
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout); // rx1. 等待套接字可读，5秒超时
		need_recompute = 1;
	}
	if (n == 0) {
		if (resplen &gt; 1 &amp;&amp; (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)))
		  { // 处理某些DNS服务器不支持处理并行请求的场景
		    /* There are quite a few broken name servers out
		       there which don't handle two outstanding
		       requests from the same source.  There are also
		       broken firewall settings.  If we time out after
		       having received one answer switch to the mode
		       where we send the second request only once we
		       have received the first answer.  */
		    if (!single_request)
		      {
			statp-&gt;options |= RES_SNGLKUP; // 这里永久改变为串行发送请求。statp是线程本地变量，
			single_request = true;         // KeyDB复制定时任务总是在同一线程中运行
			*gotsomewhere = save_gotsomewhere;
			goto retry;
		      }
		    else if (!single_request_reopen)
		      {
			statp-&gt;options |= RES_SNGLKUPREOP;
			single_request_reopen = true;
			*gotsomewhere = save_gotsomewhere;
			__res_iclose (statp, false);
			goto retry_reopen;
		      }

		    *resplen2 = 1;
		    return resplen;
		  }

		*gotsomewhere = 1;
		if (resplen2 != NULL)
		  *resplen2 = 0;
		return 0;
	}
	if (n &lt; 0) {
		if (errno == EINTR)
			goto recompute_resend;

		goto poll_err_out;
	}
	__set_errno (0);
	if (pfd[0].revents &amp; POLLOUT) { // tx3. 监听到可写事件
#ifndef __ASSUME_SENDMMSG
		static int have_sendmmsg;
#else
# define have_sendmmsg 1
#endif
		if (have_sendmmsg &gt;= 0 &amp;&amp; nwritten == 0 &amp;&amp; buf2 != NULL // 查询请求2不为空
		    &amp;&amp; !single_request) // 且允许并行发送
		  {
		    struct iovec iov[2];
		    struct mmsghdr reqs[2];
		    reqs[0].msg_hdr.msg_name = NULL;
		    reqs[0].msg_hdr.msg_namelen = 0;
		    reqs[0].msg_hdr.msg_iov = &amp;iov[0];
		    reqs[0].msg_hdr.msg_iovlen = 1;
		    iov[0].iov_base = (void *) buf;
		    iov[0].iov_len = buflen;
		    reqs[0].msg_hdr.msg_control = NULL;
		    reqs[0].msg_hdr.msg_controllen = 0;

		    reqs[1].msg_hdr.msg_name = NULL;
		    reqs[1].msg_hdr.msg_namelen = 0;
		    reqs[1].msg_hdr.msg_iov = &amp;iov[1];
		    reqs[1].msg_hdr.msg_iovlen = 1;
		    iov[1].iov_base = (void *) buf2;
		    iov[1].iov_len = buflen2;
		    reqs[1].msg_hdr.msg_control = NULL;
		    reqs[1].msg_hdr.msg_controllen = 0;
            // 发送消息，注意这里同时发送2个查询请求，返回值是实际发送的数量
		    int ndg = __sendmmsg (pfd[0].fd, reqs, 2, MSG_NOSIGNAL);
		    if (__glibc_likely (ndg == 2))
		      {
			if (reqs[0].msg_len != buflen
			    || reqs[1].msg_len != buflen2)
			  goto fail_sendmmsg;

			pfd[0].events = POLLIN;
			nwritten += 2;
		      }
		    else if (ndg == 1 &amp;&amp; reqs[0].msg_len == buflen)
		      goto just_one;
		    else if (ndg &lt; 0 &amp;&amp; (errno == EINTR || errno == EAGAIN))
		      goto recompute_resend;
		    else
		      {
#ifndef __ASSUME_SENDMMSG
			if (__glibc_unlikely (have_sendmmsg == 0))
			  {
			    if (ndg &lt; 0 &amp;&amp; errno == ENOSYS)
			      {
				have_sendmmsg = -1;
				goto try_send;
			      }
			    have_sendmmsg = 1;
			  }
#endif

		      fail_sendmmsg:
			return close_and_return_error (statp, resplen2);
		      }
		  }
		else
		  { // 不支持并行发送
		    ssize_t sr;
#ifndef __ASSUME_SENDMMSG
		  try_send:
#endif
		    if (nwritten != 0)
		      sr = send (pfd[0].fd, buf2, buflen2, MSG_NOSIGNAL);
		    else
		      sr = send (pfd[0].fd, buf, buflen, MSG_NOSIGNAL); // tx4. 发送查询请求1

		    if (sr != (nwritten != 0 ? buflen2 : buflen)) { // 发送长度和缓冲区长度不匹配
		      if (errno == EINTR || errno == EAGAIN) // 如果原因是EINTR或EAGAIN，则尝试重发
			goto recompute_resend;
		      return close_and_return_error (statp, resplen2);
		    }
		  just_one:
		    if (nwritten != 0 || buf2 == NULL || single_request)
		      pfd[0].events = POLLIN;  // 串行模式下，后续只需监听可读时间
		    else
		      pfd[0].events = POLLIN | POLLOUT; // 并行发送，如果实际仅发送1个消息，跳转到这里。后续需要继续写入发送失败的那个消息
		    ++nwritten;
		  }
		goto wait; // tx4. 发送完毕，回到上面的wait分支等待应答
	} else if (pfd[0].revents &amp; POLLIN) { // rx2. 监听到套接字可读
		int *thisanssizp; // 本次读数据到哪个缓冲
		u_char **thisansp;
		int *thisresplenp;

		if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) {
			/* We have not received any responses
			   yet or we only have one response to
			   receive.  */
			thisanssizp = anssizp;
			thisansp = anscp ?: ansp;
			assert (anscp != NULL || ansp2 == NULL);
			thisresplenp = &amp;resplen;
		} else {
			thisanssizp = anssizp2;
			thisansp = ansp2;
			thisresplenp = resplen2;
		}

		if (*thisanssizp &lt; MAXPACKET
		    /* If the current buffer is not the the static
		       user-supplied buffer then we can reallocate
		       it.  */
		    &amp;&amp; (thisansp != NULL &amp;&amp; thisansp != ansp)
#ifdef FIONREAD
		    /* Is the size too small?  */
		    &amp;&amp; (ioctl (pfd[0].fd, FIONREAD, thisresplenp) &lt; 0
			|| *thisanssizp &lt; *thisresplenp)
#endif
                    ) {
			/* Always allocate MAXPACKET, callers expect
			   this specific size.  */
			u_char *newp = malloc (MAXPACKET);
			if (newp != NULL) {
				*thisanssizp = MAXPACKET;
				*thisansp = newp;
				if (thisansp == ansp2)
				  *ansp2_malloced = 1;
			}
		}
		/* We could end up with truncation if anscp was NULL
		   (not allowed to change caller's buffer) and the
		   response buffer size is too small.  This isn't a
		   reliable way to detect truncation because the ioctl
		   may be an inaccurate report of the UDP message size.
		   Therefore we use this only to issue debug output.
		   To do truncation accurately with UDP we need
		   MSG_TRUNC which is only available on Linux.  We
		   can abstract out the Linux-specific feature in the
		   future to detect truncation.  */
		HEADER *anhp = (HEADER *) *thisansp;
		socklen_t fromlen = sizeof(struct sockaddr_in6);
		assert (sizeof(from) &lt;= fromlen);
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, // rx3. 读取应答
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);
		if (__glibc_unlikely (*thisresplenp &lt;= 0))       {
			if (errno == EINTR || errno == EAGAIN) {
				need_recompute = 1;
				goto wait;  // 如果EINTR|EAGAIN则重新等待
			}
			return close_and_return_error (statp, resplen2);
		}
		*gotsomewhere = 1;
		if (__glibc_unlikely (*thisresplenp &lt; HFIXEDSZ))       { // 消息比报文头长度还小，错误
			/*
			 * Undersized message.
			 */
			*terrno = EMSGSIZE;
			return close_and_return_error (statp, resplen2);
		}
		if ((recvresp1 || hp-&gt;id != anhp-&gt;id)
		    &amp;&amp; (recvresp2 || hp2-&gt;id != anhp-&gt;id)) { // 查询标识符不匹配，可能服务器缓慢，返回之前查询的应答
			/*
			 * response from old query, ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE1) &amp;&amp; // 安全性检查type1
		    !res_ourserver_p(statp, &amp;from)) {
			/*
			 * response from wrong server? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE2) // 安全性检查type2
		    &amp;&amp; (recvresp1 || !res_queriesmatch(buf, buf + buflen,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))
		    &amp;&amp; (recvresp2 || !res_queriesmatch(buf2, buf2 + buflen2,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))) {
			/*
			 * response contains wrong query? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {  //  rx4. 处理服务器不愿意处理请求的情况
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;
			    else
			      recvresp2 = 1;

			    goto wait;  // 事件类型仍然是POLLIN，会导致超时
			  }

			/* don't retry if called from dig */
			if (!statp-&gt;pfcode)
			  return close_and_return_error (statp, resplen2);
			__res_iclose(statp, false);
		}
		if (anhp-&gt;rcode == NOERROR &amp;&amp; anhp-&gt;ancount == 0 // rx.4 处理nodata的情况，名字请求，请求的记录类型不存在
		    &amp;&amp; anhp-&gt;aa == 0 &amp;&amp; anhp-&gt;ra == 0 &amp;&amp; anhp-&gt;arcount == 0) {
			goto next_ns;
		}
		if (!(statp-&gt;options &amp; RES_IGNTC) &amp;&amp; anhp-&gt;tc) { // rx.4 处理应答截断的情况
			/*
			 * To get the rest of answer,
			 * use TCP with same server.
			 */
			*v_circuit = 1; // 提示使用TCP重发请求
			__res_iclose(statp, false);
			// XXX if we have received one reply we could
			// XXX use it and not repeat it over TCP...
			if (resplen2 != NULL)
			  *resplen2 = 0;
			return (1);
		}
		/* Mark which reply we received.  */
		if (recvresp1 == 0 &amp;&amp; hp-&gt;id == anhp-&gt;id)
			recvresp1 = 1;
		else
			recvresp2 = 1;
		/* Repeat waiting if we have a second answer to arrive.  */
		if ((recvresp1 &amp; recvresp2) == 0) { // 如果只有一个查询请求，recvresp2一开始就标记为1，因此不会走到这个分支
			if (single_request) { // 如果是串行模式，这里开始处理第2个请求
				pfd[0].events = POLLOUT;
				if (single_request_reopen) {  // 如果需要关闭并重新打开套接字
					__res_iclose (statp, false);
					retval = reopen (statp, terrno, ns);
					if (retval &lt;= 0)
					  {
					    if (resplen2 != NULL)
					      *resplen2 = 0;
					    return retval;
					  }
					pfd[0].fd = EXT(statp).nssocks[ns];
				}
			}
			goto wait;  // 事件类型已经改为POLLOUT，因此不会发生超时
		}
		/* All is well.  We have received both responses (if
		   two responses were requested).  */
		return (resplen); // rx.5 DNS查询完毕
	} else if (pfd[0].revents &amp; (POLLERR | POLLHUP | POLLNVAL)) // poll出现错误
	  /* Something went wrong.  We can stop trying.  */
	  return close_and_return_error (statp, resplen2);
	else {
		/* poll should not have returned &gt; 0 in this case.  */
		abort ();
	}
}</pre>
<p>注释中tx.标注了DNS查询请求发送的基本过程，rx.则标注了DNS查询应答接收的基本过程。调试查询keydb-0.keydb时该函数的行为，发现以下事实：</p>
<ol>
<li>查询时串行发送的，而不是并行。因此正常流程应该是发送A查询，接收A应答，发送AAAA查询，接收AAAA应答</li>
<li>仅执行了1225行，没有执行1223行。也就是说仅仅发送了A查询，没有发送AAA查询</li>
<li>走到了1241行的分支，也就是说，<span style="background-color: #c0c0c0;">A请求的应答报文是接收到的</span>：<br />
<pre class="crayon-plain-tag">// (gdb) i r eax
// eax            0x2d     45   A应答长度45
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp,
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);</pre></p>
<p>由于接收到的应答是servfail，因此走到这个分支：</p>
<pre class="crayon-plain-tag">if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;  // 接收到第一个应答
			    else
			      recvresp2 = 1;
                // 由于同时需要进行A和AAAA查询，这里仅仅接收到A应答（串行发送）
			    goto wait; // 因此需要跳转到这里，等待套接字可写，以发送AAAA请求
			  }</pre>
</li>
<li>
<p>CoreDNS应答A查询SERVFAIL，重新跳转到wait标签：
<pre class="crayon-plain-tag">if (need_recompute) { // 等待A应答的时候，设置了超时 need_recompute，因此再次wait执行这个分支
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out: // 如果超时了，直接关闭套接字并返回错误
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0);  // 发送A请求的时候在这里pull，等待套接字可写。timeout 0表示立即返回
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout);  // 接收A应答的时候在这里poll，等待套接字可读
		need_recompute = 1; // 发送AAAA请求时，在这里等待套接字可写
	}</pre>
</li>
<li>
<p> 这时，由于nwritten已经被设置为1，因此走带有timeout的poll分支。然后在1110行出现5秒超时，并因为poll返回值是0而导致send_dg函数退出。在一次A请求处理过程中，有两次在1110行poll：
<ol>
<li>第一次是尝试A请求的应答，poll前的pollfd是{fd = 87, events = 1, revents = 4}，之后是{fd = 87, events = 1, revents = 1}</li>
<li>第二次就是因为这个跳转，poll前的pollfd是{fd = 87, events = 1, revents = 1}，超时之后是{fd = 87, events = 1, revents = 0}</li>
</ol>
</li>
</ol>
<p>poll函数原型：<pre class="crayon-plain-tag">int poll(struct pollfd *fds, nfds_t nfds, int timeout);</pre>，它等待文件描述符集合中的某个可用（可执行I/O）。文件描述符集合由参数fds指定，它是pollfd结构的数组：</p>
<pre class="crayon-plain-tag">struct pollfd {
    // 打开文件的描述符
    int   fd;         
    // 输入参数，应用程序感兴趣的事件类型。如果置零则revents中仅能返回POLLHUP,POLLERR,POLLNVAL事件
    short events;
    // 输出参数，内核填充实际发生的事件
    short revents;    
};</pre>
<p>如果文件描述符集中没有任何一个发生了events中指定的事件，则该函数会阻塞，直到超时或者被信号处理器中断。</p>
<p><span style="background-color: #c0c0c0;">事件类型1表示POLLIN，即有数据可读；事件类型4表示POLLOUT</span>，即文件描述符可写。正常情况下该函数返回就绪的（revents非零）文件描述符数量，超时返回0，出现错误则返回-1</p>
<p>第2次在1110行的poll行为难以理解：</p>
<ol>
<li>A的应答已经接收到，而由于进行的是串行发送A/AAAA，此时尚未发送AAAA请求，因此可以<span style="background-color: #c0c0c0;">预期后续不会有可读事件</span></li>
<li>poll时events设置为POLLIN（肯定会导致超时），难道不是应该设置为POLLOUT，尝试发送AAAA请求或重试A请求么？</li>
</ol>
<p>为了进行对照，我们由调试了没有发生缓慢问题的keydb-2.keydb。它在第2次执行1110行的poll时没有超时，pollfd的状态是{fd = 88, events = 1, revents = 1}。连续两次poll到可读事件，这提示进行了并行A/AAAA查询。检查变量single_request_reopen、single_request果然都是false，从CoreDNS日志上也可以看到A/AAAA</p>
<p>可能的情况是，keydb-1.keydb最初是并行发送A/AAAA查询的，后来由于某种原因，改为串行发送，从而导致出现5秒超时相关的缓慢现象。根源应该还是在glibc中，因为KeyDB调用getaddrinfo的方式是固定的。</p>
<p>回顾一下send_dg的代码，可以发现<pre class="crayon-plain-tag">statp-&gt;options</pre>决定了是否进行串行发送，statp是<pre class="crayon-plain-tag">resolv_context</pre>的一个字段，后者则是一个线程本地变量。如果某次并行发送请求后，可以接收到第一个应答，而在继续等待第二个应答时出现超时（1113行），则send_dg函数会修改statp-&gt;options，改为串行发送，<span style="background-color: #c0c0c0;">这个修改具有全局性影响</span>，以后KeyDB的复制定时任务（总是由同一线程执行）调用getaddrinfo，都会使用串行方式发送请求。</p>
<p>改变为串行方式后，由于CoreDNS应答keydb-0.keydb.以SERVFAIL，导致跳转到wait标签（1363行），进而执行了一次必然超时的poll调用。CoreDNS应答其它（加了search domain后缀的）域名以NXDOMAIN，则不会导致超时的poll调用，<span style="background-color: #c0c0c0;">因为会在1396行修改事件类型为POLLOUT</span>。</p>
<div class="blog_h1"><span class="graybg">解决方案</span></div>
<p>触发本文中的glibc缺陷，需要满足以下条件：</p>
<ol>
<li>出现某个KeyDB节点宕机的情况，并且没有修复。这会导致复制定时任务反复执行DNS查询，从而可能触发缺陷</li>
<li>某个DNS查询的应答UDP包丢失，导致当前线程串行发送DNS请求。由于UDP本身的不可靠性，随着程序不断运行，最终会发生</li>
<li>DNS服务器返回SERVFAIL、NOTIMP或者REFUSED应答</li>
</ol>
<p>第1、2个条件都是随机性的，我们没法干预，只有从第3个条件入手。作为最快速的解决方案，只需要配置KeyDB，使用全限定域名来指定replicaof即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次KeyDB缓慢的定位过程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/debugging-slow-keydb/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<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>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-69d0d59966905626305271/] 容器化 <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>
