分享一波基础的Springboot相关的八股文

1.Springboot是什么:

SpringBoot是一个全新的Java软件开发框架,很多人现在把它理解为一个脚手架。它基于快速构建理念,通过约定大于配置,开箱即用的方式,来简化Spring项目的初始搭建以及开发过程,提高开发效率

2.SpringBoot的核心优势:

Springboot为我们的开发提供了起步依赖(Start Dependency),自动配置(Auto Configuration),健康检查(Actator),嵌入式服务(Tomcat,Jetty)等核心特性,基于这些特性和优势可以更好的服务我们的开发过程。可以更好的简化项目构建,代码编写,项目配置,项目部署等,可以说SpringBoot技术是大势所趋。(Jetty与tomcat一样,都是一个开源的HTTP服务器和Servlet引擎)。

3.SpringBoot核心配置文件的类型:

SpringBoot提供了后缀为properties,yml,factories等类型的配置文件。

4.SpringBoot的启动过程是怎样的:

首先@SpingBootApplication找到xxxApplication,接着查找(find)package xxx;class XxxClass{}(启动类及子包)-->分析(@Component,@service,@Repository,@Configuration,@Bean)-->存储class信息(BeanDefinition)-->Map<String,BeanDefinition>-->将类型信息交给Spring工厂(Beanfactory),由工厂创建对象(BeanFactory,工厂)-->创建bean对象-->Map<String,Object>(存储bean的容器)(这里的Map可以存储一些实例的Bean对象,但不是所有的Bean对象都要存储,这要看Bean的作用域。例如单例作用域的对象就要存储。存储时这里的Key为使用@Component等注解定义的Bean的名字。假如没有定义,默认就类名,然后首字母小写)-->getBean("beanName")(外界可以直接从容器获取bean对象)

5.小结:

springboot基本启动过程描述如下:

(1)基于配置加载类(通过ClassLoader将指定位置的类读到内存->底层通过线程调用IO从磁盘读取到内存)。

(2)对类进行分析(创建字节码对象-Class类型,通过反射获取其配置信息)。

(3)对于指定配置(例如由spring特定注解描述)的对象存储其配置信息(借助BeanDefinition对象存储)。

(4)基于BeanDefinition对象中class的配置构建类的实例(Bean对象),并进行bean对象的管理(可能会存储到bean池)

6.Spring是一个什么框架:

Spring是一个资源整合框架,其核心是资源整合,然后以一种更加科学的方式对外提供服务,例如提高对象的应用效率,降低系统开销,提高代码的可维护性等等。其官方网址为spring.io.

7.Spring框架中有哪些重要的模块:

Spring IOC,Spring AOP,Spring WEB(处理客户请求),Spring Data(用于数据库访问)等

8.Spring框架是如何构建对象的:

Spring框架中所有Bean对象都是通过BeanFactory接口类型的工厂对象构建的,此工厂底层会基于Java中的反射技术进行实现,首先获取类的字节码对象,然后基于字节码对象获取构造方法对象,最后基于构造方法对象构建类的实例对象。

9.如何理解Spring中IOC设计:

IOC是一种设计思想,我们称之为控制反转,本质上讲解的是对象控制权问题。基于这种设计可以让初学者将对象的控制权转交给第三方,由第三方专业团队管理和应用对象。这样可以更好的避免对象的非正确使用方式,进而更好改善对象在内存中的科学应用。这个IOC设计从生活的角度可以理解为由股票操盘手负责帮你进行资金管理,由父母包办你的婚姻。再简单总结一下:IOC可以理解为饭来张口,衣来伸手

10.为什么要将对象交给Spring管理:

Spring为我们的对象赋予了很多个更加科学的特性,例如延迟加载,作用域,生命周期方法以及运行时的自动依赖注入(降低耦合,提高程序的可维护性)机制等,基于这些特征可以更好的提高对象的应用性能以及程序的可扩展性。

11.Spring框架中的Bean的特性:

Spring框架为了更加科学的管理和应用Bean对象,为其设计相关特性,例如:懒加载(@Lazy),作用域(@Scope)以及生命周期方法。

(在Spring对象池中,通过作用域(@Lazy),延迟加载(@Lazy),生命周期方法(@PostConstruct)(@PreDestory)来管理bean对象)

@Lazy注解通常会与@Scope中单例作用域对象结合使用,通过此注解可以告诉系统底层,对象可以延迟创建(何时需要何时创建)。@Scope中指定的作用域为prototype时,每次从容器获取对象,都会创建新的对象。

12.Spring中用于定义Bean的周期方法的注解:

①@PostConstruct描述bean对象的初始化方法。

②@PreDestroy描述bean对象销毁方法(singleton作用域对象销毁之前执行)

13.Spring中用于实现依赖注入的注解:

①@Autowired(按类型优先注入,类型有相同的可以参考名字)

②@Resource(按名字优先注入)

14.@Autowirred注解有什么作用:

@Autowired由spring框架定义,用于描述类中属性或相关方法(例如构造方法)。Spring框架在项目运行时假如发现由他管理的Bean对象中有使用@Autowired注解描述的属性或方法,可以按照指定规则为属性赋值(ID)。其基本规则是:首先要检测容器中是否有与属性或方法参数类型相匹配的对象,假如有并且只有一个则直接注入。其次,假如检测到有多个,还会按照@Autowired描述的属性或方法参数名查找是否有名字匹配的对象,有则直接注入,没有则抛出异常。最后,假如我们有明确要求,必须要注入类型为指定类型,名字为指定名字的对象还可以使用@Quallifier注解对其属性或参数进行描述(此注解必须配合@Autowired)。

15.@Qualifier注解的作用:

@Qualifier注解用于描述属性或方法参数,当有多个相同类型的bean却只有一个需要自动装配时,将@Qualifier注解和@Autowire注解结合使用以消除这种混淆,指定需要装配的确切的bean.

16.@Component和@Configuration注解的异同点:

@Component注解通常用于描述一般的bean对象,比如具备一定通用性的对象。

@Configuration注解通常用于描述Spring工程中的配置类,是一个增强版的@Component注解,在配置类中定义一些由@Bean注解描述的方法,然后通过这些方法对一些自己定义或第三方的Bean进行对象的创建和初始化。当我们使用这@Configuration注解描述一个配置类时,Spring框架底层会为这个@Configuration注解描述的配置类创建一个CGIB代理对象(CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。CGLIB作为一个开源项目,其代码托管在github,地址为https://github.com/cglib/cglib),然后由代理对象调用@Bean注解描述的方法,同时底层会检测方法返回的Bean是否已创建,假如已创建则不再创建。使用@Component注解描述类时,系统底层并不会为此类创建代理对象,只是创建当前类的对象,然后调用@Bean注解描述的方法,创建和初始化Bean,方法每调用一次就会创建一个新的对象。

17.Spring中的JdbcTemplate:

JdbcTemplate是Spring中提供的一个封装了JDBC操作的模板类,此类中基于模板方法定义了很多JDBC模板方法,简化了JDBC的一些基本操作步骤,可以更好提高我们的开发效率。此类在运行时需要由Spring注入一个DataSource对象,然后基于DataSource可以获取一个连接池,从池中获取访问数据库的连接。

18.Spring中的MVC设计:

MVC是一种分层架构设计思想,是Model,View,Controller的缩写,是为了将复杂问题简单化而提出的一种分而治之的设计套路,基于这种套路来提高代码的可维护性和可扩展性。Spring MVC仅仅是Spring框架中WEB模块基于MVC设计思想的落地实现,简化了web请求和响应的处理过程。

19.Spring MVC中的异常处理过程:

Spring工程中Web模块可以基于@RestControllerAdvice注解定义全局异常处理类,然后借助全局异常处理规范进行异常处理,大概过程:

发送请求到DispatcherServlet,接着反射invoke()到xxxHandler@Controller{.......}(处理异常),将异常抛回给DispatcherServlet回复

20.Spring中用于定义组件的注解:

@Component

@Service

@Controller

@Configuration

@Repository

@Bean

21.Spring MVC中描述方法的注解:

@RequestMapping

@PostMapping

@GetMapping

@DeleteMapping

@PutMapping

@PatchMapping

@ResponseBody

22.Spring MVC中描述方法参数的注解:

@RequestParam

@PathVariable

@RequestBody

@RequestPart(描述文件上传方法参数)

23.Spring MVC中的拦截器作用:

Spring框架中定义的拦截器属于SpringMVC模块执行链中的一个对象,其类型为HandlerInterceptor,基于这个类型的拦截器可以对后端@Controller注解描述的对象进行请求和响应的拦截。用于进行一些预处理操作。

24.@ResponseBody注解的作用:

@ResponseBody用于描述类或方法,用于告诉底层可以将方法的返回值转换为json格式的字符串,然后再响应到客户端。假如类中所有方法都需要使用@ResponseBody注解进行标记,则可以直接使用@RestController注解对类进行描述。

25.SpringBoot工程中如何定义切面:

基于@Aspect注解进行描述,并交给Spring管理(@Component)。

26.Spring框架常用设计模式有:

工厂模式,建造模式,策略模式,代理模式,单例模式,模板方法模式,享元模式,适配器模式

27.Spring AOP的应用场景:

日志记录,事务控制,权限控制,缓存应用,异步操作

28.Spring AOP中用于描述通知方法的注解

@Around,@Before,@After,@@AfterThrowing@AfterReturning

29.Spring框架中支持的事务方式:

①编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。

②声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需要注解和XML配置来管理事务。

30.Spring中声明式事务的实现方式:

在类或方法上添加@Transaction注解

31.Spring中定义了哪些事务隔离级别:

DEFAULT,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE

32.Spring中如何启动异步任务:

①在启动类或配置上添加@EnableAsync注解

②在执行异步方法的类上添加@Async注解

33.Spring中常见的设计模式

①建造模式:BeanDefinitionBuilder

②简单工厂模式:BeanFactory

③工厂方法模式:ProxyFactoryBean

④单例模式:单例Bean对象

⑤代理模式:Aop Proxy。

⑥策略模式:Aop代理策略,SimpleInstantiationStrategy。

⑦适配器模式:AdvisorAdapter。

⑧模板方法模式:JdbcTemplate。

⑨责任链模式:HandlerInterceptor。

⑩观察者模式:ApplicationListener。

34.MyBatis:

这是一个优秀的Java持久层(数据访问层)框架,由apache的ibatis演变而来,它通过XML或者注解方式将对象与SQL关联起来,实现了对JDBC操作的封装,简化了JDBC代码手动设置参数和手动进行结果集映射的过程。

35.MyBatis框架的优势:

①可以更好的实现JAVA代码与SQL语句的分离,提高可维护性。

②通过动态SQL可以更好的适配灵活的需求变更。

③通过SQL映射简化了JDBC代码中SQL参数及结果集处理

④合理的架构设计,提高了系统的稳定性,访问性能,可扩展性

⑤SQL为原生SQL语法,便于DBA介入对SQL进行调优。

36.MyBatis框架的劣势:

①SQL语句编写的工作量相对较大。(相对hibernate框架)

②SQL语句依赖于数据库,移植性较差。

37.SpringBoot工程下MyBatis框架的整合:

①确定已经配置好数据源(Data Source)

②添加依赖(mybatis-spring-boot-starter)

③基础配置(可选,基于业务实践有选择性的进行配置,例如驼峰命名,映射文件路劲)

④创建数据访问接口及方法并定义sql映射

⑤定义单元测试类,对具体业务方法进行单元测试。

38.MyBatis中的核心API有:

①SqlSessionFactoryBuilder(读取配置文件创建会话工厂-SqlSessionFactory)

②SqlSessionFactory(创建会话对象-SQLSession)

③SqlSession(开启与数据库会话)

④DefaultSqlSession(实现了SqlSession接口,此对象不可共享,线程不安全)

⑤SqlSessionTemplate(实现了SqlSession接口,此对象可共享,线程安全)

39.MyBatis中@Mapper注解的作用:

此注解由mybatis官方提供,用于描述数据持久层接口“,当项目启动时,系统底层会自动扫描启动类所在包以及子包中的类,假如发现某个接口使用@Mapper注解进行描述,底层会为这个接口创建一个实现类,在实现类的内部定义基于SqlSession对象的会话过程。然后还会将这个类的实例交给spring管理。

40.MyBatis中定义SQL映射的注解:

@Select

@Insert

@Delete

@Update

41.MyBatis框架中提供的ResultMap元素的作用:

ResultMap是Mybatis框架中用于实现高级映射的一个元素,它可以完成数据库表中字段名与javaBean对象中属性名不一致的映射问题,同时提供了一对一,一对多,多对多的高级映射方式

42.MyBatis中的动态SQL元素:

if,choose(when,otherwise),trim(where,set),foreach

43.MyBatis的缓存架构:

MyBatis为了提高其查询的性能,提供了一些数据缓存特性,我们可以将这些缓存分为两大类,一级缓存和二级缓存,一级缓存可以理解为SqlSessiom级缓存,同一个SqlSession会话执行多次的Sql查询,查询结果可以从缓存获取。二级缓存默认是没有开启的,需要在映射文件或数据层对象中进行开启,可以多个SqSession会话共享同一个缓存。

44.MyBatis中的插件及实现原理:

插件是Mybatis框架中的一个最重要的功能,能够基于此功能实现特定组件的特定方法进行增强。例如对查询操作进行分页设计,类似功能可以在mybatis中基于Interceptor(拦截器)接口进行实现。其原理并不复杂,就在创建相关组件对象时为其创建代理对象,然后通过代理对象为其目标业务做功能增强。

45.MyBatis框架中有哪些设计模式:

建造模式(SqlSessionFactoryBuilder);

简单工厂模式(SqlSessionFactory)

单例模式

策略模式

代理模式(XXXMapper、@Mapper)

装饰模式(CachingExecutor)

享元模式(连接池)

桥接模式(驱动程序)

适配器模式(日志log,可以将log4j等日志API转换mybatis中的实现)

模板方法模式(SqlSessionTemplate)

Redis随笔:

1.Redis是一个基于C语言编写的内存数据库,基于key/Value结构存储数据,读写速度很快,一般会用来做缓存,消息队列,分布式锁,同时还支持事务,持久化,集群等。

2.Redis中常见的数据类型:

常见的有五种基本数据类型和一些特殊数据类型,基本数据结构:String,list,set,zset和hash,特殊数据类型如位图(bitmaps),计数器(hyperloglogs)和地理空间(geospatial indexes).

(String:以字符串形式存储数据,经常用于记录用户的访问次数,文章访问量等。

Hash:以对象形式存储数据,比较方便的就是操作其中的某个字段,例如存储登录用户登录状态,实现购物车。

List:以列表形式存储数据,可记录添加顺序,允许元素重复,通常应用于发布与订阅或者说消息队列,慢查询。

Set:以集合形式存储数据,不记录添加顺序,元素不能重复,也不能保证存储顺序,通常可以做全局去重,投票系统。

zset:拍续集合,可对数据基于某个权重进行排序。可做排行榜,取TOP N操作。直播系统 中的在线用户列表,礼物排行榜,弹幕消息等。)

3.Redis为了平衡空间和时间效率,针对value的具体类型在底层会采用不同的数据结构来实现,如:

String:SDS简单动态字符串

List:Ziplist(压缩列表),Linkedlist(双端链表)

Hash:Hashtable(哈希表),Ziplist(压缩列表)

Set:Intset(整数集合),Hashtable(哈希表)

Zset:Skiplist(跳跃链表),Ziplist(压缩列表)

4.Redis对应的Java客户端:

jedis,lettuce,Redisson

5.Redis中持久化的过程:

Redis数据持久化从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事:

①客户端向数据库发送‘写命令’(数据在客户端的内存中)

②数据库接收到客户端的‘写请求’(数据在服务器的内存中)

③数据库‘调用系统API’将数据写入磁盘(数据在内核缓冲区中)

④操作系统将‘写缓冲区’传输到磁盘控制器(数据在磁盘缓存中)

⑤操作系统的磁盘控制器将数据'写入实际的物理媒介‘中(数据在磁盘中)

6.Redis中的持久化和方式:

Redis持久化是把内存中的数据同步到硬盘文件中,当Redis重启后再将硬盘文件内容重新加载到内存以实现数据恢复的目的。具体持久化方式,分别为RDB和AOF方式

7.如何理解Redis中RDB的持久化:

RDB方式的持久化是Redis数据库默认的持久化机制,是保证redis中数据可靠性的方式之一,这种方式可以按照一定的时间周期策略把内存中的数据以快照(二进制数据)的形式保存到磁盘文件中,即快照存储。对应的数据文件为dump.rdp。

8.什么情况下会RDB方式持久化:

①基于配置文件中的save规则周期性的执行持久化

②手动执行了shutdown操作会自动执行rdb方式的持久化

③手动调用了save或bgsave指令执行数据持久化

④主从复制(Master/Slave)架构下Slave连接到Master时,Master会对数据持久化,然后全量同步到Slave。

9.RDB方式持久化有哪些优势?

①RDB文件是经过压缩的二进制文件,占用空间很小,它保存了Redis某个时间点的数据集,很适合做备份

②RDB非常使用于灾难恢复,它只有一个文件,并且内容都非常紧凑,可以将它传送到别的数据中兴。

③RDB方式持久化性能比较好,执行持久化时可以fork一个子进程,由子进程处理保存工作,父进程无须执行任何磁盘I/O操作。

9.RDB方式持久化有哪些缺点:

①RDB方式在服务器故障时容易造成数据的丢失。实际项目中,我们可通过配置来控制持久化的频率。但是,如果频率太平凡,可能会对Redis性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果Redis出现宕机等情况,则意味着最多可能丢失5分钟数据。

②RDB方式使用fork子进程进行数据的持久化,子进程的内存是fork操作时父进程中数据快照的大小,如果数据快照比较大的话,fork时开辟内存会比较耗时,同时这个fork是同步操作,所以,这个过程会导致父进程无法对外提供服务。

③RDB持久化过程中的fork操作,可能会导致内存占用加倍,Linux系统fork子进程采用的是copy-on-write的方式(写时复制,修改前先复制),在redis执行RDB持久化期间,如果client写入数据很频繁,那么将增加Redis占用的内存,最坏情况下,内存的占用将达到原先的2倍。

10.Redis中AOF方式的持久化:

Redis中AOF方式的持久化是将Redis收到的每一个写命令都追加到磁盘文件的最后,类似于MySQL的binlog。当Redis重启时,会重新执行文件中保存的写命令,然后在内存中重建整个数据库的内容。

AOF持久化默认是关闭的,可以通过配置appendonly yes开启。当AOF持久化功能打开后,服务器在执行完一个写命令后,会将被执行的写命令追加到服务器端aof_buf中的内容写到磁盘。

Linux操作系统中为了提升性能,使用了页缓存(page cache)。当我们将aof_buf的内容写到磁盘上时,此时数据并没有真正的罗盘,而是存在在page cache中,为了 将page cache中的数据真正落盘,需要执行fsync/fdatasync命令来强制刷盘。这边的文件同步做的就是刷盘操作,或者叫文件刷盘。

11.AOF持久化方式的优势:

AOF比RDB更加可靠。你可以设置不同的fsync策略(no,everysec和always)。默认是everysec,在这种配置下,redis任然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。

AOF文件是一个基于纯追加模式的日志文件。即使日志因为某些原因包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等),我们也可以使用redis-check-aof工具也可以轻易地修复这种问题。

