秋招面试题汇总

本文最后更新于:2024年2月12日 晚上

计算机网络

HTTP头部包含的信息?Cookie保存在哪?cookie和session的区别?

1.HTTP Request的header信息

字段 说明 示例
请求方法 主要有:GET、POST、PUT、DELETE GET /student/2 HTTP/1.1
HOST 请求的web服务器的的域名地址 https://merickbao.top:8080
Connection 表示是否需要持久连接 Connection: keep-alive
Keep-Alive 显示此HTTP连接的Keep-Alive时间 Keep-Alive: 300
cookie HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器 cookie:csrftoken=4GERM7xvF2JJwjLv5c1if24Eb3INjDvQyYusjbaVqui6TW71rjndwApLJ73dULKX; _ym_uid=164782706831064302; _ym_d=1647827068; sessionid=l37d7wwr8lzu80zcdra2c76p7olxwzgg; _ga=GA1.1.492789418.1647827063; _ym_isad=1; _ga_T7R9K035KL=GS1.1.1661349541.139.1.1661349586.0.0.0
User-Agent 客户端运行的浏览器的详细信息 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36
Accept 指定客户端能够接收的内容类型 Accept:text/xml,application/xml,application/xhtml+xml,text/html;q=0.8,image/png,/;q=0.5
Accept-Language 指定HTTP客户端浏览器用来展示返回信息所优先选择的语言 Accept-Language: zh-cn,zh;q=0.5 这里默认为中文
Accept-Encoding 指定客户端浏览器可以支持的web服务器返回内容压缩编码类型 Accept-Encoding: gzip,deflate
Accept-Charset 浏览器可以接受的字符编码集 Accept-Charset: gb2312,utf-8;q=0.7,*;q=0.7
Content-Type 显示此HTTP请求提交的内容类型。一般只有post提交时才需要设置该属性 Content-type: application/x-www-form-urlencoded;charset:UTF-8
Referer 包含一个URL,用户从该URL代表的页面出发访问当前请求的页面 https://www.bilibili.com/video/BV17a411N7nP?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=b2ff65acf0fdb4e009be1b833f2cddc5

2.HTTP Response的Headers

字段 说明 示例
content-language 响应体的语言类型 content-language: en
Content-Length 表示web服务器返回消息正文的长度 content-length: 285391
Content-Type 返回数据的类型(例如text/html文本类型)和字符编码格式 Content-Type: text/html;charset=utf-8
Date 显示当前的时间 date: Thu, 25 Aug 2022 09:13:29 GMT

3.cookie保存在哪?

cookie一般保存在浏览器本地的sqlite数据库中(Cookies文件),浏览器在发送请求时,会将保存在该域名下的所有cookie值一起封装在header中发送给web服务器。

4.cookie和session的区别?

  • 相同点:都可以用来唯一标识客户端,解决HTTP的「无状态」
  • 不同点:
    • 1.作用范围不同:cookie保存在客户端(安全性较差),session保存在服务器端(安全性高)
    • 2.有效期不同:cookie可以设置长时间有效,而session一般失效时间较短,客户端关闭或者session超时都会失效(默认失效时间为30分钟)。
    • 3.存储大小不同:单个cookie保存的数据不能超过4KB,而session能存储的数据量远大于cookie(最高可达2GB,但是存储越多,对服务器性能影响越大)。

5.HTTP状态码

类型 描述 常用状态码
1xx 表示请求已被正常接受,还需要继续处理 100(一切正常,可以继续请求)
101(请切换协议)
2xx 表示请求已被成功接收、理解、处理 200(请求成功)
204(请求已成功处理,但无返回内容)
3xx 重定向 301(表示资源已被永久重定向)
302(临时重定向)
304(资源未变化,可以直接使用本地缓存)
4xx 客户端错误 400(请求的语法错误)
403(服务器理解请求,但拒绝执行)
404(请求的资源在服务器未找到)
5xx 服务端错误 500(表示服务端程序错误)

