<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://me.onefeng.xyz/feed.xml" rel="self" type="application/atom+xml" /><link href="https://me.onefeng.xyz/" rel="alternate" type="text/html" /><updated>2026-08-18T11:08:23+08:00</updated><id>https://me.onefeng.xyz/feed.xml</id><title type="html">ONEFENG</title><subtitle>ONEFENG的个人博客</subtitle><author><name>onefeng</name></author><entry><title type="html">从零搭建本地RAG知识库问答系统</title><link href="https://me.onefeng.xyz/2026/07/21/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0rag%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html" title="从零搭建本地RAG知识库问答系统" /><published>2026-07-21T00:00:00+08:00</published><updated>2026-07-21T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2026/07/21/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0rag%E7%B3%BB%E7%BB%9F</id><content type="html" xml:base="https://me.onefeng.xyz/2026/07/21/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0rag%E7%B3%BB%E7%BB%9F/"><![CDATA[<p>RAG（Retrieval-Augmented Generation，检索增强生成）通过“先检索资料，再让大模型回答”的方式，为大模型补充私有知识和最新信息。本文基于我的 <code class="language-plaintext highlighter-rouge">rag-demo</code> 项目，搭建一套可以本地部署的中文 RAG 系统。</p>

<p>项目不是简单的向量检索示例，而是实现了一条较完整的 RAG 链路：文档解析与切块、Embedding、Dense 向量召回、BM25 关键词召回、RRF 融合、BGE Reranker 精排，最后由 Qwen 根据资料生成带引用的答案。</p>

<h2 id="为什么需要rag">为什么需要RAG</h2>

<p>直接使用大模型进行知识问答，通常会遇到几个问题：</p>

<ol>
  <li>模型训练数据存在时间边界，无法了解最新资料</li>
  <li>企业文档、产品手册和内部制度不在公开训练数据中</li>
  <li>模型可能生成看似合理但实际错误的内容</li>
  <li>回答缺少来源，用户难以验证结论</li>
</ol>

<p>微调可以改变模型的行为和表达风格，但不适合频繁更新大量事实知识。RAG 将知识保存在外部数据库中，文档变化时只需要重新入库，不需要重新训练模型。</p>

<p>系统整体流程如下：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>文档 -&gt; 解析 -&gt; 切块 -&gt; Embedding -&gt; Milvus
                              +-&gt; Dense Vector
                              +-&gt; BM25 Sparse

问题 -&gt; Dense召回 + BM25召回 -&gt; RRF融合 -&gt; Reranker精排
     -&gt; 拼接上下文 -&gt; LLM生成答案 -&gt; 返回引用来源
</code></pre></div></div>

<h2 id="技术选型">技术选型</h2>

<p>本项目全部组件都可以在本地运行：</p>

<ul>
  <li>RAG API：FastAPI</li>
  <li>大语言模型：Ollama + Qwen2.5 7B</li>
  <li>Embedding：BAAI/bge-large-zh-v1.5</li>
  <li>Reranker：BAAI/bge-reranker-v2-m3</li>
  <li>模型服务：Hugging Face Text Embeddings Inference（TEI）</li>
  <li>向量数据库：Milvus Standalone</li>
  <li>对象存储与元数据：MinIO + etcd</li>
  <li>可视化管理：Attu</li>
</ul>

<p>Embedding 和 Reranker 默认使用 GPU。推荐 Linux、Docker Compose v2、NVIDIA Container Toolkit，以及 12GB 以上显存；当前配置更适合 16GB 显存环境。</p>

<p>先确认 Docker 容器可以使用 GPU：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">--gpus</span> all <span class="se">\</span>
  nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
</code></pre></div></div>

<h2 id="项目结构">项目结构</h2>

<p>核心代码位于 <code class="language-plaintext highlighter-rouge">app/</code> 目录：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>app/
├── chunking.py       # 文本清洗与切块
├── clients.py        # Embedding、Reranker和LLM客户端
├── config.py         # 环境变量配置
├── main.py           # FastAPI接口与生命周期
├── models.py         # 请求和响应模型
├── parsers.py        # TXT、PDF、DOCX等文件解析
├── rag.py            # RAG主流程
└── vector_store.py   # Milvus集合、入库和混合检索
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">compose.yaml</code> 定义 Milvus、etcd、MinIO、Ollama、Embedding、Reranker、API 和 Attu 服务。各模块通过 HTTP 或 Milvus SDK 通信，后续可以单独替换其中任意组件。</p>

<h2 id="启动本地环境">启动本地环境</h2>

<p>复制配置并启动服务：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp</span> .env.example .env
make up
make pull-model
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">make up</code> 会构建 API 镜像、下载 BGE 模型，并启动全部容器。第一次执行需要下载较大的模型文件，耗时取决于网络速度。</p>

<p>常用命令：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose ps
make logs
make <span class="nb">test
</span>make lint
make down
</code></pre></div></div>

<p>服务启动后检查健康状态：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8000/health
</code></pre></div></div>

<p>API 文档地址为 <code class="language-plaintext highlighter-rouge">http://localhost:8000/docs</code>，Attu 管理页面为 <code class="language-plaintext highlighter-rouge">http://localhost:3000</code>。</p>

<h2 id="文档解析">文档解析</h2>

<p>系统支持 TXT、Markdown、CSV、JSON、PDF 和 DOCX。不同格式最终都转换为普通文本，再进入统一的切块流程。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_file</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">suffix</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">filename</span><span class="p">).</span><span class="n">suffix</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">suffix</span> <span class="ow">in</span> <span class="p">{</span><span class="s">".txt"</span><span class="p">,</span> <span class="s">".md"</span><span class="p">,</span> <span class="s">".csv"</span><span class="p">,</span> <span class="s">".json"</span><span class="p">}:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">content</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">UnicodeDecodeError</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">content</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"gb18030"</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">suffix</span> <span class="o">==</span> <span class="s">".pdf"</span><span class="p">:</span>
        <span class="n">reader</span> <span class="o">=</span> <span class="n">PdfReader</span><span class="p">(</span><span class="n">io</span><span class="p">.</span><span class="n">BytesIO</span><span class="p">(</span><span class="n">content</span><span class="p">))</span>
        <span class="k">return</span> <span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">page</span><span class="p">.</span><span class="n">extract_text</span><span class="p">()</span> <span class="ow">or</span> <span class="s">""</span> <span class="k">for</span> <span class="n">page</span> <span class="ow">in</span> <span class="n">reader</span><span class="p">.</span><span class="n">pages</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">suffix</span> <span class="o">==</span> <span class="s">".docx"</span><span class="p">:</span>
        <span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">(</span><span class="n">io</span><span class="p">.</span><span class="n">BytesIO</span><span class="p">(</span><span class="n">content</span><span class="p">))</span>
        <span class="k">return</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">text</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">document</span><span class="p">.</span><span class="n">paragraphs</span><span class="p">)</span>

    <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"unsupported file type"</span><span class="p">)</span>
</code></pre></div></div>

<p>PDF 文本提取只适用于包含文字层的文件。扫描件需要先接入 OCR；复杂表格、双栏排版和图片也需要更专业的解析器，否则切块前就可能丢失语义。</p>

<h2 id="文本切块">文本切块</h2>

<p>文档不能不加处理地直接生成一个向量。一方面 Embedding 模型有输入长度限制，另一方面整篇文档只生成一个向量会丢失局部语义。</p>

<p>项目默认参数如下：</p>

<pre><code class="language-dotenv">CHUNK_SIZE=500
CHUNK_OVERLAP=80
</code></pre>

<p>切块时优先按照中文标点、英文标点和换行分割，同时保留一定重叠内容：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">chunk_text</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">500</span><span class="p">,</span> <span class="n">overlap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">80</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="n">text</span> <span class="o">=</span> <span class="n">normalize_text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">overlap</span> <span class="o">&gt;=</span> <span class="n">size</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"chunk overlap must be smaller than chunk size"</span><span class="p">)</span>

    <span class="n">units</span> <span class="o">=</span> <span class="p">[</span>
        <span class="n">unit</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">unit</span> <span class="ow">in</span> <span class="n">re</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s">"(?&lt;=[。！？!?；;\.])|\n+"</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">unit</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span>
    <span class="p">]</span>

    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">current</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">unit</span> <span class="ow">in</span> <span class="n">units</span><span class="p">:</span>
        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">unit</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">size</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">current</span><span class="p">:</span>
                <span class="n">chunks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
                <span class="n">current</span> <span class="o">=</span> <span class="s">""</span>
            <span class="n">step</span> <span class="o">=</span> <span class="n">size</span> <span class="o">-</span> <span class="n">overlap</span>
            <span class="n">chunks</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span>
                <span class="n">unit</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">size</span><span class="p">]</span>
                <span class="k">for</span> <span class="n">start</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">unit</span><span class="p">),</span> <span class="n">step</span><span class="p">)</span>
            <span class="p">)</span>
            <span class="k">continue</span>

        <span class="n">candidate</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">current</span><span class="si">}</span><span class="se">\n</span><span class="si">{</span><span class="n">unit</span><span class="si">}</span><span class="s">"</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">if</span> <span class="n">current</span> <span class="k">else</span> <span class="n">unit</span>
        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">candidate</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">size</span><span class="p">:</span>
            <span class="n">current</span> <span class="o">=</span> <span class="n">candidate</span>
            <span class="k">continue</span>

        <span class="n">chunks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
        <span class="n">prefix</span> <span class="o">=</span> <span class="n">current</span><span class="p">[</span><span class="o">-</span><span class="n">overlap</span><span class="p">:]</span> <span class="k">if</span> <span class="n">overlap</span> <span class="k">else</span> <span class="s">""</span>
        <span class="n">current</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">prefix</span><span class="si">}{</span><span class="n">unit</span><span class="si">}</span><span class="s">"</span>

    <span class="k">if</span> <span class="n">current</span><span class="p">:</span>
        <span class="n">chunks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">chunk</span> <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">chunks</span> <span class="k">if</span> <span class="n">chunk</span><span class="p">.</span><span class="n">strip</span><span class="p">()]</span>
</code></pre></div></div>

<p>Overlap 可以降低答案刚好跨越两个切片时的信息损失，但过大会产生重复召回并增加存储量。实际项目应根据文档类型、Embedding 模型长度和问答粒度进行评估，不能把固定的 500 字作为通用最优值。</p>

<h2 id="生成embedding">生成Embedding</h2>

<p>Embedding 将文本转换为浮点向量，语义相近的文本在向量空间中距离也更接近。项目通过 TEI 的 <code class="language-plaintext highlighter-rouge">/embed</code> 接口批量生成向量：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">embed</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]]:</span>
    <span class="n">vectors</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">batch_size</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">embedding_batch_size</span>

    <span class="k">for</span> <span class="n">start</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">texts</span><span class="p">),</span> <span class="n">batch_size</span><span class="p">):</span>
        <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
            <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">embedding_base_url</span><span class="p">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s">'/'</span><span class="p">)</span><span class="si">}</span><span class="s">/embed"</span><span class="p">,</span>
            <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s">"inputs"</span><span class="p">:</span> <span class="n">texts</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">start</span> <span class="o">+</span> <span class="n">batch_size</span><span class="p">],</span> <span class="s">"truncate"</span><span class="p">:</span> <span class="bp">True</span><span class="p">},</span>
            <span class="n">timeout</span><span class="o">=</span><span class="mi">120</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="n">response</span><span class="p">.</span><span class="n">raise_for_status</span><span class="p">()</span>
        <span class="n">vectors</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span>

    <span class="k">return</span> <span class="n">vectors</span>
</code></pre></div></div>

<p>批量请求可以提高 GPU 吞吐量。批次过大可能导致显存不足，过小则不能充分利用 GPU，因此项目通过 <code class="language-plaintext highlighter-rouge">EMBEDDING_BATCH_SIZE</code> 控制批量大小。</p>

<h2 id="milvus数据结构">Milvus数据结构</h2>

<p>每个文档切片在 Milvus 中保存以下字段：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">id</code>：根据来源、序号和文本生成的 SHA-256</li>
  <li><code class="language-plaintext highlighter-rouge">text</code>：切片原文</li>
  <li><code class="language-plaintext highlighter-rouge">source</code>：文件名或业务来源</li>
  <li><code class="language-plaintext highlighter-rouge">metadata</code>：部门、内容类型和切片序号等信息</li>
  <li><code class="language-plaintext highlighter-rouge">vector</code>：Dense 浮点向量</li>
  <li><code class="language-plaintext highlighter-rouge">sparse_vector</code>：Milvus 根据文本生成的 BM25 稀疏向量</li>
</ul>

