问题来源于技术中台golang开发实习生 滴滴
,自学习使用,答案搜索来自于网络,若有不对的地方欢迎指正。
目录
1 索引(答了聚簇索引和非聚簇索引)
2 非聚簇索引是如何查询的
非聚簇索引(非主键索引) :它将数据存储和索引分开,找到索引后,需要通过对应的地址找到对应的数据行。
- 缺点:当通过非聚簇索引来查找数据时,首先遍历找到对应的叶子节点,也就找到了聚簇索引的主键,**然后通过聚簇索引找到对应的数据。**所以它需要两次索引查找,效率相对较低,这是它的。
- 优点:插入记录时不会引起数据顺序的重组。所以建议使用非聚簇索引的场景是频繁更新的列。
3 隔离级别
事务隔离级别,就是是为了解决多个并行事务竞争。
三个问题:
- 脏读:A事务读取到了B事务未提交的数据
- 幻读/虚读:一个事务中两次读取的数据的数量不一致
- 不可重复读:一个事务中两次读取的数据的内容不一样
四大隔离级别:
读未提交(read uncommited):读取未提交的数据,级别最低,解决不了问题
读已提交(read commited):读取已提交的数据,可以解决脏读问题—Oracle默认
可重复读(repeatable read):可以解决脏读、不可重复读问题 --mysql默认的
串行化(serializable):可以解决脏读、不可重复读、和幻读 ,在这种隔离级别下,多个并行事务串行化执行,不会产生安全性问题。—相当于锁表
这四种隔离级别里面,只有串行化解决了全部的问题,但这种隔离级别的性能是最低的。
4 四种隔离级别的实现
-
读未提交:在这种隔离级别下,查询是不会加锁的,也由于查询的不加锁,所以这种隔离级别的一致性是最差的,可能会产生**“脏读”、“不可重复读”、“幻读”**。如无特殊情况,基本是不会使用这种隔离级别的。
-
读已提交:这是各种系统中最常用的一种隔离级别,也是SQL Server和Oracle的默认隔离级别。
数据库对普通的查询是不会加锁的。那为什么“读提交”同“读未提交”一样,都没有查询加锁,但是却能够避免脏读呢?这就要说道另一个机制“快照(snapshot)”,而这种既能保证一致性又不加锁的读也被称为“快照读(Snapshot Read)”。 -
可重复读:可重复读,顾名思义,就是专门针对“不可重复读”这种情况而制定的隔离级别,自然,它就可以有效的避免“不可重复读”。而它也是MySql的默认隔离级别。
在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改,因此,“可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”,因为幻读是由于“插入或者删除操作(Insert or Delete)”而产生的。 -
序列化:这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。这种级别下,“脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大,所以基本没人会用。
5 在RR级别下能够读到事务ID靠后未提交的修改吗
在可重复读隔离级别下,当前事务内的读操作可以看到当前事务修改的数据结果,因为数据的版本信息与当前事务的 Read View 是一致的。
6 TCP差错控制、流量控制,拥塞控制
原文太长,所以直接引用的链接
CP差错控制、流量控制、拥塞控制
7 HTTP及其版本
HTTP协议是超文本传输协议的缩写,是用于从万维网传输超文本到浏览器的传输协议
http1.0
HTTP1.0默认使用 Connection:cloose
,浏览器每次请求都需要与服务器建立一个 TCP 连接,服务器处理完成后立即断开 TCP 连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
http1.1
HTTP1.1默认使用 Connection:keep-alive
(长连接),避免了连接建立和释放的开销;通过 Content-Length 字段来判断当前请求的数据是否已经全部接受。不允许同时存在两个并行的响应。
1.0和1.1的区别
Http协议的初始版本中,每进行一次Http通信就要断开一次TCP连接。每次请求都会造成TCP连接的建立和断开,增加通信量的开销。
1.1持久化链接的特点
优点
- 持久连接也称为Http keep-alive,只要任意一端没有明确提出断开连接,则保存TCP连接状态。
- 减少了TCP连接的重复建立和断开所造成的额外开销,减去了服务器端的压力。
- 持久连接使得多数请求以管线化方式(pipelining)成为可能。可以同时并行发送多个请求,而不需要一个接一个的等待响应了。(请求打包一次传输过去,响应打包一次传递回来),管线化的前提是在持久连接下。
缺点
- 高延迟,带来页面加载速度的降低。(网络延迟问题只要由于队头阻塞,导致宽带无法被充分利用)
- 无状态特性,带来巨大的Http头部。
- 明文传输,不安全。
- 不支持服务器推送消息。
http2.0
HTTP2.0:基于SPDY,专注于性能,目标是在用户和网站直接只用一个连接
。
http2.0特点
优点
- 二进制传输 :http2.0将请求和响应数据分割为更小的帧,并且它们采用二进制编码(http1.0基于文本格式)。多个帧之间可以乱序发送,根据帧首部的流表示可以重新组装。
- Header压缩:Http2.0开发了专门的“HPACK”算法,大大压缩了Header信息。
多路复用:http2.0中引入了多路复用技术,很好的解决了浏览器限制同一个域名下的请求数量的问题
。多路复用技术可以只通过一个TCP链接就可以传输所有的请求数据。
服务端推送:HTTP2.0在一定程度上改不了传统的“请求-应答”工作模式,服务器不再完全被动地响应请求,也可以新建“流”主动向客户端发送消息
。(例如,浏览器在刚请求html的时候就提前把可能会用到的JS,CSS文件发送给客户端,减少等待延迟,这被称为“服务端推送ServerPush”)服务器也不能随便将第三方资源推送给服务器,必须经过双方确认。缺点
- TCP以及TCP+TLS建立连接的延迟(握手延迟)
- TCP的队头阻塞没有彻底解决(http2.0中,多个请求是跑在一个TCP管道中的,一旦丢包,TCP就要等待重传(丢失的包等待重新传输确认),从而阻塞该TCP连接中的所有请求)
http3.0
基于UDP协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。从而产生了HTTP3.0版本,它解决了“队头阻塞”的问题。
3.0特点
优点
- 实现了类似TCP的流量控制,传输可靠性的功能。
- 实现了快速握手功能(QUIC基于UDP,UDP是面向无连接的,不需要握手和挥手,比TCP快)
- 集成了TLS加密功能
- 多路复用,彻底解决TCP中队头阻塞的问题(单个“流”是有序的,可能会因为丢包而阻塞,但是其他流不会受到影响)
总结
- HTTP1.1的缺点:安全性不足和性能不高;
- HTTP2.0完全兼容HTTTP1.0,是“更安全的HTTP,更快的HTTPS”,头部压缩,多路复用等技术充分利用了带宽,降低了延迟。
- HTTP3.0的底层支撑协议QUIC基于UDP实现,又含TCP的特点,实现了又快又可靠的协议。
8 RESTful
Restful API是目前比较成熟的一套互联网应用程序的API设计理念,Rest是一组架构约束条件和原则,如何Rest约束条件和原则的架构,我们就称为Restful架构,Restful架构具有结构清晰、符合标准、易于理解以及扩展方便等特点,受到越来越多网站的采用!
Restful API接口规范包括以下部分:
协议
API与用户的通信协议,总是使用HTTPs协议。
域名
应该尽量将API部署在专用域名之下,如https://api.专属域名.com;如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下,如https://专属域名.com/api/。
版本
可以将版本号放在HTTP头信息中,也可以放入URL中,如https://api.专属域名.com/v1/
路径
路径是一种地址,在互联网上表现为网址,在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数,如https://api.专属域名.com/v1/students。
HTTP动词
对于资源的具体操作类型,由HTTP动词表示,HTTP动词主要有以下几种,括号中对应的是SQL命令。
- GET(SELECT):从服务器取出资源(一项或多项);
- POST(CREATE):在服务器新建一个资源;
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源);
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性);
- DELETE(DELETE):从服务器删除资源;
- HEAD:获取资源的元数据;
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
状态码
服务器会向用户返回状态码和提示信息,以下是常用的一些状态码:
200 OK - [GET]:服务器成功返回用户请求的数据;
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功;
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务);
204 NO CONTENT - [DELETE]:用户删除数据成功;
5400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作;
401 Unauthorized - [* ]:表示用户没有权限(令牌、用户名、密码错误);
403 Forbidden - [* ]:表示用户得到授权(与401错误相对),但是访问是被禁止的;
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作;
406 Not Acceptable - [GET]:用户请求的格式不可得;
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的;
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误;
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
错误处理
如果状态码是4xx,就会向用户返回出错信息,一般来说,返回的信息中将error作为键名,出错信息作为键值。
返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范:
- GET /collection:返回资源对象的列表(数组);
- GET /collection/resource:返回单个资源对象;
- POST /collection:返回新生成的资源对象;
- PUT /collection/resource:返回完整的资源对象;
- PATCH /collection/resource:返回完整的资源对象;
- DELETE /collection/resource:返回一个空文档。
Hypermedia API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
9 go map的实现
数据结构
map实现的基本数据结构是哈希表。哈希表由一个数组和一组散列函数组成。数组的每个元素称为一个“桶”,每个桶都存储一个键值对。散列函数将每个键映射到一个桶中,使得每个键都可以通过哈希函数快速定位到对应的桶。
冲突处理
在Go 1.17及更高版本中,map的实现已经从链表散列切换为红黑树,这种树形结构可以提供更快的查找性能,特别是对于大型map。
动态增长
map的实现允许动态增长,也就是说,在插入新的键值对时,如果哈希表已满,它会自动重新分配更大的存储空间,然后将所有的键值对复制到新的哈希表中。这种动态增长可以确保哈希表始终具有足够的空间来容纳所有的键值对。
10 哈希过程是什么样子的
哈希主要有三个组成部分:
- Key 可以是作为哈希函数输入的任何字符串或整数,哈希函数是确定数据结构中项的索引或存储位置的技术。
- Hash 函数 接收输入键并返回名为哈希表的数组中元素的索引。该指数被称为 hash index .
- hash表是一种数据结构,它使用名为哈希函数的特殊函数将键映射到值。哈希以关联的方式将数据存储在数组中,其中每个数据值都有自己唯一的索引。
详情看这篇博文哈希(Hash)完整教程
11 桶的增加
哈希桶
哈希桶又被称作开链法,开散列法。相比于闭散列法哈希桶更为灵活直观,存数据时不会浪费空间,开散列法存数据时由于避免哈希冲突,总会有百分之三十的空间浪费,当存储空间很大时将会造成大量的浪费。同时开散列法造成哈希冲突时不便于查找数据。所以我们有了哈希结构中的哈希桶。
哈希桶与开散列法一样的是需要开一段数组用于映射,不过不一样的是哈希桶的数组存的是指针所以哈希桶是一段指针数组。数组中每一个位置都存着一段链表,当插入数据时不用处理哈希冲突,直接将数据链接在链表即可,哈希桶的结构如下所示。
查找操作
- 哈希函数和键提取器:使用哈希函数 hf 和键提取器 kot 对插入的数据进行处理。
- 查找是否存在相同值:通过调用 Find 函数查找是否已存在相同值的元素,如果存在,则返回 false 表示插入失败。
- 动态扩容:检查当前哈希表的大小 _tables.size() 是否达到负载因子 _n,如果是,则进行动态扩容。计算新表的大小 newSize,创建一个新的哈希表 newTable,并将旧表中的数据复制到新表中。
- 插入新元素:计算待插入元素的哈希值 hashi,取模 _tables.size() 获取对应的槽索引。使用头插法,在对应槽的链表中插入新节点 newnode,并更新链表的头指针 _tables[hashi]。
更新元素计数:增加哈希表中元素的计数 _n。 - 返回插入结果:返回 true 表示插入成功
查找操作
- 哈希函数和键提取器:使用哈希函数 hf 和键提取器 kot 对待查找的键进行处理。
- 空表检查:如果哈希表 _tables 的大小为 0,表示哈希表为空,直接返回 nullptr。
- 计算哈希值和槽索引:通过哈希函数 hf 计算待查找键的哈希值 hashi。取模 _tables.size() 获取对应的槽索引。
- 遍历链表:从槽索引处获取链表的头指针 cur,进入链表遍历。
- 比较键值:使用键提取器 kot 对当前节点的数据进行处理,与待查找的键进行比较。如果相等,则找到了目标节点,返回指向该节点的指针 cur。
- 更新当前节点:如果当前节点不是目标节点,继续遍历下一个节点 cur = cur->_next。
- 查找结束:遍历完链表仍未找到目标节点,则返回 nullptr 表示未找到。
12 map线程安全吗?
Golang 的map是不安全的,所以sync包提供了线程安全的map。
为什么
map不是线程安全的。在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是不安全的。字典值本身可能会因这些操作而产生混乱,相关的程序也可能会因此发生不可预知的问题。
配合(锁)实现线程安全的MAP
悲观锁:进来的每一步操作都认为同时会有其他进程影响操作,所以提前加锁。
lock.Lock()
//map的增删改查操作
lock.UnLock()
乐观锁:因为map线程不安全是同时:读&&写||写&&写 造成的,所以在map写的时候加上锁就会提高map的性能。
//查
lock.Lock()
//map的增删改操作
lock.UnLock()
SYNC.MAP实现的原理
- 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
- 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
- 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
- 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
- 对于删除数据则直接通过标记来延迟删除
13 post就不能在URL上附加参数吗
可以,具体查看优雅的处理POST请求URL带参数的情况
14 数据库如果有很多很多数据,分页显示,如何去做
15 分布式环境如何加锁
目前分布式锁的实现方案主要包括三种:
- 基于数据库(唯一索引)
- 基于缓存(Redis,memcached,tair)
- 基于Zookeeper
基于数据库实现分布式锁:主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。
基于缓存实现分布式锁:理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的SETNX key value这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。
基于Zookeeper:Zookeeper一般用作配置中心,其实现分布式锁的原理和Redis类似,我们在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。
16 map哈希过程
这个博文写的非常详尽
Golang map 实现原理
map 又称为 hash map,在算法上基于 hash 实现 key 的映射和寻址;在数据结构上基于桶数组实现 key-value 对的存储.
以一组 key-value 对写入 map 的流程为例进行简述:
- 通过哈希方法取得 key 的 hash 值;
- hash 值对桶数组长度取模,确定其所属的桶;
- 在桶中插入 key-value 对.
hash 的性质,保证了相同的 key 必然产生相同的 hash 值,因此能映射到相同的桶中,通过桶内遍历的方式锁定对应的 key-value 对.
因此,只要在宏观流程上,控制每个桶中 key-value 对的数量,就能保证 map 的几项操作都限制为常数级别的时间复杂度.
在 map 解决 hash /分桶 冲突问题时,实际上结合了拉链法和开放寻址法两种思路. 以 map 的插入写流程为例,进行思路阐述:
(1)桶数组中的每个桶,严格意义上是一个单向桶链表,以桶为节点进行串联;
(2)每个桶固定可以存放 8 个 key-value 对;
(3)当 key 命中一个桶时,首先根据开放寻址法,在桶的 8 个位置中寻找空位进行插入;
(4)倘若桶的 8 个位置都已被占满,则基于桶的溢出桶指针,找到下一个桶,重复第(3)步;
(5)倘若遍历到链表尾部,仍未找到空位,则基于拉链法,在桶链表尾部续接新桶,并插入 key-value 对.