<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>技术 on Wimi's Space</title><link>https://wimi.space/categories/%E6%8A%80%E6%9C%AF/</link><description>Recent content in 技术 on Wimi's Space</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 24 Jun 2025 11:02:31 +0800</lastBuildDate><atom:link href="https://wimi.space/categories/%E6%8A%80%E6%9C%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>分布式系统基础理论概览</title><link>https://wimi.space/posts/2025/06/distributed-cap-and-base/</link><pubDate>Tue, 24 Jun 2025 11:02:31 +0800</pubDate><guid>https://wimi.space/posts/2025/06/distributed-cap-and-base/</guid><description>&lt;p>分布式系统理论是一组用于理解、设计和分析分布式系统的核心概念、模型和原理。这些理论帮助解决由分布性、并发性、故障和不确定性带来的挑战。基础理论包括 CAP 理论和 BASE 理论。&lt;/p>
&lt;h2 id="cap-理论">CAP 理论&lt;/h2>
&lt;h3 id="基本概念">基本概念&lt;/h3>
&lt;h4 id="一致性consistency">一致性（Consistency）&lt;/h4>
&lt;p>在分布式环境中，一致性是指数据在多个节点之间能够保持一致的特性。如果在某个节点上执行变更操作后，用户可以立即从其他任意节点上读取到变更后的数据，那么就认为这样的系统具备强一致性。&lt;/p>
&lt;h4 id="可用性availability">可用性（Availability）&lt;/h4>
&lt;p>可用性是指系统提供的服务必须一直处于可用状态，对于用户的每一个操作请求总是能够在有限的时间内返回结果。&lt;/p>
&lt;p>它主要强调以下两点：&lt;/p>
&lt;ul>
&lt;li>有限的时间内：对于用户的一个请求操作，系统必须要在指定的时间内返回处理结果，如果超过这个时间，那么系统就被认为是不可用的。&lt;/li>
&lt;li>返回结果：不论成功或者失败，都需要明确地返回响应结果。&lt;/li>
&lt;/ul>
&lt;h4 id="分区容错性partition-tolerance">分区容错性（Partition Tolerance）&lt;/h4>
&lt;p>分区容错性指定是分布式系统在遇到网络分区时，仍需要能够对外提供一致性和可用性的服务，除非是整个网络环境都发生了故障。&lt;/p>
&lt;p>这里的网络分区指的是：&lt;/p>
&lt;ul>
&lt;li>在分布式系统中，由于不同的节点会分布在不同子网中（不同机房或异地网络等），由于一些特殊的原因，可能会出现子网内部是正常的，但子网彼此之间却无法正常通信，从而导致整个系统的网络被切分成若干个独立的区域。&lt;/li>
&lt;/ul>
&lt;h3 id="核心概念">核心概念&lt;/h3>
&lt;p>在分布式系统中，一致性 (C)、可用性 (A) 和分区容错性 (P) 这三个理想特性无法同时完全满足。当发生网络分区（P）时，系统必须在一致性（C）和可用性（A）之间做出取舍。&lt;/p>
&lt;h3 id="选择策略">选择策略&lt;/h3>
&lt;p>&lt;img src="https://static.wimi.space/blog/distributed-cap-select.png" alt="">&lt;/p>
&lt;p>因为 CAP 理论不能将一致性、可用性和分区容错性都满足，所以需要根据不同系统的特性进行取舍，主要分为以下三种情况：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>保证 AC，放弃 P&lt;/p>
&lt;ul>
&lt;li>这种情况下可以将所有数据都放在一个分布式节点上，这样可以避免网络分区带来的影响，但同时也意味着放弃了系统的可扩展性。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>保证 CP，放弃 A&lt;/p>
&lt;ul>
&lt;li>这种情况下如果发生了网络分区故障，此时节点间的数据就无法同步。因此在故障修复前都需要放弃对外提供服务，直至网络恢复，数据到达一致为止。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>保证 AP，放弃 C&lt;/p>
&lt;ul>
&lt;li>这种情况相当于放弃一致性。&lt;/li>
&lt;li>具体而言，是放弃数据的强一致性，但保证数据的最终一致性。因为不论是什么系统，数据最终都需要保持一致，否则整个系统就无法使用。&lt;/li>
&lt;li>在这种策略下，在某个短暂的时间窗口内会存在数据不一致的情况。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="base-理论">BASE 理论&lt;/h2>
&lt;p>BASE 是 Basically Available（基本可用）、Soft-state（软状态）和 Eventually Consistent（最终一致性）三个短语的缩写。&lt;/p>
&lt;p>BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果，其来源于对大规模互联网系统分布式实践的总结，是基于 CAP 定理逐步演化而来的，它大大降低了我们对系统的要求。&lt;/p>
&lt;h3 id="基本概念-1">基本概念&lt;/h3>
&lt;h4 id="基本可用basically-available">基本可用（Basically Available）&lt;/h4>
&lt;p>基本可用是指分布式系统在出现不可预知的故障时，允许损失部分可用性，例如：&lt;/p>
&lt;ul>
&lt;li>延长响应时间：比如原来的的查询只需要 0.5 秒，在系统出现故障时延长到 2~3 秒；&lt;/li>
&lt;li>服务降级：比如在商品秒杀时，部分用户会被引导到一个降级页面。&lt;/li>
&lt;/ul>
&lt;h4 id="软状态soft-state">软状态（Soft-state）&lt;/h4>
&lt;p>软状态也称为弱状态，是指允许系统中的数据存在中间状态，并认为该中间状态的存在不会影响系统整体的可用性，即允许不同节点间的数据同步存在延时。&lt;/p></description></item><item><title>Go 语言的内存管理和垃圾回收</title><link>https://wimi.space/posts/2025/06/golang-memory-management/</link><pubDate>Fri, 20 Jun 2025 15:19:51 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-memory-management/</guid><description>&lt;p>程序中的数据和变量都会被分配到程序所在的虚拟内存中，内存空间包含两个重要区域：栈区（Stack）和堆区（Heap）。Go 语言的内存分配机制采用自动内存管理，由编译器和运行时协作完成。&lt;/p>
&lt;ul>
&lt;li>编译器负责栈内存的分配和释放。&lt;/li>
&lt;li>堆内存的分配则由运行时的内存分配器和垃圾收集器共同管理。&lt;/li>
&lt;/ul>
&lt;h2 id="堆内存管理">堆内存管理&lt;/h2>
&lt;p>程序在运行期间可以主动从堆区申请内存空间，这些内存由内存分配器分配并由垃圾收集器负责回收。Go 语言的内存分配器使用了多种策略来优化内存分配和回收的效率。&lt;/p>
&lt;h3 id="内存分配器">内存分配器&lt;/h3>
&lt;p>内存分配器的主要任务是将用户程序的内存申请请求转换为堆区的实际内存分配操作。当用户程序申请内存时，会通过内存分配器申请新内存，而内存分配器会负责从堆中初始化相应的内存区域。内存分配器的核心是一个内存池，它将堆区划分为多个小块，并根据需要动态分配和释放这些小块。&lt;/p>
&lt;h4 id="分配方法">分配方法&lt;/h4>
&lt;p>内存分配器通常支持多种分配方法，如线性分配器和空闲链表分配器，以满足不同的内存分配需求。&lt;/p>
&lt;h5 id="线性分配器">线性分配器&lt;/h5>
&lt;p>线性分配器是一种简单高效的内存分配方法，它通过维护一个指针来跟踪当前分配的位置，每次分配时只需将指针向后移动一定的字节数即可。当用户程序向分配器申请内存时，分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置。&lt;/p>
&lt;ul>
&lt;li>优点是分配速度非常快，因为它只需要更新指针，而不需要进行复杂的内存管理操作。&lt;/li>
&lt;li>缺点是无法释放已分配的内存，因此适用于那些生命周期较短的内存分配场景。&lt;/li>
&lt;/ul>
&lt;h5 id="空闲链表分配器">空闲链表分配器&lt;/h5>
&lt;p>空闲链表分配器是一种更复杂的内存分配方法，它维护一个空闲链表来跟踪未使用的内存块。当需要分配内存时，分配器会从空闲链表中找到合适的内存块并将其分配给用户程序。&lt;/p>
&lt;ul>
&lt;li>优点是可以释放已分配的内存，从而更有效地利用堆区的内存资源。&lt;/li>
&lt;li>缺点是分配速度相对较慢，因为需要在空闲链表中查找合适的内存块。&lt;/li>
&lt;/ul>
&lt;h4 id="go-内存分配方法">Go 内存分配方法&lt;/h4>
&lt;p>Go 语言的内存分配器采用了 TCMalloc（Thread-Caching Malloc）线程缓存分配，这是一种线程缓存的内存分配算法。它的核心理念是使用多级缓存将对象根据大小分类，并按照类别实施不同的分配策略。&lt;/p>
&lt;p>内存分配管理的对象按照大小可以分为：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>类别&lt;/th>
 &lt;th>大小&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>微对象&lt;/td>
 &lt;td>(0, 16B)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>小对象&lt;/td>
 &lt;td>[16B, 32KB]&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大对象&lt;/td>
 &lt;td>(32KB, +∞)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>通过为每个线程维护一个本地缓存来减少锁的竞争，每个线程在分配内存时，首先尝试从本地缓存中获取内存，如果本地缓存中没有足够的内存，则向全局内存池申请新的内存块。这样可以减少线程之间的锁竞争，提高内存分配的效率。&lt;/p>

&lt;details>
 &lt;summary>点击展开&lt;/summary>
 &lt;p>&lt;img src="https://static.wimi.space/blog/golang-memory-alloc.webp" alt="">
「图片来自&lt;a href="https://cloud.tencent.com/developer/article/2395092">一文搞懂 Go 内存分配器&lt;/a>」&lt;/p>
&lt;/details>