<p>Dense 字段使用 COSINE 相似度，Sparse 字段使用 BM25，并为中文配置 jieba 分词器：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FieldSchema</span><span class="p">(</span>
    <span class="s">"text"</span><span class="p">,</span>
    <span class="n">DataType</span><span class="p">.</span><span class="n">VARCHAR</span><span class="p">,</span>
    <span class="n">max_length</span><span class="o">=</span><span class="mi">65535</span><span class="p">,</span>
    <span class="n">enable_analyzer</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">analyzer_params</span><span class="o">=</span><span class="p">{</span><span class="s">"tokenizer"</span><span class="p">:</span> <span class="s">"jieba"</span><span class="p">},</span>
<span class="p">)</span>
<span class="n">FieldSchema</span><span class="p">(</span><span class="s">"vector"</span><span class="p">,</span> <span class="n">DataType</span><span class="p">.</span><span class="n">FLOAT_VECTOR</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="n">dimension</span><span class="p">)</span>
<span class="n">FieldSchema</span><span class="p">(</span><span class="s">"sparse_vector"</span><span class="p">,</span> <span class="n">DataType</span><span class="p">.</span><span class="n">SPARSE_FLOAT_VECTOR</span><span class="p">)</span>
</code></pre></div></div>

<p>同一个 <code class="language-plaintext highlighter-rouge">source</code> 再次上传时，系统会先删除旧切片，再写入新切片，避免知识库中出现多个版本的重复内容。</p>

<p>更换 Embedding 模型时必须注意向量维度。如果新模型维度与原 Collection 不一致，应该修改 Collection 名称或重建数据：</p>

<pre><code class="language-dotenv">EMBEDDING_MODEL=BAAI/bge-m3
MILVUS_COLLECTION=rag_documents_hybrid_bge_m3
</code></pre>

<h2 id="dense与bm25混合检索">Dense与BM25混合检索</h2>

<p>只使用 Dense 检索容易遗漏产品编号、错误码、姓名等精确关键词；只使用 BM25 又难以理解同义词和自然语言表达。因此项目同时发起两路召回：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dense_request</span> <span class="o">=</span> <span class="n">AnnSearchRequest</span><span class="p">(</span>
    <span class="n">data</span><span class="o">=</span><span class="p">[</span><span class="n">query_vector</span><span class="p">],</span>
    <span class="n">anns_field</span><span class="o">=</span><span class="s">"vector"</span><span class="p">,</span>
    <span class="n">param</span><span class="o">=</span><span class="p">{</span><span class="s">"metric_type"</span><span class="p">:</span> <span class="s">"COSINE"</span><span class="p">,</span> <span class="s">"params"</span><span class="p">:</span> <span class="p">{}},</span>
    <span class="n">limit</span><span class="o">=</span><span class="n">top_k</span><span class="p">,</span>
<span class="p">)</span>

<span class="n">bm25_request</span> <span class="o">=</span> <span class="n">AnnSearchRequest</span><span class="p">(</span>
    <span class="n">data</span><span class="o">=</span><span class="p">[</span><span class="n">question</span><span class="p">],</span>
    <span class="n">anns_field</span><span class="o">=</span><span class="s">"sparse_vector"</span><span class="p">,</span>
    <span class="n">param</span><span class="o">=</span><span class="p">{</span><span class="s">"metric_type"</span><span class="p">:</span> <span class="s">"BM25"</span><span class="p">,</span> <span class="s">"params"</span><span class="p">:</span> <span class="p">{}},</span>
    <span class="n">limit</span><span class="o">=</span><span class="n">top_k</span><span class="p">,</span>
<span class="p">)</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">collection</span><span class="p">.</span><span class="n">hybrid_search</span><span class="p">(</span>
    <span class="n">reqs</span><span class="o">=</span><span class="p">[</span><span class="n">dense_request</span><span class="p">,</span> <span class="n">bm25_request</span><span class="p">],</span>
    <span class="n">rerank</span><span class="o">=</span><span class="n">RRFRanker</span><span class="p">(</span><span class="n">k</span><span class="o">=</span><span class="mi">60</span><span class="p">),</span>
    <span class="n">limit</span><span class="o">=</span><span class="n">top_k</span><span class="p">,</span>
    <span class="n">output_fields</span><span class="o">=</span><span class="p">[</span><span class="s">"text"</span><span class="p">,</span> <span class="s">"source"</span><span class="p">,</span> <span class="s">"metadata"</span><span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>RRF（Reciprocal Rank Fusion）根据文档在不同结果列表中的排名进行融合。它不要求 Dense 分数和 BM25 分数位于同一数值范围，比直接对两种分数加权更加稳定。</p>

<h2 id="使用reranker精排">使用Reranker精排</h2>

<p>向量检索适合从大量数据中快速召回候选，但它通常只分别编码问题和文档。Cross-Encoder Reranker 会同时读取问题与候选文本，计算更准确的相关性分数。</p>

<p>项目先召回 <code class="language-plaintext highlighter-rouge">top_k * 4</code> 个候选，最多不超过 50 个，再精排并保留最终结果：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">candidate_k</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="nb">max</span><span class="p">(</span><span class="n">top_k</span><span class="p">,</span> <span class="n">top_k</span> <span class="o">*</span> <span class="mi">4</span><span class="p">))</span>
<span class="n">vector</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="n">embeddings</span><span class="p">.</span><span class="n">embed</span><span class="p">([</span><span class="n">question</span><span class="p">]))[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">candidates</span> <span class="o">=</span> <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">vector</span><span class="p">,</span> <span class="n">candidate_k</span><span class="p">)</span>
<span class="n">hits</span> <span class="o">=</span> <span class="k">await</span> <span class="n">reranker</span><span class="p">.</span><span class="n">rerank</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">candidates</span><span class="p">,</span> <span class="n">top_k</span><span class="p">)</span>
</code></pre></div></div>

<p>精排提高了准确率，但计算量明显高于向量检索。如果候选数量过多，延迟和 GPU 显存占用都会增加。实际服务需要在召回率、准确率和响应时间之间平衡。</p>

<h2 id="构造上下文并调用llm">构造上下文并调用LLM</h2>

<p>检索结果按照编号拼接，并通过 <code class="language-plaintext highlighter-rouge">MAX_CONTEXT_CHARS</code> 限制上下文长度：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[1] 来源：产品手册
退款申请应在订单完成后七天内提交。

[2] 来源：客服制度
退款审核通常需要两个工作日。
</code></pre></div></div>

<p>System Prompt 明确限制模型只能依据资料回答，并要求引用资料编号：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">system</span> <span class="o">=</span> <span class="p">(</span>
    <span class="s">"你是一个严谨的知识库问答助手。只能依据给定的参考资料回答。"</span>
    <span class="s">"如果资料不足，请明确回答‘根据当前知识库无法确定’，不要编造。"</span>
    <span class="s">"回答应简洁清晰，并在相关陈述后引用资料编号，例如 [1]。"</span>
<span class="p">)</span>
</code></pre></div></div>

<p>LLM 统一使用 OpenAI-compatible <code class="language-plaintext highlighter-rouge">/chat/completions</code> 接口。默认连接 Ollama，也可以切换到 vLLM、Xinference 或其他兼容服务，而不需要修改 RAG 主流程。</p>

<h2 id="rag核心流程">RAG核心流程</h2>

<p><code class="language-plaintext highlighter-rouge">RAGService</code> 将入库和查询逻辑组合起来：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">ingest</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">source</span><span class="p">,</span> <span class="n">metadata</span><span class="p">):</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="n">chunk_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">chunk_size</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">chunk_overlap</span><span class="p">)</span>
    <span class="n">vectors</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">.</span><span class="n">embed</span><span class="p">(</span><span class="n">chunks</span><span class="p">)</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">to_thread</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">store</span><span class="p">.</span><span class="n">replace_source</span><span class="p">,</span> <span class="n">source</span><span class="p">,</span> <span class="n">chunks</span><span class="p">,</span> <span class="n">vectors</span><span class="p">,</span> <span class="n">metadata</span>
    <span class="p">)</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">query</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">threshold</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">hits</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">retrieve</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">top_k</span><span class="p">,</span> <span class="n">threshold</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">hits</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">"根据当前知识库无法确定。请先上传相关资料。"</span><span class="p">,</span> <span class="p">[]</span>

    <span class="n">context</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">format_context</span><span class="p">(</span><span class="n">hits</span><span class="p">)</span>
    <span class="n">answer</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">llm</span><span class="p">.</span><span class="n">chat</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">answer</span><span class="p">,</span> <span class="n">hits</span>
</code></pre></div></div>

<p>Milvus Python SDK 是同步接口，因此使用 <code class="language-plaintext highlighter-rouge">asyncio.to_thread</code> 避免阻塞 FastAPI 的事件循环。</p>

<h2 id="文档入库接口">文档入库接口</h2>

<p>直接写入文本：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8000/v1/documents/text <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "source": "产品手册",
    "text": "退款申请应在订单完成后七天内提交。审核通常需要两个工作日。",
    "metadata": {"department": "客服"}
  }'</span>
</code></pre></div></div>

<p>上传文件：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8000/v1/documents/file <span class="se">\</span>
  <span class="nt">-F</span> <span class="s1">'file=@./your-document.pdf'</span>
</code></pre></div></div>

<p>接口会返回生成的切片数量：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"产品手册"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"chunks"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="知识库问答接口">知识库问答接口</h2>

<p>发送问题：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8000/v1/query <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"question": "退款审核需要多久？", "top_k": 5}'</span>
</code></pre></div></div>

<p>响应同时返回答案和引用，便于前端展示来源或进行人工核验：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"退款审核通常需要两个工作日。[1]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"citations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"产品手册"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"退款申请应在订单完成后七天内提交。审核通常需要两个工作日。"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.98</span><span class="p">,</span><span class="w">
      </span><span class="nl">"fusion_score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0325</span><span class="p">,</span><span class="w">
      </span><span class="nl">"rerank_score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.98</span><span class="p">,</span><span class="w">
      </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"department"</span><span class="p">:</span><span class="w"> </span><span class="s2">"客服"</span><span class="p">,</span><span class="w"> </span><span class="nl">"chunk_index"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="如何评估rag效果">如何评估RAG效果</h2>

<p>RAG 优化不能只观察最终答案。应该把链路拆成多个阶段分别评估：</p>

<ol>
  <li>解析质量：文本、表格和标题是否被正确提取</li>
  <li>切块质量：答案需要的上下文是否位于同一切片</li>
  <li>召回率：正确切片是否进入候选集合</li>
  <li>精排质量：正确切片是否出现在最终 Top K</li>
  <li>忠实度：模型回答是否完全来自检索资料</li>
  <li>引用准确性：答案中的编号是否指向对应证据</li>
  <li>性能：统计 Embedding、检索、精排和生成阶段的耗时</li>
</ol>

<p>项目已经为文本清洗和切块编写 pytest 测试。生产环境还应建立固定的“问题—标准答案—相关文档”评测集，在修改模型、切块参数或检索策略后进行回归测试。</p>

<h2 id="常见问题">常见问题</h2>

<h3 id="检索结果看起来相关但无法回答问题">检索结果看起来相关但无法回答问题</h3>

<p>通常是切块粒度不合适，或者答案依赖的标题和正文被拆开。可以保留章节标题、调整 Chunk Size，或者使用按 Markdown 标题和文档结构切分的策略。</p>

<h3 id="专有名词和编号无法召回">专有名词和编号无法召回</h3>

<p>纯 Dense 检索对精确字符串不一定敏感，应保留 BM25 关键词召回。中文场景还需要检查分词结果，自定义词典可以改善产品名和行业术语的识别。</p>

<h3 id="回答仍然出现幻觉">回答仍然出现幻觉</h3>

<p>RAG 只能降低幻觉，不能完全消除。需要同时设置严格 Prompt、相关性阈值和上下文边界。没有可靠资料时，应让系统明确拒绝回答，而不是强行生成结论。</p>

<h3 id="更新模型后milvus报维度错误">更新模型后Milvus报维度错误</h3>

<p>不同 Embedding 模型的向量维度可能不同。更换模型后使用新的 Collection 名称，并重新生成全部文档向量，不能混用旧数据。</p>

<h3 id="gpu显存不足">GPU显存不足</h3>

<p>可以降低 Embedding Batch Size、换用更小的 LLM，或者将 Embedding、Reranker 和 LLM 分配到不同 GPU。资源有限时也可以关闭 Reranker，但需要重新评估检索准确率。</p>

<h2 id="生产环境改进">生产环境改进</h2>

<p>当前项目已经包含完整的基础链路，正式部署还建议补充：</p>

<ul>
  <li>为入库和查询接口增加认证、权限和限流</li>
  <li>使用异步任务队列处理大文件和批量文档</li>
  <li>保存原始文件，并记录版本、租户和权限元数据</li>
  <li>按租户或知识库增加 Milvus 过滤条件</li>
  <li>对 Prompt Injection 和恶意文档内容进行防护</li>
  <li>记录召回结果、引用、耗时和用户反馈</li>
  <li>定期备份 Milvus、MinIO 和业务元数据</li>
  <li>对重复文档、空文本和敏感信息进行检测</li>
</ul>

<p>删除 Docker Volume 会同时删除知识库数据和已下载模型，执行下面的命令前需要确认已经备份：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose down <span class="nt">-v</span>
</code></pre></div></div>

<h2 id="总结">总结</h2>

<p>一套可用的 RAG 系统不只是“把文档转成向量”。文档解析、切块策略、混合召回、结果精排、上下文构造、引用返回和离线评测都会影响最终效果。</p>

