面试题

怎么保证异步操作 在事务提交之后执行

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            System.out.println("send email after transaction commit...");
        }
    });

TransactionSynchronizatonManger.registerSynchorinization()的方法,传入一个TransactionSynchornizationAdapter抽象类的实现,实现他的 afterCommit钩子方法。
里面还有beforeCommit()方法

怎么保证多数据源 事务一致性

  • 基于XA协议的两阶段提交方案
    开源框架 atomikos
    常用的商用数据库都支持XA协议,有个全局协调者的概念,
    第一阶段是表决阶段,所有参与者都将本地事务是否能成功的信息反馈给协调者,
    第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者 你们要提交还是回滚。


    面试题_第1张图片
    XA

    缺点:两阶段提交方案锁定资源时间长,对性能影响很大,基本不适合解决微服务事务问题。

  • TCC方案
    TCC方案在电商、金融领域落地较多。TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。基本原理如下图所示。
    面试题_第2张图片
    tcc方案

事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。

TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面:

1.对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
2.实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。

上述原因导致TCC方案大多被研发实力较强、有迫切需求的大公司所采用

\color{red}{微服务倡导服务的轻量化、易部署,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。}

重点来了!!!

  • 基于消息的最终一致性方案
    消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
    面试题_第3张图片
    基于中间件实现的最终一致性方法

消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。

事务的隔离级别、传播机制

ACID
原子性:事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
隔离性:一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
一致性:事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
持久性:持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

谈到隔离级别,我们不妨先聊一下 事务并发的时候可能会出现的问题

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

  • 脏读: 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  • 不可重复度: 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  • 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

面试题_第4张图片
image.png

mysql的默认隔离级别是 可重复读

  • 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。


    面试题_第5张图片
    示例

我们来看看在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3的返回值分别是什么。

  • 若隔离级别是“读未提交”, V1 = 2, v2= 2,V3 = 2
  • 若隔离级别是“读提交”, V1 = 1,V2=2,V3 =2
  • 若隔离级别是“可重复读, V1 = 1 ,V2 = 1,V3 =2
  • 若隔离级别是“串行化”, V1 = 2,V2 = 1,V3=2

隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。

线程池七大参数含义

  • corePollSize 核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  • maximumPoolSize 最大线程数 表明线程中最多能够创建的线程数量。
  • keepAliveTime 空闲的线程保留的时间。
  • TimeUnit 保留的时间单位
  • BlockingQueue 阻塞队列,存储等待执行的任务,ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选
  • ThreadFactory:线程工厂,用来创建线程
  • RejectedExecutionHandler 拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

*newFixedThreadPool 定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待。
*newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
如果没有现有的线程可用,那么就创建新的线程并添加到池中,线程没有使用60秒的时间被终止并从线程池里移除缓存。
*newScheduledThreadPool (四该球的)创建一个定长任务线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor 创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。


    面试题_第6张图片
    image.png

索引失效的场景

  • 索引列上使用内置函数
  • 小表查询,数据量小
  • 隐式转换导致索引失效 吧字符类型当做数字传过来
  • 对索引列进行运算导致索引失效,我所指的对索引列进行运算包括(+,-,*,/,! 等)
  • %开头
  • NOT IN和<>操作
    NOT IN可以NOT EXISTS代替,id<>3则可使用id>3 or id<3来代替。
  • 索引不会包含有NULL值的列
    只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

哪些字段应该建立索引

  • 主键自动建立唯一索引;
  • 频繁作为查询条件的字段应该创建索引
  • 查询中与其它表关联的字段,外键关系建立索引(当然现在很多企业都是不让用外键的)
  • 单键/组合索引的选择问题, 组合索引性价比更高
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
  • 查询中统计或者分组字段

哪些字段不应该建立索引

  • 表记录太少
  • 经常增删改的表或者字段
  • where条件里用不着的不经常用的
  • 过滤性(离散型)不好的,例如性别

sql执行计划 参数含义

