面试基础

280 阅读56分钟

Java

设计模式

单利模式

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这 个实例。

1)懒汉式 public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private static Singleton instance = null;
/* 私有构造方法,防止被实例化 */
private Singleton() {
    }

/* 1:懒汉式,静态工程方法,创建实例 */ public static Singleton getInstance() {
if (instance == null) { 
instance= new Singleton();
    }
return instance;
    }
}
(2)饿汉式
public class Singleton {
/* 持有私有静态实例,防止被引用 */
private static Singleton instance = new Singleton();
/* 私有构造方法,防止被实例化 */
private Singleton() {
    }
/* 1:懒汉式,静态工程方法,创建实例 */ 
public static Singleton getInstance() {
    return instance;
        }
}

java线程池的实现原理

corePoolSize:线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;

keepAliveTime:线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用;

workQueue:用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列: 1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务; 2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene; 3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene; 4、priorityBlockingQuene:具有优先级的无界阻塞队列;

handler:线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略: 1、AbortPolicy:直接抛出异常,默认策略; 2、CallerRunsPolicy:用调用者所在的线程来执行任务; 3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务; 4、DiscardPolicy:直接丢弃任务; 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

三个线程同时执行

countdownlatch、cyclibarrier、Semaphore.

线程顺序执行 使用valita定义变量 根据不同线程修改下一个线程需要的变量

image.png

线程交替执行

image.png

image.png

mysql索引

为什么不用红黑树?

红黑树在数据量大时树的高度特别大,索引查找缓慢。

mysql使用b-树的变种b+树

所有的索引数据存储在叶子节点data上,非叶子节点只存储索引,叶子节点使用指针连接,提高区域访问性能。叶子节点数据递增,叶子节点数据左边的索引等于父节点左边的索引,叶子节点数据右边的索引小于父节点右边的索引,数据有序

image-20211025113119590

每一块节点分配的磁盘空间(页)为16kb(16384byte)

innodb非主键索引 叶子节点存储的是主键的索引。

innodb推荐主键自增,原因是索引结构使用主键来生成和维护,若没有主键,则会自己找一个列此列数据不重复,生成唯一索引。

若主键自增则在增加数据时是做累加,对磁盘块数据结构不会做分裂。

回表

普通列的索引叶子节点并不存储整行数据,存储主键,通过主键的b+树去查找对应的数据。

覆盖索引
select id from user where name='zhangsan';

name:普通索引,此sql语句通过name查找到数据,此时的data域存储的是主键id,由于select查找的就是id 所以不需要回表,称谓覆盖索引

最左原则: idx(name,age,position)

image-20211025143456833

按照name ,age、position排序类似order by name ,age,position。

第一个name字段是绝对有序的,而第二字段就是无序的了

使用时where age=30 and name='bill' 会使用索引,mysql查询优化器登场,会判断纠正这条sql语句该以什么样的顺序执行效率最高

image-20211026144859781

索引失效(联合索引)

image-20211026153218026

where b=1

在(a,b)联合索引中,此索引在磁盘块中是按照a、b分别排序的,若查询时只使用到a那么此时这块区域的数据是无序的,那么就无法通过二分查找找到数据,不会使用到索引,则会进行全表扫描。

范围查找右边失效

where a>1 and b=1

可以定位到a但是b是无序的,那么就无法通过二分查找找到数据,不会使用到索引,则会进行全表扫描。

前置% 索引失效

因为最左原则,数据是按照从左到右的顺序排列即左边相同位数下是有序的,右侧是无序的,那么就无法通过二分查找找到数据,不会使用到索引,则会进行全表扫描。

MySQL存储引擎MyISAM与InnoDB区别

image.png

image.png

MyISAM索引与InnoDB索引的区别?

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

image.png

B-tree索引

B-树就是B树,多路搜索树,树高一层意味着多一次磁盘I/O,下图是3阶B树

image.png

B树的特征:
  • 关键字集合分布在整颗树中;
  • 任何一个关键字出现且只出现在一个结点中;
  • 搜索有可能在非叶子结点结束;
  • 其搜索性能等价于在关键字全集内做一次二分查找;
  • 自动层次控制;

B+TREE

B+树是B-树的变体,也是一种多路搜索树

image.png

B+树的特征:
  • 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
  • 不可能在非叶子结点命中;
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
  • 每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历。
  • 更适合文件索引系统; innodb的索引文件既包括索引也包含数据。

myisam索引文件和数据文件是分开的,索引文件存储的是数据的地址。

mysql优化

避免select* 尽量走覆盖索引 小表驱动大表 使用exits或者in(exits适用左边小表右边大表,in适用左边大表右边小表) 用连接查询代替子查询 控制索引数量不要超过5个

Spring相关

BeanDefinition类是通过注解或者配置文件声明的bean的信息描述的类。

image-20220106165824837

spring容器即beanFactory

创建容器,加载配置文件或解析注解类,封装成BeanDefinition类,再通过PostProcessor接口的子类beanFactoryPostProcessor里面的invoke方法对BeanDefinition类的信息进行设置修改替换等操作,然后进行实例化(在堆中开辟空间 ,属性是默认值);初始化(填充属性,完成额外的功能;调用Aware接口的方法,调用BeanPostProcessor(针对Bean对象)的before方法;再调用init方法;再调用after方法),最后得到完整的对象。

BeanPostProcessor做的是额外的扩展点如AOP,获取代理对象创建代理、获取代理

Bean的生命周期

image.png

1、实例化一个Bean--也就是我们常说的new;

2、按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;

3、如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值

4、如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);

5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);

6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;

7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。

8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;

注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述。

9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;

10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

image.png

循环依赖

解决:实例化初始化分开;通过三级缓存解决

三级缓存,提前暴露对象,AOP。

总:什么是循环依赖

分:说明bean的创建过程:实例化、初始化(填充属性)

1、先创建A对象,实例化A对象,此时A对象的b属性为空

2、从容器中查找B对象,如果找到了直接赋值,

3、否则创建B对象,此时B对象的a属性为空,

4、从容器中查找A对象,找不到则创建A对象

只完成实例化未完成初始化,此时可以将非完整的对象优先赋值,相当于提前暴露不完整的对象引用,解决问题的核心是将实例化和初始化分开操作。使用不同的map结构存储,就有了一级二级缓存,一级放的完整对象,二级是非完整对象;

3级缓存的value是函数式接口,意义是在整个容器运行过程中同名的bean只能有一个。

SpringAop底层原理

aop是ioc的一个扩展,先有的ioc再有的aop,在ioc的整个流程中新增的一个扩展点而已:beanPostProcessor

总:aop概念,应用场景,动态代理

分:

​ bean的创建过程有一个步骤可以对bean进行扩展,所以在beanPostProcessor的后置处理方法进行实现

​ 1、代理对象的创建过程

​ 2、通过jdk或者cglib 的方式生成代理对象

​ 3、在执行方法调用的时候,会调用到字节码文件中直接找到DynamicAdvisoredIntercepter类中找到intercepet方法,从此方法开始执行

​ 4、根据之前定义好的通知生成拦截器链

​ 5、从拦截器链中获取每一个通知开始执行

springAop底层是通过 class.forName() 方法创建的原生对象,在创建之前,先从commonClassCache中获取(这个commonClassCache是hashMap),如果没有,在通过上述方法创建,在放进concurrentHashMap之前,通过this.applyBeanPostProcessorsBeforeInitialization(bean, beanName)方法调用后置处理器对这个bean进行处理,由原生对象转换

Spring的事务是如何会滚的

