java后端笔记

网络

BIO、NIO、IO多路复用模型、epoll

阻塞IO模型(Blocking IO) 和 非阻塞IO模型(Non-Blocking IO)
两者本质就是等待数据就绪时,用户线程是阻塞还是非阻塞。就绪后拷贝都是阻塞的,同步IO
BIO
BIO在要不到数据时会休眠,直到数据到达内核缓冲区,然后线程变为内核态,从内核缓冲区拷贝到用户缓冲区,才会继续执行。
NIO
NIO在要不到数据的时候,内核会告知用户线程数据未到达内核缓冲区,此时用户线程可以干任何事情,但是要每隔一段时间问内核是否到达,到达后,和BIO一样,用户线程变为内核态,拷贝,才会继续执行。

IO多路复用模型
多路复用是在NIO的基础上,单个用户线程原本只询问单个数据状态,多路复用则是单线程询问多个数据状态。
即多路数据复用在同一用户线程上处理。

select、poll、epoll
三者都是IO多路复用模型的唤醒方式。

区别是:
其实三者在数据就绪,用户线程被唤醒后,都需要轮询哪些socket就绪,但是前两者是轮询所有socket从中找出就绪的,而epoll唤醒线程后只要判断就绪链表不为空即可(因为epoll会只把就绪的socket fd放到就绪链表)
以及,前两者在用户线程每次调用该函数时,都需要用户线程把所有fd集合拷贝到内核态,而epoll只要把新增fd拷到内核态即可。

相同点:
三者都是Linux对IO多路复用的唤醒机制,三者的本质上都是同步IO,即数据就绪后,用户线程都会切到内核态,阻塞地把数据从内核空间拷到用户空间。

以上BIO、NIO、IO多路复用都是同步IO,由用户线程参与数据拷贝的工作。

Java中的NIO其实是New IO,本质上是IO多路复用。

Redis

缓存穿透

查询不存在的数据,也没写入缓存,导致每次查询控制都直接访问数据库
1、缓存空值;
2、布隆过滤器:用多个hash code为1标识值存在,查询时都为1,表示有可能存在,其中一个为0,则一定不存在。

缓存击穿

缓存过期时,刚好有大量访问该key,都会访问数据库
1、缓存未命中,多个线程争抢互斥锁,申请到的才访问DB并加入缓存,否则睡眠后重新尝试获取缓存和锁
2、不设置redis TTL,而是在value写入逻辑过期时间,手动判断过期。如果过期,则获取锁,返回旧值,由子线程查询DB并更新缓存。若获取锁失败,也返回旧值。(其实着中国逻辑,可以理解为缓存是一直存在的)
在这里插入图片描述

缓存雪崩

大量key同时失效;redis宕机
1、设置key的TTL时添加随机值
2、使用集群提高redis可用性
3、给缓存业务添加降级限流策略(nginx、Spring Cloud Gateway)(对多种问题都适用)
4、给业务添加多级缓存(Guava、Caffeine)

双写一致性

先介绍业务,根据业务选择实际策略:强一致?最终一致?
1、先更新数据库再删除缓存:因为删除缓存的速度远快于DB,因此更新完DB后再删除缓存时被中断的可能性小。而先删除缓存再更新数据库,有可能在删除完缓存,在等待DB的时候被中断,导致其他线程用旧的值重新赋上
2、延时双删:删缓存 - 修改数据库 - 子线程延时删缓存。延时需要提供时间给数据库做主从同步,且预留时间给其他读线程把旧值写入缓存,再删除旧值。会有一段时间脏数据,时间难控制。
3、强一致:读写都加锁。读用共享锁,写用排他锁。
4、最终一致(略延迟):1、修改数据库 - MQ - 更新缓存;2、Canal:修改数据库 - Canal监听binlog - 更新缓存,基于mysql主从实现
在这里插入图片描述
在这里插入图片描述

持久化

1、RDB(Redis Database Backup file),所有数据都记录到磁盘,fork子进程共享主进程的内存数据库(页表),写入RDB文件
进程都没办法直接操作物理内存,只能操作虚拟内存,并通过页表进行映射。因此子进程只需要拷贝页表即可共享主进程内存,fork速度快,再写入新的RDB文件替换旧文件。
在备份过程中如果有新的写入,那主进程会备份原始数据,对备份数据进行读写。
在这里插入图片描述

2、AOF(Append Only File),记录所有写命令,默认关闭。AOF文件会比RDB大得多。
AOF重写:多次写的命令,只保留最后一次生效的命令
在这里插入图片描述

数据过期删除策略

1、惰性删除:用到时判断过期才删除,对CPU友好,但可能占用内存;
2、定期删除:每隔一段时间检测过期并删除。采用随机抽取N个并删除其中过期的key,如果其中过期key超过25%,再次抽取执行;
3、立即删除:在设置一个key的过期时间时,创建一个定时事件,当到达过期时间时,由事件处理器执行key的删除。能保证内存尽快释放,但在高负载和同时大量key过期时,会影响性能;
通常1和2配合使用。如果中间因为策略遗漏了过期key的删除,还有数据淘汰机制兜底。

数据淘汰

缓存过多,内存被占满
默认noevictrion,不删除任何数据,直接报错
还有对allkeys、volatile维度的ttl、random、lru、lfu的删除策略。

allkeys是对全体keys的策略,
volatile是只对设置了TTL的keys的策略,便于保留置顶数据。

ttl是TTL小的先淘汰,
random是随机,
lru Least Recently Used,最近访问时间越久的先淘汰,
lfu Least Frequently Used,最近访问频率低的先淘汰

分布式锁

结合业务:定时任务、抢单、幂等性
Redis的setnx命令,SET if not exist,根据返回值判断设置成功/失败,但控制锁的时长不好控制。太短可能提前释放,太长可能客户端宕机,没有手动释放,要等一段时间。
可以用redisson的看门狗机制,只要不显式设置过期时间,就会触发该机制,后台还是会设置过期时间,并且开启看门狗线程,定期的续期该分布式锁。都是基于lua脚本完成的,可以保证原子性。