怎么实现分布式session?

  • 方案1: 客户端存储

    将session信息保存在cookie中。

    缺点:存在安全隐患,cookie能保存的数据量有限。

  • 方案2: session复制

    所有服务器都复制维护一份相同的seesion,在同一个局域网内,一台服务器的session会广播给其他服务器。

    缺点:每个服务器都会复制session,会造成服务器内存浪费

  • 方案3: session黏性

    使用「Nginx」进行反向代理,使用「ip_hash」的负载策略,将客户端的请求根据ip固定的分配到一个服务器,这样便可以将客户端和服务器绑定。

    缺点:如果某一台服务器宕机,那么他所服务的客户端seesion将会丢失

    image-20220826105617022

  • 方案4: session集中管理(企业中常用的方案)

    使用「redis」等高性能服务器来集中管理seesion,例如spring官方提供的「spring-session」就是这样来处理session的一致性问题的。

    image-20220826110127760

I/O多路复用?select、poll、epoll的区别?

操作 select poll epoll
操作方式 遍历(轮询) 遍历(轮询) 回调
数据结构 bitmap(长度为1024) 链表 红黑树+双向链表
最大连接数 1024(x86) / 2048(x64) 无限制 无限制
最大支持的fd数量 1024 65535 65535
fd拷贝 每次调用select,都要把fd集合从用户态拷贝到内核态 每次调用poll,都要把fd集合从用户态拷贝到内核态 fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝(共享内存)
工作模式 LT LT LT / ET(高速模式,非阻塞)
工作效率 $O(n)$ $O(n)$ $O(1)$

I/O多路复用

  • IO多路复用属于同步IO模型,实现一个线程可以监听多个文件句柄(fd)
  • 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作(读写过程是阻塞的)
  • 没有文件句柄就绪就会阻塞应用程序,交出CPU
  • 多路是指网络连接,复用是指同一个线程

I/O多路复用的三种实现方式

select和poll

select和poll属于无差别轮询。

  • select:当有IO事件发生时,$O(n)$的进行轮询,找出能够读出数据或者写入数据的流
  • 单个进程能打开的「fd」是有限的,默认值为1024
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,复制开销很大

image-20220829153209549

  • poll:poll和select本质上没有区别,都是将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,唯一区别再与poll没有最大连接数限制,因为它是基于链表来存储。
  • poll和select对「socket」的扫描是线性的,采用轮询的方式,高并发时效率较低

epoll

epoll可以理解为「event poll」,epoll是基于事件驱动(每个事件关联fd)的,只有当IO事件发生时,epoll才会将该事件通知我们,可以将时间复杂度降低到$O(1)$。

所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。

image-20220829173545738

  • 没有最大并发连接限制
  • 效率高,不会随着fd的数目增加而减少效率,只关心“活跃”的连接,只有活跃的fd才会调用callback函数。
  • 使用「mmap()」来进行内存拷贝,减少复制开销。
  • 缺点:只能在「linux」下工作。
  • 在并发不高的情况下,多线程+阻塞I/O的方式性能可能更好。

TCP怎么保证传输的可靠性?

保证可靠传输的手段

  • 1.将数据分割为多个数据包
  • 2.发送方给每一个数据包都进行编号,接收放按照序号对数据包进行排序、重组
  • 3.校验和:使用16位的校验和字段,保存TCP首部和数据的校验和。可以防止在传输过程中数据包被篡改,接受方如果计算出的校验和和数据包中的不同,则不会确认收到该报文而直接丢弃。
  • 4.接收方会丢弃重复的数据
  • 5.流量控制:TCP使用可变大小的滑动窗口协议来实现流量控制,接收方只允许发送方发送接收方接收窗口大小的数据量,接收方会通过确认报文里边的窗口字段来控制发送窗口的大小,以防止丢包。
  • 6.拥塞控制:当网络用塞时,将减少数据的发送。发送方维护一个拥塞窗口,拥塞窗口的大小取决于网络的拥塞程度,动态变化。发送方的发送窗口大小取拥塞窗口和接收窗口的较小值。
  • 7.ARQ协议:自动重传请求协议,发送方没发送完一个分组就停止发送,等收到接收方的确认后再发送下一个分组。停止等待ARQ协议、连续ARQ协议。
  • 8.超时重传:当 发送方 发出一个段后,它启动一个定时器,等待接收方端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

TCP拥塞控制?