<p>本文项目通过 BGE Embedding、Milvus Dense + BM25、RRF 和 BGE Reranker 提高中文资料的检索质量，再由兼容 OpenAI API 的本地模型生成有依据的回答。后续可以继续增加多租户权限、流式输出、查询改写、父子文档检索和自动化评测，让系统逐步具备生产使用能力。</p>

<h2 id="参考">参考</h2>

<ul>
  <li>https://milvus.io/docs</li>
  <li>https://huggingface.co/BAAI/bge-large-zh-v1.5</li>
  <li>https://huggingface.co/BAAI/bge-reranker-v2-m3</li>
  <li>https://ollama.com</li>
  <li>https://fastapi.tiangolo.com</li>
</ul>]]></content><author><name>onefeng</name></author><category term="AI" /><category term="AI" /><category term="RAG" /><category term="Python" /><summary type="html"><![CDATA[使用FastAPI、Milvus、BGE和Ollama搭建支持混合检索与精排的本地RAG系统]]></summary></entry><entry><title type="html">从0到1构建MCP Server与Client</title><link href="https://me.onefeng.xyz/2026/07/21/%E6%9E%84%E5%BB%BAmcp-server-client/" rel="alternate" type="text/html" title="从0到1构建MCP Server与Client" /><published>2026-07-21T00:00:00+08:00</published><updated>2026-07-21T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2026/07/21/%E6%9E%84%E5%BB%BAmcp-server-client</id><content type="html" xml:base="https://me.onefeng.xyz/2026/07/21/%E6%9E%84%E5%BB%BAmcp-server-client/"><![CDATA[<p>MCP（Model Context Protocol，模型上下文协议）用于统一大模型与外部工具、数据源之间的连接方式。本文从零实现一个可以搜索技术文档的 MCP Server，并使用 Python Client 和大模型完成工具调用。</p>

<h2 id="mcp解决了什么问题">MCP解决了什么问题</h2>

<p>在 MCP 出现之前，不同模型平台的 Function Calling 格式并不完全一致。接入文件系统、数据库或第三方 API 时，开发者通常需要为每个平台重复编写适配代码。</p>

<p>MCP 将模型应用与外部能力拆分为两个角色：</p>

<ul>
  <li>MCP Server：提供 Tools、Resources 和 Prompts</li>
  <li>MCP Client：连接 Server，发现能力并发起调用</li>
</ul>

<p>可以把 MCP 理解为 AI 应用领域的 USB-C。Server 只需要按照协议暴露能力，Cursor、Codex、自建 Agent 等 Client 就能以统一方式使用。</p>

<p>一次完整调用大致如下：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>用户问题
  -&gt; MCP Client读取工具列表
  -&gt; 大模型判断是否调用工具
  -&gt; Client调用MCP Server
  -&gt; Server执行搜索或读取数据
  -&gt; Client把结果交给大模型
  -&gt; 大模型生成最终回答
</code></pre></div></div>

<h2 id="初始化项目">初始化项目</h2>

<p>项目使用 Python 3.11 和 uv 管理依赖。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/gobinfan/python-mcp-server-client.git
<span class="nb">cd </span>python-mcp-server-client

uv <span class="nb">sync
cp</span> .env.example .env
</code></pre></div></div>

<p>主要依赖如下：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">dependencies</span> <span class="p">=</span> <span class="p">[</span>
    <span class="py">"bs4&gt;</span><span class="p">=</span><span class="mf">0.0</span><span class="err">.</span><span class="mi">2</span><span class="s">",</span><span class="err">
</span>    <span class="py">"httpx&gt;</span><span class="p">=</span><span class="mf">0.28</span><span class="err">.</span><span class="mi">1</span><span class="s">",</span><span class="err">
</span>    <span class="py">"mcp[cli]&gt;</span><span class="p">=</span><span class="mf">1.28</span><span class="err">.</span><span class="mi">1</span><span class="p">,</span><span class="err">&lt;</span><span class="mi">2</span><span class="s">",</span><span class="err">
</span>    <span class="py">"openai&gt;</span><span class="p">=</span><span class="mf">1.66</span><span class="err">.</span><span class="mi">3</span><span class="s">",</span><span class="err">
</span><span class="p">]</span>
</code></pre></div></div>

<p>在 <code class="language-plaintext highlighter-rouge">.env</code> 中配置搜索服务和模型信息：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">SERPER_API_KEY</span><span class="o">=</span>your-serper-api-key
<span class="nv">OPENAI_API_KEY</span><span class="o">=</span>your-openai-api-key
<span class="nv">OPENAI_BASE_URL</span><span class="o">=</span>https://api.openai.com/v1
<span class="nv">OPENAI_MODEL</span><span class="o">=</span>your-model-name
</code></pre></div></div>

<p>密钥不要写入代码或提交到 Git 仓库。</p>

<h2 id="使用fastmcp构建server">使用FastMCP构建Server</h2>

<p>FastMCP 是 Python MCP SDK 提供的高层 API。创建 Server 时开启无状态 HTTP 和 JSON Response，更方便后续通过容器或多个实例部署。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">mcp.server.fastmcp</span> <span class="kn">import</span> <span class="n">FastMCP</span>

<span class="n">mcp</span> <span class="o">=</span> <span class="n">FastMCP</span><span class="p">(</span>
    <span class="s">"Agentdocs"</span><span class="p">,</span>
    <span class="n">host</span><span class="o">=</span><span class="s">"0.0.0.0"</span><span class="p">,</span>
    <span class="n">port</span><span class="o">=</span><span class="mi">8020</span><span class="p">,</span>
    <span class="n">stateless_http</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">json_response</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>项目实现了一个 <code class="language-plaintext highlighter-rouge">get_docs</code> 工具。它先把技术框架映射到官方文档域名，再通过 Serper 搜索相关页面，最后使用 BeautifulSoup 提取网页文本。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">docs_urls</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"langchain"</span><span class="p">:</span> <span class="s">"python.langchain.com/docs"</span><span class="p">,</span>
    <span class="s">"llama-index"</span><span class="p">:</span> <span class="s">"docs.llamaindex.ai/en/stable"</span><span class="p">,</span>
    <span class="s">"openai-agents-sdk"</span><span class="p">:</span> <span class="s">"openai.github.io/openai-agents-python"</span><span class="p">,</span>
    <span class="s">"mcp-doc"</span><span class="p">:</span> <span class="s">"modelcontextprotocol.io"</span><span class="p">,</span>
    <span class="s">"crew-ai"</span><span class="p">:</span> <span class="s">"docs.crewai.com"</span><span class="p">,</span>
<span class="p">}</span>

<span class="o">@</span><span class="n">mcp</span><span class="p">.</span><span class="n">tool</span><span class="p">()</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">get_docs</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">library</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="s">"""搜索指定框架的最新官方文档。"""</span>
    <span class="k">if</span> <span class="n">library</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">docs_urls</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s">"Library </span><span class="si">{</span><span class="n">library</span><span class="si">}</span><span class="s"> not supported by this tool"</span><span class="p">)</span>

    <span class="n">search_query</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"site:</span><span class="si">{</span><span class="n">docs_urls</span><span class="p">[</span><span class="n">library</span><span class="p">]</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="s">"</span>
    <span class="n">results</span> <span class="o">=</span> <span class="k">await</span> <span class="n">search_web</span><span class="p">(</span><span class="n">search_query</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">results</span><span class="p">[</span><span class="s">"organic"</span><span class="p">]:</span>
        <span class="k">return</span> <span class="s">"No results found"</span>

    <span class="n">text</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">result</span> <span class="ow">in</span> <span class="n">results</span><span class="p">[</span><span class="s">"organic"</span><span class="p">]:</span>
        <span class="n">text</span> <span class="o">+=</span> <span class="k">await</span> <span class="n">fetch_url</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="s">"link"</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">text</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@mcp.tool()</code> 会根据函数名、类型注解和 Docstring 自动生成工具名称、说明和输入 Schema。因此，清晰的参数类型和文档说明非常重要，它们会直接影响大模型选择工具的准确性。</p>

<h2 id="选择传输协议">选择传输协议</h2>

<p>项目支持三种启动方式：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 本地进程通信，适合桌面客户端</span>
uv run main.py <span class="nt">--transport</span> stdio

<span class="c"># 兼容已有SSE客户端</span>
uv run main.py <span class="nt">--transport</span> sse <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> 8020

<span class="c"># 推荐的HTTP方式</span>
uv run main.py <span class="nt">--transport</span> streamable-http <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> 8020
</code></pre></div></div>

<p>Stdio 通过标准输入输出通信，Client 负责启动 Server 进程；SSE 和 Streamable HTTP 则允许 Client 通过网络连接。当前项目中 SSE 地址为 <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8020/sse</code>，Streamable HTTP 地址为 <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8020/mcp</code>。</p>

<p>对于新项目优先使用 Streamable HTTP；只有本地工具或需要兼容旧 Client 时，再选择 Stdio 或 SSE。</p>

<h2 id="实现python-mcp-client">实现Python MCP Client</h2>

<p>Client 首先建立传输层连接，然后创建 <code class="language-plaintext highlighter-rouge">ClientSession</code> 并初始化协议会话。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">mcp</span> <span class="kn">import</span> <span class="n">ClientSession</span>
<span class="kn">from</span> <span class="nn">mcp.client.sse</span> <span class="kn">import</span> <span class="n">sse_client</span>
<span class="kn">from</span> <span class="nn">mcp.client.streamable_http</span> <span class="kn">import</span> <span class="n">streamable_http_client</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">connect_to_server</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">server_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">server_url</span><span class="p">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s">"/"</span><span class="p">).</span><span class="n">endswith</span><span class="p">(</span><span class="s">"/sse"</span><span class="p">):</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">sse_client</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">server_url</span><span class="p">)</span>
        <span class="n">streams</span> <span class="o">=</span> <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="n">__aenter__</span><span class="p">()</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">streamable_http_client</span><span class="p">(</span><span class="n">server_url</span><span class="p">)</span>
        <span class="n">streams</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="n">__aenter__</span><span class="p">())[:</span><span class="mi">2</span><span class="p">]</span>

    <span class="bp">self</span><span class="p">.</span><span class="n">session</span> <span class="o">=</span> <span class="k">await</span> <span class="n">ClientSession</span><span class="p">(</span><span class="o">*</span><span class="n">streams</span><span class="p">).</span><span class="n">__aenter__</span><span class="p">()</span>
    <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">initialize</span><span class="p">()</span>

    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">list_tools</span><span class="p">()</span>
    <span class="k">print</span><span class="p">([</span><span class="n">tool</span><span class="p">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">tool</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">tools</span><span class="p">])</span>
</code></pre></div></div>

<p>连接成功后，Client 通过 <code class="language-plaintext highlighter-rouge">list_tools()</code> 获取 Server 能力，并转换成大模型可以识别的工具格式：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">available_tools</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
        <span class="s">"type"</span><span class="p">:</span> <span class="s">"function"</span><span class="p">,</span>
        <span class="s">"function"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"name"</span><span class="p">:</span> <span class="n">tool</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="s">"description"</span><span class="p">:</span> <span class="n">tool</span><span class="p">.</span><span class="n">description</span><span class="p">,</span>
            <span class="s">"parameters"</span><span class="p">:</span> <span class="n">tool</span><span class="p">.</span><span class="n">inputSchema</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">}</span>
    <span class="k">for</span> <span class="n">tool</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">tools</span>
<span class="p">]</span>
</code></pre></div></div>

<p>如果模型返回 <code class="language-plaintext highlighter-rouge">tool_calls</code>，Client 解析工具名称和参数，通过 MCP Session 发起调用：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tool_name</span> <span class="o">=</span> <span class="n">tool_call</span><span class="p">.</span><span class="n">function</span><span class="p">.</span><span class="n">name</span>
<span class="n">tool_args</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">tool_call</span><span class="p">.</span><span class="n">function</span><span class="p">.</span><span class="n">arguments</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">call_tool</span><span class="p">(</span><span class="n">tool_name</span><span class="p">,</span> <span class="n">tool_args</span><span class="p">)</span>
</code></pre></div></div>

<p>工具结果需要连同原始 <code class="language-plaintext highlighter-rouge">tool_call_id</code> 追加到消息列表，再请求一次模型，才能得到面向用户的最终回答。</p>

<p>启动自建 Client：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run client.py http://127.0.0.1:8020/mcp
</code></pre></div></div>

<p>进入交互模式后可以提问：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>查询MCP Python SDK中streamable-http的使用方法
</code></pre></div></div>

<h2 id="在codex中配置mcp-server">在Codex中配置MCP Server</h2>

<p>如果 Server 已通过 Streamable HTTP 启动，可以在 <code class="language-plaintext highlighter-rouge">.codex/config.toml</code> 中配置：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[mcp_servers.agentdocs_http]</span>
<span class="py">url</span> <span class="p">=</span> <span class="s">"http://127.0.0.1:8020/mcp"</span>
</code></pre></div></div>