可以告诉我们,sql如何使用索引,查询的执行顺序,扫描的数据行数


面试题_第7张图片
explain
  • id 包含一组数字,表示执行select字句或操作表的顺序
  • select_type: 代表查询的类型,主要用来区分普通查询、联合查询、子查询和复杂查询,例如SIMPLE 简单查询、PRIMARY,如果有子查询,最外层查询标记为PRIMARY
  • table 哪张表
  • type 查询的访问类型、是比较重要的一个指标,保证最少 是 range级别,最好是ref,最坏是ALL全表扫
  • key 实际使用的索引,如果为NULL,不用索引
  • key_len 表示索引中使用的字节数。 key_len越长说明索引利用的越充分,


    面试题_第8张图片
    image.png
  • rows 执行查询时必须要检查的行数,越少越好

常用的设计模式以及描述

redis memche mongdb 比较

JUC里的核心类

  • ReentrantReadWriteLock 读写锁
  • CountDownLatch 线程计数器,发令枪,
    CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。
    其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞),当计数器的值变为 0 时, 因 await 方法阻塞的线程会被唤醒,继续执行。
  • CyclicBarrier 循环栅栏
面试题_第9张图片
image.png
面试题_第10张图片
image.png
  • 面试题_第11张图片
    image.png
面试题_第12张图片
image.png

AtomicInteger 以及相关原子类

Unsafe + CAS
更新的时候,传入预期值,和内存偏移量 根据内存拿到实际值,跟预期值不符就继续循环遍历 知道更新成功为止

举例说明: 就像我们git提交代码一样(提交之前不pull,本地版本跟远程仓库版本一致方可提交),我们总是乐观的认为在我之前没人改过,提交的时候发现 别人已经抢先提交,那我只能重新pull一下保持跟远程康库一致然后继续尝试提交,又倒霉的发现 有人抢先提交了,那我只能再pull一次 保持跟仓库一致然后继续提交。 提高CAS无锁算法的 效率 可以从减少循环次数 下文章,例如,我先等一会,再pull然后提交。

Synchroniz和读写锁原理

springboot的run方法里都干了啥、

面试题_第13张图片
image.png

public ConfigurableApplicationContext run(String... args) {
        //开启计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        //初始化监听器
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        //发布ApplicationStartingEvent
        listeners.starting();
 
        try {
            //装配参数和环境
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //发布ApplicationEnvironmentPreparedEvent
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            //创建ApplicationContext,并装配
            context = this.createApplicationContext();
            this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            //发布ApplicationPreparedEvent
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
            
            //发布ApplicationStartedEvent
            listeners.started(context);
            //执行Spring中@Bean下的一些操作,如静态方法等
            this.callRunners(context, applicationArguments);
        } catch (Throwable var9) {
            this.handleRunFailure(context, listeners, exceptionReporters, var9);
            throw new IllegalStateException(var9);
        }
 
        //发布ApplicationReadyEvent
        listeners.running(context);
        return context;

入口 SpringApplication.run(BeautyApplication.class, args);
首先记录整个Spring Application的加载时间!

1.初始化监听器
2.发布ApplicationStartingEvent
3.发布ApplicationEnvironmentPreparedEvent
4.打印banner
5.创建ApplicationContext,并装配
6.发布ApplicationPreparedEvent
7.refreshContext(context); //IOC容器加载过程 这里面的onRefresh() 创建了web容器
8.refreshContext(context);

  1. this.afterRefresh(context, applicationArguments);

springboot核心注解

@SpringBootApplication

@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 {

}

这玩意是个组合注解,主要包含以下几个

  • @SpringBootConfiguration 里面又包含了 @Configuration,可以让我们额外注册一些Bean,并导入一些额外的配置。 把一个类变成一个配置类,不需要额外的XML进行配置。
  • @EnableAutoConfiguration 官网上说让Spring去进行一些自动配置,由@AutoConfigurationPackage,@Import(EnableAutoConfigurationImportSelector.class),两个组合而成。
    @AutoConfigurationPackage: 让包中的类以及子包中的类能够被自动扫描到spring容器中。
    @Import(EnableAutoConfigurationImportSelector.class):这个是核心,我们说老自动配置,那他到底帮我们配置了什么,怎么配置的?
    EnableAutoConfigurationImportSelector这个类的作用就是 去META-INF/spring.factories目录下下的类并加入的程序中,
    面试题_第14张图片
    image.png

    我们可以发现帮我们配置了很多类的全路径,比如你想整合activemq,或者说Servlet
    并不是所有的类都给你创建好,而是根据程序进行决定
  • @ComponentScan: 扫描包,放入spring容器,那他在springboot当中做了什么策略呢?
    他帮我们做了一个策略,他在这里结合SpringBootConfiguration去使用,为什么是排除,因为不可能一上来全部加载,因为内存有限。
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

那么我们来总结下@SpringbootApplication:就是说,他已经把很多东西准备好,具体是否使用取决于我们的程序或者说配置,那我们到底用不用?那我们继续来看一行代码

public static void main(String[] args)
{
    SpringApplication.run(StartEurekaApplication.class, args);
}

那们来看下在执行run方法到底有没有用到哪些自动配置的东西,比如说内置的Tomcat,那我们来找找内置Tomcat,我们点进run

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return new SpringApplication(sources).run(args);
    }