当AOF文件太大时,Redis会自动在后台进行重写。重写后的新AOF文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对的安全,因为重写是在一个新的文件上进行,同时Redis会继续往旧的文件追加数据。当新文件重写完毕,Redis会把旧文件进行切换,然后开始把数据写到新文件上.

AOF文件有序地保存了对数据库执行的所有写入操作,以redis协议的格式保存,因此AOF文件很容易被人读懂。如果不小心执行了FLUSHALL命令把所有数据刷掉了,但只要AOF文件没有被重写,那么只要停止服务器,移除AOF文件末尾的FLUSHALL命令,并重启Redis,就可以将数据集恢复到FLUSHALL执行前的状态。

12.AOF持久化的劣势

AOF文件的大小一般比RDB文件大。根据所使用的fsync策略,AOF的速度可能会比RDB慢,通常fsync设置为每秒一次就能获得比较高的性能,而关闭fsync可以让AOF的速度和RDB一样快。AOF可能会因为个别命令的原因,导致AOF文件在重新载入时,无法将数据集恢复成保存时的原样。阻塞命令BRPOPLPUSH就曾经引起过这样的bug.虽然这种bug在AOF文件中并不常见,但是相较而言,RDB几乎是不可能出现这种bug的

(fsync策略:传统的 UNIX 实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘 I/O 都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的 I/O 操作。这种输出方式被称为延迟写(delayed write)

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,于是,就可能使得写到文件中的数据在一段时间内并没有写到磁盘上。这样,当系统发生故障时,这种延迟可能造成文件更新内容的丢失。 redis既然是一个内存级kv数据库,那么,这时写操作就会触发I/O,这样就会影响redis的写速度,就会变慢,所以redis给了3个级别:NO,ALWAYS,EVERYSEC。

①NO:从不手动将数据刷入磁盘,由于不要一直写磁盘,所以redis的性能,一定是最高的。但是这样只会在缓冲区满了才会刷数据到磁盘,所以,在宕机的时候,丢失的数据会有很多。

②ALWAYS:每次写,都将指令追加到AOF,这样,我们最多最多,丢失一条数据(就是最后哪一条还没来得及写进磁盘的时候)。但是,由于每笔操作都往磁盘刷写,那性能一定是会受很大的影响。

③everysec:每秒。这样的话,由于不会次次写磁盘所以对性能的影响还不至于那么大,而且,时间间隔也只有一秒,即使丢失,影响也不会太大。所以,这往往作为一个折中方案。

13.Redis的混合持久化:

混合持久化只发生于AOF重写过程。使用了混合持久化,重写后的新AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。

开启:混合持久化的配置参数为aof-use-rdb-preamble,配置为yes时开启混合持久化,在redis4刚引入时,默认是关闭混合持久化的,但是在redis5中默认已经打开了。

关闭:使用aof_use_rdb_preamble no配置即可关闭混合持久化。混合持久化本质是通过AOF后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork出的子进程先将当前全量数据以RDB方式写入到新的AOF文件,然后再将AOF重写缓冲区(aof_rewrite_buf_blocks)的增量命令以AOF方式写入到文件,写入完成后通知主进程将新的含有RDB格式和AOF格式文件替换旧的AOF文件。

优点:结合RDB和AOF的优点,更快的重写和恢复。

缺点:AOF文件里面的RDB部分不再是AOF格式,可读性差。

14.Save和Bgsave有什么不同:

SAVE生成RDB快照文件,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。BGSAVEfork子进程来生成RDB快照文件,阻塞只会发生在fork子进程的时候,之后主进程可以正常处理请求。

15.Redis为什么要AOF重写:

AOF持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF文件中的内容会越来越多,文件的体积也会越来越大。如果不加以控制,体积过大的AOF文件可能会对Redis服务器,甚至整个宿主机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。举个例子,如果你对一个计数器调用了100次INCR,那么仅仅为了保存这个计数器的当前值,AOF文件就需要使用100条记录。然而在实际上,只使用一条SET命令已经足以保存计数器的当前值了,其余99条记录实际上都是多余的。为了处理这种情况,Redis引入了AOF重写:可以在不打断服务端处理请求的情况下,对AOF文件进行重建。

16.描述一下AOF重写的过程:

描述:Redis生成新的AOF文件来代替旧AOF文件,这个新的AOF文件包括重建当前数据集所需的最少命令。具体过程是遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。

命令:有两个Redis命令可以用于触发AOF重写,一个是BGREWRITEAOF,另一个是REWRITEAOF命令;开启:AOF重写由两个参数共同控制,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,同时满足这两个条件,则触发AOF后台重写BGREWRITEAOF

关闭:auto-aof-rewrite-percentage 0,指定0的百分比,已禁用自动AOF重写功能。

REWRITEAOF:进行AOF重写,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,通常不会直接使用该命令。

BGREWRITEAOF:fork子进程来进行AOF重写,阻塞只会发生在fork子进程的时候,之后主进程可以正常处理请求。REWRITEAOF和BGREWRITEAOF的关系与SAVE和BGSAVE的关系类似。17.AOF后台重写存在的问题:

AOF后台重写使用子进程进行重写,解决了主进程阻塞的问题,但是仍然存在另外一个问题:子进程在进行AOF重写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的AOF文件保存的数据库状态不一致。

18.AOF重写存在的数据不一致问题(redis高级部分):

Redis 引入了AOF重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之后开始使用,当redis服务器执行完一个写命令之后,它会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区。这样可以保证:①现有AOF文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的AOF文件也还是安全的。②从创建子进程开始,也就是AOF重写开始,服务器执行的所有命令会被记录到AOF重写缓冲区里面。这样,当子进程完成AOF重写工作后,父进程会在serverCron中检测到子进程已经重写结束,则会执行以下工作:1.将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。2.对新的AOF文件进行改名,原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换。之后,父进程就可以继续像往常一样接受命令请求了。

19.Redis缓存穿透:

当访问一个缓存和数据库都不存在的key时,请求会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。这时缓存就好像被‘穿透’了一样,起不到任何作用。假如一些恶意的请求,故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力,甚至数据库挂掉,这就叫做缓存穿透。

如何避免:

方案①:接口校验。在正常业务流程中可能会存在少量访问不存在key的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验,用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

方案②:缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。

方案③:布隆过滤器。使用布隆过滤器存储所有可能访问的key,不存在的key直接被过滤,存在的key则再进一步查询缓存和数据库。可把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

20.如何理解Redis缓存击穿:

某一个热点key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大,压力骤增,甚至可能打垮数据库。

如何避免:

方案①:热点数据不设置过期时间,后由定时任务去异步加载数据,更新缓存。这种方式适用于比较极端的场景,例如流量特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据。

方案②:应用互斥锁。在并发的多个请求中,保证只有一个请求线程能拿到锁,并执行数据库查询操作,其他线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。

关于互斥锁的选择,网上看到的大部分文章都是选择Redis分布式锁,因为这个可以保证只有一个请求会走到数据库,这是一种思路。但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低即可,所以还有一个思路是JVM锁。JVM锁保证了在单台服务器上只有 一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。需要注意的是,无论是使用“分布式锁”,还是“JVM锁”,加锁时要按key维度去加锁。(网上很多文章都是使用一个“固定的key”加锁,这样会导致不同的key之间也会互相阻塞,造成性能严重损耗。)

21.Redis缓存雪崩:

缓存雪崩是当缓存服务器重启或者大量缓存集中在某一个时间段失效,造成瞬时数据库请求量大,压力骤增,导致系统崩溃。缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点key,缓存雪崩是一组热点key。

避免方案:

方案①:打散过期时间。不同的key,设置不同的过期时间(例如使用一个随机值),让缓存失效的时间点尽量均匀。

方案②:做二级缓存(mapper级缓存)。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

方案③加互斥锁。缓存失效后,通过加锁或者队列来控制写缓存的线程数量。比如对某个key只允许一个线程操作缓存,其他线程等待。

方案④:热点数据不过期。该方式和缓存击穿一样,要着重考虑刷新的时间间隔和数据异常如何处理。

22.Redis缓存预热:

缓存预热就是系统上线后,将相关的数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,接下来,用户直接查询事先被预热的缓存数据。解决思路:直接写个缓存刷新页面,上线时手工操作下;数据量不大,可以在项目启动的时候自动进行加载;定时刷新缓存。

23.Redis缓存更新:

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:第一种是定时去清理过期的缓存。第二种是当用户请求过来时,再判读这个请求所用到的缓存是否过期。过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂。

24.redis缓存降级:

当访问量剧增,服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如假如购物车、结算)。

