diff --git "a/Dubbo/Dubbo351円235円242円350円257円225円351円242円230円.md" "b/Dubbo/Dubbo351円235円242円350円257円225円351円242円230円.md" new file mode 100644 index 0000000..88f3748 --- /dev/null +++ "b/Dubbo/Dubbo351円235円242円350円257円225円351円242円230円.md" @@ -0,0 +1,182 @@ +## 1.Dubbo是什么? + +Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC 分布式服务框架,现已成为 Apache 基金会孵化项目。 + +其核心部分包含: + +* 集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 +* 远程通讯:提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及"请求-响应"模式的信息交换方式。 +* 自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 + +## 2. Dubbo和 Spring Cloud 有什么区别? + +最大的区别: + +- Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信; +- 而SpringCloud是基于Http协议+rest接口调用远程过程的通信,相对来说,Http请求会有更大的报文,占的带宽也会更多。但是REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。 + +模块区别: + +* Dubbo主要分为服务注册中心,服务提供者,服务消费者,还有管控中心; + +* 相比起Dubbo简单的四个模块,SpringCloud则是一个完整的分布式一站式框架,他有着一样的服务注册中心,服务提供者,服务消费者,管控台,断路器,分布式配置服务,消息总线,以及服务追踪等; + +## 3. Dubbo核心组件有哪些? + + + +![image-20210829190835070](http://blog-img.coolsen.cn/img/image-20210829190835070.png) + + + +- Provider:暴露服务的服务提供方 +- Consumer:调用远程服务消费方 +- Registry:服务注册与发现注册中心 +- Monitor:监控中心和访问调用统计 +- Container:服务运行容器 + +## 4. Dubbo都支持什么协议,推荐用哪种? + +1、 Dubbo协议:Dubbo默认使用Dubbo协议。 + +* 适合大并发小数据量的服务调用,以及服务消费者远大于提供者的情况 +* Hessian二进制序列化。 +* 缺点是不适合传送大数据包的服务。 + +2、rmi协议:采用JDK标准的rmi协议实现,传输参数和返回参数对象需要实现Serializable接口。使用java标准序列化机制,使用阻塞式短连接,传输数据包不限,消费者和提供者个数相当。 + +* 多个短连接,TCP协议传输,同步传输,适用常规的远程服务调用和rmi互操作 +* 缺点:在依赖低版本的Common-Collections包,java反序列化存在安全漏洞,需升级commons-collections3 到3.2.2版本或commons-collections4到4.1版本。 + +3、 webservice协议:基于WebService的远程调用协议(Apache CXF的frontend-simple和transports-http)实现,提供和原生WebService的互操作多个短连接,基于HTTP传输,同步传输,适用系统集成和跨语言调用。 + +4、http协议:基于Http表单提交的远程调用协议,使用Spring的HttpInvoke实现。对传输数据包不限,传入参数大小混合,提供者个数多于消费者 + +* 缺点是不支持传文件,只适用于同时给应用程序和浏览器JS调用 + +5、hessian:集成Hessian服务,基于底层Http通讯,采用Servlet暴露服务,Dubbo内嵌Jetty作为服务器实现,可与Hession服务互操作 +通讯效率高于WebService和Java自带的序列化 + +* 适用于传输大数据包(可传文件),提供者比消费者个数多,提供者压力较大 + +* 缺点是参数及返回值需实现Serializable接口,自定义实现List、Map、Number、Date、Calendar等接口 + +6、thrift协议:对thrift原生协议的扩展添加了额外的头信息。使用较少,不支持传null值 + +7、memcache:基于memcached实现的RPC协议 + +8、redis:基于redis实现的RPC协议 + +## 5. Dubbo服务器注册与发现的流程? + +- 服务容器Container负责启动,加载,运行服务提供者。 +- 服务提供者Provider在启动时,向注册中心注册自己提供的服务。 +- 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。 +- 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +- 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +- 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。 + +## 6. Dubbo内置了哪几种服务容器? + +三种服务容器: + +* Spring Container +* Jetty Container +* Log4j Container + +Dubbo的服务容器只是一个简单的 Main 方法,并加载一个简单的 Spring 容器,用于暴露服务。 + +## 7. Dubbo负载均衡的作用? + + 将负载均衡功能实现在rpc客户端侧,以便能够随时适应外部的环境变化,更好地发挥硬件作用。而且客户端的负载均衡天然地就避免了单点问题。定制化的自有定制化的优势和劣势。 + +它可以从配置文件中指定,也可以在管理后台进行配置修改。 + +事实上,它支持 服务端服务/方法级别、客户端服务/方法级别 的负载均衡配置。 + +## 8. Dubbo有哪几种负载均衡策略,默认是哪种? + +Dubbo提供了4种负载均衡实现: + +1. RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。 +2. RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。 +3. LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 +4. ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。 + +## 9. Dubbo服务之间的调用是阻塞的吗? + +默认是同步等待结果阻塞的,支持异步调用。 + +Dubbo是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 + +## 10. DubboMonitor 实现原理? + +Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 + +1. MonitorFilter 向 DubboMonitor 发送数据 +2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference +3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) +4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) +5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 + +## 11. Dubbo有哪些注册中心? + +- Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。 +- Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。 +- Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。 +- Simple 注册中心。 +- 推荐使用 Zookeeper 作为注册中心 + +## 12. Dubbo的集群容错方案有哪些? + +- Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。 +- Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +- Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +- Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +- Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2′′ 来设置最大并行数。 +- Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +## 13. Dubbo超时设置有哪些方式? + +Dubbo超时设置有两种方式: + +- 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。 +- 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。 + +## 14. Dubbo用到哪些设计模式? + +1、**工厂模式** + +Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +``` +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 +``` + +Dubbo里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 + +2、**装饰器模式** + +Dubbo在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: + +``` +EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> +ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> +ExceptionFilter +复制代码 +``` + +更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 + +3、**观察者模式** + +Dubbo的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 + +4、**动态代理模式** + +Dubbo扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 + + diff --git a/JVM/JVM.md b/JVM/JVM.md index ddccaf8..e6ef355 100644 --- a/JVM/JVM.md +++ b/JVM/JVM.md @@ -1,3 +1,4 @@ +# JVM 常考面试题 ## 1. 什么是JVM内存结构? ![](http://blog-img.coolsen.cn/img/image-20210220111553294.png) @@ -367,7 +368,7 @@ Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8 - JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。 -## 21.说一下 JVM 调优的命令? +## 21. 说一下 JVM 调优的命令? * jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 * jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 @@ -375,21 +376,409 @@ Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。 * jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。 * jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。 -## Java对象创建过程 +## 22. Java对象创建过程 1. JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲) 2. 为对象分配内存。一种办法"指针碰撞"、一种办法"空闲列表",最终常用的办法"本地线程缓冲分配(TLAB)" 3. 将除对象头外的对象内存空间初始化为0 4. 对对象头进行必要设置 -## 巨人的肩膀 +## 23. JDK新特性 -https://jishuin.proginn.com/p/763bfbd35094 +**JDK8** -https://www.javanav.com/val/93550f179edb4a77bbf4d35faa6d560c.html +支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能 -https://juejin.cn/post/6844903941805703181 +**JDK9** -https://www.cnblogs.com/chiangchou/p/jvm-2.html +```java +//Stream API中iterate方法的新重载方法,可以指定什么时候结束迭代 +IntStream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println); +``` -https://juejin.cn/post/6844903887866953735 +默认G1垃圾回收器 + +**JDK10** + +其重点在于通过完全GC并行来改善G1最坏情况的等待时间。 + +**JDK11** + +ZGC (并发回收的策略) 4TB + +用于 Lambda 参数的局部变量语法 + +**JDK12** + +Shenandoah GC (GC 算法)停顿时间和堆的大小没有任何关系,并行关注停顿响应时间。 + +**JDK13** + +增加ZGC以将未使用的堆内存返回给操作系统,16TB + +**JDK14** + +删除cms垃圾回收器、弃用ParallelScavenge+SerialOldGC垃圾回收算法组合 + +将ZGC垃圾回收器应用到macOS和windows平台 + + + + + +# 线上故障排查 + +## 1、硬件故障排查 + +如果一个实例发生了问题,根据情况选择,要不要着急去重启。如果出现的CPU、内存飙高或者日志里出现了OOM异常 + +**第一步是隔离**,第二步是**保留现场**,第三步才是**问题排查**。 + +**隔离** + +就是把你的这台机器从请求列表里摘除,比如把 nginx 相关的权重设成零。 + +**现场保留** + +**瞬时态和历史态** + +![img](https://tva1.sinaimg.cn/large/008eGmZEly1gobnwy22d2j30l10cpt9d.jpg) + +查看比如 CPU、系统内存等,通过历史状态可以体现一个趋势性问题,而这些信息的获取一般依靠监控系统的协作。 + +**保留信息** + +(1)**系统当前网络连接** + +``` +ss -antp> $DUMP_DIR/ss.dump 2>&1 +``` + + +使用 ss 命令而不是 netstat 的原因,是因为 netstat 在网络连接非常多的情况下,执行非常缓慢。 + +后续的处理,可通过查看各种网络连接状态的梳理,来排查 TIME_WAIT 或者 CLOSE_WAIT,或者其他连接过高的问题,非常有用。 + +(2)**网络状态统计** + +```java +netstat -s> $DUMP_DIR/netstat-s.dump 2>&1 +``` + + +它能够按照各个协议进行统计输出,对把握当时整个网络状态,有非常大的作用。 + +```java +sar -n DEV 1 2> $DUMP_DIR/sar-traffic.dump 2>&1 +``` + + +在一些速度非常高的模块上,比如 Redis、Kafka,就经常发生跑满网卡的情况。表现形式就是网络通信非常缓慢。 + +(3)**进程资源** + +```java +lsof -p $PID> $DUMP_DIR/lsof-$PID.dump +``` + + +通过查看进程,能看到打开了哪些文件,可以以进程的维度来查看整个资源的使用情况,包括每条网络连接、每个打开的文件句柄。同时,也可以很容易的看到连接到了哪些服务器、使用了哪些资源。这个命令在资源非常多的情况下,输出稍慢,请耐心等待。 + +(4)**CPU 资源** + +``` +mpstat> $DUMP_DIR/mpstat.dump 2>&1 +vmstat 1 3> $DUMP_DIR/vmstat.dump 2>&1 +sar -p ALL> $DUMP_DIR/sar-cpu.dump 2>&1 +uptime> $DUMP_DIR/uptime.dump 2>&1 +``` + +主要用于输出当前系统的 CPU 和负载,便于事后排查。 + +(5)**I/O 资源** + +```java +iostat -x> $DUMP_DIR/iostat.dump 2>&1 +``` + + +一般,以计算为主的服务节点,I/O 资源会比较正常,但有时也会发生问题,比如**日志输出过多,或者磁盘问题**等。此命令可以输出每块磁盘的基本性能信息,用来排查 I/O 问题。在第 8 课时介绍的 GC 日志分磁盘问题,就可以使用这个命令去发现。 + +(6)**内存问题** + +```java +free -h> $DUMP_DIR/free.dump 2>&1 +``` + + +free 命令能够大体展现操作系统的内存概况,这是故障排查中一个非常重要的点,比如 SWAP 影响了 GC,SLAB 区挤占了 JVM 的内存。 + +(7)**其他全局** + +```java +ps -ef> $DUMP_DIR/ps.dump 2>&1 +dmesg> $DUMP_DIR/dmesg.dump 2>&1 +sysctl -a> $DUMP_DIR/sysctl.dump 2>&1 +``` + + +dmesg 是许多静悄悄死掉的服务留下的最后一点线索。当然,ps 作为执行频率最高的一个命令,由于内核的配置参数,会对系统和 JVM 产生影响,所以我们也输出了一份。 + +(8)**进程快照**,最后的遗言(jinfo) + +```java +${JDK_BIN}jinfo $PID> $DUMP_DIR/jinfo.dump 2>&1 +``` + + +此命令将输出 Java 的基本进程信息,包括**环境变量和参数配置**,可以查看是否因为一些错误的配置造成了 JVM 问题。 + +**(9)dump 堆信息** + +```java +${JDK_BIN}jstat -gcutil $PID> $DUMP_DIR/jstat-gcutil.dump 2>&1 +${JDK_BIN}jstat -gccapacity $PID> $DUMP_DIR/jstat-gccapacity.dump 2>&1 +``` + + +jstat 将输出当前的 gc 信息。一般,基本能大体看出一个端倪,如果不能,可将借助 jmap 来进行分析。 + +**(10)堆信息** + +```java +${JDK_BIN}jmap $PID> $DUMP_DIR/jmap.dump 2>&1 +${JDK_BIN}jmap -heap $PID> $DUMP_DIR/jmap-heap.dump 2>&1 +${JDK_BIN}jmap -histo $PID> $DUMP_DIR/jmap-histo.dump 2>&1 +${JDK_BIN}jmap -dump:format=b,file=$DUMP_DIR/heap.bin $PID> /dev/null 2>&1 +``` + + +jmap 将会得到当前 Java 进程的 dump 信息。如上所示,其实最有用的就是第 4 个命令,但是前面三个能够让你初步对系统概况进行大体判断。因为,第 4 个命令产生的文件,一般都非常的大。而且,需要下载下来,导入 MAT 这样的工具进行深入分析,才能获取结果。这是分析内存泄漏一个必经的过程。 + +**(11)JVM 执行栈** + +```java +${JDK_BIN}jstack $PID> $DUMP_DIR/jstack.dump 2>&1 +``` + + +jstack 将会获取当时的执行栈。一般会多次取值,我们这里取一次即可。这些信息非常有用,能够还原 Java 进程中的线程情况。 + +```java +top -Hp $PID -b -n 1 -c> $DUMP_DIR/top-$PID.dump 2>&1 +``` + + +为了能够得到更加精细的信息,我们使用 top 命令,来获取进程中所有线程的 CPU 信息,这样,就可以看到资源到底耗费在什么地方了。 + +**(12)高级替补** + +```java +kill -3 $PID +``` + + +有时候,jstack 并不能够运行,有很多原因,比如 Java 进程几乎不响应了等之类的情况。我们会尝试向进程发送 kill -3 信号,这个信号将会打印 jstack 的 trace 信息到日志文件中,是 jstack 的一个替补方案。 + +```java +gcore -o $DUMP_DIR/core $PID +``` + + +对于 jmap 无法执行的问题,也有替补,那就是 GDB 组件中的 gcore,将会生成一个 core 文件。我们可以使用如下的命令去生成 dump: + +```java +${JDK_BIN}jhsdb jmap --exe ${JDK}java --core $DUMP_DIR/core --binaryheap +``` + +3. **内存泄漏的现象** + +稍微提一下 jmap 命令,它在 9 版本里被干掉了,取而代之的是 jhsdb,你可以像下面的命令一样使用。 + +```java +jhsdb jmap --heap --pid 37340 +jhsdb jmap --pid 37288 +jhsdb jmap --histo --pid 37340 +jhsdb jmap --binaryheap --pid 37340 +``` + +一般内存溢出,表现形式就是 Old 区的占用持续上升,即使经过了多轮 GC 也没有明显改善。比如ThreadLocal里面的GC Roots,内存泄漏的根本就是,这些对象并没有切断和 GC Roots 的关系,可通过一些工具,能够看到它们的联系。 + + + +## 2、报表异常 | JVM调优 + +有一个报表系统,频繁发生内存溢出,在高峰期间使用时,还会频繁的发生拒绝服务,由于大多数使用者是管理员角色,所以很快就反馈到研发这里。 + +业务场景是由于有些结果集的字段不是太全,因此需要对结果集合进行循环,并通过 HttpClient 调用其他服务的接口进行数据填充。使用 Guava 做了 JVM 内缓存,但是响应时间依然很长。 + +初步排查,JVM 的资源太少。接口 A 每次进行报表计算时,都要涉及几百兆的内存,而且在内存里驻留很长时间,有些计算又非常耗 CPU,特别的"吃"资源。而我们分配给 JVM 的内存只有 3 GB,在多人访问这些接口的时候,内存就不够用了,进而发生了 OOM。在这种情况下,没办法,只有升级机器。把机器配置升级到 4C8G,给 JVM 分配 6GB 的内存,这样 OOM 问题就消失了。但随之而来的是频繁的 GC 问题和超长的 GC 时间,平均 GC 时间竟然有 5 秒多。 + +进一步,由于报表系统和高并发系统不太一样,它的对象,存活时长大得多,并不能仅仅通过增加年轻代来解决;而且,如果增加了年轻代,那么必然减少了老年代的大小,由于 CMS 的碎片和浮动垃圾问题,我们可用的空间就更少了。虽然服务能够满足目前的需求,但还有一些不太确定的风险。 + +第一,了解到程序中有很多缓存数据和静态统计数据,为了减少 MinorGC 的次数,通过分析 GC 日志打印的对象年龄分布,把 MaxTenuringThreshold 参数调整到了 3(特殊场景特殊的配置)。这个参数是让年轻代的这些对象,赶紧回到老年代去,不要老呆在年轻代里。 + +第二,我们的 GC 时间比较长,就一块开了参数 CMSScavengeBeforeRemark,使得在 CMS remark 前,先执行一次 Minor GC 将新生代清掉。同时配合上个参数,其效果还是比较好的,一方面,对象很快晋升到了老年代,另一方面,年轻代的对象在这种情况下是有限的,在整个 MajorGC 中占的时间也有限。 + +第三,由于缓存的使用,有大量的弱引用,拿一次长达 10 秒的 GC 来说。我们发现在 GC 日志里,处理 weak refs 的时间较长,达到了 4.5 秒。这里可以加入参数 ParallelRefProcEnabled 来并行处理Reference,以加快处理速度,缩短耗时。 + +优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这带来另外一个问题。 + +**高性能的机器带来了非常大的服务吞吐量**,通过 jstat 进行监控,能够看到年轻代的分配速率明显提高,但随之而来的 MinorGC 时长却变的不可控,有时候会超过 1 秒。累积的请求造成了更加严重的后果。 + +这是由于堆空间明显加大造成的回收时间加长。为了获取较小的停顿时间,我们在堆上**改用了 G1 垃圾回收器**,把它的目标设定在 200ms。G1 是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标,就能得到不错的性能。修改之后,虽然 GC 更加频繁了一些,但是停顿时间都比较小,应用的运行较为平滑。 + +到目前为止,也只是勉强顶住了已有的业务,但是,这时候领导层面又发力,**要求报表系统可以支持未来两年业务10到100倍的增长**,并保持其可用性,但是这个"千疮百孔"的报表系统,稍微一压测,就宕机,那如何应对十倍百倍的压力呢 ? 硬件即使可以做到动态扩容,但是毕竟也有极限。 + +使用 MAT 分析堆快照,发现很多地方可以通过代码优化,那些占用内存特别多的对象: + +1、select * 全量排查,只允许获取必须的数据 + +2、报表系统中cache实际的命中率并不高,将Guava 的 Cache 引用级别改成弱引用(WeakKeys) + +3、限制报表导入文件大小,同时拆分用户超大范围查询导出请求。 + +每一步操作都使得JVM使用变得更加可用,一系列优化以后,机器相同压测数据性能提升了数倍。 + + + +## 3、大屏异常 | JUC调优 + +有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。 + +![img](https://tva1.sinaimg.cn/large/008eGmZEly1gobr4whjzwj30l1058dfx.jpg) + +接口 A 通过 HttpClient 访问服务 2,响应 100ms 后返回;接口 B 访问服务 3,耗时 2 秒。HttpClient 本身是有一个最大连接数限制的,如果服务 3 迟迟不返回,就会造成 HttpClient 的连接数达到上限,**概括来讲,就是同一服务,由于一个耗时非常长的接口,进而引起了整体的服务不可用** + +这个时候,通过 jstack 打印栈信息,会发现大多数竟然阻塞在了接口 A 上,而不是耗时更长的接口 B,这个现象起初十分具有迷惑性,不过经过分析后,我们猜想其实是因为接口 A 的速度比较快,在问题发生点进入了更多的请求,它们全部都阻塞住的同时被打印出来了。 + +为了验证这个问题,我搭建了一个demo 工程,模拟了两个使用同一个 HttpClient 的接口。fast 接口用来访问百度,很快就能返回;slow 接口访问谷歌,由于众所周知的原因,会阻塞直到超时,大约 10 s。 利用ab对两个接口进行压测,同时使用 jstack 工具 dump 堆栈。首先使用 jps 命令找到进程号,然后把结果重定向到文件(可以参考 10271.jstack 文件)。 + +过滤一下 nio 关键字,可以查看 tomcat 相关的线程,足足有 200 个,这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是,有大多数线程,都处于 BLOCKED 状态,说明线程等待资源超时。通过grep fast | wc -l 分析,确实200个中有150个都是blocked的fast的进程。 + +问题找到了,解决方式就顺利成章了。 + +1、fast和slow争抢连接资源,通过线程池限流或者熔断处理 + +2、有时候slow的线程也不是一直slow,所以就得加入监控 + +3、使用带countdownLaunch对线程的执行顺序逻辑进行控制 + + + + + + + + + +## **4、接口延迟 | SWAP调优** + +有一个关于服务的某个实例,经常发生服务卡顿。由于服务的并发量是比较高的,每多停顿 1 秒钟,几万用户的请求就会感到延迟。 + +我们统计、类比了此服务其他实例的 CPU、内存、网络、I/O 资源,区别并不是很大,所以一度怀疑是机器硬件的问题。 + +接下来我们对比了节点的 GC 日志,发现无论是 Minor GC,还是 Major GC,这个节点所花费的时间,都比其他实例长得多。 + +通过仔细观察,我们发现在 GC 发生的时候,vmstat 的 si、so 飙升的非常严重,这和其他实例有着明显的不同。 + +使用 free 命令再次确认,发现 SWAP 分区,使用的比例非常高,引起的具体原因是什么呢? + +更详细的操作系统内存分布,从 /proc/meminfo 文件中可以看到具体的逻辑内存块大小,有多达 40 项的内存信息,这些信息都可以通过遍历 /proc 目录的一些文件获取。我们注意到 slabtop 命令显示的有一些异常,dentry(目录高速缓冲)占用非常高。 + +问题最终定位到是由于某个运维工程师删除日志时,定时执行了一句命令: + +find / | grep "xxx.log" + + +他是想找一个叫做 要被删除 的日志文件,看看在哪台服务器上,结果,这些老服务器由于文件太多,扫描后这些文件信息都缓存到了 slab 区上。而服务器开了 swap,操作系统发现物理内存占满后,并没有立即释放 cache,导致每次 GC 都要和硬盘打一次交道。 + + + +**解决方式就是关闭 SWAP 分区。** + + + +swap 是很多性能场景的万恶之源,建议禁用。在高并发 SWAP 绝对能让你体验到它魔鬼性的一面:进程倒是死不了了,但 GC 时间长的却让人无法忍受。 + +## 5、**内存溢出 | Cache调优** + +> 有一次线上遇到故障,重新启动后,使用 jstat 命令,发现 Old 区一直在增长。我使用 jmap 命令,导出了一份线上堆栈,然后使用 MAT 进行分析,通过对 GC Roots 的分析,发现了一个非常大的 HashMap 对象,这个原本是其他同事做缓存用的,但是做了一个无界缓存,没有设置超时时间或者 LRU 策略,在使用上又没有重写key类对象的hashcode和equals方法,对象无法取出也直接造成了堆内存占用一直上升,后来,将这个缓存改成 guava 的 Cache,并设置了弱引用,故障就消失了。 +> +> 关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,**close 方法又没有放在 finally** 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。 + +内存溢出是一个结果,而**内存泄漏**是一个原因。内存溢出的原因有**内存空间不足、配置错误**等因素。一些错误的编程方式,不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系,这就是内存泄漏。 + +举个例子,有团队使用了 HashMap 做缓存,但是并没有设置超时时间或者 LRU 策略,造成了放入 Map 对象的数据越来越多,而产生了内存泄漏。 + +再来看一个经常发生的内存泄漏的例子,也是由于 HashMap 产生的。代码如下,由于没有重写 Key 类的 hashCode 和 equals 方法,造成了放入 HashMap 的所有对象都无法被取出来,它们和外界失联了。所以下面的代码结果是 null。 + +```java +//leak example +import java.util.HashMap; +import java.util.Map; +public class HashMapLeakDemo { + public static class Key { + String title; + public Key(String title) { + this.title = title; + } +} + +public static void main(String[] args) { + Map map = new HashMap(); + map.put(new Key("1"), 1); + map.put(new Key("2"), 2); + map.put(new Key("3"), 2); + Integer integer = map.get(new Key("2")); + System.out.println(integer); + } +} +``` + + +即使提供了 equals 方法和 hashCode 方法,也要非常小心,尽量避免使用自定义的对象作为 Key。 + +再看一个例子,关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,**close 方法又没有放在 finally** 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。 + +## 6、CPU飙高 | 死循环 + +我们有个线上应用,单节点在运行一段时间后,CPU 的使用会飙升,一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者是触发了死循环(比如著名的 HashMap 高并发引起的死循环),但排查到最后其实是 GC 的问题。 + +(1)使用 top 命令,查找到使用 CPU 最多的某个进程,记录它的 pid。使用 Shift + P 快捷键可以按 CPU 的使用率进行排序。 + +```java +top +``` + + +(2)再次使用 top 命令,加 -H 参数,查看某个进程中使用 CPU 最多的某个线程,记录线程的 ID。 + +```java +top -Hp $pid +``` + + +(3)使用 printf 函数,将十进制的 tid 转化成十六进制。 + +```java +printf %x $tid +``` + + +(4)使用 jstack 命令,查看 Java 进程的线程栈。 + +```java +jstack $pid>$pid.log +``` + + +(5)使用 less 命令查看生成的文件,并查找刚才转化的十六进制 tid,找到发生问题的线程上下文。 + +```java +less $pid.log +``` + + +我们在 jstack 日志搜关键字DEAD,以及中找到了 CPU 使用最多的几个线程id。 + +可以看到问题发生的根源,是我们的堆已经满了,但是又没有发生 OOM,于是 GC 进程就一直在那里回收,回收的效果又非常一般,造成 CPU 升高应用假死。接下来的具体问题排查,就需要把内存 dump 一份下来,使用 MAT 等工具分析具体原因了。 -https://segmentfault.com/a/1190000023182342 \ No newline at end of file diff --git a/Java-Interview b/Java-Interview new file mode 160000 index 0000000..f192c36 --- /dev/null +++ b/Java-Interview @@ -0,0 +1 @@ +Subproject commit f192c36e87aa47e9d2369b5ad68bb4902d50db27 diff --git "a/Java345円237円272円347円241円200円/Java345円237円272円347円241円200円344円270円212円.md" "b/Java345円237円272円347円241円200円/Java345円237円272円347円241円200円344円270円212円.md" index 8321ac1..901317c 100644 --- "a/Java345円237円272円347円241円200円/Java345円237円272円347円241円200円344円270円212円.md" +++ "b/Java345円237円272円347円241円200円/Java345円237円272円347円241円200円344円270円212円.md" @@ -358,7 +358,7 @@ hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返 ### 为什么重写 equals 方法必须重写 hashcode 方法 ? -断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。 +判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。 在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。 diff --git "a/Java345円271円266円345円217円221円/Java345円244円232円347円272円277円347円250円213円346円200円273円347円273円223円347円211円210円.md" "b/Java345円271円266円345円217円221円/Java345円244円232円347円272円277円347円250円213円346円200円273円347円273円223円347円211円210円.md" new file mode 100644 index 0000000..2fee8e2 --- /dev/null +++ "b/Java345円271円266円345円217円221円/Java345円244円232円347円272円277円347円250円213円346円200円273円347円273円223円347円211円210円.md" @@ -0,0 +1,799 @@ +## 1. 线程和进程有什么区别? + +线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。 + +**根本区别**:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + +**资源开销**:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + +**包含关系**:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + +**内存分配**:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的 + +**影响关系**:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。 + +**执行过程**:每个独立的进程有程序运行的入口. 顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 + +## 2. 创建线程的三种方式的对比? + +**1)采用实现Runnable. Callable接口的方式创建多线程。** + +**优势是**: + +线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。 + +在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU. 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 + +**劣势是:** + +编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 + +**2)使用继承Thread类的方式创建多线程** + +**优势是:** + +编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。 + +**劣势是:** + +线程类已经继承了Thread类,所以不能再继承其他父类。 + +**3)Runnable和Callable的区别** + +- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。 +- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。 +- Call方法可以抛出异常,run方法不可以。 +- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。 + +## 3. 为什么要使用多线程呢? + +- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,**线程间的切换和调度的成本远远小于进程**。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而**多线程并发编程正是开发高并发系统的基础**,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + +从计算机底层来说: + +- 单核时代: **在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率**。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 +- 多核时代:**多核时代多线程主要是为了提高 CPU 利用率**。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 + +## 4. 线程的状态流转 + +线程的生命周期及五种基本状态: + +![](https://youzhixueyuan.com/blog/wp-content/uploads/2019/08/20190801212341_70574.jpg) + +**Java线程具有五中基本状态** + +**1)新建状态(New)**:当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); + +**2)就绪状态(Runnable)**:当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行; + +**3)运行状态(Running)**:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; + +**4)阻塞状态(Blocked)**:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种: + +1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态; + +2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态; + +3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。 + +**5)死亡状态(Dead)**:线程执行完了或者因异常退出了run()方法,该线程结束生命周期。 + +## 5. 什么是线程死锁?如何避免死锁? + +### 死锁 + +- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 + ![](http://blog-img.coolsen.cn/img/1583327022365_13.png) + +### 死锁必须具备以下四个条件: + +- 互斥条件:该资源任意一个时刻只由一个线程占用。 +- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 +- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 + +### 如何避免线程死锁? + +只要破坏产生死锁的四个条件中的其中一个就可以了 + +- 破坏互斥条件 + 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问) +- 破坏请求与保持条件 + 一次性申请所有的资源。 +- 破坏不剥夺条件 + 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 +- 破坏循环等待条件 + 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +- 锁排序法:(必须回答出来的点) + 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁? + 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。 +- 使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁 + +## 6. 常见的对比 + +### Runnable VS Callable + +- Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Callable 接口可以返回结果或抛出检查异常 +- Runnable 接口不会返回结果或抛出检查异常, +- 如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁 +- 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule)) + +### shutdown() VS shutdownNow() + +- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。 +- shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 + shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终 + +### isTerminated() VS isShutdown() + +- isShutDown 当调用 shutdown() 方法后返回为 true。 +- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true + +## 7. sleep() 方法和 wait() 方法区别和共同点? + +### 区别 + +- sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。 +- wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。 +- sleep 方法没有释放锁,而 wait 方法释放了锁 。 +- sleep 通常被用于暂停执行Wait 通常被用于线程间交互/通信 +- sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法 + +### 相同 + +- 两者都可以暂停线程的执行。 + +## 8.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法 + +------ + +- new 一个 Thread,线程进入了新建状态; 调用start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,(调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。)这是真正的多线程工作。 +- 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + **调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** + +## 9. Thread类中的yield方法有什么作用? + + Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。 + +## 10. 谈谈volatile的使用及其原理 + +**volatile的两层语义**: + +1. volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。 + +2. jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。 + +**volatile的原理:** + +获取JIT(即时Java编译器,把字节码解释为机器语言发送给处理器)的汇编代码,发现volatile多加了lock addl指令,这个操作相当于一个内存屏障,使得lock指令后的指令不能重排序到内存屏障前的位置。这也是为什么JDK1.5以后可以使用双锁检测实现单例模式。 + +lock前缀的另一层意义是使得本线程工作内存中的volatile变量值立即写入到主内存中,并且使得其他线程共享的该volatile变量无效化,这样其他线程必须重新从主内存中读取变量值。 + +具体原理见这篇文章:https://www.javazhiyin.com/61019.html + +## 11. 如何创建线程实例并运行? + +`Thread` 类本质上是实现 `Runnable` 接口的一个实例,代表一个线程的实例。创建线程实例一般有两种方法: + + 1. 创建 Thread 的子类并重写 `run()` + +[复制代码](#) + +```java +public class MyThread extends Thread { + @Override + public void run(){ + System.out.println("MyThread running"); + } +} +``` + +`run()` 方在调用 `start()` 方法后被执行,而且一旦线程启动后 `start()` 方法后就会立即返回,而不是等到 `run()` 方法执行完毕后再返回。 + +```java +MyThread myThread = new MyThread(); +myThread.start(); +``` + +2. 实现 Runnable 接口 + +```java +public class MyRunnable implements Runnable{ + @Override + public void run(){ + System.out.println("MyRunnable running"); + } +} +``` + +在新建类时实现 `Runnable` 接口,然后在 `Thread` 类的构造函数中传入 `MyRunnable` 的实例对象,最后执行 `start()` 方法即可; + +```java +Thread thread = new Thread(new MyRunnable()); +thread.start(); +``` + +## 12. 线程阻塞的三种情况 + +当线程因为某种原因放弃 CPU 使用权后,即让出了 CPU 时间片,暂时就会停止运行,知道线程进入可运行状态(`Runnable`),才有机会再次获得 CPU 时间片转入 `RUNNING` 状态。一般来讲,阻塞的情况可以分为如下三种: + +1. **等待阻塞(Object.wait -> 等待队列)** + +`RUNNING` 状态的线程执行 `Object.wait()` 方法后,JVM 会将线程放入等待序列(waitting queue); + +2. **同步阻塞(lock -> 锁池)** + +`RUNNING` 状态的线程在获取对象的同步锁时,若该 **同步锁被其他线程占用,则 JVM 将该线程放入锁池(lock pool)中**; + +3. **其他阻塞(sleep/join)** + +`RUNNING` 状态的线程执行 `Thread.sleep(long ms)` 或 `Thread.join()` 方法,或发出 I/O 请求时,JVM 会将该线程置为阻塞状态。当 `sleep()` 状态超时,`join()` 等待线程终止或超时. 或者 I/O 处理完毕时,线程重新转入可运行状态(`RUNNABLE`); + +## 13. 线程死亡的三种方式 + +1. **正常结束** + +`run()` 或者 `call()` 方法执行完成后,线程正常结束; + +2. **异常结束** + +线程抛出一个未捕获的 `Exception` 或 `Error`,导致线程异常结束; + +3. **调用 stop()** + +直接调用线程的 `stop()` 方法来结束该线程,但是一般不推荐使用该种方式,**因为该方法通常容易导致死锁**; + +## 14. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法? + +JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ + +如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。 + +## 15. 守护线程是什么? + +守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。 + +## 16. 了解Fork/Join框架吗? + +Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。 + +Fork/Join框架需要理解两个点,**「分而治之」**和**「工作窃取算法」**。 + +**「分而治之」** + +以上Fork/Join框架的定义,就是分而治之思想的体现啦 + +![img](http://blog-img.coolsen.cn/img/1246845-20200728125400051-496644362.png) + +**「工作窃取算法」** + +把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~ + +![img](http://blog-img.coolsen.cn/img/1246845-20200728125411364-216326114.png) + +![img](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +工作盗窃算法就是,**「某个线程从其他队列中窃取任务进行执行的过程」**。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。 + +## 17. CAS了解吗? + +- CAS:全称 `Compare and swap`,即**比较并交换**,它是一条 **CPU 同步原语**。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。 + +- CAS 是一种无锁的非阻塞算法的实现。 + +- CAS 包含了 3 个操作数: + +- - 需要读写的内存值 V + - 旧的预期值 A + - 要修改的更新值 B + +- 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。) + +CAS 并发原语体现在 Java 语言中的 `sum.misc.Unsafe` 类中的各个方法。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,**原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的**,**在执行过程中不允许被中断**,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。 + +## 18. CAS有什么缺陷? + +![img](http://blog-img.coolsen.cn/img/1246845-20200728125438568-1459891419.png) + +**1. ABA 问题** + +并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。 + +可以通过AtomicStampedReference**解决ABA问题**,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。 + +**2. 循环时间长开销** + +自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。 + +很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~ + +**3. 只能保证一个变量的原子操作。** + +CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。 + + + +**可以通过这两个方式解决这个问题**: + +- 使用互斥锁来保证原子性; +- 将多个变量封装成对象,通过AtomicReference来保证原子性。 + +## 19. synchronized 和 volatile 的区别是什么? + +`volatile` 解决的是内存可见性问题,会使得所有对 `volatile` 变量的读写都直接写入主存,即 **保证了变量的可见性**。 + +`synchronized` 解决的事执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 `synchronized` 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且,`synchronized` 还会创建一个 **内存屏障**,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而 **保证操作的内存可见性**,同时也使得这个锁的线程的所有操作都 `happens-before` 于随后获得这个锁的线程的操作。 + +两者的区别主要有如下: + +1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 +2. volatile **仅能使用在变量级别**;synchronized 则可以使用在 **变量. 方法. 和类级别的** +3. volatile 仅能实现变量的修改可见性,**不能保证原子性**;而synchronized 则可以 **保证变量的修改可见性和原子性** +4. volatile **不会造成线程的阻塞**;synchronized **可能会造成线程的阻塞**。 +5. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。 + +## 20. synchronized 和 Lock 有什么区别? + +- synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁。 +- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 +- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 + +## 21. synchronized 和 ReentrantLock 区别是什么? + +**1.两者都是可重入锁** + +可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如: +一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, +两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 + +**2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** + +- synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的 +- ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成) + +**3.ReentrantLock 比 synchronized 增加了一些高级功能** + +相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:1等待可中断;2可实现公平锁;3可实现选择性通知(锁可以绑定多个条件) + +- 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 +- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。 +- ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,**用ReentrantLock类结合Condition实例可以实现"选择性通知"** + +**4.使用选择** + +- 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。 +- synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放 + +## 22. synchronized的用法有哪些? + +- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁 +- 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁 +- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁 + +特别注意: + +1如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁 + +2尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能 + +## 23. Synchronized的作用有哪些? + +1. 原子性:确保线程互斥的访问同步代码; +2. 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 "**对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值**" 来保证的; +3. 有序性:有效解决重排序问题,即 "一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作"。 + +## 24. 说一下 synchronized 底层实现原理? + +synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。 + +其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止 + +synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +## 25. 多线程中 synchronized 锁升级的原理是什么? + + synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。 + + 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。 + +## **26. synchronized 为什么是非公平锁?非公平体现在哪些地方?** + +synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点: + +1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作: + +1. 先将锁的持有者 owner 属性赋值为 null +2. 唤醒等待链表中的一个线程(假定继承者)。 + +在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。 + +2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。 + +## 27. JVM对synchronized的优化有哪些? + +从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。 + +### **1. 锁膨胀** + +上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:**无锁——>偏向锁——>轻量级锁——>重量级锁**,并且膨胀方向不可逆。 + +#### **偏向锁** + +一句话总结它的作用:**减少统一线程获取锁的代价**。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。 + +**核心思想:** + +如果一个线程获得了锁,那么锁就进入偏向模式,此时`Mark Word`的结构也就变为偏向锁结构,**当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查**`**Mark Word**`**的锁标记位为偏向锁以及当前线程ID等于**`**Mark Word**`**的ThreadID即可**,这样就省去了大量有关锁申请的操作。 + +#### **轻量级锁** + +轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。 + +#### **重量级锁** + +重量级锁是由轻量级锁升级而来,当**同一时间**有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。 + +重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。 + +### **2.锁消除** + +消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。 + +![image-20210822141520951](http://blog-img.coolsen.cn/img/image-20210822141520951.png) + +### **3. 锁粗化** + +锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。 + +![image-20210822141439642](http://blog-img.coolsen.cn/img/image-20210822141439642.png) + +### **4. 自旋锁与自适应自旋锁** + +轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 + +自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。 + +**自适应自旋锁**:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。 + +**为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?** + +重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。 + +这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。 + +而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。 + +## **28. synchronized 锁能降级吗?** + +可以的。 + +具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。 + +当锁降级时,主要进行了以下操作: + +1)恢复锁对象的 markword 对象头; + +2)重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。 + +## 29. ThreadLocal是什么? + +ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。 + +```java +//创建一个ThreadLocal变量 +static ThreadLocal localVariable = new ThreadLocal(); +``` + +**ThreadLocal的应用场景有** + +- 数据库连接池 +- 会话管理中使用 + +## 30. ThreadLocal的实现原理 + +- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。 +- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。 +- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。 + +ThreadLocal内存结构图: + +![img](http://blog-img.coolsen.cn/img/1246845-20200728125215319-1595528701.png) + + + + 由结构图是可以看出: + +- Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。 +- ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。 + +## 31. 知道ThreadLocal 内存泄露问题吗? + +先看看一下的TreadLocal的引用示意图哈, + +![img](https://img2020.cnblogs.com/blog/1246845/202007/1246845-20200728125314564-2035631421.png) + +ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,如下![img](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==): + +![img](https://img2020.cnblogs.com/blog/1246845/202007/1246845-20200728130048659-1256273260.png) + +> 弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。 + +弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会**「造成了内存泄漏问题」**。 + +如何**「解决内存泄漏问题」**?使用完ThreadLocal后,及时调用remove()方法释放内存空间。 + +## 32. 了解ReentrantLock吗? + +ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。 +ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)。 + +ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。 + +## 33. ReadWriteLock是什么? + +首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。 + +因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能 + + + +# 线程池专题 + +## 1. 为什么要用线程池? + +线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 + +使用线程池的好处: + +- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 +- **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +## 2. 执行execute()方法和submit()方法的区别是什么呢? + +* **`execute()` 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** + +* **submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功**,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 + +## 3. 你说下线程池核心参数? + +------ + +- corePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。 +- maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize +- keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。 +- workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。 +- defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现**RejectedExecutionHandler**接口。 + - AbortPolicy : 线程任务丢弃报错。默认饱和策略。 + - DiscardPolicy : 线程任务直接丢弃不报错。 + - DiscardOldestPolicy : 将workQueue**队首任务丢弃**,将最新线程任务重新加入队列执行。 + - CallerRunsPolicy :线程池之外的线程直接调用run方法执行。 +- ThreadFactory :线程工厂。新建线程工厂。 + +## 4. 线程池执行任务的流程? + +![image.png](http://blog-img.coolsen.cn/img/1460000039258685) + +1. 线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中可以创建新的线程。 +2. 当任务大于核心线程数corePoolSize,就向阻塞队列添加任务。 +3. 如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。 + +## 5. 常用的JAVA线程池有哪几种类型? + +**1、newCachedThreadPool** + +创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + +这种类型的线程池特点是: + +工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。 + +如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 + +在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。 + +**2、newFixedThreadPool** + +创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。 + +FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。 + +**3、newSingleThreadExecutor** + +创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。 + +**4、newScheduleThreadPool** + +创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。 + +## 6. 线程池常用的阻塞队列有哪些? + +![阻塞队列](http://blog-img.coolsen.cn/img/20200722164307306.png) + +
表格左侧是线程池,右侧为它们对应的阻塞队列,可以看到 5 种线程池对应了 3 种阻塞队列
+ +1. LinkedBlockingQueue + 对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。 + + 这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。 + +2. SynchronousQueue + 第二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。 + 我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。 + +3. DelayedWorkQueue +第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。 + +DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是"堆"的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。 + + +## 7. 源码中线程池是怎么复用线程的? + +------ + +源码中ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会while死循环从阻塞队列中取数据,**通过置换worker中Runnable对象,运行其run方法起到线程置换的效果**,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。 + +## 8. 如何合理配置线程池参数? + + +自定义线程池就需要我们自己配置最大线程数 maximumPoolSize ,为了高效的并发运行,这时需要看我们的业务是IO密集型还是CPU密集型。 + +**CPU密集型** +CPU密集的意思是该任务需要最大的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那么多。 + +**IO密集型** +IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上这种加速主要就是利用了被浪费掉的阻塞时间。 + +IO 密集型时,大部分线程都阻塞,故需要多配制线程数。公式为: + +```java +CPU核数*2 +CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间 +查看CPU核数: +System.out.println(Runtime.getRuntime().availableProcessors()); +``` + +当以上都不适用时,选用动态化线程池,看美团技术团队的实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html + +## 9. Executor和Executors的区别? + +Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 + +Executor 接口对象能执行我们的线程任务。ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 + +使用ThreadPoolExecutor 可以创建自定义线程池。Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果。 + +# AQS + +## 1. 说一说什么是AQS? + +1. AQS 是一个锁框架,它定义了锁的实现机制,并开放出扩展的地方,让子类去实现,比如我们在 lock 的时候,AQS 开放出 state 字段,让子类可以根据 state 字段来决定是否能够获得锁,对于获取不到锁的线程 AQS 会自动进行管理,无需子类锁关心,这就是 lock 时锁的内部机制,封装的很好,又暴露出子类锁需要扩展的地方; +2. AQS 底层是由同步队列 + 条件队列联手组成,同步队列管理着获取不到锁的线程的排队和释放,条件队列是在一定场景下,对同步队列的补充,比如获得锁的线程从空队列中拿数据,肯定是拿不到数据的,这时候条件队列就会管理该线程,使该线程阻塞; +3. AQS 围绕两个队列,提供了四大场景,分别是:获得锁、释放锁、条件队列的阻塞,条件队列的唤醒,分别对应着 AQS 架构图中的四种颜色的线的走向。 + +## 2. AQS使用了哪些设计模式? + +AQS同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): + +1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) +2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 + +这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 + +**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** + +``` +isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 +tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 +tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 +tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 +``` + +## 3. 了解AQS中同步队列的数据结构吗? + +![image-20210822170028290](http://blog-img.coolsen.cn/img/image-20210822170028290.png) + +- 当前线程获取同步状态失败,同步器将当前线程机等待状态等信息构造成一个Node节点加入队列,放在队尾,同步器重新设置尾节点 +- 加入队列后,会阻塞当前线程 +- 同步状态被释放并且同步器重新设置首节点,同步器唤醒等待队列中第一个节点,让其再次获取同步状态 + +## 4. 了解AQS 对资源的共享方式吗? + +**AQS定义两种资源共享方式** + +- Exclusive + + (独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: + + - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 + - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 + +- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 + +ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 + +不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 + +## 5. AQS 组件了解吗? + +- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 +- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 +- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 + +# Atomic 原子类 + +## 1. 介绍一下 Atomic 原子类 + +Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 + +所以,所谓原子类说简单点就是具有原子 / 原子操作特征的类。 + +并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下: + +![atomic](https://gitee.com/alan-tang-tt/yuan/raw/master/死磕 java并发包/resource/atomic1.png) + +## 2. JUC 包中的原子类是哪4类? + +**基本类型** +使用原子的方式更新基本类型: + +* AtomicInteger : 整型原子类 +* AtomicLong: 长整型原子类 +* AtomicBoolean: 布尔型原子类 + +**数组类型** +使用原子的方式更新数组里的某个元素: + +* AtomicIntegerArray: 整型数组原子类 +* AtomicLongArray: 长整型数组原子类 +* AtomicReferenceArray: 引用类型数组原子类 + +**引用类型** +使用原子的方式更新引用类型: + +* AtomicReference: 引用类型原子类 +* AtomicStampedReference: 原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +* AtomicMarkableReference: 原子更新带有标记位的引用类型。**对象属性修改类型** +* AtomicIntegerFieldUpdater: 原子更新整型字段的更新器 +* AtomicLongFieldUpdater: 原子更新长整型字段的更新器 +* AtomicMarkableReference: 原子更新带有标记位的引用类型 + +## 3. 简单介绍一下 AtomicInteger 类的原理 + + AtomicInteger 类主要利用 CAS和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 + +`AtomicInteger` 类的部分源码: + +```JAVA + // 更新操作时提供"比较并替换"的作用 + private static final Unsafe unsafe = Unsafe.getUnsafe(); + + private static final long valueOffset; + + static { + try{ + valueOffset = unsafe.objectFieldOffset(AutomicInteger.class.getDeclaredField("value")); + }catch(Exception ex){ + throw new Error(ex); + } + } + + private volatile int value; + +``` + +## 参考 + +https://www.cnblogs.com/java1024/p/13390538.html + +https://segmentfault.com/a/1190000039258680 + +https://github.com/pengMaster/BestNote + +http://static.kancloud.cn/alex_wsc/java_source_interview/1875015 + +https://blog.csdn.net/zycxnanwang/article/details/105321401 + +https://blog.csdn.net/weixin_45124488/article/details/115200512 \ No newline at end of file diff --git "a/MQ/Kafka351円235円242円350円257円225円351円242円230円.md" "b/MQ/Kafka351円235円242円350円257円225円351円242円230円.md" new file mode 100644 index 0000000..92c6baa --- /dev/null +++ "b/MQ/Kafka351円235円242円350円257円225円351円242円230円.md" @@ -0,0 +1,189 @@ +## 1. Apache Kafka是什么? + +Apach Kafka是一款分布式流处理平台,用于实时构建流处理应用。它有一个核心的功能广为人知,即作为企业级的消息引擎被广泛使用(通常也会称之为消息总线message bus)。 + +## 2. Kafka 的设计是什么样的? + +Kafka 将消息以 topic 为单位进行归纳 + +将向 Kafka topic 发布消息的程序成为 producers. + +将预订 topics 并消费消息的程序成为 consumer. + +Kafka 以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 broker. + +producers 通过网络将消息发送到 Kafka 集群,集群向消费者提供消息 + +## 3. Kafka 如何保证高可用? + +`Kafka` 的基本架构组成是:由多个 `broker` 组成一个集群,每个 `broker` 是一个节点;当创建一个 `topic` 时,这个 `topic` 会被划分为多个 `partition`,每个 `partition` 可以存在于不同的 `broker` 上,每个 `partition` 只存放一部分数据。 + +这就是**天然的分布式消息队列**,就是说一个 `topic` 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。 + +在 `Kafka 0.8` 版本之前,是没有 `HA` 机制的,当任何一个 `broker` 所在节点宕机了,这个 `broker` 上的 `partition` 就无法提供读写服务,所以这个版本之前,`Kafka` 没有什么高可用性可言。 + +在 `Kafka 0.8` 以后,提供了 `HA` 机制,就是 `replica` 副本机制。每个 `partition` 上的数据都会同步到其它机器,形成自己的多个 `replica` 副本。所有 `replica` 会选举一个 `leader` 出来,消息的生产者和消费者都跟这个 `leader` 打交道,其他 `replica` 作为 `follower`。写的时候,`leader` 会负责把数据同步到所有 `follower` 上去,读的时候就直接读 `leader` 上的数据即可。`Kafka` 负责均匀的将一个 `partition` 的所有 `replica` 分布在不同的机器上,这样才可以提高容错性。 + +![img](http://blog-img.coolsen.cn/img/Solve-MQ-Problem-With-Kafka-01.png) + +拥有了 `replica` 副本机制,如果某个 `broker` 宕机了,这个 `broker` 上的 `partition` 在其他机器上还存在副本。如果这个宕机的 `broker` 上面有某个 `partition` 的 `leader`,那么此时会从其 `follower` 中重新选举一个新的 `leader` 出来,这个新的 `leader` 会继续提供读写服务,这就有达到了所谓的高可用性。 + +写数据的时候,生产者只将数据写入 `leader` 节点,`leader` 会将数据写入本地磁盘,接着其他 `follower` 会主动从 `leader` 来拉取数据,`follower` 同步好数据了,就会发送 `ack` 给 `leader`,`leader` 收到所有 `follower` 的 `ack` 之后,就会返回写成功的消息给生产者。 + +消费数据的时候,消费者只会从 `leader` 节点去读取消息,但是只有当一个消息已经被所有 `follower` 都同步成功返回 `ack` 的时候,这个消息才会被消费者读到。 + +![img](https://gitee.com/dongzl/article-images/raw/master/2020/13-Solve-MQ-Problem-With-Kafka/Solve-MQ-Problem-With-Kafka-02.png) + +## 4. Kafka 消息是采用 Pull 模式,还是 Push 模式? + +生产者使用push模式将消息发布到Broker,消费者使用pull模式从Broker订阅消息。 + +push模式很难适应消费速率不同的消费者,如果push的速度太快,容易造成消费者拒绝服务或网络拥塞;如果push的速度太慢,容易造成消费者性能浪费。但是采用pull的方式也有一个缺点,就是当Broker没有消息时,消费者会陷入不断地轮询中,为了避免这点,kafka有个参数可以让消费者阻塞知道是否有新消息到达。 + +## 5. Kafka 与传统消息系统之间的区别 + +* Kafka 持久化日志,这些日志可以被重复读取和无限期保留 + +* Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性 + +* Kafka 支持实时的流式处理 + +## 6. 什么是消费者组? + +消费者组是Kafka独有的概念,即消费者组是Kafka提供的可扩展且具有容错性的消费者机制。 + +但实际上,消费者组(Consumer Group)其实包含两个概念,作为队列,消费者组允许你分割数据处理到一组进程集合上(即一个消费者组中可以包含多个消费者进程,他们共同消费该topic的数据),这有助于你的消费能力的动态调整;作为发布-订阅模型(publish-subscribe),Kafka允许你将同一份消息广播到多个消费者组里,以此来丰富多种数据使用场景。 + +需要注意的是:在消费者组中,多个实例共同订阅若干个主题,实现共同消费。同一个组下的每个实例都配置有相同的组ID,被分配不同的订阅分区。当某个实例挂掉的时候,其他实例会自动地承担起它负责消费的分区。 因此,消费者组在一定程度上也保证了消费者程序的高可用性。 + +[![1.jpg](http://dockone.io/uploads/article/20201024/7b359b7a1381541fbacf3ecf20dfb347.jpg)](http://dockone.io/uploads/article/20201024/7b359b7a1381541fbacf3ecf20dfb347.jpg) + +## 7. 在Kafka中,ZooKeeper的作用是什么? + +目前,Kafka使用ZooKeeper存放集群元数据、成员管理、Controller选举,以及其他一些管理类任务。之后,等KIP-500提案完成后,Kafka将完全不再依赖于ZooKeeper。 + +- "存放元数据"是指主题分区的所有数据都保存在 ZooKeeper 中,且以它保存的数据为权威,其他 "人" 都要与它保持对齐。 +- "成员管理" 是指 Broker 节点的注册、注销以及属性变更,等等。 +- "Controller 选举" 是指选举集群 Controller,而其他管理类任务包括但不限于主题删除、参数配置等。 + +KIP-500 思想,是使用社区自研的基于Raft的共识算法,替代ZooKeeper,实现Controller自选举。 + +## 8. 解释下Kafka中位移(offset)的作用 + +在Kafka中,每个主题分区下的每条消息都被赋予了一个唯一的ID数值,用于标识它在分区中的位置。这个ID数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能被修改。 + +## 9. kafka 为什么那么快? + +- Cache Filesystem Cache PageCache缓存 +- `顺序写`:由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要快。 +- `Zero-copy`:零拷技术减少拷贝次数 +- `Batching of Messages`:批量量处理。合并小的请求,然后以流的方式进行交互,直顶网络上限。 +- `Pull 拉模式`:使用拉模式进行消息的获取消费,与消费端处理能力相符。 + +## 10. kafka producer发送数据,ack为0,1,-1分别是什么意思? + +- `1`(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。 +- `0` 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。 +- `-1`producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。当ISR中所有Replica都向Leader发送ACK时,leader才commit,这时候producer才能认为一个请求中的消息都commit了。 + +## 11. Kafka如何保证消息不丢失? + +首先需要弄明白消息为什么会丢失,对于一个消息队列,会有 `生产者`、`MQ`、`消费者` 这三个角色,在这三个角色数据处理和传输过程中,都有可能会出现消息丢失。 + +![img](http://blog-img.coolsen.cn/img/Solve-MQ-Problem-With-Kafka-03.png) + +消息丢失的原因以及解决办法: + +### 消费者异常导致的消息丢失 + +消费者可能导致数据丢失的情况是:消费者获取到了这条消息后,还未处理,`Kafka` 就自动提交了 `offset`,这时 `Kafka` 就认为消费者已经处理完这条消息,其实消费者才刚准备处理这条消息,这时如果消费者宕机,那这条消息就丢失了。 + +消费者引起消息丢失的主要原因就是消息还未处理完 `Kafka` 会自动提交了 `offset`,那么只要关闭自动提交 `offset`,消费者在处理完之后手动提交 `offset`,就可以保证消息不会丢失。但是此时需要注意重复消费问题,比如消费者刚处理完,还没提交 `offset`,这时自己宕机了,此时这条消息肯定会被重复消费一次,这就需要消费者根据实际情况保证幂等性。 + +### 生产者数据传输导致的消息丢失 + +对于生产者数据传输导致的数据丢失主常见情况是生产者发送消息给 `Kafka`,由于网络等原因导致消息丢失,对于这种情况也是通过在 **producer** 端设置 **acks=all** 来处理,这个参数是要求 `leader` 接收到消息后,需要等到所有的 `follower` 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试。 + +### Kafka 导致的消息丢失 + +`Kafka` 导致的数据丢失一个常见的场景就是 `Kafka` 某个 `broker` 宕机,,而这个节点正好是某个 `partition` 的 `leader` 节点,这时需要重新重新选举该 `partition` 的 `leader`。如果该 `partition` 的 `leader` 在宕机时刚好还有些数据没有同步到 `follower`,此时 `leader` 挂了,在选举某个 `follower` 成 `leader` 之后,就会丢失一部分数据。 + +对于这个问题,`Kafka` 可以设置如下 4 个参数,来尽量避免消息丢失: + +- 给 `topic` 设置 `replication.factor` 参数:这个值必须大于 `1`,要求每个 `partition` 必须有至少 `2` 个副本; +- 在 `Kafka` 服务端设置 `min.insync.replicas` 参数:这个值必须大于 `1`,这个参数的含义是一个 `leader` 至少感知到有至少一个 `follower` 还跟自己保持联系,没掉队,这样才能确保 `leader` 挂了还有一个 `follower` 节点。 +- 在 `producer` 端设置 `acks=all`,这个是要求每条数据,必须是写入所有 `replica` 之后,才能认为是写成功了; +- 在 `producer` 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个参数的含义是一旦写入失败,就无限重试,卡在这里了。 + +## 13. Kafka 如何保证消息的顺序性 + +在某些业务场景下,我们需要保证对于有逻辑关联的多条MQ消息被按顺序处理,比如对于某一条数据,正常处理顺序是`新增-更新-删除`,最终结果是数据被删除;如果消息没有按序消费,处理顺序可能是`删除-新增-更新`,最终数据没有被删掉,可能会产生一些逻辑错误。对于如何保证消息的顺序性,主要需要考虑如下两点: + +- 如何保证消息在 `Kafka` 中顺序性; +- 如何保证消费者处理消费的顺序性。 + +### 如何保证消息在 Kafka 中顺序性 + +对于 `Kafka`,如果我们创建了一个 `topic`,默认有三个 `partition`。生产者在写数据的时候,可以指定一个 `key`,比如在订单 `topic` 中我们可以指定订单 `id` 作为 `key`,那么相同订单 `id` 的数据,一定会被分发到同一个 `partition` 中去,而且这个 `partition` 中的数据一定是有顺序的。消费者从 `partition` 中取出来数据的时候,也一定是有顺序的。通过制定 `key` 的方式首先可以保证在 `kafka` 内部消息是有序的。 + +### 如何保证消费者处理消费的顺序性 + +对于某个 `topic` 的一个 `partition`,只能被同组内部的一个 `consumer` 消费,如果这个 `consumer` 内部还是单线程处理,那么其实只要保证消息在 `MQ` 内部是有顺序的就可以保证消费也是有顺序的。但是单线程吞吐量太低,在处理大量 `MQ` 消息时,我们一般会开启多线程消费机制,那么如何保证消息在多个线程之间是被顺序处理的呢?对于多线程消费我们可以预先设置 `N` 个内存 `Queue`,具有相同 `key` 的数据都放到同一个内存 `Queue` 中;然后开启 `N` 个线程,每个线程分别消费一个内存 `Queue` 的数据即可,这样就能保证顺序性。当然,消息放到内存 `Queue` 中,有可能还未被处理,`consumer` 发生宕机,内存 `Queue` 中的数据会全部丢失,这就转变为上面提到的**如何保证消息的可靠传输**的问题了。 + +## 14. Kafka中的ISR、AR代表什么?ISR的伸缩指什么? + +- `ISR`:In-Sync Replicas 副本同步队列 +- `AR`:Assigned Replicas 所有副本 + +ISR是由leader维护,follower从leader同步数据有一些延迟(包括`延迟时间replica.lag.time.max.ms`和`延迟条数replica.lag.max.messages`两个维度,当前最新的版本0.10.x中只支持`replica.lag.time.max.ms`这个维度),任意一个超过阈值都会把follower剔除出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。 + +> AR=ISR+OSR。 + +## 15. 描述下 Kafka 中的领导者副本(Leader Replica)和追随者副本(Follower Replica)的区别 + +Kafka副本当前分为领导者副本和追随者副本。只有Leader副本才能对外提供读写服务,响应Clients端的请求。Follower副本只是采用拉(PULL)的方式,被动地同步Leader副本中的数据,并且在Leader副本所在的Broker宕机后,随时准备应聘Leader副本。 + +加分点: + +- 强调Follower副本也能对外提供读服务。自Kafka 2.4版本开始,社区通过引入新的Broker端参数,允许Follower副本有限度地提供读服务。 +- 强调Leader和Follower的消息序列在实际场景中不一致。通常情况下,很多因素可能造成Leader和Follower之间的不同步,比如程序问题,网络问题,broker问题等,短暂的不同步我们可以关注(秒级别),但长时间的不同步可能就需要深入排查了,因为一旦Leader所在节点异常,可能直接影响可用性。 + + +注意:之前确保一致性的主要手段是高水位机制(HW),但高水位值无法保证Leader连续变更场景下的数据一致性,因此,社区引入了Leader Epoch机制,来修复高水位值的弊端。 + +## 16. 分区Leader选举策略有几种? + +分区的Leader副本选举对用户是完全透明的,它是由Controller独立完成的。你需要回答的是,在哪些场景下,需要执行分区Leader选举。每一种场景对应于一种选举策略。 + +- OfflinePartition Leader选举:每当有分区上线时,就需要执行Leader选举。所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区Leader选举场景。 +- ReassignPartition Leader选举:当你手动运行kafka-reassign-partitions命令,或者是调用Admin的alterPartitionReassignments方法执行分区副本重分配时,可能触发此类选举。假设原来的AR是[1,2,3],Leader是1,当执行副本重分配后,副本集合AR被设置成[4,5,6],显然,Leader必须要变更,此时会发生Reassign Partition Leader选举。 +- PreferredReplicaPartition Leader选举:当你手动运行kafka-preferred-replica-election命令,或自动触发了Preferred Leader选举时,该类策略被激活。所谓的Preferred Leader,指的是AR中的第一个副本。比如AR是[3,2,1],那么,Preferred Leader就是3。 +- ControlledShutdownPartition Leader选举:当Broker正常关闭时,该Broker上的所有Leader副本都会下线,因此,需要为受影响的分区执行相应的Leader选举。 + + +这4类选举策略的大致思想是类似的,即从AR中挑选首个在ISR中的副本,作为新Leader。 + +## 17. Kafka的哪些场景中使用了零拷贝(Zero Copy)? + +在Kafka中,体现Zero Copy使用场景的地方有两处:基于mmap的索引和日志文件读写所用的TransportLayer。 + +先说第一个。索引都是基于MappedByteBuffer的,也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消Zero Copy带来的性能优势。由于这种不确定性,在Kafka中,只有索引应用了mmap,最核心的日志并未使用mmap机制。 + +再说第二个。TransportLayer是Kafka传输层的接口。它的某个实现类使用了FileChannel的transferTo方法。该方法底层使用sendfile实现了Zero Copy。对Kafka而言,如果I/O通道使用普通的PLAINTEXT,那么,Kafka就可以利用Zero Copy特性,直接将页缓存中的数据发送到网卡的Buffer中,避免中间的多次拷贝。相反,如果I/O通道启用了SSL,那么,Kafka便无法利用Zero Copy特性了。 + +## 18. 为什么Kafka不支持读写分离? + +在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。 + +Kafka 并不支持主写从读,因为主写从读有 2 个很明 显的缺点: + +- **数据一致性问题**。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。 +- **延时问题**。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历`网络→主节点内存→网络→从节点内存`这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历`网络→主节点内存→主节点磁盘→网络→从节点内存→从节点磁盘`这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。 + +## 参考 + +http://dockone.io/article/10853 + +https://segmentfault.com/a/1190000023716306 + +https://dongzl.github.io/2020/03/16/13-Solve-MQ-Problem-With-Kafka/index.html \ No newline at end of file diff --git "a/MQ/MQ351円235円242円350円257円225円351円242円230円.md" "b/MQ/MQ351円235円242円350円257円225円351円242円230円.md" new file mode 100644 index 0000000..ba97f6f --- /dev/null +++ "b/MQ/MQ351円235円242円350円257円225円351円242円230円.md" @@ -0,0 +1,328 @@ +## 为什么使用MQ? + +使用MQ的场景很多,主要有三个:解耦、异步、削峰。 + +- 解耦:假设现在,日志不光要插入到数据库里,还要在硬盘中增加文件类型的日志,同时,一些关键日志还要通过邮件的方式发送给指定的人。那么,如果按照原来的逻辑,A可能就需要在原来的代码上做扩展,除了B服务,还要加上日志文件的存储和日志邮件的发送。但是,如果你使用了MQ,那么,A服务是不需要做更改的,它还是将消息放到MQ中即可,其它的服务,无论是原来的B服务还是新增的日志文件存储服务或日志邮件发送服务,都直接从MQ中获取消息并处理即可。这就是解耦,它的好处是提高系统灵活性,扩展性。 +- 异步:可以将一些非核心流程,如日志,短信,邮件等,通过MQ的方式异步去处理。这样做的好处是缩短主流程的响应时间,提升用户体验。 +- 削峰:MQ的本质就是业务的排队。所以,面对突然到来的高并发,MQ也可以不用慌忙,先排好队,不要着急,一个一个来。削峰的好处就是避免高并发压垮系统的关键组件,如某个核心服务或数据库等。 + +下面附场景解释: + +### 解耦 + +场景:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃...... + +![img](http://blog-img.coolsen.cn/img/727602-20200108091205317-949408193.png) + +在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊! + +如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091329888-1880681145.png) + +总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。 + +### 异步 + +场景:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 +たす 300 +たす 450 +たす 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091632167-740723329.png) + +一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。 + +如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091722601-747710174.png) + +### 削峰 + +场景:每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。 + +使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091915241-1598228624.png) + +这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。 + +## 消息队列的缺点 + +1、 系统可用性降低 + +系统引入的外部依赖越多,越容易挂掉。 + +2、 系统复杂度提高 + +加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。 + +3、 一致性问题 + +A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,这就数据不一致了。 + +## Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? + +| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | +| ------------------------ | ------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 开发语言 | java | erlang | java | scala | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | +| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | +| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | +| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | +| 社区活跃度 | 低 | 很高 | 一般 | 很高 | + +- 中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择; +- 大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。 +- 大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。 + +# RabbitMQ + +## 1. RabbitMQ是什么? + +RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。 + +## 2. RabbitMQ特点? + +可靠性: RabbitMQ使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。 + +灵活的路由 : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个 交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 + +扩展性: 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。 + +高可用性 : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。 + +多种协议: RabbitMQ除了原生支持AMQP协议,还支持STOMP, MQTT等多种消息 中间件协议。 + +多语言客户端 :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。 + +管理界面 : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。 + +令插件机制: RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。 + +## 3. AMQP是什么? + +RabbitMQ就是 AMQP 协议的 `Erlang` 的实现(当然 RabbitMQ 还支持 `STOMP2`、 `MQTT3` 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。 + +RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。 + +## 4. AMQP的3层协议? + +Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。 + +Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。 + +TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。 + +## 5. 说说Broker服务节点、Queue队列、Exchange交换器? + +- Broker可以看做RabbitMQ的服务节点。一般请下一个Broker可以看做一个RabbitMQ服务器。 +- Queue:RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。 +- Exchange:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。 + +## 6. 如何保证消息的可靠性? + +分三点: + +* 生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。 +* RabbitMQ自身:持久化、集群、普通模式、镜像模式。 +* RabbitMQ到消费者:basicAck机制、死信队列、消息补偿机制。 + +## 7. 生产者消息运转的流程? + +1. `Producer`先连接到Broker,建立连接Connection,开启一个信道(Channel)。 + +2. `Producer`声明一个交换器并设置好相关属性。 + +3. `Producer`声明一个队列并设置好相关属性。 + +4. `Producer`通过路由键将交换器和队列绑定起来。 + +5. `Producer`发送消息到`Broker`,其中包含路由键、交换器等信息。 + +6. 相应的交换器根据接收到的路由键查找匹配的队列。 + +7. 如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。 + +8. 关闭信道。 + +9. 管理连接。 + +## 8.消费者接收消息过程? + +1. `Producer`先连接到`Broker`,建立连接`Connection`,开启一个信道(`Channel`)。 + +2. 向`Broker`请求消费响应的队列中消息,可能会设置响应的回调函数。 + +3. 等待`Broker`回应并投递相应队列中的消息,接收消息。 + +4. 消费者确认收到的消息,`ack`。 + +5. `RabbitMq`从队列中删除已经确定的消息。 + +6. 关闭信道。 + +7. 关闭连接。 + +## 9. 生产者如何将消息可靠投递到RabbitMQ? + +1. Client发送消息给MQ + +2. MQ将消息持久化后,发送Ack消息给Client,此处有可能因为网络问题导致Ack消息无法发送到Client,那么Client在等待超时后,会重传消息; + +3. Client收到Ack消息后,认为消息已经投递成功。 + +## 10. RabbitMQ如何将消息可靠投递到消费者? + +1. MQ将消息push给Client(或Client来pull消息) + +2. Client得到消息并做完业务逻辑 + +3. Client发送Ack消息给MQ,通知MQ删除该消息,此处有可能因为网络问题导致Ack失败,那么Client会重复消息,这里就引出消费幂等的问题; + +4. MQ将已消费的消息删除。 + +## 11. 如何保证RabbitMQ消息队列的高可用? + +RabbitMQ 有三种模式:`单机模式`,`普通集群模式`,`镜像集群模式`。 + +**单机模式**:就是demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式 + +**普通集群模式**:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。 + +**镜像集群模式**:这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据(元数据指RabbitMQ的配置数据)还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。 + +# RocketMQ + +## 1. RocketMQ是什么? + +RocketMQ 是阿里巴巴开源的分布式消息中间件。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如Group、Topic、Queue等。系统组成则由Producer、Consumer、Broker、NameServer等。 + +**RocketMQ 特点** + +- 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式等特点 +- Producer、Consumer、队列都可以分布式 +- Producer 向一些队列轮流发送消息,队列集合称为 Topic,Consumer 如果做广播消费,则一个 Consumer 实例消费这个 Topic 对应的所有队列,如果做集群消费,则多个 Consumer 实例平均消费这个 Topic 对应的队列集合 +- 能够保证严格的消息顺序 +- 支持拉(pull)和推(push)两种消息模式 +- 高效的订阅者水平扩展能力 +- 实时的消息订阅机制 +- 亿级消息堆积能力 +- 支持多种消息协议,如 JMS、OpenMessaging 等 +- 较少的依赖 + +## 2. RocketMQ由哪些角色组成,每个角色作用和特点是什么? + +| 角色 | 作用 | +| ---------- | ------------------------------------------------------------ | +| Nameserver | 无状态,动态列表;这也是和zookeeper的重要区别之一。zookeeper是有状态的。 | +| Producer | 消息生产者,负责发消息到Broker。 | +| Broker | 就是MQ本身,负责收发消息、持久化消息等。 | +| Consumer | 消息消费者,负责从Broker上拉取消息进行消费,消费完进行ack。 | + +## 3. RocketMQ消费模式有几种? + +消费模型由Consumer决定,消费维度为Topic。 + +1、集群消费 + +* 一条消息只会被同Group中的一个Consumer消费 + +* 多个Group同时消费一个Topic时,每个Group都会有一个Consumer消费到数据 + +2、广播消费 + +消息将对一 个Consumer Group 下的各个 Consumer 实例都消费一遍。即即使这些 Consumer 属于同一个Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。 + +## 4. RocketMQ消费消息是push还是pull? + +RocketMQ没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是**长轮询机制**,即拉取方式 + +> broker端属性 longPollingEnable 标记是否开启长轮询。默认开启 + +### 追问:为什么要主动拉取消息而不使用事件监听方式? + +事件驱动方式是建立好长连接,由事件(发送数据)的方式来实时推送。 + +如果broker主动推送消息的话有可能push速度快,消费速度慢的情况,那么就会造成消息在consumer端堆积过多,同时又不能被其他consumer消费的情况。而pull的方式可以根据当前自身情况来pull,不会造成过多的压力而造成瓶颈。所以采取了pull的方式。 + +## 5. broker如何处理拉取请求的? + +Consumer首次请求Broker + +- Broker中是否有符合条件的消息 + +- 有 + +- - 响应Consumer + - 等待下次Consumer的请求 + +- 没有 + +- - DefaultMessageStore#ReputMessageService#run方法 + - PullRequestHoldService 来Hold连接,每个5s执行一次检查pullRequestTable有没有消息,有的话立即推送 + - 每隔1ms检查commitLog中是否有新消息,有的话写入到pullRequestTable + - 当有新消息的时候返回请求 + - 挂起consumer的请求,即不断开连接,也不返回数据 + - 使用consumer的offset, + +## 6. 如何让RocketMQ保证消息的顺序消费? + +首先多个queue只能保证单个queue里的顺序,queue是典型的FIFO,天然顺序。多个queue同时消费是无法绝对保证消息的有序性的。所以总结如下: + +同一topic,同一个QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个queue里的消息。 + +## 7. RocketMQ如何保证消息不丢失? + +首先在如下三个部分都可能会出现丢失消息的情况: + +- Producer端 +- Broker端 +- Consumer端 + +1 、Producer端如何保证消息不丢失 + +- 采取send()同步发消息,发送结果是同步感知的。 +- 发送失败后可以重试,设置重试次数。默认3次。 + +- 集群部署,比如发送失败了的原因可能是当前Broker宕机了,重试的时候会发送到其他Broker上。 + +2、Broker端如何保证消息不丢失 + +- 修改刷盘策略为同步刷盘。默认情况下是异步刷盘的。 + +- 集群部署,主从模式,高可用。 + +3、Consumer端如何保证消息不丢失 + +- 完全消费正常后在进行手动ack确认。 + +## 7. rocketMQ的消息堆积如何处理? + +首先要找到是什么原因导致的消息堆积,是Producer太多了,Consumer太少了导致的还是说其他情况,总之先定位问题。 + +然后看下消息消费速度是否正常,正常的话,可以通过上线更多consumer临时解决消息堆积问题 + +### 追问:如果Consumer和Queue不对等,上线了多台也在短时间内无法消费完堆积的消息怎么办? + +- 准备一个临时的topic +- queue的数量是堆积的几倍 +- queue分布到多Broker中 +- 上线一台Consumer做消息的搬运工,把原来Topic中的消息挪到新的Topic里,不做业务逻辑处理,只是挪过去 +- 上线N台Consumer同时消费临时Topic中的数据 +- 改bug +- 恢复原来的Consumer,继续消费之前的Topic + +### 追问:堆积时间过长消息超时了? + +RocketMQ中的消息只会在commitLog被删除的时候才会消失,不会超时。也就是说未被消费的消息不会存在超时删除这情况。 + +### 追问:堆积的消息会不会进死信队列? + +不会,消息在消费失败后会进入重试队列(%RETRY%+ConsumerGroup),18次(默认18次,网上所有文章都说是16次,无一例外。但是我没搞懂为啥是16次,这不是18个时间吗 ?)才会进入死信队列(%DLQ%+ConsumerGroup)。 + +## 8. RocketMQ为什么自研nameserver而不用zk? + +1. RocketMQ只需要一个轻量级的维护元数据信息的组件,为此引入zk增加维护成本还强依赖另一个中间件了。 +2. RocketMQ追求的是AP,而不是CP,也就是需要高可用。 + * zk是CP,因为zk节点间通过zap协议有数据共享,每个节点数据会一致,但是zk集群当挂了一半以上的节点就没法使用了。 + * nameserver是AP,节点间不通信,这样会导致节点间数据信息会发生短暂的不一致,但每个broker都会定时向所有nameserver上报路由信息和心跳。当某个broker下线了,nameserver也会延时30s才知道,而且不会通知客户端(生产和消费者),只能靠客户端自己来拉,rocketMQ是靠消息重试机制解决这个问题的,所以是最终一致性。但nameserver集群只要有一个节点就可用。https://juejin.cn/post/6844904068771479559 \ No newline at end of file diff --git "a/MySQL/MySQL345円237円272円347円241円200円343円200円201円351円224円201円343円200円201円344円272円213円345円212円241円343円200円201円345円210円206円345円272円223円345円210円206円350円241円250円343円200円201円344円274円230円345円214円226円.md" "b/MySQL/MySQL345円237円272円347円241円200円343円200円201円351円224円201円343円200円201円344円272円213円345円212円241円343円200円201円345円210円206円345円272円223円345円210円206円350円241円250円343円200円201円344円274円230円345円214円226円.md" new file mode 100644 index 0000000..eb6be37 --- /dev/null +++ "b/MySQL/MySQL345円237円272円347円241円200円343円200円201円351円224円201円343円200円201円344円272円213円345円212円241円343円200円201円345円210円206円345円272円223円345円210円206円350円241円250円343円200円201円344円274円230円345円214円226円.md" @@ -0,0 +1,574 @@ +# 基础 + +![image-20210822210317322](http://blog-img.coolsen.cn/img/image-20210822210317322.png) + +## 1. 数据库的三范式是什么? + +- 第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。 +- 第二范式:要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。 +- 第三范式:任何非主属性不依赖于其它非主属性。 + +## 2. MySQL 支持哪些存储引擎? + +MySQL 支持多种存储引擎,比如 InnoDB,MyISAM,Memory,Archive 等等.在大多数的情况下,直接选择使用 InnoDB 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。 + +MyISAM 和 InnoDB 的区别有哪些: + +- InnoDB 支持事务,MyISAM 不支持 +- InnoDB 支持外键,而 MyISAM 不支持 +- InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。 +- Innodb 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高; +- InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。 +- MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 + +## 3. 超键、候选键、主键、外键分别是什么? + +* 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。 +* 候选键:是最小超键,即没有冗余元素的超键。 +* 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。 +* 外键:在一个表中存在的另一个表的主键称此表的外键。 + +## 4. SQL 约束有哪几种? + +* NOT NULL: 用于控制字段的内容一定不能为空(NULL)。 +* UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。 +* PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。 +* FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。 +* CHECK: 用于控制字段的值范围。 + +## 5. MySQL 中的 varchar 和 char 有什么区别? + +char 是一个定长字段,假如申请了`char(10)`的空间,那么无论实际存储多少内容.该字段都占用 10 个字符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间. + +在检索效率上来讲,char> varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar.例如存储用户 MD5 加密后的密码,则应该使用 char。 + +## 6. MySQL中 in 和 exists 区别 + +MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。 + +如果查询的两个表大小相当,那么用in和exists差别不大。 +如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。 +not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。 + +## 7. drop、delete与truncate的区别 + +三者都表示删除,但是三者有一些差别: + +![image-20210822203927822](http://blog-img.coolsen.cn/img/image-20210822203927822.png) + +## 8. 什么是存储过程?有哪些优缺点? + +存储过程是一些预编译的 SQL 语句。 + +1、更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。 + +2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全 + +但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好。 + +## 9. MySQL 执行查询的过程 + +1. 客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配 +2. 查缓存。(当判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中。) +3. 语法分析(SQL 语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。 +4. 优化。是否使用索引,生成执行计划。 +5. 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。 + + + +![img](https://static001.geekbang.org/infoq/41/4102b7d60fa20a0caabb127ecbb4d2f3.jpeg?x-oss-process=image/resize,p_80/auto-orient,1) + + + +更新语句执行会复杂一点。需要检查表是否有排它锁,写 binlog,刷盘,是否执行 commit。 + +# 事务 + +## 1. 什么是数据库事务? +事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。 + +事务最经典也经常被拿出来说例子就是转账了。 + +假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 + +## 2. 介绍一下事务具有的四个特征 + +事务就是一组原子性的操作,这些操作要么全部发生,要么全部不发生。事务把数据库从一种一致性状态转换成另一种一致性状态。 + +- 原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 +- 一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。 +- 隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的//操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 +- 持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。 + +## 3. 说一下MySQL 的四种隔离级别 + +- Read Uncommitted(读取未提交内容) + +在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。 + +- Read Committed(读取提交内容) + +这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓 的 不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。 + +- Repeatable Read(可重读) + +这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。 + +- Serializable(可串行化) + +通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 + +![image-20210822180308501](http://blog-img.coolsen.cn/img/image-20210822180308501.png) + +MySQL 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别 + +事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。 + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 + +InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。 + +## 4. 什么是脏读?幻读?不可重复读? + +1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据 + +2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。 + +3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。 + +不可重复读侧重于修改,幻读侧重于新增或删除(多了或少量行),脏读是一个事务回滚影响另外一个事务。 + +## 5. 事务的实现原理 + +事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。 + +每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。 + +每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。 + +## 6. MySQL事务日志介绍下? + +innodb 事务日志包括 redo log 和 undo log。 + +undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。 + +事务日志的目的:实例或者介质失败,事务日志文件就能派上用场。 + +### redo log + +redo log 不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo 中。具体的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog 是为了实现事务的持久性而出现的产物。 + +![image-20210822181340692](http://blog-img.coolsen.cn/img/image-20210822181340692.png) + +### undo log + +undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL innodb 存储引擎中用来实现多版本并发控制。 + +![image-20210822181416382](http://blog-img.coolsen.cn/img/image-20210822181416382.png) + +## 7. 什么是MySQL的 binlog? + +MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。 + +MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。 + +binlog 有三种格式,各有优缺点: + +* **statement:** 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错。 + +* **row:** 基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。 + +* **mixed:** 混合模式,根据语句来选用是 statement 还是 row 模式。 + +## **8. 在事务中可以混合使用存储引擎吗?** + +尽量不要在同一个事务中使用多种存储引擎,MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。 + +如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。 + +但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。 + +## 9. MySQL中是如何实现事务隔离的? + +读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。 + +MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。 + +详细原理看这篇文章:https://haicoder.net/note/MySQL-interview/MySQL-interview-MySQL-trans-level.html + +## 10. 什么是 MVCC? + +MVCC, 即多版本并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。 + +## 11. MVCC 的实现原理 + +对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列: + +- ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。 +- 事务 ID:记录最后一次修改该记录的事务 ID。 +- 回滚指针:指向这条记录的上一个版本。 + +我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图: + +![img](http://blog-img.coolsen.cn/img/modb_95751916-225c-11eb-b0bb-5254001c05fe.png) + +如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下: + +- 数据库会先对满足 a=1 的行加排他锁; +- 然后将原记录复制到 undo 表空间中; +- 修改 b 字段的值为 666,修改事务 ID 为 2; +- 并通过隐藏的回滚指针指向 undo log 中的历史记录; +- 事务提交,释放前面对满足 a=1 的行所加的排他锁。 + +在前面实验的第 6 步中,session2 查询的结果是 session1 修改之前的记录,这个记录就是**来自 undolog** 中。 + +因此可以总结出 MVCC 实现的原理大致是: + +InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。 + +MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。 + + + +# 锁 + +## 1. 为什么要加锁? + +当多个用户并发地存取数据时,在[数据库](https://cloud.tencent.com/solution/database?from=10680)中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。 + +保证多用户环境下保证数据库完整性和一致性。 + +## 2. 按照锁的粒度分数据库锁有哪些? + +在关系型数据库中,可以**按照锁的粒度把数据库锁分**为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。 + +行级锁 + +- 行级锁是[MySQL](https://cloud.tencent.com/product/cdb?from=10680)中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。 +- 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 + +表级锁 + +- 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。 +- 开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 + +页级锁 + +- 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁 +- 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 + +**MyISAM和InnoDB存储引擎使用的锁:** + +- MyISAM采用表级锁(table-level locking)。 +- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁 + +## 3. 从锁的类别上分MySQL都有哪些锁呢? +从锁的类别上来讲,有共享锁和排他锁。 + +* 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。 + +* 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。 + +用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。 + +锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。 + +他们的加锁开销从大到小,并发能力也是从大到小。 + +## 4. 数据库的乐观锁和悲观锁是什么?怎么实现的? +数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。 + +* 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制 + +* 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。 + +**两种锁的使用场景** + +从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。 + +但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 + +## 5. InnoDB引擎的行锁是怎么实现的? +InnoDB是基于索引来完成行锁 + +例: select * from tab_with_index where id = 1 for update; + +for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起 + +## 6. 什么是死锁?怎么解决? +死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 + +常见的解决死锁的方法 + +1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。 + +2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; + +3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率; + +如果业务处理不好可以用分布式事务锁或者使用乐观锁 + +## 7. 隔离级别与锁的关系 +在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突 + +在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁; + +在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。 + +SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。 + +## 8. 优化锁方面的意见? +* 使用较低的隔离级别 +* 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突 +* 选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁 +* 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。 +* 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响 +* 不要申请超过实际需要的锁级别 +* 数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别 +* 对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。 + +# 分库分表 + +## 1. 为什么要分库分表? + +**分表** + +比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。 + +分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。 + +**分库** + +分库就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 + +这就是所谓的分库分表。 + +![img](https://upload-images.jianshu.io/upload_images/14266602-ae74054f45f44e3d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 2. 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点? + +这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。 + +比较常见的包括: + +- cobar +- TDDL +- atlas +- sharding-jdbc +- mycat + +#### cobar + +阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。 + +#### TDDL + +淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。 + +#### atlas + +360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。 + +#### sharding-jdbc + +当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。 + +#### mycat + +基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。 + +## 3. 如何对数据库如何进行垂直拆分或水平拆分的? + +**水平拆分**的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。 + +![img](https:////upload-images.jianshu.io/upload_images/10089464-0e01dfe246b5c7ac.png?imageMogr2/auto-orient/strip|imageView2/2/w/474/format/webp) + +**垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 + +![img](https:////upload-images.jianshu.io/upload_images/10089464-ab3069913c0f097c.png?imageMogr2/auto-orient/strip|imageView2/2/w/320/format/webp) + + + +两种**分库分表的方式**: + +- 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。 +- 或者是按照某个字段hash一下均匀分散,这个较为常用。 + +range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。 + +hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表 + +# 读写分离、主从同步(复制) + +## 1. 什么是MySQL主从同步? + +主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。 + +因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。 + +## 2. MySQL主从同步的目的?为什么要做主从同步? + +1. 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。 +2. 提高数据安全-因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据 +3. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能 +4. 数据备份。一般我们都会做数据备份,可能是写定时任务,一些特殊行业可能还需要手动备份,有些行业要求备份和原数据不能在同一个地方,所以主从就能很好的解决这个问题,不仅备份及时,而且还可以多地备份,保证数据的安全 + +## 3. 如何实现MySQL的读写分离? + +其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。 + +## 4. MySQL主从复制流程和原理? + +基本原理流程,是3个线程以及之间的关联 + +主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中; + +从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中; + +从:sql执行线程——执行relay log中的语句; + +**复制过程如下**: + +![img](http://blog-img.coolsen.cn/img/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC85LzIxLzE2NWZiNjgzMjIyMDViMmU) + +Binary log:主数据库的二进制日志 + +Relay log:从服务器的中继日志 + +第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。 + +第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。 + +第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。 + + +## 5. MySQL主从同步延时问题如何解决? + +MySQL 实际上在有两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;一个是并行复制,用来 解决主从同步延时问题。 + +- 半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。 +- 并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。 + +# MySQL优化 + +## 1. 如何定位及优化SQL语句的性能问题? + +对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。 + + 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。 +![image-20210822204026552](http://blog-img.coolsen.cn/img/image-20210822204026552.png) + +## 2. 大表数据查询,怎么优化 +* 优化shema、sql语句+索引; +* 第二加缓存,memcached, redis; +* 主从复制,读写分离; +* 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统; +* 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表; + +## 3. 超大分页怎么处理? + +数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于`select * from table where age> 20 limit 1000000`,10 这种查询其实也是有可以优化的余地的. 这条语句需要 load1000000 数据然后基本上全部丢弃,只取 10 条当然比较慢. 当时我们可以修改为`select * from table where id in (select id from table where age> 20 limit 1000000,10)`.这样虽然也 load 了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快。 + +解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可. + +在阿里巴巴《Java开发手册》中,对超大分页的解决办法是类似于上面提到的第一种. + +> 【推荐】利用延迟关联或者子查询优化超多分页场景。 +> +> 说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。 +> +> 正例:先快速定位需要获取的id段,然后再关联: +> +> SELECT a.* FROM 表1 a, (select id from 表1 where 条件 LIMIT 100000,20 ) b where a.id=b.id + +## 4. 统计过慢查询吗?对慢查询都怎么优化过? + +在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。 + +慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大? + +所以优化也是针对这三个方向来的, + +* 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。 +* 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。 +* 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。 + +## 5. 如何优化查询过程中的数据访问 + +* 访问数据太多导致查询性能下降 +* 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列 +* 确认MySQL服务器是否在分析大量不必要的数据行 +* 查询不需要的数据。解决办法:使用limit解决 +* 多表关联返回全部列。解决办法:指定列名 +* 总是返回全部列。解决办法:避免使用SELECT * +* 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存 +* 是否在扫描额外的记录。解决办法: + 使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化: + 使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。 +* 改变数据库和表的结构,修改数据表范式 +* 重写SQL语句,让优化器可以以更优的方式执行查询。 + +## 6. 如何优化关联查询 + +- 确定ON或者USING子句中是否有索引。 +- 确保GROUP BY和ORDER BY只有一个表中的列,这样MySQL才有可能使用索引。 + +## 7. 数据库结构优化 + +一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。 + +需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。 + +1. **将字段很多的表分解成多个表** + +对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。 + +因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。 + +2. **增加中间表** + +对于需要经常联合查询的表,可以建立中间表以提高查询效率。 + +通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。 + +3. **增加冗余字段** + +设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。 + +表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。 + +注意: + +冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。 +## 8. MySQL数据库cpu飙升到500%的话他怎么处理? +当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 MySQLd 占用导致的,如果不是,找出占用高的进程,并进行相关处理。 + +如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。 + +一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。 + +也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。 +## 9. 大表怎么优化? + +类似的问题:某个表有近千万数据,CRUD比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么? + +当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: + +* 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内; +* 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读; +* 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑; +* 通过分库分表的方式进行优化,主要有垂直分表和水平分表。 + +## 参考 + +## MySQL + +https://blog.csdn.net/ThinkWon/article/details/104778621 + +https://haicoder.net/note/mysql-interview/mysql-interview-mysql-binlog.html + +https://www.modb.pro/db/40241 + +https://www.jianshu.com/p/05da0fc0950e + +https://blog.csdn.net/ThinkWon/article/details/104778621 \ No newline at end of file diff --git a/Mybatis/Mybatis.md b/Mybatis/Mybatis.md index 763b26d..3b0b40d 100644 --- a/Mybatis/Mybatis.md +++ b/Mybatis/Mybatis.md @@ -1,27 +1,95 @@ -## 1. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的? -1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。 解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。 +## 1. MyBatis是什么? -2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 +* Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。 +* 作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 +* 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 +* 由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。 -3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 解决: Mybatis自动将java对象映射至sql语句。 +## 2. Mybaits的优缺点 -4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 解决:Mybatis自动将sql执行结果映射至java对象。 +优点: + +* 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 +* 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; +* 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 +* 能够与Spring很好的集成; +* 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 + +缺点: + +* SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 +* SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 + +## 3. 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 + +而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +## 4. Hibernate 和 MyBatis 的区别 + +**相同点**:都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。 + +**不同点** + +1、映射关系 + +MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单。 + +Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂。 + +2、 SQL优化和移植性 + +Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 +MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。 + +3、开发难易程度和学习成本 + +Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 + +MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 + +**总结** + +MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架, + +Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。 + +## 5. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的? + +1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。 + +解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。 + +2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 + +解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 + +3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 + +解决: Mybatis自动将java对象映射至sql语句。 + +4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 + +解决:Mybatis自动将sql执行结果映射至java对象。 + +## 6. MyBatis编程步骤是什么样的? -## 2. MyBatis编程步骤是什么样的? 1、创建SqlSessionFactory 2、通过SqlSessionFactory创建SqlSession 3、 通过sqlsession执行数据库操作 4、 调用session.commit()提交事务 5、 调用session.close()关闭会话 -## 3. MyBatis与Hibernate有哪些不同? +## 7. MyBatis与Hibernate有哪些不同? + 1、Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要 程序员自己编写 Sql 语句。 2、Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常 适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需 求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性, 如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。 3、Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的 软件,如果用 hibernate 开发可以节省很多代码,提高效率 -## 4. Mybaits 的优点: +## 8. Mybaits 的优点: 1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任 何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。 @@ -31,59 +99,87 @@ 4、能够与 Spring 很好的集成; 5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射 标签,支持对象关系组件维护 -## 5. MyBatis 框架的缺点: +## 9. MyBatis 框架的缺点: 1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。 2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 -## 6. #{}和${}的区别? -{}是预编译处理,${}是字符串替换。 -Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; +## 10. #{}和${}的区别? -Mybatis在处理${}时,就是把${}替换成变量的值。 +* #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。 +* Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。 +* Mybatis在处理时 , 是 原 值 传 入 , 就 是 把 {}时,是原值传入,就是把时,是原值传入,就是把{}替换成变量的值,相当于JDBC中的Statement编译 +* 变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上单引号 ‘’ +* #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入 +* #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外 -使用#{}可以有效的防止SQL注入,提高系统安全性。 +## 11. 通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗? -## 7. 通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗? Dao接口即Mapper接口。接口的全限名就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名的拼接字符串作为key值,可唯一定位一个MapperStatement。 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 -## 8. 在Mapper中如何传递多个参数? + +## 12. 在Mapper中如何传递多个参数? + 1、若Dao层函数有多个参数,那么其对应的xml中,#{0}代表接收的是Dao层中的第一个参数,#{1}代表Dao中的第二个参数,以此类推。 2、使用@Param注解:在Dao层的参数中前加@Param注解,注解内的参数名为传递到Mapper中的参数名。 3、多个参数封装成Map,以HashMap的形式传递到Mapper中。 -## 9. Mybatis动态sql有什么用?执行原理是什么?有哪些动态sql? + +## 13. Mybatis动态sql有什么用?执行原理是什么?有哪些动态sql? + Mybatis动态sql可以在xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值完成逻辑判断,并动态拼接sql的功能。 Mybatis提供了9种动态sql标签:trim、where、set、foreach、if、choose、when、otherwise、bind -## 10. xml映射文件中,不同的xml映射文件id是否可以重复? + +## 14. xml映射文件中,不同的xml映射文件id是否可以重复? + 不同的xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复; 原因是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也不同。 -## 11. Mybatis实现一对一有几种方式?具体是怎么操作的? + +## 15. Mybatis实现一对一有几种方式?具体是怎么操作的? + 有联合查询和嵌套查询两种方式。 联合查询是几个表联合查询,通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过association配置,但另外一个表的查询是通过select配置的。 -## 12. Mybatis实现一对多有几种方式?具体是怎么操作的? + +## 16. Mybatis实现一对多有几种方式?具体是怎么操作的? + 有联合查询和嵌套查询两种方式。 联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过collection,但另外一个表的查询是通过select配置的。 -## 13. Mybatis的一级、二级缓存 + +## 17. Mybatis的一级、二级缓存 + 1、 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。 2、 二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(namespace),并且可自定义存储源,如Ehcache。默认打不开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespace)进行了增/删/改操作后,默认该作用域下所有select中的缓存将被clear。 -## 14. 使用MyBatis的Mapper接口调用时有哪些要求? + +## 18. 使用MyBatis的Mapper接口调用时有哪些要求? + 1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同; 2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType类型相同; 3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同; -4、Mapper.xml文件中的namespace即是mapper接口的类路径。 \ No newline at end of file +4、Mapper.xml文件中的namespace即是mapper接口的类路径。 + +## 19. Mybatis动态sql是做什么的?都有哪些动态sql? + +Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。 + +其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。 + +## 20. Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 + +原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 \ No newline at end of file diff --git a/Netty.md b/Netty.md new file mode 100644 index 0000000..7995495 --- /dev/null +++ b/Netty.md @@ -0,0 +1,31 @@ +1.Netty 是什么? +Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的,它封装了jdk的nio,让我们使用起来更加方法灵活。 + +2.Netty 的特点是什么? +高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。 +传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。 +封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。 +3.Netty 的优势有哪些? +使用简单:封装了 NIO 的很多细节,使用更简单。 +功能强大:预置了多种编解码功能,支持多种主流协议。 +定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。 +性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。 +稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。 +社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。 + +4. Netty 的高性能表现在哪些方面? +心跳,对服务端:会定时清除闲置会话 inactive(netty5),对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中 idleStateHandler 类 用来检测会话状态 + +串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。 + +可靠性,链路有效性检测:链路空闲检测机制,读/写空闲超时机制;内存保护机制:通过内存池重用 ByteBuf;ByteBuf 的解码保护;优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。 + +Netty 安全性:支持的安全协议:SSL V2 和 V3,TLS,SSL 单向认证、双向认证和第三方 CA证。 + +高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议(Http)、线程(主从多线程) + +流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务流中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。 + +TCP 参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K; + +SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法; diff --git a/README.md b/README.md index 34897cc..382b345 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # Java-Interview -「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等 +「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等。 +> 在线阅读:https://www.javalearn.cn/ +
+