<p>也可以由 Codex 直接以 Stdio 方式启动 Server：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[mcp_servers.agentdocs_stdio]</span>
<span class="py">command</span> <span class="p">=</span> <span class="s">"/path/to/python-mcp-server-client/.venv/bin/python"</span>
<span class="py">args</span> <span class="p">=</span> <span class="p">[</span><span class="s">"/path/to/python-mcp-server-client/main.py"</span><span class="p">,</span> <span class="s">"--transport"</span><span class="p">,</span> <span class="s">"stdio"</span><span class="p">]</span>
</code></pre></div></div>

<p>配置完成并重启 Client 后，就可以查看和调用 <code class="language-plaintext highlighter-rouge">get_docs</code> 工具。Stdio 模式下不要向标准输出随意写日志，否则可能污染 MCP 的 JSON-RPC 消息；日志应写入标准错误或文件。</p>

<h2 id="toolsresources和prompts">Tools、Resources和Prompts</h2>

<p>MCP 不只是工具调用。项目 <code class="language-plaintext highlighter-rouge">example/01_full_feature_server.py</code> 演示了三种核心能力：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">mcp</span><span class="p">.</span><span class="n">tool</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>

<span class="o">@</span><span class="n">mcp</span><span class="p">.</span><span class="n">resource</span><span class="p">(</span><span class="s">"greeting://{name}"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">greeting</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="sa">f</span><span class="s">"Hello, </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">!"</span>

<span class="o">@</span><span class="n">mcp</span><span class="p">.</span><span class="n">prompt</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">summarize_city</span><span class="p">(</span><span class="n">city</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="sa">f</span><span class="s">"Please summarize the current status of </span><span class="si">{</span><span class="n">city</span><span class="si">}</span><span class="s">."</span>
</code></pre></div></div>

<ul>
  <li>Tool 表示可以执行的动作，例如搜索、计算和写入数据库</li>
  <li>Resource 表示可以读取的上下文，例如文件、文档和配置</li>
  <li>Prompt 表示可复用的提示词模板</li>
</ul>

<p>Tool 还可以直接返回 Pydantic Model。Client 能从 <code class="language-plaintext highlighter-rouge">structuredContent</code> 获取结构化结果，比解析普通文本更加稳定。</p>

<h2 id="使用lifespan管理共享资源">使用lifespan管理共享资源</h2>

<p>数据库连接、HTTP Client 和知识库对象不应该在每次工具调用时重复创建。FastMCP 的 lifespan 可以在 Server 启动时初始化共享资源，并在关闭时统一释放。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">asynccontextmanager</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">lifespan</span><span class="p">(</span><span class="n">_</span><span class="p">:</span> <span class="n">FastMCP</span><span class="p">):</span>
    <span class="n">kb</span> <span class="o">=</span> <span class="k">await</span> <span class="n">FakeKnowledgeBase</span><span class="p">.</span><span class="n">create</span><span class="p">()</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">yield</span> <span class="n">AppContext</span><span class="p">(</span><span class="n">kb</span><span class="o">=</span><span class="n">kb</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">kb</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>

<span class="n">mcp</span> <span class="o">=</span> <span class="n">FastMCP</span><span class="p">(</span><span class="s">"LifespanServer"</span><span class="p">,</span> <span class="n">lifespan</span><span class="o">=</span><span class="n">lifespan</span><span class="p">)</span>
</code></pre></div></div>

<p>工具函数可以通过 <code class="language-plaintext highlighter-rouge">Context</code> 获取共享状态。这种方式适合连接池、缓存和需要复用的第三方 SDK Client。</p>

<h2 id="常见问题">常见问题</h2>

<h3 id="client连接成功但没有工具">Client连接成功但没有工具</h3>

<p>确认函数使用了 <code class="language-plaintext highlighter-rouge">@mcp.tool()</code>，并在 Session 初始化后调用 <code class="language-plaintext highlighter-rouge">list_tools()</code>。同时检查连接地址：SSE 通常以 <code class="language-plaintext highlighter-rouge">/sse</code> 结尾，Streamable HTTP 使用 <code class="language-plaintext highlighter-rouge">/mcp</code>。</p>

<h3 id="工具经常被错误调用">工具经常被错误调用</h3>

<p>完善函数类型注解和 Docstring，明确参数含义、可选值和返回内容。对 <code class="language-plaintext highlighter-rouge">library</code> 这类枚举参数，应在 Server 端再次校验，不能只依赖模型生成正确参数。</p>

<h3 id="请求超时或返回内容太长">请求超时或返回内容太长</h3>

<p>为外部 HTTP 请求设置超时和异常处理，并限制搜索结果数量。生产环境还应清洗 HTML、限制返回字符数，避免无关网页内容消耗模型上下文。</p>

<h3 id="如何保证安全">如何保证安全</h3>

<p>MCP Tool 本质上可以执行代码和访问数据。部署时应增加身份认证、参数校验、访问控制和审计日志；文件、Shell、数据库写入等高风险工具还应限制作用域，并在执行前要求用户确认。</p>

<h2 id="总结">总结</h2>

<p>MCP 的价值不在于替代大模型，而在于为模型访问外部世界提供统一协议。使用 FastMCP，只需要普通 Python 函数、类型注解和装饰器，就能快速构建 Server；Client 则负责发现工具、让模型选择工具、执行调用并回传结果。</p>

<p>实际项目建议从一个边界清晰的只读 Tool 开始，优先使用 Streamable HTTP，补齐超时、鉴权和日志后，再逐步接入数据库、内部 API 或自动化任务。</p>

<h2 id="参考">参考</h2>

<ul>
  <li>https://modelcontextprotocol.io</li>
  <li>https://github.com/modelcontextprotocol/python-sdk</li>
  <li>https://github.com/gobinfan/python-mcp-server-client</li>
</ul>]]></content><author><name>onefeng</name></author><category term="AI" /><category term="AI" /><category term="MCP" /><category term="Python" /><summary type="html"><![CDATA[使用Python MCP SDK构建支持Stdio、SSE和Streamable HTTP的MCP Server与Client]]></summary></entry><entry><title type="html">nvidia驱动安装配置</title><link href="https://me.onefeng.xyz/2023/08/05/nvidia%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE/" rel="alternate" type="text/html" title="nvidia驱动安装配置" /><published>2023-08-05T00:00:00+08:00</published><updated>2023-08-05T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2023/08/05/nvidia%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE</id><content type="html" xml:base="https://me.onefeng.xyz/2023/08/05/nvidia%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE/"><![CDATA[<p>安装配置nvidia驱动及cuda环境，cudnn安装配置。</p>

<h2 id="安装驱动">安装驱动</h2>

<h3 id="环境">环境</h3>

<ol>
  <li>ubuntu 20.04</li>
  <li>启动项 secure boot 关闭</li>
</ol>

<h3 id="禁用nouveau">禁用nouveau</h3>

<p>编辑文件blacklist.conf <code class="language-plaintext highlighter-rouge">vim /etc/modprobe.d/blacklist.conf</code> 在文件最后部分插入以下两行内容
<code class="language-plaintext highlighter-rouge">blacklist nouveau</code> <code class="language-plaintext highlighter-rouge">options nouveau modeset=0</code></p>

<p>更新系统</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>update-initramfs <span class="nt">-u</span>
</code></pre></div></div>

<p>重启系统</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>reboot
</code></pre></div></div>

<h3 id="环境部署">环境部署</h3>

<p>在英伟达的官网上查找你自己电脑的显卡型号然后下载相应的驱动。网址：https://www.nvidia.cn/Download/index.aspx?lang=cn</p>

<p>可执行文件赋权</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod  </span>a+x NVIDIA-Linux-x86_64-396.18.run
</code></pre></div></div>

<p>安装依赖</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>gcc make <span class="nt">-y</span>
</code></pre></div></div>

<p>运行安装脚本，根据安装提示安装</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> ./NVIDIA-Linux-x86_64-396.18.run
</code></pre></div></div>

<h2 id="安装cuda环境">安装cuda环境</h2>

<p>同样在官网下载选择合适的版本 https://developer.nvidia.com/cuda-12-4-1-download-archive?target_os=Linux&amp;target_arch=x86_64&amp;Distribution=Ubuntu&amp;target_version=20.04&amp;target_type=runfile_local</p>

<p><img src="/images/posts/runing/img_8.png" alt="" /></p>

<p>根据官方给的命令行下载，在安装过程中选择Continue,输入accept,使用空格去掉Driver,最后install安装。
安装完成后编辑用户环境变量，在~/.bashrc文件中添加以下内容</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span>/usr/local/cuda-12.4/bin:<span class="nv">$PATH</span>
<span class="nb">export </span><span class="nv">LD_LIBRARY_PATH</span><span class="o">=</span>/usr/local/cuda-12.4/lib64:<span class="nv">$LD_LIBRARY_PATH</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">nvcc -V</code> 验证cuda是否安装成功</p>

<h2 id="安装cudnn">安装cudnn</h2>

<p>1.选择合适的下载版本 https://developer.nvidia.cn/rdp/cudnn-archive</p>

<p>2.下载cudnn库文件，解压到指定目录</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>lib/<span class="k">*</span> /usr/local/cuda-12.4/lib64/
<span class="nb">cp </span>include/<span class="k">*</span> /usr/local/cuda-12.4/include/
</code></pre></div></div>

<p>3.添加执行权限</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>a+r /usr/local/cuda-12.4/include/cudnn.h /usr/local/cuda-12.4/lib64/libcudnn<span class="k">*</span>
</code></pre></div></div>

<h2 id="安装-nvidia-docker">安装 NVIDIA Docker</h2>

<p>1.添加 NVIDIA Docker 仓库</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">distribution</span><span class="o">=</span><span class="si">$(</span><span class="nb">.</span> /etc/os-release<span class="p">;</span><span class="nb">echo</span> <span class="nv">$ID$VERSION_ID</span><span class="si">)</span>
curl <span class="nt">-s</span> <span class="nt">-L</span> https://nvidia.github.io/nvidia-docker/gpgkey | <span class="nb">sudo </span>apt-key add -
curl <span class="nt">-s</span> <span class="nt">-L</span> https://nvidia.github.io/nvidia-docker/<span class="nv">$distribution</span>/nvidia-docker.list | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/nvidia-docker.list
</code></pre></div></div>

<p>2.安装</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> nvidia-docker2
<span class="nb">sudo </span>systemctl restart docker
</code></pre></div></div>]]></content><author><name>onefeng</name></author><category term="运维" /><category term="运维" /><summary type="html"><![CDATA[nvidia驱动安装配置]]></summary></entry><entry><title type="html">python源码分析(一)整体架构</title><link href="https://me.onefeng.xyz/2023/07/30/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/" rel="alternate" type="text/html" title="python源码分析(一)整体架构" /><published>2023-07-30T00:00:00+08:00</published><updated>2023-07-30T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2023/07/30/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90</id><content type="html" xml:base="https://me.onefeng.xyz/2023/07/30/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/"><![CDATA[<p>深入解析python源码，这部分主要分析python的整体架构及运行原理。</p>

<h2 id="前言">前言</h2>

<p>Python 是一门上层语言，创建者通过有意设计来隐藏背后复杂的细节 (builtins)。在解决项目问题时，很多问题也许能通过搜索引擎找到答案，
但 Python 是一门迭代速度非常快的语言，搜索引擎与专业书难以获得实效性好且准确的答案，因此多了解其架构与核心原理，可以更好地理解Python语言的使用方式、提高编程技能和调试能力。</p>

<h2 id="架构">架构</h2>

<p>如下图所示</p>

<p><img src="/images/posts/runing/img_6.png" alt="" /></p>

<p>CPython 整体架构大致分为三个模块：</p>

<ul>
  <li>代码文件 File Groups</li>
</ul>

<p>Python 所提供的的大量的模块、库、以及用户自定义的模块。用户还可以通过自定义模块来扩展 Python 系统。</p>

<ul>
  <li>解释器 Python Core</li>
</ul>

<p>又称 Python 虚拟机，对代码分析理解，翻译成字节流，并运行这些字节代码。</p>

<p>Scanner 负责词法分析的工作，将代码一行一行切分为 Token</p>

<p>Parser 则负责语法分析，将 Token 组织为抽象语法树</p>

<p>Compiler 则将语法树转化为指令集合的字节码流</p>

<p>Code Evaluator 也是我们常说 Python 虚拟机，负责执行这些字节码</p>

<ul>
  <li>运行环境 Runtime Env</li>
</ul>

<p>包括运行时的对象、基础类型结构、内存分配器和实时的运行状态信息。</p>

<p>Object 和 Type Structure 分别是程序在运行过程中生成的对象和Python中的自带内建对象，如 Int、Str、List 等</p>

<p>Memory Allocator 则负责申请创建对象需要的内存，本质就是封装了 C 语言里面的 malloc() 内存分配函数</p>

<p>Current State 负责维护运行时的各类状态信息，以便在程序执行过程中如果发生状态变化（正常态和异常态）时，仍然能正常运行</p>

<h2 id="代码结构">代码结构</h2>

<p>这里以<a href="https://github.com/python/cpython/tree/v3.11.0">python3.11.0</a>版本为例，进行说明代码目录结构</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/python/cpython.git
<span class="nb">cd </span>cpython
git checkout v3.11.0
</code></pre></div></div>

<p><img src="/images/posts/runing/img_7.png" alt="" /></p>

<ul>
  <li>
    <p>Doc: rst(reStructuredText)格式官方文档，用其生成https://docs.python.org</p>
  </li>
  <li>
    <p>Grammar: Python的EBNF(Extended Backus-Naur form)语法定义文件</p>
  </li>
  <li>
    <p>Include: .h 头文件</p>
  </li>
  <li>
    <p>Lib .py纯Python实现的标准库</p>
  </li>
  <li>
    <p>Mac: Mac-specific code，支持MacOS</p>
  </li>
  <li>
    <p>Modules: C实现的标准库，内含.c .asm.macros .h</p>
  </li>
  <li>
    <p>Objects: 内置数据类型实现</p>
  </li>
  <li>
    <p>PC: Windows-specific code，支持Windows</p>
  </li>
  <li>
    <p>PCbuild: Windows生成文件，for MSVC</p>
  </li>
  <li>
    <p>Parser: Python语法分析器源码</p>
  </li>
  <li>
    <p>Programs: main函数文件，用于生成可执行文件，如python.exe的入口文件</p>
  </li>
  <li>
    <p>Python: CPython解释器源码</p>
  </li>
  <li>
    <p>Tools: 独立工具代码，that are useful while building or extending Python</p>
  </li>
</ul>

<h2 id="总结">总结</h2>

<p>这部本探索了python架构工作原理，源码结构。比较深入的还有对象，内存管理设计，后面有时间在继续深入阅读下源码。</p>

<h2 id="参考">参考</h2>

<ul>
  <li>Python源码剖析：深度探索动态语言核心技术</li>
</ul>]]></content><author><name>onefeng</name></author><category term="python" /><category term="python" /><summary type="html"><![CDATA[python源码剖析]]></summary></entry><entry><title type="html">flask源码分析</title><link href="https://me.onefeng.xyz/2023/03/01/flask%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/" rel="alternate" type="text/html" title="flask源码分析" /><published>2023-03-01T00:00:00+08:00</published><updated>2023-03-01T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2023/03/01/flask%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90</id><content type="html" xml:base="https://me.onefeng.xyz/2023/03/01/flask%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"><![CDATA[<p>flask工作原理以及源码分析，本文所有的源码基于flask <a href="https://github.com/pallets/flask/tree/0.1">v0.1版本</a></p>

<h2 id="概念">概念</h2>

<p>单说Flask的话，代码量确实很少，但是Flask完全建立在Werkzeug之上，如果把Werkzeug的代码加起来，代码量 可就不少了。。。
但这里主要分析flask的原理，其中有比较好设计，Werkzeug代码暂时不分析了。。</p>

<p>这里有flask的几个基本概念，简单说明一下</p>

<ul>
  <li>Werkzeug</li>
</ul>

<p>Werkzeug是一个WSGI工具包，它可以作为web框架的底层库。负责核心的逻辑模块，比如路由、请求和应答的封装、WSGI 相关的函数等；</p>

<ul>
  <li>jinja</li>
</ul>

<p>负责模板的渲染，主要用来渲染返回给用户的 html 文件内容。</p>

<p>Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架，对于Werkzeug，它只是工具包，其用于接收http请求并对请求进行预处理，
然后触发Flask框架;对于jinja2模板，它用来实现对模板的处理，将模板和数据进行渲染，将渲染后的字符串返回给用户浏览器。</p>

<h2 id="解析源码">解析源码</h2>

<h3 id="run">run</h3>

<p>这里主要解析flask核心源码及实现方式，jinja和Werkzeug暂时跳过。。。</p>

<p>首先一个最简单的demo代码为</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
    <span class="k">return</span> <span class="s">"Hello World!"</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>

<p>通过Debug调试工作可以整理其调用栈如下：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>app.run<span class="o">()</span>
    run_simple<span class="o">(</span>host, port, self, <span class="k">**</span>options<span class="o">)</span>
      __call__<span class="o">(</span>self, environ, start_response<span class="o">)</span>
        wsgi_app<span class="o">(</span>self, environ, start_response<span class="o">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">run_simple</code> 为引用的wsgi方法，实际调用的__call__方法</p>

<p>把Flask类简化一下代码</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Flask</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">package_name</span><span class="p">):</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">package_name</span> <span class="o">=</span> <span class="n">package_name</span> 
        <span class="bp">self</span><span class="p">.</span><span class="n">root_path</span> <span class="o">=</span> <span class="n">_get_package_path</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">package_name</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">view_functions</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">error_handlers</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">before_request_funcs</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">after_request_funcs</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">url_map</span> <span class="o">=</span> <span class="n">Map</span><span class="p">()</span>
        
    <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s">'localhost'</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">5000</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
        <span class="s">"""Runs the application on a local development server.  If the
        :attr:`debug` flag is set the server will automatically reload
        for code changes and show a debugger in case an exception happened.
    
        :param host: the hostname to listen on.  set this to ``'0.0.0.0'``
                     to have the server available externally as well.
        :param port: the port of the webserver
        :param options: the options to be forwarded to the underlying
                        Werkzeug server.  See :func:`werkzeug.run_simple`
                        for more information.
        """</span>
        <span class="kn">from</span> <span class="nn">werkzeug</span> <span class="kn">import</span> <span class="n">run_simple</span>
        <span class="k">if</span> <span class="s">'debug'</span> <span class="ow">in</span> <span class="n">options</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">debug</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="s">'debug'</span><span class="p">)</span>
        <span class="n">options</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'use_reloader'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">debug</span><span class="p">)</span>
        <span class="n">options</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'use_debugger'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">debug</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">run_simple</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">wsgi_app</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">):</span>
        <span class="s">"""The actual WSGI application.  This is not implemented in
        `__call__` so that middlewares can be applied:

            app.wsgi_app = MyMiddleware(app.wsgi_app)

        :param environ: a WSGI environment
        :param start_response: a callable accepting a status code,
                               a list of headers and an optional
                               exception context to start the response
        """</span>
        <span class="k">with</span> <span class="bp">self</span><span class="p">.</span><span class="n">request_context</span><span class="p">(</span><span class="n">environ</span><span class="p">):</span>
            <span class="n">rv</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">preprocess_request</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">rv</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
                <span class="n">rv</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">dispatch_request</span><span class="p">()</span>
            <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">make_response</span><span class="p">(</span><span class="n">rv</span><span class="p">)</span>
            <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">process_response</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">response</span><span class="p">(</span><span class="n">environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">):</span>
        <span class="s">"""Shortcut for :attr:`wsgi_app`"""</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">wsgi_app</span><span class="p">(</span><span class="n">environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">)</span>
</code></pre></div></div>

<p>这下可能稍微清晰一点了，接下来每一步部分分开说明。</p>

<p>view_functions中保存了视图函数(处理用户请求的函数，如上面的hello())，</p>

<p>error_handlers 这个字典用来保存所有的错误处理视图函数，字典的 key 是错误类型码</p>

<p>before_request_funcs 这个列表用来保存在请求被分派之前应当执行的函数</p>

<p>before_first_request_funcs 在接收到第一个请求的时候应当执行的函数。</p>

<p>after_request_funcs 这个列表中的函数在请求完成之后被调用，响应对象会被传给这些函数</p>

<p>self.url_map 这里设置了一个 url_map 属性，并把它设置为一个 Map 对象，用以保存URI到视图函数的映射，即保存app.route()这个装饰器的信息</p>

<p>其中最核心的就是 <code class="language-plaintext highlighter-rouge">wsgi_app</code>方法，他有一个上下文 <code class="language-plaintext highlighter-rouge">request_context(environ)</code>,看一下具体实现</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">_RequestContext</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
    <span class="s">"""The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.
    """</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">,</span> <span class="n">environ</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">app</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">url_adapter</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="n">url_map</span><span class="p">.</span><span class="n">bind_to_environ</span><span class="p">(</span><span class="n">environ</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">request</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="n">request_class</span><span class="p">(</span><span class="n">environ</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">session</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="n">open_session</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">request</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">g</span> <span class="o">=</span> <span class="n">_RequestGlobals</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">flashes</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exc_type</span><span class="p">,</span> <span class="n">exc_value</span><span class="p">,</span> <span class="n">tb</span><span class="p">):</span>
        <span class="c1"># do not pop the request stack if we are in debug mode and an
</span>        <span class="c1"># exception happened.  This will allow the debugger to still
</span>        <span class="c1"># access the request object in the interactive shell.
</span>        <span class="k">if</span> <span class="n">tb</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">app</span><span class="p">.</span><span class="n">debug</span><span class="p">:</span>
            <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">pop</span><span class="p">()</span>
            
<span class="k">def</span> <span class="nf">request_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">):</span>
    <span class="s">"""Creates a request context from the given environment and binds
    it to the current context.  This must be used in combination with
    the `with` statement because the request is only bound to the
    current context for the duration of the `with` block.

    Example usage::

        with app.request_context(environ):
            do_something_with(request)

    :params environ: a WSGI environment
    """</span>
    <span class="k">return</span> <span class="n">_RequestContext</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">)</span>

</code></pre></div></div>

<p>可以看到初始化实例变量，并在 <code class="language-plaintext highlighter-rouge">__enter__</code> 方法中将实例加入 context locals <code class="language-plaintext highlighter-rouge">_request_ctx_stack</code>，<code class="language-plaintext highlighter-rouge">__exit__</code>方法取出实例对象</p>

<p>接着 <code class="language-plaintext highlighter-rouge">preprocess_request</code> 我们常见的钩子函数，在请求之前做处理，依次执行 <code class="language-plaintext highlighter-rouge">self.before_request_funcs</code>中的方法</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">preprocess_request</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Called before the actual request dispatching and will
    call every as :meth:`before_request` decorated function.
    If any of these function returns a value it's handled as
    if it was the return value from the view and further
    request handling is stopped.
    """</span>
    <span class="k">for</span> <span class="n">func</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">before_request_funcs</span><span class="p">:</span>
        <span class="n">rv</span> <span class="o">=</span> <span class="n">func</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">rv</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">rv</span>
</code></pre></div></div>

<p>大家都知道before_request这里用了一个装饰器的设计，把将要处理的函数用装饰器添加到 <code class="language-plaintext highlighter-rouge">self.before_request_funcs</code>（0.1版本还没有这个设计，直接用函数注册到实例中了。。问题不大不影响主要逻辑）</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">before_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">f</span><span class="p">):</span>
    <span class="s">"""Registers a function to run before each request."""</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">before_request_funcs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">f</span>
</code></pre></div></div>

<p>接下来就 <code class="language-plaintext highlighter-rouge">dispatch_request()</code> 函数的处理了</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">dispatch_request</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Does the request dispatching.  Matches the URL and returns the
    return value of the view or error handler.  This does not have to
    be a response object.  In order to convert the return value to a
    proper response object, call :func:`make_response`.
    """</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">endpoint</span><span class="p">,</span> <span class="n">values</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">match_request</span><span class="p">()</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">view_functions</span><span class="p">[</span><span class="n">endpoint</span><span class="p">](</span><span class="o">**</span><span class="n">values</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">HTTPException</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">handler</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">error_handlers</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">code</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">handler</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">e</span>
        <span class="k">return</span> <span class="n">handler</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">handler</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">error_handlers</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">debug</span> <span class="ow">or</span> <span class="n">handler</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span>
        <span class="k">return</span> <span class="n">handler</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</code></pre></div></div>

<p>match_request() 获取到函数名endpoint和参数values，然后根据endpoint从view_functions获取处理函数并调用。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">match_request</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Matches the current request against the URL map and also
    stores the endpoint and view arguments on the request object
    is successful, otherwise the exception is stored.
    """</span>
    <span class="n">rv</span> <span class="o">=</span> <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">top</span><span class="p">.</span><span class="n">url_adapter</span><span class="p">.</span><span class="n">match</span><span class="p">()</span>
    <span class="n">request</span><span class="p">.</span><span class="n">endpoint</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">view_args</span> <span class="o">=</span> <span class="n">rv</span>
    <span class="k">return</span> <span class="n">rv</span>
</code></pre></div></div>

<h3 id="路由">路由</h3>

<p>那么到了这里说明路由是怎么建立绑定的，我们来看看app.route()方法中做了什么：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">route</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">rule</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">add_url_rule</span><span class="p">(</span><span class="n">rule</span><span class="p">,</span> <span class="n">f</span><span class="p">.</span><span class="n">__name__</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">view_functions</span><span class="p">[</span><span class="n">f</span><span class="p">.</span><span class="n">__name__</span><span class="p">]</span> <span class="o">=</span> <span class="n">f</span>
        <span class="k">return</span> <span class="n">f</span>
    <span class="k">return</span> <span class="n">decorator</span>
    
<span class="k">def</span> <span class="nf">add_url_rule</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">rule</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
    <span class="n">options</span><span class="p">[</span><span class="s">'endpoint'</span><span class="p">]</span> <span class="o">=</span> <span class="n">endpoint</span>
    <span class="n">options</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'methods'</span><span class="p">,</span> <span class="p">(</span><span class="s">'GET'</span><span class="p">,))</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">url_map</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">Rule</span><span class="p">(</span><span class="n">rule</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">))</span>
</code></pre></div></div>

<p>可以看到吗，他直接用装饰器将路由和对应的函数加入到 <code class="language-plaintext highlighter-rouge">self.view_functions</code></p>

<h3 id="上下文">上下文</h3>

<p>这里还有一个比较重要的设计，包括（应用上下文application context 和 请求上下文request context）</p>

<p>在 Flask 中，上下文是一种用于在不同部分之间传递信息的机制。Flask 使用上下文来确保在同一个请求或应用程序上下文中共享数据，这对于处理请求和响应、访问全局对象等非常有用。</p>

<p>视图函数需要知道它执行情况的请求信息（请求的 url，参数，方法等）以及应用信息（应用中初始化的数据库等），才能够正确运行。把这些信息作为类似全局变量的东西，视图函数需要的时候，可以使用 from flask import request 获取。</p>

<p>应用上下文（App Context）：</p>

<p>应用上下文是全局的，它在整个应用程序生命周期中都存在。
它主要用于存储应用程序全局对象，如数据库连接、配置设置等。
使用 app.app_context() 可以获取应用上下文。
请求上下文（Request Context）：</p>

<p>请求上下文是与每个请求相关的，每次请求进来时都会创建一个新的请求上下文。
它主要用于存储与请求相关的数据，如请求参数、会话数据等。
使用 flask.request 可以获取请求上下文。</p>

<p><code class="language-plaintext highlighter-rouge">application context</code> 演化出来两个变量 <code class="language-plaintext highlighter-rouge">current_app</code> 和 <code class="language-plaintext highlighter-rouge">g</code>;</p>

<p><code class="language-plaintext highlighter-rouge">request context</code> 则演化出来 <code class="language-plaintext highlighter-rouge">request</code> 和 <code class="language-plaintext highlighter-rouge">session</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># context locals
</span><span class="n">_request_ctx_stack</span> <span class="o">=</span> <span class="n">LocalStack</span><span class="p">()</span>
<span class="n">current_app</span> <span class="o">=</span> <span class="n">LocalProxy</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">top</span><span class="p">.</span><span class="n">app</span><span class="p">)</span>
<span class="n">request</span> <span class="o">=</span> <span class="n">LocalProxy</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">top</span><span class="p">.</span><span class="n">request</span><span class="p">)</span>
<span class="n">session</span> <span class="o">=</span> <span class="n">LocalProxy</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">top</span><span class="p">.</span><span class="n">session</span><span class="p">)</span>
<span class="n">g</span> <span class="o">=</span> <span class="n">LocalProxy</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">_request_ctx_stack</span><span class="p">.</span><span class="n">top</span><span class="p">.</span><span class="n">g</span><span class="p">)</span>
</code></pre></div></div>

<p>那么这些全局变量是否可以在多线程环境下调用，肯定可以的。
由变量名可知，_request_ctx_stack是一个栈，跟进LocalStack类分析源码</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LocalStack</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_local</span> <span class="o">=</span> <span class="n">Local</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">push</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
        <span class="n">rv</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">_local</span><span class="p">,</span> <span class="s">'stack'</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">rv</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">_local</span><span class="p">.</span><span class="n">stack</span> <span class="o">=</span> <span class="n">rv</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">rv</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">rv</span>

    <span class="k">def</span> <span class="nf">pop</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">stack</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">_local</span><span class="p">,</span> <span class="s">'stack'</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">stack</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">None</span>
        <span class="k">elif</span> <span class="nb">len</span><span class="p">(</span><span class="n">stack</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">release_local</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">_local</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">stack</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">stack</span><span class="p">.</span><span class="n">pop</span><span class="p">()</span>
            
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">top</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="s">"""The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">_local</span><span class="p">.</span><span class="n">stack</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="k">except</span> <span class="p">(</span><span class="nb">AttributeError</span><span class="p">,</span> <span class="nb">IndexError</span><span class="p">):</span>
            <span class="k">return</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">Local</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
    <span class="n">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s">'__storage__'</span><span class="p">,</span> <span class="s">'__ident_func__'</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="c1"># 数据保存在 __storage__ 中，后续访问都是对该属性的操作
</span>        <span class="nb">object</span><span class="p">.</span><span class="n">__setattr__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s">'__storage__'</span><span class="p">,</span> <span class="p">{})</span>
        <span class="nb">object</span><span class="p">.</span><span class="n">__setattr__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s">'__ident_func__'</span><span class="p">,</span> <span class="n">get_ident</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">proxy</span><span class="p">):</span>
        <span class="s">"""Create a proxy for a name."""</span>
        <span class="k">return</span> <span class="n">LocalProxy</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">proxy</span><span class="p">)</span>

    <span class="c1"># 清空当前线程/协程保存的所有数据
</span>    <span class="k">def</span> <span class="nf">__release_local__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">__storage__</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__ident_func__</span><span class="p">(),</span> <span class="bp">None</span><span class="p">)</span>

    <span class="c1"># 下面三个方法实现了属性的访问、设置和删除。
</span>    <span class="c1"># 注意到，内部都调用 `self.__ident_func__` 获取当前线程或者协程的 id，然后再访问对应的内部字典。
</span>    <span class="c1"># 如果访问或者删除的属性不存在，会抛出 AttributeError。
</span>    <span class="c1"># 这样，外部用户看到的就是它在访问实例的属性，完全不知道字典或者多线程/协程切换的实现
</span>    <span class="k">def</span> <span class="nf">__getattr__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">__storage__</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">__ident_func__</span><span class="p">()][</span><span class="n">name</span><span class="p">]</span>
        <span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">AttributeError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__setattr__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
        <span class="n">ident</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__ident_func__</span><span class="p">()</span>
        <span class="n">storage</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__storage__</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">storage</span><span class="p">[</span><span class="n">ident</span><span class="p">][</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
        <span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
            <span class="n">storage</span><span class="p">[</span><span class="n">ident</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>

    <span class="k">def</span> <span class="nf">__delattr__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">del</span> <span class="bp">self</span><span class="p">.</span><span class="n">__storage__</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">__ident_func__</span><span class="p">()][</span><span class="n">name</span><span class="p">]</span>
        <span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">AttributeError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
