200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > java 惺惺惜惺惺想寻寻寻寻寻

java 惺惺惜惺惺想寻寻寻寻寻

时间:2021-12-16 19:21:50

相关推荐

java 惺惺惜惺惺想寻寻寻寻寻

使用RabbitMQ有什么好处?

答:1.应用解耦,实现消费者和生产者之间的解耦;2.异步处理(预约挂号业务处理成功后,异步发送短信、推送消息、日志记录,节省响应时间等);3.请求削峰,将大量并发请求放入MQ,消费系统每秒少量拉取一部分请求,实现削峰;解决通信问题(不同的平台通讯协议)。

2.消息基于什么传输?

答:由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道channel(TCP长连接内的虚拟连接)的方式来传输数据。且每条TCP连接上的信道数量没有限制。

一般一个队列对应一个消费者,对应多个消费者的时候会发生轮询

3消息怎么路由?

答:消息路由必须有三部分:交换器(是一种绑定列表,通过路由键和队列绑定)路由键、绑定键,生产者把消息发布到交换器上;绑定决定了消息如何从交换器路由到特定的队列的,消费者再和队列绑定获取消息。交换器分为三种:1.direct直连:路由键和绑定键完全匹配,消息就被投递到相应的队列;2.topic:绑定键可以使用通配符*和·;3.finout广播:直接发布到所有绑定的队列

4.如何做到消息的可靠性?确保消息正确地发送至RabbitMQ?确保消息接受方消费了消息?

答:分为以下情况:一。生产者弄丢了数据:1.选择用rabbitmq提供的事务功能,就是生产者发送数据之前开启rabbitmq事务,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错会回滚事务,重试发送消息;如果收到了消息,那么可以提交事务。但是问题是,rabbitmq事务机制一搞,基本上吞吐量会下来,因为太耗性能。2.开启confirm模式,在生产者每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。补偿机制:如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的消费者的回调接口,那么你可以重发。

二。rabbitmq弄丢了数据:rabbitmq的持久化,发送消息时设置其持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据。

三。消费端弄丢了数据:消息确认机制-->消费的时候,刚消费到,还没处理,结果进程挂了,利用ack机制(关闭 RabbitMQ 的自动ack),只有消费者确认了处理完消息(然后手动返回ack),RabbitMQ才能安全地把消息从队列中删除,在消费者代码里确保处理完的时候,再设置返回ack。

5.如何保证消息不被重复消费啊(如何保证消息消费时的幂等性)?

答:建张表,记录每条消息的发布消费情况(消息id唯一),如果已经消费了,就不在消费消息

可以在生产消息的时候给每个消息加一个全局唯一ID,消费者消费消息时根据这个ID去redis当中查询之前是否消费过。如果没有消费过,就进行消费并将这个消息的ID写入到redis当中。如果已经消费过了,就无需再次消费了

kafka的结构

Cluster-->broker-->topic(主题)-->Partition(分区)-->segment(log和index) 生产者(Producer)和消费者(Consumer队列和订阅)

Kafka 的消息是存在于文件系统之上的,每一个 Partition 最终对应一个目录,里面存储所有的消息log文件和索引文件index,每个partition中的消息都有一个唯一的编号offset偏移量来找到对应的消息和索引文件,Partition 会为每个 Consumer Group 保存一个偏移量,记录 Group 消费到的位置,Partition 不会删除消息,除非消息过期。

Kafka 中一个 topic 中的消息是被打散分配在多个 Partition(分区) 中存储的, Consumer Group 在消费时需要从不同的 Partition 获取消息,那最终如何重建出 Topic 中消息的顺序呢?

答案是:没有办法。Kafka 只会保证在 Partition 内消息是有序的,而不管全局的情况

kafka高吞吐量的原因?

答:1.读写顺序-->硬盘是机械结构,需要指针寻址找到存储数据的位置,所以,如果是随机IO,磁盘会进行频繁的寻址,导致写入速度下降。Kafka使用了顺序IO提高了磁盘的写入速度,Kafka会将数据顺序插入到文件末尾,消费者端通过控制偏移量来读取消息,虽然会导致数据无法删除(设置消息过期时间和Partition大小删除);

2.分区:kafka中的topic中的内容可以被分为多分partition存在,每个partition又分为多个段segment,所以每次操作都是针对一小部分做操作,很轻便,并且增加并行操作的能力;

3.批量发送:kafka允许进行批量发送消息,producter发送消息的时候,可以将消息缓存在本地,等到了固定条件(达到一定条数发送/一定时间发送)再发送到kafka

4.消息压缩

consumer 采用 pull(拉)模式从 broker 中读取数据(消费者可以控制消息读取速度,缺点是没消息时会循环,可以设置无消息时的等待时间再拉取)

一致性方案:

leader负责读写,副本只负责备份,如果某个分区的Leader不可用,kafka会从该分区的其他的副本(已同步副本列表)中选择一个作为新的Leader。之后所有的读写就会转移到这个新的Leader上。

kafka如何保证消息的可靠性,不丢失?

一、生产者不会丢失消息(在 producer 端设置acks=all:这个是要求每条数据,必须是写入所有 fallow 之后,才能认为是写成功了)

二、kafka丢失数据,当leader的follower没有完全同步数据的时候,leader宕机,重新选举的leader就会数据不全,这里也要设置acks=all,并且生产者设置无限重试发消息

三、消费端弄丢了数据,唯一可能导致消费者弄丢数据的情况,消费者消费了这个消息后自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但刚准备处理这个消息时消费者就挂了,导致消息丢失。

只要关闭自动提交offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。

如何保证消息的顺序性

kafka: 方案一:topic只使用一个partition分区,kafka默认保证同一个partition分区内的消息是有序的,这样消息就是全局有序,缺点是只能被consumer group里的一个消费者消费,降低了性能,不适用高并发的情况。

方案二:producer可以在发送消息时可以指定需要保证顺序的几条消息发送到同一个partition分区,这样消费者消费时,消息就是有序。

RabbitMQ:1.一个队列对应一个消费者时,消费者采用多线程的方式加速处理消息导致的,解决:在consumer中维护多个内存队列,根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。

2.一个队列对应多个消费者时,解决:queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据)发送到同一个queue当中

区别:

RabbitMQ:支持消息的可靠的传递,支持事务,不支持批量操作,不支持负载均衡。

kafka:内部采用消息的批量处理,数据的存储和获取是本地磁盘顺序批量操作,消息处理的效率高,吞吐量高,采用zookeeper对集群中的broker,consumer进行管理,可以注册topic到zookeeper上,通过zookeeper的协调机制实现负载均衡。

Redis

string(字符串),hash(哈希),list(列表),set(无序集合)及zset(有序集合)

一、持久化方式

RDB持久化是指在指定的时间间隔内将内存中的数据全量快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储(一次写入的数据量太大则会影响redis性能),RDB在定时持久化的过程中可能出先宕机,默认打开

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,3种同步策略,即每秒同步、每修改同步和不同步;日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,Redis有工具来帮助我们解决数据一致性的问题;AOF文件过大时Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行,默认关闭

RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快,AOF能最大限度的避免数据丢失,都开启的时候会使用AOF恢复。

三、Redis集群相关

哨兵:用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

1.监控==>会不断地发送心跳ping检查你的主服务器和从服务器是否运作正常;2.通知>当被监控的某个 Redis 服务器出现问题时,可以通过 API 向管理员或者其他应用程序发送通知;3.故障迁移==>当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器。

Redis3.0之前使用哨兵,之后使用Redis Clister实现数据分片

Redis Cluster: 分布式高可用方案

1.哈希槽:Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点中,当有新的节点加入或者移除的时候,再来迁移这些槽以及其对应的数据。

2.Redis主从架构:一主多从,主负责写/读,并且将数据复制到其它的 slave 节点,从节点负责读(每个Master也需要配置主从,并且内部也是采用哨兵模式)。所有的读请求全部走从节点(必须开启主节点的持久化防止丢失数据)。轻松实现水平扩容,支撑读高并发。核心机制==>redis 采用异步方式复制数据到从节点,从节点复制数据是不会影响到主节点写入和从节点查询(从redis复制完成后需要删除原数据加载新数据,期间暂停服务);

