java面试知识点总结

AI摘要
这是一份关于Java后端开发技术栈的综合性知识总结,内容涵盖了数据库优化、Spring框架、并发编程、分布式系统、中间件等多个核心领域。文档以问答形式整理了常见面试题及其解答要点,属于典型的【知识分享】型内容,旨在为开发者提供技术复习和面试准备参考。

mysql从性能上优化

1、硬件 cpu ,内存、磁盘读写速度, 网络带宽
2、架构层面,搭建mysql集群,防止单点故障,读写分离, 读多写少,可以避免读写冲突导致性能下降。
3、分库分表微服务比较常用。一般单体架构容易出问题
4、对热点数据进行redis缓存处理
5、sql优化,等等

Spring Bean 生命周期的执行流程

创建前准备,创建实例、依赖注入、容器缓存、销毁实例

1、上下文相关配置, BeanFactoryPostProcessor
2、通过反射来创建bean的实例,并扫描和解析bean的属性
3、通过Auwired或者构造方法注入
4、把bean缓存到容器及spring缓存中,
5、spring应用上下文关闭时, 该上下文的所有Bean都会被销毁

#Spring 是如何解决循环依赖问题的
我们知道,当俩个Bean相关相互持有对方的引用时,就会出现循环依赖问题,
使用注解 @Lazy延迟加载解决

Springboot 约定大于配置理解

1、约定大于配置是一种软件设计的思想,
2、基于传统的spring项目,我们要做很多与业务无关的配置,比如管理jar包,xml,应用部署到web服务器,第三方组件集成到ioc容器当中。
3、而现在的springboot项目,我们不要管理这些配置,它帮我们完成了配置。
4、springboot starter启动依赖,它能帮我们管理jar包版本
5、如果是spring mvc项目,springboot内置了tomcat,我们不要在去安装部署 tomcat
6、默认加载的配置文件application.properties

lock 和 synchronized 区别

1.从功能角度来看,都是用来解决线程安全问题的。
2.synchronized是java关键字, lock是juc包提供的接口,reentrantlock是实现类,
3、synchronized通过两种方式来控制锁的力度,方法上+代码块
4、Lock 锁的粒度是通过它里面提供的 lock()和 unlock()方法决定的,包裹在这两个 方法之间的代码能够保证线程安全性。而锁的作用域取决于 Lock 实例的生命周 期
5、lock 比 Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什么 时候释放锁,只需要调用 lock()和 unlock()这两个方法就行,同时 Lock 还提供了 非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回 true/false 来告诉当前线 程是否已经有其他线程正在使用锁

线程池如何知道一个线程的任务已经执 行完成?

1、CountDownLatch计数器 常用的
2、isTerminated()方法,可以判断线程池的运行状态
3、有一个submit()方法,它提供了一个Future的返回值

网络四元组

源ip+源端口+目标ip+目标端口

Redis 和 Mysql 如何保证数据一致性

延时双删策略,
redis.del(key);
db.update(data);
Thread.sleep(100);
redis.del(key);

Spring Boot 中自动装配机制的原理

不需要开发人员再去写 Bean 的装配配置。 在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就 可以实现自动装配。
@SpringBootApplication 是 一 个 复 合 注 解 , 真 正 实 现 自 动 装 配 的 注 解 是 @EnableAutoConfiguration。自动装配的实现主要依靠三个核心关键技术。 引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对 象

死锁的发生原因和怎么避免

1、死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共享 资源造成的相互等待的现象。 如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源 的线程就称为死锁线程。
2、导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。 所以,只能在写代码的时候,去规避可能出现的死锁问题

#请说一下你对分布式锁的理解,以及分布 式锁的实现???

说说缓存雪崩和缓存穿透的理解,以及如何避免?

1、缓存雪崩问题,就是存储在缓存里面的大量数据,在同一个时刻全部过期, 原本缓存组件抗住的大部分流量全部请求到了数据库。 导致数据库压力增加造成数据库服务器崩溃的现象。导致缓存雪崩的主要原因,我认为有两个: 缓存中间件宕机,当然可以对缓存中间件做高可用集群来避免。 缓存中大部分 key 都设置了相同的过期时间,导致同一时刻这些 key 都过期了。 对于这样的情况,可以在失效时间上增加一个 1 到 5 分钟的随机值。 2、缓存穿透问题,表示是短时间内有大量的不存在的 key 请求到应用里面,而这些不存在的key在缓存里面又找不到,从而全部穿透到了数据库,造成数据库压力。 我认为这个场景的核心问题是针对缓存的一种攻击行为,因为在正常的业务里面, 即便是出现了这样的情况,由于缓存的不断预热,影响不会很大。 而攻击行为就需要具备时间是的持续性,而只有 key 确实在数据库里面也不存在 的情况下,才能达到这个目的,所以,我认为有两个方法可以解决: 把无效的 key 也保存到 Redis 里面,并且设置一个特殊的值,比如“null”,这样 的话下次再来访问,就不会去查数据库了。
3、不过,在我看来,您提出来的这个问题,有点过于放大了它带来的影响。 首先,在一个成熟的系统里面,对于比较重要的热点数据,必然会有一个专门缓 存系统来维护,同时它的过期时间的维护必然和其他业务的 key 会有一定的差别。 而且非常重要的场景,我们还会设计多级缓存系统。 其次,即便是触发了缓存雪崩,数据库本身的容灾能力也并没有那么脆弱,数据库的主从、双主、读写分离这些策略都能够很好的缓解并发流量。 最后,数据库本身也有最大连接数的限制,超过限制的请求会被拒绝,再结合熔断机制,也能够很好的保护数据库系统,最多就是造成部分用户体验不好。 另外,在程序设计上,为了避免缓存未命中导致大量请求穿透到数据库的问题, 还可以在访问数据库这个环节加锁。虽然影响了性能,但是对系统是安全的。

讲一下 wait 和 notify 这个为什么要在 synchronized 代码块中?

1、wait 和 notify 用来实现多线程之间的协调,wait 表示让线程进入到阻塞状态, notify表示让阻塞的线程唤醒。 wait 和 notify 必然是成对出现的,如果一个线程被 wait()方法阻塞,那么必然需 要另外一个线程通过 notify()方法来唤醒这个被阻塞的线程,从而实现多线程之 间的通信。 在多线程里面,要实现多个线程之间的通信,除了管道流以外,只能通过共享变 量的方法来实现,也就是线程 t1 修改共享变量 s,线程 t2 获取修改后的共享变 量 s,从而完成数据通信。 但是多线程本身具有并行执行的特性,也就是在同一时刻,多个线程可以同时执 行。在这种情况下,线程 t2 在访问共享变量 s 之前,必须要知道线程 t1 已经修 改过了共享变量 s,否则就需要等待。 同时,线程 t1 修改过了共享变量 S 之后,还需要通知在等待中的线程 t2。 所以要在这种特性下要去实现线程之间的通信,就必须要有一个竞争条件控制线 程在什么条件下等待,什么条件下唤醒。

2、变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变 量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线 程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之 前的通信。 所以这也是为什么 wait/notify 需要放在 Synchronized 同步代码块中的原因,有 了 Synchronized 同步锁,就可以实现对多个通信线程之间的互斥,实现条件等 待和条件唤醒。 另外,为了避免 wait/notify 的错误使用,jdk 强制要求把 wait/notify 写在同步代 码块里面,否则会抛出 IllegalMonitorStateException 最后,基于 wait/notify 的特性,非常适合实现生产者消费者的模型,比如说用 wait/notify 来实现连接池就绪前的等待与就绪后的唤醒。 以上就是我对 wait/notify 这个问题的理解。

3、其实考察的就是 Synchronized、wait/notify 的设计原理和实现原理。 由于 wait/notify 在业务开发整几乎不怎么用到,所以大部分人回答不出来。 其实并发这块内容理论上来说所有程序员都应该要懂,不管是它的应用价值,还 是设计理念,非常值得学习和借鉴。

ThreadLocal是什么?它的实现原理呢?

1、是什么,ThreadLocal被称作线程局部变量,当我们定义了一个ThreadLocal变量,所有的线程共同使用这个变量,但是对于每一个线程来说,实际操作的值是互相独立的。简单来说就是,ThreadLocal能让线程拥有自己内部独享的变量。举例,当前登录人信息存储。
2、原理,ThreadLocal是如何做到线程之间相互独立的,也就是它的实现原理。这里我直接放出结论,后面再根据源码分析:每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。这就是ThreadLocal实现线程独立的原理。也就是说,ThreadLocal能够做到线程独立,是因为值并不存在ThreadLocal中,而是存储在线程对象中。下面我们根据ThreadLocal中两个最重要的方法来确认这一点。

基于数组的阻塞队列 ArrayBlockingQueue 原理

1、阻塞队列(BlockingQueue)是在队列的基础上增加了两个附加操作, 在队列为空的时候,获取元素的线程会等待队列变为非空。 当队列满时,存储元素的线程会等待队列可用。
2、由于阻塞队列的特性,可以非常容易实现生产者消费者模型,也就是生产者只需 要关心数据的生产,消费者只需要关注数据的消费,所以如果队列满了,生产者 就等待,同样,队列空了,消费者也需要等待。 要实现这样的一个阻塞队列,需要用到两个关键的技术,队列元素的存储、以及 线程阻塞和唤醒。 而 ArrayBlockingQueue 是基于数组结构的阻塞队列,也就是队列元素是存储在 一个数组结构里面,并且由于数组有长度限制,为了达到循环生产和循环消费的 目的,ArrayBlockingQueue 用到了循环数组。 而线程的阻塞和唤醒,用到了 J.U.C 包里面的 ReentrantLock 和 Condition。 Condition 相当于 wait/notify 在 JUC 包里面的实现。 以上就是我对这个问题的理解。
3、对于原理类的问题,有些小伙伴找不到切入点,不知道该怎么回答。 所谓的原理,通常说的是工作原理,比如对于 ArrayBlockingQueue 这个问题。