</code></pre></div></div>

<p>Local类有两个成员变量，分别是<code class="language-plaintext highlighter-rouge">__storage__</code>和__ident_func__</p>

<p>__storage__是一个字典，最外面字典 key 是线程或者协程的 identity，value 是另外一个字典，这个内部字典就是用户自定义的 key-value 键值对。用户访问实例的属性，就变成了访问内部的字典，外面字典的 key 是自动关联的。</p>

<p>__ident_func__是一个函数。这个函数的含义是，获取当前线程的id（或协程的id）。</p>

<p>Local类还自定义了__getattr__和__setattr__这两个方法，也就是说，我们在操作self.local.stack时， 会调用__setattr__和__getattr__方法。</p>

<p>LocalProxy 是一个 Local 对象的代理，负责把所有对自己的操作转发给内部的 Local对象。</p>

<p>通过LocalStack和LocalProxy这样的Python魔法，每个线程访问当前请求中的数据(request, session)时， 都好像都在访问一个全局变量，但是，互相之间又互不影响。</p>

<h2 id="总结">总结</h2>

<p>到了这里主要的流程已经过了一边了，后面还有 响应处理<code class="language-plaintext highlighter-rouge">process_response</code>,异常处理<code class="language-plaintext highlighter-rouge">error_handlers</code>没有分析，但实现都差不多，相对来说这一版flask的源码非常简单，但重要的能
从中领悟到开发者的设计思想，这对于我们才是最重要的。</p>]]></content><author><name>onefeng</name></author><category term="python" /><category term="python" /><category term="flask" /><summary type="html"><![CDATA[flask源码分析]]></summary></entry><entry><title type="html">Restful风格接口</title><link href="https://me.onefeng.xyz/2022/12/15/Restful%E9%A3%8E%E6%A0%BC%E6%8E%A5%E5%8F%A3%E7%90%86%E8%A7%A3/" rel="alternate" type="text/html" title="Restful风格接口" /><published>2022-12-15T00:00:00+08:00</published><updated>2022-12-15T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2022/12/15/Restful%E9%A3%8E%E6%A0%BC%E6%8E%A5%E5%8F%A3%E7%90%86%E8%A7%A3</id><content type="html" xml:base="https://me.onefeng.xyz/2022/12/15/Restful%E9%A3%8E%E6%A0%BC%E6%8E%A5%E5%8F%A3%E7%90%86%E8%A7%A3/"><![CDATA[<p>Restful风格接口的理解</p>

<h2 id="基本概念">基本概念</h2>

<p>全称是 Resource Representational State Transfer</p>

<p>通俗来讲就是，资源在网络中以某种表现形式进行状态转移。</p>

<p>分解开来：Resource：资源，即数据（前面说过网络的核心）；</p>

<p>Representational：某种表现形式，比如用JSON，XML，JPEG等；</p>

<p>State Transfer：状态变化。通过HTTP动词实现，GET,PUT,POST,DELETE</p>

<h2 id="restful-api设计规范">RESTful API设计规范</h2>

<h3 id="url设计规范">URL设计规范</h3>

<p>URL为统一资源定位器 ,接口属于服务端资源，首先要通过URL这个定位到资源才能去访问，而通常一个完整的URL组成由以下几个部分构成：</p>

<p><code class="language-plaintext highlighter-rouge">URI = scheme "://" host  ":"  port "/" path [ "?" query ][ "#" fragment ]</code></p>

<p>scheme: 指底层用的协议，如http、https、ftp</p>

<p>host: 服务器的IP地址或者域名</p>

<p>port: 端口，http默认为80端口</p>

<p>path: 访问资源的路径，就是各种web 框架中定义的route路由</p>

<p>query: 查询字符串，为发送给服务器的参数，在这里更多发送数据分页、排序等参数。</p>

<p>fragment: 锚点，定位到页面的资源</p>

<p>RESTful对path的设计做了一些规范，通常一个RESTful API的path组成如下</p>

<p><code class="language-plaintext highlighter-rouge">/{version}/{resources}/{resource_id}</code></p>

<p>version：API版本号，有些版本号放置在头信息中也可以，通过控制版本号有利于应用迭代。</p>

<p>resources：资源，RESTful API推荐用小写英文单词的复数形式。</p>

<p>resource_id：资源的id，访问或操作该资源。</p>

<p>路径规范</p>

<p>1.使用小写字母，多个单词用”-“分隔，提高URL的可读性</p>

<p>2.资源嵌套层次避免过深，尽量不超过2层</p>

<h3 id="http动词">HTTP动词</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /collection:从服务器查询资源的列表
GET /collection/resource:从服务器查询单个资源
POST /collection:在服务器创建新的资源
PUT /collection/resource:更新服务器资源
DELETE /collection/resource:从服务器删除资源
</code></pre></div></div>

<h3 id="状态码和返回数据">状态码和返回数据</h3>

<p>200 OK - [GET]：服务器成功返回用户请求的数据，该操作是幂等的（Idempotent）。</p>

<p>201 CREATED - [POST/PUT/PATCH]：用户新建或修改数据成功。</p>

<p>202 Accepted - [*]：表示一个请求已经进入后台排队（异步任务）</p>

<p>204 NO CONTENT - [DELETE]：用户删除数据成功。</p>

<p>400 INVALID REQUEST - [POST/PUT/PATCH]：用户发出的请求有错误，服务器没有进行新建或修改数据的操作，该操作是幂等的。</p>

<p>401 Unauthorized - [*]：表示用户没有权限（令牌、用户名、密码错误）。</p>

<p>403 Forbidden - [*] 表示用户得到授权（与401错误相对），但是访问是被禁止的。</p>

<p>404 NOT FOUND - [*]：用户发出的请求针对的是不存在的记录，服务器没有进行操作，该操作是幂等的。</p>

<p>406 Not Acceptable - [GET]：用户请求的格式不可得（比如用户请求JSON格式，但是只有XML格式）。</p>

<p>410 Gone -[GET]：用户请求的资源被永久删除，且不会再得到的。</p>

<p>422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时，发生一个验证错误。</p>

<p>500 INTERNAL SERVER ERROR - [*]：服务器发生错误，用户将无法判断发出的请求是否成功。</p>

<h2 id="不符合-crud-的情况">不符合 CRUD 的情况</h2>

<p>在实际资源操作中，总会有一些不符合CRUD的情况，一般有几种处理方法。</p>

<p>1.为需要的动作增加一个 endpoint，使用 POST 来执行动作，比如 POST /resend 重新发送邮件。</p>

<p>2.增加控制参数</p>

<p>添加动作相关的参数，通过修改参数来控制动作。</p>

<p>比如一个博客网站，会有把写好的文章“发布”的功能，可以用上面的POST /article/{:id}/publish方法，</p>

<p>3.把动作转换成资源</p>

<p>把动作转换成可以执行 CRUD 操作的资源， github 就是用了这种方法。</p>

<p>比如”喜欢”一个 gist，就增加一个/gists/:id/star子资源，然后对其进行操作：“喜欢”使用PUT /gists/:id/star，”取消喜欢”使用DELETE /gists/:id/star。</p>

<h2 id="总结">总结</h2>

<p>任何一门技术或者思想都有其优缺点，虽然其诞生的初衷都是为了解决我们的问题，而不是带来更大的灾难。REST同样如此。它的优点很明显，优雅、规范，行为和资源分离，更容易理解。</p>

<p>但是也有其缺点，它面向资源，这种设计思路是反程序员直觉的，因为在本地业务代码中仍然是一个个的函数，是动作，但表现在接口形式上则完全是资源的形式，
对于后端开发人员要求高，有些业务逻辑难以被抽象为资源的增删改查。甚至有些时候RESTful其实是个效率很低的东西，为了实现一个资源，你需要定义它的一套方式，
如果要联合查询又会要求对其衍生或定义一个新的资源。它提供的接口一般是“粗”粒度的，它通常返回的都是完整的数据模型，难以查询符合特殊要求的数据，有些特殊的业务要比普通的API需要更多次HTTP请求。</p>

<p>RESTful API是REST风格的API，它是一种API设计风格，规范了API设计中的一些原则。它让我们的API更加优雅、规范。但也尤其缺点，在实际使用过程中我们应该充分的取理解它，综合考量其使用场景。</p>

<h2 id="参考">参考</h2>

<ul>
  <li>https://docs.github.com/zh/rest/quickstart?apiVersion=2022-11-28</li>
  <li>https://juejin.cn/post/7128790457550635021</li>
</ul>]]></content><author><name>onefeng</name></author><category term="技术" /><category term="Restful" /><summary type="html"><![CDATA[Restful风格接口]]></summary></entry><entry><title type="html">MYSQL之ACID实现原理</title><link href="https://me.onefeng.xyz/2022/11/24/MYSQL%E4%B9%8BACID%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/" rel="alternate" type="text/html" title="MYSQL之ACID实现原理" /><published>2022-11-24T00:00:00+08:00</published><updated>2022-11-24T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2022/11/24/MYSQL%E4%B9%8BACID%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86</id><content type="html" xml:base="https://me.onefeng.xyz/2022/11/24/MYSQL%E4%B9%8BACID%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/"><![CDATA[<p>介绍mysql工作原理，ACID的实现</p>

<h2 id="基本概念">基本概念</h2>

<ul>
  <li>原子性（Atomicity）</li>
</ul>

<p>原子性确保事务是一个不可分割的工作单位，要么全部执行成功，要么全部失败回滚。如果事务在执行过程中发生错误，系统会将其恢复到事务开始前的状态，以保持数据库的一致性。</p>

<ul>
  <li>一致性（Consistency）</li>
</ul>

<p>一致性确保事务使数据库从一个一致状态变为另一个一致状态。事务在执行过程中，必须遵守预定义的规则和约束，以保证数据的完整性和正确性。</p>

<ul>
  <li>隔离性（Isolation）</li>
</ul>

<p>隔离性确保事务之间是相互隔离的，使得在一个事务看到的数据对其他事务是不可见的。这防止了事务之间的干扰和数据竞争。</p>

<ul>
  <li>持久性（Durability）</li>
</ul>

<p>持久性确保一旦事务提交，其结果将永久保存在数据库中，即使发生系统崩溃或故障，数据也不会丢失。</p>

<p>ACID 4 个特性中：一致性（consistency）是目的；原子性（atomicity）、隔离性（isolation）、持久性（durability）是手段。</p>

<h2 id="工作原理">工作原理</h2>

<h3 id="原子性">原子性</h3>

<p>事务通常由多个语句组成，原子性保证每个事务都被视为一个单独的单元,要么完全成功，要么完全失败。即一个事务（transaction）中的所有操作，要么全部执行成功，要么全部不执行。</p>

<p>简单来说：原子性的结果就是没有中间状态，如果有中间状态则一致性就不会得到满足。</p>

<p>仔细想一想没有中间结果是不可能实现的，所以 MySQL 采用了曲线救国的方式：即执行失败后，可以回滚，保证不会出现部分成功的情况，通过 undolog 实现该特性。</p>

<p>事务在执行过程中发生错误，会被回滚（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。</p>

<p>实现原理</p>

<p>1.通过 undolog 在失败时回滚保证在结果上是原子性的， 即没有中间状态。
undo log 属于逻辑日志，它记录的是 sql 执行相关的信息。当发生回滚时，InnoDB 会根据 undo log 的内容做与之前相反的工作：对于每个 insert，
回滚时会执行 delete；对于每个 delete，回滚时会执行insert；对于每个 update，回滚时会执行一个相反的 update，把数据改回去。</p>

<p>2.通过隔离性保证了在其他并发事务看来是原子性的，即中间状态对外不可见。</p>

<h3 id="一致性">一致性</h3>

<p>一致性是指事务执行结束后，数据库的完整性约束没有被破坏，事务执行的前后都是合法的数据状态。</p>

<p>数据库的完整性约束包括但不限于：实体完整性（如行的主键存在且唯一）、列完整性（如字段的类型、大小、长度要符合要求）、外键约束、用户自定义完整性（如转账前后，两个账户余额的和应该不变）。</p>

<p>可以说，一致性是事务追求的最终目标：前面提到的原子性、持久性和隔离性，都是为了保证数据库状态的一致性。此外，除了数据库层面的保障，一致性的实现也需要应用层面进行保障。</p>

<h3 id="隔离性">隔离性</h3>

<p>隔离性，指一个事务内部的操作及使用的数据对正在进行的其他事务是隔离的，并发执行的各个事务之间不能互相干扰。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。</p>

<p>正是它保证了原子操作的过程中，中间状态对其它事务不可见。</p>

<p>Mysql 隔离级别有以下四种（级别由低到高）：</p>

<ul>
  <li>ReadUncommitted 读未提交</li>
</ul>

<p>最低的隔离级别，一个事务可以读取其他事务未提交的数据。可能出现的问题包括脏读、不可重复读和幻读。</p>

<ul>
  <li>ReadCommitted 读已提交</li>
</ul>

<p>Oracle默认隔离级别。一个事务只能读取已提交的数据。可能出现的问题包括不可重复读和幻读。</p>

<ul>
  <li>RepeatableRead 可重复读</li>
</ul>

<p>确保在同一个事务中多次读取同样的数据时，会得到一致的结果。可能出现的问题是幻读。</p>

<ul>
  <li>Serializable 串行化</li>
</ul>

<p>最高的隔离级别，完全隔离事务，确保不会出现脏读、不可重复读和幻读的问题。但是并发性能较差。</p>

<p>可能出现的问题：</p>

<p>脏读（Dirty Read）：一个事务读取了另一个事务未提交的数据。这可能导致事务基于不准确的数据做出决策。</p>

<p>不可重复读（Non-repeatable Read）：在同一个事务内，读取相同数据的两次读取结果不一致。这可能导致数据的一致性问题。</p>

<p>幻读（Phantom Read）：在同一个事务内，两次查询返回的数据行数不一致。这可能导致事务基于不完整的数据做出决策。</p>

<p>隔离性追求的是并发情形下事务之间互不干扰。简单起见，我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明)，那么隔离性的探讨，主要可以分为两个方面：</p>

<ul>
  <li>
    <p>(一个事务)写操作对(另一个事务)写操作的影响：锁机制保证隔离性</p>
  </li>
  <li>
    <p>(一个事务)写操作对(另一个事务)读操作的影响：MVCC保证隔离性</p>
  </li>
</ul>

<h3 id="持久性">持久性</h3>

<p>持久性是指事务一旦提交，它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。</p>

<p>InnoDB 通过 redo log 重做日志保证了事务的持久性。</p>

<p>事务开始之后就产生 redo log，redo log 的落盘并不是随着事务的提交才写入的，而是在事务的执行过程中，便开始写入 redo log 文件中。</p>

<p>参数innodb_flush_log_at_trx_commit可设置在事务 commit 的时候必须要写入 redo log 文件。</p>

<p>redo log 具体原理：</p>

<ul>
  <li>
    <p>当数据修改时，除了修改 Buffer Pool 中的数据，还会在 redo log 记录这次操作；</p>
  </li>
  <li>
    <p>当事务提交时，会调用 fsync 接口对 redo log 进行刷盘。</p>
  </li>
  <li>
    <p>redo log 采用的是 WAL（Write-ahead logging，预写式日志），所有修改先写入日志，再更新到 Buffer Pool 。</p>
  </li>
  <li>
    <p>如果 MySQL 宕机，重启时可以读取 redo log 中的数据，对数据库进行恢复，保证了数据不会因 MySQL 宕机而丢失，从而满足了持久性要求。</p>
  </li>
</ul>]]></content><author><name>onefeng</name></author><category term="数据库原理" /><category term="mysql" /><summary type="html"><![CDATA[数据库原理]]></summary></entry><entry><title type="html">并发编程</title><link href="https://me.onefeng.xyz/2022/10/02/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/" rel="alternate" type="text/html" title="并发编程" /><published>2022-10-02T00:00:00+08:00</published><updated>2022-10-02T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2022/10/02/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B</id><content type="html" xml:base="https://me.onefeng.xyz/2022/10/02/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/"><![CDATA[<p>介绍并发编程中多进程,多线程,协程的概念和使用</p>

<h2 id="基本概念">基本概念</h2>

<p>在Python中，进程、线程和协程都是用于实现并发和并行编程的概念，但它们在实现方式、资源占用和应用场景等方面有所不同。</p>

<p>1.进程（Process）</p>

<p>进程是操作系统分配资源和调度任务的基本单位。每个进程都有自己的独立内存空间，进程间的通信需要特殊的机制。在 Python 中，
可以使用 multiprocessing 模块创建和管理多个进程。每个进程都有自己独立的 Python 解释器，因此进程之间不会共享全局变量。</p>

<p>2.线程（Thread）</p>

<p>线程是进程内的最小任务单元，它们共享同一进程的资源，包括内存空间。由于线程共享内存，因此线程之间的通信和同步相对比较容易。
Python 中的 threading 模块可以用于创建和管理多个线程。</p>

<p>3.协程（Coroutine）</p>

<p>协程是一种轻量级的线程，是由用户控制的，可以随时挂起和恢复。协程通常用于实现高效的异步编程，允许在单个线程中实现并发。
在 Python 3.5 及之后的版本中，可以使用 asyncio 模块来实现协程，利用关键字 async 和 await 来定义和调用异步函数。</p>

<h2 id="用例">用例</h2>

<p>1.进程示例</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">multiprocessing</span>

<span class="k">def</span> <span class="nf">worker1</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 1 executing"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">worker2</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 2 executing"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">process1</span> <span class="o">=</span> <span class="n">multiprocessing</span><span class="p">.</span><span class="n">Process</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker1</span><span class="p">)</span>
    <span class="n">process2</span> <span class="o">=</span> <span class="n">multiprocessing</span><span class="p">.</span><span class="n">Process</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker2</span><span class="p">)</span>

    <span class="n">process1</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
    <span class="n">process2</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="n">process1</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>
    <span class="n">process2</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"Both processes are done"</span><span class="p">)</span>