TCP的拥塞控制采用了四种算法,即慢开始拥塞避免快重传快恢复

  • 慢开始:发送方刚开始发送数据时,拥塞窗口设置为1,每经过一个RTT,就把拥塞窗口加倍。
  • 拥塞避免:让拥塞窗口缓慢增大,每经过一个RTT就把发送的拥塞窗口加1
  • 快重传:用来快速恢复丢失的数据包。接收方每收到一个失序的报文段就立即发出重复确认,当发送方连续收到三个重复确认就直接重传接收方未收到的报文段,而不必等待为其设置的重传计时器。
  • 快恢复:当发送方连续收到三个重复确认时,就把慢开始门限减半,然后继续执行拥塞避免策略。

数据到达网卡后,怎么样传输到内存?

  • 1.数据包从外界网络进入到物理网卡
  • 2.网卡将数据包通过DMA的方式写入到指定的内存区域(该区域由网卡驱动分配注册)
  • 3.网卡通过硬件中断(IRQ)告知CPU有数据来了
  • 4.CPU根据中断表,调用已经注册的中断函数,这个中断函数会调用驱动函数中相应的函数来处理数据
  • 5.驱动先暂时禁用网卡中断,表明已经知道内存中有数据了,下次网卡收到数据包直接写到内存就可以了,不用再通知CPU,以提高效率。
  • 6.启动软中断来进行耗时的具体的IO操作。由于硬中断无法被打断,如果把具体IO部分交给硬中断来处理,会导致CPU被长时间占用,而无法响应其他中断。
  • 7.服务端在监听的端口接受到TCP报文,然后会对HTTP请求进行解析,并按照HTTP报文格式封装成HTTP Request对象,共上层使用。

操作系统

数据库

数据库索引类型?like什么时候走索引?

新建索引语法

# 添加索引
# 1.直接添加
CREATE INDEX indexName ON table_name(column_name)
# 2.通过修改标结构添加
ALTER table table_name ADD INDEX indexName(columnName)
# 3.创建表结构时直接添加
CREATE TABLE mytable(  
    ID INT NOT NULL,   
    username VARCHAR(16) NOT NULL,  
    INDEX [indexName] (username(length))  
);  

# 删除索引
DROP INDEX [indexName] ON mytable; 

按数据结构分类

索引类型 特点 数据库
B+树索引 使用广泛,适合范围查找和顺序查找,千万级的数据量,树高为3~5 InnoDB、MyISAM、Memory
Hash索引 只支持等值查询,哈希索引只保存哈希值和指针 Momery
Full-text索引 适用于文本很长的时候,且需要快速检索的情况 InnoDB、MyISAM

按物理存储分类

索引类型 特点
聚簇索引 聚簇索引的叶子节点存储了一行完整的表数据(B+树索引)
二级索引(辅助索引) 聚簇索引以外的其他索引,叶子节点不存储完整的表数据,可能需要一次回表查询

按字段特征分类

索引类型 特点
主键索引 建立在主键上的索引被称为主键索引,一张数据表只能有一个主键索引,索引列值不允许有空值,通常在创建表时一起创建
唯一索引 建立在UNIQUE字段上的索引被称为唯一索引,一张表可以有多个唯一索引,索引列值允许为空,列值中出现多个空值不会发生重复冲突
普通索引 建立在普通字段上的索引被称为普通索引
前缀索引 前缀索引是指对字符类型字段的前几个字符或对二进制类型字段的前几个bytes建立的索引,而不是在整个字段上建索引。前缀索引可以建立在类型为char、varchar、binary、varbinary的列上,可以大大减少索引占用的存储空间,也能提升索引的查询效率。

按索引字段个数分类

索引类型 特点
单列索引 建立在单个列上的索引被称为单列索引
联合索引 建立在单个列上的索引被称为单列索引
覆盖索引 该索引包含「查询中」用到的所有字段

索引最左优先匹配原则?

在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。

最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。
例如:如果建立(a,b)顺序的索引,我们的条件只有b=xxx,是匹配不到(a,b)索引的;但是如果查询条件是a = 1 and b = 2或者b=2 and a=1就可以,因为优化器会自动调整a,b的顺序,并不需要严格按照索引的顺序来;再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,因为c字段是一个范围查询,它之后的字段会停止匹配。如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