然后他调用又一个run方法,我们点进来看

public ConfigurableApplicationContext run(String... args) {
   //计时器
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
   //监听器
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      Banner printedBanner = printBanner(environment);
      //准备上下文
      context = createApplicationContext();
      analyzers = new FailureAnalyzers(context);
         //预刷新context
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
     //刷新context
      refreshContext(context);
     //刷新之后的context
      afterRefresh(context, applicationArguments);
      listeners.finished(context, null);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
      return context;
   }
   catch (Throwable ex) {
      handleRunFailure(context, listeners, analyzers, ex);
      throw new IllegalStateException(ex);
   }
}

那我们关注的就是 refreshContext(context); 刷新context,我们点进来看

private void refreshContext(ConfigurableApplicationContext context) {
   refresh(context);
   if (this.registerShutdownHook) {
      try {
         context.registerShutdownHook();
      }
      catch (AccessControlException ex) {
         // Not allowed in some environments.
      }
   }
}

我们继续点进refresh(context);

protected void refresh(ApplicationContext applicationContext) {
   Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
   ((AbstractApplicationContext) applicationContext).refresh();
}

他会调用 ((AbstractApplicationContext) applicationContext).refresh();方法,我们点进来看

// 完成IoC容器的创建及初始化工作
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      //   1: 刷新前的准备工作。
      prepareRefresh();

       // 告诉子类刷新内部bean 工厂。
      //  2:创建IoC容器(DefaultListableBeanFactory),加载解析XML文件(最终存储到Document对象中)
      // 读取Document对象,并完成BeanDefinition的加载和注册工作
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      //  3: 对IoC容器进行一些预处理(设置一些公共属性)
      prepareBeanFactory(beanFactory);

      try {
         //  4:  允许在上下文子类中对bean工厂进行后处理。
         postProcessBeanFactory(beanFactory);

         //  5: 调用BeanFactoryPostProcessor后置处理器对BeanDefinition处理
         invokeBeanFactoryPostProcessors(beanFactory);

        //  6: 注册BeanPostProcessor后置处理器
         registerBeanPostProcessors(beanFactory);

         //  7: 初始化一些消息源(比如处理国际化的i18n等消息源)
         initMessageSource();

          //  8: 初始化应用事件多播器
         initApplicationEventMulticaster();

        //  9: 初始化一些特殊的bean   例如 tomcat  容器。。。
         onRefresh();

         // 10: 注册一些监听器      用户自己写的监听器
         registerListeners();

         //  11: 实例化剩余的单例bean(非懒加载方式)
      //      注意事项:Bean的IoC、DI和AOP都是发生在此步骤
         finishBeanFactoryInitialization(beanFactory);

         //12: 完成刷新时,需要发布对应的事件
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         //  销毁已经创建的单例避免占用资源
         destroyBeans();

         //  重置'active' 标签。
         cancelRefresh(ex);

         //传播异常给调用者
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

这点代码似曾相识啊 没错,就是一个spring的bean的加载过程我在,解析springIOC加载过程的时候介绍过这里面的方法,如果你看过Spring源码的话 ,应该知道这些方法都是做什么的。现在我们不关心其他的,我们来看一个方法叫做 onRefresh()方法

protected void onRefresh() throws BeansException {
   // For subclasses: do nothing by default.
}
image.png

我们既然要找Tomcat那就肯定跟web有关,我们可以看到有个ServletWebServerApplicationContext

@Override
protected void onRefresh() {
   super.onRefresh();
   try {
      createWebServer();
   }
   catch (Throwable ex) {
      throw new ApplicationContextException("Unable to start web server", ex);
   }
}

我们可以看到有一个createWebServer();方法他是创建web容器的,而Tomcat不就是web容器,那他是怎么创建的呢,我们继续看

private void createWebServer() {
   WebServer webServer = this.webServer;
   ServletContext servletContext = getServletContext();
   if (webServer == null && servletContext == null) {
      ServletWebServerFactory factory = getWebServerFactory();
      this.webServer = factory.getWebServer(getSelfInitializer());
   }
   else if (servletContext != null) {
      try {
         getSelfInitializer().onStartup(servletContext);
      }
      catch (ServletException ex) {
         throw new ApplicationContextException("Cannot initialize servlet context",
               ex);
      }
   }
   initPropertySources();
}

factory.getWebServer(getSelfInitializer());他是通过工厂的方式创建的

public interface ServletWebServerFactory {

   WebServer getWebServer(ServletContextInitializer... initializers);

}

可以看到 它是一个接口,为什么会是接口。因为我们不止是Tomcat一种web容器。


image.png

我们看到还有Jetty,那我们来看TomcatServletWebServerFactory

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
   Tomcat tomcat = new Tomcat();
   File baseDir = (this.baseDirectory != null) ? this.baseDirectory
         : createTempDir("tomcat");
   tomcat.setBaseDir(baseDir.getAbsolutePath());
   Connector connector = new Connector(this.protocol);
   tomcat.getService().addConnector(connector);
   customizeConnector(connector);
   tomcat.setConnector(connector);
   tomcat.getHost().setAutoDeploy(false);
   configureEngine(tomcat.getEngine());
   for (Connector additionalConnector : this.additionalTomcatConnectors) {
      tomcat.getService().addConnector(additionalConnector);
   }
   prepareContext(tomcat.getHost(), initializers);
   return getTomcatWebServer(tomcat);
}