Spring的事务管理是如何实现的?

总:spring的事务由aop实现,首先生成代理对象,然后按照sop的流程执行具体的操作逻辑,正常情况下通过通知完成核心功能,但在sop中则不是,而是通过TransactionIntercepter来实现的,然后调用invoke来实现具体的逻辑。

分:1、准备工作,解析各个方法上事务相关的属性,根据的具体的属性判断是否开启新事务,

​ 2、需要开启时,获取数据库连接,关闭自动提交。

​ 3、执行sql逻辑

​ 4、在操作过程中,执行失败,通过completeTransactionAfterThrowing完成事务的回滚操作,具体逻辑是doRollBack方法实现 ,实现时需要先获取连接对象,通过连接对象来回滚。

​ 5、如果没有意外,通过commitTransactionAfterReturning完成事务的提交操作,具体是通过doCommit方法提交。

​ 6、事务执行完毕之后需要清除事务相关信息cleanUpTransactionInfo

Spring的事务传播

传播特性有几种? 7种

Required,Requires_new,nested,Support,Not_Support,Never,Mandatory某一个事务嵌套另一个事务的时候怎么办?

​ A方法调用B方法,AB方法都有事务,并且传播特性不同,那么A如果有异常,B怎么办,B如果有异常,A怎么办?

​ 总:事务的传播特性指的是不同方法的嵌套调用过程中,事务应该如何进行处理,是用同一个事务还是不同的事务,当出现异常的时候会回滚还是提交,两个方法之间的相关影响,在日常工作中,使用比较多的是required,Requires_new,nested

SpringBoot自动装配

run方法从一个使用了默认配置的指定资源启动一个SpringApplication并返回ApplicationContext对象,这个默认配置如何指定呢?这个默认配置来源于@SpringBootApplication注解,这个注解是个复合注解,里面还包含了其他注解。

其中有三个注解是比较重要的:

  1. @SpringBootConfiguration:这个注解的底层是一个@Configuration注解,意思被@Configuration注解修饰的类是一个IOC容器,支持JavaConfig的方式来进行配置;

  2. @ComponentScan:这个就是扫描注解的意思,默认扫描当前类所在的包及其子包下包含的注解@Controller/@Service/@Component/@Repository等注解加载到IOC容器中;

  3. @EnableAutoConfiguration:这个注解表明启动自动装配,里面包含连个比较重要的注解@AutoConfigurationPackage和@Import。

    @AutoConfigurationPackage和@ComponentScan一样,也是将主配置类所在的包及其子包里面的组件扫描到IOC容器中,但是区别是@AutoConfigurationPackage扫描@Enitity、@MapperScan等第三方依赖的注解,@ComponentScan只扫描@Controller/@Service/@Component/@Repository这些常见注解。所以这两个注解扫描的对象是不一样的。 @Import(AutoConfigurationImportSelector.class)是自动装配的核心注解,AutoConfigurationImportSelector.class中有个selectImports方法

    selectImports方法还调用了getCandidateConfigurations方法

    get

    getCandidateConfigurations方法中,我们可以看下断言,说找不到META-INF/spring.factories,由此可见,这个方法是用来找META-INF/spring.factories文件的

    factory

    spring.factory文件是一个以key为EnableAutoConfiguration的全类名,value是一个AutoConfiguration类名的列表,以逗号分割的文件

    最终,@EnableAutoConfiguration注解通过@SpringBootApplication注解被间接的标记在了SpringBoot的启动类上,SpringApplicaton.run方法的内部就会执行selectImports方法,进而找到所有JavaConfig配置类全限定名对应的class,然后将所有自动配置类加载到IOC容器中。

    总结:SpringBoot启动的时候通过@EnableAutoConfiguration注解找到META-INF/spring.factories文件中的所有自动配置类,并对其加载,这些自动配置类都是以AutoConfiguration结尾来命名的。它实际上就是一个JavaConfig形式的IOC容器配置类,通过以Properties结尾命名的类中取得在全局配置文件中配置的属性,如server.port。 *Properties类的含义:封装配置文件的相关属性。 *AutoConfiguration类的含义:自动配置类,添加到IOC容器中。

SpringBoot启动流程总结

执行main方法
public static void main(String[] args) {
    //代码很简单SpringApplication.run();
	SpringApplication.run(ConsumerApp.class, args);
}
public static ConfigurableApplicationContext run(Class<?> primarySource,
			String... args) {
        //这个里面调用了run() 方法,我们转到定义
		return run(new Class<?>[] { primarySource }, args);
	}
//这个run方法代码也很简单,就做了两件事情
//1、new了一个SpringApplication() 这么一个对象
//2、执行new出来的SpringApplication()对象的run()方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

上面代码主要做了两件事情。第一步new了一个SpringApplication对象 ,第二部调用了run()方法。接下来我们一起看下new SpringApplication() 主要做了什么事情。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		//1、先把主类保存起来
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		//2、判断运行项目的类型
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		//3、扫描当前路径下META-INF/spring.factories文件的,加载ApplicationContextInitializer接口实例
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		//4、扫描当前路径下META-INF/spring.factories文件的,加载ApplicationListener接口实例
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

利用SPI机制扫描 META-INF/spring.factories 这个文件,并且加载 ApplicationContextInitializer、ApplicationListener 接口实例。

1、ApplicationContextInitializer 这个类当springboot上下文Context初始化完成后会调用

2、ApplicationListener 当springboot启动时事件change后都会触发

总结:上面就是SpringApplication初始化的代码,new SpringApplication()没做啥事情 ,利用SPI机制主要加载了META-INF/spring.factories 下面定义的事件监听器接口实现类

2、执行run方法
public ConfigurableApplicationContext run(String... args) {
        <!--1、这个是一个计时器,没什么好说的-->
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
        <!--2、这个也不是重点,就是设置了一些环境变量-->
        configureHeadlessProperty();
 
        <!--3、获取事件监听器SpringApplicationRunListener类型,并且执行starting()方法-->
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
 
		try {
 
            <!--4、把参数args封装成DefaultApplicationArguments,这个了解一下就知道-->
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
 
            <!--5、这个很重要准备环境了,并且把环境跟spring上下文绑定好,并且执行environmentPrepared()方法-->
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
 
            <!--6、判断一些环境的值,并设置一些环境的值-->
			configureIgnoreBeanInfo(environment);
 
            <!--7、打印banner-->
			Banner printedBanner = printBanner(environment);
 
            <!--8、创建上下文,根据项目类型创建上下文-->
			context = createApplicationContext();
 
            <!--9、获取异常报告事件监听-->
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
 
            <!--10、准备上下文,执行完成后调用contextPrepared()方法,contextLoaded()方法-->
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
            <!--11、这个是spring启动的代码了,这里就回去里面就回去扫描并且初始化单实列bean了-->
            //这个refreshContext()加载了bean,还启动了内置web容器,需要细细的去看看
			refreshContext(context);
 
            <!--12、啥事情都没有做-->
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
    
            <!--13、执行ApplicationRunListeners中的started()方法-->
			listeners.started(context);
 
            <!--执行Runner(ApplicationRunner和CommandLineRunner)-->
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, listeners, exceptionReporters, ex);
			throw new IllegalStateException(ex);
		}
		listeners.running(context);
		return context;
	}
2.1、createApplicationContext()

一起来看下context = createApplicationContext(); 这段代码,这段代码主要是根据项目类型创建上下文,并且会注入几个核心组件类。