参考日志级别设置预案:普通:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级或人工降级,并发送告警; 错误:可用率低于90%或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阈值,此时可根据情况降级;严重错误:比如因为特殊原因数据错了,进行人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

25.Memcache与Redis的区别:

①数据结构:

memcached支持简单的key-value数据结构,而redis支持丰富的数据结构:String,List,Set,Hash,SortedSet等。

②数据存储:

memcached和redis的数据都是全部在内存中。网上有一种说法“当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘,同时在内存中清楚”,这边指的是redis里的虚拟内存(Virtual Memory)功能,该功能在Redis2.0被引入,但是在Redis2.4中被默认关闭,并标记为废弃,而在后续版中被完全移除。

③持久化:memcached不支持持久化,redis支持将数据持久化到磁盘

④灾难恢复:实例挂掉后,memcached数据不可恢复,redis可通过RDB,AOF恢复,但是还是会有数据丢失问题。

⑤事件库:memcached使用Libevent事件库,redis自己封装了简易事件库AeEvent.

⑥过期键删除策略:memcached使用惰性删除,redis使用惰性删除+定期删除。

⑦内存淘策略:memcached主要为LRU算法。redis当前支持8种淘汰策略。

⑧性能比较(按“CPU单核”维度比较):由于Redis只使用单核,而Memcached可以使用多核,所以在比较上:在处理小数据时,平均每一个核上Redis比Memcached性能更高,而在100k左右的大数据时,Memcached性能要高于Redis。按“实例”维度进行比较:由于Memcached多线程的特性,在Redis6.0之前,通常情况下Memcached性能是要高于Redis的,同时实例的CPU核数越多,Memcached的性能优势越大。(网上说的 redis 的性能比 memcached 快很多,这个说法就离谱。)

