面经|面经-美团

一面:
1、消息队列如何保证可靠性?
【面经|面经-美团】
消息丢失分成三种情况,可能出现生产者、RabbitMQ、消费者。
生产者丢失数据
首先要确保写入 RabbitMQ 的消息别丢,消息队列通过请求确认机制,保证消息的可靠传输。生产开启 comfirm 模式,在生产者开启 comfirm 模式之后,每次发送消息都会分配一个唯一的id。
如果写入了 RabbitMQ 中,RabbitMQ 会回传一个 ack 消息
如果没能写入 RabbitMQ,会回调一个 nack 接口, 可以重新发送消息
一般在生产者这块避免数据丢失,都是用 confirm 机制的
RabbitMQ丢失数据
RabbitMQ 丢失数据,需要开启 RabbitMQ 持久化,开启持久化之后,生产者发送的消息会持久化到磁盘,RabbitMQ 就算是挂了,恢复启动后也会读取之前存储的数据。
还有一种少见的情况,就是RabbitMQ还没将消息持久化,自己就挂了。这种情况需要生产者那边的确认机制结合起来。只有消息被持久化到磁盘以后,才会回传 ack 消息。生产者没有接收到 ack,也可以自己重发。
消费者丢失数据
消费丢失数据,刚消费到 RabbitMQ 发送的数据,消费进程就挂了,重启进程后,RabbitMQ 也不会重新发送消息。
这个时候需要关闭 RabbitMQ 关闭自动的 ack 机制。每次在消费端处理后,再在程序里做 ack 确认,这样的话,如果没有处理完,就没有 ack 确认,那 RabbitMQ 就认为你还没有处理完,这个时候 RabbitMQ 会重新发送消息给消费者。
总结
生产者
开启确认 comfirm 机制
MQ
开启 RabbitMQ 持久化
消费者
关闭RabbitMQ 自动 ack 确认
2、如何保证消息不被重复消费?(消息队列如何保证消息消费的幂等性?)
解决消息重复消费,其实就是保证消息的消费幂等性。
幂等性的定义:
多次执行所产生的影响均与一次执行的影响相同。
所以需要从业务逻辑上设计,将消费的业务逻辑设计成幂等性。
利用数据库的唯一约束
在进行消息消费,需要取一个唯一个标识,比如 id 作为唯一约束字段,先添加数据,如果添加失败,后续做错误提示,或者不做后续操作。
Redis 设置全局唯一id
每次生产者发送消息前设置一个全局唯一id放在消息体中,并存放的 redis 里,在消费端接口上先找在redis 查看是否存在全局id,如果存在,调用消费接口并删除全局id,如果不存在,不做后续操作。
多版本(乐观锁)机制
给业务数据添加一个版本号,每次更新数据前,比较当前版本和消息中的版本是否一致,如果一致就更新数据并且版本号+1,如果不一致就不更新。这有点类似乐观锁处理机制。
总结
设计幂等需要根据具体的业务场景,如果是并发量比较大的系统,数据库一般支撑不了这么大的并发,需要使用 Redis 缓存处理。而并发不大的系统可以选择数据库。

3、消息队列的优缺点
1.消息队列的优点
1)解耦
场景:当A系统需要发送数据到BCD三个系统时。
如果使用接口调用,A系统是和BCD系统耦合在一起的,需要考虑BCD系统挂了怎么办?BCD系统消费失败怎么办?如果E系统也需要这个数据?如果B系统现在不需要这个数据?
如果使用MQ,A系统产生的数据,只要保证消息成功发送到MQ中。各个系统需要数据,自己到MQ中消费。如果新系统需要数据,直接从MQ里消费,如果老系统不需要数据了,直接取消对MQ的消费。
通过MQ,A系统和其他系统彻底解耦了。
2)异步
场景:A系统接到请求,需要在本系统写库,还需要在BCD三个系统写库。
自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。如果同步调用,最终请求总延时是 3 + 300 + 450 + 200 = 953ms。
如果使用MQ,A系统发送三个消息到三个MQ,假设耗时5ms,则A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。
3)消峰
场景:每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。
一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。
如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。