</code></pre></div></div>

<p>2.线程示例</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">threading</span>

<span class="k">def</span> <span class="nf">worker1</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 1 executing"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">worker2</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 2 executing"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">thread1</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker1</span><span class="p">)</span>
    <span class="n">thread2</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">worker2</span><span class="p">)</span>

    <span class="n">thread1</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
    <span class="n">thread2</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>

    <span class="n">thread1</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>
    <span class="n">thread2</span><span class="p">.</span><span class="n">join</span><span class="p">()</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"Both threads are done"</span><span class="p">)</span>

</code></pre></div></div>

<p>3.协程使用</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">worker1</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 1 executing"</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 1 done"</span><span class="p">)</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">worker2</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 2 executing"</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Worker 2 done"</span><span class="p">)</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="n">worker1</span><span class="p">(),</span> <span class="n">worker2</span><span class="p">())</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>

</code></pre></div></div>

<p>Eventloop 是asyncio应用的核心,把一些异步函数注册到这个事件循环上，事件循环会循环执行这些函数,当执行到某个函数时，如果它正在等待I/O返回，
如它正在进行网络请求，或者sleep操作，事件循环会暂停它的执行去执行其他的函数；当某个函数完成I/O后会恢复，下次循环到它的时候继续执行。
因此，这些异步函数可以协同(Cooperative)运行，这就是事件循环的目标。</p>