26.单线程的redis为什么这么快:

①基于内存的操作

②使用了I/O多路复用模型,select,epoll等,基于reactor模式开发了自己的网络事件处理器。

③单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。

④以上这三点是redis性能高的主要原因,其他的还有一些小优化,例如:对数据结构进行了优化,简单动态字符串,压缩列表等。

27.Redis内部存储结构:

dict本质上是为了解决算法中的查找问题(Searching),是一个用于维护Key和value映射关系的数据结构,与很多语言中的Map或dictionary类似。本质上是为了解决算法中的查找问题(Searching)

Sds:就等同于char*,它可以存储任意二进制数据,不能像C语言字符串那样以字符'\0'来标识字符串结束,因此它必然有个长度字段。skiplist(跳跃表):跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现,quicklist.ziplist压缩表:ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构。

28.Sorted Set底层数据存储结构是:

Sorted Set当前有两种数据存储结构,分别为ziplist和skiplist。

①ziplist:使用压缩列表实现,当保存的元素长度都小于64字节,同时数量小于128时,使用该方式,否则会使用skiplist。这两个参数可以通过zset-max-ziplist-entries,zset-max-ziplist-value来自定义修饰。

②skiplist:一个zset同时包含一个字典(dict)和一个跳跃表(skiplist)

29.同时使用字典和跳跃表的原因:

主要是为了提升性能。单独使用字典:在执行范围型操作(比如zrank,zrange),字典需要进行排序,至少需要O(NlogN)的时间复杂度及额外O(N)的内存空间。单独使用跳跃表:根据成员查找分值操作的复杂度从O(1)上升为O(logN)。

30.为什么使用跳跃表而不是红黑树:

①跳表的性能和红黑树差不多。

②跳表更容易实现和调试。

31.Redis数据过期后的删除策略:

常用的过期数据的删除策略就两个:

①定期删除:每隔一段时间随机抽取一批key,检查是否过期,过期了则删除。这里Redis底层会通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。

②惰性删除:只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期key没有被删除。

两者各有千秋,定期删除对内存更加友好,惰性删除对CPU更加友好。所以Redis采用的是:定期删除+惰性/懒汉式删除。

32.redis的数据淘汰策略:

Redis中默认提供6种数据淘汰策略,分别为:volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰allkeys-lru:当内存不足以容纳新写入数据时,移除最近最少使用的key(这个是最常用的)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。

33.Redis为什么是单线程?

官方FAQ(在线帮助回答)表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络宽带。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程地方案了(毕竟采用多线程会有很多麻烦),redis利用队列技术将并发访问变成串行访问。

①绝大部分请求是纯粹地内存操作(非常快速)。