那这块代码,就是我们要寻找的内置Tomcat,在这个过程当中,我们可以看到创建Tomcat的一个流程。因为run方法里面加载的东西很多,所以今天就浅谈到这里。如果不明白的话, 我们在用另一种方式来理解下,

https://github.com/zgw1469039806/gwspringbootsrater

spring核心类

zk场景

*dubbo的服务注册发布(自动),公司自研的rpc服务管理平台(手动)

  • 配置中心 改配置不需要机器 上线,相关程序对配置中心 进行节点监控,一旦配置中心数据发生变化,应用就会收到zk的通知
  • 分布式锁

zk通知机制 选举机制

  • 通知机制
    ZooKeeper 支持 watch(观察)的概念,客户端可以在每个 znode 结点上设置一个观察。如果被观察服务端的 znode结点有变更,那么 watch 就会被触发,这个 watch 所属的客户端将接收到一个通知包被告知结点已经发生变化,把 相应的事件通知给设置过 Watcher 的 Client 端。
    异步回调的触发机制

一次触发,触发一次就失效,想继续监听,需要客户端重新设置 Watcher。因此如果你得到一个 watch 事件且想在 将来的变化得到通知,必须新设置另一个 watch。

JVM运行时数据区

面试题_第15张图片
image.png
  • 程序计数器 指向当前线程所执行的字节码的行号,线程切换还能知道在哪个位置继续执行,分支,循环,跳转,异常处理都通过它完成

  • 虚拟机栈 每个方法的执行都对应这虚拟机栈的一个栈帧的入栈 出栈操作,存储存储局部变量表,操作数栈,动态链接,方法出口等信息

  • 本地方法栈 本地方法栈和虚拟机栈相同,里面的方法都是 native方法
    *堆
    堆是JVM里最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,是垃圾收集的主要区域
    存放对象实例,数组,

  • 方法区 也是线程共享的, 存储已被虚拟机加载的类信息,常量(final),静态变量(static)
    运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,

