go 面试资料整理

go 语言基础
熟悉语法,撸个百十道基础面试题就差不多了。
go 语言进阶
go 中有哪些锁?
sync.Mutex 互斥锁
sync.RWMutex 读写锁
CSP 并发模型?
CSP 并发模型它并不关注发送消息的实体,而关注的是发送消息时使用的 channel
go 语言借用了 process channel 这两个概念, process 表现为 go 里面的 goroutine
是实际并发执行的实体,每个实体之间是通过 channel 来进行匿名传递消息使之解藕,
从而达到通讯来实现数据共享。
不要通过共享内存来通信,而要通过通信来实现内存共享。
1 sync.mutex 互斥锁(获取锁和解锁可以不在同一个协程,当获取到锁之后,
未解锁,此时再次获取锁将会阻塞)
2 、通过 channel 通信
3 sync.WaitGroup
GPM 模型指的是什么? goroutine 的调度时机有哪些?如果 syscall 阻塞会发生什
么?
go 中是通过 channel 通信来共享内存的。
G :指的是 Goroutine ,也就是协程, go 中的协程做了优化处理,内存占用仅几 kb
且调度灵活,切换成本低。
P :指的是 processor, 也就是处理器,感觉也可理解为协程调度器。
M :指的是 thread ,内核线程。
调度器的设计策略:
1 、线程复用:当本线程无可运行的 G 时, M-P-G0 会处于自旋状态,尝试从全局队列
获取 G ,再从其他线程绑定的 P 队列中偷取 G ,而不是销毁线程;当本线程因为 G 进行
系统调用阻塞时,线程会释放绑定的 P 队列,如果有空闲的线程可用就复用空闲的
线程,不然就创建一个新的线程来接管释放出来的 P 队列。
2 、利用并行: GOMAXPROCS 设置 P 的数量,最多有这么多个线程分布在多个 cpu
同时运行。
3 、抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms ,防止其他 goroutine 被饿死。
go func 的流程:
1 、创建一个 G ,新建的 G 优先保存在 P 的本地队列中,如果满了则会保存到全局队列中。
2 G 只能运行在 M 中,一个 M 必须持有一个 P M P 1:1 关系, M 会从 P 的本地队
弹出一个可执行状态的 G 来执行。
3 、一个 M 调度 G 执行的过程是一个循环机制。
4 、如果 G 阻塞,则 M 也会被阻塞, runtime 会把这个线程 M P 摘除,再创建或者
复用其他线程来接管 P 队列。
5 、当 G M 不在被阻塞,即系统调用结束,会先尝试找会之前的 P 队列,如果之前
P 队列已经被其他线程接管,那么这个 G 会尝试获取一个空闲的 P 队列执行,并放
入到这个 P 的本地队列。否则这个线程 M 会变成休眠状态,加入空闲线程队列,而 G
则会被放入全局队列中。
M0
M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,
不需要在 heap 上分配, M0 负责执行初始化操作和启动第一个 G ,之后 M0 与其他的 M
样。
G0
G0 是每次启动一个 M 都会第一个创建的 goroutine G0 仅负责调度,不指向任何可执行
函数,每个 M 都会有一个自己的 G0 ,在调度或者系统调用时会使用 G0 的栈空间,全局变
G0 M0 的。
N:1----- 出现阻塞的瓶颈,无法利用多个 cpu
1:1----- 跟多线程 / 多进程模型无异,切换协程代价昂贵
M:N----- 能够利用多核,过于依赖协程调度器的优化和算法
同步协作式调度
异步抢占式调度
channel 底层的数据结构是什么?发送和接收元素的本质是什么?
type hchan struct {
qcount uint // *chan 里元素数量
dataqsiz uint // * 底层循环数组的长度,就是 chan 的容量
buf unsafe.Pointer // * 指向大小为 dataqsiz 的数组,有缓冲的 channe
l
elemsize uint16 // chan 中的元素大小
closed uint32 // chan 是否被关闭的标志
elemtype *_type // chan 中元素类型
recvx uint // * 当前可以接收的元素在底层数组索引 (<-chan)
sendx uint // * 当前可以发送的元素在底层数组索引 (chan<-)
recvq waitq // 等待接收的协程队列 (<-chan) sendq waitq // 等待发送的协程队列 (chan<-)
lock mutex // 互斥锁 , 保证每个读 chan 或者写 chan 的操作都是
原子的
}
// waitq sudog 的一个双向链表, sudog 实际上是对 goroutine 的一个封装。
type waitq struct {
first *sudog
last *sudog
}
// channel 的发送和接收操作本质上都是 " 值的拷贝 "( 只是拷贝它的值而已 )
channel 使用应该注意哪些情况,在哪些情况下会死锁 / 阻塞?
1 、一个无缓冲 channel 在一个主 go 程里同时进行读和写;
2 、无缓冲 channel go 程开启之前使用通道;
3 、通道 1 中调用了通道 2 ,通道 2 中调用了通道 1
4 、读取空的 channel
5 、超过 channel 缓存继续写入数据;
6 、向已经关闭的 channel 中写入数据不会导致死锁,但会 Panic 异常。
7 close 一个已经关闭的 channel Panic 异常。
那些类型不能作 map 的为 key map key 为什么是无序的?
map key 必须可以比较, func map slice 这三种类型不可比较,
只有在都是 nil 的情况下,才可与 nil (== or !=) 。因此这三种类型
不能作为 map key
数组或者结构体能够作为 key ????有些能,有些不能,要看字段或者元素是否可比较
1 map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key 可能分散, ke
y 的位置发生了变化。
2 go 中遍历 map 时,并不是固定从 0 bucket 开始遍历,每次都是从一个随机值序号
bucket 开始遍历,
并且是从这个 bucket 的一个随机序号的 cell 开始遍历。
3 、哈希查找表用一个哈希函数将 key 分配到不同的 bucket( 数组的下标 index) 。不同
的哈希函数实现也
会导致 map 无序。
" 迭代 map 的结果是无序的 " 这个特性是从 go1.0 开始加入的。
如何解决哈希查找表存在的 碰撞 问题( hash 冲突)?
hash 碰撞指的是:两个不同的原始值被哈希之后的结果相同,也就是不同的 key 被哈希
分配到了同一个 bucket 链表法:将一个 bucket 实现成一个链表,落在同一个 bucket 中的 key 都会插入这个链
表。
开放地址法:碰撞发生后,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始
处探测,直到找到一个
空位置存储这个 key ,当找不到位置的情况下会触发扩容。
map 是线程安全的么?
map 不是线程安全的, sync.map 是线程安全的。
在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志 " 置位 " 等于 1 ,则
直接 panic,
因为这表示有其他协程同时在进行写操作。赋值和删除函数在检测完写标志是 " 复位 " 之后,
先将
写标志位 " 置位 " ,才会进行之后的操作。
思考:为什么 sync.map 为啥是线程安全??
map 的底层实现原理是什么?
type hmap struct {
count int // len(map) 元素个数
flags uint8 // 写标志位
B uint8 // buckets 数组的长度的对数, buckets 数组的长度是 2^B
noverflow uint16
hash0 uint32
buckets unsafe.Pointer // 指向 buckets 数组
oldbuckets unsafe.Pointer // 扩容的时候, buckets 长度会是 oldbuckets
的两倍
nevacuate uintptr
extra *mapextra
}
// 编译期间动态创建的 bmap
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
go map 是数组存储的,采用的是哈希查找表,通过哈希函数将 key 分配到不同的 b
ucket
每个数组下标处存储的是一个 bucket ,每个 bucket 中可以存储 8 kv 键值对,当每个
bucket 存储的 kv 对到达 8 个之后,会通过 overflow 指针指向一个新的 bucket ,从而形成一个
链表。
map 的扩容过程是怎样的?
相同容量扩容
2 倍容量扩容
扩容时机 :
1 、当装载因子超过 6.5 时,表明很多桶都快满了,查找和插入效率都变低了,触发扩容。
扩容策略:元素太多, bucket 数量少,则将 B 1 buctet 最大数量 (2^B) 直接变为
原来 bucket 数量的 2 倍,再渐进式的把 key/value 迁移到新的内存地址。
2 、无法触发条件 1 overflow bucket 数量太多,查找、插入效率低,触发扩容。
( 可以理解为:一座空城,房子很多,但是住户很少,都分散了,找起人来很困难 )
扩容策略:开辟一个新的 bucket 空间,将老 bucket 中的元素移动到新 bucket ,使得
同一个 bucket 中的 key 排列更紧密,节省空间,提高 bucket 利用率。
map key 的定位过程是怎样的?
key 计算 hash 值,计算它落到那个桶时,只会用到最后 B bit 位,再用哈希值的高
8
找到 key bucket 中的位置。桶内没有 key 会找第一个空位放入,冲突则从前往后找到
第一个空位。
iface eface 的区别是什么?值接收者和指针接收者的区别?
iface eface 都是 Go 中描述接口的底层结构体,区别在于 iface 包含方法。
eface 则是不包含任何方法的空接口: interface{}
注意:编译器会为所有接收者为 T 的方法生成接收者为 *T 的包装方法,但是链接器会把
程序中确定不会用到的方法都裁剪掉。因此 *T T 不能定义同名方法。
生成包装方法是为了接口,因为接口不能直接使用接收者为值类型的方法。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,
不影响调用
者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
如果类型具备 " 原始的本质 " ,如 go 中内置的原始类型,就定义值接收者就好。
如果类型具备 " 非原始的本质 " ,不能被安全的复制,这种类型总是应该被共享,则可定义
为指针接收者。
context 是什么?如何被取消?有什么作用? type Context interface {
// context 被取消或者到了 deadline ,返回一个被关闭的 channel
Done() <-chan struct{}
// channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间 ( deadline)
Deadline() (deadline time.Time,ok boll)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
context goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
实现了 canceler 接口的 Context ,就表明是可取消的。
context 用来解决 goroutine 之间退出通知、元数据传递的功能。比如并发控制和超时
控制。
注意事项:
1 、不要将 Context 塞到结构体里,直接将 Context 类型作为函数的第一参数,而且一
般都
命名为 ctx
2 、不要向函数传入一个 nil Context ,如果你实在不知道传什么,标准库给你准备好
一个 Context todo
3 、不要把本应该作为函数参数的类型塞到 Context 中, Context 存储的应该是一些共同
的数据。例如:登陆的 session cookie
4 、同一个 Context 可能会被传递到多个 goroutine Context 是并发安全的。
slice 的底层数据结构是怎样的?
type slice struct {
array unsafe.Pointer // 底层数组的起始位置
len int
cap int
}
slice 的元素要存在一段连续的内存中,底层数据是数组, slice 是对数组的封装,它描
述一个数组的片段。
slice 可以向后扩展,不可以向前扩展。
s[i] 不可以超越 len(s), 向后扩展不可以超越底层数组 cap(s)
make 会为 slice 分配底层数组,而 new 不会为 slice 分配底层数组,所以 array 其实 位置会是 nil ,可以通过 append 来分配底层数组。
slice 扩容方案计算:
1 、预估扩容后的容量:即假设扩容后的 cap 等于扩容后元素的个数
if
oldCap * 2 < cap ,则 newCap = cap
else
oldLen < 1024, newCap = oldCap * 2
oldLen >= 1024, newCap = oldCap * 1.25
2 、预估内存大小( int 一个元素占 8 子节, string 一个元素占 16 子节)
假设元素类型是 int ,预估容量 newCap = 5 ,那么预估内存 = 5 * 8 = 40 byte
3 、匹配到合适的内存规格(内存分配规格为 8 16 32 48 64 80....
实际申请的内存为 48 byte cap = 48 / 8 = 6
你了解 GC 么?常见的 GC 实现方式有哪些?
GC 即垃圾回收机制:引用计数、三色标记法 + 混合写屏障机制
go GC 有那三个阶段?流程是什么?如果内存分配速度超过了标记清除速度怎
么办?
goV1.3 之前采用的是普通标记清除,流程如下:
1 、开始 STW ,暂停程序业务逻辑,找出不可达的对象和可达对象;
2 、给所有可达对象做上标记;
3 、标记完成之后,开始清除未标记的对象;
4 、停止 STW ,让程序继续运行,然后循环重复这个过程,直到程序生命周期结束。
goV1.5 三色标记法,流程如下:
1 、只要是新创建的对象,默认的颜色都标记为白色;
2 、每次 GC 回收开始,从根节点开始遍历所有对象,把遍历到的对象从白色集合
放入灰色集合;
3 、遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色
对象放入黑色集合;
4 、重复 3 中内容,直到灰色集合中无任何对象;
5 、回收白色集合中的所有对象。
犹如剥洋葱一样,一层一层的遍历着色,但同时满足以下条件会导致对象丢失:
条件 1 :一个白色对象被黑色对象引用;
条件 2 :灰色对象与白色对象之间的可达关系同时被解除。
强三色:强制性的不允许黑色对象引用白色对象。
弱三色:黑色对象可以引用白色对象,但白色对象存在其他灰色对象对它的引用,或者
可达它的链路上游存在灰色对象。 goV1.8 三色 + 混合写屏障机制,栈不启动屏障,流程如下:
1 GC 开始将栈上的对象全部扫描并标记为黑色 ( 之后不再进行重复扫描,无需 STW)
2 GC 期间,任何在栈上创建的新对象均标记为黑色;
3 、被删除的对象和被添加的对象均标记为灰色;
4 、回收白色集合中的所有对象。
总结:
v1.3 普通标记清除法,整体过程需要 STW ,效率极低;
v1.5 三色标记法 + 屏障,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫
一次栈 ( 需要 STW) ,效率普通;
v1.8 三色标记法 + 混合写屏障,堆空间启动,栈空间不启动屏障,整体过程几乎不需要 S
TW,
效率较高。
如果申请内存的速度超过预期,运行时就会让申请内存的应用程序辅助完成垃圾收集的扫
描阶段,
在标记和标记终止阶段结束之后就会进入异步的清理阶段,将不用的内存增量回收。并发
标记会
设置一个标志,并在 mallocgc 调用时进行检查,当存在新的内存分配时,会暂停分配内
存过快
的哪些 goroutine ,并将其转去执行一些辅助标记的工作,从而达到放缓内存分配和加速
GC 工作
的目的。
内存泄漏如何解决?
1 、通过 pprof 工具获取内存相差较大的两个时间点 heap 数据。 htop 可以查看内存增长
情况。
2 、通过 go tool pprof 比较内存情况,分析多出来的内存。
3 、分析代码、修复代码。
内存逃逸分析?
在函数中申请一个新的对象,如果分配在栈中,则函数执行结束可自动将内存回收;
如果分配在堆中,则函数执行结束可交给 GC 处理。
案例:
函数返回局部变量指针;
申请内存过大超过栈的存储能力。
你是如何实现单元测试的?有哪些框架?
testing GoMock testify mysql
sql 语句中 group by order by 谁先执行?
select (all | distinct) 字段或者表达式 (from 子句 ) (where 子句 ) (group by
子句 ) (having 子句 ) (order by 子句 ) (limit 子句 );
1 from 子句 : 构成提供给 select 的数据源 , 所以一般后面写表名
2 where 子句 :where 子句是对数据源中的数据进行过滤的条件设置
3 group by 子句 : 对通过 where 子句过滤后的数据进行分组
4 having 子句 : 对分组后的的数据进行过滤 , 作用和 where 类似
5 order by 子句 : 对取得的数据结果以某个标准 ( 字段 ) 进行排序 , 默认是 asc( 正序 )
6 limit 子句 : 对取出来的数据进行数量的限制 , 从第几行开始取出来几行数据
7 all | distinct 用于设置 select 出来的数据是否消除重复行,默认是 all 既不消
除所有的数据都出来; distinct 表示会消除重复行
mysql 的存储引擎有哪些?都有什么区别?
MyISAM InnoDB
区别:
1 MySAM 不支持事务、不支持外键,而 innodb 支持
2 、都是 b+ 树作为索引结构,但是实现方式不同 ,innoDb 是聚集索引, myisam 是非聚集
索引。
3 innoDb 不保存表的具体行数, count(*) 需要全表扫描,而 myisam 用一个变量保存
了整个
表的行数,速度更快。
4 innodb 组小的锁粒度是行锁, myisam 最小的锁粒度是表锁。 myisam 一个更新语句会
锁住
整张表,导致其他查询、更新都会被阻塞,因此并发当问受限。
为什么需要 B- /B+ 树?
因为传统的树是在内存中进行的数据搜索,而当数据量非常大时,内存不够用,大部分数
据只能存放在
磁盘上,只有需要的数据才加载到内存中。一般情况下内存访问的时间约为 50ns, 而磁盘
10ms 左右,
大部分时间会阻塞在磁盘 IO 上,因此要提高性能,得减少磁盘 IO 次数。
mysql 索引底层实现?
MyISAM 引擎使用 B+ 树作为索引结构,叶子节点的 data 域存放的是数据记录的地址,需
要再寻址一次才能
得到数据,是 " 非聚集索引 "
InnoDB 引擎也使用 B+ 树作为索引结构,区别如下:
1 InnoDB 的叶子节点 data 域保存了完整的数据记录,因此主键索引很高效,是 " 聚集
索引 " 2 InnoDB 非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询做到覆
盖索引
会很高效。
hash 索引底层的数据结构是哈希表,在绝大多数需求为单条记录查询的时候,可以选择
哈希索引,
查询性能最快;其余大部分场景,建议都选择 BTree 索引。
为什么 MongoDB 使用 B- 树,而 mysql 使用 B+ 树?
Mg 是类似 json 格式的数据模型,对性能要求高,属于聚合型数据库,而 B- 树恰好 key
data 域聚合
在一起,且最佳查询时间复杂度为 O(1);
mysql 是一种关系型数据库, B+ 树的特性可以增加区间访问性,而 B- 树并不支持; B+
的查询时间复杂
度始终为 O(log n) ,查询效率更加稳定。
B- 树和 B+ 树的区别?
1 B+ 树非叶子节点只存储 key 的副本,相当于叶子节点的索引,真实的 key data
都在叶子节点存储,查询时间复杂度固定为 O(log n) ,而 B- 树节点内部每个 key 都带着
data 域,
查询时间复杂度不固定,与 key 在树中的位置有关,最好为 O(1)
2 B+ 树叶子节点带有顺序指针,两两相连大大增加区间访问性,利用磁盘预读提前将访
问节点附近的数据读入内存,减
少了磁盘 IO 的次数,也可使用在范围查询,而 B- 树每个节点 key data 在一起,则无
法区间查找。
3 、磁盘是分 block 的,一次磁盘 IO 会读取若干个 block ,具体和操作系统有关,磁盘 I
O 数据大小是固定的,在一
IO 中,单个元素越小,量就越大。由于 B+ 树的节点只存储 key 的副本,这就意味着 B
+ 树单次磁盘 IO 的数据大于 B- 树,
自然磁盘 IO 次数也更少。
覆盖索引是什么?
从索引中就能查到记录,而不需要索引之后再回表中查记录,避免了回表的产生,减少了
树的搜索次数。
索引设计的原则?需要注意事项?
1 、出现在 where 子句中的列,或者连接子句中指定的列。
2 、基数较小的列,索引效果较差,没有必要在此列建立索引。
3 、取值离散大的字段放在联合索引的前面, count() 值越大说明字段唯一值越多,离散
程度高。 4 、尽量使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,节省索引空
间。
5 、应符合最左前缀匹配原则, mysql 会一直向右匹配直到遇到范围查询 (> < between
like)
就停止匹配,其中 (= in) 可以乱序, mysql 查询优化器会优化成索引可以识别的形式。
注意:
1 、不要过度索引,并非索引越多也好,索引需要空间来存储,也需要定期维护,并且索
引会降低
写操作的性能。
2 、非必要情况,所有列都应该指定列为 NOT NULL ,使用零值作为默认值。
什么是数据库事务?
事务是一个不可分割的数据操作序列,也是数据库并发控制的基本单位,其执行结果必须
使数据库
从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么
都不执行。
事务的四大特性 (ACID)?
1 、原子性:事务是最小的执行单位,包含的所有数据库操作要么全部成功,要么全部失
败回滚;
2 、一致性:执行事务前后数据都必须处于一致性状态;
3 、隔离性:一个事务未提交的业务结果是否对于其它事务可见,对应有四种事务隔离级
别。
4 、持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即使是
在数据库
系统遇到故障的情况下也不会丢失提交事务的操作。
事务隔离级别有哪些?如何解决脏读和幻读?
数据库定义了四种不同的事务隔离级别,由低到高依次为:
Read uncommitted 读取未提交 ----- 三者均可能
Read committed 读取已提交 ------ 解决了脏读,可能出现不可重复读和幻读
Repeatable read 可重复读 ------ 可能出现幻读,解决了脏读和不可重复读
Serializable 可串行化(对所涉及到的表加锁,并非行级锁)
脏读 : 读取未提交数据 (A 事务读到未提交的 B 事务数据 )
解决:设置事务级别为 Read committed
不可重复读 : 前后多次读取,数据内容不一致 ( 原因:读取了其它事务更改的数据且提交了
事务,针对 update 操作 )
解决:使用行级锁,锁定该行,事务 A 多次读取操作完成后才释放该锁,才允许其它事务
操作。
幻读:前后多次读取,数据总量不一致 ( 原因:读取了其它事务新增的数据且提交了事务, 针对 insert delete 操作 )
解决:使用表级锁,锁定整张表,事务 A 多次读取数据总量之后才释放该锁,才允许其它
事务操作。
mysql 的事务隔离级别默认为 Repeatable read( 可重复读,可能会出现幻读 )
乐观锁和悲观锁的实现方式?
乐观锁:基于数据版本号实现 ( 基于 mvcc 理论 )
悲观锁:数据库的锁机制
innoDB 存储引擎的锁的算法有那三种?
Record lock: 单个行记录上的锁
Gap lock: 间隙锁,锁定一个范围,不包括记录本身
Next-key lock: record+gap 锁定一个范围,包含记录本身
注意: ( 间隙:范围查询中,存在于范围内,但不存在的记录,这称为间隙 )
mysql mvcc 实现原理是什么?
MVCC 只适用 mysql 事务隔离级别: Read committed Repeatable Read MVCC
版本是在事务提交之后才会产生。
多版本并发控制: Undo log 实现 MVCC ,并发控制通过锁来实现。
简单来说就是在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号,而
每一个事务在启动的时候,都有一个唯一的递增的版本号,通过版本号来减少锁的争用。
1 、在插入操作时,记录的创建版本号就是事务版本号;
2 、在更新操作时,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本
号,然后插入一行新的记录;
3 、删除操作的时候,就把事务版本号作为删除版本号;
4 、查询时,符合删除版本号大于当前事务版本号并且创建版本号小于或者等于当前事务
版本号
这两个条件的记录才能被事务查询出来。
bin log redo log undo log 作用是什么?有什么区别?
bin log:
记录 mysql 服务层产生的日志,常用来进行数据恢复、数据库复制。
redo log:
记录了数据操作在物理层面的修改。 mysql 中大量使用缓存,缓存存在与内存中,
修改操作时会直接修改内存,而不是立刻修改磁盘,当内存和磁盘数据不一致时,称内存
的数据为脏页。
为了保证数据的安全性,事务进行时会不断产生 redo log ,在事务提交时进行一次 flus h 操作,
保存到磁盘中, redo log 是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写。
当数据
库或主机失效重启时,会根据 redo log 进行数据的恢复,如果 redo log 中有事务提交,
则进行
事务提交修改数据。这样实现了事务的原子性、一致性、持久性。
undo log:
当进行数据修改时除了记录 redo log ,还会记录 undo log ,它记录了修改的反向操作,
用于
数据的撤回操作,可以实现事务回滚, mvcc 就是根据 undo log 实现回溯到某个特定的
版本的
数据的。
大表数据查询,如何优化?
1 、优化 sql 语句 + 索引
2 、增加缓存, memcached redis
3 、做主从复制,读写分离
4 、拆表 --- 垂直拆分、水平拆分
Mysql 数据库 cpu 飙升如何排查?
1 、首先 top 查看是否真是由于 mysqld 占用导致的。
2 show processlist ,分析 session 情况,有没有激增,是不是有消耗资源的 sql
运行。
3 、找出消耗高的 sql explain 查看执行计划。
主从复制的实现步骤?
1 、主库 db 的操作事件被写入到 binlog
2 、从库发起连接,连接到主库
3 、此时主库创建一个 binlog dump thread 线程,把 binlog 的内容发送到从库
4 、从库启动之后,创建一个 I/O 线程,读取主库传过来的 binlog 内容并写入到 relay
log
5 、还会创建一个 SQL 线程,从 relay log 里面读取内容,从 Exec_Master_Log_Pos
开始执行读取到的操作事件,将内容写入到 slave db
kafka
kafka 为什么性能高?
1 kafka 本身是分布式集群,同时采用了分区技术,具有较高的并发度;
2 、顺序写入磁盘, Kafka producer 生产数据,要写入到 log 文件中,写的过程是
一直追加到文件末端,为顺序写。
3 、零拷贝技术 kafka 重复消费可能的原因以及处理方式?
原因 1 :消费者宕机、重启等,导致消息已经消费但是没有提交 offset
原因 2 :消费者使用自动提交 offset ,但当还没有提交的时候,有新的消费者加入或者
移除,发生了 rebalance
再次消费的时候,消费者会根据提交的偏移量来,于是重复消费了数据。
原因 3 :消息处理耗时,或者消费者拉取的消息量太多,处理耗时,超过了 max.poll.in
terval.ms 的配置时间,
导致认为当前消费者已经死掉,触发再均衡。
解决方案:消费者实现消费幂
1 、消费表
2 、数据库唯一索引
3 、缓存消费过的消息 ID
触发重平衡 (rebalanced) 的情况?
1 、有新的消费者加入消费组、或已有消费者主动离开组
2 、消费者超过 session 时间未发送心跳(已有 consumer 崩溃了)
3 、一次 poll() 之后的消息处理时间超过了 max.poll.interval.ms 的配置时间,因为
一次 poll()
处理完才会触发下次 poll() (已有 consumer 崩溃了)
4 、订阅主题数发生变更
5 、订阅主题的分区数发生变更
kafka 消息丢失的原因以及解决方式?
生产者丢失消息情况:可能因为网络问题并没有发送出去。
解决:可以给 send 方法添加回调函数 , 按一定次数、间隔重试。
消费者丢失消息情况:消费者自动提交 offset ,拿到消息还未真正消费,就挂掉了,但
offset 却被自动提交了。
解决:关闭自动提交 offset ,每次在真正消费完消息之后之后再自己手动提交 offset
这样解决了消息丢失,但会带来重复消费问题。
kafka 丢失消息情况:
leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 lead
er ,但是 leader 的数据还有一些
没有被 follower 副本的同步的话,就会造成消息丢失。
解决:设置 ack = all ack Kafka 生产者很重要的一个参数。代表则所有副本都要
接收到该消息之后该消息才算真正成功被发送。
Kafka 中的消息有序吗?
kafka 无法保证整个 topic 多个分区有序,但是由于每个分区( partition )内,
每条消息都有一个 offset ,故可以保证分区内有序 topic 的分区数可以增加或减少吗?为什么?
topic 的分区数只能增加不能减少,因为减少掉的分区也就是被删除的分区的数据难以处
.
注意:消费者组中的消费者个数如果超过 topic 的分区,那么就会有消费者消费不到数
.
kafka 是怎么维护 offset 的?
维护 offset 的原因:
由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故
障前的位置
的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset ,以便故障恢复后
继续消费。
维护 offset 的方式:
Kafka 0.9 版本之前, consumer 默认将 offset 保存在 Zookeeper 中,从 0.9 版本开
始,
consumer 默认将 offset 保存在 Kafka 一个内置的 topic 中,该 topic **__consum
er_offsets**
关于 offset 的常识:
消费者提交消费位移时提交的是当前消费到的最新消息的 offset+1 而不是 offset
kafka 集群消息积压问题如何处理?
从两个角度去分析:
1 、如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时提升消费
组的消费者数量,消费者数 = 分区数。(两者缺一不可)
2 、如果是下游的数据处理不及时,提高每批次拉取的数量。如果是因为批次拉取数据过
(拉取 数据 / 处理时间 < 生产速度),也会使处理的数据小于生产的数据,造成数据积压。
redis
redis 单线程为什么效率也这么高?
1. redis 是基于内存的,内存的读写速度非常快 .
2. redis 是单线程的,省去了很多上下文切换线程的时间 .
3. IO 多路复用
redis 有那五种常用的数据结构?应用场景以及实现原理是什么?
ziplist: 压缩列表,此数据结构是为了节约内存而开发
intset: 整数集合,是集合键的底层实现方式之一
quicklist ziplist 的一个双向链表
skiplist :跳表 1. string 计数 sds(raw embstr int)
2. hash 缓存结构数据 quicklist(hashtable ziplist)
3. list 异步消息队列 (ziplist linkedlist)
4. set( 无序、成员唯一 ) 计算共同喜好 ( 交集 ) 、统计访问 ip (intset hasht
able)
5. zset( 有序、成员唯一 ) 排行榜、延迟队列 (ziplist skiplist)
redis 的过期策略?
定期删除 + 惰性删除策略
- 定期删除策略: Redis 启用一个定时器定时监视所有的 key ,判断 key 是否过期,过
期的话就删除。
这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历
内存中所有的数据,
非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间
key 仍然可以用。
- 惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式
存在一个缺点:
如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量
的空间。
- 这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次
扫描全部的
key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰
性删除
策略互补了为检查到的 key ,基本上满足了所有要求。但是有时候就是那么的巧,既没
有被定时器抽取到,
又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制,当内存不
够用时,内存淘汰机制就会上场。
Redis 中的批量操作 Pipeline
pipeline client 一个请求, redis server 一个响应,期间 client 阻塞。
Pipeline redis 的管道命令,允许 client 将多个请求依次发给服务器,过程中不需要
等待请求的回复,而是在最后读取所有结果。
redis mysql 数据一致性解决方案?
延迟双删策略:
1 、先删除缓存,然后更新数据库,但可能在更新未完成之前,有请求穿透到 db 取了旧数
据并写入了缓存,因此需要更新完数据库之后,延迟几十毫秒,再删一次缓存。
2 、先更新数据库,再删除缓存,再延迟删一次。
如果删除失败则重试,比如放入队列循环删除。 发生缓存穿透、击穿、雪崩的原因以及解决方案?
- 缓存穿透
是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如
果从存储层查不到数据
则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的
意义。在流量大时,
可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。 (
单来说就是缓存和数据库
都不存在这个数据,这种情况称为穿透 )
解决方案:
1 、接口层增加校验,比如 id<=0 这种一定不存在的情况直接拦截掉。
2 、如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把
这个空结果进行缓存,
但它的过期时间会很短,最长不超过五分钟。
3 、采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一
定不存在的数据会被这
bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
- 缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,
请求全部转发
DB DB 瞬时压力过重雪崩。重启导致缓存失效,也可能出现并发到 DB
解决方案:
1 、考虑用加锁 ( 互斥锁 ) ,锁定 key 之后完成 db 的查询以及缓存的更新之后再释放锁
key ,从而避免失效时大量的并发请求落到底层存储系统上。
2 、缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比
1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的
事件。
3 、重启导致缓存失效,我们可以采用缓存预热,提前使用一个接口更新好缓存,再启
动服务。
- 缓存击穿 ( 场景:热点数据 )
缓存中不存在,但数据库中有的数据 ( 一般是缓存时间到期 ) 。缓存在某个时间点过期的
时候,恰好在这个时间点对这个 Key 有大量的并发
请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时
候大并发的请求可能会瞬间把后端 DB 压垮。与雪崩区别是击穿指并发查同一条数据。
解决方案:
1 、使用互斥锁 (SETNX)
2 、设置热点数据永远不过期,数据是需要维护的。
redis 的持久化方案 RDB AOF 详解?
RDB: 在指定的时间间隔内将内存中的数据集快照写入磁盘,它恢复时就是将快照文件直接
读到内存里。
AOF: 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命 令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,
新命令会被追加到文件的末尾 ,Redis 还可以在后台对 AOF 文件进行重写 (rewrite)
使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小
网络协议
浏览器访问一个网站,都经历了怎样一个流程?
1 DNS 解析 : 将域名解析成 IP 地址
2 TCP 连接: TCP 三次握手
3 、发送 HTTP 请求
4 、服务器处理请求并返回 HTTP 报文
5 、浏览器解析渲染页面
6 、断开连接: TCP 四次挥手
什么是 HTTP HTTPS 有什么区别?
1 HTTP URL http:// 开头,而 HTTPS URL https:// 开头
2 HTTP 是不安全的,而 HTTPS 是安全的
3 HTTP 标准端口是 80 ,而 HTTPS 的标准端口是 443
4 、在 OSI 网络模型中, HTTP 工作于应用层,而 HTTPS 的安全传输机制工作在传输层
5 HTTP 无法加密,而 HTTPS 对传输的数据进行加密
6 HTTP 无需证书,而 HTTPS 需要 CA 机构颁发的 SSL 证书
什么是 Http 协议无状态协议 ? 怎么解决 Http 协议无状态协议 ?
无状态协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息。
也就是说,当客户端一次 HTTP 请求完成以后,客户端再发送一次 HTTP 请求, HTTP 并不
知道当前客户端是一个 老用户
可以使用 Cookie 来解决无状态的问题, Cookie 就相当于一个通行证,第一次访问的时
候给客户端发送一个 Cookie
当客户端再次来的时候,拿着 Cookie( 通行证 ) ,那么服务器就知道这个是 老用户
HTTP 请求报文与响应报文格式?
- 请求报文包含:
1 、请求行:包含请求方法、 URI HTTP 版本信息
2 、请求头部字段
3 、空行
4 、请求内容实体
- 响应报文包含:
1 、状态行:包含 HTTP 版本、状态码、状态码的原因短语
2 、响应头部字段
3 、空行
4 、响应内容实体
HTTP 常见的状态码有哪些? 1XX 系列:指定客户端应相应的某些动作,代表请求已被接受,需要继续处理。由于 HTT
P/1.0 协议中没有定义任何
1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
2XX 系列:代表请求已成功被服务器接收、理解、并接受。这系列中最常见的有 200 20
1 状态码。
200 状态码:表示请求已成功,请求所希望的响应头或数据体将随此响应返回
201 状态码:表示请求成功并且服务器创建了新的资源,且其 URI 已经随 Location
信息返回。假如需要的资源
无法及时建立的话,应当返回 ‘202 Accepted’
202 状态码:服务器已接受请求,但尚未处理
3XX 系列:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后
续的请求地址(重定向目标
)在本次响应的 Location 域中指明。这系列中最常见的有 301 302 状态码。
301 状态码:被请求的资源已永久移动到新位置。服务器返回此响应(对 GET HEAD
请求的响应)时,会自动
将请求者转到新位置。
302 状态码:请求的资源临时从不同的 URI 响应请求,但请求者应继续使用原有位置来进
行以后的请求
304 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
如果网页自请求者上次请
求后再也没有更改过,您应将服务器配置为返回此响应 ( 称为 If-Modified-Since HTTP
标头 )
4XX 系列:表示请求错误。代表了客户端看起来可能发生了错误,妨碍了服务器的处理。
常见有: 401 404 状态码。
401 状态码:请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 状态码:服务器已经理解请求,但是拒绝执行它。与 401 响应不同的是,身份验证并
不能提供任何帮助,而且
这个请求也不应该被重复提交。
404 状态码:请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉
用户这个状况到底是暂
时的还是永久的。假如服务器知道情况的话,应当使用 410 状态码来告知旧资源因为某些
内部的配置机制问题,已
经永久的不可用,而且没有任何可以跳转的地址。 404 这个状态码被广泛应用于当服务器
不想揭示到底为何请求被
拒绝或者没有其他适合的响应可用的情况下。
5xx 系列:代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务
器意识到以当前的软硬 件资源无法完成对请求的处理。常见有 500 503 状态码。
500 状态码:服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般
来说,这个问题都会在
服务器的程序码出错时出现。
503 状态码:由于临时的服务器维护或者过载,服务器当前无法处理请求。通常,这个是
暂时状态,一段时间会恢复
网络的七层结构及其作用?
- 应用层(数据):确定进程之间通信的性质以满足用户需要以及提供网络与用户应用
- 表示层(数据):主要解决用户信息的语法表示问题,如加密解密
- 会话层(数据):提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制,
如服务器验证用户登录便是由会话层完成的
- 传输层(段):实现网络不同主机上用户进程之间的数据通信,可靠与不可靠的传输,
传输层的错误检测,流量控制等
- 网络层(包):提供逻辑地址( IP )、选路,数据从源端到目的端的传输
- 数据链路层(帧):将上层数据封装成帧,用 MAC 地址访问媒介,错误检测与修正
- 物理层(比特流):设备之间比特流的传输,物理接口,电气特性等
TCP/IP 协议四层 ?
应用层、传输层、网络层、数据链路层
TCP 协议和 UDP 协议有什么区别?
CP UDP 协议属于传输层协议,主要区别:
1 TCP 是面向连接的, UDP 是无连接的 ;
2 TCP 是可靠的, UDP 是不可靠的 ;
3 TCP 只支持点对点通信, UDP 支持一对一、一对多、多对一、多对多的通信模式 ;
4 TCP 是面向字节流的, UDP 是面向报文的 ;
5 TCP 有拥塞控制机制 ;UDP 没有拥塞控制,适合媒体通信 ;
6 TCP 首部开销 (20 个字节 ) UDP 的首部开销 (8 个字节 ) 要大 ;
TCP 协议的三次握手和四次挥手?为什么是三次和四次?
- 三次握手 ( 我要和你建立链接,你真的要和我建立链接么,我真的要和你建立链接,成
)
第一次握手: Client 将标志位 SYN 置为 1 ,随机产生一个值 seq=J ,并将该数据包发
送给 Server Client 进入 SYN_SENT 状态,
等待 Server 确认。
第二次握手: Server 收到数据包后由标志位 SYN=1 知道 Client 请求建立连接, Serv
er 将标志位 SYN ACK 都置为 1 ack=J+1
随机产生一个值 seq=K ,并将该数据包发送给 Client 以确认连接请求, Server 进入
SYN_RCVD 状态。
第三次握手: Client 收到确认后,检查 ack 是否为 J+1 ACK 是否为 1 ,如果正确则
将标志位 ACK 置为 1 ack=K+1 ,并将该数据
包发送给 Server Server 检查 ack 是否为 K+1 ACK 是否为 1 ,如果正确则连接建立
成功, Client Server 进入 ESTABLISHED
态,完成三次握手,随后 Client Server 之间可以开始传输数据了。
- 四次挥手 ( 我要和你断开链接 ; 好的,断吧。我也要和你断开链接 ; 好的,断吧 )
第一次挥手: Client 发送一个 FIN ,用来关闭 Client Server 的数据传送, Clien
t 进入 FIN_WAIT_1 状态。
第二次挥手: Server 收到 FIN 后,发送一个 ACK Client ,确认序号为收到序号 +1
( SYN 相同,一个 FIN 占用一个序号 ) Server
CLOSE_WAIT 状态。此时 TCP 链接处于半关闭状态,即客户端已经没有要发送的数
据了,但服务端若发送数据,则客户端仍要接收。
第三次挥手: Server 发送一个 FIN ,用来关闭 Server Client 的数据传送, Serve
r 进入 LAST_ACK 状态。
第四次挥手: Client 收到 FIN 后, Client 进入 TIME_WAIT 状态,接着发送一个 ACK
Server ,确认序号为收到序号 +1 Server
CLOSED 状态,完成四次挥手。
** 三次握手 ** :目的是为了防止已失效的链接请求报文突然又传送到了服务端,因而产生
错误。
** 四次挥手 ** TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。 T
CP 是全双工模式,这就意味着,当 A
B 发出 FIN 报文段时,只是表示 A 已经没有数据要发送了,而此时 A 还是能够接
受到来自 B 发出的数据 ;B A
ACK 报文段也只是告诉 A ,它自己知道 A 没有数据要发了,但 B 还是能够向 A
发送数据。
http1.0 http1.1 http2.0 有什么区别?
http1.0 http1.1 区别:
1 http1.1 默认开启长连接 keep-alive ,在一个 TCP 连接 ( 一次完整的 tcp 握手和挥
) 上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。
2 http1.0 客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且
不支持断点续传功能。 HTTP1.1 支持只发送 header 信息(不带任何 body 信息),
如果服务器认为客户端有权限请求服务器,则返回 100 ,客户端接收到 100 才开始把请求
body 发送到服务器;如果返回 401 ,客户端就可以不用发送请求 body 了节约了带宽。
3 http1.1 的请求消息和响应消息都支持 host 域,且请求消息中如果没有 host 域会报
告一个错误( 400 Bad Request )。
http1.1 http2.0 区别 : 1 http2.0 1.1 的基础上增加了多路复用,做到同一个连接并发处理多个请求,而且
并发请求的数量比 HTTP1.1 大了好几个数量级。
http1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建 TCP 连接
本身也是有开销的。
2 http1.1 不支持 header 数据的压缩, http2.0 使用 HPACK 算法对 header 的数据进
行压缩,这样数据体积小了,在网络上传输就会更快。
3 http2.0 引入了 server push ,它允许服务端推送资源给浏览器,免得客户端再次创
建连接发送请求到服务器端获取。 ( 例如:客户端向服务器发送一个获取 html 的请求,
不需要再次请求去获取 html 所依赖的 css js ,而是在第一次 html 请求时,服务器端
主动的都推送给客户端 )
ProtoBuff 协议相比其它有什么好处?
pb 是一种轻便高效的 " 结构化数据 " 存储格式,可以用于 " 结构化数据 " 的序列化和反序列
化。
1 、跨语言,支持大多数语言开发,代码开源,运行稳定可靠。
2 、性能好、效率高,占据空间和运行时间相比 json xml 小,二进制序列化格式,数
据压缩紧凑,占据字节数小。
3 、支持向后向前兼容,比如向后兼容: A B 两模块, B 升级有 "statue" 属性,可设置为
非必填或者缺省,这样 A 就被兼容了。
4 、适合数据大、传输速率敏感的场合使用。
5 、支持数据类型多。
6 、数据结构化定义灵活,可嵌套定义。
分布式系统、微服务架构
什么是分布式事务?
二阶段提交、三阶段提交
分布式锁有哪些实现方式?分别会存在什么问题,哪种实现更好?
zk(ZooKeeper) 锁实现原理:
1 )创建一个目录 mylock
2 )线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;
3 )获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,
则说明当前线程顺序号最小,获得锁;
4 )线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
5 )线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的
节点,如果是则获得锁。
redis 事务:
1 、事务提供了一种将多个命令打包,然后一次性有序( FIFO )执行的机制
2 、事务执行过程不会被中断
3 、带 WATCH 命令的事务会将客户端和被监视的键在数据库 watched_keys 字典中进行关
联,当键被修改程序会将所有监视键的客
户端 REDIS_DIRTY_CAS 标识打开 4 、在客户端提交 EXEC 命令时,会检查 REDIS_DIRTY_CAS 标识,标识打开事务将不会被
执行
5 Redis 事务具有 ACID 的特性(当服务器运行在 AOF 模式下,并且 appendfsync 选项
值为 always 时才具有持久性
redis 实现分布式锁 :
2.6.12 版本之后命令: SET key value EX 10 NX (合并了 1 2 两个步骤)
核心思路:
1 、使用 setnx 设置互斥锁
2 、为了避免异常情况导致死锁,因此需要为锁设置过期时间
3 、为了避免删锁混乱,导致锁永久失效,需要为每个请求分配唯一的 value 值,再删锁
时,验证是否属于自身的锁。
4 、为了避免在删锁的操作过程中的异常情况,如锁过期,新的请求获得锁,此时删除的
是新的锁。可以再执行任务中新启一个协程
每隔 10s 去检查主程是否还持有锁,
如果还持有锁,则为锁进行续期。
基于 lua 脚本实现 redis 的乐观锁
说下微服务架构有哪些组件,这些组件是如何实现自身功能的?
微服务架构如何设计?
如何防止超卖?
1 、使用 redis 的队列来实现。将要促销的商品数量以队列的方式存入 redis 中,每当
用户抢到一件促销商品则从队列中删除一个数据,确保商品不会超卖
2 、乐观锁,增加版本号,查询库存和更新库存时比较版本号
数据结构与算法
题目:将 6 2 10 32 9 5 18 14 30 29 从小到大进行排列 , 使用冒泡排
package main
import "fmt"
func main() {
// 定义数组
arr := [ 10 ] int { 6 , 2 , 10 , 32 , 9 , 5 , 18 , 14 , 30 , 29 }
for i := 0 ; i < len(arr); i++ {
for j := 0 ; j < len(arr)-i -1 ; j++ {
if arr[j] > arr[j+ 1 ] { arr[j], arr[j+ 1 ] = arr[j+ 1 ], arr[j]
}
}
}
fmt.Println(arr)
}
快排
选择排序
堆排
动态规划
其它
关系型数据库和非关系型数据库有什么区别?
  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值