@@ -14,8 +17,11 @@

+ + +
👍推荐免费下载几百本计算机经典 电子书 + + ## 更多 @@ -40,7 +46,7 @@ ### Java基础 -* [Java基础上](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80.md) +* [Java基础上](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80%E4%B8%8A.md) * [Java基础下](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80%E4%B8%8B.md) ### 集合 * [Java集合高频面试题](https://github.com/cosen1024/Java-Interview/blob/main/Java%E9%9B%86%E5%90%88/Java%E9%9B%86%E5%90%88%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E9%A2%98.md) @@ -55,7 +61,8 @@ ### Java并发 -* [Java多线程面试-基础](https://github.com/cosen1024/Java-Interview/blob/main/Java并发/Java多线程面试-基础.md) +* [Java多线程面试-总结版](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%B9%B6%E5%8F%91/Java%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%80%BB%E7%BB%93%E7%89%88.md) + * [进程通信和线程通信的方式](https://github.com/cosen1024/Java-Interview/blob/main/Java并发/进程通信和线程通信的方式.md) * [如何设计线程池](https://github.com/cosen1024/Java-Interview/blob/main/Java并发/如何设计线程池.md) @@ -63,6 +70,7 @@ * [AQS原理](https://github.com/cosen1024/Java-Interview/blob/main/Java并发/AQS.md) ### MySQL +* [MySQL基础、锁、事务、分库分表、优化](https://github.com/cosen1024/Java-Interview/blob/main/MySQL/MySQL%E5%9F%BA%E7%A1%80%E3%80%81%E9%94%81%E3%80%81%E4%BA%8B%E5%8A%A1%E3%80%81%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E3%80%81%E4%BC%98%E5%8C%96.md) * [MySQL基础](https://github.com/cosen1024/Java-Interview/blob/main/MySQL/MySQL.md) @@ -75,11 +83,22 @@ * [Spring](https://github.com/cosen1024/Java-Interview/blob/main/Spring/Spring.md) ## 计算机网络 -* [计算机网络上](https://github.com/cosen1024/Java-Interview/blob/main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.md) +* [计算机网络上](https://github.com/cosen1024/Java-Interview/blob/main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E4%B8%8A.md) * [计算机网络下](https://github.com/cosen1024/Java-Interview/blob/main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E4%B8%8B.md) ## 操作系统 * [操作系统](https://github.com/cosen1024/Java-Interview/blob/main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md) +## MQ +* [Kafka](https://github.com/cosen1024/Java-Interview/blob/main/MQ/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98.md) + +* [RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) + +## Dubbo +* [Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) + +## 分布式 +* [分布式](/分布式/面试题.md) + ## 关于我 -[库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) \ No newline at end of file +[库森的校招经历](https://mp.weixin.qq.com/s?__biz=MzkyMTI3Mjc2MQ==&tempkey=MTIxNV81aG91ZWFjc0E3SWQzUCtmY2ZRQ3I1QXJ0MjZPcndKU3FVdDhqWVI2dGh0bDBCTE9ZTGxWamhUcmFuODZmRFhubXBQMGtCVHd3UUhPTkRHYVh0cTZJaHhSeUx2aTJkUXJocUtSVVpVUU5qaGZoYUdFVFNPOG15X2tGbWFVM1g5bFVQVlo2SGZmbGVtdjVSU2RVZTlhSW9zT1NtcjFHeG1nNzhQbUlRfn4%3D&chksm=c184814ef6f308589c207f852920ac287d1d5eb7b05846483b6e7699448a8bf2578b7a03bc33&token=483239364&lang=zh_CN#rd) diff --git a/Redis/Redis.md b/Redis/Redis.md index 64945ab..7ada9b1 100644 --- a/Redis/Redis.md +++ b/Redis/Redis.md @@ -1,81 +1,252 @@ -## 1. Redis是什么?**简述它的优缺点?** -Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。 +# 概述 -因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 +## 1. Redis是什么?简述它的优缺点? -Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能。 +Redis本质上是一个Key-Value类型的内存数据库,很像Memcached,整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存。 -比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。 +因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value 数据库。 -另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。 Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 +**优点**: + +* 读写性能极高, Redis能读的速度是110000次/s,写的速度是81000次/s。 +* 支持数据持久化,支持AOF和RDB两种持久化方式。 +* 支持事务, Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。 +* 数据结构丰富,除了支持string类型的value外,还支持hash、set、zset、list等数据结构。 +* 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。 +* 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等特性。 + +**缺点**: + +* 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 +* 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。 ## 2. Redis为什么这么快? -1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1); +- 内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。 + +- 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。 + +- 非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。 + +- 优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。 + +- 使用底层模型不同:Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。 + +> Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。 +> +> Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个RedisServer上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。**需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现。** + +## 3. Redis相比Memcached有哪些优势? + +* 数据类型:Memcached所有的值均是简单的字符串,Redis支持更为丰富的数据类型,支持string(字符串),list(列表),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。 + +* 持久化:Redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 memcache不支持数据持久存储 。 + +* 集群模式:Redis提供主从同步机制,以及 Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据 + +* 性能对比:Redis的速度比Memcached快很多。 + +* 网络IO模型:Redis使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞IO模式。 + +* Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。 + + 这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。 + +## 4. 为什么要用 Redis 做缓存? + +**从高并发上来说:** + +- 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 + +**从高性能上来说:** + +- 用户第一次访问数据库中的某些数据。 因为是从硬盘上读取的所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据。 + +## 5. 为什么要用 Redis 而不用 map/guava 做缓存? + +缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 + +使用Redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上较为复杂。 + +对比: + +* Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了; +* Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了; +* Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里; +* Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象; +* Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了; +* Redis可单独部署,多个项目之间可以共享,本地内存无法共享; +* Redis有专门的管理工具可以查看缓存数据。 + +## 6. Redis的常用场景有哪些? + +**1、缓存** + +缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。 + +**2、排行榜** + +很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。 + +**3、计数器** + +什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。 + +**4、分布式会话** + +集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 + +**5、分布式锁** + +在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。 + +**6、 社交网络** + +点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出。 + +**7、最新列表** + +Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。 + +**8、消息系统** + +消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。 + +## 7. Redis的数据类型有哪些? + +有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。 + +**五种常用的数据类型**: + +1、String:String是最常用的一种数据类型,普通的key- value 存储都可以归为此类。其中Value既可以是数字也可以是字符串。使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。 + +2、Hash:Hash 是一个键值(key => value)对集合。Redishash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。 + +3、Set:Set是一个无序的天然去重的集合,即Key-Set。此外还提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注什么的功能实现特别方便。 + +4、List:List是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,因此支持正向、反向双重查找。通过List,我们可以很方面的获得类似于最新回复这类的功能实现。 + +5、SortedSet:类似于java中的TreeSet,是Set的可排序版。此外还支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。 + +**三种特殊的数据类型**: + +1、Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。使用Bitmap实现统计功能,更省空间。如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。 + +2、Hyperloglog。HyperLogLog 是一种用于统计基数的数据集合类型,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大 + +时,计算基数所需的空间总是固定 的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。场景:统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。 + +要注意,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。 + +3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附近的人、打车距离计算等。 + +# 持久化 + +## 8. Redis持久化机制? + +为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写入到磁盘空间中,即持久化。 + +Redis提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照`RDB`,另一种叫只追加文件`AOF`。 + +**RDB** + +在指定的时间间隔内将内存中的数据集快照写入磁盘(`Snapshot`),它恢复时是将快照文件直接读到内存里。 + +**优势**:适合大规模的数据恢复;对数据完整性和一致性要求不高 + +**劣势**:在一定间隔时间做一次备份,所以如果Redis意外`down`掉的话,就会丢失最后一次快照后的所有修改。 + +**AOF** + +以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 + +AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.。 + +**优势** + +- 每修改同步:`appendfsync always` 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 +- 每秒同步:`appendfsync everysec` 异步操作,每秒记录,如果一秒内宕机,有数据丢失 +- 不同步:`appendfsync no` 从不同步 + +**劣势** + +- 相同数据集的数据而言`aof`文件要远大于`rdb`文件,恢复速度慢于`rdb` +- `aof`运行效率要慢于`rdb`,每秒同步策略效率较好,不同步效率和`rdb`相同 + +## 9. 如何选择合适的持久化方式 + +- 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。 +- 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB。 +- 如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失。 + +**补充:Redis4.0 对于持久化机制的优化** + +Redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。 -2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的; +简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: -3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; +![](https://images2018.cnblogs.com/blog/1075473/201807/1075473-20180726181756270-1907770368.png) + +**优势**:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 + +**劣势**:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。 -4、使用多路I/O复用模型,非阻塞IO; +## 10. Redis持久化数据和缓存怎么做扩容? -5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求; +* 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。 -## 3. **Redis相比memcached有哪些优势?** +* 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。 -* redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 -* Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。 -* 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的. -* Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。 +# 过期键的删除策略、淘汰策略 -## 4. Redis 的数据类型? +## 11. Redis过期键的删除策略 -Redis 支持五种数据类型: string( 字符串),hash( 哈希), list( 列表), set( 集合) 及 zsetsorted set: 有序集合)。 +**Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用。** -- string:redis 中字符串 value 最大可为512M。可以用来做一些计数功能的缓存(也是实际工作中最常见的)。 常规计数:微博数,粉丝数等。 -- list:简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。可以实现一个简单消息队列功能,做基于redis的分页功能等。(另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。) -- set:是一个字符串类型的无序集合。可以用来进行全局去重等。(比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程) -- sorted set:是一个字符串类型的有序集合,给每一个元素一个固定的分数score来保持顺序。可以用来做排行榜应用或者进行范围查找等。 -- hash:键值对集合,是一个字符串类型的 Key和 Value 的映射表,也就是说其存储的Value是一个键值对(Key- Value)hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。 +**惰性删除**:惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回 null 给客户端,如果没有过期则返回正常信息给客户端。它的优点是简单,不需要对过期的数据做额外的处理,只有在每次访问的时候才会检查键值是否过期,缺点是删除过期键不及时,造成了一定的空间浪费。 -我们实际项目中比较常用的是 string,hash。 如果你是 Redis 的高级用户,还需要加上下面几种数据结构 HyperLogLog、Geo、Pub/Sub。 +**定期删除**:Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。 -如果你说还玩过 Redis Module,像 BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。 +附:删除key常见的三种处理方式。 -## 5. Redis的常用场景? +**1、定时删除** -1、会话缓存( Session Cache) +在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。 -最常用的一种使用 Redis 的情景是会话缓存( session cache)。用 Redis 缓存会话比其他存储( 如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时, 如果用户的购物车信息全部丢失, 大部分人都会不高兴的, 现在, 他们还会这样吗? 幸运的是, 随着 Redis 这些年的改进, 很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。 +优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。 -2、全页缓存( FPC) +缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。 -除基本的会话 token 之外, Redis 还提供很简便的 FPC 平台。回到一致性问题, 即使重启了 Redis 实例, 因为有磁盘的持久化, 用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。 此外, 对 WordPress 的用户来说, Pantheon 有一个非常好的插件 wp-redis, 这个插件能帮助你以最快速度加载你曾浏览过的页面。 +**2、惰性删除** -3、队列 +设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 -Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作, 这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言( 如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google 中搜索" Redis queues", 你马上就能找到大量的开源项目, 这些项目的目的就是利用 Redis 创建非常好的后端工具, 以满足各种队列需求。例如, Celery 有一个后台就是使用 Redis 作为 broker, 你可以从这里去查看。 +优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。 -4, 排行榜/计数器 +缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏。 -Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合( Set) 和有序集合( Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以, 我们要从排序集合中获取到排名最靠前的 10 个用户– 我们称之为" user_scores", 我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数, 你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子, 用 Ruby 实现的, 它的排行榜就是使用 Redis 来存储数据的, 你可以在这里看到。 +**3、定期删除** -5、发布/订阅 +每隔一段时间,我们就对一些key进行检查,删除里面过期的key。 -发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用, 还可作为基于发布/订阅的脚本触发器, 甚至用 Redis 的发布/订阅功能来建立聊天系统! +优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。 -## 6. redis 过期键的删除策略? +缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。 -1、定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时, 立即执行对键的删除操作。 +## 12. Redis key的过期时间和永久有效分别怎么设置? -2、惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是 否过期, 如果过期的话, 就删除该键;如果没有过期, 就返回该键。 +通过expire或pexpire命令,客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间。 -3、定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至 于要删除多少过期键, 以及要检查多少个数据库, 则由算法决定。 +与expire和pexpire命令类似,客户端可以通过expireat和pexpireat命令,以秒或毫秒精度给数据库中的某个键设置过期时间,可以理解为:让某个键在某个时间点过期。 -## 7. redis 内存淘汰机制? +## 13. Redis内存淘汰策略 -redis v4.0前提供 6种数据淘汰策略: +Redis是不断的删除一些过期数据,但是很多没有设置过期时间的数据也会越来越多,那么Redis内存不够用的时候是怎么处理的呢?答案就是淘汰策略。此类的 + +当Redis的内存超过最大允许的内存之后,Redis会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器的正常运行。 + +**Redisv4.0前提供 6种数据淘汰策略**: - volatile-lru:利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used ) - allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的) @@ -84,399 +255,891 @@ redis v4.0前提供 6种数据淘汰策略: - allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 - no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! -**redis v4.0后增加以下两种**: +**Redisv4.0后增加以下两种**: - volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到) - allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。 -## 8. redis 持久化机制 +内存淘汰策略可以通过配置文件来修改,Redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction。 -为了能够重用`Redis`数据,或者防止系统故障,我们需要将`Redis`中的数据写入到磁盘空间中,即持久化。`Redis`提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照`RDB`,另一种叫只追加文件`AOF` +# 缓存异常 -### RDB +> 缓存异常有四种类型,分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透。 -在指定的时间间隔内将内存中的数据集快照写入磁盘(`Snapshot`),它恢复时是将快照文件直接读到内存里。 +## 14. 如何保证缓存与数据库双写时的数据一致性? - `Redis`会单独创建(`fork`)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那`RDB`方式要比`AOF`方式更加的高效。`RDB`的缺点是最后一次持久化后的数据可能丢失。 +> 背景:使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。 -`Fork`的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。**`Rdb` 保存的是`dump.rdb`文件。** +共有四种方案: -**优势** +1. 先更新数据库,后更新缓存 +2. 先更新缓存,后更新数据库 +3. 先删除缓存,后更新数据库 +4. 先更新数据库,后删除缓存 -适合大规模的数据恢复 -对数据完整性和一致性要求不高 +第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。 -**劣势** +第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。 -在一定间隔时间做一次备份,所以如果`redis`意外`down`掉的话,就会丢失最后一次快照后的所有修改。 +目前主要用第三和第四种方案。 -### AOF(Append Only File) +## 15. 先删除缓存,后更新数据库 -以日志的形式来记录每个写操作,将`Redis`执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,`redis`启动之初会读取该文件重新构建数据,换言之,`redis`重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。**AOF保存的是appendonly.aof文件** +该方案也会出问题,此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作) -#### AOF重写 +1. 请求A进行写操作,删除缓存 +2. 请求B查询发现缓存不存在 +3. 请求B去数据库查询得到旧值 +4. 请求B将旧值写入缓存 +5. 请求A将新值写入数据库 -AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。 +上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。 -AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据, 每条记录有一条的set语句。重写aof文件的操作,并没有读取旧的aof文件, 而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。 +### 答案一:延时双删 -**优势** +最简单的解决办法延时双删 -- 每修改同步:`appendfsync always` 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 -- 每秒同步:`appendfsync everysec` 异步操作,每秒记录,如果一秒内宕机,有数据丢失 -- 不同步:`appendfsync no` 从不同步 +使用伪代码如下: -**劣势** +```java +public void write(String key,Object data){ + Redis.delKey(key); + db.updateData(data); + Thread.sleep(1000); + Redis.delKey(key); + } +``` -- 相同数据集的数据而言`aof`文件要远大于`rdb`文件,恢复速度慢于`rdb` -- `aof`运行效率要慢于`rdb`,每秒同步策略效率较好,不同步效率和`rdb`相同 +转化为中文描述就是 +(1)先淘汰缓存 +(2)再写数据库(这两步和原来一样) +(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。 -### Redis 4.0 对于持久化机制的优化 +如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。 -redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。 +![主从同步时间差](http://blog-img.coolsen.cn/img/1735bb5881bbb1d4~tplv-t2oaga2asx-watermark.awebp) -混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: +此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作) -![](https://images2018.cnblogs.com/blog/1075473/201807/1075473-20180726181756270-1907770368.png) +1. 请求 A 更新操作,删除了 Redis +2. 请求主库进行更新操作,主库与从库进行同步数据的操作 +3. 请 B 查询操作,发现 Redis 中没有数据 +4. 去从库中拿去数据 +5. 此时同步数据还未完成,拿到的数据是旧数据 -**优点** +此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。 -混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 -**缺点** -兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。 +![从主库中拿数据](http://blog-img.coolsen.cn/img/1735bb5881a19fec~tplv-t2oaga2asx-watermark.awebp) -## 9. 缓存雪崩、缓存击穿和缓存穿透问题解决方案 +### 答案二: **更新与读取操作进行异步串行化** -### 缓存击穿 +采用**更新与读取操作进行异步串行化** -举例:redis中存储的是热点数据,当高并发请求访问redis中热点数据的时候,如果redis中的数据过期了,会造成缓存击穿的现象,请求都打到了数据库上。 +**异步串行化** -解决办法:使用互斥锁,只让一个请求去load DB,成功之后重新写缓存,其余请求没有获取到互斥锁,可以尝试重新获取缓存中的数据。。 +我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。 -### 缓存穿透 +这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。 -缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上(举例:故意的去请求缓存中不存在的数据,导致请求都打到了数据库上,导致数据库异常。) +**读操作去重** -解决办法 +多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。 -1.首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等 -2.缓存无效 key :如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 -3.布隆过滤器.把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程 +如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存) -![](http://blog-img.coolsen.cn/img/1584173117136_7.png) +## 16. 先更新数据库,后删除缓存 -### 缓存雪崩 +这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。 -缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉 +![先更新数据库,后删除缓存](http://blog-img.coolsen.cn/img/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp) -- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。 -- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 -- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 +此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下: -![](http://blog-img.coolsen.cn/img/1584172719093_6.png) +1. 请求 A 先对数据库进行更新操作 +2. 在对 Redis 进行删除操作的时候发现报错,删除失败 +3. 此时将Redis 的 key 作为消息体发送到消息队列中 +4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作 +但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。 +![利用订阅 binlog 删除缓存](http://blog-img.coolsen.cn/img/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp) -## 10. 为什么 Redis 不支持回滚(roll back) +## 17. 什么是缓存击穿? -- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。 -- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 +缓存击穿跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。 -有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 [INCR key](http://redisdoc.com/string/incr.html#incr) 命令将键的值加上 `1` , 却不小心加上了 `2` , 又或者对错误类型的键执行了 [INCR key](http://redisdoc.com/string/incr.html#incr) , 回滚是没有办法处理这些情况的。 +从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。 -鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。 +解决方案: -## 11. Redis中跳跃表(skiplist)实现原理 +* 在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降 -跳表(skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,其效率可比拟于二叉查找树。 +* 热点数据缓存永远不过期。永不过期实际包含两层意思: + * 物理不过期,针对热点key不设置过期时间 + * 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建 -### 跳跃表来源 +## 18. 什么是缓存穿透? -跳跃表在 1990 年由 William Pugh 提出,而红黑树早在 1972 年由鲁道夫·贝尔发明了。红黑树在空间和时间效率上略胜跳表一筹,但跳跃表实现相对简单得到程序猿们的青睐。Redis 和 Leveldb 中都有采用跳跃表。 +缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。 -以下是个典型的跳跃表例子 +> 缓存穿透的关键在于在Redis中查不到key值,它和缓存击穿的根本区别在于传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示。 -image-20200714130333639 +![img](http://blog-img.coolsen.cn/img/2021013117512340.png) -按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。 +解决方法: -但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。 +* 将无效的key存放进Redis中: -下面看Redis 跳跃表的实现,如何解决的这个问题。 +当出现Redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置value="null",并设置其过期时间极短,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。但这种处理方式是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。 -### Redis 跳跃表的实现 +* 使用布隆过滤器: -为了满足自身的功能需要, Redis 基于 William Pugh 论文中描述的跳跃表进行了以下修改: +如果布隆过滤器判定某个 key 不存在布隆过滤器中,那么就一定不存在,如果判定某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一个布隆过滤器,将数据库中的所有key都存储在布隆过滤器中,在查询Redis前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,不让其访问数据库,从而避免了对底层存储系统的查询压力。 -1. 允许重复的 `score` 值:多个不同的 `member` 的 `score` 值可以相同。 -2. 进行对比操作时,不仅要检查 `score` 值,还要检查 `member` :当 `score` 值可以重复时,单靠 `score` 值无法判断一个元素的身份,所以需要连 `member` 域都一并检查才行。 -3. 每个节点都带有一个高度为 1 层的后退指针,用于从表尾方向向表头方向迭代:当执行 [ZREVRANGE](http://redis.readthedocs.org/en/latest/sorted_set/zrevrange.html#zrevrange) 或 [ZREVRANGEBYSCORE](http://redis.readthedocs.org/en/latest/sorted_set/zrevrangebyscore.html#zrevrangebyscore) 这类以逆序处理有序集的命令时,就会用到这个属性。 +> 如何选择:针对一些恶意攻击,攻击带过来的大量key是随机,那么我们采用第一种方案就会缓存大量不存在key的数据。那么这种方案就不合适了,我们可以先对使用布隆过滤器方案进行过滤掉这些key。所以,针对这种key异常多、请求重复率比较低的数据,优先使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,则可优先采用第一种方式进行缓存。 -跳跃表的结构定义: +## 19. 什么是缓存雪崩? -```c -typedef struct zskiplist { +如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。 - // 头节点,尾节点 - struct zskiplistNode *header, *tail; +造成缓存雪崩的关键在于同一时间的大规模的key失效,主要有两种可能:第一种是Redis宕机,第二种可能就是采用了相同的过期时间。 - // 节点数量 - unsigned long length; +解决方案: - // 目前表内节点的最大层数 - int level; +1、事前: -} zskiplist; -``` +* 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。如把每个Key的失效时间都加个随机值,`setRedis(Key,value,time + Math.random() * 10000);`,保证数据不会在同一时间大面积失效。 -跳跃表的节点定义: +* 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。 -```c -typedef struct zskiplistNode { +* 热点数据缓存永远不过期。永不过期实际包含两层意思: + * 物理不过期,针对热点key不设置过期时间 + * 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建 - // member 对象 - robj *obj; +* 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 主从+ 哨兵,Redis集群来避免 Redis 全盘崩溃的情况。 - // 分值 - double score; +2、事中: - // 后退指针 - struct zskiplistNode *backward; +* 互斥锁:在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降 - // 层 - struct zskiplistLevel { +* 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回"系统拥挤"之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。 - // 前进指针 - struct zskiplistNode *forward; +3、事后: - // 这个层跨越的节点数量 - unsigned int span; +开启Redis持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。 - } level[]; +## 20. 什么是缓存预热? -} zskiplistNode; -``` +缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。 + +如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。 + +缓存预热解决方案: + +* 数据量不大的时候,工程启动的时候进行加载缓存动作; + +* 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新; + +* 数据量太大的时候,优先保证热点数据进行提前加载到缓存。 + +## 21. 什么是缓存降级? + +缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。 + +在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案: + +* 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; + +* 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; + +* 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; + +* 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。 + +# 线程模型 + +## 22. Redis为何选择单线程? + +在Redis 6.0以前,Redis的核心网络模型选择用单线程来实现。先来看下官方的回答: + +> It's not very frequent that CPU becomes your bottleneck with Redis, as usually Redisis either memory or network bound. For instance, using pipelining Redisrunning on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU. + +核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,Redis真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型。 + +实际上更加具体的选择单线程的原因如下: + +* 避免过多的上下文切换开销:如果是单线程则可以规避进程内频繁的线程切换开销,因为程序始终运行在进程中单个线程内,没有多线程切换的场景。 +* 避免同步机制的开销:如果 Redis选择多线程模型,又因为 Redis是一个数据库,那么势必涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁,而我们知道 Redis不仅仅提供了简单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同,可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能。 +* 简单可维护:如果 Redis使用多线程模式,那么所有的底层数据结构都必须实现成线程安全的,这无疑又使得 Redis的实现变得更加复杂。 + +总而言之,Redis选择单线程可以说是多方博弈之后的一种权衡:在保证足够的性能表现之下,使用单线程保持代码的简单和可维护性。 + +## 23. Redis真的是单线程? + +讨论 这个问题前,先看下 Redis的版本中两个重要的节点: + +1. Redisv4.0(引入多线程处理异步任务) +2. Redis 6.0(在网络模型中实现多线程 I/O ) + +所以,网络上说的Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程。 + +且Redis6.0引入**多线程I/O**,只是用来**处理网络数据的读写和协议的解析**,而**执行命令依旧是单线程**。 + +> Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。 +> +> 在 Redisv4.0 之后增加了一些的非阻塞命令如 `UNLINK`、`FLUSHALL ASYNC`、`FLUSHDB ASYNC`。 + +## 24. Redis 6.0为何引入多线程? + +很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。 + +随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向: + +- 优化网络 I/O 模块 +- 提高机器内存读写的速度 + +后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向: + +- 零拷贝技术或者 DPDK 技术 +- 利用多核优势 + +零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。 + +总结起来,Redis支持多线程主要就是两个原因: + +* 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 + +* 多线程任务可以分摊 Redis 同步 IO 读写负荷 + +## 25. Redis 6.0 采用多线程后,性能的提升效果如何? + +Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。 + +国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。 + +## 26. 介绍下Redis的线程模型 + +Redis的线程模型包括Redis 6.0之前和Redis 6.0。 + +下面介绍的是Redis 6.0之前。 + +Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。 + +> IO多路复用是 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计的。多路指的是多个 Socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:Select,Poll,Epoll。 +> +> Epoll 是最新的也是目前最好的多路复用技术。 + +模型如下图: + +![202105092153018231.png](http://blog-img.coolsen.cn/img/202105092153018231.png) + +文件事件处理器的结构包含了四个部分: + +- 多个 Socket。Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件: + - 当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个 AE_READABLE 事件 + - 当 socket 变得可写时,socket 就会产生一个 AE_WRITABLE 事件。 +- IO 多路复用程序 +- 文件事件分派器 +- 事件处理器。事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的 socket 事件: + - 如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器 + - 如果是客户端要写数据到 Redis(读、写请求命令),那么会为 socket 关联命令请求处理器 + - 如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复处理器 + +多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。 + +下图是客户端与 Redis 通信的一次完整的流程: + +![202105092153019692.png](http://blog-img.coolsen.cn/img/202105092153019692.png) + +1. Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。 +2. 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。 +3. 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。 +4. 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。 +5. 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。 + +## 27. Redis 6.0 多线程的实现机制? + +**流程简述如下**: + +- 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。 +- 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。 +- 主线程阻塞等待 IO 线程读取 Socket 完毕。 +- 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。 +- 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。 + + +![image-20210828175543973](http://blog-img.coolsen.cn/img/image-20210828175543973.png) + +**该设计有如下特点**: + +- IO 线程要么同时在读 Socket,要么同时在写,不会同时读或写。 +- IO 线程只负责读写 Socket 解析命令,不负责命令处理。 -image-20200714131425529 +## 28. Redis 6.0开启多线程后,是否会存在线程并发安全问题? -上图就是跳跃列表的示意图,图中只画了3层,Redis 的跳跃表共有 64 层,容纳 2^64 个元素应该不成问题。 +从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。 -跳表的性质: +所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。 -1. 由很多层结构组成 -2. 每一层都是一个有序的链表 -3. 最底层(Level 1) 的链表包含所有元素 -4. 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。 -5. 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。 +## 29. Redis 6.0 与 Memcached 多线程模型的对比 -### 随机层数的设计 +- **相同点:**都采用了 Master 线程 -Worker 线程的模型。 -Redis 使用随机层数,解决插入、删除时,时间复杂度重新蜕化成O(n)的问题 +- **不同点**:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。 -它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。 + 而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。 -image-20200714140215699 +# 事务 -插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度,这让它在插入性能上明显优于平衡树。 +## 30. Redis事务的概念 -#### 随机层数的计算方式 +Redis的事务并不是我们传统意义上理解的事务,我们都知道 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis **事务的执行并不是原子性的**。 -执行插入操作时计算随机数的过程,是一个很关键的过程,它对skiplist的统计特性有着很重要的影响。这并不是一个普通的服从均匀分布的随机数,它的计算过程如下: +事务可以理解为一个**打包的批量执行脚本**,但**批量指令并非原子化**的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。 -- 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。 -- 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。 -- 节点最大的层数不允许超过一个最大值,记为MaxLevel。 +**总结:** -这个计算随机层数的伪码如下所示: + 1. Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。**鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的**。 + + 2. Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 + + 3. 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。 + +> 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。 + +## 31. Redis事务的三个阶段 + +1. multi 开启事务 +2. 大量指令入队 +3. exec执行事务块内命令,**截止此处一个事务已经结束。** +4. discard 取消事务 +5. watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。 + +事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队. + +## 32. Redis事务相关命令 + +Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 + +* WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 +* MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 +* EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。 + 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 +* UNWATCH命令可以取消watch对所有key的监控。 + +## 33. Redis事务支持隔离性吗? + +Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,**Redis 的事务是总是带有隔离性的**。 + +## 34. Redis为什么不支持事务回滚? + +* Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中. +* 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 + +## 35. Redis事务其他实现 + +* 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, + 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。 +* 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。 + +# 主从、哨兵、集群 + +## 36. Redis常见使用方式有哪些? + +Redis的几种常见使用方式包括: + +* Redis单副本; +* Redis多副本(主从); +* Redis Sentinel(哨兵); +* Redis Cluster; +* Redis自研。 + +使用场景: + +如果数据量很少,主要是承载高并发高性能的场景,比如缓存一般就几个G的话,单机足够了。 + +主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。 + +哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。 + +Redis cluster 主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有master的容量总和就是Redis cluster可缓存的数据容量。 + +## 37. 介绍下Redis单副本 + +Redis单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。 + +![image-20210829103307048](http://blog-img.coolsen.cn/img/image-20210829103307048.png) + +**优点:** + +* 架构简单,部署方便; +* 高性价比:缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务; +* 高性能。 + +**缺点:** + +* 不保证数据的可靠性; +* 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务; +* 高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用Memcached替代。 + +## 38. 介绍下Redis多副本(主从) + +Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。 + +![image-20210829103327631](http://blog-img.coolsen.cn/img/image-20210829103327631.png) + +**优点:** + +* 高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题; +* 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。 + +**缺点:** + +* 故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐; +* 主库的写能力受到单机的限制,可以考虑分片; +* 主库的存储能力受到单机的限制,可以考虑Pika; + +* 原生复制的弊端在早期的版本中也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。 + +## 39. 介绍下Redis Sentinel(哨兵) + +> 主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这种方式并不推荐,实际生产中,我们优先考虑哨兵模式。这种模式下,master 宕机,哨兵会自动选举 master 并将其他的 slave 指向新的 master。 + +Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。 + +其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个。 + +![image-20210829103343110](http://blog-img.coolsen.cn/img/image-20210829103343110.png) + +**优点:** + +* Redis Sentinel集群部署简单; +* 能够解决Redis主从模式下的高可用切换问题; +* 很方便实现Redis数据节点的线形扩展,轻松突破Redis自身单线程瓶颈,可极大满足Redis大容量或高性能的业务需求; +* 可以实现一套Sentinel监控一组Redis数据节点或多组数据节点。 + +**缺点:** + +* 部署相对Redis主从模式要复杂一些,原理理解更繁琐; +* 资源浪费,Redis数据节点中slave节点作为备份节点不提供服务; +* Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。 +* 不能解决读写分离问题,实现起来相对复杂。 + +## 40. 介绍下Redis Cluster + +> Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 Redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容。 + +Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。 + +Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。 + +Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0〜16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。 + +![image-20210829103444245](http://blog-img.coolsen.cn/img/image-20210829103444245.png) + +**优点:** + +* 无中心架构; +* 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布; +* 可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除; +* 高可用性:部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升; +* 降低运维成本,提高系统的扩展性和可用性。 + +**缺点:** + +* Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的"max redirect exception"。 +* 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。 +* 数据通过异步复制,不保证数据的强一致性。 +* 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。 +* Slave在集群中充当"冷备",不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。 +* Key批量操作限制,如使用mset、mget目前只支持具有相同slot值的Key执行批量操作。对于映射为不同slot值的Key由于Keys不支持跨slot查询,所以执行mset、mget、sunion等操作支持不友好。 +* Key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个Key分布于不同的节点上时无法使用事务功能。 +* Key作为数据分区的最小粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点。 +* 不支持多数据库空间,单机下的Redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db 0。 +* 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。 +* 避免产生hot-key,导致主库节点成为系统的短板。 +* 避免产生big-key,导致网卡撑爆、慢查询等。 +* 重试时间应该大于cluster-node-time时间。 +* Redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。 + +## 41. 介绍下Redis自研 + +Redis自研的高可用解决方案,主要体现在配置中心、故障探测和failover的处理机制上,通常需要根据企业业务的实际线上环境来定制化。 + +![image-20210829103426922](http://blog-img.coolsen.cn/img/image-20210829103426922.png) + +**优点:** + +* 高可靠性、高可用性; +* 自主可控性高; +* 贴切业务实际需求,可缩性好,兼容性好。 + +**缺点:** + +* 实现复杂,开发成本高; +* 需要建立配套的周边设施,如监控,域名服务,存储元数据信息的数据库等; +* 维护成本高。 + +## 42. Redis高可用方案具体怎么实施? + +使用官方推荐的哨兵(sentinel)机制就能实现,当主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。它有四个主要功能: + +- 集群监控,负责监控Redis master和slave进程是否正常工作。 +- 消息通知,如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 +- 故障转移,如果master node挂掉了,会自动转移到slave node上。 +- 配置中心,如果故障转移发生了,通知client客户端新的master地址。 + +## 43. 了解主从复制的原理吗? + +**1、主从架构的核心原理** + +当启动一个slave node的时候,它会发送一个PSYNC命令给master node + +如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization + +开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。 + +slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。 + +**2、主从复制的断点续传** + +从Redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份 + +master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制 + +但是如果没有找到对应的offset,那么就会执行一次resynchronization + +**3、无磁盘化复制** + +master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了 + +repl-diskless-sync repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来 + +**4、过期key处理** + +slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。 + +## 44. 由于主从延迟导致读取到过期数据怎么处理? + +1. 通过scan命令扫库:当Redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥Redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。 +2. Redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了key是否过期以及对主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null。 + +## 45. 主从复制的过程中如果因为网络原因停止复制了会怎么样? + +如果出现网络故障断开连接了,会自动重连的,从Redis 2.8开始,就支持主从复制的断点续传,可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 + +master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。 + +master node会在内存中创建一个`backlog`,master和slave都会保存一个`replica offset`,还有一个`master id`,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制。 + +但是如果没有找到对应的offset,那么就会执行一次`resynchronization`全量复制。 + +## 46. Redis主从架构数据会丢失吗,为什么? + +有两种数据丢失的情况: + +1. 异步复制导致的数据丢失:因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。 +2. 脑裂导致的数据丢失:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。 + +## 47. 如何解决主从架构数据丢失的问题? + +数据丢失的问题是不可避免的,但是我们可以尽量减少。 + +在Redis的配置文件里设置参数 ``` -randomLevel() - level := 1 - // random()返回一个[0...1)的随机数 - while random() < p and level < MaxLevel do - level := level + 1 - return level复制代码 +min-slaves-to-write 1 +min-slaves-max-lag 10 ``` -randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为: +`min-slaves-to-write`默认情况下是0,`min-slaves-max-lag`默认情况下是10。 + +上面的配置的意思是要求至少有1个slave,数据复制和同步的延迟不能超过10秒。如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了。 + +减小`min-slaves-max-lag`参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据。 + +那么对于client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入master来保证数据不丢失;也可以将数据写入kafka消息队列,隔一段时间去消费kafka中的数据。 + +## 48. Redis哨兵是怎么工作的? + +1. 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。 + +2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被当前 Sentinel 标记为主观下线。 + +3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。 + +4. 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。 + +5. 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 )。 + +6. 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。 + +7. sentinel节点会与其他sentinel节点进行"沟通",投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。 + +## 49. 故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么? + +如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息。 + +* 跟master断开连接的时长。 + 如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master. ``` -p = 1/4 -MaxLevel = 32 +( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state ``` -#### skiplist的算法性能分析 +* slave优先级。 + 按照slave优先级进行排序,slave priority越低,优先级就越高 -根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。 +* 复制offset。 + 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高 -当skiplist中有n个节点的时候,它的总层数的概率均值是多少。这个问题直观上比较好理解。根据节点的层数随机算法,容易得出: +* run id + 如果上面两个条件都相同,那么选择一个run id比较小的那个slave。 -- 第1层链表固定有n个节点; -- 第2层链表平均有n*p个节点; -- 第3层链表平均有n*p2个节点; +## 50. 同步配置的时候其他哨兵根据什么更新自己的配置呢? -计算很复杂,没有看懂,总结来说:平均时间复杂度为O(log n) +执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。 -### rank排名计算 +如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch 作为新的version号。 -图中前向指针上面括号中的数字,表示对应的span的值。即当前指针跨越了多少个节点,这个计数不包括指针的起点节点,但包括指针的终点节点。 +这个version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的。 -img +## 51. 为什么Redis哨兵集群只有2个节点无法正常工作? -举例: +哨兵集群必须部署2个以上节点。 -在这个skiplist中查找score=89.0的元素(即Bob的成绩数据),在查找路径中,我们会跨域图中标红的指针,这些指针上面的span值累加起来,就得到了Bob的排名(2+2+1)-1=4(减1是因为rank值以0起始)。需要注意这里算的是从小到大的排名,而如果要算从大到小的排名,只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。 +如果两个哨兵实例,即两个Redis实例,一主一从的模式。 -通过这种方式就能得到一条O(log n)的查找路径 +则Redis的配置quorum=1,表示一个哨兵认为master宕机即可认为master已宕机。 -## 12. Redis为什么用skiplist(跳跃表)而不用平衡树? +但是如果是机器1宕机了,那哨兵1和master都宕机了,虽然哨兵2知道master宕机了,但是这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移。 -Redis的作者 @antirez 从内存占用、对范围查找的支持和实现难易程度做了解释。[原文](https://news.ycombinator.com/item?id=1171423): +但此时哨兵1没了就只有1个哨兵了了,此时就没有majority来允许执行故障转移,所以故障转移不会执行。 -> There are a few reasons: -> -> 1) They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. -> -> 2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. -> -> 3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code. +## 52. Redis cluster中是如何实现数据分布的?这种方式有什么优点? -总结来说: +Redis cluster有固定的16384个hash slot(哈希槽),对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。 -* 不需要太多内存 -* 范围查询和平衡树一样好 -* 容易实现和调试 +Redis cluster中每个master都会持有部分slot(槽),比如有3个master,那么可能每个master持有5000多个hash slot。 -## 13. 假如 Redis 里面有 1 亿个key,其中有 10w 个key 是以某个固定的已知的前缀开头的,如果将它们全部找出来? +hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。每次增加或减少master节点都是对16384取模,而不是根据master数量,这样原本在老的master上的数据不会因master的新增或减少而找不到。并且增加或减少master时Redis cluster移动hash slot的成本是非常低的。 -使用 keys 指令可以扫出指定模式的 key 列表。 +## 53. Redis cluster节点间通信是什么机制? -追问: 如果这个 redis 正在给线上的业务提供服务, 那使用 keys 指令会有什么问题? +Redis cluster节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后U不断地i将元数据发送给其他节点让其他节点进行数据变更。 -这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间, 线上服务会停顿, 直到指令执行完毕, 服务才能恢复。这个时候可以使用 scan 指令, scan 指令可以无阻塞的提取出指定模式的 key 列表, 但是会有一定的重复概率, 在客户端做一次去重就可以了, 但是整体所花费的时间会比直接用 keys 指令长。 +> 节点互相之间不断通信,保持整个集群所有节点的数据是完整的。 +> 主要交换故障信息、节点的增加和移除、hash slot信息等。 -## 14. **Redis6.0为什么要引入多线程呢?** +这种机制的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; -Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。 +缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后。 -但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。 +# 分布式问题 -从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向: +## 54. 什么是分布式锁?为什么用分布式锁? -• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式 • 使用多线程充分利用多核,典型的实现比如 Memcached。 +锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized 、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。 -协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因: +分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。 -• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 • 多线程任务可以分摊 Redis 同步 IO 读写负荷 +思路是:在整个系统提供一个**全局、唯一**的获取锁的"东西",然后每个系统在需要加锁时,都去问这个"东西"拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个"东西",可以是Redis、Zookeeper,也可以是数据库。 -![img](https://obs-emcsapp-public.obs.cn-north-4.myhwclouds.com/wechatSpider/modb_20200720_110226.png) +一般来说,分布式锁需要满足的特性有这么几点: -**Redis6.0采用多线程后,性能的提升效果如何?** +1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁; -Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。 +2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署; -### 实现机制 +3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁; -**流程简述如下**: +4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。 -1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列 +## 55. 常见的分布式锁有哪些解决方案? -2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程 +实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案 -3、主线程阻塞等待 IO 线程读取 socket 完毕 + 1、基于关系型数据库,如MySQL +基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。 -4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行 +缺点: -5、主线程阻塞等待 IO 线程将数据回写 socket 完毕 +* 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 +* 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 +* 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 +* 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 -6、解除绑定,清空等待队列 +2、基于Redis实现 -在这里插入图片描述 +优点: -该设计有如下特点: +Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。 - 1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写 +缺点: -2、IO 线程只负责读写 socket 解析命令,不负责命令处理 +* Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮; +* key 的过期时间设置多少不明确,只能根据实际情况调整; +* 需要自己不断去尝试获取锁,比较消耗性能。 -****开启多线程后,是否会存在线程并发安全问题?** +3、基于zookeeper -Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。 +优点: -## 15. **Redis集群方案应该怎么做?都有哪些方案?** +zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。 -* codis。目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新hash节点。 -* redis cluster3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。具体看官方文档介绍。 -* 在业务代码层实现,起几个毫无关联的redis实例,在代码层,对key 进行hash计算,然后去对应的redis实例操作数据。 这种方式对hash层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。 +缺点: -## 16.讲一讲 位图(Bitmap) +在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。 -位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。 +## 56. Redis实现分布式锁 -![img](https://user-gold-cdn.xitu.io/2018/7/2/1645926f4520d0ce?imageslim) +### 分布式锁的三个核心要素 -当我们要统计月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。这时就可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到月底遍历一次位图就可以得到月度活跃用户数。不过这个方法也是有条件的,那就是 userid 是整数连续的,并且活跃占比较高,否则可能得不偿失。 +1、加锁 -## 17. 讲一讲HyperLogLog +使用setnx来加锁。key是锁的唯一标识,按业务来决定命名,value这里设置为test。 -`HyperLogLog`,下面简称为`HLL`,它是 `LogLog` 算法的升级版,作用是能够提供不精确的去重计数。存在以下的特点: +``` +setx key test +``` -- 代码实现较难。 -- 能够使用极少的内存来统计巨量的数据,在 `Redis` 中实现的 `HyperLogLog`,只需要`12K`内存就能统计`2^64`个数据。 -- 计数存在一定的误差,误差率整体较低。标准误差为 0.81% 。 -- 误差可以被设置`辅助计算因子`进行降低。 +当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败; -### 为什么用HyperLogLog +2、解锁 -如果要实现这么一个功能: +有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。 -> 统计 APP或网页 的一个页面,每天有多少用户点击进入的次数。同一个用户的反复点击进入记为 1 次。 +``` +del key +``` -用 `HashMap` 这种数据结构就可以,假设 APP 中日活用户达到`百万`或`千万以上级别`的话,我们采用 `HashMap` 的做法,就会导致程序中占用大量的内存。 +释放锁之后,其他线程就可以继续执行setnx命令来获得锁。 -估算下 `HashMap` 的在应对上述问题时候的内存占用。假设定义`HashMap` 中 `Key` 为 `string` 类型,`value` 为 `bool`。`key` 对应用户的`Id`,`value`是`是否点击进入`。明显地,当百万不同用户访问的时候。此`HashMap` 的内存占用空间为:`100万 * (string + bool)`。 +3、锁超时 -### HyperLogLog原理 +锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。 -如图,给定一系列的随机整数,我们记录下低位连续零位的最大长度 k,通过这个 k 值可以估算出随机数的数量。 +所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令, -img +``` +expire key 30 +``` -HyperLogLog与伯努利试验有关,具体可参考[HyperLogLog 算法的原理讲解](https://juejin.im/post/5c7900bf518825407c7eafd0) +### 上述分布式锁存在的问题 -## 18. `Redis`单线程如何处理那么多的并发客户端连接? -因为Redis 的线程模型:基于非阻塞的IO多路复用机制。 +**通过上述`setnx` 、`del`和`expire`实现的分布式锁还是存在着一些问题。** -Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。模型如下图: +1、SETNX 和 EXPIRE 非原子性 -[![img](http://cmsblogs.com/wp-content/resources/image.cmsblogs/sike-java/sike-redis/redis-202002171001.png)](http://cmsblogs.com/wp-content/resources/image.cmsblogs/sike-java/sike-redis/redis-202002171001.png) +假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。 -从上图可知,文件事件处理器的结构包含了四个部分: +**解决措施:** -- 多个 Socket -- IO 多路复用程序 -- 文件事件分派器 -- 事件处理器 +由于`setnx`指令本身是不支持传入超时时间的,而在Redis2.6.12版本上为`set`指令增加了可选参数, 用法如下: -多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。 +``` +SET key value [EX seconds][PX milliseconds] [NX|XX] +``` + +- EX second: 设置键的过期时间为second秒; +- PX millisecond:设置键的过期时间为millisecond毫秒; +- NX:只在键不存在时,才对键进行设置操作; +- XX:只在键已经存在时,才对键进行设置操作; +- SET操作完成时,返回OK,否则返回nil。 + +2、锁误解除 + +如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。 + +**解决办法:** + +在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。 + +具体在加锁的时候把当前线程的id当做value,可生成一个 UUID 标识当前线程,在删除之前验证key对应的value是不是自己线程的id。 + +还可以使用 lua 脚本做验证标识和解锁操作。 + +3、超时解锁导致并发 + +如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。 + +A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题: + +- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。 +- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。 + +4、不可重入 + +当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。 + +5、无法等待锁释放 + +上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。 + +- 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。 +- 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。 + +具体实现参考:https://xiaomi-info.github.io/2019/12/17/Redis-distributed-lock/ + +## 57. 了解RedLock吗? + +Redlock是一种算法,Redlock也就是 Redis Distributed Lock,可用实现多节点Redis的分布式锁。 + +RedLock官方推荐,Redisson完成了对Redlock算法封装。 + +此种方式具有以下特性: + +* 互斥访问:即永远只有一个 client 能拿到锁 +* 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。 +* 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务 + +## 58. RedLock的原理 + +假设有5个完全独立的Redis主服务器 + +1. 获取当前时间戳 + +2. client尝试按照顺序使用相同的key,value获取所有Redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的Redis服务。并且试着获取下一个Redis实例。 + + 比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁 + +3. client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个Redis实例成功获取锁,才算真正的获取锁成功 + +4. 如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移); + +5. 如果客户端由于某些原因获取锁失败,便会开始解锁所有Redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁 + +算法示意图如下: + +![image-20210829131128229](http://blog-img.coolsen.cn/img/image-20210829131128229.png) + + + +# 其他 + +## 59. Redis如何做内存优化? + +* **控制key的数量**。当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash,list,set,zset 等结构。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。 -## 参考 +* **缩减键值对象**,降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。 -https://juejin.cn/post/6844903663224225806 + - key长度:如在设计键时,在完整描述业务情况下,键值越短越好。 + - value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。 -https://juejin.cn/post/6844903716290576392 +* **编码优化**。Redis对外提供了string,list,hash,set,zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。可参考文章:https://cloud.tencent.com/developer/article/1162213 -https://www.cnblogs.com/wdliu/p/9377278.html -https://researchlab.github.io/2018/10/08/redis-11-redisio/ +## 60. 如果现在有个读超高并发的系统,用Redis来抗住大部分读请求,你会怎么设计? +如果是读高并发的话,先看读并发的数量级是多少,因为Redis单机的读QPS在万级,每秒几万没问题,使用一主多从+哨兵集群的缓存架构来承载每秒10W+的读并发,主从复制,读写分离。 +使用哨兵集群主要是提高缓存架构的可用性,解决单点故障问题。主库负责写,多个从库负责读,支持水平扩容,根据读请求的QPS来决定加多少个Redis从实例。如果读并发继续增加的话,只需要增加Redis从实例就行了。 -一份后端开发的综合笔记: +如果需要缓存1T+的数据,选择Redis cluster模式,每个主节点存一部分数据,假设一个master存32G,那只需要n*32G>=1T,n个这样的master节点就可以支持1T+的海量数据的存储了。 -> 链接:https://pan.baidu.com/s/1UNq4egIlxe3hijwjTfvW1w -> 提取码:u9go +> Redis单主的瓶颈不在于读写的并发,而在于内存容量,即使是一主多从也是不能解决该问题,因为一主多从架构下,多个slave的数据和master的完全一样。假如master是10G那slave也只能存10G数据。所以数据量受单主的影响。 +> 而这个时候又需要缓存海量数据,那就必须得有多主了,并且多个主保存的数据还不能一样。Redis官方给出的 Redis cluster 模式完美的解决了这个问题。 -![](http://blog-img.coolsen.cn/img/image-20210508113649706.png) \ No newline at end of file +## \ No newline at end of file diff --git a/Spring/Spring.md b/Spring/Spring.md index be23f4a..46b8ea5 100644 --- a/Spring/Spring.md +++ b/Spring/Spring.md @@ -1,4 +1,20 @@ -## 1. 什么是依赖注入?可以通过多少种方式完成依赖注入? +## 1. 使用Spring框架的好处是什么? + +- **轻量:**Spring 是轻量的,基本的版本大约2MB +- **控制反转:**Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们 +- **面向切面的编程(AOP):**Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开 +- **容器:**Spring 包含并管理应用中对象的生命周期和配置 +- **MVC框架:**Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品 +- **事务管理:**Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA) +- **异常处理:**Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。 + +## 2. 什么是 Spring IOC 容器? + +Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。 + +![image.png](http://blog-img.coolsen.cn/img/3101171-33099411d16ca051.png) + +## 3. 什么是依赖注入?可以通过多少种方式完成依赖注入? 在依赖注入中,您不必创建对象,但必须描述如何创建它们。您不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。 @@ -10,7 +26,7 @@ 在 Spring Framework 中,仅使用构造函数和 setter 注入。 -## 2. 区分 BeanFactory 和 ApplicationContext? +## 4. 区分 BeanFactory 和 ApplicationContext? | BeanFactory | ApplicationContext | | -------------------------- | ------------------------ | @@ -31,7 +47,16 @@ ApplicationContext的优缺点: - 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。 - 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大。 -## 3. spring 提供了哪些配置方式? +## 5. 区分构造函数注入和 setter 注入 + +| 构造函数注入 | setter 注入 | +| -------------------------- | -------------------------- | +| 没有部分注入 | 有部分注入 | +| 不会覆盖 setter 属性 | 会覆盖 setter 属性 | +| 任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 | +| 适用于设置很多属性 | 适用于设置少量属性 | + +## 6. spring 提供了哪些配置方式? - 基于 xml 配置 @@ -73,7 +98,7 @@ public class StudentConfig { } ``` -## 4. Spring 中的 bean 的作用域有哪些? +## 7. Spring 中的 bean 的作用域有哪些? - singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 - prototype : 每次请求都会创建一个新的 bean 实例。 @@ -81,7 +106,7 @@ public class StudentConfig { - session : :在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 - global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话 -## 5. 如何理解IoC和DI? +## 8. 如何理解IoC和DI? IOC就是控制反转,通俗的说就是我们不用自己创建实例对象,这些都交给Spring的bean工厂帮我们创建管理。这也是Spring的核心思想,通过面向接口编程的方式来是实现对业务组件的动态依赖。这就意味着IOC是Spring针对解决程序耦合而存在的。在实际应用中,Spring通过配置文件(xml或者properties)指定需要实例化的java类(类名的完整字符串),包括这些java类的一组初始化值,通过加载读取配置文件,用Spring提供的方法(getBean())就可以获取到我们想要的根据指定配置进行初始化的实例对象。 @@ -89,7 +114,7 @@ IOC就是控制反转,通俗的说就是我们不用自己创建实例对象 **DI:DI—Dependency** Injection,即"依赖注入":组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 -## 6. 将一个类声明为Spring的 bean 的注解有哪些? +## 9. 将一个类声明为Spring的 bean 的注解有哪些? 我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现: @@ -98,7 +123,19 @@ IOC就是控制反转,通俗的说就是我们不用自己创建实例对象 - @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 - @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 -## 7. Spring 中的 bean 生命周期? +## 10. spring 支持几种 bean scope? + +Spring bean 支持 5 种 scope: + +- **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 +- **Prototype** - 每次请求都会产生一个新的实例。 +- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 +- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 +- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 + +仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 + +## 11. Spring 中的 bean 生命周期? Bean的生命周期是由容器来管理的。主要在创建和销毁两个时期。 @@ -133,34 +170,67 @@ Bean的生命周期是由容器来管理的。主要在创建和销毁两个时 此时,Bean初始化完成,可以使用这个Bean了。 销毁过程:如果实现了DisposableBean的destroy方法,则调用它,如果实现了自定义的销毁方法,则调用之。 -## 8. 什么是 AOP? +## 12. 什么是 spring 的内部 bean? -AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. -在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** +只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 `` 或 `` 中提供了 `` 元素的使用。内部 bean 总是匿名的,它们总是作为原型。 -## 9. AOP 有哪些实现方式? +例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。 -实现 AOP 的技术,主要分为两大类: +Student.java -- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; - - 编译时编织(特殊编译器实现) - - 类加载时编织(特殊的类加载器实现)。 -- 动态代理 - 在运行时在内存中"临时"生成 AOP 动态代理类,因此也被称为运行时增强。 - - `JDK` 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。 - - `CGLIB`动态代理: 如果目标类没有实现接口,那么 `Spring AOP` 会选择使用 `CGLIB` 来动态代理目标类 。`CGLIB` ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, `CGLIB` 是通过继承的方式做的动态代理,因此如果某个类被标记为 `final` ,那么它是无法使用 `CGLIB` 做动态代理的。 +```java +public class Student { + private Person person; + //Setters and Getters +} +public class Person { + private String name; + private String address; + //Setters and Getters +} +``` -## 10. Spring AOP and AspectJ AOP 有什么区别? +bean.xml -Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 -Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 +```xml + + + + + + + + + +``` + +## 13. 什么是 spring 装配? + +当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 + +Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 + +自动装配的不同模式: -## 11. Spring中出现同名bean怎么办? +- **no** - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。 +- **byName** - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。 +- **byType** - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。 +- **构造函数** - 它通过调用类的构造函数来注入依赖项。它有大量的参数。 +- **autodetect** - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。 + +## 14. 自动装配有什么局限? + +- 覆盖的可能性 - 您始终可以使用 `` 和 `` 设置指定依赖项,这将覆盖自动装配。 +- 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。 +- 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。 + +## 15. Spring中出现同名bean怎么办? - 同一个配置文件内同名的Bean,以最上面定义的为准 - 不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件 - 同文件中ComponentScan和@Bean出现同名Bean。同文件下@Bean的会生效,@ComponentScan扫描进来不会生效。通过@ComponentScan扫描进来的优先级是最低的,原因就是它扫描进来的Bean定义是最先被注册的~ -## 12. Spring 怎么解决循环依赖问题? +## 16. Spring 怎么解决循环依赖问题? spring对循环依赖的处理有三种情况: 1构造器的循环依赖:这种依赖spring是处理不了的,直 接抛出BeanCurrentlylnCreationException异常。 @@ -182,28 +252,52 @@ Spring的单例对象的初始化主要分为三步: ![](http://blog-img.coolsen.cn/img/1584758309616_10.png) -A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象"这种循环依赖的情况。A首先完成了初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 +举例:A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象"这种循环依赖的情况。A首先完成了 -## 13. SpringMVC 工作原理了解吗? +初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories中。 -**原理如下图所示:** +此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过 -![SpringMVC运行原理](https://user-gold-cdn.xitu.io/2019/6/5/16b27eedc1634c3f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。 -上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 +此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 -**流程说明(重要):** +## 17. Spring 中的单例 bean 的线程安全问题? -1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 -2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 -3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 -4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 -5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 -6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 -7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 -8. 把 `View` 返回给请求者(浏览器) +当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 +**线程安全问题都是由全局变量及静态变量引起的。** +若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全. + +**无状态bean和有状态bean** + +- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 +- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 + +在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 +Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。 -## 14. Spring 框架中用到了哪些设计模式? +## 18. 什么是 AOP? + +AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. +在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** + +## 19. AOP 有哪些实现方式? + +实现 AOP 的技术,主要分为两大类: + +- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; + - 编译时编织(特殊编译器实现) + - 类加载时编织(特殊的类加载器实现)。 +- 动态代理 - 在运行时在内存中"临时"生成 AOP 动态代理类,因此也被称为运行时增强。 + - `JDK` 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。 + - `CGLIB`动态代理: 如果目标类没有实现接口,那么 `Spring AOP` 会选择使用 `CGLIB` 来动态代理目标类 。`CGLIB` ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, `CGLIB` 是通过继承的方式做的动态代理,因此如果某个类被标记为 `final` ,那么它是无法使用 `CGLIB` 做动态代理的。 + +## 20. Spring AOP and AspectJ AOP 有什么区别? + +Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 +Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 + +## 21. Spring 框架中用到了哪些设计模式? **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 @@ -219,12 +313,22 @@ A的某个field或者setter依赖了B的实例对象,同时B的某个field或 **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -## 15. Spring 事务实现方式有哪些? +## 22. Spring 事务实现方式有哪些? - 编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。 - 声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。 -## 16. spring事务定义的传播规则 +## 23. Spring框架的事务管理有哪些优点? + +- 它提供了跨不同事务api(如JTA、JDBC、Hibernate、JPA和JDO)的一致编程模型。 + +- 它为编程事务管理提供了比JTA等许多复杂事务API更简单的API。 + +- 它支持声明式事务管理。 + +- 它很好地集成了Spring的各种数据访问抽象。 + +## 24. spring事务定义的传播规则 - PROPAGATION_REQUIRED: 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 - PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行。 @@ -234,23 +338,198 @@ A的某个field或者setter依赖了B的实例对象,同时B的某个field或 - PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。 - PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 -## 17. .Spring 中的单例 bean 的线程安全问题? +## 25. SpringMVC 工作原理了解吗? -当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 -**线程安全问题都是由全局变量及静态变量引起的。** -若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全. +**原理如下图所示:** -**无状态bean和有状态bean** +![img](http://blog-img.coolsen.cn/img/SpingMVC-Process.jpg) -- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 -- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 +上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 -在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 -Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。 +**流程说明(重要):** + +1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 +3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 +4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +8. 把 `View` 返回给请求者(浏览器) + +## 26. 简单介绍 Spring MVC 的核心组件 + +那么接下来就简单介绍一下 `DispatcherServlet` 和九大组件(按使用顺序排序的): + +| 组件 | 说明 | +| :-------------------------- | :----------------------------------------------------------- | +| DispatcherServlet | Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作 | +| MultipartResolver | 内容类型( `Content-Type` )为 `multipart/*` 的请求的解析器,例如解析处理文件上传的请求,便于获取参数信息以及上传的文件 | +| HandlerMapping | 请求的处理器匹配器,负责为请求找到合适的 `HandlerExecutionChain` 处理器执行链,包含处理器(`handler`)和拦截器们(`interceptors`) | +| HandlerAdapter | 处理器的适配器。因为处理器 `handler` 的类型是 Object 类型,需要有一个调用者来实现 `handler` 是怎么被执行。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口、HttpRequestHandler 接口,也可以用 `@RequestMapping` 注解将方法作为一个处理器等,这就导致 Spring MVC 无法直接执行这个处理器。所以这里需要一个处理器适配器,由它去执行处理器 | +| HandlerExceptionResolver | 处理器异常解析器,将处理器( `handler` )执行时发生的异常,解析( 转换 )成对应的 ModelAndView 结果 | +| RequestToViewNameTranslator | 视图名称转换器,用于解析出请求的默认视图名 | +| LocaleResolver | 本地化(国际化)解析器,提供国际化支持 | +| ThemeResolver | 主题解析器,提供可设置应用整体样式风格的支持 | +| ViewResolver | 视图解析器,根据视图名和国际化,获得最终的视图 View 对象 | +| FlashMapManager | FlashMap 管理器,负责重定向时,保存参数至临时存储(默认 Session) | + +Spring MVC 对各个组件的职责划分的比较清晰。`DispatcherServlet` 负责协调,其他组件则各自做分内之事,互不干扰。 + +## 27. @Controller 注解有什么用? + +`@Controller` 注解标记一个类为 Spring Web MVC **控制器** Controller。Spring MVC 会将扫描到该注解的类,然后扫描这个类下面带有 `@RequestMapping` 注解的方法,根据注解信息,为这个方法生成一个对应的**处理器**对象,在上面的 HandlerMapping 和 HandlerAdapter组件中讲到过。 + +当然,除了添加 `@Controller` 注解这种方式以外,你还可以实现 Spring MVC 提供的 `Controller` 或者 `HttpRequestHandler` 接口,对应的实现类也会被作为一个**处理器**对象 + +## 28. @RequestMapping 注解有什么用? + +`@RequestMapping` 注解,在上面已经讲过了,配置**处理器**的 HTTP 请求方法,URI等信息,这样才能将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这个**控制器**的 URI 前缀 + +## 29. @RestController 和 @Controller 有什么区别? + +`@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。当然,返回什么样的数据格式,根据客户端的 `ACCEPT` 请求头来决定。 + +## 30. @RequestMapping 和 @GetMapping 注解的不同之处在哪里? + +1. `@RequestMapping`:可注解在类和方法上;`@GetMapping` 仅可注册在方法上 +2. `@RequestMapping`:可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping` 是 `@RequestMapping` 的 GET 请求方法的特例,目的是为了提高清晰度。 + +## 31. @RequestParam 和 @PathVariable 两个注解的区别 + +两个注解都用于方法参数,获取参数值的方式不同,`@RequestParam` 注解的参数从请求携带的参数中获取,而 `@PathVariable` 注解从请求的 URI 中获取 + +## 32. 返回 JSON 格式使用什么注解? + +可以使用 **`@ResponseBody`** 注解,或者使用包含 `@ResponseBody` 注解的 **`@RestController`** 注解。 + +当然,还是需要配合相应的支持 JSON 格式化的 HttpMessageConverter 实现类。例如,Spring MVC 默认使用 MappingJackson2HttpMessageConverter。 + +## 33. 什么是springmvc拦截器以及如何使用它? + +Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法: + +- preHandle:在执行实际处理程序之前调用。 +- postHandle:在执行完实际程序之后调用。 +- afterCompletion:在完成请求后调用。 + +## 34. Spring MVC 和 Struts2 的异同? + +**入口**不同 + +- Spring MVC 的入门是一个 Servlet **控制器**。 +- Struts2 入门是一个 Filter **过滤器**。 + +**配置映射**不同, + +- Spring MVC 是基于**方法**开发,传递参数是通过**方法形参**,一般设置为**单例**。 +- Struts2 是基于**类**开发,传递参数是通过**类的属性**,只能设计为**多例**。 + +**视图**不同 + +- Spring MVC 通过参数解析器是将 Request 对象内容进行解析成方法形参,将响应数据和页面封装成 **ModelAndView** 对象,最后又将模型数据通过 **Request** 对象传输到页面。其中,如果视图使用 JSP 时,默认使用 **JSTL** 。 +- Struts2 采用**值栈**存储请求和响应的数据,通过 **OGNL** 存取数据。 + +## 35. REST 代表着什么? + +REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端 + +可以看看 [REST API design and development](http://bit.ly/2zIGzWK) ,知乎上的 [《怎样用通俗的语言解释 REST,以及 RESTful?》](https://www.zhihu.com/question/28557115)了解。 + +## 36. 什么是安全的 REST 操作? + +REST 接口是通过 HTTP 方法完成操作 + +- 一些 HTTP 操作是安全的,如 GET 和 HEAD ,它不能在服务端修改资源 +- 换句话说,PUT、POST 和 DELETE 是不安全的,因为他们能修改服务端的资源 + +所以,是否安全的界限,在于**是否修改**服务端的资源 + +## 37. REST API 是无状态的吗? + +**是的**,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的 + +REST API 中的请求应该包含处理它所需的所有细节。它**不应该**依赖于以前或下一个请求或服务器端维护的一些数据,例如会话 + +**REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点** + +## 38. REST安全吗? 你能做什么来保护它? + +安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的 + +REST 通常不是安全的,需要开发人员自己实现安全机制 + +## 39. 为什么要用SpringBoot? + +在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖,如spring-core、spring-bean、spring-context等,而这些配置通常都是重复添加的,而且需要做很多框架使用及环境参数的重复配置,如开启注解、配置日志等。Spring Boot致力于弱化这些不必要的操作,提供默认配置,当然这些默认配置是可以按需修改的,快速搭建、开发和运行Spring应用。 + +以下是使用SpringBoot的一些好处: + +- 自动配置,使用基于类路径和应用程序上下文的智能默认值,当然也可以根据需要重写它们以满足开发人员的需求。 +- 创建Spring Boot Starter 项目时,可以选择选择需要的功能,Spring Boot将为你管理依赖关系。 +- SpringBoot项目可以打包成jar文件。可以使用Java-jar命令从命令行将应用程序作为独立的Java应用程序运行。 +- 在开发web应用程序时,springboot会配置一个嵌入式Tomcat服务器,以便它可以作为独立的应用程序运行。(Tomcat是默认的,当然你也可以配置Jetty或Undertow) +- SpringBoot包括许多有用的非功能特性(例如安全和健康检查)。 + +## 40. Spring Boot中如何实现对不同环境的属性配置文件的支持? + +Spring Boot支持不同环境的属性配置文件切换,通过创建application-{profile}.properties文件,其中{profile}是具体的环境标识名称,例如:application-dev.properties用于开发环境,application-test.properties用于测试环境,application-uat.properties用于uat环境。如果要想使用application-dev.properties文件,则在application.properties文件中添加spring.profiles.active=dev。 + +如果要想使用application-test.properties文件,则在application.properties文件中添加spring.profiles.active=test。 + +## 41. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? + +启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: + +@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 + +@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 + +@ComponentScan:Spring组件扫描。 + +## 42. 你如何理解 Spring Boot 中的 Starters? + +Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。 + +Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。 + +## 43. Spring Boot Starter 的工作原理是什么? + +Spring Boot 在启动的时候会干这几件事情: + +- Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。 +- 根据 spring.factories 配置加载 AutoConfigure 类 +- 根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context + +总结一下,其实就是 Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可 + +## 44. 保护 Spring Boot 应用有哪些方法? + +- 在生产中使用HTTPS +- 使用Snyk检查你的依赖关系 +- 升级到最新版本 +- 启用CSRF保护 +- 使用内容安全策略防止XSS攻击 + +## 45. Spring 、Spring Boot 和 Spring Cloud 的关系? + +Spring 最初最核心的两大核心功能 Spring Ioc 和 Spring Aop 成就了 Spring,Spring 在这两大核心的功能上不断的发展,才有了 Spring 事务、Spring Mvc 等一系列伟大的产品,最终成就了 Spring 帝国,到了后期 Spring 几乎可以解决企业开发中的所有问题。 + +Spring Boot 是在强大的 Spring 帝国生态基础上面发展而来,发明 Spring Boot 不是为了取代 Spring ,是为了让人们更容易的使用 Spring 。 + +Spring Cloud 是一系列框架的有序集合。它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。 + +Spring Cloud 是为了解决微服务架构中服务治理而提供的一系列功能的开发框架,并且 Spring Cloud 是完全基于 Spring Boot 而开发,Spring Cloud 利用 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。 + +用一组不太合理的包含关系来表达它们之间的关系。 + +Spring ioc/aop> Spring> Spring Boot> Spring Cloud ## 参考 https://juejin.cn/post/6844903860658503693 -https://www.cnblogs.com/jingmoxukong/p/9408037.html \ No newline at end of file +https://www.cnblogs.com/jingmoxukong/p/9408037.html + +http://www.ityouknow.com/springboot/2019/07/24/springboot-interview.html \ No newline at end of file diff --git "a/345円210円206円345円270円203円345円274円217円/351円235円242円350円257円225円351円242円230円.md" "b/345円210円206円345円270円203円345円274円217円/351円235円242円350円257円225円351円242円230円.md" new file mode 100644 index 0000000..fb4e9cc --- /dev/null +++ "b/345円210円206円345円270円203円345円274円217円/351円235円242円350円257円225円351円242円230円.md" @@ -0,0 +1,53 @@ +## 1.解释一下什么是CAP? +* Consistency:一致性就是在客户端任何时候看到各节点的数据都是一致的。 +* Availability:可用性就是在任何时刻都可以提供读写。 +* Partition Tolerance:分区容错性是在网络故障、某些节点不能通信的时候系统仍能继续工作。 +具体地讲在分布式系统中,在任何数据库设计中,一个Web应用最多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。 +![img](http://blog-img.coolsen.cn/img/801753-20151107213219867-1667011131.png) + + +AP(高可用&&分区容错): + +允许至少一个节点更新状态会导致数据不一致,即丧失了C性质(一致性)。会导致全局的数据不一致。 + +CP(一致&&分区容错): + +为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质(可用性)。分区同步会导致同步时间无限延长(也就是等数据同步完成之后才能正常访问) + +CA(一致&&高可用): + +两个节点可以互相通信,才能既保证C(一致性)又保证A(可用性),这又会导致丧失P性质(分区容错性)。这样的话就分布式节点受阻,无法部署子节点,放弃了分布式系统的可扩展性。因为分布式系统与单机系统不同,它涉及到多节点间的通讯和交互,节点间的分区故障是必然发生的,所以在分布式系统中分区容错性是必须要考虑的。 +## 2.什么分布式事务? + +分布式事务服务(Distributed Transaction Service,DTS)是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。 + +CAP理论告诉我们在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的,所以我们只能在一致性和可用性之间进行权衡。 + +为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。 + +## 3.了解BASE理论吗? +BASE理论指的是: + +* Basically Available(基本可用) +* Soft state(软状态) +* Eventually consistent(最终一致性) +BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,是对互联网大规模分布式系统的实践总结,强调可用性。 + +理论的核心思想就是:基本可用(Basically Available)和最终一致性(Eventually consistent)。虽然无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。 + +## 4.实现分布式事务一致性(Consistency)的方法有哪些? +最著名的就是二阶段提交协议、三阶段提交协议和Paxos算法。 + +**两阶段提交协议** + +* prepare(准备阶段) + +当开始事务调用的时候,事务处理器向事务执行者(有可能是数据库本身支持)发出命令,事务执行者进行prepare操作。 +当所有事务执行者都完成了prepare操作,就进行下一步行为。 +如果有一个事务执行者在执行prepare的时候失败了,那么通知事务处理器,事务处理器再通知所有的事务执行者执行回滚操作。 +* commit(提交阶段) + +当所有事务执行者都prepare成功以后,事务处理器会再次发送commit请求给事务执行者,所有事务执行者进行commit处理。 +当所有commit处理都成功了,那么事务执行结束。 +如果有一个事务执行者的commit处理不成功,这个时候就要通知事务处理器,事务处理器通知所有的事务执行者执行回滚(abort)操作。 +但是两阶段提交的诟病就是在于性能问题。比如由于执行链比较长,锁定资源的时间也变长了。所以在高性能的系统中都会避免使用二阶段提交。 \ No newline at end of file diff --git "a/346円223円215円344円275円234円347円263円273円347円273円237円/346円223円215円344円275円234円347円263円273円347円273円237円.md" "b/346円223円215円344円275円234円347円263円273円347円273円237円/346円223円215円344円275円234円347円263円273円347円273円237円.md" index d383a01..40a79d7 100644 --- "a/346円223円215円344円275円234円347円263円273円347円273円237円/346円223円215円344円275円234円347円263円273円347円273円237円.md" +++ "b/346円223円215円344円275円234円347円263円273円347円273円237円/346円223円215円344円275円234円347円263円273円347円273円237円.md" @@ -1,11 +1,3 @@ -操作系统常见面试题来啦~ - -先来看本期的目录: - -![目录](http://blog-img.coolsen.cn/img/image-20210610173702340.png) - - - ## 1. 进程和线程的区别? * 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。 @@ -26,7 +18,6 @@ 并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程`A`和`B`,`A`运行一个时间片之后,切换到`B`,`B`运行一个时间片之后又切换到`A`。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。 并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。 - ## 4. 进程与线程的切换流程? 进程切换分两步: @@ -173,12 +164,38 @@ - 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺 - 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系 -**如何处理死锁问题**: +### **如何处理死锁问题** + +常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。 + +**(1)死锁的预防:**基本思想就是确保死锁发生的四个必要条件中至少有一个不成立: + +> - 1 破除资源互斥条件 +> - 2 破除"请求与保持"条件:实行资源预分配策略,进程在运行之前,必须一次性获取所有的资源。缺点:在很多情况下,无法预知进程执行前所需的全部资源,因为进程是动态执行的,同时也会降低资源利用率,导致降低了进程的并发性。 +> - 3 破除"不可剥夺"条件:允许进程强行从占有者那里夺取某些资源。当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已经占有的资源会被暂时被释放,或者说被抢占了。 +> - 4 破除"循环等待"条件:实行资源有序分配策略,对所有资源排序编号,按照顺序获取资源,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。 + +**(2)死锁避免:** + +死锁预防通过约束资源请求,防止4个必要条件中至少一个的发生,可以通过直接或间接预防方法,但是都会导致低效的资源使用和低效的进程执行。而死锁避免则允许前三个必要条件,但是通过动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。银行家算法是经典的死锁避免的算法。 + +**(3)死锁检测:** + +死锁预防策略是非常保守的,他们通过限制访问资源和在进程上强加约束来解决死锁的问题。死锁检测则是完全相反,它不限制资源访问或约束进程行为,只要有可能,被请求的资源就被授权给进程。但是操作系统会周期性地执行一个算法检测前面的循环等待的条件。死锁检测算法是通过资源分配图来检测是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有存在环,也就是检测到死锁的发生。 + +> - (1)如果进程-资源分配图中无环路,此时系统没有死锁。 +> - (2)如果进程-资源分配图中有环路,且每个资源类中只有一个资源,则系统发生死锁。 +> - (3)如果进程-资源分配图中有环路,且所涉及的资源类有多个资源,则不一定会发生死锁。 + +**(4)死锁解除:** + +死锁解除的常用方法就是终止进程和资源抢占,回滚。所谓进程终止就是简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占就是从一个或者多个死锁进程那里抢占一个或多个资源。 + +**(5)鸵鸟策略:** + +把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任何措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。 + -- **忽略该问题**。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。 -- **检测死锁并且恢复。** -- 仔细地对资源进行动态分配,以**避免死锁**。 -- **通过破除死锁四个必要条件之一,来防止死锁产生。** ## 12. 进程调度策略有哪几种? @@ -241,23 +258,39 @@ - 物理内存不足时一些不常用的页可以被交换出去,腾给系统。 - 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。 +## 18. 物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别? +物理地址就是内存中真正的地址,它就相当于是你家的门牌号,你家就肯定有这个门牌号,具有唯一性。**不管哪种地址,最终都会映射为物理地址**。 -## 16. 页面替换算法有哪些? +在`实模式`下,段基址 + 段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为`物理地址`。 -在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 +但是在`保护模式`下,段基址 + 段内偏移被称为`线性地址`,不过此时的段基址不能称为真正的地址,而是会被称作为一个`选择子`的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在 GDT 中找到相应的段描述符,段描述符记录了**段的起始、段的大小**等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是`虚拟地址`。 + +不论在实模式还是保护模式下,段内偏移地址都叫做`有效地址`。有效抵制也是逻辑地址。 + +线性地址可以看作是`虚拟地址`,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址 -> 物理地址的映射。 -包括以下算法: +![image-20210807152300643](http://blog-img.coolsen.cn/img/image-20210807152300643.png) -* **最佳算法**:所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。这是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 +## 19. 页面替换算法有哪些? -* **先进先出**:选择换出的页面是最先进入的页面。该算法将那些经常被访问的页面也被换出,从而使缺页率升高。 +在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 + +![image-20210807152232136](http://blog-img.coolsen.cn/img/image-20210807152232136.png) -* **LRU**:虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。`LRU` 将最近最久未使用的页面换出。为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 `LRU` 代价很高。 +- `最优算法`在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,`因此实际上该算法不能使用`。然而,它可以作为衡量其他算法的标准。 +- `NRU` 算法根据 R 位和 M 位的状态将页面分为四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。 +- `FIFO` 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。 +- `第二次机会`算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。 +- `时钟` 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。 +- `LRU` 算法是一个非常优秀的算法,但是没有`特殊的硬件(TLB)`很难实现。如果没有硬件,就不能使用 LRU 算法。 +- `NFU` 算法是一种近似于 LRU 的算法,它的性能不是非常好。 +- `老化` 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择 +- 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。`WSClock` 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。 -* **时钟算法**:时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。它将整个环形链表的每一个页面做一个标记,如果标记是`0`,那么暂时就不会被替换,然后时钟算法遍历整个环,遇到标记为`1`的就替换,否则将标记为`0`的标记为`1`。 +**最好的算法是老化算法和WSClock算法**。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。 -## 18. 什么是缓冲区溢出?有什么危害? +## 20. 什么是缓冲区溢出?有什么危害? 缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。 @@ -268,11 +301,19 @@ 造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。 -## 19. 什么是虚拟内存? +## 21. 什么是虚拟内存? 虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。 -## 20. 讲一讲IO多路复用? +## 22. 虚拟内存的实现方式有哪些? + +虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或`永久`的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式: + +- 请求分页存储管理。 +- 请求分段存储管理。 +- 请求段页式存储管理。 + +## 23. 讲一讲IO多路复用? **IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合**: @@ -283,12 +324,12 @@ - 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。 - 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。 -## 21. 硬链接和软链接有什么区别? +## 24. 硬链接和软链接有什么区别? - 硬链接就是在目录下创建一个条目,记录着文件名与 `inode` 编号,这个 `inode` 就是源文件的 `inode`。删除任意一个条目,文件还是存在,只要引用数量不为 `0`。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。 - 符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 `Windows` 的快捷方式。当源文件被删除了,链接文件就打不开了。因为记录的是路径,所以可以为目录建立符号链接。 -## 22. 中断的处理过程? +## 25. 中断的处理过程? 1. 保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈。 2. 开中断:以便执行中断时能响应较高级别的中断请求。 @@ -296,26 +337,69 @@ 4. 关中断:保证恢复现场时不被新中断打扰 5. 恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。 -## 23. 中断和轮询有什么区别? +## 26. 中断和轮询有什么区别? * 轮询:CPU对**特定设备**轮流询问。中断:通过**特定事件**提醒CPU。 - * 轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。 -## End +## 27. 什么是用户态和内核态? + +用户态和系统态是操作系统的两种运行状态: + +> - 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。 +> - 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。 + +将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。 + +## 28. 用户态和内核态是如何切换的? + +所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即**用户态 -> 内核态 -> 用户态**,而唯一能够做这些操作的只有 `系统调用`,而能够执行系统调用的就只有 `操作系统`。 + +一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 `陷阱指令(trap instruction)`。 + +他们的工作流程如下: + +![image-20210807152619210](http://blog-img.coolsen.cn/img/image-20210807152619210.png) + +- 首先用户程序会调用 `glibc` 库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。 +- glibc 库知道针对不同体系结构调用`系统调用`的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。 +- 然后,glibc 库调用`软件中断指令(SWI)` ,这个指令通过更新 `CPSR` 寄存器将模式改为超级用户模式,然后跳转到地址 `0x08` 处。 +- 到目前为止,整个过程仍处于用户态下,在执行 SWI 指令后,允许进程执行内核代码,MMU 现在允许内核虚拟内存访问 +- 从地址 0x08 开始,进程执行加载并跳转到中断处理程序,这个程序就是 ARM 中的 `vector_swi()`。 +- 在 vector_swi() 处,从 SWI 指令中提取系统调用号 SCNO,然后使用 SCNO 作为系统调用表 `sys_call_table` 的索引,调转到系统调用函数。 +- 执行系统调用完成后,将还原用户模式寄存器,然后再以用户模式执行。 + +## 29. Unix 常见的IO模型: + +对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段: + +> - 等待数据准备就绪 (Waiting for the data to be ready) +> - 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) + +正式因为这两个阶段,linux系统产生了下面五种网络模式的方案: + +> - 阻塞式IO模型(blocking IO model) +> - 非阻塞式IO模型(noblocking IO model) +> - IO复用式IO模型(IO multiplexing model) +> - 信号驱动式IO模型(signal-driven IO model) +> - 异步IO式IO模型(asynchronous IO model) + +对于这几种 IO 模型的详细说明,可以参考这篇文章:https://juejin.cn/post/6942686874301857800#heading-13 + +其中,IO多路复用模型指的是:使用单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。 -更文不易,点赞鼓励下呗〜 +## 30. select、poll 和 epoll 之间的区别? -秋招求职交流群持续开放,扫码加我,备注秋招,拉你进群~ +(1)select:时间复杂度 O(n) -![](http://blog-img.coolsen.cn/img/image-20210508163936952.png) +select 仅仅知道有 I/O 事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 -## 参考 +(2)poll:时间复杂度 O(n) -https://segmentfault.com/a/1190000011028633 +poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。 -https://god-jiang.github.io +(3)epoll:时间复杂度 O(1) -https://segmentfault.com/a/1190000019750164 +epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。 -https://www.jianshu.com/p/c1015f5ffa74 \ No newline at end of file +> select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),就通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。 \ No newline at end of file

AltStyle によって変換されたページ (->オリジナル) /