protected ConfigurableApplicationContext createApplicationContext() {
	Class<?> contextClass = this.applicationContextClass;
	if (contextClass == null) {
		try {
			switch (this.webApplicationType) {
			case SERVLET:
				contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
				break;
			case REACTIVE:
				contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
				break;
			default:
				contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
			}
		}
		catch (ClassNotFoundException ex) {
			throw new IllegalStateException(
					"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
		}
	}
	return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

总结:

1、new了一个SpringApplication对象,使用SPI技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例

2、调用SpringApplication.run() 方法

3、调用createApplicationContext()方法创建上下文对象,创建上下文对象同时会注册spring的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等)。

4、调用refreshContext() 方法启动Spring容器和内置的Servlet容器

Mybatis

插件原理(即拦截器)

Mybatis插件又称拦截器,Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为。MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的方法调用包括:

  1. Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) 拦截执行器的方法。

  2. ParameterHandler (getParameterObject, setParameters) 拦截参数的处理。

  3. ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理。

  4. StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理。

  5. Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。

  6. StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

  7. ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。

  8. ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。

    1、拦截器链InterceptorChain

    这个pluginAll方法就是遍历所有的拦截器,然后顺序执行我们插件的plugin方法,一层一层返回我们原对象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理对象。当我们调用四大接口对象的方法时候,实际上是调用代理对象的响应方法,代理对象又会调用四大接口对象的实例。

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    
      public Object pluginAll(Object target) {
        //循环调用每个Interceptor.plugin方法
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    
      public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
      }
      
      public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
      }
    }
    

    2、Configuration通过初始化配置文件把所有的拦截器添加到拦截器链中。

    public class Configuration {
    
        protected final InterceptorChain interceptorChain = new InterceptorChain();
        //创建参数处理器
      public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        //创建ParameterHandler
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        //插件在这里插入
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
      //创建结果集处理器
      public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        //创建DefaultResultSetHandler
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        //插件在这里插入
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    
      //创建语句处理器
      public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        //创建路由选择语句处理器
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        //插件在这里插入
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }
    
      public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
      }
    
      //产生执行器
      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        //这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        //然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        //如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        //此处调用插件,通过插件可以改变Executor行为
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    }
    
    

    这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,那么下面我们在来看pluginAll干了什么

    3、Interceptor接口

    public interface Interceptor {
        Object intercept(Invocation invocation) throws Throwable;
        Object plugin(Object target);
        void setProperties(Properties properties);
    }
    setProperties方法是在Mybatis进行配置插件的时候可以配置自定义相关属性,即:接口实现对象的参数配置
    plugin方法是插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin.wrap(target, this);
    intercept方法就是要进行拦截的时候要执行的方法
    

    官方推荐插件开发方式

    @Intercepts({@Signature(type = Executor.class, method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    public class TestInterceptor implements Interceptor {
        public Object intercept(Invocation invocation) throws Throwable {
            Object target = invocation.getTarget(); //被代理对象
            Method method = invocation.getMethod(); //代理方法
            Object[] args = invocation.getArgs(); //方法参数
            // do something ...... 方法拦截前执行代码块
            Object result = invocation.proceed();
            // do something .......方法拦截后执行代码块
            return result;
        }
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    }
    
    

sentinel

降级是解决系统资源不足和海量业务请求之间的矛盾。

在暴增的流量请求下,对一些非核心流程业务、非关键业务,进行有策略的放弃,以此来释放系统资源,保证核心业务的正常运行,尽量避免这种系统资源分配的不平衡,打破二八策略,让更多的机器资源,承载主要的业务请求。服务降级不是一个常态策略,而是应对非正常情况下的应急策略。服务降级的结果,通常是对一些业务请求, 返回一个统一的结果,可以理解为是一种FailOver快速失败的策略。一般通过配置中心配置开关实现开启降级

熔断模式保护的是业务系统不被外部大流量或者下游系统的异常而拖垮。

如果开启了熔断,订单服务可以在下游调用出现部分异常时,调节流量请求,心如在出现10%的失败后,减少50%的流量请求,如果继续出现50%的异常,则减少80%的流量请求;相应的,在检测的下游服务正常后,首先恢复30%的流量,然后是50%的流量,接下来是全部流量

1.流量控制

QPS流控

order-sentinels模块

 @GetMapping("/flow")
    @SentinelResource(value = "flow",blockHandler = "flowBlockHandler")
    public String getFlow(){

        System.out.println ("flow");
        return "flow成功";
    }
    public String flowBlockHandler(BlockException e) {
        return "被限流了.....";
    }

image-20211020120458042

快速访问接口即可得到限流

image-20211020134222665

并发数线程流控
@GetMapping("/flowThread")
@SentinelResource(value = "flowThread",blockHandler = "flowBlockHandler")
public String getFlowThread() throws InterruptedException {
    TimeUnit.SECONDS.sleep(5);
    System.out.println ("flow");
    return "flowThread成功";
}

使用两个客户端在5秒内发起请求

image-20211020135214061

浏览器

image-20211020135315655

apipost测试工具

image-20211020135347212

统一异常处理

处理异常类

@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    Logger log = LoggerFactory.getLogger (this.getClass ());
    @Override
    public void handle (HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException e) throws Exception {
        Result r = null;
        if ( e instanceof FlowException ) {
            r = Result.error (100, "接口限流");
        } else if ( e instanceof DegradeException ) {
            r = Result.error (101, "服务降级");
        } else if ( e instanceof ParamFlowException ) {
            r = Result.error (102, "热点参数限流");
        } else if ( e instanceof SystemBlockException ) {
            r = Result.error (103, "系统保护规则");
        } else if ( e instanceof AuthorityException ) {
            r = Result.error (104, "授权规则不通过");
        }
        response.setStatus (500);
        response.setCharacterEncoding ("utf-8");
        response.setContentType (MediaType.APPLICATION_JSON_VALUE);
        new ObjectMapper ().writeValue (response.getWriter (), r);
    }
}

取消接口flow的@SentinelResource注解,在dashboard中设置QPS流控规则/order/flow接口进行限流,快速访问:http://localhost:8861/order/flow

image-20211020141642935

1.1 流控高级设置

流控模式

image-20211020142014770

默认为直接模式、快速失败

  1. 直接

    资源调用达到设置的阈值后直接流控抛出异常

  2. 关联

    image-20211020142438280

    对order/get进行限流设置 在关联资源位置关联order/add,那么order/add接口每秒超过2个就会触发order/get接口的限流

    image-20211020143201286

    使用jemeter对order/add接口进行访问100个线程100秒,启动后再访问order/get接口结果如下

    image-20211020152046676

  3. 链路

    image-20211020153448866

    a、b接口都调用c那么在链路模式中可对入口资源a或者b进行限制

    代码

    @Autowired
    private OrderService orderService;
    @GetMapping ( "/test1" )
    public String test1 () {
        return orderService.getUser ();
    }
    @GetMapping ( "/test2" )
    public String test2 () {
        return orderService.getUser ();
    }
    
    
@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @SentinelResource(value = "getUser")
    public String getUser () {
        return "查询用户getUser.....";
    }
}

为order/test1设置流控

image-20211020155124769

快速访问order/test2是被限流,但是返回信息是500代码

image-20211020155416401

填坑:默认将链路收敛起来不维护 ,需将其设置为false则是维护,将会展开链路

在配置文件中加入一下配置

web-context-unify: false 

image-20211020155704645

再次操作任然是500页面,是因为使用了@SentinelResource在此处不会使用统一异常,增加参数blockHandler = "getUserHandler"

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    @SentinelResource(value = "getUser",blockHandler = "getUserHandler")
    public String getUser () {
        return "查询用户getUser.....";
    }
    public String getUserHandler (BlockException e) {
        return "service中的查询用户限流了.....";
    }
}