&lt;h3 id="垃圾收集器">垃圾收集器&lt;/h3>
&lt;p>为了解决原始标记清除算法带来的长时间 STW，多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短 STW 的时间。&lt;/p>
&lt;p>Go 语言的 GC 主要经历了以下三个重要阶段：&lt;/p>
&lt;ul>
&lt;li>Go1.3 的标记清除。&lt;/li>
&lt;li>Go1.5 并发三色标记法和插入写屏障。&lt;/li>
&lt;li>Go1.8 三色标记和混合写屏障机制。&lt;/li>
&lt;/ul>
&lt;p>其核心思想是优化 STW 时间，实现低停顿的并发垃圾回收，是其并发能力的核心支撑之一。&lt;/p>
&lt;h4 id="三色标记清除法">三色标记清除法&lt;/h4>
&lt;p>该算法的核心思想是通过对对象进行标记和分类来确定哪些对象是可达的，哪些对象是不可达的，从而进行垃圾回收。根据每个对象的颜色，分到不同的颜色集合中，对象的颜色是在标记阶段完成的。&lt;/p>
&lt;p>将内存对象分为三类颜色：&lt;/p>
&lt;ul>
&lt;li>白色（待回收态）：表示对象还没有被标记，如果在垃圾回收结束时仍然是白色的，那么它将被回收。&lt;/li>
&lt;li>灰色（中间态）：表示对象已经被发现并标记，但其引用的子对象还没有被扫描，灰色对象需要进一步处理。&lt;/li>
&lt;li>黑色（存活态）：表示对象已经被扫描，并且所有引用的子对象也已经被标记，黑色对象为存活对象，不会被回收。&lt;/li>
&lt;/ul>
&lt;p>三色标记清除法的工作步骤：&lt;/p>
&lt;ol>
&lt;li>初始化阶段
&lt;ul>
&lt;li>所有对象开始时都被标记为白色。&lt;/li>
&lt;li>根对象被标记为灰色，并放入一个待处理队列中。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>标记阶段
&lt;ul>
&lt;li>重复以下步骤，直到待处理队列为空：
&lt;ul>
&lt;li>从待处理队列中取出一个灰色对象，并将其标记为黑色。&lt;/li>
&lt;li>遍历该对象的所有引用。如果被引用的对象是白色的，将其标记为灰色，并放入待处理队列中。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>清除阶段
&lt;ul>
&lt;li>遍历所有对象：
&lt;ul>
&lt;li>如果对象是白色的，说明它是不可达的，可以被回收。&lt;/li>
&lt;li>如果对象是黑色的，说明它是可达的，保留不动。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h4 id="混合写屏障">混合写屏障&lt;/h4>
&lt;p>在用户协程与 GC 协程并发执行的场景下，可能导致部分存活对象未被正确标记的情况。为了支持能够并发进行垃圾回收，Go 语言在垃圾回收过程中采用了混合屏障技术，在指针修改时触发，确保三色不变式不被破坏（没看明白，暂不做展开🤨）。&lt;/p></description></item><item><title>Go 语言的协程调度模型</title><link>https://wimi.space/posts/2025/06/golang-gpm-model/</link><pubDate>Thu, 19 Jun 2025 10:27:40 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-gpm-model/</guid><description>&lt;h2 id="进程线程和协程">进程、线程和协程&lt;/h2>
&lt;p>在操作系统中，进程是资源分配的基本单位，线程是 CPU 调度的基本单位，而协程则是用户态的轻量级线程。&lt;/p>
&lt;p>协程通过调度器在单个线程中实现多任务并发执行。协程的调度器负责管理协程的生命周期和执行顺序，通常使用协作式调度或抢占式调度。&lt;/p>
&lt;h2 id="gpm-调度模型">GPM 调度模型&lt;/h2>
&lt;p>GPM 调度模型中的 G、P、M 分别代表 Goroutine、Processor 和 Machine，它通过将 Goroutine 映射到 Processor 上，并在 Machine 上执行，实现了高效的并发执行。&lt;/p>
&lt;p>Goroutine 的轻量级特性和协作式调度使得 Go 语言能够轻松处理数以万计的并发任务。GPM 模型的设计理念和实现方式为 Go 语言的高性能并发编程提供了强大的支持。&lt;/p>
&lt;p>&lt;img src="http://tonybai.com/wp-content/uploads/goroutine-scheduler-model.png" alt="">
「图片来自 &lt;a href="https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">TonyBai&lt;/a>」&lt;/p>
&lt;h3 id="底层数据结构">底层数据结构&lt;/h3>
&lt;p>go1.23 对应结构体定义如下，省略部分字段和方法，完整源代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/runtime2.go">Github&lt;/a>&lt;/p>
&lt;pre>&lt;code class="language-go">type g struct {
 stack stack // offset known to runtime/cgo
 stackguard0 uintptr // offset known to liblink
 stackguard1 uintptr // offset known to liblink

 _panic *_panic // innermost panic - offset known to liblink
 _defer *_defer // innermost defer
 m *m // current m; offset known to arm liblink
 sched gobuf

 goid uint64

 // 省略其他字段 ...
}

type m struct {
 g0 *g // Goroutine with scheduling stack
 curg *g // current running Goroutine

 p puintptr // attached p for executing go code (nil if not executing go code)
 nextp puintptr
 oldp puintptr // the p that was attached before executing a syscall

 // 省略其他字段 ...
}

type p struct {
 m muintptr // back-link to associated m (nil if idle)

 // Queue of runnable Goroutines. Accessed without lock.
 runqhead uint32
 runqtail uint32
 runq [256]guintptr
 runnext guintptr

 // 省略其他字段 ...
}

type schedt struct {
 midle muintptr // idle m's waiting for work
 nmidle int32 // number of idle m's waiting for work
 nmidlelocked int32 // number of locked m's waiting for work
 mnext int64 // number of m's that have been created and next M ID
 maxmcount int32 // maximum number of m's allowed (or die)
 nmsys int32 // number of system m's not counted for deadlock
 nmfreed int64 // cumulative number of freed m's

 // Global runnable queue.
 runq gQueue
 runqsize int32

 // 省略其他字段 ...
}
&lt;/code>&lt;/pre>
&lt;h4 id="ggoroutine">G（Goroutine）&lt;/h4>
&lt;p>Goroutine 是 Go 语言的轻量级线程，具有以下特点：&lt;/p></description></item><item><title>Go 语言的切片和哈希表</title><link>https://wimi.space/posts/2025/06/golang-slice-and-map/</link><pubDate>Wed, 18 Jun 2025 10:11:53 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-slice-and-map/</guid><description>&lt;h2 id="数组">数组&lt;/h2>
&lt;p>Go 语言中的数组是一个由固定长度的相同类型元素组成的序列。&lt;/p>
&lt;ul>
&lt;li>数组类型定义了长度和元素类型，在内存中连续存储，通过索引的方式访问。&lt;/li>
&lt;li>数组变量属于值类型，当一个数组变量被赋值或者传递时会复制整个数组。为了避免复制数组，一般会传递指向数组的指针。&lt;/li>
&lt;li>数组的长度是固定的，长度是数组类型的一部分。长度不同的两个数组是不可以相互赋值的，因为这两个数组属于不同的类型。&lt;/li>
&lt;/ul>
&lt;h3 id="底层数据结构">底层数据结构&lt;/h3>
&lt;p>Go 语言数组类型的底层数据结构定义如下，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/cmd/compile/internal/types/type.go#L424">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-go">// Array contains Type fields specific to array types.
type Array struct {
	Elem *Type // element type
	Bound int64 // number of elements; &amp;lt;0 if unknown yet
}
&lt;/code>&lt;/pre>
&lt;p>该类型包含两个字段，分别是元素类型 Elem 和数组的大小 Bound，这两个字段共同构成了数组类型，而当前数组是否应该在堆栈中初始化也在编译期就确定了。&lt;/p>
&lt;h2 id="切片">切片&lt;/h2>
&lt;p>Go 的切片是在数组之上的抽象数据类型。数组固定长度，缺少灵活性，大部分场景下会选择使用基于数组构建的切片类型，切片类型为处理同类型数据序列提供一个方便而高效的方式。&lt;/p>
&lt;h3 id="底层数据结构-1">底层数据结构&lt;/h3>
&lt;p>Go 语言切片的底层数据结构包含三个字段，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/slice.go#L15">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-go">type slice struct {
 array unsafe.Pointer
 len int
 cap int
}
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;code>array&lt;/code>：指向底层数组类型的指针。&lt;/li>
&lt;li>&lt;code>len&lt;/code>：切片的长度，表示切片中元素的个数。&lt;/li>
&lt;li>&lt;code>cap&lt;/code>：切片的容量，表示切片可以容纳的最大元素个数。&lt;/li>
&lt;/ul>
&lt;p>切片的长度和容量可以通过内置函数 &lt;code>len()&lt;/code> 和 &lt;code>cap()&lt;/code> 获取。&lt;/p>
&lt;h3 id="常用操作">常用操作&lt;/h3>
&lt;p>切片操作并不复制切片指向的元素，创建一个新的切片会复用原来切片的底层数组，因此切片操作是非常高效的。&lt;/p></description></item><item><title>Go 语言通道的常见用法</title><link>https://wimi.space/posts/2025/06/golang-channel/</link><pubDate>Tue, 17 Jun 2025 16:37:27 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-channel/</guid><description>&lt;blockquote>
&lt;p>『不要通过共享内存来通信，我们应该通过通信来共享内存』&lt;/p>
&lt;p>Do not communicate by sharing memory; instead, share memory by communicating.&lt;/p>&lt;/blockquote>
&lt;h2 id="go-语言推崇的并发解决方案csp">Go 语言推崇的并发解决方案（CSP）&lt;/h2>
&lt;p>在很多环境中，并发编程带来的很多问题都是因为没有正确实现访问共享内存的逻辑，虽然 Go 语言中也能使用共享内存加互斥锁进行通信，但是 Go 语言提供了一种不同的并发模型，即通信顺序进程（Communicating sequential processes，CSP）。&lt;/p>
&lt;p>Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介，Goroutine 之间会通过 Channel 传递数据。这种方式可以保证在同一时间只有一个 Goroutine 能够访问对应的数据，所以数据冲突和线程竞争的问题在设计上就不可能出现。&lt;/p>
&lt;h2 id="通道的数据结构">通道的数据结构&lt;/h2>
&lt;p>Go 语言中 Channel 是 Goroutine 间重要通信的方式，是并发安全的，通道内的数据遵循 FIFO 的队列特性。&lt;/p>
&lt;p>底层的数据结构定义如下，完整代码见 &lt;a href="https://github.com/golang/go/blob/6885bad7dd86880be6929c02085e5c7a67ff2887/src/runtime/chan.go#L33">Github&lt;/a>（go1.23）：&lt;/p>
&lt;pre>&lt;code class="language-go">type hchan struct {
 qcount uint // total data in the queue
 dataqsiz uint // size of the circular queue
 buf unsafe.Pointer // points to an array of dataqsiz elements
 elemsize uint16
 closed uint32
 timer *timer // timer feeding this chan
 elemtype *_type // element type
 sendx uint // send index
 recvx uint // receive index
 recvq waitq // list of recv waiters
 sendq waitq // list of send waiters

 // lock protects all fields in hchan, as well as several
 // fields in sudogs blocked on this channel.
 //
 // Do not change another G's status while holding this lock
 // (in particular, do not ready a G), as this can deadlock
 // with stack shrinking.
 lock mutex
}
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>qcount 记录着通道内数据个数。&lt;/li>
&lt;li>dataqsiz 是缓存型通道的大小。&lt;/li>
&lt;li>buf 指向一个数组，用来实现循环队列。对于有缓冲通道，长度为 dataqsiz，对于无缓冲通道，buf 为 nil。&lt;/li>
&lt;li>sendx 是循环队列的队尾指针。&lt;/li>
&lt;li>recvx 是循环队列的队头指针。&lt;/li>
&lt;li>lock 是互斥锁，用来保护 hchan 的数据结构，保证操作的原子性。&lt;/li>
&lt;/ul>
&lt;p>hchan 的结构图如下：
&lt;img src="https://static.cyub.vip/images/202011/channel_struct.jpg" alt="">
「图片来自 &lt;a href="https://go.cyub.vip/concurrency/channel/">深入 Go 语言之旅&lt;/a>」&lt;/p></description></item><item><title>Go 语言的互斥锁和读写锁</title><link>https://wimi.space/posts/2025/06/golang-mutex-and-rwmutex/</link><pubDate>Mon, 16 Jun 2025 15:08:29 +0800</pubDate><guid>https://wimi.space/posts/2025/06/golang-mutex-and-rwmutex/</guid><description>&lt;h2 id="临界区">临界区&lt;/h2>
&lt;p>临界区是指访问共享资源的代码段，这些资源无法被多个线程或协程同时安全地访问。若多个并发执行单元同时进入临界区操作共享资源，则可能发生竞态条件，导致数据竞争和不可预测的行为。&lt;/p>
&lt;p>我们可以使用互斥锁，限定临界区只能同时由一个线程持有。当临界区由一个线程持有的时候，其它线程如果想进入这个临界区，就会返回失败，或者是等待。直到持有的线程退出临界区，这些等待线程中的某一个才有机会接着持有这个临界区。&lt;/p>
&lt;h2 id="锁的分类">锁的分类&lt;/h2>
&lt;p>在并发编程中，从锁的逻辑思想上来看，分为悲观锁和乐观锁两种类型，这两种锁代表了在并发控制中两种截然不同的哲学，核心在于处理冲突的时机。&lt;/p>
&lt;h3 id="悲观锁">悲观锁&lt;/h3>
&lt;p>总是假设最坏情况会发生。在访问数据之前就认为其他线程很可能也会修改这份数据。因此，在读取数据时就先加锁，确保在自己操作期间，任何其他线程都无法修改这份数据，直到当前操作完成并释放锁。&lt;/p>
&lt;p>具体子类型有以下几种：&lt;/p>
&lt;h4 id="互斥锁">互斥锁&lt;/h4>
&lt;p>最基本的锁类型，同一时间只允许一个线程持有锁。其他尝试获取锁的线程会被阻塞，进入锁等待或直接失败，同时线程会释放 CPU 给其他线程。互斥锁适合写操作频繁、冲突概率高的场景，能有效避免脏写。&lt;/p>
&lt;h4 id="自旋锁">自旋锁&lt;/h4>
&lt;p>与互斥锁相同，同一时间只允许一个线程持有锁；区别在与线程在未获取锁时进入忙等待，持续循环检查而非立即阻塞。适用于锁持有时间短的场景，避免了线程阻塞和唤醒过程中的内核操作和上下文切换带来的开销成本。&lt;/p>
&lt;h4 id="读写锁">读写锁&lt;/h4>
&lt;p>将锁分为读锁和写锁。读锁只要没有写锁存在，允许多个线程同时持有读锁。写锁就是互斥锁，一旦一个线程持有写锁，其他任何线程都无法获取锁。读写锁提高了线程的并发度，适用于读多写少的场景。&lt;/p>
&lt;h3 id="乐观锁">乐观锁&lt;/h3>
&lt;p>总是假设最好的情况。认为在自己操作期间，其他线程不太可能修改同一份数据，因此，在读取数据时不加锁，直接进行操作。但在提交更新前，会检查数据是否被其他事务修改过。如果没被修改，则提交成功；如果被修改了，则放弃本次修改。&lt;/p>
&lt;p>『乐观锁本质上不是真正的锁机制，不需要传统意义上的阻塞其他线程的互斥锁』。常见的乐观锁实现机制有以下几种：&lt;/p>
&lt;h4 id="版本号机制">版本号机制&lt;/h4>
&lt;p>为数据项添加一个版本号或时间戳字段。每次读取数据时，同时读取当前版本号；在更新数据前，先检查当前内存中的版本号是否与之前读取的版本号一致，不一致则说明数据已经被其他线程所修改，更新操作失败。可参考『&lt;a href="https://wimi.space/posts/2025/06/business-version-number/">版本号机制在业务中的应用&lt;/a>』。&lt;/p>
&lt;h4 id="compare-and-swapcas">Compare And Swap（CAS）&lt;/h4>
&lt;p>CAS 是一种由现代 CPU 直接支持的原子操作，是现代并发编程的基石，尤其是实现无锁数据结构和非阻塞算法中经常被用到。它的核心功能是：在一条不可分割的指令中，完成读取、比较、写入这三个步骤，确保操作的原子性。&lt;/p>
&lt;h2 id="go-语言的悲观锁">Go 语言的悲观锁&lt;/h2>
&lt;p>Go 语言在 sync 包中提供了 sync.Mutex 和 sync.RWMutex 两种锁，前者是互斥锁，后者是读写锁。Go 语言并没有直接提供自旋锁的类型，通常使用 sync/atomic 包中的 CAS 原子操作来模拟实现，留作后续补充。&lt;/p>
&lt;p>在 Go 语言中，悲观锁是一种相对原始的同步机制，在多数情况下，我们都应该使用抽象层级更高的 Channel 实现同步。&lt;/p>
&lt;h3 id="syncmutex">sync.Mutex&lt;/h3>
&lt;p>互斥锁 Mutex 提供两个方法 Lock 和 Unlock，在进入临界区之前调用 Lock 方法，退出临界区的时候调用 Unlock 方法。当一个 Goroutine 通过调用 Lock 方法获得了这个锁的拥有权后，其它请求锁的 Goroutine 就会阻塞在 Lock 方法的调用上，直到锁被释放并且自己获取到了这个锁的拥有权。&lt;/p>