redisson分布式锁可重入,在背后做了记录,key是锁的key,field是线程名,value是重入数
在这里插入图片描述

主从数据的锁一致问题

Redis主节点获取锁,还没同步给slave就挂了。
发生概率低,可以用红锁,即在多个Redis实例(n / 2 + 1)上加锁。但性能低,官方也不推荐。
AP思想:高可用redis,CP思想:强一致用zookeeper

集群方案

主从复制:高并发

读写分离,写master,读slave,一般一主多从,无法保证高可用
在这里插入图片描述

数据同步

通过replication id判断是同一个数据集,不一致则RDB全量同步,同时记录下同步期间的命令至repl_baklog,随后也进行同步。
否则增量同步,根据offset去获取repl_baklog中之后的命令数据。
在这里插入图片描述

哨兵模式:Sentinel,高可用

Sentinel集群持续监控主备设备的状态,若发现故障,会提升slave为master并通过Redis客户端。其中 通过ping命令监控。
在这里插入图片描述

主观下线:一个Sentinel认为master下线
客观下线:超过一定数量(quorum)的Sentinel认为该实例主观下线,则该实例客观下线。quorum最好超过Sentinel实例数量一半。
选主规则:
从前往后的顺序判断
1、主从断开时间超过阈值的淘汰,证明丢失数据过多;
2、选优先级高的;
3、选offset值高的;
4、选index大的(实例序号而已,无实际意义)

脑裂

master和sentinel连接出现问题,但和Redis客户端连接正常,sentinel会在slave中重新选主。那么在客户端连接新的master之前,成功发给旧master的信息都会被吞掉,旧master恢复连接变为slave后,会被新master同步并刷为旧数据。
在这里插入图片描述

解决:
1、配置最少slave数为1,否则不让写
2、降低主从节点的同步间隔,减少数据丢失,并尽快识别出没有slave节点

分片集群:海量数据存储、高并发写

多个master,每个master都有slave,master之间通过ping检测健康,客户端能访问任意一台master,都会被转发到正确的master上。
在这里插入图片描述

每个master分配一定区间的哈希槽,根据请求计算hash,转发到对应master上。如果请求提供了有效部分,用有效部分计算hash,否则用key计算。
在这里插入图片描述

华为云的Redis的主备模式,实际上是使用哨兵模式来管理,只是客户不感知哨兵存在。

为什么单线程这么快

1、纯内存操作;
2、单线程避免了上下文切换、线程安全问题;
3、使用IO多路复用模型,非阻塞IO;
4、内置了多种优化后的数据结构

因此性能瓶颈是网络IO而非运行速度,IO多路复用模型就是为了解决这个瓶颈问题的。

linux内存分为用户空间和内核空间,用户进程要访问硬件设备(网卡)时,需要:用户缓冲区 - 内核缓冲区 - 硬件设备 的交互,反之亦然。中间的效率问题涉及到:
1、等待数据就绪;
2、数据拷贝
在这里插入图片描述

三种IO方式:
1、阻塞IO:阻塞等待数据就绪,阻塞等待数据拷贝完成
2、非阻塞IO:非阻塞等待数据,但会不断询问是否就绪,导致CPU空转;阻塞等待数据拷贝完成
3、IO多路复用:一次性获取所有数据已就绪的socket列表,单线程循环地阻塞拷贝已就绪的数据。

监听socket、获取通知的方式:
1、select;2、poll;3、epoll
前两种只会通知用户进程有socket就绪,需要用户进程遍历socket列表,确认哪个就绪。epoll会在通知socket就绪的同时,就把已就绪的socket写入用户空间。
IO多路复用用的就是epoll

Redis网络模型

IO多路复用+事件派发。
每个socket会处理不同的请求,把准备就绪的请求派发给对应的处理器。
6.0之后引入多线程,因为瓶颈是网络IO,所以对涉及网络读写的请求、回复模块使用多线程解析,但具体操作命令还是单个主线程,线程安全。
在这里插入图片描述

大key和热key

大key:
key的值占用内存过高,包括字符串超长、hash值过多、列表过长等
会导致某个redis节点内存飙高,读取缓慢、占用带宽,甚至导致不断淘汰key或者拒绝添加,删除大key也会使主库阻塞过长引发同步中断或主从切换
解决办法:本质上是避免存入大key,根据业务避免,以及对hash进行拆分,对list实施淘汰策略

热key:
key的访问量极高,通常是大促等某时间段访问量飙升
对于某key的飙升,通常固定了读取节点,没办法在集群内做负载均衡,使得节点资源压力大,可能影响其他业务。可能引发缓存击穿
解决办法:对热key用不同的哈希值进行备份,让key分布在集群的不同分片中

MySQL

慢查询

定位:
端到端定位:Arthas、Prometheus、Skywalking
mysql定位:开启慢查询日志记录(开关、时间阈值)

分析:
使用EXPLAIN或DESC获取SQL语句的执行计划
EXPLAIN可以查看索引使用情况、是否有回表查询、命令的查询类型(const、eq_ref…)等
在这里插入图片描述
在这里插入图片描述

索引

帮忙mysql高效索引数据的数据结构,有序的B+树。InnoDB存储引擎的索引结构就是B+树。

B+树

1、每一次子树查询都是一次随机IO操作,B+树更矮胖,故IO次数更少;
2、数据只存在于叶子节点,每次访问次数相近,查询性能更稳定;
3、叶子节点间有双相指针,范围查询更方便;
4、因为根节点能放更多索引,因此删除节点时更不容易发生树重构

B+树vs红黑树:
红黑树是二叉树,树的深度会很大,IO次数更高,因此适合作为内存结构存储。
https://blog.csdn.net/kaiwen2001/article/details/125775109
MySQL的最小存储单元是页,每页16KB,假设主键是BIGINT,每个指针6B,一个关键字是主键加子树指针的组合,即8+6=14B,一个内部节点约存储关键字数量为16KB/14B=1170个。

聚簇索引、非聚簇索引