2.消息队列的缺点
1)系统可用性降低
外部依赖的系统多了,增加了消息队列系统。
2)系统复杂度提高
你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
3)一致性问题
数据可能不一致。
4、为什么使用B+树?
MySQL的索引是通过B+树实现的。在B+树结构当中,只有叶节点真正存放了数据(聚簇索引)或者是指向数据的指针(辅助索引),而其他节点都只是存放了指引搜索方向的数据项。
使用B+树而不使用B树的原因是,B树的数据存在各个节点,不利于遍历数据或者进行范围查询。而MySQL的B+树结构,无论是否是聚簇索引,叶节点都存储了一个指向下一个叶节点的指针,便于进行遍历和范围查询。
使用B+树而不使用AVL/红黑树的原因是,B+树的分支更多,因此B+树的高度比其二叉树也会更小,高度越小,IO次数就越少,查询效率也就更高。
使用B+树而不使用哈希表的原因是,使用哈希表进行哈希处理之后,就无法通过索引进行范围查询和排序操作,而只能通过全表查询进行;同时,当数据量较大时,哈希表的查询效率也会降低。
5、http报文参数有哪些?
https://www.cnblogs.com/sheng-247/articles/13054518.html
HTTP有两种报文:请求报文和响应报文,
6、聚簇索引VS主键索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,具体细节依赖于其实现方式。
MySQL数据库中innodb存储引擎,B+树索引可以分为:
聚簇索引(也称聚集索引,clustered index)
辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。
这两种索引内部都是B+树,聚集索引的叶子节点存放着一整行的数据。
Innobd中的主键索引是一种聚簇索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引。
InnoDB中,表数据文件本身就是按B+树组织的一个索引结构,聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分;一般建表会用一个自增主键做聚簇索引,没有的话MySQL会默认创建,但是这个主键如果更改代价较高,故建表时要考虑自增ID不能频繁update这点。
我们日常工作中,根据实际情况自行添加的索引都是辅助索引,辅助索引就是一个为了需要找主键索引的二级索引,先找到主键索引再通过主键索引找数据。
Innodb通过主键聚集数据,如果没有定义主键,innodb会选择【非空】的唯一索引代替。如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。
聚簇索引的优缺点:

优点
数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
聚簇索引对于主键的排序查找和范围查找速度非常快
缺点
插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般【定义主键为不可更新】。
二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
7、explain参数
explain参数
explain的所有参数
id:编号
select_type:查询类型
table:表
type:类型
possible_keys:预测用的索引
key:实际使用到的索引
key_len:实际使用的索引的长度
ref:表之间的引用
rows:通过索引查询到的数据两
Extra:额外的信息

select_type常见的取值有:
SIMPLE(简单表,即不使用表连接或者子查询)
PRIMARY(主查询,即外层的查询)
UNION(UNION 中的第二个或者后面的查询语句)
SUBQUERY(子查询中的第一个 SELECT)等。
table #显示该语句涉及的表
type #这列很重要,显示了连接使用了哪种类别,有无使用索引,反映语句的质量。
possible_keys #列指出MySQL能使用哪个索引在该表中找到行
key #显示MySQL实际使用的键(索引)。如果没有选择索引,键是NULL。
key_len #显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好
ref #显示使用哪个列或常数与key一起从表中选择行。
rows #显示MySQL认为它执行查询时必须检查的行数。
extra #包含MySQL解决查询的详细信息。

二面:
1、添加索引有如下原则

http://t.zoukankan.com/lgqtecng-p-6726216.html
1)、为经常需要排序和联合操作的字段建立索引
2)、为常作为查询条件的字段建立索引
3)、不要给大字段类型(text等)加索引
详细版:
适合:
1.适合索引的列是出现在where子句中的列,或者连接字句中的唯一列
比较好理解,如果建立的索引并不会经常被使用到,建立索引就只会增加空间,没有意义了
2.对于数据量较小的表,索引效果差,没有必要建立索引
如果一个表中只有几十条数据,从头到位遍历一遍速度也很快,建立索引就没有意义了
3.使用短索引,如果对长字符串进行索引,应该制定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配
也就是说索引的长度不易过长,比如一个长度为100的字符串,如果直接作为索引存储,那么这个索引建立以后会占用大量的空间,我们就可以去这个字符串的前十位作为索引,当我们要查找的数据超过十位的时候,就可以对这些前十位相同的数据逐一进行比较,这样可以尽可能的减少空间,同时满足速度
4.不要过度索引,索引需要占用额外的磁盘空间,并降低读写操作的性能, 在修改表的内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长,所以只保持需要的索引有利于查询即可
也就是说不要建立过多的索引,比如一行有十个数据,只需要将辨识度较高的列设为索引即可,因为建立出的索引也是需要额外空间的,如果同一张表建立的索引过多,因为底层是B+树,在插入数据的时候需要维护的索引过多,有些B+树甚至要重构,那么就会导致查询可能没有方便多少,但是插入和删除的开销过大,得不偿失
5.有外键的数据列一定要建立索引】
不适合:
1.频繁更新的字段不适合建立外键
因为B+树频繁更新开销较大
2.不能有效区分数据的列不适合作为索引(比如性别,区分度太低)
3.尽量扩展索引,不要新建索引,比如已经有a索引,需要添加b索引,可以将a变为(a,b)联合索引,这样只需要修改原先的索引即可
新建索引的空间占用问题
4.对于查询中很少涉及的列,重复值比较多的列不要建立索引
用不着就不用建立
5.对于定义为text、image和bit的数据列星不要建立索引