1.当从库和主库建立MS关系后,会向主数据库发送SYNC命令

2.主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写3.命令缓存起来

4.当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis

5.从Redis接收到后,会载入快照文件并且执行收到的缓存的命令

之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

选举逻辑:当slave发现自己的master变为FAIL状态时,便尝试发起选举,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

1.slave发现自己的master变为FAIL

2.将自己记录的集群currentEpoch(选举轮次标记)加1,并广播信息给集群中其他节点

3.其他节点收到该信息,只有其他节点的master响应,判断请求者的合法性,并发送结果

4.尝试选举的slave收集其他节点的master返回的结果,收到超过半数master的统一后变成新Master

5.广播Pong消息通知其他集群节点。

3.Redis实现分布式锁:Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁(当且仅当 key 不存在,将 key 的值设为 value返回1.SET if Not Exists),为了避免其他线程/进程调用SETNX命令总是返回0而进入死锁状态,设置key的过期时间30S。

redission做了优化,会在获取设置锁的value(本线程的唯一id)时候,在执行逻辑的时候开一个定时任务来循环监听逻辑执行没有,如果没有处理完成就会延长过期时间,解锁unlock()的时候判断是否是自己线程加的锁,避免解了其他线程的锁。

lua脚本:1.减少网络开销(多次请求一个脚本执行); 2.原子性(Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入); 3.事务回滚

当切换主从的时候数据丢失,导致锁失效,解决办法RedLock算法:redisji集群有基数个redis master实例,加锁时要对每一个master设key,超过一半的设值成功则表示加锁成功,其他线程获取锁的时候就无法给搞过一半的master设值,解决了master锁丢失。

双写一致性:采用延时双删策略即进入逻辑就删除Key,执行完操作,延时再删除key;分布式锁,读写锁

Redis分布式锁,必须使用者自己间隔时间轮询去尝试加锁,当锁被释放后,存在多线程去争抢锁,并且可能每次间隔时间去尝试锁的时候,都不成功,对性能浪费很大。

Zookeeper分布锁,首先创建加锁标志文件,如果文件已经存在说明其他客户端占用了,就再锁文件下创建临时节点来监听前一个节点,当有锁释放,无须争抢,按照节点顺序,依次通知使用者

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,花费的时间长

缓存雪崩:Redis中大批量key在同一时间同时失效导致所有请求都打到了MySQL。而MySQL扛不住导致大面积崩溃。解决方案:1.缓存数据的过期时间加上个随机值,防止同一时间大量数据过期现象发生;2.给每一个热点数据缓存增加相应的缓存标记,记录缓存的是否失效,失效则更新缓存;3.一般并发量不是特别多的时候,可以加MQ/加锁排队。

缓存穿透:请求缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。解决方案:1.增加请求校验,防止恶意id<=0的请求;2.从缓存和数据库中都没有取到,可以将key-value对写为key-null,缓存有效时间可以设置短点30秒,防止攻击用户反复用同一个id恶意攻击;3.布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

缓存击穿:大量请求集中对这一个热点key进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了。设置热点数据永远不过期 加上互斥锁也能搞定了

底层数据存储结构:

string-->动态字符串(保存了字符串长度和未使用长度),空间预分配减小了内存重分配的次数如果对SDS修改后,字符串len的长度小于1M,那么程序将分配和len相同长度的未使用空间;惰性空间释放对SDS进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配

hash-->用链表来解决hash冲突,开始默认使用 hash 表 1 保存键值对数据,哈希表 2 此刻没有分配空间。当数据越来多触发 rehash(阈值>5) 操作:给 hash 表 2 分配更大的空间;将 hash 表 1 的数据重新映射拷贝到 hash 表 2 中;释放 hash 表 1 的空间。采用了渐进式rehash每次处理客户端请求的时候,先从 hash 表 1 中第一个索引开始,将这个位置的数据重新计算hash放到hash表2中,渐进式rehash可以避免了集中式rehash带来的庞大计算量,就这样将 rehash 分散到多次请求过程中,避免耗时阻塞。

在渐进式rehash过程中,因为还可能会有新的键值对存进来,此时Redis的做法是新添加的键值对统一放入ht[1]中,这样就确保了ht[0]键值对的数量只会减少。当执行rehash操作时需要执行查询操作,此时会先查询ht[0],查找不到结果再到ht[1]中查询

list-->Redis3.2之前,linkedlistziplist两种编码可以进选择切换,linkedlist双向列表,listNode节点包含前后节点指针和value(字符串),空间不连续

ziplist压缩列表没有上下节点指针,节点空间连续的,牺牲读写性能换取空间利用率

压缩列表满足条件,不满足就用双向列表:1、列表对象保存的所有字符串元素的长度都小于64字节。2、列表对象保存的元素数量小于512个。

Redis3.2后统一用quicklist:存储了一个双向列表,每个列表的节点是一个ziplist,所以实际上quicklist就是linkedlist和ziplist的结合

set-->1.intset整数数组:当数据全是整数值,而且数量少于512个时才使用;否则使用hashtable拉链法

zset-->跳跃表(skiplist)是一种有序数据结构,基于有序单链表,在链表的基础上,每个结点不只包含一个指针,还可能包含多个指向后继结点的指针,这样就可以跳过一些不必要的结点,从而加快查找、删除等操作

类加载机制

jvm把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成jvm可以直接使用的java类型的过程。

过程:Java文件首先经过Javac编译器编译成字节码文件,当程序调用一个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指创建一个字节数组把类的.class文件中的数据读入到内存(方法区)中成为运行时数据结构,然后在堆内存中生成对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证(先用二进制字节流进行格式验证,验证堆中的Class对象)、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化

反射使您的程序代码能够得到装载到JVM中的类的内部信息,允许您执行程序时才得到需要类的内部信息(获取字段,调用方法等),而不是在编写代码的时候就必须要知道所需类的内部信息

动态代理

代理模式:一个类代表另一个类的功能,一般用于对要访问的类进行控制或功能增强,又无需变动该类的代码。三要素:目标接口,目标接口实现,实现invocationHandler的类

cjlb代理,是实现了MethodIntercapter接口

JVM即运行时数据区:1.栈 2.堆 3.本地方法栈 4.方法区 5.程序计数器

1.栈:是线程私有的,一个线程运行时就jvm就开辟了了一个栈内存,栈中存放一个个栈帧,每个栈帧对应一个被调用的方法,当线程执行一个方法时,就会创建一个对应的栈帧,并且将栈帧压栈,方法执行完,栈帧出栈(栈帧包含:方法执行需要的操作数栈(局部变量的临时存值的地方),局部变量表(根据指令行来控制变量的),动态链接(调用对象方法时,对象引用根据动态链接在方法区找到类元信息,然后才能执行里面的指令),返回地址(调用方法执行完后,将线程程序计数器从新执行到原先的方法)。

2.堆:线程共享的,存放对象(不绝对)和数组,常量池,堆内存 =新生代+老生代+持久代,新生代分为Eden和2个交换区,比例8:1:1,新生代老生代1:2,对象优先在Eden分配,长期存活(垃圾回收15次)和大的对象进入老年代。

3.本地方法栈 : 与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务(一个java调用非java代码的接口)。

4.方法区(元空间) : 线程共享的(1.8以前永久代,1.8后元空间),存储了每个类的类元信息(包括类的名称、方法信息、字段信息)、静态变量以及编译器编译后的代码。

5.程序计数器:当前线程正在执行或者马上要执行的下一个指令的行号;在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰。

垃圾回收

1.可达性分析算法:“GC Roots的对象作为起始点(main方法启动后new出来的对象),从这些节点开始向下搜索“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。根对象包括:栈帧中的本地变量表)中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中(Native方法)引用的对象。

1.引用计数发:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象是,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。jvm中并没有采用,不能解决对象之间循环引用的问题

GC何时回收:1会在cpu空闲的时候自动回收;2在堆内存存储满时;3主动调用System.gc()后尝试进行回收eden区满时,触发MinorGC,升到老年代的对象大于老年代剩余空间full gc

2、垃圾回收算法