再次快速访问/order/test2,结果限流了,success

image-20211020160539189

流控效果

image-20211020161237359

  1. 快速失败(默认效果)

    超出请求直接丢弃

  2. WarmUp(预热)

    在预热时常内慢慢接收配置的单机阈值请求个数。

    image-20211020161846974

    http://localhost:8861/order/flow设置warmUp流控效果 阈值设置10个,预热时常设置为5,然后快速访问即可得到慢慢增加访问成功的jemeter结果

    image-20211020162901006

  3. 排队等候

    脉冲流量,一上一下 可设置阈值 , 那么超过5个之外的请求将进行排队,可以利用低谷时期的时间a、b处理排队的请求

    image-20211020165624989

    image-20211020165851443

2.熔断降级

image-20211020170050522

image-20211020170148414

慢调用比例

image-20211020170356797

测试

	@GetMapping ( "/flowThread" )
    @SentinelResource ( value = "flowThread", blockHandler = "flowBlockHandler" )
    public String getFlowThread () throws InterruptedException {
        TimeUnit.SECONDS.sleep (2);
        System.out.println ("flow");
        return "flowThread成功";
    }

添加降级策略

image-20211020171524242

测试结果:

触发降级之后,再访问则会返回降级信息,10秒之后若不是慢调用则会访问成功,否则直接返回降级信息,不适用配置信息做返回的判断依据。

异常比例

image-20211020172757638

5此请求异常之后将会直接降级返回10秒内都是降级信息,半开状态,第一次请求若是异常则再等10,10秒内直接返回降级信息,

热点参数

接口准备

	@GetMapping ( "/get/{id}" )
    @SentinelResource ( value = "getById", blockHandler = "hotFlowBlockHandler" )
    public String getById (@PathVariable ( "id" ) Integer id) {
        System.out.println ("正常访问");
        return "正常访问";
    }

参数索引即参数在方法的哪个位置

![image-20211020174021665](/image-20211020174021665.png

image-20211020174624106

1秒内10个请求有2个请求id参数为1的数据进行降级

3.规则持久化

推模式

image-20211021102529118

依赖

		<dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>		

配置文件

nacos配置

[
    {
   "resource":"/order/flow",
   "controlBehavior":0,
   "count":2,
   "grade":1,
   "limitApp":"default",
   "strategy":0
    }
]	

模块yml文件

spring:
  application:
    name: order-sentinel
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.187.139:8858
      web-context-unify: false #默认将链路 收敛起来   不维护  需将设置为false则是维护
      datasource:
        flow-rule:
          server-addr: 192.168.187.139:8847
          username: nacos
          password: nacos
          dataId: order-sentinels-flow-rule
          rule-type: flow

4、sentinel限流流程总结

  1. 资源被entry节点包围,资源包装StringResourceWrapper。
  2. 初始化当前线程上下文root节点以及入口节点(获取当前线程上下文内容)。
  3. 查找/构建当前资源存在的资源调用链。
  4. 链式调用节点的entry方法,统计数据/判断规则是否生效。 调用顺序为: NodeSelectorSlot-ClusterBuilderSlot-StatisticSlot-SystemSlot-AuthoritySlot-FlowSlot-DegradeSlot

cloud相关

微服务架构与单体应用的区别
单体架构

单体架构存在的问题: 复杂性高 部署频率低 可靠性差 扩展能力受限

微服务架构

微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful-API)。 复杂度可控 独立部署 技术选型灵活 容错 扩展

CAP原理和BASE理论

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

CAP原则是NOSQL数据库的基石。 分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成 数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CAP三个特性只能满足其中两个,那么取舍的策略就共有三种:

CA

without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。传统的关系型数据库RDBMS:Oracle、MySQL就是CA。

CP

without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。

AP

wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,

为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。

nacos注册

springboot启动:

在bean的是实例化时会将NacosServiceRegistry和nacosAutoServiceRegistration注册到spring容器中,然后会发布一个事件

NacosServiceRegistry一旦监听到就会调用onApplicationEvent方法,然后会调用父类的AbstractAutoServiceRegistration的bind方法再调用start()方法调用register()会调用nacos-cilent中注册方法调用一系列API方法完成注册

客户端
//监听者设计模式 
public void onApplicationEvent(WebServerInitializedEvent event) {
        this.bind(event);
    }
//
public void bind(WebServerInitializedEvent event) {
        ApplicationContext context = event.getApplicationContext();
        if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
            this.port.compareAndSet(0, event.getWebServer().getPort());
            this.start();
        }
    }
//
public void start() {
        if (!this.isEnabled()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Discovery Lifecycle disabled. Not starting");
            }

        } else {
            if (!this.running.get()) {
                this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
                this.register();
                if (this.shouldRegisterManagement()) {
                    this.registerManagement();
                }

                this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
                this.running.compareAndSet(false, true);
            }

        }
    }
//
protected void register() {
        this.serviceRegistry.register(this.getRegistration());
    }
//nacos注册
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
public void register(Registration registration) {
        if (StringUtils.isEmpty(registration.getServiceId())) {
            log.warn("No service to register for nacos client...");
        } else {
            String serviceId = registration.getServiceId();
            Instance instance = this.getNacosInstanceFromRegistration(registration);

            try {
                this.namingService.registerInstance(serviceId, instance);
                log.info("nacos registry, {} {}:{} register finished", new Object[]{serviceId, instance.getIp(), instance.getPort()});
            } catch (Exception var5) {
                log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var5});
            }

        }
    }
}
服务端:

创建空的服务,如果服务不存在 则创建服务 然后进行初始化,根据nameSpaceId获取服务列表,然后调用consistencyService.put将数据放到dataStore中并且把当前的的实例加入到队列中,并且开启线程获取队列中的任务修改实例的状态

    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        
       //创建一个空的服务
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());
        
        Service service = getService(namespaceId, serviceName);
        
        if (service == null) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "service not found, namespace: " + namespaceId + ", service: " + serviceName);
        }
        
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }
//如果服务不存在 则创建服务 然后进行初始化
 public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
            throws NacosException {
        Service service = getService(namespaceId, serviceName);
        if (service == null) {
            
            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            service = new Service();
            service.setName(serviceName);
            service.setNamespaceId(namespaceId);
            service.setGroupName(NamingUtils.getGroupName(serviceName));
            // now validate the service. if failed, exception will be thrown
            service.setLastModifiedMillis(System.currentTimeMillis());
            service.recalculateChecksum();
            if (cluster != null) {
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            service.validate();
            //往双层map中存放服务 ,初始化  监听数据变化
            putServiceAndInit(service);
            if (!local) {
                addOrReplaceService(service);
            }
        }
    }

//获取服务列表添加到队列中
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {
        
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        
        Service service = getService(namespaceId, serviceName);
        
        synchronized (service) {
            //获取服务列表
            List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
            
            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            
            consistencyService.put(key, instances);
        }
    }

在DistroConsistencyServiceImpl类
//把一个key相关的数据放到Nacos集群中。
 @Override
    public void put(String key, Record value) throws NacosException {
       //将数据放到dataStore
        onPut(key, value);
        //开始将数据同步到所有远程服务器。
        distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                globalConfig.getTaskDispatchPeriod() / 2);
    }