什么是聚集索引和非聚集索引

称为非聚集索引,也叫做二级索引。 由于在 InnoDB 引擎里面,一张表的数据对应的物理文件本身就是按照 B+树来 组织的一种索引结构,而聚集索引就是按照每张表的主键来构建一颗 B+树,然 后叶子节点里面存储了这个表的每一行数据记录。 所以基于 InnoDB 这样的特性,聚集索引并不仅仅是一种索引类型,还代表着一 种数据的存储方式。 同时也意味着每个表里面必须要有一个主键,如果没有主键,InnoDB 会默认选 择或者添加一个隐藏列作为主键索引来存储这个表的数据行。一般情况是建议使 用自增 id 作为主键,这样的话 id 本身具有连续性使得对应的数据也会按照顺序
跟着Mic学架构
存储在磁盘上,写入性能和检索性能都很高。否则,如果使用 uuid 这种随机 id, 那么在频繁插入数据的时候,就会导致随机磁盘 IO,从而导致性能较低。 需要注意的是,InnoDB 里面只能存在一个聚集索引,原因很简单,如果存在多 个聚集索引,那么意味着这个表里面的数据存在多个副本,造成磁盘空间的浪费, 以及数据维护的困难。 由于在 InnoDB 里面,主键索引表示的是一种数据存储结构,所以如果是基于非 聚集索引来查询一条完整的记录,最终还是需要访问主键索引来检索。

什么是双亲委派????

怎么理解线程安全?

1、简单来说,在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用 以及线程如何去交替执行。 在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能 按照预期的结果来反馈,那么这个类就是线程安全的。 实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性。 2、原子性呢,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断 的,因为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前 后执行结果不一致的问题。 这个和数据库里面的原子性是一样的,简单来说就是一段程序只能由一个线程完 整的执行完成,而不能存在多个线程干扰。 CPU 的 上 下 文 切 换 , 是 导 致 原 子 性 问 题 的 核 心 , 而 JVM 里 面 提 供 了 Synchronized 关键字来解决原子性问题。
3、有序性,指的是程序编写的指令顺序和最终 CPU 运行的指令顺序可能出现不一 致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。 可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决。 在我看来,导致有序性、原子性、可见性问题的本质,是计算机工程师为了最大 化提升 CPU 利用率导致的。比如为了提升 CPU 利用率,设计了三级缓存、设 计了 StoreBuffer、设计了缓存行这种预读机制、在操作系统里面,设计了线程 模型、在编译器里面,设计了编译器的深度优化机制。 一上就是我对这个问题的理解
4、可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能 出现某个线程对共享变量的修改,对其他线程不是实时可见的。 导致可见性问题的原因有很多,比如 CPU 的高速缓存、CPU 的指令重排序、编 译器的指令重排序。

Dubbo是如何动态感知服务下线的?

1、首先,Dubbo默认采用 Zookeeper实现服务的注册与服务发现,简单来说啊,就是多个Dubbo服务之间的通信地址,是使用Zookeeper来维护的。 在Zookeeper 上,会采用树形结构的方式来维护Dubbo服务提供端的协议地址,Dubbo服务消费端会从Zookeeper Server上去查找目标服务的地址列表,从而完成服务的注册和消费的功能。
Zookeeper会通过心跳检测机制,来判断Dubbo服务提供端的运行状态,来决定是否应该把这个服务从地址列表剔除。
2、当Dubbo服务提供方出现故障导致Zookeeper剔除了这个服务的地址,那么Dubbo服务消费端需要感知到地址的变化,从而避免后续的请求发送到故障节点,导致请求失败。 也就是说Dubbo要提供服务下线的动态感知能力。 这个能力是通过Zookeeper里面提供的Watch机制来实现的,简单来说呢,Dubbo服务消费端会使用Zookeeper里面的Watch 来针对Zookeeper Server 端的/providers 节点注册监听,一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知 Dubbo Client 端.
3、Dubbo Client 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续 就不会把请求发送到失败的节点上,完成服务下线感知。 以上就是我对这个问题的理解!
4、Dubbo是目前非常主流的开源RPC框架,在很多的企业都有使用。理解这个RPC底层的工作原理很有必要,它能帮助开发者提高开发问题的解决效率。

#Spring 中 Bean 的作用域有哪些?

首先呢,Spring框架里面的IOC容器,可以非常方便的去帮助我们管理应用里面的Bean对象实例。我们只需要按照Spring里面提供的xml或者注解等方式去告诉IOC容器,哪些 Bean需要被IOC容器管理就行了。 其次呢,既然是Bean对象实例的管理,那意味着这些实例,是存在生命周期,也就是所谓的作用域。 理论上来说,常规的生命周期只有两种: singleton,也就是单例,意味着在整个Spring容器中只会存在一个Bean实例。 prototype,翻译成原型,意味着每次从IOC容器去获取指定Bean的时候,都会返回一个新的实例对象。 但是在基于Spring框架下的Web应用里面,增加了一个会话纬度来控制Bean的生命周期,主要有三个选择request,针对每一次http请求,都会创建一个新的Bean session,以sesssion 会话为纬度,同一个session共享同一个Bean实例,不同的session产生不同的Bean实例globalSession,针对全局 session 纬度,共享同一个Bean实例 以上就是我对这个问题的理解

Zookeeper 中的 Watch 机制的原理?

1、Zookeeper是一个分布式协调组件,为分布式架构下的多个应用组件提供了顺序访问控制能力。 它的数据存储采用了类似于文件系统的树形结构,以节点的方式来管理存储在Zookeeper上的数据。
2、Zookeeper 提供了一个 Watch 机制,可以让客户端感知到 Zookeeper Server 上存储的数据变化,这样一种机制可以让 Zookeeper 实现很多的场景,比如配置中心、注册中心等
3、Watch 机制采用了 Push 的方式来实现,也就是说客户端和 Zookeeper Server 会建立一个长连接,一旦监听的指定节点发生了变化,就会通过这个长连接把变化的事件推送给客户端。 Watch 的具体流程分为几个部分: 首先,是客户端通过指定命令比如 exists、get,对特定路径增加watch然后服务端收到请求以后,用HashMap保存这个客户端会话以及对应关注的节 点路径,同时客户端也会使用 HashMap 存储指定节点和事件回调函数的对应关系。 当服务端指定被 watch 的节点发生变化后,就会找到这个节点对应的会话,把 变化的事件和节点信息发给这个客户端。 客户端收到请求以后,从 ZkWatcherManager 里面对应的回调方法进行调用, 完成事件变更的通知

#Spring 中有哪些方式可以把 Bean 注入 到 IOC 容器?
1、使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这 个 xml,把 bean 装载到 IOC 容器中。
2、使用@CompontScan 注解来扫描声明了@Controller、@Service、@Repository、 @Component 注解的类。
3、使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义, 这种方式其实是 xml 配置方式的一种演变,是 Spring 迈入到无配置化时代的里 程碑。
4、使用@Import 注解,导入配置类或者普通的 Bean 使 用 FactoryBean 工 厂 bean , 动 态 构 建 一 个 Bean 实 例 , Spring Cloud OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的。
5、实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在 Spring Boot 里面的启动注解有用到。
6、实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 Spring Boot 里面的自动装配机制里面有用到。 以上就是我对这个问题的理解。
7、autoWired、构造等

Redis 存在线程安全问题吗?为什么?

1、第一个,从 Redis 服务端层面。 Redis Server 本身是一个线程安全的 K-V 数据库,也就是说在 Redis Server 上 执行的指令,不需要任何同步机制,不会存在线程安全问题。 虽然 Redis 6.0 里面,增加了多线程的模型,但是增加的多线程只是用来处理 网络 IO 事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多 个线程通知执行操作指令的情况。
2、为什么Redis没有采用多线程来执行指令,我认为有几个方面的原因。 Redis Server 本身可能出现的性能瓶颈点无非就是网络 IO、CPU、内存。但是 CPU 不是 Redis 的瓶颈点,所以没必要使用多线程来执行指令。 如果采用多线程,意味着对于 redis 的所有指令操作,都必须要考虑到线程安全 问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。
3、第二个,从Redis客户端层面。 虽然 Redis Server 中的指令执行是原子的,但是如果有多个Redis客户端同时 执行多个指令的时候,就无法保证原子性。 假设两个redis client 同时获取 Redis Server 上的 key1,同时进行修改和写入, 因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞 争问题,使得数据的安全性无法得到保障
4、Redis 里面的原子指令,或者对多个客户端的资源访问加锁,或者通过 Lua 脚本 来实现多个指令的操作等等。 以上就是我对这个问题的理解。

简述一下你对线程池的理解?

1、关于这个问题,我会从几个方面来回答。 首先,线程池本质上是一种池化技术,而池化技术是一种资源复用的思想,比较常见的有连接池、内存池、对象池。 而线程池里面复用的是线程资源,它的核心设计目标,我认为有两个: 减少线程的频繁创建和销毁带来的性能开销,因为线程创建会涉及到CPU上下文切换、内存分配等工作。 线程池本身会有参数来控制线程创建的数量,这样就可以避免无休止的创建线程 带来的资源利用率过高的问题,起到了资源保护的作用。 2、其次,我简单说一下线程池里面的线程复用技术。因为线程本身并不是一个受控的技术,也就是说线程的生命周期时由任务运行的状态决定的,无法人为控制。 所以为了实现线程的复用,线程池里面用到了阻塞队列,简单来说就是线程池里面的工作线程处于一直运行状态,它会从阻塞队列中去获取待执行的任务,一旦 队列空了,那这个工作线程就会被阻塞,直到下次有新的任务进来。 也就是说,工作线程是根据任务的情况实现阻塞和唤醒,从而达到线程复用的目 的。
3、最后,线程池里面的资源限制,是通过几个关键参数来控制的,分别是核心线程数、最大线程数,核心线程数表示默认长期存在的工作线程,而最大线程数是根据任务的情况动态 创建的线程,主要是提高阻塞队列中任务的。

