<?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>Blog of Code</title>
	<atom:link href="https://www.cztcode.com/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>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>Golang for range 中 Slice 安全删除元素</title>
		<link>https://www.cztcode.com/2024/5332/</link>
					<comments>https://www.cztcode.com/2024/5332/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Tue, 30 Apr 2024 04:15:43 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5332</guid>

					<description><![CDATA[常见的Golang循环中删除Slice元素方法]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>为什么简单append不安全？</h1>
<p>平时开发经常遇到删除Slice中的元素，但像下面这样是不行的</p>
<pre><code class="language-golang">package main
import &quot;fmt&quot;
func main() {
        // 初始化包含重复数字的切片
        a := []int{1, 2, 3, 4, 5, 5, 6, 5, 7, 8, 9, 10}

        fmt.Println(&quot;初始切片:&quot;, a)

        // 使用 range 遍历切片，尝试删除所有的5
        for i, value := range a {
                if value == 5 {
                        // 删除当前元素
                        a = append(a[:i], a[i+1:]...)
                        fmt.Printf(&quot;删除元素后的切片（当前删除的元素5的索引: %d）: %v\n&quot;, i, a)
                }
        }

        fmt.Println(&quot;最终切片:&quot;, a)
}</code></pre>
<p>运行后输出：</p>
<pre><code class="language-golang">初始切片: [1 2 3 4 5 5 6 5 7 8 9 10]
删除元素后的切片（当前删除的元素5的索引: 4）: [1 2 3 4 5 6 5 7 8 9 10]
删除元素后的切片（当前删除的元素5的索引: 6）: [1 2 3 4 5 6 7 8 9 10]
最终切片: [1 2 3 4 5 6 7 8 9 10]</code></pre>
<p>漏了一个元素5没有删除<br />
原因就在遍历中删除元素后，后续的元素都向前移动了一位，但 range 循环的索引 i 仍然递增，导致实际上跳过了一个本应该检查的元素（因为它现在位于前一个索引位置）。</p>
<h1>解决方法</h1>
<h2>1. 使用索引操作</h2>
<p>可以在遍历时使用额外的索引来控制修改操作，以确保不会因为切片变化而影响遍历结果。</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        n := len(a) // 获取初始长度
        for i := 0; i &lt; n; i++ {
                if a[i] == 5 {
                        // 删除元素5，并调整切片长度
                        a = append(a[:i], a[i+1:]...)
                        // 减小n和i以适应新的切片长度
                        n--
                        i--
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}</code></pre>
<h2>2. 逆序遍历</h2>
<p>另一种方法是从后向前遍历和修改数组，这样即使数组大小变化，也不会影响到未遍历到的元素。</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        for i := len(a) - 1; i &gt;= 0; i-- {
                if a[i] == 5 {
                        a = append(a[:i], a[i+1:]...)
                }
        }

        for _, item := range a {
                fmt.Println(item)
        }
}</code></pre>
<h2>3. 新建一个数组</h2>
<p>这种方法缺点是要额外空间，但是更好理解和实现</p>
<pre><code class="language-golang">package main

import &quot;fmt&quot;

func main() {
        a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        // 创建一个新的切片用于存放删除特定元素后的结果
        var b []int

        for _, item := range a {
                if item != 5 {
                        b = append(b, item)
                }
        }

        // 输出新的切片，已经不包含元素5
        for _, item := range b {
                fmt.Println(item)
        }
}</code></pre>
<p>我比较喜欢第二种，简单的改成倒序就可以，也无需额外空间<br />
关于golang slice的更多细节和扩容介绍可以看我的博客<br />
<a href="https://www.cztcode.com/2023/5094/">https://www.cztcode.com/2023/5094/</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2024/5332/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5332</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>Golang中的切片</title>
		<link>https://www.cztcode.com/2023/5094/</link>
					<comments>https://www.cztcode.com/2023/5094/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Tue, 26 Dec 2023 07:52:36 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5094</guid>

					<description><![CDATA[本文主要深入讨论了Golang语言中切片的特性、定义和动态扩容机制，并对比了切片与数组的主要区别。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>Golang中的切片</h1>
<h3>1. 定义与特性</h3>
<ul>
<li><strong>切片（Slice）</strong> 是Go语言中一个关键的数据类型，它提供了一个比数组更灵活、更强大的序列接口。</li>
<li>切片并不存储任何数据，它只是对底层数组的引用。</li>
<li>切片可以动态增长和收缩，提供了比数组更高的灵活性。</li>
</ul>
<h3>2. 创建切片</h3>
<ul>
<li><strong>直接声明</strong>：例如 <code>var s []int</code>，这创建了一个nil切片。</li>
<li><strong>从数组创建</strong>：例如 <code>s := arr[start:end]</code>，这根据数组 <code>arr</code> 的一个区间创建切片。</li>
<li><strong>使用make函数</strong>：例如 <code>s := make([]int, length, capacity)</code>，这创建了一个具有指定长度和容量的切片。</li>
</ul>
<h3>3. 切片的长度和容量</h3>
<ul>
<li><strong>长度（Length）</strong>：切片中元素的数量。</li>
<li><strong>容量（Capacity）</strong>：从切片的第一个元素到其底层数组末尾的元素数量。</li>
<li>使用 <code>len(s)</code> 可以获取切片的长度，使用 <code>cap(s)</code> 可以获取切片的容量。</li>
</ul>
<h3>4. 切片操作</h3>
<ul>
<li><strong>重新切片</strong>：可以通过 <code>s = s[start:end]</code> 来调整切片的长度，只要它在原始切片的容量范围内。</li>
<li><strong>追加元素</strong>：<code>append</code> 函数可以向切片追加元素，如果超出容量，会自动扩展。</li>
<li><strong>拷贝切片</strong>：<code>copy</code> 函数可以用来拷贝切片中的元素到另一个切片。</li>
<li><strong>删除元素</strong>：
<ul>
<li>删除切片中的第i位元素: <code>s = append(s[:i], s[i+1:]...)</code></li>
<li>删除切片中的首个元素: <code>s = s[1:]</code></li>
<li>删除切片中的最后一个元素: <code>s = s[:len(s)-1]</code></li>
<li>删除切片中的第i个到j个元素: <code>s = append(s[:i], s[j:]...)</code></li>
</ul>
</li>
</ul>
<h3>5. 切片的内部结构</h3>
<ul>
<li>切片在Go的内部实现中是一个结构体，包含三个字段：
<ul>
<li><strong>指针</strong>：指向底层数组中切片指定的开始位置。</li>
<li><strong>长度</strong>：切片中元素的数量。</li>
<li><strong>容量</strong>：从切片的开始位置到底层数组的末尾的元素数量。</li>
</ul>
</li>
</ul>
<h3>6. 切片的注意事项</h3>
<ul>
<li>切片是引用类型，对切片的修改会影响到底层数组。</li>
<li>切片之间可以共享底层数组，修改一个切片可能会影响另一个切片。</li>
<li>切片的零值是nil，一个nil切片的长度和容量都是0，但它没有底层数组。</li>
</ul>
<h3>7. 使用场景</h3>
<ul>
<li>切片非常适合用于处理动态序列数据。</li>
<li>在函数间传递切片比传递数组更高效，因为切片是引用传递。</li>
<li>切片广泛用于Go标准库中，如字符串处理、文件操作、网络编程等。</li>
</ul>
<h3>示例代码</h3>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    // 创建切片
    s := make([]int, 0, 5)

    // 追加元素
    s = append(s, 1, 2, 3)

    // 获取长度和容量
    fmt.Println(&quot;Length:&quot;, len(s), &quot;Capacity:&quot;, cap(s))

    // 切片操作
    s2 := s[1:3]
    fmt.Println(s2)
}
</code></pre>
<p>当多个切片共享同一个底层数组时，对其中一个切片所做的修改可能会影响其他切片。这是因为切片仅是对数组片段的引用，并不拥有数组中的数据。下面详细介绍这个特性：</p>
<h3>共享底层数组的特性</h3>
<ol>
<li><strong>创建共享底层数组的切片</strong>：
<ul>
<li>当你从一个已存在的切片或数组创建新切片时，新切片会引用相同的底层数组。</li>
<li>例如，<code>newSlice := originalSlice[start:end]</code> 创建了一个新切片 <code>newSlice</code>，它引用 <code>originalSlice</code> 的底层数组。</li>
</ul>
</li>
<li><strong>影响范围</strong>：
<ul>
<li>修改任一切片中的元素，如果这些元素在共享的底层数组中，其他切片也会受到影响。</li>
<li>例如，如果 <code>newSlice[0]</code> 是 <code>originalSlice[2]</code> 的同一元素，修改 <code>newSlice[0]</code> 也会改变 <code>originalSlice[2]</code> 的值。</li>
</ul>
</li>
<li><strong>容量与边界</strong>：
<ul>
<li>切片的容量是指从其起始元素到底层数组末尾的元素个数。因此，即使两个切片共享相同的底层数组，它们的容量可能不同，这取决于各自的起始位置。</li>
<li>一个切片对底层数组的修改可能超出其长度但在容量范围内，这可能会影响到其他切片。</li>
</ul>
</li>
</ol>
<h3>示例：共享底层数组的影响</h3>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    // 创建一个数组
    arr := [5]int{1, 2, 3, 4, 5}

    // 创建切片，共享相同的底层数组
    slice1 := arr[1:4]  // 包含元素 2, 3, 4
    slice2 := arr[2:5]  // 包含元素 3, 4, 5

    // 修改slice1的一个元素
    slice1[1] = 100  // 将元素3改为100

    // 打印两个切片和原始数组
    fmt.Println(&quot;slice1:&quot;, slice1)  // 输出：slice1: [2 100 4]
    fmt.Println(&quot;slice2:&quot;, slice2)  // 输出：slice2: [100 4 5]
    fmt.Println(&quot;arr:&quot;, arr)        // 输出：arr: [1 2 100 4 5]
}
</code></pre>
<h3>注意事项</h3>
<ul>
<li>当函数接收切片参数时，如果函数内部修改了切片元素，调用该函数的外部环境中相应的切片也会受到影响。</li>
<li>在需要独立修改数据而不影响原始数据时，应考虑使用 <code>copy</code> 函数来创建一个新的切片副本。</li>
</ul>
<p>理解这一点对于避免数据共享带来的意外副作用非常重要，尤其是在并发编程和复杂数据结构处理中。</p>
<h1>数组与切片的区别</h1>
<h3><strong>数组的定义</strong></h3>
<p>数组的定义需要指定元素的数量（长度固定），格式为 <strong><code>[n]T</code></strong>，其中 <strong><code>n</code></strong> 是数组的长度，<strong><code>T</code></strong> 是数组中元素的类型。例如：</p>
<ul>
<li>定义一个长度为5的整数数组：<strong><code>var arr [5]int</code></strong></li>
<li>初始化数组时指定所有元素的值：<strong><code>arr := [5]int{1, 2, 3, 4, 5}</code></strong></li>
<li>让编译器根据初始化值的数量自动确定数组长度：<strong><code>arr := [...]int{1, 2, 3, 4, 5}</code></strong></li>
</ul>
<h3><strong>切片的定义</strong></h3>
<p>切片的定义不需要指定元素的数量，格式为 <strong><code>[]T</code></strong>，只指定元素类型 <strong><code>T</code></strong>。切片的长度和容量在运行时可以改变。例如：</p>
<ul>
<li>定义一个整数切片：<strong><code>var s []int</code></strong></li>
<li>使用 <strong><code>make</code></strong> 创建切片并指定长度和容量：<strong><code>s := make([]int, length, capacity)</code></strong></li>
<li>从数组或另一个切片初始化切片：<strong><code>s := arr[start:end]</code></strong></li>
</ul>
<h3><strong>关键区别</strong></h3>
<ul>
<li><strong>长度</strong>：数组的长度是其类型的一部分，一旦声明就不能改变。切片的长度是动态的，可以通过 <strong><code>append</code></strong> 和重新切片操作来改变。</li>
<li><strong>类型表示</strong>：在类型表示上，数组需要在类型前指定长度（例如 <strong><code>[5]int</code></strong>），而切片不需要（例如 <strong><code>[]int</code></strong>）。</li>
</ul>
<p>这些区别导致了数组和切片在Go语言中的使用方式和场景上的不同。<em>arrays</em> 通常用于较小的或固定大小的数据集合，而 <em>slices</em> 用于更加动态和灵活的数据处理。</p>
<p>数组和切片在Go语言中都用于存储序列数据，但它们在使用上有明显的区别：</p>
<ol>
<li><strong>长度的固定性</strong>：
<ul>
<li><strong>数组</strong>：长度固定，声明时需指定数组的大小，且大小不能改变。</li>
<li><strong>切片</strong>：长度可变，可以动态地增加或减少元素。</li>
</ul>
</li>
<li><strong>数据类型</strong>：
<ul>
<li><strong>数组</strong>：是值类型，赋值和作为参数传递时会复制整个数组。</li>
<li><strong>切片</strong>：是引用类型，赋值和作为参数传递时只会复制切片本身（底层数组不会被复制）。</li>
</ul>
</li>
<li><strong>内存分配</strong>：
<ul>
<li><strong>数组</strong>：在编译时分配固定大小的内存。</li>
<li><strong>切片</strong>：可以在运行时动态分配和调整大小。</li>
</ul>
</li>
<li><strong>性能考虑</strong>：
<ul>
<li><strong>数组</strong>：由于是值类型，较大的数组在作为参数传递时可能会有性能问题。</li>
<li><strong>切片</strong>：更适合作为参数传递，因为只传递引用。</li>
</ul>
</li>
</ol>
<h2>作为函数参数传递</h2>
<h3>数组</h3>
<p>当数组作为参数传递给函数时，会发生数组的整体拷贝。也就是对于数组的修改不会影响到原来的数组。对于较大的数组，这可能导致性能问题。</p>
<h3>切片</h3>
<p>当切片作为函数参数传递时，实际上传递的是切片的副本。这个副本引用的是同一个底层数组。这意味着：</p>
<ul>
<li><strong>传递值</strong>：虽然切片本身是以值的形式传递的（即传递切片的副本），但由于切片内部包含对底层数组的引用，所以对切片内容的修改会影响原始切片。</li>
<li><strong>改变元素</strong>：如果在函数内部改变了切片的元素，这些改变会反映到原始切片上。</li>
<li><strong>改变大小</strong>：如果尝试改变切片的大小（例如，通过 <code>append</code> 函数添加元素），情况就会变得复杂：
<ul>
<li>如果改变后的大小不超过原切片的容量，那么这个改变也会反映到原始切片上。</li>
<li>如果改变导致切片扩容（即超出原有容量），则会分配一个新的底层数组。这时，函数内部的切片副本会指向这个新的数组，而外部的原始切片仍然指向旧的数组。</li>
</ul>
</li>
</ul>
<h3>传递切片的引用</h3>
<p>在Go中，没有直接的方式来传递切片的“引用”。但如果您想在函数内部改变切片的长度并反映到原始切片，您可以：</p>
<ul>
<li>返回修改后的切片，并在调用函数后使用这个返回值。</li>
<li>使用指向切片的指针作为参数（虽然这不是常见的做法）。</li>
</ul>
<h3>示例</h3>
<p>这个例子展示了当切片作为函数参数传递时，如何改变其大小：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func appendSlice(slice []int, values ...int) []int {
    return append(slice, values...)
}

func main() {
    originalSlice := []int{1, 2, 3}
    modifiedSlice := appendSlice(originalSlice, 4, 5)

    fmt.Println(&quot;Original Slice:&quot;, originalSlice)
    fmt.Println(&quot;Modified Slice:&quot;, modifiedSlice)
}
</code></pre>
<p>在这个例子中，<code>appendSlice</code> 函数返回一个新的切片，这个切片可能引用了一个新的底层数组（如果进行了扩容的话）。这样，可以在函数外部接收这个改变后的切片。</p>
<p>总结来说，切片作为函数参数传递时，虽然传递的是切片的副本，但由于切片内部包含对底层数组的引用，所以对其元素的修改会影响到原始切片。但是，如果需要改变切片的大小，并希望这种改变反映到原切片上，需要特别处理，比如通过返回新切片或使用指向切片的指针。</p>
<h1>切片的扩容机制</h1>
<p>切片的扩容是Go语言切片最重要的特性之一。切片的扩容过程如下：</p>
<ol>
<li><strong>初始容量</strong>：当使用 <code>make</code> 函数创建切片时，可以指定切片的初始容量。如果未指定，切片的容量默认为其长度。</li>
<li><strong>追加元素</strong>：当使用 <code>append</code> 函数向切片追加元素，且当前切片容量不足以容纳更多元素时，切片会自动扩容。</li>
</ol>
<h2>Go 1.17版本切片扩容</h2>
<p>Go 1.17切片扩容时会进行内存对齐，这个和内存分配策略相关。进行内存对齐之后，新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍。</p>
<ul>
<li>当新切片需要的容量cap大于两倍扩容的容量，则直接按照新切片需要的容量扩容；</li>
<li>当原 slice 容量 &lt; 1024 的时候，新 slice 容量变成原来的 2 倍；</li>
<li>当原 slice 容量 &gt; 1024，进入一个循环，每次容量变成原来的1.25倍,直到大于期望容量。</li>
</ul>
<h2>Go 1.18版本切片扩容</h2>
<p>Go1.18不再以1024为临界点，而是设定了一个值为256的<code>threshold</code>，以256为临界点；超过256，不再是每次扩容1/4，而是每次增加（旧容量+3*256）/4；</p>
<ul>
<li>当新切片需要的容量cap大于两倍扩容的容量，则直接按照新切片需要的容量扩容；</li>
<li>当原 slice 容量 &lt; threshold 的时候，新 slice 容量变成原来的 2 倍；</li>
<li>当原 slice 容量 &gt; threshold，进入一个循环，每次容量增加（旧容量+3*threshold）/4。</li>
</ul>
<p>最后还有一个内存对齐步骤，实际容量会比这个公式计算出来的大。</p>
<h3>内存对齐</h3>
<p>内存对齐是计算机硬件对内存访问的一种优化。对于现今的计算机硬件设计，内存是按照一个固定大小（如4 bytes, 8 bytes, 16 bytes等）的块来读取或者写入的。如果数据没有按照这样的块边界来存放，硬件可能需要多次访问内存才能读取或者写入数据，这会降低程序的性能。</p>
<p>以4 bytes为例，如果我们要读取一个4 bytes的数据，在内存中的起始地址如果不是4的倍数，硬件需要两次读取操作才能获取到完整的4 bytes数据。第一次读取会获取到数据的一部分，然后第二次读取才能获取到剩余的数据。这是因为硬件只能从地址是4的倍数的地方开始读取4 bytes的数据。</p>
<p>反之，如果数据在内存中的起始地址是4的倍数，硬件只需要一次读取操作就能获取到完整的4 bytes数据。这就是为什么内存对齐可以提高性能的原因。</p>
<p>因此在Go 1.18版本中，切片的扩容操作在确定扩容容量后，还会做一个内存对齐的步骤，确保扩容后的切片在内存中的存放位置能满足硬件的最优访问需求。</p>
<h3>源代码注释</h3>
<pre><code class="language-jsx">growth formula a bit smoother
Instead of growing 2x for &lt; 1024 elements and 1.25x for &gt;= 1024 elements,
use a somewhat smoother formula for the growth factor. Start reducing
the growth factor after 256 elements, but slowly.

starting cap    growth factor
256             2.0
512             1.63
1024            1.44
2048            1.35
4096            1.30

(Note that the real growth factor, both before and now, is somewhat
larger because we round up to the next size class.)

This CL also makes the growth monotonic (larger initial capacities
make larger final capacities, which was not true before). See discussion
at https://groups.google.com/g/golang-nuts/c/UaVlMQ8Nz3o

256 was chosen as the threshold to roughly match the total number of
reallocations when appending to eventually make a very large
slice. (We allocate smaller when appending to capacities [256,1024]
and larger with capacities [1024,...]).</code></pre>
<p>通过减小阈值并固定增加一个常数，使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变，该commit作者给出了几种原始容量下对应的“扩容系数”：</p>
<table>
<thead>
<tr>
<th>oldcap</th>
<th>扩容系数</th>
</tr>
</thead>
<tbody>
<tr>
<td>256</td>
<td>2.0</td>
</tr>
<tr>
<td>512</td>
<td>1.63</td>
</tr>
<tr>
<td>1024</td>
<td>1.44</td>
</tr>
<tr>
<td>2048</td>
<td>1.35</td>
</tr>
<tr>
<td>4096</td>
<td>1.30</td>
</tr>
</tbody>
</table>
<p>可以看到，Go1.18的扩容策略中，随着容量的增大，其扩容系数是越来越小的，可以更好地节省内存。注意这个不是一个间断函数，而是连续函数，上面的表只是在oldcap取不同值时计算出来的“扩容系数”，实际上对于不同的oldcap，扩容系数是逐渐变化的。当oldcap远大于256的时候，扩容系数将会无限接近1.25。</p>
<h3>测试扩容</h3>
<pre><code class="language-go">package main

import (
    &quot;testing&quot;
)

func TestCap(t *testing.T) {
    var s []int
    lastCap := cap(s)

    for i := 0; i &lt; 1000; i++ {
        s = append(s, i)
        newCap := cap(s)

        if newCap != lastCap {
            expectedCap := calculateExpectedCap(lastCap, i+1) // i+1 是因为添加了一个新元素
            t.Logf(&quot;Added element %d, length: %d, actual capacity: %d, expected capacity: %d\n&quot;, i, len(s), newCap, expectedCap)
            lastCap = newCap
        }
    }
}

func calculateExpectedCap(oldcap, cap int) int {
    threshold := 256
    if cap &gt; oldcap*2 {
        return cap
    }
    if oldcap &lt; threshold {
        return oldcap * 2
    }
    return oldcap + (oldcap+3*threshold)/4
}</code></pre>
<p>输出结果</p>
<pre><code class="language-jsx">    cap_test.go:17: Added element 0, length: 1, actual capacity: 1, expected capacity: 1
    cap_test.go:17: Added element 1, length: 2, actual capacity: 2, expected capacity: 2
    cap_test.go:17: Added element 2, length: 3, actual capacity: 4, expected capacity: 4
    cap_test.go:17: Added element 4, length: 5, actual capacity: 8, expected capacity: 8
    cap_test.go:17: Added element 8, length: 9, actual capacity: 16, expected capacity: 16
    cap_test.go:17: Added element 16, length: 17, actual capacity: 32, expected capacity: 32
    cap_test.go:17: Added element 32, length: 33, actual capacity: 64, expected capacity: 64
    cap_test.go:17: Added element 64, length: 65, actual capacity: 128, expected capacity: 128
    cap_test.go:17: Added element 128, length: 129, actual capacity: 256, expected capacity: 256
    cap_test.go:17: Added element 256, length: 257, actual capacity: 512, expected capacity: 512
    cap_test.go:17: Added element 512, length: 513, actual capacity: 848, expected capacity: 832
    cap_test.go:17: Added element 848, length: 849, actual capacity: 1280, expected capacity: 1252
--- PASS: TestCap (0.00s)
PASS</code></pre>
<h3>为什么实际扩容与预期不符呢？</h3>
<p>实际上，growslice 的后半部分还有更进一步的优化（内存对齐等），靠的是 roundupsize 函数，在计算完 newcap 值之后，还会有一个步骤计算最终的容量：</p>
<pre><code class="language-go">capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)</code></pre>
<p><strong><code>growslice</code></strong> 函数在 Go 中的实现不仅仅是简单的容量增长算法，它还涉及到内存对齐的优化，这是通过 <strong><code>roundupsize</code></strong> 函数来实现的。<strong><code>roundupsize</code></strong> 函数确保分配的内存大小是特定大小类的倍数，这有助于减少内存碎片和提高内存分配效率。我们无法直接模拟 <strong><code>roundupsize</code></strong> 函数的行为，因为这需要深入 Go 的内存分配器的实现细节。</p>
<p>但是确定的是，最终的容量可能会比计算出的 <strong><code>newcap</code></strong> 更大，因为要适应内存对齐的需求。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/5094/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5094</post-id>	</item>
		<item>
		<title>Golang中的map与线程安全</title>
		<link>https://www.cztcode.com/2023/5075/</link>
					<comments>https://www.cztcode.com/2023/5075/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Wed, 13 Dec 2023 12:28:26 +0000</pubDate>
				<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5075</guid>

					<description><![CDATA[golang中的map与线程安全 map介绍 ​ Golang中Map存储的是kv键值对，采用哈希表作为底层实现，用拉链法解决hash冲突。 基本特性 键值对存储：map 用于存储键值对，其中每个键都是唯一的，且映射到一个特定的值。 动态类型：在 Go 中，map 的键和值可以是任意类型，但所有键必须是相同的类型，所有值也必须是相同的类型。 动态大小：map 的大小是动态的，可以根据需要增长或缩减 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>golang中的map与线程安全</h1>
<h2>map介绍</h2>
<p>​   Golang中Map存储的是kv键值对，采用哈希表作为底层实现，用拉链法解决hash冲突。</p>
<h3>基本特性</h3>
<ol>
<li><strong>键值对存储</strong>：<code>map</code> 用于存储键值对，其中每个键都是唯一的，且映射到一个特定的值。</li>
<li><strong>动态类型</strong>：在 Go 中，<code>map</code> 的键和值可以是任意类型，但所有键必须是相同的类型，所有值也必须是相同的类型。</li>
<li><strong>动态大小</strong>：<code>map</code> 的大小是动态的，可以根据需要增长或缩减。这意味着你不需要事先知道 <code>map</code> 将存储多少元素。</li>
<li><strong>无序集合</strong>：<code>map</code> 是一个无序的数据结构，这意味着你不能指望键值对的存储或检索顺序。在理想情况下（即哈希函数分布均匀），键的访问（检索和更新）操作的时间复杂度是 O(1)。这意味着无论 <code>map</code> 中存储了多少元素，访问一个键所需的时间大致相同。</li>
<li><strong>高效的键访问</strong>：<code>map</code> 提供了高效的键访问。通过键，你可以快速检索或更新对应的值。</li>
<li><strong>哈希表实现</strong>：底层使用哈希表来实现，这使得 <code>map</code> 在大多数情况下能够提供快速的查找、添加和删除操作。
<ol>
<li>查找操作：通常是 O(1)。但在最坏的情况下（所有键都映射到同一个桶中），时间复杂度可能会退化到 O(n)。</li>
<li>添加操作：同样，理想情况下是 O(1)，但在需要扩容时（即当存储元素的数量超过当前容量的负载因子时），时间复杂度可能会暂时增加，因为需要重新哈希现有的元素到新的存储空间。</li>
<li>删除操作：理想情况下也是 O(1)。</li>
</ol>
</li>
<li><strong>并发使用注意</strong>：Go 的 <code>map</code> 在没有额外同步机制的情况下并不是并发安全的。如果你需要在多个协程（goroutine）中并发访问 <code>map</code>，需要使用互斥锁（mutex）或者其他同步技术来避免并发冲突。</li>
<li><strong>内存效率</strong>：由于 <code>map</code> 使用动态扩容机制，因此在使用时相对内存高效，只有在需要更多空间时才会扩展。</li>
</ol>
<p>对于map的实现，golang源码是这样介绍的</p>
<pre><code class="language-golang">// A map is just a hash table. The data is arranged
// into an array of buckets. Each bucket contains up to
// 8 key/value pairs. The low-order bits of the hash are
// used to select a bucket. Each bucket contains a few
// high-order bits of each hash to distinguish the entries
// within a single bucket.
//
// If more than 8 keys hash to a bucket, we chain on
// extra buckets.</code></pre>
<h2>基础使用</h2>
<h3>插入方法</h3>
<pre><code class="language-golang">// 声明一个 map
var map1 map[keyType]valueType

// 使用 make 函数初始化
map1 = make(map[keyType]valueType)

//初始化时指定大小，超过时依然会自动扩容
m := make(map[keyType]valueType, initialCapacity)

// 简洁声明并初始化
map2 := make(map[keyType]valueType)

// 初始化时直接填充数据
map3 := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}</code></pre>
<h3>读取方法</h3>
<p>读取Map中的元素时，Go提供了一种独特的方式来检查元素是否存在。例如：</p>
<pre><code>golang
value, ok := map1[key]</code></pre>
<p>这里，<code>value</code> 是与键对应的值，而 <code>ok</code> 是一个布尔值，表示该键是否在Map中。如果键存在，<code>ok</code> 为 <code>true</code>，否则为 <code>false</code>。</p>
<h3>遍历方法</h3>
<p>遍历Map的常见方法是使用 <code>for</code> 循环：</p>
<pre><code class="language-golang">for key, value := range map1 {
    fmt.Println(&quot;Key:&quot;, key, &quot;Value:&quot;, value)
}</code></pre>
<p>但是，Go中的Map是无序的。如果你需要按顺序遍历，可以使用两种方法：使用 <code>orderedmap</code> 包或先读取键，对它们排序，然后按顺序遍历。</p>
<h4>使用 orderedmap</h4>
<p><code>orderedmap</code> 是一个第三方包，允许你按照插入顺序遍历Map。使用之前，需要先安装这个包。</p>
<pre><code class="language-golang">import &quot;github.com/wk8/go-ordered-map&quot;

omap := orderedmap.New()
// 后续操作与普通map类似</code></pre>
<h4>读取键并排序</h4>
<p>如果不想使用第三方库，可以先提取所有键，对它们排序，然后按顺序访问Map：</p>
<pre><code class="language-golang">var keys []int
for k := range map1 {
    keys = append(keys, k)
}
sort.Ints(keys) // 对键进行排序

for _, k := range keys {
    fmt.Println(&quot;Key:&quot;, k, &quot;Value:&quot;, map1[k])
}</code></pre>
<h2>桶</h2>
<p>​   Go <code>map</code> 的核心是由一系列称为“桶”的数据结构组成的。每个桶可以存储多个键值对（通常是 8 个）。这些桶的目的是通过哈希函数将键均匀地分散到各个桶中，从而减少哈希冲突。</p>
<p>bmap结构，即桶，是map中最重要的底层实现之一，其设计要点如下：</p>
<ul>
<li><strong>桶是map中最小的挂载粒度</strong>：map中不是每一个key都申请一个结构通过链表串联，而是每8个kv键值对存放在一个桶中，然后桶再通以链表的形式串联起来，这样做的原因就是减少对象的数量，减轻gc的负担。</li>
<li><strong>桶串联实现拉链法</strong>：当某个桶数量满了，会申请一个新桶，挂在这个桶后面形成链表，新桶优先使用预分配的桶。</li>
<li><strong>哈希高8位优化桶查找key</strong> : 将key哈希值的高8位存储在桶的tohash数组中，这样查找时不用比较完整的key就能过滤掉不符合要求的key，tohash中的值相等，再去比较key值</li>
<li><strong>桶中key/value分开存放</strong> ： 桶中所有的key存一起，所有的value存一起，目的是为了方便内存对齐</li>
<li><strong>根据k/v大小存储不同值</strong> ： 当k或v大于128字节时，其存储的字段为指针，指向k或v的实际内容，小于等于128字节，其存储的字段为原值</li>
<li><strong>桶的搬迁状态</strong> ： 可以根据tohash字段的值，是否小于minTopHash，来表示桶是否处于搬迁状态</li>
</ul>
<h2>负载因子（Load Factor）</h2>
<h3>传统负载因子</h3>
<h4>定义与基本概念</h4>
<ul>
<li>
<p><strong>负载因子</strong>是用来衡量哈希表中空间占用率的一个核心指标。它定义为哈希表中已存储的元素数量与哈希表的总位置（槽数）数量的比例。数学上表示为：</p>
<p><code> $$ \text{负载因子} = \frac{\text{已存储的元素数量}}{\text{哈希表的总位置数量}} $$</code></p>
</li>
<li>
<p><strong>衡量指标</strong>：负载因子是衡量哈希表效率和空间利用的关键指标。它反映了表中有多少位置被占用，以及还有多少位置可用。</p>
</li>
</ul>
<h4>重要性</h4>
<ul>
<li>
<p><strong>性能影响</strong>：负载因子的值直接影响哈希表的性能。一个较高的负载因子意味着哈希表的位置被大量占用，可能增加哈希冲突，降低查找效率。而一个较低的负载因子意味着哈希表有许多空闲位置，这可能导致内存的浪费。</p>
</li>
<li>
<p><strong>扩容触发点</strong>：在哈希表的实现中，通常会设置一个负载因子的阈值。当表中的负载因子超过这个阈值时，哈希表将进行扩容，这通常包括分配更大的存储空间并重新分配（rehash）现有元素。</p>
</li>
</ul>
<h3>Go 语言中的负载因子</h3>
<p>​   在 Go 语言的 <code>map</code> 实现中，负载因子与桶（bucket）的平均填充元素数相关联。这里的负载因子并不是传统意义上的哈希表负载因子，而是与每个桶的平均键值对数量相关的一个特定实现细节。</p>
<p><code class="katex-inline">\text{负载因子} = \frac{\text{已存储的元素数量}}{\text{哈希表中桶的数量}}</code></p>
<ul>
<li>
<p><strong>桶的平均元素数量</strong>：在 Go 语言中，当每个桶的平均填充元素数量达到特定值（如 6.5）时，<code>map</code> 将进行扩容。这意味着每个桶平均存储约 6.5 个键值对。</p>
</li>
<li>
<p><strong>扩容策略</strong>：这种特定的扩容策略有助于保持 <code>map</code> 性能的平衡。它旨在优化内存使用和查找效率之间的关系，减少哈希冲突的同时避免过度占用内存空间。</p>
</li>
</ul>
<h2>哈希表（Hash Table）</h2>
<p>​   哈希表是一种用于存储键值对的数据结构，它通过一个哈希函数将键映射到表中的一个位置（称为“槽”或“桶”）来存储数据。哈希表的主要特点是它可以提供快速的数据查找，通常是常数时间复杂度 <em>O</em>(1)。哈希表的效率高度依赖于哈希函数的质量和处理冲突的方式。</p>
<h3>哈希函数</h3>
<p>​   哈希函数是哈希表的核心，它将输入（键）转换为一个整数，这个整数通常用作数组的索引。理想的哈希函数应该满足以下条件：</p>
<ul>
<li>快速计算</li>
<li>哈希值均匀分布</li>
<li>相同的输入总是产生相同的哈希值</li>
<li>不同的输入尽量产生不同的哈希值（尽管完全避免冲突是不可能的）</li>
</ul>
<h3>处理冲突</h3>
<p>​   当两个不同的键产生相同的哈希值时，就发生了冲突。处理哈希冲突的常见方法有两种：开放寻址法和拉链法。</p>
<p>​   开放寻址法（Open Addressing）是处理哈希表中冲突的另一种常用技术。与拉链法不同，开放寻址法不使用链表来存储冲突的元素，而是在哈希表数组本身中寻找空闲位置来存储这些元素。这种方法直接在哈希表的数组中存储所有元素，不需要额外的数据结构。</p>
<h3>开放寻址法</h3>
<h4>工作原理</h4>
<ol>
<li>
<p><strong>插入操作</strong>：当一个新元素需要插入哈希表时，先使用哈希函数计算其索引。如果该索引对应的位置已被占用（发生冲突），则根据某种探测序列（probe sequence）在表中寻找下一个空闲位置。</p>
</li>
<li>
<p><strong>探测序列</strong>：探测序列定义了在发生冲突时如何在哈希表中搜索空闲位置。常见的探测方法包括线性探测、二次探测和双重哈希探测。</p>
<ul>
<li><strong>线性探测（Linear Probing）</strong>：连续检查下一个槽位，直到找到空位。</li>
<li><strong>二次探测（Quadratic Probing）</strong>：探测间隔以二次方增加（例如，1, 4, 9, 16, &#8230;）。</li>
<li><strong>双重哈希（Double Hashing）</strong>：使用两个哈希函数，第二个哈希函数定义探测间隔。</li>
</ul>
</li>
<li>
<p><strong>查找操作</strong>：查找元素时，也使用哈希函数计算索引，然后遵循相同的探测序列进行搜索，直到找到目标元素或遇到空槽位（表示元素不存在）。</p>
</li>
<li>
<p><strong>删除操作</strong>：删除元素时，不能简单地将槽位设置为空，因为这会中断探测序列。通常的做法是将删除的槽位标记为已删除（而非空闲），以便探测序列可以继续通过这个槽位。</p>
</li>
</ol>
<h4>优点和缺点</h4>
<ul>
<li>
<p><strong>优点</strong>：</p>
<ul>
<li>内存利用率高，因为所有元素都存储在哈希表数组中，不需要额外的数据结构。</li>
<li>可以避免链表可能带来的缓存不友好的问题。</li>
</ul>
</li>
<li>
<p><strong>缺点</strong>：</p>
<ul>
<li>当哈希表填满时，性能可能急剧下降，因此需要及时扩容。</li>
<li>删除操作比较复杂，可能需要特殊的标记处理。</li>
<li>集群（clustering）问题：特别是在使用线性探测时，连续的占用槽位可能导致新插入的元素需要经过更长的探测序列。</li>
</ul>
</li>
</ul>
<h4>应用场景</h4>
<p>​   开放寻址法适用于元素数量较少或负载因子较低的情况。在这些情况下，开放寻址法的简单性和内存连续性可以带来性能上的优势。然而，随着元素数量的增加，特别是当负载因子接近或超过 0.7 时，拉链法通常会提供更好的性能。</p>
<p>开放寻址法是理解和实现哈希表的重要方面之一，对于需要高效空间利用和快速访问的场景特别有用。</p>
<h3>拉链法（Chaining）</h3>
<p>​   拉链法是处理哈希表中冲突的一种常用技术。它的基本思想是在哈希表的每个槽（桶）中存储一个链表。当多个键映射到同一个槽时，这些键值对会被添加到对应槽的链表中。</p>
<h4>拉链法的特点</h4>
<ul>
<li><strong>添加操作</strong>：当插入一个新的键值对时，首先使用哈希函数确定其槽位置，然后将键值对添加到该槽的链表中。</li>
<li><strong>查找操作</strong>：查找时，同样先计算哈希值以确定槽位置，然后在对应的链表中搜索具有相应键的元素。</li>
<li><strong>删除操作</strong>：删除操作也是先定位到具体的槽，然后在链表中找到并删除相应的元素。</li>
</ul>
<h4>优点和缺点</h4>
<ul>
<li><strong>优点</strong>：拉链法简单且在冲突较多的情况下仍然能保持较好的性能。它允许哈希表的负载因子超过 1，即存储的元素数量可以超过哈希表的槽数量。</li>
<li><strong>缺点</strong>：在最坏的情况下（所有键都映射到同一个槽），查找性能会退化到 O<em>(</em>n<em>)，其中 </em>n* 是键值对的数量。</li>
</ul>
<h4>拉链法在golang中map的应用</h4>
<p>​   在 Go 语言中，<code>map</code> 的底层实现采用了一种结合桶（bucket）和链表的方法，这与传统拉链法有些相似，但也存在差异。</p>
<p>​   在 Go 的 <code>map</code> 中，每个桶（由 <code>bmap</code> 结构体表示）可以存储固定数量的键值对，通常是 8 对。这意味着，不是每一个键值对都单独分配一个结构，而是多个键值对共享一个桶。这种设计减少了对象的数量，从而减轻了垃圾回收（GC）的负担。</p>
<p>​   当一个桶填满时，Go <code>map</code> 会申请一个新的桶，并将其连接到原有的桶上，形成一种链表结构。这个新桶优先使用预分配的桶。当需要在 <code>map</code> 中插入新的键值对时，会根据哈希值定位到一个特定的桶。如果该桶已满，系统会沿着链表寻找或创建新的桶来存放这个键值对。</p>
<p>​   这种方式在某种程度上体现了拉链法的思想，即通过链表来解决哈希冲突。但与传统的拉链法（每个槽位对应一个链表）不同，Go 的实现是将多个键值对存储在同一个桶内，只有在这个桶填满时，才会通过链表的方式连接到新的桶。这样既保留了拉链法处理冲突的优势，又通过限制单个桶内键值对的数量，提高了内存的使用效率和查找效率。</p>
<p>​   总的来说，Go <code>map</code> 的这种设计是对传统拉链法的一种改进和优化，它在维持良好的性能的同时，也考虑到了内存的有效使用。</p>
<h4>hash值的使用</h4>
<p>通过哈希函数，key可以得到一个唯一值，这个值根据操作系统位数，可能为32位或64位。map将这个唯一值，分成高位和低位，分别有不同的用途：</p>
<ul>
<li><strong>低位</strong>：用于寻找当前key属于哪个bucket</li>
<li><strong>高位</strong>：用于寻找当前key在bucket中的位置，bucket有个tohash字段，便是存储的高位的值，用来声明当前bucket中有哪些key，这样搜索查找时就不用遍历bucket中的每个key，只要先看看tohash数组值即可，提高搜索查找效率</li>
</ul>
<blockquote>
<p>map其使用的hash算法会根据硬件选择，比如如果cpu是否支持aes，那么采用aes哈希（从硬件层面提高效率），并且将hash值映射到bucket时，会采用位运算来规避mod的开销</p>
</blockquote>
<h2>插入过程</h2>
<h4>插入新值时的过程</h4>
<ol>
<li>调用哈希函数为给定键生成一个哈希码。</li>
<li>基于哈希码的一部分确定存储键值对的桶。</li>
<li>选定桶后，将新条目存储在该桶中。</li>
<li>将传入键的完整哈希码与初始哈希码数组中的所有哈希码（如h1, h2&#8230;）进行比较。</li>
<li>如果没有匹配的哈希码，意味着这是一个新条目。</li>
<li>如果桶中有空位，则在键值对列表末尾存储新条目。</li>
<li>否则，创建一个新桶，并将新条目存储在新桶中，旧桶的溢出指针指向这个新桶。</li>
</ol>
<h2>动态扩容</h2>
<h3>扩容过程</h3>
<ol>
<li><strong>触发扩容条件</strong>：
<ul>
<li>当 <code>map</code> 的装载因子超过 6.5 时，即元素数量相对于桶数量太多，需要进行扩容。</li>
<li>当使用的溢出桶（<code>overflow buckets</code>）数量过多时，即使桶的数量没有超过装载因子，也需要进行扩容。这种情况下，扩容的目的主要是内存整理，而不是增加桶的数量。</li>
</ul>
</li>
<li><strong>扩容操作</strong>：
<ul>
<li>扩容会新分配一个更大的数组（桶）。如果是因为装载因子过高，扩容会增加桶的数量（通常是翻倍）；如果是因为溢出桶过多，扩容后的桶数量和原来一样。</li>
<li>在扩容过程中，<code>map</code> 结构体会做一些重新赋值操作，比如切换老的桶（<code>oldbuckets</code>）。</li>
</ul>
</li>
</ol>
<h3>为什么负载因子是 6.5</h3>
<p>​   这个值是根据性能测试选择的。在这些测试中，考虑了如下因素：</p>
<ul>
<li><strong>溢出率</strong>：桶溢出的比例。溢出率过高意味着许多键被映射到了相同的桶，增加了冲突。</li>
<li><strong>内存开销</strong>：每个键值对的平均内存占用。</li>
<li><strong>查找性能</strong>：查找存在或不存在的键所需的平均步骤数。</li>
</ul>
<p>​   通过测试不同的负载因子，Go 团队发现，当负载因子为 6.5 时，这些因素之间达到了最佳平衡，即内存利用率和查找效率都处于理想状态。</p>
<p>​   注意是平均6.5，前面说每个桶最多装8个元素。</p>
<h3>渐进式搬迁</h3>
<p>​   为了避免一次性迁移大量数据带来的性能问题，Go <code>map</code> 采用了渐进式搬迁的方法。这意味着数据的迁移不是一次性完成的，而是在后续的 <code>map</code> 操作中逐渐进行。每次对 <code>map</code> 进行插入、删除或查找操作时，都会触发部分数据的搬迁。</p>
<p>​   在扩容过程中，<code>map</code> 维护了一个指向旧桶数组的指针（<code>oldbuckets</code>），以及一个迁移进度指示器（<code>nevacuate</code>）。在每次 <code>map</code> 操作期间，会根据这个进度指示器将一部分数据从旧桶迁移到新桶中。这个过程会持续进行，直到所有数据都迁移到了新桶中。</p>
<p>​   这种渐进式搬迁的方法可以在扩容期间分摊性能开销，避免了一次性重分配大量内存和数据迁移带来的性能下降。一旦所有数据都从旧桶迁移到新桶中后，<code>oldbuckets</code> 将被设置为 <code>nil</code>，这表示扩容和数据迁移已经完全完成。</p>
<p>​   渐进式搬迁策略使得 Go <code>map</code> 在处理大量数据时仍能保持较高的性能，特别是在动态扩容的情况下。这种策略的一个关键优点是，它允许 <code>map</code> 在扩容过程中继续进行高效的数据操作。</p>
<h3>缩容机制</h3>
<p>​   在 Go 语言中，<code>map</code> 的缩容机制并不像其扩容机制那样明确和直接。Go 语言的 <code>map</code> 实现主要支持扩容操作，以应对元素数量的增长，但它不支持显式的缩容过程，即在元素被删除后释放内存。这意味着，一旦 <code>map</code> 扩容后，即使删除了一些元素，它所占用的内存空间不会自动减少。</p>
<p>关于 <code>map</code> 的扩缩容机制，有以下几点需要注意：</p>
<ol>
<li>
<p><strong>伪缩容</strong>：目前 Go 语言中的 <code>map</code> 实现被认为是进行了“伪缩容”。这主要是指，虽然可以减少 <code>map</code> 中的元素数量，但 <code>map</code> 占用的内存空间实际上并不会减少。即使是在 <code>map</code> 中的元素被大量删除后，已经分配的内存也不会被释放。</p>
</li>
<li>
<p><strong>内存回收</strong>：如果需要减少 <code>map</code> 的内存占用，唯一的方法是创建一个新的 <code>map</code> 并将旧 <code>map</code> 中的元素复制到新的 <code>map</code> 中，然后丢弃旧的 <code>map</code>。这样，旧 <code>map</code> 占用的内存就可以被垃圾回收器回收。</p>
</li>
<li>
<p><strong>内存管理</strong>：因此，在使用 Go 语言的 <code>map</code> 时，应当注意内存管理。如果 <code>map</code> 用于存储大量元素并且会经常删除元素，那么可能需要定期创建新的 <code>map</code> 并复制元素，以避免内存使用无限增长。</p>
</li>
</ol>
<p>​   综上所述，Go 语言的 <code>map</code> 缩容机制并不像其他一些语言中那样自动或显式。这是一个需要开发者自己管理的方面，尤其是在涉及大型数据结构和内存管理时。</p>
<h2>线程安全问题</h2>
<p>​   关于 Go 语言中 <code>map</code> 的线程安全问题，这是一个非常重要的话题。基本来说，Go 的 <code>map</code> 在原生状态下是<strong>不保证线程安全</strong>的。这意味着在没有适当同步机制的情况下，从多个协程（goroutine）并发访问同一个 <code>map</code> 可能会导致竞态条件（race condition）和其他并发错误。</p>
<h3>实验</h3>
<pre><code class="language-golang">package main

import &quot;testing&quot;

func TestMap(t *testing.T) {
    var m = make(map[int]int)
    go func() {
        i := 0
        for {
            m[i]++
            i++
        }
    }()

    i := 0
    for {
        _ = m[i]
        i++
    }
}
</code></pre>
<p>上面代码会报错</p>
<pre><code>fatal error: concurrent map read and map write</code></pre>
<h3>原因</h3>
<ol>
<li>
<p><strong>内部状态修改</strong>：当一个协程对 <code>map</code> 进行写入（添加、修改、删除元素）操作时，它可能会更改 <code>map</code> 的内部状态。例如，在插入新元素时可能会触发扩容操作。如果此时另一个协程尝试访问 <code>map</code>，可能会遇到 <code>map</code> 结构正在变化的情况，导致不可预测的行为。</p>
</li>
<li>
<p><strong>并发写入冲突</strong>：如果多个协程同时尝试写入 <code>map</code>，它们可能会尝试修改 <code>map</code> 的同一部分，这可以导致数据损坏或程序崩溃。</p>
</li>
<li>
<p><strong>读写冲突</strong>：即使是读操作，也可能与写操作冲突。例如，一个协程正在读取 <code>map</code> 的一个元素，而另一个协程同时删除该元素或修改该元素所在的桶的结构，这可能导致读取操作发生错误。</p>
</li>
</ol>
<h3>解决方案</h3>
<h3>sync.RWMutex</h3>
<p>​   如果 <code>map</code> 的读操作远多于写操作，可以使用读写互斥锁 <code>sync.RWMutex</code>。这允许多个协程同时读取 <code>map</code>，但写操作仍然需要独占访问。</p>
<pre><code class="language-go">var rwmu sync.RWMutex
rwmu.RLock()
// 对 map 进行读操作
rwmu.RUnlock()

rwmu.Lock()
// 对 map 进行写操作
rwmu.Unlock()</code></pre>
<h3>sync.Map</h3>
<p>​   Go 标准库中的 <code>sync.Map</code> 是一个专门为并发场景设计的 map 类型。它优化了在特定使用情况下的性能，特别是在写入操作较少而读取操作频繁的场景中。<code>sync.Map</code> 提供了 <code>Load</code>、<code>Store</code>、<code>Delete</code> 等基本操作方法，这些方法都是线程安全的。</p>
<h4>适用场景</h4>
<p>根据官方文档，<code>sync.Map</code> 在以下两种场景下表现出优于普通 map 加读写互斥锁 (<code>map+RWMutex</code>) 的性能：</p>
<ol>
<li><strong>缓存系统</strong>：在一个只会增长的缓存系统中，一个键值对被写入后将被频繁读取或更新，但写入次数相对较少。</li>
<li><strong>分离的键集</strong>：多个 goroutine 分别读写不相交的键集，进行键值对的读、写和重写操作。</li>
</ol>
<pre><code class="language-go">var m sync.Map
m.Store(key, value) // 写入
value, ok := m.Load(key) // 读取
m.Delete(key) // 删除</code></pre>
<h4>实现原理</h4>
<p><code>sync.Map</code> 的高性能线程安全实现依赖于以下几个关键设计：</p>
<ol>
<li>
<p><strong>两个内部存储结构</strong>：<code>sync.Map</code> 维护一个主存储和一个只读存储。主存储用于最新的写入操作，而只读存储包含了先前版本的数据，形成一种数据快照。这使得在大多数情况下，读操作可以在无锁的情况下进行，因为它们主要针对稳定且不变的只读存储。</p>
</li>
<li>
<p><strong>延迟删除和原子操作</strong>：当从 <code>sync.Map</code> 中删除一个键值对时，它并不会立即从内存中移除，而是被标记为删除。这降低了写操作的频率。所有写入和删除操作都是通过原子操作完成的，保证了线程安全。</p>
</li>
<li>
<p><strong>读写分离</strong>：<code>sync.Map</code> 的设计将读写操作分离。由于读操作主要针对不经常变化的只读存储，因此可以在无锁的情况下安全进行，极大地提高了并发读的性能。</p>
</li>
<li>
<p><strong>动态调整</strong>：<code>sync.Map</code> 能够根据使用情况动态调整内部结构，例如，将数据从主存储迁移到只读存储，以优化性能。</p>
</li>
</ol>
<h4>性能优化</h4>
<p>​   在写入少，读取多的场景中，如缓存系统，或者当多个 goroutine 同时操作不同的键时，<code>sync.Map</code> 的这种设计可以显著提高性能。在这些场景下，由于读取操作的高效率和写入操作的原子性，<code>sync.Map</code> 能够提供比传统的带锁 map 更好的性能。</p>
<p>​   总的来说，<code>sync.Map</code> 是一个针对特定并发场景优化的数据结构，它通过内部的复杂机制，实现了在高并发环境下的高效线程安全操作。然而，它并非适用于所有情况，特别是在写入操作频繁的场景中，传统的 map 加锁可能会更加高效。因此，选择 <code>sync.Map</code> 还是普通 map 应该基于具体的应用场景和性能需求。</p>
<h3>其他并发安全map的实现库</h3>
<h3>1. orcaman/concurrent-map</h3>
<h4>GitHub: <a href="https://github.com/orcaman/concurrent-map" target="_blank" rel="noopener">orcaman/concurrent-map</a></h4>
<ul>
<li>特点：
<ul>
<li><code>orcaman/concurrent-map</code> 是一个简单易用的并发安全的哈希表实现。</li>
<li>它通过将 map 分割成多个小的 map 段（shards）来减少锁的粒度，从而提高并发性能。每个段有自己的读写锁，这减少了锁争用，提高了并发读写的效率。</li>
<li>提供基本的 map 操作如 <code>Set</code>, <code>Get</code>, <code>Remove</code>, <code>Items</code> 等。</li>
</ul>
</li>
</ul>
<h3>2. cornelk/hashmap</h3>
<h4>GitHub: <a href="https://github.com/cornelk/hashmap" target="_blank" rel="noopener">cornelk/hashmap</a></h4>
<ul>
<li>特点：
<ul>
<li><code>cornelk/hashmap</code> 是另一个高性能的并发安全的哈希表实现。</li>
<li>它特别为高并发场景优化，比如在多核处理器上运行时，能够提供非常好的性能。</li>
<li>支持扩展和收缩，可以根据存储的元素数量自动调整大小。</li>
<li>提供了丰富的 API，包括迭代器、大小估计、自定义等值比较等功能。</li>
</ul>
</li>
</ul>
<h3>3. alphadose/haxmap</h3>
<h4>GitHub: <a href="https://github.com/alphadose/haxmap" target="_blank" rel="noopener">alphadose/haxmap</a></h4>
<ul>
<li>特点：
<ul>
<li><code>alphadose/haxmap</code> 是一个较新的高性能并发安全的哈希表实现。</li>
<li>这个库的特色在于其使用了一种叫做 &quot;hopscotch hashing&quot; 的技术，这种技术旨在提高缓存利用率和降低冲突概率。</li>
<li>旨在提供高速的并发读写操作，同时保持较低的内存占用。</li>
<li>API 上提供了类似的基本操作，如添加、删除、查找等。</li>
</ul>
</li>
</ul>
<h3>总结</h3>
<p>​   这些库各有其特点和优势，选择合适的库取决于具体的应用场景和性能需求。例如，在需要处理大量并发读写操作的情况下，可能会选择 <code>cornelk/hashmap</code>；而如果希望降低内存占用并提高缓存效率，则 <code>alphadose/haxmap</code> 可能是一个好的选择。重要的是要根据实际需求和性能测试来选择最适合项目的并发安全 map 实现。</p>
<h3>结论</h3>
<p>​   在多协程环境下，对 <code>map</code> 的安全访问需要采用合适的同步机制。选择哪种机制取决于具体的使用场景和性能要求。简单地使用互斥锁可以提供安全保证，但可能会影响性能。在读多写少的场景中，<code>sync.RWMutex</code> 或 <code>sync.Map</code> 可能是更好的选择。顺便提一下，slice也不是线程安全的。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/5075/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5075</post-id>	</item>
		<item>
		<title>Next.js项目初始化配置</title>
		<link>https://www.cztcode.com/2023/5073/</link>
					<comments>https://www.cztcode.com/2023/5073/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Tue, 17 Oct 2023 09:31:48 +0000</pubDate>
				<category><![CDATA[next.js]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=5073</guid>

					<description><![CDATA[记录使用next.js(app router)+tailwind+typestript+prisma+trpc+tanstack+shadcn/ui项目初始化结构]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>记录下next.js全栈项目初始化需要的组件</p>
<ol>
<li>next.js (app router)</li>
<li>tailwind</li>
<li>typescript</li>
<li>prisma</li>
<li>trpc</li>
<li>tanstack</li>
<li>shadcn/ui</li>
</ol>
<h2>使用pnpm初始化next.js</h2>
<pre><code class="language-bash">pnpm dlx create-next-app@latest</code></pre>
<p>起好项目名字后全部默认就可以</p>
<pre><code class="language-bash">(base) ➜  ~ pnpm dlx create-next-app@latest
Packages: +1
+
Progress: resolved 1, reused 0, downloaded 1, added 1, done
✔ What is your project named? … demo
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes</code></pre>
<h2>使用eslint和prettier</h2>
<p>一、安装eslint + prettier + eslint-plugin-prettier</p>
<pre><code class="language-bash">pnpm i eslint prettier eslint-plugin-prettier eslint-config-prettier prettier-plugin-tailwindcss -D</code></pre>
<p>二、配置.eslintrc.json</p>
<pre><code class="language-tsx">{
  &quot;extends&quot;: [
    &quot;next/core-web-vitals&quot;,
    &quot;eslint:recommended&quot;,
    &quot;plugin:prettier/recommended&quot;
  ],
  // 这个是nextjs官网推荐的写法
  &quot;env&quot;: {
    &quot;es2020&quot;:true,
    &quot;es6&quot;: true,
    &quot;node&quot;: true
  },
  &quot;plugins&quot;: [
    &quot;prettier&quot;
  ],
  &quot;rules&quot;: {
    &quot;prettier/prettier&quot;: &quot;warn&quot;
  }
}</code></pre>
<p>三、新建.prettierrc.js，注意名字别打错了</p>
<pre><code class="language-tsx">module.exports = {
  // 基础配置
  singleQuote: false, // 使用双引号
  printWidth: 100, // 设置行宽度为100，稍微宽一点可能更适合现代屏幕
  semi: true, // 使用分号
  trailingComma: &#039;all&#039;, // 在多行输入的尾逗号处添加一个逗号

  // TypeScript相关配置
  arrowParens: &#039;always&#039;, // 箭头函数参数始终使用括号，有助于代码清晰
  bracketSpacing: true, // 在对象字面量声明所使用的的大括号内部添加空格

  // 与编辑器和其他工具集成
  useTabs: false, // 使用空格替代Tab
  tabWidth: 2, // 设置tab宽度为2

  // 其他配置
  proseWrap: &#039;always&#039;, // 总是折行，适用于Markdown等
  htmlWhitespaceSensitivity: &#039;css&#039;, // 尊重CSS中的空白字符
  plugins: [&#039;prettier-plugin-tailwindcss&#039;],
};
</code></pre>
<p>手动格式化</p>
<pre><code class="language-bash"> prettier --write &quot;src/**/*.{js,jsx,ts,tsx}&quot;    </code></pre>
<p>添加到package.json</p>
<pre><code class="language-bash"> &quot;prettier&quot;: &quot;prettier --write \&quot;src/**/*.{js,jsx,ts,tsx}\&quot;&quot;</code></pre>
<p>推荐开启保存自动格式化</p>
<p><img decoding="async" src="https://markdown.cztcode.com/ccc2ffb497138a58eca75f94fc5c6b3a.png" alt="image-20231017165849028" /></p>
<h2>安装husky</h2>
<p>husky负责实现在git提交前检查规范</p>
<pre><code class="language-bash">pnpm dlx husky-init &amp;&amp; pnpm install</code></pre>
<p>运行husky初始化，上面的脚本会帮我们添加一个prepare指令</p>
<pre><code class="language-bash"> pnpm run prepare   </code></pre>
<h3>修改配置</h3>
<p>把.husky/pre-commit 修改为</p>
<pre><code class="language-bash">pnpm run prettier
pnpm run lint</code></pre>
<p>记得把.husky添加到git</p>
<h2>安装commitlint</h2>
<p>commitlint可以让提交信息更规范</p>
<pre><code class="language-bash"> pnpm install  -D @commitlint/{config-conventional,cli}       </code></pre>
<p>初始化</p>
<pre><code class="language-bash">echo &quot;module.exports = {extends: [&#039;@commitlint/config-conventional&#039;]}&quot; &gt; commitlint.config.js</code></pre>
<p>把commitlint添加进husky</p>
<pre><code class="language-bash"> pnpm dlx husky add .husky/commit-msg  &#039;npx --no -- commitlint --edit ${1}&#039;</code></pre>
<p>记得把commitlint.config.js添加到git</p>
<h2>安装prisma</h2>
<p>prisma 是nextjs中热门的ORM组件</p>
<pre><code class="language-bash">pnpm dlx prisma  </code></pre>
<p>初始化prisma</p>
<pre><code class="language-bash"> pnpm dlx prisma init  </code></pre>
<p>在.env文件中更改数据库链接字符串DATABASE_URL，小项目建议去Mongodb Atlas</p>
<p>模型定义在prisma/schema.prisma，这里我给出mongodb的配置和一些简单类型</p>
<pre><code class="language-tsx">generator client {
  provider = &quot;prisma-client-js&quot;
  output   = &quot;./client&quot; // 如果写了这个就不用配置webstorm排除目录了
}

datasource db {
  provider     = &quot;mongodb&quot;
  url          = env(&quot;DATABASE_URL&quot;)
  relationMode = &quot;prisma&quot;
}

model Post {
  id       String    @id @default(auto()) @map(&quot;_id&quot;) @db.ObjectId
  slug     String    @unique
  title    String
  body     String
  author   User      @relation(fields: [authorId], references: [id])
  authorId String    @db.ObjectId
  comments Comment[]
}

model User {
  id      String   @id @default(auto()) @map(&quot;_id&quot;) @db.ObjectId
  email   String   @unique
  name    String?
  address Address?
  posts   Post[]
}

model Comment {
  id      String @id @default(auto()) @map(&quot;_id&quot;) @db.ObjectId
  comment String
  post    Post   @relation(fields: [postId], references: [id])
  postId  String @db.ObjectId
}

// Address is an embedded document
type Address {
  street String
  city   String
  state  String
  zip    String
}
</code></pre>
<p>prisma会生成client便于类型提示，在你更改完schema.prisma后要运行</p>
<pre><code class="language-bash">pnpm dlx prisma generate</code></pre>
<p>在修改表后，还要运行push指令同步到数据库</p>
<pre><code class="language-bash">pnpm dlx prisma db push</code></pre>
<p>使用studio指令查看数据库表</p>
<pre><code class="language-bash">pnpm dlx prisma studio  </code></pre>
<p>如果你使用的是webstorm，注意要把这个文件夹标记为不排除，因为node_module这里webstorm默认不会生成索引，当你使用<code>prisma generate</code>后会动态生成新的代码，而由于webstorm默认不生成索引，你就不会得到ts的代码提示。</p>
<p>排除了就会重新索引这个文件，具体路径</p>
<pre><code class="language-bash">/node_modules/.pnpm/@prisma+client@5.4.2/node_modules/.prisma</code></pre>
<p><img decoding="async" src="https://markdown.cztcode.com/d64f5bd5742d6b9422c01f78cd7482f2.png" alt="image-20231017155450168" /></p>
<p>创建一个prisma客户端实例化代码，这段代码会在开发环境复用client实例（避免next的热更新造成重复创建实例）。</p>
<pre><code class="language-tsx">import { PrismaClient } from &quot;@prisma/client&quot;;

const prismaClientSingleton = () =&gt; {
  return new PrismaClient();
};

type PrismaClientSingleton = ReturnType&lt;typeof prismaClientSingleton&gt;;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined;
};

const prisma = globalForPrisma.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== &quot;production&quot;) globalForPrisma.prisma = prisma;</code></pre>
<h2>配置shadcn/ui</h2>
<p>shadcn是一个开源的，可定制化很高的组件库，与传统的导入整个组件库不同，shadcn/ui会精确导入你使用的组件，并且你可以直接进一步修改这些组件，就像是自己写的一样。</p>
<p><a href="https://ui.shadcn.com/" target="_blank" rel="noopener">https://ui.shadcn.com/</a></p>
<pre><code class="language-bash">pnpm dlx shadcn-ui@latest init</code></pre>
<p>除了tailwind.config.js需要改成ts，其他都用默认</p>
<pre><code class="language-bash">Progress: resolved 198, reused 198, downloaded 0, added 198, done
✔ Would you like to use TypeScript (recommended)? … no / yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Zinc
✔ Where is your global CSS file? … app/globals.css
✔ Would you like to use CSS variables for colors? … no / yes
✔ Where is your tailwind.config.js located? … tailwind.config.ts
✔ Configure the import alias for components: … @/components
✔ Configure the import alias for utils: … @/lib/utils
✔ Are you using React Server Components? … no / yes
✔ Write configuration to components.json. Proceed? … yes
</code></pre>
<p>需要任何组件从这里找https://ui.shadcn.com/docs/components/</p>
<p>文档里有写每个组件如何安装</p>
<pre><code class="language-bash">pnpx dlx shadcn-ui@latest add xxx</code></pre>
<p>之后正常在page中使用即可</p>
<h2>使用trpc</h2>
<pre><code class="language-bash">pnpm add @trpc/server @trpc/client  @trpc/react-query   </code></pre>
<p>创建trpc文件夹，然后分别创建client.ts，init.ts，index.ts</p>
<p>client.ts</p>
<pre><code class="language-tsx">import { AppRouter } from &quot;@/trpc&quot;;
import { createTRPCReact } from &quot;@trpc/react-query&quot;;

export const trpc = createTRPCReact&lt;AppRouter&gt;({});
</code></pre>
<p>index.ts 这里放端点，我还使用了zod动态验证数据格式，你可以安装pnpm install zod</p>
<p>下面写了一个创建新用户的例子</p>
<pre><code class="language-ts">import db from &quot;@/lib/prisma&quot;;
import { z } from &quot;zod&quot;;
import { publicProcedure, router } from &quot;@/trpc/init&quot;;

export const appRouter = router({
  createUser: publicProcedure
    .input(
      z.object({
        name: z.string(),
        email: z.string(),
      }),
    )
    .query(async ({ input }) =&gt; {
      await db.user.create({
        data: {
          name: input.name,
          email: input.email,
        },
      });
      return { success: true };
    }),
});

// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
</code></pre>
<p>init.ts 在这里你可以定义公开procedure，或者利用middleware创建私有的</p>
<pre><code class="language-ts">import { initTRPC } from &quot;@trpc/server&quot;;

/**
 * Initialization of tRPC backend
 * Should be done only once per backend!
 */
const t = initTRPC.create();

/**
 * Export reusable router and procedure helpers
 * that can be used throughout the router
 */
export const router = t.router;
export const publicProcedure = t.procedure;
</code></pre>
<p>给一个使用kinde的例子</p>
<pre><code class="language-ts">import { getKindeServerSession } from &#039;@kinde-oss/kinde-auth-nextjs/server&#039;
import { TRPCError, initTRPC } from &#039;@trpc/server&#039;

const t = initTRPC.create()
const middleware = t.middleware

const isAuth = middleware(async (opts) =&gt; {
  const { getUser } = getKindeServerSession()
  const user = getUser()

  if (!user || !user.id) {
    throw new TRPCError({ code: &#039;UNAUTHORIZED&#039; })
  }

  return opts.next({
    ctx: {
      userId: user.id,
      user,
    },
  })
})

export const router = t.router
export const publicProcedure = t.procedure
export const privateProcedure = t.procedure.use(isAuth)
</code></pre>
<p>推荐使用tanstack帮助trpc进行请求状态管理</p>
<p><a href="https://tanstack.com/query/latest/docs/react/overview" target="_blank" rel="noopener">https://tanstack.com/query/latest/docs/react/overview</a></p>
<p>安装tanstack</p>
<pre><code class="language-bash">pnpm add @tanstack/react-query</code></pre>
<p>具体使用方法推荐查看官网</p>
<p>现在需要创建一个Provider，把trpc与tanstack结合，在conponents文件夹下</p>
<pre><code class="language-ts">&quot;use client&quot;;

import { PropsWithChildren, useState } from &quot;react&quot;;
import { QueryClient, QueryClientProvider } from &quot;@tanstack/react-query&quot;;
import { httpBatchLink } from &quot;@trpc/client&quot;;
import { trpc } from &quot;@/trpc/client&quot;;

const Providers = ({ children }: PropsWithChildren) =&gt; {
  const [queryClient] = useState(() =&gt; new QueryClient());
  const [trpcClient] = useState(() =&gt;
    trpc.createClient({
      links: [
        httpBatchLink({
          url: &quot;http://localhost:3000/api/trpc&quot;,
        }),
      ],
    }),
  );
  return (
    &lt;trpc.Provider queryClient={queryClient} client={trpcClient}&gt;
      &lt;QueryClientProvider client={queryClient}&gt;{children}&lt;/QueryClientProvider&gt;
    &lt;/trpc.Provider&gt;
  );
};

export default Providers;</code></pre>
<p>最后在最父层layout.tsx中把里面用Provider，你就可以在任何地方使用了</p>
<pre><code class="language-tsx">import type { Metadata } from &quot;next&quot;;
import { Inter } from &quot;next/font/google&quot;;
import &quot;./globals.css&quot;;
import React from &quot;react&quot;;
import Providers from &quot;@/components/Providers&quot;;

const inter = Inter({ subsets: [&quot;latin&quot;] });

export const metadata: Metadata = {
  title: &quot;Create Next App&quot;,
  description: &quot;Generated by create next app&quot;,
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;Providers&gt;
        &lt;body className={inter.className}&gt;{children}&lt;/body&gt;
      &lt;/Providers&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p>一个项目的初始化就完成啦</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/5073/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5073</post-id>	</item>
		<item>
		<title>Softmax 回归简介</title>
		<link>https://www.cztcode.com/2023/4957/</link>
					<comments>https://www.cztcode.com/2023/4957/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Thu, 24 Aug 2023 15:05:41 +0000</pubDate>
				<category><![CDATA[深度学习]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4957</guid>

					<description><![CDATA[社会科学家邓肯·卢斯于1959年在*选择模型*（choice model）的理论基础上 发明的了*softmax函数*： softmax函数能够将未规范化的预测变换为非负数并且总和为1，同时让模型保持 可导的性质。

为什么叫Softmax呢？Softmax从字面上来说，可以分成soft和max两个部分。max故名思议就是最大值的意思。Softmax的核心在于soft，而soft有软的含义，与之相对的是hard硬。很多场景中需要我们找出数组所有元素中值最大的元素。

Softmax函数是一个在数学和计算机科学中常用的函数，特别是在机器学习的分类问题中。它可以把一个含任意实数的K维向量“压缩”到另一个K维实向量中，使得每一个元素的范围都在`$$(0,1)$$`之间，并且所有元素的和为1。这使得Softmax函数的输出可以被解释为概率分布。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h2>Softmax函数</h2>
<p>社会科学家邓肯·卢斯于1959年在<em>选择模型</em>（choice model）的理论基础上 发明的了<em>softmax函数</em>： softmax函数能够将未规范化的预测变换为非负数并且总和为1，同时让模型保持 可导的性质。</p>
<p>为什么叫Softmax呢？Softmax从字面上来说，可以分成soft和max两个部分。max故名思议就是最大值的意思。Softmax的核心在于soft，而soft有软的含义，与之相对的是hard硬。很多场景中需要我们找出数组所有元素中值最大的元素。</p>
<p>Softmax函数是一个在数学和计算机科学中常用的函数，特别是在机器学习的分类问题中。它可以把一个含任意实数的K维向量“压缩”到另一个K维实向量中，使得每一个元素的范围都在<code class="katex-inline">(0,1)</code>之间，并且所有元素的和为1。这使得Softmax函数的输出可以被解释为概率分布。</p>
<p>Softmax函数的定义如下：</p>
<p><code class="katex-inline">\text{softmax}(x)_i = \frac{e^{x_i}}{\sum_{j=1}^K e^{x_j}}</code></p>
<p>其中<code class="katex-inline">x</code>是输入向量，<code class="katex-inline">K</code>是向量的维度，<code class="katex-inline">i</code>是指定的元素索引。</p>
<p>在多分类问题中，Softmax函数常用于将模型的原始输出（通常被称为logits）转换为概率分布。这样，每个类别的概率可以直接相比较，从而选择最可能的类别。</p>
<p>例如，如果一个模型用于对数字图像进行分类，并且原始输出是<code class="katex-inline">(2.0, 1.0, 0.1)</code>，则通过Softmax函数，我们可以得到一个新的向量，如<code class="katex-inline">(0.7, 0.2, 0.1)</code>，其中每个值代表对应数字的预测概率。</p>
<h2>Softmax回归</h2>
<p>Softmax回归可以看作是一个简单的神经网络，其中没有隐藏层，直接将输入与输出层连接。虽然模型相对简单，但在一些多分类问题上表现还是不错的，特别是在类别之间的边界比较清晰的情况下。</p>
<p><img decoding="async" src="https://markdown.cztcode.com/1eabd6c5a86078811b356c098e6c4b8d.svg" alt="../_images/softmaxreg.svg" /></p>
<h2>SoftLabel</h2>
<p>SoftLabel 是一种处理标签问题的技术，通常用在机器学习的分类问题中。在传统的分类任务中，每个样本只会被分配一个确定的类别标签，比如说在一个二分类问题中，标签通常是 0 或 1。但在某些情况下，我们可能希望更灵活地表达分类的不确定性或者模糊性。</p>
<p>SoftLabel 就是这样一种技术，允许每个样本有一个属于每个类别的概率分布。例如，在一个二分类问题中，一个样本的 SoftLabel 可能是 (0.6, 0.4)，这表示该样本属于第一类的概率是 60%，属于第二类的概率是 40%。</p>
<p>这种方法有以下几个优点：</p>
<ol>
<li>
<p><strong>灵活性</strong>：SoftLabel 能更灵活地表示样本之间的不确定性和模糊性，适用于那些不容易归入单一类别的样本。</p>
</li>
<li>
<p><strong>鲁棒性</strong>：对于那些具有噪声或不准确标签的数据集，SoftLabel 可以提供一种更鲁棒的方法来表达这种不确定性。</p>
</li>
<li>
<p><strong>更好的梯度信息</strong>：在训练神经网络时，SoftLabel 可以提供更丰富的梯度信息，有助于网络的训练。</p>
</li>
<li>
<p><strong>平滑化决策边界</strong>：SoftLabel 通过允许样本具有模糊的类别分配，可以使分类器的决策边界更加平滑，从而减少过拟合现象。</p>
</li>
</ol>
<p>SoftLabel 可以与各种损失函数结合使用，如交叉熵损失。它是许多现代机器学习模型中的一个重要组成部分，尤其在处理具有模糊或不确定标签的数据时。</p>
<h2>Softmax注意事项</h2>
<p>Softmax函数虽然强大和广泛应用，但在某些情况下也存在一些限制和挑战，如：</p>
<ul>
<li><strong>数值稳定性问题</strong>：Softmax涉及指数运算，可能导致数值上溢或下溢。一些改进方法，如对输入进行缩放，可以帮助缓解这个问题。</li>
<li><strong>计算成本</strong>：对于大规模的类别集合，Softmax的计算可能相对昂贵。</li>
<li><strong>不适用于非互斥类别</strong>：如果数据可以归入多个类别，传统的Softmax可能不适用。</li>
<li><strong>可能的局部最优解</strong>：在某些情况下，Softmax可能会陷入局部最优解，不一定能找到全局最优解。</li>
</ul>
<p>虽然Softmax在许多方面表现出色，但在使用过程中需要注意一些问题。其中一个常见的挑战是数值稳定性。由于涉及指数运算，可能会出现数值上溢或下溢的情况。解决这个问题的一种方法是在计算之前对输入进行缩放。此外，Softmax可能不适用于具有大量类别或非互斥类别的场景。</p>
<h2>手动实现Softmax</h2>
<p>用Softmax实现图像分类问题</p>
<pre><code class="language-python">import torch
import torchvision
from torch.utils import data
from torchvision import transforms

lr = 0.1
w = torch.normal(0, 0.01, size=(784, 10), requires_grad=True)
b = torch.zeros(10, requires_grad=True)

def sgd(params, lr, batch_size):
    &quot;&quot;&quot;Minibatch stochastic gradient descent.&quot;&quot;&quot;
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

def get_dataloader_workers():  # @save
    &quot;&quot;&quot;使用4个进程来读取数据&quot;&quot;&quot;
    return 4

def load_data_fashion_mnist(batch_size, resize=None):  # @save
    &quot;&quot;&quot;下载Fashion-MNIST数据集，然后将其加载到内存中&quot;&quot;&quot;
    trans = [transforms.ToTensor()]
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root=&quot;../data&quot;, train=True, transform=trans, download=True
    )
    mnist_test = torchvision.datasets.FashionMNIST(
        root=&quot;../data&quot;, train=False, transform=trans, download=True
    )
    return (
        data.DataLoader(
            mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers()
        ),
        data.DataLoader(
            mnist_test, batch_size, shuffle=False, num_workers=get_dataloader_workers()
        ),
    )

def softmax(X):
    X_exp = torch.exp(X)
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

def cross_entropy(y_hat, y):
    return -torch.log(y_hat[range(len(y_hat)), y])

def net(X):
    return softmax(torch.matmul(X.reshape((-1, w.shape[0])), w) + b)

def accuracy(y_hat, y):
    if len(y_hat.shape) &gt; 1 and y_hat.shape[1] &gt; 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

def evaluate_accuracy(net, data_iter):
    if isinstance(net, torch.nn.Module):
        net.eval()
    metric = Accumulator(2)
    for X, y in data_iter:
        metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

class Accumulator:
    &quot;&quot;&quot;在`n`个变量上累加。&quot;&quot;&quot;

    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

def train_epoch_ch3(net, train_iter, loss, updater):
    &quot;&quot;&quot;训练模型一个迭代周期（定义见第3章）。&quot;&quot;&quot;
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确率总和、范例数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        # 其余代码不变
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.backward()
            updater.step()
            metric.add(float(l) * len(y), accuracy(y_hat, y), y.size().numel())
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
            metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练准确率
    return metric[0] / metric[2], metric[1] / metric[2]

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
    &quot;&quot;&quot;训练模型（定义见第3章）。&quot;&quot;&quot;
    train_metrics = None
    test_acc = None
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
    train_loss, train_acc = train_metrics
    print(f&quot;train_loss {train_loss:f} train_acc {train_acc:f} test_acc {test_acc:f}&quot;)

def updater(batch_size):
    return sgd([w, b], lr, batch_size)

if __name__ == &quot;__main__&quot;:
    num_epochs = 10
    train_iter, test_iter = load_data_fashion_mnist(batch_size=256)
    train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
</code></pre>
<h2>参考资料</h2>
<p><a href="https://zh.d2l.ai/chapter_linear-networks/softmax-regression.html" target="_blank" rel="noopener">https://zh.d2l.ai/chapter_linear-networks/softmax-regression.html</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/4957/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4957</post-id>	</item>
		<item>
		<title>交叉熵损失</title>
		<link>https://www.cztcode.com/2023/4908/</link>
					<comments>https://www.cztcode.com/2023/4908/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Mon, 21 Aug 2023 11:29:15 +0000</pubDate>
				<category><![CDATA[深度学习]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/?p=4908</guid>

					<description><![CDATA[熵 信息论中熵的概念首次被香农提出，目的是寻找一种高效/无损地编码信息的方法：以编码后数据的平均长度来衡量高效性，平均长度越小越高效；同时还需满足“无损”的条件，即编码后不能有原始信息的丢失。这样，香农提出了熵的定义：无损编码事件信息的最小平均编码长度。 假设一个信息事件有 N 种可能的状态，且各状态等可能性，即每种状态的可能性为 P = \frac{1}{N}。则编码该信息所需的最小编码长度可以 [&#8230;]]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h2>熵</h2>
<p>信息论中熵的概念首次被香农提出，目的是寻找一种高效/无损地编码信息的方法：以编码后数据的平均长度来衡量高效性，平均长度越小越高效；同时还需满足“无损”的条件，即编码后不能有原始信息的丢失。这样，香农提出了熵的定义：无损编码事件信息的最小平均编码长度。</p>
<p>假设一个信息事件有 N 种可能的状态，且各状态等可能性，即每种状态的可能性为 <code class="katex-inline">P = \frac{1}{N}</code>。则编码该信息所需的最小编码长度可以用以下数学公式表示：</p>
<p><code class="katex-inline">L = \log_2 N</code></p>
<p>比如当 N = 8 ，所以每种状态的可能性 <code class="katex-inline">\ P = \frac{1}{8} = 12.5\%</code> ，那么编码该信息所需的最小编码长度为：</p>
<p><code class="katex-inline">L = \log_2 8 = 3</code></p>
<p>也就是说，8个值需要3位来编码（例如000, 001, 010, 011, 100, 101, 110, 111）。不可以减少任何1位，也不应多于3位来编码8个可能的值。</p>
<h2>熵的公式</h2>
<p>其中<code class="katex-inline">P(i)</code> 是概率分布，最小编码长度为<code class="katex-inline">-log_2 P(i)</code> ，二者相乘即为对事件i的最小编码长度，再对所有事件求和就可以得到整个信息系统的平均最小编码长度，也就是熵。</p>
<p>对一个离散变量 i 的概率分布P(i)求熵：<br />
<code class="katex-inline">H = - \sum_{i=1}^{N} P(i) \log_2 P(i)</code></p>
<p>回到上面举的编码例子，每种状态出现的概率均为<code class="katex-inline">\ P = \frac{1}{8} = 12.5\%</code> 。对于具有  N  种等可能性状态的信息系统，每种状态的可能性为 <code class="katex-inline">P = \frac{1}{N}</code>，信息的熵（平均最小编码长度）带入公式:</p>
<p><code class="katex-inline">H = - \sum_{i=1}^{N} P \log_2 P = -N \left( \frac{1}{N} \log_2 \frac{1}{N} \right) = \log_2 N</code></p>
<p>熵用单位比特（bit）来度量，并可以理解为对信息事件的平均不确定性。当  <code class="katex-inline">N = 8</code>时，熵为：</p>
<p><code class="katex-inline">H = \log_2 8 = 3 \text{ bits}</code></p>
<p>这与前面所述的最小编码长度相匹配，正好是3位。熵在此情况下就是编码8个等可能状态所需的平均最小长度。</p>
<p>压缩（最小编码长度）与预测（概率分布）有什么关系呢？ 想象一下，我们有一个要压缩的数据流。 如果我们很容易预测下一个数据，那么这个数据就很容易压缩。 为什么呢？ 举一个极端的例子，假如数据流中的每个数据完全相同，这会是一个非常无聊的数据流。 由于它们总是相同的，我们总是知道下一个数据是什么。 所以，为了传递数据流的内容，我们不必传输任何信息。也就是说，“下一个数据是xx”这个事件毫无信息量。</p>
<p>但是，如果我们不能完全预测每一个事件，那么我们有时可能会感到“惊异”。 克劳德·香农决定用信息量<code class="katex-inline">-log_2 P(i)</code></p>
<p>来量化这种惊异程度（也就是上面的最小编码长度，信息量越大最小编码长度就越长）。 在观察一个事件i时，并赋予它主观概率<code class="katex-inline">P(i)</code> 。 当我们赋予一个事件较低的概率时，我们的惊异会更大，该事件的信息量也就更大。 </p>
<p>当然，我们可以深入探讨一下为什么使用公式 <code class="katex-inline">-\log_2 P(i)</code> 来表示信息量。</p>
<p>使用对数函数来度量信息量有以下几个理由：</p>
<ol>
<li>
<p><strong>非负性</strong>：由于 <code class="katex-inline">0 \leq P(i) \leq 1</code>，因此 <code class="katex-inline">-\log_2 P(i)</code> 总是非负的。这符合我们对信息量的直观理解，即信息量不应该是负的。</p>
</li>
<li>
<p><strong>自信息的可加性</strong>：如果事件 A 和事件 B 是独立的，那么知道 A 和 B 发生所带来的总信息应该是知道 A 发生和 B 发生所带来的信息之和。对数函数具有这个性质，即 <code class="katex-inline">\log(ab) = \log(a) + \log(b)</code>。两个独立事件的“惊奇”程度是各个事件的“惊奇”程度之和。如果你已经知道了一个事件的结果，那么这不会影响你对另一个独立事件结果的“惊奇”程度。如果你对于信息可加性还有一点疑问，十分推荐看这个讲解视频<a href="https://www.bilibili.com/video/BV15V411W7VB/?share_source=copy_web&amp;vd_source=51754dc3a590a9ea4b8dbe40d56ab09d" target="_blank" rel="noopener">&quot;交叉熵&quot;如何做损失函数？打包理解&quot;信息量&quot;、&quot;比特&quot;、&quot;熵&quot;、&quot;KL散度&quot;、&quot;交叉熵&quot;</a></p>
</li>
<li>
<p><strong>低概率事件的高信息量</strong>：使用负对数函数意味着概率较低的事件会被赋予更高的信息量。这符合我们的直观感受：罕见的事情发生时，我们获得的新信息更多。</p>
</li>
<li>
<p><strong>位的选择</strong>：使用底数 2 的对数，信息量的单位是“比特”。这是一种广泛用于计算机科学和信息理论的度量单位。</p>
</li>
</ol>
<p>因此，信息量 <code class="katex-inline">-\log_2 P(i)</code> 不仅可以量化惊异，还满足了一些与信息编码和处理相关的基本性质。</p>
<p>当我们谈到编码或压缩时，这个概念变得非常直观。如果一个事件的概率为 <code class="katex-inline">P(i) = \frac{1}{2}</code>，那么编码所需的比特数就是 1。如果一个事件的概率是 <code class="katex-inline">P(i) = \frac{1}{4}</code>，则需要 2 个比特来编码，依此类推。这正好与 <code class="katex-inline">-\log_2 P(i)</code> 的值相匹配，它给出了对事件 <code class="katex-inline">i</code> 进行编码所需的最小比特数。</p>
<p>关于位的选择还有一些历史：香农使用的是纳特而不是比特表示的信息量，也就是底数不是2而是e。</p>
<p>那为什么我们用比特来表示呢？</p>
<ol>
<li>
<p><strong>比特（bit）</strong>：比特是信息论中最常用的度量单位，表示了信息的最小单位。在二进制编码中，一个比特可以表示两个等可能的状态（例如0和1），因此对数底为2。</p>
</li>
<li>
<p><strong>纳特（nat）</strong>：纳特是以自然对数为基础的信息单位，与比特的关系由以下公式给出：<br />
<code class="katex-inline">1\text{ nat} = \frac{1}{\ln 2} \text{ bits} \approx 1.4427 \text{ bits}</code><br />
这里的 <code class="katex-inline">ln</code> 表示自然对数，即对数底为e。</p>
</li>
<li>
<p><strong>选择比特的原因</strong>：</p>
<ul>
<li><strong>直观理解</strong>：在数字计算和通信系统中，二进制编码非常普遍，所以比特作为度量单位更直观。</li>
<li><strong>历史与传统</strong>：早期的信息论研究就是基于二进制的，因此比特成为了标准单位。</li>
<li><strong>硬件实现</strong>：二进制编码与现代计算机硬件和存储系统有直接关系，因此以2为底的对数在编码和压缩算法中更具有实际意义。</li>
</ul>
</li>
</ol>
<p>总的来说，比特作为信息的度量单位与我们现代通信和计算系统的实际工作方式紧密相连，因此成为了信息量的标准度量单位。纳特则更多用于一些特殊的数学和理论分析场景。</p>
<h2>熵的估计</h2>
<p>只要我们知道了任何事件的概率分布，我们就可以计算它的熵；那如果我们不知道事件的概率分布，又想计算熵，该怎么做呢？那我们来对熵做一个估计吧，熵的估计的过程自然而然的引出了交叉熵。</p>
<p>假如我们现在需要预报东京天气，在真实天气发生之前，我们不可能知道天气的概率分布；但为了下文的讨论，我们需要假设对东京天气做一段时间的观测后，可以得到真实的概率分布P。</p>
<p>在观测之前，我们只有预估的概率分布Q，使用估计得到的概率分布可以计算估计的熵，下面我整理一下提到的几个概念：</p>
<ol>
<li>
<p><strong>真实概率分布 ( P )</strong>：这是描述天气实际情况的概率分布。对东京天气做一段时间的观测后，可以得到这个分布。</p>
</li>
<li>
<p><strong>估计的概率分布 ( Q )</strong>：这是在观测之前，基于现有信息和经验得到的天气概率分布。</p>
</li>
<li>
<p><strong>估计的熵</strong>：使用估计得到的概率分布 ( Q )，可以计算估计的熵，其公式为：<br />
<code class="katex-inline">H(Q) = -\sum_{i=1}^{N} Q(i) \log_2 Q(i)</code></p>
</li>
<li>
<p><strong>真实的熵与最小平均编码长度</strong>：如果 ( Q ) 是真实的概率分布，那么就可以使用上述公式计算编码天气信息的最小平均长度。然而，估计的概率分布 ( Q ) 与真实的概率分布 ( P ) 不同，因此会引入不确定性。</p>
<ul>
<li>计算期望的概率分布是 ( Q ) ，与真实的概率分布 ( P ) 不同。</li>
<li>计算最小编码长度的概率是 <code class="katex-inline">-logQ</code> ，与真实的最小编码长度<code class="katex-inline">-logP</code> 不同。</li>
</ul>
</li>
</ol>
<p>由于估计的概率分布 <em>Q</em> 与真实的概率分布 <em>P</em> 可能存在差异，这种不准确性可能会带来额外的“成本”。那么，如何量化这个成本，如何更准确地比较两个概率分布之间的相似性和差异性呢？这就引出了交叉熵的概念。</p>
<h2>交叉熵</h2>
<p>交叉熵（cross entropy）是一种衡量两个概率分布之间的相似性或差异性的度量。具体来说，交叉熵测量的是使用以 <em>Q</em> 为概率分布的最优编码方案来编码来自概率分布 <em>P</em> 的样本所需的平均位数。它的数学表达式为：</p>
<p><code class="katex-inline">H(P, Q) = -\sum_{i=1}^{N} P(i) \log_2 Q(i)</code></p>
<p>这里的关键思想是，交叉熵衡量了使用错误的概率分布 ( Q ) 对实际概率分布 ( P ) 进行编码所需的平均长度。也就是说，你实际上要表示的是由 ( P ) 描述的事件，但你使用 ( Q ) 来构造代码。</p>
<ul>
<li>P(i)是事件 ( i ) 的真实概率。</li>
<li><code class="katex-inline">-\log_2 Q(i)</code> 是使用概率分布 ( Q ) 编码事件 ( i ) 所需的长度。</li>
</ul>
<p>将这两者相乘并对所有事件求和，得到的是使用 ( Q ) 编码由 ( P ) 描述的事件所需的平均长度。</p>
<p>如果 ( Q ) 是完全准确的（即 ( Q = P )），那么交叉熵就等于熵。但如果 ( Q ) 不准确，交叉熵就会大于熵，反映了使用不准确的概率分布进行编码的额外“成本”。</p>
<p>对于熵值的大小有两个关键点需要理解：</p>
<ol>
<li>
<p><strong>熵的最小值取决于概率分布本身：</strong> 当一个概率分布中的事件越是确定性的，即某些事件发生的概率接近于1，其他事件的概率接近于0，那么熵会趋近于0。这是因为在这种情况下，你几乎不需要额外的信息来描述随机变量的状态，因为你可以几乎确定它会取什么值。</p>
</li>
<li>
<p><strong>对于均匀分布，熵将取最大值：</strong> 均匀分布是指所有可能的事件发生的概率都是相等的。在这种情况下，你对于随机变量的下一次取值没有任何先验知识，因此你需要更多的信息来描述它的状态。这就导致了熵取最大值，表示系统的不确定性最大。</p>
</li>
</ol>
<p>综合起来，熵作为度量随机变量不确定性的指标，其值会受到概率分布的影响。当概率分布更趋向于确定性时，熵较低；而在均匀分布中，因为所有事件发生的概率相等，所以熵取最大值，表现出系统的不确定性最大。</p>
<h2>交叉熵作为损失函数</h2>
<p>假设有两个机器学习模型对一张照片分别作出了预测：Q1和Q2。其中，Q1预测狗的概率为95%，其它动物的概率为5%平均分配；Q2预测狗的概率为60%，其它动物的概率也为40%平均分配。而第一张照片的真实标签为狗，使用one-hot编码为1,0,0,0,0。</p>
<table>
<thead>
<tr>
<th style="text-align: center;">动物</th>
<th style="text-align: center;">狗</th>
<th style="text-align: center;">猫</th>
<th style="text-align: center;">兔</th>
<th style="text-align: center;">狐狸</th>
<th style="text-align: center;">老虎</th>
<th style="text-align: center;">真实标签</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;"><strong>Q1</strong></td>
<td style="text-align: center;">95%</td>
<td style="text-align: center;">1%</td>
<td style="text-align: center;">1%</td>
<td style="text-align: center;">1%</td>
<td style="text-align: center;">1%</td>
<td style="text-align: center;">1</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Q2</strong></td>
<td style="text-align: center;">60%</td>
<td style="text-align: center;">10%</td>
<td style="text-align: center;">10%</td>
<td style="text-align: center;">10%</td>
<td style="text-align: center;">10%</td>
<td style="text-align: center;">1</td>
</tr>
</tbody>
</table>
<p>其中，真实标签表示第一张照片是狗的概率为100%，这个分布是确定性分布，即熵最小值为0。</p>
<p>通过这个表格，可以更直观地看到两个模型Q1和Q2对第一张照片的预测与真实标签的比较。从中可以明显看出Q1的预测更接近真实标签。如果想进一步量化这些预测的准确性，还可以通过前面提到的交叉熵计算方法来实现。</p>
<p>对于Q1的预测，可以计算交叉熵为：<br />
<code class="katex-inline">H(P, Q1) = -\left( 1 \cdot \log_2 0.95 + 0 \cdot \log_2 0.05 + 0 \cdot \log_2 0.05 + 0 \cdot \log_2 0.05 + 0 \cdot \log_2 0.05 \right) \approx 0.074</code><br />
对于Q2的预测，可以计算交叉熵为：<br />
<code class="katex-inline">H(P, Q2) = -\left( 1 \cdot \log_2 0.6 + 0 \cdot \log_2 0.1 + 0 \cdot \log_2 0.1 + 0 \cdot \log_2 0.1 + 0 \cdot \log_2 0.1 \right) \approx 0.442</code><br />
从这个结果来看，模型Q1的预测更接近真实标签，因此交叉熵更小。这一例子清晰地展示了交叉熵如何用于比较模型预测和真实标签之间的相似性，因此可以用作分类模型的损失函数。</p>
<h2>参考资料</h2>
<ol>
<li>强推这个视频，会让你对交叉熵有更深的理解：<a href="https://www.bilibili.com/video/BV15V411W7VB/?share_source=copy_web&amp;vd_source=51754dc3a590a9ea4b8dbe40d56ab09d" target="_blank" rel="noopener">&quot;交叉熵&quot;如何做损失函数？打包理解&quot;信息量&quot;、&quot;比特&quot;、&quot;熵&quot;、&quot;KL散度&quot;、&quot;交叉熵&quot;</a></li>
<li>动手学习深度学习：<a href="https://zh.d2l.ai/chapter_linear-networks/softmax-regression.html" target="_blank" rel="noopener">https://zh.d2l.ai/chapter_linear-networks/softmax-regression.html</a></li>
<li>一文搞懂熵(Entropy),交叉熵(Cross-Entropy) &#8211; 将为帅的文章 &#8211; 知乎 <a href="https://zhuanlan.zhihu.com/p/149186719" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/149186719</a></li>
</ol>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2023/4908/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4908</post-id>	</item>
	</channel>
</rss>
