面试总结(总)

基础

MVCC

MVCC:
快照读,innodb的读,读取一个切片的记录
当前读,读取当前实时的记录

1.如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。
解释:
即,当前读取到的DB_TRX_ID,不在我们的数组id之中,即当前的事物id是已经commit的最新的id,即数据是可见的

2.如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数
据修改所涉及到的事务已经提交了,当前数据行是可见的。

即在数组id中查不到我们当前的DB_TRX_ID,也就是说我们的数据已经commit了
  1. 如果这个值大于当前系统中最大的事务 ID,说明这行数据是我们在开启事务
    之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行
    数据,那么此时这行数据就是不可见的。

    即当前的DB_TRX_ID是最新的事物id,即是我们进行了修改但是没有提交的事物id

  2. 如果这个值的大小介于数组中最小值和系统的事务 id之间(闭区间),且该
    值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。

    即当前的DB_TRX_ID不是最近的修改的ID,因为其不是最大,而且其不在数组ID中,说明已经commit了,即可见。

  3. 如果这个值的大小介于数组中最小值和系统的事务 id之间(闭区间),且该
    值在数组中(不等于当前事务id),说明这是一个未提交的事务修改的数据,不可见

    即当前事物在数组ID中,说明未提交,不是最大数组ID,有说明是在事物中进行了修改操作,即修改未提交。
    比如我们有 A、B、C、D 四个会话:
    1.首先 A、B、C 分别开启一个事务,事务 ID是 3、4、5
    2.然后 C 会话提交了事务,A、B 未提交,接下来 D 会话也开启了一个事务,事务 ID 是 6
    3.当 D 会话开启事务的时候,数组中的值就是[3,4,6]
    4.现在假设有一行数据的 DB_TRX_ID 是 5(第四种情况),那么该行数
    据就是可见的,(因为当前事务开启的时候它已经提交了);如果有一行数据的
    DB_TRX_ID 是 4,那么该行就不可见(因为未提交)

ThreadLocal

ThreadLocal提供了线程本地变量,也就是说如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量的时候,实际操作的是自己本地内存里面的变量,从而避免了线程同步问题,创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存

public static final ThreadLocal threadLocal=new ThreadLocal<>();
即开头的这一段代码就为线程本地变量

set方法:
1.首先调用thread方法获取到当前的线程
Thread t = Thread.currentThread();
2.调用自己的getMap方法,把传入的当前线程的thread返回为一个ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
3.在判断这个 返回的ThreadLocalMap是否为空,
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
即如果返回的当前线程的thread为空,会创建一个新的threadLocalMap,
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
返回不为空,直接把value值设置到ThreadLocals成员变量中。

get方法:
1.先创建一个线程
Thread t = Thread.currentThread();
2.把创建的线程丢到自己的 getMap方法中
ThreadLocalMap map = getMap(t);
与setMap方法目的一样,转换成为一个ThreadLocalMap
3.对我们的这个map进行判断
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
return setInitialValue();

在ThreadLocalMap集合中查找以当前线程为键值的threadLocals变量,然后判断threadLocals成员变量是否为null,
如果不为null,则返回当前线程的threadLocals成员变量中存储的本地变量的值;
如果为null,则调用setInitialValue()来初始化threadLocals成员变量并返回

setInitialValue()的源码如下:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

首先先调用initialValue()方法,都会先返回null,然后获取当前的线程,以当前线程为key值,获取ThreadLocalMap集合中对应的threadLocals成员变量,如果得到的threadLocals不为null,则调用set()方法进行设置,如果为null则调用createMap(t,value),并传入value=null值,返回value的值

remove方法:
1.首先会根据当前线程得到threadLocals成员变量的值,如果threadLocals不为null的话,直接移除当前ThreadLocal对象对应的value值。

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

主线程与子线程为什么不具有继承性:
使用ThreadLocal存储本地变量的时候,主线程和子线程之间不具有继承性,子线程中无法获得主线程中保存的值。

为什么会这样:
1.子线程通过其threadLocal.get进行获取值操作,
2.创建一个新线程
3.进行转化为ThreadLocalMap,并对生成的map对象进行判断
3.1 不为空则返回当前线程的threadLocals成员变量中存储的本地变量的值;
3.2 为空则调用setInitialValue()来初始化threadLocals成员变量并返回
3.2.1 通过setInitialValue()方法中的InitialValue方法会返回一个null的泛型 这个即为我们的value,所以我们的子线程是一定没办法获取到我们主线程的本地变量保存的值的。

如何解决:InheritableThreadLocal类的引入
public static final InheritableThreadLocalthreadLocal=new InheritableThreadLocal();即可

数据类型

基本数据类型:
数值型:整数:byte(1),short(2),int(4),long(8)
浮点:float(4),double(8)
字符型:char(2)
布尔 : boolean(1)

1.值不可变,不能添加属性和方法
2.比较的时候比较的是值
3.存放在栈

常量池:
对于Integer、Short、Byte、Char、Long 这些包装类,都有一个常量池,常量池的范围是-128~127之间。如果定义的包装类的值在这个范围内,则会直接返回内部缓存池中已经存在的对象的引用,而对于浮点型Float和Double这样的包装类,没有常量池机制,不管传入的值是多少,都会new一个新的对象。

引用数据类型: string,类,接口,数组
1.可以有属性和方法
2.值同时保存在栈内存和堆内存,堆存内容,栈存堆的地址

equal和==

1.equals是对的封装
2.

2.1 基本类型比较值
2.2 引用类型比较引用,即引用类型的变量所指向对象的地址
3.equals
3.1 要重写,否则与==一致
3.2 重写之后,比较多是所指向对象的内容,即堆内存的地址

过滤器和拦截器

在这里插入图片描述

拦截器和过滤器的区别:
1.过滤器: 在javaWeb中将传入的request,response提前过滤掉一些信息,或者提前设置一些参数进行业务处理(传入servlet)。
例如:传入Servlet前统一设置字符集,或者去掉一些非法字符。
2.拦截器:
面向切面编程,在service或者一个方法前/后调用一个方法。
例如:动态代理类就是拦截器的简单实现
调用方法前打印出字符串,或者在调用方法后打印出字符串
甚至在抛出异常的时候做业务逻辑的操作

区别:
1.拦截器基于java的反射机制,过滤器是基于函数的回调
2.拦截器不依赖于servlet容器,过滤器依赖于servlet容器
3.拦截器只对action请求起作用,过滤器可以对所有请求起作用
4.拦截器可以访问action上下文,值,栈里的对象,过滤器不可以 5.action声明周期中,过滤器可以呗对此调用,拦截器只在容器初始化操作的时候呗调用一次 6.拦截器可以获取IOC容器中的各个bean,过滤器不可以,在拦截器里注入一个service就可以调用业务逻辑
7.触发时机不同,过滤器是在请求进入容器后,进入servlet前处理,
8.过滤器包裹着servlet,servlet包裹着拦截器

依赖循环

在这里插入图片描述

Spring容器在初始化对象时,开发者有哪些途径来进行初始化后的操作

1.可以在配置文件中的bean标签内通过init-method属性指定初始化的方法名或者 在方法上用注解@Bean标识出来一个 方法用将上面的逻辑代码放在其中可执行

2.通过实现InitializingBean接口,实现 afterPropertiesSet() 方法,在该方法体内,进行初始化后的操作

3.使用@PostConstruct 注解标识出一个方法,进行初始化后的操作

4.前面三种都是对一个对象进行的操作,最后是通过一个特定的类 实现 BeanPostProcessor 接口,对一种对象或多种对象进行初始化的操作

@postconstruct注解

@PostConstruct注解,好多人以为是Spring提供的。其实是Java自己的注解 。Java中该注解的说明:@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet时运行,并且只会被服务器运行一次,类似Servlet的init()方法。被@PostConstruct修饰的方法会在构造函数之后,init()方法前运行。

通常会在Spring中使用到该注解,该注解的方法在整个Bean初始化中的执行顺序如下:

Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)

说一下你了解的Io

在这里插入图片描述

Properties和yaml的区别

Properties:底层是Hashtable

yaml :底层是LinkHashMap

String和StringBuilder,StringBuff的区别:

在这里插入图片描述

重载和重写的区别

在这里插入图片描述

ArryList和LinkList的区别

在这里插入图片描述

线程池的七种种创建

在这里插入图片描述

创建线程的三种方式

创建线程有三种方式
在这里插入图片描述

终止线程

Runnable和Callable的区别

在这里插入图片描述

synchronized的锁升级的原理

在这里插入图片描述

如何检测死锁

在这里插入图片描述

如何防止死锁

在这里插入图片描述

SpringMvc工作流程

在这里插入图片描述

Start() 和 Run()的区别

1.start()方法来启动线程,真正实现了多线程运行,直接继续执行以下代码

2.run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行
在这里插入图片描述

Sleep和wait的区别?两者是否会产生死锁

在这里插入图片描述

线程同步(7种同步方法)

1、同步方法

synchronized关键字修饰方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 ( 注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类 )

2、同步代码块

synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动加上内置锁,从而实现同步 ( synchronized(object){} )

3、使用特殊域变量(volatile)实现线程同步

4:使用重入锁实现线程同步

5.使用局部变量实现线程同步

6.使用阻塞队列实现线程同步

7.使用原子变量实现线程同步

原注解是什么?有几种?

所谓元注解,其主要作用就是负责注解其他注解,为其他注解提供了相关的解释说明

Java中存在五个元注解,分别是 @Target、@Retention、@Documented、@Inherited、@Repeatable

那些集合是线程不安全的

  • ArrayList

  • LinkedList

  • TreeMap

  • HashSet

  • TreeSet

  • Queue

那些集合是线程安全的

Vector,Stack,HashTable 都是线程安全的集合类,虽然这三个类是线程安全的,但并不建议使用,像 Vector和HashTable 都已经被官方标注成“即将废弃”。

什么是幂等性,如何处理幂等性

我同事在处理订单的时候遇到过这种问题

1.一个订单创建接口,第一次调用超时了,然后调用方重试了一次
2.在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
3.当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次
4.一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款。但是你先接收到已付款,然后又接收到了已创建
5.在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢。消息中间件又把消息投递给另外一台机器处理

为了解决以上问题,就需要保证接口的幂等性

有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。
除了查询功能具有天然的幂等性之外,增加、更新、删除都要保证幂等性

幂等性:实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。

解决幂等性:
1.全局唯一ID
2.去重表
3.插入或更新
4.多版本控制
5.状态机控制

1.全局唯一ID

如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。
从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。

2.去重表

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

3.插入或更新

这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。在mysql数据库中如下:

在这里插入图片描述

4.多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等
在这里插入图片描述
在这里插入图片描述

5.状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99

在这里插入图片描述

什么是0拷贝

零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术

Jsp和servlet的区别

jsp和servlet的区别和联系:
1.jsp经编译后就变成了Servlet.
(JSP的本质就是Servlet,JVM只能识别java的类,不能识别JSP的代码,Web容器将JSP的代码编译成JVM能够识别的java类)
2.jsp更擅长表现于页面显示,servlet更擅长于逻辑控制.
3.Servlet中没有内置对象,Jsp中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到.
Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器完成。
而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。


联系:  
JSP是Servlet技术的扩展,本质上就是Servlet的简易方式。JSP编译后是“类servlet”。
Servlet和JSP最主要的不同点在于:
Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML里分离开来。
而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。
JSP侧重于视图,Servlet主要用于控制逻辑
Servlet更多的是类似于一个Controller,用来做控制。

并行和并发的区别

并发:对单处理器而言–多个程序在同一时间段发生。

并行:对多处理器而言–多个程序在同一时刻发生。

Beanfactory 和FactoryBean的区别

在这里插入图片描述

你的项目中用到了那些模式

在这里插入图片描述

1.Session的工作流程:

session实现 流程:
1.当客户端访问服务器时,服务器根据需求设置seesion,将会话信息保存在服务器上,同时将标示session的session_id传递给客户端浏览器,浏览器将这个session_id保存在内存中(还有其他的存储方式,例如写在url中),我们称之为无过期时间的cookie。浏览器关闭后,这个cookie就清理掉了,它不会存在用户的cookie临时文件。以后浏览器每次请求都会额外加上这个参数值,服务
器再根据这个session_id,就能获得客户端的数据状态。浏览器每次请求都会带上由服务器为它生成的session_id。如果客户端浏览器意外关闭,服务器保存的session数据不是立即释放的,此时数据还会存在,,只要我们知道那个session_id,就可以继续通过请求获得此session的信息;但是这个时候后台的
session还存在,但是session的保存有一个过期时间,一旦超过规定时间没有客户端请求,他就会清楚这个session。

工作原理:

1、创建session :
当用户访问到一个服务器时,如果服务器启用Session,服务器就要为该用户创建一个 Session,在创建这个Session的时候,服务器首先检查这个用户发来的请求里是否包含了一个 Session_id,如果包含了一个Session_id,则说明了之前该用户已经登录过并为此用户创建过session, 那服务器就按照这个session id把这个session在服务器的内存里找出来(如果找不到,就有可能为它新创 建一个),如果客户端请求里不包含有session_id,则为该客户端创建一个session并生成一个与此 session相关的session id。这个session id将在本次响应中返回到客户端保存,而保存这个session id的正是cookie。
2、使用session
(1)保存在cookie里
(2)URL重写,将session id直接附加在URL路径的后面
(3)作为查询字符串附加在URL后面

2.cookie和Session的区别

1、存放位置不同 Cookie保存在客户端,Session保存在服务器端
2、安全性的不同 Cookie存储在浏览器中,对客户端是可见的。Session存储在服务器端,对客户端是透明的。
3、存取方式不同 Cookie中只能保管ASCII字符串,而Session能够存取任何类型的数据。
4、有效期的不同
只需要设置Cookie的过期时间属性为一个很大很大的数字,Cookie就可以在浏览器保存很长很长时 间。由于Session依赖于名为Session id的Cookie,而cookie Session id的过期时间默许为-1,只需 关闭了浏览器,该Session就会失效
5、跨域支持上的不同
Cookie支持跨域名访问,例如将domain属性设置为“.baidu.com”,则以".baidu.com"为后缀的一 切域名均能访问该Cookie。跨域名Cookie如今普遍用在网络中。而Serssion则不会支持跨域名访问。 Session仅在他所在的域名内有效。

3.token过期了怎么办?怎么处理?

token续签。
1)用户登录后生成30分钟过期的token,发回给浏览器保存。
2)浏览器每次请求携带该token访问后台,后台每次取出token的过期时间,判断剩余过期时间小于10
分钟了,后台重新生成一个30分钟过期的token给浏览器保存,
浏览器覆盖之前的token,以达到续签效果

3.Java线程池七个参数详解:核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、线程工厂、拒绝策略

corePoolSize:核心线程数

线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true
后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数
量小于corePoolSize,则马上创建新线程并运行这个任务。

maximumPoolSize:最大线程数

线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满
的情况下,创建一个新线程并执行。

keepAliveTime:空闲线程存活时间

当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。
可被回收的线程:

  1. 设置allowCoreThreadTimeout=true的核心线程。
  2. 大于核心线程数的线程(非核心线程)。

unit:时间单位

keepAliveTime的时间单位:

TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.SECONDS
TimeUnit.MINUTES
TimeUnit.HOURS
TimeUnit.DAYS

workQueue:工作队列

存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任
务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现
了BlockingQueue接口。

JDK默认的工作队列有五种
ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁, 默认使用非公平锁,入队和出队共用一个锁,互斥。
LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近 似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元 素。
PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。

threadFactory:线程工厂

创建线程的工厂,可以设定线程名、线程编号等。

handler:拒绝策略

当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策
略,拒绝策略需要实现RejectedExecutionHandler接口。

JDK默认的拒绝策略有四种:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
CallerRunsPolicy:由调用线程处理该任务。

4.线程池的执行流程(如下图)在这里插入图片描述

5.Java中的容器有哪些:

容器有Tomcat、Spring、集合容器等:

在这里插入图片描述

4.怎么保证API的安全性?(怎么确保请求参数不被别人篡改?)

利用签名验证方式保证。
1)在API的一端存储密钥,使用md5算法,对请求参数和密钥进行md5加密,加密成签名字符串,把
签名跟参数一起请求API的另一个端
2)在API的另一端获取签名信息和参数,把请求参数再加上前端的密钥(两端的密钥必须保持一致
的),重新加密一个新的签名,和传递过来的签名进行匹配,相同则代表请求合法,不合法代表不合法
(参数被篡改了)

5.怎么保证API的幂等性?(任何幂等性都是去重判断)

GET:查询类,不需要做幂等性处理
POST/PUT/DELETE: 修改类(新增,修改,删除),有必要做幂等性判断修改业务,才需要幂等性处
理1)在API的一端,在每次刷新页面时,利用密钥生成新的token,请求过程携带token到API的另一个端
2)在API的另一端,判断redis中是否存在该token,如果存在该token,代表请求已经重复提交了!!
如果redis不存在token,验证token是否合法,如果合法则把token存入redis中,并设置过期时间,代表该请求不重复。

6.基础多线程专题

6.1 项目中哪里用到多线程?(必问)

  1. 案例一:
    在商品数据从MySQl导入到Elasticsearch时,因为mysql商品数量很大,不能一次性导入,否则会内存
    溢出(OOM, out of memeory)。
    该业务采用多线程+ES批量写入完成。
  1. 案例一:
    在商品数据从MySQl导入到Elasticsearch时,因为mysql商品数量很大,不能一次性导入,否则会内存
    溢出(OOM, out of memeory)。
    该业务采用多线程+ES批量写入完成。

//分页查询(1页查询200条)
ExecutorService service = Executors.newFixedThreadPool(5);
for( xxxxx ){
if(i%200=0){
CompletableFuture.runAsync(new Runnable(){
public void run(){
//创建批处理对象
BulkRequest bulkRequest = new BulkRequest();//缓存区
//将写入请求加入到批量处理对象中
for(ApArticle apArticle:apArticleList){
//放入缓存区
bulkRequest.add(request);
}
//执行批处理请求(真正把数据发送到ES执行)
highLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
},service );
}
}

6.2、线程池的七个核心参数

自定义线程池实现:new ThreadPoolExecutor(7个参数)
corePoolSize:核心线程数(默认值:1)。allowCoreThreadTimeout=false为默认值
如果设置allowCoreThreadTimeout=false后, 当前线程数大于corePoolSize,如果线程空闲等
待时间超过
keepAliveTime,则该线程会被回收。
如果设置allowCoreThreadTimeout=true后, 当前线程数小于corePoolSize时,线程池的线程
空闲等待时间超过keepAliveTime,也会被回收
maximumPoolSize:最大线程数(默认值:Integer.MAX_VALUE) keepAliveTime:空闲线程存活时间(默认值:60秒)
unit:时间单位 (秒)
workQueue:工作队列大小(默认值:Integer.MAX_VALUE)
threadFactory:线程工厂
handler:拒绝策略。(默认AbortPolicy策略)

6.3线程池的一个任务执行过程是怎样?

当前线程数是否大于corePoolSize(5)

  1. 小于corePoolSize,直接创建线程
  2. 2.大于或等于corePoolSize,再判断工作队列是否已满(8)
    未满,创建临时线程存入工作队列的尾部,等待执行
    已满,再判断是否大于maximumPoolSize(20)
    小于maximumPoolSize,创建临时线程去执行任务
    大于或等于maximumPoolSize,执行拒绝策略

6.4 如何等到所有子线程执行完毕再往下执行主线程业务?(如何判断所有子线程都执行完毕主线程才继续执行?)

利用CountDownLatch的计数器实现(JUC包)
创建并执行线程,在子线程run方法中,调用countDown()方法进行计数+1
创建线程完毕完毕后,在主线程方法调用await()方法,该方法会等待所有子线程完毕计数器减为0,
主线程才会唤醒继续往下执行。

6.5Runnable 和 Callable 的 区别?

=相同点==
1)两者都是接口
2)两者都可以用来编写多线程代码
3)两者都可以调用Thread.start()来启动线程
不同点==
1)最大的区别,Runnable没有返回值,而实现Callable接口的任务线程能返回执行结果
2)Callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是Runnable
接口实现类中run方法的异常必须在内部处理,不能抛出
3)Runnable可以作为Thread构造器的参数,通过开启新的线程池来执行,也可以通过线程池来执行;
而Callable只能通过线程池来执行。

6.6 JMM内存模型

一个是私有线程的工作区域(共享内存),一个块是所有线程的共享区域(主内存)

6.7JDK默认的拒绝策略有四种:

1.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。(默认值)
2.DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
3.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
4.CallerRunsPolicy:由调用线程处理该任务。
在这里插入图片描述

6.8 synchronized 和 volatile 的区别?

1)volatile是变量修饰符,而synchronized则可以修饰代码块或方法。
2)volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;而synchronized加锁,会导致线程
阻塞(互斥性)
3)synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
4)synchronized会执行编译优化(对字节码重新排序),编译优化有可能导致运行过程中异常。而
volatile禁止字节码重新排序,不会引发编译优化导致的异常。
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行
可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变
化)。
并发编程三大特征:原子性。可见性,有序性
volatile作用:修饰变量,这个变量就会满足可见性与有序性,当线程修改这个变量的时候,别的线程
就会感知到这个变量进行了修改,

6.9 synchronized 和 Lock 的区别?

1)语法不同:
synchronized 是Java的关键字或修饰符,在jvm层面上,修饰方法或代码块。
Lock不是修饰符,是一个接口(加锁的工具类,该类提供很多方法加锁、释放锁等)tryLock()
unLock()
2)释放锁不同():
synchronized 获取锁的线程执行完同步代码,释放锁,且线程执行发生异常,jvm会自动让线程释
放锁(不管成功还是失败,都会自动释放锁)
Lock必须手动在finally中释放锁(必须手动释放锁)
3)死锁情况不同(
):
synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁;
Lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
4)锁判断不同:
synchronized 无法判断当前线程的上锁状态
Lock可以判断当前线程的上锁状态 tryLock unLock isLock
5)锁类型不同:
synchronized是可重入 不可判断 非公平
Lock:是可重入 可判断 可公平
公平性(线程等待时间长,优先获取锁)

6.10 分布式锁

简述:在分布式环境下,多个程序/线程都需要对某一份(或有限制)的数据进行修改时,针对程序进行控制,保证同一时间节点下,只有一个程序/线程对数据进行操作的技术。
在这里插入图片描述
redis实现分布式锁:利用redis的数据类型set,因为他是去重的;
1.获取锁,用SET 一个键值对,(成功返回true,失败返回false)如果返回的是false,就是有线程在占用锁,当线程执行完的时候要删除对应的SET的键值对。为了防止服务宕机无法释放锁,导致死锁,要在获取锁的时候加一个过去时间,就算服务宕机也会自动释放锁。拿不到锁的线程会进入阻塞状态或者非阻塞状态,一般在实际开发中建议进入非阻塞状(tryLock)态,尝试一次,成功就返回true。失败返回false。
2.可重入 :同一个线程可以多次获取同一把锁
Redisson:在 redis的Java内存数据网格,对锁要求高的系统采用,比redis分布式锁优化了可重入,有重试机制,超
时释放,主从一致性(当锁保存在redis里面,主宕机了,从没有锁的备份),里面有可重入锁,公平锁,读写锁等。

7.深拷贝和浅拷贝的区别?怎么实现深拷贝?