什么是幂等?如何解决幂等性问题?

1、所谓幂等,其实它是一个数学上的概念,在计算机编程领域中,幂等是指一个方法被多次重复执行的时候产生的影响和第一次执行的影响相同。 之所以要考虑到幂等性问题,是因为在网络通信中,存在两种行为可能会导致接口被重复执行。 用户的重复提交或者用户的恶意攻击,导致这个请求会被多次重复执行。 在分布式架构中,为了避免网络通信导致的数据丢失,在服务之间进行通信的时候都会设计超时重试的机制,而这种机制有可能导致服务端接口被重复调用。 所以在程序设计中,对于数据变更类操作的接口,需要保证接口的幂等性。 而幂等性的核心思想,其实就是保证这个接口的执行结果只影响一次,后续即便 再次调用,也不能对数据产生影响,所以基于这样一个诉求,常见的解决方法有很多。
2、使用数据库的唯一约束实现幂等,比如对于数据插入类的场景,比如创建订单, 因为订单号肯定是唯一的,所以如果是多次调用就会触发数据库的唯一约束异常, 从而避免一个请求创建多个订单的问题。 使用redis里面提供的setNX指令,比如对于MQ消费的场景,为了避免MQ重复消费导致数据多次被修改的问题,可以在接受到MQ的消息时,把这个消息通过 setNx写入到redis里面,一旦这个消息被消费过,就不会再次消费。 使用状态机来实现幂等,所谓的状态机是指一条数据的完整运行状态的转换流程, 比如订单状态,因为它的状态只会向前变更,所以多次修改同一条数据的时候, 一旦状态发生变更,那么对这条数据修改造成的影响只会发生一次。
3、当然,除了这些方法以外,还可以基于token机制、去重表等方法来实现,但是不管是什么方法,无非就是两种,要么就是接口只允许调用一次,比如唯一约束、基于redis的锁机制。 要么就是对数据的影响只会触发一次,比如幂等性、乐观锁以上就是我对这个问题的理解
总结:token机制,只允许访问一次连接,或者redis setNx, 或者数据库唯一约束

new String(“abc”)到底创建了几个对象?

首先,这个代码里面有一个new关键字,这个关键字是在程序运行时,根据已经加载的系统类String,在堆内存里面实例化的一个字符串对象。 然后,在这个String 的构造方法里面,传递了一个“abc”字符串,因为String里面的字符串成员变量是final修饰的,所以它是一个字符串常量。 接下来,JVM会拿字面量“abc”去字符串常量池里面试图去获取它对应的 String对象引用,如果拿不到,就会在堆内存里面创建一个”abc”的String对象 并且把引用保存到字符串常量池里面。 后续如果再有字面量“abc”的定义,因为字符串常量池里面已经存在了字面量 “abc”的引用,所以只需要从常量池获取对应的引用就可以了,不需要再创建。 所以,对于这个问题,我认为的答案是 如果 abc 这个字符串常量不存在,则创建两个对象,分别是abc 这个字符串常量, 以及 new String 这个实例对象。 如果abc这字符串常量存在,则只会创建一个对象

什么是可重入,什么是可重入锁?它用来 解决什么问题?

1、可重入是多线程并发编程里面一个比较重要的概念, 简单来说,就是在运行的某个函数或者代码,因为抢占资源或者中断等原因导致 函数或者代码的运行中断, 等待中断程序执行结束后,重新进入到这个函数或者代码中运行,并且运行结果 不会受到影响,那么这个函数或者代码就是可重入的。 2、而可重入锁,简单来说就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。 在多线程并发编程里面,绝大部分锁都是可重入的,比如Synchronized、 ReentrantLock 等,但是也有不支持重入的锁,比如JDK8里面提供的读写锁StampedLock。
3、锁的可重入性,主要解决的问题是避免线程死锁的问题。 因为一个已经获得同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相 当于会出现自己要等待自己释放锁,这很显然是无法成立的。 以上就是我对这个问题的理解

请你简单说一下 Mysql 的事务隔离级别 ?这个为重点

事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID,缺一不可。今天要说的就是隔离性。

好的,关于这个问题,我会从几个方面来回答。
1、首先,事务隔离级别,是为了解决多个并行事务竞争导致的数据安全问题的一种规范。具体来说,多个事务竞争可能会产生三种不同的现象。假设有两个事务T1/T2同时在执行,T1 事务有可能会读取到T2事务未提交的数据,但是未提交的事务T2可能会回滚,也就导致了T1事务读取到最终不一定存在的数据产生脏读的现象。
2、假设有两个事务T1/T2同时执行,事务T1在不同的时刻读取同一行数据的时候结果可能不一样,从而导致不可重复读的问题。 ,假设有两个事务T1/T2同时执行,事务T1 执行范围查询或者范围修改的过程中,事务T2插入了一条属于事务T1范围内的数据并且提交了,这时候在事务T1查询发现多出来了一条数据,或者在T1事务发现这条数据没有被修改, 看起来像是产生了幻觉,这种现象称为幻读。
而这三种现象在实际应用中,可能有些场景不能接受某些现象的存在,所以在 SQL 标准中定义了四种隔离级别,分别是: 读未提交,在这种隔离级别下,可能会产生脏读、不可重复读、幻读。 读已提交(RC),在这种隔离级别下,可能会产生不可重复读和幻读。 可重复读(RR),在这种隔离级别下,可能会产生幻读 串行化,在这种隔离级别下,多个并行事务串行化执行,不会产生安全性问题。 这四种隔离级别里面,只有串行化解决了全部的问题,但也意味着这种隔离级别 的性能是最低的。 在 Mysql 里面,InnoDB 引擎默认的隔离级别是 RR(可重复读),因为它需要 保证事务 ACID 特性中的隔离性特征。 以上就是我对这个问题的理解。
3、MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。

#请说一下 ReentrantLock 的实现原理?
1、首先,ReentrantLock是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题。
它的核心特性有几个:
2、它支持可重入,也就是获得锁的线程在释放锁之前再次去竞争同一把锁的时候,不需要加锁就可以直接访问。它支持公平和非公平特性
3、它提供了阻塞竞争锁和非阻塞竞争锁的两种方法,分别是lock()和tryLock()。
然后,ReentrantLock的底层实现有几个非常关键的技术。
锁的竞争,ReentrantLock是通过互斥变量,使用CAS机制来实现的。
4、没有竞争到锁的线程,使用了AbstractQueuedSynchronizer这样一个队列同步器来存储,底层是通过双向链表来实现的。当锁被释放之后,会从AQS队列里面的头部唤醒下一个等待锁的线程。
公平和非公平的特性,主要是体现在竞争锁的时候,是否需要判断AQS队列存在等待中的线程。
5、最后,关于锁的重入特性,在AQS里面有一个成员变量来保存当前获得锁的线程,当同一个线程下次再来竞争锁的时候,就不会去走锁竞争的逻辑,而是直接增加重入次数。
以上就是我对这个问题的理解。

TCP 协议为什么要设计三次握手?

TCP 协议,是一种可靠的,基于字节流的,面向连接的传输层协议。 可靠性体现在 TCP 协议通信双方的数据传输是稳定的,即便是在网络不好的情 况下,TCP 都能够保证数据传输到目标端,而这个可靠性是基于数据包确认机 制来实现的。 TCP 通信双方的数据传输是通过字节流来实现传输的 面向连接,是说数据传输之前,必须要建立一个连接,然后基于这个连接进行数 据传输 因为 TCP 是面向连接的协议,所以在进行数据通信之前,需要建立一个可靠的 连接,TCP 采用了三次握手的方式来实现连接的建立。
所谓的三次握手,就是通信双方一共需要发送三次请求,才能确保这个连接的建立。
1、客户端向服务端发送连接请求并携带同步序列号SYN。
2、服务端收到请求后,发送SYN和ACK,这里的SYN表示服务端的同步序列号,ACK表示对前面收到请求的一个确认,表示告诉客户端,我收到了你的请求。 3、客户端收到服务端的请求后,再次发送 ACK,这个ACK是针对服务端连接的一个确认,表示告诉服务端,我收到了你的请求。

#@Resource 和 @Autowired的区别
1、@Resource和@Autowired这两个注解的作用都是在 Spring 生态里面去实现 Bean 的依赖注入。 下面我分别说一下@Autowired 和@Resource 这两个注解。 闪现 [@Autowired 的作用详解 ]几个字。 首先,@Autowired 是Spring里面提供的一个注解,默认是根据类型来实现Bean的依赖注入。 @Autowired 注解里面有一个required属性默认值是 true,表示强制要求bean实例的注入,在应用启动的时候,如果IOC容器里面不存在对应类型的Bean,就会报错。 当然,如果不希望自动注入,可以把这个属性设置成false。
2、其次呢,如果在Spring IOC容器里面存在多个相同类型的Bean实例。由于@Autowired注解是根据类型来注入Bean实例的。
3、闪现[@Resource 的作用详解 ]几个字。 接下来,我再解释一下@Resource注解。 @Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持。 它的使用方式和@Autowired 完全相同,最大的差异于@Resource可以支持 ByName和 ByType两种注入方式。 如果使用name,Spring就根据bean的名字进行依赖注入,如果使用type,Spring就根据类型实现依赖注入。 如果两个属性都没配置,就先根据定义的属性名字去匹配,如果没匹配成功,再 根据类型匹配。两个都没匹配到,就报错,
4、最后,我再总结一下。 @Autowired 是根据type来匹配,@Resource可以根据name和type来匹配, 默认是name匹配。 @Autowired是Spring定义的注解,@Resource是JSR250 规范里面定义的注解,而Spring对JSR 250 规范提供了支持。 @Autowired如果需要支持name匹配,就需要配合@Primary或者@Qualifier 来实现。 以上就是我对这个问题的理解。