②采用单线程,避免了不必要的上下文切换和竞争条件。

③非阻塞IO操作。

34.Redis中的事务操作:

Redis的一个事务从开始到结束通常会经历以下3个阶段:

①事务开始:multi命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过flags属性标识。

②命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作,例如exec,discard,watch,multi命令会被立即执行,其他命令不会立即执行,而是将命令放入到一个事务队列,然后向客户端返回QUEUED回复。

③事务执行:当一个处于事务状态的客户端向服务器发送exec命令时,服务器会遍历事务队列,执行队列中的所有命令,最后将结果全部返回给客户端。

(redis的事务并不推荐在实际中使用,如果要使用事务,推荐使用lua脚本,redis会保证一个lua脚本(lua由标准C编写而成,几乎所有操作系统和平台上都可以编译,它可以灵活的嵌入到应用程序中为程序提供灵活的扩展和定制功能,redis中支持lua脚本的扩展,使用lua可以实现很多很多复杂,高频次的redis操作)里的所有命令的原子性。)

35.Redis如何实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。如果在setnx之后执行expire之前进程意外crash或者要重启维护,那会怎么样?set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的。

36.Redis是多线程还是单线程:

Redis的线程模型,要看具体的版本:

Redis6.0前的请求解析,键值数据读写,结果返回都是由一个线程完成,所以称Redis为单线程模型。Redis单线程容易阻塞,为了避免阻塞,Redis设计了子进程和异步线程的方式来完成某些耗时操作,例如使用子进程实现RDB生成,AOF重写等;

Redis6.0开始,使用多个线程来完成请求解析和结果返回,提升对网络请求的处理,提升系统整体的吞吐量,不过读写命令的执行还是单线程的。

37.Redis是否可以保证事务的原子性(事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成 功,要么全部失败):