聚簇索引:Clustered index。将数据存储和索引结构放到一起,索引结构的叶子节点存储了行数据。这种索引必须有,且只有一个。
二级索引、非聚簇索引:Secondary index。将数据和索引分开存储,叶子节点只存放主键,用于与数据关联。这种索引可以存在多个。

回表查询

在二级索引取到主键,再拿主键去聚簇索引取行数据。
在这里插入图片描述

覆盖索引

查询时使用了索引,并且需要的列,在该索引中能全部找到。不用回表查询。

超大分页处理

使用覆盖索引+分页,获取需要的主键id,作为子查询和原表关联
select xx, xx from table where id = (select id from table where a order by b limit 100000, 50);
其中a和b为联合索引

创建索引的原则

1、针对数据量较大,查询频繁的表字段;
2、常作为查询条件where、排序order by、分组group by的字段建立;
3、尽量选择区分度高的字段作为索引,尽量建立唯一索引。区分度越高,效率越高;
4、如果是字符串类型,长度较长,可以根据字段特点建立前缀索引;
5、尽量使用联合索引,减少单列索引,可以实现覆盖索引,避免回表;
6、控制索引的数量,索引越多,数据增删改的效率会降低;
7、如果索引列不能存储NULL,在建表时明确NOT NULL,优化器可以更好地确定哪个索引查询更高效

索引失效

1、违反最左前缀法则
跳过左边的索引 查询会失效
如果有左边的索引,但中间跳过了,那么只有左边的索引会生效
2、范围查询右边的列,会索引失效(所以经常范围查询的自动要放在索引最右)
3、在索引列上进行运算操作(加减、substring函数等)
4、字符穿不加单引号,可能会因为自动类型转换而失效。
5、模糊查询,以%开头的like模糊查询会失效,仅进行尾部模糊的不会失效;
6、where条件中使用了or

优化经验

表结构:
1、使用合适的字段类型;
2、字段若not null,显式设置为not null;

sql语句:
1、避免select *导致回表;
2、避免索引失效;
3、尽量union all代替union,后者会多一次过滤;
4、尽量inner join代替left、 right,如必须使用,建议小表驱动大表。inner join会优化为小表驱动大表,减少IO次数;

架构:
1、主从读写分离;
2、使用缓存技术;
3、合理分库分表;
4、使用连接池连接;

buffer pool

主内存中的区域,用于缓存磁盘上的数据。
执行增删改查时,会先操作缓冲池的数据,若无该数据则会加载。
会以一定频率刷新到磁盘,以减少磁盘IO

page

InnoDB存储引擎的磁盘管理的最小单元,每个页的默认大小16KB

redo log

记录事务提交时page的修改内容,用于保证事务的持久性(D)

redo log也分两部分,buffer存在内存,file存在磁盘。在事务提交后,会把修改信息都存到file中落盘,用于刷新脏页到磁盘若发生错误时,进行数据恢复

刷新buffer pool到磁盘是随机磁盘IO,效率较低,而报错redo log file时,内容都是追加的,是顺序磁盘IO,效率较高
这种叫WAL(Write-Ahead Logging):先写日志

redo log file会有两份文件交替保存
在这里插入图片描述

undo log

作用:1、回滚;2、mvcc
记录的是操作的相反操作,如delete和insert;update也会记录相反的操作。

用于保证事务的一致性(C)和原子性(A)

参考:https://zhuanlan.zhihu.com/p/636972366

按照范围:全局锁(库)、表锁、行锁(行锁、间隙锁、临建锁)
排斥方式:共享锁(S Lock)、排他锁(X Lock)
自增锁

事务

ACID

原子性
一致性
隔离性
持久性
在这里插入图片描述

并发事务问题

脏读:读到没提交的数据
不可重复读:读到已提交的数据,因此第二次读和第一次的结果不一样
幻读:查询不到数据,但插入时又发现已存在。即读不到已存在的数据(解决了不可重复读),在插入时才发现
三种问题至上而下的发生,解决前者,可能有后者

隔离级别:
各种隔离级别,能解决对应问题,但会引入下一种问题
MySQL默认级别:RR
在这里插入图片描述

mvcc

multi-version concurrency control,多版本并发控制
主要依赖于:隐式字段、undo log、readView

隐式字段

每个数据都会有这些隐藏字段:事务id(自增)、回滚指针(指向这条记录的上一个版本,配合undo log)、隐藏主键(若无主键则用这个)

undo log版本链

不同或相同的事务对同一条记录做修改,会生成一条undo log版本链,尾部是最旧的记录
在这里插入图片描述

当前读

读的是记录的最新版本,读取时会对记录加锁,如select for update、update、insert、delete都是

快照读

简单的select就是快照读,读的是记录的可见版本,有可能是历史记录,不加锁,非阻塞读

readview

快照读时mvcc提取数据的依据,记录和维护了当前活跃的(未提交的)事务id
记录了4个字段:当前活跃的事务id集合、最小活跃事务id、预分配的事务id(当前最大id+1)、创建readview的事务id(发起读操作的事务)
定义了一定的规则,用于判断版本链中哪个事务id的记录可以读。
顺着undo log版本链,从头到尾,根据链中的事务id,和readview中记录的字段,用一系列规则判断是否用该记录

RC:当前事务中,每次select都会生成一个readview

RR:开启事务后第一个select才会创建readview,后续select复用

RC的优势:
1、RR存在间隙锁,死锁概率比RC大
2、条件未命中索引时,RR会锁表,RC只锁行
3、RC的半一致性读增加了update的并发

在这里插入图片描述
在这里插入图片描述

主从同步

通过binlog实现。
binlog记录了所有DDL(数据定义语句)和DML(数据操作语句)
master的语句写入binlog,slave的IOThread线程会去同步该binlog到本地的relay log,SQLThread线程会读取该log并执行新的语句
在这里插入图片描述

分库分表