Kafka 怎么避免重复消费

1、首先,Kafka Broker 上存储的消息,都有一个 Offset 标记。 然后 kafka 的消费者是通过 offSet 标记来维护当前已经消费的数据, 每消费一批数据,Kafka Broker 就会更新 OffSet 的值,避免重复消费.
2、默认情况下,消息消费完以后,会自动提交Offset的值,避免重复消费。 Kafka消费端的自动提交逻辑有一个默认的5秒间隔,也就是说在5秒之后的下一次向Broker拉取消息的时候提交。 3、所以在 Consumer 消费的过程中,应用程序被强制 kill 掉或者宕机,可能会导致 Offset 没提交,从而产生重复提交的问题。 除此之外,还有另外一种情况也会出现重复消费。 在Kafka 里面有一个 Partition Balance 机制,就是把多个 Partition 均衡的分配 给多个消费者。 Consumer 端会从分配的 Partition 里面去消费消息,如果 Consumer 在默认的 5 分钟内没办法处理完这一批消息。 就会触发 Kafka 的 Rebalance 机制,从而导致 Offset 自动提交失败。 而在重新 Rebalance 之后,Consumer 还是会从之前没提交的 Offset 位置开始 消费,也会导致消息重复消费的问题.
4、基于这样的背景下,我认为解决重复消费消息问题的方法有几个。 提高消费端的处理性能避免触发Balance,比如可以用异步的方式来处理消息, 缩短单个消息消费的市场。或者还可以调整消息处理的超时时间。还可以减少一次性从Broker上拉取数据的条数。
可以针对消息生成md5然后保存到mysql或者redis里面,在处理消息之前先去mysql或者redis里面判断是否已经消费过。这个方案其实就是利用幂等性的 思想。 以上就是我对这个问题的理解

#说一说你对 Spring Cloud 的理解
Spring Cloud 是Spring官方推出来的一套微服务解决方案。 准确来说,我认为Spring Cloud其实是对微服务架构里面出现各种技术场景, 定义了一套标准规范。 然后在这套标准里面,Spring集成了Netflix公司的OSS开源套件,比如Zuul实现应用网关、Eureka实现服务注册与发现、Ribbon实现负载均衡、 Hystrix实现服务熔断我们可以使用Spring Cloud Netflix 这套组件,快速落地微服务架构以及解决微服务治理等一系列问题。 但是随着Netflix OSS相关技术组件的闭源和停止维护,所以Spring官方也自研了一些组件,比如 Gateway实现网关、 LoadBalancer实现负载均衡。 另外,Alibaba里面的开源组件也实现了Spring Cloud的标准,成为了Spring Cloud里面的另外一套微服务解决方案。 包括Dubbo做 rpc通信、Nacos实现服务注册与发现以及动态配置中心、Sentinel实现服务限流和服务降级等等。 以上就是我对Spring Cloud的理解,另外,我再补充一下,我认为 Spring Cloud 生态的出现有两个很重要的意义。 在 Spring Cloud 出现之前,为了解决微服务架构里面的各种技术问题,需要去 集成各种开源框架,因为标准和兼容性问题,所以在实践的时候很麻烦,而 Spring Cloud 统一了这样一个标准。 降低了微服务架构的开发难度,只需要在Spring Boot的项目基础上通过starter启动依赖集成相关组件就能轻松解决各种问题。 以上就是我对这个问题的理解。

ReentrantLock 是如何实现锁公平和非公平性的?

我先解释一下个公平和非公平的概念。 公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。 非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。 ReentrantLock 默认采用了非公平锁的策略来实现锁的竞争逻辑。 其次,ReentrantLock 内部使用了 AQS 来实现锁资源的竞争,
跟着Mic学架构
没有竞争到锁资源的线程,会加入到 AQS 的同步队列里面,这个队列是一个 FIFO 的双向链表。 在这样的一个背景下,公平锁的实现方式就是,线程在竞争锁资源的时候判断 AQS 同步队列里面有没有等待的线程。 如果有,就加入到队列的尾部等待。 而非公平锁的实现方式,就是不管队列里面有没有线程等待,它都会先去尝试抢 占锁资源,如果抢不到,再加入到 AQS 同步队列等待。 ReentrantLock 和 Synchronized 默认都是非公平锁的策略,之所以要这么设计, 我认为还是考虑到了性能这个方面的原因。 因为一个竞争锁的线程如果按照公平的策略去阻塞等待,同时 AQS 再把等待队 列里面的线程唤醒,这里会涉及到内核态的切换,对性能的影响比较大。 如果是非公平策略,当前线程正好在上一个线程释放锁的临界点抢占到了锁,就意味着这个线程不需要切换到内核态,虽然对原本应该要被唤醒的线程不公平, 但是提升了锁竞争的性能。 以上就是我对这个问题的理解.

Zookeeper如何实现Leader选举?