A a = new A(xxxx);
a.clone();
class A{
String name; ox1
int age;
B b; ox4
}
class A1{
String name; ox2
int age;
B b; ox5
}
1.浅拷贝(默认情况下):当如果要拷贝一个A对象,而A对象中又有一个B对象,那么如果对A拷贝的时候,重新拷贝出来一个A1对象并且重新分配内存地址,但是对于A中的B对象,仅仅只是把A1中拷贝出来的B1对象的引用指向原来的B对象而已,并没有把拷贝的B1对象也重新进行分配一个新的内存地址。
2.深拷贝:而深拷贝就是在第1的基础上,不仅重新给A1对象分配了新的内存地址,而且还给A1中的B1也重新进行分配了新的内存地址,而不只是仅仅把原本的B的引用给B1。
3.如果想要深拷贝一个对象,这个对象必须要实现 Cloneable 接口,实现 重写clone() 方法,并且在clone 方法内部,把该对象引用的其他对象也要 clone 一份,这就要求这个被引用的对象必须也要实现
Cloneable 接口并且实现 clone 方法

8.什么是序列化和反序列化?什么时候需要序列化或反序列化?序列化的底层怎么实现的?

1.什么是序列化和反序列化?
1)序列化:Java对象变为二进制数据的过程
2)反序列化:二进制的数据变为Java对象的过程

什么时候需要序列化或反序列化?
1)把JavaBean对象读写磁盘
2)让JavaBean通过网络传输 (RPC远程调用就是对象传输,就需要序列化

怎么对JavaBean序列化或反序列化:
让JavaBean实现Serializable接口

Java实现序列化的底层代码:
1)ObjectOutputStream: 对象序列化工具 writeObject(Object)
2)ObjectInputStream: 对象反序列化工具 Object readObject()

9.什么是死锁?怎么防止死锁?

死锁是指多个线程因竞争共享资源而造成的一种互相等待的僵局。
死锁的四个必要条件:
1)互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
2)占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
3)非抢占:不能强行抢占进程中已占有的资源。
4)循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。

如何预防死锁?
1)加锁顺序(线程按顺序加锁)
2)加锁时限 (线程请求所加上期限,超时就放弃,同时释放自己占有的锁) 死锁检测
redis作为锁+过期时间(50s) Reddison

10.JDK的四种线程池创建线程的方式?实际开发中用哪个?

实际开发中使用ExecutorService接口,JDK线程池接口!!!

1)固定线程数的线程池(newFixedThreadPool)
ExecutorService es = Executors.newFixedThreadPool(5);
这种线程池里面的线程被设计成存放固定数量的线程,具体线程数可以考虑为CPU核数*N(N一般为2),这种线程池适合用在稳定且固定的并发场景。

2)缓存的线程池(newCachedThreadPool)
ExecutorService es = Executors.newCachedThreadPool();
这是一个可以无限扩大的线程池。
适合处理执行时间比较小的任务
线程空闲时间超过60s就会被杀死,所以长时间处于空闲状态的时候,这种线程池几乎不占用资源,但是在使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题

3)单个线程的线程池(newSingleThreadExecutor)
ExecutorService es = Executors.newSingleThreadExecutor();
该线程池可以保证所有任务按照指定顺序执行,所以这个比较适合那些需要按序执行任务的场景

4)固定个数的线程池(newScheduledThreadPool)(延迟执行线程)
Executors.newScheduledThreadPool(5);
但实际开发中,我们比较少用上面的线程池实现类创建线程池,因为比较不好控制线程数量,会导致内存溢出OOM。
建议是ThreadPoolExecutor创建线程池,并且指定7个线程池参数

11.HashMap的扩容因子?ArrayList的初始容量和扩容机制?(必问)

HashMap初始容量为16,扩容因子为0.75,扩容到原来的2倍。
ArrayList默认容量为10。超过容量上限则扩容,扩容量为原来的1.5倍

12.Hashmap、Hashtable、ConcurrentHashMap的区别?

1)HashMap是非线程安全,HashTable线程安全,ConcurrentHashMap也是线程安全。但是需要线程安全的话,建议使用ConcurrentHashMap,而不建议使HashTable(因为没有代码优化)
2)ConcurrentHashMap在JDK1.8之前采用分段锁机制,JDK1.8之后采用CAS和synchronized来保证并发安全,(synchronized只锁定当前链表或红黑树二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。)
HashMap和ConcurrentHashMap在JDK1.8之后,都是数组+链表+红黑树。

13.类加载机制

1、把java的源代码通过编译器编译成class文件
2、把class文件放在jvm的类加载器中。
3、JVM的校验器会校验是否存在运行期错误,通过后把class文件交给解析器,
4、解析器会对class进行解析成机器码,把机器码交给系统,系统通过main方法这个入口进行运行。

14.

MySql相关

1.通过如下语句查询mysql的默认隔离级别:

通过如下 SQL 可以查看数据库实例默认的全局隔离级别和当前 session 的隔离级 别:MySQL8 之前使用如下命令查看 MySQL 隔离级别: SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

2.MySQL优化(分为数据库)

0.主键索引最好使用数字、且自增。 1.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。否 则就会1. 产生不必要的磁盘I/O,无法使用覆盖索引, 2.下面的模糊查询也将导致全表扫描:select id from t where name like ‘%c%’ 或者 ’%c‘ 若要提高效率,可以使用向左匹配 select id from t where name like c% (不能前置百分号),或 者考虑全文检索。 3.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而 定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。 4.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储 空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。 5.尽量不在sql语句上做列运算,取出数据后再在业务代码中去去数据

3.什么是分库分表

  1. 什么是分库分表
  1. 为什么需要分库分表
  2. 如何分库分表
  3. 什么时候开始考虑分库分表
  4. 分库分表会导致哪些问题
  5. 分库分表中间件简介

1. 什么是分库分表

分库:就是一个数据库分成多个数据库,部署到不同机器。
在这里插入图片描述

在这里插入图片描述

2. 为什么需要分库分表

2.1 为什么需要分库呢?

如果业务量剧增,数据库可能会出现性能瓶颈,这时候我们就需要考虑拆分数据库。从这几方面来看:
磁盘存储
业务量剧增,MySQL单机磁盘容量会撑爆,拆成多个数据库,磁盘使用率大大降低。
并发连接支撑
我们知道数据库连接是有限的。在高并发的场景下,大量请求访问数据库,MySQL单机是扛不住的!当
前非常火的微服务架构出现,就是为了应对高并发。它把订单、用户、商品等不同模块,拆分成多个应
用,并且把单个数据库也拆分成多个不同功能模块的数据库(订单库、用户库、商品库),以分担读写
压力。

2.2 为什么需要分表?

数据量太大的话,SQL的查询就会变慢。如果一个查询SQL没命中索引,千百万数据量级别的表可能会
拖垮整个数据库。

即使SQL命中了索引,如果表的数据量超过一千万的话,查询也是会明显变慢的。这是因为索引一般是
B+树结构,数据千万级别的话,B+树的高度会增高,查询就变慢啦。
小伙伴们是否还记得,MySQL的B+树的高度怎么计算的呢? 顺便复习一下吧
InnoDB存储引擎最小储存单元是页,一页大小就是16k。B+树叶子存的是数据,内部节点存的是键值
+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找
到需要的数据,B+树结构图如下:

在这里插入图片描述

如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数 =16k/1k =16 .
非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类
型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是 8+6=14 字
节, 16k/14B =16*1024B/14B = 1170
因此,一棵高度为2的B+树,能存放 1170 * 16=18720 条这样的数据记录。同理一棵高度为 3 的B+树,能存放 1170 *1170 *16 =21902400 ,大概可以存放两千万左右的记录。B+树高度一般为1-3
层,如果B+到了4层,查询的时候会多查磁盘的次数,SQL就会变慢。
因此单表数据量太大,SQL查询会变慢,所以就需要考虑分表啦

4.如何分库分表

4.1 垂直拆分

在这里插入图片描述
在业务发展初期,业务功能模块比较少,为了快速上线和迭代,往往采用单个数据库来保存数据。数据库架构如下:

在这里插入图片描述
垂直分库,将原来一个单数据库的压力分担到不同的数据库,可以很好应对高并发场景。数据库垂直拆
分后的架构如下:
在这里插入图片描述

如果一个单表包含了几十列甚至上百列,管理起来很混乱,每次都 select * 的话,还占用IO资源。这时候,我们可以将一些不常用的、数据较大或者长度较长的列拆分到另外一张表。
比如一张用户表,它包含 user_id、user_name、mobile_no、age、email、nickname、address、 user_desc ,如果 email、address、user_desc 等字段不常用,我们可以把它拆分到另外一张表,命名为用户详细信息表。这就是垂直分表

在这里插入图片描述

4.2水平分库

水平分库是指,将表的数据量切分到不同的数据库服务器上,每个服务器具有相同的库和表,只是表中的数据集合不一样。它可以有效的缓解单机单库的性能瓶颈和压力。
用户库的水平拆分架构如下:

在这里插入图片描述
如果一个表的数据量太大,可以按照某种规则(如 hash取模、range ),把数据切分到多张表去。
一张订单表,按 时间range 拆分如下:

在这里插入图片描述

分库分表策略一般有几种,使用与不同的场景:
1.range范围
2.hash取模
3.range+hash取模混合

4.2.1 range范围

range,即范围策略划分表。比如我们可以将表的主键,按照从 0~1000万 的划分为一个表,
1000~2000万 划分到另外一个表。如下图:
在这里插入图片描述

1.这种方案的优点:
这种方案有利于扩容,不需要数据迁移。假设数据量增加到5千万,我们只需要水平增加一张表就
好啦,之前 0~4000万 的数据,不需要迁移。

2.缺点:
这种方案会有热点问题,因为订单id是一直在增大的,也就是说最近一段时间都是汇聚在一张表里
面的。比如最近一个月的订单都在 1000万~2000 万之间,平时用户一般都查最近一个月的订单比
较多,请求都打到 order_1 表啦,这就导致数据热点问题

4.2.2hash取模

hash取模策略:指定的路由key(一般是user_id、订单id作为key)对分表总数进行取模,把数据分散到各个表中。
比如原始订单表信息,我们把它分成4张分表:
在这里插入图片描述

1.这种方案的优点:
hash取模的方式,不会存在明显的热点问题。

2.缺点:
如果一开始按照hash取模分成4个表了,未来某个时候,表数据量又到瓶颈了,需要扩容,这就比
较棘手了。比如你从4张表,又扩容成 8 张表,那之前 id=5 的数据是在( 5%4=1 ,即
t_order_1),现在应该放到( 5%8=5 ,即t_order_5),也就是说历史数据要做迁移了。

4.2.3range+hash取模混合

既然range存在热点数据问题,hash取模扩容迁移数据比较困难,我们可以综合两种方案一起嘛,取之之长,弃之之短。
比较简单的做法就是,在拆分库的时候,我们可以先用range范围方案,比如订单id在0 4000万的区间,划分为订单库1;id在4000万8000万的数据,划分到订单库2,将来要扩容时,id在8000万~1.2亿的数据,划分到订单库3。然后订单库内,再用hash取模的策略,把不同订单划分到不同的表。
在这里插入图片描述

5.什么时候分表?

如果你的系统处于快速发展时期,如果每天的订单流水都新增几十万,并且,订单表的查询效率明变慢时,就需要规划分库分表了。一般B+树索引高度是2~3层最佳,如果数据量千万级别,可能高度就变4层了,数据量就会明显变慢了。不过业界流传,一般500万数据就要考虑分表了。

6. 什么时候分库

业务发展很快,还是多个服务共享一个单体数据库,数据库成为了性能瓶颈,就需要考虑分库了。比如订单、用户等,都可以抽取出来,新搞个应用(其实就是微服务思想),并且拆分数据库(订单库、用户库)。

7.分库分表会导致哪些问题

分库分表之后,也会存在一些问题:
1.事务问题
2.跨库关联
3.排序问题
4.分页问题
5.分布式ID

7.1 事务问题

分库分表后,假设两个表在不同的数据库,那么本地事务已经无效啦,需要使用分布式事务了。

7.2 跨库关联

跨节点Join的问题:解决这一问题可以分两次查询实现

7.3 排序问题

跨节点的count,order by,group by以及聚合函数等问题:可以分别在各个节点上得到结果后在应用程序
端进行合并。

7.4 分页问题

方案1:在个节点查到对应结果后,在代码端汇聚再分页。
方案2:把分页交给前端,前端传来pageSize和pageNo,在各个数据库节点都执行分页,然后汇
聚总数量前端。这样缺点就是会造成空查,如果分页需要排序,也不好搞。

7.5 分布式ID

数据库被切分后,不能再依赖数据库自身的主键生成机制啦,最简单可以考虑UUID,或者使用雪花算
法生成分布式ID

8.分库分表中间件

目前流行的分库分表中间件比较多:
cobar
Mycat
Sharding-JDBC
Atlas
TDDL(淘宝)
vitess

在这里插入图片描述

9.五大约束和三大范式

10.DDL、DML、DQL、DCL与常见数据类型

11说说悲观锁和乐观锁?有哪些常见的悲观锁和乐观锁?

悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该 数据进行加锁以防止并发问题。
常见的悲观锁实现:关系数据库MySQL的行锁和表锁,Java的 synchronized 关键字,分布式锁 Redisson (Redis实现)
悲观锁优点:数据更加安全,不容易出现并发问题 悲观锁缺点:高并发时对性能较大,而且可能出现死锁现象。
乐观锁:乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时 候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁 适用于读多写少的场景,这样可以提高程序的吞吐量。
常见的乐观锁实现:版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的 次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更 新,否则重试更新操作,直到更新成功。
用户表:tb_user
id name sex version
1 jack 男 1
select version v from tb_user where id = 1; v = 1
update tb_user set sex=‘女’ where id = 1 and version = 1
乐观锁优势:并没有真正对操作加锁,对读多写少的场景性能影响小。

12请说说MySQL表级锁和行级锁?

表级锁:对整个表记录锁定,一个事务修改表数据的时候,另一个事务无法修改表数据。
语法:
lock table tb_user read local;
update tb_user ssss
insert into tb_user
unlock;
特点:锁定范围比较大,比较影响性能
应用场景:在数据迁移场景下使用
行级锁:对表的某条(某些)记录锁定
行级锁分为 共享锁 和 排他锁
update/insert
共享锁:一个事务在修改记录的时候,另一个事务无法修改记录,但是可以读取。 • update tb_user set sex=‘女’ where id = 1 lock in share mode;
排他锁:一个事务在查询/修改记录的时候,另一个事务无法修改和读取数据。 • select * from tb_user where id = 1 for update;

13MySQL一张表中有1千万条数据,现在查询比较慢,有什么优化思路?(必问)或:请问MySQL优化的思路?

sql的慢查询
首先要知道我们的系统中,到底是那些sql是比较慢的,我们可以用慢查询日志,慢查询日志是MySQL提 供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句。·
我们需要配置一个sql的执行时间阈值,一般来大多数sql语句超过1秒就是慢sql,但是有一些报表的统计呢 是比较慢的,这种没办法,然后就开启慢查询,指定一个文件路径,那么当sql执行时间超过1秒的时候,就会 统计到那个指定的文件里面。
知道了哪条sql慢之后,可以用在sql语句前面加上explain分析这条sql,里面就有些参数帮助我们分 析了,例如查询的类型是普通查询,子查询,还是连表查询,连表查询的顺序有没有按照我们想的顺序来查 (id不一样的话,id越大他越快执行),有没有用到索引,关键的参数里面有个type(访问类型)字段决定 要不要进行优化;
SQL语句优化
1)需要什么查就询什么,避免用*
2)如果知道查询结果只有一条或者只要最大或者最小的一条记录,建议加上limit1
3) 尽量比较时用于>= <= , like的百分号不要放左边
4)需要考虑对查询频繁的字段建立索引或联合(复合)索引(复合索引如果是3的话,底层也会创建3个索 引)。 但是要避免写SQL语句时索引失效的情况,如何得知SQL是否执行索引?答:使用explain查询执行计划
常见的可能导致索引失效的情况:
(1)where中索引列有运算
(2)like查询是以%开头
(3)违背最左匹配原则
(4)如果列类型是字符串,那在查询条件中需要将数据用引号引用起来,否则不走索引 最左前缀原则是这样: (name,age,sex)
where name = jack ok
where name = jack and age>=10 ok
where age >10 and sex = “男” 不ok
3)如果说单表数据太多(过千万),可以采用水平分库分表(采用ShardingJdbc技术)
4)如果业务数据用于复杂的查询场景,把MySQL数据导入到Elasticsearch中,进行全文检索,从而效率更 高。

14.MySQL索引有哪些类型?

1)普通索引(INDEX):最基本的索引,没有任何限制 creata index a_index on tb_user(name)
2)唯一索引(UNIQUE):与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值,一张表可 用多个唯一 索引。
3)主键索引(PRIMARY KEY):是一种特殊的唯一索引,值唯一,但是不允许有空值,一张表只能有一个主 键索引。
4)组合索引(INDEX)/复合索引/联合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前 缀“原则。 tb_user(name,sex,age)
5)全文索引(FULLTEXT):仅可用于 MyISAM 表, 用于在一篇文章中,检索文本信息的, 针对较大的数 据,生成全 文索引很耗时好空间。
其实索引就是类似于书籍的目录,提高数据检索的效率,实际上索引也是一张表,该表中保存了主键与索引字 段,并指向实体类的记录,所以索引列也是要占用空间的。)
虽然索引大大提高了查询效率,但是却也降低更新表的速度,因为在更新表的时候,不仅要保存数据,还要保 存索引字段

15.如何防止SQL注入?

1)JDBC使用PreparedStatement(不要使用Statement),因为PreparedStatement可以先预编译 sql语句(SQL语义确定),再进行赋值,这样不会改变sql语义。在用mybatis/mybatis-plus时,则尽 量使用#{param},不要用${param}
2)使用过滤器过滤用户输入的特殊符号( # > < where )
3)对请求参数字符串长度,格式进行限制。

16.二叉查找树(BST)、平衡二叉树(AVL)、红黑树(RBT)的区别?

认为hashmap之所以没有一开始就使用红黑树,应该是因为时间和空间的折中考虑吧,在哈希冲突比较小的时候,即使转化为红黑树之后,在时间复杂中上所产生的效果,其实也并不是特别大,而且呢,在put的时候效率可能会降低,毕竟每次put都会进行非常复杂的红黑树的这种旋转操作,另外在空间上的话,每个节点都需要去维护更多的一个指针,这就显得有点得不偿失了,最后就是HashMap之所以选择红黑树,而不是二叉搜索树,我认为最主要的原因就是二叉树在一些极端情况下,他会变成一个倾斜的结构,然后查找效率会退化成和链表差不多的效率,而红黑树它是一种平衡树,它可以防止这种退化,所以呢,可以保持这种平衡,因为红黑树又不像其它的完全的平衡二叉树那样有严格的平衡条件,所以呢,红黑树的插入效率要比完全的平衡二叉树要高,所以的话,hashmap在选择红黑树时,既可以避免级端情况下的退化,也可以兼顾查询和插入的这种效率

17.BTree、B+Tree的区别?

特点: B树叶子节点和非叶子节点都存数据(索引和数据都存储),且叶子节点数据无链指针; B+树只有叶子节点存数据(叶子存储数据,非叶子存储索引),且叶子节点数据有链指针。
B树的优势: 在B树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;
B树的缺点: 当进行范围查找时,存在“回旋查找”的问题(查找的时候需要在分支节点和叶子节点之间来回跃动),效率低
B+树的优势: 所有叶子节点形成有序链表,便于范围查询(不存在回旋查找的问题)
正是因为B+有这样的特点,所以MySQL的索引采用B+树存储!!! MySQL底层还对B+树进行优化,叶子节点采用双向链表,这样不管大于还是小于范围查询,都很快了!

18.为什么MySQL的索引采用B+树,而不是B树、Hash、二叉树、红黑树呢?

1)Hash哈希,只适合等值查询,不适合范围查询 id=5
2)一般二叉树,可能会特殊化为一个链表,相当于全表扫描
3)红黑树,是一种特化的平衡二叉树,MySQL 数据量很大的时候,索引的体积也会很大,内存放不下的而从 磁盘读取,树的层次太高的话,读取磁盘的次数就多了。 4)B树在范围查询时,存在回旋查找的问题,导致性能不高。B+树叶子节点是有序链表,更有利于范围查询。 (MySQL的索引对B+树的叶子节点进行改造,形成双向链表!!!)

19.MySQL表引擎有哪些?这些引擎有什么区别?MySQL的MyISAM和InnoDB的索引使用B+Tree实现有什么不同?

MySQL表引擎常用:
1)InnoDB
2)MyISAM
MySQL表引擎的区别:
1)InnoDB支持事务,InnoDB支持表级和行级锁,InnoDB是采用聚簇索引
2)MyISAM不支持事务,MyISAM只支持表级锁,不支持行级锁,MyISAM采用非聚簇索引
InnoDB采用聚集(聚簇)索引存储,索引和数据存储一个文件中,默认使用表的主键列值来构建B+树(表没 定义主键也会包含隐式主键) •.frm
MyISAM采用非聚集(非聚簇)索引存储,索引和数据文件是分两个文件存储的,可以没有主键索引 •.myi .myk

20.请问关于设计数据库你有什么心得?(必问)

1)关系数据库的三大范式
第一范式(1NF):字段不可分,否则就不是关系数据库
第二范式(2NF):有主键,非主键字段依赖主键;突出唯一性
第三范式(3NF):非主键字段不能相互依赖。每列都与主键有直接关系
2)冗余字段设计(解决一对多关系)
比如文章表故意冗余了作者名称字段,这样查询文章时直接显示作者名称,减少关联表查找作者表,提高查询效率。
3)一个字段存储多个ID值(用逗号隔开)(解决多对多的关系)减少表连接,提高查询效率
比如,自媒体或App文章的多个封面图片地址,设计一个封面字段images存储了所有封面地址,每个地址用逗号隔开。

21.mysql的执行顺序

-from 表名
ON 连接条件
JOIN 表名
where 查询条件
group by 分组字段
having 分组后条件
select distinct 查询字段
order by 排序条件
limit 查询起始位置, 查询条数

22.mysql回表

``回表``:由于在主键索引中,叶子节点保存了每一行的数据。而在普通索引中,叶子节点保存的是主键值,当我们使用普通索引去搜索数据的时候,先在叶子节点中找到主键,再拿着主键去主键索引中查找数据,相当于做了两次查找。

> 由于在主键索引中,叶子节点保存了每一行的数据。
> 而在普通索引中,叶子节点保存的是主键值,当我们使用普通索引去搜索数据的时候,先在叶子节点中找到主键,再拿着主键去主键索引中查找数据,相当于做了两次查找。 

innoDB两大类索引,聚集索引和普通索引;
聚集索引的叶子节点节点只存储的是行数据,
普通索引存储的是自己的字段还有主键
回表就是我们查询的时候条件是根据我们的普通索引进行查询,如果我们要展示其他字段的话,我们就
需要根据普通索引存储的id值去反查行记录。
.
举个栗子,不妨设有表:
t(id PK, name KEY, sex, flag);
画外音:id是聚集索引,name是普通索引。
表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B

在这里插入图片描述

两个B+树索引分别如上图:
(1)id为PK,聚集索引,叶子节点存储行记录;
(2)name为KEY,普通索引,叶子节点存储PK值,即id;
既然从普通索引无法直接定位行记录,那普通索引的查询过程是怎么样的呢?
通常情况下,需要扫码两遍索引树。
在这里插入图片描述

如粉红色路径,需要扫码两遍索引树
(1)先通过普通索引定位到主键值id=5;
(2)在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低

23. 主键索引和普通索引树的大小比较

因为主键索引(聚集索引)的叶子节点是数据,而
普通索引的叶子节点则是主键值,索引普通索引的索引树要小一些。

24.索引下推

索引下推(ICP):是从MySQL5.6开始引入的一个特性,通过减少回表的次数,来提高数据库的查询效率

我们的例子中是age!=99的时候直接去读取下一次数据,就不去回表了。

25.联合索引的右边字段是否有序

左边的字段是有序的

右边的字段是无序的

26.MySQL中为什么不推荐使用like %j,而推荐使用右联合j%