//notifier中有一个线程
 public void onPut(String key, Record value) {
        
        if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
            Datum<Instances> datum = new Datum<>();
            datum.value = (Instances) value;
            datum.key = key;
            datum.timestamp.incrementAndGet();
            dataStore.put(key, datum);
        }
        
        if (!listeners.containsKey(key)) {
            return;
        }
        
        notifier.addTask(key, DataOperation.CHANGE);
    }
public class Notifier implements Runnable {
  private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
		//将注册信息放入阻塞队列
     public void addTask(String datumKey, DataOperation action) {
            
            if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
                return;
            }
            if (action == DataOperation.CHANGE) {
                services.put(datumKey, StringUtils.EMPTY);
            }
            tasks.offer(Pair.with(datumKey, action));
        }
     @Override
        public void run() {
            Loggers.DISTRO.info("distro notifier started");
            
            for (; ; ) {
                try {
                    //获取注册信息,从队列中获取任务
                    Pair<String, DataOperation> pair = tasks.take();
                    //处理实例的状态信息   修改还是删除
                    handle(pair);
                } catch (Throwable e) {
                    Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
                }
            }
        }
}

在handle方法中最终写入set集合中

将instance放入到阻塞队列中BlockingQueue(内存队列),阻塞队列所在的类实现了Runnable接口,运行一个线程去处理获取实例,最终放入到set集合中,最后放入map中

@Component
public class ServiceManager implements RecordListener<Service> {
/**
 * Map(namespace, Map(group::serviceName, Service)).
 */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>()
}

nacos注册与发现

服务动态感知:

image-20211231164634983

客户端与服务端同步服务列表

客户端与服务端通过pull和push的方式进行数据验证,即客户端每隔10秒向服务端发送请求,获取服务列表,将服务列表缓存在本地,而且服务端发现某些客户端挂掉了可通过刚才发来的pull心跳得到客户端的地址端口,将服务端的列表通过udp(不需要可靠)的方式push给客户端,更新客户端的缓存,

nacos发现

客户端定时拉取注册中心的服务列表,更新本地服务列表,发送请求时获取本地服务列表,根据设定的规则选择一个服务发起http请求,或者发起请求获取注册中心的服务列表(持久实例列表和临时实例列表)。

blog.csdn.net/liyanan21/a…

客户端向服务端注册

www.cnblogs.com/wtzbk/p/143…

客户端注册时会构造一个每隔5秒发送心跳的任务BeatInfo对象,new BeatTask()将BeatInfo的信息放入其中,将其交给定时任务的run方法处理。向服务端发起请求。

服务的健康检查

Nacos Server会开启一个定时任务来检查注册服务的健康情况,对于超过15秒没收到客户端的心跳实例会将它的 healthy属性置为false,此时当客户端不会将该实例的信息发现,如果某个服务的实例超过30秒没收到心跳,则剔除该实例,如果剔除的实例恢复,发送心跳则会恢复。 当有实例注册的时候,我们会看到有个service.init()的方法,该方法的实现主要是将ClientBeatCheckTask加入到线程池当中,如下图:

有实例注册的时候

nacos高并发

/**
 * Update instance list.
 * updateIps方法是将新传进来的数据,把之前的实例复制一份出来,将新数据和副本做一些运算,最后将运算之后的数据替换之前的注册表
 * @param ips       instance list
 * @param ephemeral whether these instances are ephemeral
 */
public void updateIps(List<Instance> ips, boolean ephemeral) {
    //将实例信息取出放入toUpdateInstances集合
    Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
    
    HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
    
    //取出的实例放入oldIpMap副本
    for (Instance ip : toUpdateInstances) {
        oldIpMap.put(ip.getDatumKey(), ip);
    }
    
    List<Instance> updatedIPs = updatedIps(ips, oldIpMap.values());
    if (updatedIPs.size() > 0) {
        for (Instance ip : updatedIPs) {
            Instance oldIP = oldIpMap.get(ip.getDatumKey());
            
            // do not update the ip validation status of updated ips
            // because the checker has the most precise result
            // Only when ip is not marked, don't we update the health status of IP:
            if (!ip.isMarked()) {
                ip.setHealthy(oldIP.isHealthy());
            }
            
            if (ip.isHealthy() != oldIP.isHealthy()) {
                // ip validation status updated
                Loggers.EVT_LOG.info("{} {SYNC} IP-{} {}:{}@{}", getService().getName(),
                        (ip.isHealthy() ? "ENABLED" : "DISABLED"), ip.getIp(), ip.getPort(), getName());
            }
            
            if (ip.getWeight() != oldIP.getWeight()) {
                // ip validation status updated
                Loggers.EVT_LOG.info("{} {SYNC} {IP-UPDATED} {}->{}", getService().getName(), oldIP.toString(),
                        ip.toString());
            }
        }
    }
    
    List<Instance> newIPs = subtract(ips, oldIpMap.values());
    if (newIPs.size() > 0) {
        Loggers.EVT_LOG
                .info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
                        getName(), newIPs.size(), newIPs.toString());
        
        for (Instance ip : newIPs) {
            HealthCheckStatus.reset(ip);
        }
    }
    
    List<Instance> deadIPs = subtract(oldIpMap.values(), ips);
    
    if (deadIPs.size() > 0) {
        Loggers.EVT_LOG
                .info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
                        getName(), deadIPs.size(), deadIPs.toString());
        
        for (Instance ip : deadIPs) {
            HealthCheckStatus.remv(ip);
        }
    }
    
    toUpdateInstances = new HashSet<>(ips);
    
    if (ephemeral) {
        //将新的实例信息替换给之前旧的集合
        ephemeralInstances = toUpdateInstances;
    } else {
        persistentInstances = toUpdateInstances;
    }
}

注册中心的高并发处理

写时复制(CopyOnWrite),将注册表复制一份,将新注册的实例放入副本,读原来的注册表,内存队列(Blockqueue)异步任务。

注册防止并发覆盖

后台线程将队列里面服务信息一个一个拿出来进行注册,不会有覆盖的可能。最后将副本信息写进注册表。

会产生脏读。

不影响业务,无非就是晚一点拿到服务注册信息,可接受。

eureka和nacos比较

  1. eureka和nacos ap模式下为了高并发读写,都在内存中修改,但是分别采用了不同的策略。

  2. nacos 使用的是CopyOnWrite思想防止并发冲突。

  3. eureka使用的是3级缓存。nacos中注册的时候,会调用更新list列表

  4. eureka为了增加读写并发,在内存中运用读写分离的思想。 读写分离的弊端就是consumer获取示例数据会慢一些,因为可能有延迟。但是提高了并发。

nacos心跳机制

总: 在注册时就发送心跳,后面一直向客户通过api发送心跳给服务端,服务端接收到心跳判断是否超出15秒是则将健康状态设置为false,

如果超出时间为30秒就删除服务,服务端使用线程每隔5秒检查是否超时。

客户端
 
@Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {

        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            long instanceInterval = instance.getInstanceHeartBeatInterval();
            beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
//发送心跳
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        dom2Beat.put(buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()), beatInfo);
    //定时发送 BeatTask中的run方法
        executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

class BeatTask implements Runnable {

        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

        @Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long result = serverProxy.sendBeat(beatInfo);
            long nextTime = result > 0 ? result : beatInfo.getPeriod();
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }
	
服务端

定时任务检查服务信息