优化通常是有需求才会做的,因此要根据业务出发,考虑优化方式。
分表:通常是数据量驱动的。垂直分表和水平分表。可以举历史项目作为例子,根据时间戳分表,例如每月一表。可以结合贵公司的业务场景来假设。
分库:数据库的连接数是有上限的,通常是并发驱动的,库内的数据尽量有关联性,例如用户表,可以把历史用户放在旧库,新用户及其关联数据放在新库,同时修改业务代码适配单库到多库的过程,所以最好设计初期就考虑到扩展性。
变更过程可以挑选流量较少的时间段,停服变更,变更后进行回归测试和新功能验证

RabbitMQ

相比于其他MQ产品,与编程语言无关,是通用协议,虽然吞吐量一般(10w qps),其实基本够用,而且消息延迟微秒级,远高于其他毫秒级

kafka吞吐量非常高,常用于日志收集(日志量大)

producer - exchange - queue - consumer

公司可能一个MQ集群给多个不同服务使用,MQ可以用virtualHost进行隔离,相当于不同database

交换机只服务路由消息,不负责存储

exchange和queue的命名建议用 . 分割,虽然不强制

AMQP

Advanced Message Queuing Protocol
消息队列协议,与语言无关

work queues

任务模型,就是普通的队列方式
多个消费者绑定到同一个队列,轮训消费队列的消息。
与消费效率无关,因此消费慢的可能消息堆积,可以yml文件加上preFetch为1,确保同时最多给该消费者1个消息,处理完才获取下一条

交换机类型

Fanout

广播,广播到所有绑定的队列。如支付成功,广播给订单、积分等

Direct

定向,exchange绑定queue时可以绑定key,交换机会把带key的消息发给相同的queue。
例如交易成功则发给订单、积分,失败则只发给订单

Topic

话题,和direct类似,区别是key可以是用.分割的多个单词,且可以通配符,#表示0或多个单词,*表示1个单词

简单场景用direct,复杂场景用topic

代码声明

交换机、队列、绑定关系通常都在Configuration中声明,但是要声明很多bean

可以在RabbitListener时,原本queues指定队列,可以bindings把交换机、类型也声明了

消息转换器

发送的时候会判断传入Object是否Message类型,是则发送,否则用MessageConverter转换
默认是用JDK序列表Object,会有问题:体积大、可读性差、有漏洞
可以用Jackson转换器

保证不丢失

生产者 - MQ:通过确认机制让生产者确认成功。若失败,回调函数重发、记录日志、保存DB定时重发
MQ:交换机、队列、消息持久化。默认是内存存储,持久化可落盘
MQ - 消费者:确认机制,确认后才会删除消息

在这里插入图片描述

重复消费

1、每条消息设置id
2、幂等性方案

死信交换机(延迟队列)

延迟队列 = 死信 + TTL
对于需要实现延迟功能的消息,发送到没有消费者的队列,并设置TTL(需要延迟的时间),在消息过期后,会被转发到死信交换机,此时才会被消费。

死信消息:不会被消费,直接丢弃掉
产生:1、消费者拒绝;2、过期消息无人消费;3、队列满了,最早的消息成为死信;

如果配置了死信交换机,死信消息会投递至此,否则被丢弃

TTL可以对消息设置,也可以对用于存放延迟消息的队列设置,最终以小的TTL为准

在这里插入图片描述

解决消息堆积

1、增加消费者
2、消费者开启线程池
3、扩大队列容量
4、可以使用惰性队列:接收消息直接落盘(磁盘空间更大),要消费时才加载到内存

高可用

普通集群(低可用)

每个节点存储各自的队列,以及相互的队列引用信息。消费者若访问错了节点,会传递到对应节点消费。但若是宕机,会导致消息丢失,低可用

镜像集群(常用)

交换机、队列、消息在各节点之间同步备份。每个节点都有自己的队列,并备份另一个节点的消息

在这里插入图片描述

仲裁队列(Raft协议+镜像集群)

主从同步使用Raft协议,强一致,避免同步时宕机导致丢失

kafka

在这里插入图片描述

优势

消息确认机制、高可用、高性能

消息不丢失

生产者 - Broker:1、设置异步发送;2、设置回调重复;3、日志记录
Broker:分区包含leader和follower,需要ack确认才证明存储成功
Broker - 消费者:topic存在不同分区(Partiton),每个分区放在一个Broker内。
消费者组在同一个topic内,负责消费各自的分区,每个分区维护自增的offset,消费者根据offset来取对应消息。
但是在消费者组重平衡的时候(消费者2宕机,由消费者1顶上)可能重复消费或丢失数据,因为默认每5s自动提交一次offset,可能在两次提交中间宕机导致offset不对齐。
解决办法:采用手动提交。用异步+同步提交的方式,先异步提交避免阻塞,再同步提交确保准确。

在这里插入图片描述
在这里插入图片描述

保证有序性

如果多分区,分区间没办法保证顺序性,可以把有顺序需求的消息放到同一个分区内,用offset递增保证

高可用

集群

集群中多个broker提供服务

分区复制机制

一个topic分多个分区(P),存储在不同的broker中,每个broker除了存储自己的分区外(leader),还会备份其他broker的分区(follower)。
follower也分两种,一种是leader同步复制的(ISR in-sync-replica),这种保证准确,效率较低,一种是异步复制的(普通follower)。在leader宕机时,选主优先用ISR

在这里插入图片描述

数据清理机制

文件存储机制

topic是逻辑概念,消息真正是存储在分区内,每个分区内都是分段存储中,每个段(Segment)都有一样的文件格式
分段对于删除更方便,整个文件删除,不用在大文件中检索再删部分。文件名包含偏移量,查询也更方便。

在这里插入图片描述

清理策略

1、清理过期的消息,可配置过期时间
2、topic的大小超过一定阈值,则删除最旧的消息(默认关闭)

高性能

1、消息分区:可以扩展多设备
2、顺序读写:文件是追加写入,顺序读写比随机读写高效
3、页缓存:把磁盘数据缓存到内存,从磁盘访问改为内存访问
4、零拷贝:减少上下文切换和数据拷贝。
内存分为用户空间、内核空间。kafka要交互硬件,需要:kafka - 页缓存 - 磁盘 - 页缓存 - kafka - Socket缓存区 - 网卡。
现在把动作委托给OS,因此直接从页缓存 - 网卡
5、消息压缩:使用算法减少磁盘IO和网络IO
6、分批发送:多个消息打包发送,减少网络开销。可以设置分批大小和等待时间

