2022java面试题小总结(记得关注up,up技术好,活好,又帅)
文章目录
-
- 2022java面试题小总结(记得关注up,up技术好,活好,又帅)
-
- Java 8有哪些新特性
- 1. Java的三种代理模式
-
- 静态代理
- 动态代理
- 1、JDK动态代理有以下特点:
- 2、Cglib代理
- 2.ACID是靠什么保证的?
- 3、beanfactory和applicacontext区别
- 4、jdk1.8中HashMap在扩容的时候做了哪些优化
- 4.1 hashmap的内存结构和ConcurrentHashMap的内存结构
-
- 4.1.1 HashMap:数组+链表+红黑树
-
- 4.1.2 ConcurrentHashMap
- 5、mysql隔离级别
- 6、mysql复制原理
- 7、msql聚簇索引和非聚簇索引
- 8、spring、springmvc、springboot三者的区别和联系
- 9、springmvc的工作流程?
- 10、springboot自动装配原理
- 11、spring事务的传播机制
- 12、spring框架使用了那些设计模式以及应用场景
-
- 1.工厂设计模式
- 2.单例设计模式
- 3.代理设计模式
- 4.观察者模式
- 5.适配器模式
- 13、spring事务的实现方式以及事务时候时候会失效?
- 14、mysql的Innodb和MyIASM引擎区别
- 14、mysql中存在哪些索引类型?索引使用的数据结构类型?
- 15、mysql为啥使用的是b+tree数据结构?不用二叉树、红黑树、btree?
- 16、mysql索引什么时候会失效?
- 17、简述spring bean的生命周期
- 18、如何自定义一个spring-boot-starter
- 18.1 如何实现一个IOC容器
- 19、ABA问题以及解决方案
- 20、JVM相关问题?
- 21、java8为何把永久代移除,增加了元空间?
- 22、synchronized与Lock的区别
- 22.1拓展、synchronized和ReentrantLock区别和底层实现原理
- 22.2拓展 AQS是什么?原理?
- 23、ThreadLocal原理以及使用场景
- 24、锁的状态和升级过程
- 25、你们项目如何排查JVM问题(当然本博主那么帅,肯定有实战经验的,以下也是实战的总结)
- 26、类加载的双亲委派机制
- 26.1 Java类加载器的流程
- 27、线程以及线程池底层原理相关
- 28、请你谈谈对于valotile的理解
- 29、java并发编程之CountDownLatch、CyclicBarrier、Semaphore
- 30、spring怎么解决循环依赖的?为啥要用到了三级缓存?
- 31、你们项目中,是怎么对你们的接口进行加密处理的?
- 32、Redis五种数据类型及应用场景
- 33、redis是单线程的吗?为什么那么快?
- 34、redis的持久化机制
- 35、如何保障mysql和redis之间的数据一致性?
- 36、redis的缓存穿透,缓存击穿、缓存雪崩三者的区别以及解决方案
- 37、redis的缓存删除策略?
- 38、分布式锁的实现方式?
- 39、分布式事务的解决方案?
-
- 39.1 CAP定理
- 39.2 BASE理论
- 一、两阶段提交(2PC)
-
- 1. 运行过程
- 2. 存在的问题
- 3. 落地的方案
-
- 3.1 XA方案
- 3.2 Seata-AT方案
- 二、补偿事务(TCC)
-
- 落地的方案
- 三、解决方案之可靠消息最终一致性
-
- 方案1:本地消息表方案
-
- 基本思路就是:
- 方案2:RocketMQ事务消息方案 (推荐)
- 四、解决方案之之最大努力通知
-
- 最大努力通知与可靠消息一致性有什么不同?
- 40、分布式ID的生成方案?
- 41、mybatis的一级缓存和二级缓存
Java 8有哪些新特性
文章图片
(1)Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用Lambda 表达式可以使代码变的更加简洁紧凑。
(2) 方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。
(3) 函数式接口(FunctionalInterface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为lambda表达式。函数式接口可以现有的函数友好地支持 lambda。
(4) Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个default关键字即可实现默认方法。
(5) Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
(6) Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。Optional 类的引入很好的解决空指针异常。
(7) jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。
(8) Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
(9) 在Java8中,Base64编码已经成为Java类库的标准。Java 8 内置了 Base64 编码的编码器和解码器。
1. Java的三种代理模式
静态代理 标题静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父 类
静态代理总结:
- 可以做到不修改目标对象功能的前提下,进行目标功能拓展
- 因为要和目标对象实现相同的接口,代理类会比较多,同时接口一旦增加方法,目标对象和代理对象都要进行维护,成本比较大
1. 代理对象不需要实现接口,但是目标对象需要实现接口
2. 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象
3. 动态代理也叫做:JDK代理,接口代理
JDK中生成代理对象的API
代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
static Object newProxyInstance(ClassLoader loader, Class>[] interfaces,InvocationHandler h )
接收的三个参数依次为:
1. ClassLoader loader:指定当前目标对象使用类加载器,写法固定
2.Class>[] interfaces:目标对象实现的接口的类型,写法固定
3. InvocationHandler h:事件处理接口,需传入一个实现类,一般直接使用匿名内部类
缺点: 可以看出静态代理和JDK代理有一个共同的缺点,就是目标对象必须实现一个或多个接口,加入没有,则可以使用Cglib代理。
2、Cglib代理 上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理
Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.
1. JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用Cglib实现.
2. Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如SpringAOP和synaop,为他们提供方法的interception(拦截)
3. Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.
Cglib子类代理实现方法:
需要引入cglib的jar文件,由于Spring的核心包中已经包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
目标类不能为final
目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
2.ACID是靠什么保证的?
1. A 原子性 由undo log日志保证,它记录了需要回滚的日志信息,事务回滚是撤销已经成功执行的sql
2. C 一致性 由其他三大特性保证、程序代码要保证业务上的一致性
3. I 隔离性 由MVCC(多版本并发控制)来保证
4. D 持久性 由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log 恢复。
InnoDb redo log 写盘 ,InnoDB事务进入prepare状态。
如果前面prepare成功,binlog写盘,再继续将事务日志持久化到binlog,如果持久化成功,那么InnoDb事务则进入commit状态。(在redo log里面写一个commit记录)
确保事务执行成功的一个重要判断指标就是 在redo log中此事务是否有commit记录。
redolog刷盘会在系统空闲时进行。
3、beanfactory和applicacontext区别
文章图片
4、jdk1.8中HashMap在扩容的时候做了哪些优化
说到这个问题,就不得不讨论下hashMap扩容为2的幂次.为什么呢?
假设HashMap的容量为15转化成二进制为1111,length-1得出的二进制为1110 哈希值为1111和1110
文章图片
那么两个索引的位置都是14,就会造成分布不均匀了,增加了碰撞的几率,减慢了查询的效率,造成空间的浪费。
总结:因为2的幂-1都是11111结尾的,所以碰撞几率小。使Hash算法的结果均匀分布。
下面我们讲解下JDK1.8做了哪些优化。
我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
文章图片
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
文章图片
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,
4.1 hashmap的内存结构和ConcurrentHashMap的内存结构
https://blog.csdn.net/weixin_44460333/article/details/86770169相信看完这篇没人能难住你
4.1.1 HashMap:数组+链表+红黑树 put方法执行过程:
- 先通过hash(key) 获取 key 的 hash 值,JDK1.8中,是通过hashCode()的高16位异或低16位实现的((h = k.hashCode()^(h>>>16)))
文章图片
- 通过 (n - 1) & hash 获取 下标,n 是 数组长度 也就是通过 数据长度-1 & hash 来计算出来下标。
- 如果 hash 在 map 中不存在,那么直接把数据插入到链表。如果存在,那么进行 equals 判断,true 的话,更新 value。false 就是用尾插法插入到链表或者数组(JDK1.7 之前使用头插法、JDK 1.8 使用尾插法)
- 如果链表长度大于阈值8并且数组长度大于64则将链表变为红黑树。
JDK7会将新put值放到链表开头,导致新旧链表的顺序完全相反(多线程并发操作下,可能形成环形链表从而导致死循环)。JDK8中修复了这个BUG,放在尾部,但是并发环境下,可能会导致数据丢失。
扩容必须满足两个条件:
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
文章图片
2.Java 8 中Hashmap扩容机制
扩容会发生在两种情况下(满足任意一种条件即发生扩容):
- a 当前存入数据大于阈值即发生扩容
- b 存入数据到某一条链表上,此时数据大于8,且总数量小于64即发生扩容
文章图片
文章图片
5、mysql隔离级别
1. Read Uncommitted(读取未提交内容)
2. Read Committed(读取提交内容)
3. Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
4.Serializable(可串行化)
6、mysql复制原理
1)在Slave 服务器上执行sart slave命令开启主从复制开关,开始进行主从复制。
2)此时,Slave服务器的IO线程会通过在master上已经授权的复制用户权限请求连接master服务器,并请求从执行binlog日志文件的指定位置(日志文件名和位置就是在配置主从复制服务时执行change master命令指定的)之后开始发送binlog日志内容
3)Master服务器接收到来自Slave服务器的IO线程的请求后,二进制转储IO线程会根据Slave服务器的IO线程请求的信息分批读取指定binlog日志文件指定位置之后的binlog日志信息,然后返回给Slave端的IO线程。返回的信息中除了binlog日志内容外,还有在master服务器端记录的新的binlog文件名称,以及在新的binlog中的下一个指定更新位置。
4)当Slave服务器的IO线程获取到Master服务器上IO线程发送的日志内容、日志文件及位置点后,会将binlog日志内容依次写到Slave端自身的Relay Log(即中继日志)文件(MySQL-relay-bin.xxx)的最末端,并将新的binlog文件名和位置记录到master-info文件中,以便下一次读取master端新binlog日志时能告诉Master服务器从新binlog日志的指定文件及位置开始读取新的binlog日志内容
5)Slave服务器端的SQL线程会实时检测本地Relay Log 中IO线程新增的日志内容,然后及时把Relay LOG 文件中的内容解析成sql语句,并在自身Slave服务器上按解析SQL语句的位置顺序执行应用这样sql语句,并在relay-log.info中记录当前应用中继日志的文件名和位置点
7、msql聚簇索引和非聚簇索引
索引是存在内存中还是磁盘里呢?当然是磁盘,内存首先会造成数据丢失,而且存储空间也没有磁盘大
B+Tree结构都可以用在MyISAM和InnoDB上。mysql中,不同的存储引擎对索引的实现方式不同,大致说下MyISAM和InnoDB两种存储引擎。
在mysql数据库中,myisam引擎和innodb引擎使用的索引类型不同,myisam对应的是非聚簇索引,而innodb对应的是聚簇索引。聚簇索引也叫复合索引、聚集索引等等。
聚簇索引: “聚簇”的意思是数据行被按照一定顺序一个个紧密地排列在一起存储。一个表只能有一个聚簇索引,因为在一个表中数据的存放方式只有一种。
非聚簇索引: 又叫二级索引。二级索引的叶子节点中保存的不是指向行的物理指针,而是行的主键值。当通过二级索引查找行,存储引擎需要在二级索引中找到相应的叶子节点,获得行的主键值,然后使用主键去聚簇索引中查找数据行,这需要两次B-Tree查找。
非聚簇索引#####
以myisam为例,一个数据表table中,它是有table.frm、table.myd以及table.myi组成。table.myd记录了数据,table.myi记录了索引的数据。在用到索引时,先到table.myi(索引树)中进行查找,取到数据所在table.myd的行位置,拿到数据。所以myisam引擎的索引文件和数据文件是独立分开的,则称之为非聚簇索引。myisam类型的索引,指向数据在行的位置。即每个索引相对独立,查询用到索引时,索引指向数据的位置。
聚簇索引#####
以innodb为例,在一个数据table中,它的数据文件和索引文件是同一个文件。即在查询过程中,找到了索引,便找到了数据文件。在innodb中,即存储主键索引值,又存储行数据,称之为聚簇索引。 innodb索引,指向主键对数据的引用。非主键索引则指向对主键的引用。innodb中,没有主见索引,则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的id来当作主键索引。 在聚簇索引中,数据会被按照顺序整理排列,当使用where进行顺序、范围、大小检索时,会大大加速检索效率。非聚簇索引在存储时不会对数据进行排序,相对产生的数据文件体积也比较大。
8、spring、springmvc、springboot三者的区别和联系
Spring 是一个 IOC 和 AOP 容器框架
SpringMVC是SpringMVC是基于Spring功能之上添加的Web框架,想用SpringMVC必须先依赖Spring。
那么SpringBoot和Spring有什么区别呢?
1. Spring Boot可以建立独立的Spring应用程序;
2. 内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了;
3. 无需再像Spring那样搞一堆繁琐的xml文件的配置;
4. 可以自动配置(核心)Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使用注解注入的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml配置文件中。
5. 提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能;
6. 整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其他依赖。
9、springmvc的工作流程?
SpringMVC 工作原理(重要)
简单来说:
客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Moder)->将得到视图对象返回给用户
文章图片
流程说明(重要):
(1)客户端(浏览器)发送请求,直接请求到 DispatcherServlet。
(2)DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。
(3)解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
(4)HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。
(5)处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。
(6)ViewResolver 会根据逻辑 View 查找实际的 View。
(7)DispaterServlet 把返回的 Model 传给 View(视图渲染)。
(8)把 View 返回给请求者(浏览器)
10、springboot自动装配原理
在springboot的启动类上有个springbootApplicationz注解,这是个组合注解,包含:
@SpringBootConfiguration:标记当前类为配置类
@EnableAutoConfiguration:开启自动配置
@ComponentScan:扫描主类所在的同级包以及下级包里的Bean
通过@EnableAutoConfiguration注解,导入了一个自动配置选择器EnableAutoConfigurationImportSelector去扫描每个jar包的META-INF/XXXX.factories这个文件,这个文件是一个key-value形式的配置文件,里面存放了这个jar包依赖的具体依赖的自动配置类
文章图片
这些自动配置类又通过@EnableConfigurationProperties注解读取xxxx.properies/yml属性文件我们具体配置的值,没有配置就是用默认值。
文章图片
11、spring事务的传播机制
1.1 支持当前事务
REQUIRED (必须有)默认
含义:如果当前方法没有事务,新建一个事务,如果已经存在一个事务中,则加入到这个事务中。
SUPPORTS (可有可无)
含义:支持当前事务,如果当前没有事务,就以非事务方式执行
MANDATORY (强制)
含义:使用当前的事务,如果当前没有事务,就抛出异常。
1.2 不支持当前事务
REQUIRES_NEW
含义:新建事务,如果当前存在事务,把当前事务挂起。
NOT_SUPPORTED
含义:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER
含义: 以非事务方式执行,如果当前存在事务,则抛出异常。
1.3 NESTED
含义: 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
12、spring框架使用了那些设计模式以及应用场景
1.工厂设计模式
Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。
两者对比:
BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,
程序启动速度更快。
ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。
BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,
除了有BeanFactory的功能之外还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
2.单例设计模式 在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处:
对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
Spring中bean的默认作用域就是singleton(单例)的,除了singleton作用域,Spring中bean还有下面几种作用域:
prototype : 每次请求都会创建一个新的 bean 实例。3.代理设计模式 AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
request :每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
global-session:全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型JavaWeb插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet都有不同的会话
Spring AOP就是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理,
4.观察者模式 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
5.适配器模式 适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor、AfterReturningAdviceAdapter、AfterReturningAdviceInterceptor。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。
spring MVC中的适配器模式
在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}
假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。
。。。。等等。暂时说这几个
13、spring事务的实现方式以及事务时候时候会失效?
1.编程式事务
2.声明式事务(注解)
失效?
- 访问权限问题:非public类型
- 方法final修饰的:当某个方法被final修饰时,子类是无法继承和重载的,事务是基于动态代理去实现的,如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
- 未被Spring管理:使用spring事务的前提是:对象要被spring管理,需要创建bean实例。如果,你开发了一个Service类,但忘了加@Service注解
- 错误的传播特性:我们在使用@Transactional注解时,是可以指定propagation参数的。该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:
REQUIRED:如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值;
SUPPORTS:如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行;
MANDATORY:如果当前上下文中存在事务,否则抛出异常;
REQUIRES_NEW:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行;
NOT_SUPPORTED:如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行;
NEVER:如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码;
NESTED:如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务;
- 手动try…catch了异常。
@Service
public class OrderService {
@Transactional
public void add(OrderVO orderVO) {
try{
saveData(orderVO);
} catch(Exception e) {
log.error(e);
}
}
}
- 方法内部调用:有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:
@Service
public class OrderService {
@Transactional
public void add(OrderVO orderVO) {
saveData(orderVO);
}@Transactional
public void saveData(OrderVO orderVO) {
doSameThing();
}
}
我们看到在事务方法add中,直接调用事务方法saveData。saveData方法拥有事务的能力是因为Spring Aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以saveData方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
14、mysql的Innodb和MyIASM引擎区别
Innodb引擎:
1.Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。
2.该引擎还提供了行级锁和外键约束。
3.使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDBb表同样会锁全表。
4.它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统。
5.MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。
6.该引擎不支持FULLTEXT类型的索引(不支持全文索引)。
7.它没有保存表的行数,当SELECT COUNT(*)FROM TABLE时需要扫描全表。
8.当需要使用数据库事务时,该引擎当然是首选。
9.由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。
MyIASM引擎:
1. MyIASM是MySQL默认的引擎。
2.但是它没有提供对数据库事务的支持,也不支持行级锁和外键。
3.因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。
4.MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。
5.如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASMy也是很好的选择。
主要区别:
1.MyIASM是非事务安全的,而InnoDB是事务安全的。
2.MyIASM锁的粒度是表级的,而InnoDB支持行级锁。
3.MyIASM支持全文类型索引,而InnoDB不支持全文索引。
4.MyIASM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyIASM.
5.MyIASM表保存成文件形式,跨平台使用更加方便。
应用场景:
1.MyIASM管理非事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作,应该选择MyIASM。
2.InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB。
14、mysql中存在哪些索引类型?索引使用的数据结构类型?
mysql索引的数据结构就是用到的B+树
MySQL目前主要有以下几种索引类型:
1.普通索引
是最基本的索引,它没有任何限制
2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。
15、mysql为啥使用的是b+tree数据结构?不用二叉树、红黑树、btree?
https://blog.csdn.net/qq_17011423/article/details/104668352
16、mysql索引什么时候会失效?
1、使用!= 或者 <> 导致索引失效
文章图片
2、类型不一致导致的索引失效
文章图片
3、函数导致的索引失效
文章图片
4、运算符导致的索引失效
文章图片
5、OR引起的索引失效
文章图片
6、like查询以%开头
'abc%'不会导致索引失效
7、NOT IN、NOT EXISTS导致索引失效
8、IS NOT NULL会导致索引失效
9、复合索引不满足最左匹配原则(注意假设a,b,c字段为组合索引,查询时候where c=1 and b=1 and a=1索引是生效的,但是 where c=1 and b=1 没有a的条件,那么索引是不生效的,最左匹配原则给查询条件的没有关系)
17、简述spring bean的生命周期
https://www.cnblogs.com/kenshinobiy/p/4652008.html
文章图片
可以简述为以下九步
- 实例化bean对象(通过构造方法或者工厂方法)
- 设置对象属性(setter等)(依赖注入)
- 如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口)
- 如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身
- 将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法
- 调用Bean的初始化方法
- 将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法
- 使用Bean
- 容器关闭之前,调用Bean的销毁方法
自定义starter
18.1 如何实现一个IOC容器
- 配置文件配置包扫描路径
- 递归包扫描获取.class文件
- 反射、确定需要交给IOC管理的类
- 对需要注入的类进行依赖注入
- 配置文件中指定需要扫描的包路径
- 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
- 从配置文件中获取需要扫描的包路径,获取到当前路径下的信息,我们将当前路径下所有以.class结尾的文件添加到一个set集合中进行存储
- 遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
- 遍历这个IOC容器,获取到每一个类的实例,判断里面是否有依赖其他的类的实例,然后进行递归注入
1、基本的ABA问题
在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并交换(CPU保证原子操作),这个时间差会导致数据的变化。 假设有以下顺序事件: > 1、线程1从内存位置V中取出A > 2、线程2从内存位置V中取出A > 3、线程2进行了写操作,将B写入内存位置V > 4、线程2将A再次写入内存位置V > 5、线程1进行CAS操作,发现V中仍然是A,交换成功
尽管线程1的CAS操作成功,但线程1并不知道内存位置V的数据发生过改变
解决方案?
Java中提供了AtomicStampedReference和AtomicMarkableReference来解决ABA问题
AtomicStampedReference可以原子更新两个值:引用和版本号,通过版本号来区别节点的循环使用
AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号,
当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功
20、JVM相关问题?
JVM相关总结
Java虚拟机(JVM)你只要看这一篇就够了!
21、java8为何把永久代移除,增加了元空间?
永久代(PermGen):
JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分
习惯将方法区认为就是永久代,实际还是有本质区别的。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现。
方法区:
方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
Metaspace(元空间):
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小
比较总结:
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
22、synchronized与Lock的区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
22.1拓展、synchronized和ReentrantLock区别和底层实现原理
区别基本和上面差不多
原理:
synchronized: 方法级:ACC_SYNCHRONIZED 代码块:monitorenter、monitorexit
1、jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
ReentrantLock: **ReentrantLock主要利用CAS+AQS队列来实现** ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果: **非公平锁:**如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取; **公平锁:**如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。 22.2拓展 AQS是什么?原理?
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的双向队列。底层实现的数据结构是一个双向链表。
即 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS的大致实现思路
AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
参考:https://blog.csdn.net/striveb/article/details/86761900
23、ThreadLocal原理以及使用场景
ThreadLocal是什么?
一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。
ThreadLocal的使用场景:
- ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
- ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
Entry中的key被定义为弱引用类型,当发生GC时,key会被直接回收,无需手动清理。
而value属于强引用类型,被当前的Thread对象关联,所以说value的回收取决于Thread对象的生命周期。如果说一个线程执行完毕,线程Thread随之被释放,那么value便不存在内存泄漏的问题。然而,我们一般会通过线程池的方式来复用Thread对象来节省资源,这就会导致一个Thread对象的生命周期会非常长,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
因此,我们在使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap(一般在finally代码块中)。
24、锁的状态和升级过程
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁
文章图片
文章图片
25、你们项目如何排查JVM问题(当然本博主那么帅,肯定有实战经验的,以下也是实战的总结)
https://www.cnblogs.com/xuwc/p/13806090.html 博客参考
通过 top 命令找到 CPU 消耗最高的进程,并记住进程 ID。
再次通过 top -Hp [进程 ID] 找到 CPU 消耗最高的线程 ID,并记住线程 ID.
通过 JDK 提供的 jstack 工具 dump 线程堆栈信息到指定文件中。具体命令:jstack -l [进程 ID] >jstack.log。
对于还在正常运行的系统:
1.可以使用jmap来查看jvm中各个区域的使用情况
2.可以通过使用jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
3.可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
4.通过各个命令的结果,或者jvisualvm等工具来进行分析
5.首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
6.同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生OOM的系统:
1.一般生产系统中都会设置当系统发生OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2.我们可以利用jvisualvm等工具来分析dump文件
3.根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4.然后再进行详细的分析和调试
26、类加载的双亲委派机制
Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
文章图片
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
文章图片
27、线程以及线程池底层原理相关
https://blog.csdn.net/qq_41425590/article/details/118787434
28、请你谈谈对于valotile的理解
https://blog.csdn.net/qq_41425590/article/details/118493883
29、java并发编程之CountDownLatch、CyclicBarrier、Semaphore
https://blog.csdn.net/qq_41425590/article/details/118610954
30、spring怎么解决循环依赖的?为啥要用到了三级缓存?
https://baijiahao.baidu.com/s?id=1676046519501587416&wfr=spider&for=pc
为啥要用到了三级缓存?
https://www.cnblogs.com/semi-sub/p/13548479.html
singleFactory.getObject()返回的是代理对象,那么注入的也应该是代理对象,我们可以看到注入的确实是经过CGLIB代理的AService对象。所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象
31、你们项目中,是怎么对你们的接口进行加密处理的?
springboot项目接口API加密
当然本博主真实项目中也是通过这种方式进行接口api加签的,保证了接口的安全性
接口数据param加密方案
接口数据param加密方案1
32、Redis五种数据类型及应用场景
String: 一般做一些复杂的计数功能的缓存
List: 做简单的消息队列的功能
Hash: 单点登录
Set: 做全局去重的功能
SortedSet: 做排行榜应用,取TopN操作;延时任务;做范围查找
https://www.cnblogs.com/agilestyle/p/11532375.html
https://www.cnblogs.com/jasonZh/p/9513948.html
文章图片
33、redis是单线程的吗?为什么那么快?
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
【java面试题|2022java面试题小总结】5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
1)多路 I/O 复用模型
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
34、redis的持久化机制
RDB:
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程就是有一个fork子进程,先将数据集写入到临时文件中,写入成功后,再替换之前的文件,用二进制压缩存储。
工作方式
当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
Redis 调用forks。同时拥有父进程和子进程。
子进程将数据集写入到一个临时 RDB 文件中。
当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益
AOF:
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
https://segmentfault.com/a/1190000016021217
35、如何保障mysql和redis之间的数据一致性?
推荐1:https://blog.csdn.net/weixin_45127309/article/details/104253328?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0.pc_relevant_paycolumn_v3&spm=1001.2101.3001.4242.1&utm_relevant_index=2
推荐2:https://blog.csdn.net/huxiaodong1994/article/details/102906696
36、redis的缓存穿透,缓存击穿、缓存雪崩三者的区别以及解决方案
缓存穿透
描述:
缓存穿透是指缓存和数据库中都没有的数据, 而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
1、设置热点数据永远不过期。
2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
3、布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
4、加互斥锁,互斥锁参考代码如下:
文章图片
说明:
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,
其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,
重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的
数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。
缓存雪崩
描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。
删除策略:
1、定时删除
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器立即执行对key的删除。
优缺点:
- 节约内存,到点了就立即释放掉不必要的内存空间。
- cpu的压力会很大,不会考虑删除的时候cpu是否空闲,会影响redis服务器的响应时间和吞吐量。
数据到达过期时间,不做处理。等下次访问该数据时,发现未过期,则返回值;发现已经过期,删除(删除expires空间和key值),并返回不存在。
优缺点:
- 节约cpu性能,当数据必须删除的时候才删除。
- 内存压力很大,出现过期数据会长期占用内存的情况。
上面的两种方案都比较极端,要么牺牲空间,要么牺牲时间。有没有这种的方案?
每隔一段时间执行一次过期删除操作,并通过限制删除操作执行时长和频率来减少操作对CPU的影响,除此之外,通过定期删除过期键,有效的减少由于过期键带来的内存浪费。但是难点是,确定过期键删除的时长和频率。
但是难点是,确定过期键删除的时长和频率。
特点:
1、CPU性能占用设置了峰值,检测频度可以自己配置。
2、内存压力不是很大,长期占用内存的冷数据会被持续清理。
综上:redis内存淘汰机制 redis提供8种数据淘汰策略:
配置:maxmemory-policy volatile-lru
检测易失性数据(可能会过期的数据集server.db[i].expires)
volatile-lru --> 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-lfu–>从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
volatile-ttl–>从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random -->从已设置过期时间的数据集中任意选择数据淘汰
检测全库数据(所有数据集server.db[i].dict)
allkeys-lru --> 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用)
allkeys-random–>从数据集中任意选择数据淘汰
allkeys-lfu–>当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
放弃数据驱逐
no-eviction–>禁止驱逐数据(redis4.0默认策略),也就是说当内存不足以容纳新写入数据时,新写入操作或报错,回引发OOM(Out of memory)
38、分布式锁的实现方式?
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案。
针对分布式锁的实现,目前比较常用的有以下几种方案:
https://www.php.cn/faq/466231.html
基于数据库实现分布式锁
基于缓存(redis,memcached,tair)实现分布式锁
基于Zookeeper实现分布式锁
基于redis实现分布式锁具体方案参考
39、分布式事务的解决方案?
解决方案
在分布式系统中,要实现分布式事务,无外乎那几种解决方案。
https://blog.csdn.net/hancoder/article/details/120213532
https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html
39.1 CAP定理 CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:
C:一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
A:可用性(Availability) :在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
P:分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
应用最多的是AP组合
39.2 BASE理论 在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
一、两阶段提交(2PC) 2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
1. 运行过程
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
2.2 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
2.3 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
2.4 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景
3. 落地的方案
- AP(Application Program):即应用程序,可以理解为使用DTP分布式事务的程序。
- RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
- TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
1、需要本地数据库支持XA协议。
2、资源锁需要等到两个阶段结束才释放,性能较差。
3.2 Seata-AT方案 Seata实现2PC与传统2PC的差别:
架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。
两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到阶段2完成才释放。而Seata的做法是在阶段1就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。
二、补偿事务(TCC) 什么是TCC事务?
TCC是Try、Con?rm、Cancel三个词语的缩写,
TCC要求每个分支事务实现三个操作:预处理Try、确认Con?rm、撤销Cancel。
Try操作做业务检查及资源预留,
Con?rm做业务确认操作,
Cancel实现一个与Try相反的操作即回滚操作。
TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Con?rm操作,其中Con?rm/Cancel操作若执行失败,TM会进行重试。
TCC分为三个阶段:
- Try阶段是做业务检查一致性及资源预留,此阶段仅是一个初步操作,它和后续的Confirm一起才能真正的构成一个完整的业务逻辑;
- Confirm阶段是确认提交,Try阶段所有的分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需要引入重试机制或进行人工处理;
- Cancel阶段是在业务执行错误需要回滚的状态下,执行分支事务的业务取消了,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。如果Cancel阶段真的出错了,需要引入重试机制或进行人工处理;
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。具体参考文档https://blog.csdn.net/hancoder/article/details/120213532
还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
落地的方案
文章图片
实现方案1
Hmily实现TCC事务
三、解决方案之可靠消息最终一致性 什么是可靠消息最终一致性事务?
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
方案1:本地消息表方案 本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
基本思路就是: 消息生产方, 需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方, 需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
方案2:RocketMQ事务消息方案 (推荐) 有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
RocketMQ 是一个来自阿里巴巴的分布式消息中间件
下面以注册送积分为例来说明:
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
文章图片
为方便理解我们还以注册送积分的例子来描述 整个流程。
Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
1、Producer 发送事务消息
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
本例中,Producer 发送 ”增加积分消息“ 到MQ Server。
2、MQ Server回应消息发送成功
MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。
3、Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。本例中,Producer 执行添加用户操作。
4、消息投递
若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。
MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
5、事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer
来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
文章图片
具体参考https://blog.csdn.net/hancoder/article/details/120213532
四、解决方案之之最大努力通知 什么是最大努力通知?
发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收方
文章图片
交互流程:
1、账户系统调用充值系统接口
2、充值系统完成支付处理向账户系统发起充值结果通知(这个是重点)
若通知失败,则充值系统按策略进行重复通知
3、账户系统接收到充值结果通知修改充值状态。
4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
如果实在通知不到,就提供查询接口,让用户主动去查询一遍通过上边的例子我们总结最大努力通知方案的目标:
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
1、有一定的消息重复通知机制。
? 因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2、消息校对机制。
? 如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
最大努力通知与可靠消息一致性有什么不同? 1、解决方案思想不同
- 可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
- 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
3、技术解决方向不同
- 可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
- 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
https://segmentfault.com/a/1190000022717820
https://www.cnblogs.com/xichji/p/12381998.html
1、基于UUID
在Java的世界里,想要得到一个具有唯一性的ID,首先被想到可能就是UUID,毕竟它有着全球唯一的特性。那么UUID可以做分布式ID吗?答案是可以的,但是并不推荐!
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid);
}
UUID的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718,但UUID却并不适用于实际的业务需求。像用作订单号UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID。
当然 UUID的变种也是可以用的
1)为了解决UUID不可读,可以使用UUID to Int64的方法。
2)为了解决UUID无序的问题,NHibernate在其主键生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)
优点:
1)简单,代码方便。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
缺点:
1)没有排序,无法保证趋势递增。
2)UUID往往是使用字符串存储,查询的效率比较低。
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
4)传输数据量大
5)不可读。
2、基于数据库自增ID
3、基于数据库集群模式
前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。
那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?
解决方案:设置起始值和自增步长
文章图片
4、基于数据库的号段模式
号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存
5、基于Redis模式
文章图片
6、基于雪花算法(Snowflake)模式
雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。
文章图片
Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8bit,也就是说一个Long类型占64个bit。
Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。
- 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
- 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年
- 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。
- 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID
41、mybatis的一级缓存和二级缓存
https://www.cnblogs.com/yunianzeng/p/11826449.html
推荐阅读
- 源码系列|查看动态代理生成的类文件
- Java|数据结构与算法(java)(线性表-队列)
- vue3|vue3.0常用的composition API
- Spring|[Spring手撸专栏学习笔记]——容器事件和事件监听器
- java|用Java写出敬业福小程序
- java|支付宝集五福最全攻略!「一行黑科技」
- 程序员|你需要知道的有关Selenium异常处理的都在这儿
- java|2022年支付宝集五福|看这里100%扫敬业福
- 【Python】系列|【Python】面试官:元组列表都分不清,回去等通知pa