2021-03-01 Java 高级开发面试日记

前言

好哥哥们,这篇的话记录了安酱在面试广州一家对公电商公司相对比较难的面试题。安酱由于这块是真的没有复习,所以也就回答了个大概(寂寞)。不知道是不是由于对公的商城,并没有像高并发这类的问题,不过有问多线程相关的问题。面试题如下,已加答案。

在2021-03-02 对方HR告知结果面试通过,但是给的薪资没有达到我的预期就拒绝了...。难道说这是我成为面霸的开端吗??

如何保证 MQ 顺序消费

由于安酱简历中有提到熟悉的MQRabbitMQRocketMQ,所以以这两种MQ来分析一波。

RabbitMQ 中保证顺序消费

由于RabbitMQ 本身不支持顺序消费,所以当一个队列有多个消费者时,会出现消费混乱的情况。

解决方案

首先生产者发送消息时选择MQ点对点的发送模式,也就是说一个队列会对应一个消费者,然后把需要保证顺序的消息都发送到一个队列当中。

消费者需要关闭Autoack,并且将prefetchCount设置成一,每次只消费一条信息,处理过后进行手工ack,然后接收下一条消息。因为只有一个消费者,所以这样是能保证顺序消费的。

RocketMQ 中保证顺序消费

由于RocketMQ本身是支持顺序消费的,这里引用官网的一段话。

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

那生产者怎么确保消息能顺序发送到一个队列上呢? 可以使用Hash 取模算法,比如说同一个订单通过算法后发送到同一个队列中,再使用同步发送,只有同个订单的创建消息发送成功,再发送支付消息。这样,我们保证了发送有序。

消费者的话主要是实现MessageListenerOrderly接口,并且设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费如果非第一次启动,那么按照上次消费的位置继续消费(consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET))。

简单说一下MySQL 执行一条SQL语句的过程

这个的话还是很复杂的,这里就简单点说。以查询语句为例,跟新语句略微不一样,如下图。 mysql 执行

连接器

在查询 SQL 语句前,肯定要先建立与 MySQL 的连接,这就是由连接器来完成的。连接器负责跟客户端建立连接、获取权限、维持和管理连接

查询缓存

在建立连接后,就开始执行 select 语句了,执行前首先会查询缓存。

MySQL 拿到查询请求后,会先查询缓存,看是不是执行过这条语句。执行过的语句及其结果会以 key-value 对的形式保存在一定的内存区域中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个value 就会被直接返回给客户端。

如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,会提升效率。

分析器

如果查询缓存未命中,就要开始执行语句了。首先,MySQL 需要对 SQL 语句进行解析。

分析器先会做词法分析。SQL 语句是由多个字符串和空格组成的,MySQL 需要识别出里面的字符串分别是什么、代表什么。MySQL 从你输入的 select 这个关键字识别出来,这是查询语句。它也要把字符串 user_info 识别成表名,把字符串 id 识别成列名。之后就要做语法分析。根据词法分析的结果,语法分析器会根据语法规则,判断输入的 SQL 语句是否满足 MySQL 语法。

优化器

经过分析器的词法分析和语法分析后,还要经过优化器的处理。

优化器是在表里面有多个索引的时候,决定使用哪个索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

MySQL 通过分析器知道了要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。

开始执行的时候,要先判断一下你对这个表 user_info 有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,然后调用对应引擎的相关读接口。

Redis String 底层数据结构

Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符\0结尾的字符数组)。

采用的是SDS(简单动态字符串),数据格式如下图。
图片来源Redis设计与实现

  1. free 记录了 buf 数组中未使用的字节数量。
  2. len 保存了SDS保存字符串的长度。
  3. buf[] 数组用来保存字符串的每个元素。

优点

使用SDS优点如下:

常数复杂度获取字符串长度

由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。

杜绝缓冲区溢出

  我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

减少修改字符串的内存重新分配次数

  C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。

  而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

  1. 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

  2. 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)

二进制安全

  因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。

兼容部分 C 字符串函数

  虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。

用几个词概括一下微服务的特点

单一职责、 团队独立、 技术独立、 前后端分离、 数据库分离、独立部署、 服务治理

mybatis 中的 ${}、#{} 区别与原理

#{} 是一个参数占位符,在JDBC 预编译语句中的参数标记符。

${} 仅仅是一个字符串替换,在动态 SQL 解析阶段将会进行变量替换。

区别就在于当进行参数设值时,#{}是以字符串的方式(加了双引号)将参数替换上去。而${}只是将参数值将变量进行替换,这时就会出现Sql 注入的问题。

  IT.互联网   技术   Java

作者  :  汉雁卉

海阔凭鱼跃 天高任鸟飞




小程序

面试一点通

创作中心

分享职场知识、帮助到更多的人

软件开发

承接、各行各业、软件开发需求

最新发布