1、首先,Zookeeper集群节点由三种角色组成,分别是Leader,负责所有事务请求的处理,以及过半提交的投票发起和决策。 Follower负责接收客户端的非事务请求,而事务请求会转发给 Leader节点来处理, 另外,Follower节点还会参与Leader选举的投票。 Observer,负责接收客户端的非事务请求,事务请求会转发给 Leader 节点来处 理,另外 Observer 节点不参与任何投票,只是为了扩展Zookeeper集群来分担 读操作的压力。 其次Zookeeper集群是一种典型的中心化架构,也就是会有一个Leader作为 决策节点,专门负责事务请求的处理和数据的同步。 这种架构的好处是可以减少集群架构里面数据同步的复杂度,集群管理会更加简单和稳定。 但是,会带来Leader 选举的一个问题,也就是说,如果Leader节点宕机了,为了保证集群继续提供可靠的服务。
2、Zookeeper需要从剩下的Follower节点里面去选举一个新的节点作为Leader, 也就是所谓的Leader选举! [ 具体的实现是,每一个节点都会向集群里面的其他节点发送一个票据 Vote,这 个票据包括三个属性。 epoch, 逻辑时钟,用来表示当前票据是否过期。 zxid,事务 id,表示当前节点最新存储的数据的事务编号。 myid,服务器 id,在 myid 文件里面填写的数字。 每个节点都会选自己当 Leader,所以第一次投票的时候携带的是当前节点的信 息。接下来每个节点用收到的票据和自己节点的票据做比较,根据 epoch、zxid、myid 的顺序逐一比较,以值最大的一方获胜。比较结束以后这个节点下次再投票的时 候,发送的投票请求就是获胜的 Vote 信息。
3、然后通过多轮投票以后,每个节点都会去统计当前达成一致的票据,以少数服从 多数的方式,最终获得票据最多的节点成为 Leader。 以上就是我对这个问题的理解。 最后我再补充一下,选择 epoch/zxid/myid 作为投票评判依据的原因,我是这么 理解的。 epoch ,因为网络通信延迟的可能性,有可能在新一轮的投票里面收到上一轮 投票的票据,这种数据应该丢弃,否则会影响投票的结果和效率。 zxid, zxid 越大,说明这个节点的数据越接近 leader,所以用 zxid 做判断条件 是为了避免数据丢失的问题。 myid, 服务器 id,这个是避免投票时间过长,直接用 myid 最大值作为快速终 结投票的属性。 面试点评 Leader 选举是一个比较复杂的问题,它涉及到集群节点的数据一致性算法。 在很多中间件里面都有涉及到类似的问题,这个思想其实还是很有研究价值的。 除此之外,还有 Paxos、raft、等一致性算法

说一下你对 CompletableFuture的理解

1、CompletableFuture 是 JDK1.8 里面引入的一个基于事件驱动的异步回调类。 简单来说,就是当使用异步线程去执行一个任务的时候,我们希望在任务结束以后触发一个后续的动作。 而 CompletableFuture 就可以实现这个功能。 举个简单的例子,比如在一个批量支付的业务逻辑里面,涉及到查询订单、支付、 发送邮件通知这三个逻辑。 2、这三个逻辑是按照顺序同步去实现的,也就是先查询到订单以后,再针对这个订单发起支付,支付成功以后再发送邮件通知。 而这种设计方式导致这个方法的执行性能比较慢。
3、所以,这里可以直接使用 CompletableFuture,也就是说把查询订单的逻辑放在 一个异步线程池里面去处理。 然后基于 CompletableFuture 的事件回调机制的特性,可以配置查询订单结束后 自动触发支付,支付结束后自动触发邮件通知。 从而极大的提升这个这个业务场景的处理性能。
4、CompletableFuture 提供了 5 种不同的方式,把多个异步任务组成一个具有先后 关系的处理链,然后基于事件驱动任务链的执行。 第一种,thenCombine,把两个任务组合在一起,当两个任务都执行结束以后触 发事件回调。
5、第二种,thenCompose,把两个任务组合在一起,这两个任务串行执行,也就 是第一个任务执行完以后自动触发执行第二个任务。
6、第三种,thenAccept,第一个任务执行结束后触发第二个任务,并且第一个任务的执行结果作为第二个任务的参数,这个方法是纯粹接受上一个任务的结果,不返回新的计算值。
7、第四种,thenApply,和 thenAccept 一样,但是它有返回值。
8、第五种,thenRun,就是第一个任务执行完成后触发执行一个实现了 Runnable 接口的任务。
9、最后,我认为,CompletableFuture 弥补了原本 Future 的不足,使得程序可以 在非阻塞的状态下完成异步的回调机制

kafka如何保证消息消费的顺序性?

在对kafka的理解中,常常会被问及到kafka如何保证数据的顺序消费、kafka的数据重复消费怎么处理、如何保证kafka中数据不丢失?今天先说说数据的顺序消费问题。
关于顺序消费的几点说明:
①、kafka的顺序消息仅仅是通过partitionKey,将某类消息写入同一个partition,一个partition只能对应一个消费线程,以保证数据有序。
②、除了发送消息需要指定partitionKey外,producer和consumer实例化无区别。
③、kafka broker宕机,kafka会有自选择,所以宕机不会减少partition数量,也就不会影响partitionKey的sharding。
那么问题来了:在1个topic中,有3个partition,那么如何保证数据的消费?
1、如顺序消费中的第①点说明,生产者在写的时候,可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
2、消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。
3、但是消费者里可能会有多个线程来并发来处理消息。因为如果消费者是单线程消费数据,那么这个吞吐量太低了。而多个线程并发的话,顺序可能就乱掉了。

String、StringBuffer、StringBuilder 区别

关于String、StringBuffer、StringBuilder的区别,我想从四个角度来说明。
第一个,可变性。 String内部的value值是final修饰的,所以它是不可变类。所以每次修改String的值,都会产生一个新的对象。 StringBuffer和StringBuilder 是可变类,字符串的变更不会产生新的对象。
第二个,线程安全性。String是不可变类,所以它是线程安全的。 StringBuffer是线程安全的,因为它每个操作方法都加了synchronized同步关键字。StringBuilder 不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer,否则使用StringBuilder
第三个,性能方面。 String的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新创建新的对象以及分配内存。 其次是StringBuffer要比String 性能高,因为它的可变性使得字符串可以直接被 修改最后是StringBuilder,它比StringBuffer的性能高,因为StringBuffer加了同步锁。
第四个,存储方面。 String存储在字符串常量池里面StringBuffer和StringBuilder存储在堆内存空间。 以上就是我对这个问题的理解!

请说一下你对分布式和微服务的理解

首先我先解释一下分布式系统。 简单来说,分布式是一组通过网络进行通信,并且为了完成共同的计算任务的计 算机节点组成的系统。 分布式系统的设计理念,其实是来自于小型机或者大型机的计算能力的瓶颈和成 本的增加。 在集中式系统里面,要想提升程序的运行性能,只能不断的升级CPU以及增加 内存,但是硬件的提升本身也是有瓶颈的,所以当企业对于计算要求越来越高的时候,集中式架构已经无法满足需求了。 在这样的背景下,就产生了分布式计算,也就是把一个计算任务分配给多个计算机节点去运行。 但是对于用户或者客户端来说,感知不到背后的逻辑,就像访问单个计算机一样, 他看到的仍然是一个整体。
在分布式系统中,软件架构也需要作出相应的调整,需要把原本的单体应用进行 拆分,部署到多个计算机节点上,然后各个服务之间使用远程通信协议实现计算 结果的数据交互。 针对这种分布式部署的应用架构,我们称为 SOA(面向服务)的架构。 其次,我再解释一下微服务架构。
其实微服务架构本身就是一种分布式架构,它强调的是对部署在各个计算机上的 应用服务的粒度。 它的核心思想是,针对拆分的服务节点做更进一步的解耦。 也就是说,针对 SOA 服务化架构下的单个业务服务,以更加细粒度的方式进一 步拆分。 每个拆分出来的微服务由独立的小团队负责,最好在 3 人左右。
拆分的好处是使得程序的扩展性更强,开发迭代效率更高。 对于一些大型的互联网项目来说,微服务能够在不影响用户使用的情况下非常方 便的实现产品功能的创新和上线。 以上就是我对这个问题的理解。

#什么是深拷贝和浅拷贝?
深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
1、浅拷贝,就是只复制某个对象的指针,而不复制对象本身。 这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。
2、深拷贝,会完全创建一个一模一样的新对象,新对象和老对象不共享内存,也就意味着对新对象的修改不会影响老对象的值。
在Java里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable接口,并实现clone()方法。 然后我们可以在clone()方法里面实现浅拷贝或者深拷贝的逻辑。
3、实现深拷贝的方法有很多,比如通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象。在 clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次 克隆。 以上就是我对这个问题的理解

谈谈你对 Spring IOC 和 DI 的理解?

IOC 控制反转,DI 依赖注入
1、在传统的Java程序开发中,我们只能通过 new 关键字来创建对象,这种导致程 序中对象的依赖关系比较复杂,耦合度较高。
而IOC的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了IOC容器控制,然后在需要用到目标对象的时候,直接从容器中去获取。 有了IOC容器来管理Bean 以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升 了程序的灵活性以及功能的复用性。

2、然后,DI表示依赖注入,也就是对于IOC容器中管理的Bean,如果Bean之间存在依赖关系,那么IOC容器需要自动实现依赖对象的实例注入,通常有三种方法来描述Bean之间的依赖关系。 接口注入setter注入构造器注入另外,为了更加灵活的实现Bean实例的依赖注入,Spring还提供了@Resource和@Autowired这两个注解。 分别是根据bean的id和bean 的类型来实现依赖注入。 以上就是我对这个问题的理解。

#Nacos 配置更新的工作流程?
这个问题我需要从几个方面来回答。
1、首先,Nacos 是采用长轮训的方式向 Nacos Server 端发起配置更新查询的功能。 所谓长轮训就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更 的时候,这个连接一直打开。 直到服务端有配置或者连接超时后返回。
2、Nacos Client 端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。 一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。 在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。 于是 Nacos 针对这个场景,做了两个方面的优化。 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是3000,也就是说,每次最多拿 3000 个配置去 Nacos Server 端进行比较。 分阶段进行比较和更新; 第一阶段,客户端把这 3000 个配置的 key 以及对应的 value 值的 md5 拼接成 一个字符串,然后发送到 Nacos Server 端进行判断,服务端会逐个比较这些配 置中 md5 不同的 key,把存在更新的 key 返回给客户端。 第二阶段,客户端拿到这些变更的 key,循环逐个去调用服务单获取这些 key 的 value 值。 这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆 分成了多次小的数据包通信。 虽然会增加网络通信次数,但是对整体的性能有较大的提升。 最后,再采用长连接这种方式,既减少了 pull 轮询次数,又利用了长连接的优势, 很好的实现了配置的动态更新同步功能。 以上就是我对这个问题的理解。

谈谈常用的分布式ID设计方案?

SimpleDateFormat 是线程安全的吗? 为什么?

SimpleDateFormat不是线程安全的, SimpleDateFormat 类内部有一个 Calendar 对象引用, 它用来储存和这个 SimpleDateFormat 相关的日期信息。 当我们把 SimpleDateFormat 作为多个线程的共享资源来使用的时候。 意味着多个线程会共享 SimpleDateFormat 里面的 Calendar 引用, 多个线程对于同一个 Calendar 的操作,会出现数据脏读现象导致一些不可预料 的错误。 在实际应用中,我认为有 4 种方法可以解决这个问题。 第一种,把 SimpleDateFormat 定义成局部变量,每个线程调用的时候都创建一 个新的实例。 第二种,使用 ThreadLocal 工具,把 SimpleDateFormat 变成线程私有的 第三种,加同步锁,在同一时刻只允许一个线程操作 SimpleDateFormat
第四种,在 Java8 里面引入了一些线程安全的日期 API,比如 LocalDateTimer、 DateTimeFormatter 等。 以上就是我对这个问题的理解。

1.微服务篇

1.1.SpringCloud常见组件有哪些?

问题说明:这个题目主要考察对SpringCloud的组件基本了解

难易程度:简单

参考话术

SpringCloud包含的组件很多,有很多功能是重复的。其中最常用组件包括:

•注册中心组件:Eureka、Nacos等

•负载均衡组件:Ribbon

•远程调用组件:OpenFeign

•网关组件:Zuul、Gateway

•服务保护组件:Hystrix、Sentinel

•服务配置管理组件:SpringCloudConfig、Nacos

1.2.Nacos的服务注册表结构是怎样的?

问题说明:考察对Nacos数据分级结构的了解,以及Nacos源码的掌握情况

难易程度:一般

参考话术

Nacos采用了数据的分级存储模型,最外层是Namespace,用来隔离环境。然后是Group,用来对服务分组。接下来就是服务(Service)了,一个服务包含多个实例,但是可能处于不同机房,因此Service下有多个集群(Cluster),Cluster下是不同的实例(Instance)。

对应到Java代码中,Nacos采用了一个多层的Map来表示。结构为Map<String, Map<String, Service>>,其中最外层Map的key就是namespaceId,值是一个Map。内层Map的key是group拼接serviceName,值是Service对象。Service对象内部又是一个Map,key是集群名称,值是Cluster对象。而Cluster对象内部维护了Instance的集合。

如图:

image-20210925215305446

1.3.Nacos如何支撑阿里内部数十万服务注册压力?

问题说明:考察对Nacos源码的掌握情况

难易程度:难

参考话术

Nacos内部接收到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。

1.4.Nacos如何避免并发读写冲突问题?

问题说明:考察对Nacos源码的掌握情况

难易程度:难

参考话术

Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。

这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了。

1.5.Nacos与Eureka的区别有哪些?

问题说明:考察对Nacos、Eureka的底层实现的掌握情况

难易程度:难

参考话术

Nacos与Eureka有相同点,也有不同之处,可以从以下几点来描述:

  • 接口方式:Nacos与Eureka都对外暴露了Rest风格的API接口,用来实现服务注册、发现等功能
  • 实例类型:Nacos的实例有永久和临时实例之分;而Eureka只支持临时实例
  • 健康检测:Nacos对临时实例采用心跳模式检测,对永久实例采用主动请求来检测;Eureka只支持心跳模式
  • 服务发现:Nacos支持定时拉取和订阅推送两种模式;Eureka只支持定时拉取模式

1.6.Sentinel的限流与Gateway的限流有什么差别?

问题说明:考察对限流算法的掌握情况

难易程度:难

参考话术

限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法。

而Sentinel内部却比较复杂:

  • 默认限流模式是基于滑动时间窗口算法
  • 排队等待的限流模式则基于漏桶算法
  • 而热点参数限流则是基于令牌桶算法

1.7.Sentinel的线程隔离与Hystix的线程隔离有什么差别?

问题说明:考察对线程隔离方案的掌握情况

难易程度:一般

参考话术

Hystix默认是基于线程池实现的线程隔离,每一个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强。

Sentinel是基于信号量(计数器)实现的线程隔离,不用创建线程池,性能较好,但是隔离性一般。

2.MQ篇

2.1.你们为什么选择了RabbitMQ而不是其它的MQ?

如图:

image-20210925220034702

话术:

kafka是以吞吐量高而闻名,不过其数据稳定性一般,而且无法保证消息有序性。我们公司的日志收集也有使用,业务模块中则使用的RabbitMQ。

阿里巴巴的RocketMQ基于Kafka的原理,弥补了Kafka的缺点,继承了其高吞吐的优势,其客户端目前以Java为主。但是我们担心阿里巴巴开源产品的稳定性,所以就没有使用。

RabbitMQ基于面向并发的语言Erlang开发,吞吐量不如Kafka,但是对我们公司来讲够用了。而且消息可靠性较好,并且消息延迟极低,集群搭建比较方便。支持多种协议,并且有各种语言的客户端,比较灵活。Spring对RabbitMQ的支持也比较好,使用起来比较方便,比较符合我们公司的需求。

综合考虑我们公司的并发需求以及稳定性需求,我们选择了RabbitMQ。

2.2.RabbitMQ如何确保消息的不丢失?

话术:

RabbitMQ针对消息传递过程中可能发生问题的各个地方,给出了针对性的解决方案:

  • 生产者发送消息时可能因为网络问题导致消息没有到达交换机:
    • RabbitMQ提供了publisher confirm机制
      • 生产者发送消息后,可以编写ConfirmCallback函数
      • 消息成功到达交换机后,RabbitMQ会调用ConfirmCallback通知消息的发送者,返回ACK
      • 消息如果未到达交换机,RabbitMQ也会调用ConfirmCallback通知消息的发送者,返回NACK
      • 消息超时未发送成功也会抛出异常
  • 消息到达交换机后,如果未能到达队列,也会导致消息丢失:
    • RabbitMQ提供了publisher return机制
      • 生产者可以定义ReturnCallback函数
      • 消息到达交换机,未到达队列,RabbitMQ会调用ReturnCallback通知发送者,告知失败原因
  • 消息到达队列后,MQ宕机也可能导致丢失消息:
    • RabbitMQ提供了持久化功能,集群的主从备份功能
      • 消息持久化,RabbitMQ会将交换机、队列、消息持久化到磁盘,宕机重启可以恢复消息
      • 镜像集群,仲裁队列,都可以提供主从备份功能,主节点宕机,从节点会自动切换为主,数据依然在
  • 消息投递给消费者后,如果消费者处理不当,也可能导致消息丢失
    • SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制,消费者失败处理策略:
      • 消费者的确认机制:
        • 消费者处理消息成功,未出现异常时,Spring返回ACK给RabbitMQ,消息才被移除
        • 消费者处理消息失败,抛出异常,宕机,Spring返回NACK或者不返回结果,消息不被异常
      • 消费者重试机制:
        • 默认情况下,消费者处理失败时,消息会再次回到MQ队列,然后投递给其它消费者。Spring提供的消费者重试机制,则是在处理失败后不返回NACK,而是直接在消费者本地重试。多次重试都失败后,则按照消费者失败处理策略来处理消息。避免了消息频繁入队带来的额外压力。
      • 消费者失败策略:
        • 当消费者多次本地重试失败时,消息默认会丢弃。
        • Spring提供了Republish策略,在多次重试都失败,耗尽重试次数后,将消息重新投递给指定的异常交换机,并且会携带上异常栈信息,帮助定位问题。

2.3.RabbitMQ如何避免消息堆积?

话术:

消息堆积问题产生的原因往往是因为消息发送的速度超过了消费者消息处理的速度。因此解决方案无外乎以下三点:

  • 提高消费者处理速度
  • 增加更多消费者
  • 增加队列消息存储上限

1)提高消费者处理速度