like语句会使用索引吗?什么时候会使用索引?什么时候不会?

一般情况下,百分号加在关键词后面会走索引,例如like "prefix%",而百分号加在前面是不走索引的,例如like "%prefix"。但是当查询为覆盖索引的时候,like无论如何都会使用索引,因为Mysql会优先走覆盖索引。

like如果走了索引,或者范围查询后面的字段还走不走索引?

假设abc加了索引,select a from table where a like “张三%” and b = 18,那b走不走索引?不一定,根据索引的底层原理,a字段是确定值,b字段才会走索引。所以范围查询后面不走索引只是一个偷懒的说法。如果范围查询过后的a是个确定值,那后面还是走索引的,否则不走。

索引失效的情况

  • 1.联合索引不满足最左匹配原则
  • 2.隐式类型转换导致索引失效
  • 3.使用select *
  • 4.索引列参与运算
  • 5.错误的like使用

MySQL事务

简单来说,事务就是一组增删改操作,这一组操作要么都执行成功,要么都不执行。

事务的特点

  • Atomicity:原子性,事务是不可以被分割的,事务的原子性要求动作要么全部完成,要么一个动作都不完成。
  • Consistency:一致性,事务执行前后,系统中的数据要保持一致。
  • Isolation:隔离性,事务在并发执行过程中,各个事务之前不能互相影响,各事务是独立的。
  • Durability:持久性,事务执行完毕后,其对数据库的修改是持久的,即使数据库发生故障修改也不应该丢失。

事务的原子性、隔离性、持久性都是为一致性服务的,最终结果就是要保证数据一致性

事务的隔离级别

事务隔离级别 脏读 不可重复度 幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复度(repeatable-read)
可串行化(serialiable)

MySQL日志

1.redo log

InnoDB独有的日志,是InnoDB支持崩溃恢复的手段。

redo log记录了对「数据页」的修改,属于物理日志。对数据页的操作会先记录到「redo log buffer」里边,buffer中的数据会定时或者在「事务提交」时进行刷盘,将buffer中的内容写入到redo log文件中。

redo log采用循环写模式保存log(多个文件,以环形数组的形式进行读写,读指针和写指针),原因是之前恢复的数据再保存在redo log中就没有意义了,而且这样也可以更加轻量的记录对数据的相关操作。

2.bin log:二进制日志|归档日志

bin log是MySQL的server层实现的,所有引擎都可以使用,记录的是语句的原始逻辑(例如:给id=2的这一行的c字段加1),属于逻辑日志,bin log会按照顺序记录所有涉及更新数据的逻辑操作。

采用追加写模式保存log,所有的修改记录都会按顺序保存在bin log中。

每一个事务都对应一个「bin log cache」,记录首先写在cache里边,等事务提交时再进行刷盘,将cache中的数据写入到「page cache」,最后再「fsync」到磁盘。

3.undo log:回滚日志

记录事务所进行的所有修改,只要事务没有执行成功,就会使用undo log进行回滚。

undo log记录的是逻辑日志,即每一步修改干了啥。

回滚优先于数据持久化到磁盘上。

两阶段提交

由于redo log和bin log的刷盘(写入)时机不一样,所以可能会导致数据不一致的情况发生,这时候就需要采用两阶段提交来保证数据一致性。

方案:将redo log的写入分为两个阶段,即「prepare」和「commit」阶段。在事务中,只有在bin log写入后,redo log才会进行commit。

redo log和bin log都可以表示事务的提交状态,两阶段提交的作用就是让两种log在逻辑上保持一致。

两阶段提交实现崩溃恢复:在写入redo log和bin log时,都会记录当前事务id。

  • 1.如果在写入redo log之前崩溃,此时redo log和bin log中都没有记录,是一致的,奔溃也没事。
  • 2.如果在redo log prepare阶段之后奔溃,恢复时,由于redo log没有被标记为commit,所以redo log中记录的事务id在bin log中无法找到,执行回滚操作。
  • 3.如果在写入bin log之后,redo log commit之前崩溃,恢复时,redo log根据事务id找到对应的bin log,这时直接进行commit即可。

  • 总的来说,奔溃恢复时,只要redo log不是出于commit阶段,就拿redo log中的事务id去bin log中查找,找的到就提交,否则回滚。