这个问题要从三个层面进行说明,分别是:

①命令都正常执行,此时原子性可以保证;

②命令入队时出错,EXEC时会拒绝执行所有命令,原子性可以保证;

③命令实际执行时出错,Redis会执行剩余命令,原子性得不到保证;

38.如何保证数据库和缓存数据一致性:

实际项目中,无论是先操作数据库,还是先操作缓存,都会存在脏数据的情况。

避免方法:由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2PC,TCC,MQ事务消息等。但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。所以在实际使用中,通常不会去保证缓存和数据库的一致性,而是做出一定的牺牲,保证两者数据的最终一致性。如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。保证数据库和缓存数据最终一致性的常用方案如下:

①更新数据库,数据库产生binlog

②监听和消费binlog,执行失效缓存操作。

③如果步骤②失效缓存失败,则引入重试机制,将失败的数据通过MQ方式进行重试,同时考虑是否需要引入幂等机制。

(当出现未知的问题时,及时告警通知,人为介入处理。人为介入是最终的方法,只能靠人不断的修复各种脏数据和bug)

39.Redis做消息队列:

redis本身提供了一些组件来实现消息队列的功能,但是都或多或少存在一些缺点,相比于市面上成熟的消息队列(kafka,Rocket MQ)来说并没有优势,因此目前我们并没有使用Redis来做消息队列。

关于Redis做消息队列的常见方案有:

①Redis5.0之前可以使用List(blocking),Pub/Sub等来实现轻量级的消息发布订阅功能组件,但是这两种实现方式都有很明显的缺点,两者中相对完善的Pub/Sub的主要缺点就是消息无法持久化,如果出现网络断开,Redis宕机等,消息就会被丢弃。

②为了解决Pub/Sub模式等的缺点,Redis在5.0引入了全新的Stream,Stream借鉴了很多kafka的设计思想,有以下几个特点:提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。引入了消费者组的概念,不同组接收到的数据完全一样(前提是条件一样),但是组内的消费者则是竞争关系。Redis Stream相比于pub/sub已经有很大改善,但是相比于kafka还是没有优势,同时尚未经过大量验证,成本较高,不支持分区(partition),无法支持大规模数据等问题。

40.Redis的主从复制架构设计:

主从复制,是将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave)。且数据的复制是单向的,只能由主节点到从节点。Redis主从复制支持主从同步和从从同步两种,后者是Redis后续版本新增的功能,以减轻主节点的同步负担

41.Redis中的哨兵(Sentinel)

哨兵(Sentinel)是Redis的高可用性解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有服务器:

哨兵可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

①哨兵故障检测:

检查主观下线状态:在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令的实例(包括主服务器,从服务器,其他Sentinel在内)发送Ping命令,并通过实例返回的PING命令回复来判断实例是否在线。如果一个实例在down-after-miliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实力结构,在结构的flags属性中设置SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。

检查客观下线状态:当Sentinel将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel就会将服务器置为客观下线,在flag上打上SRI_O_DOWN标识,并对主服务器执行故障转移操作。

②哨兵故障转移流程:当哨兵检测到某个主节点客观下线之后,就会开始故障转移流程。核心流程如下:发起一次检举,选举出领头Sentinel领头Sentinel在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。领头Sentinel将剩余的所有从服务器改为复制新的主服务器。领头Sentinel更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。

42.Redis的集群架构设计:

redis集群(Cluster)将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力,集群中的每一个Redis节点都互相两两相连,客户端任意直连到集群中的任意一台,就可以对其他Redis节点进行读写的操作。同时集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群任然可以对外提供服务。

Redis集群中内置了16384个哈希槽。当客户端连接到Redis集群之后,会同时得到一份 关于这个集群的配置信息,当客户端具体对某一个key值进行操作时,会计算出它的一个Hash值,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,Redis会更根据节点数量大致均等的哈希槽映射到不同的节点。再结合集群的配置信息就能够知道这个key值应该存储在哪一个具体的Redis节点中,如果不属于自己管,那么就会使用一个特殊的MOVED命令来进行一个跳转,告诉客户端去连接这个节点以获取数据。

43.如何从Redis上亿个key中找出10个包含java的key:

①keys java命令,该命令性能很好,但是在数据量特别大的时候会有性能问题。

②scan 0 MATCH java命令,基于游标的迭代器,更好的选择SCAN命令是一个基于游标的迭代器(cursor based iterator):SCAN命令每次被调用后,都会向用户回一个新的游标,用户在下次迭代时需要使用这个新游标作为SCAN命令的游标参数,以此来延续之前的迭代过程。当SCAN命令的游标参数设置为0时,服务器将开始一次新的迭代,而当服务器向用户返回值为0的游标时,表示迭代已结束。

#我的失利项目复盘##我的成功项目解析#
全部评论
哇偶
点赞 回复 分享
发布于 12-23 15:18 河北

相关推荐

评论
16
68
分享
牛客网
牛客企业服务