后端求offer版
IP:
0关注数
21粉丝数
0获得的赞
工作年
编辑资料
链接我:

创作·137

全部
问答
动态
项目
学习
专栏
后端求offer版

【Nginx】如何使用Nginx搭建流媒体服务器实现直播?看完这篇我会了!!

写在前面最近几年,直播行业比较火,无论是传统行业的直播,还是购物、游戏、教育,都在涉及直播。作为在互联网行业奋斗了多年的小伙伴,你有没有想过如果使用Nginx搭建一套直播环境,那我们该如何搭建呢?别急,接下来,我们就一起使用Nginx来搭建一套直播环境。安装Nginx注意:这里以CentOS 6.8服务器为例,以root用户身份来安装Nginx。1.安装依赖环境yum -y install wget gcc-c++ ncurses ncurses-devel cmake make perl bison openssl openssl-devel gcc* libxml2 libxml2-devel curl-devel libjpeg* libpng* freetype* autoconf automake zlib* fiex* libxml* libmcrypt* libtool-ltdl-devel* libaio libaio-devel bzr libtool 2.安装opensslwget https://www.openssl.org/source/openssl-1.0.2s.tar.gz tar -zxvf openssl-1.0.2s.tar.gz cd /usr/local/src/openssl-1.0.2s ./config --prefix=/usr/local/openssl-1.0.2s make make install 3.安装pcrewget https://ftp.pcre.org/pub/pcre/pcre-8.43.tar.gz tar -zxvf pcre-8.43.tar.gz cd /usr/local/src/pcre-8.43 ./configure --prefix=/usr/local/pcre-8.43 make make install 4.安装zlibwget https://sourceforge.net/projects/libpng/files/zlib/1.2.11/zlib-1.2.11.tar.gz tar -zxvf zlib-1.2.11.tar.gz cd /usr/local/src/zlib-1.2.11 ./configure --prefix=/usr/local/zlib-1.2.11 make make 5.下载nginx-rtmp-modulenginx-rtmp-module的官方github地址:github.com/arut/nginx-…使用命令:git clone https://github.com/arut/nginx-rtmp-module.git 6.安装Nginxwget http://nginx.org/download/nginx-1.19.1.tar.gz tar -zxvf nginx-1.19.1.tar.gz cd /usr/local/src/nginx-1.19.1 ./configure --prefix=/usr/local/nginx-1.19.1 --with-openssl=/usr/local/src/openssl-1.0.2s --with-pcre=/usr/local/src/pcre-8.43 --with-zlib=/usr/local/src/zlib-1.2.11 --add-module=/usr/local/src/nginx-rtmp-module --with-http_ssl_module make make install 这里需要注意的是:安装Nginx时,指定的是openssl、pcre和zlib的源码解压目录,安装完成后Nginx配置文件的完整路径为:/usr/local/nginx-1.19.1/conf/nginx.conf。配置Nginx配置Nginx主要是对Nginx的nginx.conf文件进行配置,我们可以在命令行输入如下命令编辑nginx.conf文件。vim /usr/local/nginx-1.19.1/conf/nginx.conf 在文件中添加如下内容。rtmp { server { listen 1935; #监听的端口 chunk_size 4096; application hls { #rtmp推流请求路径 live on; hls on; hls_path /usr/share/nginx/html/hls; hls_fragment 5s; } } } 其中,hls_path需要可读可写的权限。接下来,我们创建/usr/share/nginx/html/hls 目录。mkdir -p /usr/share/nginx/html/hls chmod -R 777 /usr/share/nginx/html/hls 接下来,修改http中的server模块:server { listen 81; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /usr/share/nginx/html; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } 然后启动Nginx:/usr/local/nginx-1.19.1/sbin/nginx -c /usr/local/nginx-1.19.1/conf/nginx.conf 使OBS推流OBS(Open Broadcaster Software) 是以互联网流媒体直播内容为目的免费和开放源码软件。需要下载这个软件,借助这个软件进行推流(电脑没有摄像头的貌似安装不了。。。)OBS的下载链接为: https://obsproject.com/zh-cn/download%E3%80%82 安装后,桌面上会有一个如下所示的图表。打开后我们需要有一个场景,并且在这个场景下有一个流的来源(可以是窗口,如果选的是视频则会自动识别摄像头),接下来就是设置了。在配置中最需要关注的就是流的配置,由于是自建的流媒体服务器所以我们按照如下所示的方式进行配置。rtmp://你的服务器ip:端口(1935)/live #URL填写流的地址 设置完成我们就可以  开始推流了。拉流测试地址推荐一个拉流的测试地址,里面针对各种协议都能测试拉流测试,需要注意图中几个地方,由于我们使用的rtmp协议,我们选择这一栏,底下填写我们推流的地址和我们在上面obs的设置里面配置的流的名称,start, ok搞定!!!
0
0
0
浏览量2024
后端求offer版

Spring注解导入:@Import使用及原理详解

1.概述@Import 是 Spring 基于 Java 注解配置的主要组成部分,@Import 注解提供了类似 @Bean 注解的功能,向Spring容器中注入bean,也对应实现了与Spring XML中的元素相同的功能,注解定义如下:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import { ​ /** * {@link Configuration @Configuration}, {@link ImportSelector}, * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. */ Class<?>[] value(); ​ } 从定义上来看,@Import注解非常简单,只有一个属性value(),类型为类对象数组,如value={A.class, B.class},这样就可以把类A和B交给Spring容器管理。但是这个类对象需要细分为三种对象,也对应着@Import的三种用法如下:普通类实现了ImportSelector接口的类(这是重点~Spring Boot的自动配置原理就用到这种方式)实现了ImportBeanDefinitionRegistrar接口的类项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用Github地址:github.com/plasticene/…Gitee地址:gitee.com/plasticene3… 下面我们就针对@Import的三种不同用法一一举例阐述说明。2.@Import的三种用法2.1 注入普通类这种方式很简单,直接上代码,首先先随便定义一个普通类:这里我定义了一个Student类@Data public class Student {    private Long id;    private String name;    private Integer age; } 接下来就是声明一个配置类,然后使用@Import导入注入即可:@Configuration @Import({Student.class}) public class MyConfig { ​    @Bean    public Person person01() {        Person person = Person.builder().id(1l).name("shepherd").age(25).build();        return person;   } ​ ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } ​ } ​ 这里我用@Bean注入了一个bean,想证实一下@Import实现的功能与其类似,执行上面的main()结果如下:org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory myConfig com.shepherd.common.config.Student person01 可以看到,这里默认注入了一些Spring内部bean和我们在MyConfig中注入的bean,@Import({Student.class})把Student类注入到了Spring容器中,beanName默认为全限定类名com.shepherd.common.config.Student,而@Bean注入的默认为方法名,这也是两者的区别。2.2 实现了ImportSelector接口的类这一方式比较重要,也可以说是@Import最常用的方式,Spring Boot的自动装配原理就用到了这种方式,所以得认真学习一下。我们先来看看ImportSelector这个接口的定义:public interface ImportSelector { ​ /** * Select and return the names of which class(es) should be imported based on * the {@link AnnotationMetadata} of the importing @{@link Configuration} class. * @return the class names, or an empty array if none */ String[] selectImports(AnnotationMetadata importingClassMetadata); ​ /** * Return a predicate for excluding classes from the import candidates, to be * transitively applied to all classes found through this selector's imports. * <p>If this predicate returns {@code true} for a given fully-qualified * class name, said class will not be considered as an imported configuration * class, bypassing class file loading as well as metadata introspection. * @return the filter predicate for fully-qualified candidate class names * of transitively imported configuration classes, or {@code null} if none * @since 5.2.4 */ @Nullable default Predicate<String> getExclusionFilter() { return null; } ​ } selectImports( )返回一个包含了类全限定名的数组,这些类会注入到Spring容器当中。注意如果为null,要返回空数组,不然后续处理会报错空指针getExclusionFilter()该方法制定了一个对类全限定名的排除规则来过滤一些候选的导入类,默认不排除过滤。该接口可以不实现接下来我们编写一个类来实现ImportSelector接口public class MyImportSelector implements ImportSelector {    @Override    public String[] selectImports(AnnotationMetadata importingClassMetadata) {        return new String[]{"com.shepherd.common.config.Student",        "com.shepherd.common.config.Person"};   } } 最后在配置类中使用@Import导入:@Configuration @Import({MyImportSelector.class}) public class MyConfig { ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } } 执行结果如下:org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory myConfig com.shepherd.common.config.Student com.shepherd.common.config.Person 可以看出,Spring没有把MyImportSelector当初一个普通类进行处理,而是根据selectImports( )返回的全限定类名数组批量注入到Spring容器中。当然你也可以实现重写getExclusionFilter()方法排除某些类,比如你不想注入Person类,你就可以通过这种方式操作一下即可,这里就不再展示代码案例,可以自行尝试。2.3 实现了ImportBeanDefinitionRegistrar接口的类这种方式通过描述就可以知道是通过实现ImportBeanDefinitionRegistrar将要注入的类添加到BeanDefinition注册中心,这样Spring 后续会根据bean定义信息将类注入到容器中。老规矩,我们先看看ImportBeanDefinitionRegistrar的定义:public interface ImportBeanDefinitionRegistrar { ​ default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {     registerBeanDefinitions(importingClassMetadata, registry); } ​ default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } ​ } 可以看到一共有两个同名重载方法,都是用于将类的BeanDefinition注入。唯一的区别就是,2个参数的方法,只能手动的输入beanName,而3个参数的方法,可以利用BeanNameGenerator根据beanDefinition自动生成beanName自定义一个类实现ImportBeanDefinitionRegistrar:public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { ​ ​    //使用 BeanNameGenerator自动生成beanName    @Override    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Person.class);        AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();        String beanName = importBeanNameGenerator.generateBeanName(beanDefinition, registry);        registry.registerBeanDefinition(beanName, beanDefinition);   } ​    // 手动指定beanName //   @Override //   public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //       BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Student.class); //       AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); //       registry.registerBeanDefinition("student001", beanDefinition); //   } ​ } 注意,这里只能注入一个bean,所以只能实现一个方法进行注入,如果两个都是实现,前面的一个方法生效。将上面2.2小节的配置类变成@Import({MyImportBeanDefinitionRegistrar.class})即可,执行结果和上面一样的,这里不再展示了。3.@Import的实现原理探究@Import注解实现源码之前,不得不引出Spring中一个非常重要的类:ConfigurationClassPostProcessor,从名字可以看出它是一个BeanFactoryPostProcessor,主要用于处理一些配置信息和注解扫描。之前我们就在总结的 @Configuration 和 @Component区别于实现原理文章中讲述过该类的核心所在,感兴趣的可根据该文章链接跳转自行查看,不出意外地,@Import注解的扫描、解析也在其中,其流程图如下所示:ConfigurationClassPostProcessor既然是一个后置处理器,我们就直接从其后置处理方法入手即可,经过debug调试发现ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry()是完成对@Component,@ComponentScan,@Bean,@Configuration,@Import等等注解的处理的入口方法@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { int registryId = System.identityHashCode(registry); if (this.registriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); } if (this.factoriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanFactory already called on this post-processor against " + registry); } this.registriesPostProcessed.add(registryId); ​    // 处理配置类 processConfigBeanDefinitions(registry); } processConfigBeanDefinitions()处理配置类的核心逻辑:该方法比较长,碍于篇幅,我提取出有关@Import解析的核心代码,其余代码用......替代。 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames();    // 将配置类加入到configCandidates集合当中 for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (logger.isDebugEnabled()) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } } else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } ​    // 配置类集合为空,直接返回,不需要做下面的解析 // Return immediately if no @Configuration classes were found if (configCandidates.isEmpty()) { return; } ​ ...... // 解析配置类,以do...while()循环方式遍历解析所有配置类的@Component,@ComponentScan,@Bean,@Configuration,@Import // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); ​ Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); do {  // 解析注解入口 parser.parse(candidates); parser.validate(); ......      // 执行完配置类的解析之后,根据解析得到的configClasses转换为beanDefinition放入到Sping容器当中        // Read the model and create bean definitions based on its content if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses);           ...... ​ } while (!candidates.isEmpty()); ...... } 接下来的疏通一下解析配置类的流程如下所示:ConfigurationClassParser -> parse() -> processConfigurationClass() -> doProcessConfigurationClass() 上面的方法我为了方便方便省略掉了方法参数,自己调试时候如果找不到方法,可以直接搜索方法名查找调用流程。当我们看到以都do开头的方法就看到希望了,因为熟悉Spring框架源码的人都知道在Spring底层代码中,以do开头方法一般就是核心逻辑代码实现所在。doProcessConfigurationClass()源码如下: protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { ​ if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass, filter); } ​ // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } ​ // Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } ​ // Process any @Import annotations    // 处理注解@import的入口方法 processImports(configClass, sourceClass, getImports(sourceClass), filter, true); ​ // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } ​ // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } ​ // Process default methods on interfaces processInterfaces(configClass, sourceClass); ​ // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } ​ // No superclass -> processing is complete return null; } 上面包含对众多注解的处理,这里不一一讲述,后续我们讲到相应注解再解析相应代码片段,今天我直奔主题进入其中的解析@Import的方法processImports()private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter, boolean checkForCircularImports) { if (importCandidates.isEmpty()) {//准备注入的候选类集合为空 直接返回 return; } ​ if (checkForCircularImports && isChainedImportOnStack(configClass)) {//循环注入的检查 this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { this.importStack.push(configClass); try { for (SourceClass candidate : importCandidates) {//遍历注入的候选集合 /** * 如果是实现了ImportSelector接口的类 */ if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports Class<?> candidateClass = candidate.loadClass(); ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry); Predicate<String> selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { exclusionFilter = exclusionFilter.or(selectorFilter);//过滤注入的类 } if (selector instanceof DeferredImportSelector) { 延迟注入 this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } else {//调用selector当中的selectImports方法,得到要注入的类的全限定名 String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());             // 获得元类信息 Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);             // 递归的处理注入的类 processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); } } /** * 如果是ImportBeanDefinitionRegistrar 则configClass.addImportBeanDefinitionRegistrar 提前放到一个map当中 */ else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions Class<?> candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, this.environment, this.resourceLoader, this.registry);//实例化 configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());//放到一个map中 } /** * 如果是普通类 */ else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); } } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } finally { this.importStack.pop(); } } } ​ 该方法的核心逻辑:先进行注入集合的判空和循环依赖的检查,最后再进行遍历注入的候选集合,三种类型的类执行不同的逻辑:实现了ImportSelector接口的类,调用getExclusionFilter()方法,如果不为空,那么就进行过滤,过滤后调用selectImports()方法,得到要注入的类的全限定名。根据类全限定名,得到类元信息。然后递归的调用processImports()方法实现了ImportBeanDefinitionRegistrar接口的类,会实例化这个类,放入集合importBeanDefinitionRegistrars当中。普通类型的类(上面两个都不满足),那么就把它当作是配置类来处理,调用processConfigurationClass()方法,最终会放入到configurationClasses这个集合当中。经过一系列的递归调用,实现了ImportBeanDefinitionRegistrar接口的类,会放入到importBeanDefinitionRegistrars集合当中,其余的类都放入到configurationClasses集合当中。 之后就会回到processConfigBeanDefinitions方法,也就是执行完了ConfigurationClassParser的parse()方法。此时会执行loadBeanDefinitions将configurationClasses集合当中类加载的Spring容器当中,并且从 importBeanDefinitionRegistrars缓存当中拿到所有的ImportBeanDefinitionRegistrar并执行registerBeanDefinitions方法。4.总结根据上述分析,@Import注解还是依靠ConfigurationClassPostProcessor核心后置处理器实现的,所以这里想再次强调一下该类的重要性,要重点关注啦。基于以上全部就是我们对@Import注解的使用和原理分析啦,请笑纳~~~
0
0
0
浏览量2012
后端求offer版

【Nginx】面试官问我Nginx能不能配置WebSocket?我给他现场演示了一番!!

写在前面当今互联网领域,不管是APP还是H5,不管是微信端还是小程序,只要是一款像样点的产品,为了增加用户的交互感和用户粘度,多多少少都会涉及到聊天功能。而对于Web端与H5来说,实现聊天最简单的就是使用WebSocket了。而在实现WebSocket聊天的过程中,后台也往往会部署多个WebSocket服务,多个WebSocket服务之间,可以通过Nginx进行负载均衡。今天,我们就来一起说说Nginx是如何配置WebSocket的。Nginx配置WebSocketNginx配置WebSocket也比较简单,只需要在nginx.conf文件中进行相应的配置。这种方式很简单,但是很有效,能够横向扩展WebSocket服务端的服务能力。先直接展示配置文件,如下所示(使用的话直接复制,然后改改ip和port即可)map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream wsbackend{ server ip1:port1; server ip2:port2; keepalive 1000; } server { listen 20038; location /{ proxy_http_version 1.1; proxy_pass http://wsbackend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } 接下来,我们就分别分析上述配置的具体含义。首先:map $http_upgrade $connection_upgrade { default upgrade; '' close; } 表示的是:如果 $http_upgrade 不为 '' (空),则 $connection_upgrade 为 upgrade 。如果 $http_upgrade 为 '' (空),则 $connection_upgrade 为 close。其次:upstream wsbackend{ server ip1:port1; server ip2:port2; keepalive 1000; } 表示的是 nginx负载均衡:两台服务器 (ip1:port1)和(ip2:port2) 。keepalive 1000 表示的是每个nginx进程中上游服务器保持的空闲连接,当空闲连接过多时,会关闭最少使用的空闲连接.当然,这不是限制连接总数的,可以想象成空闲连接池的大小,设置的值应该是上游服务器能够承受的。最后:server { listen 20038; location /{ proxy_http_version 1.1; proxy_pass http://wsbackend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } 表示的是监听的服务器的配置listen 20038 表示 nginx 监听的端口locations / 表示监听的路径(/表示所有路径,通用匹配,相当于default)proxt_http_version 1.1 表示反向代理发送的HTTP协议的版本是1.1,HTTP1.1支持长连接proxy_pass http://wsbackend; 表示反向代理的uri,这里可以使用负载均衡变量proxy_redirect off; 表示不要替换路径,其实这里如果是/则有没有都没关系,因为default也是将路径替换到proxy_pass的后边Host $host; 表示传递时请求头不变, $host是nginx内置变量,表示的是当前的请求头,proxy_set_header表示设置请求头proxy_set_header X-Real-IP $remote_addr; 表示传递时来源的ip还是现在的客户端的ipproxy_read_timeout 3600s; 表的两次请求之间的间隔超过 3600s 后才关闭这个连接,默认的60s,自动关闭的元凶proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 表示X-Forwarded-For头不发生改变proxy_set_header Upgrade $http_upgrade; 表示设置Upgrade不变proxy_set_header Connection $connection_upgrade; 表示如果 $http_upgrade为upgrade,则请求为upgrade(websocket),如果不是,就关闭连接
0
0
0
浏览量2027
后端求offer版

【Nginx】如何封禁IP和IP段?看完这篇我会了!!

写在前面Nginx不仅仅只是一款反向代理和负载均衡服务器,它还能提供很多强大的功能,例如:限流、缓存、黑白名单和灰度发布等等。在之前的文章中,我们已经介绍了Nginx提供的这些功能。小伙伴们可以到【Nginx专题】进行查阅。今天,我们来介绍Nginx另一个强大的功能:禁用IP和IP段。禁用IP和IP段Nginx的ngx_http_access_module 模块可以封配置内的ip或者ip段,语法如下:deny IP; deny subnet; allow IP; allow subnet; # block all ips deny all; # allow all ips allow all; 如果规则之间有冲突,会以最前面匹配的规则为准。配置禁用ip和ip段下面说明假定nginx的目录在/usr/local/nginx/。首先要建一个封ip的配置文件blockips.conf,然后vi blockips.conf编辑此文件,在文件中输入要封的ip。deny 1.2.3.4; deny 91.212.45.0/24; deny 91.212.65.0/24; 然后保存此文件,并且打开nginx.conf文件,在http配置节内添加下面一行配置:include blockips.conf; 保存nginx.conf文件,然后测试现在的nginx配置文件是否是合法的:/usr/local/nginx/sbin/nginx -t 如果配置没有问题,就会输出:the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok configuration file /usr/local/nginx/conf/nginx.conf test is successful 如果配置有问题就需要检查下哪儿有语法问题,如果没有问题,需要执行下面命令,让nginx重新载入配置文件。/usr/local/nginx/sbin/nginx -s reload 仅允许内网ip如何禁止所有外网ip,仅允许内网ip呢?如下配置文件location / { # block one workstation deny 192.168.1.1; # allow anyone in 192.168.1.0/24 allow 192.168.1.0/24; # drop rest of the world deny all; } 上面配置中禁止了192.168.1.1,允许其他内网网段,然后deny all禁止其他所有ip。格式化nginx的403页面如何格式化nginx的403页面呢?首先执行下面的命令:cd /usr/local/nginx/html vi error403.html 然后输入403的文件内容,例如:<html> <head><title>Error 403 - IP Address Blocked</title></head> <body> Your IP Address is blocked. If you this an error, please contact binghe with your IP at test@binghe.com </body> </html> 如果启用了SSI,可以在403中显示被封的客户端ip,如下:Your IP Address is <!--#echo var="REMOTE_ADDR" --> blocked. 保存error403文件,然后打开nginx的配置文件vi nginx.conf,在server配置节内添加下面内容。# redirect server error pages to the static page error_page 403 /error403.html; location = /error403.html { root html; }
0
0
0
浏览量2019
后端求offer版

【Nginx】如何使用Nginx实现MySQL数据库的负载均衡?看完我懂了!!

写在前面Nginx能够实现HTTP、HTTPS协议的负载均衡,也能够实现TCP协议的负载均衡。那么,问题来了,可不可以通过Nginx实现MySQL数据库的负载均衡呢?答案是:可以。接下来,就让我们一起探讨下如何使用Nginx实现MySQL的负载均衡。前提条件注意:使用Nginx实现MySQL数据库的负载均衡,前提是要搭建MySQL的主主复制环境,关于MySQL主主复制环境的搭建,后续会在MySQL专题为大家详细阐述。这里,我们假设已经搭建好MySQL的主主复制环境,MySQL服务器的IP和端口分别如下所示。192.168.1.101    3306192.168.1.102    3306通过Nginx访问MySQL的IP和端口如下所示。192.168.1.100    3306Nginx实现MySQL负载均衡nginx在版本1.9.0以后支持tcp的负载均衡,具体可以参照官网关于模块ngx_stream_core_module的叙述,链接地址为:nginx.org/en/docs/str…nginx从1.9.0后引入模块ngx_stream_core_module,模块是没有编译的,需要用到编译,编译时需添加--with-stream配置参数,stream负载均衡官方配置样例如下所示。worker_processes auto; error_log /var/log/nginx/error.log info; events { worker_connections 1024; } stream { upstream backend { hash $remote_addr consistent; server backend1.example.com:12345 weight=5; server 127.0.0.1:12345 max_fails=3 fail_timeout=30s; server unix:/tmp/backend3; } upstream dns { server 192.168.0.1:53535; server dns.example.com:53; } server { listen 12345; proxy_connect_timeout 1s; proxy_timeout 3s; proxy_pass backend; } server { listen 127.0.0.1:53 udp; proxy_responses 1; proxy_timeout 20s; proxy_pass dns; } server { listen [::1]:12345; proxy_pass unix:/tmp/stream.socket; } } 说到这里,使用Nginx实现MySQL的负载均衡就比较简单了。我们可以参照上面官方的配置示例来配置MySQL的负载均衡。这里,我们可以将Nginx配置成如下所示。user nginx; #user root; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } stream{ upstream mysql{ server 192.168.1.101:3306 weight=1; server 192.168.1.102:3306 weight=1; } server{ listen 3306; server_name 192.168.1.100; proxy_pass mysql; } } 配置完成后,我们就可以通过如下方式来访问MySQL数据库。jdbc:mysql://192.168.1.100:3306/数据库名称 此时,Nginx会将访问MySQL的请求路由到IP地址为192.168.1.101和192.168.1.102的MySQL上。
0
0
0
浏览量2030
后端求offer版

【Nginx】并发量太高,Nginx扛不住?这次我错怪Nginx了!

写在前面最近,在服务器上搭建了一套压测环境,不为别的,就为压测下Nginx的性能,到底有没有传说中的那么牛逼!具体环境为:11台虚拟机,全部安装CentOS 6.8 64位操作系统,1台安装部署Nginx,其他10台作为客户端同时以压满CPU的线程向Nginx发送请求,对Nginx进行压测。没想到,出现问题了!!Nginx报错Nginx服务器访问量非常高,在Nginx的错误日志中不停的输出如下错误信息。2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 2020-07-23 02:53:49 [alert] 13576#0: accept() failed (24: Too many open files) 根据错误日志的输出信息,我们可以看出:是打开的文件句柄数太多了,导致Nginx报错了!那我们该如何解决这个问题呢?问题分析既然我们能够从Nginx的错误日志中基本能够确定导致问题的原因,那这到底是不是Nginx本身的问题呢?答案为:是,也不全是!为啥呢?原因很简单:Nginx无法打开那么多的文件句柄,一方面是因为我没有配置Nginx能够打开的最大文件数;另一方面是因为CentOS 6.8操作系统本身对打开的最大文件句柄数有限制,我同样没有配置操作系统的最大文件句柄数。所以说,不全是Nginx的锅!在某种意义上说,我错怪Nginx了!在CentOS 6.8服务器中,我们可以在命令行输入如下命令来查看服务器默认配置的最大文件句柄数。[root@binghe150 ~]# ulimit -n 1024 可以看到,在CentOS 6.8服务器中,默认的最大文件句柄数为1024。此时,当Nginx的连接数超过1024时,Nginx的错误日志中就会输出如下错误信息。[alert] 13576#0: accept() failed (24: Too many open files) 解决问题那我们该如何解决这个问题呢?其实,也很简单,继续往下看!使用如下命令可以把打开文件句柄数设置的足够大。ulimit -n 655350 同时修改nginx.conf , 添加如下配置项。worker_rlimit_nofile 655350; 注意:上述配置需要与error_log同级别。这样就可以解决Nginx连接过多的问题,Nginx就可以支持高并发(这里需要配置Nginx)。另外, ulimit -n还会影响到MySQL的并发连接数。把它提高,也可以提高MySQL的并发。注意: 用 ulimit -n 655350 修改只对当前的shell有效,退出后失效。永久解决问题若要令修改ulimits的数值永久生效,则必须修改配置文件,可以给ulimit修改命令放入/etc/profile里面,这个方法实在是不方便。还有一个方法是修改/etc/security/limits.conf配置文件,如下所示。vim /etc/security/limits.conf 在文件最后添加如下配置项。* soft nofile 655360 * hard nofile 655360 保存并退出vim编辑器。其中:星号代表全局, soft为软件,hard为硬件,nofile为这里指可打开的文件句柄数。最后,需要注意的是:要使 limits.conf 文件配置生效,必须要确保 pam_limits.so 文件被加入到启动文件中。查看 /etc/pam.d/login 文件中是否存在如下配置。session required /lib64/security/pam_limits.so 不存在,则需要添加上述配置项。
0
0
0
浏览量2028
后端求offer版

【Nginx】如何按日期分割Nginx日志?看这一篇就够了!!

写在前面Nginx是没有以日期格式作为文件名来存储的,也就是说,Nginx不像Tomcat,每天自动生成一个日志文件,所有的日志都是以一个名字来存储,时间久了日志文件会变得很大。这样非常不利于分析。虽然nginx没有这个功能但我们可以写一个小脚本配合计划任务来达到这样的效果。即让Nginx每天产生一个日志文件,方便我们进行后续的数据分析。分割Nginx日志首先,我们要创建一个脚本文件,用来分割Nginx日志,具体脚本如下:vim /usr/local/nginx-1.19.1/cutnginxlog.sh 脚本内容如下:#!/bin/sh # Program: # Auto cut nginx log script. # nginx日志路径 LOGS_PATH=/usr/local/nginx-1.19.1/logs TODAY=$(date -d 'today' +%Y-%m-%d) # 移动日志并改名 mv ${LOGS_PATH}/error.log ${LOGS_PATH}/error_${TODAY}.log mv ${LOGS_PATH}/access.log ${LOGS_PATH}/access_${TODAY}.log # 向nginx主进程发送重新打开日志文件的信号 kill -USR1 $(cat /usr/local/nginx-1.19.1/logs/nginx.pid) 接下来就是给cutnginxlog.sh文件授权。chmod a+x cutnginxlog.sh 接下来添加计划任务,定时执行cutnginxlog.sh脚本,以root用户执行如下命令:echo '59 23 * * * root /usr/local/nginx-1.19.1/cutnginxlog.sh >> /usr/local/nginx-1.19.1/cutnginxlog.log 2>&1' >> /etc/crontab
0
0
0
浏览量2022
后端求offer版

【Nginx】如何配置Http、Https、WS、WSS?

写在前面当今互联网领域,Nginx是使用最多的代理服务器之一,很多大厂在自己的业务系统中都是用了Nginx作为代理服务器。所以,我们有必要了解下Nginx对于Http、Https、WS、WSS的各项配置。来来来,跟冰河一些学习Nginx,一起进阶,一起头秃~~Nginx配置Http首先,我们来聊聊Nginx如何配置Http,Nginx配置Http是Nginx最常用的功能之一。在nginx.conf中配置相应的信息,如下所示。upstream message { server localhost:8080 max_fails=3; } server { listen 80; server_name localhost; location / { root html; index index.html index.htm; #允许cros跨域访问 add_header 'Access-Control-Allow-Origin' '*'; #proxy_redirect default; #跟代理服务器连接的超时时间,必须留意这个time out时间不能超过75秒,当一台服务器当掉时,过10秒转发到另外一台服务器。 proxy_connect_timeout 10; } location /message { proxy_pass http://message; proxy_set_header Host $host:$server_port; } } 此时,访问 http://localhost/message,就会被转发到 http://localhost:8080/message 上。Nginx配置Https如果业务对于网站的安全性要求比较高,此时可能就会在Nginx配置Https,具体配置信息可以参照如下方式进行。upstream message { server localhost:8080 max_fails=3; } server { listen 443 ssl; server_name localhost; ssl_certificate /usr/local/nginx-1.17.8/conf/keys/binghe.pem; ssl_certificate_key /usr/local/nginx-1.17.8/conf/keys/binghe.key; ssl_session_timeout 20m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_verify_client off; location / { root html; index index.html index.htm; #允许cros跨域访问 add_header 'Access-Control-Allow-Origin' '*'; #跟代理服务器连接的超时时间,必须留意这个time out时间不能超过75秒,当一台服务器当掉时,过10秒转发到另外一台服务器。 proxy_connect_timeout 10; } location /message { proxy_pass http://message; proxy_set_header Host $host:$server_port; } } 此时访问https://localhost/message 就会被转发到 http://localhost:8080/message上。Nginx配置WSWS的全称是WebSocket,Nginx配置WebSocket也比较简单,只需要在nginx.conf文件中进行相应的配置。这种方式很简单,但是很有效,能够横向扩展WebSocket服务端的服务能力。为了方便小伙伴们更好的理解,这里,我重点说下Nginx配置WS。先直接展示配置文件,如下所示(使用的话直接复制,然后改改ip和port即可)map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream wsbackend{ server ip1:port1; server ip2:port2; keepalive 1000; } server { listen 20038; location /{ proxy_http_version 1.1; proxy_pass http://wsbackend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } 接下来,我们就分别分析上述配置的具体含义。首先:map $http_upgrade $connection_upgrade { default upgrade; '' close; } 表示的是:如果$http_upgrade 不为 '' (空), 则$connection_upgrade 为 upgrade 。如果 $http_upgrade 为 '' (空),  则 $connection_upgrade 为 close。其次:upstream wsbackend{ server ip1:port1; server ip2:port2; keepalive 1000; } 表示的是 nginx负载均衡:两台服务器 (ip1:port1)和(ip2:port2) 。keepalive 1000 表示的是每个nginx进程中上游服务器保持的空闲连接,当空闲连接过多时,会关闭最少使用的空闲连接.当然,这不是限制连接总数的,可以想象成空闲连接池的大小,设置的值应该是上游服务器能够承受的。最后:server { listen 20038; location /{ proxy_http_version 1.1; proxy_pass http://wsbackend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } 表示的是监听的服务器的配置listen 20038 表示 nginx 监听的端口locations / 表示监听的路径(/表示所有路径,通用匹配,相当于default)proxt_http_version 1.1 表示反向代理发送的HTTP协议的版本是1.1,HTTP1.1支持长连接proxy_pass http://wsbackend; 表示反向代理的uri,这里可以使用负载均衡变量proxy_redirect off; 表示不要替换路径,其实这里如果是/则有没有都没关系,因为default也是将路径替换到proxy_pass的后边proxy_set_header Host $host; 表示传递时请求头不变, $host是nginx内置变量,表示的是当前的请求头,proxy_set_header表示设置请求头proxy_set_header X-Real-IP $remote_addr; 表示传递时来源的ip还是现在的客户端的ipproxy_read_timeout 3600s; 表的两次请求之间的间隔超过 3600s 后才关闭这个连接,默认的60s,自动关闭的元凶proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 表示X-Forwarded-For头不发生改变proxy_set_header Upgrade $http_upgrade; 表示设置Upgrade不变proxy_set_header Connection $connection_upgrade; 表示如果 $http_upgrade为upgrade,则请求为upgrade(websocket),如果不是,就关闭连接此时,访问 ws://localhost:20038 就会被转发到 ip1:port1 和 ip2:port2 上。Nginx配置WSSWSS表示WebSocket + Https,通俗点说,就是安全的WebSocket,接下来,我们来看看如何配置WSS。在配置WS时,详细描述了配置的细节信息,这里,我就不详细介绍了。map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream wsbackend{ server ip1:port1; server ip2:port2; keepalive 1000; } server{ listen 20038 ssl; server_name localhost; ssl_certificate /usr/local/nginx-1.17.8/conf/keys/binghe.com.pem; ssl_certificate_key /usr/local/nginx-1.17.8/conf/keys/binghe.com.key; ssl_session_timeout 20m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_verify_client off; location /{ proxy_http_version 1.1; proxy_pass http://wsbackend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } 此时,访问 wss://localhost:20038 就会被转发到 ip1:port1 和 ip2:port2 上。
0
0
0
浏览量2038
后端求offer版

Spring循环依赖解决方案

1.概述之前我们对Spring Bean生命周期和Bean实例化、属性填充、初始化、销毁等整体流程进行全面分析与总结,不熟悉的可查看:Spring Bean生命周期。我们也提到在创建Bean过程中贯穿着循环依赖问题,Spring使用三级缓存解决循环依赖,这也是一个重要的知识点,所以我们下面就来看看Spring是如何使用三级缓存解决循环依赖的。什么是循环依赖?循环依赖,也可以叫做循环引用,就是一个或者多个bean对象之间互相引用,存在依赖关系,大致相互引用情况如下:由上可知,循环依赖其实就是一个闭环,像图中情况二Spring在创建单例bean A的时候发现引用了B,这时候就会去容器中查找单例bean B,发现没有然后就会创建bean B,创建bean B时又发现引用了bean A,这时候又会去容器中查找bean A,发现没有,接下来就会循环重复上面的步骤,这是不是像极了死锁?其实循环依赖就是一个死循环的过程。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 2.循环依赖案例在讲述循环依赖示例前,我们先来看看什么是依赖注入?知道了依赖注入之后才能了解循环依赖出现的场景所在。2.1 什么是依赖注入依赖注入是Spring IOC(控制反转)模块的一个核心概念,DI (Dependency Injection):依赖注入是指在 Spring IOC 容器创建对象的过程中,将所依赖的对象通过配置进行注入,我们可以通过依赖注入的方式来降低对象间的耦合度。这里的配置注入可以基于XML配置文件也可以基于注解配置,当下注解配置开发是主流,所以在这里主要讨论基于注解的注入方式,基于注解的常规注入方式通常有三种:基于field属性注入基于setter方法注入基于构造器注入接下来就让我们分别来看看这三种常规的注入方式。field属性注入这种方式是我们平时开发中使用最多的,原因是这种方式使用起来非常简单,代码更加简洁。@Service public class UserService {     @Autowired     private UserDAO userDAO;  //通过属性注入   } setter方法注入@Service public class UserService {     private UserDAO userDAO;          @Autowired  //通过setter方法实现注入     public void setUserDAO(serDAO userDAO) {         this.userDAO = userDAO;     } } ​ 构造器注入@Service public class UserService {   private UserDAO userDAO;          @Autowired //通过构造器注入     public UserService(UserDAO userDAO) {         this.userDAO = userDAO;     } } ​ 2.2 循环依赖示例首先需要强调一点,虽然Spring允许Bean对象的循环依赖,但事实上,项目中存在Bean的循环依赖,是Bean对象职责划分不明确、代码质量不高的表现,如果存在大量的Bean之间循环依赖,那么代码的整体设计也就越来越糟糕。所以SpringBoot在后续的版本中终于受不了这种滥用,默认把循环依赖给禁用了!从2.6版本开始,如果你的项目里还存在循环依赖,SpringBoot将拒绝启动!我在2.7版本的Spring Boot中有两个Bean:UserService, RoleService,UserService需要查询某个用户有哪些角色,RoleService需要查询某个角色关联了哪些用户,这样就形成了相互引用循环依赖啦,代码如下:@Service public class UserService { ​    @Autowired    private RoleService roleService; } @Service public class RoleService {    @Autowired    private UserService userService; } 启动项目报错如下:存在循环依赖*************************** APPLICATION FAILED TO START *************************** ​ Description: ​ The dependencies of some of the beans in the application context form a cycle: ​ ┌─────┐ | UserService (field private com.plasticene.fast.service.impl.RoleService com.plasticene.fast.service.impl.UserService.roleService) ↑     ↓ | RoleService (field private com.plasticene.fast.service.impl.UserService com.plasticene.fast.service.impl.RoleService.userService) └─────┘ 接下来我们在配置文件中配置开启允许循环依赖spring: main:   allow-circular-references: true 项目就能正常启动了。上面演示的基于field数据注入方式的循环依赖,在开启允许循环依赖的配置的情况下项目正常启动,接下来我们基于开启配置的情况改为构造器依赖注入看看:@Service public class UserService { ​    private RoleService roleService; ​    @Autowired    public UserService(RoleService roleService) {        this.roleService = roleService;   } } @Service public class RoleService { ​    private UserService userService; ​    @Autowired    public RoleService(UserService userService) {        this.userService = userService;   } } 在开启允许循环依赖的配置启动项目还是会报错。是的,对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖3.循环依赖解决方案Spring解决循环依赖的核心思想在于提前曝光,使用三级缓存进行提前曝光。在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map作为三级缓存:public class DefaultSingletonBeanRegistry ... {  //1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"  Map<String, Object> singletonObjects = new ConcurrentHashMap(256);  //2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"  Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);  //3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"  Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16); } 我们知道在项目启动时会进行Bean的加载、注入到Spring容器中,当创建某个Bean时发现引用依赖于另一个Bean,就会进行依赖查找,就会来到顶层接口BeanFactory的#getBean()方法,所以接下来看看AbstractBeanFactory的#doGetBean()的实现逻辑,发现首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回Object sharedInstance = getSingleton(beanName); 这个#getSingleton()是在DefaultSingletonBeanRegistry实现的: @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock // 从单例缓存中加载bean Object singletonObject = this.singletonObjects.get(beanName); // 单例缓存中没有获取到bean,同时bean在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 从 earlySingletonObjects 获取 singletonObject = this.earlySingletonObjects.get(beanName); // 还是没有加载到bean,并且允许提前创建 if (singletonObject == null && allowEarlyReference) { // 对单例缓存map加锁 synchronized (this.singletonObjects) { // Consistent creation of early reference within full singleton lock // 再次从单例缓存中加载bean singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { // 再次从 earlySingletonObjects 获取 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { // 从 singletonFactories 中获取对应的 ObjectFactory ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 获得 bean singletonObject = singletonFactory.getObject(); // 添加 bean 到 earlySingletonObjects 中 this.earlySingletonObjects.put(beanName, singletonObject); // 从 singletonFactories 中移除对应的 ObjectFactory this.singletonFactories.remove(beanName); } } } } } } return singletonObject; } 这个方法就是从三级缓存中获取Bean对象,可以看到这里先从一级缓存singletonObjects中查找,没有找到的话接着从二级缓存earlySingletonObjects,还是没找到的话最终会去三级缓存singletonFactories中查找,需要注意的是如果在三级缓存中找到,就会从三级缓存升级到二级缓存了。所以,二级缓存存在的意义,就是缓存三级缓存中的 ObjectFactory 的 #getObject() 方法的执行结果,提早曝光的单例 Bean 对象。#getSingleton()返回空就会接着执行AbstractBeanFactory的#doGetBean()的下面逻辑,来到:if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } 可以看到原型模式的Bean循环依赖是直接报错,对于单例模式的Bean循环依赖Spring通过三级缓存提前曝光Bean来解决,因为单例Bean在整个容器中就一个,但是原型模式是每次都会创建一个新的Bean,无法使用缓存解决,所以直接报错了。经过一系列代码之后还是没有当前查找的Bean,就会创建一个Bean,来到代码:// 上面的缓存中没找到,需要根据不同的模式创建 // bean实例化 // Create bean instance. if (mbd.isSingleton()) {   // 单例模式  sharedInstance = getSingleton(beanName, () -> {    try {      return createBean(beanName, mbd, args);   }    catch (BeansException ex) {      // Explicitly remove instance from singleton cache: It might have been put there      // eagerly by the creation process, to allow for circular reference resolution.      // Also remove any beans that received a temporary reference to the bean.      // 显式从单例缓存中删除 Bean 实例      // 因为单例模式下为了解决循环依赖,可能他已经存在了,所以销毁它      destroySingleton(beanName);      throw ex;   } });  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } 最终来到了AbstractAutowireCapableBeanFactory的#createBean(),真正执行逻辑实现的是#doCreateBean()方法的里面代码片段如下所示:// Eagerly cache singletons to be able to resolve circular references // even when triggered by lifecycle interfaces like BeanFactoryAware. // <4> 解决单例模式的循环依赖 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } // 提前将创建的 bean 实例加入到 singletonFactories 中 // 这里是为了后期避免循环依赖 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } 这里将创建的Bean工厂对象加入到 singletonFactories 三级缓存中,用来生成半成品的Bean并放入到二级缓存中,提前曝光bean意味着别的bean引用它时依赖查找就可以在前面的#getSingleton()中拿到当前bean直接返回啦,从而解决循环依赖​ protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } } 可以看出,singletonFactories 这个三级缓存是解决 Spring Bean 循环依赖的重要所在。同时这段代码发生在 #createBeanInstance(...) 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了。所以 Spring 在这个时候将该对象提前曝光出来,可以被其他对象所使用。当然也需要注意到addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))的() -> getEarlyBeanReference(beanName, mbd, bean)匿名函数调用,使用lambda方式生成一个ObjectFactory对象放到三级缓存中,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory.getObject方式内实时生成代理对象,也就是调用#getEarlyBeanReference()进行实现的。// 这里如果当前Bean需要aop代理增强,就是这里生成代理Bean对象的 protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; } 这也是为什么 Spring 需要额外增加 singletonFactories 三级缓存的原因,解决 Spring 循环依赖情况下的 Bean 存在动态代理等情况,不然循环注入到别人的 Bean 就是原始的,而不是经过动态代理的!这里有个值得思考的问题:为什么要包装一层ObjectFactory对象存入三级缓存,说是为了解决Bean对象存在aop代理情况,那么直接生成代理对象半成品Bean放入二级缓存中,这样就可以不用三级缓存了!!!这么一说使用三级缓存的意义在哪里首先需要明确一点:正常情况下(没有循环依赖),Spring都是在创建好完成品Bean之后才创建对应的代理对象。为了处理循环依赖,Spring有两种选择:不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入。不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。显然Spring使用了三级缓存,选择第二种方案,这是为啥呢?原因是:如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。经过实例化,初始化、属性赋值等操作之后,bean对象已经是一个完整的实例了,最终会调用DefaultSingletonBeanRegistry的#addSingleton()将完整bean放入一级缓存singletonObjects。protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } 到这里一个真真正正完整的Bean已经存入Spring容器中,可以随意被使用啦。这里还涉及到一个比较细节的知识点,也是面试的一个考点:说说BeanFactory、FactoryBean及ObjectFactory三者的作用和区别?BeanFactory: BeanFactory是IOC容器的核心接口,用于管理Bean的一个工厂接口类,主要功能有实例化、定位、配置应用程序中的对象及建立这些对象间的依赖FactoryBean: 一般情况下,Spring 通过反射机制利用 bean 的 class 属性指定实现类来实例化 bean 。某些情况下,实例化 bean 过程比较复杂,如果按照传统的方式,则需要提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring 为此提供了一个 FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 bean 的逻辑。FactoryBean在BeanFacotry的实现中有着特殊的处理,如果一个对象实现了FactoryBean 那么通过它get出来的对象实际是 factoryBean.getObject() 得到的对象,如果想得到FactoryBean必须通过在 '&' + beanName 的方式获取。ObjectFactory: ObjectFactory则只是一个普通的对象工厂接口,从上面可以看到spring对ObjectFactory的应用之一就是,将创建对象 的步骤封装到ObjectFactory中,从而通过ObjectFactory在合适的时机创建合适的bean4.总结以上全部就是Spring对单例Bean的循环依赖的解决方案,核心就是使用三级缓存提前曝光Bean对象。两个Bean A,B互相引用循环依赖,Spring的解决过程如下:通过构建函数创建A对象(A对象是半成品,还没注入属性和调用init方法)。A对象需要注入B对象,发现缓存里还没有B对象,将半成品对象A放入半成品缓存。通过构建函数创建B对象(B对象是半成品,还没注入属性和调用init方法)。B对象需要注入A对象,从半成品缓存里取到半成品对象A。B对象继续注入其他属性和初始化,之后将完成品B对象放入完成品缓存。A对象继续注入属性,从完成品缓存中取到完成品B对象并注入。A对象继续注入其他属性和初始化,之后将完成品A对象放入完成品缓存。最后附上一张源码执行流程图:(可自行放大查看)
0
0
0
浏览量2025
后端求offer版

【Nginx】实现负载均衡、限流、缓存、黑白名单和灰度发布,这是最全的一篇了!(建议收藏)

Nginx安装注意:这里以CentOS 6.8服务器为例,以root用户身份来安装Nginx。1.安装依赖环境yum -y install wget gcc-c++ ncurses ncurses-devel cmake make perl bison openssl openssl-devel gcc* libxml2 libxml2-devel curl-devel libjpeg* libpng* freetype* autoconf automake zlib* fiex* libxml* libmcrypt* libtool-ltdl-devel* libaio libaio-devel bzr libtool 2.安装opensslwget https://www.openssl.org/source/openssl-1.0.2s.tar.gz tar -zxvf openssl-1.0.2s.tar.gz cd /usr/local/src/openssl-1.0.2s ./config --prefix=/usr/local/openssl-1.0.2s make make install 3.安装pcrewget https://ftp.pcre.org/pub/pcre/pcre-8.43.tar.gz tar -zxvf pcre-8.43.tar.gz cd /usr/local/src/pcre-8.43 ./configure --prefix=/usr/local/pcre-8.43 make make install 4.安装zlibwget https://sourceforge.net/projects/libpng/files/zlib/1.2.11/zlib-1.2.11.tar.gz tar -zxvf zlib-1.2.11.tar.gz cd /usr/local/src/zlib-1.2.11 ./configure --prefix=/usr/local/zlib-1.2.11 make make 5.安装Nginxwget http://nginx.org/download/nginx-1.17.2.tar.gz tar -zxvf nginx-1.17.2.tar.gz cd /usr/local/src/nginx-1.17.2 ./configure --prefix=/usr/local/nginx-1.17.2 --with-openssl=/usr/local/src/openssl-1.0.2s --with-pcre=/usr/local/src/pcre-8.43 --with-zlib=/usr/local/src/zlib-1.2.11 --with-http_ssl_module make make install 这里需要注意的是:安装Nginx时,指定的是openssl、pcre和zlib的源码解压目录,安装完成后Nginx配置文件的完整路径为:/usr/local/nginx-1.17.2/conf/nginx.conf。Nginx负载均衡配置1.负载均衡配置http { …… upstream real_server { server 192.168.103.100:2001 weight=1; #轮询服务器和访问权重 server 192.168.103.100:2002 weight=2; } server { listen 80; location / { proxy_pass http://real_server; } } } 2.失败重试配置upstream real_server { server 192.168.103.100:2001 weight=1 max_fails=2 fail_timeout=60s; server 192.168.103.100:2002 weight=2 max_fails=2 fail_timeout=60s; } 意思是在fail_timeout时间内失败了max_fails次请求后,则认为该上游服务器不可用,然后将该服务地址踢除掉。fail_timeout时间后会再次将该服务器加入存活列表,进行重试。Nginx限流配置1.配置参数limit_req_zone指令设置参数limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; limit_req_zone定义在http块中,$binary_remote_addr表示保存客户端IP地址的二进制形式。Zone定义IP状态及URL访问频率的共享内存区域。zone=keyword标识区域的名字,以及冒号后面跟区域大小。16000个IP地址的状态信息约1MB,所以示例中区域可以存储160000个IP地址。Rate定义最大请求速率。示例中速率不能超过每秒10个请求。2.设置限流location / { limit_req zone=mylimit burst=20 nodelay; proxy_pass http://real_server; } burst排队大小,nodelay不限制单个请求间的时间。3.不限流白名单geo $limit { default 1; 192.168.2.0/24 0; } map $limit $limit_key { 1 $binary_remote_addr; 0 ""; } limit_req_zone $limit_key zone=mylimit:10m rate=1r/s; location / { limit_req zone=mylimit burst=1 nodelay; proxy_pass http://real_server; } 上述配置中,192.168.2.0/24网段的IP访问是不限流的,其他限流。IP后面的数字含义:24表示子网掩码:255.255.255.016表示子网掩码:255.255.0.08表示子网掩码:255.0.0.0Nginx缓存配置1.浏览器缓存静态资源缓存用expirelocation ~* .(jpg|jpeg|png|gif|ico|css|js)$ { expires 2d; } Response Header中添加了Expires和Cache-Control,静态资源包括(一般缓存)普通不变的图像,如logo,图标等js、css静态文件可下载的内容,媒体文件协商缓存(add_header ETag/Last-Modified value)HTML文件经常替换的图片经常修改的js、css文件基本不变的API接口不需要缓存用户隐私等敏感数据经常改变的api数据接口2.代理层缓存//缓存路径,inactive表示缓存的时间,到期之后将会把缓存清理 proxy_cache_path /data/cache/nginx/ levels=1:2 keys_zone=cache:512m inactive = 1d max_size=8g; location / { location ~ \.(htm|html)?$ { proxy_cache cache; proxy_cache_key $uri$is_args$args; //以此变量值做HASH,作为KEY //HTTP响应首部可以看到X-Cache字段,内容可以有HIT,MISS,EXPIRES等等 add_header X-Cache $upstream_cache_status; proxy_cache_valid 200 10m; proxy_cache_valid any 1m; proxy_pass http://real_server; proxy_redirect off; } location ~ .*\.(gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ { root /data/webapps/edc; expires 3d; add_header Static Nginx-Proxy; } } 在本地磁盘创建一个文件目录,根据设置,将请求的资源以K-V形式缓存在此目录当中,KEY需要自己定义(这里用的是url的hash值),同时可以根据需要指定某内容的缓存时长,比如状态码为200缓存10分钟,状态码为301,302的缓存5分钟,其他所有内容缓存1分钟等等。可以通过purger的功能清理缓存。AB测试/个性化需求时应禁用掉浏览器缓存。Nginx黑名单1.一般配置location / { deny 192.168.1.1; deny 192.168.1.0/24; allow 10.1.1.0/16; allow 2001:0db8::/32; deny all; } 2. Lua+Redis动态黑名单(OpenResty)安装运行yum install yum-utils yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo yum install openresty yum install openresty-resty 查看 yum --disablerepo="*" --enablerepo="openresty" list available 运行 service openresty start 配置(/usr/local/openresty/nginx/conf/nginx.conf)lua_shared_dict ip_blacklist 1m; server { listen 80; location / { access_by_lua_file lua/ip_blacklist.lua; proxy_pass http://real_server; } } lua脚本(ip_blacklist.lua)local redis_host = "192.168.1.132" local redis_port = 6379 local redis_pwd = 123456 local redis_db = 2 -- connection timeout for redis in ms. local redis_connection_timeout = 100 -- a set key for blacklist entries local redis_key = "ip_blacklist" -- cache lookups for this many seconds local cache_ttl = 60 -- end configuration local ip = ngx.var.remote_addr local ip_blacklist = ngx.shared.ip_blacklist local last_update_time = ip_blacklist:get("last_update_time"); -- update ip_blacklist from Redis every cache_ttl seconds: if last_update_time == nil or last_update_time < ( ngx.now() - cache_ttl ) then local redis = require "resty.redis"; local red = redis:new(); red:set_timeout(redis_connect_timeout); local ok, err = red:connect(redis_host, redis_port); if not ok then ngx.log(ngx.ERR, "Redis connection error while connect: " .. err); else local ok, err = red:auth(redis_pwd) if not ok then ngx.log(ngx.ERR, "Redis password error while auth: " .. err); else local new_ip_blacklist, err = red:smembers(redis_key); if err then ngx.log(ngx.ERR, "Redis read error while retrieving ip_blacklist: " .. err); else ngx.log(ngx.ERR, "Get data success:" .. new_ip_blacklist) -- replace the locally stored ip_blacklist with the updated values: ip_blacklist:flush_all(); for index, banned_ip in ipairs(new_ip_blacklist) do ip_blacklist:set(banned_ip, true); end -- update time ip_blacklist:set("last_update_time", ngx.now()); end end end end if ip_blacklist:get(ip) then ngx.log(ngx.ERR, "Banned IP detected and refused access: " .. ip); return ngx.exit(ngx.HTTP_FORBIDDEN); end Nginx灰度发布1.根据Cookie实现灰度发布根据Cookie查询version值,如果该version值为v1转发到host1,为v2转发到host2,都不匹配的情况下转发到默认配置。upstream host1 { server 192.168.2.46:2001 weight=1; #轮询服务器和访问权重 server 192.168.2.46:2002 weight=2; } upstream host2 { server 192.168.1.155:1111 max_fails=1 fail_timeout=60; } upstream default { server 192.168.1.153:1111 max_fails=1 fail_timeout=60; } map $COOKIE_version $group { ~*v1$ host1; ~*v2$ host2; default default; } lua_shared_dict ip_blacklist 1m; server { listen 80; #set $group "default"; #if ($http_cookie ~* "version=v1"){ # set $group host1; #} #if ($http_cookie ~* "version=v2"){ # set $group host2; #} location / { access_by_lua_file lua/ip_blacklist.lua; proxy_pass http://$group; } } 2.根据来路IP实现灰度发布server { …………… set $group default; if ($remote_addr ~ "192.168.119.1") { set $group host1; } if ($remote_addr ~ "192.168.119.2") { set $group host2; }
0
0
0
浏览量2071
后端求offer版

Spring注解扫描:ComponentScan使用及原理详解

1.概述当下Spring Boot之所以能成为主流首选开发框架,得益于其核心思想:约定大于配置和Spring提供的基于注解配置式开发,解决了繁琐的XML文件配置问题,大大提高了开发效率。基于Spring MVC三层架构框架开发的项目中大量用到@Controller, @Service...等注解,即使这些类在不同包路径下,都能被注入到Spring容器中,然后可以相互之间进行依赖注入、使用。这时候就有一个问题了:Spring是如何将声明了@Component注解的Bean注入到Spring容器当中的呢?怎么做到bean的类定义可以随意写在不同包路径下?答案就是今天的主角@ComponentScan,该注解告诉Spring扫描那些包路径下的类,然后判断如果类使用了@Component,@Controller, @Service...等注解,就注入到Spring容器中。之前我们讲过一个注解@Component,它就是声明当前类是一个bean组件,那@ComponentScan注解顾名思义就是扫描声明了@Component注解的类,然后注入到Spring容器中的。这时候你可能会问@Controller, @Service...等注解为什么也会被扫描、注入到Spring容器中。接下来我们就看看这些注解@Controller, @Service, @Repository和@Component的关系,从这些注解的定义上来看都声明了@Component,所以都是@Component衍生注解,其作用及属性和@Component是一样,只不过是提供了更加明确的语义化,是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰@Controller:一般用于表现层的注解。@Service:一般用于业务层的注解。@Repository:一般用于持久层的注解。@RestController:是@Controller的衍生注解,主要用于前后端分离,接口返回JSON格式数据的表现层注解项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 接下来我们就来讲讲@ComponentScan的使用和底层实现。2.@ComponentScan的使用在讲述@ComponentScan使用之前先来看看定义:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Repeatable(ComponentScans.class)//可重复注解 public @interface ComponentScan { ​   @AliasFor("basePackages")   String[] value() default {};//基础包名,等同于basePackages ​   @AliasFor("value")   String[] basePackages() default {};//基础包名,value ​   Class<?>[] basePackageClasses() default {};//扫描的类,会扫描该类所在包及其子包的组件。 ​   Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;//注册为BeanName生成策略 默认BeanNameGenerator,用于给扫描到的Bean生成BeanName ​   Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;//用于解析bean的scope的属性的解析器,默认是AnnotationScopeMetadataResolver ​   ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;//scoped-proxy 用来配置代理方式 // no(默认值):如果有接口就使用JDK代理,如果没有接口就使用CGLib代理 interfaces: 接口代理(JDK代理) targetClass:类代理(CGLib代理) ​   String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;//配置要扫描的资源的正则表达式的,默认是"**/*.class",即配置类包下的所有class文件。     boolean useDefaultFilters() default true;//useDefaultFilters默认是true,扫描带有@Component ro @Repository ro @Service ro @Controller 的组件 ​   Filter[] includeFilters() default {};//包含过滤器 ​   Filter[] excludeFilters() default {};//排除过滤器 ​   boolean lazyInit() default false;//是否是懒加载 ​   @Retention(RetentionPolicy.RUNTIME)   @Target({})   @interface Filter {//过滤器注解 ​      FilterType type() default FilterType.ANNOTATION;//过滤判断类型 ​      @AliasFor("classes")      Class<?>[] value() default {};//要过滤的类,等同于classes ​      @AliasFor("value")      Class<?>[] classes() default {};//要过滤的类,等同于value ​      String[] pattern() default {};// 正则化匹配过滤 ​   } ​ } ​ 从定义来看,比起之前讲的@Import注解相对有点复杂,但是不用过于担心,其大部分属性使用默认即可,我们一般只需要配置一下basePackages属性指定包扫描路径即可。下面我们来看看如何使用,我在包路径下com.shepherd.common.bean下定义如下类:@Component public class Coo { } ​ @Repository public class Doo { } ​ @Service public class Eoo { } ​ @RestController public class Foo { } 然后在另一个包路径com.shepherd.common.bean1下再定义一个类:@Component public class Goo { } 最后声明一个类,使用ComponentScan注解进行包扫描:@ComponentScan("com.shepherd.common.bean") public class MyConfig { ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } ​ } 执行结果如下:myConfig coo doo eoo foo 我们发现Goo没有注入到Spring容器中,因为我们扫描的包路径是com.shepherd.common.bean,但是它在com.shepherd.common.bean1下,所以没有被扫描到,要想被扫描到只需要指定扫描包添加路径com.shepherd.common.bean1即可@ComponentScan(basePackages = {"com.shepherd.common.bean", "com.shepherd.common.bean1"}) public class MyConfig {      public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } } 执行结果如下:myConfig coo doo eoo foo goo 可以看到Goo被成功扫描、注入到Spring容器中了。从上面@ComponentScan定义看到声明了@Repeatable(ComponentScans.class),意味着该注解可以在同一个类中多次使用,这时候我想着使用两次分别指定不同的包扫描路径,解决前面Goo没有被扫描到的问题,下面的@ComponentScan多次使用等价于 @ComponentScans({@ComponentScan("com.shepherd.common.bean"), @ComponentScan("com.shepherd.common.bean1")}),代码如下:@ComponentScan("com.shepherd.common.bean") @ComponentScan("com.shepherd.common.bean1") public class MyConfig { ​  public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } } 执行结果如下:myConfig 这时候惊奇发现指定的包路径下类都没有被扫描注入,很是纳闷不知道问题出在哪里,只能debug调试了,你会发现又来到配置后置处理器ConfigurationClassPostProcessor的#processConfigBeanDefinitions()方法,这个方法会先判断有没有配置类,没有的话不再做后续的注解解析。 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames(); ​ for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (logger.isDebugEnabled()) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } }      // 判断当前bean(这里就是上面的定义的MyConfig类)是不是配置类,是的话加入配置类候选集合 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } ​ // Return immediately if no @Configuration classes were found    // 配置类集合为空,直接返回,不在做后续的相关注解解析 if (configCandidates.isEmpty()) { return; } ...... } 进入到ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)方法,核心逻辑如下:Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } ​ 这里判断当前是不是配置类,是配置类还分是FULL模式或LITE模式,两种模式的区别之前我们总结过,请查看 @Configuration 和 @Component区别于实现原理,上面定义的MyConfig没有用@Configuration注解,所以config是null,所以接下来会进入到方法isConfigurationCandidate(metadata)发现配置类LITE模式匹配规则里面并没有包含@ComponentScans注解,所以判断当前类不是配置类,自然不会再进行后面的相关注解解析了,这也就是上面多次使用@ComponentScan扫描注入不成功的问题。上面的案例都是只简单配置@ComponentScan的basePackages()属性,当然我们也可以基于@Filter进行过滤啥的,如下面Spring Boot的启动类注解:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ​ ​ } 3.@ComponentScan的实现原理@ComponentScan的底层实现流程和之前我们分析 @Import实现原理基本一致的,都是依靠配置类后置处理器ConfigurationClassPostProcessor进行处理、解析的,核心流程图如下所示:所以我们这里直接看配置类解析器ConfigurationClassParser的解析方法doProcessConfigurationClass() protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { ​    // 对@Component的解析处理,对@ComponentScan注解解析在下面,意味着会先跳过这里对@ComponentScan解析进行包扫描拿到生了@Component的beanDefinition,然后递归调用会再次来到这里解析@Component if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass, filter); } ​ // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } ​ // Process any @ComponentScan annotations 解析@ComponentScan核心所在    // 这里是调用AnnotationConfigUtils的静态方法attributesForRepeatable,获取@ComponentScan注解的属性 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {     // for循环,遍历componentScans,此时仅有一个componentScan,使用componentScanParser解析器来解析componentScan这个对象 for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions =            // componentScanParser解析器进行解析 this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed // for循环扫描到的beanDefinition信息        for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); }          // 这里递归调用前面的配置类解析器的解析方法,也就是会再次来到doProcessConfigurationClass()这个方法,会匹配到方法一开始的对@Component解析逻辑 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } ​ // Process any @Import annotations    // 处理注解@import的入口方法 processImports(configClass, sourceClass, getImports(sourceClass), filter, true); ​ // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } ​ // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } ​ // Process default methods on interfaces processInterfaces(configClass, sourceClass); ​ // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } ​ // No superclass -> processing is complete return null; } ComponentScanAnnotationParser的parse() public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); ​ Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator"); boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass); scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator : BeanUtils.instantiateClass(generatorClass)); ​ ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy"); if (scopedProxyMode != ScopedProxyMode.DEFAULT) { scanner.setScopedProxyMode(scopedProxyMode); } else { Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver"); scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); } ​ scanner.setResourcePattern(componentScan.getString("resourcePattern")); ​ for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addIncludeFilter(typeFilter); } } for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addExcludeFilter(typeFilter); } } ​ boolean lazyInit = componentScan.getBoolean("lazyInit"); if (lazyInit) { scanner.getBeanDefinitionDefaults().setLazyInit(true); } ​ Set<String> basePackages = new LinkedHashSet<>(); String[] basePackagesArray = componentScan.getStringArray("basePackages"); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } if (basePackages.isEmpty()) { basePackages.add(ClassUtils.getPackageName(declaringClass)); } ​ scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { @Override protected boolean matchClassName(String className) { return declaringClass.equals(className); } }); return scanner.doScan(StringUtils.toStringArray(basePackages)); } ​ 初始化ClassPathBeanDefinitionScanner扫描器,根据·@ComponentScan的属性,设置扫描器的属性,最后调用扫描器的doScan()方法执行真正的扫描工作。遍历扫描包,调用findCandidateComponents()方法根据基础包路径来找到候选的Bean。之后就是遍历扫描到的候选Bean,给他们设置作用域,生成BeanName等一系列的操作。然后检查BeanName是否冲突,添加到beanDefinitions集合当中,调用registerBeanDefinition注册Bean,将Bean的定义beanDefinition注册到Spring容器当中,方便后续注入bean。4.总结以上全部就是对@ComponentScan注解实现流程的解析,也是对使用了@Component的组件怎么注入到Spring容器的梳理,Spring Boot项目会默认扫描启动类包下面的所有组件,其自动配置原理实现中使用到了@ComponentScan注解,所以我们需要关注该注解啦。
0
0
0
浏览量2019
后端求offer版

Spring Boot自动配置原理详解和自定义封装实现starter

1.概述之前我们对Spring的注解导入@Import 和 注解扫描@ComponentScan分别进行了详细的总结,不清楚的可以点击链接自行阅读了解,基于这些总结的知识点,我们今天可以来分析一下Spring Boot自动配置的实现原理和自己手动封装一个starter了。我们一直在强调Spring Boot能成为当下主流首选开发框架的主要原因在于其核心思想:约定大于配置,自动配置,条件装配。基于这些特性使得Spring Boot集成其他框架非常简单快捷。使用Spring Boot创建的项目启动、执行也非常简单,只需要执行启动类的main()方法即可,不需要做其他操作,Spring Boot会自动装配相关所需依赖和配置。@SpringBootApplication public class CommonDemoApplication { ​    public static void main(String[] args) {   SpringApplication.run(CommonDemoApplication.class, args);   } ​ } 项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 2.Spring Boot自动配置原理从上面项目启动类可以看出,没有什么复杂的启动逻辑,就只使用一个注解@SpringBootApplication,这就是Spring Boot自动配置的核心入口所在,其定义如下:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ​ // 排除掉自动配置的class @AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {}; ​  // 排除掉自动配置的全路径类名 @AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {}; ​  // 配置扫描的包路径 @AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {}; ​  // 配置扫描的类 @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; ​ // beanName生成器 @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator") Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; ​  // 配置类代理模式:proxyBeanMethods:代理bean的方法  //     Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】  //     Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】 @AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true; ​ } ​ 从定义可知@SpringBootApplication是一个复合注解,所以接下来我们逐一看看其关联使用的注解。2.1 @SpringBootConfiguration@SpringBootConfiguration的定义如下:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { ​ @AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true; ​ } ​ 从定义可知,该注解就是一个配置类注解,其作用和属性和@Configuration注解一样,只是这里语义化罢了,就像@Controller和@Component一个道理。2.2 @EnableAutoConfiguration从名字上看,@EnableAutoConfiguration是自动配置核心所在,先看看其定义:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { ​ String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; ​ Class<?>[] exclude() default {}; ​ String[] excludeName() default {}; ​ } 可以看出@EnableAutoConfiguration也是一个复合注解,所以我们接下来对其关联的注解进行解析:2.2.1 @AutoConfigurationPackage该注解的作用是将添加该注解的类所在的package作为自动配置package 进行管理,也就是说当Spring Boot应用启动时默认会将启动类所在的package作为自动配置的package。老规矩,先看看其定义:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { ​ String[] basePackages() default {}; ​ Class<?>[] basePackageClasses() default {}; ​ } ​ AutoConfigurationPackages.Registrar.class的#register()public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN); ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues(); constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames)); } else { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(BasePackages.class); beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); registry.registerBeanDefinition(BEAN, beanDefinition); } } 这里就是简单注册自动配置包名,方便后续引用,如Spring Boot集成第三方OMR框架mybatis-plus,我们如下编写代码就能把DAO类注入到Spring容器中:@Mapper public interface BrandDAO extends BaseMapper<Brand> { } @Mapper是mybatis框架中的注解,并不是Spring框架中的注解,那么Spring Boot集成mybatis之后是怎么做到自动扫描@Mapper进行注入的呢?这时候我们就要关注到mybatis-plus的stater的自动配置类MybatisPlusAutoConfiguration的内部类    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {        // 当前bean工厂容器        private BeanFactory beanFactory; ​        public AutoConfiguredMapperScannerRegistrar() {       } ​        // 注册beanDefinition        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {            // 判断是否注册了自动配置包            if (!AutoConfigurationPackages.has(this.beanFactory)) {                MybatisPlusAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");           } else {                MybatisPlusAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");                // 获取到之前注册的所有自动配置包路径                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);                if (MybatisPlusAutoConfiguration.logger.isDebugEnabled()) {                    packages.forEach((pkg) -> {                        MybatisPlusAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);                   });               } ​                // 构建beanDefinition                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);                builder.addPropertyValue("processPropertyPlaceHolders", true);                // 指定扫描主机@Mapper                builder.addPropertyValue("annotationClass", Mapper.class);                // 指定扫描的包路径,也就是前面注册的自动配置包路径                builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));                BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);                Stream.of(beanWrapper.getPropertyDescriptors()).filter((x) -> {                    return x.getName().equals("lazyInitialization");               }).findAny().ifPresent((x) -> {                    builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");               });                registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());           }       } ​        public void setBeanFactory(BeanFactory beanFactory) {            this.beanFactory = beanFactory;       }   } 可以看出: @AutoConfigurationPackage和@ComponentScan一样,都是将Spring Boot启动类所在的包及其子包里面的组件扫描到IOC容器中,但是区别是@AutoConfigurationPackage扫描@Enitity、@Mapper等第三方依赖的注解,@ComponentScan只扫描@Controller/@Service/@Component/@Repository这些常见注解。所以这两个注解扫描的对象是不一样的。当然这只是直观上的区别,更深层次说,@AutoConfigurationPackage是自动配置的体现,是Spring Boot中注解,而@ComponentScan是Spring的注解2.2.2 @Import(AutoConfigurationImportSelector.class)这个注解配置是Spring Boot的自动装配核心所在,需重点关注AutoConfigurationImportSelector,定义如下:public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { } 可以看到AutoConfigurationImportSelector除了实现一系列的aware接口获取相关信息之外,就是实现了DeferredImportSelector接口,DeferredImportSelector是ImportSelector的子接口,Deferred是延迟的意思。根据之前我们总结的 @Import的使用和实现原理 可知,对@Import的解析会来到ConfigurationClassParser的#processImports(),方法代码片段如下:if (selector instanceof DeferredImportSelector) {  this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } else {  String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());  Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);  processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); } 这里会判断当前selector 是DeferredImportSelector还是ImportSelector,如果是ImportSelector,才会执行其#selectImports()方法;如果是DeferredImportSelector,会进入执行this.deferredImportSelectorHandler.handle(),该方法会把DeferredImportSelector封装成DeferredImportSelectorHolder放入到this.deferredImportSelectors集合中。根据DeferredImportSelector意思来看,就是延迟注入的意思,所以他会等Spring对配置类相关其他注解进行解析完之后,才执行这里的注入逻辑,可从ConfigurationClassParser的#parse()方法得到验证: public void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { if (bd instanceof AnnotatedBeanDefinition) { parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); } else { parse(bd.getBeanClassName(), holder.getBeanName()); } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); } }    // 等上面的解析完成之后再执行 this.deferredImportSelectorHandler.process(); } 来到DeferredImportSelectorHolder的#process()方法:public void process() { List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { if (deferredImports != null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);          // 遍历调用handler的register() deferredImports.forEach(handler::register);          // 遍历完之后执行processGroupImports() handler.processGroupImports(); } } finally { this.deferredImportSelectors = new ArrayList<>(); } } 遍历deferredImportSelectors集合,每个都会调用handler的#register()方法,这里将AutoConfigurationImportSelector的内部类AutoConfigurationGroup添加到groupings集合当中,并将对应的配置类添加到configurationClasses当中。遍历完deferredImportSelectors之后,调用handler.processGroupImports() public void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { Predicate<String> exclusionFilter = grouping.getCandidateFilter(); grouping.getImports().forEach(entry -> { ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); try { processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter), Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)), exclusionFilter, false); } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configurationClass.getMetadata().getClassName() + "]", ex); } }); } } ​ 遍历之前放在groupings中的DeferredImportSelectorGrouping对象,调用#getImports()方法,该方法返回的是延迟注入的类名封装成的Entry结点的迭代器对象。public Iterable<Group.Entry> getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); } return this.group.selectImports(); } 这里的this.group就是AutoConfigurationImportSelector的内部类AutoConfigurationGroup,遍历延迟注入类,调用#process()方法处理,该方法得到自动配置结点,将其添加到autoConfigurationEntries集合当中。再遍历自动配置结点的所有配置类的类名,添加到entries集合当中。@Override public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {  Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,     () -> String.format("Only %s implementations are supported, got %s",          AutoConfigurationImportSelector.class.getSimpleName(),          deferredImportSelector.getClass().getName()));  // 获取自动配置类entry  AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)     .getAutoConfigurationEntry(annotationMetadata);  // 放入到autoConfigurationEntries集合中  this.autoConfigurationEntries.add(autoConfigurationEntry);  for (String importClassName : autoConfigurationEntry.getConfigurations()) {    this.entries.putIfAbsent(importClassName, annotationMetadata); } } #getAutoConfigurationEntry()方法如下:protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {   if (!isEnabled(annotationMetadata)) {      return EMPTY_ENTRY;   }   AnnotationAttributes attributes = getAttributes(annotationMetadata);   // 获取到所有自动配置类的全限定类名   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);   // 根据相关设置就行排除   configurations = removeDuplicates(configurations);   Set<String> exclusions = getExclusions(annotationMetadata, attributes);   checkExcludedClasses(configurations, exclusions);   configurations.removeAll(exclusions);   configurations = getConfigurationClassFilter().filter(configurations);   fireAutoConfigurationImportEvents(configurations, exclusions);   // 封装成AutoConfigurationEntry返回   return new AutoConfigurationEntry(configurations, exclusions); } #getCandidateConfigurations(annotationMetadata, attributes)方法如下: protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; } SpringFactoriesLoader.loadFactoryNames()会调用#loadSpringFactories()方法:这里就是加载META-INF/spring.factories目录下的自动配置类,使用的是Java提供SPI(Service Provider Interface)扩展机制,不清楚该机制原理的可以看看之前总结的 SPI机制原理和使用,例如mybatis-plus的start的自动配置如下:拿到所有自动配置类之后回到上面的#processGroupImports(),grouping.getImports()获取到所有需要自动装配的类封装对象,接下来会进行一一遍历,调用#processImports()进行注入,至此Spring Boot就完成了自动装配。3.自定义封装实现一个starter首先需要新增一个maven项目,按照Spring Boot官方建议命名格式为xxx-spring-boot-starter, 当然不遵从也是可以,我就没有遵从,这里我例举上面项目推荐中的基于mybatis-plus进行二次封装的一个框架starter:plasticene-spring-boot-starter-mybatis,实现了分页插件,多租户插件集成,实体类公共字段的自动填充、复杂字段的类型处理,数据加密,以及条件构造流式查询等等功能封装。代码路径:github.com/plasticene/…。结构示意图如下:starter项目大概分两个包路径:autoconfigure存放自动配置类,core存放核心逻辑的封装,然后在resources建目录META-INF,写入配置文件spring.factories即可:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  com.plasticene.boot.mybatis.autoconfigure.PlasticeneMybatisAutoConfiguration 综上一个starter就封装好了,接下来我们只需要使用mvn install命令生成pom依赖文件进行发布,其他项目就可以引用了,如果是本地调试,不需要发布,因为install之后本地就有这个依赖包了。4.总结以上全部就是对Spring Boot自动配置原理的分析与讲解,其主要借助于@Import注解和SPI机制进行实现,搞清原理之后我们也手动封装了一个starter进行原理的理解与验证,完美诠释前面所述的实现原理。同时这也是面试高频考点,所以我们的花点心思搞懂它。
0
0
0
浏览量2015
后端求offer版

【Nginx】使用Nginx如何解决跨域问题?看完这篇原来很简单!!

写在前面当今互联网行业,大部分Web项目基本都是采用的前后端分离模式。前端为H5项目,后端为Java、PHP、Python等项目。而且大部分后端服务并不会只部署一套服务,而是会采用Nginx对后端服务进行负载均衡。那么,此时就会出现一个问题了:如果一个请求url的 协议、域名、端口 三者之间任意一个与当前页面url不同就会产生跨域的现象。那么如何使用Nginx解决跨域问题呢?接下来,我们就一起探讨下这个问题。为何会跨域?出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。Nginx如何解决跨域?这里,我们利用Nginx的反向代理功能解决跨域问题,至于,什么是Nginx的反向代理,大家就请自行百度或者谷歌吧。Nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以实现跨域访问。对于浏览器来说,访问的就是同源服务器上的一个url。而Nginx通过检测url前缀,把http请求转发到后面真实的物理服务器。并通过rewrite命令把前缀再去掉。这样真实的服务器就可以正确处理请求,并且并不知道这个请求是来自代理服务器的。Nginx解决跨域案例使用Nginx解决跨域问题时,我们可以编译Nginx的nginx.conf配置文件,例如,将nginx.conf文件的server节点的内容编辑成如下所示。server { location / { root html; index index.html index.htm; //允许cros跨域访问 add_header 'Access-Control-Allow-Origin' '*'; } //自定义本地路径 location /apis { rewrite ^.+apis/?(.*)$ /$1 break; include uwsgi_params; proxy_pass http://www.binghe.com; } } 然后我把项目部署在nginx的html根目录下,在ajax调用时设置url从http://www.binghe.com/apistest/test 变为 www.binghe.com/apis/apiste…假设,之前我在页面上发起的Ajax请求如下所示。$.ajax({ type:"post", dataType: "json", data:{'parameter':JSON.stringify(data)}, url:"http://www.binghe.com/apistest/test", async: flag, beforeSend: function (xhr) { xhr.setRequestHeader("Content-Type", submitType.Content_Type); xhr.setRequestHeader("user-id", submitType.user_id); xhr.setRequestHeader("role-type", submitType.role_type); xhr.setRequestHeader("access-token", getAccessToken().token); }, success:function(result, status, xhr){ } ,error:function (e) { layerMsg('请求失败,请稍后再试') } }); 修改成如下的请求即可解决跨域问题。$.ajax({ type:"post", dataType: "json", data:{'parameter':JSON.stringify(data)}, url:"http:www.binghe.com/apis/apistest/test", async: flag, beforeSend: function (xhr) { xhr.setRequestHeader("Content-Type", submitType.Content_Type); xhr.setRequestHeader("user-id", submitType.user_id); xhr.setRequestHeader("role-type", submitType.role_type); xhr.setRequestHeader("access-token", getAccessToken().token); }, success:function(result, status, xhr){ } ,error:function (e) { layerMsg('请求失败,请稍后再试') } });
0
0
0
浏览量2035
后端求offer版

Spring扩展点(一):后置处理器PostProcessor

1.概述之前我们对Spring相关注解进行全方面的解析与总结,在此期间反复提到了一个核心配置解析类:ConfigurationClassPostProcessor,我们称之为配置类后置处理器。什么是后置处理器呢?其实后置处理器是**Spring提供给我们的一个非常重要的扩展点**,并且Spring内部的很多功能也是通过后置处理器来完成的,ConfigurationClassPostProcessor的重要性就说明这一点,同时该扩展点也方便Spring与其他框架进行集成,如Spring集成mybatis框架,就是通过后置处理器MapperScannerConfigurer实现了扫描mapper接口注入到Spring容器中的。Spring框架中大致提供了以下三个核心后置处理器:BeanDefinitionRegistryPostProcessor,BeanFactoryPostProcessor,BeanPostProcessor,其他的后置处理器都是继承自这三个。三个扩展点的主要功能作用如下:BeanDefinitionRegistryPostProcessor:这个扩展点我们称之为beanDefinition后置处理器,可以动态注册自己的beanDefinition,可以加载classpath之外的bean。BeanFactoryPostProcessor:这个扩展点我们称之为bean工厂后置处理器,调用时机在Spring在读取beanDefinition信息之后,实例化bean之前,主要对beanDefinition的属性进行修改调整,如作用范围scope,是否懒加载lazyInit等。BeanPostProcessor:这个扩展点我们称之为bean后置处理器,调用时机是在bean实例化之后,会经过bean的初始化这一过程,该接口有两个方法,postProcessBeforeInitialization()在属性值填充之后,init()初始化方法执行之前调用。postProcessAfterInitialization()是在init初始化方法执行之后调用。根据上面各个处理器的功能作用描述可以得到三个处理器的执行顺序:BeanDefinitionRegistryPostProcessor → BeanFactoryPostProcessor → BeanPostProcessor 这也是Spring的bean生命周期流程的部分体现,这三个后置处理器调用时机都在bean的生命周期中,当然bean的生命周期也是一个重要知识点,且生命周期远不止这几个扩展点,后续会安排分析一波。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 。Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3…2. 三大后置处理器接下来我们就分别分析下这三个后置处理器:2.1 BeanDefinitionRegistryPostProcessor定义如下:public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { ​ /** * Modify the application context's internal bean definition registry after its * standard initialization. All regular bean definitions will have been loaded, * but no beans will have been instantiated yet. This allows for adding further * bean definitions before the next post-processing phase kicks in. * @param registry the bean definition registry used by the application context * @throws org.springframework.beans.BeansException in case of errors */ void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException; ​ } 可以看到BeanDefinitionRegistryPostProcessor继承自上面的BeanFactoryPostProcessor,说明BeanDefinitionRegistryPostProcessor对BeanFactoryPostProcessor提供的方法进行了增强扩展,实现BeanDefinitionRegistryPostProcessor就必须实现两个接口定义的方法。从上面代码注释翻译来看:BeanDefinitionRegistryPostProcessor主要完成所有常规beanDefinition已经加载完毕,然后可以再添加一些额外的beanDefinition,一句话总结其功能作用就是注册beanDefinition的,ConfigurationClassPostProcessor就是实现BeanDefinitionRegistryPostProcessor来完成配置类及其相关注解解析得到beanDefinition注册到Spring上下文中的。使用示例:首先我们先定义一个类:@Data @AllArgsConstructor @NoArgsConstructor public class Boo {    private Long id;    private String name; } 然后自定义个BeanDefinitionRegistryPostProcessor:@Component // 需要把该后置处理器注入Spring容器中 public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {    @Override    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {        // 注入boo        BeanDefinition beanDefinition = new RootBeanDefinition();        beanDefinition.setBeanClassName("com.shepherd.common.bean.Boo");        registry.registerBeanDefinition("boo", beanDefinition);   } ​    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {        // 该方法是BeanFactoryPostProcessor的方法,这里就不做任何逻辑处理,后面会单独演示 ​   } } 执行测试方法:@ComponentScan(basePackages = {"com.shepherd.common.config"}) @Configuration public class MyConfig { ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } } 会发现结果打印中有boo,说明上面后置处理器成功添加beanDefinition,Spring后续进行了bean的注入。其实核心配置解析类后置处理器ConfigurationClassPostProcessor就是最好的示例,不熟悉的可以跳转到之前总结的@Import的使用和实现原理,看看ConfigurationClassPostProcessor这个后置处理器是怎么实现对@Import的解析的2.2 BeanFactoryPostProcessor定义如下:@FunctionalInterface public interface BeanFactoryPostProcessor { ​ /** * Modify the application context's internal bean factory after its standard * initialization. All bean definitions will have been loaded, but no beans * will have been instantiated yet. This allows for overriding or adding * properties even to eager-initializing beans. * @param beanFactory the bean factory used by the application context * @throws org.springframework.beans.BeansException in case of errors */ void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; ​ } @FunctionalInterface注解表示它是一个函数式接口,可以使用Lambda表达式调用,当然这不是重点,重点看注释,这里我是特意把源码的注释copy出来的,翻译过来大概意思就是:所有的beanDefinition已经全部加载完毕,然后该后置处理器可以对这些beanDefinition做一些属性的修改操作。 这就是对BeanFactoryPostProcessor作用功能的描述使用示例:我们在2.1小节的案例基础中自定义一个BeanFactoryPostProcessor:@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {        System.out.println("BeanFactoryPostProcessor execute...");        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("boo");        if (Objects.nonNull(beanDefinition)) {            beanDefinition.setDescription("芽儿哟,可以的");       }   } } 这里就是对上面添加名为boo的beanDefinition进行了属性修改。调整上面的测试类如下:@ComponentScan(basePackages = {"com.shepherd.common.config"}) @Configuration public class MyConfig { ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        BeanDefinition beanDefinition = applicationContext.getBeanDefinition("boo");        System.out.println(beanDefinition.getDescription());   } } ​ 执行结果控制台打印:芽儿哟,可以的 由此可见,MyBeanFactoryPostProcessor后置处理器成功修改了boo的属性。当然BeanFactoryPostProcessor也可以注册beanDefinition的,看你怎么用。2.3 BeanPostProcessor定义如下:public interface BeanPostProcessor { ​ /** * Apply this {@code BeanPostProcessor} to the given new bean instance <i>before</i> any bean * initialization callbacks (like InitializingBean's {@code afterPropertiesSet} * or a custom init-method). The bean will already be populated with property values. * The returned bean instance may be a wrapper around the original. * <p>The default implementation returns the given {@code bean} as-is. * @param bean the new bean instance * @param beanName the name of the bean * @return the bean instance to use, either the original or a wrapped one; * if {@code null}, no subsequent BeanPostProcessors will be invoked * @throws org.springframework.beans.BeansException in case of errors * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet * 在属性注入完毕, init 初始化方法执行之前被回调 */ @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } ​ /** * Apply this {@code BeanPostProcessor} to the given new bean instance <i>after</i> any bean * initialization callbacks (like InitializingBean's {@code afterPropertiesSet} * or a custom init-method). The bean will already be populated with property values. * The returned bean instance may be a wrapper around the original. * <p>In case of a FactoryBean, this callback will be invoked for both the FactoryBean * instance and the objects created by the FactoryBean (as of Spring 2.0). The * post-processor can decide whether to apply to either the FactoryBean or created * objects or both through corresponding {@code bean instanceof FactoryBean} checks. * <p>This callback will also be invoked after a short-circuiting triggered by a * {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method, * in contrast to all other {@code BeanPostProcessor} callbacks. * <p>The default implementation returns the given {@code bean} as-is. * @param bean the new bean instance * @param beanName the name of the bean * @return the bean instance to use, either the original or a wrapped one; * if {@code null}, no subsequent BeanPostProcessors will be invoked * @throws org.springframework.beans.BeansException in case of errors * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet * @see org.springframework.beans.factory.FactoryBean * 在初始化方法执行之后,被添加到单例池 singletonObjects 之前被回调 */ @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } ​ } Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的填充、初始方法init的执行等,BeanPostProcessor就是这一阶段的对外扩展点,我们称之为Bean后置处理器。跟上面的Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被Spring自动调用。使用示例:先改造一下上面的bean定义,提供一个实例化构造方法和初始化方法:@Data @AllArgsConstructor public class Boo {    private Long id;    private String name; ​    public Boo() {        System.out.println("boo实例化构造方法执行了...");   } ​    @PostConstruct    // 该注解表示实例化之后执行该初始化方法    public void init() {        System.out.println("boo执行初始化init()方法了...");   } } ​ 然后自定义一个beanPostProcessor:@Component public class MyBeanPostProcessor implements BeanPostProcessor {    @Override    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {        System.out.println("beanPostProcessor的before()执行了...." + beanName);        return bean;   } ​    @Override    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {        System.out.println("beanPostProcessor的after()执行了...."+ beanName);        return bean;   } } 测试方法:@ComponentScan(basePackages = {"com.shepherd.common.config"}) @Configuration public class MyConfig { ​      public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);       } ​ 打印结果如下:boo实例化构造方法执行了... beanPostProcessor的before()执行了....boo boo执行初始化init()方法了... beanPostProcessor的after()执行了....boo 这严格说明BeanPostProcessor方法的执行时间点和顺序,BeanPostProcessor是在bean实例化之后,对bean的初始化init()方法前后进行回调扩展的,这时候你可能会想如果要对bean的实例化前后进行扩展怎么办?Spring肯定也想到这一点,提供了继承自BeanPostProcessor的扩展类后置处理器:InstantiationAwareBeanPostProcessorInstantiationAwareBeanPostProcessor该接口继承了BeanPostProcess接口,区别如下:BeanPostProcess接口只在bean的初始化阶段进行扩展,而InstantiationAwareBeanPostProcessor接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段。该类主要的扩展点有以下5个方法,主要在bean生命周期的两大阶段:实例化阶段 和初始化阶段 ,下面一起进行说明,按调用顺序为:postProcessBeforeInstantiation:实例化bean之前,相当于new这个bean之前postProcessAfterInstantiation:实例化bean之后,相当于new这个bean之后postProcessPropertyValues:bean已经实例化完成,在属性注入时阶段触发,@Autowired,@Resource等注解原理基于此方法实现postProcessBeforeInitialization:初始化bean之前,相当于把bean注入spring上下文之前postProcessAfterInitialization:初始化bean之后,相当于把bean注入spring上下文之后使用场景:这个扩展点非常有用 ,无论是写中间件和业务中,都能利用这个特性。比如对实现了某一类接口的bean在各个生命期间进行收集,或者对某个类型的bean进行统一的设值等等。SmartInstantiationAwareBeanPostProcessor该扩展接口集成自上面的InstantiationAwareBeanPostProcessor,有3个触发点方法:predictBeanType:该触发点发生在postProcessBeforeInstantiation之前(在图上并没有标明,因为一般不太需要扩展这个点),这个方法用于预测Bean的类型,返回第一个预测成功的Class类型,如果不能预测返回null;当你调用BeanFactory.getType(name)时当通过bean的名字无法得到bean类型信息时就调用该回调方法来决定类型信息。determineCandidateConstructors:该触发点发生在postProcessBeforeInstantiation之后,用于确定该bean的构造函数之用,返回的是该bean的所有构造函数列表。用户可以扩展这个点,来自定义选择相应的构造器来实例化这个bean。getEarlyBeanReference:该触发点发生在postProcessAfterInstantiation之后,当有循环依赖的场景,当bean实例化好之后,为了防止有循环依赖,会提前暴露回调方法,用于bean实例化的后置处理。这个方法就是在提前暴露的回调方法中触发。3.总结Spring注解开发和Spring Boot自动装配是当下主流开发首选,我们一再强调其快捷。高效性。现在在开发中间件和公共依赖工具的时候也会用到自动装配特性。让使用者以最小的代价接入。想要深入掌握自动装配套路,就必须要了解Spring对于bean的构造生命周期以及各个扩展接口,如之前我们总结的Spring基于相关注解开发,其背后核心原理就是通过ConfigurationClassPostProcessor这个后置处理器实现的,也就是说Spring通过自己提供的后置处理器扩展点实现了注解解析功能,且在别的地方也有大量应用,可见后置处理器这个扩展点的重要性不言而喻啦。
0
0
0
浏览量2013
后端求offer版

【Nginx】如何获取客户端真实IP、域名、协议、端口?看这一篇就够了!

写在前面Nginx最为最受欢迎的反向代理和负载均衡服务器,被广泛的应用于互联网项目中。这不仅仅是因为Nginx本身比较轻量,更多的是得益于Nginx的高性能特性,以及支持插件化开发,为此,很多开发者或者公司基于Nginx开发出了众多的高性能插件。使用者可以根据自身的需求来为Nginx指定某款插件以增强Nginx在某种特定场景下的功能或者提升Nginx在某种特定场景下的性能。Nginx获取客户端信息注意:本文中的客户端信息指的是:客户端真实IP、域名、协议、端口。Nginx反向代理后,Servlet应用通过request.getRemoteAddr()取到的IP是Nginx的IP地址,并非客户端真实IP,通过request.getRequestURL()获取的域名、协议、端口都是Nginx访问Web应用时的域名、协议、端口,而非客户端浏览器地址栏上的真实域名、协议、端口。直接获取信息存在哪些问题?例如在某一台IP为192.168.1.100的服务器上,Jetty或者Tomcat端口号为8080,Nginx端口号80,Nginx反向代理8080端口:server { listen 80; location / { proxy_pass http://127.0.0.1:8080; # 反向代理应用服务器HTTP地址 } } 在另一台机器上用浏览器打开http://192.168.1.100/test访问某个Servlet应用,获取客户端IP和URL:System.out.println("RemoteAddr: " + request.getRemoteAddr()); System.out.println("URL: " + request.getRequestURL().toString()); 打印的结果信息如下:RemoteAddr: 127.0.0.1 URL: http://127.0.0.1:8080/test 可以发现,Servlet程序获取到的客户端IP是Nginx的IP而非浏览器所在机器的IP,获取到的URL是Nginx proxy_pass配置的URL组成的地址,而非浏览器地址栏上的真实地址。如果将Nginx用作https服务器反向代理后端的http服务,那么request.getRequestURL()获取的URL是http前缀的而非https前缀,无法获取到浏览器地址栏的真实协议。如果此时将request.getRequestURL()获取得到的URL用作拼接Redirect地址,就会出现跳转到错误的地址,这也是Nginx反向代理时经常出现的一个问题。如何解决这些问题?既然直接使用Nginx获取客户端信息存在问题,那我们该如何解决这个问题呢?我们整体上需要从两个方面来解决这些问题:(1)由于Nginx是代理服务器,所有客户端请求都从Nginx转发到Jetty/Tomcat,如果Nginx不把客户端真实IP、域名、协议、端口告诉Jetty/Tomcat,那么Jetty/Tomcat应用永远不会知道这些信息,所以需要Nginx配置一些HTTP Header来将这些信息告诉被代理的Jetty/Tomcat;(2)Jetty/Tomcat这一端,不能再获取直接和它连接的客户端(也就是Nginx)的信息,而是要从Nginx传递过来的HTTP Header中获取客户端信息。具体实践配置nginx首先,我们需要在Nginx的配置文件nginx.conf中添加如下配置。proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; 各参数的含义如下所示。Host包含客户端真实的域名和端口号;X-Forwarded-Proto表示客户端真实的协议(http还是https);X-Real-IP表示客户端真实的IP;X-Forwarded-For这个Header和X-Real-IP类似,但它在多层代理时会包含真实客户端及中间每个代理服务器的IP。此时,再试一下request.getRemoteAddr()和request.getRequestURL()的输出结果:RemoteAddr: 127.0.0.1 URL: http://192.168.1.100/test 可以发现URL好像已经没问题了,但是IP还是本地的IP而非真实客户端IP。但是如果是用Nginx作为https服务器反向代理到http服务器,会发现浏览器地址栏是https前缀但是request.getRequestURL()获取到的URL还是http前缀,也就是仅仅配置Nginx还不能彻底解决问题。通过Java方法获取客户端信息仅仅配置Nginx不能彻底解决问题,那如何才能解决这个问题呢?一种解决方式就是通过Java方法获取客户端信息,例如下面的Java方法。/*** * 获取客户端IP地址;这里通过了Nginx获取;X-Real-IP */ public static String getClientIP(HttpServletRequest request) { String fromSource = "X-Real-IP"; String ip = request.getHeader("X-Real-IP"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Forwarded-For"); fromSource = "X-Forwarded-For"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); fromSource = "Proxy-Client-IP"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); fromSource = "WL-Proxy-Client-IP"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); fromSource = "request.getRemoteAddr"; } return ip; } 这种方式虽然能够获取客户端的IP地址,但是我总感觉这种方式不太友好,因为既然Servlet API提供了request.getRemoteAddr()方法获取客户端IP,那么无论有没有用反向代理对于代码编写者来说应该是透明的。接下来,我就分别针对Jetty服务器和Tomcat服务器为大家介绍下如何进行配置才能更加友好的获取客户端信息。Jetty服务器在Jetty服务器的jetty.xml文件中,找到httpConfig,加入配置:<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration"> ... <Call name="addCustomizer"> <Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg> </Call> </New> 重新启动Jetty,再用浏览器打开http://192.168.1.100/test测试,结果:RemoteAddr: 192.168.1.100 URL: http://192.168.1.100/test 此时可发现通过request.getRemoteAddr()获取到的IP不再是127.0.0.1而是客户端真实IP,request.getRequestURL()获取的URL也是浏览器上的真实URL,如果Nginx作为https代理,request.getRequestURL()的前缀也会是https。另外,Jetty将这个功能封装成一个模块:http-forwarded。如果不想改jetty.xml配置文件的话,也可以启用http-forwarded模块来实现。例如可以通过命令行启动Jetty:java -jar start.jar --module=http-forwarded 更多Jetty如何启用模块的相关资料可以参考:www.eclipse.org/jetty/docum…Tomcat和Jetty类似,如果使用Tomcat作为应用服务器,可以通过配置Tomcat的server.xml文件,在Host元素内最后加入:<Valve className="org.apache.catalina.valves.RemoteIpValve" />
0
0
0
浏览量2022
后端求offer版

Spring条件装配注解:@Conditional及其衍生扩展注解

条件装配是Spring Boot一大特点,根据是否满足指定的条件来决定是否装配 Bean ,做到了动态灵活性,starter的自动配置类中就是使用@Conditional及其衍生扩展注解@ConditionalOnXXX做到了自动装配的,所以接着之前总结的 Spring Boot自动配置原理和自定义封装一个starter,今天分析一下starter中自动配置类的条件装配注解。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用:Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 1 @Conditional@Conditional:该注解是在spring4中新加的,其作用顾名思义就是按照一定的条件进行判断,满足条件才将bean注入到容器中,注解源码如下:@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { ​ /** * All {@link Condition} classes that must {@linkplain Condition#matches match} * in order for the component to be registered. */ Class<? extends Condition>[] value(); ​ } 从代码中可知,该注解可作用在类,方法上,同时只有一个属性value,是一个Class数组,并且需要继承或者实现Condition接口:@FunctionalInterface public interface Condition {    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2); } @FunctionalInterface:表示该接口是一个函数式接口,即可以使用函数式编程,lambda表达式。Condition是个接口,需要实现matches方法,返回true则注入bean,false则不注入。总结:@Conditional注解通过传入一个或者多个实现了的Condition接口的实现类,重写Condition接口的matches方法,其条件逻辑在该方法之中,作用于创建bean的地方。根据上面的描述,接下来我模拟多语言环境条件装配切换不同语言的场景:语言类@Data @Builder public class Language {    private Long id;    private String content; } 条件:public class ChineseCondition implements Condition {    @Override    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {        Environment environment = context.getEnvironment();        String property = environment.getProperty("lang");        if (Objects.equals(property, "zh_CN")) {            return true;       }        return false;   } } public class EnglishCondition implements Condition {    @Override    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {        Environment environment = context.getEnvironment();        String property = environment.getProperty("lang");        if (Objects.equals(property, "en_US")) {            return true;       }        return false;   } } 配置类:@Configuration public class MyConfig {        @Bean    @Conditional(ChineseCondition.class)    public Language chinese() {                return Language.builder().id(1l).content("华流才是最屌的").build();   } ​    @Bean    @Conditional(EnglishCondition.class)    public Language english() {              return Language.builder().id(2l).content("english is good").build();   } ​ ​ ​    public static void main(String[] args) {        System.setProperty("lang", "zh_CN");        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();        // 遍历Spring容器中的beanName        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionName);       }   } } ​ 执行结果:chinese ,说明根据条件匹配到ChineseCondition返回true,成功注入bean。2 @Condition衍生注解2.1@ConditionalOnBean@ConditionalOnBean :当给定的在bean存在时,则实例化当前Bean,示例如下    @Bean    @ConditionalOnBean(name = "address")    public User (Address address) {        //这里如果address实体没有成功注入 这里就会报空指针        address.setCity("hangzhou");        address.setId(1l)        return new User("魅影", city);   } 这里加了ConditionalOnBean注解,表示只有address这个bean存在才会实例化user实现原理如下:2.2.@ConditionalOnMissingBean@ConditionalOnMissingBean:当给定的在bean不存在时,则实例化当前Bean, 与@ConditionalOnBean相反@Configuration public class BeanConfig {    @Bean(name = "notebookPC")    public Computer computer1(){        return new Computer("笔记本电脑");   }    @ConditionalOnMissingBean(Computer.class)    @Bean("reservePC")    public Computer computer2(){        return new Computer("备用电脑");   } ConditionalOnMissingBean无参的情况,通过源码可知,当这个注解没有参数时,仅当他注解到方法,且方法上也有@Bean,才有意义,否则无意义。那意义在于已被注解方法的返回值类型的名字作为ConditionalOnMissingBean的type属性的值。2.3.@ConditionalOnClass@ConditionalOnClass:当给定的类名在类路径上存在,则实例化当前Bean2.4.@ConditionalOnMissingClass@ConditionalOnMissingClass:当给定的类名在类路径上不存在,则实例化当前Bean2.5.@ConditionalOnProperty@ConditionalOnProperty:Spring Boot通过@ConditionalOnProperty来控制Configuration是否生效@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnPropertyCondition.class) public @interface ConditionalOnProperty { ​    // 数组,获取对应property名称的值,与name不可同时使用    String[] value() default {}; ​    // 配置属性名称的前缀,比如spring.http.encoding    String prefix() default ""; ​    // 数组,配置属性完整名称或部分名称    // 可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用    String[] name() default {}; ​    // 可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置    String havingValue() default ""; ​    // 缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效    boolean matchIfMissing() default false;    // 是否可以松散匹配,至今不知道怎么使用的   boolean relaxedNames() default true;} 通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值。 如果该值为空,则返回false; 如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。 如果返回值为false,则该configuration不生效;为true则生效。当然havingValue也可以不设置,只要配置项的值不是false或“false”,都加载Bean示例代码:feign: hystrix:   enabled: true fegin开启断路器hystrix: @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled") public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } ​ 结论:@Conditional及其衍生注解,是为了方便程序根据当前环境或者容器情况来动态注入bean,是Spring Boot条件装配实现的核心所在。
0
0
0
浏览量2014
后端求offer版

Spring基于AOP事务控制实现原理

1.概述对于一个系统应用而言,使用数据库进行数据存储是必然的,意味着开发过程中事务的使用及控制也是必不可少的,当然事务是数据库层面的知识点并不是Spring框架所提出的。使用JDBC开发时,我们使用connnection对事务进行控制,使用MyBatis时,我们使用SqlSession对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化,所以Spring 就在这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制和声明式事务控制。编程式事务控制:Spring提供了事务控制的类和方法,使用编码的方式对业务代码进行事务控制,事务控制代码和业务操作代码耦合到了一起,开发中几乎不使用声明式事务控制: Spring将事务控制的代码封装,对外提供了Xml和注解配置方式,通过配置的方式完成事务的控制,可以达到事务控制与业务操作代码解耦合,开发中推荐使用2.Spring事务管理和封装2.1 原生事务控制在没有框架对事务进行封装之前,我们都是使用底层的原生api来进行事务控制,如JDBC操作数据库控制事务 // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 获取mysql数据库连接 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root"); conn.setAutoCommit(false); // 获取statement statement = conn.createStatement(); // 执行sql,返回结果集 resultSet = statement.executeQuery("xxxx"); // 提交 conn.commit(); // 回滚 // conn.rollback(); 这就是原生操作事务的流程,在我们使用Spring框架开发业务系统时也是离不了这样的事务操作的,如果每与数据库交互都需要按上面步骤进行操作,就会显得十分臃肿、重复编码,所以Spring对此进行了封装来提高编程的效率,让事务控制这一过程自动化、透明化,从而做到让开发者专注业务逻辑编码,无需关注事务控制,由Spring框架AOP切面完成即可。2.2 Spring提供的事务APISpring基于模版方法设计模式实现了事务控制的封装,核心API和模板类如下:核心类解释平台事务管理器PlatformTransactionManager是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案事务定义TransactionDefinition封装事务的隔离级别、传播行为、过期时间等属性信息事务状态TransactionStatus存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等事务管理器—PlatformTransactionManagerPlatformTransactionManager是事务管理器的顶层接口,只规定了事务的基本操作:创建事务,提交事物和回滚事务。public interface PlatformTransactionManager extends TransactionManager { ​ // 打开事务 TransactionStatus getTransaction(@Nullable TransactionDefinition definition)   throws TransactionException; ​ // 提交事务 void commit(TransactionStatus status) throws TransactionException; ​  // 回滚事务 void rollback(TransactionStatus status) throws TransactionException; } Spring使用模板方法模式提供了一个抽象类AbstractPlatformTransactionManager,规定了事务管理器的基本框架,仅将依赖于具体平台的特性作为抽象方法留给子类实现,如mybatis框架的事务管理器是DatasourceTransactionManager,hibernate框架的事务管理器是HibernateTransactionManager,我曾经见过一个项目服务里的ORM框架同时使用了mybatis,hibernate两个框架,至于为啥?大概是想从hibernate转为mybatis吧....然后有这么一个问题,一个逻辑方法有两个数据库操作,一个是用mybatis实现的,一个是用hibernate实现,这个逻辑方法使用了@Transactional(rollbackFor = Exception.class),但是事务竟然没控制住~前面就是问题的原因所在,mybatis和hibernate的事务管理器都不是同一个,肯定控制不住事务的。事务状态—TransactionStatus存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等。public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { ​ /**  * 是否有Savepoint Savepoint是当事务回滚时需要恢复的状态  */ boolean hasSavepoint(); ​ /**  * flush()操作和底层数据源有关,并非强制所有数据源都要支持  */ @Override void flush(); ​ } 还从父接口TransactionExecution,SavepointManager中继承了其他方法/**  * 是否是新事务(或是其他事务的一部分)  */ boolean isNewTransaction(); ​ /**  * 设置rollback-only 表示之后需要回滚  */ void setRollbackOnly(); ​ /**  * 是否rollback-only  */ boolean isRollbackOnly(); ​ /**  * 判断该事务已经完成  */ boolean isCompleted(); /**  * 创建一个Savepoint  */ Object createSavepoint() throws TransactionException; ​ /**  * 回滚到指定Savepoint  */ void rollbackToSavepoint(Object savepoint) throws TransactionException; ​ /**  * 释放Savepoint 当事务完成后,事务管理器基本上自动释放该事务所有的savepoint  */ void releaseSavepoint(Object savepoint) throws TransactionException; 事务属性的定义—TransactionDefinitionTransactionDefinition封装事务的隔离级别、传播行为、过期时间等属性信息 /**  * 返回事务的传播级别  */ default int getPropagationBehavior() {  return PROPAGATION_REQUIRED; } ​ /**  * 返回事务的隔离级别  */ default int getIsolationLevel() {  return ISOLATION_DEFAULT; } ​ /**  * 事务超时时间  */ default int getTimeout() {  return TIMEOUT_DEFAULT; } ​ /**  * 是否为只读事务(只读事务在处理上能有一些优化)  */ default boolean isReadOnly() {  return false; } ​ /**  * 返回事务的名称  */ @Nullable default String getName() {  return null; } ​ ​ /**  * 默认的事务配置  */ static TransactionDefinition withDefaults() {  return StaticTransactionDefinition.INSTANCE; } 2.3 Spring编程式事务实现基于上面底层的API,开发者可以在代码中手动的管理事务的开启、提交、回滚等操作来完成编程式事务控制。在spring项目中可以使用TransactionTemplate和TransactionCallback进行实现手动控制事务。   //设置事务的各种属性;可以猜测TransactionTemplate应该是实现了TransactionDefinition        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);        transactionTemplate.setTimeout(30000);                //执行事务 将业务逻辑封装在TransactionCallback中        transactionTemplate.execute(new TransactionCallback<Object>() {            @Override            public Object doInTransaction(TransactionStatus transactionStatus) {                    //....   业务代码           }       }); 但是我们在开发过程中一般不使用编程式事务控制,因为比较繁琐不够优雅,一般是使用声明式进行事务控制,所以接下来就重点讲讲声明式事务。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 微信公众号:Shepherd进阶笔记 交流探讨qun:Shepherd_1263.声明式事务3.1 示例@Transactional是Spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚,直接来看看我们添加用户和角色的示例代码:​  @Transactional(rollbackFor = Exception.class)  public void addUser(UserParam param) {      String username = param.getUsername();      checkUsernameUnique(username);      User user = PtcBeanUtils.copy(param, User.class);      userDAO.insert(user);      if (!CollectionUtils.isEmpty(param.getRoleIds())) {          userRoleService.addUserRole(user.getId(), param.getRoleIds());     } } 可以看出在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。3.2 实现原理从@EnableTransactionManagement说起,该注解开启注解声明式事务,所以我们就先来看看其定义: @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(TransactionManagementConfigurationSelector.class) public @interface EnableTransactionManagement { ​ /**  * 用来表示默认使用JDK Dynamic Proxy还是CGLIB Proxy  */ boolean proxyTargetClass() default false; ​ /**  * 表示以Proxy-based方式实现AOP还是以Weaving-based方式实现AOP  */ AdviceMode mode() default AdviceMode.PROXY; ​ /**  * 顺序  */ int order() default Ordered.LOWEST_PRECEDENCE; ​ } 可以看出,和注解@EnableAspectJAutoProxy开启aop代理差不多,核心逻辑:@Import(TransactionManagementConfigurationSelector.class),TransactionManangementConfigurationSelector主要是往Spring容器中注入相关bean,核心逻辑如下:public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> { ​ /** * Returns {@link ProxyTransactionManagementConfiguration} or * {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY} * and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()}, * respectively. */ @Override protected String[] selectImports(AdviceMode adviceMode) { switch (adviceMode) { case PROXY: return new String[] {AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()}; case ASPECTJ: return new String[] {determineTransactionAspectClass()}; default: return null; } } } 3.2.1 如何生成代理类AutoProxyRegistrar通过调用AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);向容器中注册AbstractAdvisorAutoProxyCreator,这个类在之前总结的 Spring切面编程实现原理一文中重点解析过,是生成AOP代理类的核心实现所在。UserService实现类中使用了@Transational来进行数据库事务控制,AuthService中不涉及到数据库事务处理,从图中可知UserService是被CGLIB动态代理生成的代理类,而AuthService是原生类,这就是AbstractAdvisorAutoProxyCreator实现的,#getAdvicesAndAdvisorsForBean()会判断bean是否有advisor,有的话就通过动态代理生成代理对象注入到Spring容器中,这就是前面UserService代理对象的由来。 protected Object[] getAdvicesAndAdvisorsForBean( Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) { ​ List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } #findEligibleAdvisors()顾名思义就是找到符合条件的advisor protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { List<Advisor> candidateAdvisors = findCandidateAdvisors(); List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } #findCandidateAdvisors()查找所有候选的advisor @Override protected List<Advisor> findCandidateAdvisors() { // Add all the Spring advisors found according to superclass rules. List<Advisor> advisors = super.findCandidateAdvisors(); // Build Advisors for all AspectJ aspects in the bean factory. if (this.aspectJAdvisorsBuilder != null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors; } super.findCandidateAdvisors()就是获取spring内部规则的advisor,比如说事务控制的advisor:BeanFactoryTransactionAttributeSourceAdvisorthis.aspectJAdvisorsBuilder.buildAspectJAdvisors()是解析使用了@Aspect的切面类,根据切点表达式生成advisor。#findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)调用AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);筛选出能匹配当前bean的advisor。AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass)会调用#canApply()方法: public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { if (advisor instanceof IntroductionAdvisor) { return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } else if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pca = (PointcutAdvisor) advisor; return canApply(pca.getPointcut(), targetClass, hasIntroductions); } else { // It doesn't have a pointcut so we assume it applies. return true; } } 这里判断如果是PointcutAdvisor类型,就会调用canApply(pca.getPointcut(), targetClass, hasIntroductions);,上面提到的事务advisor:BeanFactoryTransactionAttributeSourceAdvisor正好符合。执行BeanFactoryTransactionAttributeSourceAdvisor的TransactionAttributeSourcePointcut对象的matches()方法来进行是否匹配判断,然后根据当前bean的所有method遍历执行判断使用有@Transational注解,来到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute()方法 @Nullable protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } ​ // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); ​ // First try is the method in the target class. TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } ​ // Second try is the transaction attribute on the target class. txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } ​ if (specificMethod != method) { // Fallback is to look at the original method. txAttr = findTransactionAttribute(method); if (txAttr != null) { return txAttr; } // Last fallback is the class of the original method. txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } ​ return null; } 最终来到SpringTransactionAnnotationParser#parseTransactionAnnotation()​ @Override public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) {   //这里就是分析Method是否被@Transactional注解标注,有的话,不用说BeanFactoryTransactionAttributeSourceAdvisor适配当前bean,进行代理,并且注入切点   //BeanFactoryTransactionAttributeSourceAdvisor   AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, Transactional.class);   if (attributes != null) {     return parseTransactionAnnotation(attributes);   }   else {     return null;   } } 上面就是判断是否需要根据@Transactional进行代理对象创建的判断过程。@Transactional的作用就是标识方法需要被代理,同时携带事务管理需要的属性信息。3.2.2 如何进行事务控制在之前Spring切面编程实现原理一文中我碍于文章篇幅只分析了JDK代理的实现方式,但在 SpringBoot 2.x AOP中会默认使用Cglib来实现,所以今天就来分析一下CGLIB这种方式。Spring的CGLIB方式生存代理对象是靠ObjenesisCglibAopProxy完成的,ObjenesisCglibAopProxy继承自CglibAopProxy,调用方法#createProxyClassAndInstance()得到基于Cglib动态代理的对象。最终的代理对象的代理方法DynamicAdvisedInterceptor的#intercept()方法 @Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; TargetSource targetSource = this.advised.getTargetSource(); try { if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool... target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; // Check whether we only have one InvokerInterceptor: that is, // no real advice, but just reflective invocation of the target. if (chain.isEmpty() && CglibMethodInvocation.isMethodProxyCompatible(method)) { // We can skip creating a MethodInvocation: just invoke the target directly. // Note that the final invoker must be an InvokerInterceptor, so we know // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); try { retVal = methodProxy.invoke(target, argsToUse); } catch (CodeGenerationException ex) { CglibMethodInvocation.logFastClassGenerationFailure(method); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } } else { // We need to create a method invocation... retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } retVal = processReturnType(proxy, target, method, retVal); return retVal; } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } } 这和以JDK实现的动态代理JdkDynamicAopProxy实现了InvocationHandler执行invoke()来进行逻辑增强套路是一样的。通过分析 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)返回的是TransactionInterceptor,然后来到new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(),最终调用TransactionInterceptor的#invoke()方法 @Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { // Work out the target class: may be {@code null}. // The TransactionAttributeSource should be passed the target class // as well as the method, which may be from an interface. Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); ​ // Adapt to TransactionAspectSupport's invokeWithinTransaction... return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); } #invokeWithinTransaction()就是通过切面实现事务控制的核心逻辑所在: @Nullable protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,   final InvocationCallback invocation) throws Throwable { ​    TransactionAttributeSource tas = getTransactionAttributeSource();  final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);  final TransactionManager tm = determineTransactionManager(txAttr); ​  //省略部分代码                //获取事物管理器  PlatformTransactionManager ptm = asPlatformTransactionManager(tm);  final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); ​  if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {   // 打开事务(内部就是getTransactionStatus的过程)   TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); ​   Object retVal;   try {    // 执行业务逻辑 invocation.proceedWithInvocation();   }   catch (Throwable ex) {    // 异常回滚    completeTransactionAfterThrowing(txInfo, ex);    throw ex;   }   finally {    cleanupTransactionInfo(txInfo);   } ​   //省略部分代码                        //提交事物   commitTransactionAfterReturning(txInfo);   return retVal; } 4.总结行文至此,Spring基于AOP自动完成事务控制的逻辑分析就完结了。Spring的声明式事务注解开发非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。但是在日常开发中却经常出现事务失效的情况,所以了解Spring事务控制实现还是很有必要的,同时还可以加深对Spring AOP应用的认识。
0
0
0
浏览量2016
后端求offer版

Spring AOP切面编程实现原理

1.概述Spring AOP是Spring框架中极为重要的核心功能,和Spring IOC并称为Spring的两大核心模块。顾名思义,AOP 即 Aspect Oriented Programming,翻译为面向切面编程。OOP面向对象编程是纵向地对一个事物的抽象,一个对象包括静态的属性信息、动态的方法信息等。而AOP是横向地对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程。Spring AOP 是利用 CGLIB 和 JDK 动态代理等方式来实现运行期动态方法增强,其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。因而 AOP 便成为了日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。我们之所以能无感知地在Spring容器bean对象方法前后随意添加代码片段进行逻辑增强,是由于Spring 在运行期帮我们把切面中的代码逻辑动态“织入”到了bean对象方法内,所以说AOP本质上就是一个代理模式。对于代理模式和动态代理技术相关知识点不熟悉的,请先看看之前我总结的:浅析动态代理实现与原理,学习一下动态代理知识点,了解CGlib 和 JDK 两种不同动态代理实现方式原理与区别,并且上面说了Spring AOP就是动态代理技术实现的,只有了解动态代理技术,才能快速掌握今天主题AOP。2.AOP切面编程示例既然AOP切面编程的特点就是可以做到对某一个功能进行统一切面处理,对业务代码无侵入,降低耦合度。那么下面我们就根据日志记录这一功能进行实例讲解,对于AOP的编程实现可以基于XML配置,也可以基于注解开发,当下注解开发是主流,所以下面我们基于注解进行示例展示。切面类定义一个切面类,来进行日志记录的统一打印。@Component  // bean组件 @Aspect // 切面类 public class LogAspect { ​ // 切入点 @Pointcut("execution(* com.shepherd.aop.service.*.*(..))") private void pt(){} ​ /** * 前置通知 */    @Before("pt()") public  void beforePrintLog(){ System.out.println("前置通知beforePrintLog方法开始记录日志了。。。"); } ​ /** * 后置通知 */ @AfterReturning("pt()") public  void afterReturningPrintLog(){ System.out.println("后置通知afterReturningPrintLog方法开始记录日志了。。。"); } /** * 异常通知 */    @AfterThrowing("pt()") public  void afterThrowingPrintLog(){ System.out.println("异常通知afterThrowingPrintLog方法开始记录日志了。。。"); } ​ /** * 最终通知 */ @After("pt()") public  void afterPrintLog(){ System.out.println("最终通知afterPrintLog方法开始记录日志了。。。"); } ​ /** * 环绕通知 */ @Around("pt()") public Object aroundPrintLog(ProceedingJoinPoint pjp){ Object rtValue = null; try{ Object[] args = pjp.getArgs();//得到方法执行所需的参数 ​ System.out.println("环绕通知aroundPrintLog方法开始记录日志了。。。前置"); ​ rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)      System.out.println(rtValue); ​ System.out.println("环绕通知aroundPrintLog方法开始记录日志了。。。后置"); return rtValue; }catch (Throwable t){ System.out.println("环绕通知aroundPrintLog方法开始记录日志了。。。异常"); throw new RuntimeException(t); }finally { System.out.println("环绕通知aroundPrintLog方法开始记录日志了。。。最终"); } } } 首先@Aspect表示该类是一个切面类,只要满足@Pointcut标注的切点表达式,就可以执行相应通知方法增强逻辑打印日志。同时我这里写了aop的所有通知:前置、后置、异常、最终、环绕,其实环绕通知就能实现其他四种通知效果了,但是我为了演示所有通知方式和通知方法执行顺序,就全写了,你可以观察一下执行结果。业务方法随便写一个业务方法:public interface MyService { String doSomething(); } @Service public class MyServiceImpl implements MyService{ ​ @Override public String doSomething() { return "========>>> 业务方法执行成功啦!!! ========>>> "; } } 配置类声明一个配置类开启aop代理功能和bean组件扫描@Configuration //配置类 @ComponentScan(basePackages = {"com.shepherd.aop"})   //扫描bean组件 @EnableAspectJAutoProxy   //开启aop切面功能 public class AopConfig { ​ public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class); MyService myService = ac.getBean(MyService.class); myService.doSomething(); ​ } } 通过以上步骤执行main方法结果如下:环绕通知aroundPrintLog方法开始记录日志了。。。前置 前置通知beforePrintLog方法开始记录日志了。。。 后置通知afterReturningPrintLog方法开始记录日志了。。。 最终通知afterPrintLog方法开始记录日志了。。。 ========>>> 业务方法执行成功啦!!! ========>>> 环绕通知aroundPrintLog方法开始记录日志了。。。后置 环绕通知aroundPrintLog方法开始记录日志了。。。最终 Spring是需要手动添加@EnableAspectJAutoProxy注解进行aop功能集成的,而Spring Boot中使用自动装配的技术,可以不手动加这个注解就实现集成,因为在自动配置类AopAutoConfiguration中已经集成了@EnableAspectJAutoProxy项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 微信公众号:Shepherd进阶笔记 交流探讨群:Shepherd_1263.AOP实现原理首先来看看Spring是如何集成AspectJ AOP的,这时候目光应该定格在在注解@EnableAspectJAutoProxy上@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { ​ /** * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed * to standard Java interface-based proxies. The default is {@code false}. * 该属性指定是否通过CGLIB进行动态代理,spring默认是根据是否实现了接口来判断使用JDK还是CGLIB, * 这也是两种代理主要区别,如果为ture,spring则不做判断直接使用CGLIB代理 */ boolean proxyTargetClass() default false; ​ /** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, i.e. no guarantees that {@code AopContext} access will work. * @since 4.3.1 * 暴露aop代理,这样就可以借助ThreadLocal特性在AopContext在上下文中获取到,可用于解决内部方法调用 a() * 调用this.b()时this不是增强代理对象问题,通过AopContext获取即可 */ boolean exposeProxy() default false; ​ } 该注解核心代码:@Import(AspectJAutoProxyRegistrar.class),这也是Spring集成其他功能通用方式了,对于注解@Import不太熟悉的,可以看看我之前总结的:@Import使用及原理详解。来到AspectJAutoProxyRegistrar类:class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { ​ /** * Register, escalate, and configure the AspectJ auto proxy creator based on the value * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing * {@code @Configuration} class. */ @Override public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { ​    // 重点 重点 重点   注入AspectJAnnotationAutoProxyCreator AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); ​    // 根据@EnableAspectJAutoProxy注解属性进行代理方式和是否暴露aop代理等设置 AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class); if (enableAspectJAutoProxy != null) { if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); } } } ​ } 可以看到这里@import就是作用就是将AnnotationAwareAspectJAutoProxyCreator注册到容器当中,这个类是Spring AOP的关键创建代理对象的时机就在创建一个Bean的时候,而创建代理对象的关键工作其实是由AnnotationAwareAspectJAutoProxyCreator完成的。从上面类图可以看出它本质上是一种BeanPostProcessor。对BeanPostProcessor后置处理器不了解的,可以查看之前总结的:后置处理器PostProcessor,这是Spring核心扩展点。 所以它的执行是在完成原始 Bean 构建后的初始化Bean(initializeBean)过程中进行代理对象生成的,最终放到Spring容器中,我们可以看下它的postProcessAfterInitialization 方法,该方法在其上级父类中AbstractAutoProxyCreator实现的:/** * Create a proxy with the configured interceptors if the bean is * identified as one to proxy by the subclass. * @see #getAdvicesAndAdvisorsForBean */ @Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } 可以看到只有当earlyProxyReferences集合中不存在cacheKey的时候,才会执行wrapIfNecessary方法。Spring AOP对象生成的时机有两个:一个是提前AOP,提前AOP的对象会被放入到earlyProxyReferences集合当中,Spring循环依赖解决方案中如果某个bean有循环依赖,同时需要代理增强,那么就会提前生成aop代理对象放入earlyProxyReferences中,关于循环依赖解决方案详解,请看之前总结的:Spring循环依赖解决方案 若没有提前,AOP会在Bean的生命周期的最后执行postProcessAfterInitialization的时候进行AOP动态代理。进入#wrapIfNecessary()方法,核心逻辑: protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { ​ ....... // Create proxy if we have advice. Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } ​ this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } #getAdvicesAndAdvisorsForBean()遍历Spring容器中所有的bean,判断bean上是否加了@Aspect注解,对加了该注解的类再判断其拥有的所有方法,对于加了通知注解的方法构建出Advisor通知对象放入候选通知链当中。接着基于当前加载的Bean通过切点表达式筛选通知,添加ExposeInvocationInterceptor拦截器,最后对通知链进行排序,得到最终的通知链。得到完整的advice通知链信息后,紧接着通过#createProxy()生成代理对象 protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { ​ if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } ​    // new 一个代理工厂对象 ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); ​    // 判断使用JDK代理还是CGLIB代理 if (proxyFactory.isProxyTargetClass()) { // Explicit handling of JDK proxy targets (for introduction advice scenarios) if (Proxy.isProxyClass(beanClass)) { // Must allow for introductions; can't just set interfaces to the proxy's interfaces only. for (Class<?> ifc : beanClass.getInterfaces()) { proxyFactory.addInterface(ifc); } } } else { // No proxyTargetClass flag enforced, let's apply our default checks... if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { evaluateProxyInterfaces(beanClass, proxyFactory); } } ​    // 构建advisor通知链 Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);  // 将通知链放入代理工厂    proxyFactory.addAdvisors(advisors);    // 将被代理的目标类放入到代理工程中 proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); ​ proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); }    // 基于代理工厂获取代理对象返回 return proxyFactory.getProxy(getProxyClassLoader()); } #proxyFactory.getProxy(getProxyClassLoader())public Object getProxy(@Nullable ClassLoader classLoader) { return createAopProxy().getProxy(classLoader); } #createAopProxy() @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } } AOP的代理方式有两种,一种是CGLIB代理,使用ObjenesisCglibAopProxy来创建代理对象,另一种是JDK动态代理,使用JdkDynamicAopProxy来创建代理对象,最终通过对应的AopProxy的#getProxy()生成代理对象,来看看JdkDynamicAopProxy的: @Override public Object getProxy() { return getProxy(ClassUtils.getDefaultClassLoader()); } ​ @Override public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); } Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)这不就是JDK动态代理技术实现模板代码嘛。到这里aop代理对象就已经生成放到Spring容器中。接下来我们就来看看AOP是怎么执行增强方法的,也就是如何执行aspect切面的通知方法的?还是以JDK实现的动态代理JdkDynamicAopProxy为例,其实现了InvocationHandler来实现动态代理,意味着调用代理对象的方法会调用JdkDynamicAopProxy中的invoke()方法,核心逻辑如下: @Override @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; ​ TargetSource targetSource = this.advised.targetSource; Object target = null; ​ try {       ....... ​ Object retVal; ​ if (this.advised.exposeProxy) { // Make invocation available if necessary.        // 暴露aop代理对象 oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } ​ // Get as late as possible to minimize the time we "own" the target, // in case it comes from a pool. target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); ​ // Get the interception chain for this method.      // 根据切面通知,获取方法的拦截链 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); ​ // Check whether we have any advice. If we don't, we can fallback on direct // reflective invocation of the target, and avoid creating a MethodInvocation. // 拦截链为空,直接反射调用目标方法      if (chain.isEmpty()) { // We can skip creating a MethodInvocation: just invoke the target directly // Note that the final invoker must be an InvokerInterceptor so we know it does // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else {        // 如果不为空,那么会构建一个MethodInvocation对象,调用该对象的proceed()方法执行拦截器链以及目标方法 // We need to create a method invocation... MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); // Proceed to the joinpoint through the interceptor chain. retVal = invocation.proceed(); } ​ // Massage return value if necessary. Class<?> returnType = method.getReturnType(); if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { // Special case: it returned "this" and the return type of the method // is type-compatible. Note that we can't help if the target sets // a reference to itself in another returned object. retVal = proxy; } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } return retVal; } finally { if (target != null && !targetSource.isStatic()) { // Must have come from TargetSource. targetSource.releaseTarget(target); } if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } } 4.总结基于以上全部就是今天要讲解的Spring AOP相关知识点啦,AOP作为Spring框架的核心模块,在很多场景都有应用到,如Spring的事务控制就是通过aop实现的。采用横向抽取机制,取代了传统纵向继承体系重复性代码,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码,从而做到保证开发者在不修改源代码的前提下,为系统中不同的业务组件添加某些通用功能,同时AOP切面编程便于减少系统的重复代码,降低模块间的耦合度,有利于系统未来的可拓展性和可维护性。
0
0
0
浏览量2015
后端求offer版

【Nginx】如何使用自签CA配置HTTPS加密反向代理访问?看了这篇我会了!!

写在前面随着互联网的发展,很多公司和个人越来越重视网络的安全性,越来越多的公司采用HTTPS协议来代替了HTTP协议。为何说HTTPS协议比HTTP协议安全呢?小伙伴们自行百度吧!我就不说了。今天,我们就一起来聊聊如何使用自签CA配置Nginx的HTTPS加密反向代理。咳咳,小伙伴们快上车。 如果这篇文章对你有所帮助,请文末留言,点个赞,给个在看和转发,大家的支持是我持续创作的最大动力!Nginx实现HTTPS出于安全访问考虑,采用的CA是本机Openssl自签名生成的,因此无法通过互联网工信Root CA验证,所以会出现该网站不受信任或安全证书无效的提示,直接跳过,直接访问即可!HTTPS的原理和访问过程服务器必要条件一个服务器私钥 KEY文件一张与服务器域名匹配的CA证书(公钥,根据私钥key生成)访问过程(1)客户端浏览器通过https协议访问服务器的443端口,并获得服务器的证书(公钥);客户端浏览器这时候会去找一些互联网可信的RootCA(权威证书颁发机构)验证当前获取到的证书是否合法有效,PS:这些RootCA是随操作系统一起预设安装在了系统里面的;(2)如果RootCA验证通过,表示该证书是可信的,并且若证书中标注的服务器名称与当前访问的服务器URL地址一致,就会直接使用该证书中包含的公钥解密服务器通过自己的KEY(私钥)加密后传输过来的网页内容,从而正常显示页面内容;(3)如果RootCA验证不通过,说明该证书是未获得合法的RootCA签名和授权,因此也就无法证明当前所访问的服务器的权威性,客户端浏览器这时候就会显示一个警告,提示用户当前访问的服务器身份无法得到验证,询问用户是否继续浏览!(通常自签名的CA证书就是这种情况)这里需要注意,验证CA的有效性,只是证明当前服务器的身份是否合法有效,是否具有公信力以及身份唯一性,防止其他人仿冒该网站;但并不会影响到网页的加密功能,尽管CA证书无法得到权威证明,但是它所包含的公钥和服务器上用于加密页面的私钥依然是匹配的一对,所以服务器用自己的私钥加密的网页内容,客户端浏览器依然是可以用这张证书来解密,正常显示网页内容,所以当用户点击“继续浏览此网站(不推荐)”时,网页就可以打开了;自签名CA证书生成1.用Openssl随机生成服务器密钥,和证书申请文件CSR2.自己给自己签发证书在服务器命令行输入如下命令办法证书。#opensslx509 -req -days 3650 -in moonfly.net.csr -signkeymoonfly.net.key -outmoonfly.net.crt -days 3650  证书的有效期,自己给自己颁发证书,想有多久有效期,就弄多久,我一下弄了10年的有效期;-inmoonfly.net.csr指定CSR文件-signkeymoonfly.net.key指定服务器的私钥key文件-outmoonfly.net.crt 设置生成好的证书文件名一条命令,自己给自己压钢印的身份证 moonfly.net.crt 就诞生了!注:其实严格来讲,这里生成的只是一张RootCA,并不是严格意义上的服务器证书ServerCA,真正的ServerCA是需要利用这张RootCA再给服务器签署办法出来的证书才算;不过我们这里只讲如何实现网页的SSL加密,所以就直接使用RootCA了,也是能正常实现加密功能的!NGINX配置启用HTTPS并配置加密反向代理配置文件修改完毕后,用nginx -t 测试下配置无误,就reload一下nginx服务,检查443端口是否在监听:配置完毕,https已经在工作了,现在可以通过https访问网站了
0
0
0
浏览量2019
后端求offer版

【Nginx】如何为已安装的Nginx动态添加模块?看完我懂了!!

写在前面很多时候,我们根据当时的项目情况和业务需求安装完Nginx后,后续随着业务的发展,往往会给安装好的Nginx添加其他的功能模块。在为Nginx添加功能模块时,要求Nginx不停机。这就涉及到如何为已安装的Nginx动态添加模块的问题。本文,就和小伙伴们一起探讨如何为已安装的Nginx动态添加模块的问题。为Nginx动态添加模块这里以安装第三方ngx_http_google_filter_module模块为例。Nginx的模块是需要重新编译Nginx,而不是像Apache一样配置文件引用.so下载第三方扩展模块ngx_http_google_filter_module# cd /data/software/ # git clone https://github.com/cuber/ngx_http_google_filter_module 查看nginx编译安装时安装了哪些模块将命令行切换到Nginx执行程序所在的目录并输入./nginx -V,具体如下:[root@binghe sbin]# ./nginx -V nginx version: nginx/1.19.1 built by gcc 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC) built with OpenSSL 1.0.2 22 Jan 2015 TLS SNI support enabled configure arguments: --prefix=/usr/local/nginx-1.19.1 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module [root@binghe sbin]# 可以看出编译安装Nginx使用的参数如下:--prefix=/usr/local/nginx-1.19.1 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module 加入需要安装的模块,重新编译这里添加 --add-module=/data/software/ngx_http_google_filter_module具体如下:./configure --prefix=/usr/local/nginx-1.19.1 --with-openssl=/usr/local/src/openssl-1.0.2 --with-pcre=/usr/local/src/pcre-8.37 --with-zlib=/usr/local/src/zlib-1.2.8 --with-http_ssl_module -–add-module=/data/software/ngx_http_google_filter_module 如上,将之前安装Nginx的参数全部加上,最后添加 --add-module=/data/software/ngx_http_google_filter_module之后,我们要进行编译操作,如下:# make //千万不要make install,不然就真的覆盖 这里,需要注意的是:不要执行make install命令。替换nginx二进制文件# 备份原来的nginx执行程序 # mv /usr/local/nginx-1.19.1/sbin/nginx /usr/local/nginx-1.19.1/sbin/nginx.bak # 将新编译的nginx执行程序复制到/usr/local/nginx-1.19.1/sbin/目录下 # cp /opt/nginx/sbin/nginx /usr/local/nginx-1.19.1/sbin/
0
0
0
浏览量2018
后端求offer版

【Nginx】如何格式化日志并推送到远程服务器?看完原来很简单!!

写在前面Nginx作为最常用的反向代理和负载均衡服务器,被广泛的应用在众多互联网项目的前置服务中,很多互联网项目直接将Nginx服务器作为整个项目的流量入口。这就使得我们可以通过对Nginx服务器日志的分析,就可以分析出整个网站的访问总量、PV、UV、VV等信息。实际上,企业的业务线众多,很难使用一台Nginx服务器来代理所有的线上服务,这就导致企业会在线上部署多台Nginx服务器。而我们如果想分析所有Nginx服务器的总流量信息时,如果分别对每个Nginx服务器进行分析,再汇总所有的信息,一方面增加了分析的复杂度,另一方面也不好维护这些日志信息。所以,大部分企业会将这些日志信息统一汇总到某个数据存储集群中,以方便的进行数据存储、维护与分析统计。那么如何对Nginx的日志进行格式化并推送到远程的服务器呢?今天,我们就一起来探讨下这个问题。配置Nginx格式化Nginx日志并推送到远程服务器,其实很简单,我们只需要在Nginx服务器的配置文件nginx.conf中进行简单的配置即可。例如,我们可以在nginx.conf文件中添加如下配置。log_format common "$remote_addr,$http_ip,$http_mac,$time_local,$status,$request_length,$bytes_sent,$body_bytes_sent,$http_user_agent,$http_referer,$request_method,$request_time,$request_uri,$server_protocol,$request_body,$http_token"; log_format main "$remote_addr,$http_ip,$http_mac,$time_local,$status,$request_length,$bytes_sent,$body_bytes_sent,$http_user_agent,$http_referer,$request_method,$request_time,$request_uri,$server_protocol,$request_body,$http_token"; access_log logs/access.log common; access_log syslog:server=192.168.1.100:9999,facility=local7,tag=nginx,severity=info main; map $http_upgrade $connection_upgrade { default upgrade; '' close; } 上述配置是将Nginx的日志各项参数以逗号分隔的形式进行输出,同时将Nginx日志实时推送到192.168.1.100:9999上。此时,我们只需要在192.168.1.100服务器上部署一个TCP或UDP服务,监听端口为9999,并在192.168.1.100服务器的防火墙开放9999端口。我们写的TCP或UDP服务就会实时接收到Nginx服务器发送过来的日志。通过这种方式,我们就可以将Nginx日志实时收集到某个存储集群中,对Nginx日志进行统一存储、维护和分析。
0
0
0
浏览量2019
后端求offer版

Spring Boot业务代码中使用@Transactional事务失效踩坑点总结

1.概述接着之前我们对Spring AOP以及基于AOP实现事务控制的上文,今天我们来看看平时在项目业务开发中使用声明式事务@Transactional的失效场景,并分析其失效原因,从而帮助开发人员尽量避免踩坑。我们知道 Spring 声明式事务功能提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional注解,即可一键开启方法的事务性配置。当然后端开发人员对数据库事务这个概念并不陌生,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。如下所示:    @Override    @Transactional(rollbackFor = Exception.class)    public void addUser(UserParam param) {        User user = PtcBeanUtils.copy(param, User.class);        userDAO.insert(user);        if (!CollectionUtils.isEmpty(param.getRoleIds())) {            userRoleService.addUserRole(user.getId(), param.getRoleIds());       }   } 新增用户的同时还添加了用户角色,这里就是使用@Transactional来控制事务保证一致性的。但大多数开发仅限于为方法标记 @Transactional来开启声明式事务,认为就可以高枕无忧了,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。正是因为声明式事务@Transactional使用简单,所以很多开发人员不注重细节点,但是@Transactional条条框框还蛮多的,可谓是细节点拉满,如果不注意也不小心就会掉进坑里,今天就让我们一起来了解使用细节,把坑填平咯。2.@Transactional话不多说,先看看该注解定义@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { ​ @AliasFor("transactionManager") String value() default ""; ​ @AliasFor("value") String transactionManager() default ""; ​ Propagation propagation() default Propagation.REQUIRED; ​ Isolation isolation() default Isolation.DEFAULT; ​ int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; ​ boolean readOnly() default false; ​ Class<? extends Throwable>[] rollbackFor() default {}; ​ String[] rollbackForClassName() default {}; ​ Class<? extends Throwable>[] noRollbackFor() default {}; ​ String[] noRollbackForClassName() default {}; ​ } 从上面看出@Transactional既可以作用于类上,也可以作用于方法上,作用于类: 表示所有该类的**public**方法都配置相同的事务属性信息。接下来再看看其属性:propagation: 设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,默认值为 Propagation.REQUIRED,其他属性值信息如下:事务传播行为解释REQUIRED(默认值)A调用B,B需要事务,如果A有事务B就加入A的事务中,如果A没有事务,B就自己创建一个事务REQUIRED_NEWA调用B,B需要新事务,如果A有事务就挂起,B自己创建一个新的事务SUPPORTSA调用B,B有无事务无所谓,A有事务就加入到A事务中,A无事务B就以非事务方式执行NOT_SUPPORTSA调用B,B以无事务方式执行,A如有事务则挂起NEVERA调用B,B以无事务方式执行,A如有事务则抛出异常MANDATORYA调用B,B要加入A的事务中,如果A无事务就抛出异常NESTEDA调用B,B创建一个新事务,A有事务就作为嵌套事务存在,A没事务就以创建的新事务执行isolation : 事务的隔离级别,默认值为 Isolation.DEFAULT。指定事务的隔离级别,事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED 和REPEATABLE_READisolation属性解释DEFAULT默认隔离级别,取决于当前数据库隔离级别,例如MySQL默认隔离级别是REPEATABLE_READREAD_UNCOMMITTEDA事务可以读取到B事务尚未提交的事务记录,不能解决任何并发问题,安全性最低,性能最高READ_COMMITTEDA事务只能读取到其他事务已经提交的记录,不能读取到未提交的记录。可以解决脏读问题,但是不能解决不可重复读和幻读REPEATABLE_READA事务多次从数据库读取某条记录结果一致,可以解决不可重复读,不可以解决幻读SERIALIZABLE串行化,可以解决任何并发问题,安全性最高,但是性能最低timeout : 事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。readOnly: 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。rollbackFor: 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。noRollbackFor: 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 微信公众号:Shepherd进阶笔记 交流探讨qun:Shepherd_1263.@Transactional失效场景、原因及修正方式3.1 同一个类中的方法通过this调用导致失效    public void addUser(UserParam param) {        User user = PtcBeanUtils.copy(param, User.class);        // 新增用户        userDAO.insert(user);        // 添加用户角色        this.addUserRole(user.getId(), param.getRoleIds());        log.info("执行结束了");   } ​    @Transactional(rollbackFor = Exception.class)    public void addUserRole(Long userId, List<Long> roleIds) {        if (CollectionUtils.isEmpty(roleIds)) {            return;       }        List<UserRole> userRoles = new ArrayList<>();        roleIds.forEach(roleId -> {            UserRole userRole = new UserRole();            userRole.setUserId(userId);            userRole.setRoleId(roleId);            userRoles.add(userRole);       });        userRoleDAO.insertBatch(userRoles);        throw new RuntimeException("发生异常咯");   } 执行#addUser()会发现事务控制失效,发生异常事务并没有回滚,用户和角色绑定都插入成功了。这里,我给出@Transactional生效原则 1,必须通过代理过的类从外部调用目标方法才能生效.Spring 是通过 AOP 技术对方法进行增强实现事务控制的,要调用增强过的方法必然是调用代理后的对象,而这里this是原生对象,并不是代理,自然就没有事务控制了。修正方式:①:将this换成代理的userService, 可以自己注入自己@Resource private UserService userService,当然也可以不用注入,直接在Spring容器中获取userService这个bean ②将#addUser()方法开启事务即加上@Transactional(rollbackFor = Exception.class),这里本就该开启,只是为了演示失效情况没加上,因为在#addUser()里面有插入用户的操作涉及到事务的所以本要开启。当然如果#addUser()只是做一些判断、逻辑处理不涉及到数据库事务操作,那么这样解决就显得有点不太合适,而且容易导致另一种事务失效的情况,即因为没有正确处理异常,导致事务即便生效也不一定能回滚。3.2 异常被catch“吃掉了”导致@Transactional失效如下所示:    @Transactional(rollbackFor = Exception.class)    public void addUser(UserParam param) {        try {            User user = PtcBeanUtils.copy(param, User.class);            // 完成一些逻辑处理                     .......                          // 添加用户角色            this.addUserRole(user.getId(), param.getRoleIds());            log.info("执行结束了");       } catch (Exception e) {            log.error(e.getMessage());       }   } ​    @Transactional(rollbackFor = Exception.class)    public void addUserRole(Long userId, List<Long> roleIds) {        if (CollectionUtils.isEmpty(roleIds)) {            return;       }        List<UserRole> userRoles = new ArrayList<>();        roleIds.forEach(roleId -> {            UserRole userRole = new UserRole();            userRole.setUserId(userId);            userRole.setRoleId(roleId);            userRoles.add(userRole);       });        userRoleDAO.insertBatch(userRoles);        throw new RuntimeException("发生异常咯");   } @Transactional生效原则2:只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。之前我们总结过 基于AOP事务控制实现原理说过在 Spring的 TransactionAspectSupport里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理: protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {           ......              try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception        // 捕获到异常,进行回滚操作,如果我们在业务方法已经捕获掉异常,这里就捕获不到了,自然就不会回滚了 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); }     ......         return result; } } 可以看到,只有捕获到异常时才进行回滚操作,如果我们在业务方法已经捕获掉异常,这里就捕获不到了,自然就不会回滚了。修正方式:就是对异常捕获尽量做到局部针对操作,不要笼统把整个方法的代码逻辑都包括进行,这样异常就抛出去了。3.3 @Transactional 属性 rollbackFor 设置错误,导致异常不满足回滚条件直接看代码:    @Transactional public void addUser(UserParam param) {      User user = PtcBeanUtils.copy(param, User.class);           .......              // 添加用户角色      this.addUserRole(user.getId(), param.getRoleIds());      log.info("执行结束了");   } ​    public void addUserRole(Long userId, List<Long> roleIds) throws Exception {        if (CollectionUtils.isEmpty(roleIds)) {            return;       }        List<UserRole> userRoles = new ArrayList<>();        roleIds.forEach(roleId -> {            UserRole userRole = new UserRole();            userRole.setUserId(userId);            userRole.setRoleId(roleId);            userRoles.add(userRole);       });        userRoleDAO.insertBatch(userRoles);        throw new Exception("发生异常咯");   } 这里#addUser()使用@transactional,但没有设置rollbackFor属性,且#addUserRole()抛出的异常是exception,不是RuntimeException,这样事务也失效了,因为默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring才会回滚事务从上面3.2小节的completeTransactionAfterThrowing(txInfo, ex);进去完成回滚操作会判断异常类型是否满足规定,DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而Error 或 RuntimeException 代表了非预期的结果,应该回滚: public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); } 修正方法:设置rollbackFor:@Transactional(rollbackFor = Exception.class)3.4 @Transactional 应用在非 public 修饰的方法上   @Transactional(rollbackFor = Exception.class)   private void addUserRole(Long userId, List<Long> roleIds) {       if (CollectionUtils.isEmpty(roleIds)) {           return;       }       List<UserRole> userRoles = new ArrayList<>();       roleIds.forEach(roleId -> {           UserRole userRole = new UserRole();           userRole.setUserId(userId);           userRole.setRoleId(roleId);           userRoles.add(userRole);       });       userRoleDAO.insertBatch(userRoles);       throw new RuntimeException("发生异常咯");   } idea也会提示爆红:Spring通过CGLIB动态代理来增强生产代理对象,CGLIB 通过继承方式实现代理类,private 方法在子类不可见,自然也就无法进行事务增强。s在基于AOP事务控制实现原理一文中也分析过,会调用到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute()方法 @Nullable protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {    // Don't allow no-public methods as required.    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {     return null;   }       ...... } 修正方式:自然是改成public3.5 @Transactional 注解传播属性 propagation 设置错误如上面我们新增的用户的同时要添加用户角色,但是假如我们希望即使添加角色错误了,还可以正常新增用户。 public void addUser(UserParam param) {      String username = param.getUsername();      checkUsernameUnique(username);      User user = PtcBeanUtils.copy(param, User.class);      // 添加用户      userDAO.insert(user); ​      // 添加用户角色      userRoleService.addUserRole(user.getId(), param.getRoleIds());     } #userRoleService.addUserRole()  @Transactional(rollbackFor = Exception.class)  private void addUserRole(Long userId, List<Long> roleIds) {      if (CollectionUtils.isEmpty(roleIds)) {          return;     }      List<UserRole> userRoles = new ArrayList<>();      roleIds.forEach(roleId -> {          UserRole userRole = new UserRole();          userRole.setUserId(userId);          userRole.setRoleId(roleId);          userRoles.add(userRole);     });      userRoleDAO.insertBatch(userRoles);      throw new RuntimeException("发生异常咯"); } 你会发现只会同时插入失败,无法实现上面所说的。这时候你可能会想到,既然addUserRole()抛出了异常不能插入用户角色,但是addUser()不想受影响,正常添加用户,那么何不在addUser()里面对userRoleService.addUserRole()进行异常捕获,不就可以解决问题了吗?真是如此吗,就让我们来验证一下:    @Transactional(rollbackFor = Exception.class)    public void addUser(UserParam param) {        User user = PtcBeanUtils.copy(param, User.class);        // 添加用户        userDAO.insert(user);        // 添加用户角色        try {            userRoleService.addUserRole(user.getId(), param.getRoleIds());       } catch (Exception e) {            log.error(e.getMessage());       }   } 执行会发现,用户同样没有添加成功,看日志报错:[1689568520410750976] [ERROR] [2023-08-10 17:25:02.023] [http-nio-18888-exec-1@56682]  com.plasticene.fast.service.impl.UserServiceImpl addUser : 发生异常咯 [1689568520410750976] [ERROR] [2023-08-10 17:25:02.097] [http-nio-18888-exec-1@56682]  com.plasticene.boot.web.core.global.GlobalExceptionHandler exceptionHandler : 【系统异常】 org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870) 可以看到发生异常咯是我们在addUser()中捕获到输出的,但是紧接着下一行发现有报出一个异常UnexpectedRollbackException。原因是,主方法添加用户的逻辑和子方法添加用户角色的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。修正方式:其实要想新增用户角色失败不影响添加用户,只需要让新增用户角色单独开启一个新事务即可。   @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)    public void addUserRole(Long userId, List<Long> roleIds) {        List<UserRole> userRoles = new ArrayList<>();        roleIds.forEach(roleId -> {            UserRole userRole = new UserRole();            userRole.setUserId(userId);            userRole.setRoleId(roleId);            userRoles.add(userRole);       });        userRoleDAO.insertBatch(userRoles);        throw new RuntimeException("发生异常啦!");   } 3.6 @Transactional长事务导致生产事故很多开发都觉得Spring的声明式事务使用非常简单,即@Transactional,所以从来不注重细节。当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接。如果我们出现了耗时的操作,比如第三方接口调用、业务逻辑复杂、大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。这就是典型的长事务问题长事务引发的常见危害有:数据库连接池被占满,应用无法获取连接资源;容易引发数据库死锁;数据库回滚时间长;在主从架构中会导致主从延时变大。服务系统开始出现故障:数据库监控平台一直收到告警短信,数据库连接不足,出现大量死锁;日志显示调用流程引擎接口出现大量超时;同时一直提示CannotGetJdbcConnectionException,数据库连接池连接占满。要想解决这个问题其实也不难,只需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开,这样就可以有效控制事务的时长从而避免长事务。当然对一个方法逻辑拆分成多个子方法很有可能造成上面叙述的事务不生效的情况,不过我相信你看到上面的总结肯定没问题啦。4.总结Spring的声明式事务使用@Transactional注解在开发时确实很方便,但是稍有不慎使用不当就会导致事务失效数据不一致、甚至是系统数据库性能问题。所以上面满满的干货总结都是出自日常工作中碰到的,有效帮你避坑。
0
0
0
浏览量2031
后端求offer版

【Nginx】面试官竟然问我Nginx如何生成缩略图,还好我看了这篇文章!!

写在前面今天想写一篇使用Nginx如何生成缩略图的文章,想了半天题目也没想好,这个题目还是一名读者帮我起的。起因就是这位读者最近出去面试,面试官正好问了一个Nginx如何生成缩略图的问题。还别说,就是这么巧呀!!就冲这标题,也要写一篇干货满满的技术好文!! 关于Nginx的安装,小伙伴们可以参考《【Nginx】实现负载均衡、限流、缓存、黑白名单和灰度发布,这是最全的一篇了!》 还有就是,我通过小程序开通了留言功能,小伙伴们如果对文章有什么好的建议和意见,或者在阅读文章时,有什么疑问,都可以在留言区进行留言!!生成缩略图方案为了手机端浏览到与手机分辨率相匹配的图片,提高 APP 访问速度以及减少用户的手机流量,需要将图片生成缩略图,这边共有以下解决方案。A.发布新闻生成多重缩略图 – 无法匹配到各种尺寸图片B.当相应缩略图不存在,则使用 PHP 或者 Java 等程序生成相应缩略图 – 需要程序员协助C.使用 Nginx 自带模块生成缩略图 – 运维即可完成D.使用 Nginx+Lua 生成缩略图经过多方的考虑,决定使用方案 C,使用 Nginx 自带模块生成缩略图。Nginx生成缩略图配置Nginx使用 Nginx 自带模块生成缩略图,模块: --with-http_image_filter_module,例如,我们可以使用如下参数安装Nginx:./configure --prefix=/usr/local/nginx-1.19.1 --with-http_stub_status_module --with-http_realip_module --with-http_image_filter_module --with-debug 接下来,修改 nginx.conf 配置文件,或者将下面的配置放到nginx.conf文件相应的 server 块中。location ~* /(\d+)\.(jpg)$ { set $h $arg_h; # 获取参数h的值 set $w $arg_w; # 获取参数 w 的值 #image_filter crop $h $w; image_filter resize $h $w;# 根据给定的长宽生成缩略图 } location ~* /(\d+)_(\d+)x(\d+)\.(jpg)$ { if ( -e $document_root/$1.$4 ) { # 判断原图是否存在 rewrite /(\d+)_(\d+)x(\d+)\.(jpg)$ /$1.$4?h=$2&w=$3 last; } return 404; } 访问图片配置完成后,我们就可以使用类似如下的方式来访问图片。www.binghe.com/123_100x10.…当我们在浏览器地址栏中输入上面的链接时,Nginx会作出如下的逻辑处理。首先判断是否存在原图 123.jpg,不存在直接返回 404(如果原图都不存在,那就没必要生成缩略图了)跳转到 www.binghe.com/123.jpg?h=1… h=100 和宽 w=10 带到 url 中。Image_filter resize 指令根据 h 和 w 参数生成相应缩略图。注意:使用Nginx生成等比例缩略图时有一个长宽取小的原则,例如原图是 100*10,你传入的是 10*2,那么Nginx会给你生成 10*1 的图片。生成缩略图只是 image_filter 功能中的一个,它一共支持 4 种参数:test:返回是否真的是图片size:返回图片长短尺寸,返回 json 格式数据corp:截取图片的一部分,从左上角开始截取,尺寸写小了,图片会被剪切resize:缩放图片,等比例缩放Nginx 生成缩略图优缺点优点:根据传入参数即可生成各种比例图片不占用任何硬盘空间缺点:消耗 CPU访问量大将会给服务器带来比较大的负担建议:生成缩略是个消耗 CPU 的操作,如果访问量比较大的站点,最好考虑使用程序生成缩略图到硬盘上,或者在前端加上 Cache缓存或者使用 CDN。
0
0
0
浏览量2020
后端求offer版

Spring注解装配:@Autowired和@Resource使用及原理详解

1.背景@Resource和@Autowired都是实现bean的注入,在日常开发中使用非常频繁,但是使用体验不太一样,笔者喜欢用@Resource,因为在使用@Autowired时IDEA会出现一些警告爆红提示:Field injection is not recommended (字段注入是不被推荐的)Spring团队不推荐属性字段注入的方式(ps:日常开发中我们一般都是字段注入,简单了然呀),建议使用基于构造函数的依赖项注入。还有报红如下:IDEA提示找不到该类型的bean,这并不是错误。。。项目启动时mapper bean会被注入到Spring上下文容器中综上情况和个人有强迫症看到警告和爆红就认为代码写得有问题,所以我选择了@Resource。但是这里要申明一下@Autowied本身是没问题的,可以尽情使用,出现以上报红是IDEA工具的问题。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 2.概述Spring 支持使用@Autowired, @Resource注解进行依赖注入2.1 @Autowired和@Resource定义@Autowired@Autowired为Spring 框架提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired。源码如下/** * @since 2.5 * @see AutowiredAnnotationBeanPostProcessor * @see Qualifier * @see Value */ @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { ​ /** * Declares whether the annotated dependency is required. * <p>Defaults to {@code true}. */ boolean required() default true; ​ } ① 按照type在上下文中查找匹配的bean,查找type为Svc的bean② 如果有多个bean,则按照name进行匹配如果有@Qualifier注解,则按照@Qualifier指定的name进行匹配,查找name为svcA的bean如果没有,则按照变量名进行匹配,查找name为svcA的bean③ 匹配不到,则报错。(@Autowired(required=false),如果设置required为false(默认为true),则注入失败时不会抛出异常@Resource@Resource 是JDK1.6支持的注解,由J2EE提供,需要导入包javax.annotation.Resource。源码如下:@Target({TYPE, FIELD, METHOD}) @Retention(RUNTIME) public @interface Resource {      String name() default "";    String lookup() default "";    Class<?> type() default java.lang.Object.class;    enum AuthenticationType {            CONTAINER,            APPLICATION   }    AuthenticationType authenticationType() default AuthenticationType.CONTAINER;    boolean shareable() default true;    String mappedName() default "";    String description() default ""; } 默认按照名称进行装配,名称可以通过name属性进行指定。也提供按照byType 注入。@Resource有两个重要的属性:name 和 type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型如果没有指定name属性,当注解写在字段上时,默认取字段名,按照名称查找。当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找依赖对象。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。@Resource的作用相当于@Autowired,只不过@Autowired按照byType自动注入2.2 @Autowired和@Resource区别依赖识别方式:@Autowired默认是byType可以使用@Qualifier指定Name,@Resource默认ByName如果找不到则ByType适用对象:@Autowired可以对构造器、方法、参数、字段使用,@Resource只能对方法、字段使用提供方:@Autowired是Spring提供的,@Resource是JSR-250提供的3.实现原理3.1 @Autowired实现原理Spring中通过AutowiredAnnotationBeanPostProcessor来解析注入注解为目标注入值。该class继承InstantiationAwareBeanPostProcessorAdapter,实现了MergedBeanDefinitionPostProcessor,PriorityOrdered, BeanFactoryAware等接口,重写的方法将在IOC创建bean的时候被调用。public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware{    /**     * Create a new {@code AutowiredAnnotationBeanPostProcessor} for Spring's     * standard {@link Autowired @Autowired} and {@link Value @Value} annotations.     * <p>Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation,     * if available.     */    @SuppressWarnings("unchecked")    public AutowiredAnnotationBeanPostProcessor() {      this.autowiredAnnotationTypes.add(Autowired.class);      this.autowiredAnnotationTypes.add(Value.class);      try {        this.autowiredAnnotationTypes.add((Class<? extends Annotation>)            ClassUtils.forName("javax.inject.Inject",        AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));        logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");     }      catch (ClassNotFoundException ex) {        // JSR-330 API not available - simply skip.     }   }   ......   } 前面已经提到了是Spring创建bean的时候调用,即当使用DefaultListableBeanFactory来获取想要的bean的时候会调用AutowiredAnnotationBeanPostProcessor,经过调用AbstractBeanFactory中的getBean方法,继续追踪源码最后在AbstractAutowireCapableBeanFactory类中的createBean方法中找到了调用解析@autowired的方法:doCreateBean() protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { ​   ...... ​ // Allow post-processors to modify the merged bean definition. // 判断是否有后置处理 // 如果有后置处理,则允许后置处理修改 BeanDefinition synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try {          // 重点 重点 重点          // 这里会调用AutowiredAnnotationBeanPostProcessor查找解析注入的元信息 applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } mbd.postProcessed = true; } } ​ ...... ​ // Initialize the bean instance. // 开始初始化 bean 实例对象 Object exposedObject = bean; try { // 对 bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 bean 的属性 // 则会递归初始依赖 bean populateBean(beanName, mbd, instanceWrapper); // 调用初始化方法 exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); } } ​ .......       return exposedObject; } 执行到applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName)时会调用AutowiredAnnotationBeanPostProcessor的postProcessMergedBeanDefinition()完成注入元信息的查找解析。进入populateBean()方法:会调用AutowiredAnnotationBeanPostProcessor方法postProcessPropertyValues()方法完成属性值注入,因为AutowiredAnnotationBeanPostProcessor是继承了InstantiationAwareBeanPostProcessorAdapter,是一个后置处理器。接下来基于这两个核心入口分别讲述一下:查找解析元信息实现了接口类MergedBeanDefinitionPostProcessor的方法postProcessMergedBeanDefinition,这个方法是合并我们定义类的信息,比如:一个类继承了其它类,这个方法会把父类属性和信息合并到子类中@Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {    // 根据bean的名称、类型查找注入的元信息 InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); findAutowiringMetadata()代码如下: private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers.    //先读缓存 String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // Quick check on the concurrent map first, with minimal locking. InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); }          //把查找出来的元信息进行构建 metadata = buildAutowiringMetadata(clazz); this.injectionMetadataCache.put(cacheKey, metadata); } } } return metadata; } ​ 构建注解元信息 buildAutowiringMetadata()​ private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {    //判断是否符合条件注解类型(@AutoWired和@Value) if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { return InjectionMetadata.EMPTY; } List<InjectionMetadata.InjectedElement> elements = new ArrayList<>(); Class<?> targetClass = clazz; do { final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();      //先处理注解字段 ReflectionUtils.doWithLocalFields(targetClass, field -> {        //是不是要找的字段 MergedAnnotation<?> ann = findAutowiredAnnotation(field); if (ann != null) {          //静态字段不支持@Autowired if (Modifier.isStatic(field.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static fields: " + field); } return; } boolean required = determineRequiredStatus(ann); currElements.add(new AutowiredFieldElement(field, required)); } });      //再处理注解方法 ReflectionUtils.doWithLocalMethods(targetClass, method -> { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod); if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {          //@Autowired不支持静态方法 if (Modifier.isStatic(method.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static methods: " + method); } return; } if (method.getParameterCount() == 0) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation should only be used on methods with parameters: " + method); } } boolean required = determineRequiredStatus(ann); PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new AutowiredMethodElement(method, required, pd)); } }); elements.addAll(0, currElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); return InjectionMetadata.forElements(elements, clazz); } ​ 基于上面方法找到了所有需要注入的元信息并进行解析,这个过程也是一个依赖查找过程。属性值注入属性注入的通过AutowiredAnnotationBeanPostProcessor方法postProcessPropertyValues()方法完成的​ ​ @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {        //元信息查找、解析,在上一步已经分析过了 InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try {            //进行注入 metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); } return pvs; } public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Collection<InjectedElement> checkedElements = this.checkedElements; Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements); if (!elementsToIterate.isEmpty()) { for (InjectedElement element : elementsToIterate) { if (logger.isTraceEnabled()) { logger.trace("Processing injected element of bean '" + beanName + "': " + element); }        //注入 element.inject(target, beanName, pvs); } } }       /** * Either this or {@link #getResourceToInject} needs to be overridden. */ protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs) throws Throwable {      //字段注入 if (this.isField) { Field field = (Field) this.member;        //设置字段权限为可以访问权限 ReflectionUtils.makeAccessible(field); field.set(target, getResourceToInject(target, requestingBeanName)); } else {        //方法注入     if (checkPropertySkipping(pvs)) { return; } try { Method method = (Method) this.member;          //设置方法权限为可以访问权限 ReflectionUtils.makeAccessible(method); method.invoke(target, getResourceToInject(target, requestingBeanName)); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } 自此基于@Autowired依赖注入核心逻辑就实现了。3.2 @Resource实现原理@Resource和@Autowired的实现逻辑和流程基本是一样的,只是@Resource是通过CommonAnnotationBeanPostProcessor实现的,这里由于具体逻辑和实现细节和上面的@Autowired差不多,这里就不再赘述。
0
0
0
浏览量2014
后端求offer版

高频面试题: 讲讲Spring bean生命周期 看这篇就足够了!

1.概述之前我们在总结Spring扩展点:后置处理器时谈到了Spring Bean的生命周期和其对Spring框架原理理解的重要性,所以接下来我们就来分析一下Bean生命周期的整体流程。首先Bean就是一些Java对象,只不过这些Bean不是我们主动new出来的,而是交个Spring IOC容器创建并管理的,因此Bean的生命周期受Spring IOC容器控制,Bean生命周期大致分为以下几个阶段:Bean的实例化(Instantiation) :Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化Bean的属性赋值(Populate) :Bean实例化之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充,Bean的属性赋值就是指 Spring 容器根据BeanDefinition中属性配置的属性值注入到 Bean 对象中的过程。Bean的初始化(Initialization) :对Bean实例的属性进行填充完之后还需要执行一些Aware接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法等。该阶段是Spring最具技术含量和复杂度的阶段,并且Spring高频面试题Bean的循环引用问题也是在这个阶段体现的;Bean的使用阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池singletonObjects中去了,即完成了Spring Bean的整个生命周期,接下来Bean就可以被随心所欲地使用了。Bean的销毁(Destruction) :Bean 的销毁是指 Spring 容器在关闭时,执行一些清理操作的过程。在 Spring 容器中, Bean 的销毁方式有两种:销毁方法destroy-method和 DisposableBean 接口。项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3… 2.Bean生命周期详解和使用案例这里我先纠正一下在Spring扩展点后置处理器总结的描述:之前说BeanDefinitionRegistryPostProcessor,BeanFactoryPostProcessor,BeanPostProcessor这三个后置处理器的调用时机都在Spring Bean生命周期中是不严谨的,按照上面我们对Bean生命周期的阶段划分,只有BeanPostProcessor作用于Bean的生命周期中,而BeanDefinitionRegistryPostProcessor,BeanFactoryPostProcessor是针对BeanDefinition的,所以不属于Bean的生命周期中。BeanPostProcessor在Bean生命周期的体现如下图所示:Bean的生命周期和人的一生一样都会经历从出生到死亡,中间是一个漫长且复杂的过程,接下来我们就来整体分析一下Bean生命周期的核心流程和相关接口回调方法的调用时机,同时这里想强调一下Bean的生命周期也是面试的高频考点,对核心流程务必要掌握清楚,这里用一张流程图进行详述展示,是重点、重点、重点。根据上面的Bean生命周期核心流程做如下代码演示示例:Bean定义:@Data @AllArgsConstructor public class Boo implements InitializingBean, DisposableBean, BeanNameAware {    private Long id;    private String name; ​    public Boo() {        System.out.println("boo实例化构造方法执行了...");   } ​    @PostConstruct    public void postConstruct() {        System.out.println("boo执行初始化@postConstruct注解标注的方法了...");   } ​    @PreDestroy    public void preDestroy() {        System.out.println("boo执行初始化@preDestroy注解标注的方法了...");   } ​    @Override    public void afterPropertiesSet() throws Exception {        System.out.println("boo执行InitializingBean的afterPropertiesSet()方法了...");   } ​    @Override    public void destroy() throws Exception {        System.out.println("boo执行DisposableBean的destroy()方法了...");   } ​    @Override    public void setBeanName(String name) {        System.out.println("boo执行BeanNameAware的setBeanName()方法了...");   } ​    private void initMethod() {        System.out.println("boo执行init-method()方法了...");   } ​    public void destroyMethod() {        System.out.println("boo执行destroy-method()方法了...");   } } ​ 实现InstantiationAwareBeanPostProcessor:@Component public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {    @Override    public Object postProcessBeforeInstantiation(Class<?> BeanClass, String BeanName) throws BeansException {        System.out.println("InstantiationAwareBeanPostProcessor的before()执行了...." + BeanName);        return null;   } ​    @Override    public boolean postProcessAfterInstantiation(Object Bean, String BeanName) throws BeansException {        System.out.println("InstantiationAwareBeanPostProcessor的after()执行了...." + BeanName);        return false;   } } 实现BeanPostProcessor:@Component public class MyBeanPostProcessor implements BeanPostProcessor {    @Override    public Object postProcessBeforeInitialization(Object Bean, String BeanName) throws BeansException {        System.out.println("BeanPostProcessor的before()执行了...." + BeanName);        return Bean;   } ​    @Override    public Object postProcessAfterInitialization(Object Bean, String BeanName) throws BeansException {        System.out.println("BeanPostProcessor的after()执行了...."+ BeanName);        return Bean;   } } 执行下面的配置类测试方法:@ComponentScan(basePackages = {"com.shepherd.common.config"}) @Configuration public class MyConfig { ​ ​    @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")    public Boo boo() {        return new Boo();   } ​ ​    public static void main(String[] args) {        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);        Boo boo = applicationContext.getBean(Boo.class);        System.out.println("拿到boo对象了:" + boo);        applicationContext.close();   } } 名为boo的Bean相关运行结果如下:beanPostProcessor的before()执行了....myConfig beanPostProcessor的after()执行了....myConfig InstantiationAwareBeanPostProcessor的before()执行了....boo boo实例化构造方法执行了... InstantiationAwareBeanPostProcessor的after()执行了....boo boo执行BeanNameAware的setBeanName()方法了... beanPostProcessor的before()执行了....boo boo执行初始化@postConstruct注解标注的方法了... boo执行InitializingBean的afterPropertiesSet()方法了... boo执行init-method()方法了... beanPostProcessor的after()执行了....boo 拿到boo对象了:Boo(id=null, name=null) boo执行初始化@preDestroy标注的方法了... boo执行DisposableBean的destroy()方法了... boo执行destroy-method()方法了... 根据控制台打印结果可以boo的相关方法执行顺序严格遵从上面流程图,同时当我们执行容器applicationContext的关闭方法close()会触发调用bean的销毁回调方法。3.浅析Bean生命周期源码实现DefaultListableBeanFactory是Spring IOC的Bean工厂的一个默认实现,IOC大部分核心逻辑实现都在这里,可关注。Bean生命周期就是创建Bean的过程,这里我们就不在拐弯抹角兜圈子,直接来到DefaultListableBeanFactory继承的AbstractAutowireCapableBeanFactory的#doCreateBean()方法,之前说过在Spring框架中以do开头的方法都是核心逻辑实现所在protected Object doCreateBean(String BeanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { ​ // Instantiate the Bean. // BeanWrapper 是对 Bean 的包装,其接口中所定义的功能很简单包括设置获取被包装的对象,获取被包装 Bean 的属性描述器 BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { // <1> 单例模型,则从未完成的 FactoryBean 缓存中删除 instanceWrapper = this.factoryBeanInstanceCache.remove(BeanName); } if (instanceWrapper == null) { // <2> 使用合适的实例化策略来创建新的实例:工厂方法、构造函数自动注入、简单初始化 instanceWrapper = createBeanInstance(BeanName, mbd, args); } // 包装的实例对象 Object Bean = instanceWrapper.getWrappedInstance(); // 包装的实例class类型 Class<?> BeanType = instanceWrapper.getWrappedClass(); if (BeanType != NullBean.class) { mbd.resolvedTargetType = BeanType; } ​ // Allow post-processors to modify the merged Bean definition. // <3> 判断是否有后置处理 // 如果有后置处理,则允许后置处理修改 BeanDefinition synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try { applyMergedBeanDefinitionPostProcessors(mbd, BeanType, BeanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), BeanName, "Post-processing of merged Bean definition failed", ex); } mbd.postProcessed = true; } } ​ // Eagerly cache singletons to be able to resolve circular references // even when triggered by lifecycle interfaces like BeanFactoryAware. // <4> 解决单例模式的循环依赖 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(BeanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace("Eagerly caching Bean '" + BeanName + "' to allow for resolving potential circular references"); } // 提前将创建的 Bean 实例加入到 singletonFactories 中 // 这里是为了后期避免循环依赖 addSingletonFactory(BeanName, () -> getEarlyBeanReference(BeanName, mbd, Bean)); } ​ // Initialize the Bean instance. // 开始初始化 Bean 实例对象 Object exposedObject = Bean; try { // <5> 对 Bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 Bean 的属性 // 则会递归初始依赖 Bean populateBean(BeanName, mbd, instanceWrapper); // <6> 调用初始化方法 exposedObject = initializeBean(BeanName, exposedObject, mbd); } catch (Throwable ex) { if (ex instanceof BeanCreationException && BeanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException( mbd.getResourceDescription(), BeanName, "Initialization of Bean failed", ex); } } ​ // <7> 循环依赖处理 if (earlySingletonExposure) { // 获取 earlySingletonReference Object earlySingletonReference = getSingleton(BeanName, false); // 只有在存在循环依赖的情况下,earlySingletonReference 才不会为空 if (earlySingletonReference != null) { // 如果 exposedObject 没有在初始化方法中被改变,也就是没有被增强 if (exposedObject == Bean) { exposedObject = earlySingletonReference; } // 处理依赖 else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(BeanName)) { String[] dependentBeans = getDependentBeans(BeanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(BeanName, "Bean with name '" + BeanName + "' has been injected into other Beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been " + "wrapped. This means that said other Beans do not use the final version of the " + "Bean. This is often the result of over-eager type matching - consider using " + "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example."); } } } } ​ // Register Bean as disposable. try { // <8> 注册 Bean的销毁逻辑 registerDisposableBeanIfNecessary(BeanName, Bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), BeanName, "Invalid destruction signature", ex); } ​ return exposedObject; } 由上面代码可知,Bean的创建过程核心步骤如下:createBeanInstance(BeanName, mbd, args) 进行Bean的实例化populateBean(BeanName, mbd, instanceWrapper)进行Bean的属性填充赋值initializeBean(BeanName, exposedObject, mbd)处理Bean初始化之后的各种回调事件registerDisposableBeanIfNecessary(BeanName, Bean, mbd)注册Bean的销毁接口解决创建Bean过程中的循环依赖,Spring使用三级缓存解决循环依赖,这也是一个重要的知识点,这里不详细阐述,后面会安排接下来我们就来看看和Bean初始化阶段相关各种回调事件执行方法#initializeBean(),分析一下上面流程图的执行顺序是怎么实现的。protected Object initializeBean(final String BeanName, final Object Bean, RootBeanDefinition mbd) {        if (System.getSecurityManager() != null) {            AccessController.doPrivileged(new PrivilegedAction<Object>() {                @Override                public Object run() {                    invokeAwareMethods(BeanName, Bean);                    return null;               }           }, getAccessControlContext());       }        else {            // 涉及到的回调接口点进去一目了然,代码都是自解释的            // BeanNameAware、BeanClassLoaderAware或BeanFactoryAware            invokeAwareMethods(BeanName, Bean);       } ​        Object wrappedBean = Bean;        if (mbd == null || !mbd.isSynthetic()) {            // BeanPostProcessor 的 postProcessBeforeInitialization 回调,这里会执行@PostConstruct标注的方法            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, BeanName);       } ​        try {            // init-methods            // 或者是实现了InitializingBean接口,会调用afterPropertiesSet() 方法            invokeInitMethods(BeanName, wrappedBean, mbd);       }        catch (Throwable ex) {            throw new BeanCreationException(                   (mbd != null ? mbd.getResourceDescription() : null),                    BeanName, "Invocation of init method failed", ex);       }        if (mbd == null || !mbd.isSynthetic()) {            // BeanPostProcessor 的 postProcessAfterInitialization 回调            wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, BeanName);       }        return wrappedBean;   } 至于Bean的销毁流程与Bean初始化类似,从上面的使用示例中看可以得出当容器关闭时,才会对Bean销毁方法进行调用。销毁过程是这样的。顺着close()-> doClose() -> destroyBeans() -> destroySingletons() -> destroySingleton() -> destroyBean() -> Bean.destroy(),会看到最终调用Bean的销毁方法。这里就不在展示源码细节啦,有兴趣的话自行去调试查看了解4.总结以上全部就是对Spring Bean生命周期的全面总结, Spring 的 Bean 容器机制是非常强大的,它可以帮助我们轻松地管理 Bean 对象,并且提供了丰富的生命周期回调方法,允许我们在 Bean 的生命周期中执行自己的特定操作,这对于我们平时工作使用中进行增强扩展至关重要,因此掌握Bean的生命周期是必须的。
0
0
0
浏览量2034
后端求offer版

Spring注解配置:@Configuration 和 @Component 区别及原理详解

1.背景随着Spring Boot的盛行,注解配置式开发受到了大家的青睐,从此告别了基于Spring开发的繁琐XML配置。这里先来提纲挈领的了解一下Spring内部对于配置注解的定义,如@Component、@Configuration、@Bean、@Import等注解,从功能上来讲,这些注解所负责的功能的确不相同,但是从本质上来讲,Spring内部都将其作为配置注解进行处理。对于一个成熟的框架来讲,简单及多样化的配置是至关重要的,那么Spring也是如此,从Spring的配置发展过程来看,整体的配置方式从最初比较“原始”的阶段到现在非常“智能”的阶段,这期间Spring做出的努力是非常巨大的,从XML到自动装配,从Spring到Spring Boot,从@Component到@Configuration以及@Conditional,Spring发展到今日,在越来越好用的同时,也为我们隐藏了诸多的细节,那么今天让我们一起探秘@Component与@Configuration。我们平时在Spring的开发工作中,基本都会使用配置注解,尤其以@Component及@Configuration为主,当然在Spring中还可以使用其他的注解来标注一个类为配置类,这是广义上的配置类概念,但是这里我们只讨论@Component和@Configuration,因为与我们的开发工作关联比较紧密,那么接下来我们先讨论下一个问题,就是 @Component与@Configuration有什么区别?项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用 Github地址:github.com/plasticene/… Gitee地址:gitee.com/plasticene3…2.@Component与@Configuration使用2.1注解定义在讨论两者区别之前,先来看看两个注解的定义:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { ​ /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) */ String value() default ""; ​ } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { ​ @AliasFor(annotation = Component.class) String value() default ""; ​ boolean proxyBeanMethods() default true; ​ } 从定义来看, @Configuration 注解本质上还是 @Component,因此 @ComponentScan 能扫描到@Configuration 注解的类。2.2注解使用接下来看看两者在我们日常开发中的使用:以这两种注解来标注一个类为配置类@Configuration public class AppConfig { } ​ @Component public class AppConfig { } 上面的程序,Spring会将其认为配置类来做处理,但是这里有一个概念需要明确一下,就是在Spring中,对于配置类来讲,其实是有分类的,大体可以分为两类,一类称为LITE模式,另一类称为FULL模式,那么对应上面的注解,@Component就是LITE类型,@Configuration就是FULL类型,如何理解这两种配置类型呢?我们先来看这个程序。当我们使用@Component实现配置类时:@Component public class AppConfig { @Bean public Foo foo() { System.out.println("foo() invoked..."); Foo foo = new Foo(); System.out.println("foo() 方法的 foo hashcode: " + foo.hashCode()); return foo; } ​ @Bean public Eoo eoo() { System.out.println("eoo() invoked..."); Foo foo = foo(); System.out.println("eoo() 方法的 foo hashcode: "+ foo.hashCode()); return new Eoo(); } ​ public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); } } 执行结果如下:foo() invoked... foo() 方法的 foo hashcode: 815992954 eoo() invoked... foo() invoked... foo() 方法的 foo hashcode: 868737467 eoo() 方法的 foo hashcode: 868737467 从结果可知,foo()方法执行了两次,一次是bean方法执行的,一次是eoo()调用执行的,所以两次生成的foo对象是不一样的。很符合大家的预期,但是当我们使用@Configuration标注配置类时,执行结果如下:foo() invoked... foo() 方法的 foo hashcode: 849373393 eoo() invoked... eoo() 方法的 foo hashcode: 849373393 这里可以看到foo()方法只执行了一次,同时eoo()方法调用foo()生成的foo对象是同一个。这也就是@Component和@Configuration的区别现象展示,那么为什么会有这样的一个现象?我们来考虑一个问题,就是eoo()方法中调用了foo()方法,很明显这个foo()这个方法就是会形成一个新对象,假设我们调用的foo()方法不是原来的foo()方法,是不是就可能不会形成新对象?如果我们在调用foo()方法的时候去容器中获取一下foo这个Bean,是不是就可以达到这样的效果?那如何才能达到这样的效果呢?有一个方法,代理!换句话说,我们调用的eoo()和foo()方法,包括AppConfig都被Spring代理了,那么这里我们明白了@Component与@Configuration最根本的区别,那就是@Configuration标注的类会被Spring代理,其实这样描述不是非常严谨,更加准确的来说应该是如果一个类的BeanDefinition的Attribute中有Full配置属性,那么这个类就会被Spring代理3.Spring如何实现FULL配置的代理如果要明白这一点,那么还需要明确一个前提,就是Spring在什么时间将这些配置类转变成FULL模式或者LITE模式的,接下来我们就要介绍个人认为在Spring中非常重要的一个类,ConfigurationClassPostProcessor。3.1ConfigurationClassPostProcessor是什么首先来简单的看一下这个类的定义:public class ConfigurationClassPostProcessor implements                                              BeanDefinitionRegistryPostProcessor,                                               PriorityOrdered,                                               ResourceLoaderAware,                                               BeanClassLoaderAware,                                               EnvironmentAware {} 由这个类定义可知这个类的类型为BeanDefinitionRegistryPostProcessor,以及实现了众多Spring内置的Aware接口,如果了解Beanfactory的后置处理器,那应该清楚ConfigurationClassPostProcessor的执行时机,当然不了解也没有问题,我们会在后面将整个流程阐述清楚,现在需要知道的是ConfigurationClassPostProcessor这个类是在什么时间被实例化的?3.2ConfigurationClassPostProcessor在什么时间被实例化要回答这个问题,需要先明确一个前提,那就是ConfigurationClassPostProcessor这个类对应的BeanDefinition在什么时间注册到Spring的容器中的,因为Spring的实例化比较特殊,主要是基于BeanDefinition来处理的,那么现在这个问题就可以转变为ConfigurationClassPostProcessor这个类是在什么时间被注册为一个Beandefinition的?这个可以在源代码中找到答案,具体其实就是在初始化这个Spring容器的时候。new AnnotationConfigApplicationContext(ConfigClass.class)  -> new AnnotatedBeanDefinitionReader(this);    -> AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);      -> new RootBeanDefinition(ConfigurationClassPostProcessor.class);        -> registerPostProcessor(BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName) 从这里可以看出,ConfigurationClassPostProcessor已经被注册为了一个BeanDefinition,上面我们讲了Spring是通过对BeanDefinition进行解析,处理,实例化,填充,初始化以及众多回调等等步骤才会形成一个Bean,那么现在ConfigurationClassPostProcessor既然已经形成了一个BeanDefinition。3.3 @Component与@Configuration的实现区别上面ConfigurationClassPostProcessor已经注册到BeanDefinition注册中心了,说明Spring会在某个时间点将其处理成一个Bean,那么具体的时间点就是在BeanFactory所有的后置处理器的处理过程。AbstractApplicationContext  -> refresh()    -> invokeBeanFactoryPostProcessors(beanFactory);      -> PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); 这个处理BeanFactory的后置处理器的方法比较复杂,简单说来就是主要处理所有实现了BeanFactoryPostProcessor及BeanDefinitionRegistryPostProcessor的类,当然ConfigurationClassPostProcessor就是其中的一个,那么接下来我们看看实现的方法:  @Override  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {    int registryId = System.identityHashCode(registry);    if (this.registriesPostProcessed.contains(registryId)) {      throw new IllegalStateException(          "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);   }    if (this.factoriesPostProcessed.contains(registryId)) {      throw new IllegalStateException(          "postProcessBeanFactory already called on this post-processor against " + registry);   }    this.registriesPostProcessed.add(registryId); ​    processConfigBeanDefinitions(registry); }    @Override  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {    int factoryId = System.identityHashCode(beanFactory);    if (this.factoriesPostProcessed.contains(factoryId)) {      throw new IllegalStateException(          "postProcessBeanFactory already called on this post-processor against " + beanFactory);   }    this.factoriesPostProcessed.add(factoryId);    if (!this.registriesPostProcessed.contains(factoryId)) {      // BeanDefinitionRegistryPostProcessor hook apparently not supported...      // Simply call processConfigurationClasses lazily at this point then.      processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);   } ​    enhanceConfigurationClasses(beanFactory);    beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory)); } 这两个方法应该就是ConfigurationClassPostProcessor最为关键的了,我们在这里先简单的总结一下,第一个方法主要完成了内部类,@Component,@ComponentScan,@Bean,@Configuration,@Import等等注解的处理,然后生成对应的BeanDefinition,另一个方法就是对@Configuration使用CGLIB进行增强,那我们先来看Spring是在哪里区分配置的LITE模式和FULL模式?在第一个方法中有一个checkConfigurationClassCandidate方法:public static boolean checkConfigurationClassCandidate(      BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {              // ...              Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {          beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);       }        else if (config != null || isConfigurationCandidate(metadata)) {          beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);       }        else {          return false;       }        // ...           } 根据程序的判断可知,如果一个类被@Configuration标注且代理模式为true,那么这个类对应的BeanDefinition将会被Spring添加一个FULL配置模式的属性,有些同学可能对这个”属性“不太理解,这里可以简单说一下,其实这个”属性“在Spring中有一个特定的接口就是AttributeAccessor,BeanDefinition就是继承了这个接口,如何理解这个AttributeAccessor呢?其实也很简单,想想看,BeanDefinition主要是做什么的?这个主要是用来描述Class对象的,例如这个Class是不是抽象的,作用域是什么,是不是懒加载等等信息,那如果一个Class对象有一个“属性”是BeanDefinition描述不了的,那这个要如何处理呢?那这个接口AttributeAccessor又派上用场了,你可以向其中存放任何你定义的数据,可以理解为一个map,现在了解BeanDefinition的属性的含义了么?在这里也能看到@Configuration(proxyBeanMethods = false)和@Component一样效果,都是LITE模式在这里第一步先判断出这个类是FULL模式还是LITE模式,那么下一步就需要开始执行对配置类的注解的解析了,在ConfigurationClassParser这个类有一个processConfigurationClass方法,里面有一个doProcessConfigurationClass方法,这里就是解析前文所列举的@Component等等注解的过程,解析完成之后,在ConfigurationClassPostProcessor类的方法processConfigBeanDefinitions,有一个loadBeanDefinitions方法,这个方法就是将前文解析成功的注解数据全都注册成BeanDefinition,这就是ConfigurationClassPostProcessor这个类的第一个方法所完成的任务,另外这个方法在这里是非常简单的描述了一下,实际上这个方法非常的复杂,需要慢慢的研究。接下来再说ConfigurationClassPostProcessor类的enhanceConfigurationClasses方法,这个方法主要完成了对@Configuration注解标注的类的增强,进行CGLIB代理,代码如下:public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {        Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<String, AbstractBeanDefinition>();        for (String beanName : beanFactory.getBeanDefinitionNames()) {            BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);            if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {//判断是否被@Configuration标注                if (!(beanDef instanceof AbstractBeanDefinition)) {                    throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +                            beanName + "' since it is not stored in an AbstractBeanDefinition subclass");               }                else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) {                    logger.warn("Cannot enhance @Configuration bean definition '" + beanName +                            "' since its singleton instance has been created too early. The typical cause " +                            "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +                            "return type: Consider declaring such methods as 'static'.");               }                configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);           }       }        if (configBeanDefs.isEmpty()) {            // nothing to enhance -> return immediately            return;       }        ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();        for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {            AbstractBeanDefinition beanDef = entry.getValue();            // If a @Configuration class gets proxied, always proxy the target class            beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);            try {                // Set enhanced subclass of the user-specified bean class                Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);                Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);//生成代理的class                if (configClass != enhancedClass) {                    if (logger.isDebugEnabled()) {                        logger.debug(String.format("Replacing bean definition '%s' existing class '%s' with " +                                "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));                   }                    //替换class,将原来的替换为CGLIB代理的class                    beanDef.setBeanClass(enhancedClass);               }           }            catch (Throwable ex) {                throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(), ex);           }       }   } 这里需要CGLIB的一些知识,我就简单的在这里总结一下,这个方法从所有的BeanDefinition中找到属性为FULL模式的BeanDefinition,然后对其进行代理增强,设置BeanDefinition的beanClass。然后在增强时有一些细节稍微需要明确一下,就是我们这个普通类中的方法,比如eoo(),foo()等方法,将会被MethodInterceptor所拦截,这个方法的调用将会被BeanMethodInterceptor所代理,到这里我们大家应该稍微明确了ConfigurationClassPostProcessor是在什么时间被实例化,什么时间解析注解配置,什么时间进行配置增强。如果看到这里不太明白,那欢迎与我来讨论。4.总结@Component在Spring中是代表LITE模式的配置注解,这种模式下的注解不会被Spring所代理,就是一个标准类,如果在这个类中有@Bean标注的方法,那么方法间的相互调用,其实就是普通Java类的方法的调用。@Configuration在Spring中是代表FULL模式的配置注解,这种模式下的类会被Spring所代理,那么在这个类中的@Bean方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用getBean方法还是invokeSuper方法,这里就是这两个注解的最根本的区别。一句话概括就是 @Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。
0
0
0
浏览量2017
后端求offer版

【Nginx】如何基于主从模式搭建Nginx+Keepalived双机热备环境?这是最全的一篇了!!

负载均衡技术负载均衡技术对于一个网站尤其是大型网站的web服务器集群来说是至关重要的!做好负载均衡架构,可以实现故障转移和高可用环境,避免单点故障,保证网站健康持续运行。由于业务扩展,网站的访问量不断加大,负载越来越高。现需要在web前端放置nginx负载均衡,同时结合keepalived对前端nginx实现HA高可用。1)nginx进程基于Master+Slave(worker)多进程模型,自身具有非常稳定的子进程管理功能。在Master进程分配模式下,Master进程永远不进行业务处理,只是进行任务分发,从而达到Master进程的存活高可靠性,Slave(worker)进程所有的业务信号都 由主进程发出,Slave(worker)进程所有的超时任务都会被Master中止,属于非阻塞式任务模型。2)Keepalived是Linux下面实现VRRP备份路由的高可靠性运行件。基于Keepalived设计的服务模式能够真正做到主服务器和备份服务器故障时IP瞬间无缝交接。二者结合,可以构架出比较稳定的软件LB方案。Keepalived介绍Keepalived是一个基于VRRP协议来实现的服务高可用方案,可以利用其来避免IP单点故障,类似的工具还有heartbeat、corosync、pacemaker。但是它一般不会单独出现,而是与其它负载均衡技术(如lvs、haproxy、nginx)一起工作来达到集群的高可用。VRRP协议VRRP全称 Virtual Router Redundancy Protocol,即  虚拟路由冗余协议。可以认为它是实现路由器高可用的容错协议,即将N台提供相同功能的路由器组成一个路由器组(Router  Group),这个组里面有一个master和多个backup,但在外界看来就像一台一样,构成虚拟路由器,拥有一个虚拟IP(vip,也就是路由器所在局域网内其他机器的默认路由),占有这个IP的master实际负责ARP相应和转发IP数据包,组中的其它路由器作为备份的角色处于待命状态。master会发组播消息,当backup在超时时间内收不到vrrp包时就认为master宕掉了,这时就需要根据VRRP的优先级来选举一个backup当master,保证路由器的高可用。在VRRP协议实现里,虚拟路由器使用 00-00-5E-00-01-XX 作为虚拟MAC地址,XX就是唯一的 VRID (Virtual Router  IDentifier),这个地址同一时间只有一个物理路由器占用。在虚拟路由器里面的物理路由器组里面通过多播IP地址 224.0.0.18  来定时发送通告消息。每个Router都有一个 1-255 之间的优先级别,级别最高的(highest  priority)将成为主控(master)路由器。通过降低master的优先权可以让处于backup状态的路由器抢占(pro-empt)主路由器的状态,两个backup优先级相同的IP地址较大者为master,接管虚拟IP。keepalived与heartbeat/corosync等比较Heartbeat、Corosync、Keepalived这三个集群组件我们到底选哪个好呢?首先要说明的是,Heartbeat、Corosync是属于同一类型,Keepalived与Heartbeat、Corosync,根本不是同一类型的。Keepalived使用的vrrp协议方式,虚拟路由冗余协议 (Virtual Router Redundancy Protocol,简称VRRP);Heartbeat或Corosync是基于主机或网络服务的高可用方式;简单的说就是,Keepalived的目的是模拟路由器的高可用,Heartbeat或Corosync的目的是实现Service的高可用。所以一般Keepalived是实现前端高可用,常用的前端高可用的组合有,就是我们常见的LVS+Keepalived、Nginx+Keepalived、HAproxy+Keepalived。而Heartbeat或Corosync是实现服务的高可用,常见的组合有Heartbeat v3(Corosync)+Pacemaker+NFS+Httpd 实现Web服务器的高可用、Heartbeat  v3(Corosync)+Pacemaker+NFS+MySQL  实现MySQL服务器的高可用。总结一下,Keepalived中实现轻量级的高可用,一般用于前端高可用,且不需要共享存储,一般常用于两个节点的高可用。而Heartbeat(或Corosync)一般用于服务的高可用,且需要共享存储,一般用于多节点的高可用。这个问题我们说明白了。那heartbaet与corosync又应该选择哪个好?一般用corosync,因为corosync的运行机制更优于heartbeat,就连从heartbeat分离出来的pacemaker都说在以后的开发当中更倾向于corosync,所以现在corosync+pacemaker是最佳组合。双机高可用一般是通过虚拟IP(飘移IP)方法来实现的,基于Linux/Unix的IP别名技术。双机高可用方法目前分为两种:1)双机主从模式:即前端使用两台服务器,一台主服务器和一台热备服务器,正常情况下,主服务器绑定一个公网虚拟IP,提供负载均衡服务,热备服务器处于空闲状态;当主服务器发生故障时,热备服务器接管主服务器的公网虚拟IP,提供负载均衡服务;但是热备服务器在主机器不出现故障的时候,永远处于浪费状态,对于服务器不多的网站,该方案不经济实惠。2)双机主主模式:即前端使用两台负载均衡服务器,互为主备,且都处于活动状态,同时各自绑定一个公网虚拟IP,提供负载均衡服务;当其中一台发生故障时,另一台接管发生故障服务器的公网虚拟IP(这时由非故障机器一台负担所有的请求)。这种方案,经济实惠,非常适合于当前架构环境。今天在此分享下Nginx+keepalived实现高可用负载均衡的主从模式的操作记录:keepalived可以认为是VRRP协议在Linux上的实现,主要有三个模块,分别是core、check和vrrp。core模块为keepalived的核心,负责主进程的启动、维护以及全局配置文件的加载和解析。check负责健康检查,包括常见的各种检查方式。vrrp模块是来实现VRRP协议的。环境说明操作系统:centos6.8,64位master机器(master-node):103.110.98.14/192.168.1.14slave机器(slave-node):103.110.98.24/192.168.1.24公用的虚拟IP(VIP):103.110.98.20    //负载均衡器上配置的域名都解析到这个VIP上应用环境如下环境安装安装nginx和keepalive服务(master-node和slave-node两台服务器上的安装操作完全一样)。安装依赖[root@master-node ~]# yum -y install gcc pcre-devel zlib-devel openssl-devel 大家可以到链接:download.csdn.net/download/l1…[root@master-node ~]# cd /usr/local/src/ [root@master-node src]# wget http://nginx.org/download/nginx-1.9.7.tar.gz [root@master-node src]# wget http://www.keepalived.org/software/keepalived-1.3.2.tar.gz 安装nginx[root@master-node src]# tar -zvxf nginx-1.9.7.tar.gz [root@master-node src]# cd nginx-1.9.7 添加www用户,其中-M参数表示不添加用户家目录,-s参数表示指定shell类型[root@master-node nginx-1.9.7]# useradd www -M -s /sbin/nologin [root@master-node nginx-1.9.7]# vim auto/cc/gcc #将这句注释掉 取消Debug编译模式 大概在179行 #CFLAGS="$CFLAGS -g" [root@master-node nginx-1.9.7]# ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-http_flv_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre [root@master-node nginx-1.9.7]# make && make install 安装keepalived[root@master-node src]# tar -zvxf keepalived-1.3.2.tar.gz [root@master-node src]# cd keepalived-1.3.2 [root@master-node keepalived-1.3.2]# ./configure [root@master-node keepalived-1.3.2]# make && make install [root@master-node keepalived-1.3.2]# cp /usr/local/src/keepalived-1.3.2/keepalived/etc/init.d/keepalived /etc/rc.d/init.d/ [root@master-node keepalived-1.3.2]# cp /usr/local/etc/sysconfig/keepalived /etc/sysconfig/ [root@master-node keepalived-1.3.2]# mkdir /etc/keepalived [root@master-node keepalived-1.3.2]# cp /usr/local/etc/keepalived/keepalived.conf /etc/keepalived/ [root@master-node keepalived-1.3.2]# cp /usr/local/sbin/keepalived /usr/sbin/ 将nginx和keepalive服务加入开机启动服务[root@master-node keepalived-1.3.2]# echo "/usr/local/nginx/sbin/nginx" >> /etc/rc.local [root@master-node keepalived-1.3.2]# echo "/etc/init.d/keepalived start" >> /etc/rc.local 配置服务先关闭SElinux、配置防火墙 (master和slave两台负载均衡机都要做)[root@master-node ~]# vim /etc/sysconfig/selinux #SELINUX=enforcing #注释掉 #SELINUXTYPE=targeted #注释掉 SELINUX=disabled #增加 [root@master-node ~]# setenforce 0 #使配置立即生效 [root@master-node ~]# vim /etc/sysconfig/iptables ....... -A INPUT -s 103.110.98.0/24 -d 224.0.0.18 -j ACCEPT #允许组播地址通信 -A INPUT -s 192.168.1.0/24 -d 224.0.0.18 -j ACCEPT -A INPUT -s 103.110.98.0/24 -p vrrp -j ACCEPT #允许 VRRP(虚拟路由器冗余协)通信 -A INPUT -s 192.168.1.0/24 -p vrrp -j ACCEPT -A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT #开通80端口访问 [root@master-node ~]# /etc/init.d/iptables restart #重启防火墙使配置生效 配置nginxmaster-node和slave-node两台服务器的nginx的配置完全一样,主要是配置/usr/local/nginx/conf/nginx.conf的http,当然也可以配置vhost虚拟主机目录,然后配置vhost下的比如LB.conf文件。其中:多域名指向是通过虚拟主机(配置http下面的server)实现;同一域名的不同虚拟目录通过每个server下面的不同location实现;到后端的服务器在vhost/LB.conf下面配置upstream,然后在server或location中通过proxy_pass引用。要实现前面规划的接入方式,LB.conf的配置如下(添加proxy_cache_path和proxy_temp_path这两行,表示打开nginx的缓存功能)[root@master-node ~]# vim /usr/local/nginx/conf/nginx.conf user www; worker_processes 8; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 65535; } http { include mime.types; default_type application/octet-stream; charset utf-8; ###### ## set access log format ###### log_format main '$http_x_forwarded_for $remote_addr $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_cookie" $host $request_time'; ####### ## http setting ####### sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; proxy_cache_path /var/www/cache levels=1:2 keys_zone=mycache:20m max_size=2048m inactive=60m; proxy_temp_path /var/www/cache/tmp; fastcgi_connect_timeout 3000; fastcgi_send_timeout 3000; fastcgi_read_timeout 3000; fastcgi_buffer_size 256k; fastcgi_buffers 8 256k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; fastcgi_intercept_errors on; # client_header_timeout 600s; client_body_timeout 600s; # client_max_body_size 50m; client_max_body_size 100m; #允许客户端请求的最大单个文件字节数 client_body_buffer_size 256k; #缓冲区代理缓冲请求的最大字节数,可以理解为先保存到本地再传给用户 gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.1; gzip_comp_level 9; gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php; gzip_vary on; ## includes vhosts include vhosts/*.conf; } [root@master-node ~]# mkdir /usr/local/nginx/conf/vhosts [root@master-node ~]# mkdir /var/www/cache [root@master-node ~]# ulimit 65535 [root@master-node ~]# vim /usr/local/nginx/conf/vhosts/LB.conf upstream LB-WWW { ip_hash; server 192.168.1.101:80 max_fails=3 fail_timeout=30s; #max_fails = 3 为允许失败的次数,默认值为1 server 192.168.1.102:80 max_fails=3 fail_timeout=30s; #fail_timeout = 30s 当max_fails次失败后,暂停将请求分发到该后端服务器的时间 server 192.168.1.118:80 max_fails=3 fail_timeout=30s; } upstream LB-OA { ip_hash; server 192.168.1.101:8080 max_fails=3 fail_timeout=30s; server 192.168.1.102:8080 max_fails=3 fail_timeout=30s; } server { listen 80; server_name dev.wangshibo.com; access_log /usr/local/nginx/logs/dev-access.log main; error_log /usr/local/nginx/logs/dev-error.log; location /svn { proxy_pass http://192.168.1.108/svn/; proxy_redirect off ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 300; #跟后端服务器连接超时时间,发起握手等候响应时间 proxy_send_timeout 300; #后端服务器回传时间,就是在规定时间内后端服务器必须传完所有数据 proxy_read_timeout 600; #连接成功后等待后端服务器的响应时间,已经进入后端的排队之中等候处理 proxy_buffer_size 256k; #代理请求缓冲区,会保存用户的头信息以供nginx进行处理 proxy_buffers 4 256k; #同上,告诉nginx保存单个用几个buffer最大用多少空间 proxy_busy_buffers_size 256k; #如果系统很忙时候可以申请最大的proxy_buffers proxy_temp_file_write_size 256k; #proxy缓存临时文件的大小 proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; proxy_max_temp_file_size 128m; proxy_cache mycache; proxy_cache_valid 200 302 60m; proxy_cache_valid 404 1m; } location /submin { proxy_pass http://192.168.1.108/submin/; proxy_redirect off ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 300; proxy_send_timeout 300; proxy_read_timeout 600; proxy_buffer_size 256k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; proxy_temp_file_write_size 256k; proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; proxy_max_temp_file_size 128m; proxy_cache mycache; proxy_cache_valid 200 302 60m; proxy_cache_valid 404 1m; } } server { listen 80; server_name www.wangshibo.com; access_log /usr/local/nginx/logs/www-access.log main; error_log /usr/local/nginx/logs/www-error.log; location / { proxy_pass http://LB-WWW; proxy_redirect off ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 300; proxy_send_timeout 300; proxy_read_timeout 600; proxy_buffer_size 256k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; proxy_temp_file_write_size 256k; proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; proxy_max_temp_file_size 128m; proxy_cache mycache; proxy_cache_valid 200 302 60m; proxy_cache_valid 404 1m; } } server { listen 80; server_name oa.wangshibo.com; access_log /usr/local/nginx/logs/oa-access.log main; error_log /usr/local/nginx/logs/oa-error.log; location / { proxy_pass http://LB-OA; proxy_redirect off ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 300; proxy_send_timeout 300; proxy_read_timeout 600; proxy_buffer_size 256k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; proxy_temp_file_write_size 256k; proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; proxy_max_temp_file_size 128m; proxy_cache mycache; proxy_cache_valid 200 302 60m; proxy_cache_valid 404 1m; } } 验证Nginx配置验证方法(保证从负载均衡器本机到后端真实服务器之间能正常通信):1)首先在本机用IP访问上面LB.cong中配置的各个后端真实服务器的url2)然后在本机用域名和路径访问上面LB.cong中配置的各个后端真实服务器的域名/虚拟路径后端应用服务器的nginx配置,这里选择192.168.1.108作为例子进行说明。由于这里的192.168.1.108机器是openstack的虚拟机,没有外网ip,不能解析域名。所以在server_name处也将ip加上,使得用ip也可以访问。[root@108-server ~]# cat /usr/local/nginx/conf/vhosts/svn.conf server { listen 80; #server_name dev.wangshibo.com; server_name dev.wangshibo.com 192.168.1.108; access_log /usr/local/nginx/logs/dev.wangshibo-access.log main; error_log /usr/local/nginx/logs/dev.wangshibo-error.log; location / { root /var/www/html; index index.html index.php index.htm; } } [root@108-server ~]# ll /var/www/html/ drwxr-xr-x. 2 www www 4096 Dec 7 01:46 submin drwxr-xr-x. 2 www www 4096 Dec 7 01:45 svn [root@108-server ~]# cat /var/www/html/svn/index.html this is the page of svn/192.168.1.108 [root@108-server ~]# cat /var/www/html/submin/index.html this is the page of submin/192.168.1.108 [root@108-server ~]# cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 192.168.1.108 dev.wangshibo.com [root@108-server ~]# curl http://dev.wangshibo.com //由于是内网机器不能联网,亦不能解析域名。所以用域名访问没有反应。只能用ip访问 [root@ops-server4 vhosts]# curl http://192.168.1.108 this is 192.168.1.108 page!!! [root@ops-server4 vhosts]# curl http://192.168.1.108/svn/ //最后一个/符号要加上,否则访问不了。 this is the page of svn/192.168.1.108 [root@ops-server4 vhosts]# curl http://192.168.1.108/submin/ this is the page of submin/192.168.1.108 然后在master-node和slave-node两台负载机器上进行测试(iptables防火墙要开通80端口):[root@master-node ~]# curl http://192.168.1.108/svn/ this is the page of svn/192.168.1.108 [root@master-node ~]# curl http://192.168.1.108/submin/ this is the page of submin/192.168.1.108 浏览器访问:在本机host绑定dev.wangshibo.com,如下,即绑定到master和slave机器的公网ip上测试是否能正常访问(nginx+keepalive环境正式完成后,域名解析到的真正地址是VIP地址)103.110.98.14 dev.wangshibo.com103.110.98.24 dev.wangshibo.comkeepalived配置1)master-node负载机上的keepalived配置[root@master-node ~]# cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak [root@master-node ~]# vim /etc/keepalived/keepalived.conf ! Configuration File for keepalived #全局定义 global_defs { notification_email { #指定keepalived在发生事件时(比如切换)发送通知邮件的邮箱 ops@wangshibo.cn #设置报警邮件地址,可以设置多个,每行一个。 需开启本机的sendmail服务 tech@wangshibo.cn } notification_email_from ops@wangshibo.cn #keepalived在发生诸如切换操作时需要发送email通知地址 smtp_server 127.0.0.1 #指定发送email的smtp服务器 smtp_connect_timeout 30 #设置连接smtp server的超时时间 router_id master-node #运行keepalived的机器的一个标识,通常可设为hostname。故障发生时,发邮件时显示在邮件主题中的信息。 } vrrp_script chk_http_port { #检测nginx服务是否在运行。有很多方式,比如进程,用脚本检测等等 script "/opt/chk_nginx.sh" #这里通过脚本监测 interval 2 #脚本执行间隔,每2s检测一次 weight -5 #脚本结果导致的优先级变更,检测失败(脚本返回非0)则优先级 -5 fall 2 #检测连续2次失败才算确定是真失败。会用weight减少优先级(1-255之间) rise 1 #检测1次成功就算成功。但不修改优先级 } vrrp_instance VI_1 { #keepalived在同一virtual_router_id中priority(0-255)最大的会成为master,也就是接管VIP,当priority最大的主机发生故障后次priority将会接管 state MASTER #指定keepalived的角色,MASTER表示此主机是主服务器,BACKUP表示此主机是备用服务器。注意这里的state指定instance(Initial)的初始状态,就是说在配置好后,这台服务器的初始状态就是这里指定的,但这里指定的不算,还是得要通过竞选通过优先级来确定。如果这里设置为MASTER,但如若他的优先级不及另外一台,那么这台在发送通告时,会发送自己的优先级,另外一台发现优先级不如自己的高,那么他会就回抢占为MASTER interface em1 #指定HA监测网络的接口。实例绑定的网卡,因为在配置虚拟IP的时候必须是在已有的网卡上添加的 mcast_src_ip 103.110.98.14 # 发送多播数据包时的源IP地址,这里注意了,这里实际上就是在哪个地址上发送VRRP通告,这个非常重要,一定要选择稳定的网卡端口来发送,这里相当于heartbeat的心跳端口,如果没有设置那么就用默认的绑定的网卡的IP,也就是interface指定的IP地址 virtual_router_id 51 #虚拟路由标识,这个标识是一个数字,同一个vrrp实例使用唯一的标识。即同一vrrp_instance下,MASTER和BACKUP必须是一致的 priority 101 #定义优先级,数字越大,优先级越高,在同一个vrrp_instance下,MASTER的优先级必须大于BACKUP的优先级 advert_int 1 #设定MASTER与BACKUP负载均衡器之间同步检查的时间间隔,单位是秒 authentication { #设置验证类型和密码。主从必须一样 auth_type PASS #设置vrrp验证类型,主要有PASS和AH两种 auth_pass 1111 #设置vrrp验证密码,在同一个vrrp_instance下,MASTER与BACKUP必须使用相同的密码才能正常通信 } virtual_ipaddress { #VRRP HA 虚拟地址 如果有多个VIP,继续换行填写 103.110.98.20 } track_script { #执行监控的服务。注意这个设置不能紧挨着写在vrrp_script配置块的后面(实验中碰过的坑),否则nginx监控失效!! chk_http_port #引用VRRP脚本,即在 vrrp_script 部分指定的名字。定期运行它们来改变优先级,并最终引发主备切换。 } } slave-node负载机上的keepalived配置[root@slave-node ~]# cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak [root@slave-node ~]# vim /etc/keepalived/keepalived.conf ! Configuration File for keepalived global_defs { notification_email { ops@wangshibo.cn tech@wangshibo.cn } notification_email_from ops@wangshibo.cn smtp_server 127.0.0.1 smtp_connect_timeout 30 router_id slave-node } vrrp_script chk_http_port { script "/opt/chk_nginx.sh" interval 2 weight -5 fall 2 rise 1 } vrrp_instance VI_1 { state BACKUP interface em1 mcast_src_ip 103.110.98.24 virtual_router_id 51 priority 99 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 103.110.98.20 } track_script { chk_http_port } } 让keepalived监控NginX的状态:1)经过前面的配置,如果master主服务器的keepalived停止服务,slave从服务器会自动接管VIP对外服务;一旦主服务器的keepalived恢复,会重新接管VIP。 但这并不是我们需要的,我们需要的是当NginX停止服务的时候能够自动切换。2)keepalived支持配置监控脚本,我们可以通过脚本监控NginX的状态,如果状态不正常则进行一系列的操作,最终仍不能恢复NginX则杀掉keepalived,使得从服务器能够接管服务。如何监控NginX的状态最简单的做法是监控NginX进程,更靠谱的做法是检查NginX端口,最靠谱的做法是检查多个url能否获取到页面。注意:这里要提示一下keepalived.conf中vrrp_script配置区的script一般有2种写法:1)通过脚本执行的返回结果,改变优先级,keepalived继续发送通告消息,backup比较优先级再决定。这是直接监控Nginx进程的方式。2)脚本里面检测到异常,直接关闭keepalived进程,backup机器接收不到advertisement会抢占IP。这是检查NginX端口的方式。上文script配置部分,"killall -0 nginx"属于第1种情况,"/opt/chk_nginx.sh" 属于第2种情况。个人更倾向于通过shell脚本判断,但有异常时exit 1,正常退出exit 0,然后keepalived根据动态调整的 vrrp_instance 优先级选举决定是否抢占VIP:如果脚本执行结果为0,并且weight配置的值大于0,则优先级相应的增加如果脚本执行结果非0,并且weight配置的值小于0,则优先级相应的减少其他情况,原本配置的优先级不变,即配置文件中priority对应的值。提示:优先级不会不断的提高或者降低可以编写多个检测脚本并为每个检测脚本设置不同的weight(在配置中列出就行)不管提高优先级还是降低优先级,最终优先级的范围是在[1,254],不会出现优先级小于等于0或者优先级大于等于255的情况在MASTER节点的 vrrp_instance 中 配置 nopreempt ,当它异常恢复后,即使它 prio 更高也不会抢占,这样可以避免正常情况下做无谓的切换以上可以做到利用脚本检测业务进程的状态,并动态调整优先级从而实现主备切换。另外:在默认的keepalive.conf里面还有 virtual_server,real_server 这样的配置,我们这用不到,它是为lvs准备的。如何尝试恢复服务由于keepalived只检测本机和他机keepalived是否正常并实现VIP的漂移,而如果本机nginx出现故障不会则不会漂移VIP。所以编写脚本来判断本机nginx是否正常,如果发现NginX不正常,重启之。等待3秒再次校验,仍然失败则不再尝试,关闭keepalived,其他主机此时会接管VIP;根据上述策略很容易写出监控脚本。此脚本必须在keepalived服务运行的前提下才有效!如果在keepalived服务先关闭的情况下,那么nginx服务关闭后就不能实现自启动了。该脚本检测ngnix的运行状态,并在nginx进程不存在时尝试重新启动ngnix,如果启动失败则停止keepalived,准备让其它机器接管。监控脚本如下(master和slave都要有这个监控脚本):[root@master-node ~]# vim /opt/chk_nginx.sh #!/bin/bash counter=$(ps -C nginx --no-heading|wc -l) if [ "${counter}" = "0" ]; then /usr/local/nginx/sbin/nginx sleep 2 counter=$(ps -C nginx --no-heading|wc -l) if [ "${counter}" = "0" ]; then /etc/init.d/keepalived stop fi fi [root@master-node ~]# chmod 755 /opt/chk_nginx.sh [root@master-node ~]# sh /opt/chk_nginx.sh 80/tcp open http 此架构需考虑的问题1)master没挂,则master占有vip且nginx运行在master上2)master挂了,则slave抢占vip且在slave上运行nginx服务3)如果master上的nginx服务挂了,则nginx会自动重启,重启失败后会自动关闭keepalived,这样vip资源也会转移到slave上。4)检测后端服务器的健康状态5)master和slave两边都开启nginx服务,无论master还是slave,当其中的一个keepalived服务停止后,vip都会漂移到keepalived服务还在的节点上;如果要想使nginx服务挂了,vip也漂移到另一个节点,则必须用脚本或者在配置文件里面用shell命令来控制。(nginx服务宕停后会自动启动,启动失败后会强制关闭keepalived,从而致使vip资源漂移到另一台机器上)最后验证(将配置的后端应用域名都解析到VIP地址上):关闭主服务器上的keepalived或nginx,vip都会自动飘到从服务器上。验证keepalived服务故障情况:1)先后在master、slave服务器上启动nginx和keepalived,保证这两个服务都正常开启:[root@master-node ~]# /usr/local/nginx/sbin/nginx [root@master-node ~]# /etc/init.d/keepalived start [root@slave-node ~]# /usr/local/nginx/sbin/nginx [root@slave-node ~]# /etc/init.d/keepalived start 2)在主服务器上查看是否已经绑定了虚拟IP[root@master-node ~]# ip addr ....... 2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 link/ether 44:a8:42:17:3d:dd brd ff:ff:ff:ff:ff:ff inet 103.110.98.14/26 brd 103.10.86.63 scope global em1 valid_lft forever preferred_lft forever inet 103.110.98.20/32 scope global em1 valid_lft forever preferred_lft forever inet 103.110.98.20/26 brd 103.10.86.63 scope global secondary em1:0 valid_lft forever preferred_lft forever inet6 fe80::46a8:42ff:fe17:3ddd/64 scope link valid_lft forever preferred_lft forever 3)停止主服务器上的keepalived:[root@master-node ~]# /etc/init.d/keepalived stop Stopping keepalived (via systemctl): [ OK ] [root@master-node ~]# /etc/init.d/keepalived status [root@master-node ~]# ps -ef|grep keepalived root 26952 24348 0 17:49 pts/0 00:00:00 grep --color=auto keepalived [root@master-node ~]# 4)然后在从服务器上查看,发现已经接管了VIP:[root@slave-node ~]# ip addr ....... 2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 link/ether 44:a8:42:17:3c:a5 brd ff:ff:ff:ff:ff:ff inet 103.110.98.24/26 brd 103.10.86.63 scope global em1 inet 103.110.98.20/32 scope global em1 inet6 fe80::46a8:42ff:fe17:3ca5/64 scope link valid_lft forever preferred_lft forever ....... 发现master的keepalived服务挂了后,vip资源自动漂移到slave上,并且网站正常访问,丝毫没有受到影响!5)重新启动主服务器上的keepalived,发现主服务器又重新接管了VIP,此时slave机器上的VIP已经不在了[root@master-node ~]# /etc/init.d/keepalived start Starting keepalived (via systemctl): [ OK ] [root@master-node ~]# ip addr ....... 2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 link/ether 44:a8:42:17:3d:dd brd ff:ff:ff:ff:ff:ff inet 103.110.98.14/26 brd 103.10.86.63 scope global em1 valid_lft forever preferred_lft forever inet 103.110.98.20/32 scope global em1 valid_lft forever preferred_lft forever inet 103.110.98.20/26 brd 103.10.86.63 scope global secondary em1:0 valid_lft forever preferred_lft forever inet6 fe80::46a8:42ff:fe17:3ddd/64 scope link valid_lft forever preferred_lft forever ...... [root@slave-node ~]# ip addr ....... 2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 link/ether 44:a8:42:17:3c:a5 brd ff:ff:ff:ff:ff:ff inet 103.110.98.24/26 brd 103.10.86.63 scope global em1 inet6 fe80::46a8:42ff:fe17:3ca5/64 scope link valid_lft forever preferred_lft forever 接着验证下nginx服务故障,看看keepalived监控nginx状态的脚本是否正常?如下:手动关闭master机器上的nginx服务,最多2秒钟后就会自动起来(因为keepalive监控nginx状态的脚本执行间隔时间为2秒)。域名访问几乎不受影响![root@master-node ~]# /usr/local/nginx/sbin/nginx -s stop [root@master-node ~]# ps -ef|grep nginx root 28401 24826 0 19:43 pts/1 00:00:00 grep --color=auto nginx [root@master-node ~]# ps -ef|grep nginx root 28871 28870 0 19:47 ? 00:00:00 /bin/sh /opt/chk_nginx.sh root 28875 24826 0 19:47 pts/1 00:00:00 grep --color=auto nginx [root@master-node ~]# ps -ef|grep nginx root 28408 1 0 19:43 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx www 28410 28408 0 19:43 ? 00:00:00 nginx: worker process www 28411 28408 0 19:43 ? 00:00:00 nginx: worker process www 28412 28408 0 19:43 ? 00:00:00 nginx: worker process www 28413 28408 0 19:43 ? 00:00:00 nginx: worker process 最后可以查看两台服务器上的/var/log/messages,观察VRRP日志信息的vip漂移情况~~~~可能出现的问题1)VIP绑定失败原因可能有:-> iptables开启后,没有开放允许VRRP协议通信的策略(也有可能导致脑裂);可以选择关闭iptables-> keepalived.conf文件配置有误导致,比如interface绑定的设备错误2)VIP绑定后,外部ping不通可能的原因是:-> 网络故障,可以检查下网关是否正常;-> 网关的arp缓存导致,可以进行arp更新,命令是"arping -I 网卡名 -c 5 -s VIP 网关"
0
0
0
浏览量2036
后端求offer版

Nginx全方位应用与性能优化

掌握Nginx配置实现负载均衡、限流、缓存、黑白名单、灰度发布等关键功能,同时深入学习Http、Https、wS、WSS的配置。了解主从模式下Nginx+Keepalived双机热备的搭建,掌握自签CA配置HTTPS加密反向代理。解决并发高峰、跨域问题,以及搭建流媒体服务器实现直播。完整而深入的Nginx应用与性能优化教程,助你成为Nginx高级应用专家。
0
0
0
浏览量2528
后端求offer版

Spring注解全面解析

春天来了,Spring注解解析也来了,Spring注解开发模式大大减少了繁琐的XML文件配置,提高了开发效率,同时也为Spring Boot的盛行打下了很好的基础,所以其重要性不言而喻。
0
0
0
浏览量2288
后端求offer版

面试官,别再问我HashMap底层实现原理了

引言大家思考一下,为什么HashMap是Java中最常用、最重要的数据结构? 其中一个原因就是HashMap的性能非常好,比如常见的基础数据结构类型有数组和链表,数组的查询效率非常高,通过数组下标实现常数级的查询性能,但是插入和删除的时候,涉及数组拷贝,性能较差。而链表的插入和删除性能更好,不需要扩容与元素拷贝,但是查询性能较差,需要遍历整个链表。 HashMap就是综合了数组和链表优点,查询与插入的效率都控制在常数级的复杂度内。 学完本篇文章,你将会学到以下内容:HashMap的底层实现原理HashMap的put方法执行流程HashMap的扩容流程HashMap为什么是线程不安全的?HashMap的容量为什么设置成2的倍数?并且是2倍扩容?HashMap在Java8版本中做了哪些变更?简介HashMap的底层数据结构由数组、链表和红黑树组成,核心是基于数组实现的,为了解决哈希冲突,采用拉链法,于是引入了链表结构。为了解决链表过长,造成的查询性能下降,又引入了红黑树结构。 类属性再看一下HashMap类中有哪些属性?public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable { /** * 默认容量大小,16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 负载系数,容量超过负载系数的时候会触发扩容,16*0.0.75=12个 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 容量最大值,2的30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 数组的默认值,空数组 */ static final Entry<?, ?>[] EMPTY_TABLE = {}; transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE; /** * 链表的节点 */ static class Entry<K, V> implements Map.Entry<K, V> { final K key; V value; Entry<K, V> next; int hash; } /** * 红黑树的节点 */ static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> { TreeNode<K, V> parent; TreeNode<K, V> left; TreeNode<K, V> right; TreeNode<K, V> prev; boolean red; TreeNode(int hash, K key, V val, Node<K, V> next) { super(hash, key, val, next); } } }初始化HashMap常见的初始化方法有两个:无参初始化有参初始化,指定容量大小。/** * 无参初始化 */ Map<Integer, Integer> map = new HashMap<>(); /** * 有参初始化,指定容量大小 */ Map<Integer, Integer> map = new HashMap<>(10);再看一下构造方法的底层实现:/** * 无参初始化 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } /** * 有参初始化,指定容量大小 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 有参初始化,指定容量大小和负载系数 */ public HashMap(int initialCapacity, float loadFactor) { // 校验参数 if (initialCapacity < 0) { throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); } if (initialCapacity > MAXIMUM_CAPACITY) { initialCapacity = MAXIMUM_CAPACITY; } if (loadFactor <= 0 || Float.isNaN(loadFactor)) { throw new IllegalArgumentException("Illegal load factor: " + loadFactor); } this.loadFactor = loadFactor; // 计算出合适的容量大小(2的倍数) this.threshold = tableSizeFor(initialCapacity); }可以看出,无参构造方法,只初始化了负载系数的大小。指定容量大小的有参构造方法也只是初始化了负载系数和容量大小,两个方法都没有初始化数组大小。 如果再有面试官问你,HashMap初始化的时候数组大小是多少?答案是0,因为HashMap初始化的时候,并没有初始化数组。put源码put方法的流程如下: 再看一下put方法的具体源码实现:/** * put 方法入口 */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 计算 hash 值(高位和低位都参与计算) */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * 实际的put方法逻辑 * @param hash key对应的hash值 * @param key 键 * @param value 值 * @param onlyIfAbsent 如果为true,则只有当key不存在时才会put,否则会put到链表的末尾 * @param evict 如果为false,表处于创建模式。 * @return 返回旧值 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K, V>[] tab; Node<K, V> p; int n, i; // 1. 如果数组为空,则执行初始化(与扩容是同一个方法) if ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length; } // 2. 如果key对应下标位置元素不存在,直接插入即可 if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null); } else { Node<K, V> e; K k; // 3. 如果key对应下标位置元素存在,直接结束,后面判断是否需要覆盖当前元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { e = p; } else if (p instanceof TreeNode) { // 4. 判断下标位置的元素类型,如果是红黑树,则执行红黑树的插入逻辑 e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); } else { // 5. 否则执行链表的插入逻辑 for (int binCount = 0; ; ++binCount) { // 6. 遍历链表,直到找到空位置为止 if ((e = p.next) == null) { // 7. 创建一个新的链表节点,并追加到末尾 p.next = newNode(hash, key, value, null); // 8. 如果链表长度达到8个,则转换为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) { treeifyBin(tab, hash); } break; } // 9. 如果在链表中找到值相同的key,则结束 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { break; } p = e; } } // 10. 判断是否需要覆盖旧值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) { e.value = value; } afterNodeAccess(e); return oldValue; } } ++modCount; // 11. 判断是否需要扩容 if (++size > threshold) { resize(); } afterNodeInsertion(evict); return null; }扩容再看一下扩容逻辑的具体实现:/** * 扩容 */ final HashMap.Node<K, V>[] resize() { HashMap.Node<K, V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 计算扩容后容量大小 // 1. 如果原来容量大于0,说明不是第一次扩容,直接扩容为原来的2倍 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { newThr = oldThr << 1; } } else if (oldThr > 0) { // 2. 把原来的阈值当成新的容量大小 newCap = oldThr; } else { // 3. 如果是第一次初始化,则容量和阈值都是用默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 4. 如果新的阈值不合适,则重新计算扩容后阈值 if (newThr == 0) { float ft = (float) newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE); } threshold = newThr; // 5. 创建一个新数组,容量使用上面计算的大小 HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap]; table = newTab; // 6. 遍历原来的数组,将元素插入到新数组 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { HashMap.Node<K, V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 7. 如果下标位置只有一个元素,则直接插入新数组即可 if (e.next == null) { newTab[e.hash & (newCap - 1)] = e; } else if (e instanceof HashMap.TreeNode) { // 8. 如果下标位置元素类型是红黑树,则执行红黑树的插入逻辑 ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap); } else { // 9. 否则执行链表的插入逻辑,使用 do-while 循环 // loHead、loTail表示低位链表的头尾节点,hiHead、hiTail表示高位链表的头尾节点 HashMap.Node<K, V> loHead = null, loTail = null; HashMap.Node<K, V> hiHead = null, hiTail = null; HashMap.Node<K, V> next; do { next = e.next; // 10. 判断当前元素高位哈希值是否为0,如果是则插入到低位链表,否则插入到高位链表 if ((e.hash & oldCap) == 0) { if (loTail == null) { loHead = e; } else { loTail.next = e; } loTail = e; } else { if (hiTail == null) { hiHead = e; } else { hiTail.next = e; } hiTail = e; } } while ((e = next) != null); // 11. 将低位链表插入到新数组中 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 12. 将高位链表插入到新数组中 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }重点关注一下扩容的第9步,链表元素转移的逻辑,使用了一个非常巧妙的方法。并不是遍历原数组每个元素,然后插入新数组,而是把原数组的链表拆成两个链表,整体插入新数组中。 假设原来数组容量是16,当前下标是1,原数组链表元素分别是 1 -> 17 -> 33 -> 49,这些元素特点是对16求余等于1。 新数组容量则32,这个链表上的元素转移到新数组中,位置会变成什么样?只会拆成两个链表。 下标是1的链表,1 -> 33 下标是17的链表,17 -> 49 1、33对16进行逻辑与操作,结果是0。17、49对16进行逻辑与操作,结果是16。get源码再看一下get方法源码实现/** * get方法入口 */ public V get(Object key) { // 调用查询节点方法 Node<K, V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * 查询节点方法 */ final Node<K, V> getNode(int hash, Object key) { Node<K, V>[] tab; Node<K, V> first, e; int n; K k; // 1. 获取下标位置节点元素,命名为first if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 2. 比较first节点哈希值与key值 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) { return first; } if ((e = first.next) != null) { // 3. 如果first节点类型是否是红黑树,就执行红黑树的查找逻辑 if (first instanceof TreeNode) { return ((TreeNode<K, V>) first).getTreeNode(hash, key); } // 4. 否则,就执行链表的查找逻辑 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { return e; } } while ((e = e.next) != null); } } // 5. 都没找到就返回null return null; }remove源码再看一下remove方法源码/** * 删除方法入口 */ public V remove(Object key) { // 调用删除节点方法 Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } /** * 删除节点方法 */ final Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K, V>[] tab; Node<K, V> p; int n, index; // 1. 判断数组是否为空,下标位置节点是否为空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K, V> node = null, e; K k; V v; // 2. 判断下标节点key是否与传入的key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { node = p; } else if ((e = p.next) != null) { // 3. 如果节点类型是红黑树,就执行红黑树的查找逻辑 if (p instanceof TreeNode) { node = ((TreeNode<K, V>) p).getTreeNode(hash, key); } else { // 4. 如果节点类型是链表,就执行链表的查找逻辑 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } // 5. 当找到节点时,执行删除节点的逻辑 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) { ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable); } else if (node == p) { tab[index] = node.next; } else { p.next = node.next; } ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }总结现在学完了HashMap底层源码实现,可以轻松回答开头的问题了。HashMap的底层实现原理答案:HashMap的底层数据结构由数组、链表和红黑树组成,核心是基于数组实现的,为了解决哈希冲突,采用拉链法,于是引入了链表结构。为了解决链表过长,造成的查询性能下降,又引入了红黑树结构。HashMap的put方法执行流程答案:上面的流程图和源码都讲的很详细。核心就是判断下标节点类型是红黑树还是链表,然后执行对应的插入逻辑。HashMap的扩容流程答案:上面的源码已经讲过,同样需要两套扩容流程,分别是红黑树的扩容转移流程和链表的扩容转移流程。HashMap为什么是线程不安全的?答案:因为插入、删除等方法没有加同步锁,在多线程并发操作时会导致数据不一致。 想要实现线程安全,有三种方案: 第一种使用Hashtable,因为Hashtable的每个方法都使用了synchronized加锁,彻底保证了线程安全,但是线程较差。Map<Integer, Integer> map = new Hashtable<>(); 第二种方案创建HashMap的时候使用Collections.synchronizedMap()包装起来,原理跟Hashtable类型,也是把HashMap的每个方法使用synchronized加锁。Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>()); 第三种方案使用ConcurrentHashMap,ConcurrentHashMap使用分段锁,性能更好,也是下篇文章要讲的内容。Map<Integer, Integer> map = new ConcurrentHashMap<>();HashMap的容量为什么设置成2的倍数?并且是2倍扩容?答案:有三个原因,加快哈希运算效率,如果容量不是2的倍数,计算下标的时候,只能通过对容量求余的方式,n % hash,n 是容量大小,hash 是key对应的哈希值。如果容量是2的倍数,就可以通过逻辑与运算计算下标,(n -1) & hash,效率更快。散列更均匀,2是最小的正整数,也是唯一的偶数质数,可以使键值分布更均匀,减少哈希冲突。扩容效率更高,上面详细讲解了链表的扩容流程,只需要把原链表分成两段,整体复制,而不需要单独计算每个key下标位置。HashMap在Java8版本中做了哪些变更?答案:Java8版本对HashMap做了较大的重构,主要变更有以下这些:引入了红黑树结构,为了解决链表过长,查询效率低下的问题。优化了链表的扩容机制,原来需要重新计算每个节点下标,现在只需要把原链表分成两段,整体复制。扩容时机变化,原来添加元素前扩容,现在添加元素后扩容。插入链表的方式变化,Java8之前采用头插法,Java8开始采用尾插法。
0
0
0
浏览量2189
后端求offer版

超简单的RabbitMQ入门,看完这篇文章就够了

本文将带大家一块学习 RabbitMQ 的核心组件的奥秘,以便读者能够更全面地理解其内部工作机制。我们将从生产者(Producer)和消费者(Consumer)讲起,然后逐一介绍交换器(Exchange)、队列(Queue)、绑定(Binding)、通道(Channel)和代理(Broker)等核心组件。生产者(Producer)定义:生产者是消息生成的源头,负责创建消息并将其发送到 RabbitMQ。作用:生产者的主要任务是生成业务数据,并将这些数据封装成消息,然后通过 RabbitMQ 发送到相应的队列或交换器。消费者(Consumer)定义:消费者是消息的终点,负责从队列中取出并处理消息。作用:消费者订阅一个或多个队列,当队列中有新消息时,消费者会自动或手动地从队列中取出并处理这些消息。队列(Queue)定义:队列是用于存储消息的缓冲区,底层使用先进先出(FIFO)的数据结构。作用:队列接收生产者发送过来的消息,并保存它们直到消费者来取走。生产者、消费者和队列是 RabbitMQ 最核心的三大组件,也是每个消息队列必备的组件。它们之间消息流转的流程如上图所示:生产者(Producer)把消息发送到队列中队列(Queue)负责存储消息消息者(Consumer)消费队列中的消息除了这三大组件外,每个消息队列又有各自的创新,而下面的组件都是 RabbitMQ 特有的。交换器(Exchange)定义:交换器是 RabbitMQ 的核心组件之一,负责接收生产者发送的消息并将它们路由到一个或多个队列。作用:根据预设的路由规则,交换器决定如何将接收到的消息分发到不同的队列。如果 RabbitMQ 中存在多个队列,就需要引入交换器,生产者把消息发送到交换器,发送的时候需要指定路由键(Routing key),路由键用来决定消息应该发送到哪些队列。交换器具体把消息路由到哪个队列,又跟绑定键有(Binding key)关,绑定键用来定义交换器与队列之间的绑定规则。 RabbitMQ 共有四种类型的交换器,分别是 Direct(直接匹配模式)、Fanout(广播模式)、Topic(基于通配符匹配模式)、Headers(基于消息头属性匹配模式)。在接下来的章节会详细讲一下这四种模式的区别以及使用场景。 虚拟主机(Virtual Host)在RabbitMQ中,Virtual Host(虚拟主机)是对消息队列的逻辑隔离,用于将不同的应用、不同的业务场景或不同的团队之间的消息队列进行分离管理。每个 Virtual Host 都是一个独立的消息队列环境,拥有自己的交换机、队列、绑定和权限等。 Virtual Host 的作用包括以下几个方面:逻辑隔离:通过使用 Virtual Host,可以实现对消息队列的逻辑隔离,将不同的应用或业务场景的消息队列分开管理,避免消息的混乱和冲突。权限控制:每个 Virtual Host 都有自己的权限控制机制,可以通过设置用户和权限来限制对特定 Virtual Host 的访问。这样可以保证不同团队或应用之间的数据安全和访问权限。环境隔离:不同的 Virtual Host 可以在同一个 RabbitMQ 服务器上运行,但它们之间是相互独立的。这样可以在同一个服务器上为多个环境提供消息队列服务,如开发环境、测试环境和生产环境等。维护和管理:每个 Virtual Host 都有自己的独立配置和管理,可以独立创建、删除、备份和恢复。这样可以更方便地进行维护和管理,减少对整个 RabbitMQ 服务器的影响。服务器节点(Broker)在 RabbitMQ 中,Broker(代理服务器)是一个中间件,用于在消息的发布者和接收者之间进行消息的路由和传递。它接收发布者发送的消息,并将其路由到相应的接收者。 Broker 在 RabbitMQ 中扮演着核心角色,它负责以下几个主要任务:接收和存储消息:Broker 接收来自消息发布者的消息,并将其存储在队列中等待被消费。它负责管理消息队列中的消息,以确保消息在被消费之前的安全存储。路由和转发消息:Broker 根据发布者指定的路由键(Routing Key)和交换机(Exchange)的规则,将消息路由到对应的队列。它负责根据消息的路由键和交换机类型进行消息的转发,以实现消息的可靠传递。队列管理:Broker 负责创建、声明和删除队列,并管理队列的属性和配置。它提供了一系列的API和管理工具,使得队列的管理变得简单而灵活。消息确认:Broker 支持消息的确认机制,即在消息被消费者处理完毕后,向发布者发送一个确认消息。这样可以确保消息被成功处理,避免数据丢失和重复消费的问题。高可用性和负载均衡:Broker 支持集群部署,可以通过多个 Broker 节点来实现高可用性和负载均衡。集群中的 Broker 节点可以相互通信和同步消息,提高系统的可靠性和性能。连接(Connection)在 RabbitMQ 中,Connection(连接)是客户端与 RabbitMQ 服务器之间的 TCP 连接。它是建立和维护客户端与 RabbitMQ 之间的通信通道,用于发送和接收消息。 由于在操作系统中,建立和销毁TCP连接是一项开销较大的操作,因此引入 Channel(通道)来减少这种开销。通道(Channel)在 RabbitMQ 中,Channel(通道)是建立在连接(Connection)之上的逻辑连接,它是进行消息传输的主要工具。Channel 可以看作是一个轻量级的 TCP 连接,用于在客户端和 RabbitMQ 服务器之间进行通信。 Channel的主要作用有以下几个方面:信道复用:通过使用Channel,可以在一条物理连接上创建多个独立的逻辑通道。这样可以避免在多个线程之间共享一个连接的复杂性,提高系统的并发性能。消息传输:Channel负责发送和接收消息。通过创建一个Channel,客户端可以向RabbitMQ服务器发送消息,或者从队列中接收消息。Channel提供了一系列的API方法,用于发送和接收消息,并处理消息的确认、拒绝等操作。队列操作:通过Channel,可以创建、声明、删除队列,绑定和解绑队列与Exchange之间的关系,设置队列的属性等。Channel提供了一系列的方法,用于管理队列,以及与Exchange和绑定相关的操作。事务支持:Channel可以支持事务操作。通过将Channel设置为事务模式,可以将一系列的操作封装在一个事务中执行。如果事务执行成功,所有的操作都会被提交,否则会进行回滚。总结在这篇文章中,我们深入探讨了 RabbitMQ 的核心组件,这些组件构成了 RabbitMQ 消息代理的基础。通过对这些组件的深入了解,我们可以更好地理解 RabbitMQ 消息传递系统的工作原理以及如何使用它来构建可靠的消息传递解决方案。在后面的文章中我们将详细介绍这些组件的用法以及核心原理。 附上下面这张图是 RabbitMQ 的核心组件的交互流程:
0
0
0
浏览量2148
后端求offer版

面试官:Java线程池是怎么统计线程的空闲时间的?

背景介绍:你刚从学校毕业后,到新公司实习,试用期又被毕业,然后你又不得不出来面试,好在面试的时候碰到个美女面试官!面试官: 小伙子,我看你简历上写的项目中用到了线程池,你知道线程池是怎样实现复用线程的?这面试官是不是想坑我?是不是摆明了不让我通过?难道你不应该问线程池有哪些核心参数?每个参数具体作用是什么?往线程池中不断提交任务,线程池的处理流程是什么?这些才是你应该问的,这些八股文我已经背熟了,你不问,瞎问什么复用线程?幸亏我看了一灯的八股文,听我给你背一遍!我: 线程池复用线程的逻辑很简单,就是在线程启动后,通过while死循环,不断从阻塞队列中拉取任务,从而达到了复用线程的目的。具体源码如下:// 线程执行入口 public void run() { runWorker(this); } // 线程运行核心方法 final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); boolean completedAbruptly = true; try { // 1. 使用while死循环,不断从阻塞队列中拉取任务 while (task != null || (task = getTask()) != null) { // 加锁,保证thread不被其他线程中断(除非线程池被中断) w.lock(); // 2. 校验线程池状态,是否需要中断当前线程 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { // 3. 执行run方法 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }runWorker方法逻辑很简单,就是不断从阻塞队列中拉取任务并执行。面试官: 小伙子,有点东西。我们都知道线程池会回收超过空闲时间的线程,那么线程池是怎么统计线程的空闲时间的?美女面试官的问题真刁钻,让人头疼啊!这问的也太深了吧!没看过源码的话,真不好回答。我: 嗯...,可能是有个监控线程在后台不停的统计每个线程的空闲时间,看到线程的空闲时间超过阈值的时候,就回收掉。面试官: 小伙子,你的想法挺不错,逻辑很严谨,你确定线程池内部是这么实现的吗?问得我有点不自信了,没看过源码不能瞎蒙。我还是去瞅一眼一灯写的八股文吧。我: 这个我知道,线程池统计线程的空闲时间的实现逻辑很简单。阻塞队列(BlockingQueue)提供了一个 poll(time, unit) 方法,作用就是:当队列为空时,会阻塞指定时间,然后返回null。线程池就是就是利用阻塞队列的这个方法,如果在指定时间内拉取不到任务,就表示该线程的存活时间已经超过阈值了,就要被回收了。具体源码如下:// 从阻塞队列中拉取任务 private Runnable getTask() { boolean timedOut = false; for (; ; ) { int c = ctl.get(); int rs = runStateOf(c); // 1. 如果线程池已经停了,或者阻塞队列是空,就回收当前线程 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); // 2. 再次判断是否需要回收线程 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 3. 在指定时间内,从阻塞队列中拉取任务 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; // 4. 如果没有拉取到任务,就标识该线程已超时 timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } } 面试官: 小伙子,可以啊,你是懂线程池源码的。再问你个问题,如果线程池抛异常了,也没有try/catch,会发生什么?美女面试官你这是准备打破砂锅问到底,铁了心不让我过,是吧?我的代码风格是很严谨的,谁写的业务代码不try/catch,也没遇到过这种情况。让我再看一下一灯总结的八股文吧。我: 有了,线程池中的代码如果抛异常了,也没有try/catch,会从线程池中删除这个异常线程,并创建一个新线程。不信的话,我们可以测试验证一下:/** * @author * @apiNote 线程池示例 **/ public class ThreadPoolDemo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); // 1. 创建一个单个线程的线程池 ExecutorService executorService = Executors.newSingleThreadExecutor(); // 2. 往线程池中提交3个任务 for (int i = 0; i < 3; i++) { executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " 关注公众号:架构"); throw new RuntimeException("抛异常了!"); }); } // 3. 关闭线程池 executorService.shutdown(); } }输出结果:pool-1-thread-1 关注公众号:架构 pool-1-thread-2 关注公众号:架构 pool-1-thread-3 关注公众号:架构 Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 抛异常了! at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Exception in thread "pool-1-thread-2" java.lang.RuntimeException: 抛异常了! at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Exception in thread "pool-1-thread-3" java.lang.RuntimeException: 抛异常了! at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)从输出结果中可以看出,线程名称并不是同一个,而是累加的,说明原线程已经被回收,新建了个线程。我们再看一下源码,验证一下:// 线程抛异常后,退出逻辑 private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) { if (completedAbruptly) decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; // 1. 从工作线程中删除当前线程 workers.remove(w); } finally { mainLock.unlock(); } // 2. 中断当前线程 tryTerminate(); int c = ctl.get(); if (runStateLessThan(c, STOP)) { if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; if (min == 0 && !workQueue.isEmpty()) min = 1; if (workerCountOf(c) >= min) return; // replacement not needed } // 3. 新建一个线程 addWorker(null, false); } }如果想统一处理异常,可以自定义线程创建工厂,在工厂里面设置异常处理逻辑。/** * @author * @apiNote 线程池示例 **/ public class ThreadPoolDemo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); // 1. 创建一个单个线程的线程池 ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> { // 2. 自定义线程创建工厂,并设置异常处理逻辑 Thread thread = new Thread(runnable); thread.setUncaughtExceptionHandler((t, e) -> { System.out.println("捕获到异常:" + e.getMessage()); }); return thread; }); // 3. 往线程池中提交3个任务 for (int i = 0; i < 3; i++) { executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " 关注公众号:架构"); throw new RuntimeException("抛异常了!"); }); } // 4. 关闭线程池 executorService.shutdown(); } }输出结果:Thread-0 关注公众号:架构 捕获到异常:抛异常了! Thread-1 关注公众号:架构 捕获到异常:抛异常了! Thread-2 关注公众号:架构 捕获到异常:抛异常了!面试官: 小伙子,论源码,还是得看你,还是你背的熟。现在我就给你发offer,薪资直接涨10%,明天9点就来上班吧,咱们公司实行996工作制。
0
0
0
浏览量2070
后端求offer版

Java阻塞队列中的异类,SynchronousQueue底层实现原理剖析

上篇文章谈到BlockingQueue的使用场景,并重点分析了ArrayBlockingQueue的实现原理,了解到ArrayBlockingQueue底层是基于数组实现的阻塞队列。但是BlockingQueue的实现类中,有一种阻塞队列比较特殊,就是SynchronousQueue(同步移交队列),队列长度为0。作用就是一个线程往队列放数据的时候,必须等待另一个线程从队列中取走数据。同样,从队列中取数据的时候,必须等待另一个线程往队列中放数据。这样特殊的队列,有什么应用场景呢?1. SynchronousQueue用法先看一个SynchronousQueue的简单用例:/** * @author * @apiNote SynchronousQueue示例 **/ public class SynchronousQueueDemo { public static void main(String[] args) throws InterruptedException { // 1. 创建SynchronousQueue队列 BlockingQueue<Integer> synchronousQueue = new SynchronousQueue<>(); // 2. 启动一个线程,往队列中放3个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 入队列 1"); synchronousQueue.put(1); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " 入队列 2"); synchronousQueue.put(2); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " 入队列 3"); synchronousQueue.put(3); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 3. 等待1000毫秒 Thread.sleep(1000L); // 4. 再启动一个线程,从队列中取出3个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take()); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take()); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }输出结果:Thread-0 入队列 1 Thread-1 出队列 1 Thread-0 入队列 2 Thread-1 出队列 2 Thread-0 入队列 3 Thread-1 出队列 3从输出结果中可以看到,第一个线程Thread-0往队列放入一个元素1后,就被阻塞了。直到第二个线程Thread-1从队列中取走元素1后,Thread-0才能继续放入第二个元素2。由于SynchronousQueue是BlockingQueue的实现类,所以也实现类BlockingQueue中几组抽象方法:为了满足不同的使用场景,BlockingQueue设计了很多的放数据和取数据的方法。操作抛出异常返回特定值阻塞阻塞一段时间放数据addofferputoffer(e, time, unit)取数据removepolltakepoll(time, unit)查看数据(不删除)element()peek()不支持不支持这几组方法的不同之处就是:    。当队列满了,再往队列中放数据,add方法抛异常,offer方法返回false,put方法会一直阻塞(直到有其他线程从队列中取走数据),offer(e, time, unit)方法阻塞指定时间然后返回false。    。当队列是空,再从队列中取数据,remove方法抛异常,poll方法返回null,take方法会一直阻塞(直到有其他线程往队列中放数据),poll(time, unit)方法阻塞指定时间然后返回null。    。当队列是空,再去队列中查看数据(并不删除数据),element方法抛异常,peek方法返回null。工作中使用最多的就是offer、poll阻塞指定时间的方法。2. SynchronousQueue应用场景SynchronousQueue的特点:队列长度是0,一个线程往队列放数据,必须等待另一个线程取走数据。同样,一个线程从队列中取数据,必须等待另一个线程往队列中放数据。这种特殊的实现逻辑有什么应用场景呢?我的理解就是,如果你希望你的任务需要被快速处理,就可以使用这种队列。Java线程池中的newCachedThreadPool(带缓存的线程池)底层就是使用SynchronousQueue实现的。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }newCachedThreadPool线程池的核心线程数是0,最大线程数是Integer的最大值,线程存活时间是60秒。如果你使用newCachedThreadPool线程池,你提交的任务会被更快速的处理,因为你每次提交任务,都会有一个空闲的线程等着处理任务。如果没有空闲的线程,也会立即创建一个线程处理你的任务。你想想,这处理效率,杠杠滴!当然也有弊端,如果你提交了太多的任务,导致创建了大量的线程,这些线程都在竞争CPU时间片,等待CPU调度,处理任务速度也会变慢,所以在使用过程中也要综合考虑。3. SynchronousQueue源码解析3.1 SynchronousQueue类属性public class SynchronousQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> { // 转换器,取数据和放数据的核心逻辑都在这个类里面 private transient volatile Transferer<E> transferer; // 默认的构造方法(使用非公平队列) public SynchronousQueue() { this(false); } // 有参构造方法,可以指定是否使用公平队列 public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); } // 转换器实现类 abstract static class Transferer<E> { abstract E transfer(E e, boolean timed, long nanos); } // 基于栈实现的非公平队列 static final class TransferStack<E> extends Transferer<E> { } // 基于队列实现的公平队列 static final class TransferQueue<E> extends Transferer<E> { } }可以看到SynchronousQueue默认的无参构造方法,内部使用的是基于栈实现的非公平队列,当然也可以调用有参构造方法,传参是true,使用基于队列实现的公平队列。// 使用非公平队列(基于栈实现) BlockingQueue<Integer> synchronousQueue = new SynchronousQueue<>(); // 使用公平队列(基于队列实现) BlockingQueue<Integer> synchronousQueue = new SynchronousQueue<>(true);本次就常用的栈实现来剖析SynchronousQueue的底层实现原理。3.2 栈底层结构栈结构,是非公平的,遵循先进后出。使用个case测试一下:/** * @author * @apiNote SynchronousQueue示例 **/ public class SynchronousQueueDemo { public static void main(String[] args) throws InterruptedException { // 1. 创建SynchronousQueue队列 SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>(); // 2. 启动一个线程,往队列中放1个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 入队列 0"); synchronousQueue.put(0); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 3. 等待1000毫秒 Thread.sleep(1000L); // 4. 启动一个线程,往队列中放1个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 入队列 1"); synchronousQueue.put(1); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 5. 等待1000毫秒 Thread.sleep(1000L); // 6. 再启动一个线程,从队列中取出1个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 7. 等待1000毫秒 Thread.sleep(1000L); // 8. 再启动一个线程,从队列中取出1个元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }输出结果:Thread-0 入队列 0 Thread-1 入队列 1 Thread-2 出队列 1 Thread-3 出队列 0从输出结果中可以看出,符合栈结构先进后出的顺序。3.3 栈节点源码栈中的数据都是由一个个的节点组成的,先看一下节点类的源码:// 节点 static final class SNode { // 节点值(取数据的时候,该字段为null) Object item; // 存取数据的线程 volatile Thread waiter; // 节点模式 int mode; // 匹配到的节点 volatile SNode match; // 后继节点 volatile SNode next; }item节点值,只在存数据的时候用。取数据的时候,这个值是null。waiter存取数据的线程,如果没有对应的接收线程,这个线程会被阻塞。mode节点模式,共有3种类型:类型值类型描述类型的作用0REQUEST表示取数据1DATA表示存数据2FULFILLING表示正在等待执行(比如取数据的线程,等待其他线程放数据)3.4 put/take流程放数据和取数据的逻辑,在底层复用的是同一个方法,以put/take方法为例,另外两个放数据的方法,add和offer方法底层实现是一样的。先看一下数据流转的过程,方便理解源码。还是以上面的case为例:    。Thread0先往SynchronousQueue队列中放入元素0    。Thread1再往SynchronousQueue队列放入元素1    。Thread2从SynchronousQueue队列中取出一个元素第一步:Thread0先往SynchronousQueue队列中放入元素0把本次操作组装成SNode压入栈顶,item是元素0,waiter是当前线程Thread0,mode是1表示放入数据。第二步:Thread1再往SynchronousQueue队列放入元素1把本次操作组装成SNode压入栈顶,item是元素1,waiter是当前线程Thread1,mode是1表示放入数据,next是SNode0。第三步:Thread2从SynchronousQueue队列中取出一个元素这次的操作比较复杂,也是先把本次的操作包装成SNode压入栈顶。item是null(取数据的时候,这个字段没有值),waiter是null(当前线程Thread2正在操作,所以不用赋值了),mode是2表示正在操作(即将跟后继节点进行匹配),next是SNode1。然后,Thread2开始把栈顶的两个节点进行匹配,匹配成功后,就把SNode2赋值给SNode1的match属性,唤醒SNode1中的Thread1线程,然后弹出SNode2节点和SNode1节点。3.5 put/take源码实现先看一下put方法源码:// 放数据 public void put(E e) throws InterruptedException { // 不允许放null元素 if (e == null) throw new NullPointerException(); // 调用转换器实现类,放元素 if (transferer.transfer(e, false, 0) == null) { // 如果放数据失败,就中断当前线程,并抛出异常 Thread.interrupted(); throw new InterruptedException(); } }核心逻辑都在transfer方法中,代码很长,理清逻辑后,也很容易理解。// 取数据和放数据操作,共用一个方法 E transfer(E e, boolean timed, long nanos) { SNode s = null; // e为空,说明是取数据,否则是放数据 int mode = (e == null) ? REQUEST : DATA; for (; ; ) { SNode h = head; // 1. 如果栈顶节点为空,或者栈顶节点类型跟本次操作相同(都是取数据,或者都是放数据) if (h == null || h.mode == mode) { // 2. 判断节点是否已经超时 if (timed && nanos <= 0) { // 3. 如果栈顶节点已经被取消,就删除栈顶节点 if (h != null && h.isCancelled()) casHead(h, h.next); else return null; // 4. 把本次操作包装成SNode,压入栈顶 } else if (casHead(h, s = snode(s, e, h, mode))) { // 5. 挂起当前线程,等待被唤醒 SNode m = awaitFulfill(s, timed, nanos); // 6. 如果这个节点已经被取消,就删除这个节点 if (m == s) { clean(s); return null; } // 7. 把s.next设置成head if ((h = head) != null && h.next == s) casHead(h, s.next); return (E) ((mode == REQUEST) ? m.item : s.item); } // 8. 如果栈顶节点类型跟本次操作不同,并且不是FULFILLING类型 } else if (!isFulfilling(h.mode)) { // 9. 再次判断如果栈顶节点已经被取消,就删除栈顶节点 if (h.isCancelled()) casHead(h, h.next); // 10. 把本次操作包装成SNode(类型是FULFILLING),压入栈顶 else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) { // 11. 使用死循环,直到匹配到对应的节点 for (; ; ) { // 12. 遍历下个节点 SNode m = s.next; // 13. 如果节点是null,表示遍历到末尾,设置栈顶节点是null,结束。 if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; // 14. 如果栈顶的后继节点跟栈顶节点匹配成功,就删除这两个节点,结束。 if (m.tryMatch(s)) { casHead(s, mn); return (E) ((mode == REQUEST) ? m.item : s.item); } else // 15. 如果没有匹配成功,就删除栈顶的后继节点,继续匹配 s.casNext(m, mn); } } } else { // 16. 如果栈顶节点类型跟本次操作不同,并且是FULFILLING类型, // 就再执行一遍上面第11步for循环中的逻辑(很少概率出现) SNode m = h.next; if (m == null) casHead(h, null); else { SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }transfer方法逻辑也很简单,就是判断本次操作类型是否跟栈顶节点相同,如果相同,就把本次操作压入栈顶。否则就跟栈顶节点匹配,唤醒栈顶节点线程,弹出栈顶节点。transfer方法中调用了awaitFulfill方法,作用是挂起当前线程。// 等待被唤醒 SNode awaitFulfill(SNode s, boolean timed, long nanos) { // 1. 计算超时时间 final long deadline = timed ? System.nanoTime() + nanos : 0L; Thread w = Thread.currentThread(); // 2. 计算自旋次数 int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); for (;;) { if (w.isInterrupted()) s.tryCancel(); // 3. 如果已经匹配到其他节点,直接返回 SNode m = s.match; if (m != null) return m; if (timed) { // 4. 超时时间递减 nanos = deadline - System.nanoTime(); if (nanos <= 0L) { s.tryCancel(); continue; } } // 5. 自旋次数减一 if (spins > 0) spins = shouldSpin(s) ? (spins-1) : 0; else if (s.waiter == null) s.waiter = w; // 6. 开始挂起当前线程 else if (!timed) LockSupport.park(this); else if (nanos > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanos); } }awaitFulfill方法的逻辑也很简单,就是挂起当前线程。take方法底层使用的也是transfer方法:// 取数据 public E take() throws InterruptedException { // // 调用转换器实现类,取数据 E e = transferer.transfer(null, false, 0); if (e != null) return e; // 没取到,就中断当前线程 Thread.interrupted(); throw new InterruptedException(); }4. 总结。SynchronousQueue是一种特殊的阻塞队列,队列长度是0,一个线程往队列放数据,必须等待另一个线程取走数据。同样,一个线程从队列中取数据,必须等待另一个线程往队列中放数据。。SynchronousQueue底层是基于栈和队列两种数据结构实现的。。Java线程池中的newCachedThreadPool(带缓存的线程池)底层就是使用SynchronousQueue实现的。。如果希望你的任务需要被快速处理,可以使用SynchronousQueue队列。
0
0
0
浏览量2045
后端求offer版

学会使用MySQL的Explain执行计划,SQL性能调优从此不再困难

上篇文章讲了MySQL架构体系,了解到MySQL Server端的优化器可以生成Explain执行计划,而执行计划可以帮助我们分析SQL语句性能瓶颈,优化SQL查询逻辑,今天就一块学习Explain执行计划的具体用法。1. explain的使用使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈。 在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,并不会执行这条SQL。 就比如下面这个:输出这么多列都是干嘛用的?其实大都是SQL语句的性能统计指标,先简单总结一下每一列的大致作用,下面详细讲一下:2. explain字段详解下面就详细讲一下每一列的具体作用。1. id列id表示查询语句的序号,自动分配,顺序递增,值越大,执行优先级越高。id相同时,优先级由上而下。2. select_type列select_type表示查询类型,常见的有SIMPLE简单查询、PRIMARY主查询、SUBQUERY子查询、UNION联合查询、UNION RESULT联合临时表结果等。3. table列table表示SQL语句查询的表名、表别名、临时表名。4. partitions列partitions表示SQL查询匹配到的分区,没有分区的话显示NULL。5. type列type表示表连接类型或者数据访问类型,就是表之间通过什么方式建立连接的,或者通过什么方式访问到数据的。具体有以下值,性能由好到差依次是:system > const > eq_ref > ref > ref_or_null > index_merge > range > index > ALLsystem当表中只有一行记录,也就是系统表,是 const 类型的特列。const表示使用主键或者唯一性索引进行等值查询,最多返回一条记录。性能较好,推荐使用。eq_ref表示表连接使用到了主键或者唯一性索引,下面的SQL就用到了user表主键id。ref表示使用非唯一性索引进行等值查询。ref_or_null表示使用非唯一性索引进行等值查询,并且包含了null值的行。index_merge表示用到索引合并的优化逻辑,即用到的多个索引。range表示用到了索引范围查询。index表示使用索引进行全表扫描。ALL表示全表扫描,性能最差。6. possible_keys列表示可能用到的索引列,实际查询并不一定能用到。7. key列表示实际查询用到索引列。8. key_len列表示索引所占的字节数。每种类型所占的字节数如下:类型占用空间char(n)n个字节varchar(n)2个字节存储变长字符串,如果是utf-8,则长度 3n + 2tinyint1个字节smallint2个字节int4个字节bigint8个字节date3个字节timestamp4个字节datetime8个字节字段允许为NULL额外增加1个字节9. ref列表示where语句或者表连接中与索引比较的参数,常见的有const(常量)、func(函数)、字段名。如果没用到索引,则显示为NULL。10. rows列表示执行SQL语句所扫描的行数。11. filtered列表示按条件过滤的表行的百分比。用来估算与其他表连接时扫描的行数,row x filtered = 252004 x 10% = 25万行12. Extra列表示一些额外的扩展信息,不适合在其他列展示,却又十分重要。Using where表示使用了where条件搜索,但没有使用索引。Using index表示用到了覆盖索引,即在索引上就查到了所需数据,无需二次回表查询,性能较好。Using filesort表示使用了外部排序,即排序字段没有用到索引。Using temporary表示用到了临时表,下面的示例中就是用到临时表来存储查询结果。Using join buffer表示在进行表关联的时候,没有用到索引,使用了连接缓存区存储临时结果。下面的示例中user_id在两张表中都没有建索引。Using index condition表示用到索引下推的优化特性。知识点总结:本文详细介绍了Explain使用方式,以及每种参数所代表的含义。无论是工作还是面试,使用Explain优化SQL查询,都是必备的技能,一定要牢记。下篇再一块学习一下SQL查询的其他优化方式,敬请期待。
0
0
0
浏览量2040
后端求offer版

深度剖析Redis九种数据结构实现原理,建议收藏

1. Redis介绍Redis 是一个高性能的键值存储系统,支持多种数据结构。包含五种基本类型 String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),和三种特殊类型 Geo(地理位置)、HyperLogLog(基数统计)、Bitmaps(位图)。每种数据结构都是为了解决特定问题而设计的,适用不同的场景。想要用好Redis,必须了解底层实现原理和使用技巧,同时结合具体的业务场景和需求进行选择和使用。无论是工作还是面试中,这些必备的知识。下面就详细介绍一下每种数据类型的使用方式、实现原理和适用场景。2. String(字符串)String(字符串)是Redis中最基本的数据结构之一,它可以存储任意类型的数据,包括数字、文本、序列化的对象等。Redis中的字符串最大可以存储512MB的数据。使用方式字符串类型的操作是最基本的,包括设置值、获取值、修改值、追加值等。字符串类型支持的操作包括:应用场景缓存:将计算结果、数据库查询结果或者配置数据存储在Redis中,可以提高应用的响应速度和吞吐量。计数器:使用Redis的自增和自减操作,实现简单的计数器功能,如网站的访问次数统计限流:使用Redis的incr和expire命令,实现固定窗口算法的流量控制,防止系统过载。分布式锁:使用SETNX操作实现分布式锁,保证同一时刻只有一个线程访问临界资源。会话管理:将用户会话信息存储在Redis中,可以实现分布式Session。内部编码Redis字符串的内部编码有三种:int编码:当字符串长度小于等于12字节并且字符串可以表示为整数时,Redis会使用int编码。这样可以节省内存,并且在执行一些命令时可以直接进行数值计算。embstr编码:当字符串长度小于等于39字节时,Redis会使用embstr编码。这种编码方式会将字符串和存储它的结构体一起分配在内存中,这样可以减少内存碎片和结构体的开销。raw编码:当字符串长度大于39字节或者字符串不能表示为整数时,Redis会使用raw编码。这种编码方式直接将字符串存储在一个结构体中,没有进行任何优化。3. Hash(哈希)使用方式哈希类型是一种键值对的集合,其中键值对的值可以是字符串、列表或者其他哈希类型。哈希类型支持的操作包括:应用场景存储对象:将对象的属性和属性值存储在哈希类型中,可以很方便地进行查询和更新操作,比如常见的用户信息就适合使用哈希类型存储。内部编码Redis哈希类型的内部编码有两种:ziplist(压缩列表):当Hash类型的元素比较少,且元素的大小比较小(小于64字节)时,Redis采用ziplist作为Hash类型的内部编码。ziplist是一种紧凑的、压缩的列表结构,可以节省内存空间。但是,ziplist只能进行线性查找,不支持快速的随机访问。hashtable(字典):当Hash类型的元素比较多,或者元素的大小比较大(大于64字节)时,Redis采用hashtable作为Hash类型的内部编码。hashtable是一种基于链表的哈希表结构,可以快速地进行随机访问。但是,hashtable需要占用更多的内存空间。4. List(列表)使用方式Redis List类型是一个有序的字符串列表,支持在列表的头部或尾部添加元素,也支持在列表任意位置插入或删除元素。支持的操作包括:使用场景Redis List类型由于支持在列表的头部或尾部添加元素,也支持在列表任意位置插入或删除元素,因此非常适合以下场景:**消息队列:**Redis List类型常被用作轻量级的消息队列,生产者将消息插入队列尾部,消费者从队列头部弹出消息进行处理,可以使用LPUSH、RPUSH、BLPOP、BRPOP等命令实现。**时间序列:**使用Redis的LPUSH和RPUSH命令,将时间序列的数据按照时间顺序添加到列表的头部或尾部,然后使用LRANGE命令,查询一段时间范围内的数据,实现时间序列的查询。**排行榜:**Redis List类型可以用于实现排行榜功能,将每个用户的得分作为元素值插入到列表中,使用LINSERT、LREM、LINDEX等命令进行排名操作,使用LRANGE命令查询排名前几的用户,可以使用LPUSH、LINSERT、LREM、LINDEX、LRANGE等命令实现。**计数器:**Redis List类型可以将每个元素视为计数器的值,可以使用LPUSH、RPUSH、LINDEX、LREM等命令实现。**最近访问记录:**Redis List类型可以用于记录最近访问的记录,将最新的访问记录插入列表头部,当列表长度超过设定的值时,使用LTRIM命令删除最旧的记录,可以使用LPUSH、LINDEX、LTRIM等命令实现。内部编码Redis List类型内部编码有两种,分别是ziplist和linkedlist。ziplistziplist是一种特殊的编码方式,它可以将小数据量的列表存储在一个连续的内存块中,节省了内存空间,同时还可以提高存取效率。ziplist编码的列表最大长度为2^16-1个元素,每个元素可以是字符串类型、整数类型或浮点数类型。在ziplist中,每个元素都被存储为一个字节数组,并包含一个前缀和一个后缀,用于标识该元素的类型和长度。linkedlistlinkedlist是一种常规的双向链表结构,它可以存储任意长度的列表,并且支持高效的插入和删除操作。在linkedlist中,每个节点都包含了一个指向前一个节点和后一个节点的指针,以及一个存储元素数据的指针。linkedlist适用于存储大数量的列表,它没有像ziplist那样的内存限制,但是会占用更多的内存空间。5. Set(集合)使用方式Redis Set(集合)是一个无序的字符串集合,其中每个元素都是唯一的,不允许重复。Redis Set类型支持的操作包括:使用场景Redis Set类型的使用场景包括:标签系统:使用Set类型存储每个标签对应的对象列表,以便快速查找包含特定标签的对象。可以使用SADD、SREM、SISMEMBER、SMEMBERS等命令实现。好友关系:将每个用户的好友列表作为一个集合,可以使用SADD、SREM、SISMEMBER、SDIFF、SINTER、SUNION等命令实现。共同好友:使用SINTER命令计算出两个用户的共同好友,可以使用SADD、SINTER、SUNION等命令实现。排名系统:将每个用户的得分作为元素值插入到集合中,使用ZADD、ZREM、ZRANK、ZSCORE等命令进行排名操作,使用ZREVRANGE命令查询排名前几的用户,可以使用ZADD、ZREM、ZRANK、ZSCORE、ZREVRANGE等命令实现。订阅关系:使用Set类型存储用户订阅的内容,以便快速获取用户订阅的内容。总的来说,Set类型适用于需要存储一组不重复的数据,并支持集合操作的场景。内部编码Redis Set类型的内部编码有两种:intset(整数集合):当Set类型只包含整数类型的数据,并且元素数量较少(小于512个)时,Redis会使用intset作为Set类型的内部编码。intset是一种紧凑的、压缩的整数集合结构,可以节省内存空间,并且支持快速的查找、插入和删除操作。在intset中,所有元素都按照从小到大的顺序排列,并且可以使用不同的编码方式(16位、32位、64位)存储不同大小范围内的整数。hashtable(字典):当Set类型包含字符串类型或者元素数量较多时,Redis会使用hashtable作为Set类型的内部编码。hashtable是一种基于链表的哈希表结构,可以快速地进行随机访问、插入和删除操作。在hashtable中,每个元素都被存储为一个字符串,并且使用哈希函数将字符串映射到一个桶中,然后在桶中进行查找、插入和删除操作。在实际使用中,当Set类型的元素全部为整数类型时,建议使用intset编码;而当Set类型的元素包含非整数类型时,才使用hashtable编码。6. Zset(有序集合)使用方式Redis中的Zset(有序集合)是一个键值对集合,其中每个元素都关联一个分值(score),通过分值进行排序,可以看作是一个字典(dict)和一个跳跃列表(skip list)的混合体,它可以存储多个相同的元素,但每个元素必须有一个唯一的score值。支持的操作包括:使用场景Redis Zset是一种有序集合,其使用场景主要包括以下几个方面:排行榜:使用Zset类型可以实现排行榜功能,将每个用户的得分作为元素值插入到集合中,使用ZADD、ZINCRBY、ZREM等命令进行排名操作,使用ZRANGE、ZREVRANGE命令查询排名前几的用户。最近访问记录:使用Zset类型可以用于记录最近访问的记录,将最新的访问记录插入集合中,使用ZREMRANGEBYRANK命令删除最旧的记录,使用ZRANGE命令查询最近访问的记录。计数器:Redis Zset可以用于实现计数器功能,比如统计某个页面的访问次数、统计某个广告的点击量等。将页面ID或广告ID作为成员(member)存储在Zset中,以访问次数或点击量作为分数(score)存储。好友关系:Redis Zset可以用于存储用户之间的关注关系以及用户之间的互动,比如点赞、评论等。可以将用户ID作为成员(member)存储在Zset中,将时间戳或者其他标识作为分数(score)存储,以此记录用户之间的互动情况。内部编码Redis Zset的内部编码有两种:ziplist编码:当Zset中元素个数小于128个,并且所有元素的长度都小于64字节时,Redis会使用ziplist编码存储Zset。这种编码方式可以节省内存空间,并且可以提高存取效率,但是不支持随机访问和范围查询。skiplist编码:当Zset中元素个数大于等于128个,或者有一个元素的长度大于64字节时,Redis会使用skiplist编码存储Zset。这种编码方式支持高效的随机访问和范围查询,但是需要占用更多的内存空间。7. Geo(地理位置)使用方式Redis Geo(地理位置)是一个键值对集合,其中每个元素都包含一个经度和纬度,可以用于存储地理位置信息并支持基于位置的搜索。Redis Geo支持的操作包括:Redis Geo类型适用于需要存储地理位置信息并支持基于位置的搜索的场景,比如附近的人、附近的商家等。使用场景Redis Geo类型的使用场景如下:位置服务:用于存储地理位置信息,如餐厅、商店、机场、医院等的经纬度信息,可以通过 Geo 库提供的命令查询指定范围内的所有商家信息。车辆监控:用于车辆位置跟踪和监控,可以将车辆的经纬度信息存储在 Redis 中,并通过 Geo 库提供的命令查询车辆的位置,以及在指定半径内的其他车辆信息。物流配送:用于存储配送员的位置信息,以及需要配送的订单信息的经纬度信息,可以通过 Geo 库提供的命令查询配送员在指定范围内的订单信息,以提高配送效率。电商推荐:用于存储用户的位置信息,以及商家和商品的经纬度信息,可以通过 Geo 库提供的命令查询指定范围内的商家和商品信息,以提供更加精准的推荐服务。游戏地图:用于存储游戏地图的位置信息和玩家的位置信息,可以通过 Geo 库提供的命令查询玩家在游戏地图上的位置,以及在指定半径内的其他玩家信息,以提供更加丰富的游戏体验。社交应用:用于存储用户的位置信息,以及附近的其他用户的位置信息,可以通过 Geo 库提供的命令查询指定范围内的用户信息,以提供更加精准的社交服务。内部编码Redis Geo类型内部使用zset来存储地理位置信息,其中元素的score值为经度,member值为经纬度组合的字符串。在使用GEORADIUS和GEORADIUSBYMEMBER命令搜索元素时,Redis会构建一个跳跃表,以实现高效的搜索。8. HyperLogLog(基数统计)使用方式Redis HyperLogLog(基数统计)是一种基于概率统计的数据结构,用于估计大型数据集合的基数(不重复元素的数量),以及对多个集合进行并、交运算等。HyperLogLog的优点是可以使用极少的内存空间,同时可以保证较高的准确性。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。使用场景HyperLogLog的使用场景主要包括以下几个方面:用户去重:使用HyperLogLog可以对海量的用户数据进行去重,快速地统计出不重复的用户数量。网站UV统计:使用HyperLogLog可以对网站的访问日志进行分析,统计出每天、每周、每月的独立访客数量。广告点击统计:使用HyperLogLog可以对广告的点击数据进行分析,统计出独立点击用户的数量,以及对多个广告进行并、交运算等。数据库查询优化:使用HyperLogLog可以对数据库中的数据进行去重,减少查询的数据量,提高查询效率。分布式计算:使用HyperLogLog可以在分布式系统中对数据进行去重、并、交等操作,以支持分布式计算。使用HyperLogLog可以大大减少内存占用和计算时间,是处理大数据量去重计数的有效工具。内部编码Redis HyperLogLog类型的内部编码使用的"稀疏矩阵"和”稠密矩阵“。当计数较少时,采用”稀疏矩阵“,其中绝大部分元素都是0。计数增多后,超过阈值后,会转换成”稠密矩阵“。9. Bitmaps(位图)使用方式Redis Bitmaps(位图)是一种紧凑的数据结构,可以用于表示一个只有0和1的数组。位图可以用于高效地存储大规模的布尔值,以及进行位运算、位图图形化等操作。Redis Bitmaps支持的操作包括:使用场景Redis Bitmaps适用于需要高效地存储大规模的布尔值,并进行位运算、统计等操作的场景。比如:统计在线用户数:使用Bitmaps类型来表示用户的在线状态,例如一个bit位表示一个用户,当用户登录时将对应的bit位置为1,当用户退出时将其位置为0。这样可以非常方便地进行在线用户的统计。黑白名单统计:在网络安全中,可以使用位图记录IP地址的访问情况、黑白名单等信息。统计用户访问行为:例如将每个页面或功能点表示为一个bit位,用户访问时将对应的bit位置为1,未访问则为0。这样就可以方便地统计用户的访问习惯,了解用户对产品的喜好和热点等信息。布隆过滤器:这是最常用的场景,布隆过滤器是一种用于快速判断某个元素是否在集合中的算法,在大数据量场景下其效率非常高。Redis的Bitmaps类型可以用来实现布隆过滤器,节约存储空间,并提高查询效率。内部编码Redis Bitmaps类型的内部编码使用了一种称为“压缩位图”的数据结构。它通过使用两个数组来存储位图数据:一个存储实际位的值,另一个存储每个字节中1的个数。这种编码方式可以大大压缩位图数据的大小。
0
0
0
浏览量2070
后端求offer版

不允许还有Java程序员不了解BlockingQueue阻塞队列的实现原理

我们平时开发中好像很少使用到BlockingQueue(阻塞队列),比如我们想要存储一组数据的时候会使用ArrayList,想要存储键值对数据会使用HashMap,在什么场景下需要用到BlockingQueue呢?1. BlockingQueue的应用场景当我们处理完一批数据之后,需要把这批数据发给下游方法接着处理,但是下游方法的处理速率不受控制,可能时快时慢。如果下游方法的处理速率较慢,很拖慢当前方法的处理速率,这时候该怎么办呢?你可能想到使用线程池,是个办法,不过需要创建很多线程,还要考虑下游方法支不支持并发,如果是CPU密集任务,可能多线程比单线程处理速度更慢,因为需要频繁上下文切换。这时候就可以考虑使用BlockingQueue,BlockingQueue最典型的应用场景就是上面这种生产者-消费者模型。生产者往队列中放数据,消费者从队列中取数据,中间使用BlockingQueue做缓冲队列,也就解决了生产者和消费者速率不同步的问题。你可能联想到了消息队列(MessageQueue),消息队列相当于分布式阻塞队列,而BlockingQueue相当于本地阻塞队列,只作用于本机器。对应的是分布式缓存(比如:Redis、Memcache)和本地缓存(比如:Guava、Caffeine)。另外很多框架中都有BlockingQueue的影子,比如线程池中就用到BlockingQueue做任务的缓冲。消息队列中发消息、拉取消息的方法也都借鉴了BlockingQueue,使用起来很相似。今天就一块深入剖析一下Queue的底层源码。2. BlockingQueue的用法BlockingQueue的用法非常简单,就是放数据和取数据。/** * @apiNote BlockingQueue示例 * @author */ public class Demo { public static void main(String[] args) throws InterruptedException { // 1. 创建队列,设置容量是10 BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 2. 往队列中放数据 queue.put(1); // 3. 从队列中取数据 Integer result = queue.take(); } }为了满足不同的使用场景,BlockingQueue设计了很多的放数据和取数据的方法。操作抛出异常返回特定值阻塞阻塞一段时间放数据addofferputoffer(e, time, unit)取数据removepolltakepoll(time, unit)取数据(不删除)element()peek()不支持不支持这几组方法的不同之处就是:当队列满了,再往队列中放数据,add方法抛异常,offer方法返回false,put方法会一直阻塞(直到有其他线程从队列中取走数据),offer方法阻塞指定时间然后返回false。当队列是空,再从队列中取数据,remove方法抛异常,poll方法返回null,take方法会一直阻塞(直到有其他线程往队列中放数据),poll方法阻塞指定时间然后返回null。当队列是空,再去队列中查看数据(并不删除数据),element方法抛异常,peek方法返回null。工作中使用最多的就是offer、poll阻塞指定时间的方法。3. BlockingQueue实现类BlockingQueue常见的有下面5个实现类,主要是应用场景不同。ArrayBlockingQueue基于数组实现的阻塞队列,创建队列时需指定容量大小,是有界队列。LinkedBlockingQueue基于链表实现的阻塞队列,默认是无界队列,创建可以指定容量大小SynchronousQueue一种没有缓冲的阻塞队列,生产出的数据需要立刻被消费PriorityBlockingQueue实现了优先级的阻塞队列,基于数据显示,是无界队列DelayQueue实现了延迟功能的阻塞队列,基于PriorityQueue实现的,是无界队列4. BlockingQueue源码解析BlockingQueue的5种子类实现方式大同小异,这次就以最常用的ArrayBlockingQueue做源码解析。4.1 ArrayBlockingQueue类属性先看一下ArrayBlockingQueue类里面有哪些属性:// 用来存放数据的数组 final Object[] items; // 下次取数据的数组下标位置 int takeIndex; // 下次放数据的数组下标位置 int putIndex; // 当前已有元素的个数 int count; // 独占锁,用来保证存取数据安全 final ReentrantLock lock; // 取数据的条件 private final Condition notEmpty; // 放数据的条件 private final Condition notFull;ArrayBlockingQueue中4组存取数据的方法实现也是大同小异,本次以put和take方法进行解析。4.2 put方法源码解析无论是放数据还是取数据都是从队头开始,逐渐往队尾移动。// 放数据,如果队列已满,就一直阻塞,直到有其他线程从队列中取走数据 public void put(E e) throws InterruptedException { // 校验元素不能为空 checkNotNull(e); final ReentrantLock lock = this.lock; // 加锁,加可中断的锁 lock.lockInterruptibly(); try { // 如果队列已满,就一直阻塞,直到被唤醒 while (count == items.length) notFull.await(); // 如果队列未满,就往队列添加元素 enqueue(e); } finally { // 结束后,别忘了释放锁 lock.unlock(); } } // 实际往队列添加数据的方法 private void enqueue(E x) { // 获取数组 final Object[] items = this.items; // putIndex 表示本次插入的位置 items[putIndex] = x; // ++putIndex 计算下次插入的位置 // 如果本次插入的位置,正好等于队尾,下次插入就从 0 开始 if (++putIndex == items.length) putIndex = 0; // 元素数量加一 count++; // 唤醒因为队列空等待的线程 notEmpty.signal(); }源码中有个有意思的设计,添加元素的时候如果已经到了队尾,下次就从队头开始添加,相当于做成了一个循环队列。像下面这样:4.3 take方法源码// 取数据,如果队列为空,就一直阻塞,直到有其他线程往队列中放数据 public E take() throws InterruptedException { final ReentrantLock lock = this.lock; // 加锁,加可中断的锁 lock.lockInterruptibly(); try { // 如果队列为空,就一直阻塞,直到被唤醒 while (count == 0) notEmpty.await(); // 如果队列不为空,就从队列取数据 return dequeue(); } finally { // 结束后,别忘了释放锁 lock.unlock(); } } // 实际从队列取数据的方法 private E dequeue() { // 获取数组 final Object[] items = this.items; // takeIndex 表示本次取数据的位置,是上一次取数据时计算好的 E x = (E) items[takeIndex]; // 取完之后,就把队列该位置的元素删除 items[takeIndex] = null; // ++takeIndex 计算下次拿数据的位置 // 如果本次取数据的位置,正好是队尾,下次就从 0 开始取数据 if (++takeIndex == items.length) takeIndex = 0; // 元素数量减一 count--; if (itrs != null) itrs.elementDequeued(); // 唤醒被队列满所阻塞的线程 notFull.signal(); return x; }4.4 总结ArrayBlockingQueue基于数组实现的阻塞队列,创建队列时需指定容量大小,是有界队列。ArrayBlockingQueue底层采用循环队列的形式,保证数组位置可以重复使用。ArrayBlockingQueue存取都采用ReentrantLock加锁,保证线程安全,在多线程环境下也可以放心使用。使用ArrayBlockingQueue的时候,预估好队列长度,保证生产者和消费者速率相匹配。
0
0
0
浏览量2029
后端求offer版

别再问我MySQL为啥没走索引?就这几种原因,全都告诉你

工作中,经常遇到这样的问题,我明明在MySQL表上面加了索引,为什么执行SQL查询的时候却没有用到索引?同一条SQL有时候查询用到了索引,有时候却没用到索引,这是咋回事?原因可能是索引失效了,失效的原因有以下几种,看你有没有踩过类似的坑?1. 数据准备:有这么一张用户表,在name字段上建个索引:CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(255) DEFAULT NULL COMMENT '姓名', `age` int DEFAULT NULL COMMENT '年龄', PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB COMMENT='用户表';2. Explain详解:想要查看一条SQL是否用到索引?用到了哪种类型的索引?可以使用explain关键字,查看SQL执行计划。例如:explain select * from user where id=1;可以看到type=const,表示使用了主键索引。explain的所有type类型如下:3. 失效原因1. 数据类型隐式转换name字段是varchar类型,如果我们使用数据类型查询,就会产生数据类型转换,虽然不会报错,但是无法用到索引。explain select * from user where name='一灯';explain select * from user where name=18;2. 模糊查询 like 以%开头explain select * from user where name like '张%';explain select * from user where name like '%张';3. or前后没有同时使用索引虽然name字段上加了索引,但是age字段没有索引,使用or的时候会全表扫描。# or前后没有同时使用索引,导致全表扫描 explain select * from user where name='一灯' or age=18;4. 联合索引,没有使用第一列索引如果我们在(name,age)上,建立联合索引,但是查询条件中只用到了age字段,也是无法用到索引的。使用联合索引,必须遵循最左匹配原则,首先使用第一列字段,然后使用第二列字段。CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(255) DEFAULT NULL COMMENT '姓名', `age` int DEFAULT NULL COMMENT '年龄', PRIMARY KEY (`id`), KEY `idx_name_age` (`name`,`age`) ) ENGINE=InnoDB COMMENT='用户表';5. 在索引字段进行计算操作如果我们在索引列进行了计算操作,也是无法用到索引的。# 在主键索引上进行计算操作,导致全表扫描 explain select * from user where id+1=2;6. 在索引字段字段上使用函数如果我们在索引列使用函数,也是无法用到索引的。7. 优化器选错索引同一条SQL有时候查询用到了索引,有时候却没用到索引,这是咋回事?这可能是优化器选择的结果,会根据表中数据量选择是否使用索引。当表中大部分name都是一灯,这时候用name='一灯'做查询,还会不会用到索引呢?索引优化器会认为,用索引还不如全表扫描来得快,干脆不用索引了。当然我们认为优化器优化的不对,也可以使用force index强制使用索引。知识点总结:
0
0
0
浏览量2052
后端求offer版

MySQL select count(*)计数很慢,有没有优化方案?

在日常开发工作中,我经常会遇到需要统计总数的场景,比如:统计订单总数、统计用户总数等。一般我们会使用MySQL 的count函数进行统计,但是随着数据量逐渐增大,统计耗时也越来越长,最后竟然出现慢查询的情况,这究竟是什么原因呢?本篇文章带你一下学习一下。1. MyISAM存储引擎计数为什么这么快?我们总有个错觉,就是感觉MyISAM引擎的count计数要比InnoDB引擎更快,实际这不是错觉。MyISAM引擎把表的总行数单独记录在磁盘上,查询的时候可以直接返回,不需要再累加统计。但是当SQL查询中有where条件的时候,就无法再使用表的总行数了,还是需要乖乖的进行累加统计,查询性能也就跟InnoDB相差无几了。为什么MyISAM引擎能够记录表的总行数,InnoDB引擎却不行?因为MyISAM引擎不支持事务,只有表锁,所以记录的总行数是准确的。而InnoDB引擎支持事务和行锁,存在并发修改的情况。又由于事务的隔离性,会出现不可重复读和幻读,记录的总行数无法保证是准确的。2. 能不能手动实现统计总行数既然InnoDB引擎没有帮我们记录总行数,我们能不能手动记录总行数,比如使用Redis。其实也是不行的,使用Redis记录总行数,至少有下面3个问题:无法实现事务之间的隔离更新丢失,因为i++不是原子操作,当然可以使用Lua脚本实现原子操作,更复杂。Redis是非关系型缓存数据库,不能当作关系型持久化数据库使用,一般需要设置过期时间。由上图中得知,虽然Redis计数加1操作放在了事务里面,但是不受事务控制的,在事务没有提交前,其他查询依然读到了最新的总行数,这就是脏读的情况。3. InnoDB引擎能否实现快速计数有一种办法,可以粗略估计表的总行数,就是使用MySQL命令:show table status like 'user';真实的总行数有100万行,预估有99万多行,误差在可接受的范围内。部分场景适用,比如粗略估计网站的总用户数。4. 四种计数方式的性能差别常见的统计总行数的方式有以下四种:count(*) 、 count(常量) 、 count(id) 、 count(字段)InnoDB引擎对count计数做了优化,会选用数据量较小的非聚簇索引进行统计。比如用户表中有三个索引,分别是主键索引、name索引和age索引,使用执行计划查看计数的时候用到了哪个索引?CREATE TABLE `user` (  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',  `name` varchar(100) DEFAULT NULL COMMENT '姓名',  `age` tinyint NOT NULL,  PRIMARY KEY (`id`),  KEY `idx_name` (`name`),  KEY `idx_age` (`age`) ) ENGINE=InnoDB COMMENT='用户表';explain select count(*) from user;用到了数据量较小的age索引。count(*) 、 count(常量) 是直接统计表中的总行数,效率较高。而 count(id) 还需要把数据返回给MySQL Server端进行累加计数。最后 count(字段)需要筛选不为null字段,效率最差。四种计数的查询性能从高到低,依次是:count(*) ≈ count(常量) > count(id) > count(字段)对于大多数情况,得到计数结果,还是老老实实使用count(*)所以推荐使用select count(*) ,别跟select * 搞混了,不推荐使用select * 的。
0
0
0
浏览量2078
后端求offer版

阿里Java面试官:CopyOnWriteArrayList底层是怎么保证线程安全的?

引言上篇文章提到ArrayList不是线程安全的,而CopyOnWriteArrayList是线程安全的。此刻我就会产生几个问题:CopyOnWriteArrayList初始容量是多少?CopyOnWriteArrayList是怎么进行扩容的?CopyOnWriteArrayList是怎么保证线程安全的?带着这几个问题,一起分析一下CopyOnWriteArrayList的源码。简介CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现,不过该数组使用了volatile关键字修饰。 实现线程安全的原理是,“人如其名”,就是 Copy On Write(写时复制),意思就是在对其进行修改操作的时候,复制一个新的ArrayList,在新的ArrayList上进行修改操作,从而不影响旧的ArrayList的读操作。 看一下源码中CopyOnWriteArrayList内部有哪些数据结构组成:public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // 加锁,用来保证线程安全 final transient ReentrantLock lock = new ReentrantLock(); // 存储元素的数组,使用了volatile修饰 private transient volatile Object[] array; // 数组的get/set方法 final Object[] getArray() { return array; } final void setArray(Object[] a) { array = a; } }CopyOnWriteArrayList的内部结构非常简单,使用ReentrantLock加锁,用来保证线程安全。使用数组存储元素,数组使用volatile修饰,用来保证内存可见性。当其他线程重新对数组对象进行赋值的时候,当前线程可以及时感知到。初始化当我们调用CopyOnWriteArrayList的构造方法的时候,底层逻辑是怎么实现的?List<Integer> list = new CopyOnWriteArrayList<>();CopyOnWriteArrayList初始化的时候,不支持指定数组长度,接着往下看,就能明白CopyOnWriteArrayList为什么不支持指定数组长度。public CopyOnWriteArrayList() { setArray(new Object[0]); }初始化过程非常简单,就是创建了一个长度为0的数组。添加元素再看一下往CopyOnWriteArrayList添加元素时,调用的 add() 方法源码实现:// 添加元素 public boolean add(E e) { // 加锁,保证线程安全 final ReentrantLock lock = this.lock; lock.lock(); try { // 获取原数组 Object[] elements = getArray(); int len = elements.length; // 创建一个新数组,长度原数组长度+1,并把原数组元素拷贝到新数组里面 Object[] newElements = Arrays.copyOf(elements, len + 1); // 直接赋值给新数组末尾位置 newElements[len] = e; // 替换原数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); } }添加元素的流程:先使用ReentrantLock加锁,保证线程安全。再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面。然后在新数组末尾位置赋值使用新数组替换掉原数组最后释放锁add() 方法添加元素的时候,并没有在原数组上进行赋值,而是创建一个新数组,在新数组上赋值后,再用新数组替换原数组。这是为了利用volatile关键字的特性,如果直接在原数组上进行修改,其他线程是感知不到的。只有重新对原数组对象进行赋值,其他线程才能感知到。 还有一个需要注意的点是,每次添加元素的时候都会创建一个新数组,并涉及数组拷贝,相当于每次都进行扩容操作。当数组较大,性能消耗较为明显。所以CopyOnWriteArrayList适用于读多写少的场景,如果存在较多的写操作场景,性能也是一个需要考虑的因素。删除元素再看一下删除元素的方法 remove() 的源码:// 按照下标删除元素 public E remove(int index) { // 加锁,保证线程安全 final ReentrantLock lock = this.lock; lock.lock(); try { // 获取原数组 Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); // 计算需要移动的元素个数 int numMoved = len - index - 1; if (numMoved == 0) { // 0表示删除的是数组末尾的元素 setArray(Arrays.copyOf(elements, len - 1)); } else { // 创建一个新数组,长度是原数组长度-1 Object[] newElements = new Object[len - 1]; // 把原数组下标前后两段的元素都拷贝到新数组里面 System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); // 替换原数组 setArray(newElements); } return oldValue; } finally { // 释放锁 lock.unlock(); } }删除元素的流程:先使用ReentrantLock加锁,保证线程安全。再创建一个新数组,长度是原数组长度-1,并把原数组中剩余元素(不包含需要删除的元素)拷贝到新数组里面。使用新数组替换掉原数组最后释放锁可以看到,删除元素的流程与添加元素的流程类似,都是需要创建一个新数组,再把旧数组元素拷贝到新数组,最后替换旧数组。区别就是新数组的长度不一样,删除元素流程中的新数组长度是旧数组长度-1,添加元素流程中的新数组长度是旧数组长度+1。 根据对象删除元素的方法源码与之类似,也是转换成下标删除,读者可自行查看。批量删除再看一下批量删除元素方法 removeAll() 的源码:// 批量删除元素 public boolean removeAll(Collection<?> c) { // 参数判空 if (c == null) { throw new NullPointerException(); } // 加锁,保证线程安全 final ReentrantLock lock = this.lock; lock.lock(); try { // 获取原数组 Object[] elements = getArray(); int len = elements.length; if (len != 0) { // 创建一个新数组,长度暂时使用原数组的长度,因为不知道要删除多少个元素。 Object[] temp = new Object[len]; // newlen表示新数组中元素个数 int newlen = 0; // 遍历原数组,把需要保留的元素放到新数组中 for (int i = 0; i < len; ++i) { Object element = elements[i]; if (!c.contains(element)) { temp[newlen++] = element; } } // 如果新数组没有满,就释放空白位置,并覆盖原数组 if (newlen != len) { setArray(Arrays.copyOf(temp, newlen)); return true; } } return false; } finally { // 释放锁 lock.unlock(); } }批量删除元素的流程,与上面类似:先使用ReentrantLock加锁,保证线程安全。再创建一个新数组,长度暂时使用原数组的长度,因为不知道要删除多少个元素。然后遍历原数组,把需要保留的元素放到新数组中。释放掉新数组中空白位置,再使用新数组替换掉原数组。最后释放锁如果遇到需要一次删除多个元素的场景,尽量使用 removeAll() 方法,因为 removeAll() 方法只涉及一次数组拷贝,性能比单个删除元素更好。并发修改问题当遍历CopyOnWriteArrayList的过程中,同时增删CopyOnWriteArrayList中的元素,会发生什么情况?测试一下:import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class Test { public static void main(String[] args) { List<Integer> list = new CopyOnWriteArrayList<>(); list.add(1); list.add(2); list.add(2); list.add(3); // 遍历ArrayList for (Integer key : list) { // 判断如果元素等于2,则删除 if (key.equals(2)) { list.remove(key); } } System.out.println(list); } }输出结果:[1, 3]不但没有抛出异常,还把CopyOnWriteArrayList中重复的元素也都删除了。 原因是CopyOnWriteArrayList重新实现迭代器,拷贝了一份原数组的快照,在快照数组上进行遍历。这样做的优点是其他线程对数组的并发修改,不影响对快照数组的遍历,但是遍历过程中无法感知其他线程对数组修改,有得必有失。 下面是迭代器的源码实现:static final class COWIterator<E> implements ListIterator<E> { /** * 原数组的快照 */ private final Object[] snapshot; /** * 迭代游标 */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } // 迭代下个元素 public E next() { if (!hasNext()) throw new NoSuchElementException(); return (E)snapshot[cursor++]; } }总结现在可以回答文章开头提出的问题了吧:CopyOnWriteArrayList初始容量是多少?答案:是0CopyOnWriteArrayList是怎么进行扩容的?答案:加锁创建一个新数组,长度原数组长度+1,并把原数组元素拷贝到新数组里面。释放锁CopyOnWriteArrayList是怎么保证线程安全的?答案:使用ReentrantLock加锁,保证操作过程中线程安全。使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。
0
0
0
浏览量2095
后端求offer版

面试必问,JVM内存模型扫盲

JVM简介JVM(Java Virtual Machine,Java虚拟机)是Java语言的核心,是一个用于解释Java字节码的虚拟计算机。它可以在运行Java程序时自动管理内存、处理异常等。Java程序员不需要关心底层硬件和操作系统的细节,只需要编写符合Java语法规范的代码,就可以实现跨平台的编程。当我们编写Java程序时,Java源代码会被编译成为Java字节码( .java 文件被编译成 .class 文件)。这些字节码可以在任何安装了Java虚拟机的平台上运行。JVM在执行Java字节码时,将其转换成特定于底层CPU和操作系统的机器代码。运行时数据区简介为了执行字节码,JVM在内存中定义了一系列的数据区,用于在运行时存储各类数据,即运行时数据区(Runtime Data Areas)。理解这些数据区及其作用,是掌握Java性能调优和错误排查的关键。JVM 运行时数据区是 Java 虚拟机在执行 Java 程序时用于数据存储的内存区域,这些区域各司其职,确保了 Java 程序的正确执行。JVM 运行时数据区主要分为五个部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、堆(Heap)、方法区(Method Area)。JVM运行时数据区在程序运行时动态地分配和释放内存,内存管理由JVM自动完成。不同的数据区域有不同的内存管理机制和垃圾回收算法,以保证程序运行的效率和稳定性。其中程序计数器、虚拟机栈、本地方法栈属于线程私有区域,跟随线程的启动和结束而建立和销毁。堆和方法区是线程共享区域,跟随虚拟机进程的启动而存在。程序计数器(Program Counter Register) 是一块较小的内存空间,作用是指示当前线程正在执行的 JVM 字节码指令地址。虚拟机栈(VM Stack) 存放的是一些基本类型的变量(如int, long)和对象引用。Java 方法执行的内存模型是以栈帧(Stack Frame)为基础的,每个方法在执行的时候都会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈(Native Method Stack) 与虚拟机栈类似,其主要服务于 JVM 使用到的 Native 方法。堆区(Heap) 是 JVM 所管理的最大一块内存空间,主要用于存放所有线程共享的 Java 对象实例。这也是垃圾回收器主要活动区域。方法区(Method Area) 是用来存储加载的类信息、常量、静态变量等数据的。这个区域是线程共享的。1. 程序计数器程序计数器(Program Counter Register)是线程私有区域,生命周期与线程一致,也是 JVM 内存中唯一一个没有任何 OutOfMemoryError 的区域。程序计数器的作用是记录当前线程正在执行的指令地址,换句话说,它指向了下一条将要被执行的 JVM 字节码指令。在 JVM 的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。当线程执行的是 Java 方法时,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为空(Undefined)。程序计数器对于现代多线程而言至关重要,因为在 CPU 切换各个线程时,需要将各个线程的程序计数器记录下来,以便在下一次切换回这个线程时,能知道该从哪里继续执行。总结:程序计数器是一块很小的内存空间,也是运行速度最快的存储区域。在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域2. 虚拟机栈与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,生命周期与线程相同。描述的是Java方法执行的内存模型。在 JVM 中,每当一个新的线程被创建,都会创建一个与之关联的私有 JVM 栈。这个栈会随着线程的运行而进行入栈(push)和出栈(pop)操作。它主要用于存储局部变量、操作数堆栈以及方法调用的情况。JVM 栈是由一系列栈帧(Stack Frame)组成的。每当一个方法被调用,一个新的栈帧就会被压入栈中,每当一个方法调用结束,一个栈帧就会被弹出栈。每个栈帧中都包含了局部变量表、操作数栈、动态链接和方法返回地址等信息。局部变量表主要存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于指针,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。操作数栈则是在执行字节码指令时用到的临时存储区,比如在进行算数运算时,操作数栈就会用来存放操作数和接收结果。Java虚拟机栈可能会抛出以下异常:如果线程请求的栈深度大于 JVM 所允许的深度,将抛出 StackOverflowError。如果 JVM 栈可以动态扩展,当扩展时无法申请到足够的内存,会抛出 OutOfMemoryError。3. 本地方法栈本地方法栈(Native Method Stack)也是线程私有,生命周期与线程相同。作用是与虚拟机栈类似,虚拟机栈是为Java 方法服务的,而本地方法栈是为 Native 方法服务的。和虚拟机栈一样,本地方法栈的大小可以是固定的也可以是动态的。如果是固定的,当线程请求的栈深度超过最大深度时,会抛出 StackOverflowError。如果是动态的,并且在尝试扩展时无法申请到足够的内存,会抛出 OutOfMemoryError。4. 堆堆(Heap)是 JVM 所管理的最大一块内存空间,也是所有线程共享的一块内存区域,在虚拟机启动时创建。堆主要用于存储对象实例和数组,这也是 Java 垃圾回收器主要活动的区域。在物理上,堆区可以处于分散的内存空间中,但在逻辑上它被视为连续的。堆区在 JVM 启动时创建,如果堆区的空间不足,将会抛出 OutOfMemoryError。堆分为新生代(Young Generation)和老年代(Old Generation)。新生代又分为 Eden 区、From Survivor 区(简称 S0)、 To Survivor 区(简称 S1)。划分这么多区域的目的是为了更好地回收内存,或者更快地分配内存。新生代中各个区域的内存占比分别是,Eden : S0 : S1 = 8 : 1 : 1新创建的对象优先在 Eden 区进行分配。当 Eden 区满时,会触发一次 Minor GC(新生代垃圾回收,也叫 Young GC),将仍然存活的对象从 Eden 区和 S0 区移动到 S1 区,下次 Minor GC 处理情况类似,把存活的对象从 Eden 区和 S1 区移动到 S0 区。当 Survivor 区也满了,还存活的对象会被移动到老年代。如果老年代也满了,将会触发 Major GC(老年代垃圾回收,也叫 Old GC)。当老年代满了,也可能触发 Full GC,Full GC 会对整个堆内存进行垃圾回收,包含新生代、老年代和方法区。Full GC 会导致较长的停顿时间,并且会消耗大量的系统资源。5. 方法区方法区(Method Area)与堆一样,是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区只是 JVM 规范中定义的一个概念,针对 Hotspot 虚拟机,JDK8 之前使用永久代(Permanent Generation,简称 PermGen)实现,JDK8 使用元空间(Metaspace)实现。JDK8 之前可以通过 -XX:PermSize 和 -XX:MaxPermSize 来设置永久代大小,JDK8 之后,使用元空间替换了永久代,改为通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间大小。运行时常量池运行时常量池(Runtime Constant Pool)是方法区中的一部分,用于存储编译期间生成的各种字面量和符号引用。在Java程序运行时,JVM将编译期生成的class文件中的常量池内容读取到运行时常量池中。运行时常量池存储了类和接口中的常量,包括字符串字面量、被声明为final的常量值等。它还存储了类和接口中的符号引用,如类和接口、字段和方法的引用等。在JVM中,运行时常量池是线程安全的。每个线程都有一个自己的线程栈,其中包含了局部变量表,而这些局部变量表中所引用的对象都位于堆中。当一个线程需要引用运行时常量池中的常量时,JVM会先将常量值从运行时常量池中复制到线程栈的局部变量表中,然后再进行引用。需要注意的是,在JDK8中,运行时常量池已经被移动到元空间(Metaspace)中。元空间是在本地内存中分配的,与JVM的堆内存是分离的,因此不会受到Java堆大小的限制。
0
0
0
浏览量2112
后端求offer版

重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似

在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS。AQS全称 AbstractQueuedSynchronizer ,即抽象队列同步器,是一种用来构建锁和同步器的框架。我们常见的并发锁ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。当我仔细研究AQS底层加锁原理,发现竟然跟Synchronized加锁原理有惊人的相似。让我突然想到一句名言,记不清怎么说了,意思是框架底层原理很相似,大家多学习底层原理。Synchronized的加锁流程在前几篇文章已经详细讲过,没看过一块再温习一下。1. Synchronized加锁流程我们先想一下Synchronized的加锁需求,如果让你设计Synchronized的对象锁存储结构,该怎么设计?多个线程执行到Synchronized代码块,只有一个线程获取锁,然后执行同步代码块(需要记录哪个线程获取了对象锁)。其他线程被阻塞(被阻塞的线程,是不是可以用链表设计个阻塞队列?)持有锁的线程调用wait方法,释放锁,等待被唤醒(等待的线程,是不是可以用链表设计个等待队列?)。被阻塞的线程开始竞争锁调用notify方法,唤醒等待的线程,被唤醒的线程进入阻塞队列,一块竞争锁。上面描述了Synchronized的加锁流程,Synchronized的对象锁存储结构是不是跟咱们想的一样?实际就是的。下面是对象锁的存储数据结构(由C++实现):ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; // 持有锁的线程 _WaitSet = NULL; // 等待队列,存储处于wait状态的线程 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 阻塞队列,存储处于等待锁block状态的线程 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }上图展示了对象锁的基本工作机制:当多个线程同时访问一段同步代码时,首先会进入 _EntryList队列中阻塞。当某个线程获取到对象的对象锁后进入临界区域,并把对象锁中的 _owner变量设置为当前线程,即获得对象锁。若持有对象锁的线程调用 wait() 方法,将释放当前持有的对象锁,_owner变量恢复为null,同时该线程进入 _WaitSet 集合中等待被唤醒。在_WaitSet集合中的线程被唤醒,会被再次放到_EntryList队列中,重新竞争获取锁。若当前线程执行完毕也将释放对象锁并复位变量的值,以便其他线程进入获取锁。Synchronized对象锁存储结构和加锁流程,竟然跟咱们想的一样。再看一下ReentrantLock的存储结构和加锁流程,有没有相似的地方。2. AQS加锁原理先分析一下,我们使用AQS的加锁需求:多个线程执行到ReentrantLock.lock方法的时候,只有一个线程获取锁,然后执行同步代码块(需要记录哪个线程获取了对象锁)。其他线程被阻塞(被阻塞的线程,是不是可以用链表设计个阻塞队列?名叫”同步队列“?)持有锁的线程调用await方法,释放锁,等待被唤醒(等待的线程,是不是可以用链表设计个等待队列?名叫”条件队列“?)。被阻塞的线程开始竞争锁调用signal方法,唤醒等待的线程,被唤醒的线程进入阻塞队列,一块竞争锁。AQS的需求跟Synchronized一模一样。我们再看一下AQS实际的加锁机制是怎么设计的?是不是跟Synchronized相似?AQS的加锁流程并不复杂,只要理解了同步队列和条件队列,以及它们之间的数据流转,就算彻底理解了AQS。当多个线程竞争AQS锁时,如果有个线程获取到锁,就把ower线程设置为自己没有竞争到锁的线程,在同步队列中阻塞(同步队列采用双向连接,尾插法)。持有锁的线程调用await方法,释放锁,追加到条件队列的末尾(条件队列采用单链条,尾插法)。持有锁的线程调用signal方法,唤醒条件队列的头节点,并转移到同步队列的末尾。同步队列的头节点优先获取到锁可以看到AQS和Synchronized的加锁流程几乎是一模一样的,AQS中同步队列就是Synchronized中EntryList,AQS中条件队列就是Synchronized中的waitSet,两个队列之间的数据转移流程也是一样的。3. 总结AQS跟Synchronized的加锁流程是一样的,都是通过同步队列和条件队列实现的,阻塞状态的线程被放到同步队列中,等待状态的线程被放到条件队列中,从条件队列唤醒的线程又被转移到同步队列末尾,一块竞争锁。看完AQS加锁流程,还没有人不懂AQS的?下篇文章再讲一下AQS加锁具体的源码实现。里面有很多精巧的设计,值得我们学习。比如:为什么同步队列要设计成双向链表?而条件队列要设计成单链表?为什么AQS加锁性能这么好(乐观锁CAS使用)?同步队列和条件队列中节点怎么用一个对象实现?释放锁后,怎么唤醒同步队列中线程?
0
0
0
浏览量2042
后端求offer版

硬核剖析ThreadLocal源码,面试官看了直呼内行

工作面试中经常遇到ThreadLocal,但是很多同学并不了解ThreadLocal实现原理,到底为什么会发生内存泄漏也是一知半解?今天作者带你深入剖析ThreadLocal源码,总结ThreadLocal使用规范,解析ThreadLocal高频面试题。1. ThreadLocal是什么ThreadLocal是线程本地变量,就是线程的私有变量,不同线程之间相互隔离,无法共享,相当于每个线程拷贝了一份变量的副本。目的就是在多线程环境中,无需加锁,也能保证数据的安全性。2. ThreadLocal的使用/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 2. 给ThreadLocal赋值 threadLocal.set("关注公众号:架构"); // 3. 从ThreadLocal中取值 String result = threadLocal.get(); System.out.println(result); // 输出 关注公众号:架构 // 4. 删除ThreadLocal中的数据 threadLocal.remove(); System.out.println(threadLocal.get()); // 输出null } }ThreadLocal的用法非常简单,创建ThreadLocal的时候指定泛型类型,然后就是赋值、取值、删除值的操作。不同线程之间,ThreadLocal数据是隔离的,测试一下:/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { IntStream.range(0, 5).forEach(i -> { // 创建5个线程,分别给threadLocal赋值、取值 new Thread(() -> { // 2. 给ThreadLocal赋值 threadLocal.set(i); // 3. 从ThreadLocal中取值 System.out.println(Thread.currentThread().getName() + "," + threadLocal.get()); }).start(); }); } }输出结果:Thread-2,2 Thread-4,4 Thread-1,1 Thread-0,0 Thread-3,3可以看出不同线程之间的ThreadLocal数据相互隔离,互不影响,这样的实现效果有哪些应用场景呢?3. ThreadLocal应用场景ThreadLocal的应用场景主要分为两类:避免对象在方法之间层层传递,打破层次间约束。    比如用户信息,在很多地方都需要用到,层层往下传递,比较麻烦。这时候就可以把用户信息放到ThreadLocal中,需要的地方可以直接使用。2.拷贝对象副本,减少初始化操作,并保证数据安全。    比如数据库连接、Spring事务管理、SimpleDataFormat格式化日期,都是使用的ThreadLocal,即避免每个线程都初始化一个对象,又保证了多线程下的数据安全。使用ThreadLocal保证SimpleDataFormat格式化日期的线程安全,代码类似下面这样:/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static void main(String[] args) { IntStream.range(0, 5).forEach(i -> { // 创建5个线程,分别从threadLocal取出SimpleDateFormat,然后格式化日期 new Thread(() -> { try { System.out.println(threadLocal.get().parse("2022-11-11 00:00:00")); } catch (ParseException e) { throw new RuntimeException(e); } }).start(); }); } }4. ThreadLocal实现原理ThreadLocal底层使用ThreadLocalMap存储数据,而ThreadLocalMap内部是一个数组,数组里面存储的是Entry对象,Entry对象里面使用key-value存储数据,key是ThreadLocal实例对象本身,value是ThreadLocal的泛型对象值。4.1 ThreadLocalMap源码static class ThreadLocalMap { // Entry对象,WeakReference是弱引用,当没有引用指向时,会被GC回收 static class Entry extends WeakReference<ThreadLocal<?>> { // ThreadLocal泛型对象值 Object value; // 构造方法,传参是key-value // key是ThreadLocal对象实例,value是ThreadLocal泛型对象值 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // Entry数组,用来存储ThreadLocal数据 private Entry[] table; // 数组的默认容量大小 private static final int INITIAL_CAPACITY = 16; // 扩容的阈值,默认是数组大小的三分之二 private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; } }4.2 set方法源码// 给ThreadLocal设值 public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 如果ThreadLocal已经设过值,直接设值,否则初始化 if (map != null) // 设值的key就是当前ThreadLocal对象实例,value是ThreadLocal泛型对象值 map.set(this, value); else // 初始化ThreadLocalMap createMap(t, value); }再看一下实际的set方法源码:// key就是当前ThreadLocal对象实例,value是ThreadLocal泛型对象值 private void set(ThreadLocal<?> key, Object value) { // 获取ThreadLocalMap中的Entry数组 Entry[] tab = table; int len = tab.length; // 计算key在数组中的下标,也就是ThreadLocal的hashCode和数组大小-1取余 int i = key.threadLocalHashCode & (len - 1); // 查找流程:从下标i开始,判断下标位置是否有值, // 如果有值判断是否等于当前ThreadLocal对象实例,等于就覆盖,否则继续向后遍历数组,直到找到空位置 for (Entry e = tab[i]; e != null; // nextIndex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1 e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 如果等于当前ThreadLocal对象实例,直接覆盖 if (k == key) { e.value = value; return; } // 当前key是null,说明ThreadLocal对象实例已经被GC回收了,直接覆盖 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 找到空位置,创建Entry对象 tab[i] = new Entry(key, value); int sz = ++size; // 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }set方法具体流程如下:从源码和流程图中得知,ThreadLocal是通过线性探测法解决哈希冲突的,线性探测法具体赋值流程如下:通过key的hashcode找到数组下标如果数组下标位置是空或者等于当前ThreadLocal对象,直接覆盖值结束如果不是空,就继续向下遍历,遍历到数组结尾后,再从头开始遍历,直到找到数组为空的位置,在此位置赋值结束线性探测法这种特殊的赋值流程,导致取值的时候,也要走一遍类似的流程。4.3 get方法源码// 从ThreadLocal从取值 public T get() { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) { // 通过ThreadLocal实例对象作为key,在Entry数组中查找数据 ThreadLocalMap.Entry e = map.getEntry(this); // 如果不为空,表示找到了,直接返回 if (e != null) { T result = (T)e.value; return result; } } // 如果ThreadLocalMap是null,就执行初始化ThreadLocalMap操作 return setInitialValue(); }再看一下具体的遍历Entry数组的逻辑:// 具体的遍历Entry数组的方法 private Entry getEntry(ThreadLocal<?> key) { // 通过hashcode计算数组下标位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 如果下标位置对象不为空,并且等于当前ThreadLocal实例对象,直接返回 if (e != null && e.get() == key) return e; else // 如果不是,需要继续向下遍历Entry数组 return getEntryAfterMiss(key, i, e); }再看一下线性探测法特殊的取值方法:// 如果不是,需要继续向下遍历Entry数组 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 循环遍历数组,直到找到ThreadLocal对象,或者遍历到数组为空的位置 while (e != null) { ThreadLocal<?> k = e.get(); // 如果等于当前ThreadLocal实例对象,表示找到了,直接返回 if (k == key) return e; // key是null,表示ThreadLocal实例对象已经被GC回收,就帮忙清除value if (k == null) expungeStaleEntry(i); else // 索引位置+1,表示继续向下遍历 i = nextIndex(i, len); e = tab[i]; } return null; } // 索引位置+1,表示继续向下遍历,遍历到数组结尾,再从头开始遍历 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }ThreadLocal的get方法流程如下:4.4 remove方法源码remove方法流程跟set、get方法类似,都是遍历数组,找到ThreadLocal实例对象后,删除key、value,再删除Entry对象结束。public void remove() { // 获取当前线程的ThreadLocalMap对象 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } // 具体的删除方法 private void remove(ThreadLocal<?> key) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; // 计算数组下标 int i = key.threadLocalHashCode & (len - 1); // 遍历数组,直到找到空位置, // 或者值等于当前ThreadLocal对象,才结束 for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 找到后,删除key、value,再删除Entry对象 if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }5. ThreadLocal使用注意事项使用ThreadLocal结束,一定要调用remove方法,清理掉threadLocal数据。具体流程类似下面这样:/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<User> threadLocal = new ThreadLocal<>(); public void method() { try { User user = getUser(); // 2. 给threadLocal赋值 threadLocal.set(user); // 3. 执行其他业务逻辑 doSomething(); } finally { // 4. 清理threadLocal数据 threadLocal.remove(); } } }如果忘了调用remove方法,可能会导致两个严重的问题:导致内存溢出如果线程的生命周期很长,一直往ThreadLocal中放数据,却没有删除,最终产生OOM导致数据错乱如果使用了线程池,一个线程执行完任务后并不会被销毁,会继续执行下一个任务,导致下个任务访问到了上个任务的数据。6. 常见面试题剖析看完了ThreadLocal源码,再回答几道面试题,检验一下学习成果怎么样。6.1 ThreadLocal是怎么保证数据安全性的?ThreadLocal底层使用的ThreadLocalMap存储数据,而ThreadLocalMap是线程Thread的私有变量,不同线程之间数据隔离,所以即使ThreadLocal的set、get、remove方法没有加锁,也能保证线程安全。6.2 ThreadLocal底层为什么使用数组?而不是一个对象?因为在一个线程中可以创建多个ThreadLocal实例对象,所以要用数组存储,而不是用一个对象。6.3 ThreadLocal是怎么解决哈希冲突的?ThreadLocal使用的线性探测法法解决哈希冲突,线性探测法法具体赋值流程如下:通过key的hashcode找到数组下标如果数组下标位置是空或者等于当前ThreadLocal对象,直接覆盖值结束如果不是空,就继续向下遍历,遍历到数组结尾后,再从头开始遍历,直到找到数组为空的位置,在此位置赋值结束6.4 ThreadLocal为什么要用线性探测法解决哈希冲突?我们都知道HashMap采用的是链地址法(也叫拉链法)解决哈希冲突,为什么ThreadLocal要用线性探测法解决哈希冲突?而不用链地址法呢?我的猜想是可能是创作者偷懒、嫌麻烦,或者是ThreadLocal使用量较少,出现哈希冲突概率较低,不想那么麻烦。使用链地址法需要引入链表和红黑树两种数据结构,实现更复杂。而线性探测法没有引入任何额外的数据结构,直接不断遍历数组。结果就是,如果一个线程中使用很多个ThreadLocal,发生哈希冲突后,ThreadLocal的get、set性能急剧下降。线性探测法相比链地址法优缺点都很明显:优点: 实现简单,无需引入额外的数据结构。缺点: 发生哈希冲突后,ThreadLocal的get、set性能急剧下降。6.5 ThreadLocalMap的key为什么要设计成弱引用?先说一下弱引用的特点:弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。ThreadLocalMap的key设计成弱引用后,会不会我们正在使用,就被GC回收了?这个是不会的,因为我们一直在强引用着ThreadLocal实例对象。/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 2. 给ThreadLocal赋值 threadLocal.set("关注公众号:架构"); // 3. 从ThreadLocal中取值 String result = threadLocal.get(); // 手动触发GC System.gc(); System.out.println(result); // 输出 关注公众号:架构 } }由上面代码中得知,如果我们一直在使用threadLocal,触发GC后,并不会threadLocal实例对象。ThreadLocalMap的key设计成弱引用的目的就是:防止我们在使用完ThreadLocal后,忘了调用remove方法删除数据,导致数组中ThreadLocal数据一直不被回收。/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建ThreadLocal static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 2. 给ThreadLocal赋值 threadLocal.set("关注公众号:架构"); // 3. 使用完threadLocal,设置成null,模仿生命周期结束 threadLocal = null; // 触发GC,这时候ThreadLocalMap的key就会被回收,但是value还没有被回收。 // 只有等到下次执行get、set方法遍历数组,遍历到这个位置,才会删除这个无效的value System.gc(); } }6.6 ThreadLocal为什么会出现内存泄漏?ThreadLocal出现内存泄漏的原因,就是我们使用完ThreadLocal没有执行remove方法删除数据。具体是哪些数据过多导致的内存泄漏呢?一个是数组的Entry对象,Entry对象中key、value分别是ThreadLocal实例对象和泛型对象值。因为我们在使用ThreadLocal的时候,总爱把ThreadLocal设置成类的静态变量,直到线程生命周期结束,ThreadLocal对象数据才会被回收。另一个是数组中Entry对象的value值,也就是泛型对象值。虽然ThreadLocalMap的key被设置成弱引用,会被GC回收,但是value并没有被回收。需要等到下次执行get、set方法遍历数组,遍历到这个位置,才会删除这个无效的value。这也是造成内存泄漏的原因之一。6.7 怎么实现父子线程共享ThreadLocal数据?只需要InheritableThreadLocal即可,当初始化子线程的时候,会从父线程拷贝ThreadLocal数据。/** * @author * @apiNote ThreadLocal示例 **/ public class ThreadLocalDemo { // 1. 创建可被子线程继承数据的ThreadLocal static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { // 2. 给ThreadLocal赋值 threadLocal.set("关注公众号:架构"); // 3. 启动一个子线程,看是否能获取到主线程数据 new Thread(() -> { System.out.println(threadLocal.get()); // 输出 关注公众号:架构 }).start(); } }
0
0
0
浏览量2078
后端求offer版

Redis为什么能抗住10万并发?揭秘性能优越的背后原因

1. Redis简介Redis是一个开源的,基于内存的,高性能的键值型数据库。它支持多种数据结构,包含五种基本类型 String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),和三种特殊类型 Geo(地理位置)、HyperLogLog(基数统计)、Bitmaps(位图),可以满足各种应用场景的需求。Redis还提供了多种特性,如持久化、事务、发布订阅、Lua脚本、管道、主从复制、哨兵机制、集群机制等,可以保证数据的安全性、一致性和可用性。Redis的速度非常快,官方称其可以达到每秒10万次的读写操作。和其他数据库相比,Redis有着明显的优势。例如,和MySQL相比,Redis的速度大约快了100倍;和MongoDB相比,Redis的速度大约快了10倍。这些优势使得Redis成为了很多互联网公司和开发者的首选数据库。那么,Redis为什么这么快呢?主要有以下几个原因:使用内存存储数据,避免了磁盘IO的开销,提高了数据访问的速度。丰富的对象类型,包含8种对象类型,满足不同场景的需求。高效的数据结构,减少了内存占用和计算复杂度,提高了数据操作的效率。单线程模型,避免了多线程之间的上下文切换和竞争条件,提升CPU利用率。非阻塞IO多路复用机制,充分利用CPU和网络资源,提高了并发处理能力。本文将详细介绍Redis为什么这么快的原理和机制,并给出一些实际应用和优化建议。2. 内存操作Redis是一种基于内存的数据库,与传统的基于磁盘的数据库(例如MySQL)不同,它将所有的数据都存储在内存中。那么,Redis为什么选择内存存储数据呢?主要有以下几个原因:内存的速度远远快于磁盘。内存读写速度可以达到每秒数百GB,而磁盘读写速度通常只有数十MB,万倍的差距。内存可以支持更多的数据结构和操作。常见的数据结构如数组、链表、树、哈希、集合等,常见的操作如排序、查找、过滤、聚合等。内存是一个灵活介质,满足各种复杂和高效的功能,不是磁盘操作可比的。内存可以支持更高的并发和扩展性。内存是一种分布式和并行的存储介质,它可以支持多个CPU核心同时访问同一块内存区域,也可以支持多个服务器之间共享同一块内存区域。磁盘是一种集中式和串行的存储介质,它只能支持一个CPU核心或一个服务器访问同一块磁盘区域,也不能支持多个服务器之间共享同一块磁盘区域。当然,Redis使用内存存储数据也有一些缺点和限制:内存限制:内存是非常昂贵的,容量通常只有几十GB或几百GB,而磁盘目前都是TB起步。所以我们通常只会把少量的、经常访问的数据存储在内存中。数据类型限制:Redis不支持复杂的数据结构,比如用户对象,通常只能序列化成字符串后再存储,查询的时候再把字符串反序列化成用户对象。数据备份问题:在服务器重启或崩溃时,存储的内存中的数据可能会丢失。通常采用持久化技术将数据保存到磁盘上,同时定期备份数据以防止数据丢失。3. 丰富的对象类型Redis包含五种基本类型 String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),和三种特殊类型 Geo(地理位置)、HyperLogLog(基数统计)、Bitmaps(位图),可以满足各种应用场景的需求。String可以用来做缓存、计数器、限流、分布式锁、分布式Session等。Hash可以用来存储复杂对象。List可以用来做消息队列、排行榜、计数器、最近访问记录等。Set可以用来做标签系统、好友关系、共同好友、排名系统、订阅关系等。Zset可以用来做排行榜、最近访问记录、计数器、好友关系等。Geo可以用来做位置服务、物流配送、电商推荐、游戏地图等。HyperLogLog可以用来做用户去重、网站UV统计、广告点击统计、分布式计算等。Bitmaps可以用来做在线用户数统计、黑白名单统计、布隆过滤器等。4. 高效的数据结构Redis有6种数据结构sds(简单动态字符串)、ziplist(压缩列表)、linkedlist(链表)、intset(整数集合)、hashtable(字典)、skiplist(跳跃表)。Redis的8种对象类型底层都是基于这5种数据结构实现的,丰富的数据结构可以减少内存占用和计算复杂度,提高数据操作的效率。5. 单线程模型Redis使用单线程模型,这意味着它只使用一个CPU来处理所有请求。因此,Redis不需要考虑多线程之间的同步、锁、竞争等问题,也不需要花费时间和资源在多线程之间的上下文切换上。这使得Redis的设计和实现更简单,性能和效率更高。那么,Redis为什么选择单线程模型呢?主要有以下几个原因:Redis性能瓶颈不在于CPU,而在于内存和网络。因为Redis使用内存存储数据,所以数据访问非常迅速,不会成为性能瓶颈。此外,Redis的数据操作大多数都是简单的键值对操作,不包含复杂计算和逻辑,因而CPU开销很小。相反,Redis的瓶颈在于内存的容量和网络的带宽,这些问题无法通过增加CPU核心来解决。Redis的单线程模型可以保证数据的一致性和原子性。由于Redis只有一个线程来处理所有的请求,所以不会出现多个线程同时修改同一个数据的情况,也不需要使用锁或事务来保证数据的一致性和原子性。Redis的单线程模型可以避免多线程编程的复杂性和难度。例如线程安全、死锁、内存泄漏、竞态条件等,降低了开发和维护的成本和风险。6. 多路IO复用模型Redis使用单线程模型来处理客户端的请求,但是它能够利用多路I/O复用技术来实现高并发和高吞吐量。那么,什么是多路I/O复用模型?多路I/O复用模型是指使用一个线程来监控多个文件描述符(fd)的读写状态,当某个fd准备好执行读或写操作时,就通知相应的事件处理器来处理。这样就避免了阻塞式I/O模型中,单个线程只能等待一个fd的问题,提高了I/O效率和利用率。例如Linux系统中提供了多种多路I/O复用技术的实现方式,如select、poll、epoll等。7. 总结本文介绍了Redis为什么如此快的原因。首先,Redis使用内存存储数据,避免了磁盘I/O的开销,提高了数据访问的速度。其次,Redis拥有丰富的对象类型,包含八种类型,满足不同的需求。此外,Redis采用了高效的数据结构,减少了内存占用和计算复杂度。Redis还使用单线程模型,避免了多线程之间的上下文切换和竞争条件,提升了CPU利用率。最后,Redis使用非阻塞I/O多路复用机制,充分利用CPU和网络资源,提高了并发处理能力。
0
0
0
浏览量2086
后端求offer版

我说ArrayList初始容量是10,面试官让我回去等通知

引言在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。ArrayList的初始容量是多少?(90%的人都会答错)ArrayList的扩容机制并发修改ArrayList元素会有什么问题如何快速安全的删除ArrayList中的元素接下来一块分析一下ArrayList的源码,看完ArrayList源码之后,可以轻松解答上面四个问题。简介ArrayList底层基于数组实现,可以随机访问,内部使用一个Object数组来保存元素。它维护了一个 elementData 数组和一个 size 字段,elementData数组用来存放元素,size字段用于记录元素个数。它允许元素是null,可以动态扩容。 初始化当我们调用ArrayList的构造方法的时候,底层实现逻辑是什么样的?// 调用无参构造方法,初始化ArrayList List<Integer> list1 = new ArrayList<>(); // 调用有参构造方法,初始化ArrayList,指定容量为10 List<Integer> list1 = new ArrayList<>(10);看一下底层源码实现:// 默认容量大小 private static final int DEFAULT_CAPACITY = 10; // 空数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认容量的数组对象 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 存储元素的数组 transient Object[] elementData; // 数组中元素个数,默认是0 private int size; // 无参初始化,默认是空数组 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 有参初始化,指定容量大小 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { // 直接使用指定的容量大小 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); } }可以看到当我们调用ArrayList的无参构造方法 new ArraryList<>() 的时候,只是初始化了一个空对象,并没有指定数组大小,所以初始容量是零。至于什么时候指定数组大小,接着往下看。添加元素再看一下往ArrayList种添加元素时,调用的 add() 方法源码:// 添加元素 public boolean add(E e) { // 确保数组容量够用,size是元素个数 ensureCapacityInternal(size + 1); // 直接在下个位置赋值 elementData[size++] = e; return true; } // 确保数组容量够用 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 计算所需最小容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果数组等于空数组,就设置默认容量为10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // 确保容量够用 private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果所需最小容量大于数组长度,就进行扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); }看一下扩容逻辑:// 扩容,就是把旧数据拷贝到新数组里面 private void grow(int minCapacity) { int oldCapacity = elementData.length; // 计算新数组的容量大小,是旧容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的容量小于最小容量,扩容后的容量就等于最小容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果扩容后的容量大于Integer的最大值,就用Integer最大值 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 扩容并赋值给原数组 elementData = Arrays.copyOf(elementData, newCapacity); }可以看到:扩容的触发条件是数组全部被占满扩容是以旧容量的1.5倍扩容,并不是2倍扩容最大容量是Integer的最大值添加元素时,没有对元素校验,允许为null,也允许元素重复。再看一下数组拷贝的逻辑,这里都是Arrays类里面的方法了:/** * @param original 原数组 * @param newLength 新的容量大小 */ public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { // 创建一个新数组,容量是新的容量大小 T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 把原数组的元素拷贝到新数组 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }最终调用了System类的数组拷贝方法,是native方法:/** * @param src 原数组 * @param srcPos 原数组的开始位置 * @param dest 目标数组 * @param destPos 目标数组的开始位置 * @param length 被拷贝的长度 */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);总结一下ArrayList的 add() 方法的逻辑:检查容量是否够用,如果够用,直接在下一个位置赋值结束。如果是第一次添加元素,则设置容量默认大小为10。如果不是第一次添加元素,并且容量不够用,则执行扩容操作。扩容就是创建一个新数组,容量是原数组的1.5倍,再把原数组的元素拷贝到新数组,最后用新数组对象覆盖原数组。需要注意的是,每次扩容都会创建新数组和拷贝数组,会有一定的时间和空间开销。在创建ArrayList的时候,如果我们可以提前预估元素的数量,最好通过有参构造函数,设置一个合适的初始容量,以减少动态扩容的次数。删除单个元素再看一下删除元素的方法 remove() 的源码:public boolean remove(Object o) { // 判断要删除的元素是否为null if (o == null) { // 遍历数组 for (int index = 0; index < size; index++) // 如果和当前位置上的元素相等,就删除当前位置上的元素 if (elementData[index] == null) { fastRemove(index); return true; } } else { // 遍历数组 for (int index = 0; index < size; index++) // 如果和当前位置上的元素相等,就删除当前位置上的元素 if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } // 删除该位置上的元素 private void fastRemove(int index) { modCount++; // 计算需要移动的元素的个数 int numMoved = size - index - 1; if (numMoved > 0) // 从index+1位置开始拷贝,也就是后面的元素整体向左移动一个位置 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 设置数组最后一个元素赋值为null,防止会导致内存泄漏 elementData[--size] = null; }删除元素的流程是:判断要删除的元素是否为null,如果为null,则遍历数组,使用双等号比较元素是否相等。如果不是null,则使用 equals() 方法比较元素是否相等。这里就显得啰嗦了,可以使用 Objects.equals()方法,合并ifelse逻辑。如果找到相等的元素,则把后面位置的所有元素整体相左移动一个位置,并把数组最后一个元素赋值为null结束。可以看到遍历数组的时候,找到相等的元素,删除就结束了。如果ArrayList中存在重复元素,也只会删除其中一个元素。批量删除再看一下批量删除元素方法 removeAll() 的源码:// 批量删除ArrayList和集合c都存在的元素 public boolean removeAll(Collection<?> c) { // 非空校验 Objects.requireNonNull(c); // 批量删除 return batchRemove(c, false); } private boolean batchRemove(Collection<?> c, boolean complement){ final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) // 把需要保留的元素左移 elementData[w++] = elementData[r]; } finally { // 当出现异常情况的时候,可能不相等 if (r != size) { // 可能是其它线程添加了元素,把新增的元素也左移 System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } // 把不需要保留的元素设置为null if (w != size) { for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }批量删除元素的逻辑,并不是大家想象的:遍历数组,判断要删除的集合中是否包含当前元素,如果包含就删除当前元素。删除的流程就是把后面位置的所有元素整体左移,然后把最后位置的元素设置为null。这样删除的操作,涉及到多次的数组拷贝,性能较差,而且还存在并发修改的问题,就是一边遍历,一边更新原数组。 批量删除元素的逻辑,设计充满了巧思,具体流程就是:把需要保留的元素移动到数组左边,使用下标 w 做统计,下标 w 左边的是需要保留的元素,下标 w 右边的是需要删除的元素。虽然ArrayList不是线程安全的,也考虑了并发修改的问题。如果上面过程中,有其他线程新增了元素,把新增的元素也移动到数组左边。最后把数组中下标 w 右边的元素都设置为null。所以当需要批量删除元素的时候,尽量使用 removeAll() 方法,性能更好。并发修改的问题当遍历ArrayList的过程中,同时增删ArrayList中的元素,会发生什么情况?测试一下:import java.util.ArrayList; import java.util.List; public class Test { public static void main(String[] args) { // 创建ArrayList,并添加4个元素 List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(2); list.add(3); // 遍历ArrayList for (Integer key : list) { // 判断如果元素等于2,则删除 if (key.equals(2)) { list.remove(key); } } } }运行结果:Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.yideng.Test.main(Test.java:14)报出了并发修改的错误,ConcurrentModificationException。 这是因为 forEach 使用了ArrayList内置的迭代器,这个迭代器在迭代的过程中,会校验修改次数 modCount,如果 modCount 被修改过,则抛出ConcurrentModificationException异常,快速失败,避免出现不可预料的结果。// ArrayList内置的迭代器 private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount; // 迭代下个元素 public E next() { // 校验 modCount checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E)elementData[lastRet = i]; } // 校验 modCount 是否被修改过 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }如果想要安全的删除某个元素,可以使用 remove(int index) 或者 removeIf() 方法。import java.util.ArrayList; import java.util.List; public class Test { public static void main(String[] args) { // 创建ArrayList,并添加4个元素 List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(2); list.add(3); // 使用 remove(int index) 删除元素 for (int i = 0; i < list.size(); i++) { if (list.get(i).equals(2)) { list.remove(i); } } // 使用removeIf删除元素 list.removeIf(key -> key.equals(2)); } }总结现在可以回答文章开头提出的问题了吧:ArrayList的初始容量是多少?答案:初始容量是0,在第一次添加元素的时候,才会设置容量为10。ArrayList的扩容机制答案:创建新数组,容量是原来的1.5倍。把旧数组元素拷贝到新数组中使用新数组覆盖旧数组对象并发修改ArrayList元素会有什么问题答案:会快速失败,抛出ConcurrentModificationException异常。如何快速安全的删除ArrayList中的元素答案:使用remove(int index) 、 removeIf() 或者 removeAll() 方法。 我们知道ArrayList并不是线程安全的,原因是它的 add() 、remove() 方法、扩容操作都没有加锁,多个线程并发操作ArrayList的时候,会出现数据不一致的情况。 想要线程安全,其中一种方式是初始化ArrayList的时候使用 Collections.synchronizedCollection() 修饰。这样ArrayList所有操作都变成同步操作,性能较差。还有一种性能较好,又能保证线程安全的方式是使用 CopyOnWriteArrayList,就是下章要讲的。// 第一种方式,使用 Collections.synchronizedCollection() 修饰 List<Integer> list1 = Collections.synchronizedCollection(new ArrayList<>()); // 第二种方式,使用 CopyOnWriteArrayList List<Integer> list1 = new CopyOnWriteArrayList<>();
0
0
0
浏览量2126
后端求offer版

再有人说synchronized是重量级锁,就把这篇文章扔给他看

synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用。但不可否认的是synchronized依然是并发首选工具,连volatile、CAS、ReentrantLock都无法动摇synchronized的地位。synchronized是工作面试中的必备技能,今天就跟着一灯一块深入剖析synchronized底层到底做了哪些优化?synchronized是用来加锁的,而锁是加在对象上面,所以需要先聊一下JVM中对象构成。1. 对象的构成Java对象在JVM内存中由三块区域组成:对象头、实例数据和对齐填充。对象头又分为:Mark Word(标记字段)、Class Pointer(类型指针)、数组长度(如果是数组)。实例数据是对象实际有效信息,包括本类信息和父类信息等。对齐填充没有特殊含义,由于虚拟机要求 对象起始地址必须是8字节的整数倍,作用仅是字节对齐。Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。重点关注一下对象头中Mark Word,里面存储了对象的hashcode、锁状态标识、持有锁的线程id、GC分代年龄等。在32为的虚拟机中,Mark Word的组成如下:2. synchronized锁优化从JDK1.6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK1.5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁等优化策略。由于使得synchronized性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,性能依次是从高到低。锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。2.1 自旋锁线程的挂起与恢复需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。自旋锁就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。2.2 自适应自旋锁JDK 1.6引入了更加智能的自旋锁,即自适应自旋锁。自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费CPU资源。有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。2.3 锁消除JVM在JIT编译时通过对运行上下文的扫描,经过逃逸分析,对于某段代码不存在竞争或共享的可能性,就会讲这段代码的锁消除,提升程序运行效率。public void method() { final Object LOCK = new Object(); synchronized (LOCK) { // do something } }比如上面代码中锁,是方法中私有的,又是不可变的,完全没必要加锁,所以JVM就会执行锁消除。2.4 锁粗化按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。public void method(Object LOCK) { synchronized (LOCK) { // do something1 } synchronized (LOCK) { // do something2 } }比如上面方法中两个加锁的代码块,完全可以合并成一个,减少频繁加锁解锁带来的开销,提升程序运行效率。2.5 偏向锁为什么要引入偏向锁?因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,通常是一个线程多次获得同一把锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。2.6 轻量级锁轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋(CAS)这等待锁释放。加锁过程: 当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。如果更新成功,当前线程就获得了锁。解锁过程: 轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)2.7 重量级锁synchronized是通过对象内部的监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。重量级锁的工作流程:当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长,所以重量级锁的开销还是很大的。在锁竞争激烈、锁持有时间长的场景,还是适合使用重量级锁的。2.8 锁升级过程2.9 锁的优缺点对比锁的性能从低到高,依次是无锁、偏向锁、轻量级锁、重量级锁。不同的锁只是适合不同的场景,大家可以依据实际场景自行选择。3. 总结synchronized锁经过多次迭代优化,已经不像以前那么重了,在JDK1.8的ConcurrentHashMap源码中已经大量使用synchronized做同步控制,大家在日常开发中可以放心使用了。
0
0
0
浏览量2039
后端求offer版

MySQL查询性能优化七种武器之索引下推

今天要讲的是MySQL的另一种查询性能优化方式 — 索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本增加的特性。1. 索引下推的作用主要作用有两个:减少回表查询的次数减少存储引擎和MySQL Server层的数据传输量总之就是了提升MySQL查询性能。2. 案例实践创建一张用户表,造点数据验证一下:CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '姓名', `age` tinyint NOT NULL COMMENT '年龄', `gender` tinyint NOT NULL COMMENT '性别', PRIMARY KEY (`id`), KEY `idx_name_age` (`name`,`age`) ) ENGINE=InnoDB COMMENT='用户表';在 姓名和年龄 (name,age) 两个字段上创建联合索引。查询SQL执行计划,验证一下是否用到索引下推:explain select * from user where name='一灯' and age>2;执行计划中的Extra列显示了Using index condition,表示用到了索引下推的优化逻辑。3. 索引下推配置查看索引下推的配置:show variables like '%optimizer_switch%';如果输出结果中,显示 index_condition_pushdown=on,表示开启了索引下推。也可以手动开启索引下推:set optimizer_switch="index_condition_pushdown=on";关闭索引下推:set optimizer_switch="index_condition_pushdown=off";4. 索引下推原理剖析索引下推在底层到底是怎么实现的?是怎么减少了回表的次数?又减少了存储引擎和MySQL Server层的数据传输量?在没有使用索引下推的情况,查询过程是这样的:存储引擎根据where条件中name索引字段,找到符合条件的3个主键ID然后二次回表查询,根据这3个主键ID去主键索引上找到3个整行记录把数据返回给MySQL Server层,再根据where中age条件,筛选出符合要求的一行记录返回给客户端画两张图,就一目了然了。下面这张图是回表查询的过程:先在联合索引上找到name=‘一灯’的3个主键ID再根据查到3个主键ID,去主键索引上找到3行记录下面这张图是存储引擎返回给MySQL Server端的处理过程:我们再看一下在使用索引下推的情况,查询过程是这样的:存储引擎根据where条件中name索引字段,找到符合条件的3行记录,再用age条件筛选出符合条件一个主键ID然后二次回表查询,根据这一个主键ID去主键索引上找到该整行记录把数据返回给MySQL Server层返回给客户端现在是不是理解了索引下推的两个作用:减少回表查询的次数减少存储引擎和MySQL Server层的数据传输量5. 索引下推应用范围适用于InnoDB 引擎和 MyISAM 引擎的查询适用于执行计划是range, ref, eq_ref, ref_or_null的范围查询对于InnoDB表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB聚集索引,完整的记录已经读入InnoDB 缓冲区。在这种情况下使用索引下推 不会减少 I/O。子查询不能使用索引下推存储过程不能使用索引下推再附一张Explain执行计划详解图:
0
0
0
浏览量2055
后端求offer版

彻底搞懂Redis持久化机制,轻松应对工作面试

1. 为什么要持久化Redis是基于内存存储的数据库,如果遇到服务重启或者崩溃,内存中的数据将会被清空。所以为了确保数据安全性和可靠性,我们需要将内存中的数据持久化到磁盘上。持久化不仅可以防止由于系统故障、重启或者其他原因导致的数据丢失。还可以用于备份、数据恢复和迁移等操作。2. Redis持久化机制概述Redis提供了两种主要的持久化机制:RDB持久化和AOF持久化。此外,还可以采用混合持久化(RDB + AOF)的方式,将这两种持久化方式结合在一起。下面我们简要概述这些持久化机制。2.1 RDB持久化RDB(Redis DataBase)持久化是一种基于快照的持久化方式。在指定的时间间隔内,如果满足一定条件(如某段时间内发生的写操作次数),Redis会生成一个包含当前内存数据的RDB文件。这个RDB文件可以用于数据恢复或备份。RDB持久化提供了较高的数据压缩率和快速的数据加载速度,但可能存在一定程度的数据丢失。2.2 AOF持久化AOF(Append Only File)持久化是一种基于日志的持久化方式。Redis将所有的写操作命令记录到一个AOF文件中。当Redis重新启动时,可以通过重放AOF文件中的命令来恢复数据。AOF持久化提供了更高的数据安全性,可以保证数据的完整性。然而,与RDB持久化相比,AOF文件通常较大,数据加载速度较慢。2.3 混合持久化(RDB + AOF)混合持久化结合了RDB持久化和AOF持久化的优点,可以在保证数据安全性的同时,提供较快的数据加载速度。在这种持久化方式下,Redis会同时生成RDB文件和AOF文件。当Redis重新启动时,优先使用AOF文件恢复数据,以确保数据的完整性。混合持久化适用于对数据安全性和性能要求较高的场景。3. RDB持久化3.1 RDB持久化原理RDB持久化是基于快照的持久化,把当前时刻全量数据持久化到磁盘上,最终生成一个RBD文件。3.2 RDB持久化触发方式RDB持久化可以通过以下几种方式触发:手动触发:使用**SAVE或BGSAVE命令。SAVE是同步命令,执行过程中会阻塞其他请求。BGSAVE**是异步命令,主进程会forks一个子进程,进行异步持久化,持久化过程中主进程仍然可以处理其他请求。自动触发:在配置文件中设置触发条件,redis.conf配置如下:bash复制代码# 900s内至少有一次写操作 save 900 1 # 300s内至少有1次写操作 save 300 10 # 60s内至少有10000次写操作 save 60 10000关闭Redis时触发:Redis在关闭服务时会自动触发一次RDB持久化。主从同步时触发:当从节点连接到主节点时,主节点会触发一次RDB持久化,并将生成的RDB文件发送给从节点进行同步。3.3 RDB持久化优缺点RDB持久化具有以下优点:高性能:由于采用子进程进行磁盘操作,主进程无需进行磁盘IO,保证了Redis的高性能。快速恢复:RDB文件包含了某一时刻的完整数据快照,可以快速恢复数据。更小的存储空间:RDB文件经过压缩,占用较小的磁盘空间。RDB持久化的缺点包括:数据丢失:由于RDB持久化是基于时间间隔的,可能存在一定程度的数据丢失。子进程占用内存:在生成RDB文件过程中,子进程会占用和主进程相同的内存空间,可能导致内存不足的问题。4. AOF持久化4.1 AOF持久化原理AOF(Append Only File)持久化是一种基于日志的持久化方式。Redis将所有的写操作命令追加到一个AOF文件中。当Redis重新启动时,可以通过重放AOF文件中的命令来恢复数据。4.2 AOF持久化配置AOF持久化的配置主要包括以下几个方面:启用AOF持久化:在配置文件中设置appendonly yes。bash复制代码# 开启aof持久化 appendonly yes # aof文件名 appendfilename "appendonly.aof"AOF文件同步策略:在配置文件中设置**appendfsync**选项。可选值包括:always:每次写操作都同步到磁盘,保证最高的数据安全性,但性能较差。everysec:每秒同步一次磁盘,提供较好的数据安全性和性能平衡。no:由操作系统决定何时同步磁盘,性能最好,但数据安全性较差。bash复制代码# 持久化策略,always表示每次写入都进行持久化 appendfsync alwaysAOF重写策略:在redis.conf文件中进行配置,控制AOF重写的触发条件。bash复制代码# 指定在执行BGSAVE或BGREWRITEAOF命令时是否禁用AOF文件同步。默认为yes,表示禁用同步。 no-appendfsync-on-rewrite yes # 定AOF文件大小增长到原始大小的百分比时进行重写。 # 默认为100,表示AOF文件大小增长到原始大小的两倍时进行重写。 auto-aof-rewrite-percentage 100 # 指定进行AOF重写的最小AOF文件大小。默认为64mb。 auto-aof-rewrite-min-size 644.3 AOF重写(Rewrite)随着写操作的不断进行,AOF文件会不断增长。为了减小AOF文件的大小,Redis提供了AOF重写功能。AOF重写会创建一个新的AOF文件,只包含当前内存中数据的最小命令集。在重写过程中,Redis会继续将新的写操作追加到原始AOF文件中。当重写完成后,新的AOF文件将替换原始AOF文件。可以手动执行bgrewriteaof命令,触发AOF重写。redis> bgrewriteaof4.4 AOF持久化优缺点AOF持久化具有以下优点:更高的数据安全性:根据同步策略的选择,AOF持久化可以保证较高的数据安全性。更好的容错性:即使AOF文件存在部分损坏,仍可以恢复大部分数据。AOF持久化的缺点包括:较大的存储空间:与RDB持久化相比,AOF文件通常较大,占用较多磁盘空间。数据加载速度较慢:由于需要重放AOF文件中的命令,数据恢复速度相对较慢。5. 混合持久化RDB持久化加载速度快,AOF持久化数据更安全,有没有一种持久化方式结合两者的优点?当然有,就是混合持久化。5.1 混合持久化原理Redis首先使用RDB持久化将内存中的数据快照存储到磁盘上,然后再使用AOF持久化将所有新的写操作追加到AOF文件中。这样做的好处是:在系统崩溃时,可以通过RDB文件进行快速的恢复,而AOF文件可以用于恢复最近的修改。RDB持久化可以减少AOF文件的大小,从而减少磁盘空间的使用。在RDB持久化中,Redis可以使用子进程来将快照写入磁盘,这样可以避免主进程的阻塞。5.2 混合持久化优缺点混合持久化具有以下优点:高数据安全性:结合了AOF持久化的高数据安全性。快速恢复:利用RDB持久化的快速数据恢复速度。提高从节点同步效率:利用RDB文件进行快速同步。混合持久化的缺点包括:较大的存储空间:需要同时维护RDB文件和AOF文件,可能占用较多的磁盘空间。5.3 混合持久化应用场景混合持久化适用于对数据安全性和性能要求较高的场景,尤其是在以下情况:需要确保数据完整性,不能容忍数据丢失。需要快速恢复数据,以减少故障恢复时间。需要提高主从同步效率,以保证高可用性和负载均衡。6. 持久化方案选择6.1 持久化方案对比持久化方式RDBAOF原理通过定期生成数据快照实现持久化通过记录所有写操作命令实现持久化数据安全性可能会丢失最近一次快照以来的数据更高,可通过配置同步策略降低数据丢失风险恢复速度较快,因为RDB文件是一个数据快照较慢,需要逐条执行AOF文件中的命令存储空间一般较小,因为RDB文件经过压缩一般较大,但可以通过AOF重写减小文件大小性能影响较小,因为快照生成过程较短可能较大,但可通过配置同步策略降低性能影响主从同步使用RDB文件进行同步,同步速度较快使用AOF文件进行同步,同步速度可能较慢应用场景适用于对数据安全性要求较低、恢复速度要求较高的场景适用于对数据安全性要求较高、可接受较慢恢复速度的场景如果同时开启了RDB和AOF持久化,Redis优先使用AOF持久化,因为AOF持久化可以保证更高的数据安全性和灵活性,而RDB持久化适用于数据恢复的场景。6.2 持久化方案选择在选择Redis持久化方案时,需要根据实际业务需求和场景权衡各个方案的优缺点。数据安全性要求:如果你的业务对数据安全性要求较高,建议使用AOF持久化或混合持久化。AOF持久化可以通过设置同步策略来保证不同程度的数据安全性。数据恢复速度:如果你的业务需要快速恢复数据,以减少故障恢复时间,建议使用RDB持久化或混合持久化。RDB文件包含某一时刻的完整数据快照,可以快速恢复数据。存储空间考虑:如果磁盘空间有限,可以考虑使用RDB持久化,因为RDB文件经过压缩,占用较小的磁盘空间。然而,如果数据安全性要求较高,可以考虑使用混合持久化,尽管这会增加存储空间的占用。主从同步效率:如果你使用了Redis主从架构,需要考虑主从同步效率。混合持久化可以利用RDB文件进行快速同步,提高从节点的同步效率。性能考虑:RDB持久化和混合持久化可以在很大程度上保持Redis的高性能。如果选择AOF持久化,请选择合适的同步策略以平衡性能和数据安全性。7. 总结本文介绍了Redis的三种持久化机制:RDB持久化、AOF持久化和混合持久化。 RDB持久化通过定期生成数据快照实现持久化,具有快速恢复和更小的存储空间等优点,但可能存在数据丢失和子进程占用内存等缺点。 AOF持久化通过记录所有写操作命令实现持久化,具有更高的数据安全性和更好的容错性等优点,但可能存在较大的存储空间和数据加载速度较慢等缺点。 混合持久化结合了RDB持久化和AOF持久化的优点,适用于对数据安全性和性能要求较高的场景。 在选择Redis持久化方案时,需要根据实际业务需求和场景权衡各个方案的优缺点。
0
0
0
浏览量2070
后端求offer版

MySQL查询性能优化七种武器之索引潜水

有读者可能会一脸懵逼?啥是索引潜水?你给起的名字的吗?有没有索引蛙泳?这个名字还真不是我起的,今天要讲的知识点就叫索引潜水(Index dive) 。先要从一件怪事说起:我先造点数据复现一下问题,创建一张用户表:CREATE TABLE `user` (  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '姓名',  `age` int(11) NOT NULL DEFAULT 0 COMMENT '年龄',  PRIMARY KEY (`id`),  KEY `idx_age` (`age`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;通过一批用户年龄,查询该年龄的用户信息,并查看一下SQL执行计划:explain select * from user where age in (1,2,3,4,5,6,7,8,9);where条件中有9个参数,重点关注一下执行计划中的预估扫描行数为279行。到这里没什么问题,预估的非常准,实际就是279行。但是,问题来了,当我们在where条件中,再加一个参数,变成了10个参数,预估扫描行数本应该增加,结果却大大减少了。explain select * from user where age in (1,2,3,4,5,6,7,8,9,10);一下子减少到了30行,可是实际行数是多少呢?实际是310行,预估扫描行数是30行,真是错到姥姥家了。MySQL咋回事啊,到底还能不能预估?不能预估的话,换其他人!大家肯定也是满脸疑惑,直到我去官网上看到了一个词语,索引潜水(Index dive) 。跟这个词语相关的,还有一个配置参数 eq_range_index_dive_limit。MySQL5.7.3之前的版本,这个值默认是10,之后的版本,这个值默认是200。可以使用命令查看一下这个值的大小:show variables like '%eq_range_index_dive_limit%';当然,我们也可以手动修改这个值的大小:set eq_range_index_dive_limit=200;这个 eq_range_index_dive_limit 配置的作用就是:当where语句in条件中参数个数小于这个值的时候,MySQL就采用索引潜水(Index dive) 的方式预估扫描行数,非常准确。当where语句in条件中参数个数大于等于这个值的时候,MySQL就采用另一种方式索引统计(Index statistics) 预估扫描行数,误差较大。MySQL为什么要这么做呢?都用索引潜水(Index dive) 的方式预估扫描行数,不好吗?其实这是基于成本的考虑,索引潜水估算成本较高,适合小数据量。索引统计估算成本较低,适合大数据量。一般情况下,我们的where语句的in条件的参数不会太多,适合使用索引潜水预估扫描行数。建议还在使用MySQL5.7.3之前版本的同学们,手动修改一下索引潜水的配置参数,改成合适的数值。如果你们项目中in条件最多有500个参数,就把配置参数改成501。这样MySQL预估扫描行数更准确,可以选择更合适的索引。快去检查一下你们的线上配置吧!
0
0
0
浏览量2041
后端求offer版

阿里面试官:LinkedHashMap是怎么保证元素有序的?

引言新手程序员在使用HashMap的时候,会有个疑问,为什么存到HashMap中的数据不是有序的? 这其实跟HashMap的底层设计有关,HashMap并不是像ArrayList那样,按照元素的插入顺序存储。而是先计算key的哈希值,再用哈希值对数组长度求余,算出数组下标,存储到下标所在的位置,如果该位置上存在链表或者红黑树,再把这个元素插入到链表或者红黑树上面。 这样设计,可以实现快速查询,也就牺牲了存储顺序。因为不同key的哈希值差别很大,所以在数组中存储是无序的。 然而,有时候我们在遍历HashMap的时候,又希望按照元素插入顺序迭代,有没有什么方式能实现这个需求? 有的,就是今天的主角LinkedHashMap,不但保证了HashMap的性能,还实现了按照元素插入顺序或者访问顺序进行迭代。 在这篇文章中,你将学到以下内容:LinkedHashMap与HashMap区别?LinkedHashMap特点有哪些?LinkedHashMap底层实现原理?怎么使用``LinkedHashMap实现 LRU 缓存?简介LinkedHashMap继承自HashMap,是HashMap的子类,内部额外维护了一个双链表,来保证元素的插入顺序或访问顺序,用空间换时间。 与HashMap相比,LinkedHashMap有三个优点:维护了元素插入顺序,支持以元素插入顺序进行迭代。维护了元素的访问顺序,支持以元素访问顺序进行迭代。最近访问或者更新的元素,会被移动到链表末尾,类似于LRU(Least Recently Used,最近最少使用)。当面试的时候,手写LRU缓存,需要用到或者参考LinkedHashMap。迭代效率更高,迭代LinkedHashMap的时候,不需要遍历整个数组,只需遍历双链表即可,效率更高。类属性public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V> { /** * 头节点 */ transient Entry<K, V> head; /** * 尾节点 */ transient Entry<K, V> tail; /** * 迭代排序方式,true表示按照访问顺序,false表示按照插入顺序 */ final boolean accessOrder; /** * 双链表的节点类 */ static class Entry<K, V> extends HashMap.Node<K, V> { /** * 双链表的前驱节点和后继节点 */ Entry<K, V> before, after; /** * 构造双链表的节点 * * @param hash 哈希值 * @param key 键 * @param value 值 * @param next 后继节点 */ Entry(int hash, K key, V value, Node<K, V> next) { super(hash, key, value, next); } } }可以看出LinkedHashMap继承自HashMap,在HashMap的单链表Node节点的基础上,增加了前驱节点before、后继节点after、头节点head、尾节点tail,扩展成了双链表节点Entry,并记录了迭代排序方式accessOrder。初始化LinkedHashMap常见的初始化方法有四个方法:无参初始化指定容量大小的初始化指定容量大小、负载系数的初始化指定容量大小、负载系数、迭代顺序的初始化/** * 无参初始化 */ Map<Integer, Integer> map1 = new LinkedHashMap<>(); /** * 指定容量大小的初始化 */ Map<Integer, Integer> map2 = new LinkedHashMap<>(16); /** * 指定容量大小、负载系数的初始化 */ Map<Integer, Integer> map3 = new LinkedHashMap<>(16, 0.75f); /** * 指定容量大小、负载系数、迭代顺序的初始化 */ Map<Integer, Integer> map4 = new LinkedHashMap<>(16, 0.75f, true);再看一下构造方法的底层实现:/** * 无参初始化 */ public LinkedHashMap() { super(); accessOrder = false; } /** * 指定容量大小的初始化 */ public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } /** * 指定容量大小、负载系数的初始化 * * @param initialCapacity 初始容量 * @param loadFactor 负载系数 */ public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } /** * 指定容量大小、负载系数、迭代顺序的初始化 * * @param initialCapacity 初始容量 * @param loadFactor 负载系数 * @param accessOrder 迭代顺序,true表示按照访问顺序,false表示按照插入顺序 */ public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }LinkedHashMap的构造方法底层都是调用的HashMap的构造方法,迭代顺序accessOrder默认是false,表示按照元素插入顺序迭代,可以在初始化LinkedHashMap的时候指定为 true,表示按照访问顺序迭代。put源码LinkedHashMap的put方法完全使用的是HashMap的put方法,并没有重新实现。不过HashMap中定义了一些空方法,留给子类LinkedHashMap去实现。 有以下三个方法:public class HashMap<K, V> { /** * 在访问节点后执行的操作 */ void afterNodeAccess(Node<K, V> p) { } /** * 在插入节点后执行的操作 */ void afterNodeInsertion(boolean evict) { } /** * 在删除节点后执行的操作 */ void afterNodeRemoval(Node<K, V> p) { } }在HashMap的put源码中就调用前两个方法: 看一下afterNodeInsertion()方法的源码,看一下再插入节点后要执行哪些操作? 在插入节点后,只执行了一个操作,就是判断是否删除最旧的节点。removeEldestEntry()方法默认返回false,表示不需要删除节点。我们也可以重写removeEldestEntry()方法,当元素数量超过阈值时,返回true,表示删除最旧的节点。/** * 在插入节点后执行的操作(删除最旧的节点) */ void afterNodeInsertion(boolean evict) { Entry<K, V> first; // 判断是否需要删除当前节点 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; // 调用HashMap的删除节点的方法 removeNode(hash(key), key, null, false, true); } } /** * 是否删除最旧的节点,默认是false,表示不删除 */ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return false; }创建节点由于afterNodeInsertion()方法并没有把新节点插入到双链表中,所以LinkedHashMap又重写创建节点的newNode()方法,在newNode()方法中把新节点插入到双链表。public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V> { /** * 创建链表节点 */ @Override Node<K, V> newNode(int hash, K key, V value, Node<K, V> e) { // 1. 创建双链表节点 LinkedHashMap.Entry<K, V> p = new LinkedHashMap.Entry<K, V>(hash, key, value, e); // 2. 追加到链表末尾 linkNodeLast(p); return p; } /** * 创建红黑树节点 */ @Override TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) { // 1. 创建红黑树节点 TreeNode<K, V> p = new TreeNode<K, V>(hash, key, value, next); // 2. 追加到链表末尾 linkNodeLast(p); return p; } /** * 追加到链表末尾 */ private void linkNodeLast(LinkedHashMap.Entry<K, V> p) { LinkedHashMap.Entry<K, V> last = tail; tail = p; if (last == null) { head = p; } else { p.before = last; last.after = p; } } }get源码再看一下 get 方法源码,LinkedHashMap的 get 方法是直接调用的HashMap的get方法逻辑,在获取到value 后,判断 value 不为空,就执行afterNodeAccess()方法逻辑,把该节点移动到链表末尾,afterNodeAccess()方法逻辑在前面已经讲过。/** * get方法入口 */ public V get(Object key) { Node<K,V> e; // 直接调用HashMap的get方法源码 if ((e = getNode(hash(key), key)) == null) { return null; } // 如果value不为空,并且设置了accessOrder为true(表示迭代顺序为访问顺序),就执行访问节点后的操作 if (accessOrder) { afterNodeAccess(e); } return e.value; }看一下afterNodeAccess()方法的源码实现,看一下在访问节点要做哪些操作? afterNodeAccess()方法的逻辑也很简单,核心逻辑就是把当前节点移动到链表末尾,分为三步:断开当前节点与后继节点的连接断开当前节点与前驱节点的连接把当前节点插入到链表末尾/** * 在访问节点后执行的操作(把节点移动到链表末尾) */ void afterNodeAccess(Node<K, V> e) { Entry<K, V> last; // 当accessOrder为true时,表示按照访问顺序,这时候才需要更新链表 // 并且判断当前节点不是尾节点 if (accessOrder && (last = tail) != e) { Entry<K, V> p = (Entry<K, V>) e, b = p.before, a = p.after; // 1. 断开当前节点与后继节点的连接 p.after = null; if (b == null) { head = a; } else { b.after = a; } // 2. 断开当前节点与前驱节点的连接 if (a != null) { a.before = b; } else { last = b; } // 3. 把当前节点插入到链表末尾 if (last == null) { head = p; } else { p.before = last; last.after = p; } tail = p; ++modCount; } }remove源码LinkedHashMap的 remove 方法完全使用的是 HashMap 的 remove 方法,并没有重新实现。不过 HashMap的 remove 中调用了afterNodeRemoval�(),执行删除节点后逻辑,LinkedHashMap重写了该方法的逻辑。 /** * 在删除节点后执行的操作(从双链表中删除该节点) */ void afterNodeRemoval(Node<K, V> e) { LinkedHashMap.Entry<K, V> p = (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after; p.before = p.after = null; // 1. 断开当前节点与前驱节点的连接 if (b == null) { head = a; } else { b.after = a; } // 2. 断开当前节点与后继节点的连接 if (a == null) { tail = b; } else { a.before = b; } }总结现在可以回答文章开头提出的问题:    1.LinkedHashMap与HashMap区别?    答案:LinkedHashMap继承自HashMap,是HashMap的子类。    2.LinkedHashMap特点有哪些?    答案:除了保证了与HashMap一样高效的查询和插入性能外,还支持以插入顺序或者访问顺序进行迭代访问。    3.LinkedHashMap底层实现原理?    答案:LinkedHashMap底层源码都是使用了HashMap的逻辑实现,使用双链表维护元素的顺序,并重写了以下三个方法:afterNodeAccess(),在访问节点后执行的操作afterNodeInsertion(),在插入节点后执行的操作。afterNodeRemoval(),在删除节点后执行的操作。怎么使用``LinkedHashMap实现 LRU 缓存?    答案:由于LinkedHashMap内部已经实现按照访问元素的迭代顺序,所以只需复用LinkedHashMap的逻辑,继承LinkedHashMap,重写removeEldestEntry()方法。import java.util.LinkedHashMap; import java.util.Map; /** * @author 一灯架构 * @apiNote 使用LinkedHashMap实现LRU缓存 */ public class LRUCache<K, V> extends LinkedHashMap<K, V> { /** * 缓存容量大小 */ private final int capacity; /** * 构造方法 * * @param capacity 缓存容量大小 */ public LRUCache(int capacity) { // 底层使用LinkedHashMap的构造方法 super(capacity, 0.75f, true); this.capacity = capacity; } /** * 当缓存容量达到上限时,移除最久未使用的节点 */ @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "One"); cache.put(2, "Two"); cache.put(3, "Three"); System.out.println(cache); // 输出: {1=One, 2=Two, 3=Three} cache.get(2); System.out.println(cache); // 输出: {1=One, 3=Three, 2=Two} cache.put(4, "Four"); System.out.println(cache); // 输出: {3=Three, 2=Two, 4=Four} } }
0
0
0
浏览量2163
后端求offer版

MySQL到底有没有解决幻读问题?这篇文章彻底给你解答

MySQL InnoDB引擎在Repeatable Read(可重复读)隔离级别下,到底有没有解决幻读的问题?网上众说纷纭,有的说解决了,有的说没解决,甚至有些大v的意见都无法达成统一。今天就深入剖析一下,彻底解决这个幻读的问题。解决幻读问题之前,先普及几个知识点。1. 并发事务产生的问题先创建一张用户表,用作数据验证:CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) DEFAULT NULL COMMENT '姓名', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='用户表';并发事务会产生下面三个问题:脏读定义: 一个事务读到其他事务未提交的数据。从上面的示例图中,可以看出,在事务2修改完数据,没有提交的情况。事务1已经读到事务2最新修改的数据,这种情况就属于脏读。不可重复读定义: 一个事务读取到其他事务修改过的数据。从上面的示例图中,可以看出,在事务2修改完数据,并提交事务后。事务1第二次查询已经读到事务2最新修改的数据,这种情况就属于不可重复读。幻读定义: 一个事务读取到其他事务最新插入的数据。从上面的示例图中,可以看出,在事务2插入完数据,并提交事务后。事务1第二次查询已经读到事务2最新插入的数据,这种情况就属于幻读。2. 快照读和当前读再普及一下快照读和当前读。快照读: 读取数据的历史版本,不对数据加锁。例如:select当前读: 读取数据的最新版本,并对数据进行加锁。例如:insert、update、delete、select for update、select lock in share mode。3. 再谈幻读问题MySQL在Repeatable Read(可重复读)隔离级别下,到底有没有解决幻读的问题?只能说是部分解决了幻读问题。首先,在快照读的情况下,是通过MVCC(复用读视图) 解决了幻读问题。想详细了解MVCC和读视图,可以翻一下上篇文章。先手动设置一下MySQL的隔离级别为可重复读:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;执行测试用例,验证一下:从上面的示例图中,可以看出,事务1的两次查询,得到的结果一致,并没有查到事务2最新插入的数据。原因是,在可重复读隔离级别下,第一次快照读的时候,生成了一个读视图。第二次快照读的时候,复用了第一次生成的读视图,所以两次查询得到的结果一致。所以,在快照读的情况下,可重复读隔离级别是解决了幻读的问题。再测试一下,在当前读的情况下,可重复读隔离级别是否解决幻读问题:从上面的示例图中,可以看出,事务1的两次查询,得到的结果不一致。在事务2插入数据,并提交事务后。事务1的第二次执行当前读(加了for update) 的时候,读到了事务2最新插入的数据。原因是,在可重复读隔离级别下,每次执行当前读会生成一个新的读视图,所以能读到其他事务最新插入的数据。所以,在当前读的情况下,可重复读隔离级别是没有解决了幻读的问题。在执行上面的测试用例的时候,我忽然想到一个问题,既然select for update的当前读,出现了幻读问题,是不是其他的当前读也会复现幻读问题,比如insert。再执行测试用例,验证一下:跟预想的一样,在insert当前读的情况下,也出现了幻读的问题(主键冲突)。那有没有什么办法?在可重复读隔离级别下,执行当前读的时候,也能解决幻读的问题?当然有的,唯一的办法就是加锁。事务1在执行第一次查询的时候,就对数据进行加锁(使用for update),防止其他事务修改数据,这样也就彻底解决了幻读问题。你觉得有什么好办法吗?
0
0
0
浏览量2071

履历