MVCC

MVCC,即多版本并发控制。MVCC的实现,是通过保存数据在某个时间点的快照来实现的。根据事务的开始的时间不同,每个事物对同一张表,同一个时刻看到的数据可能是不同的。

image-20220920231544752

MVCC原理:每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在undo log中。如果需要执行更新操作,会将原记录放入undo log中,并通过隐藏的回滚指针指向undo log中的原记录,其他事务需要查询此条记录时,便会查询undo log中这行记录的最后一个历史版本。

优点:最大的优点就是读不加锁,读写不冲突,极大的增加了MySQL的并发性。通过MVCC保证了事务的隔离性。

Redis核心数据结构

类型 说明 基本命令
Strings redis最基本的数据类型。
可以保存字节的序列,例如文本、序列化的对象、二进制数组等
字符串默认最大大小限制为512M
SET: 存储一个string value
SETNX: 只有当key不存在时才存储value,可以用来实现锁
GET: 获取key对应的value
MGET:一次性获取多个key-value
Lists 是一个string value组成的一个链表。经常被用来实现栈和队列。
最大长度限制为$2^{32}-1$个元素
LPUSH:从头部添加元素到链表
RPUSH: 从尾部添加元素
LPOP:从左边移除元素
RPOP:从右边移除元素
LLEN:获取链表长度
LMOVE:原子的将一个元素从一个列表移动到另一个列表
Sets sets是一个没有重复元素的无须集合。
可以用来去重,求交集、并集和差集。
SADD:往集合添加元素
SREM:移除指定的元素
SISMEMBER:检查一个元素是否在集合中
SINTER:求多个sets的交集
SCARD:获取集合的大小
Hashes 用来在key下面存储一个哈希表。
例如user当key,可以在user下,再存储user的name、age等和对应的值
HSET:为hash字段添加一个或多个key-value
HGET:获取对应字段的值
HMGET:返回多个字段对应的值
HINCRBY:将对应字段的值增加指定的值
Sorted sets 有序集合,当分数相等时,将按照名称的字典序排序。
可以用来实现排行榜和限流。
ZADD:添加一条记录,如果key已经存在,将更新key的分数
ZRANGE:返回排名在指定范围的元素
ZRANK:获取指定元素的排名
ZREVRANK:获取指定元素的逆序排名
Geospatial 用来存储地理位置:经、纬度
可以方便的找离的最近的位置、
半径范围内或者边界框内的坐标
GETADD:添加一条记录(经度在纬度之前)
GEOSEARCH:获取给定半径或者边界框范围内的坐标
HyperLogLog 是一种概率数据结构,可以用来估计集合中的数据量。
仅使用12KB的空间就能提供0.81%的标准差。
PFADD:添加一个元素
PFCOUNT:返回集合中的元素个数的估计值
PFMERGE:合并多个HyperLogLog
Bitmaps 是string类型的拓展,将string视为一个bit向量,并且可以进行位运算。
SETBIT:将某一位置为指定的值(0 or 1)
GETBIT:获取指定位置的bit值
BITCOUNT:获取1的个数
Streams A Redis stream is a data structure that acts like an append-only log.
You can use streams to record and simultaneously syndicate events in real time.
可以用来实现消息队列

Redis高可用方案

  • 1.数据持久化:AOF和RDB
  • 2.主从复制:读写分离,主节点进行写操作,从节点服务读操作
  • 3.哨兵模式:解决主从模式需要手动处理节点挂掉的场景,使用哨兵以一秒一次的频率来向redis节点发送ping命令,如果节点的回复时间超过最大阈值,就认为这个节点挂掉了。如果挂掉的是主节点,这时将会按照预定的策略来重新选择一个主节点,并自动进行切换。
  • 4.redis集群:拥有前面几个的所有特点,还提供了多个主从节点的集群功能,实现了真正意义上的分布式集群服务

Redis为什么使用单线程?为什么快?

1.Redis使用单线程的原因

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

  • 避免了并发环境下各种锁的性能消耗
  • 可以使用“单线程、多进程”的集群方案来充分利用多核CPU
  • 采用单线程,避免了不必要的上下文切换和竞争CPU的消耗