双亲委派,沙箱安全

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成
> 1.避免重复加载类 2.考虑安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类。。。防止核心API被篡改

类加载

加载 – 连接 – 初始化 – 使用 – 卸载
源代码(.java文件)经过编译后会变成字节码(.class文件)
类加载,指的是将类的.class文件中的二进制数据读入到内存中,把它放进运行时数据区的方法区内(Perm区)。
然后在堆区创建一个java.lang.Class对象,封装这个类在自身的方法区内的数据结构。
注意,这是仍旧没有生成针对该类的对象。后续对类的实例化,会使用堆内存中的Class对象生成具体的实例对象。

确定垃圾

  • 引用计数法
    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。该方法实现简单,效率高,但是它很难它很难解决对象之间相互循环引用的问题。
  • GCRoot可达性分析
    这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。


    面试题_第16张图片
    GC ROOT

哪些对象可以视为GC ROOT

  • 1 虚拟机栈中引用的对象
  • 2 方法区中静态属性、常量引用的对象
  • 3 Native方法引用的对象

GC过程

  • Minor GC
    大多数情况,对象出生在Eden,当Eden区没有足够空间的时候,触发一次Minor GC
    新生代GC,大多数java对象朝生夕灭,Minor GC 比较频繁,速度也比较快
  • Full GC

对象何时进入老年代

  • 新建对象 无法在新生代的幸存区存储
  • 垃圾回收超过15次 Eden中的对象经历一次minor GC ,如果还存活的话,进入s1,在s1中熬过一次 minor GC,进入s2
  • 大对象

调优参数

调优工具

  • 内存溢出定位工具——MAT
    面试题_第17张图片
    image.png

    MAT下载地址
面试题_第18张图片
image.png

线上问题排查

1.通过top命令查看当前机器的CPU使用情况,看看是哪个进程CPU高

  1. ps-ef或者jps 进一步定位,得知是怎么样一个后台程序惹事了
  2. 定位到具体的线程 ps-mp 进程id -o THREAD,tid,time
  3. 将有问题IDE线程ID转换为16进制格式 printf "%x/n" 有问题的线程ID
  4. jstack 进程ID | grep 16进制线程id 查看线程堆栈,看看带公司代码的那几行

为什么产生死锁,如何避免?

是指多个线程在运行过程中因争夺资源而造成的一种僵局。

  • 原因
    举例: 线程1按照先获得锁A,再去获得锁B的顺序执行,在此同时又一有一个锁线程2,按照先获取锁B再获得锁A的顺序执行,线程1获得了锁A,此时线程2获得了锁B,他们都在互相等待对方释放锁,因此两人都卡在那,造成死锁。
  • 预防
    1.以确定的顺序获得锁
    首先我们要尽量避免多个线程去竞争多个锁的情况,如果避免不了,在设计的时候要充分 考虑不同线程之间获取锁的顺序。
  1. 超时放弃
    当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
  • 死锁检测
    1.Jstack命令
    打印出给定的java进程ID的Java堆栈信息, Jstack工具可以用于生成java虚拟机当前时刻的线程快照。。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