在这里插入图片描述
在这里插入图片描述

zookeeper

数据存储

zk内部以树结构存储数据,每个节点是znode

一个znode包含4部分:
data数据、aci权限、stat元数据、child子节点
ls -s /test:可以看/test节点的详细信息

znode类型

持久节点、持久序号节点、临时节点

持久序号节点

create -s /test就会创建/test0000005,后面是递增的事务id,常用于分布式锁

临时节点

create -e,在当前会话关闭时销毁,可以作为注册中心用于服务注册和发现。客户端会持续发送session id心跳(ping),过期后会删除session id关联的临时节点

临时序号节点

临时的分布式锁

容器节点

create -c
没有任何子节点时,会被zk定期删除

TTL节点

可以指定到期时间,到期后zk删除

持久化机制

数据是存放在内存的

事务日志

把命令记录下来,相当于redis的aof

数据快照

每隔一段时间把内存的快照保存在快照文件,相当于redis的rdb

恢复时先用快照加载内存,再用日志进行增量恢复,恢复速度更快

删除节点

delete删除节点
deleteall删除非空节点
delete -v 版本号,如果版本号和节点当前版本号一致才能删,乐观锁思想

权限控制

对当前会话创建用户,创建节点时指定用户和权限

watch机制

注册在znode上的触发器,znode变化时通知客户端

get -w /test:对/test创建监听,但只会告诉你事件类型。监听是一次性的,可以每次都通get -w来续监听。
客户端是通过NIO来监听节点回调。

ls -w:可以监听子节点的变化
ls -R -w:可以递归监听

作用:分布式协调组件

一致性

Watch机制。多实例部署,zk可以监听实例的内部变量,并通知其他实例同步修改

分布式锁

Redis分布式锁是AP模型,特殊场景可能不一致(?)
zk分布式锁是强一致

读锁

创建一个临时序号节点,数据为read,把所有子节点(临时序号节点)中序号最小的拿出来(因为序号是递增的),如果最小的是读锁就能加(根据递归思想),如果是写锁则阻塞等待,通过watch机制当最小序号节点变化时通知当前节点

写锁

临时序号节点,数据是write,获取所有子节点。如果自己是最小序号则成功,否则证明前面还有锁,watch最近序号节点,变化后再判断自己最小

羊群效应(惊群效应)

100个写锁都在监听最小序号节点,只要这个节点释放,所有监听事件都会被触发,可能会对zk造成压力

解决:写锁只监听离自己序号最近的节点状态,就能让这批写锁有序触发

无状态化

多实例可以把数据统一存在zk,实现多实例的无状态

Curator客户端

对zk封装了大部分功能,如leader选举、分布式锁等

集群

Leader:能处理所有事物,一个集群只有一个leader
Follower:只能读,参与Leader选举
Observer:只能读,提升读性能,不参与选举

数据文件夹要带上myid文件,内容为id值
配置文件要加上:server.{myid}={server_ip}:{数据同步port}:{选举port}:{身份}

客户端连接集群时,要把集群所有ip都写入

ZAB协议

Zookeeper Atomic Broadcast
保证数据的一致性,解决了zk的崩溃恢复、主从数据同步问题

节点有四种状态:
Looking、Following、Leading、Observing

集群上线时Leader选举

第一轮投票:
looking投票是根据事务id+myid来生成自己的选票的,选票发给集群内每个节点,选票大的放入当前节点的投票箱
(事务id大的证明执行动作更大,一致则看myid)
此时票数过半则投票箱中做Leader,否则第二轮投票

第二轮投票:
每个节点把投票箱中最大的发给其他节点

因此两轮的选票不同

如果节点加入时,集群已经有过leader了,则加入的直接作为follower

奇数个节点更好判断leader,好计算过半节点

宕机时leader选举

主节点和所有从节点都会简历socket双向通道,以ping方式发送心跳,从节点定期从socket获取ping。
若主节点宕机,则socket断开,从节点获取时抛出异常,从following变looking,此时集群不对外提供服务

数据同步

写数据:
如果写到从节点,从节点会把消息转发给主节点,因此写操作都是由主节点发起的。
主节点广播到所有节点(包括自己),先把数据写到数据文件(非内存)中,写完后发ack给leader,leader会返回ack。主节点收到过半ack后,再广播commit把数据写入内存。

外部请求都是读的内存数据,因此中间态(commit之前)读不到未写入内存的数据。

这是分布式常用的二阶段提交,先写文件,再写内存。

这里用超半数而非全部节点,提供了写性能,因此确实有可能节点数据同步较慢

NIO、BIO

NIO:
与客户端连接、Watch机制都是NIO
新版本NIO改用nety做

BIO:集群选票用BIO投票

CAP

zk不是强CP模型,其实是AP模型,在commit之前都没新数据,之后都有新数据,但是commit之前的ack只要半数即可,因此有可能少部分未ack也未commit。

zk是顺序一致性,即集群内事务id一定单调递增,且一定执行了前者才执行后者。

Java

BIO、NIO、AIO

https://juejin.cn/post/6844903985158045703
BIO:同步阻塞IO
NIO:同步非阻塞IO。等待的时候该线程可以做其他事情。用户线程每隔一段时间就查看一批IO中哪个完成了就去继续。
可以用做:网络IO编程、文件IO、数据库操作、多媒体处理
AIO:异步非阻塞IO。NIO虽然非阻塞,但轮训还是会少量阻塞。AIO委托操作系统,缓冲区就绪后通知用户或调用回调函数即可

Java主要用NIO,AIO依赖于操作系统,支持的OS很少。

CPU飙升

根据进程id,用ps找到哪个线程cpu飙高,把线程id从十进制转十六进制。
用jstack pid,能看到每个线程运行到哪里,就能知道飙高的线程在哪行代码导致的。

内存

在这里插入图片描述
在这里插入图片描述

程序计数器:

线程私有的,因此线程安全。保证正在执行的字节码的行号(地址),在多线程切换时,能在断点继续运行

堆:

线程间共享的区域,存放实例对象、数组等
堆分为:年轻代(1/3)、老年代(2/3)
年轻代分为:伊甸园区(Eden)(8/10)、幸存者区S0(1/10)、S1(1/10)

在一次垃圾回收后,会从Eden和S0拷贝到S1,此时S0为空。第二次从Eden和S1拷贝到S0,此时S1为空。对象在幸存者区经过15次垃圾回收后,会去到老年代

元空间:(方法区)

保存类信息、静态变量、常量、字节码
JDK7及之前,是作为永久代在堆,在8及之后是元空间在本地内存,防止类加载不可控导致堆OOM

栈:

每个线程运行的内存(线程安全),先进后出,存放局部变量、参数等

直接内存:

不属于JVM内存结构,不由JVM管理,是操作系统内存。常见于NIO操作时,用于数据缓冲区。其分配回收成本较高,但读写性能高

常规IO:Java是没有直接操作文件的权限的,需要从用户态到内核态,委托操作系统来复制。涉及到 文件 - 系统缓冲区(Java不能运行) - Java缓冲区(代码中new byte[])
NIO: 只要 文件 - 直接内存(Java和OS都能访问)。少了多余的复制。因此适合文件IO操作。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

类加载器:

把.class字节码加载到JVM中,供JVM使用

双亲委派模型

加载某个类,会先委托上级加载器加载,一直往上,除非上级没加载,子加载器才会尝试加载。
这样可以避免重复加载类,并且保证核心类库不会被修改。

在这里插入图片描述

类装载过程

加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载

垃圾回收

定位垃圾对象

1、引用计数法
可能实例间相互引用导致内存泄漏

2、可达性分析算法
从GC Root能找到的对象,就不是垃圾

GC Root:
1、栈变量引用的对象
2、类静态变量引用
3、常量引用
4、JNI引用(Native方法)

回收算法
标记清除算法

1、可达性标记垃圾
2、回收标记垃圾

内存碎片化严重

标记整理算法

在标记清除算法的基础上,把存活对象移动到一端。

效率有一定影响,多用于老年代。

复制算法

内存分为大小相同的两个区域。
1、标记
2、把存活对象复制到另一个区域,复制同时整理到一端。

效率较高,内存利用率较低。多用于年轻代。

GC类型
MinorGC

young GC,新生代的GC,暂停时间短(SWT:Stop-The-World)

MixedGC

新生+老年代【部分区域】GC
G1收集器特有

FullGC

新生+老年代【完整区域】GC
STW长

垃圾回收器
串行垃圾回收器(serial)

适合堆较小的或个人电脑,GC时只有一个GC线程在工作,所有Java线程都STW

并行垃圾回收器(parallel)

JDK8默认回收器
GC时多个GC线程工作,所有Java线程STW

并发垃圾回收器(CMS)

CMS:Concurrent Mark Sweep

使用标记清除算法,针对老年代的回收器。
最大特点是GC时应用仍正常工作。
总体是:并发标记(与用户线程并发) - 重新标记(修复并发时用户线程变动的对象,需要STW) - 并发清除
详细待补充

G1垃圾回收器

JDK9默认。
主要是复制算法
分三部分:
1、年轻代GC
2、YoungGC+并发标记
3、混合GC

在这里插入图片描述

1、不按照比例划分年轻、老年代,而是把堆均分为多个小块,挑选空闲区域作为Eden,GC时把有效对象复制到S,后续Eden和S的有效对象复制到其他S。
当对象超过复制阈值,会放入老年代。

2、当Old超过阈值45%,触发并发标记(标记Old中存活对象),此时无需STW,然后再进行重标记解决漏标问题,此时STW(因为并发时对象状态可能变)

3、选出回收价值高的Old区域(存活对象少,能释放更多内存)(不会一次性回收所有Old),连同新生代(Eden+S)一起GC,就是MixedGC。然后逐渐回收价值次高的Old…

如果对象太大,大于一个区域,会分配多个连续区域存放

回收优先级高的区域,即Gabage First,简称G1

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

引用类型

强引用

GC不回收有强引用的对象

软引用(SoftReference)

仅有软引用的对象,首次GC不回收,再次GC时才清理

弱引用(WeakReference)

仅有弱引用的对象,GC时就回收

ThreadLocal的key是弱引用(ThreadLocal对象),value是强引用,因此ThreadLocal对象必须手动绑强引用,否则GC时key被回收,会导致value内存泄漏

虚引用

配合引用队列使用,用于释放直接内存(使用的外部资源)
后续待补充

集合

ArrayList

底层维护的是数组,在每次插入前会判断当前数组长度是否满足,满足则插入,否则扩容,即创建一个原数组长度1.5倍的数组,原数组拷贝过来,释放旧数组。数据插入新数组,后续维护新数组

LinkedList

底层是双向链表,每个节点包括值、前驱节点、后节点地址
查找复杂度:O(n)、找完的增删复杂度:O(1)

HashMap

底层使用hash表实现,是一个数组加链表或红黑树

在插入时计算key的hash值,若插入位为空则插入,否则判断key是否相同,相同则覆盖,否则证明hash冲突,把hash值相同的节点放入链表。若链表过长(超过8),则进化为红黑树,短了则退化为链表。
计算hash值采用二次hash法,相比于hash计算,hash值分布更均匀,降低冲突概率。

JDK1.7只用链表解决冲突,JDK1.8使用链接/红黑树。

ConcurrentHashMap

JDK1.7:分段数组+链表,用ReentrantLock锁的是HashEntry数组,粒度大(锁住一段数组)
JDK1.8:和HashMap一致,数组+链表/红黑树,用CAS+Sync锁保证线程安全。空节点就用CAS,链表/红黑树就用Sync锁住首节点,粒度小,并发高。
在这里插入图片描述
在这里插入图片描述

并发编程

1、原子性
2、可见性
3、有序性(指令重排只会保证单个线程的最终一致性,不保证多线程)

JMM