&lt;details>
 &lt;summary>代码示例&lt;/summary>
 &lt;p>对临界区数据并发访问，加锁和不加锁示例，&lt;a href="https://github.com/qmdx00/playground/blob/master/golang-sync-example/mutex_test.go">完整示例代码&lt;/a>：&lt;/p>
&lt;pre>&lt;code class="language-go">package main

import (
 &amp;quot;fmt&amp;quot;
 &amp;quot;sync&amp;quot;
 &amp;quot;testing&amp;quot;
)

func TestConcurrencyAdd(t *testing.T) {
 var count = 0
 var wg sync.WaitGroup

 for range 10 {
 wg.Add(1)
 go func() {
 defer wg.Done()
 for range 100000 {
 count++
 }
 }()
 }

 wg.Wait()
 fmt.Printf(&amp;quot;Want %d, Got %d\n&amp;quot;, 1000000, count)
}

func TestConcurrencyAddWithMutex(t *testing.T) {
 var count = 0
 var wg sync.WaitGroup
 var mutex sync.Mutex

 for range 10 {
 wg.Add(1)
 go func() {
 defer wg.Done()
 for range 100000 {
 mutex.Lock()
 count++
 mutex.Unlock()
 }
 }()
 }

 wg.Wait()
 fmt.Printf(&amp;quot;Want %d, Got %d\n&amp;quot;, 1000000, count)
}
&lt;/code>&lt;/pre>
&lt;p>运行结果：&lt;/p></description></item><item><title>版本号机制在业务中的应用</title><link>https://wimi.space/posts/2025/06/business-version-number/</link><pubDate>Fri, 13 Jun 2025 20:44:39 +0800</pubDate><guid>https://wimi.space/posts/2025/06/business-version-number/</guid><description>&lt;h2 id="版本号机制">版本号机制&lt;/h2>
&lt;p>使用版本号实现乐观锁是一种常见的做法，尤其在需要处理并发更新的场景中。以下是使用版本号实现乐观锁的步骤：&lt;/p>
&lt;ol>
&lt;li>添加版本号字段：在需要支持乐观锁的表中，添加一个版本号字段，通常是整型。&lt;/li>
&lt;li>读取数据时获取版本号：当读取记录时，同时获取该记录的版本号。&lt;/li>
&lt;li>更新数据时检查版本号：在更新记录之前，检查数据库中的版本号是否与之前读取的版本号一致。&lt;/li>
&lt;li>更新版本号：如果版本号一致，执行更新操作，并递增版本号。&lt;/li>
&lt;li>处理更新失败的情况：如果版本号不一致，说明记录已被其他事务更新，此时更新操作应该失败，并根据业务逻辑进行相应处理，比如重试或报错。&lt;/li>
&lt;/ol>
&lt;h2 id="项目举例">项目举例&lt;/h2>
&lt;h3 id="需求">需求&lt;/h3>
&lt;p>以账户余额增减的业务场景举例，使用版本号机制实现账户余额增减，同时生成账户流水。需要保证余额操作的最终一致性，在高并发场景下读写正确，在分布式环境下也不会有并发问题，实现省略用户登录态信息。&lt;/p>
&lt;h3 id="表结构设计">表结构设计&lt;/h3>
&lt;p>创建用户账户表和流水表，省略用户信息表。&lt;/p>