2.JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。


类加载

是java程序"一次编译,到处运行"的关键,java程序编译时,不是直接编译成目标机器的机器码,而是编译成.class的二级制的字节码文件,再由目标机器上的JVM虚拟机把.class文件翻译为对应机器的机器码执行.

"加载"就是将.class文件读到内存中

1.加载2.验证3.准备4.解析5.初始化 6.使用
验证 准备 解析 又成为连接
1.加载2.连接。3初始化

  • 加载
    加载主要是将.class文件读取到内存中,主要做三件事:1.通过类的全限定名获取该类的二进制字节流; 2.将字节流所代表的静态存储结构转化为方法区的运行时数据结构;3.在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  • 验证
    验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。
    1.文件格式验证
    2.元数据验证(是否符合Java语言规范)
    3.字节码验证(确定程序语义合法,符合逻辑)
    4.符号引用验证(确保下一步的解析能正常执行)

  • 准备
    主要为静态变量在方法区分配内存,并设置默认初始值。

  • 解析
    解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 初始化
    类加载过程中最后一步,主要是根据程序中的赋值语句主动为类变量赋值。
    注:
    1.当有父类且父类为初始化的时候,先初始化父类
    2.再进行初始化语句
什么时候需要对类进行初始化?

1.使用new该类实例化对象的时候;
2.读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final);
3.调用类静态方法的时候
4.使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化;
5.初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
6.被标明为启动类的类(即包含main()方法的类)要初始化;


java反射获取私有属性,改变值

Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
age.set(wdc,66);
或者获取所有的属性,去遍历


数据库乐观锁使用

增加一个数字类型的 “version”,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

update task set value = newValue, version = versionValue + 1 where version = versionValue;

如何开启慢查询日志

slow_query_log 设置为ON
设置满查询日志的存放位置
set global slow_query_log_file='/var/lib/mysql/test-10-226-slow.log';
设置超过几秒就记录
set global long_query_time=1;

如何设计一个线程安全的HashMap

1.将所有public方法都加上synchronized: 相当于设置了一把全局锁,所有操作都需要先获取锁(比如直接使用Collections.synchronizedMap()方法对其进行加锁操作),java.util.HashTable就是这么做的,性能很低

  1. 由于每个桶在逻辑上是相互独立的,将每个桶都加一把锁,如果两个线程各自访问不同的桶,就不需要争抢同一把锁了。这个方案的并发性比单个全局锁的性能要好,不过锁的个数太多,也有很大的开销。
    3.锁分离(Lock Stripping)技术:第2个方法把锁的压力分散到了多个桶,理论上是可行的的,但是假设有1万个桶,就要新建1万个ReentrantLock实例,开销很大。可以将所有的桶均匀的划分为16个部分,每一部分成为一个段(Segment),每个段上有一把锁,这样锁的数量就降低到了16个,JDK 7里的java.util.concurrent.ConcurrentHashMap就是这个思路。
    4.在JDK8里,ConcurrentHashMap的实现又有了很大变化,它在锁分离的基础上,大量利用了了CAS指令。并且底层存储有一个小优化,当链表长度太长(默认超过8)时,链表就转换为红黑树。链表太长时,增删查改的效率比较低,改为红黑树可以提高性能

快排基本思想

1.先从数列中取出一个数作为基准数
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3.再对左右区间重复第二步,直到各区间只有一个数

垃圾回收机制


项目中查看垃圾回收


synchronized 修饰静态 方法和普通方法的区别,获取类锁之后还能获取对象锁吗?

synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系
修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。


如何将数据分布在不同的redis

redis3.0之后哈西槽
一致性哈希算法


springAOP的实现


浏览器输入网址全过程,结合springmvc


数据库的默认隔离级别,一定会产生幻读吗?怎么解决?


负载均衡算法


SpringBean的默认范围


如何确保多台机器不重复消费


