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

<channel>
	<title>综合 &#8211; Blog of Code</title>
	<atom:link href="https://www.cztcode.com/category/all/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cztcode.com</link>
	<description></description>
	<lastBuildDate>Wed, 11 Dec 2024 20:44:41 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.cztcode.com/wp-content/uploads/2024/02/cropped-logo-32x32.webp</url>
	<title>综合 &#8211; Blog of Code</title>
	<link>https://www.cztcode.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">217219486</site>	<item>
		<title>macOS 使用 lftp 指南</title>
		<link>https://www.cztcode.com/2024/5337/</link>
					<comments>https://www.cztcode.com/2024/5337/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Fri, 01 Nov 2024 05:40:49 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5337</guid>

					<description><![CDATA[macOS 使用 lftp 指南 lftp 是一款强大的命令行文件传输工具，支持 FTP、FTPS、HTTP、HTTPS、SFTP、FISH 和 BitTorrent 等多种协议。它具备断点续传、多线程传输、带宽控制、镜像同步等多种功能，非常适合需要高效传输的用户。本文将深入介绍 lftp 的安装、基本用法、进阶功能（如多线程传输和镜像）、SSH 别名配置及一些实用技巧。 1. 安装 lftp 在 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>macOS 使用 lftp 指南</h1>
<p><code>lftp</code> 是一款强大的命令行文件传输工具，支持 FTP、FTPS、HTTP、HTTPS、SFTP、FISH 和 BitTorrent 等多种协议。它具备断点续传、多线程传输、带宽控制、镜像同步等多种功能，非常适合需要高效传输的用户。本文将深入介绍 <code>lftp</code> 的安装、基本用法、进阶功能（如多线程传输和镜像）、SSH 别名配置及一些实用技巧。</p>
<h2>1. 安装 lftp</h2>
<p>在 macOS 上，可以使用 <a href="https://brew.sh/" target="_blank" rel="noopener">Homebrew</a> 安装 <code>lftp</code>。确保 Homebrew 已安装后，执行以下命令安装 <code>lftp</code>：</p>
<pre><code class="language-bash">brew install lftp</code></pre>
<p>安装完成后，运行 <code>lftp</code> 命令检查是否成功安装。</p>
<h2>2. lftp 的基本用法</h2>
<h3>2.1 连接到服务器</h3>
<p><code>lftp</code> 支持多种协议，使用以下命令即可连接到远程服务器：</p>
<pre><code class="language-bash">lftp [协议]://[用户名]:[密码]@[服务器地址]</code></pre>
<p>例如，使用 SFTP 连接到服务器：</p>
<pre><code class="language-bash">lftp sftp://user:password@ftp.example.com</code></pre>
<p><strong>安全提示</strong>：为了避免在命令行中暴露密码，可以不写密码，让 <code>lftp</code> 提示输入：</p>
<pre><code class="language-bash">lftp sftp://user@ftp.example.com</code></pre>
<h3>2.2 常用命令</h3>
<p>连接成功后，可以在 <code>lftp</code> 提示符下使用以下命令进行文件管理：</p>
<ul>
<li>
<p><strong>列出目录内容</strong>：</p>
<pre><code class="language-bash">ls</code></pre>
</li>
<li>
<p><strong>下载文件</strong>：</p>
<pre><code class="language-bash">get [远程文件]</code></pre>
<p>示例，下载 <code>example.txt</code> 文件：</p>
<pre><code class="language-bash">get example.txt</code></pre>
</li>
<li>
<p><strong>上传文件</strong>：</p>
<pre><code class="language-bash">put [本地文件]</code></pre>
<p>示例，上传 <code>localfile.txt</code> 文件：</p>
<pre><code class="language-bash">put localfile.txt</code></pre>
</li>
<li>
<p><strong>镜像下载整个目录</strong>：</p>
<p><code>lftp</code> 的 <code>mirror</code> 命令可以将整个目录从服务器同步到本地，或从本地同步到服务器：</p>
<pre><code class="language-bash">mirror [远程目录] [本地目录]</code></pre>
<p>例如，将远程目录 <code>remote_dir</code> 下载到本地目录 <code>local_dir</code>：</p>
<pre><code class="language-bash">mirror remote_dir local_dir</code></pre>
</li>
<li>
<p><strong>退出 lftp</strong>：</p>
<pre><code class="language-bash">exit</code></pre>
</li>
</ul>
<h2>3. 高级功能：多线程传输</h2>
<p><code>lftp</code> 支持多线程下载和上传，可以显著提升文件传输速度。以下介绍 <code>pget</code>、<code>mput</code> 和 <code>mirror</code> 的多线程功能。</p>
<h3>3.1 多线程下载</h3>
<p>使用 <code>pget</code> 命令可以实现多线程下载，指定 <code>-n</code> 参数来设定线程数：</p>
<pre><code class="language-bash">pget -n [线程数] [远程文件]</code></pre>
<p>例如：</p>
<pre><code class="language-bash">pget -n 5 example.txt</code></pre>
<p>此命令会使用 5 个线程并行下载文件 <code>example.txt</code>，有效提升下载速度。</p>
<h3>3.2 多线程上传</h3>
<h4>使用 mput 进行多文件上传</h4>
<p><code>mput</code> 命令支持多线程上传多个文件，通过 <code>-P</code> 参数控制并行线程数。例如：</p>
<pre><code class="language-bash">mput -P 5 *.txt</code></pre>
<p>这将使用 5 个线程上传当前目录下的所有 <code>.txt</code> 文件，是 <code>lftp</code> 实现多文件并行上传的简单方法。</p>
<h4>使用 mirror -R 进行目录上传</h4>
<p>如果需要同步整个目录，可以使用 <code>mirror</code> 命令，配合 <code>-R</code> 参数（表示从本地上传到远程），并用 <code>-P</code> 参数指定并行线程数：</p>
<pre><code class="language-bash">mirror -R -P 5 [本地目录] [远程目录]</code></pre>
<p>例如：</p>
<pre><code class="language-bash">mirror -R -P 5 /local/dir /remote/dir</code></pre>
<p>这将使用 5 个线程将本地 <code>/local/dir</code> 目录上传到服务器上的 <code>/remote/dir</code>。</p>
<h3>mput 和 mirror -R 的对比</h3>
<ul>
<li><strong>mput</strong>：适合上传多个文件。可以选择性地上传指定文件，灵活性更高。</li>
<li><strong>mirror -R</strong>：适合上传整个目录，尤其是需要同步整个文件夹结构时。通常用于镜像同步。</li>
</ul>
<h2>4. 使用 SSH Config 配置别名</h2>
<p>使用 <code>~/.ssh/config</code> 配置 SSH 别名，可以简化连接命令。以下步骤为配置 SSH 别名：</p>
<h3>4.1 编辑 SSH 配置文件</h3>
<p>在主目录下打开 <code>~/.ssh/config</code> 文件（如果不存在可以新建）：</p>
<pre><code class="language-bash">vim ~/.ssh/config</code></pre>
<h3>4.2 添加主机别名</h3>
<p>在配置文件中添加如下内容，以便使用简短的主机名连接：</p>
<pre><code class="language-plaintext">Host myserver
    HostName ftp.example.com
    User myuser
    Port 22</code></pre>
<ul>
<li><code>Host</code> 是自定义的主机名。</li>
<li><code>HostName</code> 是实际的服务器地址。</li>
<li><code>User</code> 是用户名。</li>
<li><code>Port</code> 是端口号（默认 SFTP 为 22）。</li>
</ul>
<p>配置完成后，即可使用简洁的命令连接服务器：</p>
<pre><code class="language-bash">lftp sftp://myserver</code></pre>
<h2>5. 实用技巧</h2>
<p>以下是一些提高使用 <code>lftp</code> 效率的实用技巧。</p>
<h3>5.1 断点续传</h3>
<p><code>lftp</code> 支持断点续传，传输中断后可从上次位置继续传输。例如：</p>
<pre><code class="language-bash">get -c [远程文件]</code></pre>
<p>其中 <code>-c</code> 表示续传功能。</p>
<h3>5.2 自动重连</h3>
<p>若连接中断，<code>lftp</code> 支持自动重连。可以通过以下设置来启用：</p>
<pre><code class="language-bash">set net:reconnect-interval-base 5
set net:max-retries 3</code></pre>
<p>该配置表示每隔 5 秒重试一次，最多重试 3 次。</p>
<h3>5.3 带宽限制</h3>
<p><code>lftp</code> 支持带宽控制，可以限制传输速度：</p>
<pre><code class="language-bash">set net:limit-rate 500k</code></pre>
<p>上例将传输速度限制在 500 KB/s。</p>
<h3>5.4 自动化上传/下载脚本</h3>
<p>通过脚本可以实现自动化上传或下载。例如，以下脚本将本地目录与远程服务器同步：</p>
<pre><code class="language-bash">#!/bin/bash
lftp -e &quot;mirror -R /local/dir /remote/dir; quit&quot; sftp://myserver</code></pre>
<p>在此脚本中，<code>mirror -R</code> 命令将本地 <code>/local/dir</code> 同步到远程 <code>/remote/dir</code>，脚本运行结束后自动退出。</p>
<h3>示例：结合多线程、别名和镜像功能</h3>
<p>假设你已经通过 <code>ssh config</code> 设置了 <code>myserver</code> 别名，并需要将本地目录同步到远程服务器并加速上传：</p>
<pre><code class="language-bash">lftp -e &quot;mirror -R -P 5 /local/dir /remote/dir; quit&quot; sftp://myserver</code></pre>
<p>以上命令将本地的 <code>/local/dir</code> 目录上传到服务器的 <code>/remote/dir</code>，并使用 5 个线程加速传输。</p>
<h2>6. 总结</h2>
<p><code>lftp</code> 是一个强大且灵活的文件传输工具，适用于需要高效文件传输的用户。通过使用 <code>lftp</code> 的多线程、断点续传、镜像同步、带宽控制等功能，可以显著提升文件传输效率。利用 SSH 配置别名可以简化连接过程，而结合脚本可以实现自动化操作，是日常文件管理的优秀选择。</p>
<p>无论是简单的文件上传下载，还是复杂的多线程传输和镜像同步，<code>lftp</code> 都提供了丰富的功能来满足各种需求。希望本指南能帮助你更好地掌握 <code>lftp</code>，让文件传输更加高效便捷！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5337/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5337</post-id>	</item>
		<item>
		<title>多级评论结构设计</title>
		<link>https://www.cztcode.com/2024/5213/</link>
					<comments>https://www.cztcode.com/2024/5213/#comments</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Fri, 02 Feb 2024 12:46:16 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5213</guid>

					<description><![CDATA[社区类应用的场景离不开多级评论。本文介绍一种利用闭包表实现的多级评论结构，充分利用闭包表的优点实现高效CRUD。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>本文详细介绍了利用闭包表（Closure Table）实现多级评论结构的设计与实现，特别适用于社区类应用中的复杂评论场景。通过充分利用闭包表的优势，本文展示了如何高效地进行CRUD（创建、读取、更新、删除）操作，并提供了基于Golang和Gorm的完整示例代码。</p>
<h1>多级评论</h1>
<p>以即刻APP为例，可以看到在一个帖子（Post）下面有很多一级评论，需要显示它们的发布时间、作者头像、昵称、点赞数。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/c7f567342b7c53979e5bc5454487742c.png" alt="1" /></p>
<p>每个一级评论下面有可能有二级评论，默认显示最先的两条，如果大于两条还会显示总的评论数目。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/d1e1d455117cb24d759749e7e86bccc0.png" alt="2" /></p>
<p>当点击“共12条回复时”查看所有关于这个一级评论的二级评论和多级评论。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/5f0354f508453eb2fefa018fd01fbbc8.png" alt="3" /></p>
<p>如果有二级评论的回复，形成了更深层次的评论嵌套时，即刻这里的做法是只显示到二级评论，也就是更深级的评论嵌套只显示回复xxx，而不是继续显示嵌套评论。如果你使用微博的话，可以看到微博是显示三级评论嵌套的，比即刻多一级。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/44971996f2d5f0a754ba0357590a24d5.png" alt="4" /></p>
<h1>闭包表</h1>
<p>闭包表（Closure Table），也称为路径枚举表，是一种数据库设计模式，用于有效地存储和查询树形或图形结构中的节点之间的关系。这种设计模式在处理具有层级关系的数据时特别有用，如评论系统中的多级评论、组织结构图、类别树等。</p>
<p>在闭包表模式中，除了主要的数据表（<code>Comment</code>表）之外，还会创建一个额外的表来存储节点之间的所有关系（即每个节点与其所有祖先节点之间的关系）。这个额外的表通常包含如下字段：</p>
<ul>
<li><strong>祖先（Ancestor）</strong>: 表示层级关系中的上级节点。</li>
<li><strong>后代（Descendant）</strong>: 表示层级关系中的下级节点。</li>
<li><strong>深度（Depth）</strong>: 表示两个节点之间的层级深度。</li>
</ul>
<h3>闭包表示例数据</h3>
<p>假设<code>Comment</code>表中的数据如下：</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>ReplyTo</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>根评论1</td>
<td>NULL</td>
</tr>
<tr>
<td>2</td>
<td>根评论2</td>
<td>NULL</td>
</tr>
<tr>
<td>3</td>
<td>回复评论1</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>回复评论3</td>
<td>3</td>
</tr>
</tbody>
</table>
<p>对应的<code>CommentClosure</code>表数据将是：</p>
<table>
<thead>
<tr>
<th>AncestorID</th>
<th>DescendantID</th>
<th>Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>4</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>4</td>
<td>0</td>
</tr>
</tbody>
</table>
<p><strong>说明</strong>：在原始示例中，<code>Depth</code>字段存在错误，已根据正确的层级关系进行了修正。</p>
<h3>闭包表的优点</h3>
<ol>
<li><strong>灵活性</strong>：能够轻松地查询任意两个节点之间的关系，包括父子关系、所有后代、所有祖先等。</li>
<li><strong>性能</strong>：通过单次查询就能够获取到完整的层级结构，提高了查询效率，尤其是在处理深层次的层级结构时。</li>
<li><strong>简化操作</strong>：添加、移除或修改节点关系相对简单，特别是在节点移动或删除时，只需要更新关系表而不是整个树。</li>
<li><strong>保持数据一致性</strong>：通过在关系表中存储层级关系，可以避免数据冗余和不一致性的问题。</li>
</ol>
<p>使用闭包表的缺点是需要额外的存储空间来维护关系表，以及在修改层级结构时可能需要更新大量的记录。</p>
<p>例如，在评论系统中，可以创建一个名为<code>CommentClosure</code>的闭包表，用于存储每个评论与其所有祖先评论之间的关系。这样，无论评论层级有多深都可以通过一次查询来获取整个评论线。</p>
<p>我们使用闭包表主要是利用它能够一次查询获取整个评论线的优点。如果没有闭包表，在获取多级评论时就要递归获取评论的回复才能拿到全部的评论，这样会导致如果只设计了二级评论结构（比如即刻），需要递归查询 <code>ReplyTo</code> 字段才能找到全部的评论（因为只有二级评论，大于二级的评论结构要全部展平），因此需要多次查询数据库。</p>
<h1>数据库设计</h1>
<p>在设计评论结构时，不仅仅要使用<code>CommentClosure</code>，还需要在<code>Comment</code>表中使用<code>ReplyTo</code>字段记录评论是回复谁的。可能有的同学会问：“闭包表的祖先字段不就可以找到是回复谁的了吗？” 理论上确实是，但如果不记录<code>ReplyTo</code>字段，会发现一次查询数据库拿到全部数据并对应好谁是回复谁的评论十分麻烦。因此，需要记录<code>ReplyTo</code>字段。</p>
<p>同时，为了明确评论所属的帖子，建议在<code>Comment</code>表中添加<code>PostId</code>字段。</p>
<p>MongoDB和MySQL都适用（反正MySQL不能用外键和JOIN），字段设计如下：</p>
<h2>字段</h2>
<ol>
<li>
<p><strong>Comment 表</strong></p>
<table>
<thead>
<tr>
<th>字段名</th>
<th>类型</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ID</code></td>
<td><code>uint</code></td>
<td>评论的唯一标识符</td>
</tr>
<tr>
<td><code>Content</code></td>
<td><code>string</code></td>
<td>评论内容</td>
</tr>
<tr>
<td><code>ReplyTo</code></td>
<td><code>*uint</code></td>
<td>父级评论的ID（如果是根评论，则为<code>NULL</code>）</td>
</tr>
<tr>
<td><code>PostId</code></td>
<td><code>uint</code></td>
<td>关联帖子ID</td>
</tr>
<tr>
<td><code>TeamId</code></td>
<td><code>uint</code></td>
<td>关联团队ID</td>
</tr>
<tr>
<td><code>UserId</code></td>
<td><code>uint</code></td>
<td>发布评论的用户ID</td>
</tr>
<tr>
<td><code>CreatedAt</code></td>
<td><code>time.Time</code></td>
<td>评论创建时间</td>
</tr>
</tbody>
</table>
</li>
<li>
<p><strong>CommentClosure 表</strong></p>
<table>
<thead>
<tr>
<th>字段名</th>
<th>类型</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AncestorID</code></td>
<td><code>uint</code></td>
<td>祖先节点ID</td>
</tr>
<tr>
<td><code>DescendantID</code></td>
<td><code>uint</code></td>
<td>后代节点ID</td>
</tr>
<tr>
<td><code>Depth</code></td>
<td><code>int</code></td>
<td>两者之间的层级深度</td>
</tr>
</tbody>
</table>
</li>
</ol>
<h3>示例数据</h3>
<p>假设<code>Comment</code>表中的数据如下：</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>ReplyTo</th>
<th>PostId</th>
<th>TeamId</th>
<th>UserId</th>
<th>CreatedAt</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>根评论1</td>
<td>NULL</td>
<td>100</td>
<td>10</td>
<td>1000</td>
<td>2024-01-01 10:00:00</td>
</tr>
<tr>
<td>2</td>
<td>根评论2</td>
<td>NULL</td>
<td>100</td>
<td>10</td>
<td>1001</td>
<td>2024-01-01 10:05:00</td>
</tr>
<tr>
<td>3</td>
<td>回复评论1</td>
<td>1</td>
<td>100</td>
<td>10</td>
<td>1002</td>
<td>2024-01-01 10:10:00</td>
</tr>
<tr>
<td>4</td>
<td>回复评论3</td>
<td>3</td>
<td>100</td>
<td>10</td>
<td>1003</td>
<td>2024-01-01 10:15:00</td>
</tr>
</tbody>
</table>
<p>对应的<code>CommentClosure</code>表数据将是：</p>
<table>
<thead>
<tr>
<th>AncestorID</th>
<th>DescendantID</th>
<th>Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>4</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>4</td>
<td>0</td>
</tr>
</tbody>
</table>
<p><strong>说明</strong>：已修正<code>Depth</code>字段，确保其准确反映评论之间的层级关系。</p>
<h2>数据库索引优化</h2>
<p>为了提高查询性能，建议在<code>CommentClosure</code>表中对<code>AncestorID</code>和<code>DescendantID</code>的组合创建唯一索引：</p>
<pre><code class="language-sql">CREATE UNIQUE INDEX idx_ancestor_descendant ON CommentClosure (AncestorID, DescendantID);</code></pre>
<p>此外，在<code>Comment</code>表中，建议为<code>PostId</code>、<code>TeamId</code>、<code>UserId</code>和<code>ReplyTo</code>字段建立索引，以优化常用查询。</p>
<h1>CRUD逻辑</h1>
<h2>插入根评论</h2>
<p>当插入一个根评论时，首先在<code>Comment</code>表中添加一条记录。然后在<code>CommentClosure</code>表中添加一条记录，表示这个评论既是自己的祖先也是后代，深度为0。</p>
<h3>示例：</h3>
<p>假设插入一个新的根评论，内容为&quot;根评论3&quot;。</p>
<ol>
<li>
<p><strong><code>Comment</code>表插入前</strong>:</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>ReplyTo</th>
<th>PostId</th>
<th>TeamId</th>
<th>UserId</th>
<th>CreatedAt</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>根评论1</td>
<td>NULL</td>
<td>100</td>
<td>10</td>
<td>1000</td>
<td>2024-01-01 10:00:00</td>
</tr>
<tr>
<td>2</td>
<td>根评论2</td>
<td>NULL</td>
<td>100</td>
<td>10</td>
<td>1001</td>
<td>2024-01-01 10:05:00</td>
</tr>
<tr>
<td>3</td>
<td>回复评论1</td>
<td>1</td>
<td>100</td>
<td>10</td>
<td>1002</td>
<td>2024-01-01 10:10:00</td>
</tr>
<tr>
<td>4</td>
<td>回复评论3</td>
<td>3</td>
<td>100</td>
<td>10</td>
<td>1003</td>
<td>2024-01-01 10:15:00</td>
</tr>
</tbody>
</table>
</li>
<li>
<p><strong>插入操作</strong>:</p>
<pre><code class="language-sql">INSERT INTO Comment (Content, ReplyTo, PostId, TeamId, UserId, CreatedAt) 
VALUES ('根评论3', NULL, 100, 10, 1004, '2024-01-01 10:20:00');</code></pre>
</li>
<li>
<p><strong><code>CommentClosure</code>表插入对应记录</strong>:</p>
<p>假设新评论的ID为5。</p>
<pre><code class="language-sql">INSERT INTO CommentClosure (AncestorID, DescendantID, Depth) 
VALUES (5, 5, 0);</code></pre>
</li>
<li>
<p><strong><code>Comment</code>表和<code>CommentClosure</code>表插入后</strong>:</p>
</li>
</ol>
<ul>
<li>
<p><strong><code>Comment</code>表</strong>:</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>ReplyTo</th>
<th>PostId</th>
<th>TeamId</th>
<th>UserId</th>
<th>CreatedAt</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>根评论1</td>
<td>NULL</td>
<td>100</td>
<td>10</td>
<td>1000</td>
<td>2024-01-01 10:00:00</td>
</tr>
</tbody>
</table>
</li>
</ul>
<p>| 2    | 根评论2   | NULL    | 100    | 10     | 1001   | 2024-01-01 10:05:00 | | 3    | 回复评论1 | 1       | 100    | 10     | 1002   | 2024-01-01 10:10:00 | | 4    | 回复评论3 | 3       | 100    | 10     | 1003   | 2024-01-01 10:15:00 | | 5    | 根评论3   | NULL    | 100    | 10     | 1004   | 2024-01-01 10:20:00 |</p>
<ul>
<li>
<p><strong><code>CommentClosure</code>表</strong>:</p>
<table>
<thead>
<tr>
<th>AncestorID</th>
<th>DescendantID</th>
<th>Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>4</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>4</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>5</td>
<td>0</td>
</tr>
</tbody>
</table>
</li>
</ul>
<h2>插入回复评论</h2>
<p>当回复一个已存在的评论时，需要在<code>Comment</code>表中添加一条新记录，然后在<code>CommentClosure</code>表中根据被回复的评论添加新的关系记录。</p>
<h3>示例：</h3>
<p>假设回复评论ID为3的评论，回复内容为&quot;回复评论3的回复&quot;。</p>
<ol>
<li>
<p><strong>插入操作</strong>:</p>
<pre><code class="language-sql">INSERT INTO Comment (Content, ReplyTo, PostId, TeamId, UserId, CreatedAt) 
VALUES ('回复评论3的回复', 3, 100, 10, 1005, '2024-01-01 10:25:00');</code></pre>
</li>
<li>
<p><strong>更新<code>CommentClosure</code>表</strong>:</p>
<p>假设新评论的ID为6。</p>
<ul>
<li>首先，为新评论添加自引用记录。</li>
</ul>
<pre><code class="language-sql">INSERT INTO CommentClosure (AncestorID, DescendantID, Depth) 
VALUES (6, 6, 0);</code></pre>
<ul>
<li>接着，为每个祖先添加新的记录。由于是回复ID为3的评论，我们需要添加新评论与3的所有祖先（包括3本身）之间的关系。</li>
</ul>
<pre><code class="language-sql">INSERT INTO CommentClosure (AncestorID, DescendantID, Depth) 
VALUES (1, 6, 2); -- 1 → 3 → 6, Depth = 2
INSERT INTO CommentClosure (AncestorID, DescendantID, Depth) 
VALUES (3, 6, 1); -- 3 → 6, Depth = 1</code></pre>
<p><strong>说明</strong>：已修正<code>Depth</code>值，确保其准确反映评论之间的层级关系。</p>
</li>
<li>
<p><strong><code>CommentClosure</code>表插入后</strong>:</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>AncestorID</th>
<th>DescendantID</th>
<th>Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>4</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>6</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>6</td>
<td>1</td>
</tr>
<tr>
<td>4</td>
<td>4</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>5</td>
<td>0</td>
</tr>
<tr>
<td>6</td>
<td>6</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>通过这样的方式，我们可以有效地管理和查询多级评论的层级结构。在实际应用中，插入和查询操作通常会通过编程语言中的数据库操作库来完成（ORM，上述SQL操作和手动过程仅供理解和演示之用）。</p>
<h2>数据库事务处理</h2>
<p>在涉及多步数据库操作（如插入评论及闭包表记录）时，建议使用事务以确保数据一致性。例如，在<code>CreateComment</code>函数中，使用事务包裹所有插入操作：</p>
<pre><code class="language-go">func CreateComment(ctx context.Context, teamId, userId uint, content string, replyTo *uint) (*model.Comment, error) {
    tx := config.DB.Begin()
    if tx.Error != nil {
        return nil, tx.Error
    }

    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    // 初始化Comment对象
    comment := &amp;model.Comment{
        TeamId:  teamId,
        UserId:  userId,
        Content: content,
    }

    // 如果是回复评论，设置ReplyTo字段
    if replyTo != nil {
        comment.ReplyTo = *replyTo
    }

    // 向数据库中插入新评论
    if err := tx.Create(comment).Error; err != nil {
        tx.Rollback()
        return nil, err
    }

    // 插入闭包表记录
    selfRelation := model.CommentClosure{
        AncestorId:   comment.ID,
        DescendantId: comment.ID,
        Depth:        0,
    }
    if err := tx.Create(&amp;selfRelation).Error; err != nil {
        tx.Rollback()
        return nil, err
    }

    // 如果是回复评论，更新闭包表
    if replyTo != nil {
        var ancestorRelations []model.CommentClosure
        if err := tx.Model(&amp;model.CommentClosure{}).
            Where(&quot;descendant_id = ?&quot;, *replyTo).
            Find(&amp;ancestorRelations).Error; err != nil {
            tx.Rollback()
            return nil, err
        }

        var newRelations []model.CommentClosure
        for _, relation := range ancestorRelations {
            newRelations = append(newRelations, model.CommentClosure{
                AncestorId:   relation.AncestorId,
                DescendantId: comment.ID,
                Depth:        relation.Depth + 1,
            })
        }

        if err := tx.CreateInBatches(newRelations, 100).Error; err != nil {
            tx.Rollback()
            return nil, err
        }
    }

    // 提交事务
    if err := tx.Commit().Error; err != nil {
        return nil, err
    }

    return GetCommentById(ctx, comment.ID)
}</code></pre>
<h1>CRUD操作实现</h1>
<p>下面是基于MySQL和Gorm的完整Golang示例，其中<code>Team</code>代表动态，类似于帖子（Post）。实现了与即刻APP类似的操作逻辑。</p>
<h2>模型定义</h2>
<pre><code class="language-go">// Comment 评论表
type Comment struct {
    gorm.Model
    PostId        uint      `gorm:&quot;column:post_id;index&quot;`    // 关联帖子ID，建立索引
    TeamId        uint      `gorm:&quot;column:team_id;index&quot;`    // 关联队伍ID，建立索引
    UserId        uint      `gorm:&quot;column:user_id;index&quot;`    // 关联用户ID，建立索引
    User          User      `gorm:&quot;foreignKey:UserId&quot;`       // 关联用户信息
    Content       string    `gorm:&quot;column:content;type:text&quot;`// 评论内容
    ReplyTo       *uint     `gorm:&quot;column:reply_to;index&quot;`   // 回复的评论ID，建立索引
    Children      []Comment `gorm:&quot;-&quot;`
    ChildrenCount int       `gorm:&quot;-&quot;`
}

// CommentClosure 评论闭包表
type CommentClosure struct {
    gorm.Model
    AncestorId   uint `gorm:&quot;column:ancestor_id;index&quot;`   // 祖先评论ID
    DescendantId uint `gorm:&quot;column:descendant_id;index&quot;` // 后代评论ID
    Depth        int  `gorm:&quot;column:depth;type:int&quot;`      // 两者之间的深度
}</code></pre>
<h2>DAO层</h2>
<pre><code class="language-go">package dao

import (
    &quot;context&quot;
    &quot;errors&quot;

    &quot;github.com/CZT0/ustc-uu/internal/config&quot;
    &quot;github.com/CZT0/ustc-uu/internal/model&quot;
    &quot;github.com/CZT0/ustc-uu/internal/utils&quot;
)

// CreateComment 创建一个新的评论，可以是根评论或回复现有评论
// ctx: 上下文对象，用于控制函数执行时的行为（如超时、取消等）
// teamId: 评论所属的团队ID
// userId: 发布评论的用户ID
// content: 评论内容
// replyTo: 可选，被回复的评论ID，如果是根评论则为nil
func CreateComment(ctx context.Context, teamId, userId uint, content string, replyTo *uint) (*model.Comment, error) {
    tx := config.DB.Begin()
    if tx.Error != nil {
        return nil, tx.Error
    }

    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    // 初始化Comment对象
    comment := &amp;model.Comment{
        TeamId:  teamId,
        UserId:  userId,
        Content: content,
    }

    // 如果是回复评论，设置ReplyTo字段
    if replyTo != nil {
        comment.ReplyTo = replyTo
    }

    // 向数据库中插入新评论
    if err := tx.Create(comment).Error; err != nil {
        tx.Rollback()
        return nil, err // 插入失败，返回错误
    }

    // 插入新评论与自己的关系（depth=0），表示评论本身
    selfRelation := model.CommentClosure{
        AncestorId:   comment.ID,
        DescendantId: comment.ID,
        Depth:        0,
    }
    if err := tx.Create(&amp;selfRelation).Error; err != nil {
        tx.Rollback()
        return nil, err // 插入失败，返回错误
    }

    // 如果是回复评论，需要更新闭包表以反映新的层级关系
    if replyTo != nil {
        var ancestorRelations []model.CommentClosure
        // 获取回复目标评论的所有祖先（包括目标评论本身），以及它们到目标评论的深度
        if err := tx.Model(&amp;model.CommentClosure{}).
            Where(&quot;descendant_id = ?&quot;, *replyTo).
            Find(&amp;ancestorRelations).Error; err != nil {
            tx.Rollback()
            return nil, err
        }

        var newRelations []model.CommentClosure
        // 为每个祖先关系添加一条新记录，深度加1
        for _, relation := range ancestorRelations {
            newRelation := model.CommentClosure{
                AncestorId:   relation.AncestorId,
                DescendantId: comment.ID,
                Depth:        relation.Depth + 1,
            }
            newRelations = append(newRelations, newRelation)
        }

        // 批量插入新的闭包表记录
        batchSize := 100
        if err := tx.CreateInBatches(newRelations, batchSize).Error; err != nil {
            tx.Rollback()
            return nil, err // 插入失败，返回错误
        }
    }

    // 提交事务
    if err := tx.Commit().Error; err != nil {
        return nil, err
    }

    // 加载关联的用户信息并返回新创建的评论对象
    return GetCommentById(ctx, comment.ID)
}

// GetCommentById 根据评论ID获取评论对象，包括关联的用户信息
// ctx: 上下文对象
// id: 评论ID
func GetCommentById(ctx context.Context, id uint) (*model.Comment, error) {
    comment := &amp;model.Comment{}
    result := config.DB.WithContext(ctx).Preload(&quot;User&quot;).First(comment, id)
    if result.Error != nil {
        return nil, result.Error // 查询失败，返回错误
    }
    return comment, nil
}

// GetCommentsByTeam 根据团队ID分页查询第一级评论，并获取每个评论的部分二级评论和二级评论总数
// ctx: 上下文对象，用于控制函数执行时的行为（如超时、取消等）
// teamId: 团队ID，指定查询评论所属的团队
// first: 查询的评论数量限制，用于分页控制
// cursor: 可选，上一页最后一个评论的ID，用于实现游标分页
func GetCommentsByTeam(ctx context.Context, teamId uint, first int, cursor *uint) ([]*model.Comment, error) {
    var comments []*model.Comment
    // 构建查询，只查询第一级评论
    query := config.DB.WithContext(ctx).
        Preload(&quot;User&quot;).
        Where(&quot;comments.team_id = ? AND comments.reply_to IS NULL&quot;, teamId)

    // 如果提供了游标，只查询ID大于游标值的评论
    if cursor != nil {
        query = query.Where(&quot;comments.id &gt; ?&quot;, *cursor)
    }

    // 应用Limit和Order来支持分页和排序
    err := query.Order(&quot;comments.id&quot;).Limit(first).Find(&amp;comments).Error
    if err != nil {
        return nil, err // 查询失败，返回错误
    }

    // 检查是否查询到评论
    if len(comments) == 0 {
        return comments, nil // 如果没有一级评论，直接返回
    }

    // 准备批量查询二级评论数量和前两个二级评论
    // 获取所有一级评论的ID
    var commentIDs []uint
    for _, comment := range comments {
        commentIDs = append(commentIDs, comment.ID)
    }

    // 查询每个一级评论的二级评论总数
    type CountResult struct {
        ReplyTo uint
        Count   int
    }

    var countResults []CountResult
    err = config.DB.Model(&amp;model.Comment{}).
        Select(&quot;reply_to, COUNT(*) as count&quot;).
        Where(&quot;reply_to IN (?)&quot;, commentIDs).
        Group(&quot;reply_to&quot;).
        Scan(&amp;countResults).Error
    if err != nil {
        return nil, err
    }

    countsMap := make(map[uint]int)
    for _, res := range countResults {
        countsMap[res.ReplyTo] = res.Count
    }

    // 查询每个一级评论的前两个二级评论
    var childComments []model.Comment
    err = config.DB.Where(&quot;reply_to IN (?)&quot;, commentIDs).
        Order(&quot;id&quot;).
        Limit(2).
        Find(&amp;childComments).Error
    if err != nil {
        return nil, err
    }

    // 组织二级评论数据，映射到相应的一级评论上
    childCommentsMap := make(map[uint][]model.Comment)
    for _, child := range childComments {
        childCommentsMap[child.ReplyTo] = append(childCommentsMap[child.ReplyTo], child)
    }

    // 遍历一级评论，附加二级评论数量和前两个二级评论
    for i, comment := range comments {
        comments[i].ChildrenCount = countsMap[comment.ID] // 设置二级评论数量
        comments[i].Children = childCommentsMap[comment.ID] // 设置前两个二级评论
    }

    return comments, nil
}

// GetChildrenCommentsByID 根据评论ID分页查询其所有子评论
// ctx: 上下文对象
// commentId: 被查询的评论ID
// first: 查询的评论数量限制
// cursor: 可选，上一页最后一个子评论的ID，用于分页查询
func GetChildrenCommentsByID(ctx context.Context, commentId uint, first int, cursor *uint) ([]*model.Comment, error) {
    var descendantIds []uint
    // 构建基础查询，目标是获取所有子评论的ID
    query := config.DB.WithContext(ctx).
        Model(&amp;model.CommentClosure{}).
        Where(&quot;ancestor_id = ?&quot;, commentId).
        Order(&quot;depth, descendant_id&quot;) // 增加更具体的排序

    // 如果提供了游标，调整查询以包括游标条件
    if cursor != nil {
        query = query.Where(&quot;descendant_id &gt; ?&quot;, *cursor)
    }

    // 查询所有后代评论ID，考虑游标和数量限制
    err := query.Pluck(&quot;descendant_id&quot;, &amp;descendantIds).Error
    if err != nil {
        return nil, err // 查询失败，返回错误
    }

    // 如果没有找到后代评论，直接返回空切片
    if len(descendantIds) == 0 {
        return []*model.Comment{}, nil
    }

    // 限制结果数量，如果指定了first参数
    if first &gt; 0 &amp;&amp; len(descendantIds) &gt; first {
        descendantIds = descendantIds[:first]
    }

    // 根据后代评论ID查询评论详细信息
    var comments []*model.Comment
    err = config.DB.WithContext(ctx).
        Preload(&quot;User&quot;). // 预加载User信息
        Where(&quot;id IN (?)&quot;, descendantIds).
        Order(&quot;depth, created_at&quot;). // 确保排序与闭包表一致
        Find(&amp;comments).Error
    if err != nil {
        return nil, err // 查询失败，返回错误
    }

    return comments, nil
}

// DeleteComment 删除指定ID的评论
// ctx: 上下文对象
// id: 要删除的评论ID
// userId: 尝试删除评论的用户ID，用于权限验证
func DeleteComment(ctx context.Context, id uint, userId uint) error {
    // 首先获取评论对象，验证是否存在以及用户权限
    result, err := GetCommentById(ctx, id)
    if err != nil {
        return err // 查询失败或评论不存在，返回错误
    }
    // 验证尝试删除评论的用户是否为评论的发布者
    if result.UserId != userId {
        return errors.New(&quot;no permission&quot;) // 没有权限，返回错误
    }

    // 检查该评论是否有后代
    var count int64
    err = config.DB.WithContext(ctx).
        Model(&amp;model.CommentClosure{}).
        Where(&quot;ancestor_id = ? AND depth &gt; 0&quot;, id).
        Count(&amp;count).Error
    if err != nil {
        return err // 查询失败，返回错误
    }

    if count &gt; 0 {
        // 有后代评论时，更新评论内容为&quot;该评论已删除&quot;，而不是真正删除评论
        // 这是为了保持评论结构的完整性
        result := config.DB.WithContext(ctx).
            Model(&amp;model.Comment{}).
            Where(&quot;id = ?&quot;, id).
            Update(&quot;content&quot;, &quot;该评论已删除&quot;)
        if result.Error != nil {
            return result.Error // 更新失败，返回错误
        }
    } else {
        // 没有后代评论，可以安全删除评论本身
        // 删除评论
        result := config.DB.WithContext(ctx).Delete(&amp;model.Comment{}, id)
        if result.Error != nil {
            return result.Error // 删除失败，返回错误
        }

        // 同时删除闭包表中与该评论相关的所有记录
        result = config.DB.WithContext(ctx).
            Where(&quot;ancestor_id = ? OR descendant_id = ?&quot;, id, id).
            Delete(&amp;model.CommentClosure{})
        if result.Error != nil {
            return result.Error // 删除失败，返回错误
        }
    }

    return nil // 删除成功
}</code></pre>
<h1>查询Post下的评论</h1>
<p>假设每个评论都有<code>PostId</code>字段指向所属的Post，我们可以利用闭包表来实现对评论的高效查询。</p>
<h2>查询所有第一级评论</h2>
<pre><code class="language-sql">SELECT Comment.*
FROM Comment
WHERE Comment.PostId = ? AND Comment.ReplyTo IS NULL
ORDER BY Comment.CreatedAt;</code></pre>
<p>这个查询获取了指定Post的所有第一级评论，这是直接查询而不涉及闭包表的部分。</p>
<h2>利用闭包表查询第一级评论的所有子评论数量</h2>
<pre><code class="language-sql">SELECT AncestorId, COUNT(*) AS TotalComments
FROM CommentClosure
WHERE AncestorId IN (
    SELECT ID
    FROM Comment
    WHERE PostId = ? AND ReplyTo IS NULL
)
AND Depth &gt; 0
GROUP BY AncestorId;</code></pre>
<p>这个查询利用了闭包表的优点，直接计算了每个第一级评论下的子评论总数（包括所有层级），无需递归查询。</p>
<h2>查询某个评论下的所有子评论</h2>
<p>利用闭包表，我们可以一次性获取到某个评论下的所有子评论，包括多级嵌套评论。</p>
<pre><code class="language-sql">SELECT Child.*
FROM Comment AS Child
JOIN CommentClosure AS Closure ON Child.ID = Closure.DescendantId
WHERE Closure.AncestorId = ? -- 目标评论ID
ORDER BY Closure.Depth, Child.CreatedAt;</code></pre>
<p>这个查询通过闭包表一次性获取了指定评论下的所有子评论，<code>Closure.AncestorId = ?</code>确保了我们只查询目标评论的后代。这种方法相比于逐个查询<code>ReplyTo</code>字段，不仅简化了查询逻辑，也大大提高了查询效率，尤其是在处理深层次评论结构时。</p>
<p><strong>闭包表的优点体现</strong>：</p>
<ol>
<li><strong>子评论计数</strong>：上述计数查询通过闭包表直接统计了每个第一级评论下的所有子评论数量，避免了复杂的递归查询，大大提高了性能。</li>
<li><strong>获取所有层级的评论</strong>：通过闭包表的查询，我们可以一次性获取某个评论下的所有子评论，包括多级嵌套评论。这种方法避免了对每个<code>ReplyTo</code>字段的逐个递归查询，使得数据的提取更加高效和直观。</li>
</ol>
<p>通过这种方式，我们可以充分利用闭包表的优势，实现对评论数据的高效管理和查询，尤其是在构建具有复杂层级关系的社区类应用场景中。</p>
<h2>分页查询（使用游标）</h2>
<p>分页查询通常涉及到<code>LIMIT</code>和<code>OFFSET</code>语句，但使用游标进行分页可以提高效率，尤其是在数据量大的情况下。游标分页依赖于一个唯一标识（通常是ID），客户端存储上一次加载的最后一个ID，下次查询从这个ID开始。</p>
<pre><code class="language-sql">-- 假设上次加载的最后一个评论ID为10
SELECT * FROM Comment WHERE ID &gt; 10 ORDER BY ID LIMIT 10;</code></pre>
<p>这样，每次加载新的评论页时，只需要传递上一页最后一个评论的ID作为游标。这种方法比传统的OFFSET方法更高效，因为它避免了跳过大量行的性能开销。</p>
<h1>删除评论</h1>
<p>在删除评论时，不建议直接删除记录，尤其是当该评论还有后代时。推荐的做法如下：</p>
<ol>
<li>
<p>有后代评论时</p>
<p>：</p>
<ul>
<li>更新评论内容为“该评论已删除”，保持评论结构的完整性。</li>
</ul>
</li>
<li>
<p>无后代评论时</p>
<p>：</p>
<ul>
<li>可以安全地删除评论及其在闭包表中的所有相关记录。</li>
</ul>
</li>
</ol>
<pre><code class="language-go">func DeleteComment(ctx context.Context, id uint, userId uint) error {
    // 首先获取评论对象，验证是否存在以及用户权限
    result, err := GetCommentById(ctx, id)
    if err != nil {
        return err // 查询失败或评论不存在，返回错误
    }
    // 验证尝试删除评论的用户是否为评论的发布者
    if result.UserId != userId {
        return errors.New(&quot;no permission&quot;) // 没有权限，返回错误
    }

    // 检查该评论是否有后代
    var count int64
    err = config.DB.WithContext(ctx).
        Model(&amp;model.CommentClosure{}).
        Where(&quot;ancestor_id = ? AND depth &gt; 0&quot;, id).
        Count(&amp;count).Error
    if err != nil {
        return err // 查询失败，返回错误
    }

    if count &gt; 0 {
        // 有后代评论时，更新评论内容为&quot;该评论已删除&quot;
        result := config.DB.WithContext(ctx).
            Model(&amp;model.Comment{}).
            Where(&quot;id = ?&quot;, id).
            Update(&quot;content&quot;, &quot;该评论已删除&quot;)
        if result.Error != nil {
            return result.Error // 更新失败，返回错误
        }
    } else {
        // 没有后代评论，可以安全删除评论本身
        // 删除评论
        result := config.DB.WithContext(ctx).Delete(&amp;model.Comment{}, id)
        if result.Error != nil {
            return result.Error // 删除失败，返回错误
        }

        // 同时删除闭包表中与该评论相关的所有记录
        result = config.DB.WithContext(ctx).
            Where(&quot;ancestor_id = ? OR descendant_id = ?&quot;, id, id).
            Delete(&amp;model.CommentClosure{})
        if result.Error != nil {
            return result.Error // 删除失败，返回错误
        }
    }

    return nil // 删除成功
}</code></pre>
<p><strong>说明</strong>：</p>
<ul>
<li><strong>权限验证</strong>：确保只有评论的发布者才能删除评论，提升系统安全性。</li>
<li><strong>数据一致性</strong>：通过条件判断，确保在有后代评论时不破坏评论结构。</li>
</ul>
<h1>总结</h1>
<p>本文详细介绍了利用闭包表实现多级评论结构的设计与实现，特别适用于需要处理复杂层级关系的社区类应用。通过闭包表，我们能够高效地进行评论的创建、读取、更新和删除操作，同时保持数据的一致性和完整性。结合Golang和Gorm的实际代码示例，提供了实用的参考，实现高效的多级评论系统。</p>
<p><strong>关键要点</strong>：</p>
<ul>
<li><strong>闭包表的优势</strong>：灵活性高、查询性能优越、操作简化。</li>
<li><strong>数据一致性</strong>：通过事务处理和合理的逻辑判断，确保数据的一致性和完整性。</li>
<li><strong>性能优化</strong>：通过索引优化和合理的分页策略，提升系统的整体性能。</li>
<li><strong>实际应用</strong>：结合Golang和Gorm的示例代码，展示了闭包表在实际项目中的应用方法。</li>
</ul>
<p>通过合理地设计数据库结构和优化CRUD操作，闭包表能够有效支持多级评论系统的复杂需求，为社区类应用提供强大的数据支持和高效的用户体验。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5213/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5213</post-id>	</item>
		<item>
		<title>消息队列</title>
		<link>https://www.cztcode.com/2024/5183/</link>
					<comments>https://www.cztcode.com/2024/5183/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Wed, 10 Jan 2024 03:03:38 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5183</guid>

					<description><![CDATA[简介 消息：是指在应用之间传送的数据，消息可以非常简单，比如只包含文本字符串，也可以更复杂，可能包含嵌入对象。 消息队列（Message Queue）：是一种应用间的通信方式，消息发送后可以立即返回，由消息系统来确保信息的可靠专递，消息发布者只管把消息发布到MQ中而不管谁来取，消息使用者只管从MQ中取消息而不管谁发布的，这样发布者和使用者都不用知道对方的存在 应用场景 系统解耦：消息队列可以减少系 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>简介</h1>
<p>消息：是指在应用之间传送的数据，消息可以非常简单，比如只包含文本字符串，也可以更复杂，可能包含嵌入对象。</p>
<p>消息队列（Message Queue）：是一种应用间的通信方式，消息发送后可以立即返回，由消息系统来确保信息的可靠专递，消息发布者只管把消息发布到MQ中而不管谁来取，消息使用者只管从MQ中取消息而不管谁发布的，这样发布者和使用者都不用知道对方的存在</p>
<h1>应用场景</h1>
<ol>
<li><strong>系统解耦</strong>：消息队列可以减少系统组件之间的直接依赖。例如，在微服务架构中，服务间通过消息队列通信，这样即使某个服务暂时不可用，其他服务也能继续工作。不同的应用服务往往需要相互通信。传统的直接调用方式（如 REST API）可能会导致强耦合，即一个服务的故障可能影响到其他服务。使用消息队列，服务之间通过异步消息进行通信，减少了直接的依赖。例如，一个订单服务发送订单创建消息到队列，而库存服务和支付服务都可以独立地从队列中接收和处理这些消息。这种方法降低了服务间的耦合度，提高了整个系统的健壮性。</li>
<li><strong>数据分发</strong>：消息队列常用于在系统的不同部分间有效地分发数据。例如，在事件驱动架构中，事件可以作为消息发布到队列中，然后由一个或多个服务处理这些事件。</li>
<li><strong>事务处理</strong>：在需要处理复杂事务的系统中，消息队列可以用来确保数据一致性和事务的完整性。例如，在分布式系统中，事务可能需要跨多个服务，消息队列可以帮助在这些服务间同步事务状态。</li>
<li><strong>日志处理和监控</strong>：消息队列也可以用于收集各种系统日志，供日后分析和监控系统性能。</li>
<li><strong>实现发布/订阅模式</strong>：在这种模式下，生产者发布消息到队列，而消费者订阅这些消息。这种模式允许多个消费者独立地处理相同的消息，增加了系统的灵活性和可扩展性。</li>
<li><strong>容错和故障恢复</strong>：通过消息队列，系统可以更容易地处理部分组件的故障。即使生产者或消费者的一部分出现故障，消息队列仍然可以保存消息，待系统恢复后再进行处理。</li>
<li><strong>限流削峰</strong>：广泛应用于秒杀或抢购活动中，避免流量过大导致应用系统挂掉的情况。在高流量事件（如秒杀、大促销）期间，系统可能会遭遇巨大的请求压力。这时，消息队列可以作为缓冲层来帮助处理请求流量。用户的请求首先被发送到消息队列，然后按照消费者处理能力逐渐被处理。这种机制可以防止突然的流量峰值直接冲击数据库或后端服务，从而避免系统崩溃。</li>
</ol>
<h1>消息传递模式</h1>
<h3>1. 点对点（Point-to-Point）模式</h3>
<p>在点对点消息传递模式中，消息被发送到一个队列。在这种模式下，每个消息只有一个消费者。消息生产者（发送者）发送消息到队列，消息消费者（接收者）从队列中接收消息。消息一旦被消费，就不会再被其他消费者处理。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>每个消息只被一个消费者接收。</li>
<li>生产者和消费者之间没有时间上的依赖性。消费者可以在消息发送后的任何时间提取消息。</li>
<li>消息处理通常是顺序的，一个接一个。</li>
</ul>
<p><strong>适用场景</strong>：</p>
<ul>
<li>当消息需要被单个接收者处理时，如订单处理系统。</li>
<li>需要确保消息按顺序处理的场景。</li>
</ul>
<h3>2. 发布/订阅（Publish/Subscribe）模式</h3>
<p>在发布/订阅模式中，消息被发送到一个主题。消息生产者（发布者）发布消息到主题，而不是直接发送给特定的消费者。消息消费者（订阅者）订阅这个主题，并接收所有发送到该主题的消息。</p>
<p><strong>特点</strong>：</p>
<ul>
<li>每个消息可以被多个订阅者接收。</li>
<li>生产者和消费者之间是松耦合的。生产者只负责发布消息到主题，不关心谁是接收者。</li>
<li>支持消息的广播，即同一个消息可以被多个订阅者同时接收。</li>
</ul>
<p><strong>适用场景</strong>：</p>
<ul>
<li>需要将消息广播给多个接收者的场景，如股票市场信息更新。</li>
<li>当信息的生产者和消费者之间的耦合程度需要最小化时。</li>
</ul>
<h1>Kafka</h1>
<h3>Kafka 简介</h3>
<p>Apache Kafka 是一个分布式流处理平台，最初由 LinkedIn 开发，并于 2011 年作为开源项目加入到 Apache 软件基金会。它被设计用于高吞吐量、高可靠性和高可扩展性的数据流处理。</p>
<h3>核心概念</h3>
<ol>
<li><strong>Broker</strong>：Kafka 集群由多个服务器组成，每个服务器称为 broker。</li>
<li><strong>Topic</strong>：数据的分类单位。生产者将数据发布到特定的主题，消费者从主题订阅数据。</li>
<li><strong>Partition</strong>：为了实现可扩展性和并行处理，主题可以分成多个 partition，每个 partition 是一个有序的、不可变的记录序列。</li>
<li><strong>Offset</strong>：Partition 中的每条消息都有一个唯一的序号，称为 offset，它用于唯一标识 partition 中的每条消息。</li>
<li><strong>Producer</strong>：消息的发布者，负责发布消息到 Kafka 的主题。</li>
<li><strong>Consumer</strong>：消息的消费者，从 Kafka 的主题订阅并处理消息。</li>
<li><strong>Consumer Group</strong>：消费者组，由多个消费者组成，可以实现消息的负载均衡和容错。</li>
</ol>
<h3>关键特性</h3>
<ol>
<li><strong>高吞吐量</strong>：Kafka 能够处理数百万条消息每秒，适用于高负载环境。</li>
<li><strong>可扩展性</strong>：Kafka 集群可以水平扩展，增加更多的 broker 来处理更多的数据。</li>
<li><strong>持久性和可靠性</strong>：消息在 Kafka 中是持久化的，并且可以在多个 broker 之间进行复制，以提高数据的可靠性。</li>
<li><strong>实时性</strong>：Kafka 支持实时消息处理，使其适用于需要低延迟数据处理的场景。</li>
<li><strong>容错能力</strong>：如果一个或多个 broker 失败，Kafka 仍然能够继续工作。</li>
</ol>
<h3>应用场景</h3>
<ol>
<li><strong>日志聚合</strong>：
<ul>
<li>Kafka 可以从各种服务、系统和应用中收集日志数据。</li>
<li>这些日志数据可以用于实时监控、安全分析、操作审计等。</li>
<li>Kafka 的高吞吐量特性使其成为大规模日志数据处理的理想选择。</li>
</ul>
</li>
<li><strong>实时数据管道</strong>：
<ul>
<li>Kafka 可以在不同的系统和应用之间可靠地传输数据。</li>
<li>例如，可以将数据从数据库实时同步到搜索引擎、数据仓库或缓存系统。</li>
<li>Kafka 的持久性和可靠性保证了数据的完整性和准确性。</li>
</ul>
</li>
<li><strong>流处理</strong>：
<ul>
<li>Kafka 与流处理技术（如 Apache Flink 或 Apache Storm）结合使用，可以实时处理数据流。</li>
<li>应用包括实时分析、在线机器学习、连续计算等。</li>
<li>Kafka Streams API 支持在 Kafka 上直接构建流应用。</li>
</ul>
</li>
<li><strong>事件驱动架构</strong>：
<ul>
<li>Kafka 可以作为微服务架构中的事件总线。</li>
<li>服务可以发布和订阅事件，实现松耦合的交互。</li>
<li>适用于复杂的业务流程和实时更新场景。</li>
</ul>
</li>
</ol>
<h3>架构挑战</h3>
<ol>
<li><strong>数据保留策略</strong>：Kafka 允许配置数据的保留期限，但管理大量历史数据可能是一个挑战。</li>
<li><strong>消费者状态管理</strong>：在消费者组中，跟踪和管理每个消费者的 offset 是一个挑战。</li>
</ol>
<h3>结论</h3>
<p>Kafka 是一个功能强大的分布式流处理平台，适用于需要高吞吐量、高可靠性和实时处理的场景。其设计哲学是简洁高效，适用于构建高性能的数据管道和流应用程序。</p>
<h1>RabbitMQ</h1>
<h3>RabbitMQ 简介</h3>
<p>RabbitMQ 是一个开源的消息代理软件，用于在分布式系统中存储和转发消息。它是用 Erlang 语言编写的，因此具有高并发和高可靠性的特点。RabbitMQ 支持多种消息协议，如 AMQP（高级消息队列协议），也支持多种编程语言和平台。</p>
<h3>关键特性</h3>
<ol>
<li>
<p><strong>可靠性</strong>：RabbitMQ 提供消息持久化、交付确认和高可用性等多种机制，以确保消息的可靠传输。</p>
</li>
<li>
<p><strong>灵活的路由</strong>：通过交换机和路由键，RabbitMQ 能够灵活地控制消息传递的行为。</p>
</li>
<li>
<p><strong>多种交换机类型</strong>：如直连交换机（Direct）、主题交换机（Topic）、扇形交换机（Fanout）和头交换机（Headers），适用于不同的消息路由场景。</p>
</li>
<li>
<p><strong>跨语言和平台</strong>：RabbitMQ 支持多种编程语言和操作系统。</p>
</li>
<li>
<p><strong>集群和高可用性</strong>：RabbitMQ 支持集群部署，提高消息系统的可用性和伸缩性。</p>
</li>
<li>
<p><strong>死信队列（Dead Letter Queue, DLQ）</strong></p>
</li>
<li>
<p><strong>定义</strong>：</p>
<ul>
<li>死信队列是用于存储无法处理的消息的特殊队列。在 RabbitMQ 中，当消息因为某些原因（如无法路由、消息被拒绝、消息过期）无法正常处理时，这些消息可以被发送到一个指定的死信队列。</li>
</ul>
</li>
<li>
<p><strong>应用场景</strong>：</p>
<ul>
<li>死信处理：当消息因为业务逻辑错误、系统故障或者其他原因无法处理时，可以将这些消息重定向到死信队列，以便后续分析和处理。</li>
<li>延迟重试机制：在处理消息失败时，可以利用死信队列来实现消息的延迟重试。即先将消息发送到死信队列，等待一段时间后再重新发送到原队列。</li>
</ul>
</li>
<li>
<p><strong>配置方法</strong>：</p>
<ul>
<li>在 RabbitMQ 中，可以通过队列的参数设置来配置死信交换机（DLX）和死信路由键（DLK）。当消息成为死信后，它们会被发送到指定的 DLX，并根据 DLK 被路由到相应的队列。</li>
</ul>
</li>
<li>
<p><strong>监控与管理</strong>：</p>
<ul>
<li>死信队列的监控和管理对于确保消息系统的健康运行非常重要。它可以帮助及时发现和解决消息处理中的问题。</li>
</ul>
</li>
</ol>
<h3>应用场景</h3>
<ol>
<li><strong>任务队列（异步处理）</strong>：
<ul>
<li>RabbitMQ 广泛用于实现后台任务和异步工作队列。</li>
<li>例如，在网站中，耗时的任务（如发送电子邮件、图片处理）可以放入队列中异步处理。</li>
<li>这有助于提高应用性能和用户体验。</li>
</ul>
</li>
<li><strong>系统解耦</strong>：
<ul>
<li>在微服务架构中，RabbitMQ 用于解耦系统组件。</li>
<li>各服务通过消息队列进行通信，而不是直接调用。</li>
<li>这样即使一个服务暂时不可用，也不会影响整个系统的运行。</li>
</ul>
</li>
<li><strong>分布式事务处理</strong>：
<ul>
<li>RabbitMQ 可用于跨多个服务和组件处理分布式事务。</li>
<li>比如，在电子商务系统中，订单服务、库存服务和物流服务可以通过消息队列来协调事务。</li>
</ul>
</li>
<li><strong>消息路由与分发</strong>：
<ul>
<li>RabbitMQ 的交换机和路由键提供了强大的消息路由功能。</li>
<li>它可以基于内容、优先级或发布者将消息路由到不同的队列。</li>
<li>这适用于需要精细控制消息分发的复杂应用场景。</li>
</ul>
</li>
</ol>
<h3>架构挑战</h3>
<ol>
<li><strong>消息积压</strong>：在高负载情况下，消息可能在队列中积压，需要合理的监控和调优。</li>
<li><strong>集群管理</strong>：维护和管理 RabbitMQ 集群需要一定的运维技能，特别是在高可用和故障转移方面。</li>
</ol>
<h3>结论</h3>
<p>RabbitMQ 是一个强大的消息代理软件，适用于需要灵活的消息路由、高可靠性和跨平台支持的场景。它在企业应用中被广泛使用，特别适合于任务队列和事件通知等场景。</p>
<h1>Kafka与RabbitMQ对比</h1>
<h3>Kafka 与 RabbitMQ 详细对比</h3>
<h3>吞吐量</h3>
<ul>
<li><strong>Kafka</strong>：设计以日志处理为核心，支持高吞吐量（可达百万级消息/秒）。最适合需要处理大量数据流的场景，如日志聚合、用户活动追踪。</li>
<li><strong>RabbitMQ</strong>：更适合中等规模的吞吐量（约万级消息/秒），优于处理高优先级的小消息或需要复杂路由的场景。</li>
</ul>
<h3>有序性与消息模式</h3>
<ol>
<li>Kafka 的分区有序性
<ul>
<li><strong>Kafka</strong> 在每个分区内保证消息的顺序。这意味着，只要消息被发送到同一个分区，它们的处理顺序就如同发送顺序一样。</li>
<li>例如，在一个电子商务应用中，关于特定订单的所有更新可以发送到同一个分区中，以确保订单状态的更新按照发生的顺序进行处理。</li>
<li>然而，Kafka 不保证跨分区的消息顺序。如果一个应用需要跨多个分区处理数据，那么全局的顺序就无法得到保证。</li>
</ul>
</li>
<li>RabbitMQ 的全局有序性
<ul>
<li><strong>RabbitMQ</strong> 在单个队列中保证消息的全局有序性。这意味着，无论何时发送到队列中的消息都会按照它们到达队列的顺序来处理。</li>
<li>例如，在一个任务分发系统中，不同的任务被放入同一个队列中，系统会按照任务到达队列的顺序来逐一处理这些任务，确保所有任务的执行顺序与其被加入队列的顺序相同。</li>
<li>RabbitMQ 适合那些需要绝对顺序保证的场景，无论涉及的消息数量多少或者消费的速度如何。</li>
</ul>
</li>
</ol>
<h3>消息可靠性</h3>
<ul>
<li><strong>Kafka</strong>：依靠持久化和数据副本来保证消息不丢失，适合数据备份和恢复场景。</li>
<li><strong>RabbitMQ</strong>：提供更丰富的消息确认和持久化选项，适用于对消息可靠性要求极高的金融或商务应用。</li>
</ul>
<h3>时效性</h3>
<ul>
<li><strong>Kafka</strong>：批量处理可能引起轻微延迟，但适用于需要高吞吐量且可容忍短暂延迟的场景。</li>
<li><strong>RabbitMQ</strong>：几乎实时的消息处理，更适用于需要快速响应的系统，如在线订单处理。</li>
</ul>
<h3>运维便捷度</h3>
<ul>
<li><strong>Kafka</strong>：依赖于外部系统（如Zookeeper），且配置和管理相对复杂，需要专业的运维支持。</li>
<li><strong>RabbitMQ</strong>：易于安装和配置，自带管理界面，适合运维资源有限的小型或中型企业。</li>
</ul>
<h3>特色功能</h3>
<ul>
<li><strong>Kafka</strong>：强大的流处理能力，适合需要进行实时数据分析和处理的应用，如实时监控系统。</li>
<li><strong>RabbitMQ</strong>：支持复杂的消息路由和特性（如死信队列、优先级队列），适用于需要精细控制消息传递的业务逻辑。</li>
</ul>
<h3>选型考虑</h3>
<p>在选择 Kafka 或 RabbitMQ 时，应考虑以下因素：</p>
<ol>
<li><strong>业务场景</strong>：
<ul>
<li>对于需要处理高吞吐量、大数据流的应用（如日志聚合、实时数据分析），Kafka 更合适。</li>
<li>对于需要高度可靠的消息传递、复杂的路由和优先级处理的场景（如金融交易、任务队列），RabbitMQ 是更佳选择。</li>
</ul>
</li>
<li><strong>系统规模与资源</strong>：
<ul>
<li>大型企业或有能力投入专业运维团队的组织可能更倾向于选择 Kafka，因其高吞吐量和扩展性。</li>
<li>对于资源有限、需要简单快速部署的小型或中型企业，RabbitMQ 的易用性和低维护成本是主要优势。</li>
</ul>
</li>
<li><strong>长期维护与扩展性</strong>：
<ul>
<li>Kafka 的设计支持高度的可扩展性和灵活性，适合预期有大量数据增长的长期项目。</li>
<li>RabbitMQ 的架构更简单，适合中小型项目或不需要大规模扩展的应用。</li>
</ul>
</li>
</ol>
<h1>面试QA</h1>
<h3>1. 什么是消息队列，它是如何工作的？</h3>
<p><strong>答案</strong>：<br />
消息队列是一种用于在不同应用、系统组件之间传递消息的技术。它允许应用发送消息到队列，而不是直接发送给接收者。这样，接收者可以异步地从队列中取出并处理消息。消息队列的核心优势在于解耦生产者和消费者、提高系统的可扩展性和容错能力。</p>
<h3>2. Kafka 和 RabbitMQ 有什么区别？</h3>
<p><strong>答案</strong>：<br />
Kafka 是为处理高吞吐量数据而设计的分布式流处理平台，它在每个分区内提供消息的有序性。RabbitMQ 是一个更传统的消息代理，适用于中等吞吐量，提供更复杂的消息路由能力和全局消息顺序性。选择两者之一取决于应用的具体需求：Kafka 更适合大数据处理和流分析，而 RabbitMQ 更适合需要复杂路由和严格消息排序的场景。</p>
<h3>3. 消息队列的哪些特性对于系统设计至关重要？</h3>
<p><strong>答案</strong>：</p>
<ul>
<li><strong>异步通信</strong>：消息队列允许系统组件异步地发送和接收消息，这有助于提高系统的响应性和吞吐量。</li>
<li><strong>解耦</strong>：通过消息队列，生产者和消费者可以独立地扩展和演化，降低系统各部分之间的依赖。</li>
<li><strong>容错性</strong>：如果消费者处理消息的能力暂时下降，消息队列可以存储消息直至它们被处理。</li>
<li><strong>负载均衡</strong>：在多个消费者的情况下，消息队列可以帮助平衡工作负载。</li>
</ul>
<h3>4. 什么是死信队列，为什么要使用它？</h3>
<p><strong>答案</strong>：<br />
死信队列是一种特殊的消息队列，用于存储无法正常处理的消息。消息可能因为格式错误、处理失败或超时等原因而无法处理。使用死信队列可以帮助隔离这些问题消息，方便开发者后续分析和处理问题，同时确保主消息流程的顺畅。</p>
<h3>5. 在什么情况下会使用 Kafka 而不是 RabbitMQ？</h3>
<p><strong>答案</strong>：<br />
如果应用需要处理大量数据流，如日志聚合或实时数据分析，且对消息处理的顺序有一定要求但可以容忍跨分区的无序性，那么 Kafka 是更合适的选择。Kafka 的高吞吐量和分布式特性使其适用于需要大规模数据处理的场景。相比之下，如果应用需要复杂的消息路由、优先级队列或全局消息排序，RabbitMQ 可能是更好的选择。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5183/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5183</post-id>	</item>
		<item>
		<title>MySQL常见知识</title>
		<link>https://www.cztcode.com/2024/5096/</link>
					<comments>https://www.cztcode.com/2024/5096/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Fri, 05 Jan 2024 02:19:25 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5096</guid>

					<description><![CDATA[查询缓存 查询缓存（Query Cache）。查询缓存的主要目的是提高数据库查询的效率，尤其是在频繁查询相同数据的场景下。 MySQL 查询缓存的工作原理 缓存查询结果：当执行一个 SELECT 查询时，MySQL 首先检查查询缓存，如果发现已缓存的查询结果与当前查询相匹配，它就直接返回缓存的结果，而不是重新执行查询。 缓存的失效：如果涉及到查询的表发生了更新（如 INSERT、UPDATE、DE [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h2>查询缓存</h2>
<p>查询缓存（Query Cache）。查询缓存的主要目的是提高数据库查询的效率，尤其是在频繁查询相同数据的场景下。</p>
<h3>MySQL 查询缓存的工作原理</h3>
<ol>
<li><strong>缓存查询结果</strong>：当执行一个 SELECT 查询时，MySQL 首先检查查询缓存，如果发现已缓存的查询结果与当前查询相匹配，它就直接返回缓存的结果，而不是重新执行查询。</li>
<li><strong>缓存的失效</strong>：如果涉及到查询的表发生了更新（如 INSERT、UPDATE、DELETE 操作），与该表相关的所有查询缓存都会被清除。</li>
<li><strong>配置</strong>：可以通过配置来启用或禁用查询缓存，以及设置缓存大小等。</li>
</ol>
<h3>注意事项</h3>
<ul>
<li><strong>版本差异</strong>：在 MySQL 8.0 及更高版本中，查询缓存功能已经被移除。在这些版本中，应关注其它性能优化策略。</li>
<li><strong>适用性</strong>：查询缓存适用于那些不经常变更但经常被查询的数据。如果数据更新非常频繁，查询缓存可能会因为频繁的失效而适得其反。</li>
</ul>
<h3>被删除的原因</h3>
<ol>
<li><strong>高并发环境下的性能问题</strong>：在高并发的数据库系统中，查询缓存可能成为瓶颈。每当有数据变更时，所有相关的缓存都需要被清除，这会导致大量的缓存失效和重建操作，增加了系统的开销。</li>
<li><strong>缓存失效问题</strong>：在频繁更新的数据库中，缓存的失效率非常高。这意味着缓存的效果并不明显，反而可能因为频繁的缓存更新和失效导致额外的性能损耗。</li>
<li><strong>不适应现代硬件</strong>：随着现代硬件的发展，尤其是 CPU 速度和内存容量的提升，直接从内存读取数据的速度已经非常快。这减少了查询缓存带来的相对优势。</li>
<li><strong>优化器的进步</strong>：数据库查询优化器的进步意味着对于许多类型的查询，即使没有缓存，执行速度也已经足够快。这进一步降低了查询缓存的必要性。</li>
<li><strong>简化数据库内核</strong>：移除查询缓存有助于简化 MySQL 的内核，使得数据库更加稳定和易于维护。这对于数据库的长期发展来说是有益的。</li>
<li><strong>更有效的替代方案</strong>：例如，使用 InnoDB 引擎的数据库可以从 InnoDB 的缓冲池中获得更高效的数据缓存。另外，应用层面的缓存（如 Redis、Memcached）通常提供更灵活和高效的缓存策略。</li>
</ol>
<h2>InnoDB缓冲池</h2>
<p>InnoDB 引擎的缓冲池（Buffer Pool）是 MySQL 中一个非常关键的特性，用于提高数据库操作的效率。它是 InnoDB 存储引擎的一部分，主要用于缓存数据和索引信息，以减少对磁盘的访问次数。下面是关于 InnoDB 缓冲池的一些主要特点和工作原理：</p>
<h3>缓冲池的作用</h3>
<ol>
<li><strong>数据页缓存</strong>：当从磁盘读取数据页时，InnoDB 首先会将这些数据页缓存到缓冲池中。如果后续有查询需要这些数据，可以直接从缓冲池中读取，而不需要再次访问磁盘。</li>
<li><strong>索引页缓存</strong>：除了数据页，InnoDB 还会将索引页缓存到缓冲池中，这样可以加快索引查找和扫描的速度。</li>
<li><strong>脏页的处理</strong>：当数据页在缓冲池中被修改（称为“脏页”），InnoDB 会在适当的时候将这些更改写回磁盘。这个过程是异步的，可以提高写操作的效率。</li>
</ol>
<h3>缓冲池的管理</h3>
<ul>
<li><strong>LRU 算法</strong>：InnoDB 使用一种改进的最近最少使用（LRU）算法来管理缓冲池中页的替换。这确保了频繁访问的页保留在缓冲池中，而不常用的页被逐出。</li>
<li><strong>大小配置</strong>：缓冲池的大小是可以配置的。在一个系统中，为了最大化性能，通常会将缓冲池配置为尽可能大（考虑到系统的内存容量）。</li>
<li><strong>多实例</strong>：在有需要的情况下，可以配置多个缓冲池实例，以提高并发访问时的性能。</li>
</ul>
<h3>与查询缓存的区别</h3>
<p>与查询缓存不同，缓冲池不是直接缓存整个查询结果，而是缓存数据和索引的页。这意味着即使数据发生变化，缓冲池仍然可以提供高效的数据访问，而不像查询缓存那样需要频繁地清空和重新填充。</p>
<h2>存储引擎</h2>
<p><code>MySQL</code>支持非常多种存储引擎，我这先列举一些：</p>
<table>
<thead>
<tr>
<th>存储引擎</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>ARCHIVE</td>
<td>用于数据存档（行被插入后不能再修改）</td>
</tr>
<tr>
<td>BLACKHOLE</td>
<td>丢弃写操作，读操作会返回空内容</td>
</tr>
<tr>
<td>CSV</td>
<td>在存储数据时，以逗号分隔各个数据项</td>
</tr>
<tr>
<td>FEDERATED</td>
<td>用来访问远程表</td>
</tr>
<tr>
<td>InnoDB</td>
<td>具备外键支持功能的事务存储引擎</td>
</tr>
<tr>
<td>MEMORY</td>
<td>置于内存的表</td>
</tr>
<tr>
<td>MERGE</td>
<td>用来管理多个MyISAM表构成的表集合</td>
</tr>
<tr>
<td>MyISAM</td>
<td>主要的非事务处理存储引擎</td>
</tr>
<tr>
<td>NDB</td>
<td>MySQL集群专用存储引擎</td>
</tr>
</tbody>
</table>
<p>最常用的就是<code>InnoDB</code>和<code>MyISAM</code>，有时会提一下<code>Memory</code>。其中<code>InnoDB</code>是<code>MySQL</code>默认的存储引擎。</p>
<p>MySQL 支持多种存储引擎，其中 <code>InnoDB</code>、<code>MyISAM</code> 和 <code>Memory</code> 是最常见的三种。每种存储引擎都有其独特的特点和适用场景。</p>
<h3>1. InnoDB</h3>
<h3>特点</h3>
<ul>
<li><strong>支持事务（ACID兼容）</strong>：提供了事务的完整支持，包括提交、回滚和崩溃恢复能力。</li>
<li><strong>行级锁定</strong>：减少了查询之间的锁争用，提高并发性能， 使用的是悲观锁。</li>
<li><strong>外键支持</strong>：允许在表之间定义外键约束。</li>
<li><strong>自动崩溃恢复</strong>：拥有日志记录机制，能够在系统崩溃后恢复数据。</li>
<li><strong>支持MVCC（多版本并发控制）</strong>：提高多用户并发操作的性能。</li>
<li><strong>缓冲池</strong>：有效地缓存数据和索引，提高数据访问速度。</li>
</ul>
<h3>应用场景</h3>
<ul>
<li>适用于需要高可靠性和事务支持的应用。</li>
<li>适合读写混合的高并发场景。</li>
<li>需要外键约束保证数据完整性的应用。</li>
</ul>
<h3>注意事项</h3>
<ul>
<li>相比于 MyISAM，占用更多的磁盘空间和内存。</li>
<li>需要定期优化和备份。</li>
</ul>
<h3>2. MyISAM</h3>
<h3>特点</h3>
<ul>
<li><strong>表级锁</strong>：锁定整个表，适用于读多写少的场景。</li>
<li><strong>不支持事务</strong>：不支持 ACID 事务处理。</li>
<li><strong>高速读取</strong>：优化了读取操作，提供了快速的读取能力。</li>
<li><strong>全文索引支持</strong>：支持全文索引，适合文本搜索。</li>
</ul>
<h3>应用场景</h3>
<ul>
<li>适用于读操作远多于写操作的应用。</li>
<li>
<p>适合静态或者几乎不变的数据，如日志记录。</p>
<ul>
<li><strong>快速的插入性能</strong>：MyISAM 在处理顺序插入操作时非常高效，这是日志记录的常见场景。</li>
<li><strong>读写分离</strong>：日志记录通常是写入密集型的，但读取操作不会那么频繁。在许多应用中，日志数据经常是写入后很少读取，或者仅在需要时才读取，这减少了表级锁的影响。</li>
<li><strong>简单的数据结构</strong>：日志数据通常具有简单的数据结构，这使得 MyISAM 对其的处理更为高效。</li>
<li><strong>磁盘空间和内存使用效率</strong>：MyISAM 相比于 InnoDB 更节省磁盘空间和内存资源，对于存储大量日志数据来说，这是一个优势。</li>
<li><strong>全文索引支持</strong>：MyISAM 提供全文索引支持，这在需要对日志数据进行全文搜索时非常有用。</li>
</ul>
<p>然而，也有一些注意事项：</p>
<ul>
<li><strong>数据安全性</strong>：MyISAM 不支持事务，也没有崩溃恢复能力，这意味着在系统崩溃的情况下可能会丢失数据。</li>
<li><strong>并发写入</strong>：在高并发写入的场景下，MyISAM 的表级锁可能会成为瓶颈。</li>
</ul>
</li>
<li>全文搜索的场景。</li>
</ul>
<h3>注意事项</h3>
<ul>
<li>数据安全性较低，不支持事务和崩溃后的自动恢复。</li>
<li>高并发写入时性能下降。</li>
</ul>
<h3>3. Memory</h3>
<h3>特点</h3>
<ul>
<li><strong>数据存储在内存中</strong>：所有数据和索引都存储在内存中。</li>
<li><strong>表级锁</strong>：使用表级锁定。</li>
<li><strong>快速访问速度</strong>：内存存储提供了非常快的访问速度。</li>
<li><strong>数据的非持久性</strong>：服务器崩溃或重启会导致数据丢失。</li>
</ul>
<h3>应用场景</h3>
<ul>
<li>适合临时数据存储，如会话信息、临时计算结果等。</li>
<li>用于高速缓存，减少对磁盘的访问。</li>
</ul>
<h3>注意事项</h3>
<ul>
<li>数据非持久性，不适合存储重要数据。</li>
<li>内存限制，不适合大量数据存储。</li>
<li>高并发写入可能导致性能瓶颈。</li>
</ul>
<h3>结论</h3>
<ul>
<li><strong>InnoDB</strong> 是最通用的选择，适用于大多数需要高可靠性和事务支持的场景。</li>
<li><strong>MyISAM</strong> 适用于读密集型应用，但在数据安全性和并发写入方面存在限制。</li>
<li><strong>Memory</strong> 引擎适用于临时数据存储和高速缓存，但要注意数据的非持久性。</li>
</ul>
<p>根据应用的具体需求选择合适的存储引擎非常关键。通常在数据安全性、性能和功能需求之间需要做出权衡。</p>
<h2>乐观并发控制</h2>
<h3>主要步骤</h3>
<ol>
<li><strong>读取阶段</strong>：事务开始时，它会读取并处理数据，但不会立即锁定数据。事务在这个阶段执行其所有的读取和计算工作。</li>
<li><strong>验证阶段</strong>：在事务准备提交更改时，会进行冲突检测。这个阶段会检查在读取阶段之后是否有其他事务修改了相同的数据。</li>
<li><strong>提交/回滚阶段</strong>：如果验证阶段检测到冲突（即其他事务已经修改了数据），当前事务会被回滚。如果没有冲突，事务则成功提交其更改。</li>
</ol>
<h3>特点</h3>
<ul>
<li><strong>高并发</strong>：由于在大部分时间不需要锁定资源，乐观并发控制允许更高水平的并发。</li>
<li><strong>冲突处理</strong>：在提交阶段处理冲突，而不是在每次数据访问时处理，这减少了锁的需求和管理开销。</li>
<li><strong>适用场景</strong>：最适合读多写少的应用场景，因为写操作的冲突可能导致事务回滚，从而影响性能。</li>
<li><strong>版本控制</strong>：乐观并发控制通常与数据版本控制结合使用，以追踪数据的变化。</li>
<li><strong>回滚可能性</strong>：在高冲突的环境下，乐观并发控制可能导致较高的事务回滚率，因为每当有冲突发生时，事务就需要回滚并重试。</li>
</ul>
<p>乐观并发控制是一种有效的并发处理方法，特别适合那些读操作频繁而写操作相对较少的应用场景。在这些场景中，它可以减少锁的开销，提高系统的整体性能和吞吐量。然而，它也要求系统能够有效地处理事务的回滚和重试。</p>
<h2>四个隔离级别</h2>
<h3>1. 原子性（Atomicity）</h3>
<ul>
<li><strong>定义</strong>：原子性意味着事务中的所有操作要么全部完成，要么全部不完成。事务是不可分割的最小操作单位。</li>
<li><strong>隔离级别关系</strong>：隔离级别不直接影响原子性。原子性主要通过数据库管理系统的事务日志等机制实现。</li>
</ul>
<h3>2. 一致性（Consistency）</h3>
<ul>
<li><strong>定义</strong>：一致性确保事务完成后数据库从一个正确的状态转移到另一个正确的状态。</li>
<li><strong>隔离级别关系</strong>：隔离级别影响事务如何查看和修改数据，从而间接影响数据库的整体一致性。例如，较低的隔离级别可能允许一些不一致的现象（如脏读或不可重复读），而较高的隔离级别（如串行化）则提供更强的数据一致性保障。</li>
</ul>
<h3>3. 隔离性（Isolation）</h3>
<ul>
<li><strong>定义</strong>：隔离性是指事务的操作和效果必须独立于其他事务。</li>
<li><strong>隔离级别关系</strong>：这是四个隔离级别最直接相关的ACID特性。每个隔离级别定义了事务之间可接受的交互程度，从而控制并发事务可能引发的问题（如脏读、不可重复读、幻读）。</li>
</ul>
<h3>4. 持久性（Durability）</h3>
<ul>
<li><strong>定义</strong>：一旦事务提交，其所做的更改就会永久保存到数据库中，即使发生系统故障也不会丢失。</li>
<li><strong>隔离级别关系</strong>：隔离级别不直接影响持久性。持久性通常通过数据库的日志和恢复机制实现，确保已提交事务的更改即使在系统崩溃后也能恢复。</li>
</ul>
<h2>MYSQL的字符集编码</h2>
<h3>MySQL中的 utf8（实际上是 utf8mb3）</h3>
<ul>
<li><strong>定义</strong>：在 MySQL 中，<code>utf8</code> 实际上是 <code>utf8mb3</code> 的别名。它是一个“阉割版”的 UTF-8 实现，只能使用 1 至 3 个字节来表示每个字符。</li>
<li><strong>限制</strong>：由于只使用最多 3 个字节，<code>utf8</code>（或 <code>utf8mb3</code>）无法表示所有 UTF-8 字符。特别是，它无法表示 4 字节的字符，如某些特殊符号、表情符号（emoji）或某些罕见的文字。</li>
<li><strong>兼容性</strong>：这种字符集的设计最初是为了与早期的 MySQL 版本兼容，那时 UTF-8 的实现还不够完整。</li>
</ul>
<h3>MySQL中的 utf8mb4</h3>
<ul>
<li><strong>定义</strong>：<code>utf8mb4</code> 是 MySQL 中对完整 UTF-8 编码的支持。它能够使用 1 至 4 个字节来表示每个字符，覆盖了 Unicode 标准中的所有字符。</li>
<li><strong>优点</strong>：<code>utf8mb4</code> 能够存储任何 Unicode 字符，包括所有语言的字符和符号、表情符号等。</li>
<li><strong>推荐使用</strong>：对于需要支持多语言或特殊字符（如表情符号）的应用，推荐使用 <code>utf8mb4</code>。从 MySQL 5.5.3 版本开始，<code>utf8mb4</code> 可用并被推荐为默认字符集。</li>
</ul>
<h3>CHAR与VARCHAR</h3>
<h3>CHAR</h3>
<ul>
<li><strong>固定长度</strong>：<code>CHAR</code> 是一个固定长度的字符串。在创建表时，你需要定义一个长度，例如 <code>CHAR(10)</code>。不管实际存储的字符串长度是多少，<code>CHAR</code> 类型的字段总是使用固定的长度。</li>
<li><strong>空格填充</strong>：如果存储的字符串长度小于定义的长度，MySQL 会用空格来填充这个字段，以达到定义的长度。当从 <code>CHAR</code> 类型字段读取数据时，尾随的空格会被移除。</li>
<li><strong>性能</strong>：由于固定长度，<code>CHAR</code> 类型的读取速度通常比 <code>VARCHAR</code> 快，特别适合存储长度几乎相同的数据，例如某些代码、状态值等。</li>
<li><strong>空间使用</strong>：可能会浪费存储空间，尤其是当字段中存储的字符串远小于定义长度时。</li>
</ul>
<h3>VARCHAR</h3>
<ul>
<li><strong>可变长度</strong>：<code>VARCHAR</code> 是一个可变长度的字符串。定义时同样需要指定最大长度，例如 <code>VARCHAR(255)</code>。但它只会使用必要的空间来存储实际的字符串，并在每个字符串值前使用额外的字节来记录字符串的长度。</li>
<li><strong>灵活的空间使用</strong>：对于长度不一的字符串，<code>VARCHAR</code> 更加高效，因为它只占用必要的空间。</li>
<li><strong>长度限制</strong>：<code>VARCHAR</code> 的最大长度取决于字符集和MySQL的配置，但最大不超过 65,535 字节（包括记录长度的字节）。</li>
<li><strong>尾随空格保留</strong>：与 <code>CHAR</code> 不同，<code>VARCHAR</code> 在存储时保留字符串尾部的空格。</li>
<li>如果超过了varchar定义的长度，需要修改表结构才可以存储</li>
</ul>
<h3>使用建议</h3>
<ul>
<li><strong>长度稳定的数据</strong>：如果字段值的长度几乎总是固定的，或者长度变化不大，使用 <code>CHAR</code>。</li>
<li><strong>长度可变的数据</strong>：对于长度有较大变化的数据，例如文本、描述性字段等，使用 <code>VARCHAR</code>。</li>
</ul>
<h3>注意事项</h3>
<ul>
<li><strong>字符集影响</strong>：实际存储所需的空间还受字符集的影响，因为不同的字符集可能需要不同数量的字节来存储一个字符。</li>
<li><strong>性能考量</strong>：尽管 <code>CHAR</code> 在某些情况下读取速度更快，但这种性能优势在现代的数据库系统中可能不是非常显著，特别是考虑到存储空间的有效利用。</li>
</ul>
<h3>字符集设置</h3>
<table>
<thead>
<tr>
<th>系统变量</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>character_set_client</td>
<td>服务器解码请求时使用的字符集</td>
</tr>
<tr>
<td>character_set_connection</td>
<td>服务器处理请求时会把请求字符串从character_set_client转为character_set_connection</td>
</tr>
<tr>
<td>character_set_results</td>
<td>服务器向客户端返回数据时使用的字符集</td>
</tr>
</tbody>
</table>
<p>知道这三个字符集设置就好，在实际使用中把这三个设置成统一字符集，减少转换的系统损耗，默认他们三个都是</p>
<p><code>utf8mb4</code>。</p>
<p>在 MySQL 中，对中文进行升序（ASC）排序的行为取决于该列使用的字符集和排序规则（collation）。MySQL 支持多种字符集和排序规则，其中一些专门针对中文设计。这些排序规则影响了如何比较和排序字符串，包括中文字符。</p>
<h3>字符集和排序规则</h3>
<ol>
<li><strong>字符集（Character Set）</strong>：定义了可以在列中存储的字符类型。对于中文，常用的字符集是 <code>utf8</code> 和 <code>utf8mb4</code>。</li>
<li><strong>排序规则（Collation）</strong>：决定了如何比较字符。对于中文，MySQL 提供了多种针对不同语言习惯的排序规则，例如 <code>utf8_general_ci</code>、<code>utf8mb4_chinese_ci</code> 等。</li>
</ol>
<h3>中文排序</h3>
<p>当你对含有中文的列进行升序排序时，具体的排序行为会基于该列的排序规则。例如：</p>
<ul>
<li>使用 <code>utf8_general_ci</code> 或 <code>utf8mb4_general_ci</code>：这些是通用的排序规则，对于多语言的支持较好，但可能不完全符合特定语言的排序习惯。</li>
<li>使用 <code>utf8mb4_chinese_ci</code> 或类似针对中文的排序规则：这些排序规则会更加符合中文的排序习惯，可能基于拼音、笔画等进行排序。</li>
</ul>
<h3>示例</h3>
<p>假设你有一个中文字符串的列，你可以使用 SQL 查询进行排序，如下：</p>
<pre><code class="language-sql">SELECT * FROM your_table ORDER BY your_chinese_column ASC;</code></pre>
<p>在这个查询中，<code>your_chinese_column</code> 是包含中文字符串的列名，<code>ASC</code> 指定了升序排序。排序的具体结果将取决于该列的字符集和排序规则设置。</p>
<h3>注意事项</h3>
<ul>
<li><strong>字符集兼容性</strong>：确保使用的字符集支持中文（如 <code>utf8mb4</code>），因为一些旧的字符集（如 <code>utf8</code>）可能无法正确存储所有中文字符。</li>
<li><strong>性能考虑</strong>：使用特定的排序规则（如针对中文的）可能会对性能有一定影响，特别是在处理大量数据时。</li>
<li><strong>数据库版本</strong>：不同版本的 MySQL 可能支持不同的字符集和排序规则，所以要根据你使用的 MySQL 版本来确定可用的选项。</li>
</ul>
<h2>InnoDB 页结构</h2>
<p><code>InnoDB</code>是一个将表中的数据存储到磁盘上的存储引擎，所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的，所以需要把磁盘中的数据加载到内存中，如果是处理写入或修改请求的话，还需要把内存中的内容刷新到磁盘上。</p>
<p>将数据划分为若干个页，以页作为磁盘和内存之间交互的基本单位，InnoDB中页的大小一般为 16KB</p>
<h3>数据存储方式</h3>
<ol>
<li><strong>记录的组织</strong>：在InnoDB的数据页中，实际的数据记录（User Records）是根据主键顺序存储的。如果表没有显式定义主键，InnoDB会自动生成一个隐藏的主键来维护记录顺序。</li>
<li><strong>链表结构</strong>：每个记录在内部都有一个“next_record”指针，指向主键值下一个更大的记录，形成一个单向链表。这种组织方式便于快速按主键顺序访问记录。</li>
<li><strong>Infimum 和 Supremum 记录</strong>：数据页中包含两个特殊的虚拟记录，Infimum（最小记录）和Supremum（最大记录），它们分别代表页中最小和最大的边界值。这两个记录帮助维护页内部记录的有序性。</li>
</ol>
<h3>页目录槽（Page Directory Slots）</h3>
<ol>
<li><strong>作用</strong>：页目录槽的主要作用是提高基于主键的记录查找效率。它是数据页中的一个快速索引，用于快速定位记录。</li>
<li><strong>槽的组成</strong>：每个槽指向页内部的一个“关键记录”（通常是每个组的最大记录）。这些关键记录将页内的记录分成多个小组，每个组包含一定数量的记录。</li>
<li><strong>查找流程</strong>：
<ul>
<li>首先使用二分查找方法在页目录槽中快速定位到目标记录所在的组。</li>
<li>然后在该组内部使用链表结构顺序遍历，直到找到目标记录。</li>
</ul>
</li>
<li><strong>效率提升</strong>：这种结构大大减少了需要遍历的记录数量，尤其是在大型数据页中，能有效提高查询性能。</li>
</ol>
<h3>数据查询过程</h3>
<p>数据页可以组成一个<code>双向链表</code>，而每个数据页中的记录会按照主键值从小到大的顺序组成一个<code>单向链表</code>，每个数据页都会为存储在它里边儿的记录生成一个<code>页目录</code>，在通过主键查找某条记录的时候可以在<code>页目录</code>中使用二分法快速定位到对应的槽，然后再遍历该槽对应分组中的记录即可快速找到指定的记录</p>
<h2>InnoDB表对主键的生成策略</h2>
<p>优先使用用户自定义主键作为主键，如果用户没有定义主键，则选取一个<code>Unique</code>键作为主键，如果表中连<code>Unique</code>键都没有定义的话，则<code>InnoDB</code>会为表默认添加一个名为<code>row_id</code>的隐藏列作为主键。</p>
<h2>索引</h2>
<ol>
<li><strong>层级结构</strong>：B+树是一个多层次的树结构，其非叶子节点不存储数据，只存储键值和指向子节点的指针。叶子节点包含了所有键值的信息及其对应的记录指针。</li>
<li><strong>叶子节点的特殊结构</strong>：所有叶子节点之间以双向链表的形式连接，这种结构便于进行顺序访问和范围查询。</li>
<li><strong>高效的范围查询</strong>：由于叶子节点是按键值顺序排列的，并且相互连接，所以B+树非常适合执行范围查询。</li>
<li><strong>查询优化器的作用</strong>：查询优化器可以利用B+树的特性来优化查询计划，例如，它可以决定使用哪个索引来进行查询以及查询的顺序。</li>
<li><strong>磁盘I/O效率</strong>：B+树的结构减少了磁盘I/O的次数，因为索引的查找可以迅速定位到叶子节点，而不需要在树的每一层都进行磁盘访问。</li>
</ol>
<h3>范围查询的例子</h3>
<p>假设有一个名为 <code>Employees</code> 的表，其中有一个列 <code>salary</code> 被加上了索引。现在要查询工资在 5000 到 8000 之间的员工，SQL 查询如下：</p>
<pre><code>sqlCopy code
SELECT * FROM Employees WHERE salary BETWEEN 5000 AND 8000;</code></pre>
<p>在这个查询中，MySQL 会利用 <code>salary</code> 列上的B+树索引：</p>
<ol>
<li>首先在树的上层迅速定位到工资值为 5000 的节点。</li>
<li>由于叶子节点是顺序链接的，MySQL 将顺序遍历叶子节点，直到遇到工资值超过 8000 的记录。</li>
<li>在这个过程中，所有在这个范围内的员工记录都会被检索出来。</li>
</ol>
<p>这种范围查询的效率远高于线性搜索，特别是在大数据集的情况下，B+树索引可以显著减少必要的比较次数和磁盘I/O操作。</p>
<h3>聚簇索引</h3>
<ol>
<li><strong>定义</strong>：聚簇索引并不是一个单独的结构，而是数据表行记录的物理存储顺序。在 InnoDB 中，聚簇索引是基于表的主键构建的。</li>
<li><strong>主键作为索引</strong>：表的主键成为聚簇索引的一部分。如果没有定义主键，InnoDB 会选择唯一的非空列作为聚簇索引，如果这样的列也不存在，InnoDB 将生成一个隐藏的行ID作为聚簇索引。</li>
<li><strong>数据与索引的结合</strong>：在聚簇索引中，索引结构和行数据是紧密结合的。这意味着索引叶子节点直接包含了行数据。</li>
<li><strong>优点</strong>：提供了快速的数据访问能力，因为索引搜索可以直接定位到包含数据的页。</li>
<li><strong>限制</strong>：由于数据按照聚簇索引的顺序存储，因此主键的更新可能导致数据重新排序，影响性能。同时，聚簇索引只能有一个，因为数据只能按一种顺序物理存储。</li>
</ol>
<h3>二级索引（Secondary Index）</h3>
<ol>
<li><strong>定义</strong>：二级索引是除聚簇索引之外的索引。它们可以基于表中的非主键列构建。</li>
<li><strong>独立于数据存储</strong>：二级索引的叶子节点包含索引的键值和指向聚簇索引记录的指针（通常是主键值），而不是实际的行数据。</li>
<li><strong>间接访问</strong>：<strong>通过二级索引检索数据时，首先在二级索引中查找，然后通过索引中的指针去聚簇索引中检索实际的数据。这被称为“回表”。</strong></li>
<li><strong>优点</strong>：允许在不同的列上建立索引，提高这些列上查询和排序操作的效率。</li>
<li><strong>缺点</strong>：由于需要“回表”操作，二级索引的访问通常比聚簇索引慢。此外，维护多个索引会增加数据更新（INSERT、UPDATE、DELETE）操作的成本。</li>
</ol>
<p>在 MySQL 的 InnoDB 存储引擎中，索引主要分为两种类型：聚簇索引（Clustered Index）和二级索引（Secondary Index，也称为非聚簇索引）。这两种索引在数据的存储和访问方式上有显著的区别。</p>
<h3>索引更新代价</h3>
<h3>聚簇索引（一级索引）</h3>
<p>在聚簇索引中，数据实际上存储在索引的叶子节点上。这意味着索引和数据是紧密绑定的。以下是数据变更对聚簇索引的影响：</p>
<ul>
<li><strong>更新索引键值</strong>：如果更新的是索引列本身（通常是主键），这可能是一个成本很高的操作。因为它可能需要移动数据行到新的位置，以保持数据的物理顺序与索引顺序一致。这种情况在聚簇索引中尤其昂贵。</li>
<li><strong>更新非索引列</strong>：如果更新的是非索引列的值，那么只需要修改数据页中的数据，而不需要改变索引结构。</li>
<li><strong>插入和删除操作</strong>：插入和删除可能导致页分裂或合并，因为数据需要在物理上保持有序。插入新数据时，如果新数据的索引键介于两个现有键之间，可能需要重新调整页的内容。</li>
</ul>
<h3>非聚簇索引（二级索引）</h3>
<p>非聚簇索引存储的是索引值和指向数据行的指针（在InnoDB中是指向聚簇索引键的指针）。数据变更对非聚簇索引的影响如下：</p>
<ul>
<li><strong>更新索引列</strong>：如果更新的是非聚簇索引中的某个列的值，这将涉及更新索引中的条目，因为索引值已经改变。这通常比聚簇索引的更新成本低，因为不需要移动实际的数据行。</li>
<li><strong>更新非索引列</strong>：如果更新的是数据行中未被索引的列，那么非聚簇索引不需要任何变更。</li>
<li><strong>插入和删除操作</strong>：插入和删除操作会影响到索引的结构，可能需要在索引中添加或移除条目。由于索引项较小，这些操作通常比聚簇索引中的同等操作更快。</li>
</ul>
<h3>综合比较</h3>
<ul>
<li><strong>写入性能</strong>：通常，聚簇索引的写入性能低于非聚簇索引，因为它可能涉及更多的数据移动和页的重新组织。</li>
<li><strong>维护成本</strong>：维护多个索引意味着更高的维护成本。每当数据变更时，所有相关的索引都需要更新。</li>
<li><strong>性能平衡</strong>：合理的索引策略应当平衡查询性能和写入/更新性能。过多或不必要的索引可能会降低数据库的整体性能。</li>
</ul>
<p><code>B+</code>树索引在空间和时间上都有代价，所以没事儿别瞎建索引。</p>
<h3>B树和B+树的区别</h3>
<p>B树（B-Tree）和B+树都是自平衡的树数据结构，它们被广泛用于数据库和文件系统中。尽管它们有很多共同点，但也存在一些关键的区别：</p>
<ol>
<li><strong>节点结构</strong>:
<ul>
<li><strong>B树</strong>: 每个节点既存储键（key）也存储数据。非叶子节点包含指向子节点的指针。</li>
<li><strong>B+树</strong>: 所有的数据都仅存储在叶子节点中，非叶子节点只存储键。叶子节点包含了所有键的值以及指向记录的指针。</li>
</ul>
</li>
<li><strong>键和数据的存储</strong>:
<ul>
<li><strong>B树</strong>: 键和数据在同一个节点中存储。</li>
<li><strong>B+树</strong>: 数据仅存储在叶子节点中；非叶节点仅用于索引。</li>
</ul>
</li>
<li><strong>叶子节点的链接</strong>:
<ul>
<li><strong>B树</strong>: 叶子节点是独立的，没有相互连接。</li>
<li><strong>B+树</strong>: 所有叶子节点通过指针连接在一起，形成一个有序链表。</li>
</ul>
</li>
<li><strong>空间利用率</strong>:
<ul>
<li><strong>B树</strong>: 比B+树更多地浪费空间，因为每个节点都存储数据。</li>
<li><strong>B+树</strong>: 空间利用率更高，因为它只在叶子节点存储数据。</li>
</ul>
</li>
<li><strong>搜索效率</strong>:
<ul>
<li><strong>B树</strong>: 可能需要在非叶子节点中搜索数据。</li>
<li><strong>B+树</strong>: 所有数据都在叶子节点，因此搜索可能更为高效，尤其是范围搜索。</li>
</ul>
</li>
<li><strong>树的深度</strong>:
<ul>
<li><strong>B树</strong>: 可能更深，因为它在每个节点中存储数据。</li>
<li><strong>B+树</strong>: 通常更浅，因为非叶节点仅存储键。</li>
</ul>
</li>
</ol>
<p>总结来说，B+树通过仅在叶子节点存储数据并将它们链接起来，提高了范围搜索的效率并且更有效地利用空间。B树的结构则更为简单，但可能在空间利用和搜索效率方面略逊一筹。在实际应用中，B+树因其高效的范围搜索能力和较低的磁盘I/O需求，被广泛用于数据库索引结构。</p>
<h2>如何减少二级索引回表？</h2>
<p>减少二级索引（Secondary Index）的回表操作是提高数据库查询性能的关键。回表指的是在使用二级索引找到数据行的主键后，需要再次查询聚簇索引（Clustered Index）来获取完整的数据行。这个过程在某些情况下可能相当耗时，特别是在涉及大量数据的复杂查询中。以下是一些设计技巧，可以帮助减少回表操作：</p>
<h3>1. 使用覆盖索引</h3>
<ul>
<li><strong>定义</strong>：覆盖索引是指一个索引包含了查询中需要的所有字段。</li>
<li><strong>应用</strong>：如果一个查询可以完全通过二级索引来获取所需的数据，那么就不需要进行回表操作。</li>
<li><strong>实践</strong>：在创建二级索引时，考虑包括所有常用查询中需要的字段。</li>
</ul>
<h3>2. 索引选择性优化</h3>
<ul>
<li><strong>定义</strong>：索引的选择性是指索引中不重复项的比例。</li>
<li><strong>应用</strong>：高选择性的索引可以减少匹配给定索引值的数据行数量，从而减少需要回表的次数。</li>
<li><strong>实践</strong>：为具有高唯一值的列创建索引，如用户ID、电子邮件地址等。</li>
</ul>
<h3>3. 索引列顺序</h3>
<ul>
<li><strong>考虑</strong>：索引列的顺序对于查询性能有重要影响。</li>
<li><strong>实践</strong>：在创建复合索引时，将最常用于过滤条件的列放在索引的前面。</li>
</ul>
<h3>4. 减少索引列的宽度</h3>
<ul>
<li><strong>影响</strong>：索引的宽度直接影响其大小和效率。</li>
<li><strong>实践</strong>：使用尽可能短的数据类型，避免在索引中包含长字符串等大型数据。</li>
</ul>
<h3>5. 避免不必要的索引</h3>
<ul>
<li><strong>问题</strong>：过多的索引会增加写操作的负担，可能导致查询优化器选择不理想的索引。</li>
<li><strong>实践</strong>：定期审查和清理不常用或重复的索引。</li>
</ul>
<h3>6. 查询优化</h3>
<ul>
<li><strong>技巧</strong>：优化查询逻辑，减少需要的数据量。</li>
<li><strong>实践</strong>：使用合适的WHERE子句过滤条件，减少结果集的大小。</li>
</ul>
<h3>7. 分区策略</h3>
<ul>
<li><strong>定义</strong>：数据分区是将表中的数据分散存储在不同的部分。</li>
<li><strong>实践</strong>：对于非常大的表，考虑使用分区来改善性能。</li>
</ul>
<h3>8. 使用缓存</h3>
<ul>
<li><strong>策略</strong>：适当使用查询缓存或应用层缓存，可以减少数据库的访问次数。</li>
<li><strong>实践</strong>：缓存常见查询的结果，减少数据库的负担。</li>
</ul>
<p>通过应用这些设计技巧，可以有效地减少回表操作的次数，从而提高数据库的查询效率和整体性能。这些技巧在处理大量数据和复杂查询的大型数据库系统中尤为重要。</p>
<h2>如何选择索引？</h2>
<p>详细内容看这篇：<a href="https://relph1119.github.io/mysql-learning-notes/#/mysql/07-%E5%A5%BD%E4%B8%9C%E8%A5%BF%E4%B9%9F%E5%BE%97%E5%85%88%E5%AD%A6%E4%BC%9A%E6%80%8E%E4%B9%88%E7%94%A8-B+%E6%A0%91%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%BF%E7%94%A8" target="_blank" rel="noopener">https://relph1119.github.io/mysql-learning-notes/#/mysql/07-%E5%A5%BD%E4%B8%9C%E8%A5%BF%E4%B9%9F%E5%BE%97%E5%85%88%E5%AD%A6%E4%BC%9A%E6%80%8E%E4%B9%88%E7%94%A8-B+%E6%A0%91%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%BF%E7%94%A8</a></p>
<h3><code>B+</code>树索引适用于下面这些情况：</h3>
<ul>
<li>全值匹配</li>
<li>匹配左边的列</li>
<li>匹配范围值</li>
<li>精确匹配某一列并范围匹配另外一列</li>
<li>用于排序</li>
<li>用于分组</li>
</ul>
<h3>在使用索引时需要注意下面这些事项：</h3>
<ul>
<li>只为用于搜索、排序或分组的列创建索引</li>
<li>为列的基数大的列创建索引</li>
<li>索引列的类型尽量小</li>
<li>可以只对字符串值的前缀建立索引</li>
<li>只有索引列在比较表达式中单独出现才可以适用索引</li>
<li>为了尽可能少的让<code>聚簇索引</code>发生页面分裂和记录移位的情况，建议让主键拥有<code>AUTO_INCREMENT</code>属性。</li>
<li>定位并删除表中的重复和冗余索引</li>
<li>尽量使用<code>覆盖索引</code>进行查询，避免<code>回表</code>带来的性能损耗。</li>
</ul>
<h3>覆盖索引</h3>
<p>覆盖索引是指一个索引包含了查询所需的所有数据。换句话说，如果一个查询能够仅通过访问索引来获取所需的所有列信息，那么这个索引就被称为覆盖索引。</p>
<p>所以常见的被查询的数据可以放到索引里，这样就不用回表了。</p>
<h3>无法利用索引的情况</h3>
<ol>
<li>ASC、DESC 混用
<ul>
<li><strong>情况</strong>：在使用联合索引进行排序时，若各排序列的顺序不一致（如一部分列使用ASC，另一部分使用DESC），则无法有效利用索引。</li>
<li><strong>原因</strong>：这是因为B+树索引的结构决定了它只能以一致的顺序（全部升序或全部降序）高效地遍历数据。</li>
</ul>
</li>
<li>WHERE子句中出现非排序使用到的索引列
<ul>
<li><strong>情况</strong>：如果WHERE子句中使用了与ORDER BY子句不同的列，且这些列不是索引的一部分，那么无法利用索引进行排序。</li>
<li><strong>原因</strong>：SQL首先需要过滤出符合WHERE条件的记录，这一步无法通过索引完成，因此排序也无法利用索引。</li>
</ul>
</li>
<li>排序列包含非同一个索引的列
<ul>
<li><strong>情况</strong>：当用于排序的列不属于同一个联合索引时，索引无法用于排序。</li>
<li><strong>原因</strong>：一个索引只能按其包含的列顺序进行排序，无法跨索引进行操作。</li>
</ul>
</li>
<li>排序列使用了复杂的表达式
<ul>
<li><strong>情况</strong>：如果排序列使用了函数或计算表达式（如<code>UPPER(name)</code>），则无法利用索引进行排序。</li>
<li><strong>原因</strong>：索引是基于列的实际值建立的，不适用于修改后的值。</li>
</ul>
</li>
<li>用于分组
<ul>
<li><strong>情况</strong>：当使用GROUP BY进行分组统计时，如果分组的列顺序与索引列不一致，或者分组的列不是索引的一部分，则无法利用索引优化。</li>
<li><strong>原因</strong>：分组操作的优化类似于排序，依赖于索引列的顺序。</li>
</ul>
</li>
</ol>
<h2>查询过程</h2>
<p>对于单个表的查询来说，MySQL执行方式大致分为下面两种：</p>
<ul>
<li>
<p>使用全表扫描进行查询</p>
<p>这种执行方式很好理解，就是把表的每一行记录都扫一遍嘛，把符合搜索条件的记录加入到结果集就完了。不管是什么查询都可以使用这种方式执行，当然，这种也是最笨的执行方式。</p>
</li>
<li>
<p>使用索引进行查询</p>
<p>因为直接使用全表扫描的方式执行查询要遍历好多记录，所以代价可能太大了。如果查询语句中的搜索条件可以使用到某个索引，那直接使用索引来执行查询可能会加快查询执行的时间。使用索引来执行查询的方式五花八门，又可以细分为许多种类：</p>
<ul>
<li>针对主键或唯一二级索引的等值查询</li>
<li>针对普通二级索引的等值查询</li>
<li>针对索引列的范围查询</li>
<li>直接扫描整个索引</li>
</ul>
</li>
</ul>
<p>MySQl执行查询语句的过程叫访问方法，有很多种</p>
<h3>const</h3>
<p>通过聚簇索引或者二级索引定位一条记录的方法叫做const</p>
<pre><code class="language-sql">SELECT * FROM single_table WHERE key2 = 3841;</code></pre>
<h3>ref</h3>
<p>搜索条件为二级索引列与常数等值比较，采用二级索引来执行查询的访问方法称为：<code>ref</code>。普通二级索引并不限制索引列值的唯一性，所以可能找到多条对应的记录，也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。</p>
<pre><code class="language-sql">SELECT * FROM single_table WHERE key1 = &#039;abc&#039;;</code></pre>
<h3>ref_or_null</h3>
<p>有时候我们不仅想找出某个二级索引列的值等于某个常数的记录，还想把该列的值为<code>NULL</code>的记录也找出来，就像下面这个查询：</p>
<pre><code class="language-sql">SELECT * FROM single_demo WHERE key1 = &#039;abc&#039; OR key1 IS NULL;</code></pre>
<p>MySQL 首先使用索引找到所有与给定值相等的行，然后再次使用相同的索引找到所有为 NULL 的行。</p>
<h3>range</h3>
<p>利用索引进行范围匹配的访问方法称之为：<code>range</code></p>
<pre><code class="language-sql">SELECT * FROM single_table WHERE key2 IN (1438, 6328) OR (key2 &gt;= 38 AND key2 &lt;= 79);</code></pre>
<h3>index</h3>
<pre><code>SELECT key_part1, key_part2, key_part3 FROM single_table WHERE key_part2 = &#039;abc&#039;;Copy to clipboardErrorCopied</code></pre>
<p>由于<code>key_part2</code>并不是联合索引<code>idx_key_part</code>最左索引列，所以我们无法使用<code>ref</code>或者<code>range</code>访问方法来执行这个语句。但是这个查询符合下面这两个条件：</p>
<ul>
<li>它的查询列表只有3个列：<code>key_part1</code>, <code>key_part2</code>, <code>key_part3</code>，而索引<code>idx_key_part</code>又包含这三个列。</li>
<li>搜索条件中只有<code>key_part2</code>列。这个列也包含在索引<code>idx_key_part</code>中。</li>
</ul>
<p>也就是说我们可以直接通过遍历<code>idx_key_part</code>索引的叶子节点的记录来比较<code>key_part2 = &#039;abc&#039;</code>这个条件是否成立，把匹配成功的二级索引记录的<code>key_part1</code>, <code>key_part2</code>, <code>key_part3</code>列的值直接加到结果集中就行了。由于二级索引记录比聚簇索记录小的多（聚簇索引记录要存储所有用户定义的列以及所谓的隐藏列，而二级索引记录只需要存放索引列和主键），而且这个过程也不用进行回表操作，所以直接遍历二级索引比直接遍历聚簇索引的成本要小很多，设计<code>MySQL</code>的大佬就把这种采用遍历二级索引记录的执行方式称之为：<code>index</code>。</p>
<p>这个属于覆盖索引</p>
<h3>all</h3>
<p>最直接的查询执行方式就是我们已经提了无数遍的全表扫描，对于<code>InnoDB</code>表来说也就是直接扫描聚簇索引，设计<code>MySQL</code>的大佬把这种使用全表扫描执行查询的方式称之为：<code>all</code>。</p>
<h2>事务与隔离级别</h2>
<p>事务是一个执行单元，在这个执行单元中的所有操作要么全部完成，要么全部不完成。</p>
<h3>事务的ACID特性：</h3>
<ol>
<li><strong>原子性（Atomicity）</strong>：事务被视为最小的不可分割的工作单位，事务中的所有操作要么全部完成，要么全部不发生。</li>
<li><strong>一致性（Consistency）</strong>：事务必须确保数据库从一个一致性状态转移到另一个一致性状态，即事务的执行结果必须使数据库从一个正确的状态变到另一个正确的状态。</li>
<li><strong>隔离性（Isolation）</strong>：一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的，并发执行的各个事务之间不会相互干扰。</li>
<li><strong>持久性（Durability）</strong>：一旦事务提交，则其所做的修改将永久保存在数据库中，即使系统崩溃也不会丢失。</li>
</ol>
<p>如果严格遵守事务的ACID特性，事务的处理只能是串行化了，性能会很差。所以为了性能出现了不严格遵守ACID的事务，但他们要根据场景不同遵守不同的隔离级别</p>
<h3>SQL标准中设立了4个隔离级别：</h3>
<ul>
<li><strong>READ UNCOMMITTED</strong>：这是最低的隔离级别，允许读取未提交的数据（可能导致脏读）。</li>
<li><strong>READ COMMITTED</strong>：只能读取已提交的数据。这个级别可以避免脏读，但不能防止不可重复读。</li>
<li><strong>REPEATABLE READ</strong>：保证在同一个事务中多次读取同一数据的结果是一致的，避免了不可重复读，但可能出现幻读。</li>
<li><strong>SERIALIZABLE</strong>：最高的隔离级别，完全遵守ACID原则，防止脏读、不可重复读和幻读，但性能损耗最大。</li>
</ul>
<p>这四个隔离级别是要解决下面的问题：</p>
<table>
<thead>
<tr>
<th>隔离级别</th>
<th>脏读</th>
<th>不可重复读</th>
<th>幻读</th>
</tr>
</thead>
<tbody>
<tr>
<td>READ UNCOMMITTED</td>
<td>Possible</td>
<td>Possible</td>
<td>Possible</td>
</tr>
<tr>
<td>READ COMMITTED</td>
<td>Not Possible</td>
<td>Possible</td>
<td>Possible</td>
</tr>
<tr>
<td>REPEATABLE READ</td>
<td>Not Possible</td>
<td>Not Possible</td>
<td>Possible</td>
</tr>
<tr>
<td>SERIALIZABLE</td>
<td>Not Possible</td>
<td>Not Possible</td>
<td>Not Possible</td>
</tr>
</tbody>
</table>
<p>在这四个之外还有脏写，脏写这个问题太严重了，不论是哪种隔离级别，都不允许脏写的情况发生。</p>
<h3>事务并发执行遇到的问题</h3>
<h3>脏写（Dirty Write）</h3>
<p>如果一个事务修改了另一个未提交事务修改过的数据，那就意味着发生了<code>脏写</code>，示意图如下：</p>
<p>!<a href="https://markdown.cztcode.com/2bca8a45e9d08db8dd467424d73f8360.png" target="_blank" rel="noopener">https://markdown.cztcode.com/2bca8a45e9d08db8dd467424d73f8360.png</a></p>
<p>img</p>
<p>如上图，<code>Session A</code>和<code>Session B</code>各开启了一个事务，<code>Session B</code>中的事务先将<code>number</code>列为<code>1</code>的记录的<code>name</code>列更新为<code>&#039;关羽&#039;</code>，然后<code>Session A</code>中的事务接着又把这条<code>number</code>列为<code>1</code>的记录的<code>name</code>列更新为<code>张飞</code>。如果之后<code>Session B</code>中的事务进行了回滚，那么<code>Session A</code>中的更新也将不复存在，这种现象就称之为<code>脏写</code>。这时<code>Session A</code>中的事务就很懵逼，我明明把数据更新了，最后也提交事务了，怎么到最后说自己什么也没干呢？</p>
<h3>脏读（Dirty Read）</h3>
<p>如果一个事务读到了另一个未提交事务修改过的数据，那就意味着发生了<code>脏读</code>，示意图如下：</p>
<p>!<a href="https://relph1119.github.io/mysql-learning-notes/images/24-02.png" target="_blank" rel="noopener">https://relph1119.github.io/mysql-learning-notes/images/24-02.png</a></p>
<p>img</p>
<p>如上图，<code>Session A</code>和<code>Session B</code>各开启了一个事务，<code>Session B</code>中的事务先将<code>number</code>列为<code>1</code>的记录的<code>name</code>列更新为<code>&#039;关羽&#039;</code>，然后<code>Session A</code>中的事务再去查询这条<code>number</code>为<code>1</code>的记录，如果du到列<code>name</code>的值为<code>&#039;关羽&#039;</code>，而<code>Session B</code>中的事务稍后进行了回滚，那么<code>Session A</code>中的事务相当于读到了一个不存在的数据，这种现象就称之为<code>脏读</code>。</p>
<h3>不可重复读（Non-Repeatable Read）</h3>
<p>如果一个事务只能读到另一个已经提交的事务修改过的数据，并且其他事务每对该数据进行一次修改并提交后，该事务都能查询得到最新值，那就意味着发生了<code>不可重复读</code>，示意图如下：</p>
<p>!<a href="https://relph1119.github.io/mysql-learning-notes/images/24-03.png" target="_blank" rel="noopener">https://relph1119.github.io/mysql-learning-notes/images/24-03.png</a></p>
<p>img</p>
<p>如上图，我们在<code>Session B</code>中提交了几个隐式事务（注意是隐式事务，意味着语句结束事务就提交了），这些事务都修改了<code>number</code>列为<code>1</code>的记录的列<code>name</code>的值，每次事务提交之后，如果<code>Session A</code>中的事务都可以查看到最新的值，这种现象也被称之为<code>不可重复读</code>。</p>
<h3>幻读（Phantom）</h3>
<p>如果一个事务先根据某些条件查询出一些记录，之后另一个事务又向表中插入了符合这些条件的记录，原先的事务再次按照该条件查询时，能把另一个事务插入的记录也读出来，那就意味着发生了<code>幻读</code>，示意图如下：</p>
<p>!<a href="https://relph1119.github.io/mysql-learning-notes/images/24-04.png" target="_blank" rel="noopener">https://relph1119.github.io/mysql-learning-notes/images/24-04.png</a></p>
<p>如上图，<code>Session A</code>中的事务先根据条件<code>number &gt; 0</code>这个条件查询表<code>hero</code>，得到了<code>name</code>列值为<code>&#039;刘备&#039;</code>的记录；之后<code>Session B</code>中提交了一个隐式事务，该事务向表<code>hero</code>中插入了一条新记录；之后<code>Session A</code>中的事务再根据相同的条件<code>number &gt; 0</code>查询表<code>hero</code>，得到的结果集中包含<code>Session B</code>中的事务新插入的那条记录，这种现象也被称之为<code>幻读</code>。</p>
<p>有的同学会有疑问，那如果<code>Session B</code>中是删除了一些符合<code>number &gt; 0</code>的记录而不是插入新记录，那<code>Session A</code>中之后再根据<code>number &gt; 0</code>的条件读取的记录变少了，这种现象算不算<code>幻读</code>呢？明确说一下，这种现象不属于<code>幻读</code>，<code>幻读</code>强调的是一个事务按照某个相同条件多次读取记录时，后读取时读到了之前没有读到的记录。</p>
<p>那对于先前已经读到的记录，之后又读取不到这种情况，算什么呢？其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。</p>
<p><code>MySQL</code>虽然支持4种隔离级别，但与<code>SQL标准</code>中所规定的各级隔离级别允许发生的问题却有些出入，MySQL在REPEATABLE READ隔离级别下，是可以禁止幻读问题的发生的（间隙锁）。</p>
<h2>MVCC（多版本并发控制）</h2>
<p>MVCC（多版本并发控制）是一种用于数据库管理系统的并发控制的方法。它允许多个事务同时对数据库进行读写操作，而不会互相干扰。MVCC 的核心思想是为数据库中的每个数据项保持不同版本的历史信息，这样不同的事务可以看到同一数据的不同快照。这种机制使得读操作不会阻塞写操作，写操作也不会阻塞读操作，从而大大提高了数据库系统的并发性能。</p>
<p>MVCC 的工作原理概述如下：</p>
<ol>
<li><strong>版本创建</strong>：当事务对数据进行更新操作时，系统会创建该数据的新版本，而不是覆盖旧版本。这意味着每个数据项可能有多个版本。</li>
<li><strong>事务时间戳</strong>：每个事务被分配一个唯一的时间戳（或ID）。时间戳用于决定事务能看到哪些数据版本。</li>
<li><strong>读操作</strong>：当事务读取数据时，它会看到在该事务开始之前最新的数据版本。这意味着读操作可以无视正在进行的写操作，实现非阻塞读。</li>
<li><strong>写操作</strong>：当事务写入数据时，它会创建数据的新版本。这个新版本只能被时间戳晚于当前事务的其他事务看到。</li>
<li><strong>可见性规则</strong>：数据库使用时间戳或事务ID来判断数据版本对特定事务是否可见。</li>
<li><strong>垃圾收集</strong>：随着时间的推移，旧版本的数据可能不再被任何事务需要，因此系统会定期清理这些不再需要的数据版本。</li>
</ol>
<p>MVCC 的优势包括减少锁的需求，提高系统的并发能力，以及减少读-写冲突。但它也有一些缺点，比如可能增加存储开销（因为要存储多个版本的数据），并且需要额外的机制来处理垃圾收集。主流的数据库管理系统如 PostgreSQL 和 MySQL 的 InnoDB 存储引擎都采用了 MVCC 机制。</p>
<p>MVCC通过ReadView控制事务能够看到的数据版本：</p>
<p>对于使用<code>READ UNCOMMITTED</code>隔离级别的事务来说，由于可以读到未提交事务修改过的记录，所以直接读取记录的最新版本就好了；对于使用<code>SERIALIZABLE</code>隔离级别的事务来说，设计<code>InnoDB</code>的大佬规定使用加锁的方式来访问记录。</p>
<p>对于使用<code>READ COMMITTED</code>和<code>REPEATABLE READ</code>隔离级别的事务来说，都必须保证读到已经提交了的事务修改过的记录，也就是说假如另一个事务已经修改了记录但是尚未提交，是不能直接读取最新版本的记录的，核心问题就是：需要判断一下版本链中的哪个版本是当前事务可见的。为此，设计<code>InnoDB</code>的大佬提出了一个<code>ReadView</code>的概念。</p>
<p>READ COMMITTED —— 每次读取数据前都生成一个ReadView，事务中每次需要读取的操作前获取这个数据的最新版本</p>
<p>REPEATABLE READ —— 在第一次读取数据时生成一个ReadView，事务开始之前获取版本，在事务中一直是这个版本</p>
<p>所以这两种隔离级别生成ReadView的时机不同，READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView，而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView，之后的查询操作都重复使用这个ReadView就好了。</p>
<h2>锁</h2>
<p>并发事务会遇到四种情况，读-读、写-写、读-写和写-读</p>
<h3>一致性读（读-读）</h3>
<p>事务利用<code>MVCC</code>进行的读取操作称之为<code>一致性读</code>，所有普通的<code>SELECT</code>语句（<code>plain SELECT</code>）在<code>READ COMMITTED</code>、<code>REPEATABLE READ</code>隔离级别下都算是<code>一致性读</code>，<code>一致性读</code>并不会对表中的任何记录做<code>加锁</code>操作，其他事务可以自由的对表中的记录做改动。</p>
<h3>锁定读</h3>
<ol>
<li><strong>共享锁（S锁）</strong>：
<ul>
<li>当一个事务对数据加上共享锁后，表示该事务将读取数据。</li>
<li>其他事务可以再获得该数据的共享锁，也就是说共享锁可以被多个事务同时持有。</li>
<li>但是，如果任何事务持有数据的共享锁，那么其他事务不能对该数据加排他锁（直到所有共享锁都被释放）。</li>
</ul>
</li>
<li><strong>排他锁（X锁）</strong>：
<ul>
<li>当一个事务对数据加上排他锁时，表示该事务将修改数据。</li>
<li>排他锁确保没有其他事务可以对同一数据加任何锁（无论是共享锁还是排他锁），直到持有排他锁的事务释放锁。</li>
<li>这意味着，一旦一个事务获得了某个数据项的排他锁，其他任何事务都不能再对该数据项加锁，直到排他锁被释放。</li>
</ul>
</li>
</ol>
<h3>非读取的情况（变异）</h3>
<h3>1. DELETE 操作的锁定</h3>
<p>在InnoDB中进行<code>DELETE</code>操作时，锁定机制的主要步骤如下：</p>
<ol>
<li><strong>定位记录</strong>：首先在B+树中找到需要删除的记录。</li>
<li><strong>获取排他锁（X锁）</strong>：在找到待删除记录后，系统会对该记录加上排他锁。这意味着其他事务不能读取或修改这条记录，直到当前事务完成操作。</li>
<li><strong>执行删除</strong>：在获得排他锁之后，系统进行删除操作，通常是标记该记录为删除状态（delete mask）。</li>
</ol>
<h5>为什么标记为删除状态而不是实际删除数据？</h5>
<ol>
<li><strong>事务的回滚支持</strong>：通过仅标记记录为删除状态，数据库能够在必要时轻松地撤销删除操作（即事务回滚）。如果立即物理删除数据，则在事务需要回滚时恢复这些数据会更加复杂和耗时。</li>
<li><strong>减少磁盘I/O操作</strong>：物理删除操作通常涉及更多的磁盘I/O操作，比如重组数据文件。标记记录为删除状态相比之下是一种轻量级操作，可以迅速完成，从而提高了整体性能。</li>
<li><strong>并发控制和锁定</strong>：在多用户并发环境下，标记删除而不是物理删除可以更好地控制并发访问。例如，在一个长事务正在读取一个数据页时，另一个事务可能需要删除该页上的一条记录。通过标记删除，长事务仍然可以看到原始数据，直到它完成。</li>
<li><strong>MVCC（多版本并发控制）</strong>：InnoDB等支持MVCC的数据库系统通过保留数据的旧版本来支持高效的并发控制和非锁定读取。标记删除而非物理删除，是保持多个版本的一种简单方式。</li>
<li><strong>空间回收和重用</strong>：被标记为删除的空间可以在后续的插入操作中被重用。这种方式允许数据库有效地管理存储空间，避免了频繁的空间分配和回收。</li>
<li><strong>碎片整理</strong>：物理删除记录可能导致数据文件碎片化，降低存储效率。通过定期的碎片整理过程（例如，在数据库维护期间），这些标记为删除的记录可以被清理，从而优化数据存储。</li>
</ol>
<h3>2. INSERT 操作的锁定</h3>
<p><code>INSERT</code>操作在InnoDB中的锁定机制相对简单：</p>
<ul>
<li><strong>过程</strong>：插入新记录时通常不显式加锁。</li>
<li><strong>隐式锁定</strong>：使用隐式锁机制保护新插入的记录，使其在当前事务提交前对其他事务不可见。</li>
<li><strong>目的</strong>：减少锁竞争，提高并发性能，同时保持数据的一致性和事务的隔离性。</li>
</ul>
<h3>3. UPDATE 操作的锁定</h3>
<p>键值是指在数据库表中用于唯一标识每条记录的值。这些值通常对应于表的主键（Primary Key）或唯一键（Unique Key）。</p>
<p><code>UPDATE</code>操作的锁定机制取决于是否修改了键值：</p>
<ol>
<li><strong>未修改键值，存储空间不变</strong>：
<ul>
<li>过程：在B+树中定位到记录，获取X锁，然后在原位置进行修改。</li>
<li>锁定：加锁是为了确保在修改过程中，没有其他事务可以读取或修改这条记录。</li>
</ul>
</li>
<li><strong>未修改键值，存储空间变化</strong>（比如修改了一个字段的长度）：
<ul>
<li><strong>过程</strong>：
<ol>
<li>定位记录：在B+树中找到要更新的记录。</li>
<li>获取X锁：对该记录加排他锁（X锁），阻止其他事务访问或修改。</li>
<li>彻底删除：将原记录从存储中完全删除，而不是仅标记为删除（delete mark）。</li>
<li>插入新记录：在适当位置插入修改后的新记录。</li>
</ol>
</li>
<li>锁定：定位并加X锁是为了在更新过程中保护原记录。插入新记录时，使用隐式锁保护新记录。</li>
</ul>
</li>
<li><strong>修改了键值</strong>：
<ul>
<li>过程：
<ol>
<li>执行DELETE操作：对原记录执行标准的删除操作，通常包括定位记录、获取X锁以及标记记录为删除（delete mark）。</li>
<li>执行INSERT操作：插入一个新的记录，具有新的键值。</li>
</ol>
</li>
<li>锁定：<code>DELETE</code>操作按照删除锁定规则进行，<code>INSERT</code>操作涉及到隐式锁定。</li>
</ul>
</li>
</ol>
<h3>多粒度锁</h3>
<h3>1. 行锁</h3>
<p>行锁是数据库管理系统中最细粒度的锁类型，它仅锁定数据中的单个行。在InnoDB中，行锁是通过索引来实现的，这意味着如果操作不使用索引，则行锁可能会退化为表锁。</p>
<ul>
<li><strong>优点</strong>：行锁可以最大限度地减少锁冲突，提高系统的并发能力。</li>
<li><strong>缺点</strong>：行锁的管理需要更多的资源，且在某些情况下可能导致死锁。</li>
</ul>
<p><strong>行锁和索引的关系</strong></p>
<ol>
<li><strong>锁定索引项</strong>：在InnoDB中，当对一条记录进行操作（如读取、更新或删除）时，实际上是在锁定与该记录相关联的索引项。如果这个操作使用了索引，例如通过主键或唯一索引来定位记录，那么InnoDB会在这个索引项上放置一个行锁。</li>
<li><strong>准确锁定</strong>：使用索引可以精确地锁定目标行。例如，如果一个查询条件是<code>WHERE id = 10</code>，并且<code>id</code>是一个被索引的列，那么只有<code>id</code>为10的那一行会被锁定。</li>
</ol>
<p><strong>操作不使用索引时的情况</strong></p>
<p>当执行的操作没有利用到索引时，情况就有所不同：</p>
<ol>
<li><strong>全表扫描</strong>：如果一个操作没有使用索引，例如一个没有有效过滤条件的查询或一个更新操作的过滤条件没有涉及到索引列，数据库就会进行全表扫描。</li>
<li><strong>锁定多行</strong>：在这种情况下，为了保证操作的一致性和隔离性，InnoDB可能不得不对表中的多行甚至整个表加锁。这种行为相当于行锁“退化”为表锁，因为数据库无法确定哪些行会受到操作的影响。</li>
<li><strong>影响并发性能</strong>：当行锁退化为表锁时，会显著影响数据库的并发性能。其他事务可能无法访问该表的其他行，即使这些行实际上并不会受到当前操作的影响。</li>
</ol>
<p><strong>优化建议</strong></p>
<p>为了避免行锁退化为表锁，建议：</p>
<ol>
<li><strong>使用索引</strong>：尽量确保查询和更新操作使用到索引，特别是在涉及大量数据的表上进行操作时。</li>
<li><strong>索引设计</strong>：合理设计索引，确保常用的查询和过滤条件能够利用到这些索引。</li>
</ol>
<h3>2. 表锁</h3>
<p>表锁是数据库中较为粗粒度的锁，它会锁定整个表。在一些操作中，比如全表扫描或者某些存储引擎（如MyISAM）中，会使用表锁。</p>
<ul>
<li><strong>优点</strong>：表锁的管理相对简单，开销小。</li>
<li><strong>缺点</strong>：锁定整个表会显著降低数据库的并发能力，特别是在高并发的读写场景中。</li>
</ul>
<h3>意向锁</h3>
<p>意向锁是InnoDB中用于支持多级锁定协议的一种锁，分为意向共享锁（IS锁）和意向排他锁（IX锁）：</p>
<ul>
<li><strong>意向共享锁（IS锁）</strong>：事务想要获得一张表中某几行的共享锁时，它首先必须获取该表的IS锁。</li>
<li><strong>意向排他锁（IX锁）</strong>：事务如果想要获得一张表中某几行的排他锁，它必须首先获得该表的IX锁。</li>
</ul>
<p>意向锁是表级别的锁，主要用于指示某个事务意图对表中的行加锁，而不是实际加锁。它们的作用是：</p>
<ul>
<li><strong>表明意图</strong>：让其他事务知道有一个事务打算对表中的一行或多行进行加锁操作。</li>
<li><strong>优化锁定检查</strong>：当一个事务请求对整个表加锁时，可以通过检查意向锁来判断是否存在冲突，而不需要检查表中的每一行锁。</li>
</ul>
<h3>3. AUTO_INCREMENT</h3>
<p>在MySQL的InnoDB存储引擎中，<code>AUTO_INCREMENT</code>列的值分配和锁定是一个重要的特性，尤其是在处理并发插入操作时。<code>AUTO-INC</code>锁和轻量级锁是两种不同的机制，用于管理<code>AUTO_INCREMENT</code>列值的生成和分配。让我详细解释这两种机制，并通过示例来说明它们的区别。</p>
<h3>AUTO-INC锁</h3>
<p><code>AUTO-INC</code>锁是一种表级别的锁，用于控制对<code>AUTO_INCREMENT</code>列的访问，确保在插入过程中生成的递增值是连续的。</p>
<ul>
<li><strong>工作方式</strong>：
<ul>
<li>当一个事务开始插入操作时，它会在表级别获取<code>AUTO-INC</code>锁。</li>
<li>随后，事务为每条待插入记录的<code>AUTO_INCREMENT</code>列分配递增值。</li>
<li>插入操作完成后，<code>AUTO-INC</code>锁被释放。</li>
</ul>
</li>
<li><strong>示例</strong>：假设有一个表<code>t</code>，其中有一个<code>AUTO_INCREMENT</code>列。如果事务A开始插入记录，它会首先获取<code>AUTO-INC</code>锁，然后为插入的每条记录分配一个连续的递增值。在事务A完成插入并释放锁之前，其他事务的插入操作会被阻塞。</li>
<li><strong>优点</strong>：保证了事务中分配的递增值是连续的。</li>
<li><strong>缺点</strong>：在高并发插入的场景下，可能成为性能瓶颈，因为其他事务必须等待<code>AUTO-INC</code>锁被释放才能进行插入。</li>
</ul>
<h3>轻量级锁</h3>
<p>轻量级锁是一种更优化的机制，用于减少<code>AUTO_INCREMENT</code>值分配过程中的锁竞争。</p>
<ul>
<li><strong>工作方式</strong>：
<ul>
<li>在为插入语句生成<code>AUTO_INCREMENT</code>值时，仅临时获取轻量级锁。</li>
<li>分配完必要的值后，立即释放这个轻量级锁。</li>
</ul>
</li>
<li><strong>示例</strong>：在同一个表<code>t</code>中，如果事务B要插入2条记录，它会短暂地获取轻量级锁，分配两个递增值，然后立即释放锁。这样，即使在事务B还在处理其他操作时，其他事务也可以进行插入操作。</li>
<li><strong>优点</strong>：提高了并发插入的性能，减少了锁等待时间。</li>
<li><strong>缺点</strong>：在某些情况下（特别是当<code>innodb_autoinc_lock_mode</code>设置为2时），可能导致不同事务中的插入操作生成的递增值不是连续的。</li>
</ul>
<h3><code>innodb_autoinc_lock_mode</code>的作用</h3>
<p><code>innodb_autoinc_lock_mode</code>是一个系统变量，用于控制InnoDB如何为<code>AUTO_INCREMENT</code>列分配值：</p>
<ul>
<li><strong>值为0</strong>：总是使用<code>AUTO-INC</code>锁。</li>
<li><strong>值为1</strong>：混合模式。当可以预先确定插入记录的数量时，使用轻量级锁；否则，使用<code>AUTO-INC</code>锁。</li>
<li><strong>值为2</strong>：总是使用轻量级锁。</li>
</ul>
<p>在主从复制的场景中，<code>innodb_autoinc_lock_mode</code>的设置为2可能会导致问题，因为不同事务中的插入操作可能生成交叉的递增值，从而影响复制的一致性。</p>
<h3>结论</h3>
<p>在选择<code>AUTO-INC</code>锁和轻量级锁之间，需要权衡事务中递增值连续性的需求和系统的并发插入性能。对于大多数应用来说，<code>innodb_autoinc_lock_mode</code>的默认设置（通常是1）提供了合理的平衡。对于高并发插入的场景，可能需要考虑使用轻量级锁以提高性能。</p>
<p>在MySQL的InnoDB存储引擎中，Gap Locks（间隙锁）和Next-Key Locks（临键锁）是用于事务处理和隔离级别控制的两种重要的锁机制。它们在处理并发事务时，尤其是在防止幻读（Phantom Reads）方面起着关键作用。</p>
<h3>4. Gap Locks（间隙锁）与Next-Key Locks（临键锁）</h3>
<h3>Gap Locks（间隙锁）</h3>
<p>Gap Locks锁定一个范围，但不包括该范围内的记录本身。</p>
<ul>
<li><strong>作用</strong>：主要用于防止其他事务向这个范围内插入新的记录，从而解决幻读问题。</li>
<li><strong>应用场景</strong>：通常出现在可重复读（REPEATABLE READ）和串行化（SERIALIZABLE）隔离级别下。</li>
</ul>
<h3>示例</h3>
<p>假设有一个表：</p>
<pre><code>| id |
|----|
| 1  |
| 2  |
| 4  |
| 5  |</code></pre>
<p>如果一个事务执行了如下查询：</p>
<pre><code class="language-sql">SELECT * FROM table WHERE id &gt; 2 FOR UPDATE;</code></pre>
<p>这将在<code>id</code>为2和4之间的间隙上加锁，但不包括2和4这两条记录。这意味着其他事务不能插入<code>id</code>值为3的记录，但可以更新<code>id</code>为2或4的记录。</p>
<h3>Next-Key Locks（临键锁）</h3>
<p>Next-Key Locks是行锁和间隙锁的结合。它们锁定一个范围，并包括范围的起始记录。</p>
<ul>
<li><strong>作用</strong>：除了防止其他事务在范围内插入记录外，还防止修改范围开始的记录。</li>
<li><strong>应用场景</strong>：Next-Key Locks是InnoDB在可重复读隔离级别下默认的锁类型。</li>
</ul>
<p>示例</p>
<p>同样的表格，如果一个事务执行：</p>
<pre><code class="language-sql">SELECT * FROM table WHERE id &gt;= 4 FOR UPDATE;</code></pre>
<p>这将对<code>id</code>为4的记录以及4之后的所有间隙加锁。因此，其他事务不能插入<code>id</code>值大于等于4的记录，也不能修改<code>id</code>为4的记录。</p>
<p>结论</p>
<p>Gap Locks和Next-Key Locks是InnoDB实现高并发事务处理和隔离级别保证的关键机制。它们通过锁定记录和间隙，帮助维护数据的一致性，尤其是在可重复读和串行化隔离级别下。然而，过度使用或不恰当的使用可能会导致性能问题，比如锁竞争和死锁。因此，在设计数据库交互和事务处理时，理解和适当地使用这些锁机制非常重要。</p>
<h3>5. 隐式锁</h3>
<h3>显式加锁</h3>
<p>对读取的记录加<code>S锁</code>：</p>
<pre><code class="language-sql">SELECT ... LOCK IN SHARE MODE;</code></pre>
<p>对读取的记录加<code>X锁</code>：</p>
<pre><code class="language-sql">SELECT ... FOR UPDATE;</code></pre>
<p>隐式锁是MySQL中的一个重要概念，特别是在InnoDB存储引擎中。这种锁机制是由数据库系统自动管理的，不需要用户直接干预。在某些操作过程中，数据库会自动施加和释放锁，以保持数据的一致性和完整性。</p>
<h3>隐式锁的特点</h3>
<ol>
<li><strong>自动管理</strong>：隐式锁是由数据库系统在后台自动管理的，无需用户手动施加或释放。</li>
<li><strong>透明性</strong>：对于用户来说，隐式锁的存在和操作通常是不可见的。</li>
<li><strong>保护数据完整性</strong>：隐式锁的主要目的是确保数据在并发访问和修改时的一致性和完整性。</li>
</ol>
<h3>隐式锁的应用场景</h3>
<p>隐式锁在InnoDB存储引擎中的应用非常普遍，以下是一些典型的应用场景：</p>
<ol>
<li><strong>记录级锁</strong>：在InnoDB中，即使没有明确的<code>LOCK TABLES</code>或<code>SELECT ... FOR UPDATE</code>等语句，对于更新（比如<code>UPDATE</code>、<code>DELETE</code>）操作，InnoDB会自动在被操作的行上施加行级锁（行锁是一种隐式锁）。</li>
<li><strong>自动增量锁</strong>：在处理<code>AUTO_INCREMENT</code>字段时，InnoDB会自动使用一种特殊的锁机制来确保自增值的唯一性和连续性。</li>
<li><strong>间隙锁和临键锁</strong>：在使用范围查询并进行修改操作（如<code>SELECT ... FOR UPDATE</code>）时，InnoDB会自动使用间隙锁或临键锁来防止幻读。</li>
</ol>
<h3>隐式锁的影响</h3>
<p>尽管隐式锁大大简化了数据库操作并保证了数据的安全性，但在高并发场景下，它们可能会导致一些问题：</p>
<ol>
<li><strong>锁竞争</strong>：在高并发环境下，大量的隐式锁可能会导致锁竞争，影响数据库性能。</li>
<li><strong>死锁风险</strong>：在复杂的查询和更新操作中，隐式锁可能导致死锁，特别是当多个事务涉及相同数据集时。</li>
</ol>
<h3>管理隐式锁</h3>
<p>虽然隐式锁是自动管理的，但了解它们的存在和行为对于优化数据库性能和避免锁冲突是非常重要的。通过合理设计数据库架构、优化查询语句和适当的事务管理，可以减少隐式锁带来的负面影响。此外，了解InnoDB的锁机制和事务隔离级别也有助于更好地理解隐式锁在实际操作中的行为。</p>
<h3>6. 悲观锁与乐观锁</h3>
<p>MySQL，特别是其最常用的存储引擎InnoDB，实际上采用了多种锁机制，包括悲观锁和乐观锁。每种锁机制在不同的场景下发挥作用。下面是这两种锁在MySQL中的应用实例：</p>
<h3>悲观锁</h3>
<p>MySQL的InnoDB存储引擎主要使用悲观锁来处理并发数据修改。悲观锁在MySQL中主要表现为行锁（Row Locks）和表锁（Table Locks）。</p>
<p>示例：行锁</p>
<p>当你在InnoDB表中执行一个<code>SELECT ... FOR UPDATE</code>语句时，MySQL会对返回的所有行施加排他锁（X锁），这是一种悲观锁的体现。例如：</p>
<pre><code class="language-sql">BEGIN;SELECT * FROM users WHERE id = 101 FOR UPDATE;</code></pre>
<p>这个查询会锁定ID为101的用户记录，直到事务结束。在这个事务完成之前，其他任何试图修改或锁定这条记录的事务都会被阻塞。</p>
<h3>乐观锁</h3>
<p>MySQL并不直接支持乐观锁，但你可以在应用层实现乐观锁机制。这通常通过在表中添加一个版本号字段或时间戳字段来实现。</p>
<p>示例：版本号字段</p>
<p>假设有一个名为<code>users</code>的表，其中包含一个<code>version</code>字段，你可以使用这个字段来实现乐观锁：</p>
<ol>
<li>
<p>读取数据时获取版本号：</p>
<pre><code class="language-sql">SELECT name, version FROM users WHERE id = 101;</code></pre>
</li>
<li>
<p>更新数据时检查版本号并更新：</p>
<pre><code class="language-sql">UPDATE users SET name = 'new name', version = version + 1 WHERE id = 101 AND version = [之前读取的版本号];</code></pre>
</li>
</ol>
<p>如果<code>version</code>字段在读取和更新之间被其他事务修改过，<code>UPDATE</code>语句不会影响任何行。这意味着数据在读取后已被修改，乐观锁策略会拒绝这次更新。</p>
<p>结论</p>
<p>在MySQL中，悲观锁主要以行锁和表锁的形式存在，直接由数据库引擎管理。而乐观锁通常需要应用程序在业务逻辑层面实现，通常通过版本控制来完成。正确地使用这两种锁机制可以帮助提高数据库操作的效率和一致性。</p>
<p>当然，以下是对您提到的MySQL中<code>JOIN</code>操作的完整和详细介绍，包括了您的原始内容和我所建议的补充内容：</p>
<h2>JOIN操作</h2>
<h3>连接查询的基础</h3>
<ul>
<li><strong>连接的本质</strong>：连接操作的目的是将不同表中的记录根据特定条件组合起来，形成一个包含所需信息的新的结果集。</li>
<li><strong>驱动表的选择</strong>：首先确定第一个需要查询的表，即<code>驱动表</code>。这个选择对查询性能有重要影响。</li>
<li><strong>记录匹配</strong>：针对从驱动表产生的每条记录，分别在被驱动表中查询并进行匹配。</li>
</ul>
<h3>内连接与外连接</h3>
<ul>
<li><strong>内连接</strong>：仅当两个表都有匹配时，才返回结果。</li>
<li><strong>外连接</strong>：左连接（LEFT JOIN）和右连接（RIGHT JOIN）根据左表或右表为驱动表，即使在被驱动表中找不到匹配的记录，也能返回结果，未找到匹配的部分以NULL填充。</li>
</ul>
<h3>过滤条件</h3>
<ul>
<li><strong>WHERE子句</strong>：通用过滤条件，不符合条件的记录不会被加入最终结果。</li>
<li><strong>ON子句</strong>：专用于外连接的场景，用于确定哪些记录即使在被驱动表中无匹配也应加入结果集。</li>
</ul>
<h3>索引的作用</h3>
<ul>
<li><strong>加速查询</strong>：在连接列上使用索引可以显著提高查询效率，减少磁盘I/O。</li>
</ul>
<h3>不同类型的连接</h3>
<ul>
<li><strong>交叉连接（CROSS JOIN）</strong>：产生笛卡尔积，即两个表的每条记录都与另一个表的每条记录匹配。</li>
<li>
<p>自连接（Self Join）是一种特殊类型的SQL连接，用于将一个表与自身进行连接。这种连接通常用于处理那些存储在同一表中但需要作为两个独立实体进行比较或关联的数据。</p>
<h3>自连接的特点：</h3>
<ol>
<li><strong>同一表的不同实例</strong>：在自连接查询中，同一张表被当作两个独立的表来处理，通常通过给表分配不同的别名来实现。</li>
<li><strong>应用场景</strong>：当一个表中的数据需要与该表中的其他数据进行比较或关联时，自连接是非常有用的。例如，在员工表中找到所有员工及其直接上级，或者在一个包含日期数据的表中比较连续的日期。</li>
<li><strong>查询方式</strong>：自连接可以是内连接、外连接等，具体取决于查询的需求。</li>
</ol>
<h3>自连接的例子：</h3>
<p>假设有一个员工表 <code>Employees</code>，其中包含<code>EmployeeID</code>、<code>Name</code> 和 <code>ManagerID</code>（表示每个员工的直接上级）等字段。要找出每个员工及其直接上级的名字，可以使用自连接：</p>
<pre><code class="language-sql">SELECT    E1.Name AS EmployeeName,    E2.Name AS ManagerNameFROM    Employees AS E1LEFT JOIN    Employees AS E2 ON E1.ManagerID = E2.EmployeeID;</code></pre>
<p>这里，<code>Employees</code> 表以 <code>E1</code> 和 <code>E2</code> 两个别名出现，分别代表员工和他们的上级。通过在 <code>ON</code> 子句中比较 <code>E1.ManagerID</code> 和 <code>E2.EmployeeID</code>，实现了自连接的目的。</p>
<p>总的来说，自连接是一种在同一表上进行的连接操作，它允许在同一查询中比较或关联表中的行。</p>
</li>
</ul>
<h3>连接顺序与优化器</h3>
<ul>
<li><strong>成本估算</strong>：MySQL优化器基于成本估算来决定表的连接顺序，以找到最佳执行计划。</li>
</ul>
<h3>连接算法</h3>
<p>在数据库系统中，尤其是在进行表的连接操作时，连接算法的选择对于查询性能至关重要。主要有两种类型的连接算法：嵌套循环连接（Nested Loop Join）和散列连接（Hash Join）。</p>
<h3>嵌套循环连接（Nested Loop Join）</h3>
<p>嵌套循环连接是最基本和最常见的连接算法，适用于处理小到中等大小的数据集。</p>
<ul>
<li><strong>工作原理</strong>：
<ul>
<li>此算法通过两个表的嵌套循环来实现连接。首先，从第一个表（驱动表）中取出一行，然后在第二个表（被驱动表）中搜索匹配的行。</li>
<li>对于驱动表中的每一行，都会执行被驱动表的完整扫描，直到找到所有匹配的行。</li>
</ul>
</li>
<li><strong>应用场景</strong>：
<ul>
<li>当连接的表较小或者连接列上有有效索引时，嵌套循环连接非常高效。</li>
<li>适用于返回少量匹配行的查询，比如基于高选择性条件的查询。</li>
</ul>
</li>
<li><strong>性能考虑</strong>：
<ul>
<li>如果没有适当的索引，尤其是在被驱动表上，这种连接可能会导致大量不必要的磁盘I/O，从而影响性能。</li>
<li>性能依赖于驱动表的大小和被驱动表的索引效率。</li>
<li>使用<strong>Join Buffer</strong>在内存中缓存驱动表的记录，减少对被驱动表的重复磁盘访问。</li>
</ul>
</li>
</ul>
<h3>散列连接（Hash Join）</h3>
<p>散列连接是一种高效处理大型数据集的连接算法，特别是在没有索引或者数据量较大的场景下。这种算法在MySQL 8.0及更高版本中得到了支持。</p>
<ul>
<li><strong>工作原理</strong>：
<ul>
<li>首先，选择一个表（通常是较小的那个）来构建哈希表。算法遍历这个表，对每行的连接键应用哈希函数，并将结果存储在哈希表中。</li>
<li>然后，算法遍历第二个表，同样对每行的连接键应用哈希函数，并在哈希表中查找匹配的行。</li>
<li>由于哈希表提供了快速的查找能力，这种方法可以有效地处理大量数据。</li>
</ul>
</li>
<li><strong>应用场景</strong>：
<ul>
<li>当处理大型数据集，尤其是其中一个或两个表都没有有效索引时，散列连接特别有效。</li>
<li>适合于需要返回大量匹配行的查询，例如大规模的全表连接。</li>
</ul>
</li>
<li><strong>性能考虑</strong>：
<ul>
<li>散列连接的性能主要取决于内存。哈希表需要足够的内存空间来存储，如果内存不足，可能会导致性能下降。</li>
<li>由于不依赖于数据的物理存储顺序，散列连接在处理无序或无索引数据集时更有优势。</li>
</ul>
</li>
</ul>
<h3>使用Join Buffer的场景：</h3>
<ol>
<li><strong>无适用索引的嵌套循环连接</strong>：
<ul>
<li>当连接操作无法利用索引时（特别是在被驱动表上），使用Join Buffer可以减少对被驱动表的重复访问，提高效率。</li>
</ul>
</li>
<li><strong>小到中等规模的数据集</strong>：
<ul>
<li>对于不太大的数据集，Join Buffer可以有效地提高嵌套循环连接的性能。</li>
</ul>
</li>
<li><strong>单个或少数几个行的重复使用</strong>：
<ul>
<li>当驱动表中的行需要与被驱动表中的多行进行比较时，Join Buffer特别有用。</li>
</ul>
</li>
</ol>
<h3>使用Hash Join的场景：</h3>
<ol>
<li><strong>处理大型数据集</strong>：
<ul>
<li>Hash Join特别适合于处理大规模数据集，尤其是当表之间没有有效的索引时。</li>
</ul>
</li>
<li><strong>MySQL 8.0及以上版本</strong>：
<ul>
<li>如果您使用的是MySQL 8.0或更高版本，可以充分利用Hash Join的优势，因为这个版本对Hash Join做了优化。</li>
</ul>
</li>
<li><strong>大量匹配行的查询</strong>：
<ul>
<li>当预期的匹配行数较多时，Hash Join通常比嵌套循环连接更有效率。</li>
</ul>
</li>
</ol>
<p>嵌套循环连接因其简单性在小型数据集中广泛使用，特别是当存在高效索引时。相比之下，散列连接适用于处理大型数据集或缺乏索引的场景，特别是在MySQL的新版本中。散列连接通过哈希表来优化查找过程，使得它在这些情况下更为高效。</p>
<h3>连接的性能影响</h3>
<ul>
<li><strong>性能考量</strong>：大型表的连接可能导致性能下降。通过优化查询或调整表结构可以改善性能。</li>
</ul>
<h3>锁机制</h3>
<ul>
<li><strong>并发控制</strong>：在多用户环境下，连接操作可能涉及锁定机制，特别是涉及写操作时。</li>
</ul>
<h2>不推荐使用外键</h2>
<p>阿里的《Java 开发手册》明确规定禁止使用外键。不过这样用起来就和MongoDB差不多了…</p>
<p><img decoding="async" src="https://prod-files-secure.s3.us-west-2.amazonaws.com/28e81497-5bec-47c0-87c5-1c275daa8884/1cb460ce-1bac-485e-913f-0f3c16ae5b5d/Untitled.png" alt="Untitled" /></p>
<h2><strong>尽可能把所有列定义为 NOT NULL</strong></h2>
<p>除非有特别的原因使用 NULL 值，应该总是让字段保持 NOT NULL。</p>
<ul>
<li>索引 NULL 列需要额外的空间来保存，所以要占用更多的空间；</li>
<li>进行比较和计算时要对 NULL 值做特别的处理。</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5096/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5096</post-id>	</item>
		<item>
		<title>自建博客简明教程</title>
		<link>https://www.cztcode.com/2023/4875/</link>
					<comments>https://www.cztcode.com/2023/4875/#comments</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sun, 04 Jun 2023 12:36:23 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4875</guid>

					<description><![CDATA[从我第一次创建博客已经三年了，那是在疫情时候对一个晚上，无聊在网上乱转，偶然看见一个讲解使用阿里云创建博客的视频，跟着就创建了我的第一个Wordpress博客。 总的说来博客就是折腾，你要折腾域名，CDN，对象存储，对于Wordpress来说你还要去尝试各种插件，优化加载速度，SEO排名。但没有比拥有自己博客更棒的事情了，在上面你可以任意创作，向别人展示你的发现。我创建这个博客的时候还和几个同学一 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>从我第一次创建博客已经三年了，那是在疫情时候对一个晚上，无聊在网上乱转，偶然看见一个讲解使用阿里云创建博客的视频，跟着就创建了我的第一个Wordpress博客。</p>
<p>总的说来博客就是折腾，你要折腾域名，CDN，对象存储，对于Wordpress来说你还要去尝试各种插件，优化加载速度，SEO排名。但没有比拥有自己博客更棒的事情了，在上面你可以任意创作，向别人展示你的发现。我创建这个博客的时候还和几个同学一起更新文章（很可惜在迁移用户的时候没有把大家都迁移过来，因为似乎没有人在更新了，我就把这个博客变成个人博客了）</p>
<h1>成本</h1>
<table>
<thead>
<tr>
<th>名称</th>
<th>型号</th>
<th>价格</th>
<th>供应商</th>
</tr>
</thead>
<tbody>
<tr>
<td>域名</td>
<td>.com</td>
<td>9.15美元/年</td>
<td>CloudFlare</td>
</tr>
<tr>
<td>对象存储</td>
<td>10g以内</td>
<td>免费</td>
<td>CloudFlare</td>
</tr>
<tr>
<td>CDN</td>
<td></td>
<td>免费</td>
<td>CloudFlare</td>
</tr>
<tr>
<td>服务器</td>
<td>2c1g vps</td>
<td>16.80美元/年</td>
<td>CloudCone</td>
</tr>
</tbody>
</table>
<p>所以创建一个WordPress博客每年的维护成本在25.95美元（约184元人民币），除此之外你还可以用自己的服务器部署其他小玩具。当然相比于Github Pages 这样完全免费的静态博客，多花的服务器成本还是贵了，但你多了折腾的机会不是嘛，况且每个程序员应该都有自己的服务器吧。</p>
<h1>购买域名和服务器</h1>
<h2>不要选国内的服务提供商</h2>
<h3>备案</h3>
<p>腾讯云阿里云我都用过，但你如果在国内注册域名、购买服务器，肯定是需要备案的。我不是说备案不好，只是麻烦，你要等1个多星期才能访问自己的域名。博客还要合规，比如不能提供注册功能，博客名字必须和备案名字相同等等奇葩规定。</p>
<p>所以干脆直接去国外域名提供商购买，服务器也放在国外，这样直接没有备案的烦恼。</p>
<h3>成本</h3>
<p>从成本上考虑，CloudFlare的.com域名相比于国内是更便宜的，因为cloudflare的续费价格应该是全球最低的，Cloudflare承诺只会向收取向互联网名称与数宇地址分配机构ICANN支付的批发价 (Wholesalefee）。意思是除了必要的费用（包括域名的价格、ICANN费用），没有加价，也没有额外费用。</p>
<p>CloudFlare的CDN也不要钱啊，相比于国内1GB流量几毛钱，完全不用考虑这个问题。而且你用CloudFlare也不用担心博客被人刷流量，开森。</p>
<p>绑定银行卡后CloudFlare的S2（对象存储），10GB存储免费，做图床完全够了。</p>
<p>补一个，证书也不需要你管续费，CloudFlare可以提供免费证书并自动帮你续费。</p>
<p>国内的服务器注册时各种优惠活动，续费时价格贵上天，这也是为啥我把服务器迁移到国外了。</p>
<h2>购买</h2>
<h3>域名</h3>
<p>在这里注册CloudFlare账号：<a href="https://dash.cloudflare.com/sign-up" target="_blank" rel="noopener">https://dash.cloudflare.com/sign-up</a></p>
<p>找到域名注册页面</p>
<p><img decoding="async" src="https://markdown.cztcode.com/a46880f78e820717570dcb3db83259ff.png" alt="image-20230604171849940" /></p>
<p>输入你想注册的域名，xxxx.com，域名就是别人访问你的地址。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/0907bd29bad2a1e96c1e39a7f413aac7.png" alt="image-20230604172020310" /></p>
<p>.com这个域名有购买按钮就代表别人没有注册过，你可以使用，点击购买就好啦。（可能需要先绑定Palpal，国内银行卡即可）</p>
<h3>服务器</h3>
<p>CloudCone的服务器很便宜，而且续费和你第一次购买时的价格是一样的，很爽。<a href="https://hello.cloudcone.com/2023-hashtag-yearly-vps-sale/?utm_source=encharge&amp;utm_medium=email&amp;utm_campaign=[23rd+May]+Hashtag+VPS+Sale+-+Reminder&amp;utm_content=Go+online+with+our+VPSs+for+%2410.99!+Hashtag+2023+VPS+Sale+is+back+in-stock!" target="_blank" rel="noopener">活动地址</a></p>
<p>如果活动过期了也不要紧，他们家活动基本全年有，在网上另找活动页面即可。</p>
<p>看一下要购买的服务器价格，先不要点购买（要先注册账号充钱）。选择右面这个2C1G配置，左面的太小了不够Wordpress运行。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/187d3ab76ab8033f8e886657a72fcdd5.png" alt="image-20230604173812754" /></p>
<p>首先注册一个CloudCone账号【<a href="https://app.cloudcone.com/user/" target="_blank" rel="noopener">https://app.cloudcone.com/user/</a>】</p>
<p>找到Add Funds</p>
<p><img decoding="async" src="https://markdown.cztcode.com/8379bb416f3b4717b39cfc27c41dc8bc.png" alt="image-20230604174504596" /></p>
<p>如果你有PayPal（推荐注册一个，因为后面如果使用对象存储还要用上，国内银行卡就可以，需要实名，如果支付失败就用支付宝，好像这里需要双币信用卡），直接用Paypal付款16.80。如果你没有，下面还有一个支付宝渠道，但在这里你要付款18.80（因为有可能要再多花2美元让客服给你换一个中国能访问的IP，不一定每个人都遇到IP被禁了，但如果你不能访问的话就只能花这两美元。因为支付宝这个渠道最低支付5美元，在这里你如果没有直接多冲进去2美元，就只能多花5美元了，PayPal则没有这个限制）。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/d957f7983e21776dca43da5d23f5d299.png" alt="image-20230604174951340" /></p>
<p>现在你可以回到之前的页面购买服务器了，这里的HostName可以填server.xxx.com，操作系统debian和ubuntu你随意，我喜欢debian。Centos就不要选了，要停止维护了。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/719adf3ce2ac3151f6d422cd2fadc1a1.png" alt="image-20230604175152199" /></p>
<h2>登录服务器</h2>
<p>购买成功后会收到一封邮件，上面会告诉你登录密码</p>
<p><img decoding="async" src="https://markdown.cztcode.com/db42744bc4b7eea90681a5f1ed833be6.png" alt="image-20230604190541296" /></p>
<p>选择一个终端使用ssh，我在mac上使用的是iTerm2</p>
<pre><code class="language-bash"> ssh  root@your-server-ip</code></pre>
<p>第一次登录要使用密码，服务器的ip可以在这里看到，替换<code>your-server-ip</code>, 我的就是：ssh root@104.194.227.200</p>
<p><img decoding="async" src="https://markdown.cztcode.com/308f97c9902afce4e60323fc1b6c68e3.png" alt="image-20230604191228342" /></p>
<h3>无法登录</h3>
<p>如果你在ssh后一直没有看到任何输出，那很大概率你的ip国内无法访问。这里登录成功的请直接跳到：<code>使用密钥登录</code>部分</p>
<p><a href="https://tcp.ping.pe/" target="_blank" rel="noopener">使用这个网站检查国内是否可以访问</a></p>
<p>输入你的<code>server-ip:22</code>，如果全是绿的代表中国可以访问。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/5659f7899fe6929ec375fa4ad20ef601.png" alt="image-20230604192913990" /></p>
<p>如果下面中国这几条是红的，代表这个服务器中国不能访问，需要花2美元让客服换中国能访问的IP：</p>
<p><img decoding="async" src="https://markdown.cztcode.com/976fad8e026da0ae80f5523b98c9ef7e.png" alt="image-20230604193006744" /></p>
<h3>更换IP</h3>
<p>创建一个Ticket</p>
<p><img decoding="async" src="https://markdown.cztcode.com/a19d6b8f6c180f38c12aaaeee7889e30.png" alt="image-20230604193415150" /></p>
<p>选择你的服务器IP，然后输入下面的内容，要求扣除2美元更换一个中国能访问的IP，这里如果你的账户是Paypal付款的，需要再充入2美元。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/c226efd1621cf372372eaa6cadd2aaa9.png" alt="image-20230604193629090" /></p>
<p>Subject：Change IP Request</p>
<p>Description：：Please deduct my $2 balance to help me replace an IP address that I can use in China. Thank you</p>
<p>之后等待客服回复，然后拿着你的新IP登录。</p>
<h3>使用密钥登录</h3>
<p>因为密码登录十分不安全，第一件事就是替换成密钥登录，然后禁用密码登录。</p>
<p>首先生成密钥：</p>
<p>生成公钥指令（mac）</p>
<pre><code class="language-bash"> ssh-keygen -t rsa -b 4096</code></pre>
<p>按照提示生成后，密钥默认存储位置在<code>$HOME/.ssh</code> 目录下，<code>id_rsa.pub</code>内容就是公钥，但是这里我们不需要管位置在哪，只需要通过指令告诉服务器记住公钥。</p>
<pre><code class="language-bash"> ssh-copy-id root@your-server-ip</code></pre>
<p>看到下面的提示就成功了！以后登录就不需要输入密码了。</p>
<p>如果你想要添加多台电脑，可以多次操作，也可以把你这台电脑的密钥复制到新电脑上，或者使用cloudcone控制台添加你的公钥。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/8f7c6e1b6e09f9de9300be9213f5b8ff.png" alt="image-20230604192021675" /></p>
<h3>禁用密码登录</h3>
<p>给的密码强度太低了，很容易被爆破，这里需要禁止密码登录。</p>
<p>首先更新apt，执行</p>
<pre><code>  apt update</code></pre>
<p>然后安装Vim编辑器</p>
<pre><code> apt install vim</code></pre>
<p>如何使用Vim见：<a href="https://www.runoob.com/linux/linux-vim.html" target="_blank" rel="noopener">Linux vi/vim</a>，看懂怎么编辑文件，保存退出就好。</p>
<ol>
<li>使用以下命令编辑 SSH 服务器的配置文件： sudo vim /etc/ssh/sshd_config</li>
<li>在配置文件中找到以下行： #PasswordAuthentication yes将其更改为：PasswordAuthentication no删除该行前的 <code>#</code> 号以取消注释，并将其值更改为 <code>no</code>，表示禁用密码认证。</li>
<li>保存更改并关闭文本编辑器。</li>
<li>为了让配置生效，重启 SSH 服务。在终端中输入以下命令： sudo systemctl restart ssh</li>
</ol>
<p>完成这些操作后，密码登录将被禁用。</p>
<h2>配置DNS记录</h2>
<p>去CloudFlare，添加一个指向你服务器的A记录，这样别人输入域名就可以访问你的博客。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/c01ad1a96b9793c1e29484b98b92e143.png" alt="image-20230604195036941" /></p>
<h2>安装 wordpress</h2>
<p>首先创建一个目录</p>
<pre><code> mkdir /home/wordpress</code></pre>
<p>这里我们使用docker-compose安装，首先安装docker-compose</p>
<pre><code> apt install docker-compose</code></pre>
<p>创建一个<code>docker-compose.yaml</code>文件</p>
<pre><code> cd /home/wordpress
 vim docker-compose.yaml</code></pre>
<p>复制进去我的配置，安装wordpress和mysql</p>
<p>注意，你要自己更改<code>MYSQL_ROOT_PASSWORD</code> <code>MYSQL_PASSWORD</code>和<code>WORDPRESS_DB_PASSWORD</code>的值，要求15位以上，大小写特殊字符混合输入，避免被破解。其中MYSQL_PASSWORD和WORDPRESS_DB_PASSWORD是相同的。</p>
<pre><code class="language-yaml"> version: &#039;3&#039;

 services:
   db:
     image: mysql:latest
     user: root
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: A
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: B

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     user: root
     ports:
       - &quot;8000:80&quot;
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: B
       WORDPRESS_DB_NAME: wordpress
     volumes:
       - wp_data:/var/www/html

 volumes:
   db_data:
   wp_data:</code></pre>
<p>保存后在<code>docker-compose.yaml</code>所在文件夹下执行（注意所有docker-compose指令必须在<code>docker-compose.yaml</code>文件夹下。</p>
<pre><code>  docker-compose up -d</code></pre>
<p>这个启动命令仅仅执行一次，以后如果想要开启或者关闭容器，请执行：</p>
<table>
<thead>
<tr>
<th>docker-compose start</th>
<th>运行容器</th>
</tr>
</thead>
<tbody>
<tr>
<td>docker-compose stop</td>
<td>关闭容器</td>
</tr>
<tr>
<td>docker-compose restart</td>
<td>重启容器</td>
</tr>
</tbody>
</table>
<p>看到Done后表示启动成功</p>
<p><img decoding="async" src="https://markdown.cztcode.com/65e935b49347a0c819702644306d46ae.png" alt="image-20230604200633235" /></p>
<h2>安装Nginx</h2>
<p>我们要使用Nginx做反向代理。</p>
<pre><code class="language-bash"> apt install nginx</code></pre>
<pre><code class="language-json"> vim /etc/nginx/conf.d/wordpress.conf
 server {
     listen 80;
     server_name www.your-domain.com;
     client_max_body_size 1024m;

     location / {
         proxy_pass http://localhost:8000;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }

     error_log /var/log/nginx/error.log;
     access_log /var/log/nginx/access.log;
 }
</code></pre>
<p>保存后，刷新配置文件</p>
<pre><code class="language-bash"> nginx -s reload</code></pre>
<h2>设置wordpress</h2>
<p>在浏览器中访问你的域名<a href="www.xxxx.com">www.xxxx.com</a></p>
<p><img decoding="async" src="https://markdown.cztcode.com/8b083394106d50116ddf4f602f2cb43c.png" alt="image-20230604201713950" /></p>
<p>配置信息填写完成后，进入到仪表盘</p>
<p><img decoding="async" src="https://markdown.cztcode.com/e370f8e6b200cc3942c1860fa493f6f9.png" alt="image-20230604201909314" /></p>
<h3>插件</h3>
<p>选择安装插件</p>
<p><img decoding="async" src="https://markdown.cztcode.com/24d6809f11c8736d84989abf59b2c87b.png" alt="image-20230604201944119" /></p>
<p>这里我把我安装的插件推荐出来，这些我亲测十分好用：</p>
<p><img decoding="async" src="https://markdown.cztcode.com/b65a06fd918ebbaf7087fdc272b51a1d.png" alt="image-20230604202030670" /></p>
<p><img decoding="async" src="https://markdown.cztcode.com/f37b925b10fc536e4c7a5e472bb2d52e.png" alt="image-20230604202042891" /></p>
<p>因为我实际的服务器是4c4g，所以还安装了redis作为缓存，如果大家没有redis，可以不用安装<strong>Redis Object Cache</strong>。</p>
<p>使用CloudFlare CDN的同学记得在<strong>WP Fastest Cache</strong>里开启支持。</p>
<p>推荐大家必须要安装的插件</p>
<ol>
<li>WPvivid插件，这个插件可以备份博客，并让你迁移博客的时候十分丝滑，一键完成（其实用docker不是为了方便迁移，是为了大家安装….)，推荐设置自动备份，使用Google Dirve<img decoding="async" src="https://markdown.cztcode.com/804bf0483e2a4452638ebd686ac6ed66.png" alt="image-20230604202409448" /></li>
<li>WordFence 可以免费使用的防火墙软件，必须安装此插件防止爆破密码和XSS注入、SQL注入等攻击</li>
</ol>
<p><img decoding="async" src="https://markdown.cztcode.com/eecc98c26678417d59aa66f7deb075c4.png" alt="image-20230604202540760" /></p>
<ol>
<li>JetPack，你可以使用这个插件监控自己的博客，配置订阅系统</li>
</ol>
<p><img decoding="async" src="https://markdown.cztcode.com/8146b2781af4c22f785a990aa9e44f58.png" alt="image-20230604202441532" /></p>
<ol>
<li>Elementor 强大的页面编辑器，我博客的页面都是使用这个插件，可以实现很多特殊效果。你还可以订阅它的会员，在淘宝购买一年只需要20多。</li>
</ol>
<p><img decoding="async" src="https://markdown.cztcode.com/7c3fd391e3e68008e306ff68db96421b.png" alt="image-20230604202806454" /></p>
<h3>主题</h3>
<p>主题决定了你的博客是什么样的风格，和安装插件一样，主题同样可以选择</p>
<p>我使用的是Sydney，十分美观大方，当然大家也可以到市场里找到自己喜欢的主题</p>
<p><img decoding="async" src="https://markdown.cztcode.com/9d87d1356b969cda73c166709d69eeb7.png" alt="image-20230604202941244" /></p>
<h2>最后</h2>
<p>剩余的配置就靠大家自己玩啦，看看有哪些选项可以让博客更好看，速度更快。</p>
<p>记住博客最重要的是文章，有了自己的博客后一定要多多更新哦（不要学我）</p>
<p>欢迎互换友链：<a href="https://www.cztcode.com/">https://www.cztcode.com</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/4875/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4875</post-id>	</item>
		<item>
		<title>wordpress添加显示文章总数短代码</title>
		<link>https://www.cztcode.com/2023/4712/</link>
					<comments>https://www.cztcode.com/2023/4712/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Mon, 24 Apr 2023 11:31:29 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4712</guid>

					<description><![CDATA[在当前主题的functions.php末尾添加： 即可在主题中使用showp短代码显示当前文章总数，在文章中也可以使用短代码： 注意，每次主题更新后都需要手动添加，如果你想自动添加可以启动子主题。 子主题开启见：https://codex.wordpress.org/zh-cn:%E5%AD%90%E4%B8%BB%E9%A2%98]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>在当前主题的functions.php末尾添加：</p>


<p>即可在主题中使用showp短代码显示当前文章总数，在文章中也可以使用短代码：</p>

<p>当前文章总数为：393篇</p>


<pre class="wp-block-code"><code lang="php" class="language-php line-numbers">add_shortcode('mycode', 'my_shortcode_func');

function my_shortcode_func() {

return wp_count_posts()-&gt;publish;

}

add_shortcode('showp', 'my_shortcode_func');

function wpdaxue_disallow_tags_for_author( $taxonomy ) {

global $wp_taxonomies;

if ('post_tag' === $taxonomy) {

$wp_taxonomies[$taxonomy]-&gt;cap-&gt;assign_terms = 'edit_others_posts';

}

}

add_action('registered_taxonomy', 'wpdaxue_disallow_tags_for_author');</code></pre>


<p>注意，每次主题更新后都需要手动添加，如果你想自动添加可以启动子主题。</p>


<p>子主题开启见：<a href="https://codex.wordpress.org/zh-cn:%E5%AD%90%E4%B8%BB%E9%A2%98" target="_blank" rel="noopener">https://codex.wordpress.org/zh-cn:%E5%AD%90%E4%B8%BB%E9%A2%98</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/4712/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4712</post-id>	</item>
		<item>
		<title>博客迁移</title>
		<link>https://www.cztcode.com/2023/4661/</link>
					<comments>https://www.cztcode.com/2023/4661/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Wed, 29 Mar 2023 08:33:51 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">http://81.69.24.244/?p=4661</guid>

					<description><![CDATA[由于服务器到期，博客已经迁移到我的另一台服务器上了。文章数据都在，但是由于wordpress的限制，在导入数据时无法分配作者，原有的文章设置成了默认作者。如果你想导出数据或者更新作者，请联系我～ 历史访问记录 由于没有迁移全部数据库，历史访问记录清空了。 留作纪念：]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>由于服务器到期，博客已经迁移到我的另一台服务器上了。文章数据都在，但是由于wordpress的限制，在导入数据时无法分配作者，原有的文章设置成了默认作者。如果你想导出数据或者更新作者，请联系我～</p>



<h2 class="wp-block-heading">历史访问记录</h2>



<p>由于没有迁移全部数据库，历史访问记录清空了。</p>



<p>留作纪念：</p>



<figure class="wp-block-image aligncenter size-full is-resized is-style-default"><img fetchpriority="high" decoding="async" src="http://81.69.24.244/wp-content/uploads/2023/03/image.png" alt="" class="wp-image-4679" width="394" height="426" srcset="https://www.cztcode.com/wp-content/uploads/2023/03/image.png 788w, https://www.cztcode.com/wp-content/uploads/2023/03/image-277x300.png 277w, https://www.cztcode.com/wp-content/uploads/2023/03/image-139x150.png 139w, https://www.cztcode.com/wp-content/uploads/2023/03/image-768x830.png 768w, https://www.cztcode.com/wp-content/uploads/2023/03/image-230x249.png 230w, https://www.cztcode.com/wp-content/uploads/2023/03/image-350x378.png 350w, https://www.cztcode.com/wp-content/uploads/2023/03/image-480x519.png 480w" sizes="(max-width: 394px) 100vw, 394px" /></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/4661/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4661</post-id>	</item>
		<item>
		<title>单机Docker常见知识</title>
		<link>https://www.cztcode.com/2022/4260/</link>
					<comments>https://www.cztcode.com/2022/4260/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sat, 20 Aug 2022 08:12:40 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4260</guid>

					<description><![CDATA[最近我在学Docker，这篇文章记录了Docker的基础操作指令。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<h2 class="wp-block-heading">单机Docker</h2>



<p>最近我在学Docker，这篇文章记录了Docker的基础操作指令。</p>



<p>教程链接：<strong><a href="https://www.bilibili.com/video/BV1gr4y1U7CY?p=1&amp;vd_source=bd2e225c1a23848b80eb4771969e173f" class="rank-math-link" target="_blank" rel="noopener">尚硅谷2022版Docker实战教程</a></strong></p>



<h2 class="wp-block-heading">Docker镜像为什么这么小？</h2>



<p>docker的镜像实际上由一层一层的文件系统组成，这种层级的文件系统UnionFS。</p>



<p><code>bootfs</code>(boot file system)主要包含bootloader和kernel, bootloader主要是引导加载kernel, Linux刚启动时会加载bootfs文件系统，在Docker镜像的最底层是引导文件系统bootfs。这一层与我们典型的Linux/Unix系统是一样的，包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了，此时内存的使用权已由bootfs转交给内核，此时系统也会卸载bootfs。</p>



<p>rootfs (root file system) ，在bootfs之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。rootfs就是各种不同的操作系统发行版，比如Ubuntu，Centos等等。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220819153425131.png" alt="image-20220819153425131"/></figure>



<p>平时我们安装进虚拟机的CentOS都是好几个G，为什么docker这里才200M？？</p>



<figure class="wp-block-image size-full"><img decoding="async" src="https://www.cztcode.com/wp-content/uploads/2022/08/graphic.png" alt="" class="wp-image-4262"/></figure>



<p>对于一个精简的OS，rootfs可以很小，只需要包括最基本的命令、工具和程序库就可以了，因为底层直接用Host的kernel，自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。</p>



<h2 class="wp-block-heading">Docker run</h2>



<p>对于一个精简的OS，rootfs可以很小，只需要包括最基本的命令、工具和程序库就可以了，因为底层直接用Host的kernel，自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。</p>



<p>docker run [options] image [command] [args]</p>



<p>&#8211;name=&#8221;容器新名字&#8221; 为容器指定一个名称；</p>



<p>-d: 后台运行容器并返回容器ID，也即启动守护式容器(<strong>后台运行</strong>)；</p>



<p>-i：以交互模式运行容器，通常与 -t 同时使用；</p>



<p>-t：为容器重新分配一个伪输入终端，通常与 -i 同时使用；</p>



<pre class="wp-block-preformatted">docker rum -it ubuntu /bin/bash</pre>



<p>也即启动交互式容器(前台有伪终端，等待交互)；</p>



<p>-P: 随机端口映射，大写P</p>



<p>-p: 指定端口映射，小写p</p>



<p>[pip:hostPort:containerPort | ip::containerPort | hostPort:containerPort]</p>



<p><code>ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort</code>。</p>



<h3 class="wp-block-heading">映射所有接口地址</h3>



<p>使用 <code>hostPort:containerPort</code> 格式本地的 80 端口映射到容器的 80 端口，可以执行</p>



<pre class="wp-block-code"><code>$ docker run -d -p 80:80 nginx:alpine</code></pre>



<p>此时默认会绑定本地所有接口上的所有地址。</p>



<h3 class="wp-block-heading">映射到指定地址的指定端口</h3>



<p>可以使用 <code>ip:hostPort:containerPort</code> 格式指定映射使用一个特定地址，比如 localhost 地址 127.0.0.1</p>



<pre class="wp-block-code"><code>$ docker run -d -p 127.0.0.1:80:80 nginx:alpine</code></pre>



<h3 class="wp-block-heading">映射到指定地址的任意端口</h3>



<p>使用 <code>ip::containerPort</code> 绑定 localhost 的任意端口到容器的 80 端口，本地主机会自动分配一个端口。</p>



<pre class="wp-block-code"><code>$ docker run -d -p 127.0.0.1::80 nginx:alpine</code></pre>



<p>还可以使用 <code>udp</code> 标记来指定 <code>udp</code> 端口</p>



<pre class="wp-block-code"><code>$ docker run -d -p 127.0.0.1:80:80/udp nginx:alpine</code></pre>



<h2 class="wp-block-heading">指令</h2>



<figure class="wp-block-table"><table><thead><tr><th>指令</th><th>解释</th><th>指令</th><th>解释</th></tr></thead><tbody><tr><td>docker image</td><td>查看本机镜像</td><td></td><td></td></tr><tr><td>docker search []</td><td>查询某个镜像</td><td>&#8211; -limit [num]</td><td>前num条</td></tr><tr><td>docker pull [镜像名字:TAG]</td><td>下载镜像</td><td>TAG默认latest</td><td>TAG可选，表示版本</td></tr><tr><td>docker system df</td><td>查看空间占用</td><td></td><td></td></tr><tr><td>docker rmi</td><td>删除某个镜像</td><td>-f</td><td>强制删除</td></tr><tr><td>docker ps</td><td>查看正在运行的容器</td><td></td><td></td></tr><tr><td>docker rename [原容器名] [新容器名]</td><td>容器改名</td><td></td><td></td></tr><tr><td>docker start [name]</td><td>启动容器</td><td></td><td></td></tr><tr><td>docker exec -it [name] /bin/bash</td><td>进入容器并创建新进程(常用)</td><td></td><td></td></tr><tr><td>docker attach [name]</td><td>进入容器</td><td></td><td></td></tr><tr><td>docker restart [name]</td><td>重启容器</td><td></td><td></td></tr><tr><td>docker stop [name]</td><td>停止容器</td><td></td><td></td></tr><tr><td>docker kill [name]</td><td>强制停止容器</td><td></td><td></td></tr><tr><td>docker rm [name]</td><td>删除容器</td><td></td><td></td></tr><tr><td>docker logs [name]</td><td>查看容器日志</td><td></td><td></td></tr><tr><td>docker top [name]</td><td>查看容器内部进程</td><td></td><td></td></tr><tr><td>docker instpect [name]</td><td>查看容器配置</td><td></td><td></td></tr><tr><td>docker cp [name]: 容器内路径 主机路径</td><td>从容器内拷贝文件到主机</td><td></td><td></td></tr><tr><td>docker tag IMAGE[:TAG] Host:Port[/username/image_name] [:TAG]</td><td>镜像改名</td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">退出容器</h2>



<ol class="wp-block-list"><li>exit：使用exec启动后不停止，使用docker attach 启动后会停止</li><li>control+p+q：退出后容器不停止</li></ol>



<h2 class="wp-block-heading">守护进程Docker</h2>



<p>docker容器后台运行，就必须有一个前台进程。</p>



<p>有些程序使用docker run -d 后关闭窗口不会退出（redis），而有些会退出（ubuntu）。</p>



<h2 class="wp-block-heading">容器与镜像备份</h2>



<ul class="wp-block-list"><li><code>docker save images_name</code>：将一个镜像导出为文件，再使用<code>docker load</code>命令将文件导入为一个镜像，会保存该镜像的的所有历史记录。比<code>docker export</code>命令导出的文件大，很好理解，因为会保存镜像的所有历史记录。</li><li><code>docker export container_id</code>：将一个容器导出为文件，再使用<code>docker import</code>命令将容器导入成为一个新的镜像，但是相比<code>docker save</code>命令，容器文件会丢失所有元数据和历史记录，仅保存容器当时的状态，相当于虚拟机快照。</li></ul>



<p>两者的区别在于容器快照将会丢弃所有的历史记录和元数据信息，而镜像存储文件将保存完整记录，体积也会更大。此外从容器快照文件导入时，也可以重新指定标签等元数据。</p>



<figure class="wp-block-table"><table><thead><tr><th>指令</th><th>解释</th></tr></thead><tbody><tr><td>docker export [name] &gt; xxx.tar</td><td>容器备份</td></tr><tr><td>cat xxx.tar | docker import &#8211; 镜像用户/镜像名:镜像版本号</td><td>恢复容器备份</td></tr><tr><td>docker save [name] &gt;xxx.tar</td><td>镜像备份</td></tr><tr><td>docker load &lt; xxx.tar</td><td>镜像恢复</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">提交新镜像</h2>



<p>docker commit -m=“提交的描述信息”-a=“作者” 容器ID 目标镜像名:[标签名]</p>



<pre class="wp-block-code"><code>docker commit -m="带vim的ubuntu" -a="Jellow" 9a588b87d1fb8232f6985d23735b73aec5d4bf9c577e7d1add0a3424f5f4acd5 jellow/ubuntu:1.0</code></pre>



<h2 class="wp-block-heading">本地镜像推送到私有库</h2>



<p>&lt;!&#8211;以本地搭建私有库为例&#8211;&gt;</p>



<ol class="wp-block-list"><li>首先获取registry镜像，负责管理私有库</li></ol>



<pre class="wp-block-code"><code>docker pull registry</code></pre>



<ol class="wp-block-list" start="2"><li>启动registry</li></ol>



<pre class="wp-block-code"><code>docker run -d -p 127.0.0.1:5000:5000  -v /tmp/myregistry:/tmp/registry --privileged=true registry</code></pre>



<ol class="wp-block-list" start="3"><li>本地镜像改名为符合私服格式的镜像</li></ol>



<p>docker tag IMAGE[:TAG] Host:Port[/username/image_name] [:TAG]</p>



<pre class="wp-block-code"><code>docker tag jellow/ubuntu:1.0 127.0.0.1:5000/jellow/ubuntu:1.1</code></pre>



<ol class="wp-block-list" start="4"><li>旧版本docker默认不允许http方式推送镜像，通过配置选项来取消这个限制</li></ol>



<p>注意：20.10.9版本后默认允许http推送，不用改配置</p>



<p>这里给一下更改示例（更改后重启docker）</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220820093922005.png" alt="image-20220820093922005"/></figure>



<pre class="wp-block-code"><code>"insecure-registries": &#91;<br> &nbsp;  "127.0.0.1:5000"<br>  ]</code></pre>



<ol class="wp-block-list" start="5"><li>推送到私有库</li></ol>



<pre class="wp-block-code"><code>docker push 127.0.0.1:5000/jellow/ubuntu:1.1</code></pre>



<ol class="wp-block-list" start="6"><li>查看目前的镜像</li></ol>



<pre class="wp-block-code"><code>curl -XGET http://127.0.0.1:5000/v2/_catalog</code></pre>



<p>可以看到已经成功添加 {&#8220;repositories&#8221;:[&#8220;jellow/ubuntu&#8221;]}</p>



<ol class="wp-block-list" start="7"><li>从私有仓库pull镜像</li></ol>



<p>这个指令其实和docker push就差了push????</p>



<pre class="wp-block-code"><code>docker pull 127.0.0.1:5000/jellow/ubuntu:1.1</code></pre>



<h2 class="wp-block-heading">容器卷</h2>



<h3 class="wp-block-heading">为什么有数据卷？</h3>



<p>docker中的数据在容器删除后就会消失</p>



<p>容器卷的作用是将主机目录和容器目录做映射，容器内的数据备份可以保存在主机目录，完成数据的持久化。</p>



<ol class="wp-block-list"><li>数据卷可在容器之间共享或重用数据</li><li>卷中的更改可以直接实时生效（不用再手动复制了）</li><li>数据卷中的更改不会包含在镜像的更新中</li><li>数据卷的生命周期一直持续到没有容器使用它为止</li></ol>



<h3 class="wp-block-heading">挂载目录权限设置</h3>



<p>Docker挂载主机目录访问如果出现cannot open directory .: Permission denied</p>



<p>解决办法：在挂载目录后多加一个&#8211;privileged=true参数即可</p>



<p>如果是CentOS7安全模块会比之前系统版本加强，不安全的会先禁止，所以目录挂载的情况被默认为不安全的行为，</p>



<p>在SELinux里面挂载目录被禁止掉了，如果要开启，我们一般使用&#8211;privileged=true命令，扩大容器的权限解决挂载目录没有权限的问题，也即使用该参数，container内的root拥有真正的root权限，否则，container内的root只是外部的一个普通用户权限。</p>



<h3 class="wp-block-heading">挂载容器卷</h3>



<pre class="wp-block-preformatted">docker run -it --privileged=true -v /宿主机目录:/容器内目录:[权限] [IMAGE]</pre>



<p>权限：默认为rw读写，ro为容器只读</p>



<h4 class="wp-block-heading">继承容器卷</h4>



<pre class="wp-block-preformatted">docker run -it --privileged=true --volumes-from [容器名] [IMAGE]</pre>



<h4 class="wp-block-heading">查看容器挂载路径</h4>



<p>使用docker inspect ，Mounts记录了挂载路径</p>



<p>Source为主机路径，Destination为容器路径</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220820154012667.png" alt="image-20220820154012667"/></figure>



<p>以后就可以使用Docker安装Redis，MongoDB等数据库软件，需要注意的是这些软件的配置信息和数据最好存储在数据卷中，防止容器删除后消失。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4260/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4260</post-id>	</item>
		<item>
		<title>Mac软件分享</title>
		<link>https://www.cztcode.com/2022/4170/</link>
					<comments>https://www.cztcode.com/2022/4170/#comments</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Thu, 07 Apr 2022 13:32:19 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4170</guid>

					<description><![CDATA[最近换了MacBookPro 14，感受到了性能和续航的双重幸福。Mac中有很多免费好用的软件，作为一名程序开发者我最近找到了很多好用的软件和大家分享，也欢迎大家补充????]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p>最近换了MacBookPro 14，感受到了性能和续航的双重幸福。Mac中有很多免费好用的软件，作为一名程序开发者我最近找到了很多好用的软件和大家分享，也欢迎大家补充????</p>



<h2 class="wp-block-heading">终端-Tabby-开源免费-支持M1</h2>



<p>官网：<a href="https://tabby.sh/" target="_blank" rel="noopener">https://tabby.sh</a></p>



<p>平时写后端肯定涉及连接服务器，对于终端软件我希望</p>



<ol class="wp-block-list"><li>界面要好看</li><li>实现一键连接服务器，不用输入密码</li><li>提供sftp功能</li></ol>



<p>先看看Tabby的界面，十分简洁</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/af40d0002d27979f81ad.png" alt="img"/></figure>



<p>Tabby提供个性化的主题，超级多的选择，满足颜值党一切幻想</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/Tabby%E4%B8%BB%E9%A2%98.gif" alt="Tabby主题"/></figure>



<p>智能标签页-检测到当前进程执行完成时会通知你，再也不用等着跑完程序了！</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/adaec9560c8f2cd9638b.png" alt="img"/></figure>



<p>自定义快捷键，想怎么定制都可以（虽然我习惯默认的）</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/8ee543105196ba16d5f3.png" alt="img"/></figure>



<p>支持连字符显示</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/98afd5c7e6c37df08d26.png" alt="img"/></figure>



<p>提供sftp功能，方便进行文件传输</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407151811913.png" alt="image-20220407151811913"/></figure>



<p>我之前是使用Termius的，但与Tabby相比Termius的功能没有Tabby丰富，界面也不如Tabby美观，而且它还是收费软件（虽然免费版也够用了），显然Tabby更好啊！</p>



<p>Tabby是我第一个推荐也是最想推荐的软件，开源免费，编程的同学必备????。</p>



<h2 class="wp-block-heading">录屏-Kap-开源免费-支持M1</h2>



<p>官网：<a href="https://getkap.co/" target="_blank" rel="noopener">https://getkap.co</a></p>



<p>这个软件很简单，如果你不喜欢OBS复杂的设置选这个没错。系统自带的录屏软件录制文件太大了，Kap可以兼顾录制质量和视频大小，还提供多种格式导出。</p>



<p>可以选择录制全屏幕或者某个窗口，或者是选定的屏幕范围</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407153836998.png" alt="image-20220407153836998"/></figure>



<p>支持导出GIF、H265、WebM格式</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407155318362.png" alt="image-20220407155318362"/></figure>



<h2 class="wp-block-heading">图片压缩-IMAGE SHRINKER-开源免费-Intel版本</h2>



<p>官网：<a href="https://image-shrinker.com/" target="_blank" rel="noopener">https://image-shrinker.com</a></p>



<p>虽然不支持M1，但作为一个图片压缩软件足够用了，还支持GIF压缩！</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407160455975.png" alt="image-20220407160455975"/></figure>



<p>需要图片压缩软件是因为想节省博客流量，毕竟CDN流量是要钱的。。。</p>



<h2 class="wp-block-heading">图片上传-PicGo-开源免费-支持M1</h2>



<p>官网：<a href="https://picgo.github.io/PicGo-Doc/" target="_blank" rel="noopener">https://picgo.github.io/PicGo-Doc/</a></p>



<ul class="wp-block-list"><li>支持拖拽图片上传</li><li>支持快捷键上传剪贴板里第一张图片</li><li>支持右键图片文件通过菜单上传 (v2.1.0+)</li><li>上传图片后自动复制链接到剪贴板</li><li>支持自定义复制到剪贴板的链接格式</li><li>支持修改快捷键，默认快速上传快捷键：<code>command+shift+p</code>（macOS）</li><li>支持插件系统，已有插件支持 Gitee、青云等第三方图床</li><li>支持通过发送 HTTP 请求调用 PicGo 上传（v2.2.0+)</li></ul>



<p>我用PicGo主要搭配Typora使用，在Typora中帮助我把复制的图片传到腾讯云COS中。自己的图片自己管理，避免图片挂掉和博客中的跨域问题。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407161921793.png" alt="image-20220407161921793"/></figure>



<h2 class="wp-block-heading">笔记-Typora-买断制89元-支持M1</h2>



<p>中文官网：<a href="https://typoraio.cn/" target="_blank" rel="noopener">https://typoraio.cn</a> 购买后永久授权，支持3台设备。</p>



<p>Typora的Beta版是免费的，如果你不想付费可以使用旧的Beta版。</p>



<p>我对于笔记软件的要求</p>



<ol class="wp-block-list"><li>界面美观</li><li>支持MarkDown</li><li>价格不能太贵</li><li>支持笔记导出</li><li>iPad，iPhone，Mac多端同步</li></ol>



<p>Typora可以做到前四条，至于多端同步，我之前认为这是必须要有的功能，因为的windows笔记本续航实在太差了，想要去自习室写东西实在是不方便，而且沉得要命。所以经常只带iPad去写笔记然后多端同步到windows上。</p>



<p>但是换了Mac后这续航太顶了????，iPad直接下岗。多端同步变成了一个不是很重要的需求，Typora凭借不是很贵的价格和美观的界面脱颖而出，我建议笔记位置放在icloud中，这样也具备了笔记备份功能（坚果云也可以进行文件夹同步）。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2016.27.40.png" alt="截屏2022-04-07 16.27.40"/></figure>



<p>主题是Rainbow，图片的圆角矩形和二级标题的颜色很好看。</p>



<h2 class="wp-block-heading">视频播放器-IINA-开源免费-支持M1</h2>



<p>官网：<a href="https://iina.io/" target="_blank" rel="noopener">https://iina.io</a></p>



<p>我本来用的是potplayerX，直到有一天在后台跑一个算法模型，看会下好的英剧等结果，结果视频开始卡顿了。。。最后发现是potplayerX解码太差了，开了硬件解码播放2k视频CPU占用率到了50%。换了IINA后相同视频CPU占用只有10%。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407170053558.png" alt="image-20220407170053558"/></figure>



<p>听很多人说这是Mac上最好的视频播放器</p>



<h2 class="wp-block-heading">鼠标滚动翻转-Scroll Reverser-开源免费-支持M1</h2>



<p>官网：<a href="https://pilotmoon.com/scrollreverser/" target="_blank" rel="noopener">https://pilotmoon.com/scrollreverser/</a></p>



<p>我喜欢Mac触控板开启自然滚动，鼠标不开启自然滚动，因为鼠标的自然滚动和Windows是相反的。但是Mac系统触控板开启自然滚动后鼠标也开启了，这俩设备不支持单独调整。</p>



<p>Scroll Reverser是一个简单的小插件，帮助实现分开控制滚动方向。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407170627194.png" alt="image-20220407170627194"/></figure>



<h2 class="wp-block-heading">图片除水印-Inpaint-免费-支持M1</h2>



<p>官网：<a href="https://theinpaint.com/" target="_blank" rel="noopener">https://theinpaint.com</a></p>



<p>去除图片水印，官网也提供在线去除功能。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407171359218.png" alt="image-20220407171359218"/></figure>



<h2 class="wp-block-heading">解压工具-The Unarchiver-免费-支持M1</h2>



<p>官网：<a href="https://theunarchiver.com/" target="_blank" rel="noopener">https://theunarchiver.com</a></p>



<p>默认解压缩工具<strong>只能解压或压缩 .zip 格式文件</strong>，所以还需要其他软件做补充。The Unarchiver 解压速度很快，基本上常见的格式都支持，用起来基本无感，有一种原生应用的感觉。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/unarchiver-screenshot-01.png" alt="img"/></figure>



<p>支持格式：</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407171825104.png" alt="image-20220407171825104"/></figure>



<h2 class="wp-block-heading">状态监控-Stats-开源免费-支持M1</h2>



<p>Github主页：<a href="https://github.com/exelban/stats" target="_blank" rel="noopener">https://github.com/exelban/stats</a></p>



<p>很好用的菜单栏状态显示软件，提供多种样式选择，电池还有详细信息预测充满时间。可以用来显示CPU，GPU，内存，磁盘，传感器，网络，蓝牙的信息。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/68747470733a2f2f7365726869792e73332e65752d63656e7472616c2d312e616d617a6f6e6177732e636f6d2f4769746875625f7265706f2f73746174732f706f7075707325334676322e332e322e706e673f7633-20220407172504583.png" alt="Stats"/></figure>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407172641932.png" alt="image-20220407172641932"/></figure>



<p>我用来显示内存，CPU，电池状态，数值的颜色根据数值大小变化，CPU占用高后数字会变红。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407172708732.png" alt="image-20220407172708732"/></figure>



<h2 class="wp-block-heading">电脑管家-CleanMyMac X-买断制148元-支持M1</h2>



<p>官网：<a href="https://macpaw.com/cleanmymac" target="_blank" rel="noopener">https://macpaw.com/cleanmymac</a> 淘宝148元永久授权</p>



<p>为数不多买的软件，这种软件适合减压</p>



<p>心理烦的时候就清理下电脑 心情瞬间好多了</p>



<p>提供垃圾清理，软件更新，扫描大文件删除等功能，也可以方便的卸载程序。</p>



<p>欢迎即友们推荐开源软件替代品。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407173606373.png" alt="image-20220407173606373"/></figure>



<h2 class="wp-block-heading">快捷键显示-键指如飞-免费-支持M1</h2>



<p>官网：<a href="https://www.better365.cn/FlyKey.html" target="_blank" rel="noopener">https://www.better365.cn/FlyKey.html</a></p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407173911653.png" alt="image-20220407173911653"/></figure>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407173940996.png" alt="image-20220407173940996"/></figure>



<h2 class="wp-block-heading">菜单栏软件隐藏-Hidden Bar-免费-支持M1</h2>



<p>Mac App Store：<a href="https://apps.apple.com/cn/app/hidden-bar/id1452453066?mt=12" target="_blank" rel="noopener">https://apps.apple.com/cn/app/hidden-bar/id1452453066?mt=12</a></p>



<p>写累了，少数派替我讲一下：<a href="https://zhuanlan.zhihu.com/p/100849553" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/100849553</a></p>



<p>MacBook Pro 14 有了刘海以后菜单栏只有一半了，所以这个软件重要性++</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407174359160.png" alt="image-20220407174359160"/></figure>



<h2 class="wp-block-heading">Safari翻译插件-Polyglot-开源免费-支持M1</h2>



<p>Mac App Store：<a href="https://apps.apple.com/us/app/polyglot/id1471801525?mt=12" target="_blank" rel="noopener">https://apps.apple.com/us/app/polyglot/id1471801525?mt=12</a></p>



<p>选中即可翻译，十分方便！</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407204521477.png" alt="image-20220407204521477"/></figure>



<h2 class="wp-block-heading">分屏-Rectangle-开源免费-支持M1</h2>



<p>官网：<a href="https://rectangleapp.com/" target="_blank" rel="noopener">https://rectangleapp.com</a></p>



<p>和某付费软件功能差不多，但这个免费开源。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2020.49.44.png" alt="截屏2022-04-07 20.49.44"/></figure>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2020.50.21.png" alt="截屏2022-04-07 20.50.21"/></figure>



<h2 class="wp-block-heading">休眠控制-Amphetamine-免费-支持M1</h2>



<p>Mac App Store：<a href="https://apps.apple.com/us/app/amphetamine/id937984704?mt=12" target="_blank" rel="noopener">https://apps.apple.com/us/app/amphetamine/id937984704?mt=12</a></p>



<p>你有没有挂着下载文件不想电脑休眠的时候。</p>



<p>你有没有跑者程序不想电脑休眠的时候。</p>



<p>很好，一个完全免费的软件，让你的Mac想什么时候睡就什么时候睡。<img decoding="async" alt="截屏2022-04-07 20.57.27" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2020.57.27.png"><img decoding="async" alt="截屏2022-04-07 20.55.40" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2020.55.40.png"></p>



<h2 class="wp-block-heading">休息提醒-Recess-部分免费-支持M1</h2>



<p>Mac App Store：<a href="https://apps.apple.com/us/app/recess/id621451282?mt=12" target="_blank" rel="noopener">https://apps.apple.com/us/app/recess/id621451282?mt=12</a></p>



<p>在菜单栏里显示一个小圆环，在设定时间到后提醒你休息。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407205903236.png" alt="image-20220407205903236"/></figure>



<h2 class="wp-block-heading">剪切板- uPaste-买断制29元-支持M1</h2>



<p>少数派：<a href="https://sspai.com/item/317" target="_blank" rel="noopener">https://sspai.com/item/317</a> 29元永久使用权</p>



<p>Mac为什么没有像Windows一样的剪切板呢！！！</p>



<p>剪切板它不好用吗！！！这个功能很复杂？？？</p>



<p>总之我不得不花钱买一个剪切板，很不爽，这本来是苹果自己该做的事情。</p>



<p>uPaste算是一个能用的软件，但不能说是好用，因为界面还不够简洁。</p>



<p>如果你用它的话，在屏幕左边有这样的小彩条（可以隐藏），记录你复制的内容。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/%E6%88%AA%E5%B1%8F2022-04-07%2021.05.11.png" alt="image-20220407210429190"/></figure>



<p>我认为Paste是所有剪切板中最简洁的，最美观的，可以作为一个剪切板它是订阅制，而且太贵了。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/keep.webp" alt="keep"/></figure>



<p>如果有美观好用的剪切板，欢迎大家推荐。</p>



<h2 class="wp-block-heading">最后</h2>



<p>新款MBP的刘海其实没有那么明显，尤其是你使用了顶部是黑色的壁纸，平时根本不会注意到它。</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/IMG_1587.min.JPG" alt="IMG_1587.min"/></figure>



<p>壁纸自取</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407212723012.png" alt="image-20220407212723012"/></figure>



<p>最近在开发APP，颜值是学习的动力????</p>



<figure class="wp-block-image"><img decoding="async" src="https://markdown.cztcode.com/image-20220407212903704.png" alt="image-20220407212903704"/></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4170/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4170</post-id>	</item>
		<item>
		<title>2022中科大先研院计专11408经验分享</title>
		<link>https://www.cztcode.com/2022/4162/</link>
					<comments>https://www.cztcode.com/2022/4162/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Fri, 01 Apr 2022 02:30:02 +0000</pubDate>
				<category><![CDATA[综合]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4162</guid>

					<description><![CDATA[很开心最后被中国科技大学先进技术研究院录取了，选择了心仪的导师，做了想做的方向！收到导师的录取邮件后感觉像做梦一样，自己实在是太幸运了。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div>
<p class="is-style-iw-2em">很开心最后被中国科技大学先进技术研究院录取了，选择了心仪的导师，做了想做的方向！收到导师的录取邮件后感觉像做梦一样，自己实在是太幸运了。</p>



<figure class="wp-block-image aligncenter size-full"><img decoding="async" width="1024" height="687" src="https://www.cztcode.com/wp-content/uploads/2023/05/image.png" alt="" class="wp-image-4829" srcset="https://www.cztcode.com/wp-content/uploads/2023/05/image.png 1024w, https://www.cztcode.com/wp-content/uploads/2023/05/image-300x201.png 300w, https://www.cztcode.com/wp-content/uploads/2023/05/image-150x101.png 150w, https://www.cztcode.com/wp-content/uploads/2023/05/image-768x515.png 768w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1000x671.png 1000w, https://www.cztcode.com/wp-content/uploads/2023/05/image-230x154.png 230w, https://www.cztcode.com/wp-content/uploads/2023/05/image-350x235.png 350w, https://www.cztcode.com/wp-content/uploads/2023/05/image-480x322.png 480w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="is-style-iw-2em">我在考研的过程中也参考了很多学长学姐的经验帖，如今成功上岸后也想要帮各位后来的学弟学妹们一把。各位学弟学妹们加油，努力才可以让自己变幸运。</p>



<h1 class="wp-block-heading">写在前面</h1>



<p class="is-style-iw-2em">首先我想说，考研是一个很难的过程。从想考研到最后录取要经过相当长的时间和数不清的困难，有很多困难让我感觉自己接近失败，数学到了十月发现自己做不了真题，英语还差一个星期上考场的时候发现自己背的模板痕迹太严重了&#8230;..但就是在自己最接近失败的时刻，咬牙坚持下去，抱着最后就算失败也要给自己一个交代的态度，拼尽自己的全力。当你坐在考场上奋笔疾书时就已经成功了一半了，答应了考研就不要给自己找失败的借口。这段经历是我大学里最宝贵的一段时光，是对自己的一次历练，以后遇到更多更大的困难时，我不会再轻言放弃。</p>



<p class="is-style-iw-2em">每个人的经历都是不同的，我的经验不代表另一个人可以复制，根据自己的情况总结经验哦。</p>



<h1 class="wp-block-heading">初试</h1>



<p class="is-style-iw-2em">网上资源很多，基本上任何网课你都可以免费找到，更新很及时不用担心。另外考研的资料一定是新的，不要去学长学姐那里要资料。</p>



<h2 class="wp-block-heading">政治</h2>



<p class="is-style-iw-2em">我政治69分不算高。政治讲究的是背！前松后紧。我是在8月开始准备政治的。首先买了精讲精练+1000题，然后看徐涛网课的马原部分，9月份买腿姐的冲刺背诵手册，照着这个梳理知识点（这本书一定要买），后期做肖秀荣8套卷练选择（大题不用做），4套卷背大题（背了前三套的大题）。12月买了腿姐的冲刺班和押题班（一定要看），跟着腿姐的押题讲义背诵大题（这个出的比肖秀荣4套卷早，腿姐和肖秀荣很多押题都是重合的，所以拿着这个背你可以比别人提前十天背大题！）最后所有大题都压中，写起来无压力。科大先研政治不算分啊，各位政治不用花太多心思。今年政治反押题比较严重，选择题就开始不按常规出牌，所以建议各位同学精讲精练+1000题一定要做透，基础要打扎实了。11月以前重点做马原和近代史部分，至少刷两遍。毛概和思修部分，还有时政是每年都有大量更新的，尤其22年开了20大，毛概和思修部分肯定很多和之前的教材是不一样的，所以暑假8月最好刷完马原，9月刷完近代史+毛概部分（毛概比较多，可以提前刷科学发展观那部分），10月可以二刷，11月刷剩下的。12月是政治的重点月份，这个月时政已经定型，大题也压的差不多了，可以开背！最后强调一下，不要只跟一个老师，肖老师已经开始被反压题了，我建议腿姐和肖老师结合起来看，腿姐的资料比肖秀荣出的早，照着腿姐冲刺讲义背大题，最后用4套卷查漏补缺。</p>



<h1 class="wp-block-heading">英语</h1>



<p class="is-style-iw-2em">英语我80分，说实话英语是这几科中花的心思最少的&#8230;.但成绩也是最满意的。核心秘诀&#8212;-背单词。</p>



<p class="is-style-iw-2em">各位不要去听什么网课了，包括背单词的网课，纯粹浪费时间。有一句话说得好，网课对于考研英语的作用：单词不会，什么技巧都没用；单词会了，什么技巧都没用。我最后考试就是纯看懂文章写答案啊（PS：六级500+也是靠背单词）。所以英语不好的同学们，你大概率是因为单词记得不熟，记得不多，所以狠狠的补单词。</p>



<figure class="wp-block-image size-full"><img decoding="async" width="800" height="1024" src="https://www.cztcode.com/wp-content/uploads/2023/05/image-1.png" alt="" class="wp-image-4830" srcset="https://www.cztcode.com/wp-content/uploads/2023/05/image-1.png 800w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-234x300.png 234w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-117x150.png 117w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-768x983.png 768w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-230x294.png 230w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-350x448.png 350w, https://www.cztcode.com/wp-content/uploads/2023/05/image-1-480x614.png 480w" sizes="(max-width: 800px) 100vw, 800px" /></figure>



<p class="is-style-iw-2em">考研英语5500词我大概是过了有3遍，你至少要过3遍。如果你准备考研的时间比较早，政治可以不看，数学可以做题比较少，但英语单词绝对是要重点准备的，每天坚持背单词。我是每天用扇贝单词背200个（不是纯新单词200个，里面100多个是复习的），刚开始大概要1个小时，后来背熟了半个小时就可以完成每日打卡。这里不推荐百词斩等等一切看图背单词的软件，单词软件的作用就是建立单词和你大脑想到某个词汇对应的关系，看图背单词光记图片去了。比如我看见apple就想到苹果，我看到中文的”苹果“也想到是苹果，完全是下意识的，也就是说你背单词要背到看见这个词就知道是啥意思，所以凡是犹豫的单词请一律点击不认识，像过于简单的单词一律放到简单词本，这些词不要再出现在复习计划中浪费时间。扇贝单词里面有三本考研单词书，先背2000多个词那个，背完一遍后在学习数据里熟练度达到4.0以上才可以开下一本书，最后背完5500词。最后全背完一遍就再继续复习，直到考研那天。</p>



<p class="is-style-iw-2em">我复习的时候还用了这本书，优点是每一页下面有这一页的单词回顾。这样很方便的复习昨天的内容，每天看一章，两个月一轮。（我真不是打广告。。。。）</p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" width="300" height="300" src="https://www.cztcode.com/wp-content/uploads/2023/05/image-2.png" alt="" class="wp-image-4832" srcset="https://www.cztcode.com/wp-content/uploads/2023/05/image-2.png 300w, https://www.cztcode.com/wp-content/uploads/2023/05/image-2-150x150.png 150w, https://www.cztcode.com/wp-content/uploads/2023/05/image-2-230x230.png 230w" sizes="(max-width: 300px) 100vw, 300px" /></figure>



<p class="is-style-iw-2em">总之单词很重要，熟练度和最后成绩成正比。除了单词第二重要的就是阅读，英语一要做01年到22年到真题，其中15年以前的完形填空完全不用做，我重复一遍完全不用做！因为之前到完形难度很大，做了没有什么意义。15年后完形简单的离谱，我最后就错了一个完形，这难度和高考没有什么区别。</p>



<p class="is-style-iw-2em">阅读理解一共四篇，8月份以前每天一篇就够了，计时做，我用的黄皮书（这里强调一下英语除了真题不要做任何模拟题，我没有做过任何一套模拟题）。如果你从3月开始准备复习，01年-09年真题可以做两遍。8月份以后开始10-15年真题每天一篇阅读，9月份开始就每个星期做一套真题（刚开始你可以只做阅读理解部分）。我这个时候做真题阅读理解部分错题控制在每套4个左右，这样就基本ok，后期英语学习时间不用那么多了。</p>



<p class="is-style-iw-2em">阅读理解是有技巧的，但也需要你自己总结。比如我上来先读两个段落，两个段落是肯定能做第一题。做阅读最重要的是有整体观，读懂作者立场。考研英语难在经常问“the author intends to….”，”the author suggest that…”，再或者这句话/段落在文中有什么意义，我们从某段中学到了/得出了什么结论。很多时候就不再像高考，四级，六级那样读懂了就能选对。问这种隐含意义的题就要把握这篇文章的结构，心里刻意的去理解作者想写什么，每个句子放在这里有什么目的，想要表达什么样的观点。而不是简简单单的看作者陈述了哪些事实/表面上的逻辑。 如果理解了作者的写作倾向答案就很好选，因为题里经常出现陈述与文章近乎一致的选项，有些选项迷惑性很大，你从某种角度解释它的确对，但作者写这个词/句/段从整体上看不是这个目的，所以在两个选项犹豫的时候，要看到整体而不能仅仅看局部。最直接的例子是，有时候第一题会问作者在第一段写了xxx的目的是？这种题仅仅看第一段肯定选不准，不管第一段用了什么例子写的怎么天花乱坠，引起下文最起码要看第二段想写什么，很难通过第一段的几句话猜到下面大段篇幅论证的论点。（除非它第一段最后写的很明白自己论点，但写这么清楚还能出成考研题吗…）再者有时候带有作者态度倾向的词经常被忽略，比如下面图片里答案的解读，如果不能把握作者态度，最后一道题What would be the best title for the text? 就很难选。尤其是这种给全文定基调的题自己做的时候特别要注意每个段落的作用，一#篇文章大段篇幅都要为作者态度/观点服务，把握好全局就很好选。</p>



<h1 class="wp-block-heading">数学</h1>



<p class="is-style-iw-2em">数学我只考了105啊，不算高，能考这个分纯粹因为我选填只错了俩。在此告诉各位千万不要轻敌，我中考数学满分，高考数学140+就以为高数是好欺负的，结果到10月了才发现做真题完全不行。数学一定要多做题，多总结。不管你跟的哪个老师，看视频用的时间一定不要超过做题用的时间，不要太依赖视频了，只看视频感觉自己会了，一做真题啥都不会。</p>



<p class="is-style-iw-2em">数学我只有失败的经验，各位学弟学妹记住一个词就好了，多做题。</p>



<h1 class="wp-block-heading">408</h1>



<p class="is-style-iw-2em">408考了120算是平均水平吧。我是跟着王道那4本书学的，我也建议你不需要买其他书，前期一个月一本书，做完后面的选择题，二刷的时候再做大题，最后再刷一遍看看还有什么不会的。408知识点很多，我建议你写一下思维导图或者做做笔记，经常在脑子里过一遍概念。408只做真题就可以了，如果不放心也可以做两套模拟题，模拟题的出题风格和水平和真题差距不小，仅供参考。</p>



<p class="is-style-iw-2em">408也和数学一样，多做题。</p>



<h1 class="wp-block-heading">复试</h1>



<p class="is-style-iw-2em">复试内容不能透露，建议本科多做做项目，多参加竞赛。我是做了一个国家级创新创业项目负责人，再加上对计算机很感兴趣，本科也是计算机，所以复试感觉不是很难。自我介绍，常见问题肯定是要特别熟悉的。</p>



<h1 class="wp-block-heading">写在最后</h1>



<p class="is-style-iw-2em">给导师发邮件要趁早，大家都是海王。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2022/4162/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4162</post-id>	</item>
	</channel>
</rss>