2.1标记-清除算法分为标记和清除两个阶段:首先标记(可达性分析)出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点效率不高且会产生大量不连续的内存碎片

2.2复制算法(新生代)将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。效率高但是内存利用不足

新生代老生代1:2,新生代分为Eden和2个交换区,比例8:1:1,对象优先在Eden分配,长期存活(垃圾回收15次)和大的对象进入老年代

1. 当Eden区满的时候,会触发第一次gc,把还活着的对象拷贝到交换区1中;当Eden区再次出发gc的时候,会扫描Eden区和交换区1,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到交换区2中,Eden区和交换区1清空。

2. 当后续Eden区又发生gc的时候,会对Eden区和交换区1进行垃圾回收,存活的对象复制到交换区2,并将Eden区和交换区1区清空

3. 部分对象会在From区域和To区域中复制来复制去,如此交换15次,最终如果还存活或者大对象存入老年代。

3.3标记整理算法(老年代):与标记清除算法很相似,而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片。

分代收集算法:是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收

在jdk8的时候java废弃了永久代,但是并不意味着我们以上的结论失效,因为java提供了与永久代类似的叫做“元空间”的技术。:由于永久代内存经常不够用或发生内存泄露,元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小

垃圾收集器:

GMS:初始标记、并发标记、重新标记、并发清除 注重服务器的响应速度,停顿时间短

初始标记只标记GC Roots能够关联到的对象,并发标记是执行GC Roots 链路的过程,重新标记就是为了修正并发标记期间因用户程序执行而导致标记发生变动使得标记错误的记录

G1收集器:分代收集,基于标记-整理算法,不产生内存碎片。

可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收

并发编程的三特性:

1.原子性:操作不可分割;i=1具有原子性,直接在工作空间中进行赋值操作。i++;不具有原子性,有三个步骤1.把内存读取到工作空间(原子性)2.在工作空间中做自增运算(原子性)3.再把值刷新回内存(原子性)多个原子性操作组合在一起,就不具有原子性了。保证:Synchronized,lock

2.内存可见性:当多个线程同时访问同一个变量时,当一个线程修改了该变量的值,其他线程能立即看到被修改后的变量的值。保证:volatile,Synchronized,lock