2、索引失效的场合总结
面经|面经-美团
文章图片

3、Http接口用到了什么提交方式?
https://cloud.tencent.com/developer/article/1949755
通过 http/https 向服务端传递数据的方式,基本可以分为 5 种:url param、query、form-urlencoded、form-data、json。
1)url param
Restful 的规范允许把参数写在 url 中,比如:
http://guang.zxg/person/1111
这里的 1111 就是路径中的参数(url param),服务端框架或者单页应用的路由都支持从 url 中取出参数。
2)query
通过 url 中 ?后面的用 & 分隔的字符串传递数据。比如:
http://guang.zxg/person?name=guang&age=20
这里的 name 和 age 就是 query 传递的数据。
其中非英文的字符和一些特殊字符要经过编码,可以使用 encodeURLComponent 的 api,或者使用封装了一层的 qeury-string 库来处理
通过 URL 传递数据的方式就这 2种,后面 3 种是通过 body 传递数据的方式。
3)form-urlencoded
直接用 form 表单提交数据就是这种,它和 query 字符串的方式的区别只是放在了 body 里,然后指定下 content-type 是 application/x-www-form-urlencoded。
因为也是 query 字符串,所以也要用 encodeURIComponent 的 api 或者 query-string 库处理下。
其实这种设计也很容易理解,get 是把数据拼成 query 字符串放在 url 后面,于是设计表单的 post 提交方式的时候就直接用相同的方式把数据放在了 body 里。
通过 & 分隔的 form-urlencoded 的方式需要对内容做 url encode,如果传递大量的数据,比如上传文件的时候就不是很合适了,因为文件 encode 一遍的话太慢了,这时候就可以用 form-data
4)form data
不再是通过 & 分隔数据,而是用 --------- + 一串数字做为分隔符。因为不是 url 的方式了,自然也不用再做 url encode。
orm-data 需要指定 content type 为 multipart/form-data,然后指定 boundary 也就是分割线。
body 里面就是用 boundary 分割线分割的内容。
很明显,这种方式适合传输文件,而且可以传输多个文件。
但是毕竟多了一些只是用来分隔的 boundary,所以请求体会增大。
5)json
form-urlencoded 需要对内容做 url encode,而 form data 则需要加很长的 boundary,两种方式都有一些缺点。如果只是传输 json 数据的话,不需要用这两种。
可以直接指定content type 为 application/json 就行:
我们平时传输 json 数据基本用的是这种。
这三种是通过 body 传递数据的方式。
总结:
网页开发中向服务端传送数据是一个基本功能,常用的方式就 url param、query、form urlencoded、form data、json 这 5 种。
前 2 种是通过 url 传递数据的方式(需要对数据做 url encode),后 3 种是通过 body 传递数据。
form urlencoded 只是把 query 放在了 body 里,同样需要对数据做 url encoded,所以处理文件就不合适了。(content type 要指定为 application/x-www-form-urlencoded)
form data 是通过 boundary 分隔内容,不需要做 url encode,所以用来传文件很合适。但是如果不是传文件就没必要用了,因为多了一些 boundary 字符串比较占空间。(content type 要指定为 multipart/form-data)
json 是现在最常用的传递数据的方式,既不需要 url encoded,又不需要加没必要的 boundary。(指定 content type 为 application/json)。
当然,也可以指定别的 content type,比如 application/xml、text/plain 等,但一般不会用。
99% 情况下,我们都是通过这 5 种 http/https 的提交数据的方式和服务端交互的。
4、GET/POST区别
不同点:
1.最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。
2.GET在浏览器回退时是无害的,而POST会再次提交请求。
get会将请求参数放在请求的url中,回退操作实际上浏览器会从之前的缓存中拿结果;post每次调用都会创建新的资源。.另外在method的定义上,get是幂等的,执行多少遍不影响最终存储的结果。而post每次调用都会创建新的资源。Get put delete 都是等幂的,post请求两次得到两个不同的url,服务器创建了两份不同的资源,所以post不是等幂的。(等幂是说一次或多次请求资源状态不变)
3.GET请求会被浏览器主动cache,而POST不会。
HTTP缓存的基本目的就是使应用执行的更快,更易扩展,但是HTTP缓存通常只适用于idempotent request(可以理解为查询请求,也就是不更新服务端数据的请求),这也就导致了在HTTP的世界里,一般都是对Get请求做缓存,Post请求很少有缓存。
get多用来直接获取数据,不修改数据,主要目的就是DB的search语句的感觉。用缓存(有个代理服务器的概念)的目的就是查db的速度变快。
post则是发送数据到服务器端去存储。类似db里的update delete和insert语句的感觉。更新db的意思。数据必须放在数据库,所以一般都得去访问服务器端。
4.GET只能进行url编码,POST支持多种方式编码。
url编码指的是urlEncode,POST提交数据的方式有application/x-www-form-urlencoded(浏览器form表单默认的编码方式,提交的数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码),multipart/form-data(表单上传文件时,必须让 form 的 enctype 等于这个值),application/json(告诉服务端消息主体是序列化后的 JSON 字符串),text/xml(一个文本内容,根据自己和浏览器的约定进行传输)。
5.GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
和缓存差不多,也是为了应用执行的更快。
6.GET请求在URL中传送的参数是有长度限制的,而POST没有。
Get请求理论上是没有长度限制的,但是为了解析的方便大多数浏览器规定大小为2k。POST一般情况下是没有限制的。
7.对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
URL 是 HTTP 的一个首部。既然作为一个首部,那么根据约定,一定是 ASCII 字符的。
8.GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
get请求参数在url中,即传送的数据是在链接里面可看到,有安全问题。
post参数不是在url里面而是在请求体中,因此安全性较get高。
9.GET请求会产生一个TCP数据包,POST请求会产生两个TCP数据包。
GET请求是把http header和data一并发出去,服务器响应200,POST请求是浏览器先发送header,服务器响应100,浏览器发送data,浏览器再响应200。所以get请求只请求一次,就可以取到数据。而post请求需要请求两次才能得到想要的数据
5、除了GET/POST还有哪些?