消费者处理速度是由业务代码决定的,所以我们能做的事情包括:

  • 尽可能优化业务代码,提高业务性能
  • 接收到消息后,开启线程池,并发处理多个消息

优点:成本低,改改代码即可

缺点:开启线程池会带来额外的性能开销,对于高频、低时延的任务不合适。推荐任务执行周期较长的业务。

2)增加更多消费者

一个队列绑定多个消费者,共同争抢任务,自然可以提供消息处理的速度。

优点:能用钱解决的问题都不是问题。实现简单粗暴

缺点:问题是没有钱。成本太高

3)增加队列消息存储上限

在RabbitMQ的1.8版本后,加入了新的队列模式:Lazy Queue

这种队列不会将消息保存在内存中,而是在收到消息后直接写入磁盘中,理论上没有存储上限。可以解决消息堆积问题。

优点:磁盘存储更安全;存储无上限;避免内存存储带来的Page Out问题,性能更稳定;

缺点:磁盘存储受到IO性能的限制,消息时效性不如内存模式,但影响不大。

2.4.RabbitMQ如何保证消息的有序性?

话术:

其实RabbitMQ是队列存储,天然具备先进先出的特点,只要消息的发送是有序的,那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时,可能出现消息轮询投递给消费者的情况,而消费者的处理顺序就无法保证了。

因此,要保证消息的有序性,需要做的下面几点:

  • 保证消息发送的有序性
  • 保证一组有序的消息都发送到同一个队列
  • 保证一个队列只包含一个消费者

2.5.如何防止MQ消息被重复消费?

话术:

消息重复消费的原因多种多样,不可避免。所以只能从消费者端入手,只要能保证消息处理的幂等性就可以确保消息不被重复消费。

而幂等性的保证又有很多方案:

  • 给每一条消息都添加一个唯一id,在本地记录消息表及消息状态,处理消息时基于数据库表的id唯一性做判断
  • 同样是记录消息表,利用消息状态字段实现基于乐观锁的判断,保证幂等
  • 基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等;新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。

2.6.如何保证RabbitMQ的高可用?

话术:

要实现RabbitMQ的高可用无外乎下面两点:

  • 做好交换机、队列、消息的持久化
  • 搭建RabbitMQ的镜像集群,做好主从备份。当然也可以使用仲裁队列代替镜像集群。

2.7.使用MQ可以解决那些问题?

话术:

RabbitMQ能解决的问题很多,例如:

  • 解耦合:将几个业务关联的微服务调用修改为基于MQ的异步通知,可以解除微服务之间的业务耦合。同时还提高了业务性能。
  • 流量削峰:将突发的业务请求放入MQ中,作为缓冲区。后端的业务根据自己的处理能力从MQ中获取消息,逐个处理任务。流量曲线变的平滑很多
  • 延迟队列:基于RabbitMQ的死信队列或者DelayExchange插件,可以实现消息发送后,延迟接收的效果。

3.Redis篇

3.1.Redis与Memcache的区别?

  • redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
  • 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
  • Redis使用单线程:Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。

1574821356723

3.2.Redis的单线程问题

面试官:Redis采用单线程,如何保证高并发?

面试话术

Redis快的主要原因是:

  1. 完全基于内存
  2. 数据结构简单,对数据操作也简单
  3. 使用多路 I/O 复用模型,充分利用CPU资源

面试官:这样做的好处是什么?

面试话术

单线程优势有下面几点:

  • 代码更清晰,处理逻辑更简单
  • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为锁而导致的性能消耗
  • 不存在多进程或者多线程导致的CPU切换,充分利用CPU资源

3.2.Redis的持久化方案由哪些?

相关资料:

1)RDB 持久化

RDB持久化可以使用save或bgsave,为了不阻塞主进程业务,一般都使用bgsave,流程:

  • Redis 进程会 fork 出一个子进程(与父进程内存数据一致)。
  • 父进程继续处理客户端请求命令
  • 由子进程将内存中的所有数据写入到一个临时的 RDB 文件中。
  • 完成写入操作之后,旧的 RDB 文件会被新的 RDB 文件替换掉。

下面是一些和 RDB 持久化相关的配置:

  • save 60 10000:如果在 60 秒内有 10000 个 key 发生改变,那就执行 RDB 持久化。
  • stop-writes-on-bgsave-error yes:如果 Redis 执行 RDB 持久化失败(常见于操作系统内存不足),那么 Redis 将不再接受 client 写入数据的请求。
  • rdbcompression yes:当生成 RDB 文件时,同时进行压缩。
  • dbfilename dump.rdb:将 RDB 文件命名为 dump.rdb。
  • dir /var/lib/redis:将 RDB 文件保存在/var/lib/redis目录下。

  当然在实践中,我们通常会将stop-writes-on-bgsave-error设置为false,同时让监控系统在 Redis 执行 RDB 持久化失败时发送告警,以便人工介入解决,而不是粗暴地拒绝 client 的写入请求。

RDB持久化的优点:

  • RDB持久化文件小,Redis数据恢复时速度快

  • 子进程不影响父进程,父进程可以持续处理客户端命令

  • 子进程fork时采用copy-on-write方式,大多数情况下,没有太多的内存消耗,效率比较好。

    RDB 持久化的缺点:

  • 子进程fork时采用copy-on-write方式,如果Redis此时写操作较多,可能导致额外的内存占用,甚至内存溢出

  • RDB文件压缩会减小文件体积,但通过时会对CPU有额外的消耗

  • 如果业务场景很看重数据的持久性 (durability),那么不应该采用 RDB 持久化。譬如说,如果 Redis 每 5 分钟执行一次 RDB 持久化,要是 Redis 意外奔溃了,那么最多会丢失 5 分钟的数据。