<h2 id="应用场景">应用场景</h2>

<ul>
  <li>
    <p>使用多进程：当需要执行 CPU 密集型任务时，可以使用多进程来利用多个 CPU 核心。</p>
  </li>
  <li>
    <p>使用多线程：当需要执行 I/O 密集型任务时，可以使用多线程来提高任务的响应性能。</p>
  </li>
  <li>
    <p>使用协程：当需要在单个线程内执行高效的异步编程时，可以使用协程来减少线程切换开销。</p>
  </li>
</ul>

<p>需要根据具体的应用场景和性能需求选择适当的并发编程方式。</p>]]></content><author><name>onefeng</name></author><category term="python" /><category term="python" /><summary type="html"><![CDATA[并发编程]]></summary></entry><entry><title type="html">MYSQL之索引原理及优化建议</title><link href="https://me.onefeng.xyz/2022/09/22/MYSQL%E4%B9%8B%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE/" rel="alternate" type="text/html" title="MYSQL之索引原理及优化建议" /><published>2022-09-22T00:00:00+08:00</published><updated>2022-09-22T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2022/09/22/MYSQL%E4%B9%8B%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE</id><content type="html" xml:base="https://me.onefeng.xyz/2022/09/22/MYSQL%E4%B9%8B%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE/"><![CDATA[<p>介绍mysql索引的原理以及优化建议</p>

<h2 id="索引的本质">索引的本质</h2>

<p>MySQL官方对索引的定义为：索引（Index）是帮助MySQL高效获取数据的数据结构。提取句子主干，就可以得到索引的本质：索引是数据结构。
目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构。</p>

<p>索引的分类如下</p>

<ul>
  <li>主键索引（Primary Key Index）：</li>
</ul>

<p>主键索引是用于唯一标识每一行数据的索引，确保每个主键值都是唯一的。
在 InnoDB 存储引擎中，主键索引同时也是数据的物理存储顺序，被称为聚集索引。</p>

<ul>
  <li>唯一索引（Unique Index）：</li>
</ul>

<p>唯一索引确保索引列中的值是唯一的，但可以包含 NULL 值。
一个表可以有多个唯一索引。</p>

<ul>
  <li>普通索引（Normal Index）：</li>
</ul>

<p>普通索引也被称为非聚集索引。
它是最基本的索引类型，用于加速查询。</p>

<ul>
  <li>全文索引（Full-Text Index）：</li>
</ul>

<p>全文索引用于在文本列（如 VARCHAR 或 TEXT 类型）上进行全文搜索。
它支持模糊匹配和自然语言搜索。</p>

<ul>
  <li>空间索引（Spatial Index）：</li>
</ul>

<p>空间索引用于优化空间数据类型（如 GEOMETRY、POINT、LINESTRING）的查询。
它可以加速地理位置数据的空间查询。</p>

<h2 id="优化建议">优化建议</h2>

<p>1.like语句的前导模糊查询不能使用索引</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">*</span> from student where name like <span class="s2">"%zhang"</span><span class="p">;</span> <span class="nt">--</span>不能使用索引
<span class="k">select</span> <span class="k">*</span> from student where name like <span class="s2">"zhang%"</span><span class="p">;</span><span class="nt">--</span>非前导模糊查询，可以使用索引
</code></pre></div></div>

<p>2.union、in、or 都能够命中索引，建议使用 in</p>

<p>3.负向条件查询不能使用索引</p>

<p>负向条件有：!=、&lt;&gt;、not in、not exists、not like 等。</p>

<p>4.联合索引最左前缀原则</p>

<p>假如titles表的主索引为&lt;emp_no, title, from_date&gt;</p>

<p>情况一：全列匹配。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE <span class="nv">emp_no</span><span class="o">=</span><span class="s1">'10001'</span> AND <span class="nv">title</span><span class="o">=</span><span class="s1">'Senior Engineer'</span> AND <span class="nv">from_date</span><span class="o">=</span><span class="s1">'1986-06-26'</span><span class="p">;</span>
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span>  | possible_keys | key     | key_len | ref               | rows | Extra |
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
|  1 | SIMPLE      | titles | const | PRIMARY       | PRIMARY | 59      | const,const,const |    1 |       |
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
</code></pre></div></div>

<p>情况二：最左前缀匹配。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE <span class="nv">emp_no</span><span class="o">=</span><span class="s1">'10001'</span><span class="p">;</span>
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span> | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | titles | ref  | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
</code></pre></div></div>

<p>当查询条件精确匹配索引的左边连续一个或几个列时，如<emp_no>或&lt;emp_no, title&gt;，所以可以被用到，但是只能用到一部分，即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARY索引，但是key_len为4，说明只用到了索引的第一列前缀。</emp_no></p>

<p>情况三：查询条件用到了索引中列的精确匹配，但是中间某个条件未提供。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE <span class="nv">emp_no</span><span class="o">=</span><span class="s1">'10001'</span> AND <span class="nv">from_date</span><span class="o">=</span><span class="s1">'1986-06-26'</span><span class="p">;</span>
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span> | possible_keys | key     | key_len | ref   | rows | Extra       |
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
|  1 | SIMPLE      | titles | ref  | PRIMARY       | PRIMARY | 4       | const |    1 | Using where |
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
</code></pre></div></div>

<p>此时索引使用情况和情况二相同，因为title未提供，所以查询只用到了索引的第一列，而后面的from_date虽然也在索引中，但是由于title不存在而无法和左前缀连接，因此需要对结果进行扫描过滤from_date（这里由于emp_no唯一，所以不存在扫描）。如果想让from_date也使用索引而不是where过滤，可以增加一个辅助索引&lt;emp_no, from_date&gt;</p>

<p>情况四：查询条件没有指定索引第一列。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE <span class="nv">from_date</span><span class="o">=</span><span class="s1">'1986-06-26'</span><span class="p">;</span>
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span> | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | titles | ALL  | NULL          | NULL | NULL    | NULL | 443308 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
</code></pre></div></div>

<p>情况五：匹配某列的前缀字符串。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE <span class="nv">emp_no</span><span class="o">=</span><span class="s1">'10001'</span> AND title LIKE <span class="s1">'Senior%'</span><span class="p">;</span>
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span>  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 56      | NULL |    1 | Using where |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
</code></pre></div></div>

<p>如果通配符%不出现在开头，则可以用到索引，但根据具体情况不同可能只会用其中一个前缀</p>

<p>情况六：范围查询。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EXPLAIN SELECT <span class="k">*</span> FROM employees.titles WHERE emp_no &lt; <span class="s1">'10010'</span> and <span class="nv">title</span><span class="o">=</span><span class="s1">'Senior Engineer'</span><span class="p">;</span>
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
| <span class="nb">id</span> | select_type | table  | <span class="nb">type</span>  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 4       | NULL |   16 | Using where |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
</code></pre></div></div>

<p>范围列可以用到索引（必须是最左前缀），但是范围列后面的列无法用到索引。同时，索引最多用于一个范围列，因此如果查询条件中有两个范围列则无法全用到索引。</p>

<p>5.不能使用索引中范围条件右边的列（范围列可以用到索引），范围列之后列的索引全失效</p>

<p>范围条件有：&lt;、&lt;=、&gt;、&gt;=、between等。</p>

<p>假如有联合索引 (empno、title、fromdate)，那么下面的 SQL 中 emp_no 可以用到索引，而title 和 from_date 则使用不到索引。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">*</span> from titles where emp_no &lt; 10010 and <span class="nv">title</span><span class="o">=</span><span class="s1">'Senior Engineer'</span> and from_date between <span class="s1">'1986-01-01'</span> and <span class="s1">'1986-12-31'</span>
</code></pre></div></div>

<p>6.不要在索引列上面做任何操作（计算、函数），否则会导致索引失效而转向全表扫描</p>

<p>例如下面的 SQL 语句，即使 date 上建立了索引，也会全表扫描：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">*</span> from student where YEAR<span class="o">(</span>create_time<span class="o">)</span> &lt;<span class="o">=</span> <span class="s1">'2023'</span>
</code></pre></div></div>

<p>7.强制类型转换会全表扫描</p>

<p>字符串类型不加单引号会导致索引失效，因为mysql会自己做类型转换,相当于在索引列上进行了操作。</p>

<p>如果 phone 字段是 varchar 类型，则下面的 SQL 不能命中索引。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">*</span> from sys_user where <span class="nv">phone</span><span class="o">=</span>13612346678
</code></pre></div></div>

<p>可以优化为：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="k">*</span> from sys_user where <span class="nv">phone</span><span class="o">=</span><span class="s1">'13612346678'</span>
</code></pre></div></div>

<p>8.更新十分频繁、数据区分度不高的列不宜建立索引</p>

<p>更新会变更 B+ 树，更新频繁的字段建立索引会大大降低数据库性能。</p>

<p>“性别”这种区分度不大的属性，建立索引是没有什么意义的，不能有效过滤数据，性能与全表扫描类似。</p>

<p>一般区分度在80%以上的时候就可以建立索引，区分度可以使用 count(distinct(列名))/count(*) 来计算</p>

<p>9.利用覆盖索引来进行查询操作，避免回表，减少select * 的使用</p>

<p>覆盖索引：查询的列和所建立的索引的列个数相同，字段相同。</p>

<p>例如登录业务需求，SQL语句如下。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select </span>uid, login_time from sys_user where <span class="nv">login_name</span><span class="o">=</span>? and <span class="nv">passwd</span><span class="o">=</span>?
</code></pre></div></div>

<p>可以建立(login_name, passwd, login_time)的联合索引，由于 login_time 已经建立在索引中了，被查询的 uid 和 login_time 就不用去 row 上获取数据了，从而加速查询。</p>

<p>10.索引不会包含有NULL值的列</p>

<p>只要列中包含有NULL值都将不会被包含在索引中，复合索引中只要有一列含有NULL值，那么这一列对于此复合索引就是无效的。所以我们在数据库设计时，尽量使用not null 约束以及默认值。</p>

<p>11.is null, is not null无法使用索引</p>]]></content><author><name>onefeng</name></author><category term="数据库原理" /><category term="mysql" /><summary type="html"><![CDATA[数据库原理]]></summary></entry><entry><title type="html">邮件服务器的搭建与使用</title><link href="https://me.onefeng.xyz/2022/08/26/%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8/" rel="alternate" type="text/html" title="邮件服务器的搭建与使用" /><published>2022-08-26T00:00:00+08:00</published><updated>2022-08-26T00:00:00+08:00</updated><id>https://me.onefeng.xyz/2022/08/26/%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8</id><content type="html" xml:base="https://me.onefeng.xyz/2022/08/26/%E6%90%AD%E5%BB%BA%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8/"><![CDATA[<p>搭建ewoMail邮件服务器，本文记录服务器配置方法。</p>