2.Redis快的原因

  • 最根本原因是redis是基于内存的,先天性就决定了快
  • 使用单线程,省去了上下文切换线程的时间
  • 使用了IO多路复用技术,可以处理并发连接
  • 使用了一些特殊的数据结构对数据存储进行了优化(跳表、压缩表)

Java八股

JDK和JRE的区别?

JRE(Java Runtime Enviroment) 是 Java 的运行环境,仅仅是一个运行Java程序所必须的环境集合,包含JVM标准实现及Java核心类库。

JDK(Java Development Kit) 是Java开发工具包,提供了Java的开发环境和运行环境

GC过程

image-20220919094048821

image-20220919091044353

对象存活判断

  • 引用计数法:存在对象循环引用问题
  • 可达性分析算法:GC root:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、被同步锁持有的对象

引用类型

  • 强引用:宁可OOM,也不会被回收
  • 软引用:只有内存不足时才会回收
  • 弱引用:对象生命周期短,只要发现便会被回收
  • 虚引用:任何时候都可能会被回收

GC算法

  • 新生代GC:标记-复制算法
  • 老年代GC:标记-清除、标记-整理

类加载过程

image-20220919095426518

双亲委派机制

classloader_WPS图片

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

反射

通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。

获取Class对象的四种方式

  • Class alunbarClass = TargetObject.class;
  • Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
  • TargetObject o = new TargetObject(); Class alunbarClass2 = o.getClass();
  • Class clazz = ClassLoader.loadClass("cn.javaguide.TargetObject");

使用Class对象生成实例:TargetObject targetObject = (TargetObject) tagetClass.newInstance();

单例模式

单例模式属于创建型模式的一种,在单例模式下,单例对象的类在任何时候都只能有唯一一个实例存在。

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

1.双重校验锁实现

public class Singleton {
    
    private volatile static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

JVM监控和故障处理命令

  • jps:类似于linux的ps命令,用于查看当前系统的java进程和进程id
  • jstat:用于收集HotSpot虚拟机各方面的运行数据,对资源和性能进行实时监控,如GC情况等
  • jinfo:显示虚拟机配置信息
  • jmap:打印某个java进程在内存中的所有对象情况
  • jhat:用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
  • jstack:生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合(排查死锁)

线程池

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处:降低资源消耗、提高响应速度、提高线程的可管理性。

线程池核心参数

  • corePoolSize:核心线程数量
  • maxmumPoolSize:最大线程数
  • keepAliveTime:当线程数大于核心线程数时,多余的空闲线程能存活的最长时间
  • unit:时间单位
  • workQueue:任务队列,用来存储等待执行的任务
  • threadFactory:线程工厂,用来创建线程,一般使用默认的
  • handler:拒绝策略,当提交的任务过多而不能及时处理时,可以制定策略来处置任务

volatile和synchronized

1.32位jdk中long和double存在的问题

long和double是64位的,在32位的jdk中完成write操作是需要两次操作的(每次执行32位)。也就是long和double的write操作是非原子性的。非原子的操作在多线程环境下会有线程安全问题。比如A,B两个线程同时的去修改long类型x的值,可能x的高32位是A设置的,低32位是B设置的,导致结果不是程序想要的。

多线程环境下推荐将long和double使用volatile修饰

  • Acquire语义: Acquire 逻辑上的操作序列为’操作-向后同步’。Acquire操作要求所有后续内存访问都不得被乱序调换到该操作前执行。
  • Release语义: Release 逻辑上的操作序列为’向前同步-操作’。Release操作要求所有前导内存访问都不得被乱序调换到该操作后执行

使用Acquire和Release来将long和double所占用的64位内存空间的前后各32位上锁,证了只能有一个线程能完整的修改long和double。

2.synchronized(this) 与synchronized(class) 之间的区别

  • 对象锁:在Java中,每个对象都有一个monitor对象,这个对象就是Java对象的锁,通常称作“内置锁”或者“对象锁”。类的对象可以有多个,所以每一个对象有其独立的对象锁,互不干扰。
  • 类锁:每个类也有一个锁,称为“类锁”,类锁是通过Class类的对象锁实现的,每个类只有一个类锁。

使用synchronized(this)可以获取到对象锁,而synchronized(class)可以获取到类锁。

修饰静态方法会获取类锁,修饰非静态方法时获取对象锁。

可以根据需要来将代码块进行修饰,以获取对象锁或者类锁。

框架八股

Spring boot

@Bean

生命周期:

@AutoWird

@Transactional

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    // 事务传播机制
    Propagation propagation() default Propagation.REQUIRED;

