剑指Java面试
计算机网络
概述
协议三要素
- 语法,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。
- 语义,就是这一段内容要代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。
- 顺序,就是先干啥,后干啥。例如,可以先加上某个数值,然后再减去某个数值
OSI七层网络协议
应用层:HTTP等统一协议规范
表示层:信息的语法语义以及他们的关联,翻译,包装信息的“跨平台”
会话层:不同机器上用户之间建立及管理会话,使得程序自动收发包及自动寻址
- SSL、TLS、LDAP、RPC
传输层:分割数据,保证数据质量
- TCP/UDP协议
网络层:控制子网的运行,如逻辑编址,分组传输,路由选择
- IP/IPV6协议
数据链路层:物理寻址,数据格式化、错误检测与纠正(交换机)
- ARP,RARP地址解析协议
- PPTP、L2TP、L2F、ATMP隧道协议
物理层:物理设备的标准,数模转换,传输比特流(网卡、网线)
五层网络协议
只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层
- 应用层
- 传输层
- 网络层
- 链路层
- 物理层
TCP/IP协议
OSI协议的实现
- 应用层
- 传输层
- 网络层
- 链路层
TCP协议
简介
- 面向连接的,可靠的,基于字节流的传输层通信协议
- 将应用层的数据流分割成报文段并发送给目标节点的TCP层
- 数据包都有序号,对方确认则发送ACK确认,未收到则重传
- 使用校验和来检验数据在传输过程中是否有误
三次握手
ack的值永远是对回复的消息的序号
seq+1
- 第一次握手时,客户端发送SYN包(
syn=j
)到服务器并进入SYN_SEND
状态(服务器被动创建连接,客户端主动创建连接),“你听到了吗” - 第二次握手,服务器收到SYN包,确认客户端的SYN(
ack=j+1
),同时自己也发送一个SYN包(syn=k
),即SYN+ACK包,此时服务器进入SYN_RECV状态,“我听到了,你听得到我说话吗” - 第三次握手,客户端收到服务器发来的SYN+ACK包,向服务器发起确认包ACK(
ack=k+1
),此包发送完毕,客户端和服务器端进入ESTABLISHED
状态,完成三次握手,“听得到,现在可以开始说话了” - 三次握手的必要性,初始化
SequenceNumber
,后续使用
SYN超时
- 首次握手的隐患,SYN超时——Server收到Client的SYN,回复SYN-ACK的时候未收到ACK确认,Server会不断重试直到超时。
- 针对
SYNFlood
(SYNFlood溢出攻击
)的防护措施——SYN队列满之后,通过tcp-syncookies
参数回发SYNCookie
,若为正常连接则Client会回发SYNCookie
,直接建立连接
保活机制
- 向对方发送保活探测报文,如果未收到响应则继续发送
- 尝试次数达到保活探测数仍未收到响应则中断连接
四次挥手
- Client发送一个FIN报文(序号为u)用来关闭Client到Server的数据传输,Client进入FIN_WAIT1状态
- Server收到FIN后,发送ACK,序号为v,确认序号为u+1,Server进入CLOSE_WAIT状态
- Server发送一个FIN,序号为w,确认序号仍为u+1,可以继续向Client发送收尾数据,Server进入LAST_ACK状态
- Client收到FIN之后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,序号为u+1,确认序号为w+1
- TIME_WAIT状态的必要性:确保有足够的时间让对方收到ACK包;避免新旧连接混淆
- 为什么需要四次挥手:TCP是全双工的协议,发送方与接收方都需要FIN报文以及ACK报文
- 服务器出现大量CLOSE_WAIT状态的原因:客户端关闭连接,服务器忙于处理其它请求,没有及时关闭连接
滑动窗口
RTT,发送一个数据包到收到对应ACK的耗时
RTO,重传时间间隔,经过RTT动态计算
TCP使用滑动窗口做流量控制与乱序重排
- 保证TCP的可靠性
- 保证TCP的流控特性
AdvertisedWindow = MaxRcvBuffer - (LastByteRcvd-LastByteRead)
接收方的剩余处理能力EffectiveWindow = AdvertisedWindow-(LastByteSent-LastByteAcked)
发送方剩余发送能力
UDP协议
报文结构
- Source Port
- Destination Port
- Length
- Check Sum
- Data Octets(Optional)
特点
- 面向无连接
- 不维护状态,支持同时向多个客户传输相同的消息
- 数据报头短,额外开销较小
- 吞吐量只受限于数据生成速率,传输速率以及机器性能
- 尽最大努力交付,不提供可靠交付,不需要维护复杂的连接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并(UDP对应用程序发放的数据添加头部之后直接发送)
HTTP协议
超文本传输协议
- 支持客户‘服务器模式(请求响应模型)
- 可以传输任意类型的数据
- 无连接
- 无状态
请求结构
- 请求报文
- 请求行(请求方法 请求路径 协议版本)
- 请求头(键值对)
- 请求正文(POST请求使用),即使没有请求正文,回车换行仍然是必须的
响应结构
- 响应报文
- 状态行(状态码 状态码描述 协议版本)
- 响应头
- 响应正文
常用状态码
- 200,正常
- 400,客户端请求错误
- 401,请求未授权
- 403,拒绝提供服务
- 404,资源不存在
- 500,服务器内部错误
- 503 Server Unavailable:服务器当前时间不能处理客户端请求,一段时间之后可能恢复(服务器正在启动中或者连接池已满)
Get与Post
- HTTP报文层面:Get将请求信息放在URL,POST放在报文体中
- 数据库层面:Get符合幂等性和安全性
- 缓存:GET请求可以被缓存,被储存
Cookie与Session
Cookie
Cookie是服务器发送给客户端的特殊信息,以文本形式存储在客户端
客户端再次请求的时候,会把Cookie回发
服务器收到之后,会解析Cookie生成与客户端相对应的内容
Session
服务器的状态保持机制
解析客户端请求并操作
sessionId
,按需保存状态信息实现方式
- 使用Cookie实现,
JSESSIONID
- 使用URL回写,服务器发送给浏览器页面的所有URL都带有
JSESSIONID
- 使用URL回写,服务器发送给浏览器页面的所有URL都带有
- 使用Cookie实现,
HTTPS
HTTP:HTTP+TCP+IP
HTTPS:HTTP+SSL or TLS+TCP+IP
SSL&TLS
Security Socket Layer SSL安全套接字层,SSL3.0之后改为TLS
- 为网络通信提供安全以及数据完整性的一种安全协议
- 是操作系统对外提供的API
- 采用身份验证和数据加密保证网络通信的安全和数据的完整性
- 加密方式
- 对称加密
- 非对称加密
- 哈希算法
- 数字签名
数据传输过程
- 浏览器将支持的加密算法信息发送给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器
- 浏览器检验证书合法性,并结合证书公钥加密信息发送给服务器
- 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
- 浏览器解密响应信息,并对消息进行验真,之后进行加密数据交互
HTTPS安全隐患
- 浏览器默认填充http协议,请求需要进行跳转,有被劫持的风险
- 可以使用HSTS(HTTP Strict Transport Security)优化
Socket
Socket是TCP/IP协议的抽象,是操作系统对外开放的接口,与TCP/IP协议没有必然的联系
IP:port
- 服务器:
socket->bind->listen->accept->recv->close
- 客户端:
socket->connect->send->close
数据库
索引
数据结构选择
- 二叉查找树:树可能不平衡、树的高度太高
- B树:
- B+树:
- 非叶子节点都用来作为索引,数据都保存到叶子节点中,读写代价更低;效率更稳定,因为查询路径类似
- 所有叶子节点都通过链表连接起来,便于范围查找
密集索引和稀疏索引
非主键索引存储相关键位和其对应的主键值,并没有数据的物理地址,需要根据主键值进行回表查询。
密集索引文件中的每个搜索码值都对应一个索引值
- InnoDB如果有主键,则其为密集索引;否则第一个唯一非空索引为密集索引。如果以上条件都不满足,会生成一个隐藏主键。
稀疏索引文件只为索引码值的某些值建立索引,MyISAM都是稀疏索引
SQL调优
- 查看慢日志
- 使用explain分析SQL
- 修改sql或者尽量让SQL走索引
最左前缀匹配原则
- mysql会一致向右匹配知道遇到范围查询就停止匹配,如
a=3 and b=4 and c>5 and d=6
,对(a,b,c,d)建立联合索引,d无法使用索引。对(a,b,d,c)建立联合索引,则可以全部走索引。 - =与in可以乱序,因为优化器会帮你优化为可以识别的形式。
锁
MyISAM与InnoDB关于锁的区别
- MyISAM默认使用表级锁,不支持行级锁,也不支持事务。适用于频繁执行count语句的场景,无事务场景,以及增删改不频繁,但是查询频繁的场景。
- InnoDB默认使用行级锁,也支持表级锁。
显式锁
- select for update,排他锁
- select in share mode,共享锁
事务特性
- Atomic原子性
- Isolation隔离性
- Durability持久性
- Consistency一致性
- A、I、D是手段,C是目的
事务隔离级别
- READ-UNCOMMITTED
- READ-COMMITTED
- REPEATABLE-READ,可重复读,InnoDB默认。(查询是可重复的,看到的是自己事务开启时的旧值,但是去更新时会使用最新值,如update xx set a = a+1;注意不要自己读取旧值再保存,即update xx set a = aa)
- SERIALIZABLE
事务并发访问导致的问题
- 更新丢失,因为数据库都会自动加锁,所有隔离级别均可避免。但是在应用程序中需要自行避免。
- 脏读,读到另外一个事务未提交的数据。
- 不可重复读,某事务多次读取同一个数据时看到的数据不一致。可怕之处是会读取到一个旧的数据并以此进行更新。
- 幻读,某事务多次读取同一批数据时看到的数据不一致。
InnoDB在RR级别下即可避免幻读
- 表象:快照读(非阻塞读),伪MVCC(undo log只是串行记录历史版本,没有真正共存);本质:next-key锁(行锁+gap锁)
- gap锁,索引树中插入新数据的空隙,但是不包括数据本身
Gap锁
- 对于主键索引以及唯一索引的Gap锁
- 如果where条件全部命中,则不会使用Gap锁,只会加行锁
- 如果where条件部分命中或者全部不命中,如where id in(1,3,5),3不存在,则是部分命中,则加Gap锁
- Gap锁会使用于非唯一索引或者不走索引的当前读中
当前读与快照读
- 增删改,以及加了显式锁的查,都是当前读,都是读取最新值
- 不加锁的非阻塞读为快照读,基于MVCC机制
RC、RR级别如何实现快照读
- 数据行隐藏列DB_TRX_ID、DB_ROLL_PTR(回滚指针指向undo中修改前的行)、DB_ROW_ID
- undo日志
- read view,比较数据行DB_TRX_ID与当前最大活跃事务
- RR级别下,事务开启后的第一个快照读会创建一个read view。RC级别每次select都是创建一个新的read view。
关键语法
GROUP BY
- 单表查询下,select语句中选出的列要么是group by里面用到的列,要么就是带有列函数的列,否则会报错
- 列函数对于group by子句定义的每个组各返回一个结果
HAVING
- 通常与group by一起使用
- WHERE!过滤数据行,HAVING过滤组
- SQL顺序,WHERE>GROUP BY>HAVING
SQL书写顺序以及执行顺序
1 | (8) SELECT (9)DISTINCT<Select_list> |
- FROM->ON->JOIN->WHERE->GROUP BY->HAVING->SELECT->DISTINCT->ORDER BY->LIMIT
- ON和WHERE的最大区别在于,如果在ON应用逻辑表达式那么在OUTER JOIN中还可以把移除的行再次添加回来,而WHERE的移除的不可挽回的
- 如果应用了GROUP BY子句,那么DISTINCT是多余的
Redis
缓存中间件
MEMCACHE
支持简单数据类型
不支持数据持久化储存
不支持主从
不支持分片
REDIS
数据格式丰富
支持数据磁盘持久化储存
支持主从
支持分片
10W+QPS(query per second)
单线程,意思是单个主线程,但是有些工作会开启新的线程
使用多路IO复用模型,非阻塞IO
IO多路复用
FD(File Descriptor)文件描述符,一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射,用一个整数来表示
Select系统调用,监听可读文件描述符的个数并返回,O(n)保底方案
Epoll
Kqueue
Evport
基于React设计模式监听IO事件,文件事件处理器
数据结构
STRING
二进制安全,可以保存二进制数据等任意类型的数据,最大512M
操作是原子性的
Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示;除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF(持久化模块中的AOF缓冲区)
获取字符串长度(SDS O(1)/C 字符串 O(n))
传统的C字符串: 使用长度为N+1 的字符串数组来表示长度为N 的字符串,所以为了获取一个长度为C字符串的长度,必须遍历整个字符串。SDS:SDS 的数据结构中,有专门用于保存字符串长度的变量,我们可以通过获取len 属性的值,直接知道字符串长度C 字符串 不记录字符串长度,除了获取的时候复杂度高以外,还容易忘了为字符串重新分配足够的空间,从而导致缓冲区溢出。Redis 中SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当我们需要对一个SDS进行修改的时候,redis会在执行拼接操作之前,预先检查给定SDS空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作
SDS:对SDS进行拓展,则需要进行空间的拓展,这时候redis 会将SDS的长度修改为N字节,并且将未使用空间同样修改为N字节,此时如果再次进行修改,因为在上一次修改字符串的时候已经拓展了空间,再次进行修改字符串的时候如果发现空间足够使用,因此无须进行空间拓展。通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次
HASH
特别适合储存一个对象
渐进式 rehash,采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量
LIST
双端队列
按照String元素插入顺序排序
特点
- 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
- 无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止
- 表头和表尾:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)
- 长度计数器:链表中存有记录链表长度的属性 len
- 多态:链表节点使用 void* 指针来保存节点值,并且可以通过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数
- 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
SET
通过HASH实现
元素不允许重复
无序
可以方便地求交并补操作
ZSET
有序集合,double类型的score作为排序标准,越小越前
Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构
HyperLogLog
- 用于计算基数
Geo
- 地理位置信息
底层实现
简单动态字符串,SDS
链表
字典
跳跃表
整数集合
压缩列表
对象
内存淘汰机制
当超过最大内存
maxmemory
时,redis将按照以下策略淘汰数据redis确定驱逐某个键值对后,会删除这个数据,并将这个数据变更消息发布到本地(AOF 持久化)和从机(主从连接)
volatile-lru
:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰volatile-ttl
:从已设置过期时间的数据集中挑选将要过期的数据淘汰volatile-random
:从已设置过期时间的数据集中任意选择数据淘汰allkeys-lru
:从数据集中挑选最近最少使用的数据淘汰allkeys-random
:从数据集中任意选择数据淘汰no-enviction
:禁止驱逐数据(默认)
场景问题
海量数据查询某一固定前缀Key
海量数据下使用keys关键字会造成服务器卡断
- 使用
SCAN cursor [match pattern] [count num]
,局部查询,会返回游标以及数据,将游标传入可继续查询,一轮一轮地获取 - 基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
- 以0作为游标开始一次新的迭代,直到命令返回游标0则完成一次遍历
- 不保证每次执行都会返回指定数量的元素,支持模糊查询
- 一次返回的数量不可控,只能是大概率符合count参数
实现分布式锁
分布式锁需要解决的问题
- 互斥性
- 安全性,解锁必须上锁人
- 死锁
- 容错
初期使用SETNX(SET if Not eXists)
SETNX key value
,如果key不存在,则创建并赋值,如果存在则不进行操作EXPIRE key seconds
,设置有效期,解决SETNX长期有效的问题,相当于自动解锁缺点:不满足原子性,单个命令满足原子性,其组合不一定满足。有可能SETNX成功后EXPIRE失败导致锁无法释放。
改进
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX秒,PX毫秒;
NX只有在键值不存在的时候才对键进行操作,等效于SETNX;XX只在键值存在时,才对键进行操作
成功返回“OK”,失败返回“nil”
大量key同时过期(雪崩)
- 集中过期会造成短暂的卡顿现象
- 解决方法是在设置key过期时间时,加上随机值
实现异步队列
使用LIST
使用List作为队列,RPUSH生产队列,LPOP消费消息
缺点:没有等待队列就直接消费,拿到空数据
弥补:应用层使用Sleep机制调用LPOP重试
使用BLPOP
BLPOP key [key...] timeout
,阻塞直到队列有消息或者超时
PUB/SUB主题订阅者模式
一次生产,多次消费
- 发送者(pub)发送消息,订阅者(sub)接受消息
- 订阅者可以订阅任意数量的频道
subscribe topic
publish topic content
- 缺点:无状态,不保证可达
缓存雪崩
- 如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中
- 解决方法:在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期
缓存穿透
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义
解决缓存穿透也有两种方案:
- 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层!
- 当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间。
缓存击穿
- 缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞
- 解决方法:热点数据手动失效
缓存与数据库双写一致
问题
一般来说,执行更新操作时,我们会有两种选择:
先操作数据库,再操作缓存
先操作缓存,再操作数据库
无论我们选择哪个,我们都希望这两个操作要么同时成功,要么同时失败。所以这会演变成一个分布式事务的问题
先更新数据库,再删除缓存
- 如果原子性被破坏了:第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据
- 如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
- 缓存刚好失效
- 线程A查询数据库,得一个旧值
- 线程B将新值写入数据库
- 线程B删除缓存
- 线程A将查到的旧值写入缓存
- 删除缓存失败的解决思路:
- 将需要删除的key发送到消息队列中
- 自己消费消息,获得需要删除的key
- 不断重试删除操作,直到成功
先删除缓存,再更新数据库
如果原子性被破坏了:第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的
并发环境下,会出现缓存不一致问题
- 线程A删除了缓存
- 线程B查询,发现缓存已不存在
- 线程B去数据库查询得到旧值
- 线程B将旧值写入缓存
- 线程A将新值写入数据库
解决方法:将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化
持久化
RDB
快照,保存某个时间点的全量数据快照
1 | save seconds write_times |
- 手动触发RDB持久化的方法
save
,阻塞服务器进程进行备份bgsave
,Fork一个子进程来创建RDB文件,非阻塞lastsave
上一次备份时间
- 自动化触发RDB持久化的方法
- 配置文件里面的
save m n
定时触发(使用的是BGSAVE) - 主从复制时,主节点主动触发
- 执行Debug Reload
- 执行Shutdown而且没有开启AOF持久化的时候
- 配置文件里面的
- BGSAVE实现
- 检查是否没有其它RDB/AOF子进程
- 执行
rdbSaveBackground
方法 - Fork(Linux系统调用,创建进程,实现了copy on write 即写时复制:多个调用者共享资源,只有在写的时候才去拷贝一份资源)
- 由于COW机制,父子进程共享一个rdb文件,当父进程继续处理请求且遇到写请求时,复制资源,当子进程完成临时文件的写入时,用临时文件替换原来的历史文件
- RDB文件的载入是自动的
- 缺点是全量备份、有可能丢失最近一次快照期间的数据
AOF
Append Only File,保存写状态
记录下除了查询以外的所有数据变更指令
以append方式追加到AOF文件(增量)
配置
1
2appendonly yes // 开关
appendfsync allways|everysec(默认)|no // 配置将备份文件写入磁盘的时机:每次更新都写入|每秒|由OS决定日志重写(合并无用命令,只保留最新结果)解决文件不断增大的问题(创建,COW)
- fork一个子进程
- 子进程把新的AOF写到一个临时文件里面,不依赖原来的AOF文件
- 主进程持续将新的变动同时写入内存以及原来的AOF文件(双写)
- 主进程获取到子进程重写AOF完成的信号,只往新的AOF同步增量变动(双写切换为单写)
- 使用新的AOF文件替换掉旧的AOF文件
有点:可读性高,数据不容易丢失
缺点:文件体积大,恢复时间长
数据恢复
- 如果AOF文件存在,则直接加载AOF文件(日志回放)
- 如果AOF文件不存在,尝试加载RDB文件
- 如果两个文件都没有,无法进行数据恢复
混合模式
RDB+AOF,redis4.0以后的默认方式
Copy On Write
在
继续提供服务
的情况下,如何保证快照是精确的
?写时复制:续提供服务,只有当有人修改当前内存数据时,才去复制被修改的内存页,用于生成快照。(内存复制的时间,向后推迟;内存粒度,拆解更细)
Redis 中,执行 BGSAVE 命令,来生成 RDB 文件时,本质就是调用了 Linux 的系统调用 fork() 命令,Linux 下 fork() 系统调用,实现了 copy-on-write 写时复制
Redis fork()出一个子进程后,主进程仍然处理客户端发来的读写请求,子进程完成读内存并写到rdb文件,在此过程中如果某部分数据子进程还未来得及写入rdb文件,主进程对其进行了写的更新操作,这部分写入的新数据仍然会被子进程写到rdb备份文件里
Pipeline
类似于Linux的管道
- 批量执行指令,节省多次IO往返的时间
主从同步
原理
- 一个master写,多个slave读(读写分离)
- 最终一致性(主从数据最终趋于一致)
全量同步过程
- slave发送sync命令到master
- master启动一个后台进程,将redis中的数据快照保存到文件中
- master将保存数据快照期间收到的写命令缓存起来
- master完成第二步的写文件操作之后,将该文件发送给slave
- slave将新的aof文件替换掉旧的aof文件
- master将期间缓存到的增量写命令发送给客户端
增量同步
- master接收到操作指令,判断是否需要传播到slave
- 将操作记录追加到aof文件
- 将操作传播到其它slave:对齐主从库,往响应缓存写入指令
- 将缓存中的数据发送给slave
哨兵
解决主从同步master宕机之后主从切换的问题
- 独立的进程
- 监控多个主从服务器节点状态
- 通过api向管理员或者其它应用程序发送故障通知
- 自动故障迁移:主从切换(流言协议+投票机制)
流言协议
- 每个节点随机地与对方通信,最终所有节点的状态达成一致
- 种子节点定期随机向其他节点发送节点列表以及需要传播的消息
- 不保证信息一定会传递给所有节点,但是最终会趋于一致(类似于病毒传播)
集群
原理
- 分片:按照某种规则去划分数据,分散储存在多个节点上
- 常规的按照哈希无法实现节点的动态增删
一致性哈希算法
普通哈希算法的缺点(对节点数量取模):添加服务器时导致哈希值不一致,导致找不到正确的服务器,会导致缓存雪崩,一致性哈希算法可以减轻这种问题都不是完全解决
- 将哈希值空间组织成虚拟的圆环,对2的32次方取模
- 将数据key使用相同的哈希函数计算出哈希值,顺时针选择最近一个服务器节点保存
- 缺点:哈希环的数据倾斜,大量数据分布在环的一侧。解决方法是引入虚拟节点,每个真实节点映射到多个虚拟节点,将一个真实节点缓存在环上各个位置,查找的时候,找到虚拟节点,再映射到真实的节点
Linux
find命令
查找特定文件
1 | # 格式 |
grep命令
查找文件里符合条件的字符串
1 | # 格式 |
管道
- 将命令连接起来,将上一个命令的标准输出(错误输出无法处理)作为下一个文件的标准输入。
1 | # 过滤 --only-matching |
awk命令
一次读取一行文件,按照输入分隔符(默认是空格)进行切片,产出多个组成部分,将切片保留在内建的变量中
1 | # 格式 |
sed命令
stream editor,流编辑器,适合对行进行处理
1 | # 格式 |
JVM
Java平台无关性
java将源码转换为字节码,由不同平台的JVM解析
java文件经过javac编译为class文件
class文件可以经过javap命令反编译
JVM如何加载class文件
- Class Loader,加载class文件到内存
- Execution Engine,对命令进行解析
- Native Interface,融合不同开发语言的原生库为Java所用
- Runtime Data Area,JVM内存空间结构模型
反射机制
- 动态获取对象信息,调用对象方法的功能称为Java的反射机制。(将类中的成分映射为对象,如Class、Method、Field)
ClassLoader
- ClassLoader负责将Class文件里面的二进制数据流转载进系统,然后交给Java虚拟机进行连接,初始化等操作。
- ClassLoader种类
- BootStrapClassLoader,C++编写,加载核心库java.*
- ExtClassLoader,加载拓展库javax.*
- AppClassLoader,加载程序所在目录
- 自定义ClassLoader,定制化加载,继承ClassLoader
- 实现findClass,寻找类文件,返回defineClass函数的返回值
- 字节码增强ASM可以修改字节码的流
类加载过程
类加载方式
隐式加载:new
显式加载:loadClass、forName
类装载过程
- 加载:生成Class对象
- 链接
- 校验:检查正确性与安全性
- 准备:为类变量分配存储空间并设置初始值
- 解析:符号引用解析为直接引用
- 初始化:执行类变量赋值和静态代码块
forName与loadClass的区别
- forName得到的class是已经初始化完毕了的。如JDBC的初始化。
- loadClass得到的class是还没有完成链接的,应用于SpringIOC的懒加载。
双亲委派模型
- 自底向上检查类是否已经加载,自顶向下尝试加载类
- 是多种ClassLoader相互协作的机制,避免加载多份同样的字节码
内存模型
线程私有
- 程序计数器
- 虚拟机栈
- Java方法执行的内存模式,包含多个栈帧,栈帧包含局部变量表、操作数栈、动态链接,返回地址等
- 本地方法栈
- 主要用于native方法,与虚拟机栈类似
线程共享
- 元空间metaSpace,保存类信息
- 堆
- 常量池
- 对象实例的分配空间
元空间metaspace与永久代PermGen的区别
- 二者都是方法区的实现
- 元空间使用本地内存,永久代使用jvm内存
- 永久代的缺点
- 常量池存储在永久代中,容易溢出
- 类和方法大小难以确定,不利于指定永久代的大小
- 永久代为GC带来不必要的复杂性
- HotSpot特有实现,不兼容其它JVM
JVM参数
- Xss:每个线程虚拟机栈的大小
- Xms:堆的初始值
- Xmx:堆的最大值
垃圾回收
标记算法
- 引用计数法:通过判断对象的被引用数量,无法检测循环引用的情况
- 可达性分析:判断对象的引用链是否可达
- GCRoot
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
- 本地方法栈中JNI(native方法)的引用对象
- 活跃线程的引用对象
- GCRoot
回收算法
- 标记-清除算法
- 缺点:内存碎片多
- 复制算法:分为对象面以及空闲面
- 优点:解决内存碎片化问题,顺序分配内存,适用于对象存活率低的场景(新生代)
- 缺点:内存利用率低
- 标记整理算法:标记清除+移动,适用于对象存活率高的场景
- 优点:解决内存碎片化问题
- 缺点:消耗大
- 分代收集算法
- 按照对象生命周期不同划分为不同的区域以采用不同的垃圾回收算法
- 分代
- 年轻代:1Eden,2Survivor
- 老年代:
- 进入老年代的方法
- 经历一定的Minor GC
- Survivor放不下的对象
- 大对象直接进入老年代
- GC的分类
- Minor GC(Young GC),未成年人
- Major GC(Old GC),成年人,只有CMS会进行单独的Old GC
- Full GC,整堆回收
- 老年代空间不足
- 永久代空间不足
- CMS GC报错xxx
- Minor GC晋升到老年代的平均大小大于老年代剩余空间
新生代收集器
- Serial,单线程,复制算法,Client模式默认
- ParNew,多线程,复制算法
- Parallel Scavenge,关注吞吐量,复制算法,Server模式默认
老年代垃圾收集器
- Serial Old,单线程,标记整理算法,Client默认
- Parallel Old,多线程,标记整理算法,吞吐量优化
- CMS收集器,标记清除算法,Stop the World时间短,第一个并发处理器
- 初始标记,STW
- 并发标记
- 并发预清理
- 重新标记
- 并发清理
- 并发重置
- G1收集器,复制+标记整理算法
- 整堆规划,划分为多个大小相等的Region
- 不区分新生代与老年代
- ZGC
引用
强引用
- 内存溢出都不回收
软引用:对象有用但是非必须
- 内存空间不足时,GC回收
- 可以用来实现高速缓存
弱引用
- 无论内存是否不足,GC回收时都会回收
- 也能存活一段时间,因为GC频率不高
- 可以用来实现高速缓存
虚引用
- 必须结合引用队列使用,用于跟踪对象被垃圾收集器回收的活动,起哨兵作用:程序判断引用队列即可判断对象是否被回收
引用队列
- 实际上是一个链表头,队列由Reference的next维护
Java多线程与并发
进程和线程的区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 所有进程相关的资源都被记录在PCB中
- 线程只由堆栈寄存器,程序计数器和TCB组成
- Java的进程与线程
- 每个进程对应一个JVM实例,多个线程共享堆内容
- Java采用单线程编程模型,程序会自动创建主线程。所以UI编程时耗时的操作需要在子线程中执行
线程start和run的区别
- 调用run的时候,会使用主线程运行,只是Thread的一个方法的普通调用
- 调用start时,会使用子线程运行
- Thread.start->JVM_startThread->thread_entry->Thread.run
Tread与Runnable
- Runnable只是有一个run方法的接口,多线程最终要依赖Thread类
- Thread可以接收Runnable作为构造函数入参
- Thread是实现了Runnable接口的类,使得run支持多线程,推荐使用Runnable
如何获得线程的返回值
- 主线程等待法
- 使用Thread的join方法阻塞当前线程等待子线程处理完毕
- 通过Callable接口
- FutureTask
- 线程池,submit方法返回值为Future
线程的状态
- NEW,创建后尚未启动的线程的状态
- Runnable,包含Running和Ready状态
- Waiting,无限等待,需要显式唤醒
- Object.wait
- Thread.join
- LockSupport.park
- Timed Waiting,限期等待
- Thread.sleep
- Object、Thread、LockSupport加上超时参数
- Blocked,阻塞,等待获取排他锁
- Terminated,已终止线程
sleep和wait的区别
Object.wait,Object wait() 方法让当前线程进入等待状态。直到其他线程调用此对象的notify() 方法或 notifyAll() 方法。可以实现等待,通知效果。
Thread.sleep可以在任何地方调用,Object.wait只能在同步方法或者同步块中,因为只有获取锁之后才能释放锁
sleep只让出CPU,wait不仅让出CPU,还让出锁
notify和notifyAll的区别
- 对象锁池与等待池
- 锁池:线程等待获取对象锁的地方
- 等待池:线程调用了对象的wait方法后进入,不再去竞争锁
- notifyAll会让等待池的全部线程进入锁池
- notify只随机选择一个线程进入锁池
yield函数
- Thread调用该方法时,表示自己愿意让出CPU,但是有可能被线程调度器无视。
- yield对锁的行为没有影响
interrupt函数
- 通知线程应该中断了
- 如果线程处于阻塞状态,那么线程将立即退出阻塞状态,抛出一个InterruptedException
- 如果线程处于正常活动,那么仅仅会将其中断标志设置为true
- 需要被通知程序的配合,线程应该在循环中检查中断标志
synchronized
互斥锁的特性
- 互斥性:同一时间只允许一个线程持有某个对象锁
- 可见性:获得锁的线程对于共享变量的操作应该对其它线程立即可见
锁的分类
对象锁,锁是当前对象的实例对象
同步代码块
同步非静态方法
类锁,锁是当前对象的类对象
同步代码块
同步静态方法
同一个类的不同对象锁互不影响,类锁和对象锁互不影响
synchronized原理
- 对象头
- Monitor:每个Java对象天生自带了一个锁,也成为内部锁
锁的优化
- 自旋锁
- 自适应自旋锁
- 锁消除,如StringBuffer的锁消除
- 锁粗化,扩大锁范围
锁升级
- 无锁-》偏向锁-》轻量级锁-》重量级锁
ReentrantLock
- 可以细粒度控制加锁解锁时机
- 可以设置公平锁,按照锁的顺序按照先后调用lock的顺序
内存可见性
内存模式
- 线程不能直接操作主内存的变量,只能操作本地内存中的变量
- 如果A操作的结果需要对B操作可见,则A与B存在happens-before关系
- volatile,轻量级同步机制
CAS
- 支持原子更新操作,适用于计数器、序列发生器等场景
- 属于乐观锁机制
- atomic包基于CAS实现lock-free
线程池
- 使用Executors创建不同的线程池满足不同场景的需求
- ExecutorService,具备管理执行器和任务生命周期的方法,提交机制更加完善
- ScheduledExecutorService,支持Future和定期执行任务
- 可以使用ThreadPoolExecutor构造自己的线程池
- corePoolSize
- maximumPoolSize
- workQueue
- keepAliveTime
- threadFactory
Fork/join
- 使用Work-Stealing算法:某个线程从其它队列窃取任务来执行
线程池大小的选定
- cpu密集型:线程数=核心数+1
- IO密集型:线程数=核心数*(1+平均等待时间/平均工作时间)
Java常用类库
Spring
IOC 控制反转
DI依赖注入:把底层类作为参数传递给上层类,实现上层对下层的控制
- Set注入
- 接口注入
- 注解注入
- 构造器注入
DL依赖查找
IOC容器核心
- BeanDefinition Bean的定义
- BeanDefinitionRegistry 提供向IOC容器注册BeanDefinition对象的方法
- BeanFactory
- 提供IOC的配置机制
- 包含Bean的各种定义,便于实例化Bean
- 建立Bean之间的依赖关系
- 负责Bean生命周期的控制
- Spring框架的基础设施,面向Spring
- ApplicationContext
- 管理、装配Bean(@Configuration+@Bean,或者扫描自动装配)
- 加载资源文件
- 注册监听器,实现监听机制
- 面向使用Spring的使用者
SpringBean作用域
- singleton
- prototype
- request
- session
- globalSession
AOP
三种织入方式
- 编译时织入:AspectJ
- 类加载时织入:AspectJ
- 运行时织入:Spring采用的方式
元素
- Aspect:通用功能的代码实现,实体
- Target:被织入Aspect的对象,实体
- Join Point:可以作为切入点的机会,所有方法都可以作为切入点
- Pointcut:Aspect实际被应用在的Join Point,支持正则表达式
- Advice:类里面的方法以及这个方法如何织入到目标方法的方式
- Weaving:Aop的实现过程
AOP实现
- 目标类是接口,默认是JDK代理,否则利用继承使用CGLIB实现
Spring实现
- getBean时返回的是代理对象
- 所以Spring AOP只支持Spring管理的bean