/**
     * Schedule client beat check task with a delay.
     *发送心跳后服务端的定时逻辑处理
     * @param task client beat check task
     */
    public static void scheduleCheck(ClientBeatCheckTask task) {
        futureMap.putIfAbsent(task.taskKey(), GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
    }



for (Instance instance : instances) {
    //getLastBeat上一次的心跳时间   间隔时间为15秒
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
                    if (!instance.isMarked()) {
                        if (instance.isHealthy()) {
   //健康状态设置为false
                            instance.setHealthy(false);
                            Loggers.EVT_LOG
                                    .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
                                   instance.getIp(), instance.getPort(), instance.getClusterName(),
                                 service.getName(), UtilsAndCommons.LOCALHOST_SITE,
                                instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
                            getPushService().serviceChanged(service);
                            ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                        }
                    }
                }
            }

注册中心的高并发处理,写时复制(CopyOnWrite),将注册表复制一份,将新注册的实例放入副本,读原来的注册表,内存队列(Blockqueue)异步任务。

注册防止并发覆盖,后台线程将队列里面服务信息一个一个拿出来进行注册,不会有覆盖的可能。最后将副本信息写进注册表。

会产生脏读。不影响业务,无非就是晚一点拿到服务注册信息,可接受。

eureka和nacos ap模式下为了高并发读写,都在内存中修改,但是分别采用了不同的策略。

nacos 使用的是CopyOnWrite思想防止并发冲突。eureka使用的是3级缓存。nacos中注册的时候,会调用更新list列表:

eureka为了增加读写并发,在内存中运用读写分离的思想。 读写分离的弊端就是consumer获取示例数据会慢一些,因为可能有延迟。但是提高了并发。

blog.csdn.net/sxj159753/a…

nacos心跳机制

在注册时就发送心跳,后面一直向客户通过api发送心跳给服务端,服务端接收到心跳判断是否超出15秒是则将健康状态设置为false,服务端使用线程每隔5秒检查是否超时。

/**
     * Schedule client beat check task with a delay.
     *
     * @param task client beat check task
     */
    public static void scheduleCheck(ClientBeatCheckTask task) {
        futureMap.putIfAbsent(task.taskKey(), GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
    }
for (Instance instance : instances) {
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
                    if (!instance.isMarked()) {
                        if (instance.isHealthy()) {
                            instance.setHealthy(false);
                            Loggers.EVT_LOG
                                    .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
                                            instance.getIp(), instance.getPort(), instance.getClusterName(),
                                            service.getName(), UtilsAndCommons.LOCALHOST_SITE,
                                            instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
                            getPushService().serviceChanged(service);
                            ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                        }
                    }
                }
            }

中间件

RabbitMQ

RabbitMQ 架构简介

  1. 生产者(Publisher):发布消息到 RabbitMQ 中的交换机(Exchange)上。
  2. 交换机(Exchange):和生产者建立连接并接收生产者的消息。
  3. 消费者(Consumer):监听 RabbitMQ 中的 Queue 中的消息。
  4. 队列(Queue):Exchange 将消息分发到指定的 Queue,Queue 和消费者进行交互。
  5. 路由(Routes):交换机转发消息到队列的规则。

简述RabbitMq的交换机类型

fanout:发布订阅

direct:点对点(精准匹配)默认模式

topic:点对点(模糊匹配) image.png

Work queues

一个生产者,一个默认的交换机(DirectExchange),一个队列,两个消费者,

一个队列对应了多个消费者,默认情况下,由队列对消息进行平均分配,消息会被分到不同的消费者手中。消费者可以配置各自的并发能力,进而提高消息的消费能力,也可以配置手动 ack,来决定是否要消费某一条消息。

手动ACK:

这样可以自行决定是否消费 RabbitMQ 发来的消息,配置手动 ack 的方式如下:

spring.rabbitmq.listener.simple.acknowledge-mode=manual

消费代码:

@Component
public class HelloWorldConsumer {
    @RabbitListener(queues = HelloWorldConfig.HELLO_WORLD_QUEUE_NAME)
    public void receive(Message message,Channel channel) throws IOException {
        System.out.println("receive="+message.getPayload());
        channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)),true);
    }

    @RabbitListener(queues = HelloWorldConfig.HELLO_WORLD_QUEUE_NAME, concurrency = "10")
    public void receive2(Message message, Channel channel) throws IOException {
        System.out.println("receive2 = " + message.getPayload() + "------->" + Thread.currentThread().getName());
        channel.basicReject(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)), true);
    }
}

Publish/Subscribe

一个生产者,多个消费者,每一个消费者都有自己的一个队列,生产者没有将消息直接发送到队列,而是发送到了交换机,每个队列绑定交换机,生产者发送的消息经过交换机,到达队列,实现一个消息被多个消费者获取的目的。需要注意的是,如果将消息发送到一个没有队列绑定的 Exchange上面,那么该消息将会丢失,这是因为在 RabbitMQ 中 Exchange 不具备存储消息的能力,只有队列具备存储消息的能力 .

这种情况下,我们有四种交换机可供选择,分别是:

Direct

Fanout

Topic

Header

DirectExchange

DirectExchange 的路由策略是将消息队列绑定到一个 DirectExchange 上,当一条消息到达 DirectExchange 时会被转发到与该条消息 routing key 相同的 Queue 上,例如消息队列名为 “hello-queue”,则 routingkey 为 “hello-queue” 的消息会被该消息队列接收。DirectExchange 的配置如下:

@Configuration
public class RabbitDirectConfig {
    public final static String DIRECTNAME = "javaboy-direct";
    @Bean
    Queue queue() {
        return new Queue("hello-queue");
    }
    @Bean
DirectExchange directExchange() {
        return new DirectExchange(DIRECTNAME, true, false);
    }
    @Bean
    Binding binding() {
        return BindingBuilder.bind(queue())
                .to(directExchange()).with("direct");
    }
}
  • 首先提供一个消息队列Queue,然后创建一个DirectExchange对象,三个参数分别是名字,重启后是否依然有效以及长期未用时是否删除。
  • 创建一个Binding对象将Exchange和Queue绑定在一起。
  • DirectExchange和Binding两个Bean的配置可以省略掉,即如果使用DirectExchange,可以只配置一个Queue的实例即可。
FanoutExchange

FanoutExchange 的数据交换策略是把所有到达 FanoutExchange 的消息转发给所有与它绑定的 Queue 上,在这种策略中,routingkey 将不起任何作用,FanoutExchange 配置方式如下:

@Configuration
public class RabbitFanoutConfig {
    public final static String FANOUTNAME = "sang-fanout";
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUTNAME, true, false);
    }
    @Bean
    Queue queueOne() {
        return new Queue("queue-one");
    }
    @Bean
    Queue queueTwo() {
        return new Queue("queue-two");
    }
    @Bean
    Binding bindingOne() {
        return BindingBuilder.bind(queueOne()).to(fanoutExchange());
    }
    @Bean
    Binding bindingTwo() {
        return BindingBuilder.bind(queueTwo()).to(fanoutExchange());
    }
}

在这里首先创建 FanoutExchange,参数含义与创建 DirectExchange 参数含义一致,然后创建两个 Queue,再将这两个 Queue 都绑定到 FanoutExchange 上。

TopicExchange

TopicExchange 是比较复杂但是也比较灵活的一种路由策略,在 TopicExchange 中,Queue 通过 routingkey 绑定到 TopicExchange 上,当消息到达 TopicExchange 后,TopicExchange 根据消息的 routingkey 将消息路由到一个或者多个 Queue 上。TopicExchange 配置如下:

@Configuration
public class RabbitTopicConfig {
    public final static String TOPICNAME = "sang-topic";
    @Bean
    TopicExchange topicExchange() {
        return new TopicExchange(TOPICNAME, true, false);
    }
    @Bean
    Queue xiaomi() {
        return new Queue("xiaomi");
    }
    @Bean
    Queue huawei() {
        return new Queue("huawei");
    }
    @Bean
    Queue phone() {
        return new Queue("phone");
    }
    @Bean
    Binding xiaomiBinding() {
        return BindingBuilder.bind(xiaomi()).to(topicExchange())
                .with("xiaomi.#");
    }
    @Bean
    Binding huaweiBinding() {
        return BindingBuilder.bind(huawei()).to(topicExchange())
                .with("huawei.#");
    }
    @Bean
    Binding phoneBinding() {
        return BindingBuilder.bind(phone()).to(topicExchange())
                .with("#.phone.#");
    }
}
  • 首先创建 TopicExchange,参数和前面的一致。然后创建三个 Queue,第一个 Queue 用来存储和 “xiaomi” 有关的消息,第二个 Queue 用来存储和 “huawei” 有关的消息,第三个 Queue 用来存储和 “phone” 有关的消息。
  • 将三个 Queue 分别绑定到 TopicExchange 上,第一个 Binding 中的 “xiaomi.#” 表示消息的 routingkey 凡是以 “xiaomi” 开头的,都将被路由到名称为 “xiaomi” 的 Queue 上,第二个 Binding 中的 “huawei.#” 表示消息的 routingkey 凡是以 “huawei” 开头的,都将被路由到名称为 “huawei” 的 Queue 上,第三个 Binding 中的 “#.phone.#” 则表示消息的 routingkey 中凡是包含 “phone” 的,都将被路由到名称为 “phone” 的 Queue 上。
Routing

一个生产者,一个交换机,两个队列,两个消费者,生产者在创建 Exchange 后,根据 RoutingKey 去绑定相应的队列,并且在发送消息时,指定消息的具体 RoutingKey 即可。

发送方可靠性:确认机制

spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true

第一行属性的配置有三个取值:

  1. none:表示禁用发布确认模式,默认即此。
  2. correlated:表示成功发布消息到交换器后会触发的回调方法。
  3. simple:类似 correlated,并且支持 waitForConfirms()waitForConfirmsOrDie() 方法的调用。

接下来我们要开启两个监听,具体配置如下:

@Configuration
public class RabbitConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    public static final String JAVABOY_EXCHANGE_NAME = "javaboy_exchange_name";
    public static final String JAVABOY_QUEUE_NAME = "javaboy_queue_name";
    private static final Logger logger = LoggerFactory.getLogger(RabbitConfig.class);
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Bean
    Queue queue() {
        return new Queue(JAVABOY_QUEUE_NAME);
    }
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(JAVABOY_EXCHANGE_NAME);
    }
    @Bean
    Binding binding() {
        return BindingBuilder.bind(queue())
                .to(directExchange())
                .with(JAVABOY_QUEUE_NAME);
    }

    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("{}:消息成功到达交换器",correlationData.getId());
        }else{
            logger.error("{}:消息发送失败", correlationData.getId());
        }
    }

    @Override
    public void returnedMessage(ReturnedMessage returned) {
        logger.error("{}:消息未成功路由到队列",returned.getMessage().getMessageProperties().getMessageId());
    }
}

关于这个配置类,我说如下几点:

定义配置类,实现 RabbitTemplate.ConfirmCallbackRabbitTemplate.ReturnsCallback 两个接口,这两个接口,前者的回调用来确定消息到达交换器,后者则会在消息路由到队列失败时被调用。

定义 initRabbitTemplate 方法并添加 @PostConstruct 注解,在该方法中为 rabbitTemplate 分别配置这两个 Callback。

失败重试

自带重试机制

前面所说的事务机制和发送方确认机制,都是发送方确认消息发送成功的办法。如果发送方一开始就连不上 MQ,那么 Spring Boot 中也有相应的重试机制,但是这个重试机制就和 MQ 本身没有关系了,这是利用 Spring 中的 retry 机制来完成的,具体配置如下:

spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000ms
spring.rabbitmq.template.retry.max-attempts=10
spring.rabbitmq.template.retry.max-interval=10000ms
spring.rabbitmq.template.retry.multiplier=2

从上往下配置含义依次是:

  • 开启重试机制。
  • 重试起始间隔时间。
  • 最大重试次数。
  • 最大重试间隔时间。
  • 间隔时间乘数。(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)
业务重试

整体思路是这样:

  1. 首先创建一张表,用来记录发送到中间件上的消息,像下面这样:

img

每次发送消息的时候,就往数据库中添加一条记录。这里的字段都很好理解,有三个我额外说下:

  • status:表示消息的状态,有三个取值,0,1,2 分别表示消息发送中、消息发送成功以及消息发送失败。
  • tryTime:表示消息的第一次重试时间(消息发出去之后,在 tryTime 这个时间点还未显示发送成功,此时就可以开始重试了)。
  • count:表示消息重试次数。

消费可靠性

两种消费思路

推(push):MQ 主动将消息推送给消费者,这种方式需要消费者设置一个缓冲区去缓存消息,对于消费者而言,内存中总是有一堆需要处理的消息,所以这种方式的效率比较高,这也是目前大多数应用采用的消费方式。

拉(pull):消费者主动从 MQ 拉取消息,这种方式效率并不是很高,不过有的时候如果服务端需要批量拉取消息,倒是可以采用这种方式。

确保消费成功两种思路

为了保证消息能够可靠的到达消息消费者,RabbitMQ 中提供了消息消费确认机制。当消费者去消费消息的时候,可以通过指定 autoAck 参数来表示消息消费的确认方式。

  • 当 autoAck 为 false 的时候,此时即使消费者已经收到消息了,RabbitMQ 也不会立马将消息移除,而是等待消费者显式的回复确认信号后,才会将消息打上删除标记,然后再删除。
  • 当 autoAck 为 true 的时候,此时消息消费者就会自动把发送出去的消息设置为确认,然后将消息移除(从内存或者磁盘中),即使这些消息并没有到达消费者。

我们来看一张图:

img

如上图所示,在 RabbitMQ 的 web 管理页面:

Ready 表示待消费的消息数量。

Unacked 表示已经发送给消费者但是还没收到消费者 ack 的消息数量。

当我们将 autoAck 设置为 false 的时候,对于 RabbitMQ 而言,消费分成了两个部分:

  • 待消费的消息
  • 已经投递给消费者,但是还没有被消费者确认的消息

换句话说,当设置 autoAck 为 false 的时候,消费者就变得非常从容了,它将有足够的时间去处理这条消息,当消息正常处理完成后,再手动 ack,此时 RabbitMQ 才会认为这条消息消费成功了。如果 RabbitMQ 一直没有收到客户端的反馈,并且此时客户端也已经断开连接了,那么 RabbitMQ 就会将刚刚的消息重新放回队列中,等待下一次被消费。

综上所述,确保消息被成功消费,无非就是手动 Ack 或者自动 Ack,无他。当然,无论这两种中的哪一种,最终都有可能导致消息被重复消费,所以一般来说我们还需要在处理消息时,解决幂等性问题。