2)AOF 持久化

  可以使用appendonly yes配置项来开启 AOF 持久化。Redis 执行 AOF 持久化时,会将接收到的写命令追加到 AOF 文件的末尾,因此 Redis 只要对 AOF 文件中的命令进行回放,就可以将数据库还原到原先的状态。
  与 RDB 持久化相比,AOF 持久化的一个明显优势就是,它可以提高数据的持久性 (durability)。因为在 AOF 模式下,Redis 每次接收到 client 的写命令,就会将命令write()到 AOF 文件末尾。
  然而,在 Linux 中,将数据write()到文件后,数据并不会立即刷新到磁盘,而会先暂存在 OS 的文件系统缓冲区。在合适的时机,OS 才会将缓冲区的数据刷新到磁盘(如果需要将文件内容刷新到磁盘,可以调用fsync()fdatasync())。
  通过appendfsync配置项,可以控制 Redis 将命令同步到磁盘的频率:

  • always:每次 Redis 将命令write()到 AOF 文件时,都会调用fsync(),将命令刷新到磁盘。这可以保证最好的数据持久性,但却会给系统带来极大的开销。
  • no:Redis 只将命令write()到 AOF 文件。这会让 OS 决定何时将命令刷新到磁盘。
  • everysec:除了将命令write()到 AOF 文件,Redis 还会每秒执行一次fsync()。在实践中,推荐使用这种设置,一定程度上可以保证数据持久性,又不会明显降低 Redis 性能。

  然而,AOF 持久化并不是没有缺点的:Redis 会不断将接收到的写命令追加到 AOF 文件中,导致 AOF 文件越来越大。过大的 AOF 文件会消耗磁盘空间,并且导致 Redis 重启时更加缓慢。为了解决这个问题,在适当情况下,Redis 会对 AOF 文件进行重写,去除文件中冗余的命令,以减小 AOF 文件的体积。在重写 AOF 文件期间, Redis 会启动一个子进程,由子进程负责对 AOF 文件进行重写。
  可以通过下面两个配置项,控制 Redis 重写 AOF 文件的频率:

  • auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-percentage 100

  上面两个配置的作用:当 AOF 文件的体积大于 64MB,并且 AOF 文件的体积比上一次重写之后的体积大了至少一倍,那么 Redis 就会执行 AOF 重写。

优点:

  • 持久化频率高,数据可靠性高
  • 没有额外的内存或CPU消耗

缺点:

  • 文件体积大
  • 文件大导致服务数据恢复时效率较低

面试话术:

Redis 提供了两种数据持久化的方式,一种是 RDB,另一种是 AOF。默认情况下,Redis 使用的是 RDB 持久化。

RDB持久化文件体积较小,但是保存数据的频率一般较低,可靠性差,容易丢失数据。另外RDB写数据时会采用Fork函数拷贝主进程,可能有额外的内存消耗,文件压缩也会有额外的CPU消耗。

ROF持久化可以做到每秒钟持久化一次,可靠性高。但是持久化文件体积较大,导致数据恢复时读取文件时间较长,效率略低

3.3.Redis的集群方式有哪些?

面试话术:

Redis集群可以分为主从集群分片集群两类。

主从集群一般一主多从,主库用来写数据,从库用来读数据。结合哨兵,可以再主库宕机时从新选主,目的是保证Redis的高可用

分片集群是数据分片,我们会让多个Redis节点组成集群,并将16383个插槽分到不同的节点上。存储数据时利用对key做hash运算,得到插槽值后存储到对应的节点即可。因为存储数据面向的是插槽而非节点本身,因此可以做到集群动态伸缩。目的是让Redis能存储更多数据。

1)主从集群

主从集群,也是读写分离集群。一般都是一主多从方式。

Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。

只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。

  • 写数据时只能通过主节点完成
  • 读数据可以从任何节点完成
  • 如果配置了哨兵节点,当master宕机时,哨兵会从salve节点选出一个新的主。

主从集群分两种:

1574821993599 1574822026037

带有哨兵的集群:

1574822077190

2)分片集群

主从集群中,每个节点都要保存所有信息,容易形成木桶效应。并且当数据量较大时,单个机器无法满足需求。此时我们就要使用分片集群了。

1574822184467

集群特征:

  • 每个节点都保存不同数据

  • 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.

  • 节点的fail是通过集群中超过半数的节点检测失效时才生效.

  • 客户端与redis节点直连,不需要中间proxy层连接集群中任何一个可用节点都可以访问到数据

  • redis-cluster把所有的物理节点映射到[0-16383]slot(插槽)上,实现动态伸缩

为了保证Redis中每个节点的高可用,我们还可以给每个节点创建replication(slave节点),如图:

1574822584357

出现故障时,主从可以及时切换:

1574822602109

3.4.Redis的常用数据类型有哪些?

支持多种类型的数据结构,主要区别是value存储的数据格式不同:

  • string:最基本的数据类型,二进制安全的字符串,最大512M。

  • list:按照添加顺序保持顺序的字符串列表。

  • set:无序的字符串集合,不存在重复的元素。

  • sorted set:已排序的字符串集合。

  • hash:key-value对格式

3.5.聊一下Redis事务机制

相关资料:

参考:redisdoc.com/topic/transaction.html

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化,然后按顺序执行。但是Redis事务不支持回滚操作,命令运行出错后,正确的命令会继续执行。

  • MULTI: 用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个待执行命令队列
  • EXEC:按顺序执行命令队列内的所有命令。返回所有命令的返回值。事务执行过程中,Redis不会执行其它事务的命令。
  • DISCARD:清空命令队列,并放弃执行事务, 并且客户端会从事务状态中退出
  • WATCH:Redis的乐观锁机制,利用compare-and-set(CAS)原理,可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行

使用事务时可能会遇上以下两种错误:

  • 执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
    • Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
    • 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行,不会回滚。

为什么 Redis 不支持回滚(roll back)?

以下是这种做法的优点:

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

面试话术:

Redis事务其实是把一系列Redis命令放入队列,然后批量执行,执行过程中不会有其它事务来打断。不过与关系型数据库的事务不同,Redis事务不支持回滚操作,事务中某个命令执行失败,其它命令依然会执行。

为了弥补不能回滚的问题,Redis会在事务入队时就检查命令,如果命令异常则会放弃整个事务。

因此,只要程序员编程是正确的,理论上说Redis会正确执行所有事务,无需回滚。

面试官:如果事务执行一半的时候Redis宕机怎么办?

Redis有持久化机制,因为可靠性问题,我们一般使用AOF持久化。事务的所有命令也会写入AOF文件,但是如果在执行EXEC命令之前,Redis已经宕机,则AOF文件中事务不完整。使用 redis-check-aof 程序可以移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。

3.6.Redis的Key过期策略

参考资料:

为什么需要内存回收?

  • 1、在Redis中,set指令可以指定key的过期时间,当过期时间到达以后,key就失效了;
  • 2、Redis是基于内存操作的,所有的数据都是保存在内存中,一台机器的内存是有限且很宝贵的。

基于以上两点,为了保证Redis能继续提供可靠的服务,Redis需要一种机制清理掉不常用的、无效的、多余的数据,失效后的数据需要及时清理,这就需要内存回收了。

Redis的内存回收主要分为过期删除策略和内存淘汰策略两部分。

过期删除策略

删除达到过期时间的key。

  • 1)定时删除

对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。

  • 2)惰性删除

当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。

在计算机科学中,懒惰删除(英文:lazy deletion)指的是从一个散列表(也称哈希表)中删除元素的一种方法。在这个方法中,删除仅仅是指标记一个元素被删除,而不是整个清除它。被删除的位点在插入时被当作空元素,在搜索之时被当作已占据。

  • 3)定期删除

每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis中,同时使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取的方式删除部分Key,因此不能保证过期key 100%的删除。

Redis结合了定期删除和惰性删除,基本上能很好的处理过期数据的清理,但是实际上还是有点问题的,如果过期key较多,定期删除漏掉了一部分,而且也没有及时去查,即没有走惰性删除,那么就会有大量的过期key堆积在内存中,导致redis内存耗尽,当内存耗尽之后,有新的key到来会发生什么事呢?是直接抛弃还是其他措施呢?有什么办法可以接受更多的key?

内存淘汰策略

Redis的内存淘汰策略,是指内存达到maxmemory极限时,使用某种算法来决定清理掉哪些数据,以保证新数据的存入。

Redis的内存淘汰机制包括:

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,移除最近最少使用的 key(这个是最常用的)。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,随机移除某个 key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,移除最近最少使用的 key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,有更早过期时间的 key 优先移除。

在配置文件中,通过maxmemory-policy可以配置要使用哪一个淘汰机制。

什么时候会进行淘汰?

Redis会在每一次处理命令的时候(processCommand函数调用freeMemoryIfNeeded)判断当前redis是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的key。

在淘汰key时,Redis默认最常用的是LRU算法(Latest Recently Used)。Redis通过在每一个redisObject保存lru属性来保存key最近的访问时间,在实现LRU算法时直接读取key的lru属性。

具体实现时,Redis遍历每一个db,从每一个db中随机抽取一批样本key,默认是3个key,再从这3个key中,删除最近最少使用的key。

面试话术:

Redis过期策略包含定期删除和惰性删除两部分。定期删除是在Redis内部有一个定时任务,会定期删除一些过期的key。惰性删除是当用户查询某个Key时,会检查这个Key是否已经过期,如果没过期则返回用户,如果过期则删除。

