<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Liexing&#39;s Blog</title>
  
  <subtitle>不疯魔，不成佛</subtitle>
  <link href="http://blog.liexing.me/atom.xml" rel="self"/>
  
  <link href="http://blog.liexing.me/"/>
  <updated>2023-01-20T15:04:29.741Z</updated>
  <id>http://blog.liexing.me/</id>
  
  <author>
    <name>liexing</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Redis的List，从linkedlist和ziplist再到quicklist</title>
    <link href="http://blog.liexing.me/2019/12/28/from-ziplist-linkedlist-to-quicklist/"/>
    <id>http://blog.liexing.me/2019/12/28/from-ziplist-linkedlist-to-quicklist/</id>
    <published>2019-12-28T15:47:36.000Z</published>
    <updated>2023-01-20T15:04:29.741Z</updated>
    
    <content type="html"><![CDATA[<h2 id="List的底层编码及演进"><a href="#List的底层编码及演进" class="headerlink" title="List的底层编码及演进"></a>List的底层编码及演进</h2><p>Redis对外暴露最基本的5种结构，比如String、List、Set、ZSet和Hash，而每种结构在底层又能通过不同的数据结构来实现。在service.h中定义了底层使用的数据结构：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Objects encoding. Some kind of objects like Strings and Hashes can be</span></span><br><span class="line"><span class="comment"> * internally represented in multiple ways. The &#x27;encoding&#x27; field of the object</span></span><br><span class="line"><span class="comment"> * is set to one of this fields for this object. */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_RAW 0     <span class="comment">/* Raw representation */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_INT 1     <span class="comment">/* Encoded as integer */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_HT 2      <span class="comment">/* Encoded as hash table */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_ZIPMAP 3  <span class="comment">/* Encoded as zipmap */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_LINKEDLIST 4 <span class="comment">/* No longer used: old list encoding. */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_ZIPLIST 5 <span class="comment">/* Encoded as ziplist */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_INTSET 6  <span class="comment">/* Encoded as intset */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_SKIPLIST 7  <span class="comment">/* Encoded as skiplist */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_EMBSTR 8  <span class="comment">/* Embedded sds string encoding */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_QUICKLIST 9 <span class="comment">/* Encoded as linked list of ziplists */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OBJ_ENCODING_STREAM 10 <span class="comment">/* Encoded as a radix tree of listpacks */</span></span></span><br></pre></td></tr></table></figure><p>对于List，在Redis中的相关代码在<code>t_list.c</code>，在3.0及之前的版本中，对于list的调用为如下代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (subject-&gt;encoding == REDIS_ENCODING_ZIPLIST) &#123;</span><br><span class="line">    <span class="comment">//something</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (subject-&gt;encoding == REDIS_ENCODING_LINKEDLIST) &#123;</span><br><span class="line">    <span class="comment">//something</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    redisPanic(<span class="string">&quot;Unknown list encoding&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>即对于3.0及之前版本，对于list在底层存在两种不同的实现方式，ziplist以及linkedlist，但是在3.2版本开始，对于list的调用变成了如下形式：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (subject-&gt;encoding == OBJ_ENCODING_QUICKLIST) &#123;</span><br><span class="line">    <span class="comment">//something</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    serverPanic(<span class="string">&quot;Unknown list encoding&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>显然，在3.2及之后的版本，Redis使用了quicklist这个新的实现方式来替换以前的ziplist以及linkedlist。</p><h2 id="linkedlist"><a href="#linkedlist" class="headerlink" title="linkedlist"></a>linkedlist</h2><p>linkedlist即经典的双链表，其定义在3.0及之前版本的<code>adlist.h</code>文件中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Node, List, and Iterator are the only data structures used currently. */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">prev</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">next</span>;</span></span><br><span class="line">    <span class="type">void</span> *value;</span><br><span class="line">&#125; listNode;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list</span> &#123;</span></span><br><span class="line">    listNode *head;</span><br><span class="line">    listNode *tail;</span><br><span class="line">    <span class="type">void</span> *(*dup)(<span class="type">void</span> *ptr);</span><br><span class="line">    <span class="type">void</span> (*<span class="built_in">free</span>)(<span class="type">void</span> *ptr);</span><br><span class="line">    <span class="type">int</span> (*match)(<span class="type">void</span> *ptr, <span class="type">void</span> *key);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> len;</span><br><span class="line">&#125; <span class="built_in">list</span>;</span><br></pre></td></tr></table></figure><p>每个node包含了三个部分，指向前一个节点和后一个节点的指针，以及一个数据值。而一个list包含了指向首尾的指针、整个list的长度，以及三个函数指针，用来复制节点的值、释放节点的值，以及比较节点内容。</p><img src="/images/from-ziplist-linkedlist-to-quicklist/15773759260279.jpg"  title="linkedlist示意图" alt="linkedlist示意图"/>即对于每一个节点，value指向robj对象，而robj对象中的ptr指向实际的SDS对象，包含了长度，空余长度，真实字符串+'\0'，对于链表中每增加一个节点，需要实际内容额外42个字节（3.0.6版本，32位）的存储空间。<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> REDIS_LRU_BITS 24</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisObject</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> type:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> encoding:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> lru:REDIS_LRU_BITS; <span class="comment">/* lru time (relative to server.lruclock) */</span></span><br><span class="line">    <span class="type">int</span> refcount;</span><br><span class="line">    <span class="type">void</span> *ptr;</span><br><span class="line">&#125; robj;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="type">char</span> *sds;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sdshdr</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> len;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> <span class="built_in">free</span>;</span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>显然，linkedlist在频繁前后端插入情况下表现良好，但是查找效率比较低，并且比较耗内存。</p><h2 id="ziplist"><a href="#ziplist" class="headerlink" title="ziplist"></a>ziplist</h2><p>在Redis源码中，ziplist的实现在<code>ziplist.c</code>文件中，一开头就介绍了，ziplist是一种特殊编码的节省内存空间的双链表，能以O(1)的时间复杂度在两端<code>push</code>和<code>pop</code>数据，具有如下结构：</p><blockquote><p><code>&lt;zlbytes&gt;&lt;zltail&gt;&lt;zllen&gt;&lt;entry&gt;&lt;entry&gt;&lt;zlend&gt;</code></p></blockquote><ul><li>zlbytes是一个<code>unsigned integer</code>，保存ziplist占用的总内存空间，在重新分配内存时，借助这个字段可以不用遍历整个ziplist；</li><li>zltail是指向最后一个entry的偏移量，这样对于尾部的操作不用去遍历所有entry；</li><li>zllen固定两个字节长度，表示entry的数量，最大能表示<code>2^16-2</code>个entry，如果超过了，则其值为<code>2^16-1</code>，需要遍历entry才能知道具体的数量；</li><li>zlend固定一个字节，值固定为255，表示ziplist的结尾。</li></ul><p>zlbytes、zltail、zllen统称为ziplist的header，其空间总占用定义如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))</span></span><br></pre></td></tr></table></figure><p>新建一个空的ziplist的代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Create a new empty ziplist. */</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *<span class="title function_">ziplistNew</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> bytes = ZIPLIST_HEADER_SIZE+<span class="number">1</span>;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> *zl = zmalloc(bytes);</span><br><span class="line">    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);</span><br><span class="line">    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);</span><br><span class="line">    ZIPLIST_LENGTH(zl) = <span class="number">0</span>;</span><br><span class="line">    zl[bytes<span class="number">-1</span>] = ZIP_END;</span><br><span class="line">    <span class="keyword">return</span> zl;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>entry的定义如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zlentry</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> prevrawlensize, prevrawlen;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> lensize, len;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> headersize;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> encoding;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> *p;</span><br><span class="line">&#125; zlentry;</span><br></pre></td></tr></table></figure><p>prevrawlen和len均采用变长编码的方式来存储数据。</p><p>其中prevrawlen表示前一个节点的长度，prevrawlensize用来表示prevrawlen的大小，有1字节和5字节两种。如果prevrawlen小于254字节，则只需要一字节来保存，如果大于等于254字节，则需要5字节保存，第一个字节被置为254，其余4字节用来保存实际长度；len为当前节点长度 lensize为编码len所需的字节大小；headersize为当前节点的header大小；encoding为节点的编码方式；*p为指向节点的指针。</p><p>redis通过如下的代码来获取prevrawlen和prevrawlensize。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Encode the length of the previous entry and write it to &quot;p&quot;. Return the</span></span><br><span class="line"><span class="comment"> * number of bytes needed to encode this length if &quot;p&quot; is NULL. */</span></span><br><span class="line"><span class="type">static</span> <span class="type">unsigned</span> <span class="type">int</span> <span class="title function_">zipPrevEncodeLength</span><span class="params">(<span class="type">unsigned</span> <span class="type">char</span> *p, <span class="type">unsigned</span> <span class="type">int</span> len)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (p == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> (len &lt; ZIP_BIGLEN) ? <span class="number">1</span> : <span class="keyword">sizeof</span>(len)+<span class="number">1</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (len &lt; ZIP_BIGLEN) &#123;</span><br><span class="line">            p[<span class="number">0</span>] = len;</span><br><span class="line">            <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            p[<span class="number">0</span>] = ZIP_BIGLEN;</span><br><span class="line">            <span class="built_in">memcpy</span>(p+<span class="number">1</span>,&amp;len,<span class="keyword">sizeof</span>(len));</span><br><span class="line">            memrev32ifbe(p+<span class="number">1</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="number">1</span>+<span class="keyword">sizeof</span>(len);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于lensize和len，这二者的值和entry内存储的类型有关。如果存储string，则前两个bit位用来存储string的编码方式，后面跟上实际的长度。如果存储integer，则前两个bit位置为1，随后两个bit位指定integer的类型。具体如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">* |<span class="number">00</span>pppppp| - <span class="number">1</span> byte</span><br><span class="line">*      String value with length less than or equal to <span class="number">63</span> bytes (<span class="number">6</span> bits).</span><br><span class="line">* |<span class="number">01</span>pppppp|qqqqqqqq| - <span class="number">2</span> bytes</span><br><span class="line">*      String value with length less than or equal to <span class="number">16383</span> bytes (<span class="number">14</span> bits).</span><br><span class="line">* |<span class="number">10</span>______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - <span class="number">5</span> bytes</span><br><span class="line">*      String value with length greater than or equal to <span class="number">16384</span> bytes.</span><br><span class="line">* |<span class="number">11000000</span>| - <span class="number">1</span> byte</span><br><span class="line">*      Integer encoded as <span class="title function_">int16_t</span> <span class="params">(<span class="number">2</span> bytes)</span>.</span><br><span class="line">* |11010000| - 1 byte</span><br><span class="line">*      Integer encoded as <span class="title function_">int32_t</span> <span class="params">(<span class="number">4</span> bytes)</span>.</span><br><span class="line">* |11100000| - 1 byte</span><br><span class="line">*      Integer encoded as <span class="title function_">int64_t</span> <span class="params">(<span class="number">8</span> bytes)</span>.</span><br><span class="line">* |11110000| - 1 byte</span><br><span class="line">*      Integer encoded as 24 bit <span class="title function_">signed</span> <span class="params">(<span class="number">3</span> bytes)</span>.</span><br><span class="line">* |11111110| - 1 byte</span><br><span class="line">*      Integer encoded as 8 bit <span class="title function_">signed</span> <span class="params">(<span class="number">1</span> byte)</span>.</span><br><span class="line">* |1111xxxx| - <span class="params">(with xxxx between <span class="number">0000</span> and <span class="number">1101</span>)</span> immediate 4 bit integer.</span><br><span class="line">*      Unsigned integer from 0 to 12. The encoded value is actually from</span><br><span class="line">*      1 to 13 because 0000 and 1111 can not be used, so 1 should be</span><br><span class="line">*      subtracted from the encoded 4 bit value to obtain the right value.</span><br><span class="line">* |11111111| - End of ziplist.</span><br></pre></td></tr></table></figure><p>从<code>ZIP_DECODE_LENGTH</code>可以看出具体的解码过程和每个字段的存储位置：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Different encoding/length possibilities */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_STR_MASK 0xc0</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_MASK 0x30</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_STR_06B (0 &lt;&lt; 6)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_STR_14B (1 &lt;&lt; 6)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_STR_32B (2 &lt;&lt; 6)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Extract the encoding from the byte pointed by &#x27;ptr&#x27; and set it into</span></span><br><span class="line"><span class="comment"> * &#x27;encoding&#x27;. */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_ENTRY_ENCODING(ptr, encoding) do &#123;  \</span></span><br><span class="line"><span class="meta">    (encoding) = (ptr[0]); \</span></span><br><span class="line"><span class="meta">    <span class="keyword">if</span> ((encoding) &lt; ZIP_STR_MASK) (encoding) &amp;= ZIP_STR_MASK; \</span></span><br><span class="line"><span class="meta">&#125; while(0)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Decode the length encoded in &#x27;ptr&#x27;. The &#x27;encoding&#x27; variable will hold the</span></span><br><span class="line"><span class="comment"> * entries encoding, the &#x27;lensize&#x27; variable will hold the number of bytes</span></span><br><span class="line"><span class="comment"> * required to encode the entries length, and the &#x27;len&#x27; variable will hold the</span></span><br><span class="line"><span class="comment"> * entries length. */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do &#123;                    \</span></span><br><span class="line"><span class="meta">    ZIP_ENTRY_ENCODING((ptr), (encoding));                                     \</span></span><br><span class="line"><span class="meta">    <span class="keyword">if</span> ((encoding) &lt; ZIP_STR_MASK) &#123;                                           \</span></span><br><span class="line"><span class="meta">        <span class="keyword">if</span> ((encoding) == ZIP_STR_06B) &#123;                                       \</span></span><br><span class="line"><span class="meta">            (lensize) = 1;                                                     \</span></span><br><span class="line"><span class="meta">            (len) = (ptr)[0] &amp; 0x3f;                                           \</span></span><br><span class="line"><span class="meta">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> ((encoding) == ZIP_STR_14B) &#123;                                \</span></span><br><span class="line"><span class="meta">            (lensize) = 2;                                                     \</span></span><br><span class="line"><span class="meta">            (len) = (((ptr)[0] &amp; 0x3f) &lt;&lt; 8) | (ptr)[1];                       \</span></span><br><span class="line"><span class="meta">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (encoding == ZIP_STR_32B) &#123;                                  \</span></span><br><span class="line"><span class="meta">            (lensize) = 5;                                                     \</span></span><br><span class="line"><span class="meta">            (len) = ((ptr)[1] &lt;&lt; 24) |                                         \</span></span><br><span class="line"><span class="meta">                    ((ptr)[2] &lt;&lt; 16) |                                         \</span></span><br><span class="line"><span class="meta">                    ((ptr)[3] &lt;&lt;  8) |                                         \</span></span><br><span class="line"><span class="meta">                    ((ptr)[4]);                                                \</span></span><br><span class="line"><span class="meta">        &#125; <span class="keyword">else</span> &#123;                                                               \</span></span><br><span class="line"><span class="meta">            assert(NULL);                                                      \</span></span><br><span class="line"><span class="meta">        &#125;                                                                      \</span></span><br><span class="line"><span class="meta">    &#125; <span class="keyword">else</span> &#123;                                                                   \</span></span><br><span class="line"><span class="meta">        (lensize) = 1;                                                         \</span></span><br><span class="line"><span class="meta">        (len) = zipIntSize(encoding);                                          \</span></span><br><span class="line"><span class="meta">    &#125;                                                                          \</span></span><br><span class="line"><span class="meta">&#125; while(0);</span></span><br></pre></td></tr></table></figure><p>对len字段进行计算的过程如下面的函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Encode the length &#x27;rawlen&#x27; writing it in &#x27;p&#x27;. If p is NULL it just returns</span></span><br><span class="line"><span class="comment"> * the amount of bytes required to encode such a length. */</span></span><br><span class="line"><span class="type">static</span> <span class="type">unsigned</span> <span class="type">int</span> <span class="title function_">zipEncodeLength</span><span class="params">(<span class="type">unsigned</span> <span class="type">char</span> *p, <span class="type">unsigned</span> <span class="type">char</span> encoding, <span class="type">unsigned</span> <span class="type">int</span> rawlen)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> len = <span class="number">1</span>, buf[<span class="number">5</span>];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (ZIP_IS_STR(encoding)) &#123;</span><br><span class="line">        <span class="comment">/* Although encoding is given it may not be set for strings,</span></span><br><span class="line"><span class="comment">         * so we determine it here using the raw length. */</span></span><br><span class="line">        <span class="keyword">if</span> (rawlen &lt;= <span class="number">0x3f</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!p) <span class="keyword">return</span> len;</span><br><span class="line">            buf[<span class="number">0</span>] = ZIP_STR_06B | rawlen;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (rawlen &lt;= <span class="number">0x3fff</span>) &#123;</span><br><span class="line">            len += <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">if</span> (!p) <span class="keyword">return</span> len;</span><br><span class="line">            buf[<span class="number">0</span>] = ZIP_STR_14B | ((rawlen &gt;&gt; <span class="number">8</span>) &amp; <span class="number">0x3f</span>);</span><br><span class="line">            buf[<span class="number">1</span>] = rawlen &amp; <span class="number">0xff</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            len += <span class="number">4</span>;</span><br><span class="line">            <span class="keyword">if</span> (!p) <span class="keyword">return</span> len;</span><br><span class="line">            buf[<span class="number">0</span>] = ZIP_STR_32B;</span><br><span class="line">            buf[<span class="number">1</span>] = (rawlen &gt;&gt; <span class="number">24</span>) &amp; <span class="number">0xff</span>;</span><br><span class="line">            buf[<span class="number">2</span>] = (rawlen &gt;&gt; <span class="number">16</span>) &amp; <span class="number">0xff</span>;</span><br><span class="line">            buf[<span class="number">3</span>] = (rawlen &gt;&gt; <span class="number">8</span>) &amp; <span class="number">0xff</span>;</span><br><span class="line">            buf[<span class="number">4</span>] = rawlen &amp; <span class="number">0xff</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">/* Implies integer encoding, so length is always 1. */</span></span><br><span class="line">        <span class="keyword">if</span> (!p) <span class="keyword">return</span> len;</span><br><span class="line">        buf[<span class="number">0</span>] = encoding;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Store this length at p */</span></span><br><span class="line">    <span class="built_in">memcpy</span>(p,buf,len);</span><br><span class="line">    <span class="keyword">return</span> len;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，对于integer编码，长度恒为1，否则读取实际的string的长度值。</p><p>而实际上，encoding又是保存在len字段的第一个字节，判断是否是字符串的方法如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_STR_MASK 0xc0</span></span><br><span class="line"><span class="comment">/* Macro to determine type */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_IS_STR(enc) (((enc) &amp; ZIP_STR_MASK) &lt; ZIP_STR_MASK)</span></span><br></pre></td></tr></table></figure><p>encoding和p表示元素编码和内容，其具体的定义可参考如下函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_16B (0xc0 | 0&lt;&lt;4)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_32B (0xc0 | 1&lt;&lt;4)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_64B (0xc0 | 2&lt;&lt;4)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_24B (0xc0 | 3&lt;&lt;4)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_8B 0xfe</span></span><br><span class="line"><span class="comment">/* 4 bit integer immediate encoding */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_IMM_MASK 0x0f</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_IMM_MIN 0xf1    <span class="comment">/* 11110001 */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ZIP_INT_IMM_MAX 0xfd    <span class="comment">/* 11111101 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Check if string pointed to by &#x27;entry&#x27; can be encoded as an integer.</span></span><br><span class="line"><span class="comment"> * Stores the integer value in &#x27;v&#x27; and its encoding in &#x27;encoding&#x27;. */</span></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">zipTryEncoding</span><span class="params">(<span class="type">unsigned</span> <span class="type">char</span> *entry, <span class="type">unsigned</span> <span class="type">int</span> entrylen, <span class="type">long</span> <span class="type">long</span> *v, <span class="type">unsigned</span> <span class="type">char</span> *encoding)</span> &#123;</span><br><span class="line">    <span class="type">long</span> <span class="type">long</span> value;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (entrylen &gt;= <span class="number">32</span> || entrylen == <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">if</span> (string2ll((<span class="type">char</span>*)entry,entrylen,&amp;value)) &#123;</span><br><span class="line">        <span class="comment">/* Great, the string can be encoded. Check what&#x27;s the smallest</span></span><br><span class="line"><span class="comment">         * of our encoding types that can hold this value. */</span></span><br><span class="line">        <span class="keyword">if</span> (value &gt;= <span class="number">0</span> &amp;&amp; value &lt;= <span class="number">12</span>) &#123;</span><br><span class="line">            *encoding = ZIP_INT_IMM_MIN+value;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value &gt;= INT8_MIN &amp;&amp; value &lt;= INT8_MAX) &#123;</span><br><span class="line">            *encoding = ZIP_INT_8B;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value &gt;= INT16_MIN &amp;&amp; value &lt;= INT16_MAX) &#123;</span><br><span class="line">            *encoding = ZIP_INT_16B;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value &gt;= INT24_MIN &amp;&amp; value &lt;= INT24_MAX) &#123;</span><br><span class="line">            *encoding = ZIP_INT_24B;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value &gt;= INT32_MIN &amp;&amp; value &lt;= INT32_MAX) &#123;</span><br><span class="line">            *encoding = ZIP_INT_32B;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            *encoding = ZIP_INT_64B;</span><br><span class="line">        &#125;</span><br><span class="line">        *v = value;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Convert a string into a long long. Returns 1 if the string could be parsed</span></span><br><span class="line"><span class="comment"> * into a (non-overflowing) long long, 0 otherwise. The value will be set to</span></span><br><span class="line"><span class="comment"> * the parsed value when appropriate. */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">string2ll</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *s, <span class="type">size_t</span> slen, <span class="type">long</span> <span class="type">long</span> *value)</span>;</span><br></pre></td></tr></table></figure><p>显然如上面的描述，对于entrylen&gt;&#x3D;32不用做处理，接下来设置encoding为具体的值。</p><p>对于ziplist的push操作，在<code>ziplistPush</code>中具体定义，简单描述其流程如下：</p><ol><li>获取指向尾部或者头部节点的指针p；</li><li>获取p的prevlensize和prevlen；</li><li>通过prevlen以及coding、实际插入数据来计算待插入的节点reqlen；</li><li>如不在队尾插入，则需要校验p对应节点的prelen是否够reqlen使用，不够需要扩展，够不进行压缩，防止连锁更新；</li><li>更新队尾偏移量；</li><li>判断是否需要连锁更新；</li><li>保存插入节点内容；</li><li>ziplist的长度加一。</li></ol><p>连锁更新的执行函数以及解释如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* When an entry is inserted, we need to set the prevlen field of the next</span></span><br><span class="line"><span class="comment"> * entry to equal the length of the inserted entry. It can occur that this</span></span><br><span class="line"><span class="comment"> * length cannot be encoded in 1 byte and the next entry needs to be grow</span></span><br><span class="line"><span class="comment"> * a bit larger to hold the 5-byte encoded prevlen. This can be done for free,</span></span><br><span class="line"><span class="comment"> * because this only happens when an entry is already being inserted (which</span></span><br><span class="line"><span class="comment"> * causes a realloc and memmove). However, encoding the prevlen may require</span></span><br><span class="line"><span class="comment"> * that this entry is grown as well. This effect may cascade throughout</span></span><br><span class="line"><span class="comment"> * the ziplist when there are consecutive entries with a size close to</span></span><br><span class="line"><span class="comment"> * ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every</span></span><br><span class="line"><span class="comment"> * consecutive entry.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Note that this effect can also happen in reverse, where the bytes required</span></span><br><span class="line"><span class="comment"> * to encode the prevlen field can shrink. This effect is deliberately ignored,</span></span><br><span class="line"><span class="comment"> * because it can cause a &quot;flapping&quot; effect where a chain prevlen fields is</span></span><br><span class="line"><span class="comment"> * first grown and then shrunk again after consecutive inserts. Rather, the</span></span><br><span class="line"><span class="comment"> * field is allowed to stay larger than necessary, because a large prevlen</span></span><br><span class="line"><span class="comment"> * field implies the ziplist is holding large entries anyway.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * The pointer &quot;p&quot; points to the first entry that does NOT need to be</span></span><br><span class="line"><span class="comment"> * updated, i.e. consecutive fields MAY need an update. */</span></span><br><span class="line"><span class="type">static</span> <span class="type">unsigned</span> <span class="type">char</span> *__ziplistCascadeUpdate(<span class="type">unsigned</span> <span class="type">char</span> *zl, <span class="type">unsigned</span> <span class="type">char</span> *p);</span><br></pre></td></tr></table></figure><p>如上面的描述，可以得到ziplist的简易示意图如下，每个节点是单独的entry，每个entry中一个字段表示前一个entry的长度（长度小于254时采用一个字节编码，否则采用5个字节），一个encoding字段保存当前节点的编码方式和数据长度，content保存着entry的具体数据，可以是字符数组或整数，如果是整数且在0-12之间则不再保存content。</p><img src="/images/from-ziplist-linkedlist-to-quicklist/15774500192728.jpg"  title="ziplist示意图" alt="ziplist示意图"/><p>ziplist可以很方便的拿到头节点或者尾节点，由于每个节点都保存前一个节点的长度，因此对于任意节点可以方便的前后遍历。相比linkedlist，除了链表结构节省少量空间外，每个entry可以节省大量的额外内存（最大额外空间才10字节，对于不大于12的正整数，甚至不用content空间来进行存储）。对于主要是pop或push并且每个元素长度不大的场景来说，ziplist相比于linkedlist有较大的优势。</p><p>但是如前面所说，通过<code>ZIP_BIGLEN</code>即<code>254</code>这个分界点来确认prevlen的长度，如果每一个节点的长度原本都是253，如果在头部插入时下一个节点的prevlen需要扩展，则会导致整个ziplist都进行更新。在删除时也可能出现类似情况。但是这种情况出现的概率不大，并且在使用ziplist时，entry总量不大，因此可以忽略不计。</p><p>ziplist的弊端也很明显了，对于较多的entry或者entry长度较大时，需要大量的连续内存，并且节省的空间比例相对不在占优势，就可以考虑使用其他结构了。</p><img src="/images/from-ziplist-linkedlist-to-quicklist/15774517236868.jpg"  title="redis中list配置" alt="redis中list配置"/>如图所示是3.0.6版本redis中的默认值，即单个entry长度官方默认要求小于64时才使用ziplist，否则使用其他底层结构；entry数量也有限制，一般要求在512个(hash和list)或者128个（zset）之内才使用。<h2 id="QuickList"><a href="#QuickList" class="headerlink" title="QuickList"></a>QuickList</h2><p>前面介绍的两种结构，一种耗内存但是能应付数据较大（数量或者单个的长度）的情况，但是插入和删除成本低，而另一个则在小规模数据情况下表现很好并且非常节省内存，数据规模大时会有问题，并且插入和删除成本高。显然这时候QuickList该上场了。这时候让我们忘记3.0及之前的版本，开始进入新的结构吧。</p><p>首先看代码定义，<code>quicklist.h</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Node, quicklist, and Iterator are the only data structures used currently. */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.</span></span><br><span class="line"><span class="comment"> * We use bit fields keep the quicklistNode at 32 bytes.</span></span><br><span class="line"><span class="comment"> * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually &lt; 32k).</span></span><br><span class="line"><span class="comment"> * encoding: 2 bits, RAW=1, LZF=2.</span></span><br><span class="line"><span class="comment"> * container: 2 bits, NONE=1, ZIPLIST=2.</span></span><br><span class="line"><span class="comment"> * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.</span></span><br><span class="line"><span class="comment"> * attempted_compress: 1 bit, boolean, used for verifying during testing.</span></span><br><span class="line"><span class="comment"> * extra: 10 bits, free for future use; pads out the remainder of 32 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">quicklistNode</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">quicklistNode</span> *<span class="title">prev</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">quicklistNode</span> *<span class="title">next</span>;</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> *zl;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> sz;             <span class="comment">/* ziplist size in bytes */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> count : <span class="number">16</span>;     <span class="comment">/* count of items in ziplist */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> encoding : <span class="number">2</span>;   <span class="comment">/* RAW==1 or LZF==2 */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> container : <span class="number">2</span>;  <span class="comment">/* NONE==1 or ZIPLIST==2 */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> recompress : <span class="number">1</span>; <span class="comment">/* was this node previous compressed? */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> attempted_compress : <span class="number">1</span>; <span class="comment">/* node can&#x27;t compress; too small */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> extra : <span class="number">10</span>; <span class="comment">/* more bits to steal for future usage */</span></span><br><span class="line">&#125; quicklistNode;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* quicklistLZF is a 4+N byte struct holding &#x27;sz&#x27; followed by &#x27;compressed&#x27;.</span></span><br><span class="line"><span class="comment"> * &#x27;sz&#x27; is byte length of &#x27;compressed&#x27; field.</span></span><br><span class="line"><span class="comment"> * &#x27;compressed&#x27; is LZF data with total (compressed) length &#x27;sz&#x27;</span></span><br><span class="line"><span class="comment"> * <span class="doctag">NOTE:</span> uncompressed length is stored in quicklistNode-&gt;sz.</span></span><br><span class="line"><span class="comment"> * When quicklistNode-&gt;zl is compressed, node-&gt;zl points to a quicklistLZF */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">quicklistLZF</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> sz; <span class="comment">/* LZF size in bytes*/</span></span><br><span class="line">    <span class="type">char</span> compressed[];</span><br><span class="line">&#125; quicklistLZF;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.</span></span><br><span class="line"><span class="comment"> * &#x27;count&#x27; is the number of total entries.</span></span><br><span class="line"><span class="comment"> * &#x27;len&#x27; is the number of quicklist nodes.</span></span><br><span class="line"><span class="comment"> * &#x27;compress&#x27; is: -1 if compression disabled, otherwise it&#x27;s the number</span></span><br><span class="line"><span class="comment"> *                of quicklistNodes to leave uncompressed at ends of quicklist.</span></span><br><span class="line"><span class="comment"> * &#x27;fill&#x27; is the user-requested (or default) fill factor. */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">quicklist</span> &#123;</span></span><br><span class="line">    quicklistNode *head;</span><br><span class="line">    quicklistNode *tail;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> count;        <span class="comment">/* total count of all entries in all ziplists */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> len;          <span class="comment">/* number of quicklistNodes */</span></span><br><span class="line">    <span class="type">int</span> fill : <span class="number">16</span>;              <span class="comment">/* fill factor for individual nodes */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> compress : <span class="number">16</span>; <span class="comment">/* depth of end nodes not to compress;0=off */</span></span><br><span class="line">&#125; quicklist;</span><br></pre></td></tr></table></figure><p>乍一看貌似很复杂，但是整个结构却是非常的清晰。</p><p>首先是<code>quicklistNode</code>，这是<code>quicklist</code>的节点，可以看做对<code>ziplist</code>的高层封装。包含指向前后节点的指针，以及指向实际<code>ziplist</code>的指针zl，从定义上看，<code>quicklist</code>的节点上支持了压缩能力，并且多个字段通过位域方式申明内存节省空间。</p><p>而<code>quicklistLZF</code>用来存储压缩后的<code>ziplist</code>，占用空间4+N字节，其中N为压缩后的实际长度。</p><p>通过<code>quicklist</code>将<code>quicklistNode</code>连接起来，形成了完整的<code>quicklist</code>结构。由于<code>quicklist</code>同时包含了<code>ziplist</code>和<code>quicklist</code>的结构，因此每个<code>quicklistNode</code>的大小就非常重要：如果太大其就更接近ziplist，影响插入效率；如果太小就更接近<code>quicklist</code>，浪费空间。其通过<code>fill</code>字段来控制大小，正数表示单个节点允许的最大数量，最大为2^15，负数表示单个节点的内存空间大小，其中-1表示单个节点最多存储4kb，-2表示单个节点最多存储8kb，以此类推，-5表示单个节点最多保存64kb，在创建时默认的值为-2。这个字段的设置代码即判定是否还能继续插入数据的代码如下。compress表示压缩的深度，0表示不压缩，正数表示头尾多少个节点不压缩其余节点都压缩。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> FILL_MAX (1 &lt;&lt; 15)</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">quicklistSetFill</span><span class="params">(quicklist *quicklist, <span class="type">int</span> fill)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (fill &gt; FILL_MAX) &#123;</span><br><span class="line">        fill = FILL_MAX;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (fill &lt; <span class="number">-5</span>) &#123;</span><br><span class="line">        fill = <span class="number">-5</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    quicklist-&gt;fill = fill;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Maximum size in bytes of any multi-element ziplist.</span></span><br><span class="line"><span class="comment"> * Larger values will live in their own isolated ziplists. */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SIZE_SAFETY_LIMIT 8192</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> sizeMeetsSafetyLimit(sz) ((sz) &lt;= SIZE_SAFETY_LIMIT)</span></span><br><span class="line"></span><br><span class="line">REDIS_STATIC <span class="type">int</span> _quicklistNodeAllowInsert(<span class="type">const</span> quicklistNode *node,</span><br><span class="line">                                           <span class="type">const</span> <span class="type">int</span> fill, <span class="type">const</span> <span class="type">size_t</span> sz) &#123;</span><br><span class="line">    <span class="keyword">if</span> (unlikely(!node))</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ziplist_overhead;</span><br><span class="line">    <span class="comment">/* size of previous offset */</span></span><br><span class="line">    <span class="keyword">if</span> (sz &lt; <span class="number">254</span>)</span><br><span class="line">        ziplist_overhead = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        ziplist_overhead = <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* size of forward offset */</span></span><br><span class="line">    <span class="keyword">if</span> (sz &lt; <span class="number">64</span>)</span><br><span class="line">        ziplist_overhead += <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (likely(sz &lt; <span class="number">16384</span>))</span><br><span class="line">        ziplist_overhead += <span class="number">2</span>;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        ziplist_overhead += <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* new_sz overestimates if &#x27;sz&#x27; encodes to an integer type */</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> new_sz = node-&gt;sz + sz + ziplist_overhead;</span><br><span class="line">    <span class="keyword">if</span> (likely(_quicklistNodeSizeMeetsOptimizationRequirement(new_sz, fill)))</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (!sizeMeetsSafetyLimit(new_sz))</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> ((<span class="type">int</span>)node-&gt;count &lt; fill)</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>quicklist使用<a href="https://en.wikipedia.org/wiki/Lossless_compression">lzf</a>进行压缩，具体压缩算法略过，压缩节点的代码如下，开辟新的空间压缩ziplist数据，并且释放node-&gt;zl原有的内存，最后指向压缩后的数据并修改其他属性值。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/* Minimum ziplist size in bytes for attempting compression. */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MIN_COMPRESS_BYTES 48</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Compress the ziplist in &#x27;node&#x27; and update encoding details.</span></span><br><span class="line"><span class="comment"> * Returns 1 if ziplist compressed successfully.</span></span><br><span class="line"><span class="comment"> * Returns 0 if compression failed or if ziplist too small to compress. */</span></span><br><span class="line">REDIS_STATIC <span class="type">int</span> __quicklistCompressNode(quicklistNode *node) &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> REDIS_TEST</span></span><br><span class="line">    node-&gt;attempted_compress = <span class="number">1</span>;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Don&#x27;t bother compressing small values */</span></span><br><span class="line">    <span class="keyword">if</span> (node-&gt;sz &lt; MIN_COMPRESS_BYTES)</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    quicklistLZF *lzf = zmalloc(<span class="keyword">sizeof</span>(*lzf) + node-&gt;sz);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Cancel if compression fails or doesn&#x27;t compress small enough */</span></span><br><span class="line">    <span class="keyword">if</span> (((lzf-&gt;sz = lzf_compress(node-&gt;zl, node-&gt;sz, lzf-&gt;compressed,</span><br><span class="line">                                 node-&gt;sz)) == <span class="number">0</span>) ||</span><br><span class="line">        lzf-&gt;sz + MIN_COMPRESS_IMPROVE &gt;= node-&gt;sz) &#123;</span><br><span class="line">        <span class="comment">/* lzf_compress aborts/rejects compression if value not compressable. */</span></span><br><span class="line">        zfree(lzf);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    lzf = zrealloc(lzf, <span class="keyword">sizeof</span>(*lzf) + lzf-&gt;sz);</span><br><span class="line">    zfree(node-&gt;zl);</span><br><span class="line">    node-&gt;zl = (<span class="type">unsigned</span> <span class="type">char</span> *)lzf;</span><br><span class="line">    node-&gt;encoding = QUICKLIST_NODE_ENCODING_LZF;</span><br><span class="line">    node-&gt;recompress = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样，解压的代码如下，开辟新的空间存放解压后的数据，同时释放压缩数据的空间，node-&gt;zl指向新的解压后的数据，最后修改其他属性值。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Uncompress the ziplist in &#x27;node&#x27; and update encoding details.</span></span><br><span class="line"><span class="comment"> * Returns 1 on successful decode, 0 on failure to decode. */</span></span><br><span class="line">REDIS_STATIC <span class="type">int</span> __quicklistDecompressNode(quicklistNode *node) &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> REDIS_TEST</span></span><br><span class="line">    node-&gt;attempted_compress = <span class="number">0</span>;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="type">void</span> *decompressed = zmalloc(node-&gt;sz);</span><br><span class="line">    quicklistLZF *lzf = (quicklistLZF *)node-&gt;zl;</span><br><span class="line">    <span class="keyword">if</span> (lzf_decompress(lzf-&gt;compressed, lzf-&gt;sz, decompressed, node-&gt;sz) == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">/* Someone requested decompress, but we can&#x27;t decompress.  Not good. */</span></span><br><span class="line">        zfree(decompressed);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    zfree(lzf);</span><br><span class="line">    node-&gt;zl = decompressed;</span><br><span class="line">    node-&gt;encoding = QUICKLIST_NODE_ENCODING_RAW;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在头尾插入节点如下，如果单个ziplist满足上面说到的大小、数量限制，则使用ziplist的push函数直接插入，否则新建一个节点用来插入即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Add new entry to head node of quicklist.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Returns 0 if used existing head.</span></span><br><span class="line"><span class="comment"> * Returns 1 if new head created. */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">quicklistPushHead</span><span class="params">(quicklist *quicklist, <span class="type">void</span> *value, <span class="type">size_t</span> sz)</span> &#123;</span><br><span class="line">    quicklistNode *orig_head = quicklist-&gt;head;</span><br><span class="line">    <span class="keyword">if</span> (likely(</span><br><span class="line">            _quicklistNodeAllowInsert(quicklist-&gt;head, quicklist-&gt;fill, sz))) &#123;</span><br><span class="line">        quicklist-&gt;head-&gt;zl =</span><br><span class="line">            ziplistPush(quicklist-&gt;head-&gt;zl, value, sz, ZIPLIST_HEAD);</span><br><span class="line">        quicklistNodeUpdateSz(quicklist-&gt;head);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        quicklistNode *node = quicklistCreateNode();</span><br><span class="line">        node-&gt;zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);</span><br><span class="line"></span><br><span class="line">        quicklistNodeUpdateSz(node);</span><br><span class="line">        _quicklistInsertNodeBefore(quicklist, quicklist-&gt;head, node);</span><br><span class="line">    &#125;</span><br><span class="line">    quicklist-&gt;count++;</span><br><span class="line">    quicklist-&gt;head-&gt;count++;</span><br><span class="line">    <span class="keyword">return</span> (orig_head != quicklist-&gt;head);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Add new entry to tail node of quicklist.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Returns 0 if used existing tail.</span></span><br><span class="line"><span class="comment"> * Returns 1 if new tail created. */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">quicklistPushTail</span><span class="params">(quicklist *quicklist, <span class="type">void</span> *value, <span class="type">size_t</span> sz)</span> &#123;</span><br><span class="line">    quicklistNode *orig_tail = quicklist-&gt;tail;</span><br><span class="line">    <span class="keyword">if</span> (likely(</span><br><span class="line">            _quicklistNodeAllowInsert(quicklist-&gt;tail, quicklist-&gt;fill, sz))) &#123;</span><br><span class="line">        quicklist-&gt;tail-&gt;zl =</span><br><span class="line">            ziplistPush(quicklist-&gt;tail-&gt;zl, value, sz, ZIPLIST_TAIL);</span><br><span class="line">        quicklistNodeUpdateSz(quicklist-&gt;tail);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        quicklistNode *node = quicklistCreateNode();</span><br><span class="line">        node-&gt;zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);</span><br><span class="line"></span><br><span class="line">        quicklistNodeUpdateSz(node);</span><br><span class="line">        _quicklistInsertNodeAfter(quicklist, quicklist-&gt;tail, node);</span><br><span class="line">    &#125;</span><br><span class="line">    quicklist-&gt;count++;</span><br><span class="line">    quicklist-&gt;tail-&gt;count++;</span><br><span class="line">    <span class="keyword">return</span> (orig_tail != quicklist-&gt;tail);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>除此之外，quicklist还提供了merge、旋转、指定节点前后插入等功能，均在<code>quicklist.[h|c]</code>中，其主要在linkedlist的基础上，对于每个节点融合ziplist的特征，并且对于中间节点还提供了lzf压缩的能力，综合了linkedlist和ziplist的有点，同时具有节省内存、插入删除数据高效的特点。整个quicklist的简单示意图可如下图。</p><img src="/images/from-ziplist-linkedlist-to-quicklist/15774679538508.jpg"  title="quicklist示意图" alt="quicklist示意图"/><h2 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a>性能对比</h2><p>测试平台：macOS Catalina 10.15.2，Intel Core i7 2.2GHz，16GB 1600MHz DDR3</p><p>因为系统上已有通过homebrew安装64位的5.0.7版本Redis，因此先看这个版本。因为打算对比quicklist、ziplist以及linkedlist，所以选择list结构进行测试。为了测试存储空间、插入删除性能，在不同测试中均使用<code>redis-benchmark</code>执行相同的测试。</p><p>对于ziplist以及linkedlist，使用本地编译的64位3.0.6版本。</p><h3 id="quicklist的性能"><a href="#quicklist的性能" class="headerlink" title="quicklist的性能"></a>quicklist的性能</h3><p>首先向quicklist插入1000条定长数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 1000</span><br><span class="line">====== LPUSH ======</span><br><span class="line">  1000 requests completed <span class="keyword">in</span> 0.02 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">99.90% &lt;= 1 milliseconds</span><br><span class="line">100.00% &lt;= 1 milliseconds</span><br><span class="line">55555.56 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; memory usage mylist</span><br><span class="line">(<span class="built_in">integer</span>) 5131</span><br></pre></td></tr></table></figure><p>实际使用5131字节，相当于每个元素使用约5.1字节，空间利用率约58.5%（实际插入的是”xxx“，三个字节长）。</p><p>再向quicklist的list中插入1000000个定长数据</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 1000000</span><br><span class="line"></span><br><span class="line">====== LPUSH ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 12.36 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">99.44% &lt;= 1 milliseconds</span><br><span class="line">99.85% &lt;= 2 milliseconds</span><br><span class="line">99.91% &lt;= 3 milliseconds</span><br><span class="line">99.93% &lt;= 4 milliseconds</span><br><span class="line">99.94% &lt;= 5 milliseconds</span><br><span class="line">99.95% &lt;= 6 milliseconds</span><br><span class="line">99.96% &lt;= 7 milliseconds</span><br><span class="line">99.97% &lt;= 8 milliseconds</span><br><span class="line">99.99% &lt;= 9 milliseconds</span><br><span class="line">99.99% &lt;= 10 milliseconds</span><br><span class="line">100.00% &lt;= 11 milliseconds</span><br><span class="line">100.00% &lt;= 17 milliseconds</span><br><span class="line">100.00% &lt;= 18 milliseconds</span><br><span class="line">100.00% &lt;= 18 milliseconds</span><br><span class="line">80893.05 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7fd2ca51c2e0 refcount:1 encoding:quicklist serializedlength:72148 lru:462431 lru_seconds_idle:57 ql_nodes:612 ql_avg_node:1633.99 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:5006732</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 1000000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.80 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.47% &lt;= 1 milliseconds</span><br><span class="line">99.65% &lt;= 2 milliseconds</span><br><span class="line">99.80% &lt;= 3 milliseconds</span><br><span class="line">99.87% &lt;= 4 milliseconds</span><br><span class="line">99.89% &lt;= 5 milliseconds</span><br><span class="line">99.91% &lt;= 6 milliseconds</span><br><span class="line">99.92% &lt;= 7 milliseconds</span><br><span class="line">99.94% &lt;= 8 milliseconds</span><br><span class="line">99.95% &lt;= 9 milliseconds</span><br><span class="line">99.97% &lt;= 10 milliseconds</span><br><span class="line">99.97% &lt;= 11 milliseconds</span><br><span class="line">99.98% &lt;= 12 milliseconds</span><br><span class="line">99.98% &lt;= 13 milliseconds</span><br><span class="line">99.98% &lt;= 14 milliseconds</span><br><span class="line">99.99% &lt;= 15 milliseconds</span><br><span class="line">99.99% &lt;= 17 milliseconds</span><br><span class="line">99.99% &lt;= 21 milliseconds</span><br><span class="line">99.99% &lt;= 22 milliseconds</span><br><span class="line">99.99% &lt;= 26 milliseconds</span><br><span class="line">100.00% &lt;= 27 milliseconds</span><br><span class="line">100.00% &lt;= 29 milliseconds</span><br><span class="line">100.00% &lt;= 30 milliseconds</span><br><span class="line">100.00% &lt;= 30 milliseconds</span><br><span class="line">72442.77 requests per second</span><br></pre></td></tr></table></figure><p>可以看出，其插入速度基本都能保持在1ms以内，并且在未压缩情况下（value空间小于<code>MIN_COMPRESS_BYTES</code>即48字节，不执行压缩），共有612个quicklist节点，总共占用5006732字节内存，即每个值仅占用约5字节，而实际插入的值<code>&quot;xxx&quot;</code>本身是3字节长，约60%的空间利用率。弹出速度也大量保持在1ms以内。</p><p>接着尝试插入更长的数据，先不开启quicklist的，再看看插入和弹出性能以及内存占用情况：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -n 1000000 lpush mylist <span class="string">&quot;mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, while in traditional key-value stores you associated string keys to string values, in Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, which will be covered separately in this tutorial&quot;</span></span><br><span class="line">====== lpush mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, <span class="keyword">while</span> <span class="keyword">in</span> traditional key-value stores you associated string keys to string values, <span class="keyword">in</span> Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, <span class="built_in">which</span> will be covered separately <span class="keyword">in</span> this tutorial ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 14.27 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">97.91% &lt;= 1 milliseconds</span><br><span class="line">99.61% &lt;= 2 milliseconds</span><br><span class="line">99.83% &lt;= 3 milliseconds</span><br><span class="line">99.88% &lt;= 4 milliseconds</span><br><span class="line">99.90% &lt;= 5 milliseconds</span><br><span class="line">99.92% &lt;= 6 milliseconds</span><br><span class="line">99.93% &lt;= 7 milliseconds</span><br><span class="line">99.94% &lt;= 8 milliseconds</span><br><span class="line">99.95% &lt;= 9 milliseconds</span><br><span class="line">99.95% &lt;= 10 milliseconds</span><br><span class="line">99.96% &lt;= 11 milliseconds</span><br><span class="line">99.98% &lt;= 12 milliseconds</span><br><span class="line">99.98% &lt;= 13 milliseconds</span><br><span class="line">99.99% &lt;= 16 milliseconds</span><br><span class="line">99.99% &lt;= 17 milliseconds</span><br><span class="line">99.99% &lt;= 18 milliseconds</span><br><span class="line">100.00% &lt;= 19 milliseconds</span><br><span class="line">100.00% &lt;= 28 milliseconds</span><br><span class="line">100.00% &lt;= 28 milliseconds</span><br><span class="line">70081.99 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7fd2cfa025d0 refcount:1 encoding:quicklist serializedlength:29294298 lru:465570 lru_seconds_idle:6 ql_nodes:58824 ql_avg_node:17.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:470411768</span><br><span class="line">127.0.0.1:6379&gt; memory usage mylist</span><br><span class="line">(<span class="built_in">integer</span>) 428062336</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 1000000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.50 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.24% &lt;= 1 milliseconds</span><br><span class="line">99.58% &lt;= 2 milliseconds</span><br><span class="line">99.77% &lt;= 3 milliseconds</span><br><span class="line">99.85% &lt;= 4 milliseconds</span><br><span class="line">99.88% &lt;= 5 milliseconds</span><br><span class="line">99.91% &lt;= 6 milliseconds</span><br><span class="line">99.94% &lt;= 7 milliseconds</span><br><span class="line">99.95% &lt;= 8 milliseconds</span><br><span class="line">99.96% &lt;= 9 milliseconds</span><br><span class="line">99.96% &lt;= 10 milliseconds</span><br><span class="line">99.97% &lt;= 11 milliseconds</span><br><span class="line">99.98% &lt;= 12 milliseconds</span><br><span class="line">99.98% &lt;= 13 milliseconds</span><br><span class="line">99.99% &lt;= 14 milliseconds</span><br><span class="line">99.99% &lt;= 15 milliseconds</span><br><span class="line">99.99% &lt;= 16 milliseconds</span><br><span class="line">100.00% &lt;= 17 milliseconds</span><br><span class="line">100.00% &lt;= 18 milliseconds</span><br><span class="line">74057.62 requests per second</span><br></pre></td></tr></table></figure><p>可以看出，随着字符串的变长，实际的插入、弹出时间相差不大，每个元素占用空间<code>470411768/1000000≈470</code>字节，约98.5%的空间利用率。实际内存空间使用<code>428062336</code>字节，约408M。</p><p>如果开启压缩，设置<code>list-compress-depth</code>为1，再进行相同的测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -n 1000000 lpush mylist <span class="string">&quot;mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, while in traditional key-value stores you associated string keys to string values, in Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, which will be covered separately in this tutorial&quot;</span></span><br><span class="line">====== lpush mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, <span class="keyword">while</span> <span class="keyword">in</span> traditional key-value stores you associated string keys to string values, <span class="keyword">in</span> Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, <span class="built_in">which</span> will be covered separately <span class="keyword">in</span> this tutorial ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.99 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.09% &lt;= 1 milliseconds</span><br><span class="line">99.72% &lt;= 2 milliseconds</span><br><span class="line">99.86% &lt;= 3 milliseconds</span><br><span class="line">99.90% &lt;= 4 milliseconds</span><br><span class="line">99.92% &lt;= 5 milliseconds</span><br><span class="line">99.94% &lt;= 6 milliseconds</span><br><span class="line">99.94% &lt;= 7 milliseconds</span><br><span class="line">99.95% &lt;= 8 milliseconds</span><br><span class="line">99.96% &lt;= 9 milliseconds</span><br><span class="line">99.96% &lt;= 10 milliseconds</span><br><span class="line">99.97% &lt;= 11 milliseconds</span><br><span class="line">99.97% &lt;= 12 milliseconds</span><br><span class="line">99.98% &lt;= 13 milliseconds</span><br><span class="line">99.99% &lt;= 14 milliseconds</span><br><span class="line">99.99% &lt;= 15 milliseconds</span><br><span class="line">100.00% &lt;= 24 milliseconds</span><br><span class="line">100.00% &lt;= 25 milliseconds</span><br><span class="line">100.00% &lt;= 27 milliseconds</span><br><span class="line">71489.85 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7fd2ca4355f0 refcount:1 encoding:quicklist serializedlength:29294298 lru:465449 lru_seconds_idle:6 ql_nodes:58824 ql_avg_node:17.00 ql_ziplist_max:-2 ql_compressed:1 ql_uncompressed_size:470411768</span><br><span class="line">127.0.0.1:6379&gt; memory usage mylist</span><br><span class="line">(<span class="built_in">integer</span>) 74930099</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 1000000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.63 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.42% &lt;= 1 milliseconds</span><br><span class="line">99.69% &lt;= 2 milliseconds</span><br><span class="line">99.84% &lt;= 3 milliseconds</span><br><span class="line">99.88% &lt;= 4 milliseconds</span><br><span class="line">99.90% &lt;= 5 milliseconds</span><br><span class="line">99.91% &lt;= 6 milliseconds</span><br><span class="line">99.93% &lt;= 7 milliseconds</span><br><span class="line">99.95% &lt;= 8 milliseconds</span><br><span class="line">99.98% &lt;= 9 milliseconds</span><br><span class="line">99.98% &lt;= 10 milliseconds</span><br><span class="line">99.99% &lt;= 11 milliseconds</span><br><span class="line">99.99% &lt;= 12 milliseconds</span><br><span class="line">99.99% &lt;= 14 milliseconds</span><br><span class="line">100.00% &lt;= 31 milliseconds</span><br><span class="line">100.00% &lt;= 32 milliseconds</span><br><span class="line">100.00% &lt;= 32 milliseconds</span><br><span class="line">73351.42 requests per second</span><br></pre></td></tr></table></figure><p>可以看出，在进行lzf压缩后，插入、弹出元素的时间相差无几，但是实际的空间占用降到了<code>74930099</code>，即约71M，空间节省极大。</p><h3 id="linkedlist的性能"><a href="#linkedlist的性能" class="headerlink" title="linkedlist的性能"></a>linkedlist的性能</h3><p>在redis中，通过两处配置定义list底层使用的数据结构。<code>list-max-ziplist-entries</code>表示ziplist元素最大值，list-max-ziplist-value表示单个节点的最大长度。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Similarly to hashes, small lists are also encoded in a special way in order</span></span><br><span class="line"><span class="comment"># to save a lot of space. The special representation is only used when</span></span><br><span class="line"><span class="comment"># you are under the following limits:</span></span><br><span class="line">list-max-ziplist-entries 512</span><br><span class="line">list-max-ziplist-value 64</span><br></pre></td></tr></table></figure><p>如果元素的值的长度或者数量超过了配置值的任何一个，则ziplist会自动转变为linkedlist并且不会退化回ziplist，转换的代码如下，可以看到只允许转为<code>REDIS_ENCODING_LINKEDLIST</code>的单向转换。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">listTypeConvert</span><span class="params">(robj *subject, <span class="type">int</span> enc)</span> &#123;</span><br><span class="line">    listTypeIterator *li;</span><br><span class="line">    listTypeEntry entry;</span><br><span class="line">    redisAssertWithInfo(<span class="literal">NULL</span>,subject,subject-&gt;type == REDIS_LIST);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (enc == REDIS_ENCODING_LINKEDLIST) &#123;</span><br><span class="line">        <span class="built_in">list</span> *l = listCreate();</span><br><span class="line">        listSetFreeMethod(l,decrRefCountVoid);</span><br><span class="line"></span><br><span class="line">        <span class="comment">/* listTypeGet returns a robj with incremented refcount */</span></span><br><span class="line">        li = listTypeInitIterator(subject,<span class="number">0</span>,REDIS_TAIL);</span><br><span class="line">        <span class="keyword">while</span> (listTypeNext(li,&amp;entry)) listAddNodeTail(l,listTypeGet(&amp;entry));</span><br><span class="line">        listTypeReleaseIterator(li);</span><br><span class="line"></span><br><span class="line">        subject-&gt;encoding = REDIS_ENCODING_LINKEDLIST;</span><br><span class="line">        zfree(subject-&gt;ptr);</span><br><span class="line">        subject-&gt;ptr = l;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        redisPanic(<span class="string">&quot;Unsupported list conversion&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此启动<code>redis-server</code>时显式的指定<code>list-max-ziplist-entries</code>为0即可使用linkedlist进行测试。</p><p>插入100条数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 100</span><br><span class="line">====== LPUSH ======</span><br><span class="line">  1000 requests completed <span class="keyword">in</span> 0.01 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">100.00% &lt;= 0 milliseconds</span><br><span class="line">66666.67 requests per second</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>通过redisinsight分析其实际使用内存44kb，即单个元素占用约45字节，空间利用率约6.7%。</p><p>同样，插入1000000个定长数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 1000000</span><br><span class="line"></span><br><span class="line">====== LPUSH ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.08 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.75% &lt;= 1 milliseconds</span><br><span class="line">99.72% &lt;= 2 milliseconds</span><br><span class="line">99.83% &lt;= 3 milliseconds</span><br><span class="line">99.87% &lt;= 4 milliseconds</span><br><span class="line">99.90% &lt;= 5 milliseconds</span><br><span class="line">99.92% &lt;= 6 milliseconds</span><br><span class="line">99.94% &lt;= 7 milliseconds</span><br><span class="line">99.95% &lt;= 8 milliseconds</span><br><span class="line">99.96% &lt;= 9 milliseconds</span><br><span class="line">99.96% &lt;= 10 milliseconds</span><br><span class="line">99.97% &lt;= 11 milliseconds</span><br><span class="line">99.97% &lt;= 12 milliseconds</span><br><span class="line">99.99% &lt;= 13 milliseconds</span><br><span class="line">100.00% &lt;= 28 milliseconds</span><br><span class="line">100.00% &lt;= 29 milliseconds</span><br><span class="line">100.00% &lt;= 30 milliseconds</span><br><span class="line">76440.91 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7fc41b433aa0 refcount:1 encoding:linkedlist serializedlength:4000005 lru:472156 lru_seconds_idle:24</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 1000000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 13.80 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.47% &lt;= 1 milliseconds</span><br><span class="line">99.65% &lt;= 2 milliseconds</span><br><span class="line">99.80% &lt;= 3 milliseconds</span><br><span class="line">99.87% &lt;= 4 milliseconds</span><br><span class="line">99.89% &lt;= 5 milliseconds</span><br><span class="line">99.91% &lt;= 6 milliseconds</span><br><span class="line">99.92% &lt;= 7 milliseconds</span><br><span class="line">99.94% &lt;= 8 milliseconds</span><br><span class="line">99.95% &lt;= 9 milliseconds</span><br><span class="line">99.97% &lt;= 10 milliseconds</span><br><span class="line">99.97% &lt;= 11 milliseconds</span><br><span class="line">99.98% &lt;= 12 milliseconds</span><br><span class="line">99.98% &lt;= 13 milliseconds</span><br><span class="line">99.98% &lt;= 14 milliseconds</span><br><span class="line">99.99% &lt;= 15 milliseconds</span><br><span class="line">99.99% &lt;= 17 milliseconds</span><br><span class="line">99.99% &lt;= 21 milliseconds</span><br><span class="line">99.99% &lt;= 22 milliseconds</span><br><span class="line">99.99% &lt;= 26 milliseconds</span><br><span class="line">100.00% &lt;= 27 milliseconds</span><br><span class="line">100.00% &lt;= 29 milliseconds</span><br><span class="line">100.00% &lt;= 30 milliseconds</span><br><span class="line">100.00% &lt;= 30 milliseconds</span><br><span class="line">72442.77 requests per second</span><br></pre></td></tr></table></figure><p>其实际使用内存43M，即单个节点使用约45字节的空间，空间利用率约6.7%。但是在插入与弹出的时间消耗上，和quicklist相差不大。</p><p>再看看插入长字符串的情况：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -n 1000000 lpush mylist <span class="string">&quot;mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, while in traditional key-value stores you associated string keys to string values, in Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, which will be covered separately in this tutorial&quot;</span></span><br><span class="line">====== lpush mylist mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, <span class="keyword">while</span> <span class="keyword">in</span> traditional key-value stores you associated string keys to string values, <span class="keyword">in</span> Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, <span class="built_in">which</span> will be covered separately <span class="keyword">in</span> this tutorial ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 14.24 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">97.33% &lt;= 1 milliseconds</span><br><span class="line">99.64% &lt;= 2 milliseconds</span><br><span class="line">99.83% &lt;= 3 milliseconds</span><br><span class="line">99.87% &lt;= 4 milliseconds</span><br><span class="line">99.91% &lt;= 5 milliseconds</span><br><span class="line">99.93% &lt;= 6 milliseconds</span><br><span class="line">99.94% &lt;= 7 milliseconds</span><br><span class="line">99.94% &lt;= 8 milliseconds</span><br><span class="line">99.97% &lt;= 9 milliseconds</span><br><span class="line">99.98% &lt;= 10 milliseconds</span><br><span class="line">99.98% &lt;= 11 milliseconds</span><br><span class="line">99.99% &lt;= 12 milliseconds</span><br><span class="line">99.99% &lt;= 13 milliseconds</span><br><span class="line">100.00% &lt;= 15 milliseconds</span><br><span class="line">100.00% &lt;= 16 milliseconds</span><br><span class="line">100.00% &lt;= 17 milliseconds</span><br><span class="line">100.00% &lt;= 18 milliseconds</span><br><span class="line">100.00% &lt;= 25 milliseconds</span><br><span class="line">100.00% &lt;= 25 milliseconds</span><br><span class="line">70239.52 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7fc616f03f80 refcount:1 encoding:linkedlist serializedlength:371000005 lru:473445 lru_seconds_idle:26</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 1000000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  1000000 requests completed <span class="keyword">in</span> 12.65 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">98.67% &lt;= 1 milliseconds</span><br><span class="line">99.78% &lt;= 2 milliseconds</span><br><span class="line">99.89% &lt;= 3 milliseconds</span><br><span class="line">99.90% &lt;= 4 milliseconds</span><br><span class="line">99.92% &lt;= 5 milliseconds</span><br><span class="line">99.95% &lt;= 6 milliseconds</span><br><span class="line">99.95% &lt;= 7 milliseconds</span><br><span class="line">99.97% &lt;= 8 milliseconds</span><br><span class="line">99.98% &lt;= 9 milliseconds</span><br><span class="line">99.98% &lt;= 10 milliseconds</span><br><span class="line">99.98% &lt;= 11 milliseconds</span><br><span class="line">99.99% &lt;= 12 milliseconds</span><br><span class="line">100.00% &lt;= 13 milliseconds</span><br><span class="line">100.00% &lt;= 13 milliseconds</span><br><span class="line">79026.39 requests per second</span><br></pre></td></tr></table></figure><p>通过redisinsight分析其实际使用内存488M，即单个节点使用约512字节的空间，空间利用率约90.4%。但是在插入与弹出的时间消耗上，和quicklist以及短字符串插入都相差不大。</p><h3 id="ziplist的性能"><a href="#ziplist的性能" class="headerlink" title="ziplist的性能"></a>ziplist的性能</h3><p>最后再看看ziplist的表现。设置<code>list-max-ziplist-entries</code>与<code>list-max-ziplist-value</code>为较大的值来启动redis-server，保证使用ziplist编码来实现list。我们先插入比较少的数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 100</span><br><span class="line">====== LPUSH ======</span><br><span class="line">  100 requests completed <span class="keyword">in</span> 0.00 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">100.00% &lt;= 0 milliseconds</span><br><span class="line">50000.00 requests per second</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7f82e9100890 refcount:1 encoding:ziplist serializedlength:30 lru:473656 lru_seconds_idle:36</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 100</span><br><span class="line">====== LPOP ======</span><br><span class="line">  100 requests completed <span class="keyword">in</span> 0.00 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">100.00% &lt;= 0 milliseconds</span><br><span class="line">33333.33 requests per second</span><br></pre></td></tr></table></figure><p>分析内存占用，100个元素总共占用约553字节空间，平均一个元素约5.5字节，空间利用率约54.5%。</p><p>因为ziplist插入数据量过大可能非常的慢，甚至每秒的请求数量能到个位数，因此来看看插入100000个元素的情况：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -t lpush -n 100000</span><br><span class="line">====== LPUSH ======</span><br><span class="line">  100000 requests completed <span class="keyword">in</span> 39.20 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">37.85% &lt;= 1 milliseconds</span><br><span class="line">64.63% &lt;= 2 milliseconds</span><br><span class="line">65.22% &lt;= 3 milliseconds</span><br><span class="line">65.37% &lt;= 4 milliseconds</span><br><span class="line">65.43% &lt;= 5 milliseconds</span><br><span class="line">65.43% &lt;= 6 milliseconds</span><br><span class="line">65.45% &lt;= 7 milliseconds</span><br><span class="line">65.47% &lt;= 9 milliseconds</span><br><span class="line">65.47% &lt;= 11 milliseconds</span><br><span class="line">65.47% &lt;= 12 milliseconds</span><br><span class="line">65.47% &lt;= 13 milliseconds</span><br><span class="line">65.48% &lt;= 14 milliseconds</span><br><span class="line">65.53% &lt;= 15 milliseconds</span><br><span class="line">65.53% &lt;= 16 milliseconds</span><br><span class="line">65.53% &lt;= 17 milliseconds</span><br><span class="line">65.53% &lt;= 18 milliseconds</span><br><span class="line">65.53% &lt;= 19 milliseconds</span><br><span class="line">65.54% &lt;= 20 milliseconds</span><br><span class="line">65.54% &lt;= 23 milliseconds</span><br><span class="line">65.54% &lt;= 26 milliseconds</span><br><span class="line">65.54% &lt;= 27 milliseconds</span><br><span class="line">65.54% &lt;= 28 milliseconds</span><br><span class="line">65.54% &lt;= 29 milliseconds</span><br><span class="line">65.55% &lt;= 30 milliseconds</span><br><span class="line">65.55% &lt;= 32 milliseconds</span><br><span class="line">65.55% &lt;= 34 milliseconds</span><br><span class="line">65.55% &lt;= 35 milliseconds</span><br><span class="line">65.56% &lt;= 36 milliseconds</span><br><span class="line">65.56% &lt;= 38 milliseconds</span><br><span class="line">65.57% &lt;= 39 milliseconds</span><br><span class="line">65.57% &lt;= 40 milliseconds</span><br><span class="line">65.57% &lt;= 41 milliseconds</span><br><span class="line">65.58% &lt;= 42 milliseconds</span><br><span class="line">65.97% &lt;= 43 milliseconds</span><br><span class="line">67.04% &lt;= 44 milliseconds</span><br><span class="line">68.92% &lt;= 45 milliseconds</span><br><span class="line">70.23% &lt;= 46 milliseconds</span><br><span class="line">71.32% &lt;= 47 milliseconds</span><br><span class="line">72.07% &lt;= 48 milliseconds</span><br><span class="line">72.97% &lt;= 49 milliseconds</span><br><span class="line">74.51% &lt;= 50 milliseconds</span><br><span class="line">76.12% &lt;= 51 milliseconds</span><br><span class="line">78.02% &lt;= 52 milliseconds</span><br><span class="line">80.05% &lt;= 53 milliseconds</span><br><span class="line">81.86% &lt;= 54 milliseconds</span><br><span class="line">83.42% &lt;= 55 milliseconds</span><br><span class="line">84.66% &lt;= 56 milliseconds</span><br><span class="line">86.14% &lt;= 57 milliseconds</span><br><span class="line">87.74% &lt;= 58 milliseconds</span><br><span class="line">89.33% &lt;= 59 milliseconds</span><br><span class="line">90.45% &lt;= 60 milliseconds</span><br><span class="line">91.75% &lt;= 61 milliseconds</span><br><span class="line">93.67% &lt;= 62 milliseconds</span><br><span class="line">95.49% &lt;= 63 milliseconds</span><br><span class="line">96.69% &lt;= 64 milliseconds</span><br><span class="line">97.84% &lt;= 65 milliseconds</span><br><span class="line">98.68% &lt;= 66 milliseconds</span><br><span class="line">99.03% &lt;= 67 milliseconds</span><br><span class="line">99.20% &lt;= 68 milliseconds</span><br><span class="line">99.43% &lt;= 69 milliseconds</span><br><span class="line">99.54% &lt;= 70 milliseconds</span><br><span class="line">99.66% &lt;= 71 milliseconds</span><br><span class="line">99.78% &lt;= 72 milliseconds</span><br><span class="line">99.84% &lt;= 73 milliseconds</span><br><span class="line">99.87% &lt;= 74 milliseconds</span><br><span class="line">99.90% &lt;= 75 milliseconds</span><br><span class="line">99.92% &lt;= 76 milliseconds</span><br><span class="line">99.93% &lt;= 77 milliseconds</span><br><span class="line">99.96% &lt;= 78 milliseconds</span><br><span class="line">99.97% &lt;= 79 milliseconds</span><br><span class="line">99.99% &lt;= 80 milliseconds</span><br><span class="line">100.00% &lt;= 81 milliseconds</span><br><span class="line">2551.15 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7f82e9100890 refcount:1 encoding:ziplist serializedlength:30 lru:473656 lru_seconds_idle:36</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 100000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  100000 requests completed <span class="keyword">in</span> 21.04 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">42.08% &lt;= 1 milliseconds</span><br><span class="line">63.70% &lt;= 2 milliseconds</span><br><span class="line">65.00% &lt;= 3 milliseconds</span><br><span class="line">65.26% &lt;= 4 milliseconds</span><br><span class="line">65.36% &lt;= 5 milliseconds</span><br><span class="line">65.37% &lt;= 6 milliseconds</span><br><span class="line">65.38% &lt;= 7 milliseconds</span><br><span class="line">65.39% &lt;= 8 milliseconds</span><br><span class="line">65.44% &lt;= 9 milliseconds</span><br><span class="line">65.45% &lt;= 10 milliseconds</span><br><span class="line">65.50% &lt;= 11 milliseconds</span><br><span class="line">65.54% &lt;= 12 milliseconds</span><br><span class="line">65.54% &lt;= 13 milliseconds</span><br><span class="line">65.54% &lt;= 14 milliseconds</span><br><span class="line">65.55% &lt;= 15 milliseconds</span><br><span class="line">65.56% &lt;= 16 milliseconds</span><br><span class="line">65.57% &lt;= 17 milliseconds</span><br><span class="line">65.60% &lt;= 18 milliseconds</span><br><span class="line">65.68% &lt;= 19 milliseconds</span><br><span class="line">65.80% &lt;= 20 milliseconds</span><br><span class="line">66.01% &lt;= 21 milliseconds</span><br><span class="line">66.39% &lt;= 22 milliseconds</span><br><span class="line">67.35% &lt;= 23 milliseconds</span><br><span class="line">69.41% &lt;= 24 milliseconds</span><br><span class="line">72.64% &lt;= 25 milliseconds</span><br><span class="line">75.51% &lt;= 26 milliseconds</span><br><span class="line">77.92% &lt;= 27 milliseconds</span><br><span class="line">80.83% &lt;= 28 milliseconds</span><br><span class="line">83.25% &lt;= 29 milliseconds</span><br><span class="line">87.63% &lt;= 30 milliseconds</span><br><span class="line">91.26% &lt;= 31 milliseconds</span><br><span class="line">94.02% &lt;= 32 milliseconds</span><br><span class="line">96.35% &lt;= 33 milliseconds</span><br><span class="line">97.76% &lt;= 34 milliseconds</span><br><span class="line">98.78% &lt;= 35 milliseconds</span><br><span class="line">99.20% &lt;= 36 milliseconds</span><br><span class="line">99.45% &lt;= 37 milliseconds</span><br><span class="line">99.56% &lt;= 38 milliseconds</span><br><span class="line">99.66% &lt;= 39 milliseconds</span><br><span class="line">99.70% &lt;= 40 milliseconds</span><br><span class="line">99.78% &lt;= 41 milliseconds</span><br><span class="line">99.84% &lt;= 42 milliseconds</span><br><span class="line">99.86% &lt;= 43 milliseconds</span><br><span class="line">99.87% &lt;= 44 milliseconds</span><br><span class="line">99.90% &lt;= 45 milliseconds</span><br><span class="line">99.92% &lt;= 46 milliseconds</span><br><span class="line">99.99% &lt;= 47 milliseconds</span><br><span class="line">99.99% &lt;= 48 milliseconds</span><br><span class="line">100.00% &lt;= 48 milliseconds</span><br><span class="line">4753.08 requests per second</span><br></pre></td></tr></table></figure><p>显然，插入的速度相比quicklist、linkedlist以及小规模数据量的ziplist时明显慢了许多。并且能看到，随着数据插入越来越多，插入的速度越来越慢，从数万左右的每秒请求数量慢慢下降到最后的几千每秒请求数量。在100000个元素时，内存占用约488kb，即每个元素约5.0字节，空间利用率约60%，可以看到，空间的占用几乎是线性的关系，并且空间利用率反而增加了一些。</p><p>在弹出数据时可以看到，速度越来越快，从1k左右上升到最终的数万每秒请求数量。</p><p>对于长字符串的插入，先插入100条：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -n 100 lpush mylist <span class="string">&quot;mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, while in traditional key-value stores you associated string keys to string values, in Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, which will be covered separately in this tutorial&quot;</span></span><br><span class="line"></span><br><span class="line">====== lpush mylist mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, <span class="keyword">while</span> <span class="keyword">in</span> traditional key-value stores you associated string keys to string values, <span class="keyword">in</span> Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, <span class="built_in">which</span> will be covered separately <span class="keyword">in</span> this tutorial ======</span><br><span class="line">  100 requests completed <span class="keyword">in</span> 0.00 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">20.00% &lt;= 1 milliseconds</span><br><span class="line">100.00% &lt;= 1 milliseconds</span><br><span class="line">25000.00 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7f82e7f03b40 refcount:1 encoding:ziplist serializedlength:1130 lru:475907 lru_seconds_idle:55</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 100</span><br><span class="line">====== LPOP ======</span><br><span class="line">  100 requests completed <span class="keyword">in</span> 0.00 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">46.00% &lt;= 1 milliseconds</span><br><span class="line">100.00% &lt;= 1 milliseconds</span><br><span class="line">33333.33 requests per second</span><br></pre></td></tr></table></figure><p>可以看出，插入的时间明显比短字符串更多。插入后总共占用了47kb空间，即每个元素约481字节空间，空间利用率约96.3%。</p><p>再来看长字符串的批量插入（日志有删减）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line">$ redis-benchmark -n 100000 lpush mylist <span class="string">&quot;mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, while in traditional key-value stores you associated string keys to string values, in Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, which will be covered separately in this tutorial&quot;</span></span><br><span class="line"></span><br><span class="line">====== lpush mylist mylist str len: 463. Redis is not a plain key-value store, it is actually a data structures server, supporting different kinds of values. What this means is that, <span class="keyword">while</span> <span class="keyword">in</span> traditional key-value stores you associated string keys to string values, <span class="keyword">in</span> Redis the value is not limited to a simple string, but can also hold more complex data structures. The following is the list of all the data structures supported by Redis, <span class="built_in">which</span> will be covered separately <span class="keyword">in</span> this tutorial ======</span><br><span class="line">  100000 requests completed <span class="keyword">in</span> 361.97 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line"></span><br><span class="line">0.34% &lt;= 1 milliseconds</span><br><span class="line">1.22% &lt;= 2 milliseconds</span><br><span class="line">1.84% &lt;= 3 milliseconds</span><br><span class="line">3.02% &lt;= 4 milliseconds</span><br><span class="line">3.97% &lt;= 5 milliseconds</span><br><span class="line">4.91% &lt;= 6 milliseconds</span><br><span class="line">5.91% &lt;= 7 milliseconds</span><br><span class="line">6.47% &lt;= 8 milliseconds</span><br><span class="line">7.25% &lt;= 9 milliseconds</span><br><span class="line">7.78% &lt;= 10 milliseconds</span><br><span class="line">13.51% &lt;= 20 milliseconds</span><br><span class="line">18.07% &lt;= 30 milliseconds</span><br><span class="line">23.39% &lt;= 40 milliseconds</span><br><span class="line">28.43% &lt;= 50 milliseconds</span><br><span class="line">33.09% &lt;= 60 milliseconds</span><br><span class="line">36.63% &lt;= 70 milliseconds</span><br><span class="line">41.06% &lt;= 80 milliseconds</span><br><span class="line">46.23% &lt;= 90 milliseconds</span><br><span class="line">49.85% &lt;= 100 milliseconds</span><br><span class="line">61.87% &lt;= 120 milliseconds</span><br><span class="line">64.51% &lt;= 140 milliseconds</span><br><span class="line">65.13% &lt;= 170 milliseconds</span><br><span class="line">65.46% &lt;= 202 milliseconds</span><br><span class="line">66.86% &lt;= 320 milliseconds</span><br><span class="line">71.61% &lt;= 350 milliseconds</span><br><span class="line">78.91% &lt;= 380 milliseconds</span><br><span class="line">84.36% &lt;= 410 milliseconds</span><br><span class="line">90.07% &lt;= 440 milliseconds</span><br><span class="line">95.08% &lt;= 470 milliseconds</span><br><span class="line">98.34% &lt;= 510 milliseconds</span><br><span class="line">99.45% &lt;= 543 milliseconds</span><br><span class="line">99.80% &lt;= 570 milliseconds</span><br><span class="line">100.00% &lt;= 746 milliseconds</span><br><span class="line">276.26 requests per second</span><br><span class="line"></span><br><span class="line">$ redis-cli</span><br><span class="line">127.0.0.1:6379&gt; DEBUG OBJECT mylist</span><br><span class="line">Value at:0x7f82e7e02660 refcount:1 encoding:ziplist serializedlength:661619 lru:476557 lru_seconds_idle:579</span><br><span class="line"></span><br><span class="line">$ redis-benchmark -t lpop -n 100000</span><br><span class="line">====== LPOP ======</span><br><span class="line">  100000 requests completed <span class="keyword">in</span> 344.83 seconds</span><br><span class="line">  50 parallel clients</span><br><span class="line">  3 bytes payload</span><br><span class="line">  keep alive: 1</span><br><span class="line">0.31% &lt;= 1 milliseconds</span><br><span class="line">1.17% &lt;= 2 milliseconds</span><br><span class="line">2.41% &lt;= 3 milliseconds</span><br><span class="line">3.35% &lt;= 4 milliseconds</span><br><span class="line">4.14% &lt;= 5 milliseconds</span><br><span class="line">5.10% &lt;= 6 milliseconds</span><br><span class="line">5.84% &lt;= 7 milliseconds</span><br><span class="line">6.73% &lt;= 8 milliseconds</span><br><span class="line">7.50% &lt;= 9 milliseconds</span><br><span class="line">8.19% &lt;= 10 milliseconds</span><br><span class="line">55.85% &lt;= 100 milliseconds</span><br><span class="line">65.55% &lt;= 211 milliseconds</span><br><span class="line">85.84% &lt;= 300 milliseconds</span><br><span class="line">99.40% &lt;= 400 milliseconds</span><br><span class="line">100.00% &lt;= 72866 milliseconds</span><br><span class="line">290.00 requests per second</span><br></pre></td></tr></table></figure><p>可以看到，在单个元素比较大时，插入、弹出ziplist会更加的耗时，但是内存总共占用45M，即单个元素占用约472字节内存，内存利用率达到98%。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>从上面的试验可以看到，ziplist对空间的利用率非常高，在数据规模比较小时，耗时相对可接受，但是对于元素比较多或者是单个元素比较长时，插入、弹出的耗时非常大。而linkedlist在插入、删除元素时，元素数量、单个元素的长度对耗时影响小（耗时分布比较集中），但是空间利用率比较差，特别是数据规模较小时，空间利用率非常差。而quicklist结合了二者的优点，首先时间消耗上，数据规模对其影响小，其次是空间利用率，因为底层使用了ziplist，所以在小规模数据上空间表现也良好。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;List的底层编码及演进&quot;&gt;&lt;a href=&quot;#List的底层编码及演进&quot; class=&quot;headerlink&quot; title=&quot;List的底层编码及演进&quot;&gt;&lt;/a&gt;List的底层编码及演进&lt;/h2&gt;&lt;p&gt;Redis对外暴露最基本的5种结构，比如String、Lis</summary>
      
    
    
    
    <category term="Redis" scheme="http://blog.liexing.me/categories/Redis/"/>
    
    
    <category term="笔记" scheme="http://blog.liexing.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="redis" scheme="http://blog.liexing.me/tags/redis/"/>
    
    <category term="数据结构" scheme="http://blog.liexing.me/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="quicklist" scheme="http://blog.liexing.me/tags/quicklist/"/>
    
    <category term="ziplist" scheme="http://blog.liexing.me/tags/ziplist/"/>
    
  </entry>
  
  <entry>
    <title>两人一车，说走就走的西藏自驾游（5）：青海归来</title>
    <link href="http://blog.liexing.me/2019/12/01/Tibetan-Self-driving-Travel-Notes-5/"/>
    <id>http://blog.liexing.me/2019/12/01/Tibetan-Self-driving-Travel-Notes-5/</id>
    <published>2019-12-01T08:08:22.000Z</published>
    <updated>2023-01-20T15:07:36.858Z</updated>
    
    <content type="html"><![CDATA[<p>索引：</p><ul><li><a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/">两人一车，说走就走的西藏自驾游（2）：走上318</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/">两人一车，说走就走的西藏自驾游（3）：到达拉萨</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-4/">两人一车，说走就走的西藏自驾游（4）：青藏出藏</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-5/">两人一车，说走就走的西藏自驾游（5）：青海归来</a></li></ul><p>嗯万万没想到，拖了一年采开始写的游记，写的过程也被我拖延了半年，终于结束双十一啦，可以把这个游记彻底撸完了。</p><h1 id="Day14：格尔木-德令哈（0526）"><a href="#Day14：格尔木-德令哈（0526）" class="headerlink" title="Day14：格尔木-德令哈（0526）"></a>Day14：格尔木-德令哈（0526）</h1><p>到了青海，是的我们居然没去青海湖，惊不惊喜意不意外。早晨在KFC吃过早餐后发现商场有个卖书的活动，顺手买了本仓央嘉措诗传全集，毕竟刚从西藏回来，假装文艺一把。在格尔木时候发现以及出来半个月了，然后想开车回家后面车就不开回北京了，所以如果往东去青海湖玩，可能还得废掉几天，并且再开回成都比较麻烦，因此就打住了。看了下附件有个水上雅丹的地方，看几个月前的游记还能露营然后风景很好，所以决定去水上雅丹露营，带了一路，跟着我们翻山越岭的露营装备看上去终于能用上了。</p><p>决定好了就出发了，出格尔木不久，就进入大漠的感觉，路边很荒凉，但是路超级棒，而且还没啥车。在马路上各种角度各种拍，茫茫大漠就我们自己的感觉真的超级棒（嗯一会儿就知道多吓人了）。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575189449.jpg"  title="荒漠中各种拍" alt="荒漠中各种拍"/>继续开了不久，路边的大漠慢慢变了样，呈现出雅丹地貌的特征，大自然的鬼斧神工，很是漂亮。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575189911.jpg"  title="雅丹地貌" alt="雅丹地貌"/>开到下午四点多，穿过了茫茫大漠中的一个锂电池工厂小镇，终于到了水上雅丹，结果居然围起来了在修建了，而且现在啥都没开放，只有个非常low的旅馆，并且工作人员态度非常不好(=@__@=)。当然不能就这么挨宰，怀着一丝希望，想着绕远点应该能找到没有围起来的地方，然而想多了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575189928.jpg"  title="远远的拍了下水上雅丹" alt="远远的拍了下水上雅丹"/>结果绕着绕着就绕了太远，前面是一片（咸水）湖，景色还不错的，路把湖水分成了两块，两个颜色还不一样，湖面不时飞过几只水鸟。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575190433.jpg"  title="荒漠中的湖" alt="荒漠中的湖"/>这时候已经下午六点多了，决定不住在水上雅丹就得往回撤了，准备在前面路过的随便哪个地方露营一宿，感觉也挺棒。往回在水上雅丹外面觉得景还是很棒，把车上的小蜘蛛侠拿出来拍了几张<img src="/images/Tibetan-Self-driving-Travel-Notes/1575190701.jpg"  title="水上雅丹外的小蜘蛛" alt="水上雅丹外的小蜘蛛"/>继续往回开，不一会儿晚上八点半了，天开始黑了，路边停下来吃了个自热火锅（对的，从北京一路带来的自热火锅），边吃边和@xqq聊，今晚就这里搭帐篷了，感觉还不错。吃完饭天就黑了，不知道哪里传来声狼嚎，卧槽不行啊，两人开始打退堂鼓了。想想还是命要紧，感觉收拾收拾上车跑路吧。收拾完撤的时候才觉得这决定真的很明智，随着天黑下来，四处非常安静，我一个晚上都敢自己在乱坟堆乱窜的人居然怕黑了，感觉黑暗在往体内渗，瘆得慌。车上摇滚开到最大声，车窗紧闭，都不敢停下来上厕所，不敢换开，一路飙到德令哈。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>过路费</td><td>19</td><td></td></tr><tr><td>午饭</td><td>75.5</td><td></td></tr><tr><td>过路费</td><td>48</td><td>锡铁山</td></tr><tr><td>加油</td><td>200</td><td></td></tr><tr><td>加油</td><td>200</td><td></td></tr><tr><td>过路费</td><td>29</td><td>饮水梁</td></tr><tr><td>过路费</td><td>29</td><td></td></tr><tr><td>共计</td><td>600.5</td><td></td></tr></tbody></table><h1 id="Day15-德令哈-久治（0527）"><a href="#Day15-德令哈-久治（0527）" class="headerlink" title="Day15 德令哈-久治（0527）"></a>Day15 德令哈-久治（0527）</h1><p>接下来主要是赶路了，要开始虎头蛇尾流水账模式了。</p><p>德令哈住了一宿，和@xqq商量是时候撤啦。准备直接开回成都。刚好有个德马高速刚修通不久，准备走这条高速回去。</p><p>新高速的确新，没啥车，没啥人，服务区空的，路边居然还有野骆驼，超级原生态。<br><img src="/images/Tibetan-Self-driving-Travel-Notes/1575191911.jpg"  title="德马高速" alt="德马高速"/><br>服务区没人也没吃的，所以大中午的，终于用上了带了一路的煮面套装神器了（其实还带着米啥的，然而煮粥得等太久了，所以煮面了）。<br><img src="/images/Tibetan-Self-driving-Travel-Notes/1575192007.jpg"  title="高速上煮泡面" alt="高速上煮泡面"/><br>吃过泡面沿着这条高速跑了很久很久很久，也没看到加油站，异常心慌。随着油表上的读数越来越小，越来越慌，一路尽可能保持匀速，下坡也不太敢踩刹车。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575192345.jpg"  title="路边的山与寺" alt="路边的山与寺"/>最惨的是，路上信号还不好，周围也没什么车，寻思着要是天黑了又没油了，到时候就找个安全的地方搭帐篷，天亮了拦车求助了。到了天快黑的时候终于到了个小县城加上油，油箱总共60L容量，加了59升多的油，太悬了。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>过路费</td><td>176</td><td>大武</td></tr><tr><td>加油</td><td>320</td><td></td></tr><tr><td>过路费</td><td>84</td><td>久治</td></tr><tr><td>住宿</td><td>160</td><td></td></tr><tr><td>共计</td><td>740</td><td></td></tr></tbody></table><h1 id="Day16-久治-都江堰（0528）"><a href="#Day16-久治-都江堰（0528）" class="headerlink" title="Day16 久治-都江堰（0528）"></a>Day16 久治-都江堰（0528）</h1><p>出发不久就进入大四川地界啦，从阿坝进川，在四川界附近就遇到一场雪，景色很美，南国的雪原来也能很美。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575192713.jpg"  title="阿坝的雪" alt="阿坝的雪"/>慢慢的，海拔越来越低，景色也越来越熟悉了。告别最后一片雪山，不久终于到都江堰了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575192894.jpg"  title="最后一片雪山" alt="最后一片雪山"/>晚上在都江堰吃了个鱼，真的超级棒，还在地图上留了个收藏，下次决定再去吃。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>早餐</td><td>14</td><td></td></tr><tr><td>加油</td><td>150</td><td></td></tr><tr><td>水果</td><td>26</td><td></td></tr><tr><td>午餐</td><td>52</td><td></td></tr><tr><td>过路费</td><td>17</td><td></td></tr><tr><td>晚餐</td><td>215.65</td><td></td></tr><tr><td>都江堰门票</td><td>177.6</td><td></td></tr><tr><td>共计</td><td>655.1</td><td></td></tr></tbody></table><h1 id="Day17-都江堰-乐山（0529）"><a href="#Day17-都江堰-乐山（0529）" class="headerlink" title="Day17 都江堰-乐山（0529）"></a>Day17 都江堰-乐山（0529）</h1><p>在都江堰玩了大半天，嗯就不写都江堰游记啦，放几张图片吧。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575193181.jpg"  title="都江堰" alt="都江堰"/>都江堰还是一如既往的气势磅礴，前段时间刷《大秦帝国》顺便了解了下李冰父子和都江堰，这次来又是不一样的感觉。<p>是的又没去成青城山，上次和发小去都江堰，准备第二天去青城山，然而睡到了下午。这次真的是时间不够了，下午回乐山还得保养车，然后各种吃。车子在北京保养了一遍开出来的，开了这么远该保养了。</p><p>4s店依然很黑，宰了我一把。出了4s点我半小时就回去了，修车小哥一句咋又来了呢。嗯是的，出去刚上高速就被撞了，所以又回来了。不过还好，也就是反光镜掉了，漆磨掉了一些，其他啥事没有。</p><video width="50%" height="400" src="/images/Tibetan-Self-driving-Travel-Notes/1575193573.mp4" controls="controls"> `<video>` 不可用，该换浏览器啦.</video><p>是的然后就等着修车了，接着线各回各家各找各妈去。</p><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>过路费19</td><td>都江堰-成都</td><td></td></tr><tr><td>过路费</td><td>9.5</td><td>绕城高速</td></tr><tr><td>过路费</td><td>44.65</td><td>乐山</td></tr><tr><td>油费</td><td>280</td><td>乐山</td></tr><tr><td>共计</td><td>353.15</td><td></td></tr></tbody></table><h1 id="Day18-乐山-成都（0614）"><a href="#Day18-乐山-成都（0614）" class="headerlink" title="Day18 乐山-成都（0614）"></a>Day18 乐山-成都（0614）</h1><p>终于修好车啦，然后在家呆了半个月也差不多了，再呆几天就得被赶出家门了😁。去机场接@xqq 然后就出发直接开去杭州啦。 结果刚上高速不久，正开始打瞌睡了，就遇到前面的面包车爆胎，瞬间吓醒了。成乐高速对我也太不友好了吧。</p><video width="50%" height="400" src="/images/Tibetan-Self-driving-Travel-Notes/1575194188.mp4" controls="controls"> `<video>` 不可用，该换浏览器啦.</video>然后前面高速封路，各种绕道，绕去眉山转了一大圈才到成都，趁着@xqq飞机还早，找本科辅导员@辉哥 约饭去。久违的串串，回家半个月在家贼健康贼养生，馋死我了。跟@辉哥聊了很久，本科以来读研三年的所得与成长，希望能继续加油，不负所望。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575194455.jpg"  title="久违的串串" alt="久违的串串"/><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>住宿</td><td>363</td><td></td></tr><tr><td>机场高速</td><td>9.5</td><td></td></tr><tr><td>高速</td><td>20.9</td><td></td></tr><tr><td>高速</td><td>23</td><td></td></tr><tr><td>共计</td><td>416.4</td><td></td></tr></tbody></table><h1 id="Day19-成都-重庆（0615）"><a href="#Day19-成都-重庆（0615）" class="headerlink" title="Day19 成都-重庆（0615）"></a>Day19 成都-重庆（0615）</h1><p>接上@xqq 到酒店已经快半夜一点了，所以照常睡到中午，所以在重庆也没咋玩，嗯这趟旅行都这么佛系。</p><p>到重庆进城各种堵，到酒店收拾好已经快天黑了。去打开了网红李子坝轻轨站，也去体验了神奇的3D城市，@xqq还体验了一把中辣锅底的火锅（嗯非常不明智，小朋友们不要模仿）然后拉了一天。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575194803.jpg"  title="谜一样的重庆" alt="谜一样的重庆"/>回酒店打车也体会了一把神奇的重庆。在解放碑打了个车，师傅离我们800多米，开了快20分钟终于绕过来接到我们，然后直线不到两公里的路，开了20多分钟终于到了，师傅说自己就是重庆人，但是也总是迷路，嗯这很重庆。<img src="/images/Tibetan-Self-driving-Travel-Notes/1575195107.jpg" width="200" title="谜一样的路" alt="谜一样的路"/>以下是这一天的花费：<table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>午饭</td><td>83</td><td></td></tr><tr><td>加油</td><td>300</td><td></td></tr><tr><td>高速</td><td>121.6</td><td></td></tr><tr><td>高速</td><td>53.35</td><td>沙坪坝</td></tr><tr><td>晚饭</td><td>169</td><td></td></tr><tr><td>住宿</td><td>228</td><td></td></tr><tr><td>共计</td><td>954.95</td><td></td></tr></tbody></table><h1 id="Day20-重庆-英山（0616）"><a href="#Day20-重庆-英山（0616）" class="headerlink" title="Day20 重庆-英山（0616）"></a>Day20 重庆-英山（0616）</h1><p>这一天。。。没啥好说的，开车开到崩溃。感受就是，武汉的绕城真大啊，从西北进绕城，东北出绕城，感觉开了能有两三个小时。后来已经实在是开不动了，和@xqq商量是找个服务区露营还是咋整，最后发现累到懒得搭帐篷，所以随便找了个小城住了一晚，这可能是这趟旅程最累的一天吧。</p><p>对了再吐槽一下，从重庆出来到武汉的高速，路况明明很好，但是几乎全程限速80，然后还各种摄像头跟不要钱一样多，即使开着自适应巡航也累到爆。</p><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>230</td><td></td></tr><tr><td>高速</td><td>140.65</td><td>重庆-冷水</td></tr><tr><td>午餐</td><td>60</td><td></td></tr><tr><td>晚餐</td><td>70</td><td></td></tr><tr><td>加油</td><td>265</td><td></td></tr><tr><td>高速</td><td>470</td><td>冷水-英山</td></tr><tr><td>住宿</td><td>140</td><td></td></tr><tr><td>共计</td><td>1375.65</td><td></td></tr></tbody></table><h1 id="Day21-英山-杭州（0617）"><a href="#Day21-英山-杭州（0617）" class="headerlink" title="Day21 英山-杭州（0617）"></a>Day21 英山-杭州（0617）</h1><p>终于。。下午两点多成功到杭州了，不得不提进入杭州地界后，服务区质量上升了很多，居然服务区还有星巴克，太高大上。</p><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>过路费</td><td>10</td><td></td></tr><tr><td>过路费</td><td>10</td><td></td></tr><tr><td>加油</td><td>245</td><td></td></tr><tr><td>早餐</td><td>34</td><td></td></tr><tr><td>过路费</td><td>140？</td><td></td></tr><tr><td>过路费</td><td>95</td><td></td></tr><tr><td>午饭</td><td>20</td><td></td></tr><tr><td>过路费</td><td>60</td><td></td></tr><tr><td>共计</td><td>614</td><td></td></tr></tbody></table><p>全程总共花了1.5w左右，一共用了21天，途径了北京、河北、山西、陕西、四川、西藏、青海、重庆、湖北、安徽、江西（跑了一小段也算）是一个省市。全程轨迹记录是9600多千米，但是中间偶尔忘记开路径记录，加上在回乐山后开了很多没记录，所以理论上路程超过1w千米了。很充实的旅程，在那个5月以前，我觉得我可能这辈子都没有空去自驾一趟西藏，更别提从北京过去，到杭州的这1w公里。这一趟很累，但是收获也很多。谨以此文，纪念那年在路上的大半个月时光。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1575195797.jpg"  title="自驾数据" alt="自驾数据"/>如果需要gps轨迹数据，请和我联系。]]></content>
    
    
    <summary type="html">这是一篇拖延了一年半的游记。青海归来，该结束旅程了。</summary>
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
    <category term="生活" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="自驾" scheme="http://blog.liexing.me/tags/%E8%87%AA%E9%A9%BE/"/>
    
  </entry>
  
  <entry>
    <title>两人一车，说走就走的西藏自驾游（4）：青藏出藏</title>
    <link href="http://blog.liexing.me/2019/09/01/Tibetan-Self-driving-Travel-Notes-4/"/>
    <id>http://blog.liexing.me/2019/09/01/Tibetan-Self-driving-Travel-Notes-4/</id>
    <published>2019-09-01T07:02:45.000Z</published>
    <updated>2023-01-20T15:04:29.748Z</updated>
    
    <content type="html"><![CDATA[<p>索引：</p><ul><li><a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/">两人一车，说走就走的西藏自驾游（2）：走上318</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/">两人一车，说走就走的西藏自驾游（3）：到达拉萨</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-4/">两人一车，说走就走的西藏自驾游（4）：青藏出藏</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-5/">两人一车，说走就走的西藏自驾游（5）：青海归来</a></li></ul><h1 id="Day11：拉萨-当雄（0523）"><a href="#Day11：拉萨-当雄（0523）" class="headerlink" title="Day11：拉萨-当雄（0523）"></a>Day11：拉萨-当雄（0523）</h1><p>终于到拉萨了，昨天说了因为没有定到布达拉宫的票，所以最终也没有进布达拉宫，算是一大遗憾吧，当然了，这趟旅途遗憾可多了，比如也没能去珠穆朗玛，没有去羊湖，没有去阿里，以及前面说过的从雅鲁藏布大峡谷旁边经过也没有去看看等等~嗯有遗憾才更有兴趣下次再去（虽然并不太可能）。</p><p>Anyway，一早从酒店出发，玩到下午我们就又双叒叕启程了。@腿哥 他们预计今晚到拉萨，看上去是又要错过了，他们明天预计晚上到纳木错然而我们应该下午就从纳木错撤退，嗯友谊的小船不知道吹飞到哪里去了。</p><p>上午先去逛小昭寺，到了八廊街外面，以及震撼到不行。一大早广场上已经非常多的人（其实是中午了，这边天亮得太晚了，并且车停得非常远，走了半天），天气也很热，然而信徒们已经在聚集起来了，路上很多转着经筒默念经文的人，也有开始祭拜的人。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1567324254.jpg"  title="大昭寺外的信徒" alt="大昭寺外的信徒"/>八廊街是一个繁华的商业街，小昭寺外的转经道上信徒们一圈一圈的转着，旁边是个广场，有很多执勤的武警。周围很多酒馆、手工作坊，也许还有当年仓央嘉措最爱的密宫玛吉阿米酒馆吧，是的我们今天也没去┓( ´∀` )┏<img src="/images/Tibetan-Self-driving-Travel-Notes/1567324837.jpg"  title="八廊街" alt="八廊街"/>在外面绕了一会儿就进入小昭寺了。我只知道这是当年松赞干布给文成公主修建的，里面非常的圣洁，居然还有释迦摩尼8岁等身像，以及保存很好的数千年来的一些物品，在里面听讲解了藏传佛教的历史以及藏传佛教中的一些故事，嗯又长见识了。在寺内殿中不允许拍照，所以就在外面室外拍了几张，嗯天气很好居然看到日晕了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567327434.jpg"  title="小昭寺" alt="小昭寺"/><p>小昭寺离布达拉宫不是太远，虽然没有买到门票，但是门口还是需要去打个卡的。一路走过去，顺便买了杯咖啡，气温不高但是非常热，停下来，一阵风过来又冷得慌。来了当然要拍游客照了，@xqq 已经早早的换好了50元人民币，到了布达拉宫广场上就迫不及待的开始拍了。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1567327835.jpg"  title="布达拉宫" alt="布达拉宫"/>在广场旁边有个观景台，上面人蛮多，上去了一看才发现，这里才是人民币上布达拉宫的拍摄地，赶紧的排了几张照片。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567328236.jpg"  title="布达拉宫" alt="布达拉宫"/>从观景台上下来快4点了，已经累成狗了，完全走不动，打了个滴滴去取车，然后，对的你没有看错，我们就返程啦！辛辛苦苦十天的路程，结果就玩了半天🤣。<p>既然来的时候走的川藏，所以出藏准备走青藏，也算圆满吧（其实是想去青海湖玩玩，结果。。gg了）。出拉萨后路还不错，一路蛮平坦的，但是云层低到可怕。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1567328625.jpg"  title="回程啦" alt="回程啦"/>晚上到当雄县，感觉跟一个镇没啥两样，找了个酒店，也和村里的差不多，非常破旧，。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>大昭寺</td><td>186</td><td></td></tr><tr><td>午饭</td><td>140</td><td></td></tr><tr><td>咖啡</td><td>70</td><td></td></tr><tr><td>住宿</td><td>125</td><td></td></tr><tr><td>晚饭</td><td>80</td><td></td></tr><tr><td>水</td><td>15</td><td></td></tr><tr><td>共计</td><td>616</td><td></td></tr></tbody></table><h1 id="Day12：当雄-安多（0524）"><a href="#Day12：当雄-安多（0524）" class="headerlink" title="Day12：当雄-安多（0524）"></a>Day12：当雄-安多（0524）</h1><p>又是一早就起床了（真一早，七点多就起来了，相当于北京五点多就起床了），出酒店不远就转出大路往纳木错去了。一路上车不多，路也有的段正在修，但是景色真的贼美。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1567342613.jpg"  title="纳木错外的山" alt="纳木错外的山"/>海拔比较高（4500m左右），所以一路上路边都是冰。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567342982.jpg"  title="路边的冰" alt="路边的冰"/>翻过一个山头，就能远远的看见纳木错了，结果下了山头切身体会到望山跑死马这个词，开了好久还没看到湖。下山后是一块巨大的平地，满是牛羊，别有一番风味。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567343335.jpg"  title="远远望见纳木错" alt="远远望见纳木错"/>翻过山后的路特别棒，拍了蛮多路的照片。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567343535.jpg"  title="远远望见纳木错" alt="远远望见纳木错"/>再往前走了不远，到了纳木错旁边的类似湿地的一片小湖边，有很多放养的牦牛，景色很好，天也很蓝，水不深但是能倒影上旁边的雪山。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567343847.jpg"  title="纳木错旁的小湖" alt="纳木错旁的小湖"/>纳木错太大了，本想沿着湖绕一圈，但是不太现实，所以准备沿湖走一段就撤回。沿湖不远发现有个类似旅游集散点的地方，原来这里才是真正的游客观景入口，我们却误打误撞跑去人烟稀少的边上和野牦牛拍了一堆美照。发现湖边空气很清新，看了下空气质量，PM2.4值居然是1，这估计是测量的下界了吧。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567344378.jpg"  title="纳木错" alt="纳木错"/>纳木错不愧为圣洁雪域的圣湖，美得让人心醉。“那一年, 磕长头匍匐在山路, 不为觐见, 只为贴着你的温暖; 那一世, 转山转水转佛塔, 不为修来世, 只为途中与你相见”，仓央嘉措的两句诗，让这里变成人间圣地。<img src="/images/Tibetan-Self-driving-Travel-Notes/1567344671.jpg"  title="纳木错" alt="纳木错"/>然而因为时间关系，没能去看那念青唐拉神山过道的圣象天门，也没能去废弃的古寺，看那断壁残垣。很遗憾，但是留有念想。<p>在回当雄上主路之前，终于趁着吃饭把车给洗了，这十多天来，车上糊满了的尘土。吃饭时候还遇到老板打电话训儿子，骂他不好好学习就知道贪玩，对不起自己和孩子他爸。下午开了一路平淡的公路，终于到达安多，感觉比当雄大一些，但是依然像个小镇。到安多的时候@腿哥 他们终于到当雄了。</p><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>早餐</td><td>20</td><td></td></tr><tr><td>纳木错门票</td><td>120</td><td></td></tr><tr><td>停车费</td><td>5</td><td></td></tr><tr><td>洗车</td><td>30</td><td></td></tr><tr><td>午饭</td><td>63</td><td></td></tr><tr><td>加油</td><td>210</td><td></td></tr><tr><td>加油</td><td>180</td><td></td></tr><tr><td>住宿</td><td>200</td><td></td></tr><tr><td>共计</td><td>828</td><td></td></tr></tbody></table><h1 id="Day13：安多-格尔木（0525）"><a href="#Day13：安多-格尔木（0525）" class="headerlink" title="Day13：安多-格尔木（0525）"></a>Day13：安多-格尔木（0525）</h1><p>在高原呆了这么多天，今天终于下高原啦。然而因为两晚都在4000多米的地方睡(安多4800m，当雄4200m)，和@xqq 两人都有点不舒服，原本以为是感冒了，结果翻过昆仑雪山后居然啥事也没有了，嗯居然高反了。然后我觉得是因为高反了所以我居然错过了唐古拉山口，开过去了才在地图上瞅到。</p><p>一早出发，结果走了不久就开始堵车。青藏路上大卡车比较多，一长串的大卡车，夹在大卡车中间感觉特别压抑，卡车司机们居然有的开始下车煮泡面闲聊了。我们堵了半个小时，发现小汽车再开始往回走，下车跑到路边一看，发现外面有一条小路可以通行，赶紧也跟着绕下来了。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1569425124.jpg"  title="绕行小路" alt="绕行小路"/>绕下来发现好家伙，居然堵了好几公里，幸好没老老实实在上面守着。接下来一段路就比较平淡了，海拔挺高，天没有那么蓝了，路边看到青藏线了，然而一直没拍到好的照片了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1569425380.jpg"  title="青藏线" alt="青藏线"/>慢慢的周围山也变少了，路边桥边开始出现一根根铁杆子，后来网上搜才发现，居然是因为冻土的原因，早知道拍几张照了。后来路上的卡车也变少了，跑很久才能看到一辆车。最关键的是，在出西藏北大门之前，到老沙家门之后，我居然还看到野鹿群了。不过通天河比想象中差太远了。本想跑过去多看看野鹿的，结果瞅见远处有个貌似是野牛的骨架，还听到狼嚎，狗命要紧感觉上车继续赶路。<img src="/images/Tibetan-Self-driving-Travel-Notes/1569425723.jpg"  title="通天河，西藏北大门与野鹿群" alt="通天河，西藏北大门与野鹿群"/>中午快到沱沱河了，这时候可能是全程离@腿哥 最近的时候，因为腿哥貌似回去的火车也离我们不远了。然后我终于拍到一张不错的火车照片了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1569426039.jpg"  title="青藏线上的火车" alt="青藏线上的火车"/>在沱沱河吃完饭加油的时候，看到中国石油准备去，结果走进一看发现是中围石油，庆幸没上当，转头又碰到个中圆石油，跑不远还有家中因石油。。。<img src="/images/Tibetan-Self-driving-Travel-Notes/1569426362.jpg"  title="中围石油与中圆石油" alt="中围石油与中圆石油"/><p>在沱沱河之前是长江源头，过了沱沱河就是可可西里无人区，我居然还能离藏羚羊这么近，在无人区里看到了一群群的藏羚羊以及更多的野驴群🤣</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1569426503.jpg"  title="长江源头与可可西里" alt="长江源头与可可西里"/><p>过了可可西里就是最后一个山头啦，翻过昆仑山口就一路下高原了。昆仑山这个名字从小在各种电视剧中出现过无数次，当然路过要跑出去拍照啦，结果穿着短袖冲出去发现，外面居然在飘雪？？？刚从在可可西里不是短袖正好么？？？</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1569426798.jpg"  title="昆仑山口" alt="昆仑山口"/>下昆仑山的路一会儿飘雪一会儿下雨，云雾缭绕的，跟神仙道场一般，难怪这座山这么多故事。<img src="/images/Tibetan-Self-driving-Travel-Notes/1569427122.jpg"  title="下昆仑" alt="下昆仑"/>下昆仑的路海拔陡降，很快就从4700m降到3000m以下了，不舒服的感觉也一下子就消失了。然而在进入格尔木之前居然堵了估计能有俩小时，非常不开心。<p>如果说这是一篇西藏游记，那么下高原了，这里算是告一段落了吧。</p><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>130</td><td></td></tr><tr><td>午饭</td><td>82</td><td></td></tr><tr><td>加油</td><td>180</td><td></td></tr><tr><td>住宿</td><td>95</td><td></td></tr><tr><td>晚饭</td><td>73</td><td></td></tr><tr><td>共计</td><td>560</td><td></td></tr></tbody></table>]]></content>
    
    
    <summary type="html">这是一篇拖延了整整一年的游记。拉萨很美，该回家了。</summary>
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
    <category term="生活" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="自驾" scheme="http://blog.liexing.me/tags/%E8%87%AA%E9%A9%BE/"/>
    
  </entry>
  
  <entry>
    <title>两人一车，说走就走的西藏自驾游（3）：到达拉萨</title>
    <link href="http://blog.liexing.me/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/"/>
    <id>http://blog.liexing.me/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/</id>
    <published>2019-07-07T07:01:55.000Z</published>
    <updated>2023-01-20T15:04:29.742Z</updated>
    
    <content type="html"><![CDATA[<p>索引：</p><ul><li><a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/">两人一车，说走就走的西藏自驾游（2）：走上318</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/">两人一车，说走就走的西藏自驾游（3）：到达拉萨</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-4/">两人一车，说走就走的西藏自驾游（4）：青藏出藏</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-5/">两人一车，说走就走的西藏自驾游（5）：青海归来</a></li></ul><h1 id="Day7：巴塘-左贡（0519）"><a href="#Day7：巴塘-左贡（0519）" class="headerlink" title="Day7：巴塘-左贡（0519）"></a>Day7：巴塘-左贡（0519）</h1><p>前面说了，从巴塘出发后，晚上的歇脚点非常不好找，沿途一天能到的城镇：芒康，海拔3875m，距离上看距离巴塘就100公里左右，太近了，排除；左贡，海拔3877m，也比较高，距离250公里左右，距离上看比较合适；邦达，海拔4120m，太高了，住宿一晚估计会高反，并且距离左贡还有100公里，比较远。综合考虑了下决定今晚在左贡住宿了，今天路途虽然不远但是一路都是翻山越岭，所以可能相对更累了。对了今天比较大的进展就是，终于进入西藏地界啦，但是有一个巨大的遗憾，就是经过金沙江大桥到达界牌的时候，我忘了拍照！！都怪@xqq 开太快没给我思考时间就过去了。赶紧拍了张车窗外的金沙江，嗯这个照片离四川-西藏的界牌大概有几米吧。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1562484856.jpg"  title="全是沙子的金沙江" alt="全是沙子的金沙江"/>进入西藏地界后就开始烂路了，一长段的石子路，主要是金沙江开始涨水，冲毁了一部分路基，然后施工队也在赶工维修，有些段在旁边修加固的新路，所以现在进藏的话这段当时唯一存在的烂路应该也都没有了，整体路况应该都不错了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562485314.jpg"  title="正在修建的路" alt="正在修建的路"/>沿着金沙江走了接近两小时的烂路，一路走走停停，单向放行。路上碰到很多骑行的、徒步的、推小推车，甚至是遛狗进藏的人，很是热闹。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562485491.jpg"  title="骑行与遛狗进藏的人" alt="骑行与遛狗进藏的人"/>走过了一段烂路就差不多中午了，刚好到了一个小镇（或者小县城吧，现在看应该是在芒康）吃了午饭，修整了一下就继续出发啦。明显能感觉到山开始变得越来越凸，然后云层也越来越低，景色也依旧非常美。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562485900.jpg"  title="凸山" alt="凸山"/>一路上有羊啊牦牛啊就在路上散养着，在路上的牦牛还不让道，跟在它们屁股后面开了半天才空出个位置让我们超牛过去。天上是不是的出现一两只没见过的鸟，或者是疑似是老鹰的鸟（山里长大的我，连猫头鹰都逮家里养过，但是居然没见过老鹰）。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562486239.jpg"  title="路边的牦牛与天空的鹰" alt="路边的牦牛与天空的鹰"/>不知道大家有没有发现，不管是在海拔五千多的山上，还是三四千的各种道路上的照片，都有各种高压线以及拖着高压线的铁架，在荒无人烟的高原上，这些铁架与线特别突兀，强大的基建狂魔真的很是厉害。再往前开就是东达山口了（或者是八觉山口），<img src="/images/Tibetan-Self-driving-Travel-Notes/1562487212.jpg"  title="上山的路" alt="上山的路"/>山上的景很不错，赶紧拍了几张。离左贡不远啦，慢悠悠的开到左贡。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562487509.jpg"  title="山上的景" alt="山上的景"/>左贡感觉也是整个城（镇）都在新修，找酒店找了半天，酒店是个新店，设施也很高大上，房间窗户外有个高大上的窗户，山景也很不错。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562487996.jpg"  title="窗外的景" alt="窗外的景"/><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>200</td><td></td></tr><tr><td>午饭</td><td>65</td><td></td></tr><tr><td>加油</td><td>190</td><td></td></tr><tr><td>酒店</td><td>268</td><td></td></tr><tr><td>晚餐</td><td>65</td><td></td></tr><tr><td>共计</td><td>1267</td><td></td></tr></tbody></table><h1 id="Day8：左贡-然乌（0520）"><a href="#Day8：左贡-然乌（0520）" class="headerlink" title="Day8：左贡-然乌（0520）"></a>Day8：左贡-然乌（0520）</h1><p>嗯今天又拍了五六百张照片，这个游记写得心好累。下面是手机一些照片的截图。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1562512925.jpg"  title="照片截图" alt="照片截图"/>从左贡出发，前面就是邦达（4120m）和八宿（3280m），虽然八宿海拔比较低，但是因为在左贡呆了一晚上发现也没啥异样，所以今天就再赶赶，干脆到然乌了，总共约280公里，行程也还好不算太紧张。<p>今天的天气特别的好，之前几天云比较多，云层比较低，但是今天的天空特别晴朗，云也很少，车在四千多米海拔跑着，人也非常兴奋。窗外是起伏的群山，掺点着一些高原植物的深绿色，黄沙一样的土地上遍布牦牛群，路边时不时出现一辆事故车摆着提示过往车辆安全驾驶。随手一拍都是桌面级的美景（嗯我拍照太烂了可能桌面比较丑）</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1563719811.jpg"  title="窗外的景" alt="窗外的景"/>今天路上的汽车很少，骑行的队伍也基本没有了。看到一段特别棒的路赶紧停下车跑去拍照，又被@xqq 的全景拍下来。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563720030.jpg"  title="被xqq的全景拍下" alt="被xqq的全景拍下"/>往前走发现旁边的山顶怪石嶙峋，光秃秃的没有植物，很是奇特，别有一番风景。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563721197.jpg"  title="光秃秃的山顶" alt="光秃秃的山顶"/>下了一个雪山口，一路的回头线，然后就沿着一条河各种拐。前面看到有个关卡居然有卫兵把守，抬头一看原来这就是怒江了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563722542.jpg"  title="怒江隧道" alt="怒江隧道"/>过了怒江隧道，山突然变得威武了许多，非常的霸气，江水虽然不大但是流得很急，路有种从山里砸开的感觉，让我这种山里出来的人也觉得很震撼。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563722927.jpg"  title="怒江旁边的山" alt="怒江旁边的山"/>沿着怒江开了不久就到然乌镇了，这个镇感觉是个新建的镇，真的是到处都在新修，很多酒店都是刚开始接待客人。我们今天住的酒店虽然刚开业不久，但是感觉异常的烂。在酒店放好行李趁着天色还早赶紧去然乌湖溜一圈。到了湖边车还没停好就和@xqq 一起情不自禁感慨一句，卧槽太漂亮了吧。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563723217.jpg"  title="然乌湖" alt="然乌湖"/>这里还是一片没有开发的景，没有太多游客，没有景区大门，没有门票，有的只是湖边的玛尼堆，以及远处的雪山，还有一阵阵寒风忒冷。即使穿着冲锋衣也扛不住了，呆了半小时不到就赶紧躲回车里了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563723648.jpg"  title="然乌湖" alt="然乌湖"/>回到酒店，发现空调不能用，取暖居然只有电热毯，想想就瑟瑟发抖。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>午饭</td><td>95</td><td></td></tr><tr><td>加油</td><td>210</td><td></td></tr><tr><td>晚饭</td><td>83</td><td></td></tr><tr><td>住宿</td><td>150</td><td></td></tr><tr><td>共计</td><td>538</td><td></td></tr></tbody></table><h1 id="Day9：然乌-林芝（0521）"><a href="#Day9：然乌-林芝（0521）" class="headerlink" title="Day9：然乌-林芝（0521）"></a>Day9：然乌-林芝（0521）</h1><p>今天从然乌需要开到林芝，需要开350多公里，任务还是比较重的。昨天去看的是然乌湖三大湖的其中一个，其实然乌湖还有两个湖，早晨从酒店出发不久就看到了，就在318国道旁边。然乌湖是一个堰塞湖，貌似是刚好是在喜马拉雅、唐古拉山的对撞处，湖边还有个冰川，然而因为今天赶路没法去看了。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1563884538.jpg"  title="路边的然乌湖" alt="路边的然乌湖"/>再往前开了不远，发现路边的植物开始变得比较茂盛，山上也开始出现郁郁葱葱的绿色连着山顶的白雪。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563884686.jpg"  title="山上的植物和山顶的雪" alt="山上的植物和山顶的雪"/>路两边也是非常茂盛的树，完全看不出来这是在高原上。其中好几段路还非常笔直，印象比较深刻的一段路开了七八公里没有弯道（嗯全靠有段视频），路上还看到好多云南牌的房车，大家都是开一段，找个观景台停一段拍拍照，非常惬意。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563885103.jpg"  title="一幅南方景色的路" alt="一幅南方景色的路"/>后来才知道，原来沿着开了这么久的河携着然乌湖流出的雪山融化的水，最后会汇入雅鲁藏布江，最终流向印度洋。顺便提一嘴，今天路上有个岔路开出去不远就是雅鲁藏布江大拐弯，那里有个雅鲁藏布江大峡谷是世界第一大峡谷，然而，我们纠结了一番没有拐弯，直奔林芝了，悔死。<p>再往前到了追龙沟特大桥，@xqq 说之前这里没有桥和隧道的时候需要走一段盘山路，这里也被称为川藏路上的“天险”，之前这里垮塌的通麦大桥也被称为通麦坟场。而如今的追龙沟特大桥看上去非常的坚固，悬崖上也明显能看到防止塌方用的围栏。桥身是汹涌的河水，可惜我没有拍到。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1563885781.jpg"  title="追龙沟特大桥" alt="追龙沟特大桥"/>过了隧道之后的一长段路，植被依然很丰富，路也修得不错。但是这边太多的区间测速，所以每到一个区间测速终点，就能看到一大堆车停路边拍照啥的等时间。还好我们比较怂严格按照限速来，区间测速终点也就直接过去了（嗯一路上我都怕路边有雷达测速，怕掉坑，但是实际上好像并没有）。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563886086.jpg"  title="丰富的植被" alt="丰富的植被"/>再往前就开始翻色季拉山了，这是到林芝的最后一个雪山了。能明显的感觉到这个山口的景色更加漂亮，非常壮观。<img src="/images/Tibetan-Self-driving-Travel-Notes/1563886327.jpg"  title="色季拉山" alt="色季拉山"/>在山口修整了一小会儿，@xqq 去上厕所了，一个小女孩跑过来要吃的，我就随便给了点小零食打发走了，结果@xqq 回来说我怎么没给点其他吃的，太没同情心了，然后跑后备箱抓了几包泡面啥的跑过去给她了。<p>下山不久就到林芝了，之前听说过这个地方很多次，没想到现在就在这个城市了。这里蛮大的，在路上跑了这么久，终于感觉到进城了。晚上和@xqq 跑去吃了下特色的石锅鸡，还挺好吃的。出门特意没开车步行去的饭店，吃完饭逛街走回来，好久没有踏踏实实的在马路上走过路了。</p><p>回到酒店突然发现，即使涂满了防晒的情况下，好像手心手背开始不是一个色号了，发了个朋友圈感慨了下还被插刀。看了下轨迹，3600多公里啦。到了林芝，明天终于能到拉萨了。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1563886817.jpg" width="400" title="晒黑了" alt="晒黑了"/>btw. @腿哥 他们今天已经浪完珠穆朗玛大本营往回走了，不知道路上能不能碰上了。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>60</td><td></td></tr><tr><td>加油</td><td>150</td><td></td></tr><tr><td>晚饭</td><td>174</td><td></td></tr><tr><td>住宿</td><td>149</td><td></td></tr><tr><td>共计</td><td>533</td><td></td></tr></tbody></table><h1 id="Day10：林芝-拉萨（0522）"><a href="#Day10：林芝-拉萨（0522）" class="headerlink" title="Day10：林芝-拉萨（0522）"></a>Day10：林芝-拉萨（0522）</h1><p>终于，今天能到拉萨了。一早起来就出发啦，直接上拉林高速，传说中的西藏最美高速，其实也不算高速了，因为居然不收费，感觉是一条高大上的快速路。具体的图片可以百度搜一下，非常漂亮。嗯这趟旅程让我非常的后悔没有买个无人机，很多景都没法拍出来了。当时走的时候整条路还没有通，中间还有几条隧道正在挖掘，但是大部分都可以通车了。路上整体车不多。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1564498541.jpg"  title="拉林高速" alt="拉林高速"/>中午左右开到米拉山口，随便吃了点东西，这里拍照的人就很多了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1564499001.jpg"  title="米拉山口" alt="米拉山口"/>下午四点不到就进拉萨城了。第一感觉就是，这个城市真的超级小，这是去过的最小的省会城市了。但是整个城市笼罩着一种充满信仰的感觉。酒店窗户正对着布达拉宫，然而距离比较远，相机的套头镜头根本拍不下来，但是晚上的布达拉宫真的很漂亮，然而和@xqq 一致同意去看场电影（已经忘记看了啥了）然后早点休息第二天好好浪。下午@腿哥 说布达拉宫需要提前预约，然而发现辛辛苦苦通过网站坑爹的验证登录进去后，根本没票了。嗯佛系旅游才不去打卡。<img src="/images/Tibetan-Self-driving-Travel-Notes/1564499533.jpg"  title="酒店看布达拉宫以及坑爹的验证码" alt="酒店看布达拉宫以及坑爹的验证码"/>对了忘了说，晚上八点十五分，天还没黑，感觉至少八点半了天还亮着，下图是八点十五拍的一张照片。<img src="/images/Tibetan-Self-driving-Travel-Notes/1564500156.jpg"  title="八点十五的西藏" alt="八点十五的西藏"/>名称 | 金额 | 备注-|-|-早餐 | 40晚餐 | 66电影 | 58住宿 | 170共计 | 334]]></content>
    
    
    <summary type="html">这是一篇拖延了整整一年的游记。终于写到拉萨了。</summary>
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
    <category term="生活" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="自驾" scheme="http://blog.liexing.me/tags/%E8%87%AA%E9%A9%BE/"/>
    
  </entry>
  
  <entry>
    <title>两人一车，说走就走的西藏自驾游（2）：走上318</title>
    <link href="http://blog.liexing.me/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/"/>
    <id>http://blog.liexing.me/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/</id>
    <published>2019-07-06T17:24:13.000Z</published>
    <updated>2023-01-20T15:04:29.744Z</updated>
    
    <content type="html"><![CDATA[<p>索引：</p><ul><li><a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/">两人一车，说走就走的西藏自驾游（2）：走上318</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/">两人一车，说走就走的西藏自驾游（3）：到达拉萨</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-4/">两人一车，说走就走的西藏自驾游（4）：青藏出藏</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-5/">两人一车，说走就走的西藏自驾游（5）：青海归来</a></li></ul><p>这是一篇拖延了整整一年的游记。上次写到从下决心到到达成都的过程（<a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/#qrcode">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a>），接下来就走上318了。btw，虽然忙完618但是不知道为嘛还是忙成狗，周末感觉得撸一篇不然感觉就要鸽掉了。</p><h1 id="Day4：成都-海螺沟（0516）"><a href="#Day4：成都-海螺沟（0516）" class="headerlink" title="Day4：成都-海螺沟（0516）"></a>Day4：成都-海螺沟（0516）</h1><p>在成都休息了一下后，算是正式踏上了进藏的路了（嗯睡了个自然醒）。第一件事就是被师姐提醒，我貌似有麻烦了？（垃圾iCloud照片死活加载不出来，开场就折腾我半天）</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561904380.jpg" width="200" title="貌似我有麻烦了" alt="貌似我有麻烦了"/>昨天晚上纠结半天要不要去乐山去驻扎一晚，顺便偷偷溜回家一趟。因为之前想着的行程是青藏出藏，然后就一路开回北京，这次不回家的话估计得等我毕业完才能回去了。但是现在看来，貌似行程允许后面出藏后开回来一趟，因此就先定直接进藏了。昨晚计划好今天到海螺沟，今晚到明天上午玩，下午开始赶路，因此并不着急，因此十一点五十了刚出门还在成都吃午饭。吃完饭就出发啦。从成雅高速上高速，在高速在乐山-雅安的分路三叉路口还感慨了一下，半年没回家的我居然要绕开不会去啦（可惜只是因为浪）。在雅安拐上了修了一半的雅康高速，走了一大段车机上没有的路（这坚定了我要升级车机系统换导航的想法，恰好这几天车友会中大家说新的车机系统就要放出来了，看样子我得路上升级一把了，幸好我也不知道为啥我进藏会带个U盘）。经过了传说中的二郎山隧道，@xqq 给我科（de）普（se）了半天，我只发现隧道里面居然还有全景LED，非常的高大上，以至于我还跑去行车记录仪上下载了个照片。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561906955.jpg"  title="高大上的二郎山隧道" alt="高大上的二郎山隧道"/>下高速后就是下面这样的路了，嗯某@xqq 又开始激动了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561906304.jpg" width="200" title="下雅康高速后的路" alt="下雅康高速后的路"/>回头看一眼正在修建的雅康高速，不得不感慨基建狂魔的厉害，嗯我貌似又没有拍出来那种震撼的感觉，客官们自己脑补吧。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561906529.jpg"  title="正在修建的雅康高速" alt="正在修建的雅康高速"/>过了大渡河大桥，从泸定县绕了一下，过了大渡河大桥就看到“海螺沟欢迎您”的标语了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561907363.jpg"  title="大渡河大桥" alt="大渡河大桥"/>到了海螺沟入口的那个镇上（貌似是叫磨西镇）的时候已经比较晚了，然后在路上的时候就定了个酒店，所以就从景区大门旁边的路直接开去酒店了。这个酒店感觉大部分是木制的，踏上去咯吱咯吱响，然后最近不是旅游旺季，所以当晚就我们俩和老板娘她母亲在酒店，我们住三楼。房间阳台上可以直接去隔壁房间，然后阳台门还坏了，那天晚上打雷下雨，嗯我一点都没害怕（才怪）。不过酒店里做的家常菜不错，吃得很开心。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561907747.jpg"  title="海螺沟的酒店" alt="海螺沟的酒店"/>和@xqq随便逛了下周围，快下雨了就溜回酒店了，我趁机解决那件头疼的事——打开。嗯万幸我带了电脑，然后，bingo，就在海螺沟上打了个香山旁边的卡，妈妈再也不担心我翘班啦！<img src="/images/Tibetan-Self-driving-Travel-Notes/1561908289.jpg"  title="模拟打卡" alt="模拟打卡"/>然后开心的高速师姐我搞定这件事了，然后就被师姐给扼杀在摇篮里了，多谢师姐让我能继续做个诚实的好孩子🤣。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561908373.jpg" width="200" title="模拟打卡想法被扼杀" alt="模拟打卡想法被扼杀"/>嗯，就这样，放心大胆的开始浪了，明天打卡海螺沟。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>早餐</td><td>67</td><td></td></tr><tr><td>停车</td><td>9</td><td></td></tr><tr><td>门票</td><td>260</td><td>海螺沟</td></tr><tr><td>吃饭</td><td>85</td><td>晚饭+第二天早饭</td></tr><tr><td>住宿</td><td>128</td><td></td></tr><tr><td>索道</td><td>300</td><td></td></tr><tr><td>吃饭</td><td>65</td><td></td></tr><tr><td>加油</td><td>230</td><td></td></tr><tr><td>共计</td><td>1144</td><td></td></tr></tbody></table><h1 id="Day5：海螺沟-新都桥（0517）"><a href="#Day5：海螺沟-新都桥（0517）" class="headerlink" title="Day5：海螺沟-新都桥（0517）"></a>Day5：海螺沟-新都桥（0517）</h1><p>因为昨天让酒店帮忙订了今天去海螺沟的票，包含大巴，所以一大早起来吃了饭就卡着点在酒店门口坐大巴去景区啦。在景区发现可以多交几十块钱，然后以后能免费来（还是半价，记不清了），在@xqq 阻止下我成功没有入坑，嗯虽然这个景区还不错，但是的确半辈子都不会再来了。</p><p>刚进景区，空气非常棒，景色也很棒，然后登山步道给人一种九寨的感觉（嗯大夏天的越往上爬越冷。。这点非常像）。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561993473.jpg"  title="海螺沟的景" alt="海螺沟的景"/>登山步道沿路都是郁郁葱葱的树林，远处是斑驳的雪山（嗯我居然用“斑驳”形容雪山，对不起各位语文老师），呼吸着湿润而又清新的空气，但是贼累，并且走着热停下来冷。走了一个多小时前面一个大大的山谷里黑黢黢的，居然就是冰川？（这里是黑人问号脸）？好的先不吐槽冰川，一会儿说。在冰川山谷的边上筹到个监测站，看到是中科院的监测站，居然会莫名其妙的亲切。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561994219.jpg"  title="冰川位移监测站" alt="冰川位移监测站"/>对的这一大坨跟千层雪一样的冻泥就是传说中的存在了数十万年的冰川，和想象中的完全不一样好吧。<p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561994830.jpg"  title="冰川" alt="冰川"/><img src="/images/Tibetan-Self-driving-Travel-Notes/1561995004.jpg"  title="一号营地旁边的冰川全景" alt="一号营地旁边的冰川全景"/><br>在冰川（还是愿意叫它冰泥）旁边（貌似是一号营地）玩了一会儿太冷了，我们就回头去坐缆车准备上三号营地从上往下看冰川了。缆车上看冰川感觉挺酷，能看到悬崖数十万年间被冰川磨平的痕迹。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561995435.jpg"  title="缆车上看冰川" alt="缆车上看冰川"/>坐缆车很快到达了三号营地（海拔约3km），从三号营地望向冰川，才真正感受到宏伟壮观，原谅词语匮乏的我到观景台第一句是“卧槽”。嗯对的图片上又成功的没有展现出来了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561995775.jpg"  title="三号营地看一号冰川" alt="三号营地看一号冰川"/>那我们换个角度来看冰川有多么的壮观并且强行出镜一下。当时正在拍举世无双的大冰瀑布，这是传说中的比黄果树瀑布大出十余倍（高和宽都一千多米），国内最大的冰瀑布。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561995865.jpg"  title="强行出镜" alt="强行出镜"/><img src="/images/Tibetan-Self-driving-Travel-Notes/1561996105.jpg"  title="放大看大冰瀑布" alt="放大看大冰瀑布"/>因为中午就得下山了，并且早晨去太晚，所以并没有看到日照金山，非常的遗憾。因为昨天付了今天中午的午饭前，所以下山后再酒店吃了午饭就取车继续赶路了。在路上经过一个村堵了会儿车，路边一大堆卖水果的，乘机买了一大袋枇杷吃了个精光，然后开始翻第一座雪山（海拔4130米）。结果刚翻过雪山就遇到非常强的雨夹雪，然后车窗瞬间全是雾，去雾功能打开半天才起效，当时全是悬崖，吓蒙逼了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561997522.jpg"  title="翻第一座雪山" alt="翻第一座雪山"/>下雪山后的行程倒是很顺利，七点多到达新都桥酒店，天还没黑，停好车放好行李就去找吃的了。这一路上各种家常川菜吃得我很开心。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>晚饭</td><td>60</td><td></td></tr><tr><td>住宿</td><td>198</td><td></td></tr><tr><td>共计</td><td>258</td><td>午饭昨天付了，没计入今天</td></tr></tbody></table><h1 id="Day6：新都桥-巴塘（0518）"><a href="#Day6：新都桥-巴塘（0518）" class="headerlink" title="Day6：新都桥-巴塘（0518）"></a>Day6：新都桥-巴塘（0518）</h1><blockquote><p>终于把手机上的照片大部分都移动到nas上了，再也不用忍受iCloud那龟速加载了</p></blockquote><p>忘了说，新都桥街道在翻修，所以小镇里全是坑坑洼洼的，昨晚比较晚了一路开到酒店，早晨起来从酒店看了下，感觉整个小镇都在修。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1562429299.jpg"  title="正在翻修的新都桥" alt="正在翻修的新都桥"/>从今天开始貌似就得不停的赶路了。现在照片都在nas上可以清楚看到了，这一天两个手机一个相机拍了460多张照片，之前其实每天也都拍了这么多，所以更新贼慢。新都桥海拔3600多米，昨晚吃完饭我出去走了走，然后@xqq 跟着走了一路就不行了，回酒店就开始吸氧🤣。早晨汽车发现背后的山上还有新都桥欢饮您的字样（瞎猜的），云不错山也很好看。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562429663.jpg"  title="山上的字" alt="山上的字"/>刚出发时，发现路上骑行的人不太多，走了十几公里才发现骑行的大部队，好吧，虽然我们八点半就出发七点多就起床了，但是人家才叫一早起来。这边路不宽，双向一共两个车道，但是大家车速都不太快，因为骑行的人比较多所以开车也会特别注意，所以相对来说还算比较安全。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562430263.jpg"  title="骑行的人以及这边的路" alt="骑行的人以及这边的路"/>在路上还顺便作死了一小下下。@xqq 开着的时候我没啥事手开始犯贱，然后想着带着U盘的就顺手在副驾上升级了把车机，从此就再也不用那坑爹的车机上八百年不更新的离线地图了，能用上高德了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562430463.jpg"  title="在路上升级车机系统" alt="在路上升级车机系统"/><p>一路上景色很美，我居然想用“鲜”来形容。大概十点多的时候，发现开始各种回头线爬山了（此处同情骑行的人们），感觉这个回头线能赶上进九寨前的那十多道弯了。爬到山上一看，嗯的确是山路十八弯。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1562430573.jpg"  title="山路十八弯" alt="山路十八弯"/>山顶上是一个观景台，风景非常棒，然后有个“日照金山”的石碑，原来贡嘎雪山的日照金山需要在这里看，然而十点多了啥也没看到，不过能隐约看到贡嘎雪山。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562431186.jpg"  title="观景台的山" alt="观景台的山"/>然后观景台上居然有卖烤肠的，@xqq开心得不行（看到这里我发现，我居然记漏了烤肠的钱！）。随便吃了点零食就继续赶路啦。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562432105.jpg"  title="打卡318" alt="打卡318"/>在翻过4400多米的卡子拉山口后，遇到一段极其平坦，风景特别好的路，赶紧靠边开始拍照。对了这里补一句，在理塘吃午饭时候遇到一辆山东车，一辆北京车，山东车是一个七十多岁的大爷，自己开过来跑川藏线，和我们聊了一会儿，感觉贼霸气，希望我七十岁也能继续这么浪；北京车是一家三口，去准备分路去亚丁，老人大概50多的样子，很和蔼的一家。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562432326.jpg"  title="@xqq拍的全景" alt="@xqq拍的全景"/>路边有个佛塔，路两旁就是雪山，而且雪线就在比路高不了多少的地方，简直棒呆。嗯这里也是见识太短了，后来发现，上了高原这种地方简直不要太多。不过雪山旁边的佛塔，然后周围还没有人眼，还是很有韵味的。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562432551.jpg"  title="一段很美的路" alt="一段很美的路"/><img src="/images/Tibetan-Self-driving-Travel-Notes/1562432668.jpg"  title="雪山旁的佛塔" alt="雪山旁的佛塔"/>下午三点多的时候下坡了很长一段路，到了一个叫做姊妹湖的高原湖，景色还行，就是有点冷，没有多停留，拍了几张游客照就撤了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562433168.jpg"  title="姊妹湖" alt="姊妹湖"/>继续赶路，因为从新都桥（3630m）出发，下一个合理的住宿的地方就是巴塘（2580m)，这样海拔都不高，避免高反（后面下高原最后两晚都在4000m以上的地方住宿，我和@xqq就都有些高反了），但是巴塘出发就惨了，后面就都是3000m+了。路边到处是事故车，就被摆放在路边作为警示作用，山也慢慢变得更有特点了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1562433525.jpg"  title="路边的事故警示车与路边的山" alt="路边的事故警示车与路边的山"/>大概下午七点半到达巴塘城外的一个小酒店，是个农家院，有三四层楼，每层有六七个房间的样子，老板非常的热情，给我们介绍当地的人文地理，推荐下一站应该在哪里歇脚，以及需要注意什么，最后还送了我们一张自驾地图，很实用。嗯他家的酥油茶也超好喝。btw. 七点半了太阳还在天上，时差能非常明显的感觉到了。<p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>260</td><td></td></tr><tr><td>酒店</td><td>134</td><td></td></tr><tr><td>午饭</td><td>60</td><td>补记，大概60，具体金额忘记了</td></tr><tr><td>晚饭</td><td>85</td><td></td></tr><tr><td>共计</td><td>539</td><td></td></tr></tbody></table>]]></content>
    
    
    <summary type="html">这是一篇拖延了整整一年的游记。上次写到从下决心到到达成都的过程，接下来就走上318了。</summary>
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
    <category term="生活" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="自驾" scheme="http://blog.liexing.me/tags/%E8%87%AA%E9%A9%BE/"/>
    
  </entry>
  
  <entry>
    <title>两人一车，说走就走的西藏自驾游（1）：西藏行之起源</title>
    <link href="http://blog.liexing.me/2019/06/23/Tibetan-Self-driving-Travel-Notes/"/>
    <id>http://blog.liexing.me/2019/06/23/Tibetan-Self-driving-Travel-Notes/</id>
    <published>2019-06-23T09:51:26.000Z</published>
    <updated>2023-01-20T15:09:30.498Z</updated>
    
    <content type="html"><![CDATA[<p>索引：</p><ul><li><a href="/2019/06/23/Tibetan-Self-driving-Travel-Notes/">两人一车，说走就走的西藏自驾游（1）：西藏行之起源</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-2/">两人一车，说走就走的西藏自驾游（2）：走上318</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-3/">两人一车，说走就走的西藏自驾游（3）：到达拉萨</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-4/">两人一车，说走就走的西藏自驾游（4）：青藏出藏</a></li><li><a href="/2019/07/07/Tibetan-Self-driving-Travel-Notes-5/">两人一车，说走就走的西藏自驾游（5）：青海归来</a></li></ul><p>这是一篇拖延了整整一年的游记，这一年来一直牵挂着它，再不写下来它就要难产了。</p><p>最近有朋友想自驾西藏，然后刚好去年这几天我们正在高原上浪着，借着还没忘记一些东西，所以先写下来，补记一下当年的自由生活。再注明一下：1. 本文发生在18年；2. 本文所有图片未添加滤镜，均为手机&#x2F;相机直出相片截图（不然图片太大本文打开可能比较费劲，照片都尽可能的截图+拼接了）。</p><p>嗯本来准备一口气写完，结果gg了我觉得得花小半个月写这玩意儿，果然当时拖延是有原因的。</p><h1 id="Day0：准备工作（0511）"><a href="#Day0：准备工作（0511）" class="headerlink" title="Day0：准备工作（0511）"></a>Day0：准备工作（0511）</h1><p>大概说一下背景，5月11日晚上忘了为啥@腿哥 突然请客，然后说要抛弃我们几个，跟着本科同学去西藏租车自驾。其实我和@xqq 一直想着抽空去西藏，所以想要乘机一起去浪，晚上回宿舍后就和@xqq 刷机票，准备跟@腿哥 一起去浪。在机票下单前几秒，突然觉得自驾去西藏也行比在西藏自驾更酷，于是11号晚上睡前临时决定：走，自驾去西藏！12日睡到中午，两人赶紧的爬下床开始各种搜攻略，318国道安全吗，去西藏需要准备什么，高原反应要死人吗，无人区有狼吗，汽车常见故障及维修宝典，第一次自驾进藏走川藏线还是青藏线。反正就是一阵狂搜，然后晚上去物美买了一堆零食，在迪卡侬备了些乱七八糟的东西（帐篷、地垫、隔热垫、充气床、睡袋、充气枕头啥的都有，所以就买了些简单的露营灯这种），在京东上下单了一堆还缺的东西（血氧仪、葡萄糖、红景天、多功能逃生手链、野营套锅、各种袋装绿豆汤红豆汤啥的），再找@达叔 借上野用燃气灶、气罐、兵工铲、拖车绳这些，13号一早就出发啦！</p><table><thead><tr><th>物品名称</th><th>作用</th><th>价格</th><th>备注</th></tr></thead><tbody><tr><td>血氧仪</td><td>用来看是否发生高反</td><td>58</td><td>最开始一直测感觉都正常，因为开始并没有高反，后来忘记用了吃灰了</td></tr><tr><td>红景天</td><td>提高血氧含量</td><td>95</td><td>感觉没啥鸟用，吃了几天就没吃过了，可能没有提前吃吧</td></tr><tr><td>葡萄糖</td><td>补充能量</td><td>34.9</td><td>高反时候充点葡萄糖水会稍微好一些</td></tr><tr><td>多功能逃生手链</td><td>野外紧急情况使用，有打火石、齿刀、口哨，绳子可拆</td><td>20</td><td>鬼知道我内心戏里都发生了什么乱七八糟的事，实际上这玩意儿并没有用到</td></tr><tr><td>野营套锅</td><td>野外煮东西</td><td>80</td><td>这锅最终在“无人区”煮泡面了，很好用</td></tr><tr><td>袋装绿豆汤红豆汤</td><td>食品，野外可食用</td><td>40</td><td>万一在无人区车坏了能顶一阵儿，实际上刚到杭州租好房没啥吃的煮了几次</td></tr><tr><td>帐篷及配套</td><td>露营</td><td></td><td>并没用到，现在这些东西还在后备箱里面呆着的</td></tr><tr><td>燃气灶</td><td>煮东西，与野营套锅配套</td><td></td><td>还需要气罐，不然是废铁</td></tr><tr><td>兵工铲</td><td></td><td></td><td>┑(￣Д ￣)┍我最开始想的是需要用来打狼的</td></tr><tr><td>拖车绳、备胎、应急搭电线等修车工具</td><td></td><td></td><td></td></tr><tr><td>这里说一下，最后选择走川藏线进藏，因为川藏线海拔不断起伏，对于第一次进藏的人来说可能更容易适应高海拔，而青藏线海拔一直很高，容易发生高反。</td><td></td><td></td><td></td></tr></tbody></table><h1 id="Day1：北京-运城（0513）"><a href="#Day1：北京-运城（0513）" class="headerlink" title="Day1：北京-运城（0513）"></a>Day1：北京-运城（0513）</h1><p>一大早就出发啦，塞满后备箱以及后排，然后快出六环了发现忘记记录里程了，感觉拍张照记一下，想着等开回来看看走了多远，然而没想到，最终这辆车没机会开到北京而是停在了杭州（估计得等换车的时候它才再有机会回北京了）。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561303551.jpg"  title="满满的一车" alt="满满的一车"/>出发的第一天，其实还有很多东西并没有准备好，比如忘买氧气瓶了，这时候发现京东当日达很6啊，买了几瓶氧气，然后@xqq 看攻略买了一堆我觉得没有卵用的东西，当日达送到第一天晚上的酒店。<table><thead><tr><th>物品名称</th><th>作用</th><th>价格</th><th>备注</th></tr></thead><tbody><tr><td>氧气瓶</td><td>补充氧气</td><td>79</td><td>最开始我觉得我这种4k+海拔上还能蹦蹦跳跳的人肯定用不上，后来发现真香</td></tr><tr><td>善存片、洋参含片</td><td>补充维生素，抗疲劳？</td><td>142</td><td></td></tr><tr><td>耳温枪</td><td>测体温</td><td>110</td><td>带得有一些感冒药，但是发现忘带体温计了</td></tr></tbody></table><p>中午忘记吃什么了（貌似随便吃了点泡面和零食？），晚上我自己在服务区吃了顿自助餐，居然才35，刚从北京出来的我感觉真的是良心价啊（现在的我看着这个价格想着当年的心情突然觉得我现在真的穷成狗了），而且看上去以及吃上去还不错。而@xqq 在高速上没饿，到运城吃了顿貌似是啥砂锅，查了下账单是28，我印象中好像很不错的样子。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561304472.jpg" width="400" title="服务区的晚餐" alt="服务区的晚餐"/>大概晚上七点半下高速到运城（我居然下高速还拍了张etc收费的照片），然后到了运城去的第一个地方居然是小米之家，嗯然后这个习惯莫名其妙保持到了现在，我去一个城市居然会先去转转这个城市的小米之家。在小米之家买了个逆变器，用来给相机、电脑等供电，然后买了个指尖积木，嗯就是下图这货，对于轮班坐副驾时的手贱晚期的我来说这是整个旅程性价比最高的东西🤣。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561304884.jpg" width="200" title="米兔指尖积木" alt="米兔指尖积木"/>第一天全程高速，两个人换着开，从早晨七点半左右到晚上七点半，12小时，中午、下午在服务器休息了会儿，总共应该开了900km左右，这也差不多是全程单日行程最多的一天了。这是下午六点半左右的手机截图。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561304617.jpg" width="200" title="第一天的路线" alt="第一天的路线"/>以下是这一天的花费：<table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>保险</td><td>286</td><td></td></tr><tr><td>高速</td><td>14.5</td><td>北京</td></tr><tr><td>早餐</td><td>34</td><td></td></tr><tr><td>高速</td><td>104.5</td><td>河北</td></tr><tr><td>加油</td><td>245</td><td></td></tr><tr><td>午饭</td><td>30</td><td></td></tr><tr><td>晚饭</td><td>35</td><td></td></tr><tr><td>高速</td><td>185.25</td><td>运城</td></tr><tr><td>酒店</td><td>139</td><td>运城</td></tr><tr><td>晚饭</td><td>28</td><td>运城</td></tr><tr><td>西瓜</td><td>18</td><td></td></tr><tr><td>加油</td><td>205.18</td><td></td></tr><tr><td>共计</td><td>1324.43</td><td></td></tr></tbody></table><h1 id="Day2：运城-剑阁（0514）"><a href="#Day2：运城-剑阁（0514）" class="headerlink" title="Day2：运城-剑阁（0514）"></a>Day2：运城-剑阁（0514）</h1><p>一大早出发啦，然而今天显然没有昨天那么激动了（嗯我还记得这个）。一路上路挺好风景挺不错，从风陵渡黄河大桥过黄河（照片带GPS水印真是方便我等脑残换着）进入陕西地界，完成了全程唯一一次被交警拦下的记录，一定是@xqq 太像坏人了。</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561306403.jpg" width="600" title="渡黄河，然后被警察叔叔拦下" alt="渡黄河，然后被警察叔叔拦下"/>进入陕西地界后很快到了华山服务区，隔着服务区远远的看了几眼。上次到西安因为时（jin）间（jin）不（shi）足（lan），想着以后有的是机会就没去爬华山了，这次路过想着之后跑完西藏回程去爬，结果，至今我没有去过华山。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561306629.jpg" width="600" title="远望华山" alt="远望华山"/>从西安边上绕过后很快就开始穿秦岭了，地图上看见一层厚厚的山，一大波隧道连着隧道，幸好之前开过重庆的各种隧道，觉得没啥，某@xqq 倒是激动了半天，然后就开得困得不行。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561307152.jpg"  title="穿秦岭" alt="穿秦岭"/>传出秦岭隧道后马上就是一个服务区，赶紧的停下来休息吃饭，本乡巴佬第一次见服务区居然有KFC，还兴奋的拍了照，结果发现江浙一带这简直再平常不过了。。。北方的服务区啊，你们好好学学吧。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561307406.jpg" width="400" title="服务区的KFC" alt="服务区的KFC"/>最开始过了秦岭赶紧就快到成都了，结果发现我想多了，穿过了一堆又一堆的山，终于才差不多平坦了。我大四川的云真好看。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561307707.jpg"  title="穿过一堆山到四川" alt="穿过一堆山到四川"/>晚上到广元，准备开始此次旅程的第一次浪，去剑门关玩一趟。在入住酒店前收到个惊天大消息，实验室要求打卡了，我这毕业前一个月还能赶上，真是惨，嗯得想想办法。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561307944.jpg" width="400" title="实验室要开始打卡的噩耗" alt="实验室要开始打卡的噩耗"/>以下是这一天的花费：<table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>加油</td><td>70</td><td></td></tr><tr><td>高速</td><td>28.5</td><td>运城</td></tr><tr><td>高速</td><td>90</td><td>陕西</td></tr><tr><td>早饭</td><td>44</td><td>运城</td></tr><tr><td>午饭</td><td>79</td><td>秦岭KFC</td></tr><tr><td>油</td><td>225</td><td>加油</td></tr><tr><td>高速</td><td>200</td><td>广元</td></tr><tr><td>晚餐</td><td>50</td><td></td></tr><tr><td>高速</td><td>49.4</td><td>剑阁</td></tr><tr><td>眼药水</td><td>33</td><td></td></tr><tr><td>酒店</td><td>189</td><td>剑阁</td></tr><tr><td>共计</td><td>1057.9</td><td></td></tr></tbody></table><h1 id="Day3：剑阁-成都（0515）"><a href="#Day3：剑阁-成都（0515）" class="headerlink" title="Day3：剑阁-成都（0515）"></a>Day3：剑阁-成都（0515）</h1><p>作为四川人，“噫吁嚱，危乎高哉”、“剑阁峥嵘而崔嵬，一夫当关，万夫莫开”背了这么多年，居然第一次来剑门关，太惭愧了。整个景区超级大，因为今天计划到成都，所以只有小半天的时间玩，因此景区只走了一小部分，并且到了下图打问号的地方我就失忆了，忘了怎么出来的了┑(￣Д ￣)┍</p><img src="/images/Tibetan-Self-driving-Travel-Notes/1561382122.jpg"  title="剑门关游览路线" alt="剑门关游览路线"/>从剑门关古镇进景区就是类似栈道的路，不是特别难走，栈道也修得很好，植物长得很茂盛，全程我巨怕窜出一条蛇来。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561382823.jpg"  title="剑门关栈道" alt="剑门关栈道"/>那天慢热的，背着单反走走得巨累，结果发现叫累叫得太早了。到了关楼我以为差不多了，结果发现走了那天全程的不到一小半。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561382510.jpg"  title="关楼" alt="关楼"/>关楼超有气势，一边靠山，一边靠水，山很险水很急，难怪能一夫当关，万夫莫开。但是原谅我累（tai）崩（cai）了没能拍出来气势。过了关楼往北想去看一线天，体会什么叫累（我觉得就是这么累断片了导致我忘了怎么出去的了）。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561383866.jpg"  title="一线天" alt="一线天"/>虽然感觉有山的景区都有类似一线天的景点，但是这里比其他地方更突出那个“一线”的感觉，当然也可能是我最近长胖了觉得挤过去比较费劲吧。在费劲往上爬的时候发现居然还有一条“攀岩线”，就这条路都吓得腿软的@xqq 估计是不敢去了。<img src="/images/Tibetan-Self-driving-Travel-Notes/1561384220.jpg"  title="悬崖" alt="悬崖"/>嗯再往后就累得不行都懒得拍照了，一路就想赶紧出去豁冰阔乐。下面是当时手机上做的一段视频。<video width="50%" height="400" src="/images/Tibetan-Self-driving-Travel-Notes/1561384543.mp4" controls="controls"> `<video>` 不可用，该换浏览器啦.</video><p>出去后在古镇上随便吃了点东西就往成都去了，顺带带着@xqq 去逛了趟川大，本科四年的日子历历在目，一晃眼硕士也走到尽头了，很是感慨。晚上和@xqq 去街边小馆吃了点地道的家常川菜。</p><blockquote><p>我擦放图片好累啊<br>看账本貌似记起来，后来是坐观光车出来的，我就说咋没啥印象了<br>那啥，成都下高速貌似etc设备问题没有扣钱，开心</p></blockquote><p>以下是这一天的花费：</p><table><thead><tr><th>名称</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td>剑门关门票</td><td>220</td><td></td></tr><tr><td>观光车</td><td>10</td><td></td></tr><tr><td>可乐</td><td>8</td><td></td></tr><tr><td>观光车</td><td>20</td><td></td></tr><tr><td>公交车</td><td>10</td><td></td></tr><tr><td>午餐</td><td>93</td><td></td></tr><tr><td>加油</td><td>235</td><td>德阳</td></tr><tr><td>晚饭</td><td>78</td><td>成都</td></tr><tr><td>水果</td><td>23.8</td><td></td></tr><tr><td>饮料</td><td>11.5</td><td></td></tr><tr><td>住宿</td><td>238</td><td>成都</td></tr><tr><td>共计</td><td>837.3</td><td></td></tr></tbody></table><p>发现尽管我图片压缩得够厉害了，一篇还是好几兆的图片，并且感觉后面的行程还有好多，所以我决定拆分一下，这一篇先写这么多，希望后面的不放鸽子。。。</p>]]></content>
    
    
    <summary type="html">这是一篇拖延了整整一年的游记，这一年来一直牵挂着它，再不写下来它就要难产了。</summary>
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
    <category term="生活" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="自驾" scheme="http://blog.liexing.me/tags/%E8%87%AA%E9%A9%BE/"/>
    
  </entry>
  
  <entry>
    <title>ParallelStream的陷阱</title>
    <link href="http://blog.liexing.me/2018/11/03/parallelstream-trap/"/>
    <id>http://blog.liexing.me/2018/11/03/parallelstream-trap/</id>
    <published>2018-11-03T05:44:32.000Z</published>
    <updated>2023-01-20T15:04:29.746Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>前段时间改造链路，发现链路上可能会出现丢掉threadlocal中数据的情况，排查发现是在执行parallelStream后出现了丢标，并且这个丢标是偶发的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    List&lt;Integer&gt; lists = Lists.newArrayList();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">4</span>; i++) &#123;</span><br><span class="line">        lists.add(i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//add threadLocal</span></span><br><span class="line">    <span class="comment">//something...</span></span><br><span class="line">    <span class="comment">//get threadLocal;</span></span><br><span class="line">    lists.parallelStream().forEach(e -&gt; &#123;</span><br><span class="line">        <span class="comment">//set threadLocal;</span></span><br><span class="line">        <span class="comment">//do something</span></span><br><span class="line">        <span class="comment">//clear threadLocal;</span></span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">//get threadLocal</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>多次执行后发现，parallelStream之后的标出现偶发性的被吞。（注：这里因为使用了部分集团内部组件，因此使用threadLocal替代）</p><h2 id="什么是ParallelStream"><a href="#什么是ParallelStream" class="headerlink" title="什么是ParallelStream"></a>什么是ParallelStream</h2><p>Stream（流）是JDK8中引入的一种类似与迭代器（Iterator）的单向迭代访问数据的工具。ParallelStream则是并行的流，它通过Fork&#x2F;Join 框架（JSR166y）来拆分任务，加速流的处理过程。最开始接触parallelStream很容易把其当做一个普通的线程池使用，因此也出现了上面提到的开始的时候打标，结束的时候去掉标的动作。</p><h2 id="ForkJoinPool又是什么"><a href="#ForkJoinPool又是什么" class="headerlink" title="ForkJoinPool又是什么"></a>ForkJoinPool又是什么</h2><p>ForkJoinPool是在Java 7中引入了一种新的线程池，其简单类图如下：</p><img src="/images/parallelstream-trap/15408857246400.jpg"  title="ForkJoinPool类图" alt="ForkJoinPool类图"/>可以看到ForkJoinPool是ExecutorService的实现类，是一种线程池。创建了ForkJoinPool实例之后，可以通过调用submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务。ForkJoinTask表示线程池中执行的任务，其有两个主要的抽象子类：RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务，而RecusiveAction代表没有返回值的任务。它们的类图如下：<img src="/images/parallelstream-trap/15408861070245.jpg"  title="ForkJoinTask类图" alt="ForkJoinTask类图"/>ForkJoinPool来支持使用分治法(Divide-and-Conquer Algorithm)来解决问题，即将一个任务拆分成多个“小任务”并行计算，再把多个“小任务”的结果合并成总的计算结果。相比于ThreadPoolExecutor，ForkJoinPool能够在任务队列中不断的添加新任务，在线程执行完任务后可以再从任务列表中选择其他任务来执行；并且可以选择子任务的执行优先级，因此能够方便的执行具有父子关系的任务。ForkJoinPool内部维护了一个无限队列来保存需要执行的任务，而线程的数量则是通过构造函数传入，如果没有向构造函数中传入希望的线程数量，那么当前计算机可用的CPU数量会被设置为线程数量作为默认值（最大为MAX_CAP = 0x7fff）。<img src="/images/parallelstream-trap/15408867028506.jpg"  title="" alt=""/>## 回过头来看ParallelStream原理运行如下代码：<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    List&lt;Integer&gt; lists = Lists.newArrayList();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">10000</span>; i++) &#123;</span><br><span class="line">        lists.add(i);</span><br><span class="line">    &#125;</span><br><span class="line">    Set&lt;String&gt; sequenceThreadNameSet = Sets.newHashSet();</span><br><span class="line">    Set&lt;String&gt; parallelThreadNameSet = Sets.newHashSet();</span><br><span class="line">    lists.forEach(e -&gt; sequenceThreadNameSet.add(Thread.currentThread().getName()));</span><br><span class="line">    lists.parallelStream().forEach(e -&gt; parallelThreadNameSet.add(Thread.currentThread().getName()));</span><br><span class="line"></span><br><span class="line">    System.out.println(sequenceThreadNameSet);</span><br><span class="line">    System.out.println(parallelThreadNameSet);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>得到结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[main]</span><br><span class="line">[ForkJoinPool.commonPool-worker-7, ForkJoinPool.commonPool-worker-1, ForkJoinPool.commonPool-worker-2, main, ForkJoinPool.commonPool-worker-5, ForkJoinPool.commonPool-worker-6, ForkJoinPool.commonPool-worker-3, ForkJoinPool.commonPool-worker-4]</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看出，串行的流使用的是main线程，而parallelStream使用了线程名为<code>ForkJoinPool.commonPool-worker-*</code>的线程，而这些线程来自于：</p><img src="/images/parallelstream-trap/15408910215785.jpg"  title="ForkJoinPool_common_线程池创建" alt="ForkJoinPool_common_线程池创建"/>`java.util.concurrent.ForkJoinPool#makeCommonPool`函数在ForkJoinPool类的静态方法块中别调用，返回结果赋值给一个静态成员元素common，这个common是Java 8中引入的一个通用的静态线程池，这个线程池用来处理那些没有被显式提交到任何线程池的任务，ParallelStream其实就是自动的使用了这个通用ForkJoinPool线程池来实现并行化。<img src="/images/parallelstream-trap/15408899694768.jpg"  title="ForkJoinPool_common_线程池" alt="ForkJoinPool_common_线程池"/>代码中可以看到，线程池数量取决于`parallelism`，而`parallelism`要么在3412、3419行中从环境变量中获得，要么在3435行被赋值为处理器数量减一，之后再判定如果其值小于0或者大于MAX_CAP，则取1或者MAX_CAP。注意这里，`parallelism < 0`，也就是如果启动jvm时候对其赋值为0，则会使用0作为参数进行线程池的创建。<h2 id="commonPool线程数好像不太对？"><a href="#commonPool线程数好像不太对？" class="headerlink" title="commonPool线程数好像不太对？"></a>commonPool线程数好像不太对？</h2><p>从上面可以看出，commonPool的线程数量默认会使用处理器数量减去1，比如我的电脑是八核（其实是八线程）的，其实commonPool的线程池是7个线程，这个通过打印<code>ForkJoinPool.getCommonPoolParallelism()</code>也能看出，这样做是因为还有一个主线程，主线程加上线程池线程刚好等于cpu核心数，这样能同时跑满cpu，并且不会因为线程太多造成线程本身的切换浪费资源，这样最有效率。</p><p>回过头来看上面代码的输出：<code>[ForkJoinPool.commonPool-worker-7, ForkJoinPool.commonPool-worker-1, ForkJoinPool.commonPool-worker-2, main, ForkJoinPool.commonPool-worker-5, ForkJoinPool.commonPool-worker-6, ForkJoinPool.commonPool-worker-3, ForkJoinPool.commonPool-worker-4]</code>，发现main也参与了ParallelStream中的计算。这是因为forEach将执行forEach本身的线程也被当作为线程池中的一个工作线程进行工作，因此使用刚好等于cpu核心数的线程来执行了多个任务。</p><p>因此，前面说的丢标的问题也得到解决，因为ParallelStream任务执行时，可能将main线程作为执行线程，因此如果在forEach中清标，可能会导致主线程中的标被丢掉。解决的方式也很简单，在执行完并行流之后，重新set一下标即可。</p><h2 id="所以就无脑用ParallelStream了？"><a href="#所以就无脑用ParallelStream了？" class="headerlink" title="所以就无脑用ParallelStream了？"></a>所以就无脑用ParallelStream了？</h2><h3 id="ParallelStream可能引起阻塞"><a href="#ParallelStream可能引起阻塞" class="headerlink" title="ParallelStream可能引起阻塞"></a>ParallelStream可能引起阻塞</h3><p>对CPU密集型的任务来说，并行流使用ForkJoinPool，为每个CPU分配一个任务，这是非常有效率的，但是如果任务不是CPU密集的，而是I&#x2F;O密集的，并且任务数相对线程数比较大，那么直接用ParallelStream并不是很好的选择。如下代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    List&lt;Integer&gt; lists = Lists.newArrayList();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; Runtime.getRuntime().availableProcessors(); i++) &#123;</span><br><span class="line">        lists.add(i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">Date</span> <span class="variable">start</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line">    lists.parallelStream().forEach(e -&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//do something</span></span><br><span class="line">            Thread.sleep(<span class="number">10000</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e1) &#123;</span><br><span class="line">            e1.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    lists.parallelStream().forEach(e -&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//do something</span></span><br><span class="line">            Thread.sleep(<span class="number">10000</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e1) &#123;</span><br><span class="line">            e1.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="type">Date</span> <span class="variable">end</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line">    System.out.println((end.getTime() - start.getTime()) / <span class="number">1000</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>发现其执行时长为20秒，但是如下代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        List&lt;Integer&gt; lists = Lists.newArrayList();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; Runtime.getRuntime().availableProcessors(); i++) &#123;</span><br><span class="line">            lists.add(i);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Date</span> <span class="variable">start</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line">        <span class="type">ForkJoinPool</span> <span class="variable">forkJoinPool1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ForkJoinPool</span>(Runtime.getRuntime().availableProcessors());</span><br><span class="line">        <span class="type">ForkJoinPool</span> <span class="variable">forkJoinPool2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ForkJoinPool</span>(Runtime.getRuntime().availableProcessors());</span><br><span class="line">        <span class="type">CountDownLatch</span> <span class="variable">countDownLatch</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountDownLatch</span>(<span class="number">2</span>);</span><br><span class="line">        taskProcess(lists, forkJoinPool1, countDownLatch);</span><br><span class="line">        taskProcess(lists, forkJoinPool2, countDownLatch);</span><br><span class="line">        countDownLatch.await();</span><br><span class="line">        <span class="type">Date</span> <span class="variable">end</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line">        System.out.println((end.getTime() - start.getTime()) / <span class="number">1000</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">taskProcess</span><span class="params">(List&lt;Integer&gt; lists, ForkJoinPool forkJoinPool, CountDownLatch countDownLatch)</span> &#123;</span><br><span class="line">        forkJoinPool.submit(() -&gt; &#123;</span><br><span class="line">            lists.parallelStream().forEach(e -&gt; &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">//do something</span></span><br><span class="line">                    Thread.sleep(<span class="number">10000</span>);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e1) &#123;</span><br><span class="line">                    e1.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">            countDownLatch.countDown();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行可以发现，10秒可以执行完毕。这是因为第一份代码中，每次提交8个任务到commonPool，提交了两次，第二次的任务得等第一次执行完毕后才能执行，并且主线程也被阻塞。而第二次，使用独立的ForkJoinPool来执行线程，没有影响主线程的执行，如果在每个任务中打印一下线程名字也能看出来：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">ForkJoinPool-2-worker-1</span><br><span class="line">ForkJoinPool-1-worker-0</span><br><span class="line">ForkJoinPool-1-worker-2</span><br><span class="line">ForkJoinPool-1-worker-4</span><br><span class="line">ForkJoinPool-1-worker-5</span><br><span class="line">ForkJoinPool-1-worker-7</span><br><span class="line">ForkJoinPool-1-worker-6</span><br><span class="line">ForkJoinPool-2-worker-5</span><br><span class="line">ForkJoinPool-2-worker-4</span><br><span class="line">ForkJoinPool-1-worker-3</span><br><span class="line">ForkJoinPool-2-worker-6</span><br><span class="line">ForkJoinPool-2-worker-7</span><br><span class="line">ForkJoinPool-2-worker-2</span><br><span class="line">ForkJoinPool-2-worker-3</span><br><span class="line">ForkJoinPool-2-worker-0</span><br><span class="line">ForkJoinPool-1-worker-1</span><br></pre></td></tr></table></figure><h3 id="ParallelStream是多线程，注意线程安全"><a href="#ParallelStream是多线程，注意线程安全" class="headerlink" title="ParallelStream是多线程，注意线程安全"></a>ParallelStream是多线程，注意线程安全</h3><p>请看下面的代码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        List&lt;Integer&gt; listOfIntegers =</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt;<span class="number">100</span>; i++) &#123;</span><br><span class="line">            listOfIntegers.add(i);</span><br><span class="line">        &#125;</span><br><span class="line">        List&lt;Integer&gt; parallelStorage = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;() ;</span><br><span class="line">        listOfIntegers</span><br><span class="line">            .parallelStream()</span><br><span class="line">            .filter(i-&gt;i%<span class="number">2</span>==<span class="number">0</span>)</span><br><span class="line">            .forEach(i-&gt;parallelStorage.add(i));</span><br><span class="line"></span><br><span class="line">        parallelStorage</span><br><span class="line">            .stream()</span><br><span class="line">            .sorted((o1, o2) -&gt; &#123;</span><br><span class="line">                <span class="keyword">if</span> (o1 == <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">                &#125; <span class="keyword">else</span> <span class="keyword">if</span> (o2 == <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">return</span> o1 &gt; o2 ? <span class="number">1</span> : o1.equals(o2) ? <span class="number">0</span> : -<span class="number">1</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;)</span><br><span class="line">            .forEach(e -&gt; System.out.print(e + <span class="string">&quot; &quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行完后，发现parallelStorage中居然出现了null：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">null 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 </span><br></pre></td></tr></table></figure><p>这是因为在ArrayList中存储数据的过程不是一个线程安全的过程导致的。因此使用ParallelStream时要注意这点。</p><h2 id="一些思考"><a href="#一些思考" class="headerlink" title="一些思考"></a>一些思考</h2><p>勿在浮沙筑高台。一些常用的东西，底层的设计还是很巧妙的，而这些巧妙间却埋了不少的坑，当然踩坑还是应为CURD虽然撸得多但是对实现不了解，需要多去了解底层。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引子&quot;&gt;&lt;a href=&quot;#引子&quot; class=&quot;headerlink&quot; title=&quot;引子&quot;&gt;&lt;/a&gt;引子&lt;/h2&gt;&lt;p&gt;前段时间改造链路，发现链路上可能会出现丢掉threadlocal中数据的情况，排查发现是在执行parallelStream后出现了丢标，并且</summary>
      
    
    
    
    <category term="Java" scheme="http://blog.liexing.me/categories/Java/"/>
    
    
    <category term="parallelStream" scheme="http://blog.liexing.me/tags/parallelStream/"/>
    
    <category term="java" scheme="http://blog.liexing.me/tags/java/"/>
    
    <category term="stream" scheme="http://blog.liexing.me/tags/stream/"/>
    
  </entry>
  
  <entry>
    <title>MySQL utf8之坑</title>
    <link href="http://blog.liexing.me/2018/07/27/mysql-utf8/"/>
    <id>http://blog.liexing.me/2018/07/27/mysql-utf8/</id>
    <published>2018-07-26T16:19:19.000Z</published>
    <updated>2023-01-20T15:04:29.744Z</updated>
    
    <content type="html"><![CDATA[<p>最近遇到几个项目被MySQL的utf8编码坑，想起之前编码问题被坑的惨痛教训，记录一下，警示自己。</p><p>曾几何时，每次建库都选utf8，觉得自己比那些用乱七八糟编码的人不知道酷到哪里去了。直到好多年前的某次课程设计做项目的时候，愉快的建了个用户表：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `test_user` (</span><br><span class="line">  `id` <span class="type">int</span>(<span class="number">11</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  `name` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8;</span><br></pre></td></tr></table></figure><p>然后愉快的新增用户：<code>INSERT INTO test_user(</code>name<code>) VALUES(&quot;我是😁&quot;)</code>，接着愉快的反思人生：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Incorrect string value: &#x27;\xF0\x9F\x98\x81&#x27; for column &#x27;name&#x27; at row 1</span><br></pre></td></tr></table></figure><p>我是谁？我来自哪里？我在干嘛？难道是我代码里面的字符集用错了？不对啊我所有地方都用的utf8啊……</p><h2 id="MySQL的UTF8编码是什么？"><a href="#MySQL的UTF8编码是什么？" class="headerlink" title="MySQL的UTF8编码是什么？"></a>MySQL的UTF8编码是什么？</h2><p>首先来看官方文档：</p><blockquote><p>The character set named utf8 uses a maximum of three bytes per character and contains only BMP characters. The utf8mb4 character set uses a maximum of four bytes per character supports supplementary characters:</p></blockquote><blockquote><p> For a BMP character, utf8 and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.<br> For a supplementary character, utf8 cannot store the character at all, whereas utf8mb4 requires four bytes to store it. Because utf8 cannot store the character at all, you have no supplementary characters in utf8 columns and need not worry about converting characters or losing data when upgrading utf8 data from older versions of MySQL.</p></blockquote><p>我们再看看维基百科对UTF8编码的解释：</p><blockquote><p>UTF-8 is a variable width character encoding capable of encoding all 1,112,064 valid code points in Unicode using one to four 8-bit bytes.</p></blockquote><img src="/images/mysql-utf8/15323992645548.jpg"  title="" alt=""/>可以看出，MySQL中的utf8实质上不是标准的UTF8。MySQL中，utf8对每个字符最多使用三个字节来表示，所以一些emoji甚至是一些生僻汉字就存不下来了，比如“𡋾”。<p>MySQL一直不承认这是一个bug，他们在2010年发布了“utf8mb4”字符集来绕过这个问题，在MySQL中，utf8mb4才应该是标准的utf8编码，并且官方很鸡贼的偷偷在最新的文档中加上了，算是认识到错误了吧：</p><blockquote><p>utf8 is an alias for the utf8mb3 character set.<br>The utf8mb3 character set will be replaced by utf8mb4 in some future MySQL version. Although utf8 is currently an alias for utf8mb3, at that point utf8 will become a reference to utf8mb4. To avoid ambiguity about the meaning of utf8, consider specifying utf8mb4 explicitly for character set references instead of utf8.</p></blockquote><h2 id="MySQL-UTF8问题简史"><a href="#MySQL-UTF8问题简史" class="headerlink" title="MySQL UTF8问题简史"></a>MySQL UTF8问题简史</h2><p>MySQL从4.1版本开始支持utf8，即2003年，但是现在的utf8标准（<a href="https://tools.ietf.org/html/rfc3629">RFC 3629</a>)是在其后发布的。MySQL在2002年3月28日的4.1预览版中使用了旧版的utf8标准（<a href="https://tools.ietf.org/html/rfc2279">RFC 2279</a>)，该标准最多支持每个字符6个字节，同年9月MySQL调整其<a href="https://github.com/mysql/mysql-server/commit/43a506c0ced0e6ea101d3ab8b4b423ce3fa327d0">utf8字符集最多支持3字节</a>，而这个调整可能只是为了优化空间（05年前推荐使用CHAR类字段，而一个utf8的CHAR将会占用6字节长度）和时间性能（05年前在MySQL中使用CHAR字段会有更优的速度）。嗯可以在GitHub中看到大家对这个坑的吐槽：<br><img src="/images/mysql-utf8/15324047157494.jpg"  title="" alt=""/><img src="/images/mysql-utf8/15324047308992.jpg"  title="" alt=""/><br>但是这个字符编码发布出来，就不能轻易的修改，因为如果已经有用户开始使用了，就需要这些用户重新构建其数据库。</p><p>怎么补救呢？在上面最新文档中可以看出，他们将当前的utf8作为utf8mb3的别名，并且在将来的某一天会把utf8重新作为utf8mb4别名，这样来解决这个多年的巨坑。</p><h2 id="啥是UTF8"><a href="#啥是UTF8" class="headerlink" title="啥是UTF8"></a>啥是UTF8</h2><p>略</p><img src="/images/mysql-utf8/15324055064000.jpg"  title="" alt=""/><h2 id="utf8mb4-unicode-ci-和-utf8mb4-general-ci"><a href="#utf8mb4-unicode-ci-和-utf8mb4-general-ci" class="headerlink" title="utf8mb4_unicode_ci 和 utf8mb4_general_ci"></a>utf8mb4_unicode_ci 和 utf8mb4_general_ci</h2><p>字符除了存储，还需要排序或者比较，这个操作与编码字符集有关，称为collation，与utf8mb4对应的是utf8mb4_unicode_ci 和 utf8mb4_general_ci这两个collation。</p><h3 id="准确性"><a href="#准确性" class="headerlink" title="准确性"></a>准确性</h3><p>utf8mb4_unicode_ci 是基于标准Unicode来进行排序比较的，能保持在各个语言之间的精确排序；</p><p>utf8mb4_general_ci 并不基于Unicode排序规则，因此在某些特殊语言或者字符上的排序结果可能不是所期望的。</p><h3 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h3><p>utf8mb4_general_ci 在比较和排序时更快，因为其实现了一些性能更好的操作，但是在现代服务器上，这种性能提升几乎可以忽略不计。</p><p>utf8mb4_unicode_ci 使用Unicode的规则进行排序和比较，其排序规则为了处理一些特殊字符，实现更加复杂。</p><p>现在基本没有理由继续使用utf8mb4_general_ci了，因为其带来的性能差异很小，远不如更好的数据设计，比如使用索引等等。</p><h2 id="MySQL用错编码怎么救"><a href="#MySQL用错编码怎么救" class="headerlink" title="MySQL用错编码怎么救"></a>MySQL用错编码怎么救</h2><ol><li><p>备份，不然崩了就只有删库跑路了；</p></li><li><p>升级MySQL服务端到5.3.3及以上版本，以支持utf8md4；</p></li><li><p>将数据库、表、列的字符编码、collation改为utf8md4:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># <span class="keyword">For</span> <span class="keyword">each</span> database:</span><br><span class="line"><span class="keyword">ALTER</span> DATABASE database_name <span class="type">CHARACTER</span> <span class="keyword">SET</span> <span class="operator">=</span> utf8mb4 <span class="keyword">COLLATE</span> <span class="operator">=</span> utf8mb4_unicode_ci;</span><br><span class="line"># <span class="keyword">For</span> <span class="keyword">each</span> <span class="keyword">table</span>:</span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> table_name <span class="keyword">CONVERT</span> <span class="keyword">TO</span> <span class="type">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_unicode_ci;</span><br><span class="line"># <span class="keyword">For</span> <span class="keyword">each</span> <span class="keyword">column</span>:</span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> table_name CHANGE column_name column_name <span class="type">VARCHAR</span>(length) <span class="type">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_unicode_ci;</span><br></pre></td></tr></table></figure></li><li><p>检查列和索引键的最大长度；</p></li><li><p>修改连接、客户端、服务端的字符集；</p></li><li><p>修复和优化所有的表，以免出现一些莫名其妙的错误，可以使用如下的方式：</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># <span class="keyword">For</span> <span class="keyword">each</span> <span class="keyword">table</span></span><br><span class="line">REPAIR <span class="keyword">TABLE</span> table_name;</span><br><span class="line">OPTIMIZE <span class="keyword">TABLE</span> table_name;</span><br></pre></td></tr></table></figure><p> 或者是使用<code>mysqlcheck</code>工具：</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ mysqlcheck -u root -p --auto-repair --optimize --all-databases</span><br></pre></td></tr></table></figure></li></ol><h2 id="其他坑"><a href="#其他坑" class="headerlink" title="其他坑"></a>其他坑</h2><p><a href="https://mp.weixin.qq.com/s/ns9eRxjXZfUPNSpfgGA7UA">MySQL表字段字符集不同导致的索引失效问题</a></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434">https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434</a></li><li><a href="https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8.html">https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8.html</a></li><li><a href="https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/">https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/</a></li><li><a href="https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci">https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci</a></li><li><a href="https://mathiasbynens.be/notes/mysql-utf8mb4#utf8-to-utf8mb4">https://mathiasbynens.be/notes/mysql-utf8mb4#utf8-to-utf8mb4</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近遇到几个项目被MySQL的utf8编码坑，想起之前编码问题被坑的惨痛教训，记录一下，警示自己。&lt;/p&gt;
&lt;p&gt;曾几何时，每次建库都选utf8，觉得自己比那些用乱七八糟编码的人不知道酷到哪里去了。直到好多年前的某次课程设计做项目的时候，愉快的建了个用户表：&lt;/p&gt;
&lt;fi</summary>
      
    
    
    
    <category term="笔记" scheme="http://blog.liexing.me/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="MySQL" scheme="http://blog.liexing.me/tags/MySQL/"/>
    
    <category term="utf8" scheme="http://blog.liexing.me/tags/utf8/"/>
    
    <category term="编码" scheme="http://blog.liexing.me/tags/%E7%BC%96%E7%A0%81/"/>
    
    <category term="utf8mb4" scheme="http://blog.liexing.me/tags/utf8mb4/"/>
    
  </entry>
  
  <entry>
    <title>跨域访问踩坑日志</title>
    <link href="http://blog.liexing.me/2018/03/20/cross-domain-debug-record/"/>
    <id>http://blog.liexing.me/2018/03/20/cross-domain-debug-record/</id>
    <published>2018-03-20T12:39:49.000Z</published>
    <updated>2023-01-20T15:04:29.748Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>拨开云雾见天日 守得云开见月明</p><footer><strong>我也不知道是谁说的</strong></footer></blockquote><p>终于把毕设撸完了，我的毕设刚好值10斤肉。赶论文这段时间无比的想撸代码，第一版写完赶紧的写了一晚上 bug，愉快的熬到了半夜。效果见本站左上角，后端调用了韩寒的“一个”，每天都是不一样的话，后端主要是 Spring 那一套。计划最近写一些常用的接口并且开放出来。</p><p>在过程中遇到一个坑，尽管用了<code>@CrossOrigin</code>注解，但是返回内容依然没有Access-Control-Allow-Origin头部，postman上没有（之前写 PHP，在 nginx 中直接对接口调用加上了这个头，所以。。。嗯是我太菜），ajax去访问也没有（这里是我自己脑抽了，后面讲），导致前端无法跨域访问，但是神奇的是，如果任务起在我电脑上，同一个局域网上室友的电脑上 postman 返回正常，微笑脸。。。嗯情况大概就是这样，然后这个 bug 调了我一晚。</p><h2 id="CORS-基本概念"><a href="#CORS-基本概念" class="headerlink" title="CORS 基本概念"></a>CORS 基本概念</h2><p>这里首先说一下<a href="https://www.w3.org/TR/cors/">CORS</a>，CORS是一个 W3C的标准，全称是跨域资源共享（Cross-origin resource sharing）。CORS需要服务器返回的数据头部包含相应的信息，CORS在客户端是浏览器自动支持的（IE 走开），如果浏览器收到服务器返回数据中包含了对应的头部，则可以使用其数据。对于简单请求（HEAD、GET、POST 方法之一，并且请求的头部不能超出如Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type这几个字段，并且Content-Type只限于application&#x2F;x-www-form-urlencoded、multipart&#x2F;form-data、text&#x2F;plain这三个值），浏览器会自动在请求中增加一个 <code>Origin</code>字段，对于非简单请求，浏览器会提前进行一次“预检”请求（请求方法为OPTIONS）查询服务器一些信息，然后再发送加上了<code>Origin</code>字段的正常请求。比如发送一个<code>PUT</code>请求，浏览器首先发送“预检”请求：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">OPTIONS</span> <span class="string">/cors</span> <span class="meta">HTTP/1.1</span></span><br><span class="line"><span class="attribute">Origin</span><span class="punctuation">: </span>https://api.liexing.me</span><br><span class="line"><span class="attribute">Access-Control-Request-Method</span><span class="punctuation">: </span>PUT</span><br><span class="line"><span class="attribute">Access-Control-Request-Headers</span><span class="punctuation">: </span>X-Custom-Header</span><br><span class="line"><span class="attribute">Host</span><span class="punctuation">: </span>api.liexing.me</span><br><span class="line"><span class="attribute">Accept-Language</span><span class="punctuation">: </span>zh-CN,zh;q=0.9,en;q=0.8</span><br><span class="line"><span class="attribute">Connection</span><span class="punctuation">: </span>keep-alive</span><br><span class="line"><span class="attribute">User-Agent</span><span class="punctuation">: </span>Mozilla/5.0...</span><br></pre></td></tr></table></figure><p>“预检”请求用来向服务器确认，当前网页所在的域名是否在服务器的许可名单之中，以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复，浏览器才会发出正式的请求。<code>Access-Control-Request-Method</code>用来询问是否支持该方法，<code>Access-Control-Request-Headers</code>用来指定浏览器CORS请求会额外发送的头信息字段。如果服务器确认了请求，则做出回应：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">200</span> OK</span><br><span class="line"><span class="attribute">Date</span><span class="punctuation">: </span>Thu, 20 Mar 2018 22:30:39 GMT</span><br><span class="line"><span class="attribute">Server</span><span class="punctuation">: </span>Apache/2.0.61 (Unix)</span><br><span class="line"><span class="attribute">Access-Control-Allow-Origin</span><span class="punctuation">: </span>*</span><br><span class="line"><span class="attribute">Access-Control-Allow-Methods</span><span class="punctuation">: </span>GET, POST, PUT</span><br><span class="line"><span class="attribute">Access-Control-Allow-Headers</span><span class="punctuation">: </span>X-Custom-Header</span><br><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>text/html; charset=utf-8</span><br><span class="line"><span class="attribute">Content-Encoding</span><span class="punctuation">: </span>gzip</span><br><span class="line"><span class="attribute">Content-Length</span><span class="punctuation">: </span>0</span><br><span class="line"><span class="attribute">Keep-Alive</span><span class="punctuation">: </span>timeout=2, max=100</span><br><span class="line"><span class="attribute">Connection</span><span class="punctuation">: </span>Keep-Alive</span><br><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>text/plain</span><br></pre></td></tr></table></figure><p>其中，<code>Access-Control-Allow-Origin</code>即为允许访问的域名，可以为*或者域名，<code>Access-Control-Allow-Methods</code>表示允许跨域的方法，<code>Access-Control-Allow-Headers</code>为允许跨域需要的头部。</p><p>在预检通过之后，会发送正常的<code>PUT</code>请求，并且在头部自动添加<code>Origin</code> 字段，服务器对这个请求需要校验是否含有<code>Access-Control-Allow-Headers</code>标注的头部，是否是允许的方法以及来源域名，如果都通过则返回会带上<code>Access-Control-Allow-Origin</code>，这时候浏览器才能使用。</p><h2 id="服务端对跨域请求的识别"><a href="#服务端对跨域请求的识别" class="headerlink" title="服务端对跨域请求的识别"></a>服务端对跨域请求的识别</h2><p>在<a href="https://www.w3.org/TR/cors/">CORS</a>的文档中可以看到这样一句话：</p><blockquote><p>Server-side applications are enabled to discover that an HTTP request was deemed a cross-origin request by the user agent, through the Origin header.</p></blockquote><p>这是第一个坑，当然这是我自己的问题，因为之前写PHP时候，为了方便，直接在通过Nginx代理中加上一个<code>Access-Control-Allow-Origin</code>头部，而 Spring Boot 对于 CORS 是按照标准来的，这里后面会说。</p><h2 id="浏览器对CORS的操作"><a href="#浏览器对CORS的操作" class="headerlink" title="浏览器对CORS的操作"></a>浏览器对CORS的操作</h2><p>前面说了，浏览器在跨域访问时，会自动加上一个<code>Origin</code>头部来标注来源，这也是后端识别跨域访问的判定标准。而我在写 js 时（嗯我就是前端渣，不然咋会掉坑），域名没有加上协议，即 <code>https://</code>或者<code>//</code>，尽管浏览器给了我提示，然而我瞎了，而且还认为是浏览器 bug 换了一堆浏览器。</p><img src="/images/cross-domain-debug-record/1521556978.jpg" width="600" title="浏览器对于协议缺失的报错" alt="浏览器对于协议缺失的报错"/>## Postman 的坑<p>这下来了让我当时无比懵逼的一个坑了，就是我的 Postman 是 app 版本（Mac）的，我室友的是 Chrome 的应用版，而Chrome 应用版本的Postman在请求时，会加上一个<code>Origin</code>字段，而 app 版的不会，因此这也算为啥我室友电脑上没事╭(╯^╰)╮</p><img src="/images/cross-domain-debug-record/1521557592.png" width="600" title="Chrome应用版postman" alt="Chrome应用版postman"/><img src="/images/cross-domain-debug-record/1521558256.jpg" width="600" title="app应用版postman" alt="app应用版postman"/>从上面两个图可以看出，不同版本的 postman 发送的请求头部会有不同。<h2 id="Spring中CORS相关操作"><a href="#Spring中CORS相关操作" class="headerlink" title="Spring中CORS相关操作"></a>Spring中CORS相关操作</h2><p>Spring 中，检查是否是CORS只是看<code>Origin</code>头部，即完全按照W3C标准。Spring 通过<code>org.springframework.web.cors</code>中的<code>CorsUtils.isCorsRequest</code>来判断，如下图。<br><img src="/images/cross-domain-debug-record/1521558645.jpg" width="600" title="Spring中判断CORS" alt="Spring中判断CORS"/><br>对于 CORS 的各种操作，会在<code>org.springframework.web.cors</code>中进行，比如在<code>org.springframework.web.cors.reactive</code>中的<code>DefaultCorsProcessor。handleInternal</code>对 CORS 请求的返回进行各种校验并且添加头部操作，如下图。<br><img src="/images/cross-domain-debug-record/1521558994.jpg" width="600" title="Spring中添加CORS头" alt="Spring中添加CORS头"/><br>anyway，总结一下这次的坑，首先是自己懒没看CORS详细的规范，以及习惯性的自己用错了都加上跨域的头部；其次是自己傻ajax写错了，导致一直以为是后端代码的问题；最后是postman太坑，室友的电脑上正常我的电脑错误，导致我懵逼了半天。嗯就这样。</p>]]></content>
    
    
    <summary type="html">关于 CORS 踩的一些坑，以及介绍了一些Spring中CORS相关的代码</summary>
    
    
    
    <category term="笔记" scheme="http://blog.liexing.me/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="跨域" scheme="http://blog.liexing.me/tags/%E8%B7%A8%E5%9F%9F/"/>
    
    <category term="Spring Boot" scheme="http://blog.liexing.me/tags/Spring-Boot/"/>
    
    <category term="踩坑日志" scheme="http://blog.liexing.me/tags/%E8%B8%A9%E5%9D%91%E6%97%A5%E5%BF%97/"/>
    
  </entry>
  
  <entry>
    <title>风格迁移-生成图片</title>
    <link href="http://blog.liexing.me/2017/12/20/art-generation-with-neural-style-transfer/"/>
    <id>http://blog.liexing.me/2017/12/20/art-generation-with-neural-style-transfer/</id>
    <published>2017-12-20T14:58:58.000Z</published>
    <updated>2023-01-20T15:04:29.748Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>画由心生,境为意造 —— 白居易</p></blockquote><p>去年《你的名字》火了之后，某 APP 发布了一个风格迁移的滤镜，随后朋友圈火了一把动画风的图片秀。当时就像玩玩风格迁移，然后这个拖延症一直至今，前几天看吴恩达的深度学习课程，突然想起这个坑。我也不知道在忙到爆炸的毕设中期前哪来的勇气这么浪去玩其他(￣.￣)</p><img src="/images/art-generation-with-neural-style-transfer/1513782703.jpeg" width="600" title="你的名字" alt="你的名字"/># 风格迁移简介<p>风格迁移（Style Transfer）是深度学习非常好玩的一个应用，它可以从一张图片获得风格，另一张图片或得内容，再合成为一张新的图片，比如：</p><img src="/images/art-generation-with-neural-style-transfer/1513783405.jpg" width="600" title="sytle-transfer" alt="sytle-transfer"/>左上角是需要的内容图片，右上角是想要学习的风格图片，想要学习、生成下面的新图片。<h1 id="卷积神经网络中学习到了什么"><a href="#卷积神经网络中学习到了什么" class="headerlink" title="卷积神经网络中学习到了什么"></a>卷积神经网络中学习到了什么</h1><p>对于图片分类，我们一般使用卷积神经网络获取图片信息，最终输出类别，比如下图中的网络，其实就是一层层的堆积卷积层和池化层，最后加几个全连接层：</p><img src="/images/art-generation-with-neural-style-transfer/1513783877.jpg"  title="cnn" alt="cnn"/>我们常说，在深度神经网络中，浅层的神经元拟合简单的信息，深层的拟合更复杂的信息。那么在 CNN 中，每层卷积核分别在什么情况下激活呢？下图是上面的网络，每层卷积核激活时候的输入输入：<img src="/images/art-generation-with-neural-style-transfer/1513784174.jpg"  title="activate" alt="activate"/>可以看出，浅层的核只是拟合简单的线条，越是深层拟合的东西越复杂（可以想象卷积核移动的时候，浅层往往只关注局部的信息，而越是深层，对于输入图像关注的范围越大）。<p>在我们的生成迁移模型中，将会使用预训练好的 CNN 网络模型，来提取图形特征与内容，使用越深的网络获取到的特征越高层，因此选用较浅的层将会更加像素级的趋近输入图片，越深则越在内容上趋近。</p><h1 id="风格迁移模型"><a href="#风格迁移模型" class="headerlink" title="风格迁移模型"></a>风格迁移模型</h1><p>最早期的风格<a href="https://arxiv.org/pdf/1508.06576v2.pdf">迁移模型</a>非常缓慢，因为它把图片的生成过程当做一个“训练”过程，将风格图片和内容图片作为输入，生成最后的新图片，每生成一张图片都相当于训练一个模型。模型图如下：</p><img src="/images/art-generation-with-neural-style-transfer/1513785216.jpg"  title="a-neural-algorithm-of-artistic-style" alt="a-neural-algorithm-of-artistic-style"/><p>因此我们需要加快风格迁移的速度，需要把其当做一个“生成”或者“执行”的过程，即<a href="https://arxiv.org/pdf/1603.08155v1.pdf">快速风格迁移</a>，该模型图如下：</p><img src="/images/art-generation-with-neural-style-transfer/1513785348.jpg"  title="real-time-stype-transfer" alt="real-time-stype-transfer"/>首先随机生成一张图片 G，然后通过前面提到的网络选择出某特征层l，将内容图片和风格图片通过同样的方式得到某个特征层，再按照下面的方法得到损失函数进而通过反向传播修改 G。可以简化为如图所示：<img src="/images/art-generation-with-neural-style-transfer/1513823427.jpg"  title="real-time-stype-transfer-v2" alt="real-time-stype-transfer-v2"/>蓝色箭头为前向运算，红色箭头为反向运算，三个 Network 其实是同一个。<h1 id="损失函数"><a href="#损失函数" class="headerlink" title="损失函数"></a>损失函数</h1><p>训练机器学习模型，需要定义损失函数。从上面对风格迁移模型的描述可以看出，模型的损失由两部分来定义：内容损失与风格损失，即：</p><p>$$J(G) &#x3D; \alpha J_{Content}(C,G) + \beta J_{Style}(S,G)$$</p><p>其中，C(Content)表示内容图片，S(Style)表示风格图片，G(Generate)即第一项表示内容的损失，第二项表示风格的损失。</p><h2 id="内容损失函数"><a href="#内容损失函数" class="headerlink" title="内容损失函数"></a>内容损失函数</h2><p>那么如何来计算内容损失函数$\alpha J_{Content}(C,G)$呢？</p><p>首先应该使用一个预训练好的卷积神经网络模型，比如 VGG19，然后选择 l 层的激活来计算生成图片 G 和内容图片 C 的相似性，即计算 $a^{[l](C)}$ 和 $a^{[l](G)}$之间的相似度。这两个值越相似，那么 G 和 C 的内容就越相似。</p><p>$$J_{Content}(C,G) &#x3D; \frac{1}{2} \lVert a^{[l](C)} - a^{[l](G)} \rVert^2$$</p><h2 id="风格损失函数"><a href="#风格损失函数" class="headerlink" title="风格损失函数"></a>风格损失函数</h2><p>首先我们需要定义什么是风格。风格就是 l 层中不同通道的激活值的相关性。因为不同通道的卷积核，对不同的特征敏感，那么如果多个通道同时激活，说明图中出现了某些关联的特征，即风格。例如对于某层（l层）激活值的维度为：$n_H, n_W, n_C$，$a^{[l]}_{i,j,k}$表示$(i,j,k)$上的激活值，$G^{[l](S)}$表示风格矩阵，它是一个$n^{[l]}_C \times n^{[l]}<em>C$维度的矩阵。$G^{[l](S)}</em>{kk’}$表示在 k 通道和 k’ 通道的关联性：</p><p>$$G^{[l](S)}<em>{kk’} &#x3D; \sum</em>{i&#x3D;1}^{n_H^{[l]}} \sum_{j&#x3D;1}^{n_w^{[l]}} a^{[l]}<em>{i,j,k} a^{[l]}</em>{i,j,k’}$$</p><p>进而，风格损失函数可以定义为：</p><p>$$J_{Stype}^{[l]}(S,G) &#x3D; \frac{1}{2n_H^{[l]}n_W^{[l]}n_C^{[l]}} \sum_k \sum_{k’}\lVert G^{[l](S)}<em>{kk’} - G^{[l](G)}</em>{kk’} \rVert$$</p><p>对多个层求风格损失并且累加，可以同时得到多层的风格相似性，往往效果更好：</p><p>$$J_{Style}(S,G) &#x3D; \sum_l \lambda^{[l]}J_{Stype}^{[l]}(S,G)$$</p><h1 id="模型示例"><a href="#模型示例" class="headerlink" title="模型示例"></a>模型示例</h1><p>模型的代码具体见：<a href="https://github.com/keras-team/keras/blob/master/examples/neural_style_transfer.py">Keras 官方 examples</a></p><p>这里给出几个运行的例子（从左到右依次是 风格图、内容图、生成图）：</p><img src="/images/art-generation-with-neural-style-transfer/1513824608.jpg"  title="stype-transfer-demo-1" alt="stype-transfer-demo-1"/><img src="/images/art-generation-with-neural-style-transfer/1513825726.jpg"  title="stype-transfer-demo-2" alt="stype-transfer-demo-2"/><img src="/images/art-generation-with-neural-style-transfer/1513826175.jpg"  title="stype-transfer-demo-3" alt="stype-transfer-demo-3"/><img src="/images/art-generation-with-neural-style-transfer/1513826560.jpg"  title="stype-transfer-demo-4" alt="stype-transfer-demo-4"/><img src="/images/art-generation-with-neural-style-transfer/1513826974.jpg"  title="stype-transfer-demo-5" alt="stype-transfer-demo-5"/>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;画由心生,境为意造 —— 白居易&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;去年《你的名字》火了之后，某 APP 发布了一个风格迁移的滤镜，随后朋友圈火了一把动画风的图片秀。当时就像玩玩风格迁移，然后这个拖延症一直至今，前几天看吴恩达的深度学习课程</summary>
      
    
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/categories/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="风格迁移" scheme="http://blog.liexing.me/tags/%E9%A3%8E%E6%A0%BC%E8%BF%81%E7%A7%BB/"/>
    
    <category term="生成" scheme="http://blog.liexing.me/tags/%E7%94%9F%E6%88%90/"/>
    
    <category term="Sytle Transfer" scheme="http://blog.liexing.me/tags/Sytle-Transfer/"/>
    
  </entry>
  
  <entry>
    <title>调参工程学 - 梯度下降优化方法</title>
    <link href="http://blog.liexing.me/2017/11/16/optimizing-gradient-descent/"/>
    <id>http://blog.liexing.me/2017/11/16/optimizing-gradient-descent/</id>
    <published>2017-11-16T15:43:31.000Z</published>
    <updated>2023-01-20T15:04:29.747Z</updated>
    
    <content type="html"><![CDATA[<p>梯度下降是一个最优化算法。在深度学习中，通过梯度下降来找到损失函数的（局部）最小值，进而获得各个参数的值。</p><p>梯度下降最直观的解释如图所示，在山上某处，沿着最陡的方向向下，直到能到达的最低点。</p><img src="/images/optimizing-gradient-descent/1511081616.png" width="500" title="" alt=""/>虽然各个深度学习框架封装了若干常用的梯度下降算法，可以当做黑盒来使用，但是作为调参工程师，这些东西原理都不懂还怎么愉快的调参。<p>对于一般线性回归函数，其假设为：</p><p>$$h_{\theta}&#x3D;\sum_{j&#x3D;0}^{n}\theta_{j}x_{j}$$</p><p>对应的损失函数为：</p><p>$$J_{train}(\theta)&#x3D;\frac{1}{2m}\sum_{i&#x3D;1}^{m}(h_{\theta}(x^{(i)})-y^{(i)})^{2}$$</p><p>我们假设参数$\theta$是二维的，下图为一个二维参数（$\theta_{0}$和$\theta_{1}$）组对应能量函数的可视化图：</p><img src="/images/optimizing-gradient-descent/1511082983.png"  title="" alt=""/><p>本文大概分为：基本的梯度下降算法和梯度下降优化算法。</p><h1 id="基本的梯度下降算法"><a href="#基本的梯度下降算法" class="headerlink" title="基本的梯度下降算法"></a>基本的梯度下降算法</h1><h2 id="批量梯度下降（Batch-Graduebt-Descent-BGD）"><a href="#批量梯度下降（Batch-Graduebt-Descent-BGD）" class="headerlink" title="批量梯度下降（Batch Graduebt Descent, BGD）"></a>批量梯度下降（Batch Graduebt Descent, BGD）</h2><p>批量梯度下降算法使用整个数据集，算得整个数据集的损失，对目标参数$\theta$求导，用来更新$\theta$：</p><p>$$\theta &#x3D; \theta - \alpha\nabla_{\theta}{J(\theta)}$$</p><p>其具体的过称为：</p><ol><li><p>对上面的损失函数求偏导：</p><p> $$\frac{\partial{J(\theta)}}{\partial{\theta_j}} &#x3D; - \frac{1}{m}\sum_{i&#x3D;1}^{m}(y^i - h_{\theta}(x^i))x_{j}^{i}$$</p></li><li><p>最小化损失函数，按照每个参数$\theta$的梯度负方向来更新每个$\theta$：</p><p> $$\theta_{j} &#x3D; \theta_{j} + \alpha\frac{1}{m}\sum_{i&#x3D;1}^{m}(y^i - h_{\theta}(x^i))x_{j}^{i}$$</p></li></ol><p>这种方法，每更新一次参数，需要对整个数据集进行运算，十分缓慢并且非常占内存，而且不能实时在线更新参数。这是最原始的梯度下降方法。从迭代的次数上来看，BGD迭代的次数相对较少。其迭代的收敛曲线示意图可以表示如下：</p><img src="/images/optimizing-gradient-descent/1511083675.png"  title="" alt=""/><h2 id="随机梯度下降（Stochatic-Gradient-Decent-SGD）"><a href="#随机梯度下降（Stochatic-Gradient-Decent-SGD）" class="headerlink" title="随机梯度下降（Stochatic Gradient Decent, SGD）"></a>随机梯度下降（Stochatic Gradient Decent, SGD）</h2><p>随机梯度下降一次只使用一个样本进行目标函数梯度计算，其公式为：</p><p>$$\theta &#x3D; \theta - \alpha\nabla_{\theta}{J(\theta;x^i,j^i)}$$</p><p>即将上面的损失函数改写为：</p><p>$$J_{train}(\theta)&#x3D;\frac{1}{m}\sum_{i&#x3D;1}^{m}\frac{1}{2}(h_{\theta}(x^{(i)})-y^{(i)})^{2} &#x3D; \frac{1}{m}\sum_{i&#x3D;1}^{m}cost(\theta;x^i,j^i)$$</p><p>$$cost(\theta;x^i,j^i) &#x3D; \frac{1}{2}(h_{\theta}(x^{(i)})-y^{(i)})^{2}$$</p><p>利用每个样本的损失函数对$\theta$求偏导得到对应的梯度，来更新$\theta$：</p><p>$$\theta_{j} &#x3D; \theta_{j} + \alpha(y^i - h_{\theta}(x^i))x_{j}^{i}$$</p><p>随机梯度下降是通过每个样本来迭代更新一次，计算非常快并且适合线上更新模型。但是，SGD伴随的一个问题是噪音较BGD要多，使得SGD并不是每次迭代都向着整体最优化方向，在解空间的搜索过程看起来很盲目。其迭代的收敛曲线示意图可以表示如下：</p><img src="/images/optimizing-gradient-descent/1511084284.png"  title="" alt=""/>## 小批量梯度下降（Mini-Batch Gradient Descent, MBGD）<p>上述两种方法，BGD 样本多时，训练慢，占内存；SGD 找到的解却不如 BGD，并且干扰较大，不易于并行实现。而 MBGD 则是在二者之间找到一个平衡。它一次以小批量的训练数据计算目标函数的权重并更新参数。公式如下：</p><p>$$\theta &#x3D; \theta - \alpha\nabla_{\theta}{J(\theta;x^{(i:i+n)},j^{(i:i+n)})}$$</p><p>其中，n为每批训练集的数量，一般设为50到256。</p><p>虽然 MBGD 相对 BGD 和 SGD 更加优秀，但是仍然存在许多问题，比如：</p><ol><li>难以选择合适的学习速率：学习速率过小会造成网络收敛太慢，但是太大使得损失函数可能在最小点周围不断摇摆而永远达不到最小点；</li><li>可以随着训练降低学习率，虽然这个方法有一些作用，但是由于降低学习率的周期是人为事先设定的，所以它不能很好地适应数据内在的规律；</li><li>对特征向量中的所有的特征都采用了相同的学习率，如果训练数据十分稀疏并且不同特征的变化频率差别很大，这时候对变化频率慢得特征采用大的学习率而对变化频率快的特征采用小的学习率是更好的选择；</li><li>可能出现在垂直于优化方向上的摆动。</li></ol><p>因此我们需要更好的方法来进行梯度下降的求解。</p><h1 id="梯度下降优化算法"><a href="#梯度下降优化算法" class="headerlink" title="梯度下降优化算法"></a>梯度下降优化算法</h1><h2 id="Momentum"><a href="#Momentum" class="headerlink" title="Momentum"></a>Momentum</h2><p>对于如图所示的损失函数等高线图：<br><img src="/images/optimizing-gradient-descent/1511086529.jpg"  title="" alt=""/><br>其中中心的蓝色点表示了最优值。如图我们可以知道，其在 Y 轴比较陡峭，在 X 轴比较平缓。如果我们使用普通的梯度下降方法，如果选取较小的学习效率，则其收敛的图像如下面的第一张图。可以看出我们从某个点出发，整体趋势向着最优点前进，但是其在 Y 轴变化比较快，但是在 X 轴的前进非常缓慢。如果我们增大学习效率，则如第二张图，在 Y 轴抖动非常明显：</p><img src="/images/optimizing-gradient-descent/1511086899.jpg" width="400" title="较小学习速率" alt="较小学习速率"/><img src="/images/optimizing-gradient-descent/1511086921.jpg" width="400" title="较大学习速率" alt="较大学习速率"/>在梯度下降算法中，如果学习速率选择较大，对于陡峭方向（维度）的优化，会来回的抖动，但是如果学习速率选择较小，那么对于平缓方向（维度），会异常缓慢，非常像下图：<img src="/images/optimizing-gradient-descent/1511087223.jpg"  title="" alt=""/>即上面所说的第三条，如果不同维度选择的学习速率一样，可能导致陡峭维度的收敛图像来回摆动。因此很显然，我们应该尽可能的约束掉这样的维度的学习，而尽可能的往我们的目标方向前进。<h3 id="指数加权平均"><a href="#指数加权平均" class="headerlink" title="指数加权平均"></a>指数加权平均</h3><p>对于一组连续的数值，比如温度，可能变化（波动）较大，如下图的蓝色点。如果我们想要拟合温度，我们需要一个较为平缓的曲线（红色）。</p><img src="/images/optimizing-gradient-descent/1511087506.jpg" width="400" title="" alt=""/>如何求得这个平缓的曲线呢？我们应该想到，尽可能的平均一下前后的温度。指数平均加权是这样的思路，一个时刻的值，与上一个时刻有关（二者的加权平均）。即：$V_t = \beta V_{t-1} + (1 - \beta)\theta_t$，这里，$\theta_t$表示 t 时刻的测量值（即蓝色的点），$V_{t-1}$表示上一个时刻的计算值（即红色曲线的上一个值），$\theta$为加权参数。<h3 id="Momentum-梯度下降"><a href="#Momentum-梯度下降" class="headerlink" title="Momentum 梯度下降"></a>Momentum 梯度下降</h3><p>借助指数加权平均的思想，我们可以解决上面的抖动问题。即在SGD的基础上，加上了上一步的梯度：</p><p>$$v_t &#x3D; \gamma v_{t-1} + (1 - \gamma)\nabla_{\theta}{J(\theta)}$$<br>$$\theta &#x3D; \theta - \alpha v_t$$</p><p>其中$\gamma$通常设为0.9。由于目标函数在Y方向上摇摆，所以前后两次计算的梯度在Y方向上相反，所以相加后相互抵消，而X方向上梯度方向不变，所以X方向的梯度是累加的，其效果就是损失函数在Y方向上的震荡减小了，而更加迅速地从X方向接近最优点。如图所示：</p><img src="/images/optimizing-gradient-descent/1511088235.jpg" width="400" title="momentum梯度下降" alt="momentum梯度下降"/>也可以把这个过程和在山坡放一个球让其滚下类比：当从山顶释放一个小球时，由于重力的作用小球滚下的速度会越来越快；与此类似，冲量的作用会使相同方向的梯度不断累加，不同方向的梯度相互抵消，其效果就是逼近最优点的速度不断加快。<h2 id="Nesterov-Accelerated-Gradient"><a href="#Nesterov-Accelerated-Gradient" class="headerlink" title="Nesterov Accelerated Gradient"></a>Nesterov Accelerated Gradient</h2><p>但是上面，小球越来越快的往山谷滚动，越接近谷底越快，会导致冲过谷底。因此我们需要让小球感知坡度的变化，从而在它再次冲上山坡之前减速而避免错过山谷。NAG方法更新梯度的公式变为：</p><p>$$v_t &#x3D; \gamma v_{t-1} + (1 - \gamma)\nabla_{\theta}{J(\theta - \gamma v_{t-1})}$$<br>$$\theta &#x3D; \theta - \alpha v_t$$</p><p>即在 Momentum 的基础上，进行修正，达到减速的效果。</p><h2 id="Adagrad"><a href="#Adagrad" class="headerlink" title="Adagrad"></a>Adagrad</h2><p>我们除了想让参数更新速率自适应坡度外，还需要适合处理稀疏特征的梯度更新算法。比如，稀疏特征采用高的更新速率，其他特征采用相对较低的更新速率。Adagrad是一种适合处理稀疏特征的梯度更新算法，它对稀疏特征采用高的更新速率，而对其他特征采用相对较低的更新速率。<a href="http://blog.csdn.net/heyongluoyao8/article/details/52478715#reference_4">Dean</a>等人发现Adagrad能很好地提高SGD的鲁棒性，它已经被谷歌用来训练大规模的神经网络。</p><p>Adagrad对每个参数使用不同的参数进行更新。如果用$g_{t,i}$来表示参数$\theta_i$在第t次更新时的梯度，即$g_{t,i} &#x3D; \nabla_{\theta}{J(\theta_i)}$，则SGD的更新规则可以写作：</p><p>$$\theta_{t+1, i} &#x3D; \theta_i - \alpha g_{t,i}$$</p><p>而Adagrad的更新规则可以表示为：</p><p>$$\theta_{t+1, i} &#x3D; \theta_i - \frac{\alpha}{\sqrt{G_{t,ii} + \epsilon}} g_{t,i}$$</p><p>其中，$G_{t,ii}$是一个$\mathbb{R}^{d×d}$维的对角矩阵，其第i行第i列的元素为过去到当前第i个参数的梯度平方和，$\epsilon$是为了防止分母为0的平滑项。进一步，可以将上式向量化如下：</p><p>$$\theta_{t+1} &#x3D; \theta_i - \frac{\alpha}{\sqrt{G_{t} + \epsilon}} \bigodot g_{t}$$</p><p>这样，利用Adagrad就可以自动根据每个特征的稀疏性来设置不同的学习率。但是$G_t$累加了参数的历史梯度的平方，所以到后期学习率会越来越小，最后无法再学习到新的信息。</p><h2 id="Adadelta"><a href="#Adadelta" class="headerlink" title="Adadelta"></a>Adadelta</h2><p>Adadelta 和 Adagrad 的主要区别就是把 $G_t$变为$E[g^2]_t$，即不再累加参数所有的历史梯度平方和，转而设定一个窗口w，只求前w个历史梯度平方的平均数。而$E[g^2]<em>t &#x3D; \beta E[g^2]</em>{t-1} + (1-\beta)g_t^2$，因此Adadelta更新规则可以写作：</p><p>$$\theta_{t+1} &#x3D; \theta_i - \frac{\alpha}{\sqrt{E[g^2]<em>t + \epsilon}} \bigodot g</em>{t}$$</p><h2 id="RMSprop"><a href="#RMSprop" class="headerlink" title="RMSprop"></a>RMSprop</h2><p>RMSprop（Root Mean Square prop）由Hinton提出，实际上是Adadelta的一种特殊形式：</p><p>$$E[g^2]<em>t &#x3D; \beta E[g^2]</em>{t-1} + (1-\beta)g_t^2$$<br>$$\theta_{t+1} &#x3D; \theta_t - \frac{\alpha}{\sqrt{E[g^2]<em>t} + \epsilon} \bigodot g</em>{t}$$</p><h2 id="Adam"><a href="#Adam" class="headerlink" title="Adam"></a>Adam</h2><p>Adam的全称是Adaptive Moment Estimation, 它也是一种自适应学习率方法。可以把它看做 RMSprop 和 Momentum 的结合。</p><p>$$m_t &#x3D; \beta_1 m_{t - 1} + (1 - \beta_1)g_t$$<br>$$v_t &#x3D; \beta_2 v_{t - 1} + (1 - \beta_2)g_t^2$$</p><p>$m_t$,$v_t$分别是梯度的带权平均和带权有偏方差，由于当$\beta_1$,$\beta_2$接近于1时，这两项接近于0，因此对他们进行了偏差修正：</p><p>$$\hat{m_t} &#x3D; \frac{m_t}{1 - \beta_1^t}$$<br>$$\hat{v_t} &#x3D; \frac{v_t}{1 - \beta_2^t}$$</p><p>最终更新方程为：</p><p>$$\theta_{t+1} &#x3D; \theta_t - \frac{\alpha}{\sqrt{\hat{v_t}} + \epsilon} \bigodot m_{t}$$</p><p>一般将$\beta_1$设为0.9,$\beta_2$设为0.999, $\epsilon$设为10−8。一般在深度学习的梯度优化中，会使用 Adam。</p><h2 id="几种算法的效果对比"><a href="#几种算法的效果对比" class="headerlink" title="几种算法的效果对比"></a>几种算法的效果对比</h2><p>如图所示，所有方法都从相同位置出发，经历不同的路径到达了最小点，其中Adagrad、Adadelta和RMSprop一开始就朝向正确的方向并且迅速收敛，而冲量、NAG则会冲向错误的方向，但是由于NAG会向前多“看”一步所以能很快找到正确的方向。 </p><img src="/images/optimizing-gradient-descent/1511091783.jpg"  title="" alt=""/>下图显示了这些方法逃离鞍点的能力，鞍点有部分方向有正梯度另一些方向有负梯度，SGD方法逃离能力最差，冲量和NAG方法也不尽如人意，而Adagrad、RMSprop、Adadelta很快就能从鞍点逃离出来。<img src="/images/optimizing-gradient-descent/1511091841.jpg"  title="" alt=""/>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;梯度下降是一个最优化算法。在深度学习中，通过梯度下降来找到损失函数的（局部）最小值，进而获得各个参数的值。&lt;/p&gt;
&lt;p&gt;梯度下降最直观的解释如图所示，在山上某处，沿着最陡的方向向下，直到能到达的最低点。&lt;/p&gt;
&lt;img src=&quot;/images/optimizing-g</summary>
      
    
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/categories/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="调参" scheme="http://blog.liexing.me/tags/%E8%B0%83%E5%8F%82/"/>
    
    <category term="梯度下降" scheme="http://blog.liexing.me/tags/%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D/"/>
    
    <category term="Momentum" scheme="http://blog.liexing.me/tags/Momentum/"/>
    
    <category term="SGD" scheme="http://blog.liexing.me/tags/SGD/"/>
    
    <category term="BGD" scheme="http://blog.liexing.me/tags/BGD/"/>
    
    <category term="Adam" scheme="http://blog.liexing.me/tags/Adam/"/>
    
  </entry>
  
  <entry>
    <title>写给18岁的自己</title>
    <link href="http://blog.liexing.me/2017/11/10/a-letter-to-18/"/>
    <id>http://blog.liexing.me/2017/11/10/a-letter-to-18/</id>
    <published>2017-11-10T05:45:35.000Z</published>
    <updated>2023-01-20T15:04:29.748Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>九月中旬鹏哥给我打电话，让我给新入学的大一新生们写点东西，告诉他们我的大学四年是怎么样走过来的，然而国庆大东北去浪了，这件事情丢得一干二净。昨天鹏哥电话来催账了，才想起这件忘记得差不多的事情。</p></blockquote><blockquote><p>写给大一的新生，教他们怎么进入这个领域，如何去成长。教别人，这是一件大事。无论是工作，还是读研，亦或是技术上，有些人自己都 Low 得不行，却喜欢到处拼凑成一篇篇的“如何求职”或者“如何读研”的长文，装作一副过来人的样子到处分享。我不认为我有这个能力去“教”或者是告诉别人应该怎么做，这里只是一个心得，或者是对大学四年的一个总结吧，写个18岁的自己，各位看看就好。</p></blockquote><h1 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h1><p>写给18岁的自己，这是无数的科幻作品的题材吧。希望18岁刚进入大学的懵懂的自己，能在某个晴朗的下午，在景观水渠边漫步，在环形大道上闲逛之时，亦或是在图书馆某个能看到远处风景的角落，读到这封信。</p><p>时间的长河，其实就是一次次的选择过程。18岁的你，已经成年，虽然可以继续任性，但是做的每一个选择，都决定了之后的你，而你做的每一个选择，都基于你来的路，也即你做过的选择。</p><p>这些文字，不可避免的会带有说教的意味，但是这篇文章，毫无说教的目的，只是希望能给18岁的你一些现在的我的一些看法，亦或是现在的我觉得后悔当时没做好的一些东西，希望能够帮到你做得更好。</p><h1 id="接受"><a href="#接受" class="headerlink" title="接受"></a>接受</h1><p>从未想到，现在的我想告诉你的第一点会是这个。但是的确，学会接受，是通往社会的第一个桥梁。18岁的你，一定还是愤慨，为什么高考会如此糟糕，来到了这个之前十多年一直瞧不起的大学，恨不得掐死高考场上的自己。但是，愤慨又有什么用呢？各种班委选拔，学生会、社团的面试，明明觉得自己很优秀，可是却落选了，觉得面试自己的人眼瞎，觉得别人一定用了不知道什么样的非正常手段吧。但是，结果这样，你不爽又有什么用呢？明明自己认真的感情付出，但是却换不回预想的结果，觉得人心险恶。但是，你即使伤心，即使自暴自弃，又有什么用呢？自己认真学习，认真完成作业，但是最终成绩却不如别人玩些小把戏，你会觉得老师一定是傻X。但是你骂老师一顿又有什么用呢？</p><p>18岁了，你会发现，这个学校，这个社会，并不像自己从小长大的环境，这里的一些事，充满了各种不可抗力的因素。不管你是为此多么努力，奋斗是多么艰辛，结果却和付出不成正比。你可以自暴自弃，但是学会接受，以后你可以做得更好。和结果比起来，也许中间的过程，才是对你之后的人生更加重要的。</p><p>也许当年在一本忘了什么名字的书上看到的一句话可以帮助到你：一个人可以被毁灭，但是不能被打败。凡不能毁灭我的，将使我更强大。</p><h1 id="时间"><a href="#时间" class="headerlink" title="时间"></a>时间</h1><p>大学虽然只有四年，但是这四年决定了你今后的高度。如果挥霍掉了，之后你也许10年也补不回来。刚进入大学，脱离了家长老师的严加管教，也许你觉得尽情的玩游戏了，可以尽情的看小说了，可以随便出去玩了。也许室友每天很开心的玩，很让你心动，想和他们一起开黑一起吃鸡。你可以一起玩，但是控制好自己的时间，若干年后看着他们，你会觉得，幸好当年没和他们一样堕落下去。</p><h1 id="自驱动"><a href="#自驱动" class="headerlink" title="自驱动"></a>自驱动</h1><p>上面说的挥霍，是指把时间大量的耗费在玩上面。但是如果你把“玩”当做自己的职业，那即使是游戏，也可以定性为在学习，那么花时间在它身上，又何尝不可。</p><p>大学四年，是你能够连续性的把时间花费在学习上面的最宝贵的阶段。好好珍惜这段时间，系统性的把自己应该掌握的知识学好，这些系统性学习的东西，将会伴随你一生，成为你整个知识体系的主干。在之后的职业生涯中，你学会的各种其他知识、技能、经验，也只是这主干上的枝叶。干之不成，枝叶焉在？</p><p>18岁了，已经没有人在你耳边天天念叨好好学习了，也没有人来守着你学习了。成年的意义，在于自己知道自己应该做什么，应该如何做了。失去了身边的鞭策，你需要学会在心里给自己定一个标杆，一个人或者一个里程碑，不断的去完善自己，去鞭策自己，达到自己的目标。</p><h1 id="系统化"><a href="#系统化" class="headerlink" title="系统化"></a>系统化</h1><p>上面说到花时间学习，但是如果杂乱无章的学，往往会被一个个的散乱的知识点吞噬，事半功倍。你要学会系统的去整理知识点，找出一个个散乱知识点之间的联系，然后有针对的主动去积累学到的知识。而系统化，能够让你知道，什么是主干，什么是枝叶，主干和枝叶之间又是怎么联系起来的。</p><p>时间长河带来的是熵的增加，但是我们存在的意义却是不断积累系统化的知识体系，这是一件很有趣的对抗。18岁的你，应该开始在自己选定的领域里面，主动去积累系统化的知识，积累得越多，越系统，你能够更加透彻的看透这个领域中的迷雾，而你一辈子工作的本质，甚至是人生的本质，也是这个不断积累系统化知识的过程。</p><h1 id="兴趣点"><a href="#兴趣点" class="headerlink" title="兴趣点"></a>兴趣点</h1><p>即使选择了这个专业，也不意味着对这个专业涉及到的所有知识感兴趣。因此需要找到自己的兴趣点，把这个点深挖，做到自己能做的最好。当然这个兴趣点也行会随着时间的变化而变化，但是每到一个点，你应该真正的喜欢它，有一种执着的渴望去钻研，这样就会自然的有自驱动力。</p><blockquote><p>如果一个人靠着自制力去抵抗诱惑做某件事，那他还只是寻常人；如果他能够靠着内心对某事的执着追求去做这件事而抵御其他诱惑，这才是圣人</p></blockquote><h1 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h1><p>当你找到了兴趣点，那么你就应该有一个规划。我知道这对于你来说很难，但是你必须对自己有一个长线的规划，比如想要有个什么样的工作。有了这个长线的目标，那么你就能切分一下，比如这个工作，是否需要出国，或者这个工作是否需要读研，来制定一个中期的目标。并且不断的递归，能够制定精确到周或者月的目标更好。这样你就能更加明确的知道，现在应该做什么，做到什么程度了。比如你就能知道，你的更多的精力，是应该放在分数上，亦或是实践上。</p><h1 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h1><p>你选择的专业，是一个理工类的专业，其实更加偏重工程实践。死学知识，你也许可以做到考高分，完成自己诸如考研、保研、留学的目标，但是没有实际的实践能力，会严重制约你之后的工作。书上的东西，自己照着实践一遍，或者是一些小的完整的项目，自己尝试去完成一下。在实践的过程中，你会发现你对一些已经学到的知识，有了更加深刻的了解，亦或是发现一些自己尚不了解的知识，能够通过实践过程来学习。</p><h1 id="数学、算法、工程"><a href="#数学、算法、工程" class="headerlink" title="数学、算法、工程"></a>数学、算法、工程</h1><p>本来想告诉你，数学很重要。又想告诉你，算法很重要。想了下，工程也很重要。遂合并三者，一同告知。</p><p>数学是理工科的灵魂，也许你现在不知道学的乱七八糟的高数、线代、概统、离散等等数学有什么用，觉得认真学专业知识就够了。但是你要知道，你现在学习的专业知识，都是很基础的东西，当你以后涉及到一些更深层面的知识时，没有数学这把刀，没法在迷雾中抽丝剥茧，学到一些本质。</p><p>算法其实可以概括为数学，但是我觉得算法又不同于上面的数学。你可以理解为算法就是 ACM，就是刷各种 OJ。也许你觉得很无聊，但是在刷题的过程中，你对代码本身的理解，包括你的思维能力，都得到的提高。更加俗的来讲，算法能力可能决定了你在这个行业之后的工资水平。</p><p>工程能力，也可以理解为上面的实践能力。是你在代码世界的造物能力。工程能力需要多实践，多思考来培养，需要无数次的尝试，无数次的调试来历练。优秀的工程能力，能够让你的作品经受住各种外在需求的变更，负载起大规模的访问，能够一眼理出庞大系统逻辑。</p><h1 id="不能狂"><a href="#不能狂" class="headerlink" title="不能狂"></a>不能狂</h1><p>也许在某个阶段，你会觉得自己比别人牛逼了，觉得自己实力屌炸。但是记住，牛逼只是相对的，你要和比你弱的比较，那你又怎好意思狂；你要和比你强的比较，你又怎敢狂。不用和别人，和明天的你自己比起来，你也是菜逼，因为如果明天的你不比今天的你强，那你还不菜？</p><h1 id="朋友"><a href="#朋友" class="headerlink" title="朋友"></a>朋友</h1><p>分工和协作，是这个社会重要的两个特点。无论是同事之间，或者是朋友之间，都是一种协作的关系。尝试找到良师益友，能够在你疑惑、迷茫的时候帮助到你，你们能够相互激励，共同进步。也许很多年过后，即使你们相隔千里，但是朋友又永远在你身边。</p><h1 id="初心，底线"><a href="#初心，底线" class="headerlink" title="初心，底线"></a>初心，底线</h1><blockquote><p>世界上有两件事能深深震撼人们的心灵，一件是人们心中崇高的道德准则，另一件是我们头顶灿烂的星空。</p></blockquote><p>成长过程中，你会有许多讨厌的人，厌烦的行为。也许你会发现这些讨厌的行为，能够在短时间内给你带来一些小利小惠，但是永远坚守自己内心的道德底线，坚持做好自己。如菩萨初心，不与后心俱。</p><h1 id="精神"><a href="#精神" class="headerlink" title="精神"></a>精神</h1><p>在成长的途中，有的人会一味地追求着所谓的技术，所谓的知识，缺忽视了自己的内心。我想你应该不会这样，坚持读一些“杂乱”的书挺好的，扩充自己的眼界，学习别人的思想。但是也不要对某些领域有抵触，比如你现在讨厌的历史，在今天的我看来，也挺有趣。你也可以看看电影，<a href="http://www.imdb.com/chart/top">IMDB Top 250</a>就不错，电影往往传达出一种思想体系，从这种思想体系中，能更快的了解一种人生。</p><h1 id="爱情"><a href="#爱情" class="headerlink" title="爱情"></a>爱情</h1><p>大学，有很多人心目中纯洁爱情。可以有一份爱情，能够伴着你，使你成为你，但是有些时候也会给你搞一些事。“爱是一种甜蜜的痛苦，真诚的爱情永不是走一条平坦的道路的。”如果你发现谁让你内心涌动，不用去压抑着自己的内心。但是也不要因为想要爱情而去爱情，这样只会伤害自己。会有属于你的爱情，在之后的路上等着你的，耐心的去迎接它吧。</p><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1><p>啰里啰嗦不小心说了这么一大堆，有太多的话想告诉你，大学四年的种种事，种种经历，都想一股脑的给你塞过来。</p><p>四年很快，谢谢当年的自己成长的如此迅速，你一定想象不到这四年在江安经历的岁月，一定惊讶毕业时候的自己如此超出自己的预期。江安的日夜，明远湖的宁静，二基楼的深邃，这四年的岁月将会助你踏上未来的征程——星辰大海。加油吧！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;九月中旬鹏哥给我打电话，让我给新入学的大一新生们写点东西，告诉他们我的大学四年是怎么样走过来的，然而国庆大东北去浪了，这件事情丢得一干二净。昨天鹏哥电话来催账了，才想起这件忘记得差不多的事情。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquo</summary>
      
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="杂" scheme="http://blog.liexing.me/tags/%E6%9D%82/"/>
    
  </entry>
  
  <entry>
    <title>调参工程学 - Weight Initialization</title>
    <link href="http://blog.liexing.me/2017/10/24/deep-learning-weight-initialization/"/>
    <id>http://blog.liexing.me/2017/10/24/deep-learning-weight-initialization/</id>
    <published>2017-10-24T13:14:12.000Z</published>
    <updated>2023-01-20T15:04:29.744Z</updated>
    
    <content type="html"><![CDATA[<p>调参对深度学习的效果异常重要，甚至经常开玩笑说这是一门调参工程学。而 Weight Initialization 对模型收敛速度和模型质量有重要影响。</p><img src="/images/deep-learning-weight-initialization/1508852329.jpeg" width="500" title="Deep-Learning" alt="Deep-Learning"/># 深度学习的参数<p>深度学习的参数分为超参（hyper parameters）和普通参数。超参是模型开始训练前，人工指定的参数，比如网络的层数、每层的神经元数、学习速率以及正则项系数等。超参对模型的效果非常重要。而普通参数，就是通常的 W 以及 b。深度学习模型的本质过程就是对权重（W）进行更新。而在开始训练神经网络前，需要初始化 W 以及 b 的值，这个初始值会影响模型训练的收敛速度以及质量。本文主要讲解 W 的初始化。</p><h1 id="初始化为0"><a href="#初始化为0" class="headerlink" title="初始化为0"></a>初始化为0</h1><p>如图所示一个简单的神经网络，有一个中间层，中间层有两个神经元。</p><img src="/images/deep-learning-weight-initialization/1508853389.jpg" width="600" title="" alt=""/>如果我们初始化权重： $W^{[1]}=\begin{bmatrix} 0&0\\\ 0&0 \end{bmatrix}$或者任意上下神经元权重相同，由于对称性，通过激活函数得到的值相同，并且通过梯度下降，更新后的权重也相同。因此无论进行多少次迭代，二者的权重依然保持不变，这种情况下，多个隐藏单元就会没有意义。<img src="/images/deep-learning-weight-initialization/1508854477.jpg" width="600" title="初始化为0的损失" alt="初始化为0的损失"/>上图为一个初始化权重为0的神经网络的损失函数值变化，可以看出，损失值并没有变化。通常来说，将权重都设置为0，意味着每层网络中的神经元都一样，等同于每层只有一个神经元，其效果并不会比线性分类器（如逻辑回归）好。<h1 id="随机初始化"><a href="#随机初始化" class="headerlink" title="随机初始化"></a>随机初始化</h1><p>如果我们将权重初始化为随机数，比如：<code>np.random.randn(layers_dims[l], layers_dims[l - 1]) * 0.01</code></p><img src="/images/deep-learning-weight-initialization/1508855349.jpg" width="600" title="随机初始化的损失" alt="随机初始化的损失"/>可以看到，随着训练，损失值逐渐变小，但是最终的损失值依然比较高。但是随着层数的增加，会导致梯度消失。如果增大随机初始化的值，比如将0.01变为10，会发现依然会出现梯度消失，参数难以被继续更新。<h1 id="Xavier-initialization"><a href="#Xavier-initialization" class="headerlink" title="Xavier initialization"></a>Xavier initialization</h1><p>上述问题可以使用Xavier initialization 解决。Xavier 初始化的基本思想是保持输入和输出的方差一致。即将随机初始化的值乘以缩放因子：$\sqrt{\frac{1}{layers_dims[l-1]}}$。也就是将参数初始化变为：<code>np.random.randn(layers_dims[l], layers_dims[l - 1]) * np.sqrt(1. / layers_dims[l - 1])</code></p><p>Xavier初始化是在线性函数上推导得出的，它能够保持输出在很多层之后依然有着良好的分布，如图为使用 tanh 激活函数后的输出概率分布：<br><img src="/images/deep-learning-weight-initialization/1508856194.jpeg" width="600" title="Xavier初始化在tanh的输出概率分布" alt="Xavier初始化在tanh的输出概率分布"/><br>但是其对于 ReLU 的效果并不好，如图：<br><img src="/images/deep-learning-weight-initialization/1508856336.jpeg" width="600" title="Xavier初始化在ReLU的输出概率分布" alt="Xavier初始化在ReLU的输出概率分布"/></p><h1 id="He-initialization"><a href="#He-initialization" class="headerlink" title="He initialization"></a>He initialization</h1><p>He 初始化可以解决上面在 ReLU 激活函数时 Xavier 效果不好的问题。其思想是：在ReLU网络中，假定每一层有一半的神经元被激活，另一半为0，所以，要保持方差不变，只需要在 Xavier 的基础上再除以2。即缩放因子变为：$\sqrt{\frac{2}{layers_dims[l-1]}}$，初始化代码为：<code>np.random.randn(layers_dims[l], layers_dims[l - 1]) * np.sqrt(2. / layers_dims[l - 1])</code></p><p>其分布如下图，可见效果很好。<br><img src="/images/deep-learning-weight-initialization/1508856493.jpeg" width="600" title="He初始化在ReLU的输出概率分布" alt="He初始化在ReLU的输出概率分布"/><br>其损失如图所示：<br><img src="/images/deep-learning-weight-initialization/1508856662.jpg" width="600" title="He初始化的损失" alt="He初始化的损失"/></p><h1 id="Batch-Normalization-Layer"><a href="#Batch-Normalization-Layer" class="headerlink" title="Batch Normalization Layer"></a>Batch Normalization Layer</h1><p>If you want it, just make it! 合理的参数初始化是为了避免梯度消失，有效的反向传播，需要进入激活函数的数值有一个合理的分布，以便于反向传播时计算梯度。其思想就是在线性变化和非线性激活函数之间，将数值做一次高斯归一化和线性变化。<br><img src="/images/deep-learning-weight-initialization/1508856932.jpeg" width="600" title="He初始化的损失" alt="He初始化的损失"/><br>Batch Normalization中所有的操作都是平滑可导，因此可以有效的学习到参数$\gamma$，$\beta$。需要注意的是，训练时的$\gamma$，$\beta$由当前batch计算得出，而测试时$\gamma$，$\beta$使用训练时保存的均值。</p><p>如图表示使用随机初始化的参数，ReLU 作为激活函数，未使用 Batch Normalization 时，每层激活函数的输出分布：</p><img src="/images/deep-learning-weight-initialization/1508857325.jpeg" width="600" title="未使用BatchNormalization" alt="未使用BatchNormalization"/>下图为使用 Batch Normalization 时，每层激活函数的输出分布：<img src="/images/deep-learning-weight-initialization/1508856493.jpeg" width="600" title="使用BatchNormalization" alt="使用BatchNormalization"/>可见，使用 Batch Normalization 非常有效。<p>#参考资料</p><ol><li>Glorot X, Bengio Y. Understanding the difficulty of training deep feedforward neural networks[C]&#x2F;&#x2F;Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics. 2010: 249-256.</li><li>He K, Zhang X, Ren S, et al. Delving deep into rectifiers: Surpassing human-level performance on imagenet classification[C]&#x2F;&#x2F;Proceedings of the IEEE international conference on computer vision. 2015: 1026-1034.</li><li>Ioffe S, Szegedy C. Batch normalization: accelerating deep network training by reducing internal covariate shift (2015)[J]. arXiv preprint arXiv:1502.03167, 1735-1780.</li><li>Coursera, deep-neural-network</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;调参对深度学习的效果异常重要，甚至经常开玩笑说这是一门调参工程学。而 Weight Initialization 对模型收敛速度和模型质量有重要影响。&lt;/p&gt;
&lt;img src=&quot;/images/deep-learning-weight-initialization/150</summary>
      
    
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/categories/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="深度学习" scheme="http://blog.liexing.me/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="调参" scheme="http://blog.liexing.me/tags/%E8%B0%83%E5%8F%82/"/>
    
    <category term="Weight initialization" scheme="http://blog.liexing.me/tags/Weight-initialization/"/>
    
  </entry>
  
  <entry>
    <title>Java 异常表与异常处理原理</title>
    <link href="http://blog.liexing.me/2017/09/17/java-exception-table/"/>
    <id>http://blog.liexing.me/2017/09/17/java-exception-table/</id>
    <published>2017-09-17T07:12:11.000Z</published>
    <updated>2023-01-20T15:04:29.742Z</updated>
    
    <content type="html"><![CDATA[<p>Java 在代码中通过使用 <code>try&#123;&#125;catch()&#123;&#125;finally&#123;&#125;</code> 块来对异常进行捕获或者处理。但是对于 JVM 来说，是如何处理 try&#x2F;catch 代码块与异常的呢。</p><p>实际上 Java 编译后，会在代码后附加异常表的形式来实现 Java 的异常处理及 finally 机制（在 JDK1.4.2之前，javac 编译器使用 jsr 和 ret 指令来实现 finally 语句，但是1.4.2之后自动在每段可能的分支路径后将 finally 语句块内容冗余生成一遍来实现。JDK1.7及之后版本，则完全禁止在 Class 文件中使用 jsr 和 ret 指令）。</p><h2 id="异常表"><a href="#异常表" class="headerlink" title="异常表"></a>异常表</h2><p>属性表（attribute_info）可以存在于 Class 文件、字段表、方法表中，用于描述某些场景的专有信息。属性表中有个 Code 属性，该属性在方法表中使用，Java 程序方法体中的代码被编译成的字节码指令存储在 Code 属性中。而异常表（exception_table）则是存储在 Code 属性表中的一个结构，这个结构是可选的。</p><h2 id="异常表结构"><a href="#异常表结构" class="headerlink" title="异常表结构"></a>异常表结构</h2><p>异常表结构如下表所示。它包含四个字段：如果当字节码在第 start_pc 行到 end_pc 行之间（即[start_pc, end_pc)）出现了类型为 catch_type 或者其子类的异常（catch_type 为指向一个 CONSTANT_Class_info 型常量的索引），则跳转到第 handler_pc 行执行。如果 catch_type 为0，表示任意异常情况都需要转到 handler_pc 处进行处理。</p><table><thead><tr><th>类型</th><th>名称</th><th>数量</th></tr></thead><tbody><tr><td>u2</td><td>start_pc</td><td>1</td></tr><tr><td>u2</td><td>end_pc</td><td>1</td></tr><tr><td>u2</td><td>handler_pc</td><td>1</td></tr><tr><td>u2</td><td>catch_type</td><td>1</td></tr></tbody></table><h2 id="处理异常机制"><a href="#处理异常机制" class="headerlink" title="处理异常机制"></a>处理异常机制</h2><p>如上面所说，每个类编译后，都会跟随一个异常表，如果发生异常，首先在异常表中查找对应的行（即代码中相应的 <code>try&#123;&#125;catch()&#123;&#125;</code> 代码块），如果找到，则跳转到异常处理代码执行，如果没有找到，则返回（执行 finally 之后），并 copy 异常的应用给父调用者，接着查询父调用的异常表，以此类推。</p><h2 id="异常处理实例"><a href="#异常处理实例" class="headerlink" title="异常处理实例"></a>异常处理实例</h2><p>对于 Java 源码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">inc</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">int</span> x;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        x = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        x = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        x = <span class="number">3</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将其编译为 ByteCode 字节码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">public int inc();</span><br><span class="line">  Code:</span><br><span class="line">    Stack=1, Locals=5, Args_size=1</span><br><span class="line">    0:iconst_1//try 中的x=1</span><br><span class="line">    1:istore_1</span><br><span class="line">    2:iload_1//保存 x 到 returnValue 中</span><br><span class="line">    3:istore4</span><br><span class="line">    5:iconst_3//finally 中的 x=3</span><br><span class="line">    6:istore_1</span><br><span class="line">    7: iload4//将 returnValue 中的值放到栈顶，准备给 ireturn 返回</span><br><span class="line">    9:ireturn</span><br><span class="line">    10:astore_2//给 catch 中的 Exception e 赋值，存储在 Slot 2 中</span><br><span class="line">    11:iconst_2//catch 中的 x=2</span><br><span class="line">    12:istore_1</span><br><span class="line">    13:iload_1//保存 x 到 returnValue 中，此时 x=2</span><br><span class="line">    14:istore4</span><br><span class="line">    16:iconst_3//finally 中的 x=3</span><br><span class="line">    17:istore_1</span><br><span class="line">    18:iload4//将 returnValue 中的值放到栈顶，准备给 ireturn 返回</span><br><span class="line">    20:ireturn</span><br><span class="line">    21:astore_3//如果出现了不属于 java.lang.Exception 及其子类中的异常则到这里</span><br><span class="line">    22:iconst_3//finally 中的 x=3</span><br><span class="line">    23:istore_1</span><br><span class="line">    24:aload_3//将异常放置到栈顶，并抛出</span><br><span class="line">    25:athrow</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">  Exception table:</span><br><span class="line">    fromtotargettype</span><br><span class="line">    0510Class java/lang/Exception</span><br><span class="line">    0521any</span><br><span class="line">    101621any</span><br></pre></td></tr></table></figure><p>首先可以看到，对于 finally，编译器将每个可能出现的分支后都放置了冗余。并且编译器生成了三个异常表记录，从 Java 代码的语义上讲，执行路径分别为：</p><blockquote><ol><li>如果 try 语句块中出现了属于 Exception 及其子类的异常，则跳转到 catch 处理；</li><li>如果 try 语句块中出现了不属于 Exception 及其子类的异常，则跳转到 finally 处理；</li><li>如果 catch 语句块中出现了任何异常，则跳转到 finally 处理。</li></ol></blockquote><p>由此可以分析此段代码可能的返回结果：</p><blockquote><ol><li>如果没有出现异常，返回1；</li><li>如果出现 Exception 异常，返回2；</li><li>如果出现了 Exception 意外的异常，非正常退出，没有返回；</li></ol></blockquote><p>我们来分析字节码：</p><p>首先，0-4行，就是把整数1赋值给 x，并且将此时 x 的值复制一个副本到本地变量表的 Slot 中（即 returnValue），这个 Slot 里面的值在 ireturn 指令执行前会被重新读到栈顶，作为返回值。这是如果没有异常，则执行5-9行，把 x 赋值为3，然后返回 returnValue 中保存的1，方法结束。如果出现异常，读取异常表发现应该执行第10行，pc 寄存器指针转向10行，10-20行就是把2赋值给 x，然后把 x 赋值给 returnValue，再将 x 赋值为3，然后将 returnValue 中的2读到操作栈顶返回。第21行开始是把 x 赋值为3并且将栈顶的异常抛出，方法结束。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Java 在代码中通过使用 &lt;code&gt;try&amp;#123;&amp;#125;catch()&amp;#123;&amp;#125;finally&amp;#123;&amp;#125;&lt;/code&gt; 块来对异常进行捕获或者处理。但是对于 JVM 来说，是如何处理 try&amp;#x2F;catch 代码块与异常的呢。</summary>
      
    
    
    
    <category term="Java" scheme="http://blog.liexing.me/categories/Java/"/>
    
    
    <category term="JVM" scheme="http://blog.liexing.me/tags/JVM/"/>
    
    <category term="异常处理" scheme="http://blog.liexing.me/tags/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/"/>
    
    <category term="异常表" scheme="http://blog.liexing.me/tags/%E5%BC%82%E5%B8%B8%E8%A1%A8/"/>
    
  </entry>
  
  <entry>
    <title>macOS 开发配置</title>
    <link href="http://blog.liexing.me/2017/08/11/macos-dev-setup/"/>
    <id>http://blog.liexing.me/2017/08/11/macos-dev-setup/</id>
    <published>2017-08-11T15:49:22.000Z</published>
    <updated>2023-01-20T15:04:29.746Z</updated>
    
    <content type="html"><![CDATA[<p>某人心心念念的rmbp终于到手了，然而我还在杭州没法帮忙各种配置，这里就写一份教程好了，自己拿去照着撸吧（￣︶￣）↗ </p><p>ps. 毕竟你的是高大上的17带bar rmbp。。。然而，bar的操作我都！不！知！道！</p><p>大概介绍 macOS 的一些基本操作以及常见的软件、开发环境的搭建。</p><h2 id="基本操作介绍"><a href="#基本操作介绍" class="headerlink" title="基本操作介绍"></a>基本操作介绍</h2><ol><li><p>17的触摸板应该是二段的按压，意思就是分为轻按和重按，按到底会响两次，轻按表示单击，重按表示双击。但是也可以配置成双指按表示双击；</p></li><li><p>键盘键位，Mac的键位和Windows的不太一样，没有<code>Win</code>那个图标，取而代之的是<code>command</code>（长这样：<code>⌘</code>）但是功能差不多；<code>alt</code>又叫做<code>option</code>（一些地方画成<code>⌥</code>）；退格、<code>del</code>合并为<code>delete</code>；<code>Shift</code>画成<code>⇧</code>，<code>control</code>画成<code>⌃</code>，大小写锁定画成<code>⇪</code></p></li><li><p>一些常用的快捷键：</p><blockquote><ol><li>截屏，默认的截屏快捷键有两种：<code>⌘+shift+control+4</code>（区域截图到剪切板），<code>⌘+shift+control+3</code>（全屏截图到剪切板），<code>⌘+shift+4</code>（区域截图到桌面），<code>⌘+shift+3</code>（全屏截图到桌面）</li><li>复制：<code>⌘+c</code>，粘贴：<code>⌘+v</code>，全选：<code>⌘+a</code></li><li>输入法切换：<code>control+空格</code>（选择上一个输入法），<code>control+option+空格</code>（选择下一个输入法）</li><li>其他自己看<a href="https://support.apple.com/zh-cn/HT201236">官方的文档</a></li></ol></blockquote></li></ol><h2 id="系统基本配置"><a href="#系统基本配置" class="headerlink" title="系统基本配置"></a>系统基本配置</h2><ol><li><p>开机首先是各种基本的配置，什么语言呐区域呐用户名呐乱七八糟就不说啦。</p></li><li><p>首先是调整下分辨率。进入系统后，点击桌面左上角的，点击 系统偏好设置-显示器，这里把分辨率调了吧，默认的太太太大了，不仅看着难受，而且因为大，所以显示的东西太少了，怎么愉快的撸代码。</p></li><li><p>设置窗口的左上方，有后退前进的按钮，点击后退可以退回主菜单。</p></li><li><p>其次，在触摸板中把全部的东西都打开吧，rmbp最优秀的就是那个触摸板了。而且17款这么大的触摸板，好好用~ps.建议把“轻点来点按”打开，就不用每次都按下去了。ps. 每一个选项右边都有图示，可以看看。</p></li><li><p>然后打开一些安全设置，不然装不上第三方的软件。找到“安全性与隐私”，点击左下角的🔐并输入密码，以允许后面的修改。然后“允许从以下位置下载的应用”中选择“任何来源”。</p></li><li><p>接着在系统设置的辅助功能中，左边一列选中“鼠标与触控板”，右边点击“触控板选项…”，然后打开启用拖拽-“三指拖拽”，这样，鼠标放在窗口的标题栏，三个手指就能拖动窗口了，非常方便。</p></li><li><p>iCloud 中，登录 iCloud 账户并且开启“查找我的 Mac”选项。</p></li><li><p>Docker（类似 Windows 底部的菜单栏） 配置。建议小一些，放左边或者右边，并且自动隐藏。</p></li><li><p>接着配置网络，网络选项在桌面上面一栏，一个WiFi图标，点击可以选择网络。</p></li><li><p>其他设置：点击电池图标，勾选显示百分比；时间显示日期、星期等</p></li></ol><h2 id="系统基本介绍"><a href="#系统基本介绍" class="headerlink" title="系统基本介绍"></a>系统基本介绍</h2><h3 id="一些介绍"><a href="#一些介绍" class="headerlink" title="一些介绍"></a>一些介绍</h3><ol><li><p>首先 macOS 是基于 Unix 的发行版，所以文件结构、命令上，都和 *nix 系统非常像（但是非常多坑）。</p></li><li><p>点击 Dock 上的 Finder（即 macOS 的资源管理器），可以看到左边有一栏，里面有个应用程序（&#x2F;Applications），应用程序放在这里的。</p></li></ol><img src="/images/macos-dev-setup/1502459911.jpg"  title="" alt=""/>然后 macOS 中，用户目录是`/Users/username`，而不是`/home/username`，这个需要注意。激活 Finder 的情况下，屏幕左上角会有一排菜单，这个菜单针对每个软件可能不同。一般软件的设置都是`软件名-偏好设置`中，或者使用`⌘+,`快捷键打开软件的偏好设置。在显示中是一些显示的选项，对于 Finder 可以打开显示标签页、路径、状态等<ol start="3"><li>如果前面触摸板中打开了 Launchpad 的手势，捏拢拇指与其他三个手指即可打开 Launchpad，或者点击 Dock 中的小火箭图标，则能看到所有安装的应用程序，可以两个手指轻扫翻页。</li></ol><img src="/images/macos-dev-setup/1502460257.jpg"  title="" alt=""/>### 安装软件1. App Store 下载。App Store 中直接下载即可；2. 第三方下载安装。下载下来的软件一般是 dmg 格式的或者无格式（解压后）的。dmg 和 exe 类似，打开后，里面一般有个软件图标和一个 Application 文件夹，直接把软件图标拖到 Application 即可。或者直接把下载的无格式（如果是压缩文件，打开即可解压）拖到 Application 即可。<h2 id="软件"><a href="#软件" class="headerlink" title="软件"></a>软件</h2><h3 id="iTerm2"><a href="#iTerm2" class="headerlink" title="iTerm2"></a>iTerm2</h3><p> Mac 上传说中最强大的终端工具，主页戳<a href="https://www.iterm2.com/">这里</a>。建议把 iTerm2 固定到 Docker，方便打开，双指点按 iTerm 图标-选项-在 Docker 中固定。</p><h3 id="Homebrew"><a href="#Homebrew" class="headerlink" title="Homebrew"></a>Homebrew</h3><p>一个包管理工具，在终端（iTerm）中输入下面语句即可安装。其官网为：<a href="https://brew.sh/">点这里</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/usr/bin/ruby -e <span class="string">&quot;<span class="subst">$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>brew upgrade</code>更新软件包，然后<code>brew install 软件名</code>即可安装一些软件。</p><h4 id="Cask"><a href="#Cask" class="headerlink" title="Cask"></a>Cask</h4><p><a href="https://caskroom.github.io/">Homebrew-Cask</a>是 Homebrew 的扩展，用以安装其他一些软件，可以用于安装其他一些软件以前需要使用<code>brew cask</code>来使用 Cask，现在直接使用 Cask 的 tap 即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">brew install caskroom/cask/google-chrome <span class="comment">#安装chrome</span></span><br><span class="line">brew install caskroom/cask/java <span class="comment"># 安装java</span></span><br><span class="line">brew install caskroom/cask/visual-studio-code<span class="comment">#安装 vs code</span></span><br><span class="line">brew install caskroom/cask/iina<span class="comment">#安装iina，一个非常棒的视频播放器</span></span><br><span class="line">brew install maven <span class="comment">#安装maven</span></span><br><span class="line">brew install you-get youtube-dl <span class="comment">#安装两个视频下载工具</span></span><br><span class="line">brew install vim <span class="comment">#用 brew 管理 vim，能升级到更新版</span></span><br></pre></td></tr></table></figure><h3 id="fish"><a href="#fish" class="headerlink" title="fish"></a>fish</h3><p>90后用的 shell~其他常用的还有 zsh。使用 brew 安装即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install fish</span><br></pre></td></tr></table></figure><p>然后切换默认的 shell 为 fish：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">chsh -s /usr/local/bin/fish</span><br></pre></td></tr></table></figure><p>安装好后，还有个方便的 fish 管理工具：<a href="http://git.io/oh-my-fish">oh my fish</a>，安装命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -L https://get.oh-my.fish | fish</span><br></pre></td></tr></table></figure><p>然后可以选择<a href="https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md">主题</a>，我用的 ocean，这么安装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">omf install ocean</span><br><span class="line">omf theme ocean</span><br></pre></td></tr></table></figure><p>当然也可以安装其他主题，去上面那个链接选选。。安装好后长这样：</p><img src="/images/macos-dev-setup/1502463526.png"  title="Ocean主题" alt="Ocean主题"/><h3 id="ShadowsocksX"><a href="#ShadowsocksX" class="headerlink" title="ShadowsocksX"></a>ShadowsocksX</h3><p><a href="https://github.com/shadowsocks/ShadowsocksX-NG/releases">ShadowsocksX</a>这个就不多说了。。。</p><h3 id="Karabiner-Elements"><a href="#Karabiner-Elements" class="headerlink" title="Karabiner-Elements"></a>Karabiner-Elements</h3><p><a href="https://github.com/tekezo/Karabiner-Elements">Karabiner-Elements</a> 是一个修改键位的软件，建议用它把 <code>caps lock</code>和<code>control</code>位置替换。因为大小写锁定占据了更好的位置，而 <code>control</code> 是更加频繁使用的一个按键，调换后更方便。</p><img src="/images/macos-dev-setup/1502469911.jpg"  title="Karabiner-Elements" alt="Karabiner-Elements"/><h3 id="其他软件"><a href="#其他软件" class="headerlink" title="其他软件"></a>其他软件</h3><p><a href="https://www.jetbrains.com/idea/download/">IntelliJ IDEA</a>(不过强烈建议通过<a href="https://www.jetbrains.com/toolbox/app/?fromMenu">JetBrains Toolbox</a> 安装)、<br><a href="https://go.microsoft.com/fwlink/p/?LinkID=511647">Office 365</a>、<br><a href="http://pinyin.sogou.com/mac/">搜狗输入法</a>、<br><a href="http://mac.xunlei.com/">迅雷</a>、<br><a href="https://www.alfredapp.com/">Alfred</a>（<a href="http://www.alfredworkflow.com/">这里</a>有一堆工作流工具）、<br><a href="http://bjango.com/">iStat</a>、<br><a href="http://mac.guanjia.qq.com/">腾讯电脑管家</a>、<br><a href="http://im.qq.com/macqq/">QQ</a>、<br><a href="https://mac.weixin.qq.com/">微信</a></p><h2 id="其他参考"><a href="#其他参考" class="headerlink" title="其他参考"></a>其他参考</h2><ol><li><a href="https://github.com/hzlzh/Best-App">收集&amp;推荐优秀的 Apps&#x2F;硬件&#x2F;技巧&#x2F;周边等</a></li><li><a href="https://wsgzao.github.io/post/mac/">Mac新手入门以及常用软件推荐</a></li><li><a href="http://xialeizhou.com/2019/06/23/%E9%AB%98%E6%95%88macbook%E5%B7%A5%E4%BD%9C%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/">高效macbook工作环境配置</a></li><li><a href="https://www.gitbook.com/book/aaaaaashu/mac-dev-setup/details">Mac 开发配置手册</a></li></ol><p>自己折腾去吧~╰(￣▽￣)╭</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;某人心心念念的rmbp终于到手了，然而我还在杭州没法帮忙各种配置，这里就写一份教程好了，自己拿去照着撸吧（￣︶￣）↗ &lt;/p&gt;
&lt;p&gt;ps. 毕竟你的是高大上的17带bar rmbp。。。然而，bar的操作我都！不！知！道！&lt;/p&gt;
&lt;p&gt;大概介绍 macOS 的一些基本操</summary>
      
    
    
    
    <category term="杂" scheme="http://blog.liexing.me/categories/%E6%9D%82/"/>
    
    
    <category term="macOS" scheme="http://blog.liexing.me/tags/macOS/"/>
    
    <category term="配置" scheme="http://blog.liexing.me/tags/%E9%85%8D%E7%BD%AE/"/>
    
  </entry>
  
  <entry>
    <title>Java 内存模型</title>
    <link href="http://blog.liexing.me/2017/07/31/java-memory-model/"/>
    <id>http://blog.liexing.me/2017/07/31/java-memory-model/</id>
    <published>2017-07-31T15:15:12.000Z</published>
    <updated>2023-01-20T15:04:29.745Z</updated>
    
    <content type="html"><![CDATA[<p>Java内存模型，即 Java Memory Model（JMM），定义了 Java 虚拟机在计算机内存的工作方式。现在的 Java 内存模型主要源于1.5版本。</p><h1 id="缓存与一致性"><a href="#缓存与一致性" class="headerlink" title="缓存与一致性"></a>缓存与一致性</h1><p>在计算机中，不同硬件的处理速度不同，往往有几个数量级的差距。比如 CPU 的处理速度往往高于内存数个数量级。因此计算机体系结构中引入了告诉缓存（Cache）放在内存和处理器之间作为缓冲。将 CPU 可能将要访问的数据先放在缓存中，因为访问缓存的速度远远高于直接访问内存，因此能加速运算。</p><img src="/images/java-memory-model/1501516073.png"  title="高速缓存" alt="高速缓存"/>虽然高速缓存解决了 CPU 和内存之间的速度问题，但是引入了另一个问题：缓存一致性。在多处理系统中，每一个处理器都有自己独立的缓存，但是整个系统共享一个内存，因此需要保证每个 CPU 读写的数据一致。常见的一致性协议有 MSI、MESI、MOSI以及 Dragon Protocol 等。<p> JMM 中定义的内存访问操作与计算机的高速缓存访问是类似的。</p><h1 id="JMM"><a href="#JMM" class="headerlink" title="JMM"></a>JMM</h1><p> JMM 定义了程序中各个变量的访问规则。JMM 规定，所有的变量都存储在主内存中，但是每个线程还会有自己的工作内存，其中保存该线程使用到的变量副本，并且线程对内存的操作必须在工作线程中进行，不能直接访问主内存变量。如图所示：</p><img src="/images/java-memory-model/1501516897.png"  title="JMM内存模型" alt="JMM内存模型"/> 在命令式编程中，线程之间的通信机制有两种：共享内存和消息传递。在共享内存的模型中，线程之间通过共享内存中的一个公共状态来进行通信。在消息传递模型中，通过显示的发送消息来通信。Java线程之间的通信采用的是过共享内存模型，即 JMM。<p>线程之间的通信必须要经历将要发送的消息通过共享变量的方式写入主内存，再被其他线程读取的过程。</p><h2 id="内存间交互操作"><a href="#内存间交互操作" class="headerlink" title="内存间交互操作"></a>内存间交互操作</h2><p>为了保证其他线程读取的是最新写入的数据，因此 Java 内存模型定义了如下几种操作：</p><ol><li>lock: 作用于主内存的变量，把一个变量标识为一条线程独占状态；</li><li>unlock: 作用于主内存的变量，解锁占用状态，允许被其他线程锁定；</li><li>read: 作用于主内存变量，把一个变量值从主内存传输到线程的工作内存中，以便随后的load动作使用；</li><li>load: 作用于工作内存的变量，把 read 操作从主内存中得到的变量值放入工作内存的变量副本中；</li><li>use: 作用于工作内存的变量，把工作内存中的变量传递给虚拟机执行引擎，用于执行指令；</li><li>assign: 作用于工作内存的变量，把执行引擎的值赋予工作内存变量；</li><li>store: 作用于工作内存的变量，把工作内存的一个变量传递给主内存，以便随后的 write 的操作；</li><li>write: 作用于主内存的变量，把 store 操作从工作内存中一个变量的值传送到主内存的变量中。</li></ol><h2 id="Happens-before"><a href="#Happens-before" class="headerlink" title="Happens-before"></a>Happens-before</h2><p>从 jdk5 开始，java 使用新的 JSR-133 内存模型，基于 happens-before 的概念来阐述操作之间的内存可见性。<br>如果要把一个变量从主内存中复制到工作内存，就需要按顺寻地执行 read 和 load 操作，如果把变量从工作内存中同步回主内存中，就要按顺序地执行 store 和 write 操作。Java 内存模型只要求上述操作必须按顺序执行，而没有保证必须是连续执行。也就是 read 和 load 之间，store 和 write 之间是可以插入其他指令的。Java内存模型还规定了在执行上述八种基本操作时，必须满足如下规则：</p><ul><li>不允许 read 和 load、store 和 write 操作之一单独出现</li><li>不允许一个线程丢弃它的最近 assign 的操作，即变量在工作内存中改变了之后必须同步到主内存中。</li><li>不允许一个线程无原因地（没有发生过任何 assign 操作）把数据从工作内存同步回主内存中。</li><li>一个新的变量只能在主内存中诞生，不允许在工作内存中直接使用一个未被初始化（load 或 assign）的变量。即就是对一个变量实施 use 和 store 操作之前，必须先执行过了 assign 和 load 操作。</li><li>一个变量在同一时刻只允许一条线程对其进行 lock 操作，lock 和 unlock 必须成对出现</li><li>如果对一个变量执行 lock 操作，将会清空工作内存中此变量的值，在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值</li><li>如果一个变量事先没有被 lock 操作锁定，则不允许对它执行 unlock 操作；也不允许去 unlock 一个被其他线程锁定的变量。</li><li>对一个变量执行 unlock 操作之前，必须先把此变量同步到主内存中（执行 store 和 write 操作）。</li></ul><h2 id="指令重排序"><a href="#指令重排序" class="headerlink" title="指令重排序"></a>指令重排序</h2><p>在执行程序时为了提高性能，编译器和处理器经常会对指令进行重排序。重排序分成三种类型：</p><ol><li>编译器优化的重排序。编译器在不改变单线程程序语义放入前提下，可以重新安排语句的执行顺序。</li><li>指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性，处理器可以改变语句对应机器指令的执行顺序。</li><li>内存系统的重排序。由于处理器使用缓存和读写缓冲区，这使得加载和存储操作看上去可能是在乱序执行。</li></ol><h3 id="数据依耐性"><a href="#数据依耐性" class="headerlink" title="数据依耐性"></a>数据依耐性</h3><p>如果两个操作访问同一个变量，其中一个为写操作，此时这两个操作之间存在数据依赖性。<br>编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序，即不会重排序。</p><h3 id="as-if-serial"><a href="#as-if-serial" class="headerlink" title="as-if-serial"></a>as-if-serial</h3><p>不管怎么重排序，单线程下的执行结果不能被改变，编译器、runtime 和处理器都必须遵守 as-if-serial 语义。</p><h3 id="内存屏障"><a href="#内存屏障" class="headerlink" title="内存屏障"></a>内存屏障</h3><p>为了保证内存的可见性，Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java 内存模型把内存屏障分为 LoadLoad、LoadStore、StoreLoad和StoreStore 四种。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Java内存模型，即 Java Memory Model（JMM），定义了 Java 虚拟机在计算机内存的工作方式。现在的 Java 内存模型主要源于1.5版本。&lt;/p&gt;
&lt;h1 id=&quot;缓存与一致性&quot;&gt;&lt;a href=&quot;#缓存与一致性&quot; class=&quot;headerlink</summary>
      
    
    
    
    <category term="Java" scheme="http://blog.liexing.me/categories/Java/"/>
    
    
    <category term="Java" scheme="http://blog.liexing.me/tags/Java/"/>
    
    <category term="内存模型" scheme="http://blog.liexing.me/tags/%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="多线程" scheme="http://blog.liexing.me/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>统计学习方法（笔记）-逻辑斯谛回归与最大熵模型</title>
    <link href="http://blog.liexing.me/2017/07/13/statistical-learning-method-logistic-regression-maximum-entropy-model/"/>
    <id>http://blog.liexing.me/2017/07/13/statistical-learning-method-logistic-regression-maximum-entropy-model/</id>
    <published>2017-07-13T11:29:50.000Z</published>
    <updated>2023-01-20T15:04:29.746Z</updated>
    
    <content type="html"><![CDATA[<p>逻辑斯谛回归（Logistic regression）是统计学习中的经典<strong>分类方法</strong>，最大熵是概率模型学习的一个准则，将其推广到分类问题得到最大熵模型。逻辑斯谛回归模型与最大熵模型都是属于对数线性模型。</p><h1 id="逻辑斯谛回归模型"><a href="#逻辑斯谛回归模型" class="headerlink" title="逻辑斯谛回归模型"></a>逻辑斯谛回归模型</h1><p>逻辑斯谛分布的分布函数和密度函数如下：</p><p>$$F(x) &#x3D; P(X &lt;&#x3D; x) &#x3D; \frac{1}{1+e^{-(x - \mu) &#x2F; \gamma}}$$</p><p>$$f(x) &#x3D; F’(x) &#x3D; \frac{e^{-(x-\mu) &#x2F; \gamma}}{\gamma (1+e^{-(x-\mu) &#x2F; \gamma})^2}$$</p><p>其中$\mu$为位置参数；$\gamma &gt; 0$为形状参数，值越小，分布越集中于$\mu$附近。其分布的密度函数与分布函数如图：</p><img src="/images/statistical-learning-method/1499954622.jpg"  title="逻辑斯谛分布的密度函数与分布函数" alt="逻辑斯谛分布的密度函数与分布函数"/><p>二项逻辑斯谛回归模型是如下的条件概率分布：</p><p>$$P(Y&#x3D;1|x) &#x3D; \frac{exp(w \cdot x + b)}{1+ exp(w \cdot x + b)}$$<br>$$P(Y&#x3D;0|x) &#x3D; \frac{1}{1+ exp(w \cdot x + b)}$$</p><p>逻辑斯谛回归模型是指，输出$Y&#x3D;1$的对数几率，其是由输入$x$的线性函数表示的模型。可以用极大似然估计法估计模型参数，从而得到逻辑斯谛回归模型。学习过程通常采用的方法是梯度下降法及拟牛顿法。</p><h1 id="最大熵模型"><a href="#最大熵模型" class="headerlink" title="最大熵模型"></a>最大熵模型</h1><p>最大熵原理认为，学习概率模型时，在所有可能的概率模型（分布）中，熵最大的模型是最好的模型。通常用约束条件来确定概率模型的集合，所以最大熵原理也可以表述为在满足约束条件的模型集合汇总选取熵最大的模型。最大熵模型的学习可以形式化为约束最优化问题：</p><p>$$\max_{P \in C} H(P) &#x3D; - \sum_{x,y}\tilde{P}(x)p(y|x)logP(y|x)$$<br>$$s.t. \qquad E_p(f_i) &#x3D; E_\tilde{p}(f_i) $$<br>$$\qquad \sum_yP(y|x)&#x3D;1$$</p><p>最大熵模型是由以下条件概率分布表示的分类模型：</p><p>$$P_w(y|x) &#x3D; \frac{1}{Z_w(x)}exp(\sum_{i&#x3D;1}^{n}w_if_i(x,y))$$<br>$$Z_w(x)&#x3D;\sum_yexp(\sum_{i&#x3D;1}^{n}w_if_i(x,y))$$</p><p>其中$Z_w(x)$是规范化因子，$f_i$为特征函数，$w_i$为特征的权值。</p><p>逻辑斯谛回归模型与最大熵模型都属于对数线性模型。二者的学习一般采用极大似然估计，或正则化的极大似然估计。逻辑斯谛回归模型及最大熵模型学习可以形式化为无约束最优化问题。求解该最优化问题的算法有改进的迭代尺度法、梯度下降法、拟牛顿法。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;逻辑斯谛回归（Logistic regression）是统计学习中的经典&lt;strong&gt;分类方法&lt;/strong&gt;，最大熵是概率模型学习的一个准则，将其推广到分类问题得到最大熵模型。逻辑斯谛回归模型与最大熵模型都是属于对数线性模型。&lt;/p&gt;
&lt;h1 id=&quot;逻辑斯谛回归模型</summary>
      
    
    
    
    <category term="统计学习" scheme="http://blog.liexing.me/categories/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="笔记" scheme="http://blog.liexing.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="统计学习" scheme="http://blog.liexing.me/tags/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="机器学习" scheme="http://blog.liexing.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="逻辑斯谛回归" scheme="http://blog.liexing.me/tags/%E9%80%BB%E8%BE%91%E6%96%AF%E8%B0%9B%E5%9B%9E%E5%BD%92/"/>
    
    <category term="最大熵模型" scheme="http://blog.liexing.me/tags/%E6%9C%80%E5%A4%A7%E7%86%B5%E6%A8%A1%E5%9E%8B/"/>
    
  </entry>
  
  <entry>
    <title>统计学习方法（笔记）-决策树</title>
    <link href="http://blog.liexing.me/2016/11/07/statistical-learning-method-decision-tree/"/>
    <id>http://blog.liexing.me/2016/11/07/statistical-learning-method-decision-tree/</id>
    <published>2016-11-07T08:30:01.000Z</published>
    <updated>2023-01-20T15:04:29.743Z</updated>
    
    <content type="html"><![CDATA[<p>决策树是一种基本的分类与回归方法。模型可读性强，分类速度快，可以认为是 if-then 规则的集合。</p><h1 id="决策树模型与学习"><a href="#决策树模型与学习" class="headerlink" title="决策树模型与学习"></a>决策树模型与学习</h1><p>分类决策树模型是一种描述对实例进行分类的树形结构。内部节点表示特征或者属性，叶节点表示一个类。分类时，从根节点开始，对实例某一特征进行测试，根据测试结果分配到子节点，递归进行，直到到达叶节点，即为实例所属的类，如图：</p><img src="/images/statistical-learning-method/1478508855.jpg"  title="决策树" alt="决策树"/>可以将决策树看做一个 if-then 规则的集合，并且其路径具有互斥且完备性质。如上图，最终的类别为 Yes 和 No 两类，假设存在数据$x={青少年,是学生}$，首先判断第一个特征，年龄，为青少年，跳转到最左边节点，判断是否是学生，则类别为 Yes。<p>决策树还能表示给定特征条件下类的条件概率分布。</p><p>决策树学习本质上是从训练数据集中归纳出一组分类规则。能对训练数据集进行正确分类的决策树可能有多个，也可能没有。需要选择一个与训练数据集矛盾小，有很好泛化能力的决策树。从另一个角度，把决策树学习看做训练数据集估计条件概率模型，基于特种空间划分的类的条件概率模型有无穷多个，我们选择的模型应该对训练数据有很好的拟合，对未知数据有很好的预测。</p><p>决策树的学习损失函数通常是正则化的极大似然函数。从所有可能的决策树中选取最有决策树是 NP 完全问题，所以通常学习算法采用启发式方法，求得次最优的决策树。对于过拟合现象，采用自下而上的剪枝操作，将树变得更简单。决策树的生成只考虑局部最优，剪枝则考虑全局最优。学习常用算法有 ID3、C4.5和 CART。</p><h1 id="特征选择"><a href="#特征选择" class="headerlink" title="特征选择"></a>特征选择</h1><p>特征选择的准则通常是信息增益或者信息增益比。</p><p>熵是表示随机变量的不确定性的度量。设 X 是一个取有限个值的离散随机变量，其概率分布为：</p><p>$$P(X&#x3D;x_i)&#x3D;p_i, \quad i&#x3D;1,2,…,n$$</p><p>随机变量 X 的熵定义为：</p><p>$$H(X)&#x3D;-\sum \limits_{i&#x3D;1}^n p_i \log p_i$$</p><p>上式中，若$p_i&#x3D;0$，定义$0\log 0&#x3D;0$。对数底可以为2或者 e，这时熵的单位分别为比特或纳特。由定义，熵只依赖于 X 的分布，与 X 的取值无关，所以也可以记为：</p><p>$$H(p)&#x3D;-\sum \limits_{i&#x3D;1}^n p_i \log p_i$$</p><p>熵越大，随机变量的不确定性越大，所以：$0 \le H(p) \le \log n$。</p><p>对于随机变量 (X,Y)，其联合概率分布：</p><p>$$P(X&#x3D;x_i,Y&#x3D;y_j)&#x3D;P_{ij},\quad i&#x3D;1,2,…,n;j&#x3D;1,2,…,m$$</p><p>条件熵表示在已知随机变量 X 的条件下，变量 Y 的不确定性，定义为 X 给定条件下 Y 的条件概率分布的熵对 X 的数学期望：</p><p>$$H(Y|X)&#x3D;\sum \limits_{i&#x3D;1}^n p_iH(Y|X&#x3D;X_i)$$</p><p>这里$p_i&#x3D;P(X&#x3D;x_i), i&#x3D;1,2,…,n$。</p><p>信息增益表示得知特征 X 的信息而是类 Y 的信息的不确定性减少的程度，也称为互信息。</p><p>特征 A 对训练数据集 D 的信息增益 g(D,A) 定义为集合 D 的经验熵 H(D) 与特征 A 给定条件下 D 的检验条件熵之差，即：</p><p>$$g(D,A)&#x3D;H(D)-H(D|A)$$</p><p>因此，决策树特征选择的方法可以为：对训练集 D，计算其每个特征的信息增益，并比较它们的大小，选择信息增益最大的特征。</p><p>但是，以信息增益作为划分的特征，可能存在偏向于选择取值较多的特征的问题，因此可以改为使用信息增益比：</p><p>$$g_R(D,A)&#x3D;\frac {g(D,A)}{H_A(D)}$$</p><p>其中，$H_A(D)&#x3D;-\sum \limits_{i&#x3D;1}^n \frac {|D_i|}{|D|}\log_2 \frac {|D_i|}{|D|}$</p><p>#决策树的生成</p><h2 id="ID3-算法"><a href="#ID3-算法" class="headerlink" title="ID3 算法"></a>ID3 算法</h2><p>ID3 核心是在决策树的各个节点上应用信息增益准则选择特征，递归地构建决策树。从根节点开始，对节点计算所有可能的特征的信息增益，选择信息增益最大的特征作为节点的特征，由该特征的不同取值建立子节点。ID3相当于用极大似然法进行概率模型的选择。但是 ID3 算法只有树的生成，所以容易产生过拟合。</p><h2 id="C4-5-算法"><a href="#C4-5-算法" class="headerlink" title="C4.5 算法"></a>C4.5 算法</h2><p>C4.5 与 ID3 类似，但是生成过程，使用的是信息增益比来选择特征。</p><h1 id="决策树的剪枝"><a href="#决策树的剪枝" class="headerlink" title="决策树的剪枝"></a>决策树的剪枝</h1><p>决策树生成过程，可能会导致过拟合。可以使用剪枝对已生成的树进行简化。具体的，剪枝从已生成的树上裁掉一些子树或者叶节点，并将其根节点或父节点作为新的叶节点，从而简化分类树模型。</p><p>决策树的剪枝往往通过极小化决策树整体的损失函数或代价函数来实现。</p><p>设树 T 的叶节点个数为 |T|，t 是树 T 的叶节点，该叶节点有$N_t$个样本点，其中 k 类样本有$N_{tk}$个，$H_{t}(T)$为叶节点 t 上的经验熵，$\alpha \ge 0$为参数，则决策树学习的损失函数为：</p><p>$$C_{\alpha}(T)&#x3D;\sum \limits_{t&#x3D;1}^{|T|}N_tH_t(T)+\alpha{T}$$</p><p>其中经验熵：</p><p>$$H_t(T)&#x3D;-\sum_k \frac{N_{tk}}{N_t}\log \frac{N_{tk}}{N_t}$$</p><p>将损失函数记为：</p><p>$$C_{\alpha}(T)&#x3D;C(T)+\alpha |T|$$</p><p>其中第一项表示对训练数据的预测误差，|T|表示复杂度，参数$\alpha \ge 0$控制两者间的影响。</p><p>剪枝时，计算每个节点的经验熵，然后递归地从树的叶节点往上回缩。如果一组叶节点回缩到其父节点之前与之后的整体树分别为$T_B$和$T_A$，如果$C_\alpha (T_A) \le C_\alpha(T_B)$则进行剪枝，将父节点变为新的叶节点。</p><p>剪枝算法可以由动态规划算法实现。</p><h1 id="CART-算法"><a href="#CART-算法" class="headerlink" title="CART 算法"></a>CART 算法</h1><p>CART，全称 classification and regression tree，即分类与回归树，既可以用于分类也能用于回归。CART 是在给定输入随机变量 X 条件下输出随机变量 Y 的条件概率分布的学习方法，其假设决策树是二叉树。</p><h2 id="CART-生成"><a href="#CART-生成" class="headerlink" title="CART 生成"></a>CART 生成</h2><p>决策树的生成就是递归地构建儿茶决策树的过程。对回归树用平方误差最小准则，对分类树用基尼指数最小化准则，进行特征选择，生成二叉树。</p><h3 id="回归树的生成"><a href="#回归树的生成" class="headerlink" title="回归树的生成"></a>回归树的生成</h3><p>假设将输入划分为 M 个单元$R_1,R_2,…,R_M$，并且在每个单元$R_m$上有一个固定输出值$c_m$，于是回归树模型可以表示为：</p><p>$$f(x)&#x3D;\sum_{m&#x3D;1}^Mc_mI(x \in R_M)$$</p><p>当输入空间划分确定时，可以用平方误差$\sum_\limits{x_i \in R_M}(y_i-f(x_i))^2$来表示回归树对于数据的预测误差，用平方误差最小准则求解每个单元上的最有输出值。采用启发式的方法划分输入空间。</p><p>生成算法为：</p><ol><li><p>选择最优切分变量 j 和切分点 s，求解：</p><p> $$\min \limits_{j,s}[\min \limits_{c_1} \sum_{x_i\in R_1(j,s)}(y_i-c_1)^2 + \min \limits_{c_2} \sum_{x_i\in R_2(j,s)}(y_i-c_2)^2]$$</p><p> 遍历变量 j，对固定的切分变量 j 扫描切分点 s，选择使上式达到最小值的对 (j,s)。</p></li><li><p>对选用的对 (j,s) 换分区域并决定相应的输出值：</p><p> $$R_1(j,s)&#x3D;{x|x^{(j)} \le s}, \quad R_2(j,s)&#x3D;{x|x^{(j)} \gt s}$$</p><p> $$\hat{c_m}&#x3D;\frac{1}{N_m}\sum_{x_i \in R_m(j,s)}y_i,\quad x\in R_m,\quad m&#x3D;1,2$$</p></li><li><p>继续对子区域调用1、2步骤，直到满足停止条件。</p></li><li><p>将输入空间划分为 M 个区域，生成决策树：</p></li></ol><p>$$f(x)&#x3D;\sum_{m&#x3D;1}^{M}\hat{c_m}I(x\in R_m)$$</p><h3 id="分类树的生成"><a href="#分类树的生成" class="headerlink" title="分类树的生成"></a>分类树的生成</h3><p>分类树使用基尼指数选择最优特征，同时决定该特征的最优二值切分点。假设有 K 个分类，样本点属于第 k 类的概率为$p_k$，则概率分布的基尼指数为：</p><p>$$Gini(p)&#x3D;\sum_{k&#x3D;1}^{K}p_k(1-p_k)&#x3D;1-\sum_{k&#x3D;1}^{K}p_k^2$$</p><p>对于二分类问题，如果样本属于第一类的概率是 p，则：</p><p>$$Gini(p)&#x3D;2p(1-p)$$</p><p>如果样本 D 根据特征 A 是否取某一可能值$\alpha$被分割成$D_1$和$D_2$两部分，则在特征 A 的条件下，集合 D 的基尼指数为：</p><p>$$Gini(D,A)&#x3D;\frac{|D_1|}{D}Gini(D_1)+\frac{|D_2|}{D}Gini(D_2)$$</p><p>基尼指数Gini(D,A)表示经 A&#x3D;a 分割后集合 D 的不确定性。基尼指数值越大，样本集合的不确定性也就越大。</p><p>CART 生成算法为：1. 计算现有特征对数据集 D 的基尼指数，对每一个特征 A，对可能的每个取值$\alpha$的测试为“是”或者“否”将 D 分割成两部分，计算$A&#x3D;\alpha$ 时的基尼指数；2. 选择基尼指数最小的特征及其对应的且分店作为最优特征和最优切分点，将 D 分配到两个子节点，并递归调用1，2。停止计算的条件是节点中样本个数小雨预定阈值，或者样本集的基尼指数小雨预定阈值，或者没有更多特征。</p><h2 id="CART-剪枝"><a href="#CART-剪枝" class="headerlink" title="CART 剪枝"></a>CART 剪枝</h2><p>对于生成的决策树$T_0$，进行 CART 剪枝：</p><ol><li><p>设 $k&#x3D;0,T&#x3D;T_0$</p></li><li><p>设 $\alpha&#x3D;+\infty$</p></li><li><p>自下而上对各内部节点 t 计算$C(T_t),|T_t|$以及</p><p> $$g(t)&#x3D;\frac{C(t)-C(T_t)}{|T_t| - 1}$$</p><p> $$\alpha&#x3D;\min(\alpha,g(t))$$</p><p> 这里，$T_t$表示以 t 为根节点的字数，$C(T_t)$是对训练数据的预测误差，$|T_t|$是$T_t$的叶节点数量</p></li><li><p>自上而下的访问内部节点 t，如果有$g(t)&#x3D;\alpha$，进行剪枝，并对叶节点 t 以多数表决法决定其类，得到树 T</p></li><li><p>设$k&#x3D;k+1,\alpha_k&#x3D;\alpha,T_k&#x3D;T$</p></li><li><p>如果 T 不是由根节点单独构成的树，则回到步骤4</p></li><li><p>采用交叉验证法在子树序列$T_0,T_1,…,T_n$中选取最优子树$T_\alpha$</p></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;决策树是一种基本的分类与回归方法。模型可读性强，分类速度快，可以认为是 if-then 规则的集合。&lt;/p&gt;
&lt;h1 id=&quot;决策树模型与学习&quot;&gt;&lt;a href=&quot;#决策树模型与学习&quot; class=&quot;headerlink&quot; title=&quot;决策树模型与学习&quot;&gt;&lt;/a&gt;决策树模</summary>
      
    
    
    
    <category term="统计学习" scheme="http://blog.liexing.me/categories/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="笔记" scheme="http://blog.liexing.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="统计学习" scheme="http://blog.liexing.me/tags/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="机器学习" scheme="http://blog.liexing.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="k 邻近法" scheme="http://blog.liexing.me/tags/k-%E9%82%BB%E8%BF%91%E6%B3%95/"/>
    
    <category term="knn" scheme="http://blog.liexing.me/tags/knn/"/>
    
  </entry>
  
  <entry>
    <title>统计学习方法（笔记）-朴素贝叶斯法</title>
    <link href="http://blog.liexing.me/2016/11/03/statistical-learning-method-naive-bayes/"/>
    <id>http://blog.liexing.me/2016/11/03/statistical-learning-method-naive-bayes/</id>
    <published>2016-11-03T12:52:47.000Z</published>
    <updated>2023-01-20T15:04:29.747Z</updated>
    
    <content type="html"><![CDATA[<p>朴素贝叶斯（naïve Bayes）法是基于被也是定力与特征条件独立假设的分布方法。首先对于训练数据集，基于特征条件独立假设学习输入输出的联合概率分布，然后对于给定的输入，利用贝叶斯定理求出后验概率最大的输出。</p><h1 id="朴素贝叶斯法的学习与分类"><a href="#朴素贝叶斯法的学习与分类" class="headerlink" title="朴素贝叶斯法的学习与分类"></a>朴素贝叶斯法的学习与分类</h1><p>朴素贝叶斯法是典型的生成学习方法。利用训练数据学习$P(X|Y)$和$P(Y)$的估计，得到联合概率分布</p><p>$$P(X,Y) &#x3D; P(X)P(X|Y)$$</p><p>概率估计方法可以是极大似然估计或者贝叶斯估计。</p><p>朴素贝叶斯法的基本假设使条件独立性。</p><p>$$P(X&#x3D;x|Y&#x3D;c_k)&#x3D;P(X^{(1)}&#x3D;x^{(1)},…,X^{(n)}&#x3D;x^{(n)}|Y&#x3D;c_k)\\ &#x3D;\prod \limits_{j&#x3D;1}^nP(X^{(j)}&#x3D;x^{(j)}|Y&#x3D;c_k)$$</p><p>条件独立假设使朴素贝叶斯法变得简单，但是有时候会牺牲一定的分类准确率。</p><p>朴素贝叶斯法利用贝叶斯定理与学到的联合概率模型进行分类预测，将输入的 x 分到后验概率最大的类 y：</p><p>$$y&#x3D;\arg \max <em>{c_k}P(Y&#x3D;c_k)\prod \limits</em>{j&#x3D;1}^nP(X^{(j)}&#x3D;x^{(j)}|Y&#x3D;c_k)$$</p><p>后验概率最大等价于0-1函数损失时的期望风险最小化。</p><h1 id="朴素贝叶斯法的参数估计"><a href="#朴素贝叶斯法的参数估计" class="headerlink" title="朴素贝叶斯法的参数估计"></a>朴素贝叶斯法的参数估计</h1><p>在朴素贝叶斯法中，学习即估计$P(Y&#x3D;c_k)$和$P(X^{(j)}&#x3D;x^{(j)}|Y&#x3D;c_k)$</p><h2 id="极大似然估计"><a href="#极大似然估计" class="headerlink" title="极大似然估计"></a>极大似然估计</h2><p>先验概率$P(Y&#x3D;c_k)$的极大似然估计是：</p><p>$$P(Y&#x3D;c_k)&#x3D;\frac{\sum \limits_{i&#x3D;1}^NI(y_i&#x3D;c_k)}{N},\quad k&#x3D;1,2,…,K$$</p><p>设第 j 个特征$x^{(j)}$可能取值集合为${a_{j1},a_{j2},…,a_{jS_j} }$，则条件概率为：</p><p>$$P(X^{(j)}&#x3D;a_{jl}|Y&#x3D;c_k)&#x3D;\frac{\sum \limits_{i&#x3D;1}^NI(x_i^{(j)}&#x3D;a_{jl},y_i&#x3D;c_k)}{\sum \limits_{i&#x3D;1}^NI(y_i&#x3D;c_k)}\ j&#x3D;1,2,…,n;\quad l&#x3D;1,2,…S;\quad k&#x3D;1,2,…,K$$</p><h2 id="贝叶斯估计"><a href="#贝叶斯估计" class="headerlink" title="贝叶斯估计"></a>贝叶斯估计</h2><p>极大似然估计可能会出现估计的概率值为 0 的情况，这会影响到后验概率的计算结果。可以使用贝叶斯估计来解决这一问题。贝叶斯估计即加入一个系数防止0情况出现。</p><p>$$P_\lambda(Y&#x3D;c_k)&#x3D;\frac{\sum \limits_{i&#x3D;1}^NI(y_i&#x3D;c_k)+\lambda}{N+K\lambda},\quad k&#x3D;1,2,…,K$$</p><p>$$P_\lambda(X^{(j)}&#x3D;a_{jl}|Y&#x3D;c_k)&#x3D;\frac{\sum \limits_{i&#x3D;1}^NI(x_i^{(j)}&#x3D;a_{jl},y_i&#x3D;c_k)+\lambda}{\sum \limits_{i&#x3D;1}^NI(y_i&#x3D;c_k)+S_j\lambda}\ j&#x3D;1,2,…,n;\quad l&#x3D;1,2,…S;\quad k&#x3D;1,2,…,K$$</p><p>上面两式中，$\lambda\geq0$，当取1时，称为拉普拉斯平滑。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;朴素贝叶斯（naïve Bayes）法是基于被也是定力与特征条件独立假设的分布方法。首先对于训练数据集，基于特征条件独立假设学习输入输出的联合概率分布，然后对于给定的输入，利用贝叶斯定理求出后验概率最大的输出。&lt;/p&gt;
&lt;h1 id=&quot;朴素贝叶斯法的学习与分类&quot;&gt;&lt;a hr</summary>
      
    
    
    
    <category term="统计学习" scheme="http://blog.liexing.me/categories/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="笔记" scheme="http://blog.liexing.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="统计学习" scheme="http://blog.liexing.me/tags/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="机器学习" scheme="http://blog.liexing.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="朴素贝叶斯" scheme="http://blog.liexing.me/tags/%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF/"/>
    
  </entry>
  
  <entry>
    <title>统计学习方法（笔记）-k 近邻法</title>
    <link href="http://blog.liexing.me/2016/11/02/statistical-learning-method-knn/"/>
    <id>http://blog.liexing.me/2016/11/02/statistical-learning-method-knn/</id>
    <published>2016-11-02T08:55:58.000Z</published>
    <updated>2023-01-20T15:04:29.746Z</updated>
    
    <content type="html"><![CDATA[<p>k 近邻法（knn）是一种基本的分类与回归方法。这里只讨论分类问题。knn 分类可以简单理解为，找到距离输入实例最近的 k 个训练数据集中的实例点，其中多数属于某个类，则新输入的实例也属于某个类。</p><h1 id="knn-算法"><a href="#knn-算法" class="headerlink" title="knn 算法"></a>knn 算法</h1><p>根据上述描述，其算法表示为：</p><p>$$y&#x3D;\arg \max \limits_{c_j} \sum \limits_{x_i \in N_{k}(x)} I(y_i &#x3D; c_j),\quad i&#x3D;1,2,…,N;j&#x3D;1,2,…,K$$</p><p>其中，$N_{k}(x)$表示涵盖训练集 T 中与 x 最邻近的 k 个点的邻域。I 为指示函数，当$y_i&#x3D;c_j$时为1，否则为0。</p><p>显然，k 近邻法没有显示的学习过程。</p><h1 id="knn-模型"><a href="#knn-模型" class="headerlink" title="knn 模型"></a>knn 模型</h1><p>knn 实际使用的模型对应于对特征空间的划分。模型由三个要素——距离度量、k 值的选择和分类策略规则决定。当上面三要素与训练集确定后，对于任意输入实例，所属的类别唯一地确定。</p><p>相当于在特征空间中划分一些子空间，判断新输入落入哪个子空间中即可。knn 中，k 为1叫做最近邻。这时候对每个训练集实例点划分一个区域（单元），如图：</p><img src="/images/statistical-learning-method/1478078547.jpg"  title="k近邻法的模型对应特征空间的一个划分" alt="k近邻法的模型对应特征空间的一个划分"/>对于 knn，距离度量是一个比较关键的因素，一般使用欧式距离，也可能使用其他距离。对于两点$x_i,x_j\in \mathcal{X}$，其距离定义为：<p>$$L_p(x_i,x_j)&#x3D;(\sum \limits_{l&#x3D;1}^n |x_i^{(l)}-x_j^{(l)}|^p)^{\frac{1}{p}}$$</p><p>这里，$p\geq 1$，p&#x3D;1 时叫做曼哈顿距离，p&#x3D;2 时叫做欧式距离，$p&#x3D;\infty$时，为求各个坐标距离的最大值：</p><p>$$L_{\infty}(x_i,x_j)&#x3D;\max \limits_l|x_i^{(l)}-x_j^{(l)}|$$</p><p>不同的距离度量所确定的最近邻点是不同的。如图：</p><img src="/images/statistical-learning-method/1478081508.jpg"  title="L_p距离间的关系" alt="L_p距离间的关系"/>其次，k 值的选择对结果影响也很大。k 较小时，近似误差小，但是估算误差大，噪声对其影响大，模型复杂，容易过拟合；k 较大时，可减小估算误差，但是近似误差大，模型变得简单。在应用时，取较小 k 的值，通过交叉验证来选取合适的值。<p>knn 的分类策略往往是多数表决，其等价于经验风险最小化。</p><h1 id="kd-树"><a href="#kd-树" class="headerlink" title="kd 树"></a>kd 树</h1><p>knn 最简单的实现是线性扫描，但是其开销巨大，不可取。</p><p>kd 树是一种对 k 维空间中的实例点进行存储以便对其进行快速检索的二叉树形数据结构。</p><p>构造 kd 树相当于不断地用垂直于坐标轴的超平面将 k 维空间切分，构成一系列的 k 维超矩形区域。kd 树的每个节点对应于一个 k 维超矩形区域。通常，一次选择坐标轴对空间切分，选择训练实例点在选定坐标轴的中位数为切分点，直到子区域没有实例点，这样得到的 kd 树是平衡的（搜索效率未必最优）。</p><p>搜索时，从根开始，递归向下访问，如果目标点当前维的坐标小于切分点的坐标，则移动到左子树，否则移动到右子树，直到叶节点，这时候取当前叶节点为当前最近点。这时候递归向上回退，如果该节点比当前实例点距离目标点更近，则当前点为当前最近点；检查当前最近点另一个子节点对应的区域是否与目标点为球心、目标点与当前最近点为半径的超球体相交，如果相交则另一个子节点中可能存在更近的点，移动到另一子节点递归进行搜索，如果不想交则向上回退；直到会退到根节点，最后的当前最近点即为目标点的最近邻点。</p><p>如果实例点是随机分布的，则搜索的平均复杂度为$O(\log N)$，kd 树更适合训练实例点远大于空间维数时的 k 近邻搜索。当空间位数接近训练实例数时，效率会迅速下降，几乎接近线性扫描。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;k 近邻法（knn）是一种基本的分类与回归方法。这里只讨论分类问题。knn 分类可以简单理解为，找到距离输入实例最近的 k 个训练数据集中的实例点，其中多数属于某个类，则新输入的实例也属于某个类。&lt;/p&gt;
&lt;h1 id=&quot;knn-算法&quot;&gt;&lt;a href=&quot;#knn-算法&quot; </summary>
      
    
    
    
    <category term="统计学习" scheme="http://blog.liexing.me/categories/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="笔记" scheme="http://blog.liexing.me/tags/%E7%AC%94%E8%AE%B0/"/>
    
    <category term="统计学习" scheme="http://blog.liexing.me/tags/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="机器学习" scheme="http://blog.liexing.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="k 邻近法" scheme="http://blog.liexing.me/tags/k-%E9%82%BB%E8%BF%91%E6%B3%95/"/>
    
    <category term="knn" scheme="http://blog.liexing.me/tags/knn/"/>
    
  </entry>
  
</feed>