Java Memory Model
JAVA自己提供的一套内存模型,能屏蔽操作系统的内存模型实现跨平台。
有主内存作为各线程的共享内存,每个线程有各自的工作内存,备份了共享变量副本,需要通过共享内存相互同步。
在这里插入图片描述

JAVA内存区域和JAVA内存模型的关系?

JAVA内存区域划分了数据的存储区域,例如堆存放对象实例;
JAVA内存模型和并发编程、跨平台相关,规范了线程和主内存间共享变量的使用规范,增强了程序的可移植性。

happens-before原则

为了平衡JMM下的并发问题和编译器、处理器的优化性能,只要不改变程序的执行结果(单线程和正确的多线程),不管怎么优化重排都行,否则禁止重排

CAS

Compare And Swap,在无锁下保证变量操作的原子性,JUC(Java.Util.Concurrent)内、AQS(AbstractQueuedSynchronizer)框架、AtomicXXX类都用到。
在修改共享变量时,对比共享内存的值和自身缓存的值是否一致,一致则合入共享内存,否则拷贝进来 重新操作再对比,直到一致为止,因此也叫自旋锁。
底层是依赖OS的CAS操作。
是乐观锁的思想。

可能有的问题:
1、ABA问题:仅对比值一致,不代表值没有被其他现场修改过,有可能被改为B又改回A了。
解决思路是:变量内加入版本号、时间戳等唯一标识符。AtomicStampedReference的compareAndSet就是检查引用一致和标志是否符合预期。
2、循环时间长:JVM支持暂停,延迟尝试。

volatile

表示这个共享变量是不稳定的

1、可见性:保证了可见性,(多线程的)共享变量被volatile修饰,修改后会立刻从线程内存写到主内存。否则修改后不确定何时写入。
2、可见性:避免了JIT对代码优化导致读共享变量失败,例如子线程while(flag)被优化为while(true)
3、有序性:volatile变量的写操作阻止上方其他写操作到下面,读操作组织下方其他操作到上面
使用场景:1、作为子线程的状态标识;2、单例模式的double check
在这里插入图片描述

sync 重量级锁

本质是monitor,是重量级锁,底层依赖操作系统的Mutex Lock实现,OS实现线程间切换涉及到用户态和内核态的切换、线程上下文切换,因此性能较低。
javap -v xx.class查看字节码信息,用monitorenter和monitorexit框起来,其中monitorexit会有两次,是为了防止线程抛异常导致死锁。

对象锁,会关联Monitor结构,具体是用对象的mark word指向Monitor。
Monitor包含WaitSet、EntryList、Owner。WaitSet是线程等待,EntryList是线程阻塞,Owner是获得锁。Owner释放后,EntryList不是排队,会争抢Owner,非公平锁。
在这里插入图片描述

轻量级锁(自旋锁)

同步代码块不存在竞争、或者线程是交替执行该代码块,会用轻量级锁。
用CAS交换Lock Record开头和mark word,用完了再换回。
支持同线程重入,第二个线程起,Lock Record开头值为null,用于计算重入次数。
如果多线程冲突(CAS失败N次),就升级为重量级锁。
CAS自旋是cpu空转,但轻微的自旋空转,能换取用户态和内核态切换的开销。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

偏向锁

只有一个线程会用到该对象锁。
只有第一次CAS时将线程ID放到mark word,重入时不用再CAS,只要判断现场ID是自己。
在这里插入图片描述

偏向锁 → 轻量级锁 → 重量级锁,是单向的升级过程,不会重新降级。

悲观锁和乐观锁

悲观锁:
用sync关键字或ReentrantLock类等锁住代码块。
通常用于写比较多的情况下(多写场景,竞争激烈),避免频繁失败和重试影响性能,且开销是固定的。

乐观锁:
提交修改时再验证是否被其他线程修改,可以用版本号、CAS等思想

AQS

AbstractQueuedSynchronizer,抽象队列同步器,是构建锁或者其他同步组件的基础框架。
实现有:
ReentrantLock 阻塞式锁、
Semaphore 信号量、
CountDownLatch 倒计时器

内部维护一个FIFO的队列 和 state状态量

线程用CAS的方式修改state,修改成功则获得锁,否则进入队列。
state释放后,可以实现公平锁,也可以实现非公平锁的争抢。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ReentrantLock

主要利用CAS+AQS实现,还支持了其他功能:超时释放、公平锁、多个条件变量等。
在这里插入图片描述

ThreadLocal

内存泄露

1、ThreadLocal是每个Thread线程内部维护了一个ThreadLocalMap的成员变量,Map中的每个元素是一个Entry,key-value形式,key是对ThreadLocal对象的弱引用,value是具体set和get的Obect。
弱引用是如果引用的对象只剩下弱引用,那么GC的时候就会把该对象回收,因此key被回收了,则无法通过key找到value,且Entry中对value是强引用,value将一直不会被回收。
会有这种情况,在new ThreadLocal的时候,并没有绑定到某个变量上,那么这个ThreadLocal(即key)则会只剩下Entry的弱引用。

2、如果线程池中有固定的核心线程数,线程使用了ThreadLocal存储对象,如果在线程任务结束时不手动remove对象,则对象会随着核心现场的存活一直存在。

因此:
1、ThreadLocal需要强引用绑定到变量,通常绑定到静态变量上;
2、用完ThreadLocal线程结束前,手动remove。

Map冲突处理

ThreadLocalMap和HashMap类似,通过hash code取模得到数组的下标idx。如果遇到冲突,HashMap是在该idx上使用链表或红黑树存储,而ThreadLocalMap是使用线性探测法找到合适的idx,直到遇到相同的key进行替换value,或者为空直接存入。

Spring

SpringBoot设计得好的地方

1、DI+IoC
采用控制反转加依赖注入的模式,降低了系统内类之间的耦合性,采用了工厂模式的设计思想,开发者把类定义出来,但不用考虑如何实例化该类。
同时对循环依赖有比较好的优化方式,使用缓存机制和@Lazy注解来解决。

