<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Michael</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://mikeah2011.github.io/</id>
  <link href="https://mikeah2011.github.io/" rel="alternate"/>
  <link href="https://mikeah2011.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Michael</rights>
  <subtitle>技术笔记 · macOS · PHP · Go · K8s · 网络</subtitle>
  <title>Michael's Blog</title>
  <updated>2026-05-01T13:12:43.910Z</updated>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="macOS" scheme="https://mikeah2011.github.io/categories/macOS/"/>
    <category term="macOS" scheme="https://mikeah2011.github.io/tags/macOS/"/>
    <category term="Shell" scheme="https://mikeah2011.github.io/tags/Shell/"/>
    <content>
      <![CDATA[<figure class="highlight shell"><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 prompt_"># </span><span class="language-bash">安裝必要的環境</span></span><br><span class="line">xcode-select --install</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 brew</span></span><br><span class="line">/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</span><br><span class="line">echo &#x27;# Set PATH, MANPATH, etc., for Homebrew.&#x27; &gt;&gt; ~/.zprofile</span><br><span class="line">echo &#x27;eval &quot;$(/opt/homebrew/bin/brew shellenv)&quot;&#x27; &gt;&gt; ~/.zprofile</span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 iterm2</span></span><br><span class="line">brew install iterm2</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 ohmyzsh 插件</span></span><br><span class="line">sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;</span><br><span class="line">echo &quot;source ~/.zshrc&quot; &gt;&gt; ~/.zprofile</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 zsh 主題</span></span><br><span class="line">git clone https://github.com/romkatv/powerlevel10k.git $ZSH_CUSTOM/themes/powerlevel10k</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 zsh 插件</span></span><br><span class="line">brew install zsh-autosuggestions</span><br><span class="line">echo &quot;source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh&quot; &gt;&gt; ~/.zprofile</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">生成 SSH-key</span></span><br><span class="line">ssh-keygen -t ed25519 -C &quot;michael.ma@kkday.com&quot;</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看</span></span><br><span class="line">less ~/.ssh/id_ed25519.pub</span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 PHP 多版本</span></span><br><span class="line">brew tap shivammathur/php</span><br><span class="line">brew install php@7.1</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 PHP 版本切換器</span></span><br><span class="line">brew install brew-php-switcher</span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安裝 NGINX PostgreSQL redis 服務</span></span><br><span class="line">brew install nginx postgresql@15 redis</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">所有的項目文件都放在家目錄下分門別類</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">軟鏈 NGINX 服務配置文件到 家目錄</span></span><br><span class="line">ln -s $(brew --repo)/etc/nginx/servers ~/</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://mikeah2011.github.io/post/MacOS/%E5%B8%B8%E7%94%A8command.html</id>
    <link href="https://mikeah2011.github.io/post/MacOS/%E5%B8%B8%E7%94%A8command.html"/>
    <published>2025-05-25T10:00:00.000Z</published>
    <summary>macOS 日常开发常用 Shell 命令速查：环境安装、Homebrew、文件处理、系统维护与开发工具配置。</summary>
    <title>macOS 常用命令</title>
    <updated>2026-05-01T13:12:43.910Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <category term="反射" scheme="https://mikeah2011.github.io/tags/%E5%8F%8D%E5%B0%84/"/>
    <content>
      <![CDATA[<p><a href="https://www.php.net/manual/zh/language.operators.type.php"><code>instanceof</code></a>與<a href="https://www.php.net/manual/zh/function.method-exists.php"><code>method_exists</code></a>的用法區別。<a href="https://stackoverflow.com/questions/28767294/instanceof-or-method-exist-which-one-should-use">參考StackoverFlow</a></p><p><code>instanceof</code>為保留關鍵字，用於檢查對象是否屬於某個類。如果對象是類的實例，則比較返回<code>true</code>，否則返回 <code>false</code>。通常我們理解為 類型運算符兩邊為對象或類進行比較，等同於<code>===</code>；所以，與CommonService的判斷比較也是可取的。</p><p><code>method_exists</code>為內置函數，用於檢查對象或類是否具有指定的方法。通常我們理解為 對象或類中是否存在指定方法，返回值也為 <code>true</code> 或 <code>false</code>。</p><p>結論：倆者的比較維度不同。</p><table><thead><tr><th>比较项</th><th><a href="https://www.php.net/manual/zh/language.operators.type.php"><code>instanceof</code></a></th><th><a href="https://www.php.net/manual/zh/function.method-exists.php"><code>method_exists</code></a></th></tr></thead><tbody><tr><td>性质</td><td>关键保留字</td><td>内置函数</td></tr><tr><td>用途</td><td>检查对象是否属于某一个类</td><td>检查对象或类是否具有指定的方法</td></tr><tr><td>返回</td><td><code>true</code> 或 <code>false</code></td><td><code>true</code> 或 <code>false</code></td></tr><tr><td></td><td></td><td></td></tr></tbody></table><p><code>instanceof</code>不会告诉你传递的对象是否包含该方法，只告诉你它是那个方法的一个实例。</p><p>dyld[43914]: Library not loaded: &#x2F;opt&#x2F;homebrew&#x2F;opt&#x2F;icu4c&#x2F;lib&#x2F;libicui18n.71.dylib<br>  Referenced from: <F5F1E51B-0E61-30B8-A4D3-2A7FBF9FFB91> &#x2F;opt&#x2F;homebrew&#x2F;Cellar&#x2F;<a href="mailto:&#x70;&#104;&#112;&#64;&#55;&#x2e;&#51;">php@7.3</a>&#x2F;7.3.33_3&#x2F;bin&#x2F;php<br>  Reason: tried: </p><p>‘&#x2F;opt&#x2F;homebrew&#x2F;opt&#x2F;icu4c&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file),</p><p>‘&#x2F;System&#x2F;Volumes&#x2F;Preboot&#x2F;Cryptexes&#x2F;OS&#x2F;opt&#x2F;homebrew&#x2F;opt&#x2F;icu4c&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file), </p><p>‘&#x2F;opt&#x2F;homebrew&#x2F;opt&#x2F;icu4c&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file),</p><p> ‘&#x2F;usr&#x2F;local&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file), </p><p>‘&#x2F;usr&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file, not in dyld cache), </p><p>‘&#x2F;opt&#x2F;homebrew&#x2F;Cellar&#x2F;icu4c&#x2F;72.1&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file), </p><p>‘&#x2F;System&#x2F;Volumes&#x2F;Preboot&#x2F;Cryptexes&#x2F;OS&#x2F;opt&#x2F;homebrew&#x2F;Cellar&#x2F;icu4c&#x2F;72.1&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file), </p><p>‘&#x2F;opt&#x2F;homebrew&#x2F;Cellar&#x2F;icu4c&#x2F;72.1&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file),</p><p> ‘&#x2F;usr&#x2F;local&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file), </p><p>‘&#x2F;usr&#x2F;lib&#x2F;libicui18n.71.dylib’ (no such file, not in dyld cache)</p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/instanceof%E4%B8%8Emethod_exists.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/instanceof%E4%B8%8Emethod_exists.html"/>
    <published>2023-03-05T10:00:00.000Z</published>
    <summary>`instanceof`與`method_exists`的用法區別。參考StackoverFlow `instanceof`為保留關鍵字，用於檢查對象是否屬於某個類。如果對象是類的實例，則比較返回`true`，否則返回 `false`。通常…</summary>
    <title>instanceof 与 method_exists</title>
    <updated>2026-05-01T13:12:43.907Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="macOS" scheme="https://mikeah2011.github.io/categories/macOS/"/>
    <category term="macOS" scheme="https://mikeah2011.github.io/tags/macOS/"/>
    <category term="Homebrew" scheme="https://mikeah2011.github.io/tags/Homebrew/"/>
    <content>
      <![CDATA[<blockquote><p><a href="https://brew.sh/"><code>brew</code></a> 神器</p></blockquote><p>眾所周知，<code>brew</code> 是 <code>MacOS</code> 系統的管理工具，如果是你重度 <code>Linux</code> 系統使用者，你可能也會知道她。</p><p>身為 <code>Mac</code> 用戶，你真的會用嗎？在看到這裡之前，你可能跟我一樣，都不太清楚她~，今天我們就一起了解了解她。</p><p>官方安裝指令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</span><br></pre></td></tr></table></figure><p>PS：如果提示相關 <code>git</code> 錯誤，建議可以執行 <code>xcode-select --install</code> 嘗試下。</p><p>用 <code>brew</code>管理 <code>APP</code> 可以自動選擇對應芯片的版本，媽媽再也不操心我到處尋找 <code>APP</code>，還擔心我裝錯版本…<img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221206154428957.png" alt="image-20221206154428957"></p><p>總結：</p><ol><li><p>最新的版本；<code>release laster</code></p></li><li><p>最合適的版本；<code>Apple M1</code> or <code>Intel</code></p></li><li><p>支持批量安裝；<code>brew install app1 app2...</code></p></li><li><p>自動遷移到<code>/Applications/</code>下，無需手動拖拽</p></li><li><p>一鍵卸載&amp;批量&amp;安全</p><p><code>brew cask uninstall app1 app2...</code></p></li><li><p>一鍵更新；</p><p><code>brew upgrade app1 app2...</code></p></li><li><p>支持重裝；</p><p><code>brew reinstall</code>，舊APP會被備份至<code>$(brew --repo)/Caskroom</code>下，且APP 數據均會被保留；<img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221206211600038-20221207100345553.png" alt="image-20221206211600038"></p><p>唯一的缺陷就是不支持重裝除 <code>brew</code>方式之外的<code>APP</code> ，需要手動卸載掉後才可以安裝；<img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221206160819960.png" alt="image-20221206160819960"></p></li></ol><blockquote><p><code>brew</code> 常用命令和常量</p></blockquote><table><thead><tr><th><code>brew</code> 命令</th><th>釋義</th><th>別名</th></tr></thead><tbody><tr><td><code>$(brew --repo)</code></td><td>倉庫目錄</td><td><code>HOMEBREW_PREFIX</code></td></tr><tr><td><code>brew config</code></td><td>查看 <code>brew</code> 的配置信息</td><td></td></tr><tr><td><code>brew doctor</code></td><td>檢查 <code>brew</code> 健康狀況</td><td></td></tr><tr><td><code>brew info</code></td><td>查看應用詳情</td><td></td></tr><tr><td><code>brew install</code></td><td>安裝</td><td></td></tr><tr><td><code>brew list</code></td><td>查看當前已安裝的應用列表</td><td><code>brew ls</code></td></tr><tr><td><code>brew reinstall</code></td><td>重裝</td><td></td></tr><tr><td><code>brew search</code></td><td>檢索應用</td><td></td></tr><tr><td><code>brew services cleanup</code></td><td>卸載服務</td><td></td></tr><tr><td><code>brew services kill</code></td><td>殺掉服務進程</td><td></td></tr><tr><td><code>brew services list</code></td><td>查看安裝的服務列表</td><td><code>brew services ls</code></td></tr><tr><td><code>brew services restart</code></td><td>重啟服務</td><td></td></tr><tr><td><code>brew services start</code></td><td>啟動服務</td><td></td></tr><tr><td><code>brew services stop</code></td><td>停止服務</td><td></td></tr><tr><td><code>brew uninstall</code></td><td>卸載</td><td></td></tr><tr><td><code>brew update</code></td><td>更新 <code>brew</code> 配置</td><td></td></tr><tr><td><code>brew upgrade</code></td><td>更新應用</td><td></td></tr></tbody></table><blockquote><p>以下是我個人經過測試可以安裝的 <code>APP</code> 列表</p></blockquote><table><thead><tr><th>序號</th><th><code>APP</code></th><th>是否安裝</th><th>是否免費</th><th>用途</th><th>備註</th></tr></thead><tbody><tr><td>1</td><td><code>aldente</code></td><td>✅</td><td>✔️</td><td>電源管理工具</td><td>社區版，付費版請訂閱 <code>Pro</code></td></tr><tr><td>2</td><td><code>alfred</code></td><td>✅</td><td>❌</td><td>記憶工具</td><td>部分功能是需要訂閱付費的</td></tr><tr><td>3</td><td><code>apipost</code></td><td>✅</td><td>✔️</td><td><code>api</code> 接口文檔調試工具</td><td>免費，也有企業團隊付費版</td></tr><tr><td>4</td><td><code>asana</code></td><td>✅</td><td>✔️</td><td>項目管理</td><td>免費</td></tr><tr><td>5</td><td><code>bartender</code></td><td>✅</td><td>❌</td><td>任務欄管理工具</td><td>部分功能是需要訂閱付費的</td></tr><tr><td>6</td><td><code>bob</code></td><td>✅</td><td>✔️</td><td>翻譯工具</td><td>社區版，付費版請前往<code>App Store</code>自行購買</td></tr><tr><td>7</td><td><code>brew-php-switcher</code></td><td>✅</td><td>✔️</td><td><code>PHP</code> 多版本切換工具</td><td></td></tr><tr><td>8</td><td><code>cleanmymac</code></td><td>✅</td><td>❌</td><td>清理工具</td><td>部分功能是需要訂閱付費的</td></tr><tr><td>9</td><td><code>composer</code></td><td>✅</td><td>✔️</td><td><code>PHP</code> 擴展包管理工具</td><td></td></tr><tr><td>10</td><td><code>google-chrome</code></td><td>✅</td><td>✔️</td><td>瀏覽器</td><td>免費</td></tr><tr><td>11</td><td><code>istat-menus</code></td><td>✅</td><td>❌</td><td>狀態工具</td><td>部分功能是需要訂閱付費的</td></tr><tr><td>12</td><td><code>iterm2</code></td><td>✅</td><td>✔️</td><td>終端工具</td><td>免費</td></tr><tr><td>13</td><td><code>jetbrains-toolbox</code></td><td>✅</td><td>❌</td><td><code>jetbrains</code> 工具箱</td><td>管理的應用是訂閱付費的</td></tr><tr><td>14</td><td><code>nginx</code></td><td>✅</td><td>✔️</td><td><code>NGINX web</code> 服務</td><td></td></tr><tr><td>15</td><td><code>nordlayer</code></td><td>✅</td><td>❌</td><td><code>VPN</code> 工具</td><td>企業訂閱付費</td></tr><tr><td>16</td><td><code>php</code></td><td>✅</td><td>✔️</td><td><code>PHP</code> 服務</td><td><code>brew tap shivammathur/php</code></td></tr><tr><td>17</td><td><code>postgresql</code></td><td>✅</td><td>✔️</td><td><code>PostgreSQL</code> 服務</td><td></td></tr><tr><td>18</td><td><code>qq</code></td><td>✅</td><td>✔️</td><td><code>QQ</code></td><td>免費</td></tr><tr><td>19</td><td><code>RunCat</code></td><td>❎</td><td>❌</td><td>指示 <code>Mac</code> 的運行狀況，</td><td>暫時沒找到…</td></tr><tr><td>20</td><td><code>slack</code></td><td>✅</td><td>✔️</td><td>辦公通訊</td><td>免費</td></tr><tr><td>21</td><td><code>tree</code></td><td>✅</td><td>✔️</td><td>檔案結構樹形化</td><td></td></tr><tr><td>22</td><td><code>uPic</code></td><td>✅</td><td>✔️</td><td>圖床</td><td><code>brew install bigwig-club/brew/upic --cask</code></td></tr><tr><td>23</td><td><code>utools</code></td><td>✅</td><td>✔️</td><td>效率工具</td><td>既是插件也是應用</td></tr><tr><td>24</td><td><code>wechat</code></td><td>✅</td><td>✔️</td><td>微信</td><td>免費</td></tr><tr><td>25</td><td><code>wechatwebdevtools</code></td><td>✅</td><td>✔️</td><td>微信開發者工具</td><td>免費</td></tr><tr><td>26</td><td><code>wechatwork</code></td><td>✅</td><td>✔️</td><td>企業微信</td><td>免費</td></tr><tr><td>27</td><td><code>zsh-autosuggestions</code></td><td>✅</td><td>✔️</td><td>命令猜想插件</td><td></td></tr></tbody></table><p>以下是對應的命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install aldentealfredapipostasanabartenderbobbrew-php-switchercleanmymaccomposergoogle-chromeistat-menusiterm2jetbrains-toolboxnginxnordlayerphppostgresqlqqRunCatslacktreeuPicutoolswechatwechatwebdevtoolswechatworkzsh-autosuggestions </span><br></pre></td></tr></table></figure><p>用完之後，是不是就釋放了 <code>dmg</code> 、<code>apk</code> 包，節省了空間不說，還很方便。</p><p>所以，以後如果想安裝什麼 <code>APP</code>，是不是可以優先考慮 <code>brew</code>，平台都不是問題，也支持 <code>Linux</code>。</p><p>PS： <code>brew</code> 依賴於 <code>GitHub</code> 的訪問環境</p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/MacOS/brew.html</id>
    <link href="https://mikeah2011.github.io/post/MacOS/brew.html"/>
    <published>2022-12-08T09:11:30.000Z</published>
    <summary>`brew` 神器 眾所周知，`brew` 是 `MacOS` 系統的管理工具，如果是你重度 `Linux` 系統使用者，你可能也會知道她。 身為 `Mac` 用戶，你真的會用嗎？在看到這裡之前，你可能跟我一樣，都不太清楚她~，今天我們就一…</summary>
    <title>macOS APP 管理神器——brew</title>
    <updated>2026-05-01T13:12:43.910Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PhpStorm" scheme="https://mikeah2011.github.io/categories/PhpStorm/"/>
    <category term="JetBrains Tools" scheme="https://mikeah2011.github.io/tags/JetBrains-Tools/"/>
    <content>
      <![CDATA[<p>首先，自行下载<a href="https://www.jetbrains.com/">JetBrains</a>的产品，通常推荐<a href="https://download-cdn.jetbrains.com/toolbox/jetbrains-toolbox-1.26.4.13374.dmg">JetBrains ToolBox</a>。</p><p>其次，<a href="https://3.jetbra.in/">访问网站</a>选择对当前网络环境访问效率最好的域名站点。</p><p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221103154438681.png" alt="image-20221103154438681"></p><p>下载&#96;&#96;jetbra.zip<code>包，首行第一句话就是下载。查看</code>readme.txt&#96;，有具体破解步骤。</p><p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221103153959624.png" alt="image-20221103153959624"></p><p>解压后直接双击点击这个 <code>install.sh</code> 即可。</p><p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221103154120642.png" alt="image-20221103154120642"></p><p>打开PhpStorm使用 code 码激活，鼠标放在 第三步骤的网站中  PhpStorm  <code>Copy to clipboard</code> 点击赋值，粘贴激活即可。</p><p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221103154315312.png" alt="image-20221103154315312"></p><p>其他产品雷同。</p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/Other/JetBrains%E7%B3%BB%E5%88%97%E4%BA%A7%E5%93%81%E6%BF%80%E6%B4%BB%E6%96%B9%E6%B3%95.html</id>
    <link href="https://mikeah2011.github.io/post/Other/JetBrains%E7%B3%BB%E5%88%97%E4%BA%A7%E5%93%81%E6%BF%80%E6%B4%BB%E6%96%B9%E6%B3%95.html"/>
    <published>2022-10-20T15:05:07.000Z</published>
    <summary>首先，自行下载JetBrains的产品，通常推荐JetBrains ToolBox。 其次，访问网站选择对当前网络环境访问效率最好的域名站点。 下载``jetbra.zip`包，首行第一句话就是下载。查看`readme.txt`，有具体破解…</summary>
    <title>JetBrains系列产品激活方法</title>
    <updated>2026-05-01T13:12:43.910Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="Redis" scheme="https://mikeah2011.github.io/categories/Redis/"/>
    <category term="Redis" scheme="https://mikeah2011.github.io/tags/Redis/"/>
    <content>
      <![CDATA[<p>为了便于大家查找问题，了解全貌，整理个目录，我们可以快速全局了解关于Redis 缓存，面试官一般喜欢问哪些问题？</p><p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/redis_all.png" alt="图片"></p><p>接下来，我们逐条来看看每个问题及解决方案</p><p><strong>Redis 有哪些特性？</strong></p><ul><li>性能高， 读的速度是100000次&#x2F;s，写的速度是80000次&#x2F;s</li><li>数据持久化，支持RDB 、AOF</li><li>支持事务。通过<code>MULTI</code>和<code>EXEC</code>指令包起来。</li><li>多种数据结构类型</li><li>主从复制</li><li>其他特性：发布&#x2F;订阅、通知、key过期等</li></ul><p><strong>Redis 为什么这么快？</strong></p><ul><li>完全基于内存，没有磁盘IO上的开销，异步持久化除外</li><li>单线程，避免多个线程切换的性能损耗</li><li>非阻塞的IO多路复用机制</li><li>底层的数据存储结构优化，使用原生的数据结构提升性能。</li></ul><p><strong>Redis 底层的基础数据结构有哪些？</strong></p><ul><li>字符串。没有采用C语言的传统字符串，而是自己实现的一个简单动态字符串SDS的抽象类型，并保存了长度信息。</li><li>链表（linkedlist）。双向无环链表结构，每个链表的节点由一个listNode结构来表示，每个节点都有前置和后置节点的指针</li><li>字典（hashtable）。保存键值对的抽象数据结构，底层使用hash表，每个字典带有两个hash表，供平时使用和rehash时使用。</li><li>跳跃表（skiplist）。跳跃表是有序集合的底层实现之一。redis跳跃表由zskiplist和zskiplistNode组成，zskiplist用于保存跳跃表 信息(表头、表尾节点、⻓度等)，zskiplistNode用于表示表跳跃节点，每个跳跃表的层高都是1- 32的随机数，在同一个跳跃表中，多个节点可以包含相同的分值，但是每个节点的成员对象必须是唯一的，节点按照分值大小排序，如果分值相同，则按照成员对象的大小排序。</li><li>整数集合（intset）。用于保存整数值的集合抽象数据结构，不会出现重复元素，底层实现为数组。</li><li>压缩列表（ziplist）。为节约内存而开发的顺序性数据结构，可以包含多个节点，每个节点可以保存一个字节数组或者整数值。</li></ul><p><strong>Redis 支持哪些数据类型？</strong></p><p>五种常用数据类型：<code>String</code>、<code>Hash</code>、<code>Set</code>、<code>List</code>、<code>SortedSet</code>。</p><p>三种特殊的数据类型：<code>Bitmap</code>、<code>HyperLogLog</code>、<code>Geospatial</code>，</p><p>​其中Bitmap 、HyperLogLog的底层都是 String 数据类型，</p><p>​Geospatial 底层是 Sorted Set 数据类型。</p><ul><li>字符串对象string：int整数、embstr编码的简单动态字符串、raw简单动态字符串</li><li>列表对象list：ziplist、linkedlist</li><li>哈希对象hash：ziplist、hashtable</li><li>集合对象set：intset、hashtable</li><li>有序集合对象zset：ziplist、skiplist</li></ul><p><strong>Redis 常用的 5 种数据结构和应用场景？</strong></p><ul><li>String：缓存、计数器、分布式锁等</li><li>List：链表、队列、微博关注人时间轴列表等</li><li>Hash：用户信息、Hash 表等</li><li>Set：去重、赞、踩、共同好友等</li><li>Zset：访问量排行榜、点击量排行榜等</li></ul><p><strong>为什么采用单线程？</strong></p><p>CPU不会成为Redis的制约瓶颈，Redis主要受内存、网络限制。例如，在一个普通的 Linux 系统上，使用pipelining 可以每秒传递 100 万个请求，所以如果您的应用程序主要使用 O(N) 或 O(log(N)) 命令，则几乎不会使用太多 CPU，属于IO密集型系统。</p><p><strong>Redis 6.0 之后又改用多线程呢?</strong></p><p>Redis的多线程主要是处理数据的读写、协议解析。执行命令还是采用单线程顺序执行。</p><p>主要是因为redis的性能瓶颈在于网络IO而非CPU，使用多线程进行一些周边预处理，提升了IO的读写效率，从而提高了整体的吞吐量。antirez 在 RedisConf 2019 分享时提到，Redis 6 引入的多线程 IO 对性能提升至少一倍以上。</p><p><strong>过期键Key 的删除策略有哪些？</strong></p><p>有3种过期删除策略。</p><p>惰性删除、定期删除、定时删除</p><ul><li>惰性删除。使用key时才进行检查，如果已经过期，则删除。缺点：过期的key如果没有被访问到，一直无法删除，一直占用内存，造成空间浪费。</li><li>定期删除。每隔一段时间做一次检查，删除过期的key，每次只是随机取一些key去检查。</li><li>定时删除。为每个key设置过期时间，同时创建一个定时器。一旦到期，立即执行删除。缺点：如果过期键比较多时，占用CPU较多，对服务的性能有很大影响。</li></ul><p><strong>如果Redis的内存空间不足，淘汰机制？</strong></p><ul><li>volatile-lru：从已设置过期时间的key中，移出最近最少使用的key进行淘汰</li><li>allkeys-lru：当内存不足以容纳新写入数据时，在键空间中，移除最近最少使用的key（这个是最常用的）</li><li>volatile-ttl：从已设置过期时间的key中，移出将要过期的key</li><li>volatile-random：从已设置过期时间的key中，随机选择key淘汰</li><li>allkeys-random：从key中随机选择key进行淘汰</li><li>no-eviction：禁止淘汰数据。当内存达到阈值的时候，新写入操作报错</li><li>volatile-lfu：从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(LFU(Least Frequently Used)算法，也就是最频繁被访问的数据将来最有可能被访问到)</li><li>allkeys-lfu：当内存不足以容纳新写入数据时，在键空间中，移除最不经常使用的key。</li></ul><p><strong>Redis 突然挂了怎么解决？</strong></p><p>1、从系统可用性角度思考，Redis Cluster引入主备机制，当主节点挂了后，自动切换到备用节点，继续提供服务。</p><p>2、Client端引入本地缓存，通过开关切换，避免Redis突然挂掉，高并发流量把数据库打挂。</p><p><strong>Redis 持久化有哪些方式？</strong></p><p>1、快照RDB。将某个时间点上的数据库状态保存到<code>RDB文件</code>中，RDB文件是一个压缩的二进制文件，保存在磁盘上。当Redis崩溃时，可用于恢复数据。通过<code>SAVE</code>或<code>BGSAVE</code>来生成RDB文件。</p><ul><li>SAVE：会阻塞redis进程，直到RDB文件创建完毕，在进程阻塞期间，redis不能处理任何命令请求。</li><li>BGSAVE：会fork出一个子进程，然后由子进程去负责生成RDB文件，父进程还可以继续处理命令请求，不会阻塞进程。</li></ul><p>2、只追加文件AOF。</p><p>​以日志的形式记录每个写操作（非读操作）。当不同节点同步数据时，读取日志文件的内容将写指令从前到后执行一次，即可完成数据恢复。</p><p><strong>Redis 常用场景</strong></p><ul><li>1、缓存，有句话说的好，「性能不够，缓存来凑」</li><li>2、分布式锁，利用Redis 的 setnx</li><li>3、分布式session</li><li>4、计数器，通过incr命令</li><li>5、排行榜，Redis 的 有序集合</li><li>6、其他</li></ul><p><strong>Redis 缓存要注意的七大经典问题？</strong></p><p>列举了亿级系统，高访问量情况下Redis缓存可能会遇到哪些问题？以及对应的解决方案。</p><ul><li>1、缓存集中失效</li><li>2、缓存穿透</li><li>3、缓存雪崩</li><li>4、缓存热点</li><li>5、缓存大Key</li><li>6、缓存数据的一致性</li><li>7、数据并发竞争预热</li></ul><p><strong>Redis 集群方案有哪几种？</strong></p><ul><li>主从复制模式</li><li>Sentinel（哨兵）模式</li><li>Redis Cluster模式</li></ul><p><strong>Redis 主从数据同步（主从复制）的过程？</strong></p><ul><li>1、slave启动后，向master发送sync命令</li><li>2、master收到sync之后，执行bgsave保存快照，生成RDB全量文件</li><li>3、master把slave的写命令记录到缓存</li><li>4、bgsave执行完毕之后，发送RDB文件到slave，slave执行</li><li>5、master发送缓冲区的写命令给slave，slave接收命令并执行，完成复制初始化。</li><li>6、此后，master每次执行一个写命令都会同步发送给slave，保持master与slave之间数据的一致性</li></ul><p><strong>主从复制的优缺点？</strong></p><p>1、优点：</p><ul><li>master能自动将数据同步到slave，可以进行读写分离，分担master的读压力</li><li>master、slave之间的同步是以非阻塞的方式进行的，同步期间，客户端仍然可以提交查询或更新请求</li></ul><p>缺点：</p><ul><li>不具备自动容错与恢复功能，master 节点宕机后，需要手动指定新的 master</li><li>master宕机，如果宕机前数据没有同步完，则切换IP后会存在数据不一致的问题</li><li>难以支持在线扩容，Redis的容量受限于单机配置</li></ul><p><strong>Sentinel（哨兵）模式的优缺点？</strong></p><p>哨兵模式基于主从复制模式，增加了<strong>哨兵来监控</strong>与<strong>自动处理故障</strong>。</p><p>1、优点：</p><ul><li>哨兵模式基于主从复制模式，所以主从复制模式有的优点，哨兵模式也有</li><li>master 挂掉可以自动进行切换，系统可用性更高</li></ul><p>2、缺点：</p><ul><li>Redis的容量受限于单机配置</li><li>需要额外的资源来启动sentinel进程</li></ul><p><strong>Redis Cluster 模式的优缺点？</strong></p><p>实现了Redis的分布式存储，即每台节点存储不同的内容，来解决在线扩容的问题。</p><p>1、优点：</p><ul><li>无中心架构，数据按照slot分布在多个节点</li><li>集群中的每个节点都是平等的，每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接，而且这些连接保持活跃，这样就保证了我们只需要连接集群中的任意一个节点，就可以获取到其他节点的数据。</li><li>可线性扩展到1000多个节点，节点可动态添加或删除</li><li>能够实现自动故障转移，节点之间通过<code>gossip协议</code>交换状态信息，用投票机制完成slave到master的角色转换</li></ul><p>缺点：</p><ul><li>数据通过异步复制，不保证数据的强一致性</li><li>slave充当 “冷备”，不对外提供读、写服务，只作为故障转移使用。</li><li>批量操作限制，目前只支持具有相同slot值的key执行批量操作，对mset、mget、sunion等操作支持不友好</li><li>key事务操作支持有限，只支持多key在同一节点的事务操作，多key分布在不同节点时无法使用事务功能</li><li>不支持多数据库空间，一台redis可以支持16个db，集群模式下只能使用一个，即<code>db 0</code>。Redis Cluster模式不建议使用pipeline和multi-keys操作，减少max redirect产生的场景。</li></ul><p><strong>Redis 如何做扩容？</strong></p><p>为了避免数据迁移失效，通常使用<code>一致性哈希</code>实现动态扩容缩容，有效减少需要迁移的Key数量。</p><p>但是Cluster 模式，采用固定Slot槽位方式（16384个），对每个key计算CRC16值，然后对16384取模，然后根据slot值找到目标机器，扩容时，我们只需要迁移一部分的slot到新节点即可。</p><p><strong>Redis 的集群原理?</strong></p><p>一个redis集群由多个节点node组成，而多个node之间通过<code>cluster meet</code>命令来进行连接，组成一个集群。</p><p>数据存储通过分片的形式，整个集群分成了<code>16384</code>个slot，每个节点负责一部分槽位。整个槽位的信息会同步到所有节点中。</p><p>key与slot的映射关系：</p><ul><li>健值对 key，进行 <code>CRC16</code> 计算，计算出一个 16 bit 的值</li><li>将 16 bit 的值对 16384 取模，得到 0 ～ 16383 的数表示 key 对应的哈希槽</li></ul><p><strong>Redis 如何做到高可用？</strong></p><p>哨兵机制。</p><p>​具有自动故障转移、集群监控、消息通知等功能。</p><p>​哨兵可以同时监视所有的主、从服务器，当某个master下线时，自动提升对应的slave为master，然后由新master对外提供服务。</p><p><strong>什么是 Redis 事务？</strong></p><p>Redis事务是一组命令的集合，将多个命令打包，然后把这些命令按顺序添加到队列中，并且按顺序执行这些命令。</p><p>Redis事务中没有像Mysql关系型数据库事务隔离级别的概念，不能保证原子性操作，也没有像Mysql那样执行事务失败会进行回滚操作</p><p><strong>Redis 事务执行流程？</strong></p><p>通过<code>MULTI</code>、<code>EXEC</code>、<code>WATCH</code>等命令来实现事务机制，事务执行过程将一系列多个命令按照顺序一次性执行，在执行期间，事务不会被中断，也不会去执行客户端的其他请求，直到所有命令执行完毕。</p><p>具体过程：</p><ul><li>服务端收到客户端请求，事务以<code>MULTI</code>开始</li><li>如果正处于事务状态时，则会把后续命令放入队列同时返回给客户端<code>QUEUED</code>，反之则直接执行这 个命令</li><li>当收到客户端的<code>EXEC</code>命令时，才会将队列里的命令取出、顺序执行，执行完将当前状态从事务状态改为非事务状态</li><li>如果收到 <code>DISCARD</code> 命令，放弃执行队列中的命令，可以理解为Mysql的回滚操作，并且将当前的状态从事务状态改为非事务状态</li></ul><blockquote><p>WATCH 监视某个key，该命令只能在MULTI命令之前执行。如果监视的key被其他客户端修改，EXEC将会放弃执行队列中的所有命令。UNWATCH 取消监视之前通过WATCH 命令监视的key。通过执行EXEC 、DISCARD 两个命令之前监视的key也会被取消监视。</p></blockquote><p><strong>Redis 与 Guava 、Caffeine 有什么区别？</strong></p><p>缓存分为本地缓存和分布式缓存。</p><p>1、Caffeine、Guava，属于本地缓存，特点：</p><ul><li>直接访问内存，速度快，受内存限制，无法进行大数据存储。</li><li>无网络通讯开销，性能更高。</li><li>只支持本地应用进程访问，同步更新所有节点的本地缓存数据成本较高。</li><li>应用进程重启，数据会丢失。</li></ul><p>所以，本地缓存适合存储一些不易改变或者低频改变的高热点数据。</p><p>2、Redis属于分布式缓存，特点：</p><ul><li>集群模式，支持大数据量存储</li><li>数据集中存储，保证数据的一致性</li><li>数据跨网络传输，性能低于本地缓存。但同一个机房，两台服务器之间请求跑一个来回也就需要500微秒，比起其优势，这点损耗完全可以忽略，这也是分布式缓存受欢迎的原因。</li><li>支持副本机制，有效的保证了高可用性。</li></ul><p><strong>如何实现一个分布式锁？</strong></p><ul><li>1、数据库表，性能比较差</li><li>2、使用Lua脚本 (包含 SETNX + EXPIRE 两条指令)</li><li>3、SET的扩展命令（SET key value [EX][PX] [NX|XX]）</li><li>4、Redlock 框架</li><li>5、Zookeeper Curator框架提供了现成的分布式锁</li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/02_Redis/Redis.html</id>
    <link href="https://mikeah2011.github.io/post/02_Redis/Redis.html"/>
    <published>2022-08-20T16:05:07.000Z</published>
    <summary>为了便于大家查找问题，了解全貌，整理个目录，我们可以快速全局了解关于Redis 缓存，面试官一般喜欢问哪些问题？ 接下来，我们逐条来看看每个问题及解决方案 *Redis 有哪些特性？** 性能高， 读的速度是100000次/s，写的速度是8…</summary>
    <title>Redis常见的问题及方案</title>
    <updated>2026-05-01T13:12:43.904Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="网络" scheme="https://mikeah2011.github.io/categories/%E7%BD%91%E7%BB%9C/"/>
    <category term="网络" scheme="https://mikeah2011.github.io/tags/%E7%BD%91%E7%BB%9C/"/>
    <category term="VPN" scheme="https://mikeah2011.github.io/tags/VPN/"/>
    <content>
      <![CDATA[<blockquote><p>飛機場VPN</p></blockquote><table><thead><tr><th align="center">序號</th><th align="center">飛機場</th><th align="center">基礎價</th><th align="center">流量</th><th align="center">時長</th><th>推薦星級</th></tr></thead><tbody><tr><td align="center">1</td><td align="center"><a href="https://www.quickq.io/apps?code=bz3zqwvz">QuickQ</a></td><td align="center">$15</td><td align="center">-</td><td align="center">30 天</td><td>⭐️</td></tr><tr><td align="center">2</td><td align="center"><a href="https://veee401.vip/register">Veee</a></td><td align="center">$12.99</td><td align="center">-</td><td align="center">90 天</td><td>⭐️</td></tr><tr><td align="center">3</td><td align="center"><a href="https://zfd.ink/auth/register?code=xUdQ">追風島</a></td><td align="center">¥19</td><td align="center">1000GB</td><td align="center">30 天</td><td>⭐️⭐️</td></tr><tr><td align="center">4</td><td align="center"><a href="https://portal.cgray.net/#/auth/register?code=MNs0IMkE&intro=">CGRAY</a></td><td align="center">¥16</td><td align="center">80GB</td><td align="center">30 天</td><td>⭐️⭐️</td></tr><tr><td align="center">5</td><td align="center"><a href="https://agneo.co/?rc=w5d9uyle">AgentNEO</a></td><td align="center">¥18</td><td align="center">20GB</td><td align="center">30 天</td><td>⭐️⭐️</td></tr><tr><td align="center">6</td><td align="center"><a href="https://rabbitpro.net/">RABBITPRO</a></td><td align="center">¥29</td><td align="center">200G</td><td align="center">30 天</td><td>⭐️⭐️</td></tr><tr><td align="center">7</td><td align="center"><a href="https://conyss.com/#/register?code=93OpmlRN">Conyss</a></td><td align="center">¥30</td><td align="center">20GB</td><td align="center">30 天</td><td>⭐️⭐️</td></tr><tr><td align="center">8</td><td align="center"><a href="https://paoche.info/#/register?code=7xRwd9ZX">超跑</a></td><td align="center">¥9.9</td><td align="center">30GB</td><td align="center">30 天</td><td>⭐️⭐️⭐️</td></tr><tr><td align="center">9</td><td align="center"><a href="https://hutao.cloud/auth/register?code=kbwp">Hutao</a></td><td align="center">¥9</td><td align="center">50GB</td><td align="center">31 天</td><td>⭐️⭐️⭐️</td></tr><tr><td align="center">10</td><td align="center"><a href="https://www.mojie.cyou/#/register?code=g3Wwabv2">魔戒</a></td><td align="center">¥12</td><td align="center">130GB</td><td align="center">不限時</td><td>⭐️⭐️⭐️⭐️</td></tr><tr><td align="center"></td><td align="center"></td><td align="center">¥1</td><td align="center">2GB</td><td align="center">不限時</td><td>⭐️⭐️⭐️⭐️⭐️</td></tr></tbody></table>]]>
    </content>
    <id>https://mikeah2011.github.io/post/Other/VPN.html</id>
    <link href="https://mikeah2011.github.io/post/Other/VPN.html"/>
    <published>2022-06-03T10:00:00.000Z</published>
    <summary>飛機場VPN | 序號 | 飛機場 | 基礎價 | 流量 | 時長 | 推薦星級 | | :--: | :----------------------------------------------------------: | :----…</summary>
    <title>VPN 笔记</title>
    <updated>2026-05-01T13:12:43.910Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="SQL" scheme="https://mikeah2011.github.io/categories/SQL/"/>
    <category term="MySQL" scheme="https://mikeah2011.github.io/tags/MySQL/"/>
    <content>
      <![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/6411.png" alt="图片"></p><blockquote><p>线上SQL的调优经验</p></blockquote><ul><li><code>slow_query_log</code> 日志中收集到的慢 SQL ，结合 <code>explain</code> 分析是否命中索引。</li><li>减少索引扫描行数，有针对性的优化慢 SQL。</li><li>建立联合索引，由于联合索引的每个叶子节点包含检索字段的信息，按最左前缀原则匹配后，再按其它条件过滤，减少回表的数据量。</li><li>还可以使用虚拟列和联合索引来提升复杂查询的执行效率。</li><li>监控sql执行情况，发邮件、短信报警，便于快速识别慢查询sql</li><li>打开数据库慢查询日志功能</li><li>简化业务逻辑</li><li>代码重构、优化</li><li>异步处理</li><li>sql优化</li><li>索引优化</li></ul><blockquote><p>SQL优化</p></blockquote><ol><li><p>分页优化。比如电梯直达，<code>limit 100000,10</code> 先查找起始的主键id，再通过<code>id&gt;#{value}</code>往后取10条</p></li><li><p>尽量使用<code>覆盖索引</code>，索引的叶节点中已经包含要查询的字段，减少<code>回表查询</code></p></li><li><p>SQL优化（索引优化、小表驱动大表、虚拟列、适当增加冗余字段减少连表查询、联合索引、排序优化、慢日志 Explain 分析执行计划）。</p><ol><li><p><code>where</code>子句</p><p>​    避免对字段进行<code>null</code>值判断、表达式操作等；</p><p>​    <code>where</code>表之间的连接，必须写在其他<code>where</code>条件之前；</p><p>​    可过滤掉最大数量记录的条件必须写在 <code>where</code> 子句的末尾，<code>having</code>最后；</p></li><li><p>优先考虑在<code>where</code>及<code>order by</code>涉及的列上建立索引；</p></li><li><p>避免在索引列上使用函数运算、<code>is null</code>和<code>is not null</code>；</p></li><li><p>避免使用<code>select *</code>;</p></li><li><p>使用 </p><p><code>union all</code> 代替 <code>union</code></p><p><code>exists</code>代替<code>in</code></p><p><code>not exists</code> 代替<code> not in</code></p><p>连接查询代替子查询；</p></li><li><p>批量操作</p></li><li><p>小表驱动大表；</p></li><li><p>多用limit</p></li><li><p>in中值太多  分页</p></li><li><p>增量查询</p></li><li><p>高效的分页</p></li><li><p>join表不宜过多</p></li><li><p>控制索引数量</p></li><li><p>选择合理的字段类型</p></li><li><p>提升group by 的效率</p></li><li><p>索引优化</p></li></ol></li><li><p>设计优化（避免使用NULL、用简单数据类型如int、减少 text 类型、分库分表）。</p></li><li><p>硬件优化（使用SSD 减少 I&#x2F;O 时间、足够大的网络带宽、尽量大的内存）</p></li></ol>]]>
    </content>
    <id>https://mikeah2011.github.io/post/01_MySQL/SQL%E4%BC%98%E5%8C%96.html</id>
    <link href="https://mikeah2011.github.io/post/01_MySQL/SQL%E4%BC%98%E5%8C%96.html"/>
    <published>2022-05-20T23:15:47.000Z</published>
    <summary>线上SQL的调优经验 `slow_query_log` 日志中收集到的慢 SQL ，结合 `explain` 分析是否命中索引。 减少索引扫描行数，有针对性的优化慢 SQL。 建立联合索引，由于联合索引的每个叶子节点包含检索字段的信息，按最…</summary>
    <title>MySQL优化经验总结</title>
    <updated>2026-05-01T13:12:43.902Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="Redis" scheme="https://mikeah2011.github.io/categories/Redis/"/>
    <category term="Redis" scheme="https://mikeah2011.github.io/tags/Redis/"/>
    <content>
      <![CDATA[<h1 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h1><h2 id="1-说说什么是Redis"><a href="#1-说说什么是Redis" class="headerlink" title="1.说说什么是Redis?"></a>1.说说什么是Redis?</h2><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iat60I3icga9By11qSyNsNe8IvaoJAhsvjP4uBaCFDOoXCiaNGffUAcYKKg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p>Redis图标</p><p>Redis是一种基于键值对（key-value）的NoSQL数据库。</p><p>比一般键值对数据库强大的地方，Redis中的value支持string（字符串）、hash（哈希）、 list（列表）、set（集合）、zset（有序集合）、Bitmaps（位图）、 HyperLogLog、GEO（地理信息定位）等多种数据结构，因此 Redis可以满足很多的应用场景。</p><p>而且因为Redis会将所有数据都存放在内存中，所以它的读写性能非常出色。</p><p>不仅如此，Redis还可以将内存的数据利用快照和日志的形式保存到硬盘上，这样在发生类似断电或者机器故障的时候，内存中的数据不会“丢失”。</p><p>除了上述功能以外，Redis还提供了键过期、发布订阅、事务、流水线、Lua脚本等附加功能。</p><p>总之，Redis是一款强大的性能利器。</p><h2 id="2-Redis可以用来干什么？"><a href="#2-Redis可以用来干什么？" class="headerlink" title="2.Redis可以用来干什么？"></a>2.Redis可以用来干什么？</h2><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatrrBMiatBgLc3eGcIXxSwu5vc3l8LAgNDh68pd4OPiapR25nzaUVmvp6Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p>Redis</p><ol><li><p>缓存</p><p>这是Redis应用最广泛地方，基本所有的Web应用都会使用Redis作为缓存，来降低数据源压力，提高响应速度。</p></li><li><p>计数器 Redis天然支持计数功能，而且计数性能非常好，可以用来记录浏览量、点赞量等等。</p></li><li><p>排行榜 Redis提供了列表和有序集合数据结构，合理地使用这些数据结构可以很方便地构建各种排行榜系统。</p></li><li><p>社交网络 赞&#x2F;踩、粉丝、共同好友&#x2F;喜好、推送、下拉刷新。</p></li><li><p>消息队列 Redis提供了发布订阅功能和阻塞队列的功能，可以满足一般消息队列功能。</p></li><li><p>分布式锁 分布式环境下，利用Redis实现分布式锁，也是Redis常见的应用。</p></li></ol><p>Redis的应用一般会结合项目去问，以一个电商项目的用户服务为例：</p><ul><li>Token存储：用户登录成功之后，使用Redis存储Token</li><li>登录失败次数计数：使用Redis计数，登录失败超过一定次数，锁定账号</li><li>地址缓存：对省市区数据的缓存</li><li>分布式锁：分布式环境下登录、注册等操作加分布式锁</li><li>……</li></ul><h2 id="3-Redis-有哪些数据结构？"><a href="#3-Redis-有哪些数据结构？" class="headerlink" title="3.Redis 有哪些数据结构？"></a>3.Redis 有哪些数据结构？</h2><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatGE9iatwDjj6iavP6gcrD5XXNyrry9WExaUTKSeWbraodiaq3PcoNwFK7Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">Redis有五种基本数据结构。</p><p><strong>string</strong></p><p>字符串最基础的数据结构。字符串类型的值实际可以是字符串（简单的字符串、复杂的字符串（例如JSON、XML））、数字 （整数、浮点数），甚至是二进制（图片、音频、视频），但是值最大不能超过512MB。</p><p>字符串主要有以下几个典型使用场景：</p><ul><li>缓存功能</li><li>计数</li><li>共享Session</li><li>限速</li></ul><p><strong>hash</strong></p><p>哈希类型是指键值本身又是一个键值对结构。</p><p>哈希主要有以下典型应用场景：</p><ul><li>缓存用户信息</li><li>缓存对象</li></ul><p><strong>list</strong></p><p>列表（list）类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构，它可以充当栈和队列的角色</p><p>列表主要有以下几种使用场景：</p><ul><li>消息队列</li><li>文章列表</li></ul><p><strong>set</strong></p><p>集合（set）类型也是用来保存多个的字符串元素，但和列表类型不一 样的是，集合中不允许有重复元素，并且集合中的元素是无序的。</p><p>集合主要有如下使用场景：</p><ul><li>标签（tag）</li><li>共同关注</li></ul><p><strong>sorted set</strong></p><p>有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是，它给每个元素设置一个权重（score）作为排序的依据。</p><p>有序集合主要应用场景：</p><ul><li>用户点赞统计</li><li>用户排序</li></ul><h2 id="4-Redis为什么快呢？"><a href="#4-Redis为什么快呢？" class="headerlink" title="4.Redis为什么快呢？"></a>4.Redis为什么快呢？</h2><p>Redis的速度⾮常的快，单机的Redis就可以⽀撑每秒十几万的并发，相对于MySQL来说，性能是MySQL的⼏⼗倍。速度快的原因主要有⼏点：</p><ol><li><strong>完全基于内存操作</strong></li><li>使⽤单线程，避免了线程切换和竞态产生的消耗</li><li>基于⾮阻塞的IO多路复⽤机制</li><li>C语⾔实现，优化过的数据结构，基于⼏种基础的数据结构，redis做了⼤量的优化，性能极⾼</li></ol><h2 id="5-能说一下I-O多路复用吗？"><a href="#5-能说一下I-O多路复用吗？" class="headerlink" title="5.能说一下I&#x2F;O多路复用吗？"></a>5.能说一下I&#x2F;O多路复用吗？</h2><p>引用知乎上一个高赞的回答来解释什么是I&#x2F;O多路复用。假设你是一个老师，让30个学生解答一道题目，然后检查学生做的是否正确，你有下面几个选择：</p><ul><li>第一种选择：按顺序逐个检查，先检查A，然后是B，之后是C、D。。。这中间如果有一个学生卡住，全班都会被耽误。这种模式就好比，你用循环挨个处理socket，根本不具有并发能力。</li><li>第二种选择：你创建30个分身，每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者- 线程处理连接。</li><li>第三种选择，你站在讲台上等，谁解答完谁举手。这时C、D举手，表示他们解答问题完毕，你下去依次检查C、D的答案，然后继续回到讲台上等。此时E、A又举手，然后去处理E和A。</li></ul><p>第一种就是阻塞IO模型，第三种就是I&#x2F;O复用模型。</p><p>多路复用模型</p><p>Linux系统有三种方式实现IO多路复用：select、poll和epoll。</p><p>例如epoll方式是将用户socket对应的fd注册进epoll，然后epoll帮你监听哪些socket上有消息到达，这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。</p><p>这样，整个过程只在进行select、poll、epoll这些调用的时候才会阻塞，收发客户消息是不会阻塞的，整个进程或者线程就被充分利用起来，这就是事件驱动，所谓的reactor模式。</p><h2 id="6-Redis为什么早期选择单线程？"><a href="#6-Redis为什么早期选择单线程？" class="headerlink" title="6. Redis为什么早期选择单线程？"></a>6. Redis为什么早期选择单线程？</h2><p>官方解释：<a href="https://redis.io/topics/faq">https://redis.io/topics/faq</a></p><p>官方FAQ表示，因为Redis是基于内存的操作，CPU成为Redis的瓶颈的情况很少见，Redis的瓶颈最有可能是内存的大小或者网络限制。</p><p>如果想要最大程度利用CPU，可以在一台机器上启动多个Redis实例。</p><p>PS：网上有这样的回答，吐槽官方的解释有些敷衍，其实就是历史原因，开发者嫌多线程麻烦，后来这个CPU的利用问题就被抛给了使用者。</p><p>同时FAQ里还提到了， Redis 4.0 之后开始变成多线程，除了主线程外，它也有后台线程在处理一些较为缓慢的操作，例如清理脏数据、无用连接的释放、大 Key 的删除等等。</p><h2 id="7-Redis6-0使用多线程是怎么回事"><a href="#7-Redis6-0使用多线程是怎么回事" class="headerlink" title="7.Redis6.0使用多线程是怎么回事?"></a>7.Redis6.0使用多线程是怎么回事?</h2><p>Redis不是说用单线程的吗？怎么6.0成了多线程的？</p><p>Redis6.0的多线程是用多线程来处理数据的<strong>读写和协议解析</strong>，但是Redis<strong>执行命令</strong>还是单线程的。</p><p>这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU，使⽤多线程能提升IO读写的效率，从⽽整体提⾼Redis的性能。</p><h1 id="持久化"><a href="#持久化" class="headerlink" title="持久化"></a>持久化</h1><h2 id="8-Redis持久化⽅式有哪些？有什么区别？"><a href="#8-Redis持久化⽅式有哪些？有什么区别？" class="headerlink" title="8.Redis持久化⽅式有哪些？有什么区别？"></a>8.Redis持久化⽅式有哪些？有什么区别？</h2><p>Redis持久化⽅案分为RDB和AOF两种。</p><p><strong>RDB</strong></p><p>RDB持久化是把当前进程数据生成<strong>快照</strong>保存到硬盘的过程，触发RDB持久化过程分为手动触发和自动触发。</p><p>RDB⽂件是⼀个压缩的⼆进制⽂件，通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的，所以即使Redis崩溃或者退出，只要RDB⽂件存在，就可以⽤它来恢复还原数据库的状态。</p><p>手动触发分别对应save和bgsave命令:</p><ul><li>save命令：阻塞当前Redis服务器，直到RDB过程完成为止，对于内存比较大的实例会造成长时间阻塞，线上环境不建议使用。</li><li>bgsave命令：Redis进程执行fork操作创建子进程，RDB持久化过程由子进程负责，完成后自动结束。阻塞只发生在fork阶段，一般时间很短。</li></ul><p>以下场景会自动触发RDB持久化：</p><ul><li>使用save相关配置，如“save m n”。表示m秒内数据集存在n次修改时，自动触发bgsave。</li><li>如果从节点执行全量复制操作，主节点自动执行bgsave生成RDB文件并发送给从节点</li><li>执行debug reload命令重新加载Redis时，也会自动触发save操作</li><li>默认情况下执行shutdown命令时，如果没有开启AOF持久化功能则自动执行bgsave。</li></ul><p><strong>AOF</strong></p><p>AOF（append only file）持久化：以独立日志的方式记录每次写命令， 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性，目前已经是Redis持久化的主流方式。</p><p>AOF的工作流程操作：命令写入 （append）、文件同步（sync）、文件重写（rewrite）、重启加载 （load）流程如下：</p><p>1）所有的写入命令会追加到aof_buf（缓冲区）中。</p><p>2）AOF缓冲区根据对应的策略向硬盘做同步操作。</p><p>3）随着AOF文件越来越大，需要定期对AOF文件进行重写，达到压缩 的目的。</p><p>4）当Redis服务器重启时，可以加载AOF文件进行数据恢复。</p><h2 id="9-RDB-和-AOF-各自有什么优缺点？"><a href="#9-RDB-和-AOF-各自有什么优缺点？" class="headerlink" title="9.RDB 和 AOF 各自有什么优缺点？"></a>9.RDB 和 AOF 各自有什么优缺点？</h2><p><strong>RDB | 优点</strong></p><ol><li>只有一个紧凑的二进制文件 <code>dump.rdb</code>，非常适合备份、全量复制的场景。</li><li><strong>容灾性好</strong>，可以把RDB文件拷贝道远程机器或者文件系统张，用于容灾恢复。</li><li><strong>恢复速度快</strong>，RDB恢复数据的速度远远快于AOF的方式</li></ol><p><strong>RDB | 缺点</strong></p><ol><li><strong>实时性低</strong>，RDB 是间隔一段时间进行持久化，没法做到实时持久化&#x2F;秒级持久化。如果在这一间隔事件发生故障，数据会丢失。</li><li><strong>存在兼容问题</strong>，Redis演进过程存在多个格式的RDB版本，存在老版本Redis无法兼容新版本RDB的问题。</li></ol><p><strong>AOF | 优点</strong></p><ol><li><strong>实时性好</strong>，aof 持久化可以配置 <code>appendfsync</code> 属性，有 <code>always</code>，每进行一次命令操作就记录到 aof 文件中一次。</li><li>通过 append 模式写文件，即使中途服务器宕机，可以通过 redis-check-aof 工具解决数据一致性问题。</li></ol><p><strong>AOF | 缺点</strong></p><ol><li>AOF 文件比 RDB <strong>文件大</strong>，且 <strong>恢复速度慢</strong>。</li><li><strong>数据集大</strong> 的时候，比 RDB <strong>启动效率低</strong>。</li></ol><h2 id="10-RDB和AOF如何选择？"><a href="#10-RDB和AOF如何选择？" class="headerlink" title="10.RDB和AOF如何选择？"></a>10.RDB和AOF如何选择？</h2><ul><li>一般来说， 如果想达到足以媲美数据库的 <strong>数据安全性</strong>，应该 <strong>同时使用两种持久化功能</strong>。在这种情况下，当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据，因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。</li><li>如果 <strong>可以接受数分钟以内的数据丢失</strong>，那么可以 <strong>只使用 RDB 持久化</strong>。</li><li>有很多用户都只使用 AOF 持久化，但并不推荐这种方式，因为定时生成 RDB 快照（snapshot）非常便于进行数据备份， 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快，除此之外，使用 RDB 还可以避免 AOF 程序的 bug。</li><li>如果只需要数据在服务器运行的时候存在，也可以不使用任何持久化方式。</li></ul><h2 id="11-Redis的数据恢复？"><a href="#11-Redis的数据恢复？" class="headerlink" title="11.Redis的数据恢复？"></a>11.Redis的数据恢复？</h2><p>当Redis发生了故障，可以从RDB或者AOF中恢复数据。</p><p>恢复的过程也很简单，把RDB或者AOF文件拷贝到Redis的数据目录下，如果使用AOF恢复，配置文件开启AOF，然后启动redis-server即可。</p><p><strong>Redis</strong> 启动时加载数据的流程：</p><ol><li>AOF持久化开启且存在AOF文件时，优先加载AOF文件。</li><li>AOF关闭或者AOF文件不存在时，加载RDB文件。</li><li>加载AOF&#x2F;RDB文件成功后，Redis启动成功。</li><li>AOF&#x2F;RDB文件存在错误时，Redis启动失败并打印错误信息。</li></ol><h2 id="12-Redis-4-0-的混合持久化了解吗？"><a href="#12-Redis-4-0-的混合持久化了解吗？" class="headerlink" title="12.Redis 4.0 的混合持久化了解吗？"></a>12.Redis 4.0 的混合持久化了解吗？</h2><p>重启 Redis 时，我们很少使用 <code>RDB</code> 来恢复内存状态，因为会丢失大量数据。我们通常使用 AOF 日志重放，但是重放 AOF 日志性能相对 <code>RDB</code> 来说要慢很多，这样在 Redis 实例很大的情况下，启动需要花费很长的时间。</p><p><strong>Redis 4.0</strong> 为了解决这个问题，带来了一个新的持久化选项——<strong>混合持久化</strong>。将 <code>rdb</code> 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志，而是 <strong>自持久化开始到持久化结束</strong> 的这段时间发生的增量 AOF 日志，通常这部分 AOF 日志很小：</p><p>于是在 Redis 重启的时候，可以先加载 <code>rdb</code> 的内容，然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放，重启效率因此大幅得到提升。</p><h1 id="高可用"><a href="#高可用" class="headerlink" title="高可用"></a>高可用</h1><p>Redis保证高可用主要有三种方式：主从、哨兵、集群。</p><h2 id="13-主从复制了解吗？"><a href="#13-主从复制了解吗？" class="headerlink" title="13.主从复制了解吗？"></a>13.主从复制了解吗？</h2><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatLD7fUVicic3VU2RSul8o5FtwMHysK4Da9wOPicD1XOicy0IcsVgSgjjE0Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">Redis主从复制简图</p><p><strong>主从复制</strong>，是指将一台 Redis 服务器的数据，复制到其他的 Redis 服务器。前者称为 <strong>主节点(master)</strong>，后者称为 <strong>从节点(slave)</strong>。且数据的复制是 <strong>单向</strong> 的，只能由主节点到从节点。Redis 主从复制支持 <strong>主从同步</strong> 和 <strong>从从同步</strong> 两种，后者是 Redis 后续版本新增的功能，以减轻主节点的同步负担。</p><blockquote><p>主从复制主要的作用?</p></blockquote><ul><li><strong>数据冗余：</strong> 主从复制实现了数据的热备份，是持久化之外的一种数据冗余方式。</li><li><strong>故障恢复：</strong> 当主节点出现问题时，可以由从节点提供服务，实现快速的故障恢复 <em>(实际上是一种服务的冗余)</em>。</li><li><strong>负载均衡：</strong> 在主从复制的基础上，配合读写分离，可以由主节点提供写服务，由从节点提供读服务 <em>（即写 Redis 数据时应用连接主节点，读 Redis 数据时应用连接从节点）</em>，分担服务器负载。尤其是在写少读多的场景下，通过多个从节点分担读负载，可以大大提高 Redis 服务器的并发量。</li><li><strong>高可用基石：</strong> 除了上述作用以外，主从复制还是哨兵和集群能够实施的 <strong>基础</strong>，因此说主从复制是 Redis 高可用的基础。</li></ul><h2 id="14-Redis主从有几种常见的拓扑结构？"><a href="#14-Redis主从有几种常见的拓扑结构？" class="headerlink" title="14.Redis主从有几种常见的拓扑结构？"></a>14.Redis主从有几种常见的拓扑结构？</h2><p>Redis的复制拓扑结构可以支持单层或多层复制关系，根据拓扑复杂性可以分为以下三种：一主一从、一主多从、树状主从结构。</p><p>1.一主一从结构</p><p>一主一从结构是最简单的复制拓扑结构，用于主节点出现宕机时从节点提供故障转移支持。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatHxPZOg5XsibcVUgTvFpGMpmkspmDU5J6YSxzwX1kzZdUp1T2l1Rj7Kw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">2.一主多从结构</p><p>一主多从结构（又称为星形拓扑结构）使得应用端可以利用多个从节点实现读写分离（见图6-5）。对于读占比较大的场景，可以把读命令发送到从节点来分担主节点压力。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatD0icwxuse1PEIC5Cp5j05rnOTowEbpHk1ZVCgkLnUAzRpn5ICxg7QTg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">3.树状主从结构</p><p>树状主从结构（又称为树状拓扑结构）使得从节点不但可以复制主节点数据，同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层，可以有效降低主节点负载和需要传送给从节点的数据量。</p><h2 id="15-Redis的主从复制原理了解吗？"><a href="#15-Redis的主从复制原理了解吗？" class="headerlink" title="15.Redis的主从复制原理了解吗？"></a>15.Redis的主从复制原理了解吗？</h2><p>Redis主从复制的工作流程大概可以分为如下几步：</p><ol><li>保存主节点（master）信息 这一步只是保存主节点信息，保存主节点的ip和port。</li><li>主从建立连接 从节点（slave）发现新的主节点后，会尝试和主节点建立网络连接。</li><li>发送ping命令 连接建立成功后从节点发送ping请求进行首次通信，主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。</li><li>权限验证 如果主节点要求密码验证，从节点必须正确的密码才能通过验证。</li><li>同步数据集 主从复制连接正常通信后，主节点会把持有的数据全部发送给从节点。</li><li>命令持续复制 接下来主节点会持续地把写命令发送给从节点，保证主从数据一致性。</li></ol><h2 id="16-说说主从数据同步的方式？"><a href="#16-说说主从数据同步的方式？" class="headerlink" title="16.说说主从数据同步的方式？"></a>16.说说主从数据同步的方式？</h2><p>Redis在2.8及以上版本使用psync命令完成主从数据同步，同步过程分为：全量复制和部分复制。</p><p>主从数据同步方式</p><p><strong>全量复制</strong>一般用于初次复制场景，Redis早期支持的复制功能只有全量复制，它会把主节点全部数据一次性发送给从节点，当数据量较大时，会对主从节点和网络造成很大的开销。</p><p>全量复制的完整运行流程如下：</p><ol><li>发送psync命令进行数据同步，由于是第一次进行复制，从节点没有复制偏移量和主节点的运行ID，所以发送psync-1。</li><li>主节点根据psync-1解析出当前为全量复制，回复+FULLRESYNC响应。</li><li>从节点接收主节点的响应数据保存运行ID和偏移量offset</li><li>主节点执行bgsave保存RDB文件到本地</li><li>主节点发送RDB文件给从节点，从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件</li><li>对于从节点开始接收RDB快照到接收完成期间，主节点仍然响应读写命令，因此主节点会把这期间写命令数据保存在复制客户端缓冲区内，当从节点加载完RDB文件后，主节点再把缓冲区内的数据发送给从节点，保证主从之间数据一致性。</li><li>从节点接收完主节点传送来的全部数据后会清空自身旧数据</li><li>从节点清空数据后开始加载RDB文件</li><li>从节点成功加载完RDB后，如果当前节点开启了AOF持久化功能， 它会立刻做bgrewriteaof操作，为了保证全量复制后AOF持久化文件立刻可用。</li></ol><p><strong>部分复制</strong>部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施， 使用psync{runId}{offset}命令实现。当从节点（slave）正在复制主节点 （master）时，如果出现网络闪断或者命令丢失等异常情况时，从节点会向 主节点要求补发丢失的命令数据，如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点，这样就可以保持主从节点复制的一致性。</p><ol><li>当主从节点之间网络出现中断时，如果超过repl-timeout时间，主节点会认为从节点故障并中断复制连接</li><li>主从连接中断期间主节点依然响应命令，但因复制连接中断命令无法发送给从节点，不过主节点内部存在的复制积压缓冲区，依然可以保存最近一段时间的写命令数据，默认最大缓存1MB。</li><li>当主从节点网络恢复后，从节点会再次连上主节点</li><li>当主从连接恢复后，由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点，要求进行部分复制操作。</li><li>主节点接到psync命令后首先核对参数runId是否与自身一致，如果一 致，说明之前复制的是当前主节点；之后根据参数offset在自身复制积压缓冲区查找，如果偏移量之后的数据存在缓冲区中，则对从节点发送+CONTINUE响应，表示可以进行部分复制。</li><li>主节点根据偏移量把复制积压缓冲区里的数据发送给从节点，保证主从复制进入正常状态。</li></ol><h2 id="17-主从复制存在哪些问题呢？"><a href="#17-主从复制存在哪些问题呢？" class="headerlink" title="17.主从复制存在哪些问题呢？"></a>17.主从复制存在哪些问题呢？</h2><p>主从复制虽好，但也存在一些问题：</p><ul><li>一旦主节点出现故障，需要手动将一个从节点晋升为主节点，同时需要修改应用方的主节点地址，还需要命令其他从节点去复制新的主节点，整个过程都需要人工干预。</li><li>主节点的写能力受到单机的限制。</li><li>主节点的存储能力受到单机的限制。</li></ul><p>第一个问题是Redis的高可用问题，第二、三个问题属于Redis的分布式问题。</p><h2 id="18-Redis-Sentinel（哨兵）了解吗？"><a href="#18-Redis-Sentinel（哨兵）了解吗？" class="headerlink" title="18.Redis Sentinel（哨兵）了解吗？"></a>18.Redis Sentinel（哨兵）了解吗？</h2><p>主从复制存在一个问题，没法完成自动故障转移。所以我们需要一个方案来完成自动故障转移，它就是Redis Sentinel（哨兵）。</p><p>Redis Sentinel</p><p>Redis Sentinel ，它由两部分组成，哨兵节点和数据节点：</p><ul><li><strong>哨兵节点：</strong> 哨兵系统由一个或多个哨兵节点组成，哨兵节点是特殊的 Redis 节点，不存储数据，对数据节点进行监控。</li><li><strong>数据节点：</strong> 主节点和从节点都是数据节点；</li></ul><p>在复制的基础上，哨兵实现了 <strong>自动化的故障恢复</strong> 功能，下面是官方对于哨兵功能的描述：</p><ul><li><strong>监控（Monitoring）：</strong> 哨兵会不断地检查主节点和从节点是否运作正常。</li><li><strong>自动故障转移（Automatic failover）：</strong> 当 <strong>主节点</strong> 不能正常工作时，哨兵会开始 <strong>自动故障转移操作</strong>，它会将失效主节点的其中一个 <strong>从节点升级为新的主节点</strong>，并让其他从节点改为复制新的主节点。</li><li><strong>配置提供者（Configuration provider）：</strong> 客户端在初始化时，通过连接哨兵来获得当前 Redis 服务的主节点地址。</li><li><strong>通知（Notification）：</strong> 哨兵可以将故障转移的结果发送给客户端。</li></ul><p>其中，监控和自动故障转移功能，使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能，则需要在与客户端的交互中才能体现。</p><h2 id="19-Redis-Sentinel（哨兵）实现原理知道吗？"><a href="#19-Redis-Sentinel（哨兵）实现原理知道吗？" class="headerlink" title="19.Redis Sentinel（哨兵）实现原理知道吗？"></a>19.Redis Sentinel（哨兵）实现原理知道吗？</h2><p>哨兵模式是通过哨兵节点完成对数据节点的监控、下线、故障转移。</p><ul><li><p><strong>定时监控</strong>Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控：</p></li><li><ol><li>每隔10秒，每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构</li><li>每隔2秒，每个Sentinel节点会向Redis数据节点的__sentinel__：hello 频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息</li><li>每隔1秒，每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测，来确认这些节点当前是否可达</li></ol></li><li><p><strong>主观下线和客观下线</strong>主观下线就是哨兵节点认为某个节点有问题，客观下线就是超过一定数量的哨兵节点认为主节点有问题。</p></li></ul><ol><li>主观下线 每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测，当这些节点超过 down-after-milliseconds没有进行有效回复，Sentinel节点就会对该节点做失败判定，这个行为叫做主观下线。</li><li>客观下线 当Sentinel主观下线的节点是主节点时，该Sentinel节点会通过sentinel is- master-down-by-addr命令向其他Sentinel节点询问对主节点的判断，当超过 <quorum>个数，Sentinel节点认为主节点确实有问题，这时该Sentinel节点会做出客观下线的决定</li></ol><ul><li><p><strong>领导者Sentinel节点选举</strong>Sentinel节点之间会做一个领导者选举的工作，选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举。</p></li><li><p><strong>故障转移</strong></p><p>领导者选举出的Sentinel节点负责故障转移，过程如下：</p></li><li><ol><li>在从节点列表中选出一个节点作为新的主节点，这一步是相对复杂一些的一步</li><li>Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点</li><li>Sentinel领导者节点会向剩余的从节点发送命令，让它们成为新主节点的从节点</li><li>Sentinel节点集合会将原来的主节点更新为从节点，并保持着对其关注，当其恢复后命令它去复制新的主节点</li></ol></li></ul><h2 id="20-领导者Sentinel节点选举了解吗？"><a href="#20-领导者Sentinel节点选举了解吗？" class="headerlink" title="20.领导者Sentinel节点选举了解吗？"></a>20.领导者Sentinel节点选举了解吗？</h2><p>Redis使用了Raft算法实 现领导者选举，大致流程如下：</p><ol><li>每个在线的Sentinel节点都有资格成为领导者，当它确认主节点主观 下线时候，会向其他Sentinel节点发送sentinel is-master-down-by-addr命令， 要求将自己设置为领导者。</li><li>收到命令的Sentinel节点，如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令，将同意该请求，否则拒绝。</li><li>如果该Sentinel节点发现自己的票数已经大于等于max（quorum， num（sentinels）&#x2F;2+1），那么它将成为领导者。</li><li>如果此过程没有选举出领导者，将进入下一次选举。</li></ol><h2 id="21-新的主节点是怎样被挑选出来的？"><a href="#21-新的主节点是怎样被挑选出来的？" class="headerlink" title="21.新的主节点是怎样被挑选出来的？"></a>21.新的主节点是怎样被挑选出来的？</h2><p>选出新的主节点，大概分为这么几步：</p><ol><li>过滤：“不健康”（主观下线、断线）、5秒内没有回复过Sentinel节 点ping响应、与主节点失联超过down-after-milliseconds*10秒。</li><li>选择slave-priority（从节点优先级）最高的从节点列表，如果存在则返回，不存在则继续。</li><li>选择复制偏移量最大的从节点（复制的最完整），如果存在则返 回，不存在则继续。</li><li>选择runid最小的从节点。</li></ol><h2 id="22-Redis-集群了解吗？"><a href="#22-Redis-集群了解吗？" class="headerlink" title="22.Redis 集群了解吗？"></a>22.Redis 集群了解吗？</h2><p>前面说到了主从存在高可用和分布式的问题，哨兵解决了高可用的问题，而集群就是终极方案，一举解决高可用和分布式问题。</p><ol><li><strong>数据分区：</strong> 数据分区 <em>(或称数据分片)</em> 是集群最核心的功能。集群将数据分散到多个节点，一方面 突破了 Redis 单机内存大小的限制，<strong>存储容量大大增加</strong>；<strong>另一方面</strong> 每个主节点都可以对外提供读服务和写服务，<strong>大大提高了集群的响应能力</strong>。</li><li><strong>高可用：</strong> 集群支持主从复制和主节点的 <strong>自动故障转移</strong> <em>（与哨兵类似）</em>，当任一节点发生故障时，集群仍然可以对外提供服务。</li></ol><h2 id="23-集群中数据如何分区？"><a href="#23-集群中数据如何分区？" class="headerlink" title="23.集群中数据如何分区？"></a>23.集群中数据如何分区？</h2><p>分布式的存储中，要把数据集按照分区规则映射到多个节点，常见的数据分区规则三种：</p><h4 id="方案一：节点取余分区"><a href="#方案一：节点取余分区" class="headerlink" title="方案一：节点取余分区"></a>方案一：节点取余分区</h4><p>节点取余分区，非常好理解，使用特定的数据，比如Redis的键，或者用户ID之类，对响应的hash值取余：hash（key）%N，来确定数据映射到哪一个节点上。</p><p>不过该方案最大的问题是，当节点数量变化时，如扩容或收缩节点，数据节点映射关 系需要重新计算，会导致数据的重新迁移。</p><p>节点取余分区</p><h4 id="方案二：一致性哈希分区"><a href="#方案二：一致性哈希分区" class="headerlink" title="方案二：一致性哈希分区"></a>方案二：一致性哈希分区</h4><p>将整个 Hash 值空间组织成一个虚拟的圆环，然后将缓存节点的 IP 地址或者主机名做 Hash 取值后，放置在这个圆环上。当我们需要确定某一个 Key 需 要存取到哪个节点上的时候，先对这个 Key 做同样的 Hash 取值，确定在环上的位置，然后按照顺时针方向在环上“行走”，遇到的第一个缓存节点就是要访问的节点。</p><p>比如说下面 这张图里面，Key 1 和 Key 2 会落入到 Node 1 中，Key 3、Key 4 会落入到 Node 2 中，Key 5 落入到 Node 3 中，Key 6 落入到 Node 4 中。</p><p>这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中 相邻的节点，对其他节点无影响。</p><p>但它还是存在问题：</p><ul><li>缓存节点在圆环上分布不平均，会造成部分缓存节点的压力较大</li><li>当某个节点故障时，这个节点所要承担的所有访问都会被顺移到另一个节点上，会对后面这个节点造成力。</li></ul><h4 id="方案三：虚拟槽分区"><a href="#方案三：虚拟槽分区" class="headerlink" title="方案三：虚拟槽分区"></a>方案三：虚拟槽分区</h4><p>这个方案 一致性哈希分区的基础上，引入了 <strong>虚拟节点</strong> 的概念。Redis 集群使用的便是该方案，其中的虚拟节点称为 <strong>槽（slot）</strong>。槽是介于数据和实际节点之间的虚拟概念，每个实际节点包含一定数量的槽，每个槽包含哈希值在一定范围内的数据。</p><p>在使用了槽的一致性哈希分区中，槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点 之间的关系，增加或删除节点对系统的影响很小。仍以上图为例，系统中有 <code>4</code> 个实际节点，假设为其分配 <code>16</code> 个槽(0-15)；</p><ul><li>槽 0-3 位于 node1；4-7 位于 node2；以此类推….</li></ul><p>如果此时删除 <code>node2</code>，只需要将槽 4-7 重新分配即可，例如槽 4-5 分配给 <code>node1</code>，槽 6 分配给 <code>node3</code>，槽 7 分配给 <code>node4</code>，数据在其他节点的分布仍然较为均衡。</p><h2 id="24-能说说Redis集群的原理吗？"><a href="#24-能说说Redis集群的原理吗？" class="headerlink" title="24.能说说Redis集群的原理吗？"></a>24.能说说Redis集群的原理吗？</h2><p>Redis集群通过数据分区来实现数据的分布式存储，通过自动故障转移实现高可用。</p><h4 id="集群创建"><a href="#集群创建" class="headerlink" title="集群创建"></a>集群创建</h4><p>数据分区是在集群创建的时候完成的。</p><p><strong>设置节点</strong>Redis集群一般由多个节点组成，节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes，让Redis运行在集群模式下。<strong>节点握手</strong>节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信， 达到感知对方的过程。节点握手是集群彼此通信的第一步，由客户端发起命 令：cluster meet{ip}{port}。完成节点握手之后，一个个的Redis节点就组成了一个多节点的集群。</p><p>**分配槽（slot）**Redis集群把所有的数据映射到16384个槽中。每个节点对应若干个槽，只有当节点分配了槽，才能响应和这些槽关联的键命令。通过 cluster addslots命令为节点分配槽。</p><p>分配槽</p><h4 id="故障转移"><a href="#故障转移" class="headerlink" title="故障转移"></a>故障转移</h4><p>Redis集群的故障转移和哨兵的故障转移类似，但是Redis集群中所有的节点都要承担状态维护的任务。</p><p><strong>故障发现</strong>Redis集群内节点通过ping&#x2F;pong消息实现节点通信，集群中每个节点都会定期向其他节点发送ping消息，接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信一直失败，则发送节 点会认为接收节点存在故障，把接收节点标记为主观下线（pfail）状态。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatxkibaNUVTkZz7hs6icfTMdLQzpFhszOZiciaT1Fic1ickbNgmWRN9cEwJtZw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">当某个节点判断另一个节点主观下线后，相应的节点状态会跟随消息在集群内传播。通过Gossip消息传播，集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatK447hYI8W5ugzWowQ6TRZtsuEHh4m1jQ0sxq2VZz1kl4jTmnictwuHg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p><strong>故障恢复</strong></p><p>故障节点变为客观下线后，如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它，从而保证集群的高可用。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatJ6VpUzQ5Uz9hq24BsnqseiataQ8x6ToCk41QnqrMbAw8iahYD3JUQic9Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">故障恢复流程</p><ol><li>资格检查 每个从节点都要检查最后与主节点断线时间，判断是否有资格替换故障 的主节点。</li><li>准备选举时间 当从节点符合故障转移资格后，更新触发故障选举的时间，只有到达该 时间后才能执行后续流程。</li><li>发起选举 当从节点定时任务检测到达故障选举时间（failover_auth_time）到达后，发起选举流程。</li><li>选举投票 持有槽的主节点处理故障选举消息。投票过程其实是一个领导者选举的过程，如集群内有N个持有槽的主节 点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点，因此只能有一个从节点获得N&#x2F;2+1的选票，保证能够找出唯一的从节点。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatbeOyjLjqq4eBSyBibGr3RCC1UHre5ianicxW1PmL9PtGibuNFcicGUfyJ8Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></li><li>替换主节点 当从节点收集到足够的选票之后，触发替换主节点操作。</li></ol><blockquote><p><strong>部署Redis集群至少需要几个物理节点？</strong></p></blockquote><p>在投票选举的环节，故障主节点也算在投票数内，假设集群内节点规模是3主3从，其中有2 个主节点部署在一台机器上，当这台机器宕机时，由于从节点无法收集到 3&#x2F;2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。</p><h2 id="25-说说集群的伸缩？"><a href="#25-说说集群的伸缩？" class="headerlink" title="25.说说集群的伸缩？"></a>25.说说集群的伸缩？</h2><p>Redis集群提供了灵活的节点扩容和收缩方案，可以在不影响集群对外服务的情况下，为集群添加节点进行扩容也可以下线部分节点进行缩容。其实，集群扩容和缩容的关键点，就在于槽和节点的对应关系，扩容和缩容就是将一部分<code>槽</code>和<code>数据</code>迁移给新节点。</p><p>例如下面一个集群，每个节点对应若干个槽，每个槽对应一定的数据，如果希望加入1个节点希望实现集群扩容时，需要通过相关命令把一部分槽和内容迁移给新节点。缩容也是类似，先把槽和数据迁移到其它节点，再把对应的节点下线。</p><h1 id="缓存设计"><a href="#缓存设计" class="headerlink" title="缓存设计"></a>缓存设计</h1><h2 id="26-什么是缓存击穿、缓存穿透、缓存雪崩？"><a href="#26-什么是缓存击穿、缓存穿透、缓存雪崩？" class="headerlink" title="26.什么是缓存击穿、缓存穿透、缓存雪崩？"></a>26.什么是缓存击穿、缓存穿透、缓存雪崩？</h2><p>PS:这是多年黄历的老八股了，一定要理解清楚。</p><h4 id="缓存击穿"><a href="#缓存击穿" class="headerlink" title="缓存击穿"></a>缓存击穿</h4><p>一个并发访问量比较大的key在某个时间过期，导致所有的请求直接打在DB上。</p><p>解决⽅案：</p><ol><li>加锁更新，⽐如请求查询A，发现缓存中没有，对A这个key加锁，同时去数据库查询数据，写⼊缓存，再返回给⽤户，这样后⾯的请求就可以从缓存中拿到数据了。</li><li>将过期时间组合写在value中，通过异步的⽅式不断的刷新过期时间，防⽌此类现象。</li></ol><h4 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h4><p>缓存穿透指的查询缓存和数据库中都不存在的数据，这样每次请求直接打到数据库，就好像缓存不存在一样。</p><p>缓存穿透将导致不存在的数据每次请求都要到存储层去查询，失去了缓存保护后端存储的意义。</p><p>缓存穿透可能会使后端存储负载加大，如果发现大量存储层空命中，可能就是出现了缓存穿透问题。</p><p>缓存穿透可能有两种原因：</p><ol><li>自身业务代码问题</li><li>恶意攻击，爬虫造成空命中</li></ol><p>它主要有两种解决办法：</p><ul><li><strong>缓存空值&#x2F;默认值</strong></li></ul><p>一种方式是在数据库不命中之后，把一个空对象或者默认值保存到缓存，之后再访问这个数据，就会从缓存中获取，这样就保护了数据库。</p><p>缓存空值&#x2F;默认值</p><p>缓存空值有两大问题：</p><ol><li>空值做了缓存，意味着缓存层中存了更多的键，需要更多的内存空间（如果是攻击，问题更严重），比较有效的方法是针对这类数据设置一个较短的过期时间，让其自动剔除。</li><li>缓存层和存储层的数据会有一段时间窗口的不一致，可能会对业务有一定影响。例如过期时间设置为5分钟，如果此时存储层添加了这个数据，那此段时间就会出现缓存层和存储层数据的不一致。这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。</li></ol><ul><li><strong>布隆过滤器</strong>除了缓存空对象，我们还可以在存储和缓存之前，加一个布隆过滤器，做一层过滤。</li></ul><p>布隆过滤器里会保存数据是否存在，如果判断数据不不能再，就不会访问存储。两种解决方案的对比：</p><h4 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h4><p>某⼀时刻发⽣⼤规模的缓存失效的情况，例如缓存服务宕机、大量key在同一时间过期，这样的后果就是⼤量的请求进来直接打到DB上，可能导致整个系统的崩溃，称为雪崩。</p><p>缓存雪崩是三大缓存问题里最严重的一种，我们来看看怎么预防和处理。</p><ul><li><strong>提高缓存可用性</strong></li></ul><ol><li>集群部署：通过集群来提升缓存的可用性，可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。</li><li>多级缓存：设置多级缓存，第一级缓存失效的基础上，访问二级缓存，每一级缓存的失效时间都不同。</li></ol><ul><li><strong>过期时间</strong></li></ul><ol><li>均匀过期：为了避免大量的缓存在同一时间过期，可以把不同的 key 过期时间随机生成，避免过期时间太过集中。</li><li>热点数据永不过期。</li></ol><ul><li><strong>熔断降级</strong></li></ul><ol><li>服务熔断：当缓存服务器宕机或超时响应时，为了防止整个系统出现雪崩，暂时停止业务服务访问缓存系统。</li><li>服务降级：当出现大量缓存失效，而且处在高并发高负荷的情况下，在业务系统内部暂时舍弃对一些非核心的接口和数据的请求，而直接返回一个提前准备好的 fallback（退路）错误处理信息。</li></ol><h2 id="27-能说说布隆过滤器吗？"><a href="#27-能说说布隆过滤器吗？" class="headerlink" title="27.能说说布隆过滤器吗？"></a>27.能说说布隆过滤器吗？</h2><p>布隆过滤器，它是一个连续的数据结构，每个存储位存储都是一个<code>bit</code>，即<code>0</code>或者<code>1</code>, 来标识数据是否存在。</p><p>存储数据的时时候，使用K个不同的哈希函数将这个变量映射为bit列表的的K个点，把它们置为1。</p><p>我们判断缓存key是否存在，同样，K个哈希函数，映射到bit列表上的K个点，判断是不是1：</p><ul><li>如果全不是1，那么key不存在；</li><li>如果都是1，也只是表示key可能存在。</li></ul><p>布隆过滤器也有一些缺点：</p><ol><li>它在判断元素是否在集合中时是有一定错误几率，因为哈希算法有一定的碰撞的概率。</li><li>不支持删除元素。</li></ol><h2 id="28-如何保证缓存和数据库数据的⼀致性？"><a href="#28-如何保证缓存和数据库数据的⼀致性？" class="headerlink" title="28.如何保证缓存和数据库数据的⼀致性？"></a>28.如何保证缓存和数据库数据的⼀致性？</h2><p>根据CAP理论，在保证可用性和分区容错性的前提下，无法保证一致性，所以缓存和数据库的绝对一致是不可能实现的，只能尽可能保存缓存和数据库的最终一致性。</p><h4 id="选择合适的缓存更新策略"><a href="#选择合适的缓存更新策略" class="headerlink" title="选择合适的缓存更新策略"></a>选择合适的缓存更新策略</h4><p><strong>1. 删除缓存而不是更新缓存</strong></p><p>当一个线程对缓存的key进行写操作的时候，如果其它线程进来读数据库的时候，读到的就是脏数据，产生了数据不一致问题。</p><p>相比较而言，删除缓存的速度比更新缓存的速度快很多，所用时间相对也少很多，读脏数据的概率也小很多。</p><ol><li><strong>先更数据，后删缓存</strong>先更数据库还是先删缓存？这是一个问题。</li></ol><p>更新数据，耗时可能在删除缓存的百倍以上。在缓存中不存在对应的key，数据库又没有完成更新的时候，如果有线程进来读取数据，并写入到缓存，那么在更新成功之后，这个key就是一个脏数据。</p><p>毫无疑问，先删缓存，再更数据库，缓存中key不存在的时间的时间更长，有更大的概率会产生脏数据。</p><p>目前最流行的缓存读写策略cache-aside-pattern就是采用先更数据库，再删缓存的方式。</p><h4 id="缓存不一致处理"><a href="#缓存不一致处理" class="headerlink" title="缓存不一致处理"></a>缓存不一致处理</h4><p>如果不是并发特别高，对缓存依赖性很强，其实一定程序的不一致是可以接受的。</p><p>但是如果对一致性要求比较高，那就得想办法保证缓存和数据库中数据一致。</p><p>缓存和数据库数据不一致常见的两种原因：</p><ul><li>缓存key删除失败</li><li>并发导致写入了脏数据</li></ul><p>缓存一致性</p><p><strong>消息队列保证key被删除</strong>可以引入消息队列，把要删除的key或者删除失败的key丢尽消息队列，利用消息队列的重试机制，重试删除对应的key。</p><p>这种方案看起来不错，缺点是对业务代码有一定的侵入性。</p><p><strong>数据库订阅+消息队列保证key被删除</strong>可以用一个服务（比如阿里的 canal）去监听数据库的binlog，获取需要操作的数据。</p><p>然后用一个公共的服务获取订阅程序传来的信息，进行缓存删除操作。这种方式降低了对业务的侵入，但其实整个系统的复杂度是提升的，适合基建完善的大厂。</p><p><strong>延时双删防止脏数据</strong>还有一种情况，是在缓存不存在的时候，写入了脏数据，这种情况在先删缓存，再更数据库的缓存更新策略下发生的比较多，解决方案是延时双删。</p><p>简单说，就是在第一次删除缓存之后，过了一段时间之后，再次删除缓存。</p><p>延时双删</p><p>这种方式的延时时间设置需要仔细考量和测试。</p><p><strong>设置缓存过期时间兜底</strong></p><p>这是一个朴素但是有用的办法，给缓存设置一个合理的过期时间，即使发生了缓存数据不一致的问题，它也不会永远不一致下去，缓存过期的时候，自然又会恢复一致。</p><h2 id="29-如何保证本地缓存和分布式缓存的一致？"><a href="#29-如何保证本地缓存和分布式缓存的一致？" class="headerlink" title="29.如何保证本地缓存和分布式缓存的一致？"></a>29.如何保证本地缓存和分布式缓存的一致？</h2><p>PS:这道题面试很少问，但实际工作中很常见。</p><p>在日常的开发中，我们常常采用两级缓存：本地缓存+分布式缓存。</p><p>所谓本地缓存，就是对应服务器的内存缓存，比如Caffeine，分布式缓存基本就是采用Redis。</p><p>那么问题来了，本地缓存和分布式缓存怎么保持数据一致？Redis缓存，数据库发生更新，直接删除缓存的key即可，因为对于应用系统而言，它是一种中心化的缓存。</p><p>但是本地缓存，它是非中心化的，散落在分布式服务的各个节点上，没法通过客户端的请求删除本地缓存的key，所以得想办法通知集群所有节点，删除对应的本地缓存key。</p><p>可以采用消息队列的方式：</p><ol><li>采用Redis本身的Pub&#x2F;Sub机制，分布式集群的所有节点订阅删除本地缓存频道，删除Redis缓存的节点，同事发布删除本地缓存消息，订阅者们订阅到消息后，删除对应的本地key。但是Redis的发布订阅不是可靠的，不能保证一定删除成功。</li><li>引入专业的消息队列，比如RocketMQ，保证消息的可靠性，但是增加了系统的复杂度。</li><li>设置适当的过期时间兜底，本地缓存可以设置相对短一些的过期时间。</li></ol><h2 id="30-怎么处理热key？"><a href="#30-怎么处理热key？" class="headerlink" title="30.怎么处理热key？"></a>30.怎么处理热key？</h2><blockquote><p>**什么是热Key？**所谓的热key，就是访问频率比较的key。</p></blockquote><p>比如，热门新闻事件或商品，这类key通常有大流量的访问，对存储这类信息的 Redis来说，是不小的压力。</p><p>假如Redis集群部署，热key可能会造成整体流量的不均衡，个别节点出现OPS过大的情况，极端情况下热点key甚至会超过 Redis本身能够承受的OPS。</p><blockquote><p><strong>怎么处理热key？</strong></p></blockquote><p>对热key的处理，最关键的是对热点key的监控，可以从这些端来监控热点key:</p><ol><li>客户端 客户端其实是距离key“最近”的地方，因为Redis命令就是从客户端发出的，例如在客户端设置全局字典（key和调用次数），每次调用Redis命令时，使用这个字典进行记录。</li><li>代理端 像Twemproxy、Codis这些基于代理的Redis分布式架构，所有客户端的请求都是通过代理端完成的，可以在代理端进行收集统计。</li><li>Redis服务端 使用monitor命令统计热点key是很多开发和运维人员首先想到，monitor命令可以监控到Redis执行的所有命令。</li></ol><p>只要监控到了热key，对热key的处理就简单了：</p><ol><li>把热key打散到不同的服务器，降低压⼒</li><li>加⼊⼆级缓存，提前加载热key数据到内存中，如果redis宕机，⾛内存查询</li></ol><h2 id="31-缓存预热怎么做呢？"><a href="#31-缓存预热怎么做呢？" class="headerlink" title="31.缓存预热怎么做呢？"></a>31.缓存预热怎么做呢？</h2><p>所谓缓存预热，就是提前把数据库里的数据刷到缓存里，通常有这些方法：</p><p>1、直接写个缓存刷新页面或者接口，上线时手动操作</p><p>2、数据量不大，可以在项目启动的时候自动进行加载</p><p>3、定时任务刷新缓存.</p><h2 id="32-热点key重建？问题？解决？"><a href="#32-热点key重建？问题？解决？" class="headerlink" title="32.热点key重建？问题？解决？"></a>32.热点key重建？问题？解决？</h2><p>开发的时候一般使用“缓存+过期时间”的策略，既可以加速数据读写，又保证数据的定期更新，这种模式基本能够满足绝大部分需求。</p><p>但是有两个问题如果同时出现，可能就会出现比较大的问题：</p><ul><li>当前key是一个热点key（例如一个热门的娱乐新闻），并发量非常大。</li><li>重建缓存不能在短时间完成，可能是一个复杂计算，例如复杂的 SQL、多次IO、多个依赖等。在缓存失效的瞬间，有大量线程来重建缓存，造成后端负载加大，甚至可能会让应用崩溃。</li></ul><blockquote><p><strong>怎么处理呢？</strong></p></blockquote><p>要解决这个问题也不是很复杂，解决问题的要点在于：</p><ul><li>减少重建缓存的次数。</li><li>数据尽可能一致。</li><li>较少的潜在危险。</li></ul><p>所以一般采用如下方式：</p><ol><li>互斥锁（mutex key） 这种方法只允许一个线程重建缓存，其他线程等待重建缓存的线程执行完，重新从缓存获取数据即可。</li><li>永远不过期 “永远不过期”包含两层意思：</li></ol><ul><li>从缓存层面来看，确实没有设置过期时间，所以不会出现热点key过期后产生的问题，也就是“物理”不过期。</li><li>从功能层面来看，为每个value设置一个逻辑过期时间，当发现超过逻辑过期时间后，会使用单独的线程去构建缓存。</li></ul><h2 id="33-无底洞问题吗？如何解决？"><a href="#33-无底洞问题吗？如何解决？" class="headerlink" title="33.无底洞问题吗？如何解决？"></a>33.无底洞问题吗？如何解决？</h2><blockquote><p><strong>什么是无底洞问题？</strong></p></blockquote><p>2010年，Facebook的Memcache节点已经达到了3000个，承载着TB级别的缓存数据。但开发和运维人员发现了一个问题，为了满足业务要求添加了大量新Memcache节点，但是发现性能不但没有好转反而下降了，当时将这 种现象称为缓存的“<strong>无底洞</strong>”现象。</p><p>那么为什么会产生这种现象呢?</p><p>通常来说添加节点使得Memcache集群 性能应该更强了，但事实并非如此。键值数据库由于通常采用哈希函数将 key映射到各个节点上，造成key的分布与业务无关，但是由于数据量和访问量的持续增长，造成需要添加大量节点做水平扩容，导致键值分布到更多的 节点上，所以无论是Memcache还是Redis的分布式，批量操作通常需要从不同节点上获取，相比于单机批量操作只涉及一次网络操作，分布式批量操作会涉及多次网络时间。</p><blockquote><p><strong>无底洞问题如何优化呢？</strong></p></blockquote><p>先分析一下无底洞问题：</p><ul><li>客户端一次批量操作会涉及多次网络操作，也就意味着批量操作会随着节点的增多，耗时会不断增大。</li><li>网络连接数变多，对节点的性能也有一定影响。</li></ul><p>常见的优化思路如下：</p><ul><li>命令本身的优化，例如优化操作语句等。</li><li>减少网络通信次数。</li><li>降低接入成本，例如客户端使用长连&#x2F;连接池、NIO等。</li></ul><h1 id="Redis运维"><a href="#Redis运维" class="headerlink" title="Redis运维"></a>Redis运维</h1><h2 id="34-Redis报内存不足怎么处理？"><a href="#34-Redis报内存不足怎么处理？" class="headerlink" title="34.Redis报内存不足怎么处理？"></a>34.Redis报内存不足怎么处理？</h2><p>Redis 内存不足有这么几种处理方式：</p><ul><li>修改配置文件 redis.conf 的 maxmemory 参数，增加 Redis 可用内存</li><li>也可以通过命令set maxmemory动态设置内存上限</li><li>修改内存淘汰策略，及时释放内存空间</li><li>使用 Redis 集群模式，进行横向扩容。</li></ul><h2 id="35-Redis的过期数据回收策略有哪些？"><a href="#35-Redis的过期数据回收策略有哪些？" class="headerlink" title="35.Redis的过期数据回收策略有哪些？"></a>35.Redis的过期数据回收策略有哪些？</h2><p>Redis主要有2种过期数据回收策略：</p><p><strong>惰性删除</strong></p><p>惰性删除指的是当我们查询key的时候才对key进⾏检测，如果已经达到过期时间，则删除。显然，他有⼀个缺点就是如果这些过期的key没有被访问，那么他就⼀直⽆法被删除，⽽且⼀直占⽤内存。</p><p><strong>定期删除</strong></p><p>定期删除指的是Redis每隔⼀段时间对数据库做⼀次检查，删除⾥⾯的过期key。由于不可能对所有key去做轮询来删除，所以Redis会每次随机取⼀些key去做检查和删除。</p><h2 id="36-Redis有哪些内存溢出控制-内存淘汰策略？"><a href="#36-Redis有哪些内存溢出控制-内存淘汰策略？" class="headerlink" title="36.Redis有哪些内存溢出控制&#x2F;内存淘汰策略？"></a>36.Redis有哪些内存溢出控制&#x2F;内存淘汰策略？</h2><p>Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略，Redis支持六种策略：</p><ol><li>noeviction：默认策略，不会删除任何数据，拒绝所有写入操作并返 回客户端错误信息，此 时Redis只响应读操作。</li><li>volatile-lru：根据LRU算法删除设置了超时属性（expire）的键，直 到腾出足够空间为止。如果没有可删除的键对象，回退到noeviction策略。</li><li>allkeys-lru：根据LRU算法删除键，不管数据有没有设置超时属性， 直到腾出足够空间为止。</li><li>allkeys-random：随机删除所有键，直到腾出足够空间为止。</li><li>volatile-random：随机删除过期键，直到腾出足够空间为止。</li><li>volatile-ttl：根据键值对象的ttl属性，删除最近将要过期数据。如果 没有，回退到noeviction策略。</li></ol><h2 id="37-Redis阻塞？怎么解决？"><a href="#37-Redis阻塞？怎么解决？" class="headerlink" title="37.Redis阻塞？怎么解决？"></a>37.Redis阻塞？怎么解决？</h2><p>Redis发生阻塞，可以从以下几个方面排查：</p><ul><li><p><strong>API或数据结构使用不合理</strong></p><p>通常Redis执行命令速度非常快，但是不合理地使用命令，可能会导致执行速度很慢，导致阻塞，对于高并发的场景，应该尽量避免在大对象上执行算法复杂 度超过O（n）的命令。</p><p>对慢查询的处理分为两步：</p></li><li><ol><li>发现慢查询：slowlog get{n}命令可以获取最近 的n条慢查询命令；</li><li>发现慢查询后，可以从两个方向去优化慢查询：1）修改为低算法复杂度的命令，如hgetall改为hmget等，禁用keys、sort等命 令 2）调整大对象：缩减大对象数据或把大对象拆分为多个小对象，防止一次命令操作过多的数据。</li></ol></li><li><p><strong>CPU饱和的问题</strong></p><p>单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis单核CPU使用率跑到接近100%。</p><p>针对这种情况，处理步骤一般如下：</p></li><li><ol><li>判断当前Redis并发量是否已经达到极限，可以使用统计命令redis-cli-h{ip}-p{port}–stat获取当前 Redis使用情况</li><li>如果Redis的请求几万+，那么大概就是Redis的OPS已经到了极限，应该做集群化水品扩展来分摊OPS压力</li><li>如果只有几百几千，那么就得排查命令和内存的使用</li></ol></li><li><p><strong>持久化相关的阻塞</strong></p><p>对于开启了持久化功能的Redis节点，需要排查是否是持久化导致的阻塞。</p></li><li><ol><li>fork阻塞 fork操作发生在RDB和AOF重写时，Redis主线程调用fork操作产生共享 内存的子进程，由子进程完成持久化文件重写工作。如果fork操作本身耗时过长，必然会导致主线程的阻塞。</li><li>AOF刷盘阻塞 当我们开启AOF持久化功能时，文件刷盘的方式一般采用每秒一次，后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时，fsync操作需要等 待，直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒，为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。</li><li>HugePage写操作阻塞 对于开启Transparent HugePages的 操作系统，每次写命令引起的复制内存页单位由4K变为2MB，放大了512 倍，会拖慢写操作的执行时间，导致大量写操作慢查询。</li></ol></li></ul><h2 id="38-大key问题了解吗？"><a href="#38-大key问题了解吗？" class="headerlink" title="38.大key问题了解吗？"></a>38.大key问题了解吗？</h2><p>Redis使用过程中，有时候会出现大key的情况， 比如：</p><ul><li>单个简单的key存储的value很大，size超过10KB</li><li>hash， set，zset，list 中存储过多的元素（以万为单位）</li></ul><blockquote><p><strong>大key会造成什么问题呢？</strong></p></blockquote><ul><li>客户端耗时增加，甚至超时</li><li>对大key进行IO操作时，会严重占用带宽和CPU</li><li>造成Redis集群中数据倾斜</li><li>主动删除、被动删等，可能会导致阻塞</li></ul><blockquote><p><strong>如何找到大key?</strong></p></blockquote><ul><li>bigkeys命令：使用bigkeys命令以遍历的方式分析Redis实例中的所有Key，并返回整体统计信息与每个数据类型中Top1的大Key</li><li>redis-rdb-tools：redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具，它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。</li></ul><blockquote><p><strong>如何处理大key?</strong></p></blockquote><p>大key处理</p><ul><li><p><strong>删除大key</strong></p></li><li><ul><li>当Redis版本大于4.0时，可使用UNLINK命令安全地删除大Key，该命令能够以非阻塞的方式，逐步地清理传入的Key。</li><li>当Redis版本小于4.0时，避免使用阻塞式命令KEYS，而是建议通过SCAN命令执行增量迭代扫描key，然后判断进行删除。</li></ul></li><li><p><strong>压缩和拆分key</strong></p></li><li><ul><li>当vaule是string时，比较难拆分，则使用序列化、压缩算法将key的大小控制在合理范围内，但是序列化和反序列化都会带来更多时间上的消耗。</li><li>当value是string，压缩之后仍然是大key，则需要进行拆分，一个大key分为不同的部分，记录每个部分的key，使用multiget等操作实现事务读取。</li><li>当value是list&#x2F;set等集合类型时，根据预估的数据规模来进行分片，不同的元素计算后分到不同的片。</li></ul></li></ul><h2 id="39-Redis常见性能问题和解决方案？"><a href="#39-Redis常见性能问题和解决方案？" class="headerlink" title="39.Redis常见性能问题和解决方案？"></a>39.Redis常见性能问题和解决方案？</h2><ol><li>Master 最好不要做任何持久化工作，包括内存快照和 AOF 日志文件，特别是不要启用内存快照做持久化。</li><li>如果数据比较关键，某个 Slave 开启 AOF 备份数据，策略为每秒同步一次。</li><li>为了主从复制的速度和连接的稳定性，Slave 和 Master 最好在同一个局域网内。</li><li>尽量避免在压力较大的主库上增加从库。</li><li>Master 调用 BGREWRITEAOF 重写 AOF 文件，AOF 在重写的时候会占大量的 CPU 和内存资源，导致服务 load 过高，出现短暂服务暂停现象。</li><li>为了 Master 的稳定性，主从复制不要用图状结构，用单向链表结构更稳定，即主从关为：Master&lt;–Slave1&lt;–Slave2&lt;–Slave3…，这样的结构也方便解决单点故障问题，实现 Slave 对 Master 的替换，也即，如果 Master 挂了，可以立马启用 Slave1 做 Master，其他不变。</li></ol><h1 id="Redis应用"><a href="#Redis应用" class="headerlink" title="Redis应用"></a>Redis应用</h1><h2 id="40-使用Redis-如何实现异步队列？"><a href="#40-使用Redis-如何实现异步队列？" class="headerlink" title="40.使用Redis 如何实现异步队列？"></a>40.使用Redis 如何实现异步队列？</h2><p>我们知道redis支持很多种结构的数据，那么如何使用redis作为异步队列使用呢？一般有以下几种方式：</p><ul><li><strong>使用list作为队列，lpush生产消息，rpop消费消息</strong></li></ul><p>这种方式，消费者死循环rpop从队列中消费消息。但是这样，即使队列里没有消息，也会进行rpop，会导致Redis CPU的消耗。可以通过让消费者休眠的方式的方式来处理，但是这样又会又消息的延迟问题。</p><p>-<strong>使用list作为队列，lpush生产消息，brpop消费消息</strong></p><p>brpop是rpop的阻塞版本，list为空的时候，它会一直阻塞，直到list中有值或者超时。</p><p>这种方式只能实现一对一的消息队列。</p><ul><li><strong>使用Redis的pub&#x2F;sub来进行消息的发布&#x2F;订阅</strong></li></ul><p>发布&#x2F;订阅模式可以1：N的消息发布&#x2F;订阅。发布者将消息发布到指定的频道频道（channel），订阅相应频道的客户端都能收到消息。</p><p>但是这种方式不是可靠的，它不保证订阅者一定能收到消息，也不进行消息的存储。</p><p>所以，一般的异步队列的实现还是交给专业的消息队列。</p><h2 id="41-Redis-如何实现延时队列"><a href="#41-Redis-如何实现延时队列" class="headerlink" title="41.Redis 如何实现延时队列?"></a>41.Redis 如何实现延时队列?</h2><ul><li><strong>使用zset，利用排序实现</strong></li></ul><p>可以使用 zset这个结构，用设置好的时间戳作为score进行排序，使用 zadd score1 value1 ….命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务，通过循环执行队列任务即可。</p><h2 id="42-Redis-支持事务吗？"><a href="#42-Redis-支持事务吗？" class="headerlink" title="42.Redis 支持事务吗？"></a>42.Redis 支持事务吗？</h2><p>Redis提供了简单的事务，但它对事务ACID的支持并不完备。</p><p>multi命令代表事务开始，exec命令代表事务结束，它们之间的命令是原子顺序执行的：</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; multi </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379&gt; sadd user:a:follow user:b </span><br><span class="line">QUEUED </span><br><span class="line">127.0.0.1:6379&gt; sadd user:b:fans user:a </span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379&gt; sismember user:a:follow user:b </span><br><span class="line">(integer) 0</span><br><span class="line">127.0.0.1:6379&gt; exec 1) (integer) 1</span><br><span class="line">2) (integer) 1</span><br></pre></td></tr></table></figure><p>Redis事务的原理，是所有的指令在 exec 之前不执行，而是缓存在 服务器的一个事务队列中，服务器一旦收到 exec 指令，才开执行整个事务队列，执行完毕后一次性返回所有指令的运行结果。</p><p>因为Redis执行命令是单线程的，所以这组命令顺序执行，而且不会被其它线程打断。</p><p><strong>Redis事务的注意点有哪些？</strong></p><p>需要注意的点有：</p><ul><li>Redis 事务是不支持回滚的，不像 MySQL 的事务一样，要么都执行要么都不执行；</li><li>Redis 服务端在执行事务的过程中，不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。</li></ul><p><strong>Redis 事务为什么不支持回滚？</strong></p><p>Redis 的事务不支持回滚。</p><p>如果执行的命令有语法错误，Redis 会执行失败，这些问题可以从程序层面捕获并解决。但是如果出现其他问题，则依然会继续执行余下的命令。</p><p>这样做的原因是因为回滚需要增加很多工作，而不支持回滚则可以<strong>保持简单、快速的特性</strong>。</p><h2 id="43-Redis和Lua脚本的使用了解吗？"><a href="#43-Redis和Lua脚本的使用了解吗？" class="headerlink" title="43.Redis和Lua脚本的使用了解吗？"></a>43.Redis和Lua脚本的使用了解吗？</h2><p>Redis的事务功能比较简单，平时的开发中，可以利用Lua脚本来增强Redis的命令。</p><p>Lua脚本能给开发人员带来这些好处：</p><ul><li>Lua脚本在Redis中是原子执行的，执行过程中间不会插入其他命令。</li><li>Lua脚本可以帮助开发和运维人员创造出自己定制的命令，并可以将这 些命令常驻在Redis内存中，实现复用的效果。</li><li>Lua脚本可以将多条命令一次性打包，有效地减少网络开销。</li></ul><p>比如这一段很（烂）经（大）典（街）的秒杀系统利用lua扣减Redis库存的脚本：</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></pre></td><td class="code"><pre><span class="line">-- 库存未预热</span><br><span class="line">if (redis.call(&#x27;exists&#x27;, KEYS[2]) == 1) then</span><br><span class="line">     return -9;</span><br><span class="line"> end;</span><br><span class="line"> -- 秒杀商品库存存在</span><br><span class="line"> if (redis.call(&#x27;exists&#x27;, KEYS[1]) == 1) then</span><br><span class="line">     local stock = tonumber(redis.call(&#x27;get&#x27;, KEYS[1]));</span><br><span class="line">     local num = tonumber(ARGV[1]);</span><br><span class="line">     -- 剩余库存少于请求数量</span><br><span class="line">     if (stock &lt; num) then</span><br><span class="line">         return -3</span><br><span class="line">     end;</span><br><span class="line">     -- 扣减库存</span><br><span class="line">     if (stock &gt;= num) then</span><br><span class="line">         redis.call(&#x27;incrby&#x27;, KEYS[1], 0 - num);</span><br><span class="line">         -- 扣减成功</span><br><span class="line">         return 1</span><br><span class="line">     end;</span><br><span class="line">     return -2;</span><br><span class="line"> end;</span><br><span class="line"> -- 秒杀商品库存不存在</span><br><span class="line"> return -1;</span><br></pre></td></tr></table></figure><h2 id="44-Redis的管道了解吗？"><a href="#44-Redis的管道了解吗？" class="headerlink" title="44.Redis的管道了解吗？"></a>44.Redis的管道了解吗？</h2><p>Redis 提供三种将客户端多条命令打包发送给服务端执行的方式：</p><p>Pipelining(管道) 、 Transactions(事务) 和 Lua Scripts(Lua 脚本) 。</p><p><strong>Pipelining</strong>（管道）</p><p>Redis 管道是三者之中最简单的，当客户端需要执行多条 redis 命令时，可以通过管道一次性将要执行的多条命令发送给服务端，其作用是为了降低 RTT(Round Trip Time) 对性能的影响，比如我们使用 nc 命令将两条指令发送给 redis 服务端。</p><p>Redis 服务端接收到管道发送过来的多条命令后，会一直执命令，并将命令的执行结果进行缓存，直到最后一条命令执行完成，再所有命令的执行结果一次性返回给客户端 。</p><p><strong>Pipelining的优势</strong></p><p>在性能方面， Pipelining 有下面两个优势：</p><ul><li><strong>节省了RTT</strong>：将多条命令打包一次性发送给服务端，减少了客户端与服务端之间的网络调用次数</li><li><strong>减少了上下文切换</strong>：当客户端&#x2F;服务端需要从网络中读写数据时，都会产生一次系统调用，系统调用是非常耗时的操作，其中设计到程序由用户态切换到内核态，再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候，就会发生 10 次用户态到内核态的上下文切换，但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端，就只会产生一次上下文切换。</li></ul><h2 id="45-Redis实现分布式锁了解吗？"><a href="#45-Redis实现分布式锁了解吗？" class="headerlink" title="45.Redis实现分布式锁了解吗？"></a>45.Redis实现分布式锁了解吗？</h2><p>Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”，当别的进程也要来占时，发现已经有人蹲在那里了，就只好放弃或者稍后再试。</p><ul><li><strong>V1：setnx命令</strong></li></ul><p>占坑一般是使用 setnx(set if not exists) 指令，只允许被一个客户端占坑。先来先占， 用完了，再调用 del 指令释放茅坑。</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></pre></td><td class="code"><pre><span class="line">&gt; setnx lock:fighter true</span><br><span class="line">OK</span><br><span class="line">... do something critical ...</span><br><span class="line">&gt; del lock:fighter</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p>但是有个问题，如果逻辑执行到中间出现异常了，可能会导致 del 指令没有被调用，这样就会陷入死锁，锁永远得不到释放。</p><ul><li><strong>V2:锁超时释放</strong></li></ul><p>所以在拿到锁之后，再给锁加上一个过期时间，比如 5s，这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。</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></pre></td><td class="code"><pre><span class="line">&gt; setnx lock:fighter true</span><br><span class="line">OK</span><br><span class="line">&gt; expire lock:fighter 5</span><br><span class="line">... do something critical ...</span><br><span class="line">&gt; del lock:fighter</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p>但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了，可能是因为机器掉电或者是被人为杀掉的，就会导致 expire 得不到执行，也会造成死锁。</p><p>这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。</p><ul><li><strong>V3:set指令</strong></li></ul><p>这个问题在Redis 2.8 版本中得到了解决，这个版本加入了 set 指令的扩展参数，使得 setnx 和expire 指令可以一起执行。</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">set lock:fighter3 true ex 5 nx OK ... do something critical ... &gt; del lock:codehole</span><br></pre></td></tr></table></figure><p>上面这个指令就是 setnx 和 expire 组合在一起的原子指令，这个就算是比较完善的分布式锁了。</p><p>当然实际的开发，没人会去自己写分布式锁的命令，因为有专业的轮子——<strong>Redisson</strong>。</p><h1 id="底层结构"><a href="#底层结构" class="headerlink" title="底层结构"></a>底层结构</h1><p>这一部分就比较深了，如果不是简历上写了精通Redis，应该不会怎么问。</p><h2 id="46-说说Redis底层数据结构？"><a href="#46-说说Redis底层数据结构？" class="headerlink" title="46.说说Redis底层数据结构？"></a>46.说说Redis底层数据结构？</h2><p>Redis有*<em>动态字符串(sds)*</em>、*<em>链表(list)*</em>、*<em>字典(ht)*</em>、*<em>跳跃表(skiplist)*</em>、*<em>整数集合(intset)*</em>、*<em>压缩列表(ziplist)</em>* 等底层数据结构。</p><p>Redis并没有使用这些数据结构来直接实现键值对数据库，而是基于这些数据结构创建了一个对象系统，来表示所有的key-value。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iaty3Co6T9D2F9F8u3lvPK9awfficQY6QMJkCXdy5ynnLNicbic903JvaBSA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">我们常用的数据类型和编码对应的映射关系：</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatwtGpQUP0ibTpFcnK88Eib6MhnGibCG29zRveFHBV2rWNq4H3y0pTrbXEA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">简单看一下底层数据结构，如果对数据结构掌握不错的话，理解这些结构应该不是特别难：</p><ol><li><p><strong>字符串</strong>：redis没有直接使⽤C语⾔传统的字符串表示，⽽是⾃⼰实现的叫做简单动态字符串SDS的抽象类型。</p><p>C语⾔的字符串不记录⾃身的⻓度信息，⽽SDS则保存了⻓度信息，这样将获取字符串⻓度的时间由O(N)降低到了O(1)，同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重分配次数。</p></li></ol><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iatia4JRrKexylvrmMmgBxtmfW9b8vudwCqofW0ZyCbDxHC0gIpAvhOZ5Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片">SDS</p><ol><li><strong>链表linkedlist</strong>：redis链表是⼀个双向⽆环链表结构，很多发布订阅、慢查询、监视器功能都是使⽤到了链表来实现，每个链表的节点由⼀个listNode结构来表示，每个节点都有指向前置节点和后置节点的指针，同时表头节点的前置和后置节点都指向NULL。</li></ol><p>链表linkedlist</p><ol><li><strong>字典dict</strong>：⽤于保存键值对的抽象数据结构。Redis使⽤hash表作为底层实现，一个哈希表里可以有多个哈希表节点，而每个哈希表节点就保存了字典里中的一个键值对。每个字典带有两个hash表，供平时使⽤和rehash时使⽤，hash表使⽤链地址法来解决键冲突，被分配到同⼀个索引位置的多个键值对会形成⼀个单向链表，在对hash表进⾏扩容或者缩容的时候，为了服务的可⽤性，rehash的过程不是⼀次性完成的，⽽是渐进式的。</li><li><strong>跳跃表skiplist</strong>：跳跃表是有序集合的底层实现之⼀，Redis中在实现有序集合键和集群节点的内部结构中都是⽤到了跳跃表。Redis跳跃表由zskiplist和zskiplistNode组成，zskiplist⽤于保存跳跃表信息（表头、表尾节点、⻓度等），zskiplistNode⽤于表示表跳跃节点，每个跳跃表节点的层⾼都是1-32的随机数，在同⼀个跳跃表中，多个节点可以包含相同的分值，但是每个节点的成员对象必须是唯⼀的，节点按照分值⼤⼩排序，如果分值相同，则按照成员对象的⼤⼩排序。</li><li><strong>整数集合intset</strong>：⽤于保存整数值的集合抽象数据结构，不会出现重复元素，底层实现为数组。</li><li><strong>压缩列表ziplist</strong>：压缩列表是为节约内存⽽开发的顺序性数据结构，它可以包含任意多个节点，每个节点可以保存⼀个字节数组或者整数值。</li></ol><p>压缩列表组成</p><h2 id="47-Redis-的-SDS-和-C-中字符串相比有什么优势？"><a href="#47-Redis-的-SDS-和-C-中字符串相比有什么优势？" class="headerlink" title="47.Redis 的 SDS 和 C 中字符串相比有什么优势？"></a>47.Redis 的 SDS 和 C 中字符串相比有什么优势？</h2><p>C 语言使用了一个长度为 <code>N+1</code> 的字符数组来表示长度为 <code>N</code> 的字符串，并且字符数组最后一个元素总是 <code>\0</code>，这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。</p><p>C语言的字符串</p><blockquote><p><strong>C语言的字符串可能有什么问题？</strong></p></blockquote><p>这样简单的数据结构可能会造成以下一些问题：</p><ul><li><strong>获取字符串长度复杂度高</strong> ：因为 C 不保存数组的长度，每次都需要遍历一遍整个数组，时间复杂度为O(n)；</li><li>不能杜绝 <strong>缓冲区溢出&#x2F;内存泄漏</strong> 的问题 : C字符串不记录自身长度带来的另外一个问题是容易造成缓存区溢出（buffer overflow），例如在字符串拼接的时候，新的</li><li>C 字符串 <strong>只能保存文本数据</strong> → 因为 C 语言中的字符串必须符合某种编码（比如 ASCII），例如中间出现的 <code>&#39;\0&#39;</code> 可能会被判定为提前结束的字符串而识别不了；</li></ul><blockquote><p><strong>Redis如何解决？优势？</strong></p></blockquote><p>Redis sds</p><p>简单来说一下 Redis 如何解决的：</p><ol><li><strong>多增加 len 表示当前字符串的长度</strong>：这样就可以直接获取长度了，复杂度 O(1)；</li><li><strong>自动扩展空间</strong>：当 SDS 需要对字符串进行修改时，首先借助于 <code>len</code> 和 <code>alloc</code> 检查空间是否满足修改所需的要求，如果空间不够的话，SDS 会自动扩展空间，避免了像 C 字符串操作中的溢出情况；</li><li><strong>有效降低内存分配次数</strong>：C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配，SDS 使用了 <strong>空间预分配</strong> 和 <strong>惰性空间释放</strong> 机制，简单理解就是每次在扩展时是成倍的多分配的，在缩容是也是先留着并不正式归还给 OS；</li><li><strong>二进制安全</strong>：C 语言字符串只能保存 <code>ascii</code> 码，对于图片、音频等信息无法保存，SDS 是二进制安全的，写入什么读取就是什么，不做任何过滤和限制；</li></ol><h2 id="48-字典是如何实现的？Rehash-了解吗？"><a href="#48-字典是如何实现的？Rehash-了解吗？" class="headerlink" title="48.字典是如何实现的？Rehash 了解吗？"></a>48.字典是如何实现的？Rehash 了解吗？</h2><p>字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 <strong>hash</strong> 结构的数据会用到字典外，整个 Redis 数据库的所有 <code>key</code> 和 <code>value</code> 也组成了一个 <strong>全局字典</strong>，还有带过期时间的 <code>key</code> 也是一个字典。<em>(存储在 RedisDb 数据结构中)</em></p><blockquote><p><strong>字典结构是什么样的呢？</strong></p></blockquote><p><strong>Redis</strong> 中的字典相当于 Java 中的 <strong>HashMap</strong>，内部实现也差不多类似，采用哈希与运算计算下标位置；通过 **”数组 + 链表” *<em>的*<em>链地址法</em></em> 来解决哈希冲突，同时这样的结构也吸收了两种不同数据结构的优点。</p><blockquote><p><strong>字典是怎么扩容的？</strong></p></blockquote><p>字典结构内部包含 <strong>两个 hashtable</strong>，通常情况下只有一个哈希表 ht[0] 有值，在扩容的时候，把ht[0]里的值rehash到ht[1]，然后进行 <strong>渐进式rehash</strong> ——所谓渐进式rehash，指的是这个rehash的动作并不是一次性、集中式地完成的，而是分多次、渐进式地完成的。</p><p>待搬迁结束后，h[1]就取代h[0]存储字典的元素。</p><h2 id="49-跳跃表是如何实现的？原理？"><a href="#49-跳跃表是如何实现的？原理？" class="headerlink" title="49.跳跃表是如何实现的？原理？"></a>49.跳跃表是如何实现的？原理？</h2><p>PS:跳跃表是比较常问的一种结构。</p><p>跳跃表（skiplist）是一种有序数据结构，它通过在每个节点中维持多个指向其它节点的指针，从而达到快速访问节点的目的。</p><blockquote><p><strong>为什么使用跳跃表?</strong></p></blockquote><p>首先，因为 zset 要支持随机的插入和删除，所以它 <strong>不宜使用数组来实现</strong>，关于排序问题，我们也很容易就想到 <strong>红黑树&#x2F; 平衡树</strong> 这样的树形结构，为什么 Redis 不使用这样一些结构呢？</p><ol><li><strong>性能考虑：</strong> 在高并发的情况下，树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作，相对来说跳跃表的变化只涉及局部；</li><li><strong>实现考虑：</strong> 在复杂度与红黑树相同的情况下，跳跃表实现起来更简单，看起来也更加直观；</li></ol><p>基于以上的一些考虑，Redis 基于 <strong>William Pugh</strong> 的论文做出一些改进后采用了 <strong>跳跃表</strong> 这样的结构。</p><p>本质是解决查找问题。</p><blockquote><p><strong>跳跃表是怎么实现的？</strong></p></blockquote><p>跳跃表的节点里有这些元素：</p><ul><li><p><strong>层</strong>跳跃表节点的level数组可以包含多个元素，每个元素都包含一个指向其它节点的指针，程序可以通过这些层来加快访问其它节点的速度，一般来说，层的数量月多，访问其它节点的速度就越快。</p><p>每次创建一个新的跳跃表节点的时候，程序都根据幂次定律，随机生成一个介于1和32之间的值作为level数组的大小，这个大小就是层的“高度”</p></li><li><p><strong>前进指针</strong>每个层都有一个指向表尾的前进指针（level[i].forward属性），用于从表头向表尾方向访问节点。</p><p>我们看一下跳跃表从表头到表尾，遍历所有节点的路径：</p></li><li><p><strong>跨度</strong>层的跨度用于记录两个节点之间的距离。跨度是用来计算排位（rank）的：在查找某个节点的过程中，将沿途访问过的所有层的跨度累计起来，得到的结果就是目标节点在跳跃表中的排位。</p><p>例如查找，分值为3.0、成员对象为o3的节点时，沿途经历的层：查找的过程只经过了一个层，并且层的跨度为3，所以目标节点在跳跃表中的排位为3。</p></li><li><p><strong>分值和成员</strong>节点的分值（score属性）是一个double类型的浮点数，跳跃表中所有的节点都按分值从小到大来排序。</p><p>节点的成员对象（obj属性）是一个指针，它指向一个字符串对象，而字符串对象则保存这一个SDS值。</p></li></ul><h2 id="50-压缩列表了解吗？"><a href="#50-压缩列表了解吗？" class="headerlink" title="50.压缩列表了解吗？"></a>50.压缩列表了解吗？</h2><p>压缩列表是 Redis <strong>为了节约内存</strong> 而使用的一种数据结构，是由一系列特殊编码的连续内存快组成的顺序型数据结构。</p><p>一个压缩列表可以包含任意多个节点（entry），每个节点可以保存一个字节数组或者一个整数值。</p><p>压缩列表由这么几部分组成：</p><ul><li><strong>zlbyttes</strong>:记录整个压缩列表占用的内存字节数</li><li><strong>zltail</strong>:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节</li><li><strong>zllen</strong>:记录压缩列表包含的节点数量</li><li><strong>entryX</strong>:列表节点</li><li><strong>zlend</strong>:用于标记压缩列表的末端</li></ul><p>压缩列表示例</p><h2 id="51-快速列表-quicklist-了解吗？"><a href="#51-快速列表-quicklist-了解吗？" class="headerlink" title="51.快速列表 quicklist 了解吗？"></a>51.快速列表 quicklist 了解吗？</h2><p>Redis 早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表 linkedlist，也就是说当元素少时使用 ziplist，当元素多时用 linkedlist。</p><p>但考虑到链表的附加空间相对较高，<code>prev</code> 和 <code>next</code> 指针就要占去 <code>16</code> 个字节（64 位操作系统占用 <code>8</code> 个字节），另外每个节点的内存都是单独分配，会家具内存的碎片化，影响内存管理效率。</p><p>后来 Redis 新版本（3.2）对列表数据结构进行了改造，使用 <code>quicklist</code> 代替了 <code>ziplist</code> 和 <code>linkedlist</code>，quicklist是综合考虑了时间效率与空间效率引入的新型数据结构。</p><p>quicklist由list和ziplist结合而成，它是一个由ziplist充当节点的双向链表。<img src="https://mmbiz.qpic.cn/mmbiz_png/PMZOEonJxWf5IyvQkjc4vibibgKwWma1iaticzSicZ6us2RWa52icIPTPicKDgbibXZvTD7cjvJpOCqXTtibP878hPem0Gg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><h1 id="其他问题"><a href="#其他问题" class="headerlink" title="其他问题"></a>其他问题</h1><h2 id="52-假如Redis里面有1亿个key，其中有10w个key是以某个固定的已知的前缀开头的，如何将它们全部找出来？"><a href="#52-假如Redis里面有1亿个key，其中有10w个key是以某个固定的已知的前缀开头的，如何将它们全部找出来？" class="headerlink" title="52.假如Redis里面有1亿个key，其中有10w个key是以某个固定的已知的前缀开头的，如何将它们全部找出来？"></a>52.假如Redis里面有1亿个key，其中有10w个key是以某个固定的已知的前缀开头的，如何将它们全部找出来？</h2><p>使用 <code>keys</code> 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间，线上服务会停顿，直到指令执行完毕，服务才能恢复。这个时候可以使用 <code>scan</code>指令，<code>scan</code> 指令可以无阻塞的提取出指定模式的 <code>key</code> 列表，但是会有一定的重复概率，在客户端做一次去重就可以了，但是整体所花费的时间会比直接用 <code>keys</code> 指令长。</p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/02_Redis/Redis%E9%9D%A2%E8%AF%95.html</id>
    <link href="https://mikeah2011.github.io/post/02_Redis/Redis%E9%9D%A2%E8%AF%95.html"/>
    <published>2022-03-20T15:05:07.000Z</published>
    <summary>基础 1.说说什么是Redis? Redis图标 Redis是一种基于键值对（key-value）的NoSQL数据库。 比一般键值对数据库强大的地方，Redis中的value支持string（字符串）、hash（哈希）、 list（列表）、…</summary>
    <title>Redis全部</title>
    <updated>2026-05-01T13:12:43.905Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="MySQL" scheme="https://mikeah2011.github.io/categories/MySQL/"/>
    <category term="索引" scheme="https://mikeah2011.github.io/tags/%E7%B4%A2%E5%BC%95/"/>
    <content>
      <![CDATA[<p>二叉树 - 链表结构  - 相当于全表扫描</p><p>大于自己的值，放在右边，比自己小的放在左边</p><p>红黑树 - 二叉平衡树 - 数据量大时，树的高度不可控</p><p>B-Tree树  </p><p>​非叶子节点也会存储数据 没有维护双向指针</p><p>​树的高度 由非叶子节点 的叶叉树来决定的</p><p>​16^n^  &#x3D; 2Kw</p><p>B+Tree - 多叉平衡树    B-Tree树的变种  树的高度可控</p><p>​非叶子节点，作为一个冗余，不存储数据</p><p>​1170 * 1170 * 16 &#x3D; 2Kw</p><p>叶节点大小 16KB - 16384字节</p><p>Hash  - hash运算  hashmap 桶  -    一次 I&#x2F;O  </p><p>​hash冲突 </p><p>​仅支持 &#x3D; 等值，不支持范围查询</p><p>主键 必须有，否则MySQL会自行选择一列作为 聚簇索引，如果没有，MySQL 会做个 隐藏的 聚簇索引</p><p>且为整型 </p><p>数值存储空间要小</p><p>数值比较效率要比字符串比较高</p><p>数值自增排序，避免页分裂</p><p>ASCII值  </p><p>二级索引 - 非聚簇索引 </p><p>回表&amp;跳表</p><p>联合索引(复合索引)</p><p>​最左前缀原则</p><p>​排好序</p><p>​跳过最左字段，属于是无序的结构</p><p>MVCC 多版本  并发控制 </p><p>processon.com&#x2F;mindmap&#x2F;5dbeda6ee4b0ece75948831d</p><p>buffer pool 缓存机制</p><p>​LRU算法</p><p>​脏页</p><p>  <img src="https://cdn.jsdelivr.net/gh/mikeah2011/oss@main/uPic/image-20221004191753050.png" alt="image-20221004191753050"></p><p>分布式消息中间件RabbitMQ 、 RocketMQ 、 Kafka</p><p>分布式存储中间件Redis 、 MongoDB 、 FastDFS、 ElasticSearch、 ELK</p><p>分布式架构(微服务框架) Zookeeper 、 Dubbo 、ShardingSphere 、 Netty</p><p>cs.usfca.edu</p><p><a href="https://cs.usfca.edu/~galles/visualization/Algorithms.html">https://cs.usfca.edu/~galles/visualization/Algorithms.html</a></p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/01_MySQL/%E7%B4%A2%E5%BC%95/%E7%BA%A2%E9%BB%91%E6%A0%91.html</id>
    <link href="https://mikeah2011.github.io/post/01_MySQL/%E7%B4%A2%E5%BC%95/%E7%BA%A2%E9%BB%91%E6%A0%91.html"/>
    <published>2021-07-23T09:45:07.000Z</published>
    <summary>二叉树 - 链表结构 - 相当于全表扫描 大于自己的值，放在右边，比自己小的放在左边 红黑树 - 二叉平衡树 - 数据量大时，树的高度不可控 B-Tree树 ​ 非叶子节点也会存储数据 没有维护双向指针 ​ 树的高度 由非叶子节点 的叶叉树…</summary>
    <title>MySQL索引数据结构原理</title>
    <updated>2026-05-01T13:12:43.903Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <category term="工作原理" scheme="https://mikeah2011.github.io/tags/%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/"/>
    <category term="生命周期" scheme="https://mikeah2011.github.io/tags/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/"/>
    <content>
      <![CDATA[<h1 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h1><blockquote><p><strong>PHP 生命周期 &#x3D; MINIT → RINIT → 执行 → RSHUTDOWN → MSHUTDOWN。</strong><br><strong>CLI&#x2F;CGI 模式</strong>每个请求跑完整 5 步；<strong>FPM 模式</strong>只跑中间 3 步（M 阶段进程启动时跑一次）；<strong>Swoole&#x2F;Workerman</strong> 更进一步，<strong>只跑 1 步</strong>。</p></blockquote><h1 id="五大阶段"><a href="#五大阶段" class="headerlink" title="五大阶段"></a>五大阶段</h1><table><thead><tr><th>阶段</th><th>触发时机</th><th>典型工作</th></tr></thead><tbody><tr><td><strong>MINIT</strong> (Module Init)</td><td>进程启动</td><td>加载扩展、注册类&#x2F;函数&#x2F;常量</td></tr><tr><td><strong>RINIT</strong> (Request Init)</td><td>每个请求开始</td><td>初始化 <code>$_GET/$_POST/$_SESSION</code>、扩展请求级状态</td></tr><tr><td><strong>Execute</strong></td><td>RINIT 之后</td><td>把 PHP 源码编译成 opcode 并执行</td></tr><tr><td><strong>RSHUTDOWN</strong></td><td>请求结束</td><td>调注册的 shutdown 函数、清理临时变量</td></tr><tr><td><strong>MSHUTDOWN</strong></td><td>进程退出</td><td>卸载扩展、释放永久内存</td></tr></tbody></table><h1 id="CLI-vs-FPM-vs-Swoole"><a href="#CLI-vs-FPM-vs-Swoole" class="headerlink" title="CLI vs FPM vs Swoole"></a>CLI vs FPM vs Swoole</h1><h2 id="CLI（每次都完整-5-步）"><a href="#CLI（每次都完整-5-步）" class="headerlink" title="CLI（每次都完整 5 步）"></a>CLI（每次都完整 5 步）</h2><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">php script.php</span><br></pre></td></tr></table></figure><ul><li>启动：MINIT → RINIT → Execute → RSHUTDOWN → MSHUTDOWN → 退出</li><li><strong>慢</strong> —— 每跑一次都要重新加载扩展、解析 INI、编译 opcode</li></ul><h2 id="FPM（M-步只跑一次）"><a href="#FPM（M-步只跑一次）" class="headerlink" title="FPM（M 步只跑一次）"></a>FPM（M 步只跑一次）</h2><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></pre></td><td class="code"><pre><span class="line">[启动]   MINIT</span><br><span class="line">[请求1]  RINIT → Execute → RSHUTDOWN</span><br><span class="line">[请求2]  RINIT → Execute → RSHUTDOWN</span><br><span class="line">...</span><br><span class="line">[退出]   MSHUTDOWN</span><br></pre></td></tr></table></figure><ul><li>worker 进程常驻，处理 N 个请求才退出（<code>pm.max_requests</code>）</li><li>配合 OPcache，opcode 缓存，<strong>编译只发生一次</strong></li></ul><h2 id="Swoole-Workerman（M-R-都只跑一次）"><a href="#Swoole-Workerman（M-R-都只跑一次）" class="headerlink" title="Swoole &#x2F; Workerman（M+R 都只跑一次）"></a>Swoole &#x2F; Workerman（M+R 都只跑一次）</h2><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></pre></td><td class="code"><pre><span class="line">[启动]   MINIT → RINIT → 业务初始化（$app = new Application()）</span><br><span class="line">[请求1]  -&gt; handle($req)</span><br><span class="line">[请求2]  -&gt; handle($req)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><ul><li>全程在内存里，<strong>没有 RINIT&#x2F;RSHUTDOWN 开销</strong></li><li>框架（Hyperf &#x2F; EasySwoole）启动时就把容器、路由、ORM 全部初始化好</li><li><strong>代价</strong>：得自己处理状态污染、内存泄漏、协程上下文</li></ul><h1 id="一个请求里发生的事（FPM）"><a href="#一个请求里发生的事（FPM）" class="headerlink" title="一个请求里发生的事（FPM）"></a>一个请求里发生的事（FPM）</h1><ol><li><strong>Nginx</strong> 接到请求，通过 fastcgi 协议把请求转给 PHP-FPM master</li><li><strong>FPM master</strong> 派给空闲 worker（或新建）</li><li>worker 跑 <strong>RINIT</strong>：填 <code>$_SERVER</code> <code>$_GET</code> <code>$_POST</code>，扩展级 hook</li><li>worker 编译 PHP 文件 → opcode（OPcache 命中则跳过）</li><li><strong>执行 opcode</strong>，业务代码跑起来</li><li>输出 buffer → fastcgi 回 Nginx</li><li><strong>RSHUTDOWN</strong>：执行注册的 <code>register_shutdown_function</code>、释放变量</li><li>worker 回到空闲，等下一个请求；处理够 <code>max_requests</code> 后退出</li></ol><h1 id="性能影响最大的几点"><a href="#性能影响最大的几点" class="headerlink" title="性能影响最大的几点"></a>性能影响最大的几点</h1><table><thead><tr><th>优化点</th><th>提升倍数</th><th>说明</th></tr></thead><tbody><tr><td>开 <strong>OPcache</strong></td><td>2-5x</td><td>没有它每个请求都重新编译，直接砍 60% 性能</td></tr><tr><td><code>opcache.validate_timestamps=0</code>（生产）</td><td>+10-20%</td><td>不再 stat 文件检查改动</td></tr><tr><td>用 <strong>FPM + 长连接 PDO</strong></td><td>+30%</td><td>比 CLI 模式快得多</td></tr><tr><td>切到 <strong>Swoole &#x2F; RoadRunner</strong></td><td>5-10x</td><td>跳过 R 阶段，常驻内存</td></tr><tr><td><strong>预加载（PHP 7.4+ preload）</strong></td><td>+5-10%</td><td>MINIT 时就编译好核心类</td></tr></tbody></table><h1 id="SAPI-是什么"><a href="#SAPI-是什么" class="headerlink" title="SAPI 是什么"></a>SAPI 是什么</h1><blockquote><p><strong>SAPI (Server API)</strong> &#x3D; PHP 与外部环境对接的抽象层。CLI、FPM、Apache mod_php、Swoole 都各有自己的 SAPI。</p></blockquote><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">php -r <span class="string">&#x27;echo php_sapi_name();&#x27;</span></span><br><span class="line"><span class="comment"># cli</span></span><br></pre></td></tr></table></figure><p>写扩展或框架时，常需要 <code>if (php_sapi_name() === &#39;cli&#39;)</code> 判断当前模式。</p><h1 id="调试小技巧"><a href="#调试小技巧" class="headerlink" title="调试小技巧"></a>调试小技巧</h1><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 把 RSHUTDOWN 阶段的执行打印出来</span></span><br><span class="line"><span class="title function_ invoke__">register_shutdown_function</span>(function () &#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">&quot;请求结束，内存峰值：&quot;</span> . <span class="title function_ invoke__">memory_get_peak_usage</span>(<span class="literal">true</span>) . <span class="string">&quot;\n&quot;</span>;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>PHP 手册 - 生命周期: <a href="https://www.php.net/manual/zh/internals2.structure.php">https://www.php.net/manual/zh/internals2.structure.php</a></li><li>Swoole 文档: <a href="https://wiki.swoole.com/">https://wiki.swoole.com/</a></li><li>鸟哥（Laruence）博客: <a href="https://www.laruence.com/">https://www.laruence.com/</a></li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.html"/>
    <published>2021-04-18T10:00:00.000Z</published>
    <summary>PHP 从启动到退出的 5 个阶段（Module Init → Request Init → Execute → Request Shutdown → Module Shutdown），CLI 和 FPM 的差别在哪、Swoole 又凭什么快 10 倍。</summary>
    <title>PHP 生命周期与 SAPI</title>
    <updated>2026-05-01T13:12:43.908Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <category term="require" scheme="https://mikeah2011.github.io/tags/require/"/>
    <category term="include" scheme="https://mikeah2011.github.io/tags/include/"/>
    <content>
      <![CDATA[<h1 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h1><blockquote><p><strong><code>require</code> 失败 → Fatal Error（脚本停）；<code>include</code> 失败 → Warning（脚本继续）。</strong><br>加 <code>_once</code> 后缀都能保证「只加载一次」。</p></blockquote><h1 id="核心对比"><a href="#核心对比" class="headerlink" title="核心对比"></a>核心对比</h1><table><thead><tr><th>语句</th><th>找不到&#x2F;解析失败</th><th>适用</th></tr></thead><tbody><tr><td><code>require</code></td><td>E_COMPILE_ERROR，<strong>脚本中止</strong></td><td>必须加载的核心文件（配置、入口）</td></tr><tr><td><code>require_once</code></td><td>同上，且<strong>多次调用只执行一次</strong></td><td>函数&#x2F;类定义（防重复声明报错）</td></tr><tr><td><code>include</code></td><td>E_WARNING，<strong>继续执行</strong></td><td>可选模板片段（如可选侧栏）</td></tr><tr><td><code>include_once</code></td><td>同上，且<strong>只加载一次</strong></td><td>可选函数库</td></tr></tbody></table><figure class="highlight php"><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">require</span> <span class="string">&#x27;config.php&#x27;</span>;      <span class="comment">// 没了直接死，应该死</span></span><br><span class="line"><span class="keyword">include</span> <span class="string">&#x27;sidebar.html&#x27;</span>;    <span class="comment">// 没了页面少个边栏，能接受</span></span><br><span class="line"><span class="keyword">require_once</span> <span class="string">&#x27;User.php&#x27;</span>;   <span class="comment">// class User 二次 require 会报 &quot;Cannot redeclare&quot;</span></span><br></pre></td></tr></table></figure><h1 id="一个最小验证"><a href="#一个最小验证" class="headerlink" title="一个最小验证"></a>一个最小验证</h1><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// a.php</span></span><br><span class="line"><span class="keyword">echo</span> <span class="string">&quot;before\n&quot;</span>;</span><br><span class="line"><span class="keyword">require</span> <span class="string">&#x27;not_exist.php&#x27;</span>;   <span class="comment">// 换成 include 看差别</span></span><br><span class="line"><span class="keyword">echo</span> <span class="string">&quot;after\n&quot;</span>;            <span class="comment">// require: 不会执行；include: 会执行</span></span><br></pre></td></tr></table></figure><h1 id="once-是怎么做到的"><a href="#once-是怎么做到的" class="headerlink" title="_once 是怎么做到的"></a><code>_once</code> 是怎么做到的</h1><p>PHP 内核维护一个”已加载文件 realpath 表”，<code>_once</code> 加载前先查表：</p><ul><li>命中 → 跳过</li><li>未命中 → 加载并写表</li></ul><blockquote><p><strong>坑</strong>：用相对路径或符号链接可能让 PHP 认为是不同文件而重复加载。<strong>永远用 <code>__DIR__ . &#39;/path&#39;</code></strong>。</p></blockquote><h1 id="性能：-once-比普通慢吗？"><a href="#性能：-once-比普通慢吗？" class="headerlink" title="性能：_once 比普通慢吗？"></a>性能：<code>_once</code> 比普通慢吗？</h1><p>历史上 <code>_once</code> 因为 realpath 比较略慢，PHP 5.3 起优化后差距 &lt; 5%。<strong>现代代码不需要为此做选择</strong>，可读性优先。</p><h1 id="返回值"><a href="#返回值" class="headerlink" title="返回值"></a>返回值</h1><p>加载的文件可以 <code>return</code> 一个值，PHP 配置文件常用：</p><figure class="highlight php"><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="comment">// config.php</span></span><br><span class="line"><span class="keyword">return</span> [</span><br><span class="line">    <span class="string">&#x27;db&#x27;</span> =&gt; [<span class="string">&#x27;host&#x27;</span> =&gt; <span class="string">&#x27;127.0.0.1&#x27;</span>, <span class="string">&#x27;port&#x27;</span> =&gt; <span class="number">3306</span>],</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 入口</span></span><br><span class="line"><span class="variable">$config</span> = <span class="keyword">require</span> <span class="keyword">__DIR__</span> . <span class="string">&#x27;/config.php&#x27;</span>;</span><br><span class="line"><span class="keyword">echo</span> <span class="variable">$config</span>[<span class="string">&#x27;db&#x27;</span>][<span class="string">&#x27;host&#x27;</span>];</span><br></pre></td></tr></table></figure><p>Laravel、Symfony 的 <code>config/*.php</code> 全是这个套路。</p><h1 id="autoload-时代：require-几乎只在入口出现"><a href="#autoload-时代：require-几乎只在入口出现" class="headerlink" title="autoload 时代：require 几乎只在入口出现"></a>autoload 时代：require 几乎只在入口出现</h1><p>PHP 5.3+ 的 SPL autoload + Composer 之后：</p><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 整个项目通常只有一处 require</span></span><br><span class="line"><span class="keyword">require</span> <span class="keyword">__DIR__</span> . <span class="string">&#x27;/vendor/autoload.php&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable">$user</span> = <span class="keyword">new</span> <span class="title class_">App\Domain\User</span>();   <span class="comment">// 自动加载</span></span><br></pre></td></tr></table></figure><p><strong>业务代码里不应该再出现 <code>require</code> 加载类</strong>，那是 PHP 4 时代的写法。</p><h1 id="速查表"><a href="#速查表" class="headerlink" title="速查表"></a>速查表</h1><table><thead><tr><th>想做的事</th><th>用</th></tr></thead><tbody><tr><td>加载入口&#x2F;配置</td><td><code>require</code></td></tr><tr><td>加载类（极少数手动场景）</td><td><code>require_once</code></td></tr><tr><td>嵌入模板片段</td><td><code>include</code></td></tr><tr><td>现代项目加载类</td><td><strong>autoload，不要手写 require</strong></td></tr></tbody></table><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>PHP 手册 - require: <a href="https://www.php.net/manual/zh/function.require.php">https://www.php.net/manual/zh/function.require.php</a></li><li>PHP 手册 - include: <a href="https://www.php.net/manual/zh/function.include.php">https://www.php.net/manual/zh/function.include.php</a></li><li>Composer Autoload: <a href="https://getcomposer.org/doc/04-schema.md#autoload">https://getcomposer.org/doc/04-schema.md#autoload</a></li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_require%E4%B8%8Einclude.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_require%E4%B8%8Einclude.html"/>
    <published>2021-04-15T10:00:00.000Z</published>
    <summary>require / include 在 PHP 里加载文件的核心区别只有一个 —— 失败时是 Fatal Error 还是 Warning。本文加上 _once 变体、性能、autoload 时代的&quot;几乎不再用&quot;语义对比。</summary>
    <title>require 与 include 的区别（含 _once）</title>
    <updated>2026-05-01T13:12:43.908Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <category term="DI" scheme="https://mikeah2011.github.io/tags/DI/"/>
    <category term="设计模式" scheme="https://mikeah2011.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <content>
      <![CDATA[<h1 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h1><blockquote><p><strong>依赖注入（Dependency Injection）&#x3D; 一个类不自己创建依赖，而是由外部传入。</strong><br><strong>IoC 容器</strong> &#x3D; 自动帮你完成”传入”这个动作的工厂 + 注册表。</p></blockquote><h1 id="一、问题：为什么不能自己-new"><a href="#一、问题：为什么不能自己-new" class="headerlink" title="一、问题：为什么不能自己 new"></a>一、问题：为什么不能自己 new</h1><figure class="highlight php"><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="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> MysqlOrderRepo <span class="variable">$repo</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;repo = <span class="keyword">new</span> <span class="title class_">MysqlOrderRepo</span>(<span class="keyword">new</span> <span class="title function_ invoke__">PDO</span>(...));   <span class="comment">// ❌</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>毛病：</p><ol><li><strong>不可换实现</strong> —— 想换 Redis？改源码</li><li><strong>不可测</strong> —— 单元测试想 mock 数据库？改不动</li><li><strong>强耦合</strong> —— <code>OrderService</code> 必须知道 <code>PDO</code> 的连接串</li><li><strong>依赖隐藏</strong> —— 看构造器看不出它依赖谁</li></ol><h1 id="二、三种注入方式"><a href="#二、三种注入方式" class="headerlink" title="二、三种注入方式"></a>二、三种注入方式</h1><h2 id="1-构造器注入（推荐）"><a href="#1-构造器注入（推荐）" class="headerlink" title="1. 构造器注入（推荐）"></a>1. 构造器注入（推荐）</h2><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"><span class="keyword">private</span> OrderRepo <span class="variable">$repo</span></span>) </span>&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable">$svc</span> = <span class="keyword">new</span> <span class="title class_">OrderService</span>(<span class="keyword">new</span> <span class="title class_">MysqlOrderRepo</span>(<span class="variable">$pdo</span>));</span><br></pre></td></tr></table></figure><p><strong>最佳实践</strong>：依赖必填、对象不可变、看签名一目了然。</p><h2 id="2-Setter-注入"><a href="#2-Setter-注入" class="headerlink" title="2. Setter 注入"></a>2. Setter 注入</h2><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> OrderRepo <span class="variable">$repo</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">setRepo</span>(<span class="params">OrderRepo <span class="variable">$repo</span></span>): <span class="title">void</span> </span>&#123; <span class="variable language_">$this</span>-&gt;repo = <span class="variable">$repo</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>适用</strong>：可选依赖、运行时切换。缺点：对象创建后状态不完整。</p><h2 id="3-接口注入-属性注入"><a href="#3-接口注入-属性注入" class="headerlink" title="3. 接口注入 &#x2F; 属性注入"></a>3. 接口注入 &#x2F; 属性注入</h2><figure class="highlight php"><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="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="meta">#[Inject</span><span class="meta">]</span> <span class="keyword">public</span> OrderRepo <span class="variable">$repo</span>;   <span class="comment">// PHP 8 attribute，框架解析</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>适用</strong>：框架内部魔法。缺点：依赖魔法、IDE 补全难。</p><h1 id="三、面向接口编程"><a href="#三、面向接口编程" class="headerlink" title="三、面向接口编程"></a>三、面向接口编程</h1><p>DI 真正的价值要配合<strong>接口</strong>：</p><figure class="highlight php"><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="class"><span class="keyword">interface</span> <span class="title">OrderRepo</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">find</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$id</span></span>): ?<span class="title">Order</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MysqlOrderRepo</span> <span class="keyword">implements</span> <span class="title">OrderRepo</span> </span>&#123; <span class="comment">/*...*/</span> &#125;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RedisOrderRepo</span> <span class="keyword">implements</span> <span class="title">OrderRepo</span> </span>&#123; <span class="comment">/*...*/</span> &#125;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FakeOrderRepo</span>  <span class="keyword">implements</span> <span class="title">OrderRepo</span> </span>&#123; <span class="comment">/*for test*/</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"><span class="keyword">private</span> OrderRepo <span class="variable">$repo</span></span>) </span>&#123;&#125;   <span class="comment">// 类型是接口！</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试时：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$svc</span> = <span class="keyword">new</span> <span class="title class_">OrderService</span>(<span class="keyword">new</span> <span class="title class_">FakeOrderRepo</span>());   <span class="comment">// 不碰数据库就能测</span></span><br></pre></td></tr></table></figure><h1 id="四、IoC-容器：自动注入"><a href="#四、IoC-容器：自动注入" class="headerlink" title="四、IoC 容器：自动注入"></a>四、IoC 容器：自动注入</h1><p>手动 <code>new</code> 几十层依赖太累，IoC 容器替你做：</p><figure class="highlight php"><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"><span class="variable">$container</span>-&gt;<span class="title function_ invoke__">bind</span>(<span class="title class_">OrderRepo</span>::<span class="variable language_">class</span>, <span class="title class_">MysqlOrderRepo</span>::<span class="variable language_">class</span>);</span><br><span class="line"><span class="variable">$svc</span> = <span class="variable">$container</span>-&gt;<span class="title function_ invoke__">make</span>(<span class="title class_">OrderService</span>::<span class="variable language_">class</span>);   <span class="comment">// 自动 new MysqlOrderRepo + PDO</span></span><br></pre></td></tr></table></figure><h2 id="30-行手写一个容器"><a href="#30-行手写一个容器" class="headerlink" title="30 行手写一个容器"></a>30 行手写一个容器</h2><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Container</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">array</span> <span class="variable">$bindings</span> = [];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">bind</span>(<span class="params"><span class="keyword">string</span> <span class="variable">$abstract</span>, <span class="keyword">string</span>|<span class="keyword">callable</span> <span class="variable">$concrete</span></span>): <span class="title">void</span> </span>&#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;bindings[<span class="variable">$abstract</span>] = <span class="variable">$concrete</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">make</span>(<span class="params"><span class="keyword">string</span> <span class="variable">$abstract</span></span>): <span class="title">object</span> </span>&#123;</span><br><span class="line">        <span class="variable">$concrete</span> = <span class="variable language_">$this</span>-&gt;bindings[<span class="variable">$abstract</span>] ?? <span class="variable">$abstract</span>;</span><br><span class="line">        <span class="keyword">if</span> (<span class="title function_ invoke__">is_callable</span>(<span class="variable">$concrete</span>)) <span class="keyword">return</span> <span class="variable">$concrete</span>(<span class="variable language_">$this</span>);</span><br><span class="line"></span><br><span class="line">        <span class="variable">$ref</span> = <span class="keyword">new</span> <span class="title class_">ReflectionClass</span>(<span class="variable">$concrete</span>);</span><br><span class="line">        <span class="variable">$ctor</span> = <span class="variable">$ref</span>-&gt;<span class="title function_ invoke__">getConstructor</span>();</span><br><span class="line">        <span class="keyword">if</span> (!<span class="variable">$ctor</span>) <span class="keyword">return</span> <span class="keyword">new</span> <span class="variable">$concrete</span>();</span><br><span class="line"></span><br><span class="line">        <span class="variable">$args</span> = [];</span><br><span class="line">        <span class="keyword">foreach</span> (<span class="variable">$ctor</span>-&gt;<span class="title function_ invoke__">getParameters</span>() <span class="keyword">as</span> <span class="variable">$p</span>) &#123;</span><br><span class="line">            <span class="variable">$type</span> = <span class="variable">$p</span>-&gt;<span class="title function_ invoke__">getType</span>();</span><br><span class="line">            <span class="keyword">if</span> (<span class="variable">$type</span> &amp;&amp; !<span class="variable">$type</span>-&gt;<span class="title function_ invoke__">isBuiltin</span>()) &#123;</span><br><span class="line">                <span class="variable">$args</span>[] = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">make</span>(<span class="variable">$type</span>-&gt;<span class="title function_ invoke__">getName</span>());   <span class="comment">// 递归注入</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$ref</span>-&gt;<span class="title function_ invoke__">newInstanceArgs</span>(<span class="variable">$args</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用法</span></span><br><span class="line"><span class="variable">$c</span> = <span class="keyword">new</span> <span class="title class_">Container</span>();</span><br><span class="line"><span class="variable">$c</span>-&gt;<span class="title function_ invoke__">bind</span>(<span class="title class_">OrderRepo</span>::<span class="variable language_">class</span>, <span class="title class_">MysqlOrderRepo</span>::<span class="variable language_">class</span>);</span><br><span class="line"><span class="variable">$svc</span> = <span class="variable">$c</span>-&gt;<span class="title function_ invoke__">make</span>(<span class="title class_">OrderService</span>::<span class="variable language_">class</span>);</span><br></pre></td></tr></table></figure><p>Laravel &#x2F; Symfony 的容器本质就是这套，加了<strong>单例、上下文绑定、循环依赖检测、属性注入</strong>等增强。</p><h1 id="五、DI-vs-Service-Locator"><a href="#五、DI-vs-Service-Locator" class="headerlink" title="五、DI vs Service Locator"></a>五、DI vs Service Locator</h1><p>很多人混淆两者：</p><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// DI（推荐）：依赖在构造器里声明</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"><span class="keyword">private</span> OrderRepo <span class="variable">$repo</span></span>) </span>&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service Locator（反模式）：依赖藏在内部</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">process</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="variable">$repo</span> = <span class="title class_">ServiceLocator</span>::<span class="title function_ invoke__">get</span>(<span class="title class_">OrderRepo</span>::<span class="variable language_">class</span>);   <span class="comment">// ❌</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Service Locator 把依赖隐藏到方法内部，<strong>等于没做 DI</strong>。</p><h1 id="六、什么时候不用-DI"><a href="#六、什么时候不用-DI" class="headerlink" title="六、什么时候不用 DI"></a>六、什么时候<strong>不用</strong> DI</h1><ul><li>极小脚本 &#x2F; 一次性工具</li><li>全是静态工具函数（无状态）</li><li>性能极致敏感的热路径（容器有反射开销，但生产环境一般已编译&#x2F;缓存掉）</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>Martin Fowler, <em>Inversion of Control Containers and the Dependency Injection pattern</em>: <a href="https://martinfowler.com/articles/injection.html">https://martinfowler.com/articles/injection.html</a></li><li>PHP-FIG PSR-11 Container Interface: <a href="https://www.php-fig.org/psr/psr-11/">https://www.php-fig.org/psr/psr-11/</a></li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5.html"/>
    <published>2021-04-12T10:00:00.000Z</published>
    <summary>依赖注入 = 把依赖从&quot;自己 new&quot;改为&quot;外部传进来&quot;。本文讲清三种注入方式、IoC 容器原理、为什么 Laravel/Symfony 都离不开它，以及自己手写一个 30 行的容器。</summary>
    <title>依赖注入（DI）与 IoC 容器</title>
    <updated>2026-05-01T13:12:43.907Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <category term="性能优化" scheme="https://mikeah2011.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <category term="GC" scheme="https://mikeah2011.github.io/tags/GC/"/>
    <content>
      <![CDATA[<h1 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h1><blockquote><p><strong>PHP GC &#x3D; 引用计数为主 + 循环引用收集器为辅。</strong> 引用计数归零立刻释放，解决不了的循环引用由后台收集器周期清理。</p></blockquote><h1 id="一、引用计数（refcount）"><a href="#一、引用计数（refcount）" class="headerlink" title="一、引用计数（refcount）"></a>一、引用计数（refcount）</h1><p>PHP 的每个变量底层是 <code>zval</code>，里面有 <code>refcount</code> 和 <code>is_ref</code> 两个字段。</p><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="variable">$a</span> = <span class="string">&#x27;hello&#x27;</span>;   <span class="comment">// refcount = 1</span></span><br><span class="line"><span class="variable">$b</span> = <span class="variable">$a</span>;        <span class="comment">// refcount = 2 （写时复制，COW）</span></span><br><span class="line"><span class="keyword">unset</span>(<span class="variable">$a</span>);      <span class="comment">// refcount = 1</span></span><br><span class="line"><span class="keyword">unset</span>(<span class="variable">$b</span>);      <span class="comment">// refcount = 0 → 立即释放</span></span><br></pre></td></tr></table></figure><p>用 <code>xdebug_debug_zval()</code> 或 <code>debug_zval_refcount()</code> 可以看到 refcount。</p><blockquote><p>注意：PHP 7+ 的 zval 实现有重大改动（zval 嵌入栈、引用单独 zend_reference），但<strong>算法语义没变</strong>。</p></blockquote><h1 id="二、引用计数解决不了的：循环引用"><a href="#二、引用计数解决不了的：循环引用" class="headerlink" title="二、引用计数解决不了的：循环引用"></a>二、引用计数解决不了的：循环引用</h1><figure class="highlight php"><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Node</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="variable">$next</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable">$a</span> = <span class="keyword">new</span> <span class="title class_">Node</span>();</span><br><span class="line"><span class="variable">$b</span> = <span class="keyword">new</span> <span class="title class_">Node</span>();</span><br><span class="line"><span class="variable">$a</span>-&gt;next = <span class="variable">$b</span>;   <span class="comment">// b refcount=2</span></span><br><span class="line"><span class="variable">$b</span>-&gt;next = <span class="variable">$a</span>;   <span class="comment">// a refcount=2</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">unset</span>(<span class="variable">$a</span>);       <span class="comment">// a refcount=1（b 还指着它）</span></span><br><span class="line"><span class="keyword">unset</span>(<span class="variable">$b</span>);       <span class="comment">// b refcount=1（a 还指着它）</span></span><br><span class="line"><span class="comment">// 谁也不为 0，但外部已经访问不到 → 内存泄漏</span></span><br></pre></td></tr></table></figure><p>PHP 5.3 引入 <strong>循环引用收集器（Cycle Collector）</strong>，定期扫描这些”孤岛”。</p><h1 id="三、循环收集算法（同步-GC）"><a href="#三、循环收集算法（同步-GC）" class="headerlink" title="三、循环收集算法（同步 GC）"></a>三、循环收集算法（同步 GC）</h1><p>整套算法基于 IBM 的论文 <em>“Concurrent Cycle Collection in Reference Counted Systems”</em>：</p><ol><li><strong>Roots Buffer</strong>：每次 refcount 减少但 ≠ 0 的 zval，被加入”可疑列表”</li><li>列表满了（默认 10000 个）触发：<ul><li><strong>Mark</strong>：从可疑根出发，遍历能到达的所有 zval，refcount 全部 -1</li><li><strong>Scan</strong>：再遍历一次，refcount &gt; 0 的还原（说明是外部引用）</li><li><strong>Collect</strong>：refcount &#x3D; 0 的真正释放</li></ul></li></ol><figure class="highlight ini"><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"><span class="comment">; php.ini</span></span><br><span class="line"><span class="attr">zend.enable_gc</span> = <span class="literal">On</span>  <span class="comment">; 默认开</span></span><br></pre></td></tr></table></figure><h1 id="四、手动控制"><a href="#四、手动控制" class="headerlink" title="四、手动控制"></a>四、手动控制</h1><figure class="highlight php"><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="title function_ invoke__">gc_enabled</span>();          <span class="comment">// GC 是否开启</span></span><br><span class="line"><span class="title function_ invoke__">gc_enable</span>();           <span class="comment">// 开</span></span><br><span class="line"><span class="title function_ invoke__">gc_disable</span>();          <span class="comment">// 关</span></span><br><span class="line"><span class="title function_ invoke__">gc_collect_cycles</span>();   <span class="comment">// 手动触发收集，返回回收数量</span></span><br><span class="line"><span class="title function_ invoke__">gc_status</span>();           <span class="comment">// PHP 7.3+ 看 GC 统计</span></span><br></pre></td></tr></table></figure><h2 id="何时手动调？"><a href="#何时手动调？" class="headerlink" title="何时手动调？"></a>何时手动调？</h2><ul><li><strong>CLI 长任务</strong>（队列消费者、daemon）：每 N 个任务调一次，避免缓冲区慢慢撑爆</li><li><strong>内存敏感的循环</strong>：处理大数组后立刻 <code>unset()</code> + <code>gc_collect_cycles()</code></li><li><strong>Swoole &#x2F; Workerman</strong>：常驻进程必须关注 GC</li></ul><h1 id="五、内存泄漏排查思路"><a href="#五、内存泄漏排查思路" class="headerlink" title="五、内存泄漏排查思路"></a>五、内存泄漏排查思路</h1><figure class="highlight php"><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">// 1. 看当前内存</span></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">memory_get_usage</span>(<span class="literal">true</span>);   <span class="comment">// 实际向 OS 申请的</span></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">memory_get_peak_usage</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 在循环里打点</span></span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$items</span> <span class="keyword">as</span> <span class="variable">$i</span> =&gt; <span class="variable">$item</span>) &#123;</span><br><span class="line">    <span class="title function_ invoke__">process</span>(<span class="variable">$item</span>);</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$i</span> % <span class="number">1000</span> === <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">&quot;i=<span class="subst">$i</span> mem=&quot;</span> . <span class="title function_ invoke__">memory_get_usage</span>(<span class="literal">true</span>) . PHP_EOL;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 配合 gc_status 看回收次数</span></span><br><span class="line"><span class="title function_ invoke__">print_r</span>(<span class="title function_ invoke__">gc_status</span>());</span><br></pre></td></tr></table></figure><p><strong>典型泄漏源</strong>：</p><table><thead><tr><th>场景</th><th>现象</th><th>修复</th></tr></thead><tbody><tr><td>单例里挂 listener，listener 反向引用单例</td><td>长跑后 OOM</td><td>用 <code>WeakMap</code>（PHP 8+）或显式 <code>unset</code></td></tr><tr><td>全局数组缓存无上限</td><td>内存稳步上涨</td><td>LRU + 限容</td></tr><tr><td>ORM 里 entity 互相 hasMany</td><td>批量处理后没释放</td><td>处理完一批 detach</td></tr><tr><td><code>static</code> 局部变量累积</td><td>每次调用都涨</td><td>改成实例属性或外部缓存</td></tr></tbody></table><h1 id="六、PHP-8-新特性：WeakMap"><a href="#六、PHP-8-新特性：WeakMap" class="headerlink" title="六、PHP 8 新特性：WeakMap"></a>六、PHP 8 新特性：WeakMap</h1><figure class="highlight php"><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="variable">$cache</span> = <span class="keyword">new</span> <span class="built_in">WeakMap</span>();</span><br><span class="line"><span class="variable">$obj</span> = <span class="keyword">new</span> <span class="title class_">SomeBigObject</span>();</span><br><span class="line"><span class="variable">$cache</span>[<span class="variable">$obj</span>] = <span class="string">&#x27;元数据&#x27;</span>;   <span class="comment">// $obj 被 unset 后，$cache 里的条目自动消失</span></span><br></pre></td></tr></table></figure><p>专门为缓存&#x2F;装饰器场景设计，<strong>不影响目标对象的引用计数</strong>。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>PHP 手册 - GC: <a href="https://www.php.net/manual/zh/features.gc.php">https://www.php.net/manual/zh/features.gc.php</a></li><li><em>Concurrent Cycle Collection in Reference Counted Systems</em>: <a href="https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf">https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf</a></li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.html"/>
    <published>2021-04-10T10:00:00.000Z</published>
    <summary>PHP 垃圾回收 = 引用计数 + 循环引用收集器。本文讲清 refcount 怎么涨怎么减、5.3 引入的循环引用算法怎么跑、什么时候手动 gc_collect_cycles，配合实际内存泄漏排查思路。</summary>
    <title>PHP 垃圾回收机制（GC）</title>
    <updated>2026-05-01T13:12:43.907Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="Redis" scheme="https://mikeah2011.github.io/categories/Redis/"/>
    <category term="Redis" scheme="https://mikeah2011.github.io/tags/Redis/"/>
    <content>
      <![CDATA[<p>数据结构：memcache仅支持简单的key-value形式，Redis支持的数据更多（string字符串，set集合，list列表，hash散列，zset有序集合）；</p><p>多线程：memcache支持多线程，Redis支持单线程</p><p>持久化：Redis支持持久化，memcache不支持持久化</p><p>分布式：Redis做主从结构，memcache服务器需要通过hash一致化来支撑主从结构</p><p>实际运用中可以redis，memcache结合，memcache可作为session存储的方式，session都是KV类型键值对。</p><ol><li><p>Redis中，并不是所有的数据都一直存储在内存中的，这是和Memcache相比一个最大的区别。</p></li><li><p>Redis在很多方面具备数据库的特征，或者说就是一个数据库系统，而Memcache只是简单的K&#x2F;V缓存。</p></li><li><p>他们的扩展都需要做集群；实现方式：master-slave、Hash。</p></li><li><p>在100k以上的数据中，Memcache性能要高于Redis。</p></li><li><p>如果要说内存使用效率，使用简单的key-value存储的话，Memcached的内存利用率更高，而如果Redis采用hash结构来做key-value存储，由于其组合式的压缩，其内存利用率会高于Memcache。当然，这和你的应用场景和数据特性有关。</p></li><li><p>如果你对数据持久化和数据同步有所要求，那么推荐你选择Redis，因为这两个特性Memcache都不具备。即使你只是希望在升级或者重启系统后缓存数据不会丢失，选择Redis也是明智的。</p></li><li><p>Redis和Memcache在写入性能上面差别不大，读取性能上面尤其是批量读取性能上面Memcache更强</p></li><li><p>Redis 提供了多种不同级别的持久化方式：</p></li></ol><p>RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照（point-in-time snapshot）。</p><p>AOF 持久化记录服务器执行的所有写操作命令，并在服务器启动时，通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存，新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写（rewrite），使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。</p><p>Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下， 当 Redis 重启时， 它会优先使用 AOF 文件来还原数据集， 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。</p><p>你甚至可以关闭持久化功能，让数据只在服务器运行时存在。</p><blockquote><p>归纳</p></blockquote><table><thead><tr><th></th><th>Redis</th><th>Memcache</th></tr></thead><tbody><tr><td>数据结构</td><td>string、set集合、list列表、hash散列、zset有序集合</td><td>key-value</td></tr><tr><td>多线程</td><td>NO</td><td>YES</td></tr><tr><td>持久化</td><td>RDB快照、AOF日志</td><td>NO</td></tr><tr><td>分布式</td><td>主从结构</td><td>hash一致化</td></tr><tr><td>读写</td><td>读11w&#x2F;s，写8w&#x2F;s</td><td>读更优</td></tr><tr><td>特性</td><td>具备数据库特征，NoSQL</td><td>K&#x2F;V缓存</td></tr><tr><td>性能</td><td>100KB以内的数据，更优</td><td>100KB以上的数据，更优</td></tr><tr><td>集群</td><td>Master-slave hash</td><td>Master-slave hash</td></tr><tr><td>数据存储</td><td>并非所有的数据都一直存储在内存中</td><td>所有的都存在内存中</td></tr><tr><td>内存使用率</td><td>如果是hash结构的key-value，组合式的压缩，会高</td><td>高</td></tr></tbody></table>]]>
    </content>
    <id>https://mikeah2011.github.io/post/02_Redis/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_Redis%E5%92%8CMemcache.html</id>
    <link href="https://mikeah2011.github.io/post/02_Redis/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_Redis%E5%92%8CMemcache.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>数据结构：memcache仅支持简单的key-value形式，Redis支持的数据更多（string字符串，set集合，list列表，hash散列，zset有序集合）； 多线程：memcache支持多线程，Redis支持单线程 持久化：Re…</summary>
    <title>Redis缓存</title>
    <updated>2026-05-01T13:12:43.905Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="Redis" scheme="https://mikeah2011.github.io/categories/Redis/"/>
    <category term="Redis" scheme="https://mikeah2011.github.io/tags/Redis/"/>
    <content>
      <![CDATA[<blockquote><p>背景</p></blockquote><p>Redis是不会存在并发问题的，因为他是单进程的，再多的命令都是一个接一个地执行的。</p><blockquote><p>场景</p></blockquote><ol><li><p>GET &amp; SET </p></li><li><p>利用Jedis等客户端对Redis进行并发访问</p></li><li><p>远程访问Redis的时候，因为网络等原因造成高并发访问、延迟返回</p></li></ol><p>我们使用的时候，可能会出现并发问题，比如获得和设定这一对。</p><p>Redis的为什么 有高并发问题？Redis的的出身决定。</p><p>Redis是一种单线程机制的nosql数据库，基于key-value，数据可持久化落盘。</p><p>由于单线程所以Redis本身并没有锁的概念，多个客户端连接并不存在竞争关系，</p><p>但是利用Jedis等客户端对Redis进行并发访问时会出现问题。</p><blockquote><p>原因</p></blockquote><p>发生【连接超时】、【数据转换错误】、【阻塞】、【客户端关闭连接】等问题，</p><p>这些问题均是由于【客户端连接混乱】造成。</p><p>单线程的天性决定，高并发对同一个键的操作会排队处理，</p><p>如果并发量很大，可能造成后来的请求超时。</p><p>在远程访问Redis的时候，因为网络等原因造成高并发访问延迟返回的问题。</p><blockquote><p>解决办法</p></blockquote><ol><li>客户端角度，将连接进行池化，同时对读写Redis操作采用内部锁 sync hronized；</li><li>服务器角度，利用setnx变向实现锁机制；</li></ol>]]>
    </content>
    <id>https://mikeah2011.github.io/post/02_Redis/%E9%98%B2%E6%AD%A2%E9%AB%98%E5%B9%B6%E5%8F%91.html</id>
    <link href="https://mikeah2011.github.io/post/02_Redis/%E9%98%B2%E6%AD%A2%E9%AB%98%E5%B9%B6%E5%8F%91.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>
      <![CDATA[背景 Redis是不会存在并发问题的，因为他是单进程的，再多的命令都是一个接一个地执行的。 场景 1. GET & SET 2. 利用Jedis等客户端对Redis进行并发访问 3. 远程访问Redis的时候，因为网络等原因造成高并发访问、…]]>
    </summary>
    <title>Redis高并发</title>
    <updated>2026-05-01T13:12:43.906Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="SQL" scheme="https://mikeah2011.github.io/categories/SQL/"/>
    <category term="MySQL" scheme="https://mikeah2011.github.io/tags/MySQL/"/>
    <content>
      <![CDATA[<p>当Mysql执行一条查询的SQl的时候大概发生了以下的步骤：</p><ol><li>客户端发送查询语句给服务器。</li><li>服务器首先进行用户名和密码的验证以及权限的校验。</li><li>然后会检查缓存中是否存在该查询，若存在，返回缓存中存在的结果。若是不存在就进行下一步。注意：Mysql 8就把缓存这块给砍掉了。</li><li>接着进行语法和词法的分析，对SQl的解析、语法检测和预处理，再由优化器生成对应的执行计划。</li><li>Mysql的执行器根据优化器生成的执行计划执行，调用存储引擎的接口进行查询。服务器将查询的结果返回客户端。</li></ol><p>Mysql中语句的执行都是都是分层执行，每一层执行的任务都不同，直到最后拿到结果返回，主要分为<strong>Service层</strong>和<strong>引擎层</strong>。</p><p><strong>在Service层中包含：连接器、分析器、优化器、执行器。引擎层以插件的形式可以兼容各种不同的存储引擎，主要包含的有InnoDB和MyISAM两种存储引擎</strong>。具体的执行流程图如下所示：</p><p><img src="/images/6420.png" alt="图片"></p>]]>
    </content>
    <id>https://mikeah2011.github.io/post/01_MySQL/%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E7%9A%84%E6%B5%81%E7%A8%8B.html</id>
    <link href="https://mikeah2011.github.io/post/01_MySQL/%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E7%9A%84%E6%B5%81%E7%A8%8B.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>当Mysql执行一条查询的SQl的时候大概发生了以下的步骤： 1. 客户端发送查询语句给服务器。 2. 服务器首先进行用户名和密码的验证以及权限的校验。 3. 然后会检查缓存中是否存在该查询，若存在，返回缓存中存在的结果。若是不存在就进行下…</summary>
    <title>SQL查询语句的流程</title>
    <updated>2026-05-01T13:12:43.902Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="MySQL Lock" scheme="https://mikeah2011.github.io/categories/MySQL-Lock/"/>
    <category term="MySQL" scheme="https://mikeah2011.github.io/tags/MySQL/"/>
    <category term="锁" scheme="https://mikeah2011.github.io/tags/%E9%94%81/"/>
    <content>
      <![CDATA[<p>[TOC]</p><h2 id="锁机制"><a href="#锁机制" class="headerlink" title="锁机制"></a>锁机制</h2><blockquote><p>计算机协调 多个进程或线程 并发访问某一资源的机制。</p></blockquote><h3 id="锁的类型及其特点"><a href="#锁的类型及其特点" class="headerlink" title="锁的类型及其特点"></a>锁的类型及其特点</h3><p>开销、加锁、颗粒度、冲突、并发等角度分析</p><p>表级锁(table-level locking) 开销大，加锁快，不会出现死锁，颗粒度大，锁冲突概率高，并发度小；</p><p>页面锁(page-level locking) 开销、加锁效率、颗粒度介于表锁和行锁之间，会出现死锁，并发度一般；</p><p>行级锁(row-level locking)  开销大，加速慢，会出现死锁，颗粒度小，锁冲突概率小，并发度高；</p><h3 id="锁的类型应用场景"><a href="#锁的类型应用场景" class="headerlink" title="锁的类型应用场景"></a>锁的类型应用场景</h3><p>表锁 - 适合 以查询为主，只有少量按索引条件更新数据的应用，如web应用；</p><p>行锁 - 适合 大量按索引条件并发 更新少量不同数据，同时又有并发查询的应用，如在线事务处理系统；</p><h3 id="锁的支持度及情况"><a href="#锁的支持度及情况" class="headerlink" title="锁的支持度及情况"></a>锁的支持度及情况</h3><p>MyISAM 只支持 表锁</p><p>InnoDB 默认支持 行锁，也支持表锁</p><figure class="highlight plaintext"><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">-- 查询表锁争用情况</span><br><span class="line">show status like &#x27;table%&#x27;; # Table_locks_waited 大则表锁争用情况严重</span><br></pre></td></tr></table></figure><h3 id="锁的维度划分"><a href="#锁的维度划分" class="headerlink" title="锁的维度划分"></a>锁的维度划分</h3><p>共享锁(S) - 读锁</p><p>排他锁(X) - 写锁</p><p>意向共享锁(IS) </p><p>意向排他锁(IX)</p><p>间隙锁</p><h3 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h3><h4 id="死锁的产生原因"><a href="#死锁的产生原因" class="headerlink" title="死锁的产生原因"></a>死锁的产生原因</h4><ol><li>系统资源不足</li><li>进程运行 推进的顺序&#x2F;速度 不同</li><li>资源分配不当</li></ol><h4 id="死锁的必要条件"><a href="#死锁的必要条件" class="headerlink" title="死锁的必要条件"></a>死锁的必要条件</h4><ol><li>互斥一个资源每次只能被一个进程使用</li><li>请求与保持    一个进程因请求资源而阻塞时，对已获得的资源保持不放</li><li>不可剥夺        进程已获得的资源，在未使用完之前，不能强行剥夺</li><li>循环等待        若干进程之间形成一种头尾相接的循环等待资源关系</li></ol><h3 id="避免死锁"><a href="#避免死锁" class="headerlink" title="避免死锁"></a>避免死锁</h3><ul><li><p>合理的设计索引，区分度高的列放到组合索引前面，使业务 SQL 尽可能通过索引定位更少的行，减少锁竞争。</p></li><li><p>调整业务逻辑 SQL 执行顺序， 避免 update&#x2F;delete 长时间持有锁的 SQL 在事务前面。</p></li><li><p>避免大事务，将大事务拆成多个小事务</p></li><li><p>以固定的顺序访问表和行。</p><p>比如两个更新数据的事务，</p><p>事务 A 更新数据的顺序为 1，2;</p><p>事务 B 更新数据的顺序为 2，1。</p><p>这样更可能会造成死锁。</p></li><li><p>在并发比较高的系统中，不要显式加锁，特别是是在事务里显式加锁。</p><p>如 select … for update 语句，</p><p>如果是在事务里（运行了 start transaction 或设置了autocommit 等于0）,</p><p>那么就会锁定所查找到的记录。</p></li><li><p>尽量用主键&#x2F;索引去查找记录</p></li><li><p>优化 SQL 和表设计，减少同时占用太多资源的情况。</p><p>比如说，避免多个表join，将复杂 SQL 分解为多个简单的 SQL。</p></li></ul><h4 id="死锁的解除与预防"><a href="#死锁的解除与预防" class="headerlink" title="死锁的解除与预防"></a>死锁的解除与预防</h4><p>打破必要条件之一即可解锁，预防必要条件即可预防死锁。</p><ol><li>按同一顺序访问对象</li><li>避免事务中的用户交互</li><li>保持事务简短并在一个批处理中</li><li>使用低隔离级别</li><li>使用绑定连接</li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><table><thead><tr><th align="center">维度|锁类型</th><th align="center">表级锁</th><th align="center">页面锁</th><th align="center">行级锁</th></tr></thead><tbody><tr><td align="center">内存开销</td><td align="center">大</td><td align="center">中</td><td align="center">大</td></tr><tr><td align="center">加锁效率</td><td align="center">快</td><td align="center">中</td><td align="center">慢</td></tr><tr><td align="center">颗粒度</td><td align="center">大</td><td align="center">中</td><td align="center">小</td></tr><tr><td align="center">锁冲突概率</td><td align="center">高</td><td align="center">中</td><td align="center">低</td></tr><tr><td align="center">是否会死锁</td><td align="center">否</td><td align="center">是</td><td align="center">是</td></tr><tr><td align="center">并发度</td><td align="center">小</td><td align="center">一般</td><td align="center">高</td></tr><tr><td align="center">应用特点</td><td align="center">查询&amp;少量索引条件的更新</td><td align="center">-</td><td align="center">并发查询&amp;大量索引条件的不同数据更新</td></tr><tr><td align="center">应用场景</td><td align="center">Web</td><td align="center">-</td><td align="center">在线事务处理系统</td></tr><tr><td align="center">存储引擎</td><td align="center">MyISAM、InnoDB</td><td align="center">-</td><td align="center">InnoDB</td></tr><tr><td align="center">锁争用参数</td><td align="center"><code>show status like &#39;table%&#39;</code> <br />Table_locks_waited<br /></td><td align="center">-</td><td align="center"><code>show status like &#39;InnoDB_row_lock%&#39;;</code><br />InnoDB_row_lock_waits<br />InnoDB_row_lock_time_avg<br /></td></tr></tbody></table>]]>
    </content>
    <id>https://mikeah2011.github.io/post/01_MySQL/%E9%94%81%E6%9C%BA%E5%88%B6.html</id>
    <link href="https://mikeah2011.github.io/post/01_MySQL/%E9%94%81%E6%9C%BA%E5%88%B6.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>[TOC] 锁机制 计算机协调 多个进程或线程 并发访问某一资源的机制。 锁的类型及其特点 开销、加锁、颗粒度、冲突、并发等角度分析 表级锁(table-level locking) 开销大，加锁快，不会出现死锁，颗粒度大，锁冲突概率高，并…</summary>
    <title>MySQL - 锁</title>
    <updated>2026-05-01T13:12:43.904Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="MySQL" scheme="https://mikeah2011.github.io/categories/MySQL/"/>
    <category term="MySQL" scheme="https://mikeah2011.github.io/tags/MySQL/"/>
    <category term="面试题" scheme="https://mikeah2011.github.io/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <content>
      <![CDATA[<blockquote><p>本文是浓缩版速答。<strong>每题尽量一句话讲清结论</strong>；详细原理点对应链接。</p></blockquote><h1 id="一、索引"><a href="#一、索引" class="headerlink" title="一、索引"></a>一、索引</h1><p><strong>Q: 为什么 MySQL 用 B+ 树而不是 B 树&#x2F;红黑树&#x2F;Hash？</strong><br>B+ 树非叶节点不存数据 → 一个节点能装更多 key → 树更矮 → 磁盘 IO 少；叶子节点链表 → 范围查询快。Hash 不支持范围、不支持排序。</p><p><strong>Q: 聚簇索引 vs 二级索引？</strong></p><ul><li>聚簇索引：叶子节点存整行数据，InnoDB 主键即聚簇索引</li><li>二级索引：叶子节点存主键值，需要”回表”</li></ul><p><strong>Q: 什么是覆盖索引？</strong> → <a href="/2019/05/15/MySQL/%E7%B4%A2%E5%BC%95/%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/">详细</a><br>SELECT 的字段全在二级索引里，不用回表。EXPLAIN 显示 <code>Using index</code>。</p><p><strong>Q: 最左前缀原则？</strong><br>联合索引 <code>(a, b, c)</code> 只能命中 <code>a</code> &#x2F; <code>a+b</code> &#x2F; <code>a+b+c</code>，不能 <code>b</code> 或 <code>b+c</code>。</p><p><strong>Q: 索引为什么会失效？</strong> → <a href="/2019/05/20/MySQL/%E7%B4%A2%E5%BC%95/%E5%A4%B1%E6%95%88%E5%8E%9F%E5%9B%A0/">12 种原因</a><br>函数运算、隐式转换、<code>%xxx</code>、OR 含非索引列、违反最左前缀、负向查询…</p><p><strong>Q: 什么时候不该建索引？</strong> → <a href="/2019/05/10/MySQL/%E7%B4%A2%E5%BC%95/%E4%BC%98%E7%BC%BA%E7%82%B9/">优缺点</a><br>小表、低选择性字段、写多读少、字段经常更新。</p><h1 id="二、事务与隔离级别"><a href="#二、事务与隔离级别" class="headerlink" title="二、事务与隔离级别"></a>二、事务与隔离级别</h1><p><strong>Q: ACID 是什么？</strong></p><ul><li>A 原子性：要么全做，要么全不做（undo log）</li><li>C 一致性：业务规则不被破坏（前 3 项的结果）</li><li>I 隔离性：并发事务相互不感知（锁 + MVCC）</li><li>D 持久性：提交后不丢（redo log）</li></ul><p><strong>Q: 4 种隔离级别？</strong></p><table><thead><tr><th>级别</th><th>脏读</th><th>不可重复读</th><th>幻读</th></tr></thead><tbody><tr><td>READ UNCOMMITTED</td><td>✗</td><td>✗</td><td>✗</td></tr><tr><td>READ COMMITTED</td><td>✓</td><td>✗</td><td>✗</td></tr><tr><td><strong>REPEATABLE READ</strong>（InnoDB 默认）</td><td>✓</td><td>✓</td><td>✓（间隙锁）</td></tr><tr><td>SERIALIZABLE</td><td>✓</td><td>✓</td><td>✓</td></tr></tbody></table><p><strong>Q: MVCC 是什么？</strong><br>多版本并发控制。每行有 <code>trx_id</code> 和 <code>roll_ptr</code>，读时按 ReadView 找对自己可见的版本。<strong>实现 RC&#x2F;RR 两种隔离级别下的非阻塞读</strong>。</p><p><strong>Q: 当前读 vs 快照读？</strong></p><ul><li>快照读：普通 <code>SELECT</code>，走 MVCC 历史版本</li><li>当前读：<code>SELECT ... FOR UPDATE</code> &#x2F; <code>LOCK IN SHARE MODE</code> &#x2F; 所有写操作，加锁读最新</li></ul><h1 id="三、锁"><a href="#三、锁" class="headerlink" title="三、锁"></a>三、锁</h1><p><strong>Q: InnoDB 锁类型？</strong></p><table><thead><tr><th>锁</th><th>说明</th></tr></thead><tbody><tr><td>共享锁 S &#x2F; 排他锁 X</td><td>读锁 &#x2F; 写锁</td></tr><tr><td><strong>行锁</strong></td><td>锁单行</td></tr><tr><td><strong>间隙锁 (Gap Lock)</strong></td><td>锁两行之间的”缝”，防幻读</td></tr><tr><td><strong>临键锁 (Next-Key)</strong></td><td>行锁 + 间隙锁，RR 隔离级别默认加它</td></tr><tr><td><strong>意向锁 IS&#x2F;IX</strong></td><td>表级，标记”我打算在某行加 S&#x2F;X 锁”</td></tr></tbody></table><p><strong>Q: 死锁怎么排查？</strong></p><figure class="highlight sql"><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"><span class="keyword">SHOW</span> ENGINE INNODB STATUS;   <span class="comment">-- 看 LATEST DETECTED DEADLOCK</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> performance_schema.data_locks;</span><br></pre></td></tr></table></figure><p><strong>Q: 死锁怎么避免？</strong></p><ul><li>多个事务以<strong>相同顺序</strong>访问表&#x2F;行</li><li>事务保持<strong>短小</strong></li><li>用<strong>低隔离级别</strong>（RC 不加间隙锁）</li><li>给热点资源建索引（避免锁升级到表锁）</li></ul><h1 id="四、SQL-优化"><a href="#四、SQL-优化" class="headerlink" title="四、SQL 优化"></a>四、SQL 优化</h1><p><strong>Q: EXPLAIN 重点看哪几列？</strong></p><ul><li><code>type</code>：访问类型，从好到差 <code>system &gt; const &gt; eq_ref &gt; ref &gt; range &gt; index &gt; ALL</code></li><li><code>key</code>：实际用到的索引</li><li><code>rows</code>：预估扫描行数</li><li><code>Extra</code>：<code>Using index</code>（覆盖）&#x2F; <code>Using filesort</code>（额外排序，差）&#x2F; <code>Using temporary</code>（临时表，差）</li></ul><p><strong>Q: 慢 SQL 怎么排查？</strong></p><ol><li>开慢查询日志 <code>slow_query_log = 1</code></li><li>用 <code>mysqldumpslow</code> &#x2F; <code>pt-query-digest</code> 聚合</li><li>单条 <code>EXPLAIN</code> &#x2F; <code>EXPLAIN ANALYZE</code></li><li><code>SHOW PROFILE</code> 看每阶段耗时</li></ol><p><strong>Q: LIMIT 100000, 20 怎么优化？</strong><br>延迟关联：先用覆盖索引拿 20 个 id，再 join 主表。详见 <a href="/2019/05/15/MySQL/%E7%B4%A2%E5%BC%95/%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/">覆盖索引</a>。</p><p><strong>Q: COUNT(*) vs COUNT(1) vs COUNT(列)？</strong></p><ul><li><code>COUNT(*)</code> &#x2F; <code>COUNT(1)</code> 等价，统计行数（含 NULL）</li><li><code>COUNT(列)</code> 不统计 NULL</li><li>InnoDB 都需要扫描，没有 MyISAM 那种缓存</li></ul><h1 id="五、存储引擎"><a href="#五、存储引擎" class="headerlink" title="五、存储引擎"></a>五、存储引擎</h1><p><strong>Q: MyISAM vs InnoDB？</strong></p><table><thead><tr><th>维度</th><th>MyISAM</th><th>InnoDB</th></tr></thead><tbody><tr><td>事务</td><td>✗</td><td>✓</td></tr><tr><td>行锁</td><td>✗（表锁）</td><td>✓</td></tr><tr><td>外键</td><td>✗</td><td>✓</td></tr><tr><td>崩溃恢复</td><td>弱</td><td>强（redo log）</td></tr><tr><td>COUNT(*)</td><td>缓存，O(1)</td><td>需扫描</td></tr><tr><td>全文索引</td><td>✓</td><td>5.6+ ✓</td></tr></tbody></table><p><strong>结论</strong>：除非纯只读统计场景，<strong>永远用 InnoDB</strong>。</p><h1 id="六、主从复制-高可用"><a href="#六、主从复制-高可用" class="headerlink" title="六、主从复制 &amp; 高可用"></a>六、主从复制 &amp; 高可用</h1><p><strong>Q: 主从复制原理？</strong></p><ol><li>主库写入 binlog</li><li>从库 IO 线程拉取 binlog → 写 relay log</li><li>从库 SQL 线程回放 relay log</li></ol><p><strong>Q: 主从延迟怎么解决？</strong></p><ul><li>大事务拆小</li><li>从库开<strong>并行复制</strong> (<code>slave_parallel_workers</code>)</li><li>关键读走主库（”读己写”场景）</li><li>上 ProxySQL &#x2F; 中间件路由</li></ul><p><strong>Q: binlog 三种格式？</strong></p><ul><li><code>STATEMENT</code>：记 SQL，体积小但函数&#x2F;触发器主从结果可能不一致</li><li><code>ROW</code>：记每行变化，<strong>安全推荐</strong>，体积大</li><li><code>MIXED</code>：自动二选一</li></ul><h1 id="七、其它高频"><a href="#七、其它高频" class="headerlink" title="七、其它高频"></a>七、其它高频</h1><p><strong>Q: redo log 和 binlog 区别？</strong></p><ul><li>redo log：InnoDB 引擎层，物理日志（”在某页改某字节”），循环写</li><li>binlog：Server 层，逻辑日志，追加写，用于复制和恢复</li></ul><p><strong>Q: 一条 SQL 的执行流程？</strong><br>连接器 → 查询缓存（8.0 移除）→ 分析器（词法&#x2F;语法）→ 优化器（选索引、决定 join 顺序）→ 执行器 → 存储引擎</p><p><strong>Q: 表设计三范式？</strong><br>1NF：字段不可再分；2NF：非主键完全依赖主键；3NF：非主键不传递依赖。<strong>实际开发常反范式（冗余字段）换性能</strong>。</p><h1 id="配图"><a href="#配图" class="headerlink" title="配图"></a>配图</h1><p><img src="/images/643.png" alt="索引执行流程"></p><p><img src="/images/%E7%B4%A2%E5%BC%95%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98.png" alt="索引常见面试问题脑图"></p><h1 id="进一步阅读"><a href="#进一步阅读" class="headerlink" title="进一步阅读"></a>进一步阅读</h1><ul><li><a href="/2019/04/20/MySQL/%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/">MySQL 数据类型选型</a></li><li><a href="/2019/05/15/MySQL/%E7%B4%A2%E5%BC%95/%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/">覆盖索引</a></li><li><a href="/2019/05/10/MySQL/%E7%B4%A2%E5%BC%95/%E4%BC%98%E7%BC%BA%E7%82%B9/">索引优缺点</a></li><li><a href="/2019/05/20/MySQL/%E7%B4%A2%E5%BC%95/%E5%A4%B1%E6%95%88%E5%8E%9F%E5%9B%A0/">索引失效 12 种原因</a></li><li>《高性能 MySQL》第 3、4、5、6 章</li></ul>]]>
    </content>
    <id>https://mikeah2011.github.io/post/01_MySQL/%E9%9D%A2%E8%AF%95%E9%A2%98.html</id>
    <link href="https://mikeah2011.github.io/post/01_MySQL/%E9%9D%A2%E8%AF%95%E9%A2%98.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>MySQL 高频面试题速答：索引（B+ 树、聚簇/二级、覆盖、最左前缀、失效）、事务（ACID、隔离级别、MVCC）、锁（行锁/间隙锁/意向锁、死锁）、存储引擎、SQL 优化、主从复制。一题一行，配跳转链接。</summary>
    <title>MySQL 面试题速查</title>
    <updated>2026-05-01T13:12:43.904Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="设计模式" scheme="https://mikeah2011.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <content>
      <![CDATA[<table><thead><tr><th align="center">设计模式</th><th align="center">概念</th><th align="center">代码</th></tr></thead><tbody><tr><td align="center">工厂模式</td><td align="center">用来实现创建对象和对象的使用分离，将对象的创建交给专门的工厂类负责<br />简单工厂模式<br />工厂模式<br />抽象工厂模式</td><td align="center">简单工厂模式，根据不同的入参new实例化不同的类对象</td></tr><tr><td align="center">单例模式</td><td align="center">保证一个类仅有一个实例，并提供一个访问它的全局访问点。</td><td align="center"></td></tr><tr><td align="center">适配器模式</td><td align="center">将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。</td><td align="center"></td></tr><tr><td align="center">观察者模式</td><td align="center">定义对象间的一种一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都得到通知并被自动更新，简单来说该模式相当于源 - 监听（Source-Listener）模式（即监听器）、发布 - 订阅（Publish-Subscribe）模式</td><td align="center"></td></tr><tr><td align="center">策略模式</td><td align="center">用相同的方法实现不同的功能</td><td align="center"></td></tr><tr><td align="center">注册树模式</td><td align="center"></td><td align="center"></td></tr></tbody></table>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>| 设计模式 | 概念 | 代码 | | :--------: | :----------------------------------------------------------: | :----------------------…</summary>
    <title>常见的设计模式</title>
    <updated>2026-05-01T13:12:43.908Z</updated>
  </entry>
  <entry>
    <author>
      <name>Michael</name>
    </author>
    <category term="PHP" scheme="https://mikeah2011.github.io/categories/PHP/"/>
    <category term="PHP" scheme="https://mikeah2011.github.io/tags/PHP/"/>
    <content>
      <![CDATA[<table><thead><tr><th align="center">区别</th><th align="center">接口类</th><th align="center">抽象类</th></tr></thead><tbody><tr><td align="center">条件</td><td align="center">必须实现接口类中所有的<strong>公共</strong>方法</td><td align="center">必须定义父类中所有的<strong>抽象</strong>方法</td></tr><tr><td align="center">关键字</td><td align="center"><code>interface</code></td><td align="center"><code>abstract</code></td></tr><tr><td align="center">方式</td><td align="center">实现<code>implements</code></td><td align="center">继承<code>extends</code></td></tr><tr><td align="center">特性</td><td align="center">实现多个接口</td><td align="center">只能继承一个抽象类</td></tr><tr><td align="center">修饰符</td><td align="center"><code>public</code> 公有的</td><td align="center">保持一致或更为宽泛</td></tr></tbody></table>]]>
    </content>
    <id>https://mikeah2011.github.io/post/05_PHP/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_interface%E4%B8%8Eabstract.html</id>
    <link href="https://mikeah2011.github.io/post/05_PHP/%E6%AF%94%E8%BE%83%E5%8C%BA%E5%88%AB_interface%E4%B8%8Eabstract.html"/>
    <published>2021-03-20T15:05:07.000Z</published>
    <summary>| 区别 | 接口类 | 抽象类 | | :----: | :--------------------------------: | :------------------------------: | | 条件 | 必须实现接口类中所有的…</summary>
    <title>接口与抽象类</title>
    <updated>2026-05-01T13:12:43.908Z</updated>
  </entry>
</feed>