paxos算法


springMVC流程

面试题_第19张图片
image.png

面试题_第20张图片
image.png

第一步:发起请求到前端控制器(DispatcherServlet)

第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)

第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略

第四步:前端控制器调用处理器适配器去执行Handler

第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler

第六步:Handler执行完成给适配器返回ModelAndView

第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)

第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可

第九步:视图解析器向前端控制器返回View

第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)

第十一步:前端控制器向用户响应结果


左连接右连接的区别

左连接是返回主表的所有信息,从表只返回满足条件的
右连接 是返回主表满足条件的信息,从表全部返回
内连接 只满足返回条件的


JVM有哪些垃圾回收器,如何查看默认垃圾回收器,怎么配置垃圾回收器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器那么垃圾收集器就是内存回收的具体实现。
GC回收算法是内存回收的方法论,总要有落地的实现,
4种主要的垃圾收集器:Serial(塞锐呕)串行回收、parallel(拍锐咯)并行回收、CMS并发、G1

  • 串行垃圾回收器
    为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。 用餐-打扫卫生-用餐。 用餐被打断

  • 并行垃圾回收
    多个垃圾收集线程并行工作,此时用户线程也是暂停的,适用于科学计算等弱交互场景,跟前台的交互不是特别强,允许稍微停一下。 多个清洁工 打扫卫生, 打扫的快一点,但是客户还是要暂停用餐。

  • 并发垃圾回收器
    生产环境,程序是不能停的。 用户线程和辣鸡收集线程可以同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司多用它,适用于对响应时间有要求的场景。(前台程序不能停)
    边打扫边吃饭。

面试题_第21张图片
image.png

1.8之前主要是这三种垃圾回收器

  • G1垃圾回收器(1.8 )
    将堆内存分割成不同的区域然后并发的对其进行垃圾回收


    面试题_第22张图片
    image.png

1.8可以用G1了, 1.8默认是并行垃圾回收,1.9默认开始是G1 java11 是更高的版本 ZGC

怎么查看默认的垃圾回收器? 生产上如何配置垃圾收集器? 谈谈对垃圾收集器的理解?

串行回收 -xx:+userSerialGC
并行回收 -xx:+UserParallelGC
并发回收 CMS(ConcMarkSweep)
G1
查看默认的垃圾回收


面试题_第23张图片
image.png

-xx:+printCommandLineFlags


面试题_第24张图片
image.png

如何认为修改默认垃圾回收

========================
七种垃圾回收(实际用六种,有一种废弃了,底层源码目前六种)

  1. Serial 收集器

Serial收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是 它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。 它会在用户不可见的情况下把用户正常工作的线程全部停掉。想象一下,当你结束一天的工作回到家中,喝着冰阔乐刷着副本正要打Boss,突然你的电脑说他要休息5分钟,你会是什么感觉?
存在即合理,当然Serial 收集器也有优于其他垃圾收集器的地方,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
它的 新生代采用复制算法,老年代采用标记整理算法。

  1. ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为(控制参数、收集算法、分配规则、回收策略等等)和 Serial 收集器完全一样。
除了支持多线程收集,ParNew 相对 Serial 似乎并没有太多改进的地方。但是它却是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。ParNew单核状态下不如Serial,多核线程下才有优势。
新生代采用复制算法,老年代采用标记整理算法。

  1. Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,也是采用复制算法+并行。听起来和ParNew差不多对不对,那么它有什么特别之处呢?
Parallel Scavenge 收集器关注点是吞吐量(CPU运行代码的时间与CPU总消耗时间的比值)。 而CMS 等垃圾收集器的关注点更多的是缩短用户线程的停顿时间(提高用户体验)。停顿时间越短就越适合和用户进行交互(响应速度快,可以优化用户体验),而高吞吐量则可以高效的利用CPU时间,尽快完成用户的计算任务。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

  1. Serial Old 收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