但是这两个策略都无法保证过期key一定删除,漏网之鱼越来越多,还可能导致内存溢出。当发生内存不足问题时,Redis还会做内存回收。内存回收采用LRU策略,就是最近最少使用。其原理就是记录每个Key的最近使用时间,内存回收时,随机抽取一些Key,比较其使用时间,把最老的几个删除。

Redis的逻辑是:最近使用过的,很可能再次被使用

3.7.Redis在项目中的哪些地方有用到?

(1)共享session

在分布式系统下,服务会部署在不同的tomcat,因此多个tomcat的session无法共享,以前存储在session中的数据无法实现共享,可以用redis代替session,解决分布式系统间数据共享问题。

(2)数据缓存

Redis采用内存存储,读写效率较高。我们可以把数据库的访问频率高的热点数据存储到redis中,这样用户请求时优先从redis中读取,减少数据库压力,提高并发能力。

(3)异步队列

Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。而且Redis中还有pub/sub这样的专用结构,用于1对N的消息通信模式。

(4)分布式锁

Redis中的乐观锁机制,可以帮助我们实现分布式锁的效果,用于解决分布式系统下的多线程安全问题

3.8.Redis的缓存击穿、缓存雪崩、缓存穿透

1)缓存穿透

参考资料:

  • 什么是缓存穿透

    • 正常情况下,我们去查询数据都是存在。那么请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去。这种查询不存在数据的现象我们称为缓存穿透
  • 穿透带来的问题

    • 试想一下,如果有黑客会对你的系统进行攻击,拿一个不存在的id 去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。
  • 解决办法

    • 缓存空值:之所以会发生穿透,就是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。那么我们就可以为这些key对应的值设置为null 丢到缓存里面去。后面再出现查询这个key 的请求的时候,直接返回null 。这样,就不用在到数据库中去走一圈了,但是别忘了设置过期时间。
    • BloomFilter(布隆过滤):将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。在缓存之前在加一层 BloomFilter ,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。

话术:

缓存穿透有两种解决方案:其一是把不存在的key设置null值到缓存中。其二是使用布隆过滤器,在查询缓存前先通过布隆过滤器判断key是否存在,存在再去查询缓存。

设置null值可能被恶意针对,攻击者使用大量不存在的不重复key ,那么方案一就会缓存大量不存在key数据。此时我们还可以对Key规定格式模板,然后对不存在的key做正则规范匹配,如果完全不符合就不用存null值到redis,而是直接返回错误。

2)缓存击穿

相关资料

  • 什么是缓存击穿?

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

当这个key在失效的瞬间,redis查询失败,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

  • 解决方案:
    • 使用互斥锁(mutex key):mutex,就是互斥。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用Redis的SETNX去set一个互斥key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现互斥的效果。
    • 软过期:也就是逻辑过期,不使用redis提供的过期时间,而是业务层在数据中存储过期时间信息。查询时由业务程序判断是否过期,如果数据即将过期时,将缓存的时效延长,程序可以派遣一个线程去数据库中获取最新的数据,其他线程这时看到延长了的过期时间,就会继续使用旧数据,等派遣的线程获取最新数据后再更新缓存。

推荐使用互斥锁,因为软过期会有业务逻辑侵入和额外的判断。

面试话术

缓存击穿主要担心的是某个Key过期,更新缓存时引起对数据库的突发高并发访问。因此我们可以在更新缓存时采用互斥锁控制,只允许一个线程去更新缓存,其它线程等待并重新读取缓存。例如Redis的setnx命令就能实现互斥效果。

3)缓存雪崩

相关资料

缓存雪崩,是指在某一个时间段,缓存集中过期失效。对这批数据的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

解决方案:

  • 数据分类分批处理:采取不同分类数据,缓存不同周期
  • 相同分类数据:采用固定时长加随机数方式设置缓存
  • 热点数据缓存时间长一些,冷门数据缓存时间短一些
  • 避免redis节点宕机引起雪崩,搭建主从集群,保证高可用

面试话术:

解决缓存雪崩问题的关键是让缓存Key的过期时间分散。因此我们可以把数据按照业务分类,然后设置不同过期时间。相同业务类型的key,设置固定时长加随机数。尽可能保证每个Key的过期时间都不相同。

另外,Redis宕机也可能导致缓存雪崩,因此我们还要搭建Redis主从集群及哨兵监控,保证Redis的高可用。

3.9.缓存冷热数据分离

背景资料

Redis使用的是内存存储,当需要海量数据存储时,成本非常高。

经过调研发现,当前主流DDR3内存和主流SATA SSD的单位成本价格差距大概在20倍左右,为了优化redis机器综合成本,我们考虑实现基于热度统计 的数据分级存储及数据在RAM/FLASH之间的动态交换,从而大幅度降低成本,达到性能与成本的高平衡。

基本思路:基于key访问次数(LFU)的热度统计算法识别出热点数据,并将热点数据保留在redis中,对于无访问/访问次数少的数据则转存到SSD上,如果SSD上的key再次变热,则重新将其加载到redis内存中。

目前流行的高性能磁盘存储,并且遵循Redis协议的方案包括:

因此,我们就需要在应用程序与缓存服务之间引入代理,实现Redis和SSD之间的切换,如图:

image-20200521115702956

这样的代理方案阿里云提供的就有。当然也有一些开源方案,例如:github.com/JingchengLi/swapdb

3.10.Redis实现分布式锁

分布式锁要满足的条件:

  • 多进程互斥:同一时刻,只有一个进程可以获取锁
  • 保证锁可以释放:任务结束或出现异常,锁一定要释放,避免死锁
  • 阻塞锁(可选):获取锁失败时可否重试
  • 重入锁(可选):获取锁的代码递归调用时,依然可以获取锁

1)最基本的分布式锁:

利用Redis的setnx命令,这个命令的特征时如果多次执行,只有第一次执行会成功,可以实现互斥的效果。但是为了保证服务宕机时也可以释放锁,需要利用expire命令给锁设置一个有效期

setnx lock thread-01 # 尝试获取锁
expire lock 10 # 设置有效期

面试官问题1:如果expire之前服务宕机怎么办?

要保证setnx和expire命令的原子性。redis的set命令可以满足:

set key value [NX] [EX time] 

需要添加nx和ex的选项:

  • NX:与setnx一致,第一次执行成功
  • EX:设置过期时间

面试官问题2:释放锁的时候,如果自己的锁已经过期了,此时会出现安全漏洞,如何解决?

在锁中存储当前进程和线程标识,释放锁时对锁的标识判断,如果是自己的则删除,不是则放弃操作。

但是这两步操作要保证原子性,需要通过Lua脚本来实现。

if redis.call("get",KEYS[1]) == ARGV[1] then
    redis.call("del",KEYS[1])
end

2)可重入分布式锁

如果有重入的需求,则除了在锁中记录进程标识,还要记录重试次数,流程如下:

1574824172228

下面我们假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20

获取锁的步骤:

  • 1、判断lock是否存在 EXISTS lock
    • 存在,说明有人获取锁了,下面判断是不是自己的锁
      • 判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
        • 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end
        • 存在,说明是自己获取的锁,重入次数+1:HINCRBY lock threadId 1,去到步骤3
    • 2、不存在,说明可以获取锁,HSET key threadId 1
    • 3、设置锁自动释放时间,EXPIRE lock 20

释放锁的步骤:

  • 1、判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
    • 不存在,说明锁已经失效,不用管了
    • 存在,说明锁还在,重入次数减1:HINCRBY lock threadId -1,获取新的重入次数
  • 2、判断重入次数是否为0:
    • 为0,说明锁全部释放,删除key:DEL lock
    • 大于0,说明锁还在使用,重置有效时间:EXPIRE lock 20

对应的Lua脚本如下:

首先是获取锁:

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

if(redis.call('exists', key) == 0) then -- 判断是否存在
    redis.call('hset', key, threadId, '1'); -- 不存在, 获取锁
    redis.call('expire', key, releaseTime); -- 设置有效期
    return 1; -- 返回结果
end;

if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己    
    redis.call('hincrby', key, threadId, '1'); -- 不存在, 获取锁,重入次数+1
    redis.call('expire', key, releaseTime); -- 设置有效期
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

然后是释放锁:

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
    return nil; -- 如果已经不是自己,则直接返回
end;
local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1

if (count > 0) then -- 判断是否重入次数是否已经为0
    redis.call('EXPIRE', key, releaseTime); -- 大于0说明不能释放锁,重置有效期然后返回
    return nil;
else
    redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除
    return nil;
end;

3)高可用的锁

面试官问题:redis分布式锁依赖与redis,如果redis宕机则锁失效。如何解决?

此时大多数同学会回答说:搭建主从集群,做数据备份。

这样就进入了陷阱,因为面试官的下一个问题就来了:

面试官问题:如果搭建主从集群做数据备份时,进程A获取锁,master还没有把数据备份到slave,master宕机,slave升级为master,此时原来锁失效,其它进程也可以获取锁,出现安全问题。如何解决?

关于这个问题,Redis官网给出了解决方案,使用RedLock思路可以解决:

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

3.11.如何实现数据库与缓存数据一致?

面试话术:

实现方案有下面几种:

  • 本地缓存同步:当前微服务的数据库数据与缓存数据同步,可以直接在数据库修改时加入对Redis的修改逻辑,保证一致。
  • 跨服务缓存同步:服务A调用了服务B,并对查询结果缓存。服务B数据库修改,可以通过MQ通知服务A,服务A修改Redis缓存数据
  • 通用方案:使用Canal框架,伪装成MySQL的salve节点,监听MySQL的binLog变化,然后修改Redis缓存数据
本作品采用《CC 协议》,转载必须注明作者和本文链接
MissYou-Coding
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
Coding Peasant @ 互联网
文章
196
粉丝
10
喜欢
61
收藏
66
排名:596
访问:1.3 万
私信
所有博文
社区赞助商