delete,put;
6、面向对象的基本原则?再详细说一下依赖倒转?
基本原则:
一、可维护性
高内聚、低耦合
高内聚,是针对一个组件(类)内部而言,如果一个组件干了好几件不相关的事情,那么组件内部就比较散,出问题是迟早的事情。中原大战之后,蒋介石表面上统一了各个军阀,实际上,他们内部之间不够内聚,一盘散沙。
低耦合,是针对多个组件之间的关系。老死不相往来,是理想国,也就没有什么矛盾,但这是不可能的。所以,尽量减少类之间的依赖。
二、面向对象设计
1、单一责任原则 (SRP):其核心思想为:一个类,最好只做一件事,只有一个引起它的变化
2、开放/封闭原则(OCP):其核心思想是:软件实体应该是可扩展的,而不可修改的。
3、里氏替换原则(LSP):其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础
4、接口分离原则(ISP):其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。
5、依赖反转原则(DIP):其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
http://www.wjhsh.net/manshufeier-p-9543491.html
7、如何保证接口的幂等性(防止重复提交)?
https://blog.csdn.net/weixin_47409774/article/details/124066771
幂等解决方案:
1、token 机制(令牌机制)
1、服务端提供了发送token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,
就必须在执行业务前,先去获取token,服务器会把token 保存到redis 中。
2、然后调用业务接口请求时,把token 携带过去,一般放在请求头部。
3、服务器判断token 是否存在redis 中,存在表示第一次请求,然后删除token,继续执行业
务。
4、如果判断token 不存在redis 中,就表示是重复操作,直接返回重复标记给client,这样
就保证了业务代码,不被重复执行。
2.各种锁机制
3、各种唯一约束:
1、数据库唯一约束
2、redis set 防重
4、防重表
使用订单号orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且
他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避
免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个
事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的redis 防重也算

5、全局请求唯一id
调用接口时,生成一个唯一id,redis 将数据保存到集合中(去重),存在即处理过。
可以使用nginx 设置每一个请求的唯一id;

8、三次握手和四次挥手:
面经|面经-美团
文章图片


面经|面经-美团
文章图片


9、四次挥手时为什么客户端要等待2MSL(TIME_WAIT/CLOSE_WAIT)
防止发出的ACK信息没有被收到,从而导致连接无法被正常关闭。等待2个MSL,如果服务器没有收到ACK报文,就会再给客户端发送一个FIN,客户端就能正常给服务器传回ACK报文。
10、redis的基本数据结构?
Redis的五个基本数据类型有string, list, hash, set, zset。
string:string是key-value的数据类型,同时Redis的字符串是可修改的。
list: list是链表,Redis的list实现为双向链表。
hash: hash实际上就类似于Java的HashMap,适合用于存储对象。
set: set是一种无序集合,存储的数据无序且不重复。提供了检测集合中是否存在该数据的接口。
zset: zset是有序集合,存储的数据是有序的。它添加了一个权重参数score,可以通过权重进行排序。
11、zset是怎么实现的?有哪些命令?
https://blog.csdn.net/peinanwei__/article/details/123910162
跳表主要包含以下的部分:
表头:主要负责维护跳表的指针节点
节点:每个节点当中存储了对象、score、若干个层
层:保存着连接到其他元素的前进指针 每当插入一个节点,会随机设置该节点的层数。之后从高层向底层逐渐尝试查找合适的位置。
面经|面经-美团
文章图片



    推荐阅读