新生代采用复制算法,老年代采用标记整理算法。

  1. Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

  1. CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常重视服务的响应速度,以期给用户最好的体验。。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。
CMS一款优秀的垃圾收集器,
主要优点:并发收集、低停顿。
但是它有下面三个明显的缺点:
1.对 CPU 资源敏感;
2.无法处理浮动垃圾;

  1. 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间 碎片产生。
  1. G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,开发人员希望在未来可以换掉CMS收集器,它有如下特点
 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。这就意味着不会产生大量的内存碎片
 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

面试题_第25张图片
image.png
面试题_第26张图片
jvm源码

面试题_第27张图片
image.png

垃圾回收链接

insert a,b,c value

线上排查问题

生产环境变慢,诊断思路,性能评估
整机: top
cpu: vmstat
内存: free
磁盘: df -h
硬盘IO:
网络IO:
以上都会导致性能变慢


面试题_第28张图片
top

右上角,load average 有三个参数,五分钟负载,十分钟负载,十五分钟负载,这三个值相加 除以3 乘 百分百,如果高于百分之 60 就说明负载很高 或者 uptime 只看 load average

    1. cpu 高问题
      先用top命令找出cpu占比最高的线程
      ps -ef 或者 jps进一步定位,得知是怎么样一个后台程序给我们惹事了
      定位到具体的线程或者代码 ps-mp进程 id -o THREAD ,tid,time 定位到具体哪个线程出问题
      将有问题的线程ID转换为16进制格式 printf "%x\n" 有问题线程ID
      jstack 进程ID | grep tid(16)-A60 看看线程的信息, 不管他们的堆栈,就看哪有公司的名字。。。 就是这个代码惹的货
    1. 内存高问题
    1. 如何分析dump(堆的内存镜像)
      1.设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。
      2.jmap dump整个堆, jmap -dump:format=b,file=jmap.info PID
      工具: mat(eclipse) jhat(jdk自带的)


      面试题_第29张图片
      image.png
    1. 经常用哪些分析工具

jinfo java配置相关
jmap 内存映像工具
jstat 统计信息监视

*常用调优参数
boolean 类型
-XX:[+-] 表示是否启用jvm的某个参数
非boolean类型
XX: = 表示name属性的值为value
Xms:初始堆内存大小(-XX:initialHeapSize)
-Xmx:最大堆内存大小(-XX:MaxHeapSize)
-XX:NewRatio 新生代老年代之间的比例
-XX:MetaSpaceSize MetaSpace大小
打印参数相关的
-XX:+PrintGCDetails 显示gc详细信息
-XX:+PrintHeapAtGC 发生gc时打印出堆栈信息
-XX:+PrintTenuringDistribution 打印出对象的分布情况

gc常用参数

jps:查看java的相关进程
jinfo:产看正在运行的jvm的参数
jstat:查看jvm的统计信息(包括类加载信息,垃圾收集信息,jit编译信息)

  • 如何选择垃圾回收器
    1.优先调整堆的大小,让jvm自己选择
    2.内存小于100M,使用串行垃圾收集器
    3.单核没有停顿时间的要求,使用串行,或者jvm自己选择
    4.允许停顿时间超过1s 选择并行或者jvm自己选择
    5.若响应时间重要,使用并发收集器

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

设定堆内存大小-Xmx:堆内存最大限制。设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代-XX:NewSize:新生代大小-XX:NewRatio 新生代和老生代占比-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了。
-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。
-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。

什么是堆外内存

JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。
对内 内存 就是我们常说的 堆 新生代+老年代

内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。
优点:
1.减少了垃圾回收,使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

  1. 提升复制速度(io效率)
    堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。

http 和 hppts关系

http是超文本传输协议,信息是明文传输(内容可能被窃听),https则是具有安全协议的ssl加密传输协议。
https协议需要到ca申请证书,一般免费的证书很少,需要交费。 https更安全
http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443.
http + 加密 + 认证 + 完整性保护 = https

你可能感兴趣的:(面试题)