消息拒绝
@Component
public class ConsumerDemo {
    @RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)
    public void handle(Channel channel, Message message) {
        //获取消息编号
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //拒绝消息
            channel.basicReject(deliveryTag, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}	

消费者收到消息之后,可以选择拒绝消费该条消息,拒绝的步骤分两步:

  1. 获取消息编号 deliveryTag。
  2. 调用 basicReject 方法拒绝消息。

调用 basicReject 方法时,第二个参数是 requeue,即是否重新入队。如果第二个参数为 true,则这条被拒绝的消息会重新进入到消息队列中,等待下一次被消费;如果第二个参数为 false,则这条被拒绝的消息就会被丢掉,不会有新的消费者去消费它了。

需要注意的是,basicReject 方法一次只能拒绝一条消息。

消息确认
手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)
public void handle3(Message message,Channel channel) {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
        //消息消费的代码写到这里
        String s = new String(message.getBody());
        System.out.println("s = " + s);
        //消费完成后,手动 ack
        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        //手动 nack
        try {
            channel.basicNack(deliveryTag, false, true);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
幂等性问题

消费者在消费完一条消息后,向 RabbitMQ 发送一个 ack 确认,此时由于网络断开或者其他原因导致 RabbitMQ 并没有收到这个 ack,那么此时 RabbitMQ 并不会将该条消息删除,当重新建立起连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。同时,由于类似的原因,消息在发送的时候,同一条消息也可能会发送两次

采用 Redis,在消费者消费消息之前,现将消息的 id 放到 Redis 中,存储方式如下:

  • id-0(正在执行业务)
  • id-1(执行业务成功)

如果 ack 失败,在 RabbitMQ 将消息交给其他的消费者时,先执行 setnx,如果 key 已经存在(说明之前有人消费过该消息),获取他的值,如果是 0,当前消费者就什么都不做,如果是 1,直接 ack。

死信队列

死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有 后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效

死信的来源

消息 TTL 过期

队列达到最大长度(队列满了,无法再添加数据到 mq 中)

消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.

延迟队列

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

延迟队列使用场景

1.订单在十分钟之内未支付则自动取消

2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。

3.用户注册成功后,如果三天内没有登陆则进行短信提醒。

4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。

5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

一.rocketmq安装启动

  • Start Name Server

  > nohup sh bin/mqnamesrv &
  > tail -f ~/logs/rocketmqlogs/namesrv.log
  The Name Server boot success...
  • Start Broker

  > nohup sh bin/mqbroker -n localhost:9876 &
  > tail -f ~/logs/rocketmqlogs/broker.log 
  The broker[%s, 172.30.30.233:10911] boot success...
  • Send & Receive Messages

> export NAMESRV_ADDR=localhost:9876
 > sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
 SendResult [sendStatus=SEND_OK, msgId= ...

 > sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
 ConsumeMessageThread_%d Receive New Messages: [MessageExt...
  • Shutdown Servers

> sh bin/mqshutdown broker
The mqbroker(36695) is running...
Send shutdown request to mqbroker(36695) OK

> sh bin/mqshutdown namesrv
The mqnamesrv(36664) is running...
Send shutdown request to mqnamesrv(36664) OK

二.集群搭建理论

架构图

image.png

数据复制与刷盘策略

image.png

复制策略

根据Broker集群中各个节点间关系的不同,Broker集群可以分为以下几类:

  • 同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK

  • 异步复制:消息写入master后,master立即向producet返回成功ACK,无需等待slave同步数据成功


    异步复制降低系统写入延迟,提高吞吐量

刷盘策略

刷盘策略指的是broker中消息的落盘方式,即消息发送到broker内存后消息持久化到磁盘的方式。分为同步刷盘与异步刷盘:

  1. 同步刷盘:当消息持久化到broker的磁盘后才算是消息写入成功。

  2. 异步刷盘:当消息写入到broker的内存后即表示消息写入成功,无需等待消息持久化到磁盘。

    异步刷盘策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量。

    消息写入到Broker的内存,一般是写入到了PageCache。

    对于异步刷盘策略,消息会写入到PageCache后立即返回成功ACK。但并不会立即做落盘操作,而是当PageCache到达一定量时会自动进行落盘。

Broker集群模式

根据Broker集群中各个节点间关系的不同,Broker集群可以分为以下几类:

单Master

只有一个broker。这种方式也只能是在测试时使用,生产环境下不能使用,因为存在单点问题。

多Master

broker集群仅由多个master构成,不存在Slave。同一Topic的各个Queue会平均分布在各个master节点上。

  • 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高。
  • 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅(不可消费),消息实时性会受到影响。

优点得前提是配置了RAID10,否则宕机会出现消息丢失 ​

Redis持久化

总体介绍

官网介绍:www.redis.io

Redis 提供了2个不同形式的持久化方式。

  1. RDB(Redis DataBase)

  2. AOF(Append Of File)

    RDB是什么

    在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

备份是如何执行的

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是 最后一次持久化后的数据可能丢失

Fork

  1. Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  2. 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术
  3. 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

RDB持久化流程 

 

 如何触发RDB快照;保持策略

 配置文件中默认的快照配置

 

命令 save VS bgsave

save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。

bgsave

**Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

可以通过lastsave 命令获取最后一次成功执行快照的时间。

flushall命令

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

在存储快照后,还可以让redis使用CRC64算法来进行数据校验,

但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能

优势

  1. 适合大规模的数据恢复
  2. 对数据完整性和一致性要求不高更适合使用
  3. 节省磁盘空间
  4. 恢复速度快

劣势

  1. Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  2. 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  3. 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

AOF Append Only File

是什么

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

AOF持久化流程

(1)客户端的请求写命令会被append追加到AOF缓冲区内;

(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

AOF默认不开启

可以在redis.conf中配置文件名称,默认为 appendonly.aof

AOF文件的保存路径,同RDB的路径一致。

AOF和 RDB **同时开启, **redis 听谁的?

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

AOF启动 **/ **修复 **/ **恢复

  1. AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
  2. 正常恢复
  1. 修改默认的appendonly no,改为yes
  2. 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
  3. 恢复:重启redis然后重新加载
  1. 异常恢复
  1. 修改默认的appendonly no,改为yes
  2. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复
  3. 备份被写坏的AOF文件
  4. 恢复:重启redis,然后重新加载

AOF同步频率设置

appendfsync always 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好 appendfsync everysec 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。 appendfsync no redis不主动进行同步,把同步时机交给操作系统。

重写流程

(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。

(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。 2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

Redis应用问题解决

缓存穿透

问题描述****

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

image.png

解决方案****

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:

(1) 对空值缓存: 如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

(4) 进行实时监控: 当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务****

 

缓存击穿

问题描述****

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

image.png

解决方案****

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

解决问题:

(1)预先设置热门数据: 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

(2)实时调整: 现场监控哪些数据热门,实时调整key的过期时长

(3)使用锁:****

(1) 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。

(2) 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key

(3) 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;

(4) 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

image.png

缓存雪崩

问题描述****

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key

 

正常访问

image.png

缓存失效瞬间

image.png  

缓存失效时的雪崩效应对底层系统的冲击非常可怕!

解决方案:

(1) 构建多级缓存架构: nginx缓存 + redis缓存 +其他缓存(ehcache等)****

(2) 使用锁或队列

用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况

(3) 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(4) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

分布式锁

使用setNx加锁,业务处理完成删除锁

问题:并发出现误删锁

将当前线程存入redis中,通过判断是否为当前线程,去删除

问题:某一个拥有锁的线程未释放锁是宕机造成死锁

加入过期时间,防止死锁产生。

问题:拥有锁的线程执行任务时间久,但是过期时间没那么长,导致提前释放锁

加入守护线程为锁续命也就是增加时间

问题:以上操作不具备原子性,无法保证全部执行成功。

使用Lua脚本保证redis操作的原子性,实现分布式锁.

1、实现的原理

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。

分布式事务

zhuanlan.zhihu.com/p/470281769