Go后端研发三面面试题
binlog有什么用?
binlog是存储mysql的数据变更,我们可以通过监听binlog知道数据库发生了哪些变更,通常可以使用binlog进行数据同步、数据备份以及主从复制等等...
binlog的数据格式有哪些?
binlog日志有三种数据格式
-
STATEMENT
:每一条修改数据的原声SQL
语句都会被记录到binlog
中。但有动态函数的问题,比如你用了uuid
或者now
这些函数,那么就会导致主库上执行的结果并不是从库执行的结果,这种随时在变的函数就会导致复制前后的数据不一致。 -
ROW
:记录行数据最终被修改成什么样了,不会出现STATEMENT
下动态函数的问题。但ROW
的缺点是每行数据的变化结果都会被记录,比如执行UPDATE user_user SET a = 1 WHERE id > 100
语句,那么有多少行数据产生了变化,日志就会记录多少,这会使binlog
文件过大,而在STATEMENT
格式下只会记录一个update
语句而已; -
MIXED
:包含了STATEMENT
和ROW
模式,它会根据不同的情况自动使用ROW
模式和STATEMENT
模式
如何监听binlog?
一般有两种方案:
- 基于
canal
中间件进行监听binlog
- 基于
flinkcdc
监听binlog
其实这两种区别不大,业内常用的是flinkcdc
来监听。原理就是模拟主从复制,将自身模拟程一个slave
节点,向master
节点发送dump
协议,当master
节点收到dump
协议请求之后,就开始推送binlog
到slave
。
mysql dump 之后需要进行什么处理?
一般接受到的是byte
流数据,我们需要解析 binary log
对象才能拿到正在的变更后的数据。那么在接收到主服务器上的 dump
数据后,会根据数据的类型(SQL 查询语句或 Binlog)来进行处理,如果是STATEMENT
格式就直接执行sql
,如果是ROW
格式就直接记录数据。(不确定是不是这样)
唯一索引和联合索引有什么区别?
-
唯一索引是指索引列的取值必须是唯一的,索引列中的值不能重复。如果尝试插入重复值,数据库会抛出唯一性约束错误。
-
联合索引是指索引包含多个列,通过这些列的组合值进行索引。当查询时涉及到联合索引的所有列,数据库会使用该联合索引进行优化查询,提高多列查询的效率,特别是当这些列经常一起作为查询条件时。联合索引中的列顺序很重要,查询时必须按照最左匹配原则进行查询,否则索引无法生效。
联合索引可以是唯一索引吗?
可以的,这样就意味着索引列的组合值必须是唯一举个例子:我们创建了一个联合的唯一索引,username
和email
作为联合的唯一索引列
create table users (
user_id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
UNIQUE KEY idx_unique_username_email (username, email)
);
当我们插入数据, 如果重复了就会报错
那mysql索引结构是什么样的?
mysql的索引结构是B+树,
- 根节点:包含指向子节点的指针。
- 中间节点:包含索引键值和指向子节点的指针。
- 叶子节点:包含索引键值和指向实际数据行的指针。
数据按照索引键值的顺序存储在叶子节点中,这样可以通过在树中进行一系列比较操作来快速定位到所需的数据行。
叶子节点之间通过指针连接,形成一个有序的链表,这样可以支持范围查询和排序操作。
InnoDB 中 的聚集索引的叶节点就是最终的数据节点,InnoDB中的非聚集索引叶子节点指向的是相应的主键值。而MyISAM中非聚集索引的主键索引树和二级索引树的叶节仍然是索引节点,但它有一个指向最终数据的指针
一个索引的建立过程是什么样的?
-
插入数据:当插入一个新的数据项时,首先在叶子节点中找到合适的位置插入数据。如果插入后叶子节点的数据项数量超过了阶数的限制,就需要进行节点分裂操作。
-
节点分裂:当一个节点中的数据项数量超过了阶数的限制时,该节点需要进行分裂。分裂操作会将该节点中的数据项分为两部分,然后将中间值上移到父节点中,以保持 B+树的平衡性。
-
向上递归:如果父节点也满足不了插入新数据项的条件,就需要继续向上递归进行节点分裂操作,直到根节点。如果根节点也满了,则根节点会分裂成两个节点,同时树的高度增加一层。
-
更新索引:在每次节点分裂后,需要更新父节点的索引信息,确保索引的正确性。
-
删除数据:删除数据时,首先在叶子节点中找到要删除的数据项,然后将其删除。如果删除后导致节点的数据项数量低于阶数要求的最小值,需要进行节点合并操作。
-
节点合并:当一个节点中的数据项数量低于阶数要求的最小值时,该节点需要与其兄弟节点进行合并操作。合并操作会将两个节点合并成一个节点,并将父节点中的相应索引项删除。
为什么走索引加快了?
-
减少数据扫描:当数据库表中有索引时,MySQL可以通过索引快速定位到符合查询条件的数据行,而不需要对整个表进行扫描。这样可以大大减少需要扫描的数据量,提高查询速度。
-
加快数据定位:索引使得数据库系统能够更快速地定位到需要的数据行,而不需要逐行查找。通过索引,MySQL可以跳过大部分数据行,直接定位到目标数据行,从而减少了数据访问的时间。
-
降低磁盘I/O操作:索引可以减少磁盘I/O操作的次数。由于索引使得数据定位更快速,数据库系统需要读取的数据页数减少,从而减少了磁盘I/O操作,提高了查询效率。
如果我订单表达到一定规模之后mysql单表是撑不住了,怎么办?
首先要做好调研工作,根据当前业务的发展情况,去选择合适的技术方案。
- 如果这个业务比较重要(能赚钱),那么我们可以做分库分表,就可以有强的维护性和可扩展性。
- 如果这个业务相对来说不那么重要(辅助性业务),可以用当前比较热门的分布式数据库做DTS,比如 tidb、ES,减少我们的投入精力。
redis 有哪些存储日志的形式?同步还是异步?
RDB
持久化就是指的讲当前进程的全量数据生成快照存入到磁盘中,触发RDB
机制又分为手动触发与自动触发。AOF
持久化是以独立的日志记录每次写命令,也就是增量数据,所以AOF
主要就是解决持久化的实时性。
是否异步?
AOF
是同步的RDB
的save
是同步的,bgsave
是异步的
AOF和主进程的关系如下:
Redis -> 写数据 -> AOF缓存 -> 磁盘
AOF的写入是同步的,AOF写入不会阻塞当前的写命令,但是有可能会阻塞下一个写命令
注意是写命令进行日志写入,读命令才会记录日志
那AOF具体是怎么存储日志的?
- Redis 写操作命令结束后,会将命令重写到 AOF 缓冲区
- 然后通过系统调用,将 AOF 缓冲区的数据拷贝到了内核缓冲区
- 然后内核会将数据写入硬盘,具体内核缓冲区的数据什么时候写入到硬盘,由内核决定,而这也是数据丢失的一个隐患。
AOF不断的写日志不是会有很多的io操作吗?怎么避免?
设置redis的aof的落盘策略。
Always
:每次写操作命令执行完后,实时将AOF
日志数据写回硬盘Everysec
:每次写操作命令执行完后,先将命令写入到AOF
文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘No
:Redis
不控制写回硬盘的时间,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定什么时候将缓冲区内容写回硬盘
如果我们需要极致的性能,选择No就可以了,当然高回报的背后是高风险,由于我们并不知道操作系统会什么时候写入硬盘,所以如果redis宕机了,我们将会丢失这段时间的数据。所以在生产当中我们一般会使用AOF
+ RDB
的组合方式来保证AOF
的持久化。
RDB是怎么进行操作?
redis
一般会有两个命令生产RDB,一个是save
,另一个是bgsave
,区别在于是否阻塞主线程
save
是和操作命令在同一个线程里面,如果有大key写入RDB文件,会造成阻塞。bgsave
是创建一个子进程来写入RDB文件,不会造成阻塞。
RDB
和everysec
模式的AOF区别就是一个是全量,一个是增量
那么在bgsave中,通过fork
函数创建的子进程会和父进程共享同一片内存数据,因为在fork的时候,子进程会复制父进程的页表数据,而这两个进程的页表数据一摸一样也就表示会指向同一片物理内存地址。
这样是为了减少创建子进程时的性能损耗,加快子进程的创建速度,毕竟创建子进程的过程中,是会阻塞主线程的。
那如果数据库和redis需要缓存一致性怎么解决?
我们一般都是先更新数据库再更新redis的,在业务允许的下,可以接受一定时间段的数据不一致,因为我们业务场景都只是要求数据的最终一致性就可以了。
那我不考虑最终一致性,我要强一致性,怎么解决?
-
那我们可以需要牺牲一点性能了,我们将 redis 和 mysql 的数据绑定在一起,redis 使用 lua 脚本更新,和mysql 的更新数据绑定在一点,要么一起成功,一起失败,如果失败则需要告警,走降级兜底,然后人工快速介入。
-
又或者可以把缓存删掉,然后再更新数据库,此时大量的请求将会打到DB,这时候,我们就需要使用singlefight 这种在缓存失效的时候使用的合并请求的方式减轻DB的压力。
其实无论怎么选型都不会非常完美的,CAP必定会牺牲一个,即使是99.9999%高可用的阿里云也会有宕机的情况。这个一致性如果要深究一篇文章都讲不完...这里不再过度描述..
具体是怎么建立的ssl tls加密?
- 客户端 Hello:客户端向服务器发送一个Client Hello消息,其中包含支持的加密算法、协议版本、随机数等信息。
- 服务器 Hello:服务器收到Client Hello后,选择加密算法和协议版本,并向客户端发送一个Server Hello消息。
- 证书验证:服务器还会发送自己的数字证书给客户端,客户端会验证证书的有效性,包括证书是否由受信任的证书颁发机构颁发。
- 密钥交换:如果证书验证通过,客户端生成一个随机数,使用服务器的公钥(从证书中提取)加密后发送给服务器,服务器使用自己的私钥解密得到该随机数,用于生成对称加密的会话密钥。
- 完成握手:握手阶段完成后,客户端和服务器都知道如何加密通信,并且共享一个会话密钥用于加密和解密数据。
四次握手主要是交换以下信息:
-
数字证书:该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;
-
三个随机数:这三个随机数构成了后续通信过程中用来对数据进行对称加密解密的对话密钥。
- 首先客户端先发第一个随机数N1,然后服务器回了第二个随机数N2(这个过程同时把之前提到的证书发给客户端),这两个随机数都是明文的;
- 而第三个随机数N3(预主密钥),客户端用数字证书的公钥进行非对称加密,发给服务器,服务器用只有自己知道的私钥来解密,获取第三个随机数。
- 服务端和客户端都有了三个随机数N1+N2+N3,然后两端就使用这三个随机数来生成“对话密钥”,在此之后的通信都是使用这个“对话密钥”来进行对称加密解密。因为这个过程中,服务端的私钥只用来解密第三个随机数,从来没有在网络中传输过,这样的话,只要私钥没有被泄露,那么数据就是安全的。
加密通信协议:就是双方商量使用哪一种加密方式,假如两者支持的加密方式不匹配,则无法进行通信;
一句话概括也就是用非对称加密来生成对称加密的密钥,非对称加密的代价是很高的。
页的概念你清楚吗?
对于进程来说,使用的都是虚拟地址, 虚拟地址空间划分为多个固定大小的虚拟页(VP),物理地址空间划分为多个固定大小的物理页(PP),虚拟页和物理页的大小是一样,通常为4KB。页的主要功能是做出虚拟地址对物理地址的映射。
- 页表:每个进程维护一个单独的页表,页表是一种数组结构,存放着各虚拟页的状态,是否映射,是否缓存。
- 页表项:页表中的每个条目称为页表项,其中包含了虚拟页号和物理页框号之间的映射关系。
- 页面调度:当程序访问一个虚拟地址,而对应的物理页不在内存中时,会发生页面调度,操作系统会从磁盘中将相应的页面加载到内存中。
- 页面置换:当内存中的页面不足时,操作系统需要选择一个页面进行置换(page replacement),将其写回磁盘并加载新的页面。
通过使用页面和页表,操作系统可以实现虚拟内存管理,提高内存利用率和程序的运行效率。虚拟内存技术允许程序看到一个比实际物理内存更大的地址空间,同时可以将不常用的页面置换到磁盘上,从而提高系统的整体性能和稳定性。
页碎是什么?
页碎片是指在虚拟内存系统中,由于分配和释放内存的过程中导致的页面不连续、零散的现象。 在虚拟内存管理中,内存通常被划分为固定大小的页面,而应用程序请求的内存空间可能不是页面大小的整数倍,这就导致了页面的碎片化。
而大量的页碎可能会导致以下问题:
- 分配性能下降:当系统需要分配大块连续内存时,如果内存中存在大量碎片,系统可能需要进行额外的合并操作,降低了分配性能。
- 缓存失效率增加:页面碎片化也可能导致缓存的失效率增加,因为数据分散存储在不同的页面中,需要更多的缓存行加载,降低了缓存的效率。
- 内存利用率下降:由于页面被分割成小块或者存在空隙,导致实际可用内存空间比总内存空间要少。
- 缓存失效率增加:页面碎片化也可能导致缓存的失效率增加,因为数据分散存储在不同的页面中,需要更多的缓存行加载,降低了缓存的效率。
通常会采取一些策略来优化内存分配和释放,比如使用内存池、动态内存分配算法、碎片整理等技术来减少碎片化,提高内存利用率和系统性能。
为什么需要内存对齐?
内存对齐,就是将数据存放到一个是字的整数倍的地址指向的内存之中。处理器在执行指令去操作内存中的数据,这些数据通过地址来获取。为了能让复杂数据结构对齐,编译器一般会对数据结构做一些填充。
内存对齐总的来说就是两个原因:提升效率和避免出错。
- 某些处理器只能存取对齐的数据,存取非对齐的数据可能会引发异常;
- 某些处理不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐数据需要额外花费更多的时钟周期;
- 有些处理器虽然支持非对齐数据访问,但是会引发对齐陷阱;
- 某些处理只支持简单数据指令非对齐存取,不支持复杂数据指令非对齐存取。
而我们可能需要进行类型转换、位运算、使用特定平台的指令、内存拷贝这些额外的操作取非对齐的数据,而这也需要额外的指令来访问和处理这些数据,也造成了不必要的开销。
如何解决接口幂等问题?
幂等性,用数学语言表达就是:f(x)=f(f(x))
说起接口幂等性问题,只需记住一句口诀,那就是“一锁、二判、三更新”。
下面我来详细解释一下这个口诀:
- 一锁:首先,你得给接口加个锁,这样别人就不能在你操作的时候来插一脚了。
这锁可以是分布式锁,也可以是悲观锁,但关键是要确保它是互斥的,也就是说同一时间只能有一个人用。
- 二判:接下来,就是判断操作的幂等性了。怎么判断呢?你可以基于状态机、业务流水表或者数据库的唯一索引来做。
简单来说,就是看看这个操作是不是已经做过了,如果做过了,那就别再做了。
- 三更新:最后一步,就是更新数据了。你得把操作的结果保存到数据库里,这样下次再来查的时候就能看到这个结果了。
如何解决重复下单问题?
在支付过程中,你是如何解决重复下单问题
方案一:提交订单按钮置灰
想要避免用户重复提交,最常规的办法就是,当用户点击下单后,还没等到服务器回应之前,就把那个按钮变成灰色,让它不能再点。
虽然前端页面可以尽量防止用户重复提交表单,但有时候网络不给力,导致请求没发出去,或者发出去没得到回应,这时候也可能出现重传的情况。而且啊,很多RPC框架和网关都有自动重试的功能,所以只靠前端来防止重复请求,那是不太可能的。
当然,这种方案也不是真的没有价值。在访问量特别大的时候,这个方法可以从浏览器这边先拦住一部分请求,让后端服务器轻松一点,起到过滤流量的作用。
这个方案的好处就是简单,基本上可以防止因为用户不小心多次点击提交按钮而造成的重复提交问题。
但它也有个不足,那就是对于用户的前进后退操作,或者按F5刷新页面等情况,它就没辙了。
方案二:请求唯一ID+数据库唯一索引约束
接下来向大家介绍一种最简单的、成本最低的解决方案。
防重最重要的第一步是什么?
那肯定是需要识别出是否是重复请求
所以,需要客户端在请求下单接口的时候,需要生成一个唯一的请求号:requestId,服务端就可以拿这个请求号,判断是否重复请求。
flowchart TB
root[用户进入提交页面]
node_a[页面加载,用户进入提交页面,
前端生成唯一ID:RequestID, 并埋入页面]
node_b[用户点击提交,并带上此次唯一ID]
node_c{检查ID是否有效}
node_d[提交完结]
node_e[提示错误原因]
root ==> node_a
node_a ==> node_b
node_b ==> node_c
node_c ==> |有效|node_d
node_c ==> |无效|node_e
实现的逻辑,流程如下:
-
用户进入订单提交页时,系统会生成一个唯一的请求ID并隐藏在页面里。
-
点击提交时,系统会检查这个ID是否已被使用。若未使用,继续处理;若已使用,则提示重复提交。
-
最关键的是,这个ID会被存入系统的独特名单中,确保每个ID都是唯一的,以此防止重复提交。
但请注意,在高并发情况下(如每秒10万请求),这种方法可能不够用。
方案三:reids分布式锁+请求唯一ID
在上一个方案中我们提到,对于下单流量不多的系统,我们可以用一个叫做“请求唯一ID”再配合给数据表加个“唯一索引”的方法来防止订单重复提交。
但你们知道吗?
随着我们生意越来越好,订单越来越多,可能每秒钟的订单请求就从几十飙升到几百、几千,甚至几万!
这时候,数据库就累得不行了,成了我们下单流程里的“大瓶颈”。
这时候就需要以引入一个叫redis的“缓存小助手”来帮数据库分担压力。
下面,我们以引入redis缓存中间件,向大家介绍具体的解决方案。
flowchart TB
root[用户进入提交页面]
node_a[页面加载,用户进入提交页面,调用服务端接口,
获取请求唯一ID:RequestID, 并埋入页面]
node_b[生成RequestID,
并将RequestID写入Redis]
node_c[用户点击提交按钮
请求需带上请求唯一ID]
node_d{ID是否
有效}
node_e[提示错误原因]
node_f{尝试对
RequestID加锁}
node_g[提示:服务正在处理
请勿重复提交]
node_h[下单:清理购物车优惠券]
node_i[流程执行成功,
清空redis缓存,
释放redis锁]
root ==> node_a
node_a ==> node_b
node_a ==> node_c
node_c ==> node_d
node_d ==> |无效|node_e
node_d ==> |有效|node_f
node_f ==> |加锁失败|node_g
node_f ==> |加锁成功|node_h
node_h ==> |加锁成功|node_i
流程如下:
-
用户进入订单提交界面时,系统调用后端API获取并生成请求唯一ID,将其存储至Redis缓存并返回至前端,前端将此ID嵌入页面。
-
用户点击提交按钮时,后端检查Redis中是否存在该请求唯一ID。若不存在,返回错误信息;若存在,继续后续验证流程。
-
利用Redis的分布式锁机制,对请求ID进行短暂锁定。锁定成功则继续处理;锁定失败则返回提示信息:“订单正在处理中,请勿重复提交。”
-
处理完成后,确保释放Redis中的锁,并清理已处理订单的请求唯一ID。
-
关于数据库唯一索引:虽然理论上可省略,但添加可提高数据一致性和防止潜在的数据冲突。
该方案经过扩展,可高效应对10万QPS(每秒查询率)的高并发场景。
方案四:reids分布式锁+token
在之前的那个方案里,每次下单都得先跑去服务端要个请求的唯一ID,也就是那个requestId,感觉就像是在走个多余的步骤,是吧?那这样不就多了一个专门要ID的请求了吗?
那有没有办法省掉这一步,让我们的下单过程更快更顺畅呢?
答案是肯定的!
我们可以换个思路,不用每次都去服务端要ID了。
我们可以根据用户请求的一些关键信息,按照某种特定的方式,自己生成一个“通行证”,也就是一个token,来代替那个专用的requestId。
这样,我们就不用专门跑去找服务端要ID了,省去了中间那一步,下单过程就更快了。
那么,怎么生成这个token呢?
其实也不难,我们可以把几个重要的信息组合起来,比如
应用名+接口名+方法名+请求参数签名(请求header、body参数,取SHA1值)
把这些信息放在一起,就能生成一个独一无二的token了。
flowchart TB
root[用户进入提交页面]
node_a[服务端接收到请求,根据特点规则
生成本次请求的唯一性token]
node_b[生成token,
并将token存入Redis]
node_c{尝试
对RequestID
加锁}
node_e[提示:服务正在处理,
请勿重复提交]
node_h[下单:清理购物车优惠券]
node_i[流程执行成功,
清空redis缓存,
释放redis锁]
root ==> node_a
node_a ==> node_b
node_a ==> node_c
node_c ==> |加锁失败|node_e
node_c ==> |加锁成功|node_h
node_h ==> |加锁成功|node_i
大致流程如下:
-
用户点击提交按钮,然后服务端就会收到这个请求。收到之后,服务端会根据一些规则给这次请求算出一个独一无二的“身份证”,也就是请求ID。
-
服务端会尝试用Redis的“锁匠”给这个“身份证”上个锁,时间有限哦。如果锁上了,那就继续处理订单;如果锁不上,那就说明服务正在忙,别重复提交了。
-
最后一步,如果成功锁上了,别忘了处理完事情后要把锁打开,不然下次别人再来的时候可能会搞错。
现在来说说方案四和方案三的区别。最主要的区别就在于怎么给请求生成这个“身份证”。
方案四是在服务端这边,通过把几个关键信息组合起来,给请求造一个“身份证”。
这样做的好处是,既能防止订单重复提交,又能让接口测试变得更简单。
而且,方案四的性能还比方案三要好一些呢!
方案五:技术+产品+运营支持
虽然我们已经有了很棒的处理方案,但说实话,有时候用户还是可能因为不小心点错了,收到两份相同的商品才发现自己下重复了。
你知道,就算是世界上最顶尖的技术,也做不到100%完美无缺,总会有那么一点点小漏洞。
所以,为了彻底解决这个问题,我们不仅要靠技术,还得靠产品设计和运营团队的支持。
当这种情况真的发生时,就得靠我们的运营和客服团队来帮忙解决了。
其实,就连像淘宝、京东、拼多多这样的大电商平台,也会遇到类似的问题,他们都是通过运营手段来配合处理的。
所以,大家放心,我们也有办法应对的!