<h2 id="安装">安装</h2>

<p>使用docker一键安装</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run  <span class="nt">-d</span> <span class="nt">-h</span> mail.onefeng.xyz <span class="nt">--restart</span><span class="o">=</span>always <span class="se">\</span>
  <span class="nt">-p</span> 25:25 <span class="se">\</span>
  <span class="nt">-p</span> 109:109 <span class="se">\</span>
  <span class="nt">-p</span> 110:110 <span class="se">\</span>
  <span class="nt">-p</span> 143:143 <span class="se">\</span>
  <span class="nt">-p</span> 465:465 <span class="se">\</span>
  <span class="nt">-p</span> 587:587 <span class="se">\</span>
  <span class="nt">-p</span> 993:993 <span class="se">\</span>
  <span class="nt">-p</span> 995:995  <span class="se">\</span>
  <span class="nt">-p</span> 82:80 <span class="se">\</span>
  <span class="nt">-p</span> 8082:8080 <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/mysql/:/ewomail/mysql/data/ <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/vmail/:/ewomail/mail/ <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/ssl/certs/:/etc/ssl/certs/ <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/ssl/private/:/etc/ssl/private/ <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/rainloop:/ewomail/www/rainloop/data <span class="se">\</span>
  <span class="nt">-v</span> <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/ssl/dkim/:/ewomail/dkim/ <span class="se">\</span>
  <span class="nt">--name</span> ewomail bestwu/ewomail
</code></pre></div></div>

<p>通过ip:8082登录ewoMail管理界面，默认初始用户admin,密码ewomail123，进入后修改管理员密码</p>

<p><img src="/images/posts/runing/img_1.png" alt="" /></p>

<p>用管理员账号创建一个邮箱，之后就可以通过ip:82 登录邮箱界面了</p>

<p><img src="/images/posts/runing/img_2.png" alt="" /></p>

<h2 id="dns配置">DNS配置</h2>

<p>DNS配置如下，将ip和域名修改为自己的，其中dkim._domainkey通过命令生成</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>ewomail amavisd showkeys
</code></pre></div></div>

<p><img src="/images/posts/runing/img_3.png" alt="" /></p>

<h2 id="修改容器">修改容器</h2>

<p>测试发送邮件时可能出现</p>

<p><img src="/images/posts/runing/img_4.png" alt="" /></p>

<p>需要进入容器进行修改 <code class="language-plaintext highlighter-rouge">docker exec -it ewomail bash</code></p>

<p>修改文件 <code class="language-plaintext highlighter-rouge">vi /etc/postfix/main.cf</code> ,将带有 10024 的一行注释掉</p>

<p><img src="/images/posts/runing/img_5.png" alt="" /></p>]]></content><author><name>onefeng</name></author><category term="运维" /><category term="docker" /><summary type="html"><![CDATA[邮件服务器的搭建与使用]]></summary></entry></feed>