JDK、JRE、JVM之间的区别hashCode()与equals()之间的关系
String、StringBuffer、StringBuilder的区别泛型中extends和super的区别
==和equals⽅法的区别重载和重写的区别
List和Set的区别
ArrayList和LinkedList区别
谈谈ConcurrentHashMap的扩容机制
Jdk . 到Jdk . HashMap 发⽣了什么变化(底层)? 说⼀下HashMap的Put⽅法
深 拷 ⻉ 和 浅 拷 ⻉ HashMap的扩容机制原理
CopyOnWriteArrayList的底层原理是怎样的什么是字节码?采⽤字节码的好处是什么? Java中的异常体系是怎样的
在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常? Java中有哪些类加载器
说说类加载器双亲委派模型JVM中哪些是线程共享区 你们项⽬如何排查JVM问题
⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程? 怎么确定⼀个对象到底是不是垃圾?
JVM有哪些垃圾回收算法? 什么是STW?
JVM参数有哪些?
说说对线程安全的理解对守护线程的理解
ThreadLocal的底层原理
并发、并⾏、串⾏之间的区别Java死锁如何避免?
线程池的底层⼯作原理
线程池为什么是先添加列队⽽不是先创建最⼤线程? ReentrantLock中的公平锁和⾮公平锁的底层实现ReentrantLock中tryLock()和lock()⽅法的区别CountDownLatch和Semaphore的区别和底层原理Sychronized的偏向锁、轻量级锁、重量级锁Sychronized和ReentrantLock的区别
谈谈你对AQS的理解,AQS如何实现可重⼊锁? 谈谈你对IOC的理解
单例Bean和单例模式Spring事务传播机制
Spring事务什么时候会失效?
Spring中的Bean创建的⽣命周期有哪些步骤Spring中Bean是线程安全的吗ApplicationContext和BeanFactory有什么区别Spring中的事务是如何实现的
Spring中什么时候@Transactional会失效Spring容器启动流程是怎样的
Spring⽤到了哪些设计模式
Spring Boot中常⽤注解及其底层实现Spring Boot是如何启动Tomcat的Mybatis的优缺点
#{}和${}的区别是什么? 索引的基本原理
索引设计的原则?
事务的基本特性和隔离级别什么是MVCC
简述MyISAM和InnoDB的区别
Explain语句结果中各个字段分表表示什么
索引覆盖是什么
最左前缀原则是什么Innodb是如何实现事务的
B树和B+树的区别,为什么Mysql使⽤B+树Mysql锁有哪些,如何理解
Mysql慢查询该如何优化? 什么是RDB和AOF
Redis的过期键的删除策略简述Redis事务实现
Redis 主从复制的核⼼原理
Redis有哪些数据结构?分别有哪些典型的应⽤场景? Redis分布式锁底层是如何实现的?
Redis主从复制的核⼼原理Redis集群策略
缓存穿透、缓存击穿、缓存雪崩分别是什么Redis和Mysql如何保证数据⼀致
Redis的持久化机制
Redis单线程为什么这么快简述Redis事务实现
什么是CAP理论什么是BASE理论什么是RPC
数据⼀致性模型有哪些
分布式ID是什么?有哪些解决⽅案?
分布式锁的使⽤场景是什么?有哪些实现⽅案? 什么是分布式事务?有哪些实现⽅案?
什么是ZAB协议
为什么Zookeeper可以⽤来作为注册中⼼Zookeeper中的领导者选举的流程是怎样的? Zookeeper集群中节点之间数据是如何同步的Dubbo⽀持哪些负载均衡策略
Dubbo是如何完成服务导出的?
Dubbo是如何完成服务引⼊的? Dubbo的架构设计是怎样的? 负载均衡算法有哪些
分布式架构下,Session 共享有什么⽅案如何实现接⼝的幂等性
简述zk的命名服务、配置管理、集群管理 讲下Zookeeper中的watch机制Zookeeper和Eureka的区别
存储拆分后如何解决唯⼀主键问题雪花算法原理
如何解决不使⽤分区键的查询问题
Spring Cloud有哪些常⽤组件,作⽤是什么? 如何避免缓存穿透、缓存击穿、缓存雪崩? 分布式系统中常⽤的缓存⽅案有哪些
缓存过期都有哪些策略? 常⻅的缓存淘汰算法
布隆过滤器原理,优缺点分布式缓存寻址算法
Spring Cloud和Dubbo有哪些区别? 什么是服务雪崩?什么是服务限流?
什么是服务熔断?什么是服务降级?区别是什么? SOA、分布式、微服务之间有什么关系和区别? 怎么拆分微服务?
怎样设计出⾼内聚、低耦合的微服务? 有没有了解过DDD领域驱动设计?
什么是中台?
你的项⽬中是怎么保证微服务敏捷开发的? 如何进⾏消息队列选型?
RocketMQ的事务消息是如何实现的
为什么RocketMQ不使⽤Zookeeper作为注册中⼼呢? RocketMQ的实现原理
RocketMQ为什么速度快
正在上传…重新上传取消消息队列如何保证消息可靠传输消息队列有哪些作⽤
死信队列是什么?延时队列是什么? 如何保证消息的⾼效读写?
epoll和poll的区别
TCP的三次握⼿和四次挥⼿
浏览器发出⼀个请求到收到响应经历了哪些步骤? 跨域请求是什么?有什么问题?怎么解决?
零拷⻉是什么
JDK、JRE、JVM之间的区别
- JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运⾏Java程序所需的各种⼯具和资源,包括Java编译器、Java运⾏时环境,以及常⽤的Java类库等
- JRE( Java Runtime Environment) ,Java运⾏环境,⽤于运⾏Java的字节码⽂件。JRE中包括了JVM以及JVM⼯作所需要的类库,普通⽤户⽽只需要安装JRE来运⾏Java程序,⽽程序开发者必须安装JDK来编译、调试程序。
- JVM(Java Virtual Mechinal),Java虚拟机,是JRE的⼀部分,它是整个java实现跨平台的最核⼼的部分,负责运⾏字节码⽂件。
我们写Java代码,⽤txt就可以写,但是写出来的Java代码,想要运⾏,需要先编译成字节码,那就需要编译器,⽽JDK中就包含了编译器javac,编译之后的字节码,想要运⾏,就需要⼀个可以执⾏字节码的程序,这个程序就是JVM(Java虚拟机),专⻔⽤来执⾏Java字节码的。
如果我们要开发Java程序,那就需要JDK,因为要编译Java源⽂件。
如果我们只想运⾏已经编译好的Java字节码⽂件,也就是*.class⽂件,那么就只需要JRE。 JDK中包含了JRE,JRE中包含了JVM。
另外,JVM在执⾏Java字节码时,需要把字节码解释为机器指令,⽽不同操作系统的机器指令是有可能不⼀样的,所以就导致不同操作系统上的JVM是不⼀样的,所以我们在安装JDK时需要选择操作系统。 另外,JVM是⽤来执⾏Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运
⾏,⽐如Apache Groovy, Scala and Kotlin 等等。
hashCode()与equals()之间的关系
正在上传…重新上传取消在Java中,每个对象都可以调⽤⾃⼰的hashCode()⽅法得到⾃⼰的哈希值(hashCode),相当于对象的 指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可 以利⽤hashCode来做⼀些提前的判断,⽐如:
- 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
- 如果两个对象的hashCode相同,不代表这两个对象⼀定是同⼀个对象,也可能是两个对象
- 如果两个对象相等,那么他们的hashCode就⼀定相同
在Java的⼀些集合类的实现中,在⽐较两个对象是否相等时,会根据上⾯的原则,会先调⽤对象的hashCode()⽅法得到hashCode进⾏⽐较,如果hashCode不相同,就可以直接认为这两个对象不相
同,如果hashCode相同,那么就会进⼀步调⽤equals()⽅法进⾏⽐较。⽽equals()⽅法,就是⽤来最终确定两个对象是不是相等的,通常equals⽅法的实现会⽐较重,逻辑⽐较多,⽽hashCode()主要就是得到⼀个哈希值,实际上就⼀个数字,相对⽽⾔⽐较轻,所以在⽐较两个对象时,通常都会先根据hashCode想⽐较⼀下。
所以我们就需要注意,如果我们重写了equals()⽅法,那么就要注意hashCode()⽅法,⼀定要保证能遵守上述规则。
String、StringBuffer、StringBuilder的区别
- String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是可变的
- StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更⾼
泛型中extends和super的区别
- <? extends T>表示包括T在内的任何T的⼦类
- <? super T>表示包括T在内的任何T的⽗类
==和equals⽅法的区别
● ==:如果是基本数据类型,⽐较是值,如果是引⽤类型,⽐较的是引⽤地址
- equals:具体看各个类重写equals⽅法之后的⽐较逻辑,⽐如String类,虽然是引⽤类型,但是String类中重写了equals⽅法,⽅法内部⽐较的是字符串中的各个字符是否全部相等。
正在上传…重新上传取消重载和重写的区别
- 重载(Overload): 在⼀个类中,同名的⽅法如果有不同的参数列表(⽐如参数类型不同、参数个数不同)则视为重载。
- 重写(Override): 从字⾯上看,重写就是 重新写⼀遍的意思。其实就是在⼦类中把⽗类本身有的⽅法重新写⼀遍。⼦类继承了⽗类的⽅法,但有时⼦类并不想原封不动的继承⽗类中的某个⽅法,所 以在⽅法名,参数列表,返回类型都相同(⼦类中⽅法的返回值可以是⽗类中⽅法返回值的⼦类)的 情况下, 对⽅法体进⾏修改,这就是重写。但要注意⼦类⽅法的访问修饰权限不能⼩于⽗类的。
List和Set的区别
- List:有序,按对象插⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使⽤Iterator取出所有元素,在逐⼀遍历,还可以使⽤get(int index)获取指定下标的元素
- Set:⽆序,不可重复,最多允许有⼀个Null元素对象,取元素时只能⽤Iterator接⼝取得所有元素,在逐⼀遍历各个元素
ArrayList和LinkedList区别
- ⾸先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
- 由于底层数据结构不同,他们所适⽤的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
- 另外ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以LinkedList还可以当做队列来使⽤
谈谈ConcurrentHashMap的扩容机制
-
- 版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相对于⼀个⼩型的HashMap
- 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
- 先⽣成新的数组,然后转移元素到新数组中
- 版本
-
-
-
正在上传…重新上传取消扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
-
-
-
- 版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现
- 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
- 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进⾏扩容
- ConcurrentHashMap是⽀持多个线程同时扩容的
- 扩容之前也先⽣成⼀个新的数组
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作
- 版本
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率
- 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要 判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
- 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希 算法,节省CPU资源
说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
- 根据Key通过哈希算法与与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中 是Node对象)并放⼊该位置
- 如果数组下标位置元素不为空,则要分情况讨论
- 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中
- 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
正在上传…重新上传取消ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会 判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链 表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成 红⿊树
ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法
深拷⻉和浅拷⻉
深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。
- 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
- 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象
HashMap的扩容机制原理
-
- 版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
- 将元素添加到新数组中去
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
- 版本
-
- 版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表或红⿊树
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
- 统计每个下标位置的元素个数
- 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置
- 版本
-
-
-
-
正在上传…重新上传取消如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
-
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
-
-
CopyOnWriteArrayList的底层原理是怎样的
- ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
- 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
- 写操作结束之后会把原数组指向新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应
⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很⾼的场景
什么是字节码?采⽤字节码的好处是什么?
编译器(javac)将Java源⽂件(*.java)⽂件编译成为字节码⽂件(*.class),可以做到⼀次编译到处运⾏, windows上编译好的class⽂件,可以直接在linux上运⾏,通过这种⽅式做到跨平台,不过Java的跨平台有⼀个前提条件,就是不同的操作系统上安装的JDK或JRE是不⼀样的,虽然字节码是通⽤的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各⾃ 的JDK或JRE。
采⽤字节码的好处,⼀⽅⾯实现了跨平台,另外⼀⽅⾯也提⾼了代码执⾏的性能,编译器在编译源代码 时可以做⼀些编译期的优化,⽐如锁消除、标量替换、⽅法内联等。
Java中的异常体系是怎样的
- Java中的所有异常都来⾃顶级⽗类Throwable。
- Throwable下有两个⼦类Exception和Error。
- Error表示⾮常严重的错误,⽐如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError, 通常这些错误出现时,仅仅想靠程序⾃⼰是解决不了的,可能是虚拟机、磁盘、操作系统层⾯出现 的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不⼤,因为程序可能已经 根本运⾏不了了。
- Exception表示异常,表示程序出现Exception时,是可以靠程序⾃⼰来解决的,⽐如NullPointerException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。
-
正在上传…重新上传取消Exception的⼦类通常⼜可以分为RuntimeException和⾮RuntimeException两类 - RunTimeException表示运⾏期异常,表示这个异常是在代码运⾏过程中抛出的,这些异常是⾮检查
异常,程序中可以选择捕获处理,也可以不处理。这些异常⼀般是由程序逻辑错误引起的,程序应该从逻辑⻆度尽可能避免这类异常的发⽣,⽐如NullPointerException、IndexOutOfBoundsException等。
- ⾮RuntimeException表示⾮运⾏期异常,也就是我们常说的检查异常,是必须进⾏处理的异常,如果
正在上传…重新上传取消不处理,程序就不能检查异常通过。如IOException、SQLException等以及⽤户⾃定义的Exception异
。
在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?
异常相当于⼀种提示,如果我们抛出异常,就相当于告诉上层⽅法,我抛了⼀个异常,我处理不了这个异常,交给你来处理,⽽对于上层⽅法来说,它也需要决定⾃⼰能不能处理这个异常,是否也需要交给它的上层。
所以我们在写⼀个⽅法时,我们需要考虑的就是,本⽅法能否合理的处理该异常,如果处理不了就继续 向上抛出异常,包括本⽅法中在调⽤另外⼀个⽅法时,发现出现了异常,如果这个异常应该由⾃⼰来处理,那就捕获该异常并进⾏处理。
Java中有哪些类加载器
JDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
- BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的 jar包和class⽂件。
- ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的
jar包和class类。
- AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。
说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
- BootstrapClassLoader
-
正在上传…重新上传取消ExtClassLoader - AppClassLoader
AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰进⾏加载。
JVM中哪些是线程共享区
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
正在上传…重新上传取消你们项⽬如何排查JVM问题
对于还在正常运⾏的系统:
- 可以使⽤jmap来查看JVM中各个区域的使⽤情况
- 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
- 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了
- 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
- ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表 示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻 代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效
- 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
- ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
- 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
- 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
- 然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- ⾸先把字节码⽂件内容加载到⽅法区
- 然后再根据类信息在堆区创建对象
- 对象⾸先会分配在堆区中年轻代的Eden区,经过⼀次Minor GC后,对象如果存活,就会进⼊
Suvivor区。在后续的每次Minor GC中,如果对象⼀直存活,就会在Suvivor区来回拷⻉,每移动
⼀次,年龄加1
- 当年龄超过15后,对象依然存活,对象就会进⼊⽼年代
- 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉
正在上传…重新上传取消怎么确定⼀个对象到底是不是垃圾?
- 引⽤计数算法: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
- 可达性算法: 这种⽅式是在内存中,从根对象向下⼀直找引⽤,找到的对象就不是垃圾,没找到的对象就是垃圾。
JVM有哪些垃圾回收算法?
- 标记清除算法:
- 标记阶段:把垃圾内存标记出来
- 清除阶段:直接将垃圾内存回收。
- 这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
- 复制算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩ 相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽ 且,他的效率跟存活对象的个数有关。
- 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清 除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将边界以外的所有内存直接清除。
什么是STW?
STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将JVM内存冻结的⼀种状态。在STW 状态下,JAVA的所有线程都是停⽌执⾏的-GC线程除外,native⽅法可以执⾏,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
JVM参数有哪些?
JVM参数⼤致可以分为三类:
- 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
- ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
- 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。
正在上传…重新上传取消说说对线程安全的理解
线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到 正常的结果,⽐如i++,i初始化值为0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那 么最终的结果应该就是⼀个线程的结果为1,⼀个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。
所以线程安全,主要指的是⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果。
对守护线程的理解
线程分为⽤户线程和守护线程,⽤户线程就是普通线程,守护线程就是JVM的后台线程,⽐如垃圾回收 线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭。我们可以通过设置thread.setDaemon(true)来把⼀个线程设置为守护线程。
ThreadLocal的底层原理
- ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的
值
- 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
- ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)
正在上传…重新上传取消
并发、并⾏、串⾏之间的区别
- 串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
- 并⾏(Parallelism):两个任务同时执⾏
- 并发(Concurrency):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后
⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的
Java死锁如何避免?
造成死锁的⼏个原因:
- ⼀个资源每次只能被⼀个线程使⽤
- ⼀个线程在阻塞等待某个资源时,不释放已占有资源
- ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
- 若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
- 要注意加锁时限,可以针对所设置⼀个超时时间
- 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
正在上传…重新上传取消
线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
- 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
- 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数
线程池为什么是先添加列队⽽不是先创建最⼤线程?
当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之 后,才会新开线程。这就相当于,⼀个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中,然后这 10个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员⼯了。
ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使
⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
正在上传…重新上传取消
ReentrantLock中tryLock()和lock()⽅法的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
- lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
正在上传…重新上传取消对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()
正在上传…重新上传取消⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。
Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就可以直接获取到了
- 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
- 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运
⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
Sychronized和ReentrantLock的区别
- sychronized是⼀个关键字,ReentrantLock是⼀个类
- sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
- sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有⼀个锁升级的过程
谈谈你对AQS的理解,AQS如何实现可重⼊锁?
- AQS是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。
- 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下, 有不⽤的意义。
- 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释放锁state就减1。
正在上传…重新上传取消谈谈你对IOC的理解
通常,我们认为Spring有两⼤特性IoC和AOP,那到底该如何理解IoC呢?
对于很多初学者来说,IoC这个概念给⼈的感觉就是我好像会,但是我说不出来。
那么IoC到底是什么,接下来来说说我的理解,实际上这是⼀个⾮常⼤的问题,所以我们就把它拆细了来回答,IoC表示控制反转,那么:
- 什么是控制?控制了什么?
- 什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
- 为什么要反转?反转之前有什么问题?反转之后有什么好处? 这就是解决这⼀类⼤问题的思路,⼤⽽化⼩。
那么,我们先来解决第⼀个问题:什么是控制?控制了什么? 我们在⽤Spring的时候,我们需要做什么:
- 建⼀些类,⽐如UserService、OrderService
- ⽤⼀些注解,⽐如@Autowired
但是,我们也知道,当程序运⾏时,⽤的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象⾥的属性是什么时候赋的值?谁赋的?所有这些都是我们程序 员做的,以为我们只是写了类⽽已,所有的这些都是Spring做的,它才是幕后⿊⼿。
这就是控制:
- 控制对象的创建
- 控制对象内属性的赋值
如果我们不⽤Spring,那我们得⾃⼰来做这两件事,反过来,我们⽤Spring,这两件事情就不⽤我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(⽐如某个属性上加@Autowired),⽽这其实就是第⼆个问题的答案,这就是反转,表示⼀种对象控制权的转移。
那反转有什么⽤,为什么要反转?
如果我们⾃⼰来负责创建对象,⾃⼰来给对象中的属性赋值,会出现什么情况?
⽐如,现在有三个类:
-
正在上传…重新上传取消A类,A类⾥有⼀个属性C c; - B类,B类⾥也有⼀个属性C c;
- C类
现在程序要运⾏,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:
- A a = new A();
- B b = new B();
- C c = new C();
- a.c = c;
- b.c = c;
这五⾏代码是不⽤Spring的情况下多出来的代码,⽽且,如果类在多⼀些,类中的属性在多⼀些,那相应的代码会更多,⽽且代码会更复杂。所以我们可以发现,我们⾃⼰来控制⽐交给Spring来控制,我们 的代码量以及代码复杂度是要⾼很多的,反⾔之,将对象交给Spring来控制,减轻了程序员的负担。
总结⼀下,IoC表示控制反转,表示如果⽤Spring,那么Spring会负责来创建对象,以及给对象内的属 性赋值,也就是如果⽤Spring,那么对象的控制权会转交给Spring。
单例Bean和单例模式
单例模式表示JVM中某个类的对象只会存在唯⼀⼀个。
⽽单例Bean并不表示JVM中只能存在唯⼀的某个类的Bean对象。
Spring事务传播机制
多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法A是⼀个事务的⽅法,⽅法A执⾏过程中调
⽤了⽅法B,那么⽅法B有⽆事务以及⽅法B对事务的要求不同都会对⽅法A的事务具体执⾏造成影响, 同时⽅法A的事务对⽅法B的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类 型所决定。
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存在事务,则加⼊这个事务
- SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏
- MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。
正在上传…重新上传取消NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务
- NEVER:不使⽤事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则REQUIRED的操作⼀样(开启⼀个事 务)
Spring事务什么时候会失效?
spring事务的原理是AOP,进⾏了切⾯增强,那么失效的根本原因是这个AOP不起作⽤了!常⻅情况有如下⼏种
1、发⽣⾃调⽤,类⾥⾯使⽤this调⽤本类的⽅法(this通常省略),此时这个this对象不是代理类,⽽是UserService对象本身!
解决⽅法很简单,让那个this变成UserService的代理类即可!
2、⽅法不是public的:@Transactional 只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在
⾮ public ⽅法上,可以开启 AspectJ 代理模式。3、数据库不⽀持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
Spring中的Bean创建的⽣命周期有哪些步骤
Spring中⼀个Bean的创建⼤概分为以下⼏个步骤:
- 推断构造⽅法
- 实例化
- 填充属性,也就是依赖注⼊
- 处理Aware回调
- 初始化前,处理@PostConstruct注解
- 初始化,处理InitializingBean接⼝
- 初始化后,进⾏AOP
当然其实真正的步骤更加细致,可以看下⾯的流程图
正在上传…重新上传取消
Spring中Bean是线程安全的吗
Spring本身并没有针对Bean做线程安全的处理,所以:
- 如果Bean是⽆状态的,那么Bean则是线程安全的
- 如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作⽤域没有关系,Bean的作⽤域只是表示Bean的⽣命周期范围,对于任何⽣命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
ApplicationContext和BeanFactory有什么区别
BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
Spring中的事务是如何实现的
-
正在上传…重新上传取消Spring事务底层是基于数据库事务和AOP机制的 - ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
- 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
- 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
- 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重要的⼀步
- 然后执⾏当前⽅法,⽅法中会执⾏sql
- 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
- 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
- Spring事务的隔离级别对应的就是数据库的隔离级别
- Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
- Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
Spring中什么时候@Transactional会失效
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是不会失效的。
同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效
Spring容器启动流程是怎样的
-
- 在创建Spring容器,也就是启动Spring时:
- ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
-
-
正在上传…重新上传取消然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去进
-
⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
-
- 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化 后这⼀步骤中
- 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
- Spring启动结束
- 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的
- 在Spring启动过程中还会去处理@Import等注解
正在上传…重新上传取消Spring⽤到了哪些设计模式
Spring Boot中常⽤注解及其底层实现
- @SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的组合,这三个注解是:
-
-
正在上传…重新上传取消@SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配置类 - @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
- @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前⽬录
-
- @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注 解的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
- @Controller、@Service、@ResponseBody、@Autowired都可以说
Spring Boot是如何启动Tomcat的
- ⾸先,SpringBoot在启动时会先创建⼀个Spring容器
- 在创建Spring容器过程中,会利⽤@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会⽣成⼀个启动Tomcat的Bean
- Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端⼝等,然后启动Tomcat
Mybatis的优缺点
优点:
- 基于 SQL 语句编程,相当灵活,不会对应⽤程序或者数据库的现有设计造成任何影响,SQL 写在XML ⾥,解除 sql 与程序代码的耦合,便于统⼀管理;提供 XML 标签, ⽀持编写动态 SQL 语句, 并可重⽤。
- 与 JDBC 相⽐,减少了 50%以上的代码量,消除了 JDBC ⼤量冗余的代码,不需要⼿动开关连接;
- 很好的与各种数据库兼容( 因为 MyBatis 使⽤ JDBC 来连接数据库,所以只要JDBC ⽀持的数据库 MyBatis 都⽀持)。
- 能够与 Spring 很好的集成;
- 提供映射标签, ⽀持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, ⽀持对象关系组件维护。
缺点:
-
正在上传…重新上传取消SQL 语句的编写⼯作量较⼤, 尤其当字段多、关联表多时, 对开发⼈员编写SQL 语句的功底有⼀定要求。 - SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。
#{}和${}的区别是什么?
#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调⽤ PreparedStatement 来赋值; Mybatis在处理${}时,会将sql中的${}替换成变量的值,调⽤ Statement 来赋值;
使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。
索引的基本原理
索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。 索引的原理:就是把⽆序的数据变成有序的查询
- 把创建了索引的列的内容进⾏排序
- 对排序结果⽣成倒排表
- 在倒排表内容上拼上数据地址链
- 在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据
索引设计的原则?
查询更快、占⽤空间更⼩
- 适合索引的列是出现在where⼦句中的列,或者连接⼦句中指定的列
- 基数较⼩的表,索引效果较差,没有必要在此列建⽴索引
正在上传…重新上传取消使⽤短索引,如果对⻓字符串列进⾏索引,应该指定⼀个前缀⻓度,这样能够节省⼤量索引空间, 如果搜索词超过索引前缀⻓度,则使⽤索引排除不匹配的⾏,然后检查其余⾏是否可能匹配。
- 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进
⾏更新甚⾄重构,索引列越多,这个时间就会越⻓。所以只保持需要的索引有利于查询即可。
- 定义有外键的数据列⼀定要建⽴索引。
- 更新频繁字段不适合创建索引
- 若是不能有效区分数据的列不适合做索引列(如性别,男⼥未知,最多也就三种,区分度实在太低)
- 尽量的扩展索引,不要新建索引。⽐如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
- 对于那些查询中很少涉及的列,重复值⽐较多的列不要建⽴索引。
- 对于定义为text、image和bit的数据类型的列不要建⽴索引。
事务的基本特性和隔离级别
事务基本特性ACID分别是:
原⼦性指的是⼀个事务中的操作要么全部成功,要么全部失败。
⼀致性指的是数据库总是从⼀个⼀致性的状态转换到另外⼀个⼀致性的状态。⽐如A转账给B100块钱, 假设A只有90块,⽀付之前我们数据库⾥的数据都是符合约束的,但是如果事务执⾏成功了,我们的数据库 数据就破坏约束了,因此事务不能成功,这⾥我们说事务提供了⼀致性的保证
隔离性指的是⼀个事务的修改在最终提交前,对其他事务是不可⻅的。持久性指的是⼀旦事务提交,所做的修改就会永久保存到数据库中。 隔离性有4个隔离级别,分别是:
- read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
⽤户本来应该读取到id=1的⽤户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
- read commit 读已提交,两次读取结果不⼀致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。
⽤户开启事务读取id=1⽤户,查询到age=10,再次读取发现结果=20,在同⼀个事务⾥同⼀个查询读取到不同的结果叫做不可重复读。
-
正在上传…重新上传取消repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都⼀样,但是有可能产
⽣幻读。
- serializable 串⾏,⼀般是不会使⽤的,他会给每⼀⾏读取的数据加锁,会导致⼤量超时和锁竞争的问题。
什么是MVCC
MVCC(Multi-Version Concurrency Control , 多 版 本 并 发 控 制 ) 指 的 就 是 在 使 ⽤ READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执⾏普通的SEELCT操作时访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执⾏,从⽽提升系统性能。READ
COMMITTD、REPEATABLE READ这两个隔离级别的⼀个很⼤不同就是:⽣成ReadView的时机不同,READ COMMITTD在每⼀次进⾏普通SELECT操作前都会⽣成⼀个ReadView,⽽REPEATABLE READ只在第⼀次进⾏普通SELECT操作前⽣成⼀个ReadView,之后的查询操作都重复使⽤这个ReadView就好了。
简述MyISAM和InnoDB的区别
MyISAM:
- 不⽀持事务,但是每次查询都是原⼦的;
- ⽀持表级锁,即每次操作是对整个表加锁;
- 存储表的总⾏数;
- ⼀个MYISAM表有三个⽂件:索引⽂件、表结构⽂件、数据⽂件;
- 采⽤⾮聚集索引,索引⽂件的数据域存储指向数据⽂件的指针。辅索引与主索引基本⼀致,但是辅 索引不⽤保证唯⼀性。
InnoDb:
- ⽀持ACID的事务,⽀持事务的四种隔离级别;
- ⽀持⾏级锁及外键约束:因此可以⽀持写并发;
- 不存储总⾏数;
- ⼀个InnoDb引擎存储在⼀个⽂件空间(共享表空间,表⼤⼩不受操作系统控制,⼀个表可能分布在多个⽂件⾥),也有可能为多个(设置为独⽴表空,表⼤⼩受操作系统⽂件⼤⼩限制,⼀般为2G),受操作系统⽂件⼤⼩的限制;
正在上传…重新上传取消主键索引采⽤聚集索引(索引的数据域存储数据⽂件本身),辅索引的数据域存储主键的值;因此
从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使⽤⾃增主键,防⽌插⼊ 数据时,为维持B+树结构,⽂件的⼤调整。
Explain语句结果中各个字段分表表示什么
列名 | 描述 |
id | 查询语句中每出现⼀个SELECT关键字,MySQL 就会为它分配⼀个唯⼀的id值,某些⼦查询会被优化为join查询,那么出现的id会⼀样 |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的查询⽅式(全表扫描、索引) |
possible_keys | 可能⽤到的索引 |
key | 实际上使⽤的索引 |
key_len | 实际使⽤到的索引⻓度 |
ref | 当使⽤索引列等值查询时,与索引列进⾏等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分 ⽐ |
Extra | ⼀些额外的信息,⽐如排序等 |
索引覆盖是什么
索引覆盖就是⼀个SQL在执⾏时,可以利⽤索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL⾛完索引后不⽤回表了,所需要的字段都在当前索引的叶⼦节点上存在,可以直接作为结果返回了
正在上传…重新上传取消最左前缀原则是什么
当⼀个SQL想要利⽤索引是,就⼀定要提供该索引所对应的字段中最左边的字段,也就是排在最前⾯的 字段,⽐如针对a,b,c三个字段建⽴了⼀个联合索引,那么在写⼀个sql时就⼀定要提供a字段的条件,这样才能⽤到联合索引,这是由于在建⽴a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段 从左往右去⽐较⼤⼩进⾏排序的,所以如果想要利⽤B+树进⾏快速查找也得符合这个规则
Innodb是如何实现事务的
Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以⼀个update语句为例:
-
- Innodb在收到⼀个update语句后,会先根据条件找到数据所在的⻚,并将该⻚缓存在Buffer Pool 中
- 执⾏update语句,修改Buffer Pool中的数据,也就是内存中的数据
- 针对update语句⽣成⼀个RedoLog对象,并存⼊LogBuffer中
- 针对update语句⽣成undolog⽇志,⽤于事务回滚
- 如果事务提交,那么则把RedoLog对象进⾏持久化,后续还有其他机制将Buffer Pool中所修改的数据⻚持久化到磁盘中
- 如果事务回滚,则利⽤undolog⽇志进⾏回滚
B树和B+树的区别,为什么Mysql使⽤B+树
B树的特点:
- 节点排序
- ⼀个节点了可以存多个元素,多个元素也排序了
B+树的特点:
- 拥有B树的特点
- 叶⼦节点之间有指针
- ⾮叶⼦节点上的元素在叶⼦节点上都冗余了,也就是叶⼦节点中存储了所有的元素,并且排好顺序
Mysql索引使⽤的是B+树,因为索引是⽤来加快查询的,⽽B+树通过对数据进⾏排序所以是可以提⾼查询速度的,然后通过⼀个节点中可以存储多个元素,从⽽可以使得B+树的⾼度不会太⾼,在Mysql中⼀ 个Innodb⻚就是⼀个B+树节点,⼀个Innodb⻚默认16kb,所以⼀般情况下⼀颗两层的B+树可以存2000
正在上传…重新上传取消万⾏左右的数据,然后通过利⽤B+树叶⼦节点存储了所有数据并且进⾏了排序,并且叶⼦节点之间有指针,可以很好的⽀持全表扫描,范围查找等SQL语句。
Mysql锁有哪些,如何理解
按锁粒度分类:
- ⾏锁:锁某⾏数据,锁粒度最⼩,并发度⾼
- 表锁:锁整张表,锁粒度最⼤,并发度低
- 间隙锁:锁的是⼀个区间
还可以分为:
- 共享锁:也就是读锁,⼀个事务给某⾏数据加了读锁,其他事务也可以读,但是不能写
- 排它锁:也就是写锁,⼀个事务给某⾏数据加了写锁,其他事务不能读,也不能写
还可以分为:
- 乐观锁:并不会真正的去锁某⾏记录,⽽是通过⼀个版本号来实现的
- 悲观锁:上⾯所的⾏锁、表锁等都是悲观锁
在事务的隔离级别实现中,就需要利⽤锁来解决幻读
Mysql慢查询该如何优化?
- 检查是否⾛了索引,如果没有则优化SQL利⽤索引
- 检查所利⽤的索引,是否是最优索引
- 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
- 检查表中数据是否过多,是否应该进⾏分库分表了
- 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源
什么是RDB和AOF
正在上传…重新上传取消RDB:Redis DataBase,在指定的时间间隔内将内存中的数据集快照写⼊磁盘,实际操作过程是fork⼀个⼦进程,先将数据集写⼊临时⽂件,写⼊成功后,再替换之前的⽂件,⽤⼆进制压缩存储。
优点:
- 整个Redis数据库将只包含⼀个⽂件 dump.rdb,⽅便持久化。
- 容灾性好,⽅便备份。
- 性能最⼤化,fork ⼦进程来完成写操作,让主进程继续处理命令,所以是 IO 最⼤化。使⽤单独⼦进程来进⾏持久化,主进程不会进⾏任何 IO 操作,保证了 redis 的⾼性能
- 相对于数据集⼤时,⽐ AOF 的启动效率更⾼。
缺点:
- 数据安全性低。RDB 是间隔⼀段时间进⾏持久化,如果持久化之间 redis 发⽣故障,会发⽣数据丢失。所以这种⽅式更适合数据要求不严谨的时候)
- 由于RDB是通过fork⼦进程来协助完成数据持久化⼯作的,因此,如果当数据集较⼤时,可能会导致整个服务器停⽌服务⼏百毫秒,甚⾄是1秒钟。
AOF:Append Only File,以⽇志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以⽂本的⽅式记录,可以打开⽂件看到详细的操作记录
优点:
- 数据安全,Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是⾮常⾼的,所差的是⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的 数据将会丢失。⽽每修改同步,我们可以将其视为同步持久化,即每次发⽣的数据变化都会被⽴即 记录到磁盘中。。
- 通过 append 模式写⽂件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis- check-aof ⼯具解决数据⼀致性问题。
- AOF 机制的 rewrite 模式。定期对AOF⽂件进⾏重写,以达到压缩的⽬的
缺点:
- AOF ⽂件⽐ RDB ⽂件⼤,且恢复速度慢。
- 数据集⼤的时候,⽐ rdb 启动效率低。
- 运⾏效率没有RDB⾼
AOF⽂件⽐RDB更新频率⾼,优先使⽤AOF还原数据,AOF⽐RDB更安全也更⼤,RDB性能⽐AOF好, 如果两个都配了优先加载AOF。
正在上传…重新上传取消Redis的过期键的删除策略
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
- 惰性过期:只有当访问⼀个key时,才会判断该key是否已过期,过期则清除。该策略可以最⼤化地节省CPU资源,却对内存⾮常不友好。极端情况可能出现⼤量的过期key没有再次被访问,从⽽不 会被清除,占⽤⼤量内存。
- 定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的expires字典中⼀定数量的key,并清除其 中已过期的key。该策略是⼀个折中⽅案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可 以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的 指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有 键。)
Redis中同时使⽤了惰性过期和定期过期两种过期策略。
简述Redis事务实现
1、事务开始
MULTI命令的执⾏,标识着⼀个事务的开始。MULTI命令会将客户端状态的 flags 属性中打开
REDIS_MULTI 标识来完成的。2、命令⼊队
当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执⾏不同的操作。如果客 户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的⼀个,⽴即执⾏这个命令,否则将命令放
⼊⼀个事务队列⾥⾯,然后向客户端返回 QUEUED 回复
- 如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中⼀个,那么服务器⽴即执⾏这个命令。
- 如果客户端发送的是四个命令以外的其他命令,那么服务器并不⽴即执⾏这个命令。
⾸先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属
正在上传…重新上传取消性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。
如果正确,将这个命令放⼊⼀个事务队列⾥⾯,然后向客户端返回 QUEUED 回复事务队列是按照FIFO的⽅式保存⼊队的命令
3、事务执⾏
客户端发送 EXEC 命令,服务器执⾏ EXEC 命令逻辑。
- 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执⾏。
- 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执⾏事务队列中的所有命令,最后将返回结果全部返回给客户端;
redis 不⽀持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。
Redis 事务不⽀持检查那些程序员⾃⼰逻辑错误。例如对 String 类型的数据库键执⾏对 HashMap 类型的操作!
- WATCH 命令是⼀个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)⾏为。可以监控⼀个或多个键,⼀旦其中有⼀个键被修改(或删除),之后的事务就不会执⾏,监控⼀直持续到EXEC 命令。
- MULTI命令⽤于开启⼀个事务,它总是返回OK。MULTI执⾏之后,客户端可以继续向服务器发送任意多条命令,这些命令不会⽴即被执⾏,⽽是被放到⼀个队列中,当EXEC命令被调⽤时,所有队列中的命令才会被执⾏。
- EXEC:执⾏所有事务块内的命令。返回事务块内所有命令的返回值,按命令执⾏的先后顺序排列。 当操作被打断时,返回空值 nil 。
- 通过调⽤DISCARD,客户端可以清空事务队列,并放弃执⾏事务, 并且客户端会从事务状态中退
出。
- UNWATCH命令可以取消watch对所有key的监控。
Redis 主从复制的核⼼原理
通过执⾏slaveof命令或设置slaveof选项,让⼀个服务器去复制另⼀个服务器的数据。主数据库可以进
⾏读写操作,当写操作导致数据变化时会⾃动将数据同步给从数据库。⽽从数据库⼀般是只读的,并接
正在上传…重新上传取消受主数据库同步过来的数据。⼀个主数据库可以拥有多个从数据库,⽽⼀个从数据库只能拥有⼀个主数
据库。
全量复制:
- 主节点通过bgsave命令fork⼦进程进⾏RDB持久化,该过程是⾮常消耗CPU、内存(⻚表复制)、硬盘IO的
- 主节点通过⽹络将RDB⽂件发送给从节点,对主从节点的带宽都会带来很⼤的消耗
- 从节点清空⽼数据、载⼊新RDB⽂件的过程是阻塞的,⽆法响应客户端的命令;如果从节点执⾏bgrewriteaof,也会带来额外的消耗
部分复制:
- 复制偏移量:执⾏复制的双⽅,主从节点,分别会维护⼀个复制偏移量offset
- 复制积压缓冲区:主节点内部维护了⼀个固定⻓度的、先进先出(FIFO)队列 作为复制积压缓冲区, 当主从节点offset的差距过⼤超过缓冲区⻓度时,将⽆法执⾏部分复制,只能执⾏全量复制。
- 服务器运⾏ID(runid):每个Redis节点,都有其运⾏ID,运⾏ID由节点在启动时⾃动⽣成,主节点会将⾃⼰的运⾏ID发送给从节点,从节点会将主节点的运⾏ID存起来。 从节点Redis断开重连的时
候,就是根据运⾏ID来判断同步的进度:
-
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使⽤部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进⾏全量复制。
Redis有哪些数据结构?分别有哪些典型的应⽤场景?
Redis的数据结构有:
- 字符串:可以⽤来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符 串,Redis分布式锁的实现就利⽤了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
- 哈希表:可以⽤来存储⼀些key-value对,更适合⽤来存储对象
- 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使⽤,可以⽤来缓存类似微信公众号、微博等消息流数据
- 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进⾏交集、并集、差集操作, 从⽽可以实现类似,我和某⼈共同关注的⼈、朋友圈点赞等功能
- 有序集合:集合是⽆序的,有序集合可以设置顺序,可以⽤来实现排⾏榜功能
正在上传…重新上传取消Redis分布式锁底层是如何实现的?
- ⾸先利⽤setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁
- 然后还要利⽤lua脚本来保证多个redis操作的原⼦性
- 同时还要考虑到锁过期,所以需要额外的⼀个看⻔狗定时任务来监听锁是否需要续约
- 同时还要考虑到redis节点挂掉后的情况,所以需要采⽤红锁的⽅式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到
Redis主从复制的核⼼原理
Redis的主从复制是提⾼Redis的可靠性的有效措施,主从复制的流程如下:
- 集群启动时,主从库间会先建⽴连接,为全量复制做准备
- 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB
- 在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis的服务就被中断 了。但是,这些请求中的写操作并没有记录到刚刚⽣成的RDB⽂件中。为了保证主从库的数据⼀致性,主 库会在内存中⽤专⻔的replication buffer,记录RDB⽂件⽣成收到的所有写操作。
- 最后,也就是第三个阶段,主库会把第⼆阶段执⾏过程中新收到的写命令,再发送给从库。具体的操作
是,当主库完成RDB⽂件发送后,就会把此时replocation buffer中修改操作发送给从库,从库再执⾏这些操作。这样⼀来,主从库就实现同步了
- 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步
Redis集群策略
Redis提供了三种集群策略:
- 主从模式:这种模式⽐较简单,主库可以读写,并且会和从库进⾏数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要⼿动修改IP,另外,这种模式也⽐较难进⾏扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能⽀持特⼤数据量
- 哨兵模式:这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕 机,然后在从库中选择⼀个库作为进的主库,另外哨兵也可以做集群,从⽽可以保证但某⼀个哨兵 节点宕机后,还有其他哨兵节点可以继续⼯作,这种模式可以⽐较好的保证Redis集群的⾼可⽤,但 是仍然不能很好的解决Redis的容量上限问题。
- Cluster模式:Cluster模式是⽤得⽐较多的模式,它⽀持多主多从,这种模式会按照key进⾏槽位的 分配,可以使得不同的key分散到不同的主节点上,利⽤这种模式可以使得整个集群⽀持更⼤的数据
正在上传…重新上传取消容量,同时每个主节点可以拥有⾃⼰的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。
对于这三种模式,如果Redis要存的数据量不⼤,可以选择哨兵模式,如果Redis要存的数据量⼤,并且需要持续的扩容,那么选择Cluster模式。
正在上传…重新上传取消缓存穿透 缓存击穿 缓存雪崩分别是什么
缓存中存放的⼤多都是热点数据,⽬的就是防⽌请求可以直接从缓存中获取到数据,⽽不⽤访问Mysql。
- 缓存雪崩:如果缓存中某⼀时刻⼤批热点数据同时过期,那么就可能导致⼤量请求直接访问Mysql 了,解决办法就是在过期时间上增加⼀点随机值,另外如果搭建⼀个⾼可⽤的Redis集群也是防⽌缓存雪崩的有效⼿段
- 缓存击穿:和缓存雪崩类似,缓存雪崩是⼤批热点数据失效,⽽缓存击穿是指某⼀个热点key突然失效,也导致了⼤量请求直接访问Mysql数据库,这就是缓存击穿,解决⽅案就是考虑这个热点key不 设过期时间
- 缓存穿透:假如某⼀时刻访问redis的⼤量key都在redis中不存在(⽐如⿊客故意伪造⼀些乱七⼋糟 的key),那么也会给数据造成压⼒,这就是缓存穿透,解决⽅案是使⽤布隆过滤器,它的作⽤就是如果它认为⼀个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加⼀层布隆过滤器来 拦截不存在的key
Redis和Mysql如何保证数据⼀致
- 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不⼀致
- 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种⽅案能解决1
⽅案的问题,但是在⾼并发下性能较低,⽽且仍然会出现数据不⼀致的问题,⽐如线程1删除了 Redis缓存数据,正在更新Mysql,此时另外⼀个查询再查询,那么就会把Mysql中⽼数据⼜查到Redis中
- 延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟⼏百毫秒再删除Redis缓存数据, 这样就算在更新Mysql时,有其他线程读了Mysql,把⽼数据读到了Redis中,那么也会被删除掉, 从⽽把数据保持⼀致
Redis的持久化机制
RDB:Redis DataBase 将某⼀个时刻的内存快照(Snapshot),以⼆进制的⽅式写⼊磁盘。
⼿动触发:
-
正在上传…重新上传取消save命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所
以在⽣产环境⼀定要慎⽤
- bgsave命令,fork出⼀个⼦进程执⾏持久化,主进程只在fork过程中有短暂的阻塞,⼦进程创建之后,主进程就可以响应客户端请求了
- ⾃动触发:
- save m n :在 m 秒内,如果有 n 个键发⽣改变,则⾃动触发持久化,通过bgsave执⾏,如果设置多个、只要满⾜其⼀就会触发,配置⽂件有默认配置(可以注释掉)
- flushall:⽤于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB⽂件,同时也会⽣成dump.rdb、内容为空
- 主从同步:全量同步时会⾃动触发bgsave命令,⽣成rdb发送给从节点
优点:
- 整个Redis数据库将只包含⼀个⽂件 dump.rdb,⽅便持久化。
- 容灾性好,⽅便备份。
- 性能最⼤化,fork ⼦进程来完成写操作,让主进程继续处理命令,所以是 IO 最⼤化。使⽤单独⼦进程来进⾏持久化,主进程不会进⾏任何 IO 操作,保证了 redis 的⾼性能
- 相对于数据集⼤时,⽐ AOF的启动效率更⾼。缺点:
- 数据安全性低。RDB 是间隔⼀段时间进⾏持久化,如果持久化之间 redis 发⽣故障,会发⽣数据丢失。所以这种⽅式更适合数据要求不严谨的时候)
- 由于RDB是通过fork⼦进程来协助完成数据持久化⼯作的,因此,如果当数据集较⼤时,可能会导致整个服务器停⽌服务⼏百毫秒,甚⾄是1秒钟。会占⽤cpu
AOF:Append Only File 以⽇志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录, 以⽂本的⽅式记录,可以打开⽂件看到详细的操作记录,调操作系统命令进程刷盘
- 所有的写命令会追加到 AOF 缓冲中。
- AOF 缓冲区根据对应的策略向硬盘进⾏同步操作。
- 随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写,达到压缩的⽬的。
- 当 Redis 重启时,可以加载 AOF ⽂件进⾏数据恢复。同步策略:
每秒同步:异步完成,效率⾮常⾼,⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的数据将会丢 失每修改同步:同步持久化,每次发⽣的数据变化都会被⽴即记录到磁盘中,最多丢⼀条 不同步:由操作系统控制,可能丢失较多数据
优点:
- 数据安全
-
正在上传…重新上传取消通过 append 模式写⽂件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis- check-aof ⼯具解决数据⼀致性问题。 - AOF 机制的 rewrite 模式。定期对AOF⽂件进⾏重写,以达到压缩的⽬的缺点:
- AOF ⽂件⽐ RDB ⽂件⼤,且恢复速度慢。
- 数据集⼤的时候,⽐ rdb 启动效率低。
- 运⾏效率没有RDB⾼
对⽐:
- AOF⽂件⽐RDB更新频率⾼,优先使⽤AOF还原数据。AOF⽐RDB更安全也更⼤
- RDB性能⽐AOF好
- 如果两个都配了优先加载AOF
Redis单线程为什么这么快
Redis基于Reactor模式开发了⽹络事件处理器、⽂件事件处理器 fileeventhandler。它是单线程的, 所以 Redis才叫做单线程的模型,它采⽤IO多路复⽤机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现⾼性能的⽹络通信模型,⼜可以跟内部其他单
线程的模块进⾏对接,保证了 Redis内部的线程模型的简单性。
⽂件事件处理器的结构包含4个部分:多个Socket、IO多路复⽤程序、⽂件事件分派器以及事件处理器
(命令请求处理器、命令回复处理器、连接应答处理器等)。
多个 Socket 可能并发的产⽣不同的事件,IO多路复⽤程序会监听多个 Socket,会将 Socket 放⼊⼀个队列中排队,每次从队列中有序、同步取出⼀个 Socket 给事件分派器,事件分派器把 Socket 给对应 的事件处理器。
然后⼀个 Socket 的事件处理完之后,IO多路复⽤程序才会将队列中的下⼀个 Socket 给事件分派器。
⽂件事件分派器会根据每个 Socket 当前产⽣的事件,来选择对应的事件处理器来处理。
- Redis启动初始化时,将连接应答处理器跟AE_READABLE事件关联。
- 若⼀个客户端发起连接,会产⽣⼀个AE_READABLE事件,然后由连接应答处理器负责和客户端建
⽴ 连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件和命令请求处理 器关联,使 得客户端可以向主服务器发送命令请求。
- 当客户端向Redis发请求时(不管读还是写请求),客户端socket都会产⽣⼀个AE_READABLE事 件,触发命令请求处理器。处理器读取客户端的命令内容, 然后传给相关程序执⾏。
- 当Redis服务器准备好给客户端的响应数据后,会将socket的AE_WRITABLE事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在socket产⽣⼀个AE_WRITABLE事件,由对应命令回
正在上传…重新上传取消复处 理器处理,即将准备好的响应数据写⼊socket,供客户端读取。
- 命令回复处理器全部写完到 socket 后,就会删除该socket的AE_WRITABLE事件和命令回复处理器的映射。
单线程快的原因:
- 纯内存操作
- 核⼼是基于⾮阻塞的IO多路复⽤机制
- 单线程反⽽避免了多线程的频繁上下⽂切换带来的性能问题
简述Redis事务实现
- 事务开始:MULTI命令的执⾏,标识着⼀个事务的开始。MULTI命令会将客户端状态的 flags属性中打开REDIS_MULTI标识来完成的。
- 命令⼊队:当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执⾏不同 的操作。如果客 户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的⼀个,⽴即执⾏这个命令,否则将命令放⼊⼀ 个事务队列⾥⾯,然后向客户端返回QUEUED回复,如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中⼀个,那么服务器⽴即执⾏这个命令。如果客户端发送的是四个命令以外的其他命令,那么服务器并不⽴即执⾏这个命令。⾸先检查 此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性关闭REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放⼊⼀个事务队列⾥
⾯,然后向客户端返回 QUEUED 回复事务队列是按照FIFO的⽅式保存⼊队的命令
- 事务执⾏:客户端发送 EXEC 命令,服务器执⾏ EXEC 命令逻辑。如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执⾏。 否则客户端处于事务状态(flags有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执⾏事务队列中的所有命令,最后将返回结果全部返回给客户端;Redis不
⽀持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。Redis事务不⽀持检查那些程序员
⾃⼰逻辑错误。例如对 String 类型的数据库键执⾏对 HashMap 类型的操作!
什么是CAP理论
CAP理论是分布式领域中⾮常重要的⼀个指导理论,C(Consistency)表示强⼀致性,A
(Availability)表示可⽤性,P(Partition Tolerance)表示分区容错性,CAP理论指出在⽬前的硬件
正在上传…重新上传取消条件下,⼀个分布式系统是必须要保证分区容错性的,⽽在这个前提下,分布式系统要么保证CP,要么保证AP,⽆法同时保证CAP。
分区容错性表示,⼀个系统虽然是分布式的,但是对外看上去应该是⼀个整体,不能由于分布式系统内部的某个结点挂点,或⽹络出现了故障,⽽导致系统对外出现异常。所以,对于分布式系统⽽⾔是⼀定 要保证分区容错性的。
强⼀致性表示,⼀个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提供服务的,不然就会造成数据不⼀致,所以强⼀致性和可⽤性是不能同时满⾜的。
可⽤性表示,⼀个分布式系统对外要保证可⽤。
什么是BASE理论
由于不能同时满⾜CAP,所以出现了BASE理论:
- BA:Basically Available,表示基本可⽤,表示可以允许⼀定程度的不可⽤,⽐如由于系统故障, 请求时间变⻓,或者由于系统故障导致部分⾮核⼼功能不可⽤,都是允许的
- S:Soft state:表示分布式系统可以处于⼀种中间状态,⽐如数据正在同步
- E:Eventually consistent,表示最终⼀致性,不要求分布式系统数据实时达到⼀致,允许在经过⼀段时间后再达到⼀致,在达到⼀致过程中,系统也是可⽤的
什么是RPC
RPC,表示远程过程调⽤,对于Java这种⾯试对象语⾔,也可以理解为远程⽅法调⽤,RPC调⽤和HTTP调⽤是有区别的,RPC表示的是⼀种调⽤远程⽅法的⽅式,可以使⽤HTTP协议、或直接基于TCP 协议来实现RPC,在Java中,我们可以通过直接使⽤某个服务接⼝的代理对象来执⾏⽅法,⽽底层则通过构造HTTP请求来调⽤远端的⽅法,所以,有⼀种说法是RPC协议是HTTP协议之上的⼀种协议,也是可以理解的。
数据⼀致性模型有哪些
- 强⼀致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对
⽤户 最友好的,就是⽤户上⼀次写什么,下⼀次就保证能读到什么。根据 CAP理论,这种实现需要牺牲可⽤性。
- 弱⼀致性:系统在数据写⼊成功之后,不承诺⽴即可以读到最新写⼊的值,也不会具体的承诺多久 之后 可以读到。⽤户读到某⼀操作对系统数据的更新需要⼀段时间,我们称这段时间为“不⼀致性
正在上传…重新上传取消窗⼝”。
- 最终⼀致性:最终⼀致性是弱⼀致性的特例,强调的是所有的数据副本,在经过⼀段时间的同步之后, 最终都能够达到⼀个⼀致的状态。因此,最终⼀致性的本质是需要系统保证最终数据能够达到⼀致,⽽ 不需要实时保证系统数据的强⼀致性。到达最终⼀致性的时间 ,就是不⼀致窗⼝时间,在没有故障发⽣的前提下,不⼀致窗⼝的时间主要受通信延迟,系统负载和复制副本的个数影响。最终⼀致性模型根据其提供的不同保证可以划分为更多的模型,包括因果⼀致性和会话⼀致性等。
分布式ID是什么?有哪些解决⽅案?
在开发中,我们通常会需要⼀个唯⼀ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护⼀个⾃增数字来作为ID都是可以的,但对于⼀个分布式系统,就会有可能会出现ID冲突,此时有以下解决⽅案:
- uuid,这种⽅案复杂度最低,但是会影响存储空间和性能
- 利⽤单机数据库的⾃增主键,作为分布式ID的⽣成器,复杂度适中,ID⻓度较之uuid更短,但是受到单机数据库性能的限制,并发量⼤的时候,此⽅案也不是最优⽅案
- 利⽤redis、zookeeper的特性来⽣成id,⽐如redis的⾃增命令、zookeeper的顺序节点,这种⽅案和单机数据库(mysql)相⽐,性能有所提⾼,可以适当选⽤
- 雪花算法,⼀切问题如果能直接⽤算法解决,那就是最合适的,利⽤雪花算法也可以⽣成分布式 ID,底层原理就是通过某台机器在某⼀毫秒内对某⼀个数字⾃增,这种⽅案也能保证分布式架构中的系统id唯⼀,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。
分布式锁的使⽤场景是什么?有哪些实现⽅案?
在单体架构中,多个线程都是属于同⼀个进程的,所以在线程并发执⾏时,遇到资源竞争时,可以利⽤ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使⽤。
⽽在分布式架构中,多个线程是可能处于不同进程中的,⽽这些线程并发执⾏遇到资源竞争时,利⽤ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要⼀个分布式锁⽣成器,分布式系统中的应⽤程序都可以来使⽤这个⽣成器所提供的锁,从⽽ 达到多个进程中的线程使⽤同⼀把锁。
⽬前主流的分布式锁的实现⽅案有两种:
-
正在上传…重新上传取消zookeeper:利⽤的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是⾼⼀致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱 - redis:利⽤redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是⾼可⽤, 因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦redis中的数据出现了不⼀致),可能会出现多个客户端同时加到锁的情况
什么是分布式事务?有哪些实现⽅案?
在分布式系统中,⼀次业务处理可能需要多个应⽤来实现,⽐如⽤户发送⼀次下单请求,就涉及到订单系统创建订单、库存系统减库存,⽽对于⼀次下单,订单创建与减库存应该是要同时成功或同时失败 的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这 类问题,就需要⽤到分布式事务。常⽤解决⽅案有:
- 本地消息表:创建订单时,将减库存消息加⼊在本地事务中,⼀起提交到数据库存⼊本地消息表, 然后调⽤库存系统,如果调⽤成功则修改本地消息状态为成功,如果调⽤库存系统失败,则由后台 定时任务从本地消息表中取出未成功的消息,重试调⽤库存系统
- 消息队列:⽬前RocketMQ中⽀持事务消息,它的⼯作原理是:
- ⽣产者订单系统先发送⼀条half消息到Broker,half消息对消费者⽽⾔是不可⻅的
- 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
- 并且⽣产者订单系统还可以提供Broker回调接⼝,当Broker发现⼀段时间half消息没有收到任何操作命令,则会主动调此接⼝来查询订单是否创建成功
- ⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
- 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理
- Seata:阿⾥开源的分布式事务框架,⽀持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的
什么是ZAB协议
ZAB协议是Zookeeper⽤来实现⼀致性的原⼦⼴播协议,该协议描述了Zookeeper是如何实现⼀致性的,分为三个阶段:
- 领导者选举阶段:从Zookeeper集群中选出⼀个节点作为Leader,所有的写请求都会由Leader节点来处理
- 数据同步阶段:集群中所有节点中的数据要和Leader节点保持⼀致,如果不⼀致则要进⾏同步
- 请求⼴播阶段:当Leader节点接收到写请求时,会利⽤两阶段提交来⼴播该写请求,使得写请求像事务⼀样在其他节点上执⾏,达到节点上的数据实时⼀致
正在上传…重新上传取消但值得注意的是,Zookeeper只是尽量的在达到强⼀致性,实际上仍然只是最终⼀致性的。
为什么Zookeeper可以⽤来作为注册中⼼
可以利⽤Zookeeper的临时节点和watch机制来实现注册中⼼的⾃动注册和发现,另外Zookeeper中的 数据都是存在内存中的,并且Zookeeper底层采⽤了nio,多线程模型,所以Zookeeper的性能也是⽐较
⾼的,所以可以⽤来作为注册中⼼,但是如果考虑到注册中⼼应该是注册可⽤性的话,那么Zookeeper 则不太合适,因为Zookeeper是CP的,它注重的是⼀致性,所以集群数据不⼀致时,集群将不可⽤,所以⽤Redis、Eureka、Nacos来作为注册中⼼将更合适。
Zookeeper中的领导者选举的流程是怎样的?
对于Zookeeper集群,整个集群需要从集群节点中选出⼀个节点作为Leader,⼤体流程如下:
- 集群中各个节点⾸先都是观望状态(LOOKING),⼀开始都会投票给⾃⼰,认为⾃⼰⽐较适合作为leader
- 然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先⽐较zxid,zxid⼤者获胜,zxid如果相等则⽐较myid,myid⼤者获胜
- ⼀个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更⼤的节点,并将选票放⼊⾃⼰的投票箱中,并将新的选票发送给其他节点
- 如果pk是平局则将接收到的选票放⼊⾃⼰的投票箱中
- 如果pk赢了,则忽略所接收到的选票
- 当然⼀个节点将⼀张选票放⼊到⾃⼰的投票箱之后,就会从投票箱中统计票数,看是否超过⼀半的节点都和⾃⼰所投的节点是⼀样的,如果超过半数,那么则认为当前⾃⼰所投的节点是leader
- 集群中每个节点都会经过同样的流程,pk的规则也是⼀样的,⼀旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是⼀样的,所以各个节点最终选出来的leader也是⼀样
的,这样集群的leader就选举出来了
Zookeeper集群中节点之间数据是如何同步的
- ⾸先集群启动时,会先进⾏领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer
- 然后Leader会和其他节点进⾏数据同步,采⽤发送快照和发送Diff⽇志的⽅式
- 集群在⼯作过程中,所有的写请求都会交给Leader节点来进⾏处理,从节点只能处理读请求
- Leader节点收到⼀个写请求时,会通过两阶段机制来处理
-
正在上传…重新上传取消Leader节点会将该写请求对应的⽇志发送给其他Follower节点,并等待Follower节点持久化⽇志成功 - Follower节点收到⽇志后会进⾏持久化,如果持久化成功则发送⼀个Ack给Leader节点
- 当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据
- 然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各⾃本地内存数据
- 同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执⾏更新本地内存数据
- 最后Leader节点返回客户端写请求响应成功
- 通过同步机制和两阶段提交机制来达到集群中节点数据⼀致
Dubbo⽀持哪些负载均衡策略
-
- 随机:从多个服务提供者随机选择⼀个来处理本次请求,调⽤量越⼤则分布越均匀,并⽀持按权重设置随机概率
- 轮询:依次选择服务提供者来处理请求, 并⽀持按权重进⾏轮询,底层采⽤的是平滑加权轮询算法
- 最⼩活跃调⽤数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最⼩的服务器 来处理
- ⼀致性哈希:相同参数的请求总是发到同⼀个服务提供者
Dubbo是如何完成服务导出的?
- ⾸先Dubbo会将程序员所使⽤的@DubboService注解或@Service注解进⾏解析得到程序员所定义的 服务参数,包括定义的服务名、服务接⼝、服务超时时间、服务协议等等,得到⼀个
ServiceBean。
- 然后调⽤ServiceBean的export⽅法进⾏服务导出
- 然后将服务信息注册到注册中⼼,如果有多个协议,多个注册中⼼,那就将服务按单个协议,单个注册中⼼进⾏注册
- 将服务信息注册到注册中⼼后,还会绑定⼀些监听器,监听动态配置中⼼的变更
- 还会根据服务协议启动对应的Web服务器或⽹络框架,⽐如Tomcat、Netty等
Dubbo是如何完成服务引⼊的?
- 当程序员使⽤@Reference注解来引⼊⼀个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引⽤的服务名、服务接⼝是什么
- 然后从注册中⼼进⾏查询服务信息,得到服务的提供者信息,并存在消费端的服务⽬录中
- 并绑定⼀些监听器⽤来监听动态配置中⼼的变更
- 然后根据查询得到的服务提供者信息⽣成⼀个服务接⼝的代理对象,并放⼊Spring容器中作为Bean
Dubbo的架构设计是怎样的?
Dubbo中的架构设计是⾮常优秀的,分为了很多层次,并且每层都是可以扩展的,⽐如:
- Proxy服务代理层,⽀持JDK动态代理、javassist等代理机制
- Registry注册中⼼层,⽀持Zookeeper、Redis等作为注册中⼼
- Protocol远程调⽤层,⽀持Dubbo、Http等调⽤协议
- Transport⽹络传输层,⽀持netty、mina等⽹络传输框架
- Serialize数据序列化层,⽀持JSON、Hessian等序列化机制
正在上传…重新上传取消各层说明
-
正在上传…重新上传取消config 配置层:对外配置接⼝,以 , 始化配置类,也可以通过 spring 解析配置⽣成配置类
为中⼼,可以直接初
- proxy 服务代理层:服务接⼝透明代理,⽣成服务的客户端 Stub 和服务器端 Skeleton, 以
正在上传…重新上传取消ServiceProxy 为中⼼,扩展接⼝为
- registry 注册中⼼层:封装服务地址的注册与发现,以服务 URL 为中⼼,扩展接⼝为
正在上传…重新上传取消RegistryFactory , Registry ,
-
正在上传…重新上传取消cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中⼼,以 为中⼼,扩
正在上传…重新上传取消展接⼝为 ,
, Router ,
-
正在上传…重新上传取消monitor 监控层:RPC 调⽤次数和调⽤时间监控,以 为中⼼,扩展接⼝为
正在上传…重新上传取消MonitorFactory , ,
- protocol 远程调⽤层:封装 RPC 调⽤,以
, Result 为中⼼,扩展接⼝为
正在上传…重新上传取消Protocol , , Exporter
- exchange 信息交换层:封装请求响应模式,同步转异步,以
, Response 为中⼼,扩
正在上传…重新上传取消展接⼝为 , , ,
正在上传…重新上传取消transport ⽹络传输层:抽象 mina 和 netty 为统⼀接⼝,以 为中⼼,扩展接⼝为
正在上传…重新上传取消Channel , , Client , Server ,
正在上传…重新上传取消serialize 数据序列化层:可复⽤的⼀些⼯具,扩展接⼝为 , ,
正在上传…重新上传取消ObjectOutput ,
关系说明
-
正在上传…重新上传取消在 RPC 中,Protocol 是核⼼层,也就是只要有 Protocol + Invoker + Exporter 就可以完成⾮透明
的 RPC 调⽤,然后在 Invoker 的主过程上 Filter 拦截点。
- 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不⽤ Client 和 Server 的原因是 Dubbo 在很多场景下都使⽤ Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统⼀概念。
- ⽽ Cluster 是外围概念,所以 Cluster 的⽬的是将多个 Invoker 伪装成⼀个 Invoker,这样其它⼈只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有⼀个提供者时,是不需要 Cluster 的。
- Proxy 层封装了所有接⼝的透明化代理,⽽在其它层都以 Invoker 为中⼼,只有到了暴露给⽤户使
⽤时,才⽤ Proxy 将 Invoker 转成接⼝,或将接⼝实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务⼀样调远程服务。
- ⽽ Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会⽤上, Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,⽽ Exchange 层是在传输层之上封装了 Request-Response 语义。
- Registry 和 Monitor 实际上不算⼀层,⽽是⼀个独⽴的节点,只是为了全局概览,⽤层的⽅式画在
⼀起。
正在上传…重新上传取消
负载均衡算法有哪些
1、轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每⼀台服务器,⽽不关⼼服 务器实际的连接数和当前的系统负载。
2、随机法:通过系统的随机算法,根据后端服务器的列表⼤⼩值来随机选取其中的⼀台服务器进⾏访问。由概率统计理论可以得知,随着客户端调⽤服务端的次数增多,其实际效果越来越接近于平均分配 调⽤量到后端的每⼀台服务器,也就是轮询的结果。
3、源地址哈希法:源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的⼀个数值,
⽤该数值对服务器列表的⼤⼩进⾏取模运算,得到的结果便是客服端要访问服务器的序号。采⽤源地址 哈希法进⾏负载均衡,同⼀IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同⼀台后端服务器进⾏访问。
正在上传…重新上传取消4、加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能⼒ 也不相同。给配置⾼、负载低的机器配置更⾼的权重,让其处理更多的请;⽽配置低、负载⾼的机器, 给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这⼀问题,并将请求顺序且按照权重分 配到后端。
5、加权随机法:与加权轮询法⼀样,加权随机法也根据后端机器的配置,系统的负载分配不同的权 重。不同的是,它是按照权重随机请求后端服务器,⽽⾮顺序。
6、最⼩连接数法:最⼩连接数算法⽐较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处 理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的⼀台服务器 来处理当前的请求,尽可能地提⾼后端服务的利⽤效率,将负责合理地分流到每⼀台服务器。
分布式架构下,Session 共享有什么⽅案
1、采⽤⽆状态服务,抛弃session 2、存⼊cookie(有安全⻛险)
3、服务器之间进⾏ Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务器数量⽐较多的时候,同步是会有延迟甚⾄同步失败;
4、 IP 绑定策略
使⽤ Nginx (或其他复杂均衡软硬件)中的 IP 绑定策略,同⼀个 IP 只能在指定的同⼀个机器访问,但是这样做失去了负载均衡的意义,当挂掉⼀台服务器的时候,会影响⼀批⽤户的使⽤,⻛险很⼤;
5、使⽤ Redis 存储
把 Session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问⼀次 Redis ,但是这种⽅案带来的好处也是很⼤的:
- 实现了 Session 共享;
- 可以⽔平扩展(增加 Redis 服务器);
- 服务器重启 Session 不丢失(不过也要注意 Session 在 Redis 中的刷新/失效机制);
正在上传…重新上传取消不仅可以跨服务器 Session 共享,甚⾄可以跨平台(例如⽹⻚端和 APP 端)。
如何实现接⼝的幂等性
- 唯⼀id。每次操作,都根据操作和内容⽣成唯⼀的id,在执⾏之前先判断id是否存在,如果不存在则 执⾏后续操作,并且保存到数据库或者redis等。
- 服务端提供发送token的接⼝,业务调⽤接⼝前先获取token,然后调⽤业务接⼝请求时,把token携带过去,务器判断token是否存在redis中,存在表示第⼀次请求,可以继续执⾏业务,执⾏业务完成后,最后需要把redis中的token删除
- 建去重表。将业务中有唯⼀标识的字段保存到去重表,如果表中存在,则表示已经处理过了
- 版本控制。增加版本号,当版本号符合时,才能更新数据
- 状态控制。例如订单有状态已⽀付 未⽀付 ⽀付中 ⽀付失败,当处于未⽀付的时候才允许修改为⽀
付中等
简述zk的命名服务、配置管理、集群管理
命名服务:
通过指定的名字来获取资源或者服务地址。Zookeeper可以创建⼀个全局唯⼀的路径,这个路径就可以 作为⼀个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。⼀些分布式服 务框架(RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据特定的名字来获取 资源的实体、服务地址和提供者信息等
配置管理:
实际项⽬开发中,经常使⽤.properties或者xml需要配置很多信息,如数据库连接信息、fps地址端⼝等 等。程序分布式部署时,如果把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即znode会发⽣变化时,可以通过改变zk中某个⽬录节点的内容,利⽤watcher通知给各个客户端,从⽽ 更改配置。
集群管理:
集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器和加⼊机器。zookeeper可以⽅ 便集群机器的管理,它可以实时监控znode节点的变化,⼀旦发现有机器挂了,该机器就会与zk断开连
正在上传…重新上传取消接,对应的临时⽬录节点会被删除,其他所有机器都收到通知。新机器加⼊也是类似。
讲下Zookeeper中的watch机制
客户端,可以通过在znode上设置watch,实现实时监听znode的变化
Watch事件是⼀个⼀次性的触发器,当被设置了Watch的数据发⽣了改变的时候,则服务器将这个改变发送给设置了Watch的客户端
- ⽗节点的创建,修改,删除都会触发Watcher事件。
- ⼦节点的创建,删除会触发Watcher事件。
⼀次性:⼀旦被触发就会移除,再次使⽤需要重新注册,因为每次变动都需要通知所有客户端,⼀次性可以减轻压⼒,3.6.0默认持久递归,可以触发多次
轻量:只通知发⽣了事件,不会告知事件内容,减轻服务器和带宽压⼒
Watcher 机制包括三个⻆⾊:客户端线程、客户端的 WatchManager 以及 ZooKeeper 服务器
- 客户端向 ZooKeeper 服务器注册⼀个 Watcher 监听,
- 把这个监听信息存储到客户端的 WatchManager 中
- 当 ZooKeeper 中的节点发⽣变化时,会通知客户端,客户端会调⽤相应 Watcher 对象中的回调
⽅法。watch回调是串⾏同步的
Zookeeper和Eureka的区别
zk:CP设计(强⼀致性),⽬标是⼀个分布式的协调系统,⽤于进⾏资源的统⼀管理。当节点crash后,需要进⾏leader的选举,在这个期间内,zk服务是不可⽤的。
eureka:AP设计(⾼可⽤),⽬标是⼀个服务注册发现系统,专⻔⽤于微服务的服务发现注册。
Eureka各个节点都是平等的,⼏个节点挂掉不会影响正常节点的⼯作,剩余的节点依然可以提供注册和 查询服务。⽽Eureka的客户端在向某个Eureka注册时如果发现连接失败,会⾃动切换⾄其他节点,只要
正在上传…重新上传取消有⼀台Eureka还在,就能保证注册服务可⽤(保证可⽤性),只不过查到的信息可能不是最新的(不保证强⼀致性)
同时当eureka的服务端发现85%以上的服务都没有⼼跳的话,它就会认为⾃⼰的⽹络出了问题,就不会 从服务列表中删除这些失去⼼跳的服务,同时eureka的客户端也会缓存服务信息。eureka对于服务注册 发现来说是⾮常好的选择。
存储拆分后如何解决唯⼀主键问题
- UUID:简单、性能好,没有顺序,没有业务含义,存在泄漏mac地址的⻛险
- 数据库主键:实现简单,单调递增,具有⼀定的业务可读性,强依赖db、存在性能瓶颈,存在暴露业务 信息的⻛险
- redis,mongodb,zk等中间件:增加了系统的复杂度和稳定性
- 雪花算法
雪花算法原理
第⼀位符号位固定为0,41位时间戳,10位workId,12位序列号,位数可以有不同实现。
优点:每个毫秒值包含的ID值很多,不够可以变动位数来增加,性能佳(依赖workId的实现)。时间戳值在⾼位,中间是固定的机器码,⾃增的序列在低位,整个ID是趋势递增的。能够根据业务场景数据库 节点布置灵活调整bit位划分,灵活度⾼。
缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的ID⽣成,所以⼀般基于此的算法发现时钟回拨,都会抛异常处理,阻⽌ID⽣成,这可能导致服务不可⽤。
正在上传…重新上传取消如何解决不使⽤分区键的查询问题
- 映射:将查询条件的字段与分区键进⾏映射,建⼀张单独的表维护(使⽤覆盖索引)或者在缓存中维护
- 基因法:分区键的后x个bit位由查询字段进⾏hash后占⽤,分区键直接取x个bit位获取分区,查询字段进⾏hash获取分区,适合⾮分区键查询字段只有⼀个的情况
- 冗余:查询字段冗余存储
Spring Cloud有哪些常⽤组件,作⽤是什么?
- Eureka:注册中⼼
- Nacos:注册中⼼、配置中⼼
- Consul:注册中⼼、配置中⼼
- Spring Cloud Config:配置中⼼
- Feign/OpenFeign:RPC调⽤
- Kong:服务⽹关
- Zuul:服务⽹关
- Spring Cloud Gateway:服务⽹关
- Ribbon:负载均衡
- Spring CLoud Sleuth:链路追踪
- Zipkin:链路追踪
- Seata:分布式事务
- Dubbo:RPC调⽤
- Sentinel:服务熔断
- Hystrix:服务熔断
如何避免缓存穿透、缓存击穿、缓存雪崩?
缓存雪崩是指缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内 承受⼤量请求⽽崩掉。
解决⽅案:
- 缓存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣。
- 给每⼀个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓
存。
- 缓存预热互斥锁
正在上传…重新上传取消缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承
受⼤量请求⽽崩掉。解决⽅案:
- 接⼝层增加校验,如⽤户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太⻓会导致正常情况也没法使⽤)。这样可以防⽌攻击⽤户 反复⽤同⼀个id暴⼒攻击
- 采⽤布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的 bitmap 中,⼀个⼀定不存在的数据会被这个 bitmap 拦截掉,从⽽避免了对底层存储系统的查询压⼒
缓存击穿是指缓存中没有但数据库中有的数据(⼀般是缓存时间到期),这时由于并发⽤户特别多,同时读缓存没读到数据,⼜同时去数据库去取数据,引起数据库压⼒瞬间增⼤,造成过⼤压⼒。和缓存雪 崩不同的是,缓存击穿指并发查同⼀条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从⽽ 查 数据库。
解决⽅案:
- 设置热点数据永远不过期。加互斥锁
分布式系统中常⽤的缓存⽅案有哪些
- 客户端缓存:⻚⾯和浏览器缓存,APP缓存,H5缓存,localStorage 和 sessionStorage CDN缓存:内容存储:数据的缓存,内容分发:负载均衡
- nginx缓存:静态资源
- 服务端缓存:本地缓存,外部缓存
- 数据库缓存:持久层缓存(mybatis,hibernate多级缓存),mysql查询缓存 操作系统缓存:
PageCache、BufferCache
缓存过期都有哪些策略?
- 定时过期:每个设置过期时间的key都需要创建⼀个定时器,到过期时间就会⽴即清除。该策略可以
⽴ 即清除过期的数据,对内存很友好;但是会占⽤⼤量的CPU资源去处理过期的数据,从⽽影响缓存的响应时间和吞吐量
- 惰性过期:只有当访问⼀个key时,才会判断该key是否已过期,过期则清除。该策略可以最⼤化地节省CPU资源,但是很消耗内存、许多的过期数据都还存在内存中。极端情况可能出现⼤量的过期
正在上传…重新上传取消key没有 再次被访问,从⽽不会被清除,占⽤⼤量内存。
- 定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的expires字典中⼀定数量的key(是随机的), 并清除其中已过期的key。该策略是定时过期和惰性过期的折中⽅案。通过调整定时扫描的时间间隔和 每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
- 分桶策略:定期过期的优化,将过期时间点相近的key放在⼀起,按时间扫描分桶。
常⻅的缓存淘汰算法
- FIFO(First In First Out,先进先出),根据缓存被存储的时间,离当前最远的数据优先被淘汰;
- LRU(LeastRecentlyUsed,最近最少使⽤),根据最近被使⽤的时间,离当前最远的数据优先被淘汰;
- LFU(LeastFrequentlyUsed,最不经常使⽤),在⼀段时间内,缓存数据被使⽤次数最少的会被
淘汰。
布隆过滤器原理,优缺点
- 位图:int[10],每个int类型的整数是4*8=32个bit,则int[10]⼀共有320 bit,每个bit⾮0即1,初始
化时都是0
- 添加数据时:将数据进⾏hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个, 则 ⼀个数据添加会将多个(hash函数个数)bit改为1,多个hash函数的⽬的是减少hash碰撞的概率
- 查询数据:hash函数计算得到hash值,对应到bit中,如果有⼀个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中
优点:
- 占⽤内存⼩
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,⼀般⽐较⼩),与数据量⼤⼩⽆关哈希函数相互之间没有关系,⽅便硬件并⾏运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求⽐较严格的场合有很⼤优势 数据量很⼤时,布
隆过滤器可以表示全集
- 使⽤同⼀组散列函数的布隆过滤器可以进⾏交、并、差运算
缺点:
- 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中不能获取元素本身
-
正在上传…重新上传取消⼀般情况下不能从布隆过滤器中删除元素
分布式缓存寻址算法
- hash算法:根据key进⾏hash函数运算、结果对分⽚数取模,确定分⽚ 适合固定分⽚数的场景, 扩展分⽚或者减少分⽚时,所有数据都需要重新计算分⽚、存储
- ⼀致性hash:将整个hash值得区间组织成⼀个闭合的圆环,计算每台服务器的hash值、映射到圆环中。使⽤相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到的第⼀个服务器就是数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的⼀个服务器之间的值 存在hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
- hash slot:将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进⾏hash决定存放的slot,新增及删除节点时,将slot进⾏迁移即可
Spring Cloud和Dubbo有哪些区别?
Spring Cloud是⼀个微服务框架,提供了微服务领域中的很多功能组件,Dubbo⼀开始是⼀个RPC调⽤框架,核⼼是解决服务调⽤间的问题,Spring Cloud是⼀个⼤⽽全的框架,Dubbo则更侧重于服务调
⽤,所以Dubbo所提供的功能没有Spring Cloud全⾯,但是Dubbo的服务调⽤性能⽐Spring Cloud⾼, 不过Spring Cloud和Dubbo并不是对⽴的,是可以结合起来⼀起使⽤的。
什么是服务雪崩?什么是服务限流?
-
- 当服务A调⽤服务B,服务B调⽤C,此时⼤量请求突然请求服务A,假如服务A本身能抗住这些请 求,但是如果服务C抗不住,导致服务C请求堆积,从⽽服务B请求堆积,从⽽服务A不可⽤,这就是服务雪崩,解决⽅式就是服务降级和服务熔断。
- 服务限流是指在⾼并发请求下,为了保护系统,可以对访问服务的请求进⾏数量上的限制,从⽽防
⽌系统不被⼤量请求压垮,在秒杀中,限流是⾮常重要的。
什么是服务熔断?什么是服务降级?区别是什么?
- 服务熔断是指,当服务A调⽤的某个服务B不可⽤时,上游服务A为了保证⾃⼰不受影响,从⽽不再调⽤服务B,直接返回⼀个结果,减轻服务A和服务B的压⼒,直到服务B恢复。
-
正在上传…重新上传取消服务降级是指,当发现系统压⼒过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压
⼒,这就是服务降级。
相同点:
- 都是为了防⽌系统崩溃
- 都让⽤户体验到某些功能暂时不可⽤
不同点:熔断是下游服务故障触发的,降级是为了降低系统负载
SOA、分布式、微服务之间有什么关系和区别?
- 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
- SOA是⼀种⾯向服务的架构,系统的所有服务都注册在总线上,当调⽤服务时,从总线上查找服务信息,然后调⽤
- 微服务是⼀种更彻底的⾯向服务的架构,将系统中各个功能个体抽成⼀个个⼩的应⽤程序,基本保持⼀个应⽤对应的⼀个服务的架构
怎么拆分微服务?
拆分微服务的时候,为了尽量保证微服务的稳定,会有⼀些基本的准则:
- 微服务之间尽量不要有业务交叉。
- 微服务之前只能通过接⼝进⾏服务调⽤,⽽不能绕过接⼝直接访问对⽅的数据。
- ⾼内聚,低耦合。
怎样设计出⾼内聚、低耦合的微服务?
⾼内聚低耦合,是⼀种从上⽽下指导微服务设计的⽅法。实现⾼内聚低耦合的⼯具主要有 同步的接⼝调
⽤ 和 异步的事件驱动 两种⽅式。
有没有了解过DDD领域驱动设计?
什么是DDD: 在2004年,由Eric Evans提出了, DDD是⾯对软件复杂之道。Domain-Driven- Design –Tackling Complexity in the Heart of Software
正在上传…重新上传取消⼤泥团: 不利于微服务的拆分。⼤泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂, 这个泥团⼜会膨胀成为⼤泥团。
DDD只是⼀种⽅法论,没有⼀个稳定的技术框架。DDD要求领域是跟技术⽆关、跟存储⽆关、跟通信⽆关。
什么是中台?
所谓中台,就是将各个业务线中可以复⽤的⼀些功能抽取出来,剥离个性,提取共性,形成⼀些可复⽤的组件。
⼤体上,中台可以分为三类 业务中台、数据中台和技术中台。⼤数据杀熟-数据中台
中台跟DDD结合: DDD会通过限界上下⽂将系统拆分成⼀个⼀个的领域, ⽽这种限界上下⽂,天⽣就成了中台之间的逻辑屏障。
DDD在技术与资源调度⽅⾯都能够给中台建设提供不错的指导。
DDD分为战略设计和战术设计。 上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。
你的项⽬中是怎么保证微服务敏捷开发的?
- 开发运维⼀体化。
- 敏捷开发: ⽬的就是为了提⾼团队的交付效率,快速迭代,快速试错
- 每个⽉固定发布新版本,以分⽀的形式保存到代码仓库中。快速⼊职。任务⾯板、站⽴会议。团队
⼈员灵活流动,同时形成各个专家代表
- 测试环境- ⽣产环境 -开发测试环境SIT-集成测试环境-压测环境STR-预投产环境-⽣产环境PRD
- 晨会、周会、需求拆分会
如何进⾏消息队列选型?
- Kafka:
- 优点: 吞吐量⾮常⼤,性能⾮常好,集群⾼可⽤。
- 缺点:会丢数据,功能⽐较单⼀。
- 使⽤场景:⽇志分析、⼤数据采集
- RabbitMQ:
- 优点: 消息可靠性⾼,功能全⾯。
- 缺点:吞吐量⽐较低,消息积累会严重影响性能。erlang语⾔不好定制。
- 使⽤场景:⼩规模场景。
-
正在上传…重新上传取消RocketMQ:- 优点:⾼吞吐、⾼性能、⾼可⽤,功能⾮常全⾯。
- 缺点:开源版功能不如云上商业版。官⽅⽂档和周边⽣态还不够成熟。客户端只⽀持java。
- 使⽤场景:⼏乎是全场景。
RocketMQ的事务消息是如何实现的
-
- ⽣产者订单系统先发送⼀条half消息到Broker,half消息对消费者⽽⾔是不可⻅的
- 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
- 并且⽣产者订单系统还可以提供Broker回调接⼝,当Broker发现⼀段时间half消息没有收到任何操作命令,则会主动调此接⼝来查询订单是否创建成功
- ⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
- 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理
为什么RocketMQ不使⽤Zookeeper作为注册中⼼呢?
根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能保证 服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不可⽤的状 态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性⽽设计。
基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分领域,划
正在上传…重新上传取消分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A的设计,导致 服务之间是不连通的。
持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重了。⽽且本身 存储的数据应该是⾼度定制化的。
消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息的时 候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时间内对于⽣ 产者和消费者并不会产⽣太⼤影响。
RocketMQ的实现原理
RocketMQ由NameServer注册中⼼集群、Producer⽣产者集群、Consumer消费者集群和若⼲Broker
(RocketMQ进程)组成,它的架构原理是这样的:
Broker在启动的时候去向所有的NameServer注册,并保持⻓连接,每30s发送⼀次⼼跳
Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择⼀台服务器 来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
RocketMQ为什么速度快
因为使⽤了顺序存储、Page Cache和异步刷盘。我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多,写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache,最后由操作系统异步将缓存中的数据刷到磁盘
消息队列如何保证消息可靠传输
消息可靠传输代表了两层意思,既不能多也不能少。
- 为了保证消息不多,也就是消息不能重复,也就是⽣产者不能重复⽣产消息,或者消费者不能重复消费消息
- ⾸先要确保消息不多发,这个不常出现,也⽐较难控制,因为如果出现了多发,很⼤的原因是⽣产者⾃⼰的原因,如果要避免出现问题,就需要在消费端做控制
- 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决⽣产者重复发送消息的问题
正在上传…重新上传取消消息不能少,意思就是消息不能丢失,⽣产者发送的消息,消费者⼀定要能消费到,对于这个问题,就要考虑两个⽅⾯
- ⽣产者发送消息时,要确认broker确实收到并持久化了这条消息,⽐如RabbitMQ的confirm机制, Kafka的ack机制都可以保证⽣产者能正确的将消息发送给broker
- broker要等待消费者真正确认消费到了消息时才删除掉消息,这⾥通常就是消费端ack机制,消费 者接收到⼀条消息后,如果确认没问题了,就可以给broker发送⼀个ack,broker接收到ack后才会删除消息
消息队列有哪些作⽤
- 解耦:使⽤消息队列来作为两个系统之间的通讯⽅式,两个系统不需要相互依赖了
- 异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
- 流量削峰:如果使⽤消息队列的⽅式来调⽤某个系统,那么消息将在队列中排队,由消费者⾃⼰控 制消费速度
死信队列是什么?延时队列是什么?
- 死信队列也是⼀个消息队列,它是⽤来存放那些没有成功消费的消息的,通常可以⽤来作为消息重 试
- 延时队列就是⽤来存放需要在指定时间被处理的元素的队列,通常可以⽤来处理⼀些具有过期性操 作的业务,⽐如⼗分钟内未⽀付则取消订单
如何保证消息的⾼效读写?
零拷⻉: kafka和RocketMQ都是通过零拷⻉技术来优化⽂件读写。传统⽂件复制⽅式: 需要对⽂件在内存中进⾏四次拷⻉。
零拷⻉: 有两种⽅式, mmap和transfile,Java当中对零拷⻉进⾏了封装, Mmap⽅式通过MappedByteBuffer对象进⾏操作,⽽transfile通过FileChannel来进⾏操作。Mmap 适合⽐较⼩的⽂件,通常⽂件⼤⼩不要超过1.5G ~2G 之间。Transfile没有⽂件⼤⼩限制。RocketMQ当中使⽤Mmap⽅式来对他的⽂件进⾏读写。
在kafka当中,他的index⽇志⽂件也是通过mmap的⽅式来读写的。在其他⽇志⽂件当中,并没有使⽤零拷⻉的⽅式。Kafka使⽤transfile⽅式将硬盘数据加载到⽹卡。
正在上传…重新上传取消epoll和poll的区别
- select模型,使⽤的是数组来存储Socket连接⽂件描述符,容量是固定的,需要通过轮询来判断是否发⽣了IO事件
- poll模型,使⽤的是链表来存储Socket连接⽂件描述符,容量是不固定的,同样需要通过轮询来判断是否发⽣了IO事件
- epoll模型,epoll和poll是完全不同的,epoll是⼀种事件通知模型,当发⽣了IO事件时,应⽤程序才进⾏IO操作,不需要像poll模型那样主动去轮询
TCP的三次握⼿和四次挥⼿
TCP协议是7层⽹络协议中的传输层协议,负责数据的可靠传输。在建⽴TCP连接时,需要通过三次握⼿来建⽴,过程是:
- 客户端向服务端发送⼀个SYN
- 服务端接收到SYN后,给客户端发送⼀个SYN_ACK
- 客户端接收到SYN_ACK后,再给服务端发送⼀个ACK
在断开TCP连接时,需要通过四次挥⼿来断开,过程是:
- 客户端向服务端发送FIN
- 服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
- 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
- 客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了
浏览器发出⼀个请求到收到响应经历了哪些步骤?
- 浏览器解析⽤户输⼊的URL,⽣成⼀个HTTP格式的请求
- 先根据URL域名从本地hosts⽂件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进
⾏域名解析,得到IP地址
- 浏览器通过操作系统将请求通过四层⽹络协议发送出去
- 途中可能会经过各种路由器、交换机,最终到达服务器
- 服务器收到请求后,根据请求所指定的端⼝,将请求传递给绑定了该端⼝的应⽤程序,⽐如8080被tomcat占⽤了
- tomcat接收到请求数据后,按照http协议的格式进⾏解析,解析得到所要访问的servlet
- 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller中的⽅法,并执⾏该⽅法得到结果
- Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过⽹络发送给浏览器所在的服务器
正在上传…重新上传取消浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染
跨域请求是什么?有什么问题?怎么解决?
跨域是指浏览器在发起⽹络请求时,会检查该请求所对应的协议、域名、端⼝和当前⽹⻚是否⼀致,如 果不⼀致则浏览器会进⾏限制,⽐如在www.baidu.com的某个⽹⻚中,如果使⽤ajax去访问 www.jd.com是不⾏的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏 览器要做这层限制,是为了⽤户信息安全。但是如果开发者想要绕过这层限制也是可以的:
- response添加header,⽐如resp.setHeader("Access-Control-Allow-Origin", "*");表示可以访问所有⽹站,不受是否同源的限制
- jsonp的⽅式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
- 后台⾃⼰控制,先访问同域名下的接⼝,然后在接⼝中再去使⽤HTTPClient等⼯具去调⽤⽬标接⼝
- ⽹关,和第三种⽅式类似,都是交给后台服务来进⾏跨域访问
零拷⻉是什么
零拷⻉指的是,应⽤程序在需要把内核中的⼀块区域数据转移到另外⼀块内核区域去时,不需要经过先 复制到⽤户空间,再转移到⽬标内核区域去了,⽽直接实现转移。
正在上传…重新上传取消