<?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/"
	>

<channel>
	<title>CFC归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/tag/cfc/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/tag/cfc/</link>
	<description>CrazyFay</description>
	<lastBuildDate>Sat, 08 Apr 2023 12:30:43 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.5.2</generator>

<image>
	<url>https://www.crazyfay.com/wp-content/uploads/2023/04/cropped-DockerGopher-32x32.png</url>
	<title>CFC归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/tag/cfc/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>团队协作项目及准备内容 &#8211; CFC 例会 2023.2.27</title>
		<link>https://www.crazyfay.com/2023/02/27/%e5%9b%a2%e9%98%9f%e5%8d%8f%e4%bd%9c%e9%a1%b9%e7%9b%ae%e5%8f%8a%e5%87%86%e5%a4%87%e5%86%85%e5%ae%b9-cfc-%e4%be%8b%e4%bc%9a-2023-2-27/</link>
					<comments>https://www.crazyfay.com/2023/02/27/%e5%9b%a2%e9%98%9f%e5%8d%8f%e4%bd%9c%e9%a1%b9%e7%9b%ae%e5%8f%8a%e5%87%86%e5%a4%87%e5%86%85%e5%ae%b9-cfc-%e4%be%8b%e4%bc%9a-2023-2-27/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Mon, 27 Feb 2023 06:20:40 +0000</pubDate>
				<category><![CDATA[CFC例会]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[服务端架构]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=224</guid>

					<description><![CDATA[<p>CFC-PPCS 项目流程协作系统 1. 项目参考 开源项目 Pear Project - 梨子项目管理系统  [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/27/%e5%9b%a2%e9%98%9f%e5%8d%8f%e4%bd%9c%e9%a1%b9%e7%9b%ae%e5%8f%8a%e5%87%86%e5%a4%87%e5%86%85%e5%ae%b9-cfc-%e4%be%8b%e4%bc%9a-2023-2-27/">团队协作项目及准备内容 &#8211; CFC 例会 2023.2.27</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>CFC-PPCS</h1>
<p>项目流程协作系统</p>
<h2>1. 项目参考</h2>
<p>开源项目</p>
<p><a href="https://home.vilson.xyz/#/home">Pear Project - 梨子项目管理系统 | 轻量级的在线项目/任务协作系统 (vilson.xyz)</a> (本家主站)</p>
<p><a href="https://beta.vilson.xyz/#/home/6v7be19pwman2fird04gqu53">Pear Project (vilson.xyz)</a> (功能体验) 官方账号: 18681140825 密码: 123456</p>
<p><a href="https://github.com/a54552239/pearProjectApi">a54552239/pearProjectApi: 后端代码 (github.com)</a> (后端源码为php)</p>
<p><a href="https://github.com/a54552239/pearProject">a54552239/pearProject: 前端代码 (github.com)</a> (前端源码为Vue2)</p>
<h2>2. 服务端架构</h2>
<p>go语言实现，采用原生gRPC微服务架构，api层采用Gin框架，ORM层采用Gorm框架，etcd作为注册中心，MinIO作为对象存储，数据库采用MySQL与非关系型数据库Redis，MySQL采用 ProxySQL + MGR 的高可用集群（业务无感知，代码无侵入）。在<strong>后续迭代升级</strong>中，可进一步采用nacos作为配置中心，引入Jaeger分布式链路追踪与ELK日志采集模块。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20230227103632549.png" alt="image-20230227103632549" /></p>
<h2>3. 项目结构</h2>
<h3>3.0 go workspace</h3>
<p>便于不同模块本地开发相互引用</p>
<h3>3.1 api</h3>
<p>处理前端发来的http请求，通过etcd发现服务进行grpc调用服务</p>
<pre><code class="language-shell">project-api
├── api
│   ├── api.go              匿名导入其他模块api的依赖
│   ├── middle              Gin中间件目录
│   │   └── middleware.go
│   └── user                按模块将api分组
│       ├── route.go        注册路由，构建路由树
│       ├── rpc.go          初始化rpc客户端
│       └── user.go         api方法实现
├── config
│   ├── config.go           初始化、读取配置
│   └── config.yaml         配置文件
├── go.mod
├── go.sum
├── main.go                 启动方法
├── pkg
│   └── model               数据传输模型（dto）
│       ├── page.go         格式化分页配置
│       └── user            按模块分类
│           └── user.go     用户模块传输模型
└── router                  
    └── router.go           路由注册方法</code></pre>
<h3>3.2 common</h3>
<p>通用工具模块</p>
<h3>3.3 grpc</h3>
<p>protoc 生成的pb文件</p>
<h3>3.4 mico service</h3>
<p>包含具体业务逻辑的服务模块，内部代码结构参考</p>
<p><a href="https://kirov7.github.io/p/cfc-studio-golang开发规范/#项目结构核心部分示例">CFC Studio Golang开发规范 项目结构核心部分示例 (kirov7.github.io)</a></p>
<pre><code class="language-shell">.
├── api                             暂时存放proto文件
│   └── proto
│       ├── gen                     暂时存放proto编译后文件  
│       ├── gen.bat                 编译脚本(仅仅为了方便，下同)
│       ├── gen.sh
│       └── loginService.proto      proto文件，定义grpc服务方法与数据
├── config
│   ├── config.go
│   └── config.yaml
├── go.mod
├── go.sum
├── internal                        数据模型与数据操作，对外不可见
│   ├── dao                         数据访问层，实现repo的接口对数据表进行操作
│   │   ├── member.go               封装对于member表的数据操作的实现
│   │   ├── organization.go
│   │   ├── redis.go                封装redis操作，实现repo.cache的接口
│   │   └── trans.go                实现自定义的事务操作接口，封装事务的操作框架
│   ├── data                        数据库映射结构体与部分模型转换方法
│   │   ├── member                  按数据表划分
│   │   │   └── member.go           member表与结构体的映射
│   │   └── organization
│   │       └── organization.go     organization表与结构体的映射
│   ├── database                    封装数据库连接
│   │   ├── conn.go                 数据库连接的抽象接口
│   │   ├── gorms
│   │   │   └── gorm.go             gorm实现的数据库连接接口
│   │   └── transaction             事务操作的接口
│   │       └── transaction.go
│   ├── domain                      领域层，封装领域的行为操作
│   │   ├── member.go               
│   │   └── organization.go
│   ├── interceptor                 grpc拦截器
│   └── repo                        封装数据查询的接口
│       ├── cache.go                缓存操作的接口
│       ├── member.go               抽象对member数据表的操作
│       └── organization.go
├── main.go                         启动方法，执行各种初始化操作
├── pkg                             服务具体实现与所依赖的常量与全局变量
│   ├── model                       常量与全局变量
│   │   ├── biz.go                  业务逻辑中用到的常量/枚举值
│   │   ├── code.go                 封装的错误代码
│   │   └── redisKey.go             Redis Key的前缀
│   └── service                     grpc方法的实现
│       └── login.service.v1        调用domian整理组合数据实现业务需求
│           └── loginService.go
└── router
    └── router.go                   grpc与etcd注册与路由
</code></pre>
<h4>3.4.1 user</h4>
<p>用户、组织等</p>
<h4>3.4.2 project</h4>
<p>项目、任务等</p>
<h2>4. 项目组件与通用模块介绍</h2>
<h3>4.1 etcd</h3>
<p>作用：用作服务注册，与服务发现</p>
<h3>4.2 MinIO</h3>
<p>作用：本地部署的对象存储OSS，用在项目附件等需求文件存储的地方</p>
<h3>4.3 Redis</h3>
<p>作用：部分用户信息存储(缓存token)、验证码存储、缓存优化等</p>
<p>可视化工具：</p>
<p><a href="https://goanother.com/cn/">Another Redis Desktop Manager | 更快、更好、更稳定的Redis桌面(GUI)管理客户端，兼容Windows、Mac、Linux，性能出众，轻松加载海量键值 (goanother.com)</a></p>
<h3>4.4 ProxySQL + MGR</h3>
<p>作用：</p>
<ul>
<li>
<p>MGR：MySQL Group Replication（Mysql组复制），基于paxos算法的强一致性的高可用、高拓展、高可靠MySQL集群方案</p>
<p><a href="https://zhuanlan.zhihu.com/p/405290212">带你走进MySQL全新高可用解决方案-MGR - 知乎 (zhihu.com)</a></p>
</li>
<li>
<p>ProxySQL：灵活强大的MySQL代理层，通过伪装成MySQL服务端，代理执行SQL语句，并负责读写分离、节点探活、故障无感知转移等</p>
<p><a href="https://zhuanlan.zhihu.com/p/110733834">【sql】MySQL-ProxySQL中间件的使用 - 知乎 (zhihu.com)</a></p>
</li>
</ul>
<p>理论上在ProxySQL外层还需加一层keepalived配置VIP，资源有限，暂且忽略</p>
<h2>5. 其他内容</h2>
<h3>5.1 开发规范</h3>
<p>参考我整理的规范</p>
<p><a href="https://github.com/Kirov7/cfc-golang-develop-norms">Kirov7/cfc-golang-develop-norms: CFC Studio 内部Golang开发规范 (github.com)</a></p>
<h3>5.2 准备环境</h3>
<p>服务端所需的中间件环境以在203的内网服务器中部署完成，可放心使用</p>
<h3>5.3 代码托管平台</h3>
<p>代码托管平台放在实验室私有仓库 gitea 上</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/27/%e5%9b%a2%e9%98%9f%e5%8d%8f%e4%bd%9c%e9%a1%b9%e7%9b%ae%e5%8f%8a%e5%87%86%e5%a4%87%e5%86%85%e5%ae%b9-cfc-%e4%be%8b%e4%bc%9a-2023-2-27/">团队协作项目及准备内容 &#8211; CFC 例会 2023.2.27</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/27/%e5%9b%a2%e9%98%9f%e5%8d%8f%e4%bd%9c%e9%a1%b9%e7%9b%ae%e5%8f%8a%e5%87%86%e5%a4%87%e5%86%85%e5%ae%b9-cfc-%e4%be%8b%e4%bc%9a-2023-2-27/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CFC-Golang 开发规范</title>
		<link>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/</link>
					<comments>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 16 Feb 2023 02:36:55 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[开发规范]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=19</guid>

					<description><![CDATA[<p>CFC-Golang 开发规范 注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结 Github [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>CFC-Golang 开发规范</h1>
<blockquote>
<p>注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结<br />
Github地址：<a href="https://github.com/Kirov7/cfc-golang-develop-norms">https://github.com/Kirov7/cfc-golang-develop-norms</a></p>
</blockquote>
<h2>为什么需要编程规范？</h2>
<p>编程规范又叫代码规范，是团队之间在程序开发时需要遵守的约定。俗话说，无规矩不成方圆，一个开发团队应该就一种编程规范达成一致。编程规范有很多好处，我们简单说几个最主要的。</p>
<ul>
<li>促进团队合作</li>
</ul>
<p>现代项目大多是由团队完成的，但是如果每个人书写出的代码风格迥异，最后集成代码时很容易杂乱无章、可读性极差。相反，风格统一的代码将大大提高可读性，易于理解，促进团队协作。</p>
<ul>
<li>规避错误</li>
</ul>
<p>每一种语言都有容易犯的错误，Go语言也不例外。但是编码规范可以规避掉像Map并发读写等问题。不仅如此，规范的日志处理、错误处理还能够加快我们查找问题的速度。</p>
<ul>
<li>提升性能</li>
</ul>
<p>优秀的开发者，能够在头脑中想象出不同程序运行的过程和结果，写出高性能的程序非常考验开发者的内功。但每个人的水平都有差异，这一点并不可控。但是如果我们将高性能编程的常见手段归纳整理出来，开发者只需要遵守这些简单的规则，就能够规避性能陷阱、极大提升程序性能。</p>
<ul>
<li>便于维护</li>
</ul>
<p>我们习惯于关注编写代码的成本，但实际上维护代码的成本要高得多。大部分的项目是在前人的基础上完成开发的。我们在开发代码的时候，也会花大量时间阅读之前的代码。符合规范的代码更容易上手维护、更少出现牵一发动全身的耦合现象、也更容易看出业务处理逻辑。</p>
<p>知道了编程规范的好处，那我们应该规范什么内容呢？这其实涉及到我们对好代码的定义。针对这个问题，每个人都能够说个几句。但总体来说，好的代码首先是整洁、一致的，同时它还是高效、健壮和可扩展的。</p>
<p>有一些规范可以是强制的，因为我们可以通过工具和代码review强制要求用户遵守，还有一些规范是建议的，因为它更具有灵活性，很难被约束。在后面的规范中，[强制 xxx]中的“xxx”代表的就是可强制检查的工具。</p>
<h2>整洁、一致</h2>
<p>好代码的第一个要求，是整洁和一致。有一句话是这样说的：</p>
<blockquote>
<p>Any fool can write code that a computer can understand. Good programmers write code that humans can understand.</p>
</blockquote>
<p>它的意思是，任何傻瓜都可以编写计算机可以理解的代码，而优秀的程序员编写的是人类可以理解的代码。</p>
<p>如果我们的代码看起来乱七八糟，就像喝醉的人写的那样，这样不严谨的代码让我们有理由相信，项目的其他各个方面也隐藏着对细节的疏忽，并埋下了重大的隐患。</p>
<p>阅读整洁的代码就像看到精心设计的手表或汽车一样赏心悦目，因为它凝聚了团队的智慧。</p>
<p>阅读整洁的代码也像读到的武侠小说，书中的文字被脑中的图像取代，你看到了角色，听到了声音，体验到了悲怆和幽默。</p>
<p>但是，明白什么是整洁的代码并不意味着你能写出整洁的代码。就好像我们知道如何欣赏一幅画不意味着我们能成为画家。</p>
<p>整洁的代码包括对格式化、命名、函数等细节的密切关注，更需要在项目中具体实践。接下来我们就来看看整洁代码关注的这些细节和最佳的实践。</p>
<h3><strong>格式化</strong></h3>
<p><strong>代码长度</strong></p>
<p>代码应该有足够的垂直密度，能够肉眼一次获得到更多的信息。同时，单个函数、单行、单文件也需要限制长度，保证可阅读性和可维护性。</p>
<p>[强制 lll] 一行内不超过 120 个字符，同时应当避免刻意断行。如果你发现某一行太长了，要么改名，要么调整语义，往往就可以解决问题了。</p>
<p>[强制 funlen] 单个函数的行数不超过 40 行，过长则表示函数功能不专一、定义不明确、程序结构不合理，不易于理解。当函数过长时，可以提取函数以保持正文小且易读。</p>
<p>[强制] 单个文件不超过 2000 行，过长说明定义不明确，程序结构划分不合理，不利于维护。</p>
<p><strong>代码布局</strong></p>
<p>我们先试想一篇写得很好的报纸文章。在顶部，你希望有一个标题，它会告诉你故事的大致内容，并影响你是否要阅读它。文章的第一段会为你提供整个故事的概要、粗略的概念，但是隐藏了所有细节。继续向下阅读，详细信息会逐步增加。</p>
<p>[建议]  Go 文件推荐按以下顺序进行布局。</p>
<ol>
<li>
<p>包注释：对整个模块和功能的完整描述，写在文件头部</p>
</li>
<li>
<p>Package：包名称</p>
</li>
<li>
<p>Imports：引入的包</p>
</li>
<li>
<p>Constants：常量定义</p>
</li>
<li>
<p>Typedefs：类型定义</p>
</li>
<li>
<p>Globals：全局变量定义</p>
</li>
<li>
<p>Functions：函数实现</p>
</li>
</ol>
<p>每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时，也用一个空行分割。示例如下：</p>
<pre><code class="language-go">/*
注释
*/
package http

import (
 &quot;fmt&quot;
 &quot;time&quot;
)

const (
 VERSION = &quot;1.0.0&quot;
)
type Request struct{
}

var msg = &quot;HTTP success&quot;

func foo() {
 //...
}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。</p>
<p><strong>空格与缩进</strong></p>
<p>为了让阅读代码时视线畅通，自上而下思路不被打断，我们需要使用一些空格和缩进。</p>
<p>空格是为了分离关注点，将不同的组件分开。缩进是为了处理错误和边缘情况，与正常的代码分隔开。</p>
<p>较常用的有下面这些规范：</p>
<p>[强制 gofmt] 注释和声明应该对齐。示例如下：</p>
<pre><code class="language-go">type T struct {
    name    string // name of the object
    value   int    // its value
}</code></pre>
<p>[强制 gofmt] 小括号()、中括号[]、大括号{} 内侧都不加空格。</p>
<p>[强制 gofmt] 逗号、冒号（slice中冒号除外）前都不加空格，后面加 1 个空格。</p>
<p>[强制 gofmt] 所有二元运算符前后各加一个空格，作为函数参数时除外。例如<code>b := 1 + 2</code>。[强制 gofmt] 使用 Tab 而不是空格进行缩进。</p>
<p>[强制 nlreturn] return前方需要加一个空行，让代码逻辑更清晰。</p>
<p>[强制 gofmt] 判断语句、for语句需要缩进1个 Tab，并且右大括号<code>}</code>与对应的 if 关键字垂直对齐。例如：</p>
<pre><code class="language-go">if xxx {

} else {

}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。这同样适用于常量、变量和类型声明：</p>
<pre><code class="language-go">import (
    &quot;fmt&quot;
    &quot;hash/adler32&quot;
    &quot;os&quot;

    &quot;appengine/foo&quot;
    &quot;appengine/user&quot;
    &quot;github.com/foo/bar&quot;
    &quot;rsc.io/goversion/version&quot;
)</code></pre>
<p>[推荐] 避免 else 语句中处理错误返回，避免正常的逻辑位于缩进中。如下代码实例，else中进行错误处理，代码逻辑阅读起来比较费劲。</p>
<pre><code class="language-go">if something.OK() {
        something.Lock()
        defer something.Unlock()
        err := something.Do()
        if err == nil {
                stop := StartTimer()
                defer stop()
                log.Println(&quot;working...&quot;)
                doWork(something)
                &lt;-something.Done() // wait for it
                log.Println(&quot;finished&quot;)
                return nil
        } else {
                return err
        }
} else {
        return errors.New(&quot;something not ok&quot;)
}</code></pre>
<p>如果把上面的代码修改成下面这样会更加清晰：</p>
<pre><code class="language-go">if !something.OK() {
    return errors.New(&quot;something not ok&quot;)
}
something.Lock()
defer something.Unlock()
err := something.Do()
if err != nil {
    return err
}
stop := StartTimer()
defer stop()
log.Println(&quot;working...&quot;)
doWork(something)
&lt;-something.Done() // wait for it
log.Println(&quot;finished&quot;)
return nil</code></pre>
<p>[推荐] 函数内不同的业务逻辑处理建议用单个空行加以分割。</p>
<p>[推荐] 注释之前的空行通常有助于提高可读性——新注释的引入表明新思想的开始。</p>
<h3><strong>命名</strong></h3>
<blockquote>
<p>Good naming is like a good joke. If you have to explain it, it’s not funny.<br />
———Dave Cheney</p>
</blockquote>
<p>一个好的名字应该满足几个要素：</p>
<ul>
<li>短，容易拼写；</li>
<li>保持一致性；</li>
<li>意思准确，容易理解，没有虚假和无意义的信息。</li>
</ul>
<p>例如，像下面这样的命名就是让人迷惑的：</p>
<pre><code class="language-go">int d; // elapsed time in days</code></pre>
<p>[强制 revive] Go中的命名统一使用驼峰式、不要加下划线。</p>
<p>[强制 revive] 缩写的专有名词应该大写，例如： ServeHTTP、IDProcessor。</p>
<p>[强制] 区分变量名应该用有意义的名字，而不是使用阿拉伯数字：a1, a2, .. aN。</p>
<p>[强制] 不要在变量名称中包含你的类型名称。</p>
<p>[建议]变量的作用域越大，名字应该越长。</p>
<p>现代 IDE 已经让更改名称变得更容易了，巧妙地使用IDE的功能，能够级联地同时修改多处命名。</p>
<p><strong>包名</strong></p>
<p>包名应该简短而清晰。</p>
<p>[强制] 使用简短的小写字母，不需要下划线或混合大写字母。</p>
<p>[建议]  合理使用缩写，例如：</p>
<pre><code>strconv（字符串转换）
syscall（系统调用）
fmt（格式化的 I/O）</code></pre>
<p>[强制] 避免无意义的包名，例如<code>util</code>,<code>common,base</code>等。</p>
<p><strong>接口命名</strong></p>
<p>[建议]单方法接口由方法名称加上 -er 后缀或类似修饰来命名。例如：<code>Reader</code>, <code>Writer</code>, <code>Formatter</code>, <code>CloseNotifier</code> ，当一个接口包含多个方法时，请选择一个能够准确描述其用途的名称（例如：net.Conn、http.ResponseWriter、io.ReadWriter）。</p>
<p><strong>本地变量命名</strong></p>
<p>[建议]尽可能地短。在这里，i 指代 index，r 指代 reader，b 指代 buffer。</p>
<p>例如，下面这段代码就可以做一个简化：</p>
<pre><code class="language-go">for index := 0; index &lt; len(s); index++ {
    //
}</code></pre>
<p>可以替换为：</p>
<pre><code class="language-go">for i := 0; i &lt; len(s); i++ {
    //
}</code></pre>
<p><strong>函数参数命名</strong></p>
<p>[建议]如果函数参数的类型已经能够看出参数的含义，那么函数参数的命名应该尽量简短：</p>
<pre><code class="language-go">func AfterFunc(d Duration, f func()) *Timer
func Escape(w io.Writer, s []byte)</code></pre>
<p>[建议]如果函数参数的类型不能表达参数的含义，那么函数参数的命名应该尽量准确：</p>
<pre><code class="language-go">func Unix(sec, nsec int64) Time
func HasPrefix(s, prefix []byte) bool</code></pre>
<p><strong>函数返回值命名</strong></p>
<p>[建议] 对于公开的函数，返回值具有文档意义，应该准确表达含义，如下所示：</p>
<pre><code class="language-go">func Copy(dst Writer, src Reader) (written int64, err error)

func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)</code></pre>
<p><strong>可导出的变量名</strong></p>
<p>[建议] 由于使用可导出的变量时会带上它所在的包名，因此，不需要对变量重复命名。例如bytes包中的ByteBuffer替换为Buffer，这样在使用时就是bytes.Buffer，显得更简洁。类似的还有把strings.StringReader修改为strings.Reader，把<strong>errors.NewError 修改为errors.New。</strong></p>
<p><strong>Error值命名</strong></p>
<p>[建议] 错误类型应该以Error结尾。</p>
<p>[建议] Error变量名应该以Err开头。</p>
<pre><code class="language-go">type ExitError struct {
    ...
}
var ErrFormat = errors.New(&quot;image: unknown format&quot;)</code></pre>
<h3><strong>函数</strong></h3>
<p>[强制 cyclop] 圈复杂度（Cyclomatic complexity）&lt;10。</p>
<p>[强制 gochecknoinits] 避免使用init函数。</p>
<p>[强制 revive] Context 应该作为函数的第一个参数。</p>
<p>[强制] 正常情况下禁用unsafe。</p>
<p>[强制] 禁止return裸返回，如下例中第一个return：</p>
<pre><code class="language-go">func (f *Filter) Open(name string) (file File, err error) {
    for _, c := range f.chain {
        file, err = c.Open(name)
        if err != nil {
            return
        }
    }
    return f.source.Open(name)
}</code></pre>
<p>[强制] 不要在循环里面使用defer，除非你真的确定defer的执行流程。</p>
<p>[强制] 对于通过:=进行变量赋值的场景，禁止出现仅部分变量初始化的情况。例如在下面这个例子中，f函数返回的res是初始化的变量，但是函数返回的err其实复用了之前的err：</p>
<pre><code class="language-go">var err error
res,err := f()</code></pre>
<p>[建议] 函数返回值大于 3 个时，建议通过 struct 进行包装。</p>
<p>[建议] 函数参数不建议超过 3 个，大于 3 个时建议通过 struct 进行包装。</p>
<h3>控制结构</h3>
<p>[强制] 禁止使用goto。</p>
<p>[强制 gosimple] 当一个表达式为 bool 类型时，应该使用 expr 或 !expr 判断，禁止使用 == 或 != 与 true / false 比较。</p>
<p>[强制 nestif] if 嵌套深度不大于5。</p>
<h3><strong>方法</strong></h3>
<p>[强制 revive] receiver 的命名要保持一致，如果你在一个方法中将接收器命名为 &quot;c&quot;，那么在其他方法中不要把它命名为 &quot;cl&quot;。</p>
<p>[强制] receiver 的名字要尽量简短并有意义，禁止使用 this、self 等。</p>
<pre><code class="language-go">func (c Client) done() error {
 // ...
}
func (cl Client) call() error {
 // ...
}</code></pre>
<h3>注释</h3>
<p>Go提供C风格的注释。有/**/ 的块注释和 // 的单行注释两种注释风格。注释主要有下面几个用处。</p>
<ol>
<li>注释不仅仅可以提供具体的逻辑细节，还可以提供代码背后的意图和决策。</li>
<li>帮助澄清一些晦涩的参数或返回值的含义。一般来说，我们会尽量找到一种方法让参数或返回值的名字本身就是清晰的。但是当它是标准库的一部分时，或者在你无法更改的第三方库中，一个清晰的注释会非常有用。</li>
<li>强调某一个重要的功能。例如，提醒开发者修改了这一处代码必须连带修改另一处代码。</li>
</ol>
<p>总之，好的注释给我们讲解了what、how、why，方便后续的代码维护。</p>
<p>[强制] 无用注释直接删除，无用的代码不应该注释而应该直接删除。即使日后需要，我们也可以通过Git快速找到。</p>
<p>[强制] 使用行注释而不是尾注释，注释一律写在所描述内容的上一行。</p>
<p>[强制] 统一使用中文注释，中英文字符之间严格使用空格分隔。</p>
<pre><code class="language-go">// 从 Redis 中批量读取属性，对于没有读取到的 id ， 记录到一个数组里面，准备从 DB 中读取</code></pre>
<p>[强制] 注释不需要额外的格式，例如星号横幅。</p>
<p>[强制] 包、函数、方法和类型的注释说明都是一个完整的句子，以被描述的对象为主语开头。Go源码中都是这样的。</p>
<p>示例如下：</p>
<pre><code class="language-go">// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool)</code></pre>
<p>[强制] Go语言提供了<a href="https://tip.golang.org/doc/comment">文档注释工具go doc</a>，可以生成注释和导出函数的文档。文档注释的写法可以参考文稿中的链接。</p>
<p>[强制 godot] 注释最后应该以句号结尾。</p>
<p>[建议] 当某个部分等待完成时，可用 <code>TODO:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 大部分情况下使用行注释。块注释主要用在包的注释上，不过块注释在表达式中或禁用大量代码时很有用。</p>
<p>[建议] 当某个部分存在已知问题需要修复或改进时，可用 <code>FIXME:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 需要特别说明某个问题时，可用 <code>NOTE:</code> 开头的注释。</p>
<h3>结构体</h3>
<p>[强制] 不要将 Context 成员添加到 Struct 类型中。</p>
<h3>Commit 规范</h3>
<p><strong>commit message格式</strong></p>
<pre><code class="language-text">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</code></pre>
<p>[强制] <strong>type(必须)</strong></p>
<p>用于说明git commit的类别，只允许使用下面的标识。</p>
<p>feat：新功能（feature）。</p>
<p>fix/to：修复bug，可以是QA发现的BUG，也可以是研发自己发现的BUG。</p>
<ul>
<li>fix：产生diff并自动修复此问题。适合于一次提交直接修复问题</li>
<li>to：只产生diff不自动修复此问题。适合于多次提交。最终修复问题提交时使用fix</li>
</ul>
<p>docs：文档（documentation）。</p>
<p>style：格式（不影响代码运行的变动）。</p>
<p>refactor：重构（即不是新增功能，也不是修改bug的代码变动）。</p>
<p>perf：优化相关，比如提升性能、体验。</p>
<p>test：增加测试。</p>
<p>chore：构建过程或辅助工具的变动。</p>
<p>revert：回滚到上一个版本。</p>
<p>merge：代码合并。</p>
<p>sync：同步主线或分支的Bug。</p>
<p>[建议] <strong>scope(可选)</strong></p>
<p>scope用于说明 commit 影响的范围，比如数据层、控制层、视图层等等，视项目不同而不同。</p>
<p>例如在Angular，可以是location，browser，compile，compile，rootScope， ngHref，ngClick，ngView等。如果你的修改影响了不止一个scope，你可以使用*代替。</p>
<p>[强制] <strong>subject(必须)</strong></p>
<p>subject是commit目的的简短描述，不超过50个字符。</p>
<p>建议使用中文（感觉中国人用中文描述问题能更清楚一些）。</p>
<ul>
<li>结尾不加句号或其他标点符号。</li>
<li>根据以上规范git commit message将是如下的格式：</li>
</ul>
<pre><code class="language-text">fix(DAO):用户查询缺少username属性 
feat(Controller):用户查询接口开发</code></pre>
<blockquote>
<p>这样规范 git commit 有以下优点</p>
</blockquote>
<ul>
<li>便于开发者对提交历史进行追溯，了解发生了什么情况。</li>
<li>一旦约束了commit message，意味着我们将慎重的进行每一次提交，不能再一股脑的把各种各样的改动都放在一个git commit里面，这样一来整个代码改动的历史也将更加清晰。</li>
<li>格式化的commit message才可以用于自动化输出Change log。</li>
</ul>
<h2>高效</h2>
<p>[强制] Map在初始化时需要指定长度<code>make(map[T1]T2, hint)</code>。</p>
<p>[强制] Slice在初始化时需要指定长度和容量<code>make([]T, length, capacity)</code>。</p>
<p>我们来看下下面这段程序，它的目的是往切片中循环添加元素。</p>
<pre><code class="language-go">func createSlice(n int) (slice []string) {
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>从功能上来看，这段代码没有问题。但是，这种写法忽略了一个事实，如下图所示，往切片中添加数据时，切片会自动扩容，Go运行时会创建新的内存空间并执行拷贝。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20221124144357089.png" alt="image-20221124144357089" /></p>
<p>自动扩容显然是有成本的。在循环操作中执行这样的代码会放大性能损失，减慢程序的运行速度。性能损失的对比可参考<a href="https://github.com/uber-go/guide/blob/master/style.md#prefer-specifying-container-capacity">这篇文章</a>。我们可以改写一下上面这段程序，在初始化时指定合适的切片容量：</p>
<pre><code class="language-go">func createSlice(n int) []string {
   slice := make([]string, 0, n*3)
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>这段代码在一开始就指定了需要的容量，最大程度避免了内存的浪费。同时，运行时不需要再执行自动扩容操作，加速了程序的运行。</p>
<p>[强制]  time.After()在某些情况下会发生泄露，替换为使用Timer。</p>
<p>[强制] 数字与字符串转换时，<a href="https://gist.github.com/evalphobia/caee1602969a640a4530">使用strconv，而不是fmt</a>。</p>
<p>[强制] 读写磁盘时，使用<a href="https://www.instana.com/blog/practical-golang-benchmarks/#file-i-o">读写buffer</a>。</p>
<p>[建议] 谨慎使用Slice的截断操作和append操作，除非你知道下面的代码输出什么：</p>
<pre><code class="language-go">x := []int{1, 2, 3, 4}
y := x[:2]
fmt.Println(cap(x), cap(y))
y = append(y, 30)
fmt.Println(&quot;x:&quot;, x)
fmt.Println(&quot;y:&quot;, y)</code></pre>
<p>[建议] 任何书写的协程，都需要明确协程什么时候退出。</p>
<p>[建议] 热点代码中，内存分配复用内存可以使用 sync.Pool <a href="https://www.instana.com/blog/practical-golang-benchmarks/#object-creation">提速</a>。</p>
<p>[建议] 将频繁的字符串拼接操作（+=），替换为<strong>StringBuffer 或 StringBuilder。</strong></p>
<p>[建议] 使用正则表达式重复匹配时，利用Compile提前编译<a href="https://www.instana.com/blog/practical-golang-benchmarks/#regular-expressions">提速</a>。</p>
<p>[建议]  当程序严重依赖Map时，Map的Key使用int而不是string将<a href="https://www.instana.com/blog/practical-golang-benchmarks/#map-access">提速</a>。</p>
<p>[建议]  多读少写的场景，使用读写锁而不是写锁将提速。</p>
<h2>健壮性</h2>
<p>[强制] 除非出现不可恢复的程序错误，否则不要使用 panic 来处理常规错误，使用 error 和多返回值。</p>
<p>[强制] 永远只在 main 函数和 init 函数中调用 log.Fatal() 方法。</p>
<p>[强制] 永远先关闭 写channel 再关闭 读channel，否则对已关闭 写channel 进行写入时会引发 panic 。</p>
<p>[强制 <a href="https://revive.run/r#error-strings">revive</a>] 错误信息不应该首字母大写（除专有名词和缩写词外），也不应该以标点符号结束。因为错误信息通常在其他上下文中被打印。</p>
<p>[强制 <a href="https://golangci-lint.run/usage/linters/#errcheck">errcheck</a>] 不要使用 _ 变量来丢弃 error。如果函数返回 error，应该强制检查。</p>
<p>[强制] 在 Release模式 使用 context 的时候需要设定超时时间。</p>
<p>[建议] 在处理错误时，如果我们逐层返回相同的错误，那么在最后日志打印时，我们并不知道代码中间的执行路径。例如找不到文件时打印的<code>No such file or directory</code>，这会减慢我们排查问题的速度。因此，在中间处理err时，需要使用fmt.Errorf 或<a href="https://godoc.org/github.com/pkg/errors">第三方包</a>给错误添加额外的上下文信息。像下面这个例子，在fmt.Errorf中，除了实际报错的信息，还加上了授权错误信息<code>authenticate failed</code> ：</p>
<pre><code class="language-go">func AuthenticateRequest(r *Request) error {
        err := authenticate(r.User)
        if err != nil {
                return fmt.Errorf(&quot;authenticate failed: %v&quot;, err)
        }
        return nil
}</code></pre>
<p>当有多个错误需要处理时，可以考虑将fmt.Errorf放入defer中：</p>
<pre><code class="language-go">func DoSomeThings(val1 int, val2 string) (_ string, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf(&quot;in DoSomeThings: %w&quot;, err)
        }
    }()
    val3, err := doThing1(val1)
    if err != nil {
        return &quot;&quot;, err
    }
    val4, err := doThing2(val2)
    if err != nil {
        return &quot;&quot;, err
    }
    return doThing3(val3, val4)
}</code></pre>
<p>[强制] 利用recover捕获panic时，需要由defer函数直接调用。</p>
<p>例如，下面例子中的panic是可以被捕获的：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>但是下面这个例子中的panic却不能被捕获：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer func() {
        printRecover()
    }()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>[强制] 不用重复使用recover，只需要在每一个协程的最上层函数拦截即可。recover只能够捕获当前协程，而不能跨协程捕获panic，下例中的panic就是无法被捕获的。</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()
    go func() {
        panic(&quot;OMG!&quot;)
    }()
    // ...
}</code></pre>
<p>[强制] 有些特殊的错误是recover不住的，例如Map的并发读写冲突。这种错误可以通过race工具来检查。</p>
<h2>扩展性</h2>
<p>[建议] 利用接口实现扩展性。接口特别适用于访问外部组件的情况，例如访问数据库、访问下游服务。另外，接口可以方便我们进行功能测试。关于接口的最佳实践，需要单独论述。</p>
<p>[建议] 使用功能选项模式(options 模式)对一些公共API的构造函数进行扩展，大量第三方库例如gomicro、zap等都使用了这种策略。</p>
<pre><code class="language-go">db.Open(addr, db.DefaultCache, zap.NewNop())
可以替换为=&gt;
db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(log),
)</code></pre>
<h2>内部实践</h2>
<h3>Gin</h3>
<h4>项目启动</h4>
<p>[强制] 在大型的项目开发中，采用优雅服务启停设计，采用 httpServer.ListenAndServer() 方法进行web服务的启动，而不是 Gin 框架的方法，web服务需额外启动一个 goroutine 来运行，通过信号监听的方式控制结束程序。</p>
<pre><code class="language-golang">func main() {
    r := gin.Default()
    srv := &amp;http.Server{
        Addr:    &quot;:80&quot;,
        Handler: r,
    }

    // 优雅的启停
    go func() {
        log.Printf(&quot;web server running in %s \n&quot;, srv.Addr)
        if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
            log.Fatalln(err)
        }
    }()

    quit := make(chan os.Signal)
    // SIGINT 用户发送 INTR 字符(Ctrl + C)触发 kill -2
    // SIGTERM 结束程序 (可以被捕获、阻塞或忽略)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    &lt;-quit
    log.Println(&quot;Shutting Down project Web server...&quot;)

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalln(&quot;Web server shutdown, cause by : &quot;, err)
    }
    select {
    case &lt;-ctx.Done():
        log.Println(&quot;wait timeout...&quot;)
    }
    log.Println(&quot;Web Server stop success...&quot;)
}</code></pre>
<blockquote>
<p>以上代码可进一步封装，以保证 main 的代码结构清晰</p>
</blockquote>
<h4>返回值格式</h4>
<p>[强制] json格式的返回值，code部分统一采用 net/http 包中的 http.StatusOK，obj 则为业务对象的统一格式封装。</p>
<pre><code class="language-golang">ctx.JSON(http.StatusOK, result.Success(list))
ctx.JSON(http.StatusOK, result.Fail(http.StatusBadRequest, &quot;参数错误&quot;))</code></pre>
<p>[强制] 业务对象中的统一字段为：code、msg、data，其中code为错误代码，由具体的服务实现方定义，需要保证全局唯一，如果没有错误则固定为 200 ，msg为错误信息，由具体的服务实现方定义，需要简洁的描述错误的原因，错误可统一定义为全局变量，当在api层产生参数错误、数据校验失败等请求值错误，则使用 http.StatusBadRequest 作为错误码， data 则为前端所需要的业务对象，推荐使用在api服务中封装返回值结构体实例。</p>
<p>对应结构体实例：</p>
<pre><code class="language-golang">type BusinessCode int
type Result struct {
    Code BusinessCode `json:&quot;code&quot;`
    Msg  string       `json:&quot;msg&quot;`
    Data any          `json:&quot;data&quot;`
}</code></pre>
<p>[强制] 在给前端返回 json 后必须 return 。</p>
<h3>grpc</h3>
<h4>proto文件与命名规范</h4>
<p>[强制] proto文件中的 service、rpc Function、message 以大驼峰的方式命名。</p>
<p>[推荐] 尽可能的将 proto 文件中 service 命名为 xxxService 、 RPC 方法的入参命名为 XxxRequest，出参命名为 XxxResponse。</p>
<p>[强制] 将引用的生成 rpc 包重命名为 xxxRpc 以避免module中的本地模块产生冲突。</p>
<pre><code class="language-go">import(
    departmentRpc &quot;github.com/Kirov7/project-grpc/project/department&quot;   
)</code></pre>
<p>[推荐] 编译 proto 文件时推荐不要直接生成到会被其他模块引用的包下，建议生成到指定目录后手动迁移文件。</p>
<h4>注册grpc服务</h4>
<p>[建议] 优雅的启动 grpcServer 示例，其中 RegisterGrpc() 返回的 grpc.Server 可供上层调用 grpcSever.Stop() 等，可对 grpcServer 生命周期进行管控。</p>
<pre><code class="language-golang">type gRPCConfig struct {
    Addr         string
    RegisterFunc func(server *grpc.Server)
}

func RegisterGrpc() *grpc.Server {
    c := gRPCConfig{
        Addr: config.AppConf.GrpcConfig.Addr,
        RegisterFunc: func(g *grpc.Server) {
            login.RegisterLoginServiceServer(g, loginServiceV1.NewLoginService())
      },
    }
    //cacheInterceptor := interceptor.NewCacheInterceptor()
    //s := grpc.NewServer(cacheInterceptor.CacheInterceptor())
    s := grpc.NewServer()
    c.RegisterFunc(s)
    listen, err := net.Listen(&quot;tcp&quot;, c.Addr)
    if err != nil {
        log.Printf(&quot;listen port %s fail\n&quot;, c.Addr)
    }
    go func() {
        log.Printf(&quot;grpc server started as %s \n&quot;, c.Addr)
        err = s.Serve(listen)
        if err != nil {
            log.Printf(&quot;server started error: %s\n&quot;, err)
            return
        }
    }()
    return s
}</code></pre>
<h3>项目结构核心部分示例</h3>
<p>此示例结合了部分DDD领域驱动设计的思想，但并非完全按照DDD的模式进行组织，旨在优化项目组织结构的同时，尽量避免过于复杂抽象的概念。</p>
<h4>main.go</h4>
<p>进行各类初始化操作，如初始化http服务、路由注册、初始化grpc客户端、grpc服务注册等。</p>
<h4>config 包</h4>
<p>内有 config.go 和 config.yaml 文件，config.go 中拥有包含配置的全局变量，<strong>推荐使用 viper 进行配置读取</strong>。</p>
<h4>internal 包</h4>
<p>internal包 是 golang 中特殊的一个包，它对外部module不可见，可用来存放数据操作等敏感内容。</p>
<p>可包含domain、repository、dao、rpc、data等包</p>
<ul>
<li>
<p>domain：领域服务，负责表达业务概念，业务状态信息以及业务规则，通过调用 repository 、rpc 或其他 domain 进行数据操作与数据整合，实现完整的某一领域模块的业务逻辑。在进行单元测试时可以绕过 service 直接对 domain 进行细粒度的测试，简化测试流程。</p>
</li>
<li>
<p>repository：仓库，负责封装数据的查询、创建、更新、删除等逻辑的接口，屏蔽底层实现，供使用者调用</p>
</li>
<li>
<p>dao：数据操作层，实现 repository 的接口，封装对MySQL、Redis等数据库操作的具体实现的。</p>
</li>
<li>
<p>rpc：rpc客户端，对其他rpc服务进行远程调用的客户端。</p>
</li>
<li>
<p>data：数据表的直接映射，并包含对于数据传输模型的转换。</p>
</li>
</ul>
<h4>pkg 包</h4>
<p>pkg包 包含了服务具体实现与其所依赖的预定义的常量与全局变量。</p>
<p>可包含model、service等包</p>
<ul>
<li>model：预定义的常量与全局变量，便于在业务中重用，简化操作逻辑。可包含预定于的 rediskey、业务所需的枚举值、预定义的错误等。</li>
<li>service：业务层，接受客户端传输的数据，对请求数据进行校验与转换，通过调用 domain 层进行业务操作，整合调用的结果值，返回给客户端。</li>
</ul>
<h2>工具</h2>
<p>要人工来保证团队成员遵守了上述的编程规范并不是一件容易的事情。因此，我们有许多静态的和动态的代码分析工具帮助团队识别代码规范的错误，甚至可以发现一些代码的bug。</p>
<h3>golangci-lint</h3>
<p>golangci-lint 是当前大多数公司采用的静态代码分析工具，词语Linter 指的是一种分析源代码以此标记编程错误、代码缺陷、风格错误的工具。</p>
<p>而golangci-lint是集合多种Linter的工具。要查看支持的 Linter 列表以及启用/禁用了哪些Linter，可以使用下面的命令：</p>
<pre><code>golangci-lint help linters</code></pre>
<p>Go语言定义了实现Linter的API，它还提供了golint工具，用于集成了几种常见的Linter。在<a href="https://cs.opensource.google/go/x/tools/+/refs/tags/v0.1.11:go/analysis/passes/unreachable/unreachable.go">源码</a>中，我们可以查看怎么在标准库中实现典型的Linter。</p>
<p>Linter的实现原理是静态扫描代码的AST（抽象语法树），Linter的标准化意味着我们可以灵活实现自己的Linters。不过golangci-lint里面其实已经集成了包括golint在内的总多Linter，并且有灵活的配置能力。所以在自己写Linter之前，建议先了解golangci-lint现有的能力。</p>
<p>在大型项目中刚开始使用golang-lint会出现大量的错误，这种情况下我们只希望扫描增量的代码。如下所示，可以通过在<a href="https://golangci-lint.run/usage/configuration/">golangci-lint配置文件</a>中调整new-from-rev参数，配置以当前基准分支为基础实现增量扫描</p>
<pre><code class="language-yaml">linters:
 enable-all: true
issues:
 new-from-rev: master</code></pre>
<h3>Pre-Commit</h3>
<p>在代码通过Git Commit提交到代码仓库之前，git 提供了一种pre-commit的hook能力，用于执行一些前置脚本。在脚本中加入检查的代码，就可以在本地拦截住一些不符合规范的代码，避免频繁触发CI或者浪费时间。pre-commit的配置和使用方法，可以参考<a href="https://github.com/pingcap/tidb/blob/master/hooks/pre-commit">TiDB</a>。</p>
<h3>并发检测 race</h3>
<p>Go 1.1 提供了强大的检查工具race来排查数据争用问题。race 可以用在多个Go指令中，一旦检测器在程序中找到数据争用，就会打印报告。这份报告包含发生race冲突的协程栈，以及此时正在运行的协程栈。可以在编译时和运行时执行race，方法如下：</p>
<pre><code class="language-shell">$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg</code></pre>
<p>在下面这个例子中， 运行中加入race检查后直接报错。从报错后输出的栈帧信息中，我们能看出具体发生并发冲突的位置。</p>
<pre><code class="language-shell">» go run -race 2_race.go
==================
WARNING: DATA RACE
Read at 0x00000115c1f8 by goroutine 7:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x3a
Previous write at 0x00000115c1f8 by goroutine 6:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x56</code></pre>
<p>第四行Read at 表明读取发生在2_race.go 文件的第5行，而第七行Previous write 表明前一个写入也发生在2_race.go 文件的第5行。这样我们就可以非常快速地定位数据争用问题了。</p>
<p>竞争检测的成本因程序而异。对于典型的程序，内存使用量可能增加 5~10 倍，执行时间会增加2~20倍。同时，竞争检测器会为当前每个defer和recover语句额外分配8字节。在Goroutine退出前，这些额外分配的字节不会被回收。这意味着，如果有一个长期运行的Goroutine，而且定期有defer 和recover调用，那么程序内存的使用量可能无限增长。（这些内存分配不会显示到 runtime.ReadMemStats或runtime / pprof 的输出。）</p>
<h3>覆盖率</h3>
<p>一般我们会使用代码覆盖率来判断代码书写的质量，识别无效代码。go tool cover 是go语言提供的识别代码覆盖率的工具。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WEB服务端系统架构演进概述 &#8211; CFC例会2022.9.18</title>
		<link>https://www.crazyfay.com/2022/09/18/web%e6%9c%8d%e5%8a%a1%e7%ab%af%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e6%bc%94%e8%bf%9b%e6%a6%82%e8%bf%b0-cfc%e4%be%8b%e4%bc%9a2022-9-18/</link>
					<comments>https://www.crazyfay.com/2022/09/18/web%e6%9c%8d%e5%8a%a1%e7%ab%af%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e6%bc%94%e8%bf%9b%e6%a6%82%e8%bf%b0-cfc%e4%be%8b%e4%bc%9a2022-9-18/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 18 Sep 2022 06:20:10 +0000</pubDate>
				<category><![CDATA[CFC例会]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[服务端架构]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=123</guid>

					<description><![CDATA[<p>注：本系列内容为个人整理与总结的架构演进的鸟瞰图，其中部分概念的解释由我个人总结得出，仅代表我个人的理解，难免 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/09/18/web%e6%9c%8d%e5%8a%a1%e7%ab%af%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e6%bc%94%e8%bf%9b%e6%a6%82%e8%bf%b0-cfc%e4%be%8b%e4%bc%9a2022-9-18/">WEB服务端系统架构演进概述 &#8211; CFC例会2022.9.18</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<blockquote>
<p>注：本系列内容为个人整理与总结的架构演进的鸟瞰图，其中部分概念的解释由我个人总结得出，仅代表我个人的理解，难免存在纰漏，如有任何问题均可随时提出并指正</p>
</blockquote>
<h1>服务端 or 客户端</h1>
<blockquote>
<p>什么是服务端和客户端，它和前端后端有什么关联和区别</p>
</blockquote>
<h2>认识 “服务”</h2>
<p>服务，是根据功能抽象出的概念。比如说，处理用户登录信息的认证服务,负责持久化存储数据的数据库服务，以及为了加快查询速度的缓存服务等</p>
<h2>服务、进程、端口？</h2>
<p><strong>端口</strong>的主要作用是表示一台计算机中的特定<strong>进程</strong>所提供的<strong>服务</strong>。网络中的计算机是通过 <code>IP地址</code> 来代表其身份的，它只能表示某台特定的计算机，但是一台计算机上可以同时提供很多个服务，如数据库服务、FTP服务(文件传输)、Web服务等，我们就通过端口号来区别相同计算机所提供的这些不同的服务，如常见的端口号80表示的是HTTP服务端口，端口号3306是MySQL的默认端口号，端口号8080是Java的开源的流行Web应用服务器Tomcat的默认端口号等。一个IP地址的端口通过16bit进行编号，最多可以有65536个端口，即端口号的取值范围是<code>0 ~ 65535</code>，其中0～1023是被规定好了的，被称作“众所周知的端口”(Well Known Ports)，且在同一台计算机上端口号不能重复，否则，就会产生端口号冲突这样的例外</p>
<h3>Go语言搭建一个最简单的Web服务器</h3>
<p><a href="http://192.168.1.151:9999/">http://192.168.1.151:9999</a></p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
)

func IndexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, &quot;hello world&quot;)
}

func main() {
    http.HandleFunc(&quot;/&quot;, IndexHandler)
    http.ListenAndServe(&quot;:9999&quot;, nil)
}</code></pre>
<h1>前端后端，分？还是不分？</h1>
<blockquote>
<p>我们所说的前后端分离到底是什么样的</p>
</blockquote>
<p>前后端一体模板引擎渲染方式的架构示例：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916110523047.png" alt="image-20220916110523047" /></p>
<p>现流行的前后端分离式架构示例：CSR</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916163603087.png" alt="image-20220916163603087" /></p>
<h3>前后端分离的优势与劣势</h3>
<ul>
<li>
<p>优势：</p>
<ol>
<li>
<p>以实现真正的前后端解耦，优化开发流程，划分职责界限</p>
</li>
<li>
<p>减少后端服务器的并发负载压力</p>
</li>
<li>
<p>更友好的错误提示</p>
<p>...</p>
</li>
</ol>
</li>
<li>
<p>劣势：</p>
<ol>
<li>前端响应较慢，资源消耗严重，在业务复杂的情况下，一个页面可能要发起多次HTTP请求才能将页面渲染完毕，且在Json返回的数据量比较大的情况下，渲染的十分缓慢，会出现页面卡顿的情况</li>
<li>不利于SEO，搜索引擎的爬虫无法爬下JS异步渲染的数据</li>
</ol>
</li>
</ul>
<blockquote>
<p>为了解决不足，后续在前后端分离的基础上衍生出了引入NodeJS作为中间层的进行服务端渲染的架构方案......</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220918133719987.png" alt="image-20220918133719987" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916220734923.png" alt="image-20220916220734923" /></p>
</blockquote>
<h2>服务端与客户端的交互</h2>
<p><strong>数据传递主要方式：</strong></p>
<ol>
<li>form表单</li>
<li><strong>ajax</strong>
<ul>
<li>Ajax(Asynchronous JavaScript and XML) 异步JavaScript和XML，一种用于创建快速动态网页的异步刷新技术。通过在后台与服务器进行少量数据交换，AJAX 可以使网页实现异步更新。即可以在不重新加载整个网页的情况下，对网页的某部分进行更新。</li>
</ul>
</li>
</ol>
<p><strong>数据传递主要格式：</strong></p>
<ol>
<li>XML</li>
<li><strong>JSON</strong>
<ul>
<li>JSON(JavaScript Object Notation,) 是一种轻量级的数据交换格式，采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于阅读和编写，同时也易于机器解析和生成，并有效地提升网络传输效率。</li>
</ul>
</li>
</ol>
<h3>前后端 JSON通信API示例</h3>
<p><code>评论列表功能</code></p>
<p>接口url：/comments/article/{id}</p>
<p>请求方式：GET</p>
<p>请求参数：</p>
<table>
<thead>
<tr>
<th>参数名称</th>
<th>参数类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>long</td>
<td>文章id（路径参数）</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>返回数据：</p>
<pre><code class="language-json">{
    &quot;success&quot;: true,
    &quot;code&quot;: 200,
    &quot;msg&quot;: &quot;success&quot;,
    &quot;data&quot;: [
        {
            &quot;id&quot;: 1,
            &quot;author&quot;: {
                &quot;nickname&quot;: &quot;GodLanBo&quot;,
                &quot;avatar&quot;: &quot;http://tva1.sinaimg.cn/large/005Uj3w8ly1h68mgfr3bcj31hc0u0mz9.jpg&quot;,
                &quot;id&quot;: 202
            },
            &quot;content&quot;: &quot;很久没和朋友去唱歌了&#x1f3a4;，一下午嗓子都莫得了「=。=」&quot;,
            &quot;childrens&quot;: [
                {
                    &quot;id&quot;: 2,
                    &quot;author&quot;: {
                        &quot;nickname&quot;: &quot;群垫底&quot;,
                        &quot;avatar&quot;: &quot;http://tva1.sinaimg.cn/large/005Uj3w8ly1h68mgfr3bcj31hc0u0mz9.jpg&quot;,
                        &quot;id&quot;: 203
                    },
                    &quot;content&quot;: &quot;和朋友一起玩桌游，简简单单四个角色，玩不腻=。=&quot;,
                    &quot;childrens&quot;: [],
                    &quot;createDate&quot;: &quot;2013-01-14 20:30&quot;,
                    &quot;level&quot;: 2,
                    &quot;toUser&quot;: {
                        &quot;nickname&quot;: &quot;GodLanBo&quot;,
                        &quot;avatar&quot;: &quot;http://tva1.sinaimg.cn/large/005Uj3w8ly1h68mgfr3bcj31hc0u0mz9.jpg&quot;,
                        &quot;id&quot;: 1
                    }
                }
            ],
            &quot;createDate&quot;: &quot;2013-01-14 02:03&quot;,
            &quot;level&quot;: 1,
            &quot;toUser&quot;: null
        }
    ]
}</code></pre>
<h3>JSON应用示例</h3>
<p><a href="http://120.48.87.191:9998/">http://120.48.87.191:9998</a></p>
<p>代码实例通过共享屏幕展示</p>
<hr />
<h1>被追认的名号 - 单体架构</h1>
<blockquote>
<p>“单体”只是表明系统中主要的过程调用都是进程内调用，不会发生进程间通信，仅此而已。</p>
</blockquote>
<h2>什么是单体架构</h2>
<p>“单体架构”在整个软件架构演进的历史进程里，是出现时间最早、应用范围最广、使用人数最多、统治历史最长的一种架构风格，但“单体”这个名称，却是在微服务开始流行之后才“事后追认”所形成的概念。</p>
<h2>单体架构不可拆分？</h2>
<p>说起单体架构、巨石系统的缺点时，在脑海中闪过的第一个特点就是它的“不可拆分”，难以扩展，因此才不能支撑越来越大的软件规模。这种想法看似合理，其实是有失偏颇的，至少不完整。</p>
<p>从<strong>纵向角度</strong>看，没有哪个系统是不分层的，分层架构（Layered Architecture）已是现在几乎所有信息系统建设中都普遍认可、采用的软件设计方法，无论是单体还是微服务，抑或是其他架构风格，都会对代码进行纵向层次划分，收到的外部请求在各层之间以不同形式的数据结构进行流转传递，触及最末端的数据库后按相反的顺序回馈响应，如图所示。对于这个意义上的“可拆分”，单体架构完全不会展露出丝毫的弱势，反而可能会因更容易开发、部署、测试而获得一些便捷性上的好处。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220907153415823.png" alt="image-20220907153415823" /></p>
<p>从<strong>横向角度</strong>来看，单体架构也可以支持按照技术、功能、职责等维度，将软件拆分为各种模块，以便重用和管理代码。横向扩展（Scale Horizontally）的角度来衡量，在负载均衡器之后同时部署若干个相同的单体系统副本，以达到分摊流量压力的效果。</p>
<h3>拓展：三层架构与MVC架构</h3>
<ul>
<li>
<p><strong>三层架构</strong></p>
<blockquote>
<p>对应上图的Presentation Layer、Business Layer、Persistence Layer</p>
<p>三层架构有时也被称作：<strong>表示层(web层)、业务逻辑层(service层)、数据访问层(dao层)</strong></p>
<p>三层架构是从整个业务应用角度对程序的划分，其分层逻辑来源于“<strong>高内聚，低耦合</strong>”的思想。</p>
</blockquote>
<p>1、表现层（UI）：通俗讲就是展现给用户的界面，即用户在使用一个系统的时候他的所见所得。<br />
2、业务逻辑层（BLL）：针对具体问题的操作，也可以说是对数据层的操作，对数据业务逻辑处理。<br />
3、数据访问层（DAL）：该层所做事务直接操作数据库，针对数据的增添、删除、修改、更新、查找等。</p>
</li>
<li>
<p>MVC架构</p>
<blockquote>
<p>MVC模式实现了数据和视图的分离</p>
<p>属于组合设计模式的范畴，就如同其他设计模式一样，模式的出现就是为了对某种功能的优化，而MVC模式可以看做是对三层架构中表现层的一种细分优化。</p>
</blockquote>
<p>1、模型（Model）：模型持有所有的数据、状态和程序逻辑。模型独立于视图和控制器。<br />
2、视图（View）：用来呈现模型。视图通常直接从模型中取得它需要显示的状态与数据。对于相同的信息可以有多个不同的显示形式或视图。<br />
3、控制器（Controller）：位于视图和模型中间，负责接受用户的输入，将输入进行解析并反馈给模型。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916224115958.png" alt="image-20220916224115958" /></p>
<p>MVC架构<strong>工作流程：</strong></p>
<p>（1）用户通过 View 页面向服务端提出请求，可以是表单请求、超链接请求、AJAX 请求等<br />
（2）服务端 Controller 控制器接收到请求后对请求进行解析，找到相应的 Model 对用户请求进行处理<br />
（3）Model 处理后，将处理结果再交给 Controller<br />
（4）Controller 在接到处理结果后，根据处理结果找到要作为向客户端发回的响应 View 页面。页面经渲染（数据填充）后，再</p>
</li>
</ul>
<p><strong>三层架构和MVC架构的关联</strong></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916221013452.png" alt="image-20220916221013452" /></p>
<blockquote>
<p>在当下前后端分离的主流趋势下，MVC架构进行不断的改进，但其中数据与视图分离的思想是一直受用的。</p>
<p>可以简单的认为前后端分离将View从后端中分离了出来交给前端处理，即浏览器发送Ajax请求，然后服务端接受该请求并返回JSON数据返回给浏览器，最后在浏览器中进行界面渲染。</p>
<p>即如图所示：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916224709575.png" alt="image-20220916224709575" /></p>
</blockquote>
<h2>单体架构的优点与瓶颈</h2>
<p>对于小型系统——即由单台机器就足以支撑其良好运行的系统，单体不仅易于开发、易于测试、易于部署，且由于系统中各个功能、模块、方法的调用过程都是进程内调用，不会发生<a href="https://zh.wikipedia.org/wiki/行程間通訊">进程间通信</a>（Inter-Process Communication，IPC。广义上讲，可以认为 RPC 属于 IPC 的一种特例，这里两个“PC”不是同个单词的缩写），因此也是运行效率最高的一种架构风格。</p>
<p>只有基于软件的性能需求超过了单机，软件的开发人员规模明显超过了“<a href="https://wiki.mbalib.com/wiki/两个披萨原则">2 Pizza Team</a>的&quot;大型的单体架构系统&quot;才能更明显的体现出单体架构的瓶颈。</p>
<p>在“<strong>拆分</strong>”这方面，单体系统的真正缺陷不在如何拆分，而在拆分之后的<strong>隔离与自治能力</strong>上的欠缺。由于所有代码都运行在同一个进程空间之内，所有模块、方法的调用都无须考虑网络分区、对象复制这些麻烦的事和性能损失。获得了进程内调用的简单、高效等好处的同时，也意味着如果任何一部分代码出现了缺陷，过度消耗了进程空间内的资源，所造成的影响也是全局性的、难以隔离的。譬如内存泄漏、线程爆炸、阻塞、死循环等问题，都将会影响整个程序，而不仅仅是影响某一个功能、模块本身的正常运作。如果消耗的是某些更高层次的公共资源，譬如端口号或者数据库连接池泄漏，影响还将会波及整台机器，甚至是集群中其他单体副本的正常工作。</p>
<p>同样，由于所有代码都共享着同一个进程空间，不能隔离，也就很难做到单独停止、更新、升级某一部分代码，因为不可能有“停掉半个进程，重启 1/4 个程序”这样不合逻辑的操作，所以从可维护性来说，单体系统也是不占优势的。程序升级、修改缺陷往往需要制定专门的停机更新计划，做灰度发布、A/B 测试也相对更复杂。</p>
<p>由于隔离能力的缺失，单体除了难以阻断错误传播、不便于动态更新程序以外，还面临难以技术异构的困难，每个模块的代码都通常需要使用一样的程序语言，乃至一样的编程框架去开发。</p>
<p>为了允许程序出错，为了获得隔离、自治的能力，为了可以技术异构等目标，是继为了性能与算力之后，让程序选择分布式又一个理由。</p>
<hr />
<h1>集群与分布式架构</h1>
<blockquote>
<p>从追求“尽量不出错”，到正视“出错是必然”</p>
</blockquote>
<p><strong>集群</strong>（cluster）就是<strong>一组计算机</strong>，它们作为一个整体向用户提供一组网络资源，这些单个的计算机系统就是集群的节点（node）。集群提供了以下关键的特性。</p>
<ol>
<li>
<p><strong>可扩展性。</strong>集群的性能不限于单一的服务实体，新的服务实体可以动态的加入到集群，从而增强集群的性能。</p>
</li>
<li>
<p><strong>高可用性。</strong>集群通过服务实体冗余使客户端免于轻易遭遇到“out of service”警告。当一台节点服务器发生故障的时候，这台服务器上所运行的应用程序将在另一节点服务器上被自动接管。消除单点故障对于增强数据可用性、可达性和可靠性是非常重要的。</p>
</li>
<li>
<p><strong>负载均衡。</strong>负载均衡能把任务比较均匀的分布到集群环境下的计算和网络资源，以便提高数据吞吐量。</p>
</li>
<li>
<p><strong>错误恢复。</strong>如果集群中的某一台服务器由于故障或者维护需要而无法使用，资源和应用程序将转移到可用的集群节点上。这种由于某个节点中的资源不能工作，另一个可用节点中的资源能够透明的接管并继续完成任务的过程叫做错误恢复。</p>
</li>
</ol>
<p>一个比较典型的例子，由Nginx负载的多web节点集群架构。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220916231514901.png" alt="image-20220916231514901" style="zoom: 80%;" /></p>
<p><strong>分布式系统</strong>是<strong>一组计算机</strong>，透过网络相互连接传递消息与通信后并协调它们的行为而形成的系统。组件之间彼此进行交互以实现一个共同的目标。</p>
<p>分布式与集群的联系与区别如下：</p>
<p>(一) 分布式是指将不同的业务分布在不同的地方。</p>
<p>(二) 而集群指的是将几台服务器集中在一起，实现同一业务。</p>
<p>(三) 分布式的每一个节点，都可以做集群，而集群并不一定就是分布式的。而分布式，从狭义上理解，也与集群差不多，但是它的组织比较松散，不像集群，有一定组织性，一台服务器宕了，其他的服务器可以顶上来。分布式的每一个节点，都完成不同的业务，一个节点宕了，这个业务就不可访问了。</p>
<h1>革命性质的探索 - 面向服务的架构(SOA)</h1>
<blockquote>
<p>面向服务的架构是一次具体地、系统性地成功解决分布式服务主要问题的架构模式。</p>
</blockquote>
<h2>垂直拆分与服务拆分</h2>
<p>在前面提到的各种将服务进行拆分方法，最终的效果都是单体的，每个最终运行的服务都是包含了所有功能的整体，这就会导致每个运行的服务中的任意应用出现问题时都会影响到整个服务，也就是前面提到的<strong>隔离与自治能力</strong>上的欠缺。但是如果当某些应用相对独立，与其他应用没有直接交互的情况下（比如不同的报表生成业务应用），我们可以尝试将其进行<strong>垂直拆分</strong>，将互不直接影响的应用搬到不同的进程上运行，这样，当某一应用发生问题之后，其爆炸半径不会直接波及到其他独立的正常运行的应用。但是老死不相往来的应用比较稀少，这种垂直拆分并没有太高的工程实践上的可行性。</p>
<p>但是既然可以根据应用把架构做垂直拆分，那么根据模块/职责对架构进行水平拆分并相互建立通信的方式是否有可行性呢？</p>
<p>按照这个思路，可以把原本包含了众多复杂逻辑的模块，按照功能但愿抽象成多个<strong>服务</strong>，以服务为一等公民，并为服务之间的<strong>通信</strong>定义标准，随后便演进出了<strong>SOA架构</strong>的雏形。</p>
<h2>再谈服务与通信标准</h2>
<ul>
<li><strong>服务，是根据功能抽象出的概念。比如说，处理用户登录信息的认证服务，负责持久化存储数据的数据库服务，以及为了加快查询速度的缓存服务等</strong></li>
<li><strong>通讯标准，是服务之间通信的基石。没有实现定义好的通信标准，不同服务之间就不能相互调用，难以协作</strong></li>
</ul>
<p>为了服务之间更好的通信，有两个大的发展方向：中心化和去中心化。在历史的进程中，率先冲锋的便是中心化的模式。</p>
<h2>SOA 的野蛮生长</h2>
<p>如图所示两种SOA发展中有代表性的SOA架构。当系统演化至事件驱动架构时，远程服务调用协议SOAP诞生了。此时“面向服务的架构”（Service Oriented Architecture，SOA）已经有了它登上软件架构舞台所需要的全部前置条件。许多巨头科技公司一起创立了<a href="http://www.oasis-opencsa.org/">Open CSA (opens new window)</a>组织（Open Composite Services Architecture），这便是 SOA 的官方管理机构。OpenCAS制定了一系列精密严谨成体系的基础平台与技术组件，从技术可行性上 SOA 成功地解决了分布式环境下出现的主要技术问题。</p>
<p>不同的服务间采用 SOAP 作为远程调用的协议，利用一个被称为<a href="https://zh.wikipedia.org/zh-hans/企业服务总线">企业服务总线 (opens new window)</a>（Enterprise Service Bus，ESB）的消息管道来实现各个子系统之间的通信交互，初次之外还定义了很多其他的组件严谨的解决了多方面的问题。</p>
<p><img decoding="async" src="C:\Users\25222\AppData\Roaming\Typora\typora-user-images\image-20220917001207883.png" alt="image-20220917001207883" style="zoom: 70%;" /> <img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917001223202.png" alt="image-20220917001223202" style="zoom: 50%;" /></p>
<p>但是，过于严格的规范定义带来过度的复杂性。而构建在 SOAP 基础之上的 ESB、BPM、SCA、SDO 等诸多上层建筑，进一步加剧了这种复杂性。过于精密的流程和理论也需要懂得复杂概念的专业人员才能够驾驭。最终很难作为一种具有广泛普适性的软件架构风格来推广。</p>
<h1>遍地开花的时代 - 微服务架构</h1>
<blockquote>
<p>当SOA革命以最终无人问津的结局告终之时，去中心化的分布式方案在悄然生长，而去中心化的最终形态，就是<strong>微服务架构</strong>。</p>
</blockquote>
<p><strong>微服务是一种通过多个小型服务组合来构建单个应用的架构风格，这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言，不同的数据存储技术，运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。</strong></p>
<p>在微服务时代，以服务来划分团队工作，服务对应的开发团队有直接对服务运行质量负责的责任，也应该有着不受外界干预地掌控服务各个方面的权力，譬如选择与其他服务异构的技术来实现自己的服务。</p>
<p>微服务明确地提倡数据应该按领域分散管理、更新、维护、存储，在单体服务中，一个系统的各个功能模块通常会使用<strong>同一个数据库</strong>，诚然中心化的存储天生就更容易避免一致性问题，但是，同一个数据实体在不同服务的视角里，它的抽象形态往往也是不同的。譬如，Bookstore 应用中的书本，在销售领域中关注的是价格，在仓储领域中关注的库存数量，在商品展示领域中关注的是书籍的介绍信息，如果作为中心化的存储，所有领域都必须修改和映射到同一个实体之中，这便使得不同的服务很可能会互相产生影响而丧失掉独立性。因此，服务拆分，数据库也进行拆分，松解服务之间的耦合，每个团队可以更专注于自身负责的业务，也分散减轻了数据库的访问压力。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917101101513.png" alt="image-20220917101101513" /></p>
<h2>微服务组件</h2>
<h3>注册中心</h3>
<p>注册中心可以说是微服务架构中的地址簿，它记录了服务和服务地址的映射关系，在分布式架构中，服务会注册到这里，当服务需要调用其他服务时，就在这里找到服务的地址，进行调用。</p>
<ul>
<li>服务注册中心给客户端提供可供调用的服务列表，客户端在进行远程服务调用时，根据服务列表然后选择服务提供方的服务地址进行服务调用。服务注册中心在分布式系统中大量应用，是分布式系统中不可获取的组件。</li>
<li>注册中心是整个服务调用的核心部分，如果服务不存在注册中心，那么通过网关会调用不到，导致失败。</li>
</ul>
<h3>配置中心</h3>
<p>管理各个环境的配置文件参数，比如说数据库，缓存，存储，业务应用并且支持管理每个不同的环境的配置。</p>
<ul>
<li>本地配置在服务启动加载，修改配置不需要重启服务</li>
<li>多个环境（dev，prod，sit，uat）容易混淆，会产生错误，导致服务运行异常</li>
<li>出现配置错误时，不容易回滚到指定的版本</li>
</ul>
<h3>API网关</h3>
<p>API网关是微服务架构中提供路由转发与鉴权等功能，首先，它会提供最基本的<strong>路由</strong>服务，将客户端请求转发后台业务服务；其次，作为一个入口，它还可以进行<strong>认证，鉴权，限流</strong>等操作。</p>
<ul>
<li>客户端访问的统一对接接口</li>
<li>防止内部接口直接暴露给外部客户端（隐藏内部服务）</li>
<li>API网关通过提供一个额外的保护层来防止恶意攻击，例如SQL注入，XML解析器漏洞和拒绝服务</li>
<li>服务网关的前置过滤器中，所有请求过来进行权限校验</li>
<li>日志访问与审计</li>
</ul>
<h3>服务限流</h3>
<p>服务限流：指当系统资源不够的情况下，不足以应对大量的用户与数据接口请求时，为了保证优先的资源能够正常服务，因此对系统按照预设的规则进行流量限制或功能限制的一种方法。</p>
<ul>
<li>发生错误或者超时，不让调用接口，调用本地fallback（容错）</li>
<li>解决高并发请求，一旦达到规定请求，熔断，报错</li>
</ul>
<h3>链路跟踪</h3>
<p>调用链是整个分布式系统中跟踪一个用户请求的过程，包括数据采集，数据传输，数据存储，数据分析和数据可视化展示工具，也是微服务中代码的调试和服务监控的性能分析工具。</p>
<p>分布式web系统中，客户端的一次请求操作，可能需要经过系统中多个模块，多个中间件，多台机器的相互协作才能完成，并且这一系列调用请求中，有些是串行处理的，有些是并发执行的，那么如何确定客户端的一次操作背后调用了哪些应用，哪些模块，经过了哪些节点，每个模块的调用先后顺序是怎样的，每个模块的性能问题如何？随着业务系统模型的日趋复杂化，分布式系统中继续一套链路追踪（trace）系统来解决这些问题（快速定位）。</p>
<h3>RPC调用</h3>
<ul>
<li>
<p>RPC就是从一台机器（客户端）上通过参数传递的方式调用另一台机器（服务器）上的一个函数或方法（可以统称为服务）并得到返回的结果。</p>
</li>
<li>
<p>RPC 会隐藏底层的通讯细节（不需要直接处理Socket通讯或Http通讯） RPC 是一个请求响应模型。</p>
</li>
<li>
<p>客户端发起请求，服务器返回响应（类似于Http的工作方式） RPC 在使用形式上像调用本地函数（或方法）一样去调用远程的函数（或方法）。</p>
</li>
</ul>
<h2>微服务架构实例</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/5e554428-f0e9-4b0d-a8a7-c18e18f5280a.png" alt="" /></p>
<p>烂大街的谷粒商城项目</p>
<h1>云计算与云原生</h1>
<blockquote>
<p>如今被炒的火热的云计算和云原生到底是什么</p>
</blockquote>
<h2>虚拟化与容器技术</h2>
<p>因为不同的应用或者程序可能并不适用于同一个系统中，理想状态下可以每台服务器用于一个特定的任务或者应用程序，但问题是，但多数服务器在运行计算时只会使用他们整体处理能力的一小部分，不能充分利用服务器的处理能力。</p>
<p><strong>虚拟化</strong>就解决了这个问题，将多台服务器整合到一台服务器中，运行多个虚拟环境，每个VM都有自己的操作系统（这些操作系统可以是不同的），可以在其上安装应用程序。</p>
<p>虚拟机的设计原理彼此间是隔离的，并且与虚拟主机隔离，这意味着一个应用程序中的安全问题不会影响在另一个虚拟机中运行的另一个应用程序。同样，如果一个应用程序崩溃并需要重新启动服务器，那么可以重新启动它的VM，而不影响任何其他VM的运行。此外虚拟化还有利于可扩展性，VM是以计算机文件的形式存在的，因此这个文件可以很容易地通过网络(甚至通过存储介质)复制或移动到新的虚拟主机上，也就有了更强的可移植性。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917101659810.png" alt="image-20220917101659810" /></p>
<p>但是虚拟机模拟了整个操作系统，而这层操作系统往往都是可以通用的，即VM的操作相对更重，将操作系统复制了多份造成了一些不必要的开销，一台服务器中可以运行的虚拟机数量比较有限。</p>
<p>于是，可以共享操作系统的<strong>容器技术</strong>登上了舞台</p>
<p>与虚拟化相反，容器主机需要运行自己的操作系统以及容器系统。容器由单个应用程序（或微服务）以及需要运行的其他重要文件组成，利用容器主机的操作系统内核、二进制文件和库来运行。这些共享文件作为只读文件公开给容器。在容器主机上运行的其他容器也共享主机的内核、二进制文件和库。容器技术的核心（以Docker为例），主要基于Linux内核提供资源操作，进行资源隔离(namespace)、资源分配控制(Cgroup)、镜像的制作与加载(UnionFS)。</p>
<p>由于容器比虚拟机“轻”得多，并且启动速度也快得多，这也使得它们成为运行微服务的理想工具，当对微服务的需求扩大时，可以启用容器，当需求减少时可以删除。它们也可以在公有云和私有云以及传统数据中心之间轻松移动。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917105601758.png" alt="image-20220917105601758" /></p>
<p><strong>虚拟机与容器</strong></p>
<p>由于多个容器之间使用的还是同一个宿主机的操作系统内核，因此导致了容器与虚拟机之间存在一些重要区别：</p>
<ul>
<li>容器比虚拟机小得多或“轻”得多，通常由几兆字节组成，并且所需的硬件资源也少得多。这意味着一台物理服务器可以承载的容器比虚拟机要多得多。</li>
<li>容器可以在几秒甚至几毫秒内启动。相比之下，虚拟机的启动时间比较长。</li>
<li>由于容器都共享其主机的操作系统，因此所有应用程序都必须在同一操作系统上运行。相比之下，运行在虚拟主机上的虚拟机可以运行不同的操作系统（例如Linux，Unix和Windows）。</li>
<li>使用容器时，只需要对容器主机的操作系统进行补丁和更新。而虚拟机则需对每个操作系统都进行补丁和更新。</li>
<li>如果一个容器导致容器主机的操作系统崩溃，则在该主机上运行的所有容器都将失败。</li>
<li>容器主机的操作系统内核中的安全漏洞将影响其所托管的所有容器。</li>
</ul>
<blockquote>
<p>容器技术也是虚拟化技术的子类</p>
</blockquote>
<p><strong>容器编排工具</strong></p>
<p>当应用服务的数量不断增加，部署的容器数量开始变的庞大，一个应用数十乃至数百个松散结合的容器式组件构成，而这些组件需要通过相互间的协同合作。这时对单独组件和应用层的工作进行组织的流程的<strong>容器编排管理系统</strong>也有了存在的必要。Docker Swarm、Apache Mesos 和 Kubernetes 也随之登上舞台。</p>
<h2>云计算的蓬勃</h2>
<ul>
<li><strong>云计算</strong>：即通过网络按需提供可动态伸缩的廉价计算资源服务。</li>
</ul>
<p><strong>计算资源的层次划分</strong></p>
<ol>
<li>
<p><strong>第一层次</strong>，是最底层的硬件资源，主要包括CPU（计算资源），硬盘（存储资源），还有网卡（网络资源）等。</p>
</li>
<li>
<p><strong>第二层次</strong>，要高级一些，用户不直接使用硬件资源，而是使用云厂商直接提供配置好底层环境的各类中间服务，如负载均衡服务、数据库服务、对象存储服务、缓存服务等</p>
</li>
<li>
<p><strong>第三层次</strong>，更高级一些，用户直接使用厂商提供的专门的垂直软件应用服务，不需要关注底层的架构与实现细节，做到真正开箱即用的软件服务</p>
</li>
</ol>
<p>以上三个层次的划分，对应了云计算中的三个重要概念</p>
<ul>
<li><strong>IaaS</strong>: Infrastructure-as-a-Service（基础设施即服务）</li>
<li><strong>PaaS</strong>: Platform-as-a-Service（平台即服务）</li>
<li><strong>SaaS</strong>: Software-as-a-Service（软件即服务）</li>
</ul>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917114504059.png" alt="image-20220917114504059" /></p>
<h2>云原生时代的到来</h2>
<p>云原生，实际是云原生计算的简称，它是云计算发展到现在的这一种形态。</p>
<p>云原生技术可以为企业在公有云、私有云、混合云等新型的动态环境中，构建和运行<strong>可弹性拓展</strong>的应用提供了可能。</p>
<p>云原生主要涉及四个大方面：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917150755650.png" alt="image-20220917150755650" /></p>
<ol>
<li><strong>弹性资源</strong>：基于虚拟化容器以及灵活的编排调度机制，可以为云服务体哦概念股快速扩缩容能力，而且极大程度地提高了物理资源的利用率。在这方面。<strong>Kubernetes（k8s）</strong>技术已经成为了行业的标准。</li>
<li><strong>微服务架构</strong>：微服务也是云原生的重要基石之一。依托于功能单元解构，使得云服务具备了快速迭代的可能，业务得以循序发展；统一的通信标准能够帮助越来越多的组件加入到云原生的生态中，同时使得各组件之间的交互变得更容易。</li>
<li><strong>DevOps</strong>：设计 =&gt; 开发 =&gt; 测试 =&gt; 交付 =&gt; 开发 =&gt; 测试 =&gt; 交付，自动化的流程使得软件的工作流程更高效，将微服务架构的优势进一步发挥。</li>
<li><strong>服务网格</strong>：如果说微服务架构中最重要的进步，是将庞大的单体服务按照业务功能解耦开来，那么，服务网格的重要进步就是将业务逻辑于网络通信和治理解耦开来。业务不再需要关心异构系统中RPC中间件治理能力的不统一，这样使得复杂的治理能力成为可能</li>
</ol>
<h1>后微服务时代 - 云原生架构</h1>
<blockquote>
<p>从软件层面独力应对微服务架构问题，发展到软、硬一体，合力应对架构问题的时代，此即为云原生所引领的“后微服务时代”。</p>
</blockquote>
<h2>All-in-Kubernetes ？</h2>
<p>微服务时代所取得的成就，本身就离不开以 Docker 为代表的早期容器化技术的巨大贡献。在早期的容时候器只被简单地视为一种可快速启动的服务运行环境，目的是方便程序的分发部署，这个阶段针对单个应用进行封装的容器并未真正参与到分布式问题的解决之中。</p>
<p>这个状况一直持续到2017 年，各个容器技术的公司，都主动或被迫纷纷支持起了 k8s 的集成， Kubernetes 赢得容器战争的胜利正式宣告结束。</p>
<p>在以往的架构演进中，许多软件解决的问题都有对应的硬件解决方案，如负载均衡问题可以不知负载均衡器，服务发现问题可以设置DNS服务器。通过配置硬件的方式，业务开发人员就可以不必过多关心由硬件直接解决的问题。但在微服务时代，人们选择在软件的代码层面而不是硬件的基础设施层面去解决复杂分布式问题，很大程度上是因为由硬件构成的基础设施，跟不上由软件构成的应用服务的灵活性的无奈之举。</p>
<p>在容器技术的发展之下，构建在Linux操作系统之上的K8S将底层的操作系统中的概念进一步封装，已经能够被看做一个可对容器调度分配控制的“<strong>云操作系统</strong>”，在这个“云操作系统”的平台上，可以对服务发现、配置中心、服务网关、负载均衡、服务安全、跟踪监控提供的”基础设施层面“的解决方案。至此软件与硬件的界限便已经模糊。一旦虚拟化的硬件能够跟上软件的灵活性，那些与业务无关的技术性问题便有可能从软件层面剥离，悄无声息地解决于硬件基础设施之内，让软件得以只专注业务。</p>
<h2><strong>服务网格(Service Mesh)</strong></h2>
<p>Kubernetes 成为容器战争胜利者标志着后微服务时代的开端，但是 k8s 基础设施的解决方案仍有缺陷，甚至单从功能性上来看，全部依托于k8s 还不如基于Spring Cloud这些微服务框架的提供的方案，基础设施是针对整个容器来管理的，<strong>粒度相对粗旷</strong>，只能到容器层面，对单个远程服务就很难有效管控。</p>
<p>举个例子，微服务 A 调用了微服务 B 的两个服务，称为 B1和 B2，假设 B1表现正常但 B2出现了持续的 500 错，那在达到一定阈值之后就应该对 B2进行熔断，以避免产生雪崩效应。如果仅在基础设施层面来处理，这会遇到一个两难问题，切断 A 到 B 的网络通路则会影响到 B1的正常调用，不切断的话则持续受 B2的错误影响。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220917184353467.png" alt="image-20220917184353467" /></p>
<p>为了解决这一类问题，虚拟化的基础设施很快完成了第二次进化，引入了今天被称为“<strong>服务网格</strong>”（Service Mesh）的“边车代理模式”（Sidecar Proxy）</p>
<p>这个场景里指的具体含义是由系统自动在服务容器（通常是指 Kubernetes 的 Pod）中注入一个通信代理服务器，相当于那个挎斗，以类似网络安全里中间人攻击的方式进行流量劫持，在应用毫无感知的情况下，悄然接管应用所有<strong>对外通信</strong>。这个代理除了实现正常的服务间通信外（称为数据平面通信），还接收来自控制器的指令（称为控制平面通信），根据控制平面中的配置，对数据平面通信的内容进行分析处理，以实现熔断、认证、度量、监控、负载均衡等各种附加功能。这样便实现了既不需要在应用层面加入额外的处理代码，也提供了几乎不亚于程序代码的精细管理能力。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/53cd62863ccdf3ca83c70eb4eeb96ee4.png" alt="img" style="zoom: 67%;" /></p>
<h1>探索中的Serverless架构</h1>
<blockquote>
<p>如果说微服务架构是分布式系统这条路的极致，那无服务架构，也许就是“不分布式”的云端系统这条路的起点。</p>
</blockquote>
<p>人们研究分布式架构，最初是由于<strong>单台机器的性能无法满足系统的运行需要</strong>，尽管在后来架构演进过程中，容错能力、技术异构、职责划分等各方面因素都成为架构需要考虑的问题，但其中获得更好性能的需求在架构设计中依然占很大的比重。对软件研发而言，不去做分布式无疑才是最简单的，如果单台服务器的性能可以是<strong>无限的</strong>，那架构演进的结果肯定会与今天有很大的差别，分布式也好，容器化也好，微服务也好，恐怕都未必会如期出现，最起码不必一定是像今天这个样子。</p>
<p>真正的无限性能是肯定无法做到的，但是相对意义的无限性能已经成为了现实，依靠云原生提供的快速扩缩容能力，云厂商可以向我们提供一种我们可以认为是可靠的服务，让我们不去关心服务的性能问题。开发人员就可以专心于业务，只需要写好代码上传到云厂商提供的平台。这便是<strong>Serverless架构</strong>（无服务），无服务指的更多的是一种不需要关注服务器的思维角度，专业的事”让专业的人做“，服务性能的保障完全交给云厂商。</p>
<p>除此之外无服务的架构还为云厂商和使用者提供了一种新的商业收费模式，即按量付费，可以根据资源占用量和占用时间进行收费，实现使用多少资源花多少钱。这使得资源的利用效率可以得到明显的提升。</p>
<p><strong>无服务</strong>现在还没有一个特别权威的“官方”定义，但它的概念并没有前面各种架构那么复杂，本来无服务也是以“简单”为主要卖点的，它只涉及两块内容：后端设施（Backend）和函数（Function）。</p>
<ul>
<li><strong>后端设施</strong>是指数据库、消息队列、日志、存储，等等这一类用于支撑业务逻辑运行，但本身无业务含义的技术组件，这些后端设施都运行在云中，无服务中称其为“后端即服务”（Backend as a Service，BaaS）。</li>
<li><strong>函数</strong>是指业务逻辑代码，这里函数的概念与粒度，都已经很接近于程序编码角度的函数了，其区别是无服务中的函数运行在云端，不必考虑算力问题，不必考虑容量规划（从技术角度可以不考虑，从计费的角度你的钱包够不够用还是要掂量一下的），无服务中称其为“函数即服务”（Function as a Service，FaaS）。</li>
</ul>
<p>无服务的愿景是让开发者只需要纯粹地关注业务，不需要考虑技术组件，后端的技术组件是现成的，可以直接取用，没有采购、版权和选型的烦恼；不需要考虑如何部署，部署过程完全是托管到云端的，工作由云端自动完成；不需要考虑算力，有整个数据中心支撑，算力可以认为是无限的；也不需要操心运维，维护系统持续平稳运行是云计算服务商的责任而不再是开发者的责任。</p>
<p>在当前阶段来看，无服务天生“无限算力”的假设决定了它必须要按使用量（函数运算的时间和占用的内存）计费以控制消耗算力的规模，因而函数不会一直以活动状态常驻服务器，请求到了才会开始运行，这导致了函数不便依赖服务端状态，也导致了函数会有冷启动时间，响应的性能不可能太好（目前无服务的冷启动过程大概是在数十到百毫秒级别，对于 Java 这类启动性能差的应用，甚至能到接近秒的级别）。</p>
<p>serverless与”微服务“和”云原生“并不是迭代演进的关系，并不是说serverless就比前几种架构更先进，serverless目前还在发展的阶段，对无服务未来的发展仍应持谨慎乐观的态度。在未来也许分布式与非分布式的界限也会变得模糊，两条看似不同路线最终在云端的数据中心交汇。</p>
<blockquote>
<p>We can only see a short distance ahead, but we can see plenty there that needs to be done.</p>
<p>尽管目光所及之处，只是不远的前方，即使如此，依然可以看到那里有许多值得去完成的工作在等待我们。</p>
<p>—— Alan Turing ，1950</p>
</blockquote>
<hr />
<p>​                                                                                                                                                                                                                           ~END~</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/09/18/web%e6%9c%8d%e5%8a%a1%e7%ab%af%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e6%bc%94%e8%bf%9b%e6%a6%82%e8%bf%b0-cfc%e4%be%8b%e4%bc%9a2022-9-18/">WEB服务端系统架构演进概述 &#8211; CFC例会2022.9.18</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/09/18/web%e6%9c%8d%e5%8a%a1%e7%ab%af%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e6%bc%94%e8%bf%9b%e6%a6%82%e8%bf%b0-cfc%e4%be%8b%e4%bc%9a2022-9-18/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL索引及其优化总结 &#8211; CFC例会2022.4.17</title>
		<link>https://www.crazyfay.com/2022/04/17/mysql%e7%b4%a2%e5%bc%95%e5%8f%8a%e5%85%b6%e4%bc%98%e5%8c%96%e6%80%bb%e7%bb%93-cfc%e4%be%8b%e4%bc%9a2022-4-17/</link>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 17 Apr 2022 09:56:30 +0000</pubDate>
				<category><![CDATA[CFC例会]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[数据库]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=116</guid>

					<description><![CDATA[<p>MySQL索引及其优化 MySQL的基础架构 Server层 ：核心服务功能与跨引擎功能的实现（如所有的内置函 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/04/17/mysql%e7%b4%a2%e5%bc%95%e5%8f%8a%e5%85%b6%e4%bc%98%e5%8c%96%e6%80%bb%e7%bb%93-cfc%e4%be%8b%e4%bc%9a2022-4-17/">MySQL索引及其优化总结 &#8211; CFC例会2022.4.17</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>MySQL索引及其优化</h1>
<h2>MySQL的基础架构</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220407190405002.png" alt="image-20220407190405002" /></p>
<ul>
<li>Server层 ：核心服务功能与跨引擎功能的实现（如所有的内置函数、存储过程、触发器等）
<ul>
<li>连接器：管理连接，权限验证</li>
<li>命中缓存：命中缓存则直接返回结果（8.0版本后删除）</li>
<li>分析器：词法分析，语法分析</li>
<li>优化器：执行计划生成，索引选择</li>
<li>执行器：操作引擎返回结果</li>
</ul>
</li>
<li>存储引擎：数据的存储与提取，插件式的架构模式
<ul>
<li>InnoDB *</li>
<li>MyISMA</li>
</ul>
</li>
</ul>
<h2>SQL语句的执行流程</h2>
<blockquote>
<p>一条SQL查询语句是怎么执行的</p>
</blockquote>
<pre><code class="language-sql">SELECT * FROM student WHERE id = 12004030124;</code></pre>
<p>在整体架构层面分析此条语句的执行过程</p>
<p><code>连接器</code> </p>
<p>连接器负责跟客户端建立连接、获取权限、维持和管理连接。并负责校验用户身份。</p>
<pre><code class="language-sql">mysql [-h$ip -p$port] -u$user -p [password 不建议把密码写在此条语句]</code></pre>
<p>命令行中的mysql是客户端工具，在执行完词条语句之后将会与服务器进行连接，即通过TCP三次握手建立连接之后会校验用户身份</p>
<p>连接器会验证用户输入的用户名和密码，若存在问题则会返回错误并且客户端程序结束执行</p>
<p>在通过身份登录验证之后，连接器会查询该用户的权限并保存，接下来所有的操作权限都基于此次查询，即使用户权限发生了更改，在此次连接没有重新的建立的情况下，用户的权限不会直接更改。</p>
<p><code>查询缓存</code></p>
<p>在建立好连接之后就可以进行sql语句的执行了。</p>
<p>执行逻辑将会来到第二部：查询缓存</p>
<p>在拿到查询请求的适合，会先去查询缓存，缓存中以key-value的形式存储查询操作和查询结果，如果缓存中记录了执行过此查询操作，则直接返回该查询操作所对用的结果，即不需要再向下执行复杂的查询工作，可以直接返回查询操作的结果大大的简化了执行时间，如果说没有命中缓存，则向下执行通过IO过程查询，在返回时将查询结果返回到查询缓存中存储。使用缓存是需要代价的，而且往往弊大于利，因为每次表更新都会将查询结果清空，所以对于更新压力大的业务数据库来说，缓存命中率极低，在很多情况下查询缓存甚至是一种性能负担，所以在非表更新极少的静态表之外，不建议使用查询缓存。</p>
<blockquote>
<p>注：在mysql8.0之后查询缓存被彻底的删除</p>
</blockquote>
<p><code>分析器</code></p>
<p>在没有命中缓存的情况下，就到了分析器</p>
<p>先进行 &quot;词法分析&quot;，mysql需要知道sql语句中的内容，识别关键字，例如识别到select关键字，并分析出student为表名，id为列名。在完成&quot;词法分析&quot;后，分析器将进行&quot;语法分析&quot;，分析sql语句是否存在语法问题，若存在则返回错误提示并终止执行。</p>
<p><code>优化器</code></p>
<p>在分析完执行内容之后，优化器会进行相应的优化</p>
<p>优化器会选择如何使用索引和如何进行表连接顺序，并选择执行效率最高的方案执行语句。</p>
<p><code>执行器</code></p>
<p>在优化器选择好执行方案之后，执行器就开始执行sql语句</p>
<p>首先执行器会判断当前用户是否拥有查询此表的权限啊，若无权限在返回错误并终止执行。若有权限打开表使用存储引擎的接口进行操作。例如在此条语句中，因为表中没有定义索引，执行过程大致如下：</p>
<ol>
<li>调用存储引擎接口读取表的第一行，判断是否与条件匹配，即id是否等于0</li>
<li>调用存储引擎接口读取下一行，判断是否匹配，重复执行直到读取完最后一行</li>
<li>将查询到的结果集返回给客户端</li>
</ol>
<p>如果表中存在索引，查询方式也大致相同，调用的是满足条件的第一行和下一行</p>
<h2>索引</h2>
<h3>什么是索引</h3>
<p>在关系数据库中，索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构，它是表中一列或多列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。</p>
<p>索引（Index）是帮助MySQL⾼效获取数据的数据结构。提取句⼦ 主⼲，就可以得到索引的本质：索引是数据结构</p>
<h3>索引的作用</h3>
<ul>
<li>保证数据的准确性</li>
<li>提高检索速度</li>
<li>提高系统性能</li>
</ul>
<h3>索引的类型</h3>
<p>system &gt; const &gt; eq_ref &gt; ref &gt; range &gt; index &gt; all</p>
<h2>InnoDB索引的数据结构</h2>
<p>⼀般来说，索引本身也很⼤，不可能全部存储在内存中，因此索引往往以索引⽂件的形式存储的 磁盘上。这样的话，索引查找过程中就要产⽣磁盘I/O消耗，相对于内存存取，I/O存取的消耗要⾼⼏个 数量级，所以评价⼀个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进 复杂度。换句话说，索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。</p>
<p><font color=blue>分析: </font>有哪些数据结构可以用来作为索引的存储容器</p>
<h3>HashTable</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/src=http___nimg.ws.126.net__url=http___dingyue.ws.126.net_2020_1223_2dd7c986j00qlrut10012c000rq00eam.jpg&amp;thumbnail=650x2147483647&amp;quality=80&amp;type=jpg&amp;refer=http___nimg.ws.126.jpg" alt="" /></p>
<ul>
<li>
<p><strong>优点：</strong>HashTable是字典（dict）的一种经典实现，通过对Key进行散列值计算，我们可以直接得到对应数据的存放位置，可以实现时间复杂度为O(1)的极快的查找速度。</p>
</li>
<li>
<p><strong>缺点：</strong></p>
<ol>
<li>
<p>不支持模糊匹配，由于哈希计算没有局部特性，例如hash(枫阿雨)跟hash(枫阿)没有关系</p>
<ol start="2">
<li>不支持范围匹配，没有顺序性</li>
<li>不支持组合索引，因为hash值需要合在一起计算，所以不能支持最左匹配原则</li>
<li>哈希冲突问题</li>
</ol>
</li>
</ol>
</li>
</ul>
<h3>跳表</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/2d61084503f683b17305049a5eedf60e.png" alt="" /></p>
<ul>
<li>
<p><strong>优点：</strong>跳表基于链式结构，可以多点之间的直接连接，每个节点可以有多个指针指向不同的next节点，查询的时间复杂度为O(n)，但如果节点指针设计的好，可以跳过某些不需要查询的节点，直接定位到数据，可以让n的值变小，从而带来比较大的性能提升</p>
</li>
<li>
<p><strong>缺点：</strong></p>
<ol>
<li>MySQL数据库的存储介质在磁盘当中，而链式结构的结构体存放在内存当中，而且MySLQL进行数据索引的时候是以块的形式，即每个块为16KB的内存页，然后在内存页中进行数据的定位，而跳表所使用的是我们所谓的链表中的Node节点，而且指针管理非常复杂，不适用于磁盘存储介质</li>
<li>MySQL中涉及的查询较多且复杂，如果使用联合索引在跳表的数据结构下，假设有两个字段做联合索引，首先我们需要按首字段进行排序，基于此基础上再对第二个字段进行排序，如果在查询过程中使用跳表，除了要维护第一个的多个节点的跳跃指针，还要想办法维护第二个节点的跳跃指针，指针的管理将会非常的困难，而且还要分别进行不同列的标记。如果多个字段的联合索引则更复杂。</li>
<li>客观上，跳表这种数据结构的出现并应用的时间较晚，此时MySQL已有了自己的实现方式 </li>
</ol>
</li>
</ul>
<h3>红黑树</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/15811884567814.png" alt="" /></p>
<ul>
<li>
<p><strong>优点：</strong>红黑树是一种近似平衡（不完全平衡），结点非黑即红的树，它的树高最高不会超过 2logn，因此查找的时间复杂度为 O(logn)，无论是增删改查，它的性能都十分稳定。</p>
</li>
<li>
<p><strong>缺点：</strong></p>
<ol>
<li>因为二叉树的只有两个子节点，相同存储容量时，树的高度太高，每次节点的访问都对应着一次磁盘IO，红黑树属于一种二叉树，虽然拥有稳定平衡的功能，但是大量的磁盘IO在应用程序中是灾难性的，即若红黑树的高度为20，那么最坏情况下读取一个数据需要进行20次磁盘IO这显然是无法接受的</li>
</ol>
</li>
</ul>
<h3>B-树</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/12058546-44a71668594a77d9.png" alt="" /></p>
<ul>
<li><strong>优点：</strong>B-树是一种专门为磁盘数据读取设计的一种度为n的多路平衡查找树。既然二叉树因为每个结点最多只有两个子结点，最终在存储大量数据时导致树高太高，因此不适合当做 MySQL 的索引，那么让树的每个结点尽可能多的拥有多个子结点，这样在大量储存数据时，树高就相对低很多了，磁盘IO的次数也就大大减少</li>
<li><strong>缺点：</strong>
<ol>
<li>每个节点中既要存索引信息，又要存其对应的数据，如果数据很大，那么当树的体量很大时，每次读到内存中的树的信息就会不太够。</li>
<li>B树遍历整个树的过程和二叉树本质上是一样的，仍需要中序遍历，B树相对二叉树虽然提高了磁盘IO性能，但并没有解决遍历元素效率低下的问题。</li>
</ol>
</li>
</ul>
<h3><strong>B+树</strong></h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/12058546-2ae10c0ddc8ac9ea.png" alt="" /></p>
<ul>
<li>
<p><strong>优点：</strong>B+树相比B树，本质上是一样的，区别就在与B+树的所有根节点都不带有任何数据信息，只有索引信息，所有数据信息全部存储在叶子节点里，这样，整个树的每个节点所占的内存空间就变小了，读到内存中的索引信息就会更多一些，相当于减少了磁盘IO次数，且所有叶子节点都会在同一层，B+树会以一个链表的形式将所有叶子节点的信息全部串联起来，这样，遍历所有数据信息只需要顺序遍历叶子节点就可以了。不仅如此，B+树还有一个相应的优质特性，就是B+树的查询效率是非常稳定的，因为所有信息都存储在了叶子节点里面，从根节点到所有叶子节点的路径是相同的。</p>
</li>
<li>
<p><strong>缺点：</strong>B+树在非叶子节点上不存储数据，有些时候会相对B树有更多的磁盘IO</p>
</li>
</ul>
<h2>索引组织表</h2>
<p>相比于正常存储方式。优化了遍历搜索的性能损失。</p>
<h2>聚集索引</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/12058546-0da96cb9de1ff1c3.png" alt="" /></p>
<h2>辅助索引（非聚集索引）</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/12058546-8cb0dbfd433253b4.png" alt="" /></p>
<p>回表</p>
<p>通过辅助索引定位到聚集索引</p>
<h2>索引的优化</h2>
<h3>使用层面</h3>
<h4>索引列选择</h4>
<p>选择适当的索引，索引应有区分度。如果某个字段的取值范围很广，几乎没有重复，即属于<strong>高选择性</strong> 的字段适合作为索引</p>
<h4>最左前缀原则</h4>
<p>如果进行模糊查询，查找 name 的第一个字为”孙“开头的所有人的id，即SQL语句为</p>
<pre><code class="language-SQL">SELECT id FROM student WHERE name like &#039;孙%&#039;;</code></pre>
<p>由于在B+ 树结构的索引中，索引项时按照索引定义里面出现的字段顺序排序的，索引在查找的时候，可以快速定位到ID为 100 的 “孙a” ，然后直接向右遍历所有姓名为 “孙” 开头的人，直到条件不满足位置。也就是说，我们找到第一个满足条件的之后，直接向右遍历就可以了，由于索引是有序的，所有满足条件的人都会聚集到一起。</p>
<p>而这种定位到最左边，然后向右遍历寻找，就是我们所说的最左前缀原则。</p>
<h4>联合索引</h4>
<p>联合索引时指对表上的多个列进行索引</p>
<pre><code class="language-sql">ALTER TABLE buy_log ADD KEY(user_id);
ALTER TABLE buy_log ADD KEY(user_id, buy_date);
ALTER TABLE buy_log ADD KEY(user_id, buy_date, price);</code></pre>
<p><strong>情况1：</strong>如果只对于userid进行查询</p>
<pre><code class="language-SQL">SELECT * FROM buy_log WHERE user_id = 2;</code></pre>
<p>索引选择：优化器的最终选择是索引userid，因为该索引的叶子节点包含单个键值，所以理论上一个页能存放的记录应该更多。</p>
<p><strong>情况2：</strong>对于userid查询并根据buy_date排序，或 对于userid和buy_date查询并根据price排序</p>
<pre><code class="language-sql">SELECT * FROM buy_log WHERE user_id = 1 ORDER BY buy_date DESC LIMIT 3;
SELECT * FROM buy_log WHERE user_id = 1 AND buy_date = 2020 ORDER BY price DESC LIMIT 3;</code></pre>
<p>索引选择：优化器最终选择的是联合索引(user_id, buy_date)，因为在联合索引中buy_date已经排序好了。根据该联合索引去除数据，无须再对buy_date做一次额外的排序操作。</p>
<p><strong>情况三：</strong>对于userid查询并根据price排序</p>
<pre><code class="language-SQL">SELECT * FROM buy_log WHERE user_id = 1 ORDER BY price DESC LIMIT 3;</code></pre>
<p>此时联合索引不能直接得到结果，其还需要执行一次排序操作，因为索引（user_id，price）并未排序</p>
<h4>覆盖索引</h4>
<p>即从辅助索引中就可以得到查询的记录（此时不能够使用select * 操作，只能对特定的索引字段进行select），而不需要查询聚集索引的记录。使用覆盖索引的一个好处是辅助索引不包含整行记录的所有信息，故其大小要远小于聚集索引，因此可以<strong>减少大量的IO操作</strong>。</p>
<pre><code class="language-SQL">SELECT COUNT(*) FROM buy_log;</code></pre>
<p><strong>InnoDB存储引擎并不会选择通过查询聚集索引来进行统计。由于buy_log表上还有辅助索引，而辅助索引远小于聚集索引，而辅助索引远小于聚集索引，选择辅助索引可以减少IO操作。</strong></p>
<blockquote>
<p>用一句人话概括：直接从辅助索引拿数据</p>
</blockquote>
<h4>避免索引失效</h4>
<ol>
<li>
<p>Like 开头以%开头 导致失效</p>
<pre><code class="language-SQL">SELECT id FROM student WHERE name = ‘%三’;</code></pre>
</li>
<li>
<p>OR 前后有任意一项不是索引字段则失效</p>
<pre><code class="language-SQL">-- age 为索引字段
SELECT id FROM student WHERE age > 10 OR age < 20；-- 索引正常使用
SELECT id FROM student WHERE age > 10 OR score > 80；-- 索引失效
SELECT id FROM student WHERE score > 80 OR age > 10；-- 索引失效</code></pre>
</li>
<li>
<p>联合查询  </p>
<pre><code class="language-sql">-- 联合索引（age，score）id为主键
SELECT id FROM student WHERE age > 10 AND score > 80; -- 索引正常使用
SELECT id FROM student WHERE score > 80; -- 索引失效
-- 没有从联合索引的首元素开始进行索引，则索引失效</code></pre>
</li>
<li>
<p>索引字段进行算数运算 导致索引失效</p>
<pre><code class="language-sql">SELECT id FROM student WHERE age = 10; -- 索引正常使用
SELECT id FROM student WHERE age - 1 = 10; -- 索引失效</code></pre>
</li>
<li>
<p>NOT 取负面（取非）的结果集 导致索引失效</p>
<pre><code class="language-sql">SELECT id FROM student WHERE age != 10; -- 索引失效
SELECT id FROM student WHERE age <> 10; -- 索引失效
SELECT id FROM student WHERE age IS NOT 10; -- 索引失效</code></pre>
</li>
<li>
<p>NULL 可能 导致索引失效</p>
<pre><code class="language-SQL">-- 并不会百分百造成索引失效，
-- MySQL不会对NULL值创建索引，即NULL值在创建索引时会被抛弃
-- （所以逻辑上永远不会为空的字段应加上非空约束，如有逻辑上为空的情况，建议设置默认值约束）
SELECT id FROM student WHERE age IS NULL; -- 索引失效
SELECT id FROM student WHERE age IS NOT NULL; -- 索引失效</code></pre>
</li>
<li>
<p>方法函数 索引列使用内置函数时 可能 导致索引失效</p>
<pre><code class="language-sql">--
SELECT birth_date FROM student WHERE DATE_ADD(birth_date, -1) = CURRENT_DATE(); -- 索引失效
SELECT birth_date FROM student WHERE birth_date = CURRENT_DATE() + 1; -- 正常使用</code></pre>
</li>
<li>
<p>类型转换 导致索引失效</p>
<pre><code class="language-sql">-- phone varchar(11)
-- MySQL中的内置函数默认自动把字段值数字类型转换为字符，以匹配phone的类型
-- 因为字符串转整型会出现很多种情况，如"111" " 111" "111a"都会转换为整型的 111，故此时不使用索引
SELECT phone FROM student WHERE phone = 12345678901; -- 索引失效
SELECT phone FROM student WHERE phone = '12345678901'; -- 索引正常使用
-- 注：当隐式转换时 整型 转 字符串 则不受影响，因为整型转字符串的结果是唯一的</code></pre>
</li>
<li>
<p>同一语句在某些版本 可能 导致索引失效</p>
<pre><code class="language-SQL">-- age 为索引字段
SELECT * FROM student age > 3;</code></pre>
<p>是否使用索引？</p>
<p>首先，age是辅助索引，根据age查找到的是主键，仍需回表去查询聚簇索引来获得整行的数据。所以有两种可能。</p>
<ol>
<li>因为age已经排好序，通过索引查找 age &gt; 3 效率很高。所以有使用age索引的必要。</li>
<li>因为有回表的过程，排好序对应的主键id未必是排好序的，仍需多次额外的io查找，不如直接遍历聚簇索引</li>
</ol>
<p>两种理由都有道理。MySQL在 5.6版本 前后有不同的选择。</p>
<ul>
<li>
<p>5.6版本 之前，无论是否有age的辅助索引，都要走全表扫描，即遍历聚簇索引，不会使用辅助索引。</p>
<p>如果直接回表的话会有3次IO多次查询，如果有n条数据需要回表，即额外需要 n* 3 = 3n 次IO，代价较大（这其实就是离散读的概念），</p>
<p>如果直接全表扫描的话就是聚簇索引3次IO定位到叶子节点，然后根据叶子节点的链表遍历，可能也有多次IO，也并非轻松</p>
<p>所以MySQL设置了一个阈值，当需要查询的数据占到总数据的一定量的时候就会全表扫描，没有到达阈值的时候就根据辅助索引回表多次查询。所以可能失效也可能不失效。</p>
</li>
<li>
<p>5.6版本 之后，引入了 <code>Multi-Range Read (MRR)</code>优化，专门为解决离散读的问题。</p>
<p>执行上述查询语句的时候会进行3次IO使用辅助索引找到所有 age &gt; 3 的主键id，然后将这些数据放在<strong>缓存</strong>中，并将这些id进行了<strong>排序</strong>（在InnoDB引擎层面进行这些操作）。然后根据这些排序的主键id进行查询，省略了多次回表的过程。在支持MRR优化后，针对离散读的场景，能够优化10倍以上的效率。</p>
</li>
</ul>
</li>
</ol>
<h3>存储引擎层面</h3>
<h4>MRR优化（针对离散读）</h4>
<p><strong>离散读</strong>：</p>
<p>假设表：t_index。其中 id 为主键；c1 与 c2 组成了联合索引（c1，c2）；此外 c1 还是一个 <strong>单独索引</strong>。</p>
<p>​   进行如下查找操作：</p>
<pre><code class="language-sql">SELECT * FROM t_index WHERE c1 &gt; 1 AND c1 &lt; 100000;</code></pre>
<p>在最后的索引使用中，优化器选择了 PRIMARY id 聚集索引，也就是表扫描（table scan），而非 c1 辅助索引扫描（index scan）。</p>
<p>因为如果强制使用c1索引，就会造成<strong>离散读</strong>。具体原因在于用户要选取的数据是整行信息，而c1作为辅助索引不能覆盖到我们要查询的信息，因此在对c1索引查询到指定数据后，还需要一次书签访问来查找整行数据的信息。虽然c1索引中数据是顺序存放的，但是再进行聚簇索引查找的数据是无序的，因此变味了磁盘上的<strong>离散读</strong>操作。如果要求访问的数据量很小，则优化器还会选择辅助索引，但是当访问的数据占整个表中数据的蛮大一部分时（一般是20%左右），优化器会选择通过聚簇索引来查找数据。</p>
<p><strong>MRR</strong>：</p>
<p>在MySQL 5.6之后开始支持Multi-Range Read（MRR）优化。MRR 优化的目的就是为了减少磁盘的随机访问，并且将随机访问转化为较为顺序的数据访问，这对于IO-bound类型的SQL查询语句可带来性能极大的提升。<strong>MRR可适用于eq_ref、ref、range级别的索引</strong></p>
<blockquote>
<p>索引的级别：system &gt; const &gt; <strong>eq_ref</strong> &gt; <strong>ref</strong> &gt; <strong>range</strong> &gt; index &gt; all</p>
</blockquote>
<p>MRR优化的执行过程：</p>
<ol>
<li>将查询得到的辅助索引键值存放于一个缓存中，这时缓存中的数据时根据辅助索引键值排序的。</li>
<li>将缓存中的键值根据RowID进行排序</li>
<li>根据RowID的排序顺序来访问实际的数据文件</li>
</ol>
<p>MRR 还可以将某些范围查询拆分为键值对，以此来进行批量的数据查询。这样的好处是可以在拆分过程中，直接过过滤一些不符合条件的数据</p>
<pre><code class="language-sql">SELECT * FROM student WHERE id &gt;= 1000 AND id &lt; 2000 AND age = 1000;</code></pre>
<p>表 student 有 （id, age）的联合索引，因此索引根据id、age的位置关系进行排序。若没有MRR，此时查询类型为Range，SQL优化器会先将id大于1000且小于2000的数据都去除，即使age不等于1000。待取出行数据后再根据age条件进行过滤。这会导致无用的数据被取出。如果有大量的数据且其age不等于1000，则启用MRR优化会使性能有巨大的提升。</p>
<p>若启用MRR优化，优化器会先将查询条件进行拆分，然后再进行数据查询。久上述查询语句而言。优化器会将查询条件拆分为 (1000, 1000), (1001, 1000), (1002, 1000) , ..., (1999, 1000)，最后再根据这些拆分出的条件进行数据的查询。</p>
<p><strong>优化策略：在非必要的情况，拒绝使用 select <em> ，在必须 select </em> 的情况下，尽量使用MySQL 5.6 以后的版本并开启MRR</strong></p>
<h4>ICP优化</h4>
<p>和MRR优化一样，Index Condition Pushdown (ICP) 同样是MySQL 5.6开始支持的一种根据索引进行查询的优化方式。在之前的版本中，当进行索引查询时，首先根据索引来查找记录，然后再根据WHERE条件来过滤记录。在支持ICP后。MySQL数据库会在取出索引的同时，判断是否可以进行WHERE条件的过滤，也就是将WHERE的部分过滤操作放在了存储引擎层。在某些查询下，可以大大减少上层SQL层对记录的索取（fetch），从而提高数据库的整体性能</p>
<h4>FIC（快速索引创建）优化</h4>
<p>在5.5版本之前，MySQL数据库对于索引的添加或者删除的DDL操作，MySQL数据库的操作过程为以下几步：</p>
<ol>
<li>首先<strong>创建</strong>一张新的<strong>临时表</strong>，表结构为通过命令ALTER TABLE新定义的结构</li>
<li>然后把原表中的<strong>数据导入到临时表</strong></li>
<li>接着<strong>删除原表</strong></li>
<li>最后把临时表<strong>重命名</strong>为原来的表名</li>
</ol>
<p>如果对于一张大表进行索引的添加和删除操作，那么会需要很长的时间。更关键的是若有大量事务需要访问正在被修改的表，这意味着数据库服务不可用。</p>
<p>从InnoDB 1.0.x 版本开始支持一宗称为 FIC （Fast Index Creation）的索引创建方式</p>
<p>对于辅助索引的创建，InnoDB存储引擎会对创建索引的表加上一个 <strong>S锁</strong> 。在创建的过程中，不需要重建表，因此速度较之前提高很多，并且数据库的可用性也得到了提高。删除辅助索引操作就更难了，InnoDB存储引擎只需要更新内部视图，并将辅助索引的空间标记为可用（不影响辅助索引的使用，因为可读），<strong>同时删除</strong>MySQL数据库内部视图上对该表的索引定义即可</p>
<p>但是，由于FIC在索引的创建过程中对表加上了S锁，因此在创建的过程中只能对该表进行读操作，若有大量的事务需要对目标表进行写操作，那么数据库的服务同样不可用。此外，FIC反射光hi只限定于辅助索引，对于主键的创建和删除通用需要重建一张表</p>
<h4>Online DDL（在线数据定义）</h4>
<p>在MySQL5.6版本开始支持Online DDL操作，其允许辅助索引创建的同时，还允许其他诸如INSERT、UPDATE、DELETE这类DML操作，极大地提高了MySQL数据库在生产环境中的可用性</p>
<p>不仅是辅助索引，以下几类DDL操作东可以通过“在线”的方式进行操作：</p>
<ul>
<li><strong>辅助索引的创建与删除</strong></li>
<li><strong>改变子增值</strong></li>
<li><strong>添加或删除外键约束</strong></li>
<li><strong>列的重命名</strong></li>
</ul>
<p>使用语法：</p>
<pre><code class="language-sql">ALTER TABLE tbl_name
| ADD{INDEX | KEY} [index_name]
[index_type] (index_col_name,...) [index_option]...
ALGORITHM [=] {DEFAULT | INPLACE | COPY}
LOCK [=] {DEFAULT | NONE |SHARED | EXCLUSIVE}</code></pre>
<p><strong>ALGORITHM</strong> 制定了创建或删除索引的算法，<strong>COPY</strong> 表示按照MySQL 5.1版本之前的工作方式，即创建临时表的方式。<strong>INPLACE</strong> 表示索引创建或删除操作不需要创建临时表。 <strong>DEFAULT</strong> 表示根据参数 old_alter_table 来判断是通过 INPLACE 还是 COPY的算法，该参数的默认值为OFF，表示采用个INPLACE的方式</p>
<p><strong>LOCK</strong> 部分索引创建或删除时对表添加锁的情况：</p>
<ol>
<li>
<p><strong>NONE</strong></p>
<p>执行索引创建或者删除操作时，对目标表不添加任何的锁，即事务仍然可以进行读写操作，不会受到阻塞。因此这种模式可以获得最大的并发度。</p>
</li>
<li>
<p><strong>SHARE</strong></p>
<p>和之前的FIC类似，执行索引创建或删除操作时，对目标表加上一个<strong>S锁</strong>，对于并发地读事务，依然可以执行，但是遇到写事务，就会发生等待操作。</p>
</li>
<li>
<p><strong>EXCULSIVE</strong></p>
<p>在EXCULSIVE模式下，执行索引创建或删除操作时，对目标表加上一个<strong>X锁</strong>。读写事务都不能进行，因此会阻塞所有的线程，这和COPY方式运行得到的状态类似，但是不需要像COPY方式那也创建一张临时表。</p>
</li>
<li>
<p><strong>DEFAULT</strong></p>
<p>DEFAULT模式会先判断当前操作是否可以使用NONE模式，若不能，则判断是否可以使用SHARE模式，最后判断是否可以使用EXCLUSIVE模式，也就是说DEFAULT会通过判断事务的最大并发性来判断执行DDL的模式。</p>
</li>
</ol>
<p><strong>InnoDB存储引擎实现Online DDL 的原理是在执行创建或者删除操作的同时，将INSERT、UPDATE、DELETE这类DML操作日志写入到一个缓存中，待完成索引创建后再将重做应用到表上，以此达到数据的一致性。</strong></p>
<p><strong>由于Online DDL 在创建索引完成后再通过重做日志达到数据库的最终一致性，这意味着再索引创建过程中，SQL优化器不会选择正在创建中的索引。</strong></p>
<h2>MySQL表设计与索引调优</h2>
<h3>表的设计阶段</h3>
<p>单行数据量的要求，MySQL底层的内存页的大小为16KB，所以一条数据如果是16KB，则一个内存页只能存储一条数据，这是不可接受的，如果1条数据为1KB则能存储16条数据。之所以提到内存页，因为MySQL的每次IO就是读取一个内存页的数据，所以要保证单行数据量的值要尽量的小。为此，在初期需求分析的时候就要做到数据尽量的小，比如存放一个UUID，如果要存放32位的UUID，则直接定死为32位，不要浪费存储空间，否则当数据达到一定规模的时候会影响到B+树的整体结构，B+树拥有高扇出性，每一个节点对应着16KB的内存页，对于3层高的B+树，根节点存放着16KB的主键与指针，假设主键为bigint8个字节，指针固定为6个字节，所有对应着有16KB / 14B 约等于 1170 个指针，每个指针指向一个节点，第二层的每个节点的结构与根节点均相似，所以第二层则总共扇出1170 <em> 1170 个页子节点，若每条数据长1KB，则一个三层高的B+树能够存储 1170 </em> 1170 * 16 条数据，约等于两千万多条，如果一个数据大小为16KB则存储容量就大打折扣了。所以在设计阶段就要保证数据大小。在做设计的时候也应该或用枚举，比如性别男女分别用0和1来表示，一可以保证检索快速二可以控制数据量的大小，就能增加相同高度的B+树的数据容纳量。</p>
<h3>数据操作时索引的设计</h3>
<p>MySQL中的三种索引，聚簇索引、辅助索引、覆盖索引。</p>
<p>聚簇索引，每张表只有一个，即 PRIMARY KEY；辅助索引，是开发时额外增加的索引，每个辅助索引对应着一颗B+树；覆盖索引，本质是没有树的。在SQL的设计的时候首先要确保一点，不要使用SELECT <em>，它必须要走聚集索引，也就是要全表扫描，只有在聚集索引中才有全部的数据。如果要使用的话尽量使用5.6以后的版本，因为5.6以后有了针对离散读的MRR优化和ICP优化，这样 SELECT </em> 的查询速度会更快。着重提一下覆盖索引，在对于索引进行设计的时候，如果需要查询n个字段，如果保证这个n个字段都可以作为索引的话就尽量设置索引，因为在辅助索引中储存索引的值和主键的值，就可以避免回表查询。在书写SQL的时候，多使用执行计划查看索引是否失效，因为在特定的场景和函数下，MySQL的索引可能会失效</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/04/17/mysql%e7%b4%a2%e5%bc%95%e5%8f%8a%e5%85%b6%e4%bc%98%e5%8c%96%e6%80%bb%e7%bb%93-cfc%e4%be%8b%e4%bc%9a2022-4-17/">MySQL索引及其优化总结 &#8211; CFC例会2022.4.17</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>从BST、AVL树、2-3树杀到BLT(红黑树)，常见树状数据结构解读 &#8211; CFC例会2021.10.10</title>
		<link>https://www.crazyfay.com/2021/10/10/%e4%bb%8ebst%e3%80%81avl%e6%a0%91%e3%80%812-3%e6%a0%91%e6%9d%80%e5%88%b0blt%e7%ba%a2%e9%bb%91%e6%a0%91%ef%bc%8c%e5%b8%b8%e8%a7%81%e6%a0%91%e7%8a%b6%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%a7%a3/</link>
					<comments>https://www.crazyfay.com/2021/10/10/%e4%bb%8ebst%e3%80%81avl%e6%a0%91%e3%80%812-3%e6%a0%91%e6%9d%80%e5%88%b0blt%e7%ba%a2%e9%bb%91%e6%a0%91%ef%bc%8c%e5%b8%b8%e8%a7%81%e6%a0%91%e7%8a%b6%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%a7%a3/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 10 Oct 2021 15:36:07 +0000</pubDate>
				<category><![CDATA[CFC例会]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[数据结构]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=132</guid>

					<description><![CDATA[<p>让我们开始吃树~ 提起树状数据结构的家族，我们不得不从二叉树开始说起。 在学二叉树的时候，我们知道，二叉树是指 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2021/10/10/%e4%bb%8ebst%e3%80%81avl%e6%a0%91%e3%80%812-3%e6%a0%91%e6%9d%80%e5%88%b0blt%e7%ba%a2%e9%bb%91%e6%a0%91%ef%bc%8c%e5%b8%b8%e8%a7%81%e6%a0%91%e7%8a%b6%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%a7%a3/">从BST、AVL树、2-3树杀到BLT(红黑树)，常见树状数据结构解读 &#8211; CFC例会2021.10.10</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>让我们开始吃树~</h1>
<p>提起树状数据结构的家族，我们不得不从二叉树开始说起。</p>
<p>在学二叉树的时候，我们知道，二叉树是指每个结点最多只有两个子结点的树。</p>
<p>二叉树，是指树中每个结点最多只有两个结点的树。当然，二叉树本身好像没有什么太大的作用。我们平时所说的二叉树，基本上就是指二叉排序树(二叉查找树)。</p>
<h2>二叉查找树（BST）</h2>
<p>二叉查找树就是在二叉树的基础上增加有序性，这个有序性一般是指自然顺序，有了有序性，我们就可以使用二叉树来快速的查找、删除、插入元素了。</p>
<p>但是，二叉查找树有个非常严重的问题，试想，还是这三个元素，如果按照A、B、C的顺序插入元素会怎样？</p>
<p>那么如果按照顺序插入，就会变成一个单链表。没错，当按照元素的自然顺序插入元素的时候，二叉查找树就退化成单链表了，单链表的插入、删除、查找元素的时间复杂度是多少？？</p>
<p>所以，在极限状态下，二叉查找树的时间复杂度是非常差的。</p>
<p>既然，插入元素后有可能导致二叉查找树的性能变差，我们是不是增加了一些手段，让插入后的二叉查找树依然性能良好？</p>
<p>这种手段，就叫做平衡。可以做到自平衡的树就叫做平衡树。</p>
<h2>平衡树</h2>
<p>平衡树，是指插入、删除元素后可以自平衡的二叉查找树。我们平衡的手段，就是旋转。</p>
<p>平衡这个概念一直都有，直到62年，发明了第一种平衡树—-AVL树。</p>
<p>严格来说，平衡树是指可以自平衡的二叉查找树，关键词就是：自平衡、二叉、查找（有序）。</p>
<h2>AVL树</h2>
<p>AVL树是指任意节点的两个子树的高度差不超过1的平衡树。</p>
<p>当数据量非常多的时候，你会非常难以判断这是否是一颗AVL树，比如</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/7E29AAC1F02A0FC1DD4D2F6242232D5F.png" alt="7E29AAC1F02A0FC1DD4D2F6242232D5F" /></p>
<p>如果把上面看成一颗二叉排序树，他是一颗AVL树，其实你很难一眼就看出来他是一颗AVL树，这就是AVL树的第一个缺点，不够直观，特别是当结点特别多的时候。</p>
<p>第二个缺点就是，在插入和删除的时候自平衡的过程非常的复杂。</p>
<p>基于这些缺点，所以，后来又发展出来了各种各样的神奇的平衡树。</p>
<h2>多路平衡二叉树</h2>
<h3>索引</h3>
<p>一般来说，我们操作的数据都是存储在内存（CPU）中的，但如若我们要操作的数据集非常大，大到内存已经没办法处理了怎么办呢？如数据库中的上千万条记录的数据表、硬盘中的上万个文件等，他们必然不能都存储在内存中，而是存储在外存中的。</p>
<p>对于外存中的数据，常见如数据库，我们通常通过索引表来进行数据查找和交互，一般来说，索引表本身也很大，因为数据库的数据非常多，因此索引也不可能全部存储在内存中，因此索引表往往也是以索引文件的形式存储的磁盘上。Mysql的MyISAM引擎的索引文件和数据文件是分离的，一张数据库表就有它对应的索引表，索引表中一个索引对应一条数据库记录的磁盘地址，内存中发起请求通过指定索引的查找索引表即可定位唯一的一条数据；当然Mysql的InnoDB引擎的索引表也是数据表，即索引表保存了索引和完整的数据记录。但是不管怎么说数据的查找都会依赖索引，并且建议通过索引查找，因为如果没有走索引，那就会走全表扫描，使得查找效率大大降低。 因为索引文件同样存储在磁盘上，这样的话，索引查找过程中每查找一次就要产生一次磁盘I/O消耗，相对于CPU存取，I/O存取的消耗要高几个数量级，访问磁盘的成本大概是访问内存的十万倍左右。</p>
<p>实际上，考虑到磁盘IO是非常高昂的操作，计算机操作已经系统做了一些优化，当一次IO时，不光把当前磁盘地址的数据，而是把相邻的数据也都读取到内存缓冲区内，局部预读性原理告诉我们，当计算机访问一个地址的数据的时候，与其相邻的数据也会很快被访问到。</p>
<p>每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关，一般为4k或8k大小的连续磁盘块，也就是我们读取一页内的数据时候，实际上才发生了一次IO，这个理论对于索引的数据结构设计非常有帮助，通常，索引节点的大小被设计为一页的大小。</p>
<h3>多路平衡查找树</h3>
<p>对于一旦涉及到这样的外部存储设备（外存），关于时间复杂度的计算就会发生变化，访问某个表/集合元素的时间已经不仅仅是寻找该元素所需比较次数的函数，我们必须考虑对硬盘IO进行操作的时间。由于IO耗时远大于CPU耗时，所以此处评价一个数据结构作为索引的优劣最重要的指标就是要尽量减少查找过程中磁盘IO的存取次数。</p>
<p>我们之前谈的树，都是一个节点可以有多个孩子，但是它自身只存储一个元素。二叉树限制更多，节点最多只能有两个孩子。一个节点只能存储一个元素，在元素非常多的时候，就使得要么树的度非常大（节点拥有子树的个数的最大值），要么树的高度非常大，甚至两者都必须足够大才行，这就使得IO次数非常多，这显然成了时间效率上的瓶颈，并且由于一次IO读取一个节点的数据，普通二叉树并不能容纳更多的数据，这样又造成了磁盘块空间的浪费。</p>
<p>以上种种限制迫使我们设计出每一个节点可以存储多个元素，并且数据结构的高度可控的数据结构，为此引入了多路查找树的概念。一颗平衡多路查找树同样可以使得数据的查找效率保证在O(logN)这样的对数级别上，此时底数为叉数或者阶。</p>
<p>多路查找树（muitl-way search tree），其每一个节点的孩子数可以多于两个，且每一个节点处可以存储多个元素。由于它是一颗平衡查找树，所有元素之间存在某种特定的排序关系。在这里，每一个节点可以存储多少个元素，以及它的孩子数的多少是非常关键的。</p>
<h2>2-3树</h2>
<p>因为AVL树所带来的搜索性能的提升，不足以弥补平衡树所带来的性能损耗。所以，就开始思考，有没有一种绝对平衡的树。没有高度差，没有高度差就没有平衡因子，没有平衡因子就没有旋转操作。</p>
<p>随着这种思考，衍生出了2-3树。也就是二叉-三叉树。</p>
<p>2-3树就是一种绝对平衡的树，任意节点到它所有的叶子结点的深度都是相等的。</p>
<h3>定义</h3>
<p>一颗2-3树或为一颗空树，或有以下结点组成：</p>
<p>2-节点：含有一个元素和两个子树，左子树的所有元素的值均小于它的父结点，右子树所有元素的值均大于它的父结点。</p>
<p>3-节点：还有两个元素和三个子树（左中右 子树），左子树所有元素的值均小于它的父结点，中子树所有元素的值都位于父结点两个元素之间，右子树所有元素的值均大于它的父结点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/B2803DEAA17FCC1531BE221167C2774B.png" alt="B2803DEAA17FCC1531BE221167C2774B" /></p>
<h3>2-3树查找元素</h3>
<p>2-3树的查找类似于二分，根据元素的大小来决定来决定查找的方向。要判断一个元素是否存在，我们就要先将待查找元素和根元素比较，如果他和任意一个相等，那查找命中，否则根据比较结果来选择查找方向。</p>
<h3>2-3树插入元素</h3>
<p>插入元素首先进行查找命中。若查找命中则不插此元素。如果需要支持重复的元素则将这个元素对象添加一个属性count。若查找未命中，则在叶子结点中插入这个元素。</p>
<p>空树的插入很简单，创建一个结点就可以了。如果不是空树，插入又分成了四种情况：</p>
<h4>1、向2-结点中插入元素</h4>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3ED36C433E50EA763289F84365918580.png" alt="3ED36C433E50EA763289F84365918580" /></p>
<h4>2、向一颗只含有一个3-节点的树中插入元素</h4>
<p>如果命中查找结束于3-节点，先临时将其成为4-节点，把待插入元素添加到其中，然后将4-节点转化为3个2-节点，中间的节点成为左右节点的父节点。如果之前临时4-节点有父节点，就会变成向一个父节点为2-节点的3-节点中插入元素，中间节点与父节点为2-节点的合并。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3F37574E0D7E518219A587287BF06A72.png" alt="3F37574E0D7E518219A587287BF06A72" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/0DC1D717FF21D0C9D0032817E131C5E7.png" alt="0DC1D717FF21D0C9D0032817E131C5E7" /></p>
<h4>3、向一个父结点为2-节点的3-节点中插入元素</h4>
<p>同前者</p>
<h4>4、向一个父结点为3-节点的3-节点中插入元素</h4>
<p>插入元素后一直向上分解临时的4-节点，直到遇到2-节点的父节点变成3-节点不再分解。如果达到树根节点还是4-节点，则进行分解根节点，此时树高+1（只有分解根节点才会增加树高），下面动画2-3树插入会出这个例子。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/A905BF2AC6C40E37686814BBE3F01CA8.png" alt="A905BF2AC6C40E37686814BBE3F01CA8" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/EC12747008D32B0297348F6048F7D6EC.png" alt="EC12747008D32B0297348F6048F7D6EC" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DC1F61426E9BF80FFDE20C2998B6D82F.png" alt="DC1F61426E9BF80FFDE20C2998B6D82F" /></p>
<h3>2-3树的删除操作</h3>
<p>2-3树的删除也分为三种情况，与插入相反。</p>
<h4>1、当删除元素为于3-节点的叶子结点上</h4>
<p>只需要删除该元素即可，不会影响到整棵树的其他结点结构。</p>
<h4>2、当删除元素位于非叶子结点</h4>
<p>使用中序遍历找到待删除节点的后继节点，然后将后继节点与待删除节点位置互换，此时就将问题转化为删除节点为叶子节点（平衡树的非叶子节点中序遍历后继节点肯定是叶子节点），如果该叶子是3-节点，则跟情况（1）一样，如果该节点是2-节点，则跟后面的情况（3）一样；</p>
<h4>3、当删除元素位于2-结点的叶子结点上</h4>
<p>删除元素2-结点的叶子结点的步骤相对很复杂，删除后需要做出相应的判断。并根据判断结果调整树的结构。</p>
<h5>1、删除结点为2结点，父结点为2结点，兄弟结点为3结点。</h5>
<p>操作步骤：当前待删除节点的父节点是2-节点、兄弟节点是3-节点，将父节点移动到当前待删除节点位置，再将兄弟节点中最接近当前位置的key移动到父节点中。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/518D67EA6D8509D16FAD9D1904A8ACDD.png" alt="518D67EA6D8509D16FAD9D1904A8ACDD" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/2E5024C27092234C297400CEDA7B9F83.png" alt="2E5024C27092234C297400CEDA7B9F83" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/B315F2F64D1D370C81EF93A1AD5C09EE.png" alt="B315F2F64D1D370C81EF93A1AD5C09EE" /></p>
<h5>2、删除结点为2-结点，父结点为2-结点，兄弟结点为2-结点</h5>
<p>操作步骤：当前待删除节点的父节点是2-节点、兄弟节点也是2-节点，先通过移动兄弟节点的中序遍历直接后驱到兄弟节点，以使兄弟节点变为3-节点；</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/95B6DE7B306E67E29947EFFF89F42EDD.png" alt="95B6DE7B306E67E29947EFFF89F42EDD" /></p>
<p>删除结点4位2-结点，兄弟结点7也为2结点，需要中序遍历得到兄弟结点7的直接后继8，然后结点7和8构成3-结点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/2A6B444B73D9E8964CBC550C7F456150.png" alt="2A6B444B73D9E8964CBC550C7F456150" /></p>
<p>重复1情况</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3C2ABC11B792416137572369FB0DB94E.png" alt="3C2ABC11B792416137572369FB0DB94E" /></p>
<h5>3、删除结点为2-结点，父结点为3-结点</h5>
<p>操作步骤：当前待删除节点的父节点是3-节点，拆分父节点使其成为2-节点，再将再将父节点中最接近的一个拆分key与中孩子合并，将合并后的节点作为当前节点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/AAB07A766FB58AE68FFD61B2D04FC8AE.png" alt="AAB07A766FB58AE68FFD61B2D04FC8AE" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/57283249AA855D78DF813CD4F1EFE570.png" alt="57283249AA855D78DF813CD4F1EFE570" /></p>
<h5>2-3树为满二叉树，删除叶子结点</h5>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/B4AB2DA1688378CB75073FCEE7120E83.png" alt="B4AB2DA1688378CB75073FCEE7120E83" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/B6B20279263A44F14C8D76BCBABCCF0B.png" alt="B6B20279263A44F14C8D76BCBABCCF0B" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/63E811D4A31FC56C749DA5C775DA82BA.png" alt="63E811D4A31FC56C749DA5C775DA82BA" /></p>
<p>2-3 树作为一种平衡查找树，查询效率比普通的二叉排序树要稳定许多。但是2-3树需要维护两种不同类型的结点，查找和插入操作的实现需要大量的代码，而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。</p>
<p>可以看到，上面自平衡的过程中，出现了一种节点，它具有四个子节点和三个数据元素，这个节点可以称作4节点，如果把4节点当作是可以允许存在的，那么，就出现了另一种树：2-3-4树。</p>
<h2>2-3-4树</h2>
<p>2-3-4树，它的每个非叶子节点，要么是2节点，要么是3节点，要么是4节点，且可以自平衡，所以称作2-3-4树。</p>
<p>2节点、3节点、4节点的定义在上面已经提及，我们再重申一下：</p>
<p>2节点：包含两个子节点和一个数据元素；</p>
<p>3节点：包含三个子节点和两个数据元素；</p>
<p>4节点：包含四个子节点和三个数据元素；</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/12B4C9A2A5A4050E63B3BCBCC514B79D.png" alt="12B4C9A2A5A4050E63B3BCBCC514B79D" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/389431E5C233E35BE980B59763EA9D0E.png" alt="389431E5C233E35BE980B59763EA9D0E" /> 插入M，依旧符合2-3-4树的规则。在插入N呢？</p>
<p>插入N，L上移。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/850CBC62655548D8E981256180E8D93F.png" alt="850CBC62655548D8E981256180E8D93F" /></p>
<p>F上移 <img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/9B452644301239F387791283F00FE9AB.png" alt="9B452644301239F387791283F00FE9AB" /></p>
<p>是不是挺简单的，至少比AVL树那种左旋右旋简单得多。同样地，在2-3-4树自平衡的过程中出现了临时的5节点，所以，如果允许5节点的存在呢？嗯，2-3-4-5树由此诞生！同样地，还有2-3-4-5-6树、2-3-4-5-6-7树……子子孙孙，无穷尽也~所以，有人就把这一类树归纳为一个新的名字：<strong>B树</strong>。</p>
<h2>B树</h2>
<p>B树，表示的是一类树，它允许一个节点可以有多于两个子节点，同时，也是自平衡的，叶子节点的高度都是相同的。所以，为了更好地区分一颗B树到底属于哪一类树，我们给它一个新的属性：度（Degree）。具有度为3的B树，表示一个节点最多有三个子节点，也就是2-3树的定义。具有度为4的B树，表示一个节点最多有四个子节点，也就是2-3-4树的定义。</p>
<p>B树，一个节点可以存储多个元素，有利于缓存磁盘数据，整体的时间复杂度趋向于O(log n)，原理也比较简单，所以，经常用于数据库的索引，包括早期的mysql也是使用B树来作为索引的。但是，B树有个大缺陷，比如，我要按范围查找元素，以上面的2-3-4树为例，查找大于B且小于K的所有元素，该怎么实现呢？很难，几乎无解，所以，后面又出现替代B树的方案：B+树。当然了，B+树不是本节的重点，本节的重点是红黑树。 来了来了，有意思的红黑树来了~~</p>
<h2>红黑树</h2>
<p>我们先用图来体会</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6496AA7628604F9B1E5EAF31283A5AC0.png" alt="6496AA7628604F9B1E5EAF31283A5AC0" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/EAAAED290A50BB6C57414B7F5CB6D1C5.png" alt="EAAAED290A50BB6C57414B7F5CB6D1C5" /></p>
<p>我可以跟大家说，这棵树，就是一颗红黑树。红黑树就是2-3-4树。</p>
<p>我们知道2-3-4的插入、删除、查找元素的原理是相当简单的，那么，我们是不是可以利用2-3-4树来记忆红黑树呢？答案是肯定的，我们就来看看如何利用2-3-4树来快速掌握红黑树，再也不用死记硬背了~~</p>
<p>现在，我们来看一看红黑树的精髓所在，我们来看一看，什么是红黑树的黑高，为什么要有红黑树，红黑树的旋转跟AVL有什么区别，如何去选择？红黑树是如何保持平衡的？直接进入正题</p>
<h2>什么是红黑树</h2>
<p>红黑树是一颗自平衡的二叉排序树，树上的每一个结点都遵循下面的规则（特别注意，这里的自平衡和平衡二叉树AVL的高度有区别）。我们再来看一看红黑树的定义：</p>
<p>1、每一个结点都有一个颜色，要么是红色，要么是黑色。</p>
<p>2、树的根结点为黑色</p>
<p>3、树中不存在两个相邻的红色节点（红色节点的父结点和孩子结点均不为黑色）</p>
<p>4、从任意一个结点出发，包括根结点，到其任何后代NULL结点（默认都是黑色啊）的每条路径都具有相同数量的黑色结点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/71865477AD0E83C3DD9551E34BF50344.png" alt="71865477AD0E83C3DD9551E34BF50344" /></p>
<p>这就是一颗典型的红黑树，树中的每个结点的颜色要么是黑色，要么是红色；根结点 6 为黑色结点；树中不存在两个相邻的红色结点，比如结点 15 为红色结点，其父亲节点 6 与两个孩子结点就一定是黑色，而不能是红色； 从结点到其后代的 NUll结点 的每条路径上具有相同数目的黑色结点，比如根结点 6 到其左子树的 NULL结点 包含三个黑色结点，到其右子树所有的 NULL 结点也包含三个黑色结点。 可能还不够清晰，为此我对上图做了修改为所有默认为黑色的 NULL 结点给了一个标记。</p>
<p>现在解释规则的第四条简直不能再清晰了！比如根结点 6 到 NULL结点 a 的路径 6→2→a 上的黑色结点为 3 个，从根结点 6 到结点 c 的路径 6→15→10→9→c 中包含的黑色结点个数也是 3 个，同理从根结点 6 到其他所有 NULL结点 的黑色结点数都是 3 。再举个栗子，从红色结点 15 到NULL结点 d 的路径 15→18→g 包含 2 个黑色结点，到NULL结点 c 的路径 15→10→9→c 也包含黑色结点 2 个，从结点 15 到其所有后代的 NULL结点的 黑色结点数目都是 2 。</p>
<h2>为什么要有红黑树</h2>
<p>大多数二叉排序树BST的操作（查找、最大值、最小值、插入、删除等等）都是O（h）的时间复杂度，h 为树的高度。但是对于斜树而言（BST极端情况下出现），BST的这些操作的时间复杂度将达到O（n），n就是结点数。为了保证BST的所有操作的时间复杂度的上限为O（logn） ，就要想办法把一颗BST树的高度一直维持在logn，而红黑树就做到了这一点，红黑树的高度始终都维持在logn，n 为树中的顶点数目。</p>
<p>这个时候就有一个疑问，不对啊。AVL树不也始终是一个均值么？</p>
<h2>红黑树RBT与平衡二叉树AVL的比较</h2>
<p>AVL 树比红黑树更加平衡，但AVL树在插入和删除的时候也会存在大量的旋转操作。所以当你的应用涉及到频繁的插入和删除操作，切记放弃AVL树，选择性能更好的红黑树；当然，如果你的应用中涉及的插入和删除操作并不频繁，而是查找操作相对更频繁，那么就优先选择 AVL 树进行实现。</p>
<h2>一颗红黑树到底是如何保持平衡的呢？</h2>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/E3654B58B591C1A33A74CECDCC4210FB.png" alt="E3654B58B591C1A33A74CECDCC4210FB" /></p>
<p>举一个很简单但是很经典的例子，包含三个结点的单链是不可能出现在红黑树当中的。关于这一点，我们可以自己绘制一条单链，然后尝试为其着色，来判断。</p>
<p>从上图中可以发现，将根结点 9 涂黑色，其他结点分四种情况着色，结果都不满足红黑树的性质要求。唯一的办法就是调整树的高度</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/E5B35A3EEBB7DD434042973E18FD67DC.png" alt="E5B35A3EEBB7DD434042973E18FD67DC" /></p>
<p>这就算我们对于红黑树的初探，然后我们来看两个重要的概念。</p>
<h2>什么是一颗红黑树的黑高？</h2>
<p>在一颗红黑树中，从某个结点 x 出发（不包含该结点）到达一个叶结点的任意一条简单路径上包含的黑色结点的数目称为 黑高 ，记为 bh(x) 。所以我们发现，其实6和15的黑高是一样的，都是2。</p>
<p>计算结点 6 的黑高，从结点 6 到结点 c 的路径是 6→15→10→9→c ，其中黑色结点为 6、10、c ，但是在计算黑高时，并不包含结点本身，所以从结点 6 到结点 c 的路径上的黑色结点个数为 2 ，那么 bh(6)=2 ；从结点 15 到结点 c 的路径为 15→10→9→c ，其中黑色结点为 10、c ，所以从结点 15 到结点 c 的路径上黑色结点数目为 2 ，bh(15)=2 。</p>
<p>因为红黑树的黑高为其根结点的黑高。所以根据红黑树的性质3和性质4，一颗红黑树的黑高bh一定&gt;= h/2。</p>
<table>
<thead>
<tr>
<th><code>1 2 3 </code></th>
<th><code>Number of nodes from a node to its farthest descendant leaf is no more than twice as the number of nodes to the nearest descendant leaf.  从一个结点到其最远的后代叶结点的顶点数目不会超过从该结点到其最近的叶结点的结点数目的两倍。 </code></th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>Copy</p>
<p>其中黑高 bh 就表示从根结点 6 到离它最近的叶结点 2 包含的结点数 2 ，而 h 则表示从根结点 6 到其最远的叶结点 9 所包含的结点数目 4 ，显然这一公式是合理的。</p>
<p>引出来一个道理：一颗有n个结点的红黑树的高度h&lt;=2lg(n+1)。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/E86059A00EBED8EF4471C6B7B5D10C61.png" alt="E86059A00EBED8EF4471C6B7B5D10C61" /></p>
<p>这就合并成了一颗2-3-4树，这棵树中的每一个结点有2、3、4个孩子结点，而一颗2-3-4树的叶结点有着相同的深度h‘。</p>
<p>也正是基于这个，所以对于有n个结点的红黑树而言，不论查找、删除、最大值、最小值等等的时间复杂都平均下来了，也就是O（logn）。</p>
<h2>红黑树的插入</h2>
<p>其实红黑树的操作也很简单，就是比AVL多了一个着色的操作。</p>
<p>在AVL中，我们通过左旋和右旋来调整由于插入和删除所造成的不平衡的问题。在红黑树中，我们使用两种方式：</p>
<p>1、重新着色</p>
<p>2、旋转</p>
<p>当红黑树中出现不平衡的状态，我们首先会考虑重新着色，如果重新着色依旧不能使红黑树平衡，那么就考虑旋转。插入操作主要有两种情况，具体取决于叔叔结点的颜色。如果叔叔结点是红色的，我们会重新着色。如果叔叔结点是黑色的，我们会旋转或者重新着色，或者两者都考虑。</p>
<p>假设x是新插入的一个结点。</p>
<p>1、进行标准的BST插入并将新插入的结点设置为红色</p>
<p>2、如果x是根结点，将x的颜色转化为黑色（整棵树的黑高增加1）</p>
<p>3、如果x的父结点p不是黑色并且x不是根结点，则：</p>
<p>1）、如果x的叔叔结点u是红色；</p>
<p>2）、如果x的叔叔结点u是黑色，则对于x、x的父结点p和x的爷爷结点g有四种情况：</p>
<table>
<thead>
<tr>
<th><code> 1 2 3 4 5 6 7 8 9 10 11 12 13 </code></th>
<th><code>					LL（p是g的左孩子且x是p的左孩子） 					LR（P是g的右孩子且x是p的右孩子） 					RR（p是g的右孩子且x是p的右孩子） 					RL（p是g的右孩子且x是p的左孩子） 					将插入结点x的父结点p和叔叔结点u的颜色变成黑色 					将x的爷爷结点g设置为红色 					将g看作是x，对于新的x重复2、3两步 </code></th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>Copy</p>
<h3>插入结点x的叔叔结点u是红色</h3>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/A872E605C02B25828EE1BABE4FD90F68.png" alt="A872E605C02B25828EE1BABE4FD90F68" /></p>
<p>对于新插入结点 x，我们执行标准的 BST 插入之后，将插入结点 x 着色为红色；如果插入结点不是根结点，且x的父结点 p 为红色结点，分为 1) 和 2) 两种情况处理，我们先看的是 1) 的情况：x 的叔叔结点 u 为红色，如下图所示：</p>
<p>第一步：将父亲结点p和叔叔结点u都设置为黑色</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/EB1EE1A5D235916CF858F83EF95A1593.png" alt="EB1EE1A5D235916CF858F83EF95A1593" /></p>
<p>第二步：将g的颜色设置为红色</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/783D67F6415A8E54A62C584F495E0853.png" alt="783D67F6415A8E54A62C584F495E0853" /></p>
<p>第三步：针对于g结点，在执行第二，第三步。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/F584AD4A6EFB98D5841EDF667F96A98F.png" alt="F584AD4A6EFB98D5841EDF667F96A98F" /></p>
<h3>插入结点x的叔叔结点u为黑色</h3>
<p>当插入结点x的叔叔结点为黑色的时候，根据插入接待你x、x的父结点p和x的爷爷结点g可能出现的位置关系，分为四种情况。</p>
<h4>LL</h4>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/BAC53F055512CDC1DF608A348CDE2030.png" alt="BAC53F055512CDC1DF608A348CDE2030" /></p>
<h4>LR</h4>
<p>首先通过左旋p转化为LL的情况：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/387FD926658BB053CADF74B2946DD6C8.png" alt="387FD926658BB053CADF74B2946DD6C8" /></p>
<p>然后按照LL的情况处理：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/F94D034B6484308267D98246D5BE3361.png" alt="F94D034B6484308267D98246D5BE3361" /></p>
<h4>RR</h4>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3D9FFA2BFAA18346D3D9AA290341CA81.png" alt="3D9FFA2BFAA18346D3D9AA290341CA81" /></p>
<h4>RL</h4>
<p>先右旋转化成RR的情况</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/589F56B2687BBA47E27255CF6C386513.png" alt="589F56B2687BBA47E27255CF6C386513" /></p>
<p>然后按照RR的情况处理</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/753194AB972C1327F80AA836F49139AF.png" alt="753194AB972C1327F80AA836F49139AF" /></p>
<p>到这里，插入排序就全部讲完了。但我也知道很多人可能会有疑惑，那就让我们来构造一颗红黑树，看看运用上述规则到底是否适用。</p>
<h3>红黑树插入操作示例</h3>
<p>下面就带大家构造一颗稍微复杂一点儿的红黑树：</p>
<p>初始时，我们已知的插入依次插入：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/485ED9801A998E3AEB85E0617689B04F.png" alt="485ED9801A998E3AEB85E0617689B04F" /></p>
<p>第一步：插入结点2，结点2就是根结点，设置为黑色：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/840C811471AACCAA8A6FA71A4CB0D81C.png" alt="840C811471AACCAA8A6FA71A4CB0D81C" /></p>
<p>第二步：插入结点6，首先执行标准的BST插入操作并将结点6设置为红色，但6号结点的父结点为2的颜色为黑色，所以什么都不用做。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/FFF3BBAF8F0B30424F3814FD5140AAFD.png" alt="FFF3BBAF8F0B30424F3814FD5140AAFD" /></p>
<p>第三步：插入结点9，执行BST插入，设置为红色。其父结点6颜色为红色，且叔叔结点null为黑色，属于RR的情况。故对其爷爷结点2进行左旋操作，并交换g和p的颜色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6DCE43C564A5DBFA78A84821E59145F3.png" alt="6DCE43C564A5DBFA78A84821E59145F3" /></p>
<p>第四步：插入结点 10，执行标准的 BST 插入操作并设置为红色，其父结点 9为红色，其叔叔结点 2为红色结点，将其父结点 9 和叔叔结点涂黑色，并将其爷爷结点涂红色并设置为新的x，判断其爷爷结点 6 ，发现为根结点，重新更新为黑色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DC12CBBFC81E33FF8814DEE4890FE4EC.png" alt="DC12CBBFC81E33FF8814DEE4890FE4EC" /></p>
<p>第五步：插入结点 12，执行标准的BST插入操作并设置为红色，其父结点 10为红色，其叔叔结点为黑色，其爷爷结点 9为红色，RR 的情况，则左旋 g ，交换 g 和 p 的颜色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/9892751E774B518DC8EB24FFEFA0FCBE.png" alt="9892751E774B518DC8EB24FFEFA0FCBE" /></p>
<p>第六步：插入结点15</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/04CF406253747A5DED5FFA8350F8EB80.png" alt="04CF406253747A5DED5FFA8350F8EB80" /></p>
<p>第七步：插入20</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DF87DC3B353A0EB16ED35469A53B5691.png" alt="DF87DC3B353A0EB16ED35469A53B5691" /></p>
<p>第八步：插入18</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/D3BA3649C4614D0FD53D27C995DE9510.png" alt="D3BA3649C4614D0FD53D27C995DE9510" /></p>
<p>第九步：插入结点1</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DAE605F639F0FD305D27E68875182679.png" alt="DAE605F639F0FD305D27E68875182679" /></p>
<p>第十步：插入结点5</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/804C82BD07329C8927A6D38D29D6E232.png" alt="804C82BD07329C8927A6D38D29D6E232" /></p>
<p>第十一步：插入结点13</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/089C087A20E2B5F5A989ADCA450A1482.png" alt="089C087A20E2B5F5A989ADCA450A1482" /></p>
<h2>红黑树的删除</h2>
<p>说起红黑树的删除操作，就不得不提我们讲的红黑树的插入。与红黑树的插入操作类似，红黑树的删除也是重新着色和旋转来保证每一次删除操作后依旧满足红黑树的属性。</p>
<p>在插入操作中，通过判断插入结点 x 的叔叔结点 u 的颜色来确定恰当的平衡操作。而删除操作中，是通过检查兄弟结点的颜色来决定恰当的平衡操作。 红黑树中插入一个结点最容易出现两个连续的红色结点，违背红黑树的性质3（红黑树中不存在两个相邻的红色结点）。而删除操作，最容易造成子树黑高（Black Height）的变化（删除黑色结点可能导致根结点到叶结点黑色结点的数目减少，即黑高降低）。 与插入操作相比，红黑树的删除操作相对复杂一点，但多点儿耐心，还是没有问题的。为了理解删除操作，我们先来看一个 双黑（Double Black） 的概念。</p>
<p>当删除结点 v 是黑色结点，且其被其黑色子节点替换时，其子结点就被标记为 双黑。</p>
<p>所以说，删除操作最主要的任务就是将转化为双黑结点转换为我们普通的黑色结点。</p>
<h3>删除操作总纲</h3>
<p>删除操作总体上分为三步，我们先提高挈领地看一下，有个宏观概念，然后步步为营，攻陷删除。</p>
<p>首先我们假定要删除的结点为 v ，u 是用来替换 v 的孩子结点（注意，当 v 是叶结点时， u 是 NULL结点，且NULL结点我们还是当做黑色结点处理）。</p>
<p>删除操作总纲：</p>
<p>1、执行标准的BST的删除操作</p>
<p>2、简单情况：u或者v时红色</p>
<p>3、复杂情况：u和v都是黑色</p>
<p>1）u是双黑结点</p>
<p>2）当前结点u是双黑结点且不是根结点</p>
<p>a）u的兄弟结点s是黑色且s的孩子结点至少有一个是红色（LL、LR、RR、RL）</p>
<p>b）u的兄弟结点s是黑色且它的两个孩子都是黑色</p>
<p>c）u的兄弟结点s是红色（s是其父结点p的左孩子、s是其父结点的右孩子）</p>
<p>3）当前结点u是双黑结点且是根结点</p>
<h4>1、执行标准的BST删除操作</h4>
<p>在标准的 BST 删除操作中，我们最终都会以删除一个叶子结点或者只有一个孩子的结点而结束（对于内部节点，就是要删除结点左右孩子都存在的情况，最终都会退化到删除结点是叶子结点或者是只有一个孩子的情况）。所以我们仅需要处理被删除结点是叶结点或者仅有一个孩子的情况。</p>
<h4>2、简单情况：u或者v是红色</h4>
<p>如果 u 或者 v 是红色，我们将替换结点 v 的结点 u 标记为黑色结点（这样黑高就不会变化）。注意这里是 u 或者 v 是红色结点，因为在一棵红黑树中，是不允许有两个相邻的红色结点的，而结点 v 是结点 u 的父结点，因此只能是 u 或者 v 是红色结点。</p>
<p>删除结点 v 为黑色结点 10 ，替换结点 v 的结点 u 为红色结点 9 的情况：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/E998BC18E2221335D655D54A880860C3.png" alt="E998BC18E2221335D655D54A880860C3" /></p>
<p>删除结点v为红色结点20，替换结点v的结点u为黑色NULL结点的情况：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/82C031BACEA211F8CF24F745921F8BF7.png" alt="82C031BACEA211F8CF24F745921F8BF7" /></p>
<h4>3、复杂情况 u和v都是黑色结点</h4>
<h5>3.1 结点u是双黑结点</h5>
<p>当要删除结点 v 和孩子结点 u 都是黑色结点，删除结点 v ，导致结点 u 变为双黑结点。当 u 变成双黑结点时，我们的主要任务将变成将该双黑结点 u 变成普通的单黑结点。一定要特别注意，我们在上篇就提到的，NULL结点为黑色结点 ， 所以删除黑色的叶子结点就会产生一个双黑结点。</p>
<h5>3.2 当前结点u是双黑结点且不是根结点</h5>
<p>当前结点 u 是双黑结点且不是根结点，又包含三种情况进行处理。我们约定结点 u 的兄弟结点为 s .</p>
<h6>u的兄弟结点s是黑色且s的孩子结点至少有一个是红色</h6>
<p>对于这种情况，需要对 u 的兄弟结点 s 进行旋转操作，我们将 s 的一个红色子结点用 r 表示，u 和 s 的父结点用 p 表示，那么结点 p 、s 和 r 的位置将出现以下四种情况（LL、LR、RR、RL）。</p>
<p>LL（s 是 p 的左孩子，r 是 s 的左孩子，或者 s 的两个孩子都是红色结点）：</p>
<p>我们删除下图中的结点 25 为例进行说明：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/47F45A75A7A4C77EC8F93844551F637A.png" alt="47F45A75A7A4C77EC8F93844551F637A" /></p>
<p>删除结点 25 ，用结点 25 的NULL结点 替换结点 25 ，产生一个双黑结点 u ，双黑结点 u 的兄弟结点 s 为 15 ，结点 s 是其父结点 20（p） 的左孩子，其左孩子 10（r） 正好是红色结点。即为 LL 情况。</p>
<p>s 的左孩子 r 颜色设置为 s 的颜色，s 的颜色设置为父结点 p 的颜色，然后右旋p结点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/B365E1DCE80C79C724740016C9198FD7.png" alt="B365E1DCE80C79C724740016C9198FD7" /></p>
<p>LR（s是p的左孩子，r是s的右孩子，或者s的两个孩子都是红色）</p>
<p>删除结点25，不过结点25的兄弟结点15只有一个右孩子18</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/ED0BD044441090991C09B4870B1A6AB4.png" alt="ED0BD044441090991C09B4870B1A6AB4" /></p>
<p>将结点r的颜色设置为p的颜色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/77109905F56AF51695852D06CAEF95A8.png" alt="77109905F56AF51695852D06CAEF95A8" /></p>
<p>左旋结点15（s）</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/4529DFE05EF3F73472843C26F6DDF7C5.png" alt="4529DFE05EF3F73472843C26F6DDF7C5" /></p>
<p>右旋结点20（p），p的颜色设置为黑色，双黑变单黑</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6FE0B534D1B200D6C123DCC8056BC1E5.png" alt="6FE0B534D1B200D6C123DCC8056BC1E5" /></p>
<p>RR（s 是 p 的右孩子，r 是 s 的右孩子，或者 s 的两个孩子都是红色结点）： 删除结点 2 ，用结点 2 的NULL结点 a 替换结点 2 ，产生一个双黑结点 u ，双黑结点 u 的兄弟结点 s 为 15 ，结点 s 是其父结点 6（p） 的右孩子，其右孩子 18（r） 正好是红色结点。即为 RR 情况（仔细观察其实和 LL 情况是对称的）。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DDFF3AB3A5E287C3DF08F8A3882B1645.png" alt="DDFF3AB3A5E287C3DF08F8A3882B1645" /></p>
<p>r的颜色变为s的颜色，s的颜色变为p的颜色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/4D5A9C4C0E5177DADEDD6ACE4F419D16.png" alt="4D5A9C4C0E5177DADEDD6ACE4F419D16" /></p>
<p>左旋p，p的颜色设置为黑色，双黑变单黑</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/6B9B2BEA5A26F17C1C7B179C288C11EA.png" alt="6B9B2BEA5A26F17C1C7B179C288C11EA" /></p>
<p>RL情况（s 是 p 的右孩子，r 是 s 的左孩子，或者 s 的两个孩子都是红色结点）： 该情况与 LR情况是对称的</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/090DF18F173F907F0A93EE7BE9AAB1B5.png" alt="090DF18F173F907F0A93EE7BE9AAB1B5" /></p>
<p>结点r的颜色变为p的颜色</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/971DF875FBA49689BB16F34575809EB8.png" alt="971DF875FBA49689BB16F34575809EB8" /></p>
<p>右旋结点15</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/5EA6A746A5BE1B2DDDCF0EEDC08402CD.png" alt="5EA6A746A5BE1B2DDDCF0EEDC08402CD" /></p>
<p>左旋结点6（p），p的颜色设置为黑色，双黑变单黑</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DC92A4314FC4883F40A9B5BCA154997B.png" alt="DC92A4314FC4883F40A9B5BCA154997B" /></p>
<h6>u 的兄弟结点 s 是黑色且 s 的两个孩子结点都是黑色</h6>
<p>对于这种情况需要递归地进行处理，如果删除结点后得到的双黑结点的父结点此时为黑色，则结点 u 变单黑，且结点 u 的父结点 p 变双黑，然后对结点 u 的父结点 p 继续进行处理，直到当前处理的双黑结点的父结点为红色结点，此时将双黑结点的父结点设置为黑色，双黑结点变为单黑结点（红色 + 双黑 = 单黑）。</p>
<p>假设以 10 为根结点的子树为整棵树的左子树，删除结点 9 ，产生双黑结点 c 且其兄弟结点 12（s） 为黑色，兄弟结点的左右孩子均为黑色。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/A7F83400044B0B8EEF2E1A8C0B2FA1DC.png" alt="A7F83400044B0B8EEF2E1A8C0B2FA1DC" /></p>
<p>此时双黑结点的兄弟结点 12 变为红色结点，然后将 u 的父结点 10 变为双黑结点，一直向上判断。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/2BE847F90AF58CEBCF33B268983BA852.png" alt="2BE847F90AF58CEBCF33B268983BA852" /></p>
<p>那么这个过程什么时候结束呢？</p>
<p>如下图，删除结点12，得到一个双黑结点u，双黑结点的兄弟结点31及兄弟结点的孩子结点均为黑色，且双黑结点的父结点19为红色结点，刚好是不再继续向上判断的情况：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/191961C3F6241419FD3C17C13D9058CA.png" alt="191961C3F6241419FD3C17C13D9058CA" /></p>
<p>此时只需要将结点 u 的兄弟结点 31 的颜色变为红色，双黑结点 u 的父结点 19 由红色变为黑色结点，双黑结点 u 变为单黑结点。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/101EC1DCE86A281B1A21810F8331E517.png" alt="101EC1DCE86A281B1A21810F8331E517" /></p>
<h6>u的兄弟结点s是红色结点</h6>
<p>当前 u 的兄弟结点 s 是红色结点时，通过旋转操作将 u 当前的兄弟结点向上移动，并对 u 的父结点和其旋转前的兄弟结点重新着色，接着继续对结点 u 旋转后的兄弟结点 s 进行判断，确定相应的平衡操作。旋转操作将 u 的兄弟结点情况又会转换为前面刚提到的3.2（a）和（b）的情况。根据兄弟结点 s 是父结点 p 的左右孩子又分为两种情况。</p>
<p>情况一：u 的兄弟结点 s 是父结点 p 的左孩子 ,对结点 p 进行右旋操作。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/F060EB4BDB2288074296B591D15C8C40.png" alt="F060EB4BDB2288074296B591D15C8C40" /></p>
<p>删除结点 18 ，产生一个双黑结点 u ，且 u 的兄弟结点 s 是红色，兄弟结点 s 是其父结点的左孩子，接着就是对其父结点 15 进行右旋操作。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/DB4E292F57206B059158F41EC6222152.png" alt="DB4E292F57206B059158F41EC6222152" /></p>
<p>对结点 15 进行右旋操作，并且对旋转前的 p 和 s 进行重新着色后，继续对双黑结点旋转后的兄弟结点进行判断，发现此时正好和 3.2（b）的情况是一样，进行相应处理，如下图所示。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/15FB505213F4A542203AAB3B72D660DD.png" alt="15FB505213F4A542203AAB3B72D660DD" /></p>
<p>情况二：u 的兄弟结点 s 是父结点 p 的左孩子 ,对结点 p 进行左旋操作（这种情况与上面的是对称的）。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20210820222516333.png" alt="image-20210820222516333" /></p>
<p>删除结点 6 ，产生一个双黑结点 u ，且 u 的兄弟结点 10（s） 为红色，s 是父结点 p 的右孩子，左旋P</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/AB814B4FD3EC65616B544B91175BE54D.png" alt="AB814B4FD3EC65616B544B91175BE54D" /></p>
<p>对双黑结点 u 旋转后的兄弟结点继续判断：</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/BC3CB355D3B57DB51009CCA721C8E472.png" alt="BC3CB355D3B57DB51009CCA721C8E472" /></p>
<p>3.3 当前结点u是双黑结点且是根结点</p>
<p>当前结点 u 是双黑结点且是根结点时，直接将双黑结点变为单黑结点，整颗红黑树的黑高减 1.</p>
<h2>红黑树与AVL树的比较</h2>
<p>红黑树中的每个结点需要一个存储位表示结点的颜色，可以是红或黑。通过对任何一条从根到叶子的路径上各个结点着色方式的限制，红黑树确保对于每一个结点其到叶子结点的最长路径不会超过最短路径的两倍，因此，红黑树是一种弱平衡二叉树（由于是弱平衡，可以看到，在相同结点的情况下，AVL树的高度&lt;=红黑树），相对于要求严格的AVL树来说，它的旋转次数少，所以对于插入，删除操作较多的情况下，使用红黑树。</p>
<blockquote>
<p>来自一年后的补充:<br />
红黑树的逻辑操作，第一次啃需要很长时间，啃下来之后长时间不复习，也很容易忘记，真正熟练的理解红黑树的详细逻辑是有一定困难的，而有时候不需要在这些地方上浪费太多的时间。（几乎忘得一干二净的我如此补充到</p>
</blockquote>
<p><a rel="nofollow" href="https://www.crazyfay.com/2021/10/10/%e4%bb%8ebst%e3%80%81avl%e6%a0%91%e3%80%812-3%e6%a0%91%e6%9d%80%e5%88%b0blt%e7%ba%a2%e9%bb%91%e6%a0%91%ef%bc%8c%e5%b8%b8%e8%a7%81%e6%a0%91%e7%8a%b6%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%a7%a3/">从BST、AVL树、2-3树杀到BLT(红黑树)，常见树状数据结构解读 &#8211; CFC例会2021.10.10</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2021/10/10/%e4%bb%8ebst%e3%80%81avl%e6%a0%91%e3%80%812-3%e6%a0%91%e6%9d%80%e5%88%b0blt%e7%ba%a2%e9%bb%91%e6%a0%91%ef%bc%8c%e5%b8%b8%e8%a7%81%e6%a0%91%e7%8a%b6%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%a7%a3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