&lt;details>
 &lt;summary>表结构定义&lt;/summary>
 &lt;pre>&lt;code class="language-SQL">CREATE TABLE `account` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `user_id` bigint NOT NULL COMMENT '用户id',
 `balance` decimal(18,2) NOT NULL COMMENT '当前余额，精确到分',
 `version` int NOT NULL DEFAULT '0' COMMENT '版本号',
 `status` tinyint NOT NULL DEFAULT '1' COMMENT '账户状态（1：正常、2：冻结）',
 `created_at` datetime NOT NULL,
 `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`),
 UNIQUE KEY `udx_uid` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `account_flow` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `flow_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '流水号',
 `account_id` bigint NOT NULL COMMENT '关联的账户id',
 `amount` decimal(18,2) NOT NULL COMMENT '变动金额（正：进账，负：出账）',
 `balance_before` decimal(18,2) NOT NULL COMMENT '变动前余额',
 `balance_after` decimal(18,2) NOT NULL COMMENT '变动后余额',
 `type` tinyint NOT NULL COMMENT '流水类型（1：充值、2：消费、3：退款、4：提现）',
 `biz_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '业务单号',
 `version_seq` int NOT NULL COMMENT '关联账户的版本号（用于追溯）',
 `created_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`),
 UNIQUE KEY `udx_fno` (`flow_no`),
 KEY `idx_aid_cat` (`account_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
&lt;/code>&lt;/pre>
&lt;/details>

&lt;p>假设一个用户只能有一个账户，对用户 ID 创建唯一索引。流水表记录操作关联的版本和变动前后余额，方便追溯操作和统计分析。流水号全局唯一，示例用 UUID（具体业务可使用分布式 ID 来保证唯一性）同时创建了唯一索引约束，其他索引可根据业务需求自行增加。&lt;/p></description></item><item><title>Redis 过期删除和内存淘汰策略</title><link>https://wimi.space/posts/2025/06/redis-memory-strategy/</link><pubDate>Thu, 12 Jun 2025 10:01:00 +0800</pubDate><guid>https://wimi.space/posts/2025/06/redis-memory-strategy/</guid><description>&lt;p>Redis 的过期删除策略 和内存淘汰策略 是两个不同机制，分别用于处理过期键的清理和内存不足时的数据淘汰问题。&lt;/p>
&lt;h2 id="过期删除策略">过期删除策略&lt;/h2>
&lt;p>常见的三种过期删除策略分别是定时删除、惰性删除和定期删除。&lt;/p>
&lt;h3 id="定时删除策略">定时删除策略&lt;/h3>
&lt;p>定时删除策略的做法是，在设置 Key 的过期时间时，同时创建一个定时事件，当时间到达时，由事件处理器自动执行 Key 的删除操作。&lt;/p>
&lt;ul>
&lt;li>定时删除可以保证过期 Key 会被尽快删除，也就是内存可以被尽快地释放（定时删除对内存是最友好的）。&lt;/li>
&lt;li>在过期 Key 比较多的情况下，删除过期 Key 可能会占用相当一部分 CPU 时间，会对服务器的响应时间和吞吐量造成影响（定时删除策略对 CPU 不友好）。&lt;/li>
&lt;/ul>
&lt;h3 id="惰性删除策略">惰性删除策略&lt;/h3>
&lt;p>惰性删除策略的做法是，不主动删除过期键，每次从数据库访问 Key 时，都检测 Key 是否过期，如果过期则删除该 Key。&lt;/p>
&lt;ul>
&lt;li>每次访问时才检查 Key 是否过期，所以该策略只会使用很少的系统资源（惰性删除策略对 CPU 时间最友好）。&lt;/li>
&lt;li>如果一个 Key 已经过期但仍然保留在数据库中，只要一直没有被访问，它所占用的内存就不会释放，造成了一定的内存空间浪费（惰性删除策略对内存不友好）。&lt;/li>
&lt;/ul>
&lt;h3 id="定期删除策略">定期删除策略&lt;/h3>
&lt;p>定期删除策略的做法是，每隔一段时间随机从数据库中取出一定数量的 Key 进行检查，并删除其中的过期 Key。&lt;/p>
&lt;ul>
&lt;li>通过限制删除操作执行的时长和频率，来减少删除操作对 CPU 的影响，同时也能删除一部分过期的数据减少了过期键对空间的无效占用。&lt;/li>
&lt;li>内存清理方面没有定时删除效果好，同时没有惰性删除使用的系统资源少。&lt;/li>
&lt;/ul>
&lt;p>难以确定删除操作执行的时长和频率。如果执行的太频繁，会变得和定时删除策略一样对 CPU不友好；如果执行的太少，则过期Key占用的内存不会及时得到释放。&lt;/p>
&lt;h3 id="redis-的删除策略">Redis 的删除策略&lt;/h3>
&lt;p>前面的三种过期删除策略，每一种都有优缺点，单独使用某一个策略都不能满足实际需求。Redis选择『惰性删除 + 定期删除』这两种策略配和使用，以求在合理使用CPU时间和避免内存浪费之间取得平衡。&lt;/p>
&lt;h2 id="内存淘汰策略">内存淘汰策略&lt;/h2>
&lt;p>当 Redis 的运行内存已经超过 Redis 设置的最大内存之后，会使用内存淘汰策略删除符合条件的 Key，以此来保障 Redis 高效的运行。&lt;/p>
&lt;p>Redis 内存淘汰策略共有八种，这八种策略大体分为『不进行数据淘汰』和『进行数据淘汰』两类策略。&lt;/p>
&lt;h3 id="不进行数据淘汰的策略">不进行数据淘汰的策略&lt;/h3>
&lt;ul>
&lt;li>noeviction（Redis 3.0 之后默认的内存淘汰策略）：当运行内存超过最大设置内存时，不淘汰任何数据，此时新的数据写入会被禁止。&lt;/li>
&lt;/ul>
&lt;h3 id="进行数据淘汰的策略">进行数据淘汰的策略&lt;/h3>
&lt;p>针对『进行数据淘汰』这一类策略，又可以细分为『在设置了过期时间的数据中进行淘汰』和『在所有数据范围内进行淘汰』这两类策略。&lt;/p></description></item><item><title>Redis 缓存策略和缓存异常</title><link>https://wimi.space/posts/2025/06/redis-cache-strategy/</link><pubDate>Wed, 11 Jun 2025 11:21:05 +0800</pubDate><guid>https://wimi.space/posts/2025/06/redis-cache-strategy/</guid><description>&lt;h2 id="缓存读写策略">缓存读写策略&lt;/h2>
&lt;p>Redis 中常用的缓存读写策略主要有以下几种，各有其适用场景和优缺点。&lt;/p>
&lt;h3 id="旁路缓存cache-aside">旁路缓存（Cache Aside）&lt;/h3>
&lt;p>服务端需同时维护数据库和缓存，读取时优先从缓存获取数据，缓存未命中则从数据库读取并回写缓存；写入时直接更新数据库并删除缓存。&lt;/p>
&lt;p>读策略：&lt;/p>
&lt;ol>
&lt;li>从缓存中读取数据，读取到就直接返回。&lt;/li>
&lt;li>缓存中读取不到数据，就从数据库中读取数据并返回。&lt;/li>
&lt;li>把读到的数据写入到缓存中。&lt;/li>
&lt;/ol>
&lt;p>写策略：&lt;/p>
&lt;ol>
&lt;li>先更新数据库。&lt;/li>
&lt;li>再直接删除缓存。&lt;/li>
&lt;/ol>
&lt;p>优点：&lt;/p>
&lt;ul>
&lt;li>简单易实现，适用于小规模系统。&lt;/li>
&lt;li>缓存数据不会因过期导致雪崩问题。&lt;/li>
&lt;li>通过缓存加速读取性能，减轻数据库压力。&lt;/li>
&lt;/ul>
&lt;p>缺点：&lt;/p>
&lt;ul>
&lt;li>首次请求数据一定不在缓存中，可通过预热热点数据缓解。&lt;/li>
&lt;li>数据一致性依赖业务逻辑维护，存在短暂不一致风险。&lt;/li>
&lt;li>写操作频繁时可能频繁删除缓存，影响性能。&lt;/li>
&lt;/ul>
&lt;h3 id="读写穿透readwrite-through">读写穿透（Read/Write Through）&lt;/h3>
&lt;p>单独提供一个缓存服务接管所有读写操作，服务端把缓存视为主要数据存储，从中读取数据并将数据写入其中，不直接与数据库交互。读取时若缓存未命中则由缓存层同步从数据库加载；写入时缓存层同时更新缓存和数据库。&lt;/p>
&lt;p>读写穿透策略实际只是在旁路缓存策略之上进行了封装，旁路策略的读写操作是由客户端来完成的，而读写穿透策略则是由缓存服务来完成的，后者对客户端是透明的，从而减轻了客户端的职责，简化了客户端的数据操作。&lt;/p>
&lt;ul>
&lt;li>在旁路缓存下，发生读请求的时候，如果缓存中不存在对应的数据，则由客户端自己负责把数据写入缓存，而读写穿透策略则是缓存服务自己来写入缓存的，这对客户端是不可见的。&lt;/li>
&lt;li>和旁路缓存一样，读写穿透也有首次请求数据一定不在缓存中的问题，对于热点数据可以提前放入缓存中。&lt;/li>
&lt;/ul>
&lt;h3 id="异步写入write-behind">异步写入（Write Behind）&lt;/h3>
&lt;p>仅在缓存层进行写操作，后续以异步方式更新数据库，降低实时写入压力。&lt;/p>
&lt;p>异步写入和读写穿透策略很相似，两者都是由缓存服务来负责缓存和数据库的读写。不同的是，读写穿透是同步更新缓存和数据库，而异步写入则是只更新缓存，不直接更新数据库，而是改为异步批量的方式来更新数据库。&lt;/p>
&lt;h2 id="缓存异常问题">缓存异常问题&lt;/h2>
&lt;p>引入了缓存层，就会有缓存异常的三个问题，分别是缓存雪崩、缓存击穿、缓存穿透。&lt;/p>
&lt;h3 id="缓存雪崩">缓存雪崩&lt;/h3>
&lt;p>当大量缓存在同一时间过期或 Redis 故障宕机时，如果此时有大量的请求无法从 Redis 中获取缓存数据，全部去直接访问数据库，会导致数据库压力激增，严重的会导致数据库宕机，从而导致一系列的连锁反应造成整个系统崩溃，这就是『缓存雪崩』问题。&lt;/p>
&lt;p>发生缓存雪崩有以下两个原因：&lt;/p>
&lt;ul>
&lt;li>大量数据同时过期&lt;/li>
&lt;li>Redis 故障宕机&lt;/li>
&lt;/ul>
&lt;p>针对大量数据同时过期而引发的缓存雪崩问题，常见的应对方法有下面这几种：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>均匀设置过期时间&lt;/p>
&lt;ul>
&lt;li>如果要给缓存数据设置过期时间，应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时，给这些数据的过期时间加上一个随机数，这样就保证数据不会在同一时间过期。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>互斥锁保证同一时间内只有一个请求构建缓存&lt;/p>
&lt;ul>
&lt;li>当业务线程在处理用户请求时，如果发现访问的数据不在 Redis 里，就加个互斥锁，保证同一时间内只有一个请求来构建缓存，当缓存构建完成后，再释放锁。未能获取互斥锁的请求，要么等待锁释放后重新读取缓存，要么就返回空值或者默认值。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>后台更新缓存&lt;/p>
&lt;ul>
&lt;li>业务线程不再负责更新缓存，缓存也不设置有效期，而是让缓存『永久有效』，并将更新缓存的工作交由后台线程定时更新。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>针对 Redis 故障宕机引发的缓存雪崩问题，常见的应对方法有下面这几种：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>服务熔断或请求限流机制&lt;/p>
&lt;ul>
&lt;li>Redis 故障宕机而导致缓存雪崩问题时，可以启动服务熔断机制，暂停业务应用对缓存服务的访问，直接返回错误，不用再继续访问数据库，从而降低对数据库的访问压力，保证数据库系统的正常运行，然后等到 Redis 恢复正常后，再允许业务应用访问缓存服务。&lt;/li>
&lt;li>为了减少对业务的影响，也可以启用请求限流机制，只将少部分请求发送到数据库进行处理，再多的请求就在入口直接拒绝服务，等到 Redis 恢复正常并把缓存预热完后，再解除请求限流的机制。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>构建 Redis 缓存高可靠集群&lt;/p>
&lt;ul>
&lt;li>如果 Redis 缓存的主节点故障宕机，从节点可以切换成为主节点，继续提供缓存服务，避免了由于 Redis 故障宕机而导致的缓存雪崩问题。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="缓存击穿">缓存击穿&lt;/h3>
&lt;p>业务中被频繁地访问的数据被称为热点数据，如果缓存中的某个热点数据过期了，此时大量的请求访问了该热点数据，就无法从缓存中读取，直接访问数据库，数据库很容易就被高并发的请求冲垮，这就是『缓存击穿』的问题。&lt;/p></description></item><item><title>Redis 持久化技术</title><link>https://wimi.space/posts/2025/06/redis-persistence-desc/</link><pubDate>Tue, 10 Jun 2025 09:42:44 +0800</pubDate><guid>https://wimi.space/posts/2025/06/redis-persistence-desc/</guid><description>&lt;p>Redis 的读写操作都是在内存中，所以 Redis 性能才会高，但是当 Redis 重启后，内存中的数据就会丢失，为了保证内存中的数据不会丢失，Redis 实现了数据持久化的机制，这个机制会把数据存储到磁盘，这样在 Redis 重启就能够从磁盘中恢复原有的数据。&lt;/p>
&lt;p>&lt;img src="https://static.wimi.space/blog/redis-persistence.png" alt="">&lt;/p>
&lt;p>Redis 共有三种数据持久化的方式：&lt;/p>
&lt;ul>
&lt;li>AOF 日志：每执行一条写操作命令，就把该命令以追加的方式写入到一个文件里；&lt;/li>
&lt;li>RDB 快照：将某一时刻的内存数据，以二进制的方式写入磁盘；&lt;/li>
&lt;li>混合持久化方式：Redis 4.0 新增的方式，集成了 AOF 和 RDB 的优点；&lt;/li>
&lt;/ul>
&lt;h2 id="rdbredis-database-backup">RDB（Redis Database Backup）&lt;/h2>
&lt;p>&lt;img src="https://static.wimi.space/blog/redis-rdb.png" alt="">&lt;/p>
&lt;p>Redis 可以通过创建快照来获得存储在内存里面的数据在『某个时间点』上的副本。Redis 创建快照之后，可以对快照进行备份。通过快照的方式，将内存中的数据定期保存到磁盘上，生成一个紧凑的二进制文件（.rdb）。可以将快照复制到其他服务器从而创建具有相同数据的服务器副本，还可以将快照留在原地以便重启服务器的时候使用。&lt;/p>
&lt;ul>
&lt;li>优点：文件体积小，恢复速度快，适合大规模数据备份。&lt;/li>
&lt;li>缺点：如果在两次快照之间发生崩溃，最近的写操作可能会丢失。&lt;/li>
&lt;/ul>
&lt;p>Redis 提供了两个命令来生成 RDB 快照文件：&lt;/p>
&lt;ul>
&lt;li>SAVE：同步保存操作，会阻塞 Redis 主线程；&lt;/li>
&lt;li>BGSAVE：fork 出一个子进程，子进程执行，不会阻塞 Redis 主线程，默认选项。&lt;/li>
&lt;/ul>
&lt;h3 id="rdb-基本流程">RDB 基本流程&lt;/h3>
&lt;ol>
&lt;li>通常是由客户端显式执行上述命令触发，或者由配置的规则触发 RDB 快照。&lt;/li>
&lt;li>子进程遍历数据库中的键值对，将数据序列化为 RDB 文件格式，并写入临时文件。主进程继续处理请求，期间修改的数据会通过写时复制（Copy-on-Write）技术记录到内存缓冲区。&lt;/li>
&lt;li>子进程完成写入后，用临时文件原子替换旧的 RDB 文件（如 dump.rdb），确保数据一致性。&lt;/li>
&lt;li>完成后通过 LASTSAVE 命令可查看最后一次生成 RDB 的时间。&lt;/li>
&lt;/ol>
&lt;h2 id="aofappend-only-file">AOF（Append-Only File）&lt;/h2>
&lt;p>&lt;img src="https://static.wimi.space/blog/redis-aof.png" alt="">&lt;/p>
&lt;p>与快照持久化相比，AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF（append only file）方式的持久化（Redis 6.0 之后已经默认是开启了）。AOF 通过日志的方式，记录所有对数据库的写操作，保存在一个日志文件（.aof）中。重启时，Redis 会重放这些操作来恢复数据。&lt;/p></description></item><item><title>Redis 的基本介绍</title><link>https://wimi.space/posts/2025/06/redis-basic-introduction/</link><pubDate>Mon, 09 Jun 2025 11:00:43 +0800</pubDate><guid>https://wimi.space/posts/2025/06/redis-basic-introduction/</guid><description>&lt;blockquote>
&lt;p>⚠️️ 整理自互联网，用于概念了解。&lt;/p>&lt;/blockquote>
&lt;h2 id="redis-是什么">Redis 是什么&lt;/h2>
&lt;p>Redis（Remote Dictionary Server）是一种基于内存的数据库，对数据的读写操作都是在内存中完成，因此读写速度非常快，常用于缓存， 消息队列、分布式锁等场景。并且，Redis 存储的是 KV 键值对数据，为了满足不同的业务场景，Redis 内置了多种数据类型实现。并且 Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、内存淘汰机制、过期删除机制等等。&lt;/p>
&lt;h2 id="redis-常见数据类型">Redis 常见数据类型&lt;/h2>
&lt;p>更多常用命令查询见 &lt;a href="https://redis.io/docs/latest/commands/">Redis 命令手册&lt;/a>。&lt;/p>
&lt;h3 id="string">String&lt;/h3>
&lt;p>String 是最基本的 key-value 结构，key 是唯一标识，value 是具体的值。&lt;/p>
&lt;p>value 可以是字符串，也可以是数字（整数或浮点数），value 最多可以容纳的数据长度是 512M。&lt;/p>
&lt;h3 id="list">List&lt;/h3>
&lt;p>List 是简单的字符串列表，按照插入顺序排序，可以从头部或尾部向 List 列表添加元素。&lt;/p>
&lt;p>列表的最大长度为 2^32-1，即每个列表支持超过 40 亿个元素。&lt;/p>

&lt;details>
 &lt;summary>常用指令&lt;/summary>
 &lt;pre>&lt;code class="language-bash"># 将一个或多个值 value 插入到 key 列表的表头（最左边），最后的值在最前面
LPUSH key value [value ...]

# 将一个或多个值 value 插入到 key 列表的表尾（最右边）
RPUSH key value [value ...]

# 移除并返回 key 列表的头元素
LPOP key

# 移除并返回 key 列表的尾元素
RPOP key

# 返回列表 key 中指定区间内的元素，区间以偏移量 start 和 stop 指定，从 0 开始
LRANGE key start stop

# 从 key 列表表头弹出一个元素，没有就阻塞 timeout 秒，如果 timeout=0 则一直阻塞
BLPOP key [key ...] timeout

# 从 key 列表表尾弹出一个元素，没有就阻塞 timeout 秒，如果 timeout=0 则一直阻塞
BRPOP key [key ...] timeout
&lt;/code>&lt;/pre>
&lt;/details>

&lt;h3 id="hash">Hash&lt;/h3>
&lt;p>Hash 是一个键值对 key-value 集合，特别适合用于存储对象。&lt;/p></description></item><item><title>MySQL 锁相关</title><link>https://wimi.space/posts/2025/06/mysql-lock-desc/</link><pubDate>Fri, 06 Jun 2025 09:48:39 +0800</pubDate><guid>https://wimi.space/posts/2025/06/mysql-lock-desc/</guid><description>&lt;p>数据库锁设计的初衷是处理并发问题。作为多用户共享的资源，当出现并发访问的时候，数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。&lt;/p>
&lt;h1 id="mysql-锁的分类">MySQL 锁的分类&lt;/h1>
&lt;h2 id="根据锁的范围分类">根据锁的范围分类&lt;/h2>
&lt;p>根据加锁的范围，MySQL 里面的锁大致可以分成全局锁、表级锁和行级锁三类。&lt;/p>
&lt;h3 id="全局锁">全局锁&lt;/h3>
&lt;p>全局锁就是对整个数据库实例加锁。可以通过下面命令操作全局锁：&lt;/p>
&lt;pre>&lt;code class="language-SQL">-- 全局读锁
FLUSH TABLES WITH READ LOCK;

-- 释放锁
UNLOCK TABLES;
&lt;/code>&lt;/pre>
&lt;p>加全局读锁后，整个库就处于只读状态了，之后其他线程的以下语句会被阻塞：&lt;/p>
&lt;ul>
&lt;li>数据更新语句（数据的增删改）。&lt;/li>
&lt;li>数据定义语句（包括建表、修改表结构等）。&lt;/li>
&lt;li>更新类事务的提交语句。&lt;/li>
&lt;/ul>
&lt;p>全局锁主要应用于做全库逻辑备份，这样在备份数据库期间，不会因为数据或表结构的更新，而出现备份文件的数据与预期的不一样。&lt;/p>
&lt;p>加上全局锁后，整个数据库都是只读状态，这期间业务只能读取而不能更新数据，会导致业务停滞。如果数据库引擎支持事务的『可重复读隔离级别』，在备份数据库之前开启事务，会先创建读视图，整个事务执行期间都在用这个读视图，由于 MVCC 的支持，备份期间业务依然可以对数据进行更新操作。&lt;/p>
&lt;p>备份数据库的工具是 mysqldump，在使用 mysqldump 时加上 -single-transaction 参数的时候，就会在备份数据库之前先开启事务。这种方法只适用于支持『可重复读隔离级别的事务』的存储引擎。InnoDB 存储引擎默认就是可重复读隔离级别，可以采用这种方式来备份数据库。对于不支持事务的存储引擎如 MyISAM，在备份时只能使用全局锁的方式。&lt;/p>
&lt;h3 id="表级锁">表级锁&lt;/h3>
&lt;p>MySQL 里面表级别的锁有两种：一种是表锁，一种是元数据锁（METADATA LOCK）。&lt;/p>
&lt;h4 id="表锁">表锁&lt;/h4>
&lt;p>可以通过下面命令操作表锁：&lt;/p>
&lt;pre>&lt;code class="language-SQL">-- 表级别的共享锁，也就是读锁；允许当前会话读取被锁定的表，但阻止其他会话对这些表进行写操作。
LOCK TABLES table_name READ;

-- 表级别的独占锁，也就是写锁；允许当前会话对表进行读写操作，但阻止其他会话对这些表进行任何操作（读或写）。
LOCK TABLES table_name WRITE;

-- 释放当前会话的所有表锁。
UNLOCK TABLES;
&lt;/code>&lt;/pre>
&lt;p>在还没有出现更细粒度的锁的时候，表锁是最常用的处理并发的方式，不过尽量避免在使用 InnoDB 引擎的表使用表锁，因为表锁的颗粒度太大，会影响并发性能。InnoDB 有更细粒度的行级锁支持。&lt;/p>
&lt;h4 id="auto-inc-自增锁">AUTO-INC 自增锁&lt;/h4>
&lt;p>当一个表有 AUTO_INCREMENT 列时，MySQL 会在插入新记录时自动为该表加上 AUTO-INC 锁，这是一种特殊的表级锁，目的是为了保证在高并发插入时，AUTO_INCREMENT 列的值能够正确地递增，不会出现重复值。&lt;/p></description></item><item><title>MySQL 索引相关</title><link>https://wimi.space/posts/2025/06/mysql-index-desc/</link><pubDate>Thu, 05 Jun 2025 15:31:54 +0800</pubDate><guid>https://wimi.space/posts/2025/06/mysql-index-desc/</guid><description>&lt;h2 id="innodb-的索引模型">InnoDB 的索引模型&lt;/h2>
&lt;p>&lt;img src="https://static.wimi.space/blog/mysql-index-model.png" alt="">&lt;/p>
&lt;p>根据叶子节点的内容，索引类型分为主键索引和非主键索引。&lt;/p>
&lt;ul>
&lt;li>主键索引的叶子节点存的是整行数据。在 InnoDB 里，主键索引也被称为聚簇索引（clustered index）。&lt;/li>
&lt;li>非主键索引的叶子节点内容是主键的值。在 InnoDB 里，非主键索引也被称为二级索引（secondary index）。&lt;/li>
&lt;/ul>
&lt;h3 id="b-树和数据页">B+ 树和数据页&lt;/h3>
&lt;p>InnoDB 的数据是按数据页为单位来读写的，默认数据页大小为16 KB。每个数据页之间通过双向链表的形式组织起来，物理上不连续，但是逻辑上连续。&lt;/p>
&lt;p>InnoDB 使用 B+ 树作为索引，B+ 树中的每个节点都是一个数据页，结构示意图如下：&lt;/p>
&lt;p>&lt;img src="https://cdn.xiaolincoding.com//mysql/other/7c635d682bd3cdc421bb9eea33a5a413.png" alt="">
「图片来自 &lt;a href="https://cdn.xiaolincoding.com//mysql/other/7c635d682bd3cdc421bb9eea33a5a413.png">小林 Coding&lt;/a>」&lt;/p>
&lt;h3 id="回表和覆盖索引">回表和覆盖索引&lt;/h3>
&lt;p>用表 T(id, k)，id 为主键索引，k 为非主键索引，来举例分析：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>通过主键索引查询方式，如 &lt;code>SELECT * FROM T WHERE id = ?&lt;/code> 只需要搜索 id 这棵 B+ 树；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>通过非主键索引查询方式，如 &lt;code>SELECT * FROM T WHERE k = ?&lt;/code> 则需要先搜索 k 索引树，得到 id 的值，再到 id 索引树搜索一次。这个过程称为『回表』。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>如果执行的语句是 &lt;code>SELECT id FROM T WHERE k = ?&lt;/code>，这时只需要查 id 的值，而 id 的值已经在 k 索引树上了，因此可以直接提供查询结果，不需要回表，这个过程称为『覆盖索引』。&lt;/p></description></item><item><title>MySQL 的事务隔离级别</title><link>https://wimi.space/posts/2025/06/mysql-transaction-isolation/</link><pubDate>Wed, 04 Jun 2025 16:14:24 +0800</pubDate><guid>https://wimi.space/posts/2025/06/mysql-transaction-isolation/</guid><description>&lt;p>MySQL的事务隔离作用是确保事务之间的操作相互隔离，避免数据不一致问题，从而保证数据库的一致性和并发事务的正确执行。&lt;/p>
&lt;p>它定义了多个并发事务如何看到彼此的数据变更。其核心作用在于解决并发事务操作同一数据时可能引发的数据一致性问题，确保每个事务都像是在独立运行的环境中操作数据。&lt;/p>
&lt;h2 id="并发事务会导致的问题">并发事务会导致的问题&lt;/h2>
&lt;h3 id="脏读">脏读&lt;/h3>
&lt;p>事务读取到其他事务尚未提交的无效数据。例如，事务 A 修改数据但未提交，事务 B 读取该数据后若A回滚，则 B 读到的是脏数据。&lt;/p>
&lt;h3 id="不可重复读">不可重复读&lt;/h3>
&lt;p>同一事务内多次读取同一数据，因其他事务提交了修改导致结果不一致。例如，事务 A 读取某行数据后，事务 B 更新并提交该行，A 再次读取时数据变化。&lt;/p>
&lt;h3 id="幻读">幻读&lt;/h3>
&lt;p>事务按条件查询数据时，其他事务插入符合条件的新数据并提交，导致当前事务再次查询时出现“幻影”记录。例如，事务 A 查询年龄 =20 的记录，事务 B 插入一条年龄 =20 的新记录并提交，A 再次查询时会多出一条数据。&lt;/p>
&lt;h2 id="事务隔离级别">事务隔离级别&lt;/h2>
&lt;p>多个事务并发执行时，可能会导致上述的问题，对事务的一致性产生不同程度的影响，SQL 标准提出了四种隔离级别来规避这些问题，隔离级别越高性能效率越低，隔离级别由低到高分别为：&lt;/p>
&lt;h3 id="读未提交read-uncommitted">读未提交（READ UNCOMMITTED）&lt;/h3>
&lt;p>最低的隔离级别，允许事务读取其他事务尚未提交的数据变更。&lt;/p>
&lt;p>这可能导致脏读、不可重复读或幻读问题。例如，事务A修改数据但未提交，事务B读取该数据后若A回滚，则B读到的是无效数据（脏读）。&lt;/p>
&lt;h3 id="读已提交read-committed">读已提交（READ COMMITTED）&lt;/h3>
&lt;p>此级别解决了脏读问题，但可能出现不可重复读和幻读。&lt;/p>
&lt;p>例如，同一事务内多次读取同一数据，结果因其他事务提交而不同。&lt;/p>
&lt;h3 id="可重复读repeatable-read">可重复读（REPEATABLE READ）&lt;/h3>
&lt;p>保证同一事务内多次读取相同数据的结果保持一致，避免脏读和不可重复读，但理论上仍可能遇到幻读。&lt;/p>
&lt;p>例如，事务A读取符合条件的数据，事务B插入新数据并提交后，A再次查询发现多出数据。解决的方案有两种：&lt;/p>
&lt;ul>
&lt;li>针对快照读（普通 SELECT 语句），是通过 MVCC 方式解决了幻读，因为可重复读隔离级别下，事务执行过程中看到的数据，一直跟这个事务启动时看到的数据是一致的，即使中途有其他事务插入了一条数据，是查询不出来这条数据的，所以就很好了避免幻读问题。&lt;/li>
&lt;li>针对当前读（SELECT &amp;hellip; FOR UPDATE 等语句），是通过 Next-key Lock（记录锁+间隙锁）方式解决了幻读，因为当执行该语句的时候，会加上 Next-key Lock，如果有其他事务在锁范围内插入了一条记录，那么这个插入语句就会被阻塞，无法成功插入，所以就很好了避免幻读问题。&lt;/li>
&lt;/ul>
&lt;p>这两个解决方案是很大程度上解决了幻读现象，但是还是有个别的情况造成的幻读现象是无法解决的。&lt;/p>
&lt;h3 id="串行化serializable">串行化（SERIALIZABLE）&lt;/h3>
&lt;p>最高的隔离级别，强制事务串行执行，完全隔离并发操作，避免脏读、不可重复读和幻读。&lt;/p>
&lt;p>但会显著降低数据库性能，仅在严格要求数据一致性时使用。&lt;/p>
&lt;h2 id="场景举例">场景举例&lt;/h2>
&lt;pre>&lt;code class="language-SQL">CREATE TABLE `T` (`value` int) ENGINE=InnoDB;
INSERT INTO `T` (`value`) VALUES (1);
&lt;/code>&lt;/pre>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th style="text-align: center">操作步骤&lt;/th>
 &lt;th style="text-align: center">事务 A&lt;/th>
 &lt;th style="text-align: center">事务 B&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td style="text-align: center">1&lt;/td>
 &lt;td style="text-align: center">启动事务，得到值 1&lt;/td>
 &lt;td style="text-align: center">开启事务&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">2&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;td style="text-align: center">查询得到值 1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">3&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;td style="text-align: center">将 1 改成 2&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">4&lt;/td>
 &lt;td style="text-align: center">查询得到值 V1&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">5&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;td style="text-align: center">提交事务 B&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">6&lt;/td>
 &lt;td style="text-align: center">查询得到值 V2&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">7&lt;/td>
 &lt;td style="text-align: center">提交事务 A&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td style="text-align: center">8&lt;/td>
 &lt;td style="text-align: center">查询得到值 V3&lt;/td>
 &lt;td style="text-align: center">&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>在不同的隔离级别下，事务 A 中的 V1、V2、V3 返回值分别是什么。&lt;/p></description></item><item><title>MySQL 三大日志和两阶段提交</title><link>https://wimi.space/posts/2025/06/mysql-undo-redo-binlog/</link><pubDate>Wed, 04 Jun 2025 10:39:54 +0800</pubDate><guid>https://wimi.space/posts/2025/06/mysql-undo-redo-binlog/</guid><description>&lt;h2 id="walwrite-ahead-logging">WAL（Write-Ahead Logging）&lt;/h2>
&lt;p>WAL 是 MySQL 数据库的核心日志机制，其核心原则是在将数据变更应用到数据库之前，先将这些变更写入日志。这种方式保证了即使系统发生崩溃，数据库也能通过回放日志恢复到一致的状态。&lt;/p>
&lt;p>以 InnoDB 存储引擎为例，当事务修改数据时，MySQL 会先将变更记录到 Redo log 中，再将数据写入磁盘。这样可以避免每次数据修改都要直接写入到数据文件，因为日志文件的写入操作通常比直接写入数据文件更高效。这种机制减少了直接磁盘 IO 的开销，并确保在崩溃恢复时能通过日志重放恢复未落盘的数据。&lt;/p>
&lt;h2 id="mysql-三大日志">MySQL 三大日志&lt;/h2>
&lt;p>MySQL 使用多种日志来实现不同的功能，不同的日志类型解决了数据库运行中不同层面的关键问题，最常见的是 Redo log、Undo log 和 Binlog。它们都遵循 WAL 机制，但用途和实现方式不同。&lt;/p>
&lt;ul>
&lt;li>Redo log 是 InnoDB 存储引擎级别的日志，主要服务于崩溃恢复。&lt;/li>
&lt;li>Undo log 是 InnoDB 存储引擎级别的日志，主要用于事务回滚和多版本并发控制（MVCC）的实现。&lt;/li>
&lt;li>Binlog 是 MySQL 服务级别的日志，主要服务于数据备份、主从复制和数据恢复。&lt;/li>
&lt;/ul>
&lt;h3 id="redo-log">Redo Log&lt;/h3>
&lt;p>Redo Log 是 MySQL InnoDB 存储引擎的重要组成部分，用于记录数据页的物理变更，保证事务的持久性和数据恢复，确保了数据库的稳定性和数据的一致性。&lt;/p>
&lt;p>Redo Log 首先存储在内存中的日志缓冲区中，在合适的时间从缓冲区刷写到磁盘上的日志文件中，默认有 ib_logfile1 和 ib_logfile2 两个日志文件，大小固定，循环使用。Redo Log 中记录的数据页变更信息通常包括表空间 ID、页号、偏移量和新值等。有了 Redo log，InnoDB 就可以保证即使数据库发生异常重启，之前提交的记录都不会丢失，这个能力称为 crash-safe。&lt;/p>
&lt;ul>
&lt;li>作用：用于崩溃恢复（Crash recovery）。&lt;/li>
&lt;li>特点：Redo log 是物理日志，记录的是数据库 Page 的低级别变更。当事务提交时，Redo log 确保这些变更可以在系统崩溃后被重新应用到数据库中，从而保证已提交事务的持久性。&lt;/li>
&lt;/ul>
&lt;h3 id="undo-log">Undo Log&lt;/h3>
&lt;p>Undo Log 是 MySQL InnoDB 存储引擎中的一种核心日志机制，它主要解决了事务的回滚和实现多版本并发控制 (MVCC)。它是保证数据库 ACID 特性 中 原子性和隔离性的重要基础。&lt;/p></description></item><item><title>MySQL 的基本架构</title><link>https://wimi.space/posts/2025/06/mysql-architecture-desc/</link><pubDate>Tue, 03 Jun 2025 11:26:52 +0800</pubDate><guid>https://wimi.space/posts/2025/06/mysql-architecture-desc/</guid><description>&lt;blockquote>
&lt;p>⚠️️ 整理自互联网，用于概念了解。&lt;/p>&lt;/blockquote>
&lt;p>MySQL 的架构采用经典的『客户端/服务器模型』和『分层设计』，核心组件协同工作以处理查询、管理数据、保证事务和提供高可用性等。其核心架构通常分为以下几个主要层次和组件，&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/pluggable-storage-overview.html">详见官方文档&lt;/a>。&lt;/p>
&lt;p>&lt;img src="https://dev.mysql.com/doc/refman/8.4/en/images/mysql-architecture.png" alt="">&lt;/p>
&lt;h2 id="连接层">连接层&lt;/h2>
&lt;h3 id="connectors">Connectors&lt;/h3>
&lt;p>连接器 &lt;a href="https://www.mysql.com/products/connector/">Connectors&lt;/a> 是 MySQL 官方提供的驱动程序，用于让不同编程语言编写的应用程序（客户端）能够与 MySQL 服务器通信。它实现了 MySQL 的通信协议，封装了底层连接细节，使开发者无需处理网络传输、数据封包等复杂操作。严格来说连接器是客户端的组件，而非服务端。&lt;/p>
&lt;h2 id="服务层">服务层&lt;/h2>
&lt;h3 id="sql-interface">SQL Interface&lt;/h3>
&lt;p>是 MySQL 服务层的核心组件之一，负责接收客户端发送的 SQL 命令，并将执行结果返回给客户端。是连接客户端请求与数据库内部处理的桥梁。&lt;/p>
&lt;p>主要的核心功能有：&lt;/p>
&lt;ol>
&lt;li>接收 SQL 命令：客户端通过连接器发送的 SQL 语句首先到达 SQL 接口。支持多种交互协议。&lt;/li>
&lt;li>分发 SQL 到内部组件：将 SQL 语句交给解析器进行语法和语义分析，生成解析树。解析后的指令交给优化器生成最优执行计划。&lt;/li>
&lt;li>处理执行结果：接收存储引擎返回的数据（如查询结果集），将结果转换为客户端兼容的格式（如二进制流、表格形式）。通过连接层将结果返回给客户端。&lt;/li>
&lt;/ol>
&lt;h3 id="parser">Parser&lt;/h3>
&lt;p>解析 SQL 语句，检查语法正确性，分解 SQL 为可执行的逻辑单元。&lt;/p>
&lt;h3 id="optimizer">Optimizer&lt;/h3>
&lt;p>优化 SQL 查询，生成高效的执行计划。基于索引、表统计信息选择最佳执行路径（如选择 JOIN 顺序、索引使用）。&lt;/p>
&lt;h3 id="executor">Executor&lt;/h3>
&lt;p>根据优化器生成的执行计划，调用存储引擎提供的接口来执行操作。如果启用了查询缓存（在 MySQL 5.7 及更早版本中），会先检查查询缓存。&lt;/p>
&lt;h3 id="caches">Caches&lt;/h3>
&lt;p>作用：缓存查询结果以提高性能。功能：命中相同查询直接返回结果（8.0 已移除，推荐外部缓存如 Redis）。&lt;/p>
&lt;h2 id="存储引擎层">存储引擎层&lt;/h2>
&lt;p>负责数据的物理存储、检索和管理。MySQL 的核心优势之一是其可插拔的存储引擎架构。存储引擎是表级别的，同一个数据库的不同表可以使用不同的存储引擎。存储引擎通过定义良好的 API 与服务层交互，这些 API 屏蔽了不同引擎实现的差异。&lt;/p>
&lt;p>常见引擎：&lt;/p>
&lt;h3 id="innodb">InnoDB&lt;/h3>
&lt;p>MySQL 的默认引擎&lt;/p></description></item><item><title>Go 语言项目布局（转载文章）</title><link>https://wimi.space/posts/2021/06/golang-project-layout/</link><pubDate>Tue, 29 Jun 2021 11:18:01 +0800</pubDate><guid>https://wimi.space/posts/2021/06/golang-project-layout/</guid><description>&lt;blockquote>
&lt;p>转载文章，&lt;a href="https://github.com/golang-standards/project-layout/blob/master/README_zh.md">原文链接&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>这是 Go 应用程序项目的基本布局。它不是核心 Go 开发团队定义的官方标准；然而，它是 Go 生态系统中一组常见的老项目和新项目的布局模式。其中一些模式比其他模式更受欢迎。它还具有许多小的增强，以及对任何足够大的实际应用程序通用的几个支持目录。&lt;/p>
&lt;h2 id="go-目录">Go 目录&lt;/h2>
&lt;h3 id="cmd">/cmd&lt;/h3>
&lt;p>本项目的主干。&lt;/p>
&lt;p>每个应用程序的目录名应该与你想要的可执行文件的名称相匹配（例如，/cmd/myapp）。&lt;/p>
&lt;p>不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用，那么它应该位于 /pkg 目录中。如果代码不是可重用的，或者你不希望其他人重用它，请将该代码放到 /internal 目录中。你会惊讶于别人会怎么做，所以要明确你的意图!&lt;/p>
&lt;p>通常有一个小的 main 函数，从 /internal 和 /pkg 目录导入和调用代码，除此之外没有别的东西。&lt;/p>
&lt;h3 id="internal">/internal&lt;/h3>
&lt;p>私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。请注意，这个布局模式是由 Go 编译器本身执行的。注意，你并不局限于顶级 internal 目录。在项目树的任何级别上都可以有多个内部目录。&lt;/p>
&lt;p>你可以选择向 internal 包中添加一些额外的结构，以分隔共享和非共享的内部代码。这不是必需的（特别是对于较小的项目），但是最好有有可视化的线索来显示预期的包的用途。你的实际应用程序代码可以放在 /internal/app 目录下（例如 /internal/app/myapp），这些应用程序共享的代码可以放在 /internal/pkg 目录下（例如 /internal/pkg/myprivlib）。&lt;/p>
&lt;h3 id="pkg">/pkg&lt;/h3>
&lt;p>外部应用程序可以使用的库代码（例如 /pkg/mypubliclib）。其他项目会导入这些库，希望它们能正常工作，所以在这里放东西之前要三思） 注意，internal 目录是确保私有包不可导入的更好方法，因为它是由 Go 强制执行的。/pkg 目录仍然是一种很好的方式，可以显式地表示该目录中的代码对于其他人来说是安全使用的好方法。&lt;/p>
&lt;p>当根目录包含大量非 Go 组件和目录时，这也是一种将 Go 代码分组到一个位置的方法，这使得运行各种 Go 工具变得更加容易。&lt;/p>
&lt;p>如果你的应用程序项目真的很小，并且额外的嵌套并不能增加多少价值（除非你真的想要），那就不要使用它。当它变得足够大时，你的根目录会变得非常繁琐时（尤其是当你有很多非 Go 应用组件时），请考虑一下。&lt;/p>
&lt;h3 id="vendor">/vendor&lt;/h3>
&lt;p>应用程序依赖项（手动管理或使用你喜欢的依赖项管理工具，如新的内置 Go Modules 功能）。go mod vendor 命令将为你创建 /vendor 目录。请注意，如果未使用默认情况下处于启用状态的 Go 1.14，则可能需要在 go build 命令中添加 -mod=vendor 标志。&lt;/p></description></item><item><title>Go 语言反射使用示例</title><link>https://wimi.space/posts/2021/05/golang-reflect-examples/</link><pubDate>Fri, 28 May 2021 09:57:14 +0800</pubDate><guid>https://wimi.space/posts/2021/05/golang-reflect-examples/</guid><description>&lt;h2 id="反射常用操作示例">反射常用操作示例&lt;/h2>
&lt;h3 id="判断-struct-是否实现了指定的-interface">判断 Struct 是否实现了指定的 Interface&lt;/h3>
&lt;pre>&lt;code class="language-go">type I interface {
 Foo()
 Bar()
}

type S struct {}

func (*S) Foo() {
 fmt.Println(&amp;quot;foo&amp;quot;)
}

func (*S) Bar() {
 fmt.Println(&amp;quot;bar&amp;quot;)
}

func main() {

 iType := reflect.TypeOf((*I)(nil)).Elem()

 fmt.Println(reflect.TypeOf(S{}).Implements(iType)) // output: false
 fmt.Println(reflect.TypeOf(&amp;amp;S{}).Implements(iType)) // output: true
}
&lt;/code>&lt;/pre>
&lt;h3 id="反射调用-struct-的指定方法">反射调用 Struct 的指定方法&lt;/h3>
&lt;pre>&lt;code class="language-go">type S struct{}

func (*S) Hello() {
 fmt.Println(&amp;quot;hello world&amp;quot;)
}

func main() {
 ref := reflect.New(reflect.TypeOf(S{}))
 method := ref.MethodByName(&amp;quot;Hello&amp;quot;)
 if method.IsValid() {
 method.Call(nil)
 }
}
&lt;/code>&lt;/pre>
&lt;h3 id="通过函数签名动态调用函数">通过函数签名动态调用函数&lt;/h3>
&lt;pre>&lt;code class="language-go">// foo sum of nums
func foo(nums ...int) int {
 var sum int
 for _, num := range nums {
 sum += num
 }
 return sum
}

// println string
func bar(s string) {
 fmt.Println(s)
}

func exec(call interface{}, args ...interface{}) ([]reflect.Value, error) {
 f := reflect.ValueOf(call)
 if f.Kind() != reflect.Func {
 return nil, errors.New(&amp;quot;call must be func&amp;quot;)
 }

 n := len(args)
 params := make([]reflect.Value, n)
 for i := 0; i &amp;lt; n; i++ {
 params[i] = reflect.ValueOf(args[i])
 }
 return f.Call(params), nil
}

func main() {
 // foo(1, 2, 3, 4, 5) return 15
 res, _ := exec(foo, 1, 2, 3, 4, 5)
 fmt.Println(res[0].Int()) // 15

 // bar(&amp;quot;hello world&amp;quot;)
 _, _ = exec(bar, &amp;quot;hello world&amp;quot;)
}
&lt;/code>&lt;/pre>
&lt;h3 id="通过-tag-修改对应字段的默认值">通过 Tag 修改对应字段的默认值&lt;/h3>
&lt;pre>&lt;code class="language-go">type T struct {
 X int `default:&amp;quot;10&amp;quot;`
 S string `default:&amp;quot;hello&amp;quot;`
 B bool `default:&amp;quot;true&amp;quot;`
}

func (t T) String() string {
 return fmt.Sprintf(&amp;quot;[ %v, %v, %v ] &amp;quot;, t.X, t.S, t.B)
}

func setDefault(tag string, value reflect.Value, field reflect.StructField) {
 // 获取 tag 内容
 df := field.Tag.Get(tag)

 // 判断是否可改（私有属性无法设置）
 if value.CanSet() {
 // 根据类型修改为不同的值
 switch field.Type.Kind() {
 case reflect.String:
 value.SetString(df)
 case reflect.Int:
 i, err := strconv.Atoi(df)
 if err != nil {
 value.SetInt(0)
 } else {
 value.SetInt(int64(i))
 }
 case reflect.Bool:
 value.SetBool(df == &amp;quot;true&amp;quot;)
 case reflect.Float64:
 i, err := strconv.Atoi(df)
 if err != nil {
 value.SetFloat(0)
 } else {
 value.SetFloat(float64(i))
 }
 default:
 }
 }
}

func main() {
 t := T{}
 fmt.Println(t) // output: [ 0, , false ]

 // 反射设置默认值
 tt := reflect.TypeOf(t)
 vt := reflect.ValueOf(&amp;amp;t)
 for i := 0; i &amp;lt; tt.NumField(); i++ {
 field := tt.Field(i)
 value := vt.Elem().Field(i)
 setDefault(&amp;quot;default&amp;quot;, value, field)
 }

 fmt.Println(t) // output: [ 10, hello, true ]
}
&lt;/code>&lt;/pre>
&lt;h2 id="参考文档">参考文档&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.golang.org/laws-of-reflection">The Laws of Reflection&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://halfrost.com/go_reflection/">Go reflection 三定律与最佳实践&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect">Go 语言设计与实现｜反射&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>快速排序算法</title><link>https://wimi.space/posts/2021/03/algorithm-quick-sort/</link><pubDate>Mon, 22 Mar 2021 09:37:53 +0800</pubDate><guid>https://wimi.space/posts/2021/03/algorithm-quick-sort/</guid><description>&lt;blockquote>
&lt;p>快速排序是一种常用的内部排序算法，平均时间复杂度为 O(nlogn)。&lt;/p>&lt;/blockquote>
&lt;h2 id="算法思路">算法思路&lt;/h2>
&lt;p>快速排序使用&lt;strong>分治法来把一个序列分为较小和较大的两个子序列，然后递归地排序两个子序列&lt;/strong>。步骤如下：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>挑选基准值&lt;/strong>: 从序列中选择任意一个元素作为基准，通常选择序列的第一个元素。&lt;/li>
&lt;li>&lt;strong>分割序列&lt;/strong>: 将序列中比基准元素小的元素放到基准前面，大的放在基准后面。&lt;/li>
&lt;li>&lt;strong>递归排序&lt;/strong>: 对较小的和较大的子序列分别进行上述操作。&lt;/li>
&lt;/ol>
&lt;h2 id="举例说明">举例说明&lt;/h2>
&lt;p>例如对序列 [ 3, 5, 4, 1, 2, 6 ] 进行快速排序&lt;/p>
&lt;ol>
&lt;li>
&lt;p>选取元素 3 作为基准元素。
&lt;img src="https://static.wimi.space/blog/quick-sort-01.png" alt="">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>将比 3 小的元素放在 3 前面，大的放在 3 后面，将其分割成两个子序列。
&lt;img src="https://static.wimi.space/blog/quick-sort-02.png" alt="">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>重复上述两个步骤，直到子序列划分完毕。
&lt;img src="https://static.wimi.space/blog/quick-sort-03.png" alt="">&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="划分序列">划分序列&lt;/h2>
&lt;p>序列划分有两种常用的方式。&lt;/p>
&lt;h3 id="方法一">方法一&lt;/h3>
&lt;blockquote>
&lt;p>从左到右遍历并调整交换位置，用 offset 记录偏移的量，用于填充基准元素。&lt;/p>&lt;/blockquote>
&lt;ol>
&lt;li>
&lt;p>初始化 offset 基准元素后的第一个位置 ( 即 offset = low + 1 )，从 offset 位置开始往后扫描，找到比 3 小的元素 1，交换 1 和基准外第一个位置的元素 5，交换后 offset 自增。
&lt;img src="https://static.wimi.space/blog/quick-sort-04.png" alt="">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>找到比 3 小的元素 2，交换 2 和基准外第二个位置的元素 4，交换后 offset 自增。
&lt;img src="https://static.wimi.space/blog/quick-sort-05.png" alt="">&lt;/p></description></item><item><title>Git 常用操作和提交规范</title><link>https://wimi.space/posts/2021/03/git-common-commands/</link><pubDate>Fri, 19 Mar 2021 13:05:59 +0800</pubDate><guid>https://wimi.space/posts/2021/03/git-common-commands/</guid><description>&lt;h2 id="git-提交规范">Git 提交规范&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>prefix&lt;/th>
 &lt;th>description&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>feat&lt;/td>
 &lt;td>A new feature&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fix&lt;/td>
 &lt;td>A bug fix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>docs&lt;/td>
 &lt;td>Documentation only changes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>style&lt;/td>
 &lt;td>Changes that do not affect the meaning of the code&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>refactor&lt;/td>
 &lt;td>A code change that neither fixes a bug nor adds a feature&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>perf&lt;/td>
 &lt;td>A code change that improves performance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>test&lt;/td>
 &lt;td>Adding missing tests or correcting existing tests&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>build&lt;/td>
 &lt;td>Changes that affect the build system or external dependencies&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ci&lt;/td>
 &lt;td>Changes to our CI configuration files and scripts&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>chore&lt;/td>
 &lt;td>Other changes that don&amp;rsquo;t modify src or test files&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>revert&lt;/td>
 &lt;td>Reverts a previous commit&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="git-常用操作记录">Git 常用操作记录&lt;/h2>
&lt;h3 id="创建远端分支">创建远端分支&lt;/h3>
&lt;pre>&lt;code class="language-git"> git push --set-upstream origin &amp;lt;branch-name&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="删除远端分支">删除远端分支&lt;/h3>
&lt;pre>&lt;code class="language-git">git push origin :&amp;lt;branch-name&amp;gt;
# 或者
git push origin --delete &amp;lt;branch-name&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="从远程分支中创建并切换到本地分支">从远程分支中创建并切换到本地分支&lt;/h3>
&lt;pre>&lt;code class="language-git">git checkout -b &amp;lt;branch-name&amp;gt; origin/&amp;lt;branch-name&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="删除分支">删除分支&lt;/h3>
&lt;pre>&lt;code class="language-git">git branch -D &amp;lt;branch-name&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="重命名本地分支">重命名本地分支&lt;/h3>
&lt;pre>&lt;code class="language-git">git branch -m &amp;lt;new-branch-name&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="交互式变基到指定记录">交互式变基到指定记录&lt;/h3>
&lt;p>压缩commit记录&lt;/p></description></item><item><title>IPv4 Network &amp; CIDR</title><link>https://wimi.space/posts/2021/03/network-ipv4-and-cidr/</link><pubDate>Wed, 10 Mar 2021 13:26:44 +0800</pubDate><guid>https://wimi.space/posts/2021/03/network-ipv4-and-cidr/</guid><description>&lt;h2 id="ipv4网络分类">IPv4网络分类&lt;/h2>
&lt;blockquote>
&lt;p>&lt;a href="https://www.sojson.com/convert/subnetmask.html">子网掩码在线换算&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>IPv4地址被分为三部分：&lt;/p>
&lt;ul>
&lt;li>网络部分（network）&lt;/li>
&lt;li>子网部分（subnetwork）(现在常被认为是网络部分的一部分)&lt;/li>
&lt;li>主机部分（host）&lt;/li>
&lt;/ul>
&lt;h3 id="各个类别的范围">各个类别的范围&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分类&lt;/th>
 &lt;th>前缀码&lt;/th>
 &lt;th>开始地址&lt;/th>
 &lt;th>结束地址&lt;/th>
 &lt;th>对应CIDR修饰&lt;/th>
 &lt;th>默认子网掩码&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A类地址&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>0.0.0.0&lt;/td>
 &lt;td>127.255.255.255&lt;/td>
 &lt;td>/8&lt;/td>
 &lt;td>255.0.0.0&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B类地址&lt;/td>
 &lt;td>10&lt;/td>
 &lt;td>128.0.0.0&lt;/td>
 &lt;td>191.255.255.255&lt;/td>
 &lt;td>/16&lt;/td>
 &lt;td>255.255.0.0&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C类地址&lt;/td>
 &lt;td>110&lt;/td>
 &lt;td>192.0.0.0&lt;/td>
 &lt;td>223.255.255.255&lt;/td>
 &lt;td>/24&lt;/td>
 &lt;td>255.255.255.0&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D类地址&lt;/td>
 &lt;td>1110&lt;/td>
 &lt;td>224.0.0.0&lt;/td>
 &lt;td>239.255.255.255&lt;/td>
 &lt;td>/4&lt;/td>
 &lt;td>未定义&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E类地址&lt;/td>
 &lt;td>1111&lt;/td>
 &lt;td>240.0.0.0&lt;/td>
 &lt;td>255.255.255.255&lt;/td>
 &lt;td>/4&lt;/td>
 &lt;td>未定义&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="按位来表示">按位来表示&lt;/h3>
&lt;blockquote>
&lt;p>n 表示该二进制位是网络位&lt;/p>
&lt;p>H 表示该二进制位是主机位&lt;/p>
&lt;p>X 表示该二进制位无特定作用&lt;/p>&lt;/blockquote>
&lt;pre>&lt;code class="language-text">A类地址
 0. 0. 0. 0 = 00000000.00000000.00000000.00000000
127.255.255.255 = 01111111.11111111.11111111.11111111
 0nnnnnnn.HHHHHHHH.HHHHHHHH.HHHHHHHH
B类地址
128. 0. 0. 0 = 10000000.00000000.00000000.00000000
191.255.255.255 = 10111111.11111111.11111111.11111111
 10nnnnnn.nnnnnnnn.HHHHHHHH.HHHHHHHH

C类地址
192. 0. 0. 0 = 11000000.00000000.00000000.00000000
223.255.255.255 = 11011111.11111111.11111111.11111111
 110nnnnn.nnnnnnnn.nnnnnnnn.HHHHHHHH

D类地址
224. 0. 0. 0 = 11100000.00000000.00000000.00000000
239.255.255.255 = 11101111.11111111.11111111.11111111
 1110XXXX.XXXXXXXX.XXXXXXXX.XXXXXXXX

E类地址
240. 0. 0. 0 = 11110000.00000000.00000000.00000000
255.255.255.255 = 11111111.11111111.11111111.11111111
 1111XXXX.XXXXXXXX.XXXXXXXX.XXXXXXXX
&lt;/code>&lt;/pre>
&lt;h2 id="ipv4子网划分">IPv4子网划分&lt;/h2>
&lt;p>子网的划分是一个将主机部分的若干位分配到网络部分的过程。例如，对于一个给定的A类网络：10.0.0.0，子网掩码 255.255.0.0 可以将其划分为256个子网（从 10.0.0.0 到 10.255.0.0）第一个八位位组表示网络地址，第二个表示子网号，而最后两个表示主机部分。用&lt;strong>子网掩码对主机地址进行位与操作&lt;/strong>，就能够提取出完整的子网地址。&lt;/p></description></item></channel></rss>