Redis篇章
Redis使用场景
缓存穿透
- 什么是缓存穿透
- 当查询的数据不存在时,每次查询都需要经过redis和db但是无法查询到数据,称做缓存穿透。
- 如何解决缓存穿透
- 将DB返回的空缓存到redis中,可以有效防止缓存穿透。
- 使用布隆过滤器
- 什么是布隆过滤器
- 在redis之前,当查询数据时,会先经过布隆过滤器查看数据是否存在,如果存在,则到redis中去查找,若数据不存在,则直接返回。
- 布隆过滤器底层是用bitmap(位图)来实现的,对数据进行三次hash运算,获得三个索引值,起初所有的bitmap的值都是0,将三个索引值变为1。
- 缺点:当数组长度小时,容易产生误判,即一个数据不存在时,但是经过三次hash运算获得的三个索引值,都存有数据1,则产生了误判,但是位图的误判率小于百分之5,可以忽略。
- 什么是布隆过滤器
缓存击穿
- 什么是缓存击穿
- 当缓存的key过期了,在这个过期的节点,突然有大量访问请求进入,导致服务器处理不过来,称为缓存击穿。不同于缓存穿透,穿透是经过了redis和db都无法查询到数据。击穿是将缓存穿烂了。
- 如何解决缓存击穿
- 互斥锁:强一致、性能差
- 当一个请求过来,开启线程一,当线程一访问redis缓存时,没有命中,然后去获取互斥锁,接着去db查询数据,重建缓存,最后写入缓存,并释放锁。而在释放锁之前,有线程二出现,线程二查询缓存没有命中,获取互斥锁失败,休眠一会继续重试,直到查询到了缓存。返回的数据永远同db中相同,称为强一致,性能差则是当没有查询到缓存时,只能有一个线程获取锁,其他线程只能等待。
- 逻辑过期:高可用、性能优
- 不设置key的过期时间,设置一个逻辑过期字段。当线程一查询缓存发现逻辑过期,获取互斥锁,然后开启新线程二(该线程去查询db重建缓存,重置逻辑过期时间,最后释放锁),而线程一则会返回过期数据。其他线程类似,无论如何都会返回数据。保证了每次访问都能有返回数据,性能非常好。
- 两种解决方法的比较:都会过去互斥锁,但是唯一的不同在于,一个只会返回一致的数据,另外一个获取锁失败会返回过期数据。
- 互斥锁:强一致、性能差
缓存雪崩
- 什么是缓存雪崩
- 当大量缓存都设置了相同的过期时间,当缓存都过期了的时候,或者是redis服务器宕机,大量请求同时访问db,db压力过大雪崩。与击穿相比,击穿是某一个Key过期。
- 如何解决缓存雪崩
- 给缓存key设置不同的过期时间。
- 开启redis集群
- 采用限流熔断机制进行保底处理
- 开启多级缓存
如何保证数据库和Redis的数据一致性
- 双写一致性:当修改了数据库的数据的同时也要同时更新缓存的数据。
- 读操作:缓存命中,直接返回;缓存没有命中查询数据库,写入缓存,设定超时时间。
- 写操作:延迟双删,先删除缓存---->修改数据库---->延时一会----->删除缓存
- 先删除缓存还是先修改数据库
- 都可能出现脏数据
- 为什么要双删?
- 降低脏数据的出现
- 为什么要延时?
- 让主从数据库同步,但是延时的时间不好控制,所以还是有可能出现脏数据
- 先删除缓存还是先修改数据库
- 添加分布式锁
- 效率太低
- redisson的读写锁
- 共享锁:其他线程只能读,进行读操作时添加。
- 排它锁:其他线程没有任何操作权限,进行写操作时添加。
- 强一致、性能低。
- 异步通知
- 使用MQ消息中间件
- 更新数据之后,通知缓存删除。
- 使用基于mysql的主从同步的Canal
- 不需要修改业务代码,伪装为MySQL的一个从节点,读取mysql的binlog数据更新缓存。
- 使用MQ消息中间件
Redis作为缓存,数据的持久化是怎么做的
- RDB:Redis Database Backup file(Redis数据备份文件),redis数据快照。把内存中的所有数据都记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据。
- 如何开启RDB
- 手动开启:save 、bgsave
- 自动开启:在redis.conf文件中配置
- 执行原理
- 当bgsave时,主进程会fork一个子进程出来,主进程通过页表来操作真实内存数据,子进程fork的也只是主进程的页表,此时内存中的数据是read-only,子进程读取数据到一个新的RDB中,并且会替换原先的RDB。
- 如果需要写数据,主进程进行写数据时,会对原数据进行拷贝,然后在备份上进行读写操作,同时修改页表对应的内存数据为备份数据。
- AOF:Append Only File(追加文件)。Redis处理的每一个命令都会记录在AOF文件,可以看做是命令日志文件。默认关闭,需要修改redis.conf来开启AOF。AOF的命令记录的频率也通过配置文件来配置。AOF记录的命令操作,对同一个key的命令通常只有最后一个写操作才有意义,可以通过执行bgwriteaof命令,让AOF文件执行重写功能,用最少得命令达到相同效果。
- RDB与AOF的比较
- 持久化方式:RDB是对整个内存进行快照,AOF记录每一次执行的命令。
- 数据完整性:不完整,两次备份之间会丢失数据(例如,备份时间设置为60s,这时间内redis宕机了),AOF相对完整,取决于刷盘策略。
- 文件大小:AOF文件体积很大。
- 宕机恢复速度:RDF快
- 数据恢复优先级:RDF低,因为数据完整性不如AOF。
- 系统资源占用:RDF高,备份整个内存,要占用大量CPU和内存消耗。AOF低,主要是磁盘IO资源,但是AOF重写时,CPU和内存资源占用大。
- 使用场景:RDB可以容忍数分钟的数据丢失,追求更快的启动速度。AOF是对数据安全性要求高。
假如Redis的key过期之后,会立即删除吗?(数据过期策略)
- 惰性删除:当访问某个Key时,如果key过期了则删除,否则返回这个数据。
- 优点:对一些不常用的Key 不用去定期检查是否过期。
- 缺点:浪费内存,如果key已经过期了,但是一直没有使用,则会一直保存在内存中。
- 定期删除:每隔一段时间,选择一些key进行过期检查,如果过期则删除。每个key都会检查到。
- 定期删除的两种模式
- SLOW模式:定时任务,执行频率默认为10hz(每秒执行10次),每次不超过25ms,通过redis.conf文件修改。
- FAST模式:执行频率不固定,但每次间隔不低于2ms,每次耗时不超过1ms
- 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。也能有效释放过期key占用的内存。
- 缺点:难以确定删除操作执行的时长和频率。
- 定期删除的两种模式
- redis的过期策略是惰性删除和定期删除两种策略进行配合使用。
Redis的数据淘汰策略
- redis提供了8种不同的数据淘汰策略,默认是不删除任何数据,内存不足直接报错。
- 两种重要的淘汰策略:
- LRU:最近最久未使用算法
- LFU:最少频率使用算法。
- 最常用的淘汰策略:allkeys-lru:挑选最近最久未使用的数据淘汰策略,留下来的都是经常访问的热点数据。
Redis分布式锁
- 背景
- 当一个项目进行集群的时候,synchonize锁已经无法满足需要,引入分布式锁,让每个集群的线程都共享同一个锁。
- redisson
- 执行流程:线程一获取redission锁,同时会开辟一个新线程watchdog来对锁进行续期。线程二获取redission锁时,会有一个while循环来重复获取锁,这个循环次数可以控制。
- 原理:底层时setnx和lua脚本(保证原子性)
- 可以重入吗:可以,底层时hash结构,key是锁,value是线程信息和重入的次数。
- 能解决主从数据一致的问题吗:
- 什么是主从数据一致?
- redis集群中所有数据一致,当主节点还没有将数据同步到从节点时,主节点宕机了,redis提供的哨兵模式,会在从节点中找到一个作为主节点。新线程来访问redis时,会访问新的主节点。导致宕机前的线程和新线程持有同一把锁 ,可能导致脏数据。
- 如何解决?
- 不能解决,但是可以使用红锁,但是这样的话,性能太低了。建议采用zookeeper实现的分布式锁。
- 什么是主从数据一致?
其他redis面试题
主从复制
- 是什么
- 单节点的redis的并发能力有限,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,主节点负责写数据,从节点负责读数据。
- 全量同步:
- 从节点请求主节点同步数据(replication id、offset)
- 主节点判断是否是第一次请求,是的话就与从节点同步版本信息。
- 主节点执行bgsave,生成rdb文件,发送给从节点去执行。
- 在rdb文件生成期间,主节点下执行的命令会记录到一个日志文件中。
- 把日志文件发送给从节点进行同步。
- 增量同步:
- 从节点请求主节点同步数据,主节点判断不是第一次请求,获取从节点的offset
- 主节点的日志文件获取offset值之后的数据,发送给从节点进行数据同步。
哨兵模式
- 哨兵的作用
- 监控:监控主从节点是否正常工作
- 自动故障恢复:
- 通知:通知客户端去redis服务器获取数据
- redis脑裂
- 什么是脑裂
- redis集群时,当主服务器和从服务器不在一个网络分区时,网络延迟时,哨兵没有感知到主服务器的存在,这时,哨兵会将一个从变主,当哨兵感知到老主的存在时,会将老主降为从节点,则会导致大量数据丢失。
- 如何预防脑裂
- 通过redis.conf进行配置,可以设置最少得从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
- 什么是脑裂
分片集群
- 分片集群有什么作用?
- 集群中有多个master,每个master保存不同的数据
- 每个master有多个slave节点
- master之间通过Ping检测彼此健康状态
- 客户端请求可以访问任意集群节点,最终都会被转发到正确的节点
- redis分片集群中数据时怎么存储和读取的?
- redis分片集群引入了哈希槽的概念,redis集群有16384个哈希槽
- 将16384个插槽分配到不同的实例
- 读写数据:根据Key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,则有效部分是大括号的内容,否则是key本身),余数作为插槽,寻找插槽所有的实例。
redis是单线程的,为什么还那么快?
- 结论
- redis直接操作内存
- 单线程,避免了不必要的上下文切换可竞争条件,不存在线程安全问题
- 采用I/O多路复用模型
- 解释一下I/O多路复用模型
- Redis是纯内存操作,执行速度快,性能瓶颈是网络延迟,I/O多路复用模型主要就是实现了高效的网络请求。
- I/O多路复用:
- 单线程同时监听多个socket,只要有一个socket有回应时就会得到通知,避免无效等待,充分利用CPU资源。目前采用的是epoll模式,会在socket就绪的同时,将其写入用户空间,不需要redis挨个便利socket来判断是否就绪,提升了性能。
- Redis网络模型
- 使用I/O多路复用结合事件的处理器来应对多个socket。
- 连接应答处理器
- 命令回复处理器,在6.0之后,使用多线程
- 命令请求处理器,在6.0之后,将命令的转化使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程。
- 使用I/O多路复用结合事件的处理器来应对多个socket。
MySQL篇章
优化
在MySQL中,如何定位慢查询?
- 采用第三工具,例如运维工具Skywalking,可以监测处哪个接口执行慢。
- 在mysql中开启慢日志查询,例如设置的值为2s,则一旦sql执行 超过2s就会记录到日志中(调试阶段)
那这个SQL语句执行很慢,如何分析呢?
可以采用MySQL自带的分析工具EXOLAIN
- 通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)
- 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
- 通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改字段来修复。
索引概念和底层数据结构
什么是索引?
- 索引是帮助MySQL高效获取数据的数据结果
- 提高数据检索的效率
- 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗
有了解过底层数据结构吗?
MySQL的innoDB引擎采用的B+树的数据结构来存储索引
- 阶数更多,路径更短
- 叶子节点存储数据,非叶子节点只存储指针
- B+树便于扫库和区间耗材想你,叶子节点是一个双向链表
什么是聚簇索引?什么是非聚簇索引(二级索引)
聚集索引:数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
二级索引:数据与索引分开,B+树的叶子节点保存对应的主键,可以有多个
什么是回表查询?
通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表。
什么是覆盖索引?
查询使用了索引,并且返回的列在索引中全部能找到。
MySQL超大分页怎么处理?
- 问题:在数据量较大的时候,limit分页查询,需要对数据进行排序,效率低
- 解决方案:覆盖索引+子查询,先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据,因为查询id的时候采用的是覆盖索引。
索引创建的原则
- 数据量大,查询频繁的表
- 常作为where order by group by 操作的字段
- 字段内容区分度高
- 尽量使用联合索引
- 控制索引的数量
什么情况下,索引会失效?
- 违反最左前缀法则:当联合索引时,必须按照从左到右索引来查询,不能跳过索引。
- 范围查询右边的列,不能使用索引:如果依次查询的条件有一个条件是范围,则这个条件右边的查询列不能使用索引。
- 对索引列进行运算操作
- 字符串不加单引号
- 以%开头的模糊查询
谈谈你对sql的优化的经验?
- 表的优化设计(参考阿里开发手册)
- 设置合适的数值
- 设置合适的字符串类型
- 索引优化
- 为常作为where,order by ,group by 的字段创建索引
- 为字段区分度高的字段创建索引
- 使用联合索引
- 控制索引的数量
- 避免索引失效的操作:例如最左,范围查询右边,索引列进行运算操作等
- sql语句优化
- select语句指定字段名称
- 避免造成索引失效的写法
- 避免在where中对字段进行表达式操作
- Join优化,使用inner join,内连接会对两个表进行优化,优先把小表放到外边,大表放到里边。
- 主从负载、读写分离,不让数据的写入,影响读操作
- 分库分表
其他面试题
事务的特性是什么?详细说一下
- 原子性Atomicity
- 一致性Consistency
- 隔离性Isolation
- 持久性Durability
- 例子:A向B转账500元,转账成功,A扣除500,B增加500,原子性是要不全部成功,要不全部失败。在转账的过程中,数据要保存一致,A扣除500,B必须增加500。转账过程不受其他事务的影响。转账完成之后,要不数据持久化。
并发事务问题、隔离级别
- 并发事务问题
- 脏读:一个事务读到另一个事务还没有提交的数据
- 不可重复读:一个事务先后读取到同一条记录,但两次读取的数据不同
- 幻读:一个事务查询一条数据时,刚开始不存在,但是在插入数据时,发现这行数据已经存在。
- 怎么解决?
- 对事务进行隔离
- 读未提交(read uncommitted):不能解决
- 读已提交(read committed):解决了脏读
- 可重复读(默认级别 repeatable read):解决了脏读、不可重复读
- 串行化(serializable):全部解决
- 对事务进行隔离
undo log 和 redo log 的区别?
- redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
- undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
- redo log保证了事务的持久性,undo log 保证了事务的原子性和一致性。
事务中的隔离性如何保证?
- 锁:排他锁(当一个事务获取了排它锁,其他事务就无法获取)
- mvcc:多版本并发控制
解释一下mvcc
MySQL中的多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突。
- 隐藏字段
- trx_id(事务id):记录每一次操作的事务id,自增
- roll_pointer(回滚指针):指向上一个版本的事务版本记录地址
- undo log:
- 回滚日志,存储老版本数据
- 版本链:多个书屋并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
- readView解决的是一个事务查询选择版本的问题
- 根据readView的匹配规则和当前一些事务id判断该访问哪个版本的数据
- 不同的隔离界别快照读是不一样的,最终的访问的结果不一样
- RC(读已提交):每一次执行快照读时生成ReadView
- RR(可重复读):仅在事务中第一次执行快照读时生成ReadView,后续复用。
MySQL主从同步原理
核心就是二进制日志binlog,包含DDL(数据定义语言)和DML(数据操纵语言)
- 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。
- 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log。
- 从库重做中继日志中的事件,将改变反映它自己的数据。
分库分表
- 水平分库:将一个库的数据拆分到多个库中,解决海量数据存储的和高并发的问题
- 水平分表:解决单表存储和性能的问题
- 垂直分库:根据业务进行拆分,例如用户服务和订单服务各放一个数据库中
- 垂直分表:冷热数据分离,例如一个数据表中的多个字段,有些字段很少用,这个字段则提取出来单独放置一个表中。
框架篇
Spring
单例bean是线程安全的吗?
不是线程安全的。Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。因为一般在spring中的bean都是注入无状态的对象,没有线程安全问题。如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决。
AOP的相关面试题
什么是AOP?
面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合。
项目中使用到了吗?
记录操作日志、登录权限校验,通过环绕通知 + 切点表达式/自定义注解
Spring中的事务是如何实现的?
利用了AOP编程,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
事务失效的场景
- 异常捕获处理,自己处理了异常(自己进行catch),没有抛出,解决:手动抛出
- 抛出检查异常,配置rollbackFor属性为Exception
- 非public方法导致的事务失效,改为public
Spring的bean的生命周期
- 通过BeanDefinition获取bean的定义信息
- spring在进行实例时,会将xml中的的信息封装成一个BeanDefinition对象。
- 调用构造函数实例化bean
- bean的依赖注入
- 处理Aware接口(实现三个aware接口,重写三个方法)
- Bean的后置处理器BeanPostProcessor-前置
- 初始化方法,实现initializingBean接口,自定义的初始化方法
- Bean的后置处理器BeanPostProcessor-后置
- 销毁bean
Spring的循环依赖
- 什么是循环依赖:两个或两个以上的bean相互持有对方,比如A依赖B,B依赖A。
- 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
- 二级缓存:缓存早起的bean对象
- 三级缓存:缓存的是ObejctFactory,对象工厂,用来创建某个对象
- 构造方法出现了循环依赖如何解决?
- 使用@Lazy进行懒加载
SpringMVC的执行流程
- 用户发出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用handlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器,再一起返回给DispatcherServlet
- DispatcherServlet调用HandlerAdapter(处理器适配器)
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
- 方法上添加了@ResponseBody
- 通过HttpMessageConverter来返回结果转换为JSON并响应
SpringBoot自动配置原理
- 引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
- @EnableAutoConfiguration是实现自动化配置的核心注解,该注解通过@import注解导入对应的配置选择器。
- 内部就是读取了该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名,在这些配置类中所定义的Bean会根据条件注解所制定的条件来决定是否需要将其导入到Spring容器中。
- 条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
Spring框架常见注解
Spring的注解
- Component Controller Service Responsitory:实例化bean
- @Autowired
- @Qualifier:根据名称进行依赖注入
- @Scope:标注bean的作用范围
- @Configuration
- @ComponentScan:用于指定Spring在初始化容器时要扫描的包
- @Bean
- @Import
- @Aspect、Befor、After、Around、Pointcut
SpringMVC的注解
- @RequestMapping
- @RequestBody
- @RequestParam:指定请求参数的名称
- @PathVirable:从请求路径中获取请求参数(/user/id),传递给方法的形式参数
- @ResponseBody
- @RequestHeader:获取指定的请求头数据
- @RestController:@Controller + @ResponseBody
SpringBoot常见注解
- @SpringBootConfiguration:组合了@Configuration注解,实现配置文件的功能。
- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选择。
- @ComponentScan:Spring组件扫描
Mybatis执行流程
- 读取Mybatis配置文件:mybatis-config.xml加载运行环境和映射文件
- 构造会话工厂SqlSessionFactory
- 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
- 操作数据库的接口,Excutor执行器,同时负责查询缓存的维护。
- Executor接口的执行方法中有一个MapperStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
Mybaits是否支持延迟加载?
支持延迟加载,但默认没有开启。
- 什么叫做延迟加载?
- 例如,一个用户表有一个字段表示对应的订单表,一个用户对应多个订单,然后当查询用户的时候,订单的数据不会立即查询。订单字段加上fetch = lazy。
- 原理
- 使用CGLIB创建目标的代理对象
- 当调用目标方法时,进入拦截器Invoke方法,发现目标方法是null值,执行sql查询
- 获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了。
Mybatis的一级、二级缓存用过吗?
- 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当session进行flush或者close之后,该session中的所有Cache将被清空,默认打开一级缓存。
- 二级缓存:基于namespace和mapper的作用域,不依赖于sql session。需要单独开启,核心配置,mapper映射文件。
二级缓存什么时候会清理缓存中的数据?
当某一个作用域(一级缓存session或者二级缓存namespaces)进行增删改操作,默认该作用域下索引select的缓存被清空。
集合篇
ArrayList底层的实现原理是什么?
- 动态数组
- 初始容量为0,当第一次添加数据时,才会初始化容量为10
- 进行扩容的时候,为原来容量的1.5倍,每次扩容时都需要拷贝数组。
- 在添加数据时,
- 判断size+1是否大于当前的数组长度,则调用grow方法扩容。
如何实现数组和List之间的转换?
- 数组转list集合:调用java.utils.Arrays包的asList方法
- list集合转数组:调用list集合的toArray(参数为数组类型和长度)
- 如果数组或者list集合内容改变,转换后的集合或者数组会变吗?
- asList会变化,因为底层使用的是,Arrays类中的一个内部类ArraysList,最终指向的都是同一个内存地址。
- toArray不会变化,底层是对数组的拷贝。
ArrayList和LinkedList的区别是什么
- 底层数据结构
- 动态数组
- 双向链表
- 效率
- 前者可以根据索引查询
- 头尾增删都是O(1),其他需要遍历,为O(n)
- 空间
- 前者内存连续,节省内存
- 双向链表更占用内存
- 安全问题
- 都不是线程安全
说一下HashMap的实现原理?
- 底层数据结构是散列表(数组+链表\红黑树)
- 添加数据时,计算key的值确定元素在数组中的下标
- key相同则替换
- 不同则存入链表或红黑树中
HashMap的jdk1.7和1.8有什么区别?
- 1.8之前采用的拉链法,数组和链表
- 1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转化为红黑树。
HashMap的put方法的具体流程
- 判断table是否为空或者长度是否为0,则进行resize扩容
- 根据key计算hash值得到数组索引
- 判断该位置是否为空,直接添加
- 否则,
- 判断首个元素的key是否相等,相等则覆盖value
- 判断链表是否为红黑树,在树中插入键值对
- 否则在链表尾部插入数据,然后判断链表长度是否大于8,进入树化方法(在这个方法里面会判断如果数组长度小于64,则进行扩容,反之才真正树化)
- 插入成功后,判断实际存在的键值对数量size是否超过数组长度0.75。
HashMap的扩容机制
调用resize方法。
- 首先判断oldCap是否为0,如果为0,则将newCap设置为16,newThr为16*0.75=12
- 如果大于0,则将newCap和newThr扩容为原来的2倍。
- 同时,将数组进行复制,判断节点是否为空,
- 节点为空,跳过
- 节点不为空,判断下一个节点是否为空(判断是否产生哈希冲突)
- 为空,e.hash&newcap-1计算索引,添加到新数组。
- 不为空,判断是否是树节点,红黑树添加。反之,遍历链表,可能需要拆分链表,判断e.hash&oldCap是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+oldCap这个位置
HashMap的寻址算法
- 计算对象的hashCode()
- 调用hash方法进行二次哈希,hashcode值右移16位再进行异或运算。扰动算法,让哈希分布更加均匀。
- 最后cap-1&hash得到索引。
HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是2的n次幂,可以使用位运算代替取模
- 扩容时重新计算索引效率更高:hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
HashMap在1.7的情况下的多线程死循环问题?
- jdk1.7中,hashMap的底层数据结构是数组和链表,进行扩容时,迁移链表是头插法。
- 链表AB,线程一将A移入新链表,线程二介入,将老链表的AB移入新链表,BA,B指向A,线程二执行完毕。线程一将B移入新链表,但是此时由于线程二的原因,B指向了A,导致了死循环。JDK8使用尾插法解决了这个死循环问题。
微服务篇
SpringCloud的五大组件?
- Eureka、
- Ribbon:负载均衡
- Feign:远程调用
- Hystrix:服务熔断
- Zuul、Gateway:网关
SpringCloudAlibba的五大组件?
- Nacos
- Ribbon
- Feign
- Sentinel
- Gateway
Nacos和Eureka的区别
- 共同点
- 都支持服务注册和发现
- 健康检查
- 不同点
- Nacos支持服务端主动监测提供者状态:临时实例采用心跳模式,非临时实例采用主动监测模式
- 非临时实例心跳不健康不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时。
- Nacos集群默认采用AP(高可用)方式,当集群中存在非临时实例时,采用CP(强一致)模式,Eureka采用AP方式
- Nacos支持配置中心
Ribbon负载均衡
你们项目负载均衡如何实现?
使用Ribbon,远程调用的Feign的底层的负载均衡使用的是ribbon
Ribbon负载均衡的策略有哪些?
- 简单轮询
- 按照权重轮询
- 随机选择一个可用的服务器
- 区域敏感策略:以区域可用的服务器为基础进行服务器的选择。
如果想自定义负载均衡策略如何实现
- 实现IRule接口,可以指定负载均衡策略(全局)
- 在客户端的配置文件中,可以配置某一个服务器调研的负载均衡策略(局部)
什么是服务雪崩,如何解决?
- 服务雪崩是微服务中,某一个服务失败,导致整个链路的服务都失败。
- 如何解决:
- 服务降级:一般与feign接口整合,编写降级逻辑,针对某个接口
- 服务熔断:接口调用超时比率达到一个阈值,会开启熔断,后续的请求直接执行设置的默认方法,达到服务降级的效果。
微服务如何监控
采用Skywalking。
- 可以看到哪些接口和服务比较慢,可以进行针对性的分析和优化。
- 可以设置告警规则,如果报错,分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的Bug,第一时间修复。