%j进行查找的话不好查找,我们只能去便利全表进行扫描,

而j%的话因为左的值是固定的故而不用进行全表扫描,效率更高。

我们称之为最左匹配原则

27.## 6.like关键字有没有办法用索引

like关键字并不是完全用不上索引,如果说左边的数字固定也是可以用上索引的,即j%

28.什么是隐性事务,什么导致隐性事务

隐性事务:当我们开启一个事务之后,需要commit或者rollback来结束一个事务,但是有时候,``一些操作会自动帮我们提交事务,而这就叫做隐性事务`

导致隐性事务:

1.所有的DDL: 当你在执行 DDL 语句前,事务就已经提交 了。这就意味着带有 DDL 语句的事务将来没有办法 rollback。

2.所有的DCL: 当然,除了 GRANT 和 REVOKE 之外,其他的创建、更新或者删除用户的操作也会导致事务隐式提交。

主要有:

  • REATE USER…
  • DROP USER…
  • ALTER USER…
  • SET PASSWORD…

3.新事务的开启:如果一个事务还没提交,你又开启了一个新的事务,那么此时前一个事务也会隐式提交。

4.各种锁操作: 给表上锁、解锁也会导致事务隐式提交

5.从机的操作: 我们在从机上执行的一些操作如 start slave 、 stop slave 、 reset slave 以及 change master to 等语句也会隐式提交事务。

6.其他表操作: 其他的一下操作如刷新权限(flush privileges)、优化表(optimize table)、修复表(repair table) 等操作,也会导致事务的隐式提交

切记,你在事务里只要写增删改查就行了

29.DDL , DML ,DQL ,DCL

DDL:表的创建,修改,删除,更新

DML:数据的增删改

DCL:增删改用户,增改用户的权限

DQL: 对数据进行查询

30. 前缀索引

前缀索引: 对文本的前几个字符建立索引,具体几个字符在建立索引的时候指定,这样建立起来的索引更小,查询速度更快。(MySQL的前缀索引在查询时是内部自动完成匹配的,并不需要使用Left函数)。

31.为什么不整个字段建立索引

为什么只对前几个字符建立索引,而不是整个字段建立索引:使用前缀索引,可能都是因为整个字段的数据量太大,没有必要针对整个字段建立索引。而前缀索引仅仅是选择一个字段的部分字符作为索引,这样一方面可以节约索引空间,另一方面则可以提高索引效率,但是会降低索引的选择性。

32.索引选择性

索引选择性:指不重复的索引值和数据表的记录总数的比值, 取值范围在 [0,1] 之间。索引的选择 性越高则查询效率越高,因为选择性高的索引可以让 MySQL 在查找时过滤掉更 多的行。

是不是选择性越高的索引越好呢?当然不是!索引选择性最 高为 1,如果索引选择性为 1,就是唯一索引了,搜索的时候就能直接通过搜索条 件定位到具体一行记录!这个时候虽然性能最好,但是也是最费空间的,这不符 合我们创建前缀索引的初衷。

33.S锁和X锁

S锁:共享锁,也称之为读锁,即Read Lock,S锁之间是共享的,或者说是互不阻塞的。当事务读取一条记录时,需要先获取该记录的S锁。

加了s锁的记录只能读不能改

X锁: 排他锁,也称之为写锁,即Write Lock,如同它的名字,x锁是具有排他性的,即一个写锁会阻塞其他的X锁和S锁。

当事务需要修改一条记录,需要先获取该记录的X锁。

34.快照读和当前读

快照读:SnapShot Read)是一种一致性不加锁的读,是 InnoDB 存储引擎并发 如此之高的核心原因之一

在可重复读的隔离级别下,事务启动的时候,就会针对当前库拍一个照片(快 照),快照读读取到的数据要么就是拍照时的数据,即事务开启那一瞬间数据库 中的数据,要么就是当前事务自身插入/修改过的数据。

我们日常所用的不加锁的查询,都属于快照读。

当前读:当前读是读取最新数据,而不是历史版本的数据,换言之,在可重复读隔离级别下,如果使用了当前读,也可以读到别的事务 已提交的数据。

35.MDL锁

MDL锁:全称为meta data lock, 中文叫元数据锁,是从MySQL5.5开始引入的锁,是为了解决DDL操作和DML操作之间操作一致性。从锁的作用范围上来说,MDL算是一种表级锁,是一个server层的锁。

即MDL中,读读共享,读写互斥,写写互斥。

36.物理日志和逻辑日志有什么区别

binlog是MySQL自己提供的,redo log 是存储引擎InnoDB提供的。

binlog是一种逻辑日志:

1.他里面所记录的是一条SQL语句的原始逻辑,例如给某一个字段+1

2.文件写满后,会自动切换到下一个日志文件继续写,而不会覆盖以前的日志。

3.配置 binlog 的时候,尽量指定binlog的有效期,这样使文件在到期后,日志文件会自动删除。

4.binlog是MySQL自己提供的

redo log的物理日志:

1.在某个数据页上做了什么修改。

  1. 循环写入的,即后面写入的可能 会覆盖前面写入的
  2. redo log 是存储引擎InnoDB提供的。

37.redo log 比写io的优势,为什么设计redo log

因为 Innodb 是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,不仅效率低,也浪费资源。

效率低是因为这些数据页在物理上并不连续,将数据页刷到磁盘会涉及到随机 IO。

写 redo log 与写磁盘 IO不同的是 redo log 是顺序 IO,而写数据涉及到随机 IO,写数据需要寻址,找到对应的位置,然后更新/添加/删除,而写 redo log 则是在一个固定的位置循环写入,是顺序 IO,所以速度要高于写数据。

而redo log 本身又分为:

  1. 日志缓冲(redo log buffer),该部分日志是易失性的
  2. 重做日志 (redo log file) ,这是磁盘上的日志文件,该部分日志是持久的。

MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer ,后续在某个时间点再一次性将多个操作记录写到 redo log file ,这种先写日志再写磁盘的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术(预写日志)。

38.什么是两阶段提交,为什么要进行两阶段提交

在这里插入图片描述

从上图中看出:最后提交事务的三个步骤:

  1. 写入redo log ,处于prepare状态
  2. 写binlog
  3. 修改redo log 状态变为commit

由于redo log 的提交分为prepare 和commit两个阶段,所以我们称之为两阶段提交。

为什么要两阶段提交:

先写binlog再写redolog:redolog中没有关于R的记录,所以恢复崩溃之后,插入R记录的这个事务是无效的。

先写redolog再写 binlog: binlog中还没有关于R的记录,所以当从机从主机同步数据的时候,或者我们使用binlog恢复数据的时候,就不会同步到R这条记录。

39. 两阶段提交如何保证数据一致性

情况一:一阶段提交之后崩溃了,即 写入 redo log,处于 prepare 状态 的时候崩溃了,此时:由于 binlog 还没写,redo log 处于 prepare 状态还没提交,所以崩溃恢复的时候,这个事务会回滚,此时 binlog 还没写,所以也不会传到备库。

情况二:假设写完 binlog 之后崩溃了,此时:
redolog 中的日志是不完整的,处于 prepare 状态,还没有提交,那么恢复的时候,首先检查 binlog 中的事务是否存在并且完整,如果存在且完整,则直接提交
事务,如果不存在或者不完整,则回滚事务。

情况三:假设 redolog 处于 commit 状态的时候崩溃了,那么重启后的处理方案同情况二。
由此可见,两阶段提交能够确保数据的一致性。

40. 为什么要小表驱动大表

小表驱动大表效率才高,大表驱动小表效率就会比较低 ,MySQL8.0对这个进行了优化差异并不明显。但是我们尽量使用大表驱动小表。

小表驱动大表,先通过in执行小表,再去大表查询

select * from employee e where e.departmentId in(select d.id from department d where d.name='技术部') limit 10;

大表驱动小表,通过exists先执行大表,再去小表查询

select * from employee e where exists(select 1 from department d where d.id=e.departmentId and d.name='技术部') limit 10;

41.你了解的索引

按照功能分,可以分四种:

普通索引

唯一性索引

主键索引

全文索引

按照存储方式分,可以分两种:

聚集索引

非聚集索 :二级索引/辅助索引

42.聚集索引和非聚集索引

聚集索引:在存储的时候,可以按照主键(不是必须,看情况)来排序存储数据,B+Tree 的叶子结点就是完整的数据行,查找的时候,找到了主键也就找到了完整的数据行。

非聚集索引:数据库会有单独的存储空间来存放,涉及到回表的操作。

43.索引的设计原则

在这里插入图片描述

  1. SQL优化大全
1、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2、对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
3、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫
描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0 4、尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,
如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10 union all select id from t where num=20 5、下面的查询也将导致全表扫描:(不能前置百分号) select id from t where name like ‘%c%’
若要提高效率,可以考虑全文检索。
6、in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3 7、如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但
优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访
问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num 8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。
如:
select id from t where num/2=100
应改为: select id from t where num=100*2 9、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。
如:
select id from t where substring(name,1,3)=’abc’–name以abc开头的id select id from t where datediff(day,createdate,’2005-11-30ʹ)=0–’2005-11-30ʹ生成的 id
应改为: select id from t where name like ‘abc%’ select id from t where createdate>=’2005-11-30ʹ and createdate<’2005-12-1ʹ
10、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确
使用索引。
11、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为
条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相
一致。
12、不要写一些没有意义的查询,如需要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)
13、很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14、并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复
时,SQL查询可能不会去利用索引,如一表中有字段 sex,male、female几乎各一半,那么即使在sex
上建了索引也对查询效率起不了作用。
15、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update
的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而
定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。
16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存
储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁
更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
17、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性
能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数
字型而言只需要比较一次就够了。
18、尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储
空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19、任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索
引)。
21、避免频繁创建和删除临时表,以减少系统表资源的消耗。
22、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或
常用表中的某个数据集时。但是,对于一次性事件,最好使 用导出表。
23、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免
造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后
insert。
24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后
drop table ,这样可以避免系统表的较长时间锁定。
25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改
写。
26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通
常更有效。
27、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐
行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比
使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一
种方法的效果更好。
28、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT
OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
29、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
30、尽量避免大事务操作,提高系统并发能力。

分布式事务

1.ACID

A 原子性 在一个事务的所有操作,要么全部成功,要么全部失败
C 一致性 事务执行的前后,数据是保持一致性,例如转账例子
I 隔离性 多个并发的事务应该要相互隔离
如果隔离行不好,则产生
脏读 一个事务读取到另一个事务未提交的数据
不可重复读 一个事务读到到另一个事务的已经提交更新数据
幻读 一个事务多次读取结果(新增的已提交的数据)不一致
为了防止这些现象,修改数据库的隔离级别
脏读 不可重复读 幻读
read uncommited(读未提交) 有 有 有
read commmited(读已提交) 无 有 有 (oracle默认隔离级别)
repeatable read(可重复读) 无 无 有 (mysql默认隔离级别)
Serializable ( 串行化) 无 无 无

D 持久性 事务一旦提交,则永久保存,即使故障也不丢失

2脏读,幻读,不可重复读概念

在这里插入图片描述

3.过CAP定理和BASE理论

CAP定理,C是指一致性,A指可用性,P指容错性。Consistency Availability Partition tolerance它在说P必然存在的,C和A只能选择一个特性。在CAP定理,只存在CP或AP。

BASE理论,是对CAP一种补充:
BA指基本可用,S代表软状态,E代表最终一致性
CAP定理说如果选择了一致性,就放弃了可用性,但BASE理论说选择了一致性,只是损失了部分可用,意味处于基本可用。
CAP定理说如果选择了可用性,就放弃了一致性,但BASE理论说选择了可用行,只是存在临时的不一致状态,这种临时的不一致性称为软状态,这种软状态过后最终会达成一致性

4.项目中有用到分布式事务么?什么技术?Seata有哪些模式?你们用的哪个模式?

有用到。用到springcloudAlibaba的Seata,Seata有四种模式,分别为XA模式,AT模式,TCC模式,SAGA模式。
我的项目中使用到AT模式/TCC模式。

5.请问Seata的AT模式是AP还是CP?那大概解释一下Seata的AT模式的原理

AT模式是一种AP模式(强可用,弱一致性)。
Seata的AT模式的执行流程大概就这样:
Seata架构中存在三大组件,TC(TC是事务协调者),TM(事务管理器)和RM(资源管理器)。
1.首先,由TM向TC发出开始全局事务的请求,TC在全局事务表中记录数据。
2.接着,由TM通知各个的RM调度各自的分支事务,这时分支事务开始执行啦。分支事务先向TC进行注 册分支事务,开始执行SQL语句并提交,在SQL执行的前后,AT模式会把更新记录的前后数据保存到undo_log日志表中作为数据快照,再上报事务执行结果给TC。
3.最后,TC收集到所有分支事务的执行状态,进行分析,决定是否提交还是回滚,如果提交,则TC向所有RM发出删除undo_log日志记录的请求。如果回滚,则TC向所有RM发出读取undo_log数据快照做数据恢复的请求。
4.在AT的执行过程中,我了解到会有脏读的情况存在,Seata考虑到了,利用全局事务锁表,在每个分支事务提交之前,判断是否能获取全局事务锁,决定是否提交,这样就控制脏写。

在这里插入图片描述

6.大概解析一下Seata的TCC模式的原理

Seata的TCC模式的执行流程大概就这样:
Seata架构中存在三大组件,TC(TC是事务协调者),TM(事务管理器)和RM(资源管理器)。
其实TCC模式,就是我们在一个业务编写三个方法,成为try方法,confirm方法,cancel方法。
1.首先,由TM向TC发出开始全局事务的请求,TC在全局事务表中记录数据。
2.接着,由TM通知各个的RM调度各自的分支事务,这时分支事务开始执行啦。分支事务先执行try方法,进行资源预留(如冻结金额),然后向TC提交事务状态。
3.最后,TC收集到所有分支事务的执行状态,进行分析,决定是否提交还是回滚,如果提交,则调用confirm方法(把预留资源清除),如果回滚,则调用cancel方法(利用预留资源恢复原有的数据)。

7.Seata的AT、TCC、XA模式的区别:

AT 模式

  1. 两阶段提交。
  2. 二阶段提交还是回滚,是全自动化的,开发者不需要任何额外的工作。
  3. 代码零侵入。
  4. 实现的原理,有一个 undo_log 表。
  5. 所谓的回滚,实际上是一个反向补偿。
    .
    TCC模式 Try-Confirm-Cancel
  6. 业务侵入。
  7. 不需要 undo log 表。
  8. 回滚也是反向补偿。
    4. 提交、回滚都是开发者自己实现。
    .
    XA模式
    XA 规范,数据库支持分布式事务的规范,目前像 MySQL、Oracle、SQL Server 等,都是支持 XA 规范 的。
    1. XA 中的回滚,是真正的回滚,而不是反向补偿。
    2. XA对资源锁定的时间过长,导致并发效率很低。
    3. XA 是一种强一致性的分布式事务解决方案,
    而 AT 和 TCC 都属于最终一致性

7.Seata的TCC模式的原理?

Seata的TCC模式的执行流程大概就这样:
Seata架构中存在三大组件,TC(TC是事务协调者),TM(事务管理器)和RM(资源管理器)。其实TCC模式,就是我们在一个业务编写三个方法,成为try方法,confirm方法,cancel方法。
首先,由TM向TC发出开始全局事务的请求,TC在全局事务表中记录数据。
接着,由TM通知各个的RM调度各自的分支事务,这时分支事务开始执行啦。分支事务先执行try方法,进行资源预留(如冻结金额),然后向TC提交事务状态。
最后,TC收集到所有分支事务的执行状态,进行分析,决定是否提交还是回滚,如果提交,则调用confirm方法(把预留资源清除),如果回滚,则调用cancel方法(利用预留资源恢复原有的数据)。

8. 事务失效的场景

在这里插入图片描述

9.事务的隔离级别

SERIALIZABLE 序列化
REPEATABLE READ 重复读取
READ COMMITTED 提交读
READ UNCOMMITTED 未提交读


3.1 SERIALIZABLE 序列化
事务不可以并发执行
一次只能执行一个事务
隔离级别最高,所以它不存在任何问题
但是因为它的效率比较低,所以一般不用它


3.2 REPEATABLE READ 重复读取
MySQL 数据库中的默认事务隔离级别
理解:指在同一个事务内,多次读取同一个数据
事务不会被看成一个序列,但是正在执行事务的变化仍然不能被外部看到。
例子:如果用户在当前事务对数据进行了更改,然后执行 SELECT 语句(显然此时查询的是更改之后的数据),同时在另一个事务中执行了同一条 SELECT 语句多次,结果总是与更改之前的数据是相同的
这个隔离级别中解决了事务的不可重复读
幻读问题没有解决


3.3 READ COMMITTED 提交读
可以看到其他事务对数据的修改
即,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT 语句可能返回不同的结果
存在两种问题:不可重复读和幻读


3.4 READ UNCOMMITTED 未提交读
事务之间最小限度的隔离,在这种隔离级别中,可以读到别的事务未提交的数据
存在三种问题:脏读、不可重复读、幻读

10.事务没有开启的原因

在这里插入图片描述

Mybatis相关

1.Mybatis的特有注解

增删改查:@Insert、@Update、@Delete、@Select、@MapKey、@Options、@SelelctKey、
@Param、@InsertProvider、@UpdateProvider、@DeleteProvider、@SelectProvider
结果集映射:@Results、@Result、@ResultMap、@ResultType、@ConstructorArgs、@Arg、
@One、@Many、@TypeDiscriminator、@Case
缓存:@CacheNamespace、@Property、@CacheNamespaceRef、@Flush

2.Mybatis的原理

3.MyBatis # 和 $ 区别

  1. #{}占位符,KaTeX parse error: Expected 'EOF', got '#' at position 7: {}拼接符,#̲{} 解析为一个 JDBC 预…{}仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。

  2. #{} 解析之后会将String类型的数据自动加上引号,其他数据类型不会;而${} 解析之后是什么就是什么,他不会当做字符串处理。

  3. #{} 很大程度上可以防止SQL注入(SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作);而${} 主要用于SQL拼接的时候,有很大的SQL注入隐患。

4.在某些特殊场合下只能用${},不能用#{}。例如:在使用排序时ORDER BY ${id},如果使用#{id},则会被解析成ORDER BY “id”,这显然是一种错误的写法。

4.Mybatis的基本工作原理就是

封装sql语句,然后使用JDBC操作数据库并返回一个JAVA类,SqlSessionFactoryBuilder—

SqlSessionFactory–>Session–>Mapper–>sql语句–>返回java类

Spring相关

1.Spring特有注解

声明bean的注解
@Component 组件,没有明确的角色
@Service 在业务逻辑层使用(service层)
@Repository 在数据访问层使用(dao层)
@Controller 在展现层使用,控制器的声明(C)

注入bean的注解
@Autowired 由Spring提供
@Resource 由JSR-250提供

java配置类相关注解
@Bean 注解在方法上,声明当前方法的返回值为一个bean,替代xml中的方式(方法上)
@Configuration 声明当前类为配置类,其中内部组合了@Component注解,表明这个类是一个
bean(类上)
@ComponentScan 用于对Component进行扫描,相当于xml中的(类上)

切面(AOP)相关注解
@Aspect声明一个切面(类上) 使用@After、@Before、@Around定义建言(advice),可直接将拦
截规则(切点)作为参数。
@After 在方法执行之后执行(方法上) @Before 在方法执行之前执行(方法上) @Around 在方法
执行之前与之后执行(方法上)
@PointCut 声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的
支持(类上)

@Value注解
@Value 为属性注入值 注入操作系统属性@Value(“#{systemProperties[‘os.name’]}”)String osName;
注入表达式结果@Value(“#{ T(java.lang.Math).random() * 100 }”) String randomNumber;
注入其它bean属性@Value(“#{domeClass.name}”)String name;
注入文件资源@Value(“classpath:com/hgs/hello/test.txt”)String Resource file;
注入网站资源@Value(“http://www.cznovel.com”)Resource url;
注入配置文件Value(“${book.name}”)String bookName;

定时任务相关
@EnableScheduling 在配置类上使用,开启计划任务的支持(类上)
@Scheduled 来申明这是一个任务,包括cron,fixDelay,fixRate等类型(方法上,需先开启计划任务的
支持)

2.Spring的Ioc与DI是什么,原理是什么

IOC全名(Inversion of control ) , 控制反转就是把创建和管理 bean 的过程转移给了第三方。 IOC底层
原理:xml解析、工厂模式、反射。

OC就是控制反转,以前是我们自己去new新建对象,现在是spring容器帮我们新建对象,就是把创建
对象的权力转移给spring容器,我们要用的时候直接从spring容器里面那就好了;简单理解就是利用
Spring的Bean工厂为我们生产Bean。(控制反转)
DI就是依赖注入,是实现ioc这种思想,它是把我们需要用到的对象注入到Spring的Bean工厂中

3.Spring的AOP是什么,什么是切点切面,AOP在项目中的实际应用(结合自己的项目讲),AOP底层原理:

什么是aop

aop就是面向切面编程。 在运行期间会为目标对象生成一个代理对象 ,然后在这个代理类中增强被代理者的功能,统一增添某种功能。 在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待

AOP是面向切面编程。用于对目标类方法的业务进行增强,如日志增强或事务增强等等。
AOP底层有两种方式实现,JDK 动态代理和 CGLIB 动态代理。
目标类有接口,则执行JDK动态代理(接口代理)。
目标类没有接口,则执行Cglib动态代理(类代理)。

什么是切点:

定义:如果通知定义了“什么”和“何时”。那么切点就定义了“何处”。切点会匹配通知所要织入的一个或者
多个连接点。
通常使用明确的类或者方法来指定这些切点。
作用:定义通知被应用的位置(在哪些连接点)

什么是通知:

定义:切面也需要完成工作。在 AOP 术语中,切面的工作被称为通知。
工作内容:通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决何时执行这
个工作。
Spring 切面可应用的 5 种通知类型:
Before——在方法调用之前调用通知
After——在方法完成之后调用通知,无论方法执行成功与否
After-returning——在方法执行成功之后调用通知
After-throwing——在方法抛出异常后进行通知
Around——通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

什么是切面(切点+通知):

定义:切面是通知和切点的集合,通知和切点共同定义了切面的全部功能——它是什么,在何时何处完
成其功能。就是我们的切面。

AOP的底层实现原理

Spring AOP动态代理机制:
Spring在运行期间会为目标对象生成一个代理对象,并在代理对象中实现对目标对象的增强。
SpringAOP通过两种动态代理机制,实现对目标对象执行横向植入的。
代理技术 描述
JDK 动态代理 Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的java.lang.reflect.Proxy 类进行代理。
CGLIB 动态代理 若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。

aop在项目中的应用:

1.结合@EnabledAspectJAutoProxy(是Spring AOP开启的标志,在启动类标记此注解,即可加载对应的切面类逻辑 )和@Apsect注解通过注解标记需要解决幂等性接口,然后使用aop拦截注解,再结合
redis和token从而解决幂等性问题。
2.Spring的事务利用的底层原理就是AOP 。
3.除此之外我还利用了Aop+Redis(UUID)+Token拦截注解实现了对接口的幂等性处理,比如访问使用rabbitMQ生成订单的请求幂等性处理,

4.spring事务的传播性?事务的底层原理,是什么情况下事务会失效?怎么解决?

1.事务的原理

事务的操作本来应该由数据库进行控制,但是为了方便用户进行业务逻辑的控制,spring对事务功能进行了 扩展实现。一般我们很少使用编程式事务,更多的是使用@Transactional注解实现。当使用了 @Transactional注解后事务的自动功能就会关闭,由spring帮助实现事务的控制。Spring的事务管理是 通过AOP代理实现的,对被代理对象的每个方法进行拦截,在方法执行前启动事务,在方法执行完成后根据是 否有异常及异常的类型进行提交或回滚。

2.事务的实现方式:

spring框架提供了两种事务实现方式:编程式事务、声明式事务 编程式事务:在代码中进行事务控制。优点:精度高。缺点:代码耦合度高 声明式事务:通过@Transactional注解实现事务控制

3.事务的传播性:

事物的失效情况:
Spring它对JDBC的隔离级别作出了补充和扩展,其提供了7种事务传播行为。 1、PROPAGATION_REQUIRED:默认事务类型,如果没有,就新建一个事务;如果有,就加入当前事务。适 合绝大多数情况。
2、PROPAGATION_REQUIRES_NEW:如果没有,就新建一个事务;如果有,就将当前事务挂起。
3、PROPAGATION_NESTED:如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。
4、PROPAGATION_SUPPORTS:如果没有,就以非事务方式执行;如果有,就使用当前事务。
5、PROPAGATION_NOT_SUPPORTED:如果没有,就以非事务方式执行;如果有,就将当前事务挂起。即无 论如何不支持事务。
6、PROPAGATION_NEVER:如果没有,就以非事务方式执行;如果有,就抛出异常
。 7、PROPAGATION_MANDATORY:如果没有,就抛出异常;如果有,就使用当前事务。

4.事务的隔离等级(加):

Isolation Level(事务隔离等级):

  1. SERIALIZABLE 序列化:最严格的级别,事务串行执行,资源消耗最大;
    2.REPEATABLE READ 重复读取:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免 了“脏读取”和“不可复读取”的情况,但是带来了更多的性能损失。
    3.READ COMMITTED 提交读:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改 但未提交的数据,免了“脏读取”。该级别适用于大多数系统。
    4.READ UNCOMMITTED 未提交读:保证了读取过程中不会读取到非法数据。

5.事物的失效情况:

1.访问权限问题 方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public 的。说白了,在AbstractFallbackTransactionAttributeSource类的 computeTransactionAttribute方法中有个判断,如果目标方法不是public,则 TransactionAttribute返回null,即不支持事务。
2. 方法用final修饰 如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
3. 方法用static修饰 如果某个方法是static的,同样无法通过动态代理,变成事务方法。
4.方法内部调用 方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以 updateStatus方法不会生成事务。由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
5.未被spring管理 在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需 要创建bean实例。通常情况下,我们通过@Controller、@Service、@Component、@Repository等注 解,可以自动实现bean实例化和依赖注入的功能。类没有加@Service注解,那么该类不会交给spring管 理,所以它的方法也不会生成事务。
6.多线程调用 如果事务方法A中,调用了事务方法B,但是事务方法B是在子线程中调用的。 这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想B方法 中抛了异常,A方法也回滚是不可能的。如果看过spring事务源码的朋友,可能会知道spring的事务是通过 数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。我们说的同一个事 务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到 的数据库连接肯定是不一样的,所以是不同的事务。
7.表不支持事务 众所周知,在mysql5之前,默认的数据库引擎是myisam。有个很致命的问题是:不支持事务。如果只是单表 操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整 的情况。此外,myisam还不支持行锁和外键。所以在实际业务场景中,myisam使用的并不多。在mysql5以 后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。有时候我们在开发的过程中,发现某张表的 事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务。
8.未开启事务 有时候,事务没有生效的根本原因是没有开启事务。你看到这句话可能会觉得好笑。开启事务不是一个项目 中,最最最基本的功能吗?为什么还会没有开启事务?没错,如果项目已经搭建好了,事务功能肯定是有的。但 如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?如果 你使用的是springboot项目,那么你很幸运。因为springboot已经默默的帮你开启了事务。但如果你使用 的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘 了配置,事务肯定是不会生效的。
9.错误的传播特性 如果如果我们在手动设置propagation参数的时候,把传播特性设置错了,事务方法的事务传播特性定义成 了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
10.自己吞了异常 事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常。这种情况下spring事务当然不会 回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。如果想要spring事务能 够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
11.手动抛了别的异常即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。开发人员自己捕获了异 常,又手动抛出了异常:Exception,事务同样不会回滚。因为spring事务,默认情况下只会回滚 RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回 滚。
12.自定义了回滚异常 在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设 置rollbackFor参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例 如:保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而 BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回 滚。即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。这是为什么 呢?因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一 般情况下,将该参数设置成:Exception或Throwable。

5.Spring中Bean的生命周期:

所谓Bean的生命周期,就是一个Bean从创建到销毁,所经历的各种方法调用。 简单的来说,一个Bean的生 命周期分为四个阶段:
1、实例化(Instantiation)
2、属性设置(populate)
3、初始化(Initialization)
4、销毁(Destruction)

6.Spring Bean的生命周期?(必会)—背诵篇

1)调用Bean的构建函数(创建对象,底层反射创建对象)(控制反转 IOC) 2)给Bean的成员变量赋值(依赖注入 DI) 3)执行一些Aware接口,这些接口是可选的,例如 BeanNameWare BeanFactoryAware
ApplicationContxtAware 4)调用InitializingBean接口的afterProertiesSet方法(可选)
5)调用Bean的init方法(前提是声明该方法)(可选)
@PostConstruct注解声明init方法
6)调用Bean的destroy方法(前提是声明该方法)(可选)(通常不会执行,IOC容器主动销毁
applicationContext.close())
@PreDestroy注解声明destroy方法

7 哪些情况会Spring事务失效(必会)?

1)注解@Transactional配置的方法非public权限修饰
2)注解@Transactional所在类非Spring容器管理的bean
3)注解@Transactional所在类中,注解修饰的方法被本类的其他内部方法调用(建议说)
4)业务代码抛出异常类型非RuntimeException,事务失效 FileNotFoundException()
5)业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new
RuntimeExecption异常
6)注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(很少见)
事务传播行为 共7个,只要知道常用的这4个即可
@Transactional(propagation = Propagation.REQUIRED) 默认值,当前方法必须处在事务中。前
面的业务方法有事务,同共享前面的事务;如果前面没有事务,则开启新事务
@Transactional(propagation = Propagation.SUPPORTS) 前面业务方法有事务,同共享前面的事
务,如果前面没有事务,则不开事务,通常在查询业务中
@Transactional(propagation = Propagation.REQUIRES_NEW) 不管前面业务是否有事务,都要独
立创建新事务。
@Transactional(propagation = Propagation.NOT_SUPPORTED) 不支持事务
7)mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(很少见)MyISAM不支持事务
参考文章

8. spring的事务的实现方式有哪些

声明式事务

编程式事务

10.没状态的Bean是怎么保证Bean的线程安全的

在这里插入图片描述

SpringMVC相关

1.SpringMVC的特有注解

@EnableWebMvc 在配置类中开启Web MVC的配置支持,如一些ViewResolver或者
MessageConverter等,若无此句,重写WebMvcConfigurerAdapter方法(用于对SpringMVC的配
置)。
@Controller 声明该类为SpringMVC中的Controller
@RequestMapping 用于映射Web请求,包括访问路径和参数(类或方法上)
@ResponseBody 支持将返回值放在response内,而不是一个页面,通常用户返回json数据(返回值
旁或方法上)
@RequestBody 允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前)
@PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放
在参数中前,即可获取该值,通常作为Restful的接口实现方法。
@RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类
上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
@ControllerAdvice 通过该注解,我们可以将对于控制器的全局配置放置在同一个位置,注解了
@Controller的类的方法可使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上,
这对所有注解了 @RequestMapping的控制器内的方法有效。
@ExceptionHandler 用于全局处理控制器里的异常
@InitBinder 用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。
@ModelAttribute 本来的作用是绑定键值对到Model里,在@ControllerAdvice中是让全局的
@RequestMapping都能获得在此处设置的键值对。

2. SpringMVC工作流程

用户发送请求至前端控制器DispatcherServlet;
DispatcherServlet收到请求调用HandlerMapping处理器映射器; 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;
DispatcherServlet通过HandlerAdapter处理器适配器调用处理器; 执行处理器(Controller,也叫后端控制器); Controller执行完成返回ModelAndView; HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet;
DispatcherServlet将ModelAndView传给ViewReslover视图解析器; ViewReslover解析后返回具体View;
DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中); DispatcherServlet响应用户。

3.请说说Spring是如何解决循环依赖问题的?

属性注入的循环依赖:
@Component
public class TestService1{
@Autowired
private TestSerivice2 service2;
}
@Component
public class TestService2{
@Autowired
private TestSerivice1 service1;
}
spring内部有三级缓存(三个Map结构):
1)singletonObjects 一级缓存,用于存放完全初始化好的 bean(最终要被用户使用)
2)earlySingletonObjects 二级缓存,存放原始的 bean 对象(尚未填充属性),避免Bean实例化完成
但未初始。
3)singletonFactories 三级缓存,存放 bean 工厂对象(ObjectFactory),用于生成AOP代理对象
(@Transactional或@Async)。
用户从SpringIOC容器获取一个Bean的底层,会先从一级缓存获取Bean;一级缓存拿不到,从二级缓存获取;二级缓存获取不到从三级缓存获取。

SpringBoot相关

1.SpringBoot自动化原理

自动装配呢,简单来说就是自动把第三方组件的bean装载到springIOC容器里面,不需要开发人员再 去写bean的装配配置。在springboot应用里面,只需要在启动类上加@springbootApplication注解就 可以实现自动装配。 @SpringbootApplication是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration 自动装配的实现主要依赖三个核心技术

  1. 引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配 置类,在这个 配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。
  2. 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个 配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。 这样 SpringBoot 就可 以知道第三方 jar 包里面的配置类的位置,这个步骤主要是 用到了 Spring 里面 SpringFactoriesLoader来完成的。
  3. SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口,实现对这些配置类的动态加载。 以上,就是我对 Spring Boot 自动装配机制的理解。 (这个spring.factories⽂件也是⼀组⼀组的key=value的形式,其中⼀个key是 EnableAutoConfiguration类的全类名,⽽它的value是⼀个xxxxAutoConfiguration的类名的列 表),然后将所有⾃动配置类加载到Spring容器中

2 SpringBoot自定义start

1.创建一个 Maven 或 Gradle 项目作为您的自定义 starter 项目。
2.在项目中定义一个自动配置类,用于配置自定义的 bean、组件或其他功能。这个类通常使用 @Configuration 和 @ConditionalOnClass 等注解来进行条件化配置。
3.创建一个 starter 模块,将自动配置类打包成一个 jar 包,并提供一个标准的 Maven 或 Gradle 依赖描述文件。
4.创建一个 starter 的 starters 模块,这个模块主要是用来打包整个 starter 项目,提供给其他项目使用。
5.在 starter 模块中提供必要的自定义配置,如 application.properties 或 application.yml 文件,以便用户可以在应用中配置相关参数。
6.编写文档和示例,以便其他开发者可以快速使用您的 starter。

2.在SpringBoot项目中如何让一个Bean放到IOC容器?

1)直接在Bean上面加@Component,让该Bean的包名在项目启动类同级或子级包下。
2)直接在Bean上面加@Component,在项目下写@Configuration配置类,通过@ComponentScan注
解自行扫描Bean的目录
3)在项目下写@Configuration配置类(放在启动类同级或子级包下),直接写一个方法创建Bean对
象,方法上添加@Bean注解
4)在项目下写@Configuration配置类(不放在启动类同级或子级下),在resources/METAINF/spring.factories文件 把配置类添加到SpringBoot自动装配列表中,让其加载,在配置类中写一个
方法创建Bean对象,方法上添加@Bean注解
5)直接在启动类写一个方法创建bean对象,方法上面添加@Bean注解

3.@Configuration 和@Component的区别

@Configuration 中所有带 @Bean 注解的方法都会被动态代理(cglib),因此调用该方法返回的都是同一个实例。而 @Conponent 修饰的类不会被代理,每实例化一次就会创建一个新的对象。

4.请问SpringBoot核心注解有哪些?

关键点:根据SpringBoot自动装配的思路来答
@SpringBootApplication: SpringBoot启动类入口标记注解
@EnableAutoConfiguration: 开启自动装配
@AutoConfigurationPackage: 自动导入启动器依赖包
@ConditionalOnClass: 让@Configuration在有条件的情况下生效
@ComponentScan: 设置SpringBoot环境扫描包的路径

4.SpringBoot的自动装配原理?(必问–方便背诵篇)

1)在启动类启动时加载@SpringBootApplication注解
2)在@SpringBootApplication注解里面包含三个注解:@ComponentScan,@Configuration,
@EnableAutoConfiguration
3)@Configuration表明启动类是一个配置类
4)@ComponentScan自动扫描启动类所在目录及子目录在Spring组件,让其实例化
5)@EnableAutoConfiguration注解里面包含AutoConfigurationImportSelector配置类
6)在AutoConfigurationImportSelecto配置类中会读取springboot自动配置包中的META-INF的
spring.factories文件
7)该spring.factories文件包含一两百个SpringBoot写好的自动配置类
8)这些自动配置并不是默认生效的,而是根据环境中导入starter启动器依赖及自动配置类上
@ConditionalOnClass注解来决定该配置类是否生效。
9)一旦自动配置类生效了,里面@Bean注解会把创建实例放入IOC容器
10)我们在项目中就可以随时使用@Autowired进行注入并使用

5. 2.0之后是jdk动态代理还是CGlib动态代理

可以看到,从 Spring Boot2.0 开始,如果用户什么都没有配置,那么默认情况下使用的是 Cglib 代理。

通过 @ConditionalOnProperty这个注解发现的。

SpringSecurity系列

1.SpringSecurity的登录流程:

在这里插入图片描述
认证成功之后确保用户的权限以及登录状态:

*1. 当用户登录成功的时候,会将当前用户登录的信息(上面认证流程第九步的Authencation信息,为了 安全可以将密码信息去掉)存入到 SecurityContextHolder 中,SecurityContextHolder 底层是一 个 ThreadLocal,当前用户信息被存入到 ThreadLocal 中。(接上文的第十步) *2. 在登录请求返回数据的时候(认证成功,登录请求准备回去了),返回的时候会经过过滤器链里面的 SecurityContextPersistenceFilter 过滤器,在该过滤器中,会拿出来 SecurityContextHolder 中的用户信息,然后将之存入到 HttpSession 中,同时清除掉 SecurityContextHolder 中的用户信 息。*3. 接下来,当用户访问 /hello 的时候,一样也会经过 SecurityContextPersistenceFilter,在 该过滤器中,会从 HttpSession 中读取出来当前用户数据,并存入到 SecurityContextHolder 中, 在接下来的各种认证和权限的判断中,都会从 SecurityContextHolder 中获取。 *

2.Spring Security与Shiro的区别:

相同点 1、认证功能
2、授权功能
3、加密功能
4、会话管理
5、缓存支持
6、rememberMe功能

不同点:
1、Spring Security 基于Spring 开发,项目若使用 Spring 作为基础,配合 Spring Security 做权限更加方便,而 Shiro 需要和 Spring 进行整合开发;
2、Spring Security 功能比 Shiro 更加丰富些,例如安全维护方面;
3、Spring Security 社区资源相对比 Shiro 更加丰富;
4、Shiro 的配置和使用比较简单,Spring Security 上手复杂些;
5、Shiro 依赖性低,不需要任何框架和容器,可以独立运行.Spring Security 依赖Spring容器;
6、shiro 不仅仅可以使用在web中,它可以工作在任何应用环境中。在集群会话时Shiro最重要的一个好处 或许就是它的会话是独立于容器的。

3.美团的leaf-snowflake加密id

Leaf-snowflake 基本上就是沿用了snowflake的设计,
ID组成结构: 正数位 (占1比特)+ 时间戳 (占41比特)+ 机器ID (占5比特)+ 机房ID (占5比
特)+ 自增值 (占12比特),总共64比特组成的一个Long类型。
Leaf-snowflake 不同于原始snowflake算法地方,主要是在workId的生成上, Leaf-snowflake
依靠 Zookeeper 生成 workId ,也就是上边的 机器ID (占5比特)+ 机房ID (占5比特)。 Leaf 中workId是基于ZooKeeper的 顺序Id 来生成的,每个应用在使用Leaf-snowflake时,启动时都会
都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
Leaf-snowflake 启动服务的过程大致如下:
启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过
(是否有该顺序子节点)。
如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的
workerID号,启动服务。
但 Leaf-snowflake 对Zookeeper是一种弱依赖关系,除了每次会去ZK拿数据以外,也会在本机文件系
统上缓存一个 workerID 文件。一旦ZooKeeper出现问题,恰好机器出现故障需重启时,依然能够保证
服务正常启动。
启动 Leaf-snowflake 模式也比较简单,起动本地ZooKeeper,修改一下项目中的 leaf.properties
文件,关闭 leaf.segment模式 ,启用 leaf.snowflake 模式即可。
leaf-snowflake优点:
ID号码是趋势递增且唯一的8byte的64位数字,满足上述数据库存储的主键要求。

Shiro系列

Redis系列

为什么要做缓存?

数据库响应太慢了,所以我们要做缓存,把数据都缓存到redis中去,你需要的时候我就去redis中去查看,redis中要是有我就去给你返回,redis中要是没有,我就去数据库里面去查询

1.Redis在项目中的具体应用(结合自己的项目讲)

1.使用Redis做分布式锁,在高并发下防止商品、订单、使得这些模块的请求数据库在同一时刻只有一个线程能够操作。
2.使用redis做缓存,缓存商品页面的文字信息、以及同事写购物车好像也是使用redis做缓存。
3.使用redis+aop+token做接口的幂等性处理,

2.redis的数据类型,持久化方式

String,list,Hashs,Zset,set

1)RDB 和 AOF
2) RDB原理是对整个当前内存数据进行快照备份,体积小。AOF原理是每条操作指令都会持久化到文
件,导致文件体积比较大
RDB的两次备份时间间隔最短1分钟,时间长,容易导致数据丢失。而AOF默认间隔1秒1次,时间
短,数据完整性高!
恢复速度上来说,RDB比AOF稍快一些,因为体积小。
3)实际项目我们公司有运维团队维护Redis服务器的配置,我暂时没去修改过,但是我了解到通常为了
保证数据完整更高,同时
开启RDB和AOF,Redis在启动时也会优先加载完整性更高的AOF ,这时RDB备份更多的为了在AOF文
件损坏的情况下使用

3Redis集群实现

Redis集群通过分布式存储的方式解决了单Redis实例的海量数据存储的问题,对于分布式(多Redis) 存储,需要考虑的重点就是如何将数据进行拆分到不同的Redis服务器上 Redis集群采用的算法是哈希槽分区算法。Redis集群中有16384个哈希槽(槽的范围是 0 -16383, 哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分的 哈希槽。在对数据进行操作的时候,集群会对使用CRC16算法对key进行计算并对16384取模(slot = CRC16(key)%16383),得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的 Redis节点,然后直接到这个对应的节点上进行存取操作。 使用哈希槽的好处就在于可以方便的添加或者移除节点,并且无论是添加删除或者修改某一个节点,都不 会造成集群不可用的状态。当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;当需要 移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了

4.Redis集群中主从搭建

主从模式中,Redis部署了多台机器,有主节点,负责写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制。主从复制包括全量复制,增量复制两种。一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制,
全量复制流程如下:
在这里插入图片描述

  1. slave发送sync命令到master。
  2. master接收到sync命令后,执行bgsave命令,生成RDB全量文件。
  3. master使用缓冲区,记录RDB快照生成期间的所有写命令。
  4. master执行完bgsave后,向所有slave发送RDB快照文件。
  5. slave收到RDB快照文件后,载入、解析收到的快照。
  6. master使用缓冲区,记录RDB同步期间生成的所有写的命令。
  7. master快照发送完毕后,开始向slave发送缓冲区中的写命令。
  8. salve接受命令请求,并执行来自master缓冲区的写命令。 redis2.8版本之后,已经使用psync来替代sync,因为sync命令非常消耗系统资源,psync的效率更高

slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发增量复制。

当master节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 Master节点 上调用的每一个命令会使用replicationFeedSlaves()来同步到Slave节点。
执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且 slave节点不为空,就会执行此函数。
这个函数作用就是:
把用户执行的命令发送到所有的slave节点,让slave节点执行。 流程如下:
在这里插入图片描述

5.Redis集群中哨兵的选举机制(什么是哨兵机制?哨兵机制的工作原理是什么?)

1.由哨兵节点定期监控发现主节点是否出现了故障,每个哨兵节点每隔1秒会向主节点、从节点及其它哨兵节点 发送一次ping命令做一次心跳检测。如果主节点在一定时间范围内不回复或者是回复一个错误消息,那么这个 哨兵就会认为这个主节点主观下线了(单方面的),当超过半数哨兵节点认为该主节点下线了,这样就客观下 线了。
2.当主节点出现故障,此时哨兵节点会通过Raft算法〈选举算法)实现选举机制共同选举出一个哨兵节点为 leader,来负责处理主节点的故障转移和通知。所以整个运行哨兵的集群的数量不得少于3个节点。
3.由leader哨兵节点执行故障转移,过程如下:
●多个sentinel发现并确认一个master有问题
●多个sentinel(哨兵)中选举出一个哨兵leader
●将某一个从节点升级为新的主节点,让其它从节点指向新的主节点;
●让原主节点恢复也变成从节点,并指向新的主节点;
●通知客户端主节点已经更换。
需要特别注意的是,客观下线是主节点才有的概念:如果从节点和哨兵节点发生故障,被哨兵主观下线后,不 会再有后续的客观下线和故障转移操作
主节点的选举:
1过滤掉不健康的(已下线的),没有回复哨兵ping响应的从节点
2选择配置文件中从节点优先级最高的(replication-priority,默认值为100)
3选择复制偏移量最大的,也就是复制最完整的从节点。 sentinel中的三个定时任务
(1)每10秒每个sentinel对master和slave执行info发现slave节点
(2)每2秒确认主从关系:每2秒每个sentinel通过master节点的channel交换信息(pub/sub)通过 sentinel__:hello频道交互对节点的“看法”和自身信息
(3)每1秒每个sentinel对其他sentinel和redis执行ping
在这里插入图片描述
在这里插入图片描述

6.讲一下如何确保redis的缓存数据和数据库数据的数据是一致的

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。 举一个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读 取数据写入缓存,此时缓存中为脏数据。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。 因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。 如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。
二、缓存和数据库一致性解决方案
1.第一种方案:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
伪代码如下:
public void write(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(500); redis.delKey(key); }
具体的步骤就是:先删除缓存;
再写数据库;
休眠500毫秒;
再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求 造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的 耗时基础上,加几百ms即可。比如:休眠1秒。
设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达 缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
该方案的弊端:结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加 了写请求的耗时

7.Redis的持久化策略有哪些?有什么区别吗?项目中一般用哪种?

1)RDB 和 AOF
2) RDB原理是对整个当前内存数据进行快照备份,体积小。AOF原理是每条操作指令都会持久化到文
件,导致文件体积比较大
RDB的两次备份时间间隔最短1分钟,时间长,容易导致数据丢失。而AOF默认间隔1秒1次,时间
短,数据完整性高!
恢复速度上来说,RDB比AOF稍快一些,因为体积小。
3)实际项目我们公司有运维团队维护Redis服务器的配置,我暂时没去修改过,但是我了解到通常为了
保证数据完整更高,同时
开启RDB和AOF,Redis在启动时也会优先加载完整性更高的AOF ,这时RDB备份更多的为了在AOF文件损坏的情况下使用

8.如果系统中存入Redis的数据过多,导致Redis压力过大,怎么解决?

可以搭建Redis主从架构+哨兵集群。

9.什么是Redis缓存穿透,如何解决?

缓存穿透是指用户在短时间内发出海量的请求访问数据库和缓存都不存在的数据时,引发数据库可能崩
溃的行为。
解决办法:
1)设置key-null + 30秒过期时间,这样能应付同一个key的穿透情况。
第1次访问某key时,如果缓存和数据库都不存在,则把该key存入redis,value为null,这样后续访
问该key就走redis缓存了。
防止过多无效key占用内存,必须设置过期时间 (如30秒)
缺点:额外的内存消耗,可能造成短期的不一致(合理的设置TTL,不一致性很短,如果接受不了可以
在新增的时候吧key缓存在redis里面,覆盖之前的)
2)布隆过滤器(BloomFilter)
创建一个位数组,然后数组位置上只有0和1两种状态,当有请求携带信息过来,会通过哈希算法取余位
数组长度然后放在为数组不同的下标位置上,如果存在就是1,不存在就是0。布隆过滤器说有不一定
有,没有的话一定没有。

在这里插入图片描述

10什么是Redis缓存雪崩,如何解决?

同一时刻大量缓存失效,导致请求全部打到数据库,引发数据库宕机
解决办法:
1)缓存数据的过期时间设置固定值+随机值,防止同一时间大量数据过期现象发生
2)采用多级缓存,如加入JVM缓存,Nginx本地缓存,而且不同缓存的过期时间尽可能分散
3)将热点数据设置为永不过期
在这里插入图片描述

11.什么是Redis缓存击穿,如何解决?

缓存雪崩的一种,雪崩是多个key失效,击穿是指某个key失效,同一时刻,大量的相同请求会直接打到
数据库。
解决办法:
1)将热点数据设置为永不过期
2)数据库查询加上分布式锁
在这里插入图片描述

12.缓存倾斜怎么办

在这里插入图片描述

13Redis的淘汰策略

设置了过期时间的淘汰策略:
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
对所有数据:
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操 作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢 失
指定淘汰机制的方式:maxmemory-policy 具体策略,设置Redis的最大内存:maxmemory 字节大小

14.key的生存时间到了,Redis会立即删除吗?

不会立即删除。

  • 定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。
  • 惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。

15. 为什么使用Lua脚本

Lua脚本的优势:
>优势1:使用``方便``,Redis中内置了对Lua脚本的支持。
>优势2:Lua脚本可以在Redis服务端``原子的去执行多``个``Redis命令``。
>优势3:由于``网络``在很大程度上会影响到Redis的性能,而使用Lua脚本可以让多个命令一次被执行,可以有效的解决网络给Redis造成的性能的问题。

``为什么要使用Lua脚本``:尽管Redis在6的时候已经默认使用多线程了,但是``本质最核心的还是单线程``来进行操作的,就会出现很多问题,但是如果我们在Redis中使用Lua脚本的话,Redis就默认Lua脚本中一系列的操作为一个原子操作。(Redis中默认支持了Lua脚本的支持,我们可以直接使用)

在Redis中,使用Lua脚本,主要以两种思路:
1.提前在Redis服务端i下好Lua脚本,然后在java客户端去调用脚本``(这里我们推荐这种方法)``
2.可以直接在Java端去写Lua的脚本,写好后需要执行的时候每次将脚本发送到Redis上去执行。

16.什么是 Redis?简述它的优缺点?

Redis 本质上是一个 Key-Value 类型的内存数据库,很像 memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。

缺点:
(1) . 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
(2) .局限在较小数据量的高性能操作和运算上。

优点:
(1) memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型
(2) redis 的速度比 memcached 快很多
(3) redis 可以持久化其数据

17.Redis的那几种数据淘汰策略

noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但 DEL 和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存
放。

18.为什么 Redis 需要把所有数据放到内存中?

1.Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。
2.不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能

19.MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis中的数据都是热点数据?

redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

21.Redis 有哪些适合的场景

(1)会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Mem
cached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车
信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为
人知的商业平台 Magento 也提供 Redis 的插件。
(2)全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实
例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FP
C。
再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。
此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快
速度加载你曾浏览过的页面。
(3)队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队
列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop
操作。
如果你快速的在 Google 中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是
利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作 为 broker,你可以从这里去查看。
(4)排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也
使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。
所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“user_scores”,我们只需要像下
面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执
行:
ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。
(5)发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见
人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建
立聊天系统

22.Redis 和 Redisson 有什么关系(重)?

Redisson 是一个高级的分布式协调 Redis 客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象
(Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, List
Multimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, Atomi
cLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

23.Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。

24.Jedis 与 Redisson 对比有什么优缺点?

Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;
Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,
不支持排序、事务、管道、分区等 Redis 特性。Redisson 的宗旨是促进使用者对 Redis 的关注分离,从
而让使用者能够将精力更集中地放在处理业务逻辑上。

25. Redis 如何设置密码及验证密码?

设置密码:config set requirepass 123456
授权密码:auth 123456

26. Redis 哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通 过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

27. Redis 集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,
每个节点都会有 N-1 个复制品.

28. Redis 集群会有写操作丢失吗?为什么?

Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

29.Redis 集群之间是如何复制的?

异步复制

30.Redis 集群最大节点个数是多少?

16384 个。

31.Redis 集群如何选择数据库?

Redis 集群目前无法做数据库选择,默认在 0 数据库。

32.Redis 中的管道有什么用?

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务
器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功
能,大大加快了从服务器下载新邮件的过程。

33.怎么理解Redis 事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会
被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

34.Redis 事务相关的命令有哪几个

MULTI、EXEC、DISCARD、WATCH

35.Redis key 的过期时间和永久有效分别怎么设置

EXPIRE 和 PERSIST 命令。

36.Redis 如何做内存优化

尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。

37. Redis 回收进程如何工作的

一个客户端运行了新的命令,添加了新的数据。Redi 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
一个新的命令被执行,等等。所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

38.redis 和 memcached 什么区别?为什么高并发下有时单线程的 redis 比多线程的memcached 效率要高?

1.mc 可缓存图片和视频。rd 支持除 k/v 更多的数据结构;
2.rd 可以使用虚拟内存,rd 可持久化和 aof 灾难恢复,rd 通过主从支持数据备份;
3.rd 可以做消息队列。
原因:mc 多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。

39.redis 主从复制如何实现的?redis 的集群模式如何实现?redis 的 key 是如何寻址的?

主从复制实现:主节点将自己内存中的数据做一份快照,将快照发给从节点,从节点将数据恢复到内存中。之后再每次增加新数据的时候,主节点以类似于 mysql 的二进制日志方式将语句发送给从节点,从节点拿到主节点发送过来的语句进行重放。
分片方式:
-客户端分片
-基于代理的分片
● Twemproxy
● codis
-路由查询分片
● Redis-cluster(本身提供了自动将数据分散到 Redis Cluster 不同节点的能力,整个数据集合的某个数据子集存储在哪个节点对于用户来说是透明的)
redis-cluster 分片原理:Cluster 中有一个 16384 长度的槽(虚拟槽),编号分别为 0-16383。每个 Master 节点都会负责一部分的槽,当有某个 key 被映射到某个 Master 负责的槽,那么这个 Master 负责为这个 key 提供服务,至于哪个 Master 节点负责哪个槽,可以由用户指定,也可以在初始化的时候自动生成,只有 Master 才拥有槽的所有权。Master 节点维护着一个 16384/8 字节的位序列,Master 节点用 bit 来标识对于某个槽自己是否拥有。比如对于编号为 1 的槽,Master 只要判断序列的第二位(索引从 0 开始)是不是为 1 即可。
这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D, 我需要从节点 A、B、 C 中得部分槽到 D 上。

40.使用 redis 如何设计分布式锁?说一下实现思路?使用 zk 可以吗?如何实现?这两种有什么区别?

redis:
1.线程 A setnx(上锁的对象,超时时的时间戳 t1),如果返回 true,获得锁。
2.线程 B 用 get 获取 t1,与当前时间戳比较,判断是是否超时,没超时 false,若超时执行第 3 步;
3.计算新的超时时间 t2,使用 getset 命令返回 t3(该值可能其他线程已经修改过),如果t1==t3,获得锁,如果 t1!=t3 说明锁被其他线程获取了。
4.获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。

zk:
1.客户端对某个方法加锁时,在 zk 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点 node1;
2.客户端获取该路径下所有已经创建的子节点,如果发现自己创建的 node1 的序号是最小的,就认为这个客户端获得了锁。
3.如果发现 node1 不是最小的,则监听比自己创建节点序号小的最大的节点,进入等待。
4.获取锁后,处理完逻辑,删除自己创建的 node1 即可。
区别:zk 性能差一些,开销大,实现简单。

41.知道 redis 的持久化吗?底层如何实现的?有什么优点缺点?

RDB(Redis DataBase:在不同的时间点将 redis 的数据生成的快照同步到磁盘等介质上):内存到硬盘的快照,定期更新。缺点:耗时,耗性能(fork+io 操作),易丢失数据。

AOF(Append Only File:将 redis 所执行过的所有指令都记录下来,在下次 redis 重启时,只需要执行指令就可以了):写日志。缺点:体积大,恢复速度慢。
bgsave 做镜像全量持久化,aof 做增量持久化。因为 bgsave 会消耗比较长的时间,不够实时,在停机的时候会导致大量的数据丢失,需要 aof 来配合,在 redis 实例重启时,优先使用 aof 来恢复内存的状态,如果没有 aof 日志,就会使用 rdb 文件来恢复。Redis 会定期做aof 重写,压缩 aof 文件日志大小。

Redis4.0 之后有了混合持久化的功能,将 bgsave 的全量和 aof 的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。bgsave 的原理,fork 和 cow, fork 是指 redis 通过创建子进程来进行 bgsave 操作,cow 指的是 copy onwrite,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

42.选择缓存时,什么时候选择 redis,什么时候选择 memcached

选择 redis 的情况:
1、复杂数据结构,value 的数据是哈希,列表,集合,有序集合等这种情况下,会选择redis, 因为 memcache 无法满足这些数据结构,最典型的的使用场景是,用户订单列表,用户消息,帖子评论等。
2、需要进行数据的持久化功能,但是注意,不要把 redis 当成数据库使用,如果 redis挂了,内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有 cache 预热的过程。对于只读和数据一致性要求不高的场景可以采用持久化存储
3、高可用,redis 支持集群,可以实现主动复制,读写分离,而对于memcache 如果想要实现高可用,需要进行二次开发。
4、存储的内容比较大,memcache 存储的 value 最大为 1M。选择 memcache 的场景:
1、纯 KV,数据量非常大的业务,使用 memcache 更合适,原因是,
a)memcache 的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间,redis 是临时申请空间,可能导致碎片化。
b)虚拟内存使用,memcache 将所有的数据存储在物理内存里,redis 有自己的 vm 机制,理论上能够存储比物理内存更多的数据,当数据超量时,引发 swap,把冷数据刷新到磁盘上,从这点上,数据量大时,memcache 更快
c)网络模型,memcache 使用非阻塞的 IO 复用模型,redis 也是使用非阻塞的 IO 复用模型,但是 redis 还提供了一些非 KV 存储之外的排序,聚合功能,复杂的 CPU 计算,会阻塞整个 IO 调度,从这点上由于 redis 提供的功能较多,memcache 更快些
d) 线程模型,memcache 使用多线程,主线程监听,worker 子线程接受请求,执行读写,这个过程可能存在锁冲突。redis 使用的单线程,虽然无锁冲突,但是难以利用多核的特性提升吞吐量。

43.缓存和数据库不一致怎么办

假设采用的主存分离,读写分离的数据库,
如果一个线程 A 先删除缓存数据,然后将数据写入到主库当中,这个时候,主库和从库同步没有完成,线程 B 从缓存当中读取数据失败,从从库当中读取到旧数据,然后更新至缓存,这个时候,缓存当中的就是旧的数据。
发生上述不一致的原因在于,主从库数据不一致问题,加入了缓存之后,主从不一致的时间被拉长了处理思路:在从库有数据更新之后,将缓存当中的数据也同时进行更新,即当从库发生了数据更新之后,向缓存发出删除,淘汰这段时间写入的旧数据

44.主从数据库不一致如何解决

场景描述,对于主从库,读写分离,如果主从库更新同步有时差,就会导致主从库数据的不一致
1、忽略这个数据不一致,在数据一致性要求不高的业务下,未必需要时时一致性
2、强制读主库,使用一个高可用的主库,数据库读写都在主库,添加一个存,提升数据读取的性能。
3、选择性读主库,添加一个缓存,用来记录必须读主库的数据,将哪个库,哪个表,哪个主键,作为缓存的 key,设置缓存失效的时间为主从库同步的时间,如果缓存当中有这个数据,直接读取主库,如果缓存当中没有这个主键,就到对应的从库中读取。

45.Redis 常见的性能问题和解决方案

1、master 最好不要做持久化工作,如 RDB 内存快照和 AOF 日志文件
2、如果数据比较重要,某个 slave 开启 AOF 备份,策略设置成每秒同步一次
3、为了主从复制的速度和连接的稳定性,master 和 Slave 最好在一个局域网内
4、尽量避免在压力大得主库上增加从库
5、主从复制不要采用网状结构,尽量是线性结构,Master<–Slave1<----Slave2

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

使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

47.使用 Redis 做过异步队列吗,是如何实现的

使用 list 类型保存数据信息,rpush 生产消息,lpop 消费消息,当 lpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

48.Redis 如何实现延时队列

使用 sortedset,使用时间戳做 score, 消息内容作为 key,调用 zadd 来生产消息,消费者使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。

49. redis解决幂等性问题

幂等性问题:数据库需要修改、增加、删除,要预防客户的重复请求并不像需要操作,因此后端要处理。(如购物商城重复点击购买按钮,需要解决掉重复提交的请求。)

解决思路:
创建两个接口,一个用来生成并返回token令牌,同时在返回之前将token设置过期时间并存入redis中,该令牌内部是随机的字符串(UUID)。另一个接口用来检查redis中是否有请求体中携带来的令
牌,如果有则放行,同时删除该令牌的缓存,如果redis中没有此令牌则拦截下来,有可能是没有令牌、亦或是令牌过期需要刷新页面。(关键点,这里使用注解来给需要解决幂等性问题的接口标注,因此需要使用拦截器或者aop来处理带有该标识注解接口。)

解决方法:
1.创建一个注解@Idempontent:注解的作用是用来标识需要处理幂等性问题的方法。

2.创建RedisService:里面自动装配了RedisTemplate ,通过其来进行redis操作。RedisService里面包含三个方法:(1:将信息以key-value形式存入redis中并且设置过期时间;(2:判断是否redis中存在
某个key ;(3:从redis中删除某个数据。

3.创建TokenService:里面包含两个方法,(1:通过UUID生成字符串令牌并调用RedisService里面的
第一个方法将令牌信息存入redis; (2: 通过RedisService里面的第二个方法检查请求中携带的令牌信息并判断是否存在,请求体通过RequestContextHolder得到,如果判断存在就调用RedisService里面的
第三个方法删除此令牌,防止客户重复请求造成的幂等性问题;

4.自定义异常IdempontentException类,通过继RuntimeException,然后使用构造方法自定义异常

5.使用SpringBoot里面的提供的全局异常处理,通过使用@RestControllerAdvice 和
@ExceptionHandler 来匹配上面自定义异常发生时返回的异常信息和状态码。

6:使用拦截器或者aop来处理幂等性问题。

7:自定义客户访问接口,并在需要处理幂等性问题的接口处加上标识注解@Idempontent。再定义一个令牌获取接口,内部调用TokenServicer的生成令牌方法,客户访问的时候需要先获取令牌再登录,需要两个请求。

50. 双写一致性

数据在缓存和数据库中都有数据,当我们更改数据库中的数据但是没办法改变redis中·的数据就会出现问题。总而言之:双写一致性的概率是没办法避免的只能降低概率。

RabbitMQ

1.死信队列还可以再进入一个死信队列吗

可以

2.重要角色重要组件

在这里插入图片描述

4.RabbitMQ的优势

在这里插入图片描述

5.为什么要在项目中使用Mq

关键词:解耦,异步,削锋
1)解耦。A系统开始调用B和C系统,如果采用普通远程调用(Feign接口)实现,以后A系统还需要调用D系统,修改A系统代码来实现对D系统的调用
如果改用MQ来实现A系统和B、C系统的调用,可以通过MQ的消息给BC系统通讯,以后扩展D系统时,只改变A系统发出的消息内容即可,调用D系统,而不需要A系统的代码。

2)异步。A系统调用B和C系统的业务方法时,需要采用同步调用(Feign),方法调用的总耗时时两个系统调用方法时长总和,如果B或C系统存在耗时业务,导致整个方法调用耗时过长,影响用户响应体验。
如果改用MQ实现系统间调用,生产者A系统,只需要把消息发送MQ,MQ接收消息后,立即响应,而无需要等待消费者的响应,这样大大缩短调用耗时,提高用户体验。

3)削锋。在类似秒杀的高并发业务中,如果直接把大量请求打到数据库,可能导致数据库崩溃。这时,可以在存入数据库之前把消息先存入MQ,通过MQ排队执行下单请求,这样降低对MySQL压力。

6.如何保证MQ消息不丢失?(MQ如何保证消息的可靠性?)

三个维度:生产者,MQ服务器,消费者

1)利用生产者确认机制保证可靠消息
首先,生产者发送消息给交换机,MQ会给生产者发送ACK确认,我们可以使用ConfirmCallback的回调函数接受ACK确认信息

在ConfirmCallback中,
判断ack为true,代表消息成功投递到交换机
判断ack为false,代表消息投递交换机失败,我们可能根据业务需求存储到失败消息表或者进行消息重投。

其次,MQ服务器创建的交换机,队列,消息都有持久化的特性。我建议三者都设置为持久化,这样在MQ服务器意外宕机或重启,消息都不会丢失。通常Spring整合RabbitMQ的API,三者都默认为持久化。

最后,消费者和MQ之间存在消息确认机制,可以利用确认机制保证消息可靠性。

在消费者的配置中,通常开启消费者确认auto自动确认模式(默认值),MQ只有在消费者执行业务逻辑成功后,才会发送ACK给MQ,MQ才会移除信息。如果失败,会重新放回MQ队列中。

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

第一句话:采用消息幂等性判断。(去重判断)
1)首先,我们应该给每条消息分配一个全局唯一的ID,标记消息的唯一性
2)
方案一:MySQL的唯一约束来去重(并发不高)
我们可以在消费方添加一张消息去重表,该表添加消息ID字段,且该字段设置为unique唯一,每次执行消费者逻辑前,往去重表插入当前消息ID,如果可以成功插入,代表没消费过,则正常执行消费逻
辑。如果报错,代表消息重复消费了,则不执行消费逻辑。

方案二:Redis的key唯一性来去重(并发高)
我们每次执行消费者逻辑前,把当前消息ID存入Redis作为key(调用
redisTemplate.opsForValue().setIfAbsent(key,value)),根据返回布尔结果,如果为true,代表消费没有消费过,可以执行消费逻辑,如果返回false,代表消息重复执行了,则不能执行消费逻辑。
底层指令:setnx key value 1 代表key不存在 0 代表key存在

8.请问什么是死信队列?项目中一般用来做什么?

死信队列:
1)普通消息被拒绝了
2)普通消息设置了TTL过期时间且到期了
3)如果达到了队列的上限,后续的消息也会进行死信

死信队列的应用场景:
上个月我帮上家公司做的酒店订单支付超时,就是利用死信队列+TTL过期时间,完成延迟任务执行,如下单后,不付款15分钟取消订单。
下单后,1分钟后自动发送催付款短信给用户

9.解决MQ消息堆积问题?

1)创建多个消费者同时消费
2)利用多线程并发处理消费任务
3)采用MQ集群

10.RabbitMQ的消息发送模式?

1)简单模式(*)
在这里插入图片描述
2)工作队列模式
在这里插入图片描述

3)发布订阅模式
在这里插入图片描述
4)路由模式
在这里插入图片描述
5)主题模式

在这里插入图片描述

11.Publish/Subscribe 发布订阅模式的四种交换机

1.Driect 直连交换机,直连
2.Fanout扇形交换机, 广播
3.Topic主机交换机 ,
4.Header头部交换机 , 队列

12.RabbitMQ 和 Kafka 有什么区别?

RabbitMQ:
优势:
1)支持语言非常广
2)稳定性很好,采用Erlang语言开发
3)吞吐量不算低,万级
4)RabbitMQ官方提供7种消息发送模式,开发者轻松选择合适的模式进行开发即可
缺点:
1)采用Erlang,太小众,研究源码很难

Kafka:
优势:
1)高吞吐量,百万级
2)稳定性好,采用zookeeper进行注册(Zookeep采用CP模式,高一致模式)
3)可以应用在大数据数据处理领域(KafkaStream)
缺点:
1)支持的开发语言比较少
2)耦合zk,依赖zookeeper进行注册

13.RabbitMQ部分面试会问的情况:

1:搭建邮件发送功能,里面解决的问题有(消息发送可靠性确认机制、重试机制、幂等性问题的解
决),要把整个流程捋顺,很重要。
2:延迟队列(比如邮件在新增员工信息后,延迟10分钟才发送邮件)
3:削峰填谷:(比如秒杀系统,10000个请求,先让消息进入消息中间件,然后消费者用pull(拉模式)
手动取出前面一百条消息处理)

复习RabbitMQ:

例如发送邮件接口中网络请求发生阻塞,无法响应。

运用消息中间件就可以实现客户和服务端之间的解耦,如果客户端直接调用服务端就绑定在一起,访问失败就没了,而拥有中间件就会保存客户端的请求,直到服务端解决成功,就算处理失败也可以在消息
中间件中再次获取客户请求。

消息中间件分为两大类:

(1:JMS (java 消息服务,从java代码的层面上去规范了如何使用,因此不支持跨平台):例如ActiveMQ(springboot3.0之后不支持)
(2:AMQP:(从协议的层面上去规范,消息必须以什么格式被接受或者发送,代码怎么写不在意,规定果格式,因此支持跨平台):RabbitMQ
RabbitMQ大概发送流程 :发布者—》交换机—》队列—》消费者,对于消息发布者来说只需要把消息
发布到RabbitMQ就够了,RabbitMQ内部有交换机和队列以及路由绑定策略。

具体流程:消费者(publisher)发送消息到交换机(exchange),选择的交换机不同就会使用不同的路由策略去绑定队列并发送消息到队列,然后消费者通过@RabbitListener(queue= 队列名)监听队
列,一有消息进入队列就处理。

RabbitMQ里面有一个虚拟主机,需要在applciation.properties中配置。类似于excel里面的sheet表单,将使用RabbitMQ的消息发布者隔离开来,连用户名密码都隔离开来,更不用说他们的消息。

生产者和消费者两个之间没有强耦合的关系,比如生产者发消息不需要消费者在线。使用了消息中间件的意义就是已经解偶了生产者消费者。

RabbitMQ使用方法:

在linux使用docker安装的指令:
docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management 其中,第一个端口代表网页查看的端口地址,第二个端口代表java代码使用的端口。安装好之后就可以在网页端查看了。

java端代码(简单使用):
(0:依赖包:springWeb+RabbitMQ,创建一个新模块。
(1:创建一个RabbitConfig配置类(使用@Configuration注解),内部将队列名字常量化,然后写一个RabbitMQ的Queue的bean实例(创建一个队列),其中返回时new Queue(“常量化的队列名”,“是否
持久化的布尔值”,“是否排它性的布尔值”,“是否自动删除的布尔值”) 时有几个参数,其中

参数“常量化的队列名”是上面配置好的队列名,之后交换机就根据这个来找到这个队列;

参数“持久化”的意思是把RabbitMq重启后这个队列是否还存在;

参数“排他性”的意思是具有排他性的队列,只能是哪个连接创建的该队列,哪个连接才能操作该队列;

参数“自动删除”的意思是没有消费者监视此队列的时候是否需要自动删除。

(2:创建一个消费者(使用@Component注解注册进Spring框架),内部创建一个方法( 方法上使用
@RabbitListener(queues=RabbitConfig.队列常量名) ),这样就能监视该队列。
(3:创建一个新模块,用来写Producer(生产者),里面的依赖包:springWeb+RabbitMQ。
(4:在生产者模块里面先创建一个配置类,配置类里面拥有一个常量化队列名。
(5:在生产者模块里面的测试方法里面自动装配RabbitTemplate( Springboot提供的模板,可以用来
发送任意对象或者Message对象),Message对象使用send(),其他的对象使用coverAndSend(队
列常量名,传递的对象)。

direct、handlers、topic、fanout交换机的策略不同点:

direct交换机拥有最严格的路由策略,它需要比较routingkey、以及队列是否绑定了指定名称的交换机,要满足这两个条件才能推送数据至队列上。

topic交换机在direct的基础上放宽了路由策略,除了核验队列是否绑定了指定名称的交换机,topic交换机与队列绑定时配置的routingkey还支持通配符模式,因此就产生了类似于数据库中模糊查询的功能,
从而实现了通过关键字的模糊查询匹配到不同的队列。

handlers交换机并未采用routingkey的路由策略,而是采用了响应头信息,通过对比生产者信息的响应头信息以及核验是否绑定交换机来实现功能-----将数据推送至队列中。

fanout交换机也没有采用routingkey的路由策略,而是仅仅判断队列是否绑定了指定名称的交换机,如
果有多个队列与这个指定的交换机绑定在一起,那么数据都将会被推送值这些队列上去。

RPC(远程过程调用协议)

RPC(两个进程之间跨进程调用,比如说想在java代码里面调用mysql),当然如果不用RPC,用rabbitMQ也可以自己实现,比如两个应用A、B想通信,B应用先监听a队列,然后A应用给B应用监听的
队列a发消息,然后应用B收到消息就做出回应发送给A应用监听的的队列b,A应用就受到了B应用的回应,实现了两个应用间的跨进程调用。

但是rabittMQ在RPC方面提供了专门的接口来供两个不同的SpringBoot工程之间的互相调用,当然这种调用是通过消息中间件来实现的(Client和server端都有自己监听的队列,双方通过对方监听的队列实现通信)。

步骤:
(1:创建一个新模块 rpc_client(客户端) ,和另一个新模块 rpc_server(服务端),都各自添加Web和rabbitMQ依赖。
(2:在两个模块的application.properties中都配置rabbitMQ的基本信息。(用户名、密码、端口、域名、虚拟主机),除了前面这些基本的,还需要有两个配置信息:

【1】消息确认的方式(spring.rabbiyMQ.publisher-confirm-type=correlated),每次发送消息出去后,发出去的信息对象的响应头里都会有一个correlated.id,这样对方收到消息后的响应信息的信息头里也会携带这个correlated.id,等服务端返回的时候也会将此correlationId同时放在返回信息里面返回来。于是客户端通过比对发送信息后信息对象内的correlationId和服务端返回对象的correlationId就知道发送的信息是否被接受(被消费)。

【2】 spring.rabbitMQ.publisher-return=true:消息如果发送失败的话,就返回来,我会接到一个相应的通知,我就知道失败了。


(3:在客户端模块里面创建一个RabbitConfig,在里面配置的有:发送队列的常量名、接收队列的常量名、交换机的常量名、注册交换机(topic交换机当做direct交换机)的实例、注册两个队列的实例、绑定交换机和队列的Binding注册。除此之外还要配置:

【1】:重新配置RabbitTemplate,通过参数ConnectionFactory 放入RabbitTemplate的构造方法中得到RabbitTemplate对象,然后就可以设置返回队列的地址(rabbitTemplate.setReplyAddress( 接收数据的队列名称));设置超时时间( rabbitTemplate.serReplyTimeOut(6000) )意思是超过六秒没收到消息就报错。

【2】:重新配置队列的监听器方式(SimpleMessageListenerContainer),不用像之前的监听到队列然后得到消息,设置了之后就相当于RPC里面有个方法,一调用就能返回对方发送到队列中的数据。

(4:创建一个/hello接口,里面首先构建了一个消息对象,然后使用rabbitTemplate.saveAndRecieve()方法,发送信息的同时收到服务端响应的消息对象,由于之前设置了“消息确认”的配置,在返回的响应信息的响应头里面会有一个correlationId,同时本地信息发出去之后也会有一个correlationId,此时比较这两个correlationId是否相等,如果相等就证明发过去的信息被消费成功,并且获得的响应对象的身体部分有返回的内容。

(5:配置服务端,在服务端模块里面只需要配置:发送队列的常量名、接收队列的常量名、交换机的常量名、注册交换机(topic交换机当做direct交换机)的实例、注册两个队列的实例、绑定交换机和队列的Binding注册。然后正常监听客户端的发送队列,从接收到的信息里面提取出correlationId然后返
回给客户端进行比对。

(6:只需要通过网页访问/hello接口,客户端就会自动发送信息给服务端并且获得服务端的响应信息,比对成功correlationId后,就会将服务端的回复信息显示在页面上。

确保消息发送成功

消息从发布者----》交换机----》队列—》消费者,那么从交换机到队列这个过程其实是rabbitMQ自己的一段代码,一般来说这个过程是不太会出错的(除非交换机没有和队列绑定到一起),否则就不是我们
能处理的问题了。

因此消息发送可靠性其实要解决的主要问题是,必须这两个问题同时时解决,否则都无法发送成功:
(1:确认消息从到达交换机
(2:确保消息到达queue

为了解决的上面两个问题,我们只需要在代码中实际上做好三件事就可以了:
(1:确认消息从到达交换机。
(2:确保消息到达queue。
(3:开启定时任务,定时投递那些发送失败的信息。

这三个步骤前面两个步骤rabbitMQ有现成的解决方案来确保消息到达rabbitMQ(交换机+队列),
rabbitMQ给出了两种方案(注意 仅仅是上面的两个步骤!!):

(1:开启事务机制(由于步骤较多,导致达不到高并发的性能要求,因此一般不使用)

1.在配置好客户端访问RabbitMQ的环境后,首先需要在任意一个配置类里面提供一个RabbitMQ事务管理器(在讲Spring事务的时候说过事务需要配置的三大件之一----事务管理器,例如数据库的
(dataSourceManager),在RabbitMQ中,事务管理器的名字叫做RabbitTransationMananget, 在配置的时侯实例化这个事务管理器时它的构造方法需要一个参数—ConnectionFactory(是RabbitMQ与应用程序建立的连接管理器),然后事务管理器就注册进Spring容器里面了,注意!!注册这个事务管理器时@Bean方法返回的值的类型是PlatformTransactionManager类,是所有不同程序的定义事务管理器的统一父接口,是由Spring规范的接口,只是说reuturn 的时候是new
RabbitTransationMananget (ConnectionFactory connectionFactory)。

2.接下来,在消息生产者上面添加事务注解(@Transation,如果此时有数据库其他方法使用了数据库的事务或者其他程序的事物,就要自动装配事务管理器的名字,在各自的@Transation(“事务管理器
名字”)中指定才能使用自身服务器的事物,没有则直接使用@Transation注解即可),除此之外,还要设置通信信道为事务模式(rabbitTemplate.setChannelTransacted( true ))。

3.3.运行生产者的代码(通过RabbitTemplate.convertAndSend(交换机名字 , 队列名字 , 消息具体内容))产生数据,如果在这个方法执行时产生异常(比如手动加上了一个算数异常1/0),那么信息就发
送不到RabbitMQ了,这个生产者推送信息的过程就会被回滚。具体步骤如下:当我们开启事务后,生产者(客户端)发送消息给RabbitMQ就会多出三个步骤(下面的1 、2、5步骤):
(3.1:客户端发出请求,请求将amqp信道设置为事务模式。
(3.2:RabbitMQ回应客户端,同意将信道设置为事务模式。
(3.3:客户端发送消息给RabbitMQ. (3.4:客户端发送信息过程成功,就提交事务。
(3.5:服务端给出响应,确认事务提交(此时在RabbitMQ的网页端的信道里面就能看到消息数增加了)

(2:发送方确认机制(在实际工作中使用的方案)
特点:发送过去的消息是否有被收到,RabbitMQ收到的话你就和我说一声消息收到了,RabbitMQ没收到的话也说一声没有收到消息。(就相当于加了一个回调)

java代码配置步骤:

(1:在生产者模块的application.properties中加入下面这两行配置:

#表示开启消息到达交换机的确认回调,有三个属性 (1)none:表示不开启发布确认模式,默认就是这个,如果消息没发送成功也不会有说明。 (2)correlated:表示成功发布消息到交换机后会触发的回调方法 (4)simple:类似correlated,可以阻塞的去调用回调方法 spring.rabbitmq.publisher-confirm-type=correlated #消息未到达队列(就是到了交换机没到队列就会触发一个方法,一般是自己的代码写错了),如果消息成功 到了队列他就没有任何提示 spring.rabbitmq.publisher-returns=true

(2:创建一个配置类RabbitTemplateConfig,让他实现两个RabbitTemplate模板提供的回调接口

第一个接口:RabbitTemplate.ConfirmCallBack 对应的是上面消息到交换机的回调配置,在配置类中实现此接口中的方法:confirm(CorrelationData correlationData, boolean ack, String cause),如果消息到达或者未到达交换机,都会触发该方法。“correlationData”就是消息发送出去后返回一个包含回调信息的对象,通过其里面的id可以锁定是响应哪一次发送的消息。 “ack” 就是发送成功就是true,发送失败就是false。“cause”就是失败的原因。因此,根据“ ack ”实参来判断消息是否到交换机,然后打印日志。

第二个接口:RabbitTemplate .ReturnCallBack 对应的是上面消息未到达队列的回调配置,在配置类中实现此接口中的方法:returnedMessage(ReturnedMessage returned),消息未到达队列,会触发该方法。可在其内部打印日志。需要自己去写重试。

(3:在配置类中注册RabbitTemplate模板类,这是SpringBoot自动提供的,但是我们需要在这个基础再加一些属性,由于SpringBoot自动装配默认是单例模式,因此直接在这个模板类上面添加属性就好了。需要使用到

@PostConstruct注解,它的作用是在这个配置类被SpringBoot自动初始化之后,就自动执行所修饰方法里的操作,通过rabbitTemplate.setConfirmCallback()和
rabbitTemplate.setReturnsCallback()将两个回调接口的实现类加入rabbitTemplate模板中。

注意:事务机制和发送方机制不能同时开启,否则会报错(具体见文件“消息发送可靠性的确认”),大意是不能从“can’t tx mode switch comfirm mode。

消息发送失败是的重试(也就是上面的解决问题3-------开启定时任务,定时投递那些发送失败的信息。):

从两个方面去解决:
(1:自带的重试机制(retry)

自带重试机制:上面说的事务机制和发送方确认机制,都是生产者确认消息发送成功的方法,如果发送方(生产者)一开始就没连接上rabbitMQ,那么SpringBoot也可以通过配置实现重试机制,是Spring家族提供的retry机制来完成的。RabbitMQ把他给集成进来了,可以直接用,不只是RabbitMQ可以用,其他软件也可以用。只针对连接不上RabbitMQ(或者连接不上其他程序服务端,比如redies)的情况缺点:没有办法去具体到业务上去,不会告诉你是为什么失败了,是什么操作时失败了,是一种比较笼统的重试方法。

只需要在application.properties配置如下:

# 开启重试机制
 spring.rabbitmq.template.retry.enabled=true
# 初始化的时间间隔,就是发现执行失败,多久后开始重试 spring.rabbitmq.template.retry.initial-interval=1000ms 
# 最大重试次数,超过这个次数就需要人工介入 spring.rabbitmq.template.retry.max-attempts=5 
# 间隔乘数,从初始化时间间隔开始,每一次重试失败后都会时间间隔都会变成上一次时间间隔*间隔乘数 
spring.rabbitmq.template.retry.multiplier=1.2
 # 最大重试间隔时间,(每一次重试失败后都会时间间隔都会变成上一次时间间隔*间隔乘数,但是不能超过 这个时间) 
spring.rabbitmq.template.retry.max-interval=2000ms

(2:业务重试
需要结合具体的业务环境去配置重试。针对生产者消息发送失败时的重试,可以设置具体的重试条件灵活应对不同业务。(下面有业务重试介绍案例)

消息消费确认机制

(1:push:推,也就是消费者监听了某个队列,只要这个队列一有消息,就会被MQ主动推给消费者,缺点,消息太多时被推进内存,然后消费者处理不过来可能会被拖垮。优点:效率较高,使用较多。

(2:pull:拉,自己从队列中拉取信息,
缺点:效率较低,
优点:自己控制消费的节奏。使用较少,看具体业务。

在java里面消费者接受消息的操作:和push消费情况不同,消费方法上面不能使用@RabbitListener(queue=“队列名”)注解监听指定队列,方法参数列表里面也没有消息形参,而是需要自己通过RabbitTemplate模板去调用 receiveAndConvert(队列的名字)方法,里面的参数就是配置类里面的队列名字。

rabbitMQ网页端介绍:

在Queues选项视图下面,分别对应着rabbitMQ服务器中的所有队列的展示表格,其中有三个列名:“Ready”这个列名展示的数据表示该行数据所表示的队列中待消费的消息数量;

"Unacked"列名表示未确认的消息数量-------正常情况下队列中的消息从rabbitMQ里的队列发送到消费者手里去消费,消费者消费完成后,消费者需要返回一个反馈,告诉它这条消息我已经消费成功了,然
后rabbitMQ就会从队列中删除这条消息。而"Unacked"表示的消息数量,就是消息已经发给java代码去执行了,但是java端执行完成并未给出一个反馈的消息的数量。”total“列名表示该行数据所表示的队列里的总消息数目,它等于ready + Unacked的消息数之和。

在push(推)的模式下实现消息消费确认机制:

(1:在默认情况下,推模式的消费方法是自动确认消息是否消费成功的,如果这个消费的方法抛异常,那么这个消息会自动回到队列中,然后客户端会立刻进行重试,然后又把消息拉到客户端处理(不停的从unacked到ready),如果一直消费失败取消消费了,那么消息就会停留在最后一次ready,等待下一个消费者使用。并不会丢失消息

(2:也可以在application.properties中手动设置监听器的确认消费模式:

可以设置消息的确认方式为手动的(manual),默认情况下是自动确认(auto),也可以设置不管,相当于 关闭确认消费模式(none) spring.rabbitmq.listener.simple.acknowledge-mode=manual

在这种手动模式下,消费者即使消费成功但如果不返回一个标记,消息又会返回队列中,永远不会删除这个消息。因此需要返回一个标记,我们可以通过两个消费方法中的参数来返回,如下

@RabbitListener(queues = DirectConfig.MY_DIRECT_QUEUE_NAME_01)
		public void handleMsg(Message message, Channel channel) throws IOException {
		//获取消息的唯一标记
		 long deliveryTag = message.getMessageProperties().getDeliveryTag();
		 try {
		 		byte[] body = message.getBody();
		 		 System.out.println("new String(body, 0, body.length) = " + new String(body, 0, body.length));
		 		 int i = 1 / 0; 
		 		 //确认消息消费成功,第一个参数是消息的唯一标记,第二个参数表示是否是一个批处理,如 果是就要将其他消息是否成功也返回给MQ
		 		 channel.basicAck(deliveryTag, false); } catch (Exception e)
		 		  { 
		 		  e.printStackTrace();
		 		  //告诉mq消息消费失败,第三个参数是“requeue”,表示是否重新将消息放入队列。
		 		   channel.basicNack(deliveryTag, false, true);
		 		   } 
		 		   }

在pull(拉)模式下实现消息消费确认机制:
拉模式的自动确认消息消费模式可以使用事务来处理消息消费确认,如下:

/** 拉模式,也可以自动处理(利用事务去处理) **/
@Test 
@Transactional
		public void test07() {
			 //给信道设置为事务模式
			 	rabbitTemplate.setChannelTransacted(true);
			 	String s = (String) rabbitTemplate.receiveAndConvert(DirectConfig.MY_DIRECT_QUEUE_NAME_01);
			 	int i = 1 / 0;
			 	System.out.println("s = " + s);
拉模式的手动确认消息消费模式可以如下(Spring没有提供接口,很复杂):
@Test
		public void test08() { 
		//创建一个不带事务模式的消息通道
		Channel channel =	rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);
		long deliveryTag = 0; 
		try {
		//手动拉一条消息回来
		GetResponse getResponse = channel.basicGet(DirectConfig.MY_DIRECT_QUEUE_NAME_01, false);
		deliveryTag = getResponse.getEnvelope().getDeliveryTag(); channel.basicAck(deliveryTag, false);
		} catch (IOException e) {
		 e.printStackTrace();
		  try {
		  channel.basicNack(deliveryTag, false, true); 
		  } catch (IOException ex) {
		  	ex.printStackTrace();
			  		} 
		  		} 
		  	}

消息发送失败业务重试

在之前第三阶段的人事管理系统中新增一个邮件自动发送功能,在新增一个员工后,就会自动实现发送邮件的功能,步骤如下:

1、员工信息在接口端增加员工成功后,实现增加员工功能的接口在增加的同时需要实现向消息中间件(RabbitMQ)中的直连交换机发送消息(增加员工的操作成不成功都无所谓,下面的”empId“参数有
说明),然后直连交换机将信息发布到Routingkey和生产者生产消息时指定的Routingkey一致的绑定队列当中。(实际上就是新增员工的接口,需要同时给RabbitMQ发送一条员工数据)

2.为了保证生产者生产的消息能够传送到RabbitMQ(交换机和队列)中,因此要使用上面提及的”发送方确认机制“用来确保数据发送的可靠性。所以在数据库中有一张名为”mail_send_log“的数据表,这张表
有几个参数,其中:
” createTime “ 用来记录此消息创建的时间,
”routekey“每个员工信息发送时使用的routingkey值。记录好之后方便发送失败后取出来设置发送方法的参数。。
”exchange“每个员工信息发送时使用的交换机值。记录好之后方便发送失败后取出来重试的时候设置发送方法的参数。
” empId “用来匹配传送的Employee对象数据(),在使用
RabbitMQTemplate.convertAndSend()时,除了前面的交换机名字、routingkey名字、消息、还有最后一个参数Correlation回调信息(要使用此回调,需要先在application.properties中添加两条配置属性,也就是spring.rabbitmq.publisher-confirm-type=correlated和spring.rabbitmq.publisherreturns=true;然后还需要在RabbitMQConfig里面实现两个接口并重新设置RabbitMQTemplate模
板,具体看上文的”发送方确认机制“),这个参数的目的是用来绑定生产者发送去RabbitMQ的消息,
这样即使发送失败需要重试的时候就知道查找数据库中的哪一条员工信息重新发送给消费者,消费者再
获取此员工信息生成邮件。
"status"用来记录员工信息发送到RabbitMQ是否成功,他有三个状态,0代表发送中,1代表代表发送成功,2代表发送失败。
” tryTime“参数,表示推迟重试时间,到达这个时间才开始重试,这里固定在消息创建时间(“createTime”)的一分钟后。
”count“重试次数,超过三次就认定为发送失败,不会再继续发送。

3.创建业务重试,重试应该使用计时任务(在重试任务类的执行方法上写入@Schdule,同时别忘了在主程序类上写上@EnabledSchdule注解),之后在@Schdule(”corn“)注解中使用corn表达式,每隔十秒执行一次

4.为了避免重试产生重复数据,重试的时候应该联合 “status"和” tryTime“参数一起来判断,如果只根据”status“参数来判断成功或者失败是不行的,由于重试是一个周期性方法,方法内部每隔十秒遍历查
找”mail_send_log“表,查找出所有状态为0的数据,再根据其”empId“查找到对应新增的员工信息,重新发送员工数据到RabbitMQ传送给消费者。但是在这个过程中网络请求受网络影响较大,当网速不好的时候,可能一直处于发送中也就是状态0,如果开启了重试就会造成队列中有多条相同数据,因此需要再配合”tryTime“使用,需要重试时遍历表”mail_send_log“的所有数据,依次判断这些数据各自是否当前时间(System.CurrentTimeMills得到当前时间)大于”tryTime“且"status”==0,如果是,就说明需要重试。(但是就算这样也不能完全规避网络请求慢造成的重复数据发送,只能说用一分钟的规避掉了大部分的重复数据,由于还是有可能网络请求因为其他原因延迟时间超过了一分钟才成功,因此队列中还是会有相同的员工信息)。重试时还需注意”count“,如果重试成功,设置status为1,重试失败”count“要递增,当”count“大于2(从0开始,也就是重试了3次),此时还未成功的话就将状态status设置为2,发送失败,就只能进行人工干预了。

结合项目的消息消费问题

在项目中消费者这里只需要处理幂等性问题就可以了。

大致思路:
(1:在mail模块里创建消费者,在模块pom.xml里面添加redis依赖
(2:在application.properties中将消费确认模式设置为手动,配置好rabbitMQ参数,配置好redis所
需参数。
(3:在RabbitMQ中配置交换机和队列的常量名。

(4:创建消费者,使用推模式监听指定队列,然后解决幂等性的方法就是每次接收到参数通过
redisTemplate.opsForHash().entrys(key)可以将Hash转换成Map得到具体的Map(key为employee.id,value也为employee.id),然后通过判断这个map里面是否包含和从队列中接收到的employee的id,如若包含,就证明已经处理过,如若不包含,就可以消费,然后就将它以hash的形式
存入redis,key为“mail_log”,value就是一个Hash
(key employee.id,value随意)。

消息有效期+死信交换机/死信队列----文件同名

问题思考1:消息发送到队列上一直没人消费,消息还有会不会过期。
答:不会过期,一直会存在队列上(如果队列能够一直存在的话)。除非给队列中的消息设置过期时间。

1:给队列中的消息设置过期时间的两种实现方式
(1:给消息对象设置过期时间
判断逻辑:当这个消息准备被消费的时候(ready)才会判断是否过期,要是过期了就在队列上去掉,然后判断下一条准备消费的消息。
写java代码步骤:
(1.1 添加依赖 (springWeb+RabbitMQ)

<dependency>
		<groupId>org.springframework.boot</groupId> 
		<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

		<dependency> 
			<groupId>org.springframework.boot</groupId>
			 <artifactId>spring-boot-starter-web</artifactId>
		 </dependency>

(1.2:添加RabbitMQ的配置

1.端口 2.虚拟机域名 3.MQ用户名 4.MQ密码 5.虚拟主机

(1.3:配置交换机常量名、注册@Bean交换机实例和队列常量名、注册@Bean队列实例
(1.4: 创建一个消费者,自动装配RabbitMQTemplate,创建一个Message对象并在内部设置过期时间,借用send()发送到MQ。方法代码如下:

@Autowired 
RabbitTemplate rabbitTemplate; 

@Test
 void contextLoads() {
  	 Message msg = MessageBuilder.withBody(("hello 123>>>>"+new Date()).getBytes())
  	 	 //给消息设置过期时间,单位是毫秒
  	 	 	 .setExpiration("10000") .build(); 
  	 	 	 //因为配置消息的过期时间是在消息对象Message里面配置的,因此不是其他对象因此不能用 convertAndSend(),因为convertAndSend()会先转换成Message对象,而这里不需要转换。
  	 	 	  rabbitTemplate.send(交换机名字, routingkey名字, msg); }

(2: 给队列设置消息过期时间
判断逻辑:比如说给队列设置队列中消息的过期时间为3分钟,那么三分钟之内没被消费他就过期了。是由于消息在队列上是有顺序的,消息是从左到右进去队列的,如果给队列设置消息过期时间,
那么他就会定期自动从右到左遍历,通过时间窗大致判断哪个是最后一个过期的,然后将第一个消息右边)到最后一个过期的消息中间所有消息在队列上去除掉(包头包尾)。

java代码步骤:
	前面的步骤和上面的消息设置过期时间的1.1-1.2步骤是一样的,但是不同点在于,给队列设置消息过期时间是在注册队列Bean实例的时候,在返回的队列实例的时候,向其参数列表中添加一个增加了过期时间的键值对的Map。注意点!!!!:如果队列之前已经被创建,就需要删除队列重新设置,否则会报错,无法在原有的队列上添加属性。代码如下:
@Bean
	Queue Queue01() { 
	Map<String, Object> args = new HashMap<>();
	 //配置消息有效期,消息到期未被消费,就会进入到死信交换机,并由死信交换机转发给死信队列 args.put("x-message-ttl", 10000);
	  return new Queue(MY_DIRECT_QUEUE_NAME_01, true, false, false, args); 
	  }

(3:注意,如果两种方式都设置了,那么就以过期时间短的设置为准。
答:去了死信交换机绑定的死信队列。

2:消息过期(死信消息)去向死信交换机和死信队列:
(1:进入死信交换机和死信队列的几种条件(变成死信消息的集中条件):
。消息被拒绝或者访问失败(Basic.reject/basic.nack),并且设置第三个参数requeue为false,那么消息将不会返回队列,而是跑到死信交换机去。
。消息过期
。队列达到最大长度

(2:死信交换机的配置以及死信队列的配置
(1:死信交换机本质上也是一个交换机,绑定在死信交换机上面的队列就是死信队列。
(2:死信交换机和普通的交换机一样配置,指定的是那四种
(headers/topic/fanout/direct),然后绑定死信队列。
(3:配置死信队列也和普通的队列配置一样,直接返回new Queue(死信队列名,持久化布尔
值,批处理布尔值,自动删除布尔值)
(4:将死信队列和私信交换机绑定在一起,和普通交换机绑定普通队列操作一样。
(5:配置一个正常的交换机和队列,在配置这个正常队列的时候加入配置的死信交换机和
MQ。
(6:此时需要和死信队列关联的普通队列和一般的队列配置方法略有不同,需要在注册这个正常队列实例的时候,向队列中添加一个HashMap(第一个键值对:“x-dead-letter-exchange”-----配置的死信交换机名;第二个键值对:“x-dead-letter-routing-key”----“死信交换机绑定时配置的routingkey”,第三个键值对:“x-message-ttl”-----队列消息过期时间),然后通过返回实例时的第四个参数“argument”将hashmap传入队列实例中。代码如下:

@Bean
	Queue directQueue01() {
		 Map<String, Object> args = new HashMap<>()
		 //配置消息有效期,消息到期未被消费,就会进入到死信交换机,并由死信交换机转发给死信队列
		 args.put("x-message-ttl", 10000);
		  //指定死信交换机
		  args.put("x-dead-letter-exchange", MY_DLX_DIRECT_EXCHANGE_NAME); 
		  args.put("x-dead-letter-routing-key", MY_DLX_DIRECT_QUEUE_NAME_01); 
		  return new Queue(MY_DIRECT_QUEUE_NAME_01, true, false, false, args); }

(7:将正常队列与死信队列关联好后,接下里在正常队列里,如果出现了消息过期、拒绝消费和消费失败且不回到正常队列(”requeue“=false)、队列达到最大长度这些情况,就会将消息发送到
死信队列。

(3:死信交换机和死信队列的作用—处理延迟消息
接着上面的步骤,模拟正常队列的生产者生产信息,然后再建立一个死信队列的消费者(和普通的推模式的消费者一样写法)用来消费信息(由于给队列里的消息设置了过期时间,因此等到过期就会直接到死信消费队列了),在死信消费者里面打印日志消息,使用Logger类(private final Logger logger = LoggerFactory.getLogger(当前类类名.class),然后调用logger.info(msg)打印具体接收到的信息)

死信队列实现延迟消息小结:

简单来说:整理流程大概就是:生产者生产的消息发送到队列中,给消息设置一个过期时间,然后消息到达那个时间后才会进入死信队列,然后被死信消费者处理,实现了延迟的功能。工作中大都使用死信队列完成延迟功能。对比使用定时任务来实现延时功能更加方便。自带自动的消费确认模式(前文提到的push模式的自动事务模式)。

利用插件实现延迟消息队列,文件同名

1.定时任务(corn表达式)和延迟消息队列的区别
使用周期性任务(@schduling+corn表达式)也能实现一部分的定时任务并延迟处理,比如每天凌晨三点进行数据库备份这种开始时间固定的情景。
但是开始时间不确定的定时任务,使用corn表达式很难满足要求,这时候就需要使用延迟消息队列来处理。例如电商下单,要求20分钟内下单付款。(给消息设置过期时间20分钟,消费者一直未进行下单操作就进入死信队列,然后死信消费者进行删除订单操作)

2.在RabbitMQ中延迟队列的用法,有两种:
(1:使用上面提及的自带的消息机制和死信队列机制,实现定时任务。
(2:使用RabbitMQ的rabbitmq_delayed_message_exchange插件来实现定时任务,这种方案简单一些。
(3:

3.使用延迟插件实现延迟消息队列
1.安装插件:见视频的hithub安装链接。
2.使用插件:
(2.1:创建一个新的maven项目,添加两个依赖(Springweb+RabbitMQ)
(2.2:创建一个配置类,正常配置队列常量名、交换机常量名。以及使用插件要额外设置的一个自定义的交换机的类型名,这个值是固定的,如下:

public static final String MY_DELAYED_EXCHANGE_TYPE="x-delayed-message"

再向spring注册一个Queue的@Bean,和正常的队列实例一样。然后定义一个customExchange交换机(由Spring提供的名字),依然是之前的四种交换机类型,如下:

@Bean
CustomExchange delayedExchange() { 
Map<String, Object> args = new HashMap<>(); 
//配置当前交换机的类型是直连交换机
 args.put("x-delayed-type", "direct"); 
 //1.交换机的名字
  //2.交换机的类型名,这个是固定的 
  //3.持久化   
  //4.自动删除
 //5.将配置的hashmap加入交换机 
    return new CustomExchange(MY_DELAYED_EXCHANGE_NAME, MY_DELAYED_EXCHANGE_TYPE, true, false, args); }

配置与一个和普通一样的消费者,监听延迟消息队列。用来消费延迟队列上的延迟消息。
设置一个特殊的生产者,在消息头里设置一个插件定义的延迟参数“x-delay”,以及延迟时间“3000”(毫秒),代码如下:

@Autowired 
	RabbitTemplate rabbitTemplate; 
	@Test 
	void contextLoads() {
			 Message msg = MessageBuilder.withBody(("hello delayed msg>>>"+new Date()).getBytes()) 
			 //发送消息时,设置延迟时间为 3 秒 
			 .setHeader("x-delay", 3000)
			  .build(); 
			  rabbitTemplate.send(交换机常量名,队列常量名,msg);
			   }
至此就实现了利用插件实现延迟功能。

插件实现延迟功能小结:主要就是设置customexchange(交换机),和设置生产者时在消息头里设置延迟时间。

在java代码逻辑使用插件和死信队列实现延迟消息的不同点:
答:使用死信队列的方式:通过设置在正常队列里消息过期时间,从而实现延迟,等设置的消息过期时间到了,正常队列的消息过期了被移送至死信队列,才能处理死信消息,实现延时操作。
使用延迟插件直接设置消息延迟时间,无需转移到其他队列,直接在当前队列等待延迟时间到达,然后执行操作。

rabbitMQ中的用户信息

如何添加一个RabbitMQ用户:
(1:在网页端点击Admin选项试图 , 点击“add a user ”,输入用户名(username)、密码(password)、角色选项(Tag)(在旁边点选),填好之后点击 " Add User "。
(2 : 此时如果直接使用这个用户访问之前配置好的虚拟主机(配置文件里的"virtual hosts”)为“/ ”的代码并操作,会报错,提示不允许访问这个“ /”虚拟主机。其实是由于我们虽然安装的时候是
RabbitMQ ,但是平时使用RabbitMQ时所有的操作都是针对其里面的 ”虚拟主机“ 做的操作,比如说创建一个队列/channel(通道)/交换机等都是在虚拟主机里面做的。因此我们在网页端创建的用户应该要
和一个虚拟主机绑定在一起。(可以在网页端点击右上角的” virtual hosts “,切换查看不同的虚拟主机所拥有的交换机/队列/通道)
(3 : 可以新建的用户去访问之前的虚拟主机,点击用户名可以修改与该用户绑定的虚拟主机(一个用户可以同时绑定多个虚拟主机)。也可以新建一个虚拟主机。那么怎么让创建的用户绑定一个虚拟
主机呢?在网页端先点击“Admin”再点击“virtual hosts ”,直接输入一个虚拟主机名字,其他不用写,就可以新建虚拟主机了。
(4: 完成上面操作就可以正常操作了。

rabbitMQ的集群搭建–文件同名

1、集群其实就是将RabbitMQ部署在多台服务器上,分为两种:
(1:普通集群(消息只存在集群中的一个RabbitMQ实例上)

特点:多个服务上的RabbitMQ进行消息通信,部署好普通集群后,比如说在其中一个RabbitMQ上创建了一个队列,其他的RabbitMQ上也会有这个队列的信息,但是注意!!,并非将这个队列同步到其他RabbitMQ上了,而是通过一种元数据(其实就是该队列的一些配置)同步在其他RabbitMQ上实现的。 这样每当消费者需要消费这条队列上的消息时,都会通过他访问到的RabbitMQ上的元数据定位到该队列所在的RabbitMQ上,然后访问这个RabbitMQ里的队列进行消息消费。 这种普通集群的特点是,提高了集群的吞吐量,因为有更多的存放队列的空间了,但是却不能实现高可用性,因为一旦某个 ”存放消费者需要消费的消息队列”的RabbitMQ挂了,那么元数据也定位不到去 “ 存放所需消息队列的RabbitMQ ”,导致整个集群就无法使用了,同时万一队列没有设置持久化,那么
消息就永久丢失了。
		(2:镜像集群
		特点:每当集群中生产者提供给了任何一个RabbitMQ实例,那么元数据和队列信息都会被同步到所有的RabbitMQ上(相当于都拥有了副本),这样就算一台跑着RabbitMQ的服务器挂掉了,集群也还是能够正常支持消费者的操作。
  1. RabbitMQ集群中的节点类型。
    (1:RAM node(内存节点):内存节点将所有的交换机、队列、绑定、用户、权限、以及虚拟主机这些东西元数据定义,都存储在内存中。好处就是声明创建这些东西时很快。
    (2:Disk node(磁盘节点):将元数据存储在磁盘中,防止重启的时候丢失系统配置,单节点(不是集群环境,只有一个RabbitMQ的时候)只允许磁盘节点。
    注意点!!!:(1)RabbitMQ要求集群中必须有一个磁盘节点,所有其他节点可以是内存节点,
    (2)当有节点加入或者离开集群时,都要将变更通知磁盘节点。
    (3)如果集群中唯一的磁盘节点崩溃了,集群依然可以保持运行,但是无法进行其他操作(增删改查),直到磁盘节点恢复。
    (4):为了保证集群信息的可靠性,当不知道使用哪种结点的时候,建议直接用磁盘节点

调优专题

1.请说说JVM调优?( 必问 )

我在开发中一般很少需要进行JVM调优,但我了解过一点JVM调优的参数:
OOM(Out of Memeory 内存溢出)
1)设定堆内存大小(比较常用的)
-Xmx:堆内存最大限制。
2)设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
3)设定垃圾回收器算法
年轻代用 -XX:+UseParNewGC
年老代用-XX:+UseConcMarkSweepGC
在这里插入图片描述

SpringCloud系列

1.什么是微服务

微服务架构是一种特殊的分布式架构
1)职责独立。尽量每个业务模块拆分成一个服务,每个服务的粒度尽可能小。
2)协议独立。不同于SOA架构的RPC协议,微服务架构多采用Http协议,能做到语言独立,平台独
立。
3)数据库独立。每个微服务独立有一个数据库(MySQL),实现数据独立,数据隔离。
4)部署独立。 每个微服务可以独立部署并测试,使用,交付。

2.微服务技术栈

在这里插入图片描述
**加粗样式**

1.面试题:SpringCloud vs SpringCloudAlibaba 什么关系?

SpringCloud 属于Spring家族的一员 
 Eureka Gateway Feign Ribbon Hystrix SpringCloudConfig Slueth
 看依赖:spring-cloud-starter-eureka-server/client
SpringCloudAlibaba 属于SpringCloud的一套组件
 Nacos Sentinel(流量控制) Seata 
 看依赖:spring-cloud-starter-alibaba-xxxx

2.面试题: Sentinel 和 Hystrix 的区别?

 1)隔离方式不同,Sentinel采用信号量,Hystrix默认采用线程池,信号号量性能比线程池好
 2)熔断策略不同,Sentinel可以基于超 时和异常比例,Hystrix只支持异常比例
 3)限流功能不同,Sentinel有丰富限流功能(QPS,链路模式等),Hystrix限流功能非弱
 4)第三方框架整合,Sentinel可以整合SpringCloud和Dubbo,Hystrix只能整合SpringCLoud

3.面试题:项目中使用了熔断机制吗?

项目使用了Sentinel作为熔断机制。Sentinel实现熔断机制主要有线程隔离和熔断降级
首先我们项目要在yml配置中开启Sentinel的线程隔离和熔断降级功能,然后在Sentinel界面加上隔离最大并发线程数或熔断参数配置,接着给Feign接口定制一个服务降级实现类,在隔离和熔断发生后,给用户提示友好信息。

feign:
 sentinel:
 enabled: true # 开启sentinel支持
public class UserClientFallbackFactory implements FallbackFactory {
 @Override
 public UserClient create(Throwable throwable) {
 return new UserClient() {
 @Override
 public User findById(Long id) {
 User user = new User();
 user.setUsername("查无此人");
 user.setAddress("查无地址");
 return user;
 }
 };
 }
}


1)线程隔离:
 在服务消费方加入服务调用占用线程数统计,一旦超过线程数上限,则做服务降级(在服务消费方定制一个降级处理方法,定制失败消息)

2)熔断降级:
 在服务消费方加入超时或异常比例统计程序,该程序一旦统计超过比例,一旦比例超过阈值,则做服务降级(在服务消费方定制一个降级处理方法,定制失败消息),熔断有时长,时间到达会尝试请求1次,如果成功,则正常调用,如果失败,继续熔断。

4.面试题:熔断 和 降级 的区别

1)熔断:是一个Sentinel或Hystrix框架的一个自带的机制,该机制统计超时比例或异常比例的程序。
当熔断达到异常比例或超时比例的阈值时,就会发生降级。
2)降级:其实本质就是一个Fallback实现类,返回给用户友好信息。
3)通常在项目中可以加入线程隔离或熔断机制,一旦程序线程隔离或者熔断了,就会进入降级。

5.面试题:请解释一下SpringCloud中Feign接口调用的过程?

将feign接口抽取成一个模块 然后去打包到本地仓库,消费方导入依赖。
首先,#Feign接口写在服务消费方,消费方在调用Feign接口的时候,会扫描Feign接口上面的注解(如
@FeignClient、@GetMapping @RequestParam等),然后拼接需要调用的url路径和参数值,接着,
在底层利用JDK动态代理产生代理对象,代理对象底层使用RestTemplate向服务提供方发出请求,获取响应结果

响应结果返回到消费方后,会进行结果解析(如返回json数据的话会利用Jackson框架转换为对象给我们返回)

6.面试题:能不能大概解释一下熔断器的执行流程?

首先,熔断器就是一个超时比例或异常比例的统计程序,该程序放在服务消费方,当超时比例或异常比例没有达到阈值,熔断器处于关闭状态,请求可以正常通过。
但是,当超时比例或异常比例到达阈值,熔断器开启,所有请求会立即被降级,请求降级后会执行我们定制的Fallback接口,返回给用户友好提示信息。
接着,熔断器会等待一段时间(如5s),然后进入半开状态,半开状态会放行一个请求尝试调用,如果失败,继续保持打开状态,如果成功,则回到关闭状态。

7、面试题:Nacos和Eureka的区别?

相同点:
1)两者支持服务注册和服务拉取
2)两者都支持服务者心跳机制实现健康检测(续约)
不同点:
1)Nacos可以实现服务注册发现,也可以做配置管理;Eureka只能做服务注册发现。
2)Nacos有非临时实例而Eureka没有,非临时实例在心跳不正常的时候是不会被剔除的。
2)Nacos临时实例心跳不正常会被剔除,非临时实例(永久实例)则不会被剔除;而Eureka只能注册
临时实例,实例失效会被剔除(Eureka不支持永久实例)
spring.cloud.nacos.discovery.ephemeral=false 创建永久实例
3)Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式;
而Eureka只有心跳模式;
4)Nacos支持服务列表变更的消息主动通知模式,服务列表更新更及时,减少服务调用失败的机率;
而Eureka采用被动定时服务列表拉取更新;

8.面试题:说一下你对微服务的了解

微服务介绍:
(1:微服务本身是一种思想,springcloud是它落地的一个实现。
(2:以前我们做的是单体应用,如果我们想更进一步就是微服务了。

微服务里面涉及到技术点(微服务里面有哪些组件):
1、比如说我们想将一个电商项目做成一个微服务:

				(1:像我们平时写的都是单体的,都是一个project,需要什么功能就往里面写,发布的时候就一个project,打一个jar或者war包就完事了。这种就是单体项目。
				(2:单体项目存在的问题:
						(2.1:开发复杂,不好维护(所有功能都写在一起,所以一个项目特别大,光是编译都要十几分钟,需要修改代码的时候重新跑起来都要很久)。
						(2.2:发版问题,项目功能之间耦合度太大,一个功能不能用整个系统不能用。像这种单体应用就是典型的一错具错
						(2.3: 版本升级不便。不敢轻易升级以前当体应用的项目,否则太多地方要改了,所以就存在着很多的老应用
						 (2.4:团队人比较多的时候也不便

电商项目比如说具有两个功能,使用微服务的话,那么你这个团队只负责“ 订单服务 ”,就只需要运行你们“ 订单服务 ”的代码就可了,不会别人负责的模块是否运行成功所影响。版本升级的话也只用升级自己的“ 订单服务 ” 模块,不影响其他的功能模块。通过接口调用别人的模块就可以了。

在微服务里面。功能模块之间互不影响,但是项目必须相互之间需要调用。(一个springboot需要调用另外一个springboot项目)。可是在微服务里面有一个原则,我们不能在需要调用一个功能模块的时候把地址写死了(为了松耦合,不让代码之间耦合度太高。),所以我们就需要一个“ 服务注册中心 ” 。

“ 服务注册中心 ” ,相当于一个电话本,所有服务启动的时候, 都要注册到 “ 服务注册中心 ”里面,将自己这个服务的基本信息告诉“ 服务注册中心 ”,像这样的话,大家需要调用其他模块的时候只需要记住“ 服务注册中心 ”的地址就可以找到所需的模块了。比如说服务注册中心上面有一个“ 订单服务 ” (服务名+ip地址+端口号,例bussiness:10.3.23.14:8080) 和一个“ 商品中心 ”功能(服务名+ip地址+端口号,例如order:10.4.5.6:9090),以及一个物流功能(服务名+ip地址+端口号,例如send_goods:91.32.23.71:7070)。除了服务名+ip地址+端口号这些信息,也要告诉服务注册中心自己有哪些功能(其实就是一些接口,表明自己能干啥)。服务中心组件有很多种 -------- Spring CloudEureka / Consul / Zookeeper / 以及阿里巴巴的Nacos / 携程弄得Apollo,上面提到的组件可能会有其他功能,但是都会有服务注册中心的功能。

还有一个问题,所有功能将信息告诉了服务注册中心,这时候比如功能A想要调用功能B服务,那么模块A就需要直到模块B的地址。那么模块A就需要先去服务注册中心查询模块B的地址,第二步再去调用,因此调用的工具也有跟多种:OpenFeign(Http调用工具)、Dubbo(dubbo协议,底层是socket)、RabbitMQ(消息驱动)。 可以使用这些工具实现服务之间的调用

但是毕竟是网络调用,无法确保每次调用都能成功。此时比如说通过模块A调用模块B,但是模块B挂了或者是网络断了由于网络请求调用不到,那么客户的所有请求都会积压在模块A上,本来模块A是好的,却在此时导致模块A挂了,不仅是模块A,其他想要调用故障模块的功能模块都将会奔溃,这就是我们说的 “ 服务雪崩 ”(也叫故障蔓延,本来就一个模块xx调不能被调用,其他模块会由于调不通故
障模块从而请求积压挂掉,这样的话故障就蔓延开来),因此我们需要解决这些问题,就是说当功能A调不通B的时候我们需要有一些容错机制确保一个模块调不通一个故障模块的时候给出一个响应,先不要
把好的功能给拖垮了,因此我们需要一些容错工具不让请求拖垮好的功能模块:
1.Hystrix
2.ResiLience4j( SpringCloud官方推荐的,但是大家并没有被大规模使用)
3.Sentinel(阿里巴巴提供的,目前使用的较多)

7、接下来我们还需要一个东西,就是说你会发现这些服务之间相互访问的时候,每个服务不可能直接让你访问,他们都会有一些安全认证机制、权限管理。要是有是上百个服务,就要记住上百个地
址,因此会很麻烦。所有我们就需要一个服务网关(服务的统一口),当客户端想要请求的时候,这些请求不会去访问那些功能服务模块,而是会去访问服务网关(服务的统一入口),在这里面进行请求预处理和请求分发、例如统一的权限校验等操作就会在这里完成。但是和Nginx是有区别的(服务网关是我们自己写的代码,自己定制自己的功能。而使用Nginx不能够自己编码,对开发来说不好做定制功能)。目前的服务网关主要有两个:
1.GateWay(异步的,非阻塞的,并发能力强很多。目前比较火)
2.Zuul(是一种阻塞式的,并发能力会弱一些。曾经比较火)

8、每一个服务模块大家都要配置他的信息(数据文件、redis配置、数据库配置),我们自己写的时候要写上百个配置文件,因此还需要有一个东西,叫做 “ 分布式配置中心 ”,将所有的配置文件放在这里统一进行管理。以后要修改配置信息的时候来这修改即可。因此以后这些服务模块启动的时候都需要来“ 分布式配置中心 ”读取配置信息。管理起来很方便。” 分布式配置中心的工具 “有:
1、Spring Cloud Config

  1. 我们还需要有一个工具叫做 “ 服务链路追踪 ” ,比如说我们的调用流程:客户端发请求–>服务网关–>模块A–>模块B等服务–>最终给我一个相应,结果这个调用过程中出错了,或者说在调用很慢的情况下,我们需要检查问题出在哪里,总不能一个一个去debug,这样就太慢了,此时就需要用到“ 服务链路追踪 ” 。工具有:
    1.zipkin,监控并产生数据。
    2.sleuth,把那些产生的数据给你串起来。

微服务流程所使用工具小结:

服务注册中心:eureka、zookeeper、consul、nacos、Apollo
分布式配置中心:Spring Cloud Config
服务网关:Spring Cloud Gateway\Spring Cloud Netfilx Zuul
服务之间调用:OpenFeign/Dubbo/Spring Cloud Bus/Spring Cloud Stream
服务容错、降级、限流:Resilience4j、Hystrix 、Sentienl
服务链路追踪:zipkin、sleuth

微服务三大技术流体系(各大公司都在弄这个微服务的开源工具,都是为了同时绑定销售他们的云服务):

阿里巴巴:Spring Cloud Alibaba( Dubbo(服务调用) 、nacos(服务注册中心和服务配置中
心功能)、Sentienl(限流工具)、seata(分布式事务)
赖飞:Spring Cloud Netflix(Eureka 、Feign (服务调用)、 Ribbon、Hystrix 、Zuul) (18年
开始,赖飞不在继续开源,因此不火了 )
官方:Spring Cloud ( Spring Cloud Config 、Spring Cloud Bus、Spring Cloud Stream )
注意点:在工作中一般都是搭配使用,因为他们每一个都有自己的强项,不是说非要学哪一套。

微服务复习:

服务注册中心+服务注册客户端(服务模块)

服务注册中心:主要讲两个(eureka 、nacos)前者是一个spingboot工程,后者是一个软件,其他的自己看笔记了解。

Eureka(SpringBoot工程) 创建过程:

1、创建一个空的工程,在它里面创建一个model(SpringBoot项目)
2、添加两个依赖,一个“ SpringWeb”,一个Eureka Server。
3、在主启动类上加一个注解,@EnableEurekaServer(开启Eureka服务端,也就是一个服务注册中心)
4、在配置文件application.properties中配置属性:
		(4.1:服务名称:spring.application.name=eureka
		  (4.2:是否将当前服务注册到eureka上,false表示不注册:(因为当前服务就是为了建立一个服务注册中心,没必要把自己注册到自己上面)eureka.client.register-with-eureka=false。
		  (4.3:是否获取eureka上注册的其他服务。false表示不要。(可能服务注册中心还有其他的服务,这里是询问你需不需要获取们):eureka.client.fetch-registry=false
		  	(4.4:修改一下端口号 :server.port=1111
		  	(4.5:通过输入localhost:1111(上面指定的端口号),查看网页版本的服务注册中心。

5、创建一个具体的微服务(Eureka Client端)模块添加到之前的工程中。需要添加两个依赖:Spring Web+ Eureka Discovery Client。注意点:Eureka Client端是微服务,需要注册到Eureka Server端也就是服务注册中心里面去的。

6、在主启动类中添加注解–@EnableEurekaClient,标记当前项目是一个Eureka客户端(微服务)

7、在这个微服务工程中的application.properties文件中添加配置:

	(7.1:给这个微服务项目取一个名字 :(一定要取名,否则接下来在服务注册中心里面注册上去的名字就叫Unknown) spring.application.name=client01
	(7.2:配置服务注册中心的地址:eureka.client.service-url.defaultZone=http://localhost:1111/eureka (defaultZone是一个默认的分区,如果你的服务注册中心不仅仅是当前项目还有很多项目在里面,那么可以分区,我们这里使用的是默认的分区)

Eureka集群的搭建

简介:Eureka集群其实就是将前面的Eureka.server服务端的项目启动多个,启动一个就只有一个,启动十个就只有十个(多个eureka的实例)。
搭建eureka集群具体步骤:
1、在电脑 windows目录下有个System32–>driver–>drivers–>etc–>host

#相当自定义了两个域名,一个叫eurekaA,一个叫eurekaB,他们的地址是127.0.0.1 127.0.0.1 eurekaA eurekaB +电脑的名字

	2.、按照上面的创建一个eureka-server端(服务注册中心),再搞一个applicationa.properties(运用之前学过的Profiles问题,一个项目可以有多个profile,启动的时候可以指定用哪一个环境去启动)(使用指令spring.profiles.active=a,指定配置环境为application-a.properties里的配置)。要将原先的eureka.client.register-with-eureka=true(将当前服务端注册到eureka上),同时eureka.client.fecth-registry=true(获取eureka上注册的其他服务)。application-a.properties的配置如下面代码
# 给当前服务取一个名字 
spring.application.name=eureka 
# 设置端口号 
server.port=1111 
# 设置域名名称(此名称代表了域名,比如在项目自己的计算机上,这个名称就代表了127.0.0.1)
 eureka.instance.hostname=eurekaA
# 默认情况下,Eureka Server 也是一个普通的微服务,所以当它还是一个注册中心的时候,他会有 两层 身份:1.注册中心;2.普通服务,即当前服务会自己把自己注册到自己上面来 # register-with- eureka 设置为 true,表示当前项目注册到注册中心上 eureka.client.register-with-eureka=true 
# 表示是否从 Eureka Server 上获取注册信息 
eureka.client.fetch-registry=true
 # A 服务要注册到 B 上面
  eureka.client.service-url.defaultZone=http://eurekaB:1112/eureka

3、再如法炮制创建一个application-b.properties,配置和上面差不多,相同的是服务名、都需要将自己注册到自己里面、都需要从 Eureka Server 上获取里面的注册信息 ,不同的是端口号为1112、域名名称为eurekaB、这次是要将服务注册到A上面。配置代码如下:

# 给当前服务取一个名字
 spring.application.name=eureka 
 # 设置端口号 server.port=1112
  # 设置域名名称(此名称代表了域名,比如在项目自己的计算机上,这个名称就代表了127.0.0.1)
   eureka.instance.hostname=eurekaB
    # 默认情况下,Eureka Server 也是一个普通的微服务,所以当它还是一个注册中心的时候,他会有 两层 身份:1.注册中心;2.普通服务,即当前服务会自己把自己注册到自己上面来
     # register-with- eureka 设置为 true,表示当前项目注册到注册中心上 eureka.client.register-with-eureka=true 
    # 表示是否从 Eureka Server 上获取注册信息
     eureka.client.fetch-registry=true
     # A 服务要注册到 B 上面
      eureka.client.service-url.defaultZone=http://eurekaA:1111/eureka

4、将此项目运用“ Pcakage ”功能打成一个jar包,并在使用“ java -jar xxxxx.jar(打好的jar包名) ”指令后面加上指定配置环境的指令:
–spring.profiles.active=a,然后启动。(注意,此时jar启动后可能会一直报错,因为内部会一直根据配置文件尝试注册服务到eurekaB去,而此时eurekaB还未启动,先不予理会,等后面eurekaB启动就好啦。)

5、按照第四步的方法启动一次jar包,只不过这次指定的环境是
–spring.profiles.active=b。启动后会根据配置文件将eurekaB注册到A中。

6、启动之后就可以在eureka网页端看到,两个互相注册的eureka服务端(服务注册中心)组成了一个集群。

eureka集群搭建小结:其实就是启动两个eureka实例,这两个eureka之间首先将自己注册到自身,然后和其他eureka服务端的互相注册即可。

那么问题来了,我要是有三个eureka搭建集群呢,那是不是一定这三个中,每个eureka服务端都需要注册另外两个的eureka呢。

答:假设有三个eureka服务端A、B、C三个,Eureka Server 的连接方式,可以是单线的,就是A–>B–>C ,此时,A 的数据也会和 C 之间互相同步。但是一般不建议这种写法,因为如果此时eurekaB挂了,那么A就连接不到C了,因此在我们配置 serviceUrl (目标eureka服务端的地址)时,可以指定多个注册地址,即 A 可以注册到 B 上,也可以同时注册到 C 上。这样B或者C任意一个挂了都不影响。

Eureka 的工作细节:
Eureka 的组成:
Eureka 本身可以分为两大部分,Eureka Server 和 Eureka Client

Eureka Server的功能:
(1:提供注册功能,所有的服务注册功能都注册到Eureka Server上面来。
(2:提供注册表,注册表就是所有注册上来的服务的一个列表,当Eureka Client调用服务注册中心(Eureka Server)的时候,需要获取这个注册表,一般来说这个注册表会缓存下来,只有等到缓存失效了,才会直接去获取最新的注册表。
(3:同步状态,其实就是Eureka 客户端通过注册、心跳机制等,和Eureka 服务端同步当前客户端的状态,如果某个Eureka客户端(服务)掉线了,就会立即告知其他的Eureka客户端(注册进来的服务),这个服务已经不能调用了。

Eureka Client的功能:
(1:服务注册:Eureka Client主要是用来简化每一个服务和Eureka Server(服务注册中心)之间的交互(其实本来可以自己提交一个http请求把带注册的服务信息告诉Eureka,但是那样太麻烦了),所以提供了一个Eureka-Client依赖包,直接通过配置属性就可以实现提供自己的一些元数据信息,例如ip地址、端口、名称、运行状态。
(2:服务续约:当客户端Eureka Client注册成功到Eureka Server之后,需要每隔三十秒就要向Eureka Server(服务注册中心)发送一条心跳消息,来告诉服务注册中心我还在,我没挂。如果连续九十秒没收到客户端的心跳消息(就是三次都没发,大概率排除网络的问题),那么服务端就会认为这个服务已经掉线了,将这个服务从服务注册列表中移除。
(3:服务下线:当Eureka Client下线时,会给Eureka Server主动发一条消息,告诉EurekaServer,我下线了。
(4:缓存注册表信息,客户端Eureka Client会每隔三十秒向Eureka Server获取最新的注册表信息并缓存在本地。注册表包含着(ip、端口等信息)。当然这里面存在着最大120秒的误差(注册表上的服务挂了服务端需要90s才能发现+客户端需要三十秒才能更新本地缓存的注册表),这个时候就需要容错机制来解决了(容错机制能够保证哪怕有120s的错误时间也能够正常运行)。

服务注册与调用

集群里面有两个eureka,其实只要往一个里面注册服务即可,两个之间会自己自动同步消息。

利用纯手写Http请求方式搭建服务注册与调用(Eureka客户端之间调用):
(1:新建一个模块“ storage”,相当于一个生产者,是一个商品的库存服务,添加两个依赖,一个springWeb,一个Client.
(2:新建一个模块“ business ”,也是添加两个依赖,一个springWeb,一个Client。是提供“从库存中拿商品的服务”,相当于消费者 。
(3:在“storage”模块中,新增一个Rest风格的RestController层,里面有一个“ deduct() ”方法,访问url为“/deduct”。在模块“Business”中,新增一个Rest风格的Controller层,里面有一个“ hello() ”方法,访问此接口的url为“ /hello ”
(4:分别使用以下配置将两个服务注入到服务注册中心(Eureka.server服务端):

storage:

spring.application.name=storage 
eureka.client.service-url.defaulZone=http://local:1111/eureka

business:

spring.application.name=business 
sever.port=2000 
eureka.client.service-url.defaulZone=http://local:2000/eureka

(5:在SpringBoot的主启动类上面使用注解@EnableEurekaClient,表示启用Eureka客户端。

(6:由于要在“business”服务上面访问“storage”,因此要得到“ ip、端口”,所以要到eureka.server(服务注册中心)去查询。因此要在“ Business ”模块里的controller类里面自动装配一个工具“ DiscoveryClient ” 类,用来在服务注册中心查询不同的服务信息。注意:看代码提示,discoveryClient有一个接口,是SpringCloud提供的,是一个规范、标准。另外的一个是赖飞提供的,相当于jdbc和mysql驱动的关系,我们自动装配的当然是SpringCloud规范的接口,这样以后可以自动切换任何体系(例如使用阿里巴巴的nacos来作为服务注册中心),而这一行代码却不用变。接下来根据服务名字查询具体服务,返回的是一个List集合,因为“storage”服务可能是集群化部署的,所以可能会有多个。代码如下:

@RestController 
public class HelloController {
//注意这里使用 Spring Cloud 中的接口 
//通过 DiscoveryClient 可以在 Eureka 上查询不同服务的信息
 @Autowired 
 DiscoveryClient discoveryClient;

/*** 这是一个用户下单的接口 */
@GetMapping("/hello") 
public void hello() throws IOException {
//根据服务名查询服务信息,由于 storage 可能是集群化部署,所以返回的是一个 List 集合 List<ServiceInstance> list = discoveryClient.getInstances("storage"); 
//获取一个实例对象 
ServiceInstance instance = list.get(count.getAndAdd(1) % list.size());
	}
}	

(7:因为我们这里只有一个“ storage ”服务,因此上面返回的list集合的第一个( list.get(0) )就是Storage服务的实例对象( ServiceInstance ),我们可以通过这个对象得到这个“storage”服务的主机地址和服务端口号。代码如下:

@RestController 
public class HelloController {
 //注意这里使用 Spring Cloud 中的接口 
 //通过 DiscoveryClient 可以在 Eureka 上查询不同服务的信息
 @Autowired 
 DiscoveryClient discoveryClient;
  AtomicInteger count = new AtomicInteger(1);
/*** 这是一个用户下单的接口 */
@GetMapping("/hello")
 public void hello() throws IOException {
  //根据服务名查询服务信息,由于 storage 可能是集群化部署,所以返回的是一个 List 集合 
  List<ServiceInstance> list = discoveryClient.getInstances("storage");
   //获取一个实例对象
    ServiceInstance instance = list.get(count.getAndAdd(1) % list.size()); 
    //获取 storage 服务的主机地址 
    String host = instance.getHost(); 
    //获取 storage 服务的端口号
     int port = instance.getPort();
     		 }
       }

(8 接下来纯手工去服务注册中心上查询并调用我想调用的那个服务的地址,然后利用得到的服务实例对象属性,我们自己拼接一个请求的url地址出来,然后用最原始的http工具去请求,并且同时处理负载均衡功能(均衡的给各个服务器分配任务),使用一个原子整形类(AtomicInteger类,之所以不用
Integer是为了线程安全,这个类在不同线程里是共享的,数值共享。)然后只要有线程进来就递增加,并且与List的长度求余实现请求多个时候的一个轮询功能。代码如下:

@RestController
public class HelloController { 
//注意这里使用 Spring Cloud 中的接口 
//通过 DiscoveryClient 可以在 Eureka 上查询不同服务的信息 
@Autowired 
DiscoveryClient discoveryClient; 
AtomicInteger count = new AtomicInteger(1);


/*** 这是一个用户下单的接口 */ 
@GetMapping("/hello")
public void hello() throws IOException { 
//根据服务名查询服务信息,由于 storage 可能是集群化部署,所以返回的是一个 List 集合 
List<ServiceInstance> list = discoveryClient.getInstances("storage");
 //获取一个实例对象 
 ServiceInstance instance = list.get(count.getAndAdd(1) % list.size());
  //获取 storage 服务的主机地址 String host = instance.getHost(); 
  //获取 storage 服务的端口号
   int port = instance.getPort(); 
   URL url = new URL("http://" + host + ":" + port + "/deduct"); 
   HttpURLConnection con = (HttpURLConnection) url.openConnection();
    //建立连接
     con.connect(); 
     if (con.getResponseCode() == 200) { 
     //说明请求成功 
     BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream())); String s = br.readLine(); System.out.println("s = " + s); br.close();
     String s = br.readLine(); 
     System.out.println("s = " + s); 
     br.close();
      			} 
    	  } 
      }

(9:在storage的配置文件中,添加端口属性,然后在Controller类中自动装配端口属性,并在“hello()”返回的时候加上端口值,代码如下:
storage端配置文件:

spring.application.name=storage
 eureka.client.service-url.defaulZone=http://local:1111/eureka 
 server.port=2008

storage的Controller类:

@RestController 
public class StorageController {
 @Value("${server.port}")
  Integer port;
   /*** 商品扣库存 * @return */
    @GetMapping("/deduct") 
    public String deduct() { 
    System.out.println("deduct"); 
    return "hello deduct:" + port;
    		 }
      }

(10:先启动原有的“storage” 服务, 然后将“ storage ” 模块打包,然后使用 java -jar xxxxxxxxxxxx.jar(打好的包) --server.port=2009再启动一次(注意端口号不同了),这样就构建出了“ storage ” 服务集群,这时候在网页端使用localhost:2000/hello调用“ business ”的服务,多调用两次,就会发现返回的值端口部分不一样,说明调用了“ storage ”集群部署的不同节点。

项目了解

1.请问你们项目团队大概多少人?怎么分配的?

上家公司的研发团队大概20人左右,1名架构师,13名后端开发,前端3人,有1人运维,1人做测试
的,1个项目经理和助理。
我们项目经理那边同时开发的不止一个项目,我当时参与的是那个外卖APP,大概有4名后端开发
一起做,前端那边6个人

2.你们项目大概有几个微服务模块?项目多少张表?

我的项目大概10几个模块,核心业务微服务大概5-6个左右,比如订单模块,支付模块,购物车模块,
商品服务,库存服务等。
因为每个微服务是独立数据库(10来个微服务,10个数据库),数据库表少的有几张,多的有几十张吧。

2.请说说你在项目中遇到的难点?(高频题)

分布式事务 :
我在乐玩APP项目中遇到一个下单锁定库存业务,该业务跨三个服务(商品模块、库存模块。订单模块),同时更新三个数据库的表,在运行中发现该业务如果失败了,一边表成功,一边表不成功,这种情况组长说不允许出现这种数据不一致的情况,后来知道了是分布式事务的问题。我去研究了下seata,顺利用seata的AT模式解决了这个问题。

3.项目中用到哪些设计模式?(必会)

单例模式:DataSource单例 ,Spring默认就是单例模式。
工厂模式:SqlSessionFactory创建SqlSession ( SqlSessionFactory.openSession())
模板方法模式:RabbitTemplate RedisTemplate
代理模式:Spring的AOP做日志(JDK动态接口代理或Cglib类代理)@Slf4j还有Spring事务使用注解
@Transactional,Spring事务底层原理也是Aop
建造者模式:SqlSessionFactoryBuilder ,Minio创建Client的时候也用了建造者模式

4.如何解决商品详情页或者其他页面 高并发 访问的问题?

场景:商品详情页,订单详情页,秒杀页面
我们会把页面进行Freemarker静态化,利用MinIO存储静态页面
流程:
首先,在后台商品发布后,立即为商品页面生成静态页面,这里采用Freemarker结合商品页面模板生
成静态页面
然后,我们把生成的商品详情静态页存储到MinIO,这样可以提高商品详情页访问的并发量,减少详
情页查询数据库的压力。
最后,用户直接访问MinIO中的静态页面,无需访问数据库
静态化技术有什么缺点?
每次商品内容修改,需要重新生成静态页面

5.知道ThreadLocal吗?讲讲你对ThreadLocal的理解 (项目中多线程并发安全问题如何解决?)

先讲作用:在同一个项目中的同一个线程范围内共享数据(一个线程存入数据,另一个线程无法读取或修改的)防止多线程之间并发问题
再讲使用方式:
其实ThreadLocal底层使用一种ThreadLocalMap结构,key是存储当前线程标记(
Thread.currentThread() ),value存储我们写入的值
set(Object obj) 往当前线程存入数据(例如JDBC连接即Connection对象),底层 map.put(‘当前线
程唯一标记’,obj);
Object get(): 从当前线程取出之前存入的数据,底层Object map.get(‘当前线程唯一标记’)
remove(): 移除当前线程存入的数据,底层 map.remove(‘当前线程唯一标记’)
最后讲注意事项:(ThreadLocal可能存在内存泄漏的问题(可能存在并发数据问题),怎么解决?)
在使用完TheadLocal的数据后,建议手动移除线程数据,防止内存泄漏( 由于疏忽或错误造成程序未
能释放已经不能再使用的内存 )和防止并发数据问题(比如说Tomcat服务器里面的线程池的线程本身可以复用,由于没有移除,就会被一直锁在里面)。

6.如何避免定时任务的重复调度?

利用分布式任务调度技术,如XXL-Job
XXL-Job创建任务时可以设置任务调度规则,规则可以设置为轮询策略或随机或分片广播

7 你们项目的日志怎么管理?线上日志如何查看?

项目日志管理: 我们是SpringBoot架构的项目,采用的是默认的日志框架logback,只需要在resources下面配置 logback.xml,定义日志格式和输出目录即可。通常项目会1天产生1个日志文件。 在一些业务接口执行成功或者失败的时候,通过log.info或log.error方法记录业务日志。
线上日志查看的几个常用Linux命令:
head -n 10 test.log 查询日志文件中的头10行日志
tail -n 10 test.log 查询日志尾部最后10行的日志;
cat -n test.log | grep “debug” 查询关键字的日志
docker logs -f 容器ID/容器名称

8.请问了解过敏捷开发吗?

了解过,所谓敏捷开发就是区别于瀑布模型的开发模式,主要特点是增量和迭代开发。 迭代开发将一个大任务,分解成多次连续的开发,本质就是逐步改进(每一次的迭代会在上次迭代之前进行增 量开发)。开发者先快速发布一个有效但不完美的最简版本,然后不断迭代。每一次迭代都包含规划、设计、 编码、测试、评估五个步骤,不断改进产品,添加新功能。通过频繁的发布,以及跟踪对前一次迭代的反馈, 最终接近较完善的产品形态。
你们项目多久迭代一次? 2周左右

9.什么是瀑布模型?

在这里插入图片描述

瀑布模型是一个软件开发生命周期模型,开发过程是通过设计一系列阶段顺序展开的,从系统需求分析开始直 到产品发布和维护,项目开发进程从一个阶段“流动”到下一个阶段 优点:可强迫开发人员采用规范的方法;严格的规定了每个阶段必须提交什么文档 缺点:不适合需求改动较多的项目

10.解决秒杀的几种方式:

1:乐观锁,给数据库添加字段名version, 通过利用 version 字段来判断数据是否被修改
2:对于上面超卖现象,主要问题出现在事务中锁释放的时机,事务未提交之前,锁已经释放。(事务
提交是在整个方法执行完)。如何解决这个问题呢,就是把加锁步骤提前
可以在 controller 层进行加锁
对于上面在控制层进行加锁的方式,可能显得不优雅,那就还有另一种方式进行在事务之前加锁,
那就是 AOP ,可以使用 Aop 在业务方法执行之前进行加锁
3:利用阻塞队类,也可以解决高并发问题。其思想就是把接收到的请求按顺序存放到队列中,消费者
线程逐一从队列里取数据进行处理,看下具体代码。
阻塞队列:这里使用静态内部类的方式来实现单例模式,在并发条件下不会出现问题。

11请问你们的前后端项目开发流程?(请问你们需求开发的流程?)

1)需求分析。分析业务需求,要实现什么功能
2)定义接口。接口包含请求方式,请求路径,请求参数,响应返回值。后端需要和前端工程师商量
3)开发和测试接口。后端一般使用poastman来测试接口
4)和前端联调。找前端一起调试接口。

12有没有做过持续集成或持续部署技术?

部署的过程,一般由公司运维团队去完成,但我也有了解过持续集成,它就是一套自动化项目部署的环境和流 程,项目的需求变更可以非常快速在该平台上部署。 一般采用的软件是Jenkins。

13请问在你们项目的git是怎么使用的?(你们项目组对Git的使用有什么规范?)

1)我们组长一般分配给我任务的时候,一般该任务会开启该特定分支(不是master分支)。
2)涉及该任务的开发人员,拉取项目代码到最新版本,然后必须在本地切换到特定分支进行代码编写
3)组员写各自的代码后提交到该特定分支,然后组长审查该分支下各组员的代码
4)由组长把分支代码合并到master上面去 在这个过程可能涉及的细节:
++ 1)代码提交时做好备注(根据项目的风格进行备注,模块名:类:方法:功能)
++ 2)组员之间的代码冲突非常注意合并细节(小的冲突直接在IDEA处理完毕进行提交,大的冲突人为介入解 决)

14.怎么保证一个工具类,不被实例化

方法1:将该类定义成抽象类
这种方式虽然能避免该类不能被创建实例,但是他的子类可以创建对象
方法2:将该类的构造方法私有化
这种方式正常情况下,不能new对象,但是,可以通过java反射,来创建对象,所以该方式也不可行
方式3:在方法2的基础上,在私有构造方法中,抛出异常

15.项目中哪里用到多线程?(必问)

在商品数据从MySQl导入到Elasticsearch时,因为mysql商品数量很大,不能一次性导入,否则会内存
溢出(OOM, out of memeory)。
该业务采用多线程+ES批量写入完成。
//分页查询(1页查询200条)
ExecutorService service = Executors.newFixedThreadPool(5);
for( xxxxx ){
 if(i%200=0){
 CompletableFuture.runAsync(new Runnable(){
 public void run(){
//创建批处理对象
 BulkRequest bulkRequest = new BulkRequest();//缓存区
 //将写入请求加入到批量处理对象中
 for(ApArticle apArticle:apArticleList){
 //放入缓存区
 bulkRequest.add(request);
 }
 //执行批处理请求(真正把数据发送到ES执行)
 highLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
 }
 },service );
 }
}

16.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值