2、AOP
支持面向切面编程,对业务无关的统一能力进行抽象,包括预处理和后处理,能不嵌入业务代码的前提下实现统一功能,包括日志、事务、鉴权等。

3、自动装配
降低了常用技术的使用复杂度,统一了配置入口,并且提供了大量的默认参数,降低了开发复杂度

4、内嵌了Tomcat服务器,能快速调试和部署。

5、基于注解能完成大部分的bean定义、依赖定义、bean扫描配置等,省去大量的xml配置。

DI、IoC、AOP

DI:依赖注入
IoC:控制反转
AOP:面向切面编程

DI和IoC是配合进行的,目的是降低系统的耦合。
IoC是控制反转,原本的类与类之间的创建是在类内执行的,现在由外部的IoC容器统一管理。IoC容器就像是工厂模式,在工厂内把我们所需的实例创建出来
那么类之间的依赖,在IoC创建后,通过DI,即依赖注入到依赖的类上。
AOP是面向切面编程,通过预编译期方式和运行期动态代理的方式,实现功能的统一维护的技术,包括统一鉴权、日志记录、事务等。这种方式可以不影响原本业务代码逻辑进行增强,包括预处理和后处理。
AOP有两种动态代理的方式,在有接口的情况下使用JDK动态代理,通过接口实现类的动态代理。没接口的情况下使用CGLIB动态代理,通过创建子类的代理对象实现。

bean生命周期

构造函数 - 依赖注入 - Aware接口 - 后置处理before - 初始化方法 - 后置处理after - 销毁bean

循环依赖

一级缓存就是单例池
通过二级缓存存放半成品对象,三级缓存存放工厂对象
如果对象是通过工厂创建,就需要三级缓存参与
缓存只能解决构造函数之后依赖的问题。如果在构造函数循环依赖,可以在其中一个对象的形参用@Lazy

在这里插入图片描述

通过工厂创建A时发现无法创建,那么就先把工厂放入三级缓存,优先把所依赖的B创建出来。
而创建B时发现依赖A,且A工厂已在三级缓存,那么取出A工厂,创建半成品A到二级缓存,注入B,后续流程和二级缓存一致。
在这里插入图片描述

mvc执行流程

根据请求查询对应handler,执行handler返回值,根据返回的ModelAndView进行渲染为视图返回

但是现在都是前后端分离的接口开发,因此handler的返回值通过converter转换为json返回即可

在这里插入图片描述

在这里插入图片描述

自动配置

启动类注解 - 注解内嵌套Selector - 读取META-INF配置文件(包含所有配置类) - 每个配置类都会:
校验环境是否包含必须的字节码(即包含必要的运行环境) - 判断用户无自定义的bean则创建bean

SpringBootApplication注解

SpringBootConfiguration:证明是一个配置类

EnableAutoConfiguration:import了AutoConfigurationPackages.Registrar.class 和 AutoConfigurationImportSelector.class
Registrar实现了bean definition,把当前包加入到registry扫描路径下
AutoConfigurationImportSelector实现了环境接口aware和ImportSelector。读取外部资源META-INF的自动配置类列表,放入内存。每个自动配置类都会判断环境是否包含需要的字节码环境,符合环境才会加载,及创建bean。开发者通过maven引入环境来加载。
ComponentScan:配置扫描路径和规则

总而言之就是把当前包加入扫描路径,并把自动配置类加载到环境。

SpringCloud

注册中心:记录微服务的远程调用地址,并动态管理
配置中心:环境配置
远程调用:微服务间的远程调用
负载均衡:微服务集群的负载均衡
服务熔断:用于服务降级和熔断
网关:对外暴露接口

在这里插入图片描述
在这里插入图片描述

远程调用其实是访问负载均衡,由LB去找注册中心
在这里插入图片描述

分布式

CAP

C 一致性
A 可用性
P 分区容错性
只能同时满足其中两个
虽然P发生的概率很低,但是当P发生的时候,P是必然存在的,例如分布式系统节点间数据同步异常(多种可能性导致)那么此时要么遵循AP要么CP。
如果要保证系统的高可用,可以持续提供服务,则不能保证分布式系统的数据的强一致,即AP,例如某节点查询的时候新数据,另一节点查询的是旧数据(数据同步失败)。
否则是CP,在节点间数据同步成功前,都不会对外正常提供服务。

AP和CP的选择取决于业务对于强一致性的需求。

BASE

对CAP的解决思路
基本可用、允许临时不一致、在软状态后达到最终一致性。
各个微服务通过事务协调器进行协调。
AP:先提交事务,若有失败的,再反向操作。这样可以达到非阻塞,并且最终一致性,但会有短时间多个微服务不一致
CP:先执行 不提交,等所有都成功后,再提交事务。能保证强一致,但不提交事务可能会阻塞

分布式事务解决方案

seata框架

包含三块:
TC(事务协调者,维护全局和分支状态)
TM(事务管理者,定义全局范围)
RM(资源管理者,管理分支资源)

在这里插入图片描述

MQ

异步事务
注意MQ和MYSQL在事务内执行,异步相当于封装在MQ内

在这里插入图片描述

接口幂等性

1、通过数据库唯一索引,避免重复插入
2、使用token+redis,第一次访问获取token,以用户为key放入redis,第二次请求带上token访问,token存在redis则删除并处理业务
3、分布式锁:获得锁的操作,获取不了的直接返回
4、乐观锁:用版本号作为更新条件

服务熔断、服务降级

服务降级:
对于同一个模块,对于重要性较低的功能进行直接返回,把更多资源留给重点保障的功能。
例如评论相对不重要,可以统一返回体验良好的提示。
例如支付很重要,则用资源重点保障。

服务熔断:
服务间是一个调用链的关系。例如A调用B,此时B已经响应缓慢甚至无响应,那么此时A可以触发服务熔断,停止对B的调用,采用机制进行快速错误返回。等监测到B恢复后,再恢复对B的调用。
可以避免微服务雪崩效应。

节选自

黑马程序员:新版Java面试专题视频教程:https://www.bilibili.com/video/BV1yT411H7YK?p=1
javaguide:https://javaguide.cn/home.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值