    // 隔离级别,Isolation.DEFAULT:表示和数据库使用相同的隔离级别
    Isolation isolation() default Isolation.DEFAULT;

    // 事务处理的最长时间
    int timeout() default -1;

    String timeoutString() default "";

    // 标识当前事务是否是只读的
    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

@Transactional失效的场景:

  • 使用在非public方法上,此时不支持回滚
  • 属性prapagation、rollbackFor参数设置错误
  • 异常被catch处理了,导致无法回滚
  • 数据库本身就不支持事务

系统设计

处理重复请求

由于黑客拦截重放攻击、前端故障、用户操作不当等,可能会导致服务端收到大量重复的请求,当请求涉及到写入时,会造成严重的后果。

解决方案:

方案1: 使用唯一请求编号去重

客户端每次请求时,都会带着服务端生成的唯一编号,服务端每次收到请求,先检查「redis」中是否已经存在该唯一编号,存在的话就认为是重复请求;否则接受请求,并将该编号写入redis。

缺点:不切合实际,大多数场景的请求都不会带有唯一编号。

方案2: 使用业务参数去重

将请求参数、方法等拼接成一个字符串,以唯一标识一个请求,例如:

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;

当参数过长时,可以使用参数拼接后的「md5」值来进行「KEY」的拼接,例如:

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParamMD5;

要注意的是,用来进行拼接的参数,要避免使用时间类型、地理位置类型等易变的数据类型,因为用户可能在1秒内点击三次,如果使用参数里的请求时间的话,这三次请求会被认为是不同的请求。

限流

限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。

方案1: Redis实现简单限流

场景:在指定时间内,最多只允许通过一定数量的请求。

方案:维护一个滑动窗口,窗口大小为时间区间大小,窗口内的数据条数要小于限定的请求数量。

具体过程:每来一个请求,就以当前时间的毫秒数作为zset中member的score值,member名称需要保证不重复即可。当当前时间戳和一分钟之前时间戳之间的member数量小于限定值时,就接受该请求,并将该请求写入zset,否则拒绝请求。

zcount key min max # 获取score值在区间[min, max]之前的元素数量

分布式锁

方案1:Redis实现分布式锁

  • 场景:用户抢优惠券,领完一张,优惠券数量减一,领完便不允许再抢。由于采用多台服务器来处理请求,如果不加以限制,便会导致数据不一致发生。
  • 方案:采用redis实现分布式锁,通过互斥的方式让服务器来对优惠券数量进行操作,防止多个客户端同时去更新优惠券数量。

设计锁的思路:

  • 1.互斥性:在任意时刻,只能有一个客户端能持有锁
  • 2.不能产生死锁:需要有释放锁的策略
  • 3.非剥夺:加锁和释放锁必须是同一个客户端

具体方案:

先使用setnx (set if not exists)来尝试将某个key设值(加锁),如果失败,就不断的重试。如果成功,便进行数据库的更新,然后将key从redis中移除(delete)。

上述方案可能会由于加锁的服务器异常而无法移除key,从而导致死锁,所以要为key设置一个过期时间(expire)来避免。

可以使用一个自己独有的value(雪花算法:分布式全局唯一ID、UUID)来标识某个key是不是自己生成的。

方案二:使用ZooKeeper实现分布式锁

方案三:使用数据库锁实现分布式锁

  • 悲观锁:使用select ... where name = 'lock'...forupdate获得排他锁,name字段必须要走索引,否则会锁住整张表。
  • 乐观锁:基于数据版本,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个”version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据。

Linux

常用指令

指令 说明 用法
top 查看总体的运行状态和CPU使用率
telnet 远程登录,判断远程服务器端口是否打开
free 查看内存使用情况
df 查看硬盘使用情况

参考资料

1.彻底理解 IO 多路复用实现机制

2.如何优雅处理重复请求/并发请求?

3.https://zh.wikipedia.org/zh-cn/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F