3.有序性:程序执行顺序不一定按照代码的顺序执行(编译期有编译重排序;JVM执行某段代码时,不一定会按照代码顺序执行,但会保证执行结果一致;为了提高代码执行效率,可能会优化代码执行顺序(指令重排序)。保证:volatile,Synchronized

Happens-before原则:1.程序次序原则结果不许变;2.锁定原则:后一次加锁必须等前一次解锁

3.Volatile原则:霸道原则;4.传递原则:A在B前执行,B在C前执行,A必须在C前

volatile关键字,当一个共享变量被一个线程修改后,会立即更新到主存,其他线程需要读取该变量时,会去内存读取新值(保证可见性和有序性),类似于缓存一致性协议。

Volatile:保证线程可见性且提供了一定的有序性,但是无法保证原子性.它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据

Mark word:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳

Synchronized 修饰非静态方法:1.某个对象实例内,程同防止多个线时访问这个对象的synchronized方法

2.修饰类.class或者静态方法时,防止多个线程同时访问这个类(包括所有实例对象)中的synchronized方法

底层实现:当有synchronized修饰的时候,在编译成class文件的时候加上monitorenter指令(线程加载class文件时会识别加了锁的),在java中每个类都设有一个monitor监视器(C++),某一线程占有这个对象的时候,先看monitor 的计数器是不是0,如果是0还没有线程占有,就占有这个对象,并且给对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1。监视器锁本质又是依赖于底层的操作系统的互斥锁来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”

偏向锁:

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较 对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,当有其他线程竞争锁时,markword会去判断当前线程 是否执行完毕,如果没有执行完毕当前线程,会把当前线程的偏向锁升级为轻量级锁。(偏向锁是为了在没有线程竞争锁时提高性能)

轻量级锁:

加锁过程--->1. 线程在自己的栈桢中创建锁记录 LockRecord;2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中;3. 将锁记录中的 Owner 指针指向锁对象;4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。这时候就说明获取到了轻量级锁,解锁相当于锁升级的逆过程,把获取到的LockRecord替换到markWord中,如果成功代表没有竞争,如果失败了,代表有竞争这时候就会锁膨胀,导致升级为重量级锁。

重量级锁:

当升级为重量级锁时,意味着线程只能被挂起或者堵塞,等待被唤醒,监视器锁本质又是依赖于底层的操作系统的互斥锁来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间。

一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

其他锁优化:

1.锁消除: 删除不必要的加锁操作(根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁)

2.锁粗化:如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部

公平锁:多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

非公平锁:线程会先直接尝试获取锁,获取不到才会到等待队列的队尾等待,如果获取到了就直接加锁(队列中的继续排队并阻塞)

NIO(同步非阻塞)

Channel(通道):Channel是一个对象,可以通过它读取和写入数据(有4种类型)。相比较IO中的流:

Channel是双向的,既可以读又可以写,而流是单向的

Channel可以进行异步的读写

Buffer(缓冲区):是存储数据的区域,读写数据的中转池,缓冲区本质就是数组,Java 提供了许多不同类型的缓冲区,除了布尔类型以外,其它基本数据类型都有对应的缓冲区数组对象。

属性:capacity缓冲区数组的总长度,limit缓冲区中可以操作数据的大小,必须满足 limit <= capacity

position当前缓冲区中正在操作数据的下标位置,必须满足 position <= limit;mark标记位置

Buffer 读写数据步骤:1.写入数据到 Buffer;2.调用 flip() 方法完成读写模式的切换;3.从 Buffer 中读取数据;4.调用 clear() 方法或者 compact() 方法。

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

Selector(选择器):多个channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。因此,单个线程可以监听多个数据通道它底层利用了多路复用 IO机制,让选择器可以监听多个 IO 连接,根据 IO 的状态响应到服务器端进行处理。基于 Reactor 线程模型。在 Reactor 模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作

多路复用器NioSelectorServer中重要的方法

1 .Selector.open()//创建多路复用器

2 . socketChannel.register(selector,SelectionKey.OP_READ)//将channel注册到多路复用器上

3 .selector.select()//阻塞等待需要处理的事件发生

总结:NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效的遍历,将事件处理交给了操作系统内核(操作系统中断程序实现),大大提高了效率。

线程池相关

什么事线程池?

答:java.util.concurrent.Executor接口的实现用于创建线程池多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力

包括四部分:线程池管理器,工作线程,任务接口,任务队列

线程池作用:1.重复利用已创建的线程降低线程创建和销毁造成的消耗;2.增加响应速度任务可以不需要的等到线程创建就能立即执行;

线程池线程创建时机:流程主要是ThreadPoolExecutor 的 execute ()1.首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。2.其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里,等待线程池中的线程去队列中取任务。满了,则进入下个流程。3.最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了则执行拒绝策略。

线程池参数:1.corePoolSize核心线程数;2.workqueue任务队列;3.最大线程数;4.keepAliveTime空闲线程的保活时间,如果某线程的空闲时间超过这个值都没有任务给它做,则会被关闭。注意这个值并不会对所有线程起作用,如果线程池中的线程数少于等于核心线程数,那么这些线程不会被关闭;5.handler,线程池满了,又有新的任务提交的时候,该采取什么策略

线程池关闭:shutdown或shutdownNow方关闭线程池,原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

SpringBoot的启动机制

@SpringBootApplication是复合注解,可分为三个注解

1.@Configuration:等于MVC的@Configuration ,用来IOC管理注入bean和让当前类变为配置类。

2.@EnableAutoConfiguration 实现自动配置,原理:SpringFactoriesLoader从classpath中搜寻所有META-INF/spring.fatories文件,并将其中Key对应的Value配置的相关类的全路径,通过反射的方式实例化为对应的标注了@Configuration的类,形成IOC容器配置类由spring管理,比如:WebSeveletFactoryAutoConfiguration类就会将Tomcat加载进来,WebMVCAutoConfiguration类来实现mvc的功能

3.@ComponentScan:设置需要扫描的包

二、Spring中bean的生命周期

1.Bean配置、解析、注册阶段: 是通过各种BeanDefinitionReader读取各种配置来源信息(比如读取xml文件、注解等),并将其转化为BeanDefintion的过程。(BeanDefinition的作用就是去定义并描述一个Spring Bean,方便后续解析实例化等操作。)

不管是是通过xml配置文件的<Bean>标签,还是通过注解配置的@Bean,还是@Compontent标注的类,还是扫描得到的类,它最终都会通过BeanDefinitionBuilder解析成一个BeanDefinition对象

BeanDefinitionRegistry我们经常用到的AnnotationConfigApplicationContext,但实际上其内部是转发给了DefaultListableBeanFactory#registerBeanDefinition()进行注册(将BeanDefinition放入 ConcurrentHashMap中(spring的一个扩展点实现接口BeanFactoryPostProcessor 在此处来插入我们定义的代码。在配置数据库的dataSource时使用到的${}占位符的值,就是它注入进去的)

4.BeanDefinition合并阶段 bean定义可能存在多级父子关系,递归合并得到一个包含完整信息的RootBeanDefinition

5.Bean Class加载阶段 将bean的类名转换为Class对象,然后赋值给beanClass字段,在堆中申请空间 ,也代表着bean的类型

6.Bean实例化阶段 实例化前这个List会尝试调用InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation方法,如果返回bean,那么将跳过spring的Bean实例化逻辑(spring的一个扩展点,允许开发者在这个方法中直接返回bean的一个实例)这个过程会通过反射来调用bean的构造方法来创建bean的实例(spring为开发者提供了接口AutowiredAnnotationBeanPostProcessor,允许开发者判断用哪个构造器)。

9.Bean属性设置,初始化阶段

1.@Autoward、value、resource 属性设置 by type by name

2.Bean Aware接口回调 如果bean实例实现了相应的Aware接口,就将BeanNameAware、BeanClassLoaderAware、BeanFactoryAware注入当前bean中

3.Bean初始化前操作 调用postProcessBeforeInitialization方法(spring的一个扩展点,允许开发者在这里bean手动赋值),若返回null则结束方法,该方法有2个实现类:1.ApplicationContextAwareProcessor (注入6个Aware接口对象) monAnnotationBeanPostProcessor(调用@PostConstruct标注的方法,将会在依赖注入完成后被自动调用,构造方法之后执行,初始化方法之前执行)

Bean初始化操作 1。如果要做初始化扩展则继承InstantiationBean实现afterPropertiesSet方法,2调用定义bean的时候指定的初始化方法<bean>、@bean、setInitBeanName()

10.所有单例bean初始化完成后阶段 BeanPostProcessor#postProcessAfterInitialization()方法判断该bean是否需要被AOP代理增强,如果需要的话,则会在该步骤返回一个代理对象

岳北小学

11.Bean销毁阶段 三种方式:BeanFactory.destroyBean(); BeanFactory.destroySingletons(),ApplicationContext.close()

属性注入注解区别:

@Autowired--->spring注解按type注入,AutowiredAnnotationBeanPostProcessor来解析如果发现找到多个bean,则又按照byName方式比对,如果还有多个,则报出异常;具体步骤:1.根据type找 2.再看刷选出的bean有没有@primary的 3.没有的话再看有没有设置优先级的bean 4.最后才进行name的筛选

@Resource--->Java自带注解,按name注入,

循环依赖:

1.首先spring.refresh(),A调用doGetBean-->getSingleton(name)依次从一级、二级、三级缓存singletonObjects中去获取,都取不到;

2.执行getSingleton(name,BeanFactory)生产BeanFactory,调用addSinglatonFactory()提前暴露存入三级缓存singletonFactory中(放入前会调用beforeSingletonCreation()判断是否是排除的bean,并且放入CreatingSet<>);

3.属性填充populateBean()注入B,同样getSingleton()从缓存中找,找不到,走A同样的流程并且放入三级缓存和CreatingSet<>;

3.1 在B注入A的时候执行getSingleton(name),依次到三个缓存中找,在三级缓存singletonFactory找到了A,判断是否单例/循环依赖/正在创建,就将A从三级转移到二级中(这时候如果A类有AOP代理的话就会触发getEarlyBeanReference()这个方法会遍历所有后置处理器来判断是否需要代理,生成A的代理对象放入二级earlySingletonObjects),这时候B就有了半成品的A引用了。

3.2 B初始化完成后会执行后置处理器postProcessAfterInitialization(),来判断B有没有AOP如果有就会生成代理对象放入一级缓存(没有代理则是原始bean)

4.这时候A就可以将单例池中的B注入进来,开始初始化,Aware接口回调、前置处理器、初始化完成后执行后置处理器postProcessAfterInitialization(),由于在初始化B的时候已经将A的代理对象放入二级earlySingletonObjects,这里直接将二级中的A代理转移到一级singletonObjects中就可以了。

如果循环依赖中没有AOP那么2个缓存就可以了,正因为有了AOP才需要将beanFactory提前暴露在三级缓存singletonFactory中,beanFactory可以根据情况是否生成代理对象来覆盖原始对象,而且如果只是单纯的AOP没有循环引用,那么代理对象是在初始化后的后置处理器来完成的。

BeanFactoryPostProcessor bean工厂后置处理器作用,可以修改bd,也可以自定义bean

ClassPathXmlApplicationContext 执行构造方法-->AbstractApplicationContext.refresh()

主要逻辑:synchronized加锁 -- 防止容器的频繁启动销毁

1. prepareRefresh() -- 记录启动时间、标记状态、处理配置文件中的占位符

2. obtainFreshBeanFactory() -- 初始化 BeanFactory、加载 Bean、注册 Bean 等

2.1 refreshBeanFactory() :如果 ApplicationContext中已经加载过 BeanFactory了,销毁所有 Bean,关闭 BeanFactory;初始化一个 DefaultListableBeanFactory,loadBeanDefinitions()根据配置,使用BeanDefinitionReader加载配置、解析,生成DB,然后RegisterBeanDefinitions将DB注册到BeanFactory中

3. prepareBeanFactory() -- 准备 Bean 容器

3.1 设置类加载器、添加BeanPostProcessor用来实现aware接口的回调、添加事件监听BeanPostProcessor(如果是ApplicationListener 的子类,那么将其添加到 listener 列表中)

3.2 注册bean的前置、后置处理器,处理国际化

4. finishBeanFactoryInitialization() 初始化bean

Mysql相关

Sql语句执行流程:

1、客户端发起一个请求时,连接器身份认证和权限鉴别后建立一个连接

2、服务端会检查缓存,如果命中则直接返回,否则继续之后后面步骤

3、分析器sql语句进行解析,然后对其进行词法分析,语法分析以及预处理

4、优化器sql优化(逻辑变换、代价优化、索引选择)生成执行计划

5、执行器调用存储引擎层API来执行查询,返回结果

Mysql存储引擎分为5级,表空间,段,簇,页,行

Page:为了高效管理物理空间,对簇进一步细分,就得到了页。簇是由连续的页(Page)组成的空间,一个簇中有 64 个连续的页。 (1MB/16KB=64)。这些页面在物理上和逻辑上都是连续的。

一个数据页,有16K大小

二 Mysql的B+Tree结构相关:

1.二叉排序树:对于一个节点,它的左子树的孩子节点值都要小于它本身,它的右子树的孩子节点值都要大于它本身;

2.平衡二叉树:左右子树深度差绝对值不能超过 1;(这种数据结构可以达到平衡的效果,但是最终为什么没有采用这种结构呢。原因是浪费了极大的空间,数据一旦过大,进行的磁盘IO过多)

3.B 树/多路平衡查找树:类似普通的二叉树,所有键值分布在整个树中,分叉数(路数)永远比关键字数多 1

4.B+Tree (多路搜索树

不同在于:

所有关键字存储在叶子节点,非叶子节点不存储真正的 data,从而可以快速定位到叶子结点。

所有叶子节点增加了一个链指针,意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同,很适合查找范围数据。

因此,B+Tree 可以对 <,<=,=,>,>=,BETWEEN,IN,以及不以通配符开始的 LIKE 使用索引

三 为什么 MySQL 索引选择了 B+树而不是 B 树?

1.B+树更适合外部存储(一般指磁盘存储),由于内节点(非叶子节点)不存储 data,所以一个节点可以存储更多的内节点,每个节点能索引的范围更大更精确。也就是说使用 B+树单次磁盘 I/O 的信息量相比较 B 树更大,I/O 效率更高。

2.MySQL 是关系型数据库,经常会按照区间来访问某个索引列,B+树的叶子节点间按顺序建立了链指针,加强了区间访问性,所以 B+树对索引列上的区间范围查询很友好。而 B 树每个节点的 key 和 data 在一起,无法进行区间查找。

B+树的结构,能在大数据量时保证树形的扁平(树高低),减少磁盘IO次数,20G数据只需要3层

自增主键:MySQL 8.0版本后自增主键由内存转移到red log中,才有了自增的持久化(5.7及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值max(id),然后将max(id)+1作为这个表当前的自增值)

不连续的原因:1.未持久化;2.事务回滚;3.其他唯一主键冲突(插入未设置的自增主键,Mysql会现设一个自增主键导致表的自增值改变,但是主键发生冲突没插进去)

Innodb为什么推荐用自增ID:1、自增主键:每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页(寻址快,索引维护代价小,减少页的分裂);2、随机主键:每次新增纪录为了维护现有索引结构,都要被插到现有索引页的中间某个位置,而需要移动数据

2、过长的字段作为主键,因为所有辅助索引都引用主索引,会令辅助索引变得过大

四 索引相关知识

回表查询

非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表;如果 select 的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表

联合索引最左匹配原则

表上面创建了ABC三列索引。此时A索引有效,AB索引有效,ABC索引有效;之所以左边的带头大哥不能少,是因为索引是按照从左到右的格式创建的,只有左边的排序完成后,才会对右边的字段进行排序

索引下推(ICP)优化

我们有A过滤条件和B过滤条件,数据有500W条,在不开启ICP下,A过滤条件筛选出200W条,B条件筛选出100W条。这两个条件都是从500W的初始数据中获取。然后两者之间取交集

然后假设我开启了这个开关,那么是A过滤条件筛出200W的数据后,在这200W数据内,在进行B条件的筛选。这样会极大的提高筛选效率

索引的创建

1、在用于 where 判断 order 排序和 join 的(on)字段上创建索引

2、索引的个数不要过多。---浪费空间,更新慢

3、区分度低的字段,例如性别,不要建索引。

4、频繁更新的值,不要作为主键或者索引。

5、组合索引把散列性高(区分度高)的值放在前面

6、不要再列上进行计算,不适用not in 和<>。

7、用in代替or,in要小于200

什么时候用不到索引

1、索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式、计算(+ - * /)

2、字符串不加引号,出现隐式转换

3、like 条件中前面带%

Mysql中的缓存Buffer Pool

对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的。

同时会把增删改对应的日志写入redo log中。

Buffer Pool中有三个链表

freel链表:空闲链表用来记录Buffer Pool中空闲块,当查询数据时,先将磁盘中的页加载到Buffer Pool中,先来freel链表中获取一个空白页对应的指针,然后将查询的数据放到指针对应的缓存池中,再删除freel链表的该指针。

Flush链表:记录Buffer Pool中的修改数据的⻚(脏⻚),后台定时任务可以直接从脏⻚链表中获取对应脏⻚持久化到内存中。

LRU链表:当Buffer Pool存满数据时,进行淘汰机制的,删除最近最少使⽤的。LRU链表,通过划分3/8冷数据区域和5/8热数据区域。当某⻚被⽤到时,先添加到冷数据区域的链表头部,如果该⻚大于1s后⼜被⽤到了,则该⻚从冷数据区域转移⾄热数据区域,这样,如果现在执⾏全表扫描,那么对于某⻚(因为一页里有很多行)来说,基本上都是在1s内被重复快速的被使⽤到,此时该⻚是不会转移⾄热数据区域的,仍然保持在冷数据区域。

更新操作,1.首先会先更新Buffer Pool里的页的数据,2.生成一个redLog对象,binLog对象,3.再提交/回滚之前会生成undoLog对象,用来还原Buffer Pool里的页的数据;4.事务提交时进行redLog的持久化操作。

事务隔离级别:

原子性、一致性、隔离性、持久性

未提交读:可以读到其他事务未提交的数据,但没有办法保证你读到的数据最终一定是其他提交后的数据,如果中间其他事务发生回滚,那就会出现脏数据问题,读未提交没办法解决脏数据问题。

提交读:是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据,导致该事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的(不可重复读)

可重复读:事务不会读到其他事务对已有数据的修改,及时其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的(如果其他事务将数据修改为之前的值那么就是幻读)

MySQL在可重复读隔离级别下解决了幻读的:产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作更新的是记录之间的“间隙”数据。因此,为了解决幻读问题,InnoDB 引入间隙锁,目的一方面是为了防止幻读,以满足相关隔离级别的要求,另外一方面,是为了满足其恢复和复制的需要

串行化:解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束

快照读:在可重复读隔离级别下,在不加锁的情况下MySQL会根据回滚指针选择从undo log记录中获取快照数据,而不总是获取最新的数据,这也就是为什么另一个事务提交了数据,在当前事务中看到的依然是另一个事务提交之前的数据

RR隔离级别快照并不是在事务BEGIN就开始产生了,而是要等到事务当中的第一次查询之后才会产生快照,之后的查询就只读取这个快照数据

MySQL 采用了 MVCC (多版本并发控制) 的方式来实现事务隔离的(提交读、重复度)

数据库表中的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,就是使其产生的事务的 id;

可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照,快照能读到的数据规则:

读操作可以分成两类:快照读与当前读 。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁(select)。当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录(插入/更新/删除操作)。

InnoDB为每一行添加了三个隐藏列:DB_TRX_ID(存储了插入或更新语句的最后一个事务的事务ID);DB_ROLL_PTR回滚指针指向写入回滚段的undo log记录,读取记录的时候会根据指针去读取undo log中的记录;DB_ROW_ID,这是在InnoDB表没有主键的时候会用来作为主键

行锁:可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

间隙锁:范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录加锁,锁定记录之间的范围,但不包含记录本身。防止幻读。

临键锁:范围查询并命中,查询命中了索引。会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,也会把相邻的下一个区间也会锁住

InnoDB行锁是通过给索引上的索引项加锁来实现的,意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

InnoDB实现了以下两种类型的行锁。

共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁 意向共享锁(IS表锁):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。意向排他锁(IX表锁):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

Mysql日志原理

Bin log:server层的日志,记录操作的方法是逻辑性的语句,采用“追加写”的形式

redo log:innodb特有的,记录的是数据库中页的修改。大小是固定的,采用循环写的模式

事务提交步骤:1、prepare阶段->写入redolog日志(prepare状态) 2、commit阶段->2.1写入binlog日志 2.2最终commit(redlog为状态)

redo log会经过两次提交

宕机恢复原则:

如果在prepare和commit之间崩溃,崩溃恢复的时候事务回滚;如果redlog有commit标识的话就提交,如果redlog是prepare状态那么bin log完整则提交,否则回滚

redo log是属于存储引擎层的日志,bin log是属于Server层日志,属于两个独立的日志文件,采用两阶段提交就是为了使两个日志文件逻辑上保持一致

Elasticsearch是一款功能强大的开源分布式搜索与数据分析引擎

倒排索引:传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。

词项字典、映射表(数据id)、标记 映射表数据太多导致倒排索引的缓存太大(针对词项的倒排表中的多个id,使用压缩的来降低内存占用,压缩原理:)

在默认情况下,我们创建一个库的时候,默认会帮我们创建5个主分片(primary shrad)和5个副分片(replica shard)。同一个节点上面,副本和主分片是一定不会在一台机器上面的,就是拥有相同数据的分片,是不会在同一个节点上面的

Keyword 类型是不会分词的,直接根据字符串内容建立反向索引,Text 类型在存入 ES 的时候,会先分词,然后根据分词后的内容建立反向索引

库-索引(index)表-类型(type)行-文档(doc)列-字段(field)、

分词后,组成词项字典

ES请求路由原理:

路由算法:shard = hash(routing) % shards数量

每次增删改查一个document的时候,都会带过来一个routing number(默认为document_id),routing经过路由算法后就找到了document所在的 primary shard,这也是创建document后,replica shard可以修改,而primary shard不能再修改的原因。

ES增删改请求内部流程原理:

1.客户端发送请求到一个node,这个node就是协调节点

2.协调节点对document进行路由,将请求转发给对应的primary shard(primary shard创建本地document,添加索引,将老数据标记为删除)

3.primary shard会将数据同步到他自己对应的所有的replica shard中

4.处理完成后将结果反馈给协调节点,再由协调节点返回给客户端

ES读请求内部流程原理:与增删改的区别在于,协调节点路由算法将请求转移给相应的primary 或者replica shard上(由round-robin随机轮询算法决定)

ES批量新增document时用了特殊的json请求参数格式,方便分割成不同的document路由到对应的shard中去,避免了传统大的json数据与java语言的来回转换,不浪费空间,保证性能

deep paging(深度分页)性能问题:

在ES分页查询数据时,协调节点接受到请求时,通过路由将请求发送到多个该索引的primary shard 上,查询的结果响应给协调节点,协调节点进行排序后响应该客户端;当深度分页时,例如一个索引有三个shard,每个shard有1000条数据,当用户要分页查询第20页,每页10条数据时,那么每个shard都需要查询20*10=200条数据给协调节点,协调节点对这200*3=600条数据进行排序后,选择第200~210的数据返回客户端,深度越深数据量越大。

消耗网络带宽,因为所搜过深的话,各 shard 要把数据传递给 coordinate node,这个过程是有大量数据传递的,消耗网络。消耗内存,各 shard 要把数据传送给 coordinate node,这个传递回来的数据,是被 coordinate node 保存在内存中的,这样会大量消耗内存。消耗cup,coordinate node 要把传回来的数据进行排序,这个排序过程很消耗cpu。

doc values:

搜索的时候,要依靠倒排索引;排序的时候,需要依靠正排索引,看到每个document的每个field,然后进行排序,所谓的正排索引,其实就是doc values在建立索引的时候,一方面会建立倒排索引,以供搜索用;一方面会建立正排索引,也就是doc values,以供排序,聚合,过滤等操作使用

doc values是被保存在磁盘上的,此时如果内存足够,os会自动将其缓存在内存中,性能还是会很高;如果内存不足够,os会将其写入磁盘上

type底层原理:

type,是一个index中用来区分类似的数据的,类似的数据,但是可能有不同的fields,而且有不同的属性来控制索引建立、分词器

lucene其实是没有type的概念的,在底层存储数据时中,实际上将type作为一个document的field来存储,即_type,es通过_type来进行type的过滤和筛选

一个index中的多个type,实际上是放在一起存储的,因此一个index下,不能有多个type重名,而且存储时每个document都拥有type下所有的field,只不过没设置的field会为空值,这也是为什么一个type下的所有feild类型一样,也是type尽量存放相似的document的原因(如果差异很大那么内存会有大量的空值的field)

描述ES 索引文档的过程: 1.协调节点根据路由算法找到合适的分片;2.当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 Memory Buffer,然后定时(每隔 1 秒)写入到 缓存Cache;3.Momery Buffer 和缓存Cache 的数据可能会丢失,ES接收到请求后,同时也会写入到 translog 中,当 缓存cache 中的数据写入到磁盘中时才会清除日志,来保证数据的可靠性的

MongoDB是一个面向文档存储的分布式数据库,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组,有很好的聚集查询功能。

项目上使用是基于MongoDB 自动更新、Json数据、聚集查询的特性的

在存入mongo的时候,会先存入内存中的缓存中,当缓存的大小到达2G或者达到60S的时候会同步到磁盘中,同时可以设置Journal缓冲区每100毫秒向磁盘中的Journal日志同步一次操作命令,这样来确保数据不丢失,同时可以通过设置写策略进一步确保;mongod实例每次启动时都会检查journal日志文件看是否需要恢复。

HashMap 原理

1.HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组(HashMap 里面是一个数组,然后数组中每个元素是一个单向链表)。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。

首先HashMap里面实现一个静态内部类Entry,其重要的属性有key , value, hash,Entry<k,v> next

1.8是放在尾部节点。

Hash(key)的过程:hash^(hash>>16) 1.先进行hashCode()得到h(32位);2.再将h往右位移16位得到temp;3.将h和temp异或运算得到最终hash。第二步:就是将h的高16位移到低16位的位置,高16位置全部填充0,目的是为了将h的高位也参与到第三步的异或运算中来,结合后面的求数组下标,就会是数组下标分布的更均匀,减少hash冲突。(位操作比取余快!)

数组下标=hash&(n-1)也就是hash与上数组长度-1(与:都为1则为1,否则0),这里要求n始终是2的整数次幂,这也是为什么HashMap里面的数组容量始终为2^n,比如设置10,他初始化为大于10的2的次幂数为:16,负载因子默认0.75,当容量到达2^n*0.75(扩容阈值),会自动扩容到2倍(是new的新数组)的原因.

put方法逻辑:1.根据key计算hash,数组为空时初始化数组resize()并put;2.数组不空则计算数组的下标,并判断该下标有值没有,没值则put;3.如果下标有值,再判断key是否相等,相等则修改并返回原始值;4.如果下标相等,key不等,会执行一个无限循环来判断下标的节点的next是不是null,不是null就会继续判断链表后面的节点是不是null,最终都是添加到链表末尾,如果链表长度大于8就会转成红黑树

树化:如果链表长度大于8并且数组长度大于64才树化,否则扩容resize();

扩容机制:resize()由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择

HashMap特点

HashMap里面的数组容量始终为2^n,比如设置10,他初始化为大于10的2的次幂数为:16,负载因子默认0.75,当容量到达2^n*0.75(扩容阈值),会自动扩容到2倍(是new的新数组)。

map底层为什么要用红黑树实现

红黑树是二叉查找树,但在每个节点增加一个存储为表示节点的颜色,可以是红色或黑色(非红即黑),通过对任意一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长两倍。因此,它是一种弱平衡二叉树,相对于严格的AVL树来说,它的旋转次数少,所以对于查找、插入、删除较多的情况下,通常使用红黑树

二. ConcurrentHashMap

1.整个 ConcurrentHashMap 由一个个 Segment(分段锁) 组成,简单理解就是ConcurrentHashMap 是一个 Segment 数组,默认安全级别16(Segment的个数),Segment 通过继承 ReentrantLock(重入锁) 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全(默认有16个segment,最多同时支持16个线程并发)。

2.JDK1.8后放弃了分段锁,引入了CAS机制,synchronized+CAS+HashEntry+红黑树,

JDK1.7——>ReentrantLock+Segment+HashEntry 。它既不允许key值为null,也不允许value值为null,由于HashEntry的next域是final的,所以新节点只能在链表的表头处插入,put一个Key/Value对时,首先会获得Key的哈希值并对其再次哈希,然后根据最终的hash值定位到这条记录所应该插入的段

三. HashSet如何防止检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度

HashMap无序,TreeMap升序,LinkedHashMap存入顺序

Hashtable:key和value都不允许出现null值,默认容量为11,扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍

ArrayList的初始容量为0,第一次添加元素时,会将容量设置为10,如果容量不够,则每次会扩大50%。

关于HashMap和Hashtable的区别:

1.由于历史原因,Hashtable是基于Dictionary类,HashMap是1.2之后实现Map接口的实现(从其命名的不规范就可以看出年代之久。正常应该是HashTable,如果修改的话,大量程序需要修改)

2.最重要的不同是HashTable是同步的(每个方法前面都加上了synchronized关键字),而HashMap不是线程同步的,可以利用Collections类的synchronizedMap()方法创建一个安全的Map对象,并把它作为一个封装对象返回,利用这个对象可以让你同步访问潜在的HashMap. Map m = Collections.synchronizedMap(new HashMap(...));

3.HashMap中key,value允许为空,即HashMap中只有一条记录可以是一个空的key,但任意数量的条目可以是空的value,而Hashtable的key,value不允许为空。

fail-fast 机制:快速失败机制,在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast

避免fail-fast方法:1.如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法;2.使用java并发包(java.util.concurrent)中的类来代替ArrayList和hashMap,比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组。

hash冲突的解决办法:

1.开放定址法:插入元素时,如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中(会导致相同hash值的元素挨在一起和其他hash值对应的槽被占用)。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续从该槽遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)

2.链地址法:

3.重复哈希:

4.建立公共溢出区:

CAS: Compare And Swap

对比和交换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B;更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B;如果不相同就将旧的预期值A更新成内存中的,然后再次尝试。

从思想上来说 : synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

原理:1.CAS的自旋(无限循环),用volatile关键字来保证(保证线程间的可见性)

2.unsafe类提供了硬件级别的原子操作(而是通过本地(native)方法来访问底层操作系统)来获取对象内存地址,修改属性等

3.CAS机制中使用了3个基本操作数:内存地址V,预期原值A,新值B

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作

1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。JDK1.5后Atomic类增加版本号的比较,compareAndSet()首先检查当前引用的标志与预期引用的标志是否相等,如果都相等,就修改。

2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,CPU开销大。

3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

CAS原理:CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时将内存值修改为B。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS的原子性是由CPI硬件指令实现保证的,即调用native方法调用由C++编写的硬件级别指令,jdk中提供Unsafe类执行这些操作。

ABA问题解决:

版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用先入先出队列锁实现的,即将暂时获取不到锁的线程加入到队列中

AQS内部使用int成员变量来表示同步状态,通过内置双向链表来完成获取资源线程的排队,工作线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列双向链表里。当前节点为head的直接后继才会不断的循环尝试获取锁。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。

我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作AQS帮我们完成了

acquire()--获取同步状态:

a.首先调用重写tryAcquire()获取同步状态,成功则直接返回;

b.线程获取同步状态失败,就构造一个node结点,加入同步队列中;

c.加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便进入阻塞状态,直到被中断或者被前驱结点唤醒。

release()--释放同步状态:

ZooKeeper

是一个开源的分布式应用程序的协调服务;设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的系统,并以提供简单易用的接口给用户使用。实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Leader:是事务请求的唯一调度和处理者,保证集群事务处理的顺序序性,是集群内部各服务器的调度者。

Follower:处理客户端非事务请求,转发事务请求给 Leader 服务器,参与事务请求的投票(需要半数以上服务器通过才能通知leader commit数据;leader发起的提案,要求follower投票),参与 Leader 的选举投票。

Observer(观察者):处理客户端非事务请求,转发事务请求给 Leader 服务器,不参加任何形式的投票,它的存在是为了提高 zk 集群的读性能。

客户端会随机连接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据,如果是写请求,那么请求会被转发给leader提交事务,然后 leader 会广播事务,只要有超过半数节点写入成功,那么写请求就会被提交

ZAB协议:实现分布式数据一致性协议,是一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播。

当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了数据同步之后,ZAB 协议就会退出恢复模式。那么整个服务框架就可以进人消息广播模式了。

当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播。那么新加入的服务器就会自觉地进人数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

消息广播原理

简化版本的二阶段提交过程

1. leader 接收到消息请求后,将消息赋予一个全局唯一的64 位自增 id,叫:zxid,通过 zxid 的大小比较既可以实现因果有序这个特征

2. leader 为每个 follower 准备了一个 FIFO 队列(实现了全局有序这一个特点)将带有 zxid的消息作为一个提案分发给所有的 follower

3. 当 follower 接收到 proposal,先把 proposal 写到磁盘,写入成功以后再向 leader 回复一个 ack

4. 当 leader 接收超过半数节点的 ACK 后,leader 就会向这些 follower 发送 commit 命令,同时会在本地执行该消息

5. 当 follower 收到消息的 commit 命令以后,会提交该消息

崩溃恢复:主要使用了zxid来防止崩溃的leader成为新的leader,并且新的 leader 会让它的旧的zxid未被 COMMIT 的提案清除;1. 所有提案被 COMMIT 之前必须有超过半数的 follower ACK,即必须有超过半数节点的服务器的事务日志上有该提案的 proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被 COMMIT 消息的提案;2.zxid特性当老的leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的提案清除。

zxid:事务 id,为了保证事务的顺序一致性,zookeeper 采用了递增的事务 id 号来标识事务。所有的提议都在被提出的时候加上了 zxid。zxid 是一个 64 位的数字,它高 32 位是 epoch编号(ZAB 协议通过 epoch 编号来区分 Leader 周期变化的策略),用来标识 leader是否改变,每次一个 leader 被选出来,它都会有一个新的epoch=(原epoch+1),标识当前属于那个 leader 的统治时期。低 32 位用于递增计数。

服务器启动时的 leader 选举

在集群初始化阶段,当有一台服务器 Server1 启动时,它本身是无法进行和完成 Leader 选举,当第二台服务器 Server2 启动时,这个时候两台机器可以相互通信,每台机器都试图找到 Leader,于是进入 Leader 选举过程。选举过程如下

(1) 每个 Server 发出投票。由于是初始情况,Server1和 Server2 都会将自己作为 Leader 服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID、epoch,使用(myid, ZXID,epoch)来表示,此时 Server1的投票为(1, 0),Server2 的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。

(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK,PK 规则如下

i. 优先检查 ZXID。ZXID 比较大的服务器优先作为Leader

ii. 如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。对于 Server1 而言,它的投票是(1, 0),接收 Server2的投票为(2, 0),首先会比较两者的 ZXID,均为 0,再比较 myid,此时 Server2 的 myid 最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,它不需要更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于 Server1、Server2 而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了 Leader。

(5) 改变服务器状态。一旦确定了 Leader,每个服务器就会更新自己的状态,如果是 Follower,那么就变更FOLLOWING,如果是 Leader,就变更为 LEADING。

运行中的 leader 选举

当集群中的 leader 服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的Leader 选举,服务器运行期间的 Leader 选举和启动时期的 Leader 选举基本过程是一致的。

(1) 变更状态。Leader 挂后,余下的非 Observer 服务器都会将自己的服务器状态变更为 LOOKING,然后开始进入 Leader 选举过程。

(2) 每个 Server 会发出一个投票。在运行期间,每个服务器上的 ZXID 可能不同,此时假定 Server1 的 ZXID 为123,Server3的ZXID为122;在第一轮投票中,Server1和 Server3 都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票。与启动时过程相同。

(3) 处理投票。与启动时过程相同,此时,Server1 将会成为 Leader。

(4) 统计投票。与启动时过程相同。

(5) 改变服务器的状态。与启动时过程相同

负载均衡:使用 Zookeeper 的临时节点来维护 Server 的地址列表,然后选择负载均衡策略来对请求进行分配()

事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更

分布式锁算法流程:利用有序节点名称唯一性及事件监听机制来实现分布式锁

获取锁的时候,首先就尝试创建一个临时节点,如果已经存在了,说明有其他客户端已经占用这把锁了,这时就对这个临时节点注册一个监听器,当释放锁的时候,删除那个临时节点即可。

客户端连接zookeeper,并在/lock下创建临时的且有序子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;执行业务代码;完成业务流程后,删除对应的子节点释放锁。

curator这个开源项目提供的zookeeper分布式锁实现

FastLeaderElection 选举过程

其实在这个投票过程中就涉及到几个类,FastLeaderElection:FastLeaderElection实现了Election接口,实现各服务器之间基于 TCP 协议进行选举Notification:内部类,Notification 表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的 id、zxid、选举周期等信息ToSend:ToSend表示发送给其他服务器的选举投票信息,也包含了被选举者的 id、zxid、选举周期等信息Messenger : Messenger 包 含 了 WorkerReceiver 和WorkerSender 两个内部类;

WorkerReceiver实现了 Runnable 接口,是选票接收器。其会不断地从 QuorumCnxManager 中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue 中WorkerSender 也实现了 Runnable 接口,为选票发送器,其会不断地从 sendqueue 中获取待发送的选票,并将其传递到底层 QuorumCnxManager 中

SpringCloud

1.微服务和分布式的区别

分布式,就是将偌大的系统划分为多个模块(这一点和微服务很像)部署到不同机器上(因为一台机器可能承受不了这么大的压力或者说一台非常好的服务器的成本可能够好几台普通的了),各个模块通过接口进行数据交互,其实 分布式也是一种微服务;微服务强调的时解耦;

ngnix 反向代理,负载均衡

CAP:一致性,可用性,分区容错性

一致性和可用性,为什么不可能同时成立?

如果保证 S2 的一致性,那么 S1 必须在写操作时,锁定 S2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,S2 不能读写,没有可用性

微服务在Eureka上注册后,会每30秒发送心跳包,Eureka通过心跳来判断服务时候健康,同时会定期删除超过90秒没有发送心跳服务

eureka对比Zookeeper:

Zookeeper在设计的时候遵循的是CP原则,即一致性,Zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时剩余节点会重新进行leader选举,问题在于,选举leader的时间太长:30~120s,且选举期间整个Zookeeper集群是不可用的,这就导致在选举期间注册服务处于瘫痪状态,在云部署的环境下,因网络环境使Zookeeper集群失去master节点是较大概率发生的事情,虽然服务能够最终恢复,但是漫长的选举时间导致长期的服务注册不可用是不能容忍的。

Eureka在设计的时候遵循的是AP原则,即可用性。Eureka各个节点(服务)是平等的, 没有主从之分,几个节点down掉不会影响正常工作,剩余的节点(服务) 依然可以提供注册与查询服务,而Eureka的客户端在向某个Eureka注册或发现连接失败,则会自动切换到其他节点,也就是说,只要有一台Eureka还在,就能注册可用(保证可用性), 只不过查询到的信息不是最新的(不保证强一致),除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%节点都没有正常心跳,那么eureka就认为客户端与注册中心出现了网络故障,此时会出现一下情况:

1: Eureka 不再从注册列表中移除因为长时间没有收到心跳而过期的服务。

2:Eureka 仍然能够接收新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点可用)

3:当网络稳定后,当前实例新的注册信息会被同步到其它节点中

Feign声明式远程服务调用,关键机制就是使用了动态代理,调用接口会调用 Feign创建的动态代理,Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址,最后针对这个地址,发起请求、解析响应

Ribbon是基于Netflix 的一套客户端负载均衡的组件@LoadBalanced,默认轮询;自定义策略要实现AbstractLoadBalancerRule,重写方法就行了

负载均衡算法:随机算法、加权轮询、

一致性hash:将请求的ip、路径、参数进行hash得到hash值,设置hash环的多个临界值,每个服务对应一段hash范围,请求的hash对应哪个范围则由哪个服务处理。

最小活跃性算法:初始时所有的服务提供者活跃数等于0,每收到一个请求活跃数+1,完成请求后活跃数-1,在服务运行一段时间后,性能好的处理请求快活跃数就小,处理请求的能力就强。

灾难性雪崩:服务链条中的某一个服务不可用,导致一系列的服务不可用,最终造成服务逻辑崩溃。

解决方式:降级、隔离、熔断、请求缓存、请求合并

降级-->当请求超时、资源不足等情况发生时,当客户端远程调用的服务端出错时,不返回错误信息,直接执行客户端的fallbackMethod返回基础数据。

请求缓存-->调用服务时首先访问缓存redis,查看缓存数据,如果缓存中有数据,则不调用服务端, @cache注解管理,但需要维护好缓存,要解决非幂等性(不推荐)

熔断-->一定时间内异常请求比例(请求超时、网络故障、服务异常等)达到阀值时,启动熔断器,熔断器一旦启动,则会停止调用具体服务逻辑,通过fallbackMethod快速返回托底数据,保证服务链的完整;自动恢复机制:当熔断器启动后,每隔5秒,尝试将新的请求发送给服务提供者,如果服务可正常执行并返回结果,则关闭熔断器,服务恢复。

隔离-->1.线程池隔离是将并发请求量大的部分服务使用独立的线程池处理,避免因个别服务并发过高导致整体应用宕机,线程名默认为服务的分组名,可以在注解的参数上直接配置线程池的参数,tomcat容器默认的线程池会将请求转交给Hystrix定义的线程池,处理结束后,由定义的线程池进行返回,无需还回tomcat容器默认的线程池,但硬件计算开销加大(CPU计算、线程切换,资源调度,上下文转换等)2.信号量隔离请求并发量默认值是10。常见配置500~1000,如果并发请求超过配置,其他请求进入fallbackMethod逻辑(使用tomcat线程池,效率较高)

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个调用服务出问题时,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

Zuul是微服务网关,底层使用Ribbon来实现请求的路由,并内置Hystrix,可选择性提供网关fallback逻辑,Zuul过滤器必须继承ZuulFilter父类

前置过滤:是请求进入Zuul之后,立刻执行的过滤逻辑。路由后过滤:是请求进入Zuul之后,并Zuul实现了请求路由后执行的过滤逻辑,路由后过滤,是在远程服务调用之前过滤的逻辑。后置过滤:远程服务调用结束后执行的过滤逻辑。异常过滤:是任意一个过滤器发生异常或远程服务调用无结果反馈的时候执行的过滤逻辑。无结果反馈,就是远程服务调用超时

SpringCloud Alibaba:

Nacos:服务发现和服务健康监测,动态配置服务,springCloud继承Nacos是spring-clou-commons包的spring.factories中包含AutoServiceRegistrationAutoConfiguration配置类,NacosAutoServiceRegistration监听WebServerInitializedEvent事件(当Webserver初始化完成之后),调用register()方法进行服务注册并完成心跳方法,Nacos是通过Spring的事件机制继承到SpringCloud中去的

心跳机制就是客户端通过schedule定时向服务端发送一个数据包 ,然后启动一个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障。Nacos服务端会根据客户端的心跳包不断更新服务的状态

Nacos服务地址动态感知:

临时节点采用心跳注册 1.客户端发起时间订阅后,每10s发送一次Pull请求,获得服务端最新的地址列表;2.服务端和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Push消息给Nacos客户端,并更新本地服务地址列表

Sentinel:面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性

seate:分布式事务

AT模式:第一阶段:seate会拦截解析更新的业务sql,然后将业务数据再更新前后保存到日志表中,业务数据和回滚日志记录在同一个本地事务中提交。第二阶段:没问题则提交并删除日志,有问题就回滚,通过第一阶段的日志进行反向补偿。

TCC模式:Try - Confirm - Cancel三个操作,二阶段提交;幂等性:加唯一标识。你可以生成唯一标识符并将其添加到业务中代表已经处理过。

TCP链接

第一次握手:建立连接时,发送连接请求报文段,将SYN标识位置为1并发送同步序列编号(seq=x)到服务器,并进入SYN_SENT状态,等待服务器确认。

第二次握手:服务器收到报文后,将SYN标识位置为1,ACK置为1,同时自己也发送一个SYN包(seq=y,ack=x+1),此时服务器进入SYN_RECV半链接状态;

第三次握手:客户端收到服务器报文后,向服务器发送确认包(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

1、客户端发送一个报文给服务端(没有数据),其中FIN设置为1表示请求释放链接,Seq = u,客户端进入FIN_WAIT_1状态。

2、服务端收到来自客户端的请求,发送一个ACK给客户端,ack = u+1 Seq = u,服务端年进入CLOSE_WAIT状态。

前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求"

3、服务端做好了释放服务器端到客户端方向上的连接准备后,发送一个FIN给客户端,ack = u+1 Seq = w,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态,并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过 来的数据。

4、客户端收到FIN后,确认了服务器端已做好释放连接的准备,进入TIME_WAIT状态,接着发送一个ACK给服务端,Acknowledge置为w+1,Sequence Number置为u+1,最后客户端和服务端都进入CLOSED状态。

问题:

1.客户端在TIME-WAIT阶段要等2MSL?

答:目的是确认服务器端是否收到客户端发出的ACK确认报文;所以客户端在最后发送完ACK确认报文之后,会等带2MSL;如果服务器端在1MSL内没有收到客户端发出的ACK确认报文,就会再次向客户端发出FIN报文。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。