近期面了一些后端的内容,准备先对其中一些内容进行总结下,便于个人加深理解。行文可能仅支持个人能看懂理解就行。如有理解不到位的地方请谅解。终结面试后再来一一回顾整理。当然,友情提示下:面试前最好是提前做好项目亮点和难点、个人优点和缺点、为什么找工作的介绍准备,在面试中不断总结提炼,形成较好的表述。本文会持续更新到换工作结束,再分模块整理。(由于文字较多,可能存在没被发现的错别字,欢迎及时指出,感谢)
JVM内存区域是什么样的?各有什么内容?
JVM内存区域包含以下几部分:
程序计数器:线程私有。当前线程所执行字节码的行号指示器,便于线程切换后回到正确的执行位置,无OOM情况;
Java虚拟机栈:线程私有。方法执行时同步创建栈帧,存储局部变量表、操作数栈、动态连接和方法出口等信息。方法执行过程就是入栈到出栈的一个过程。(补充:局部变量表中有基本数据类型、对象引用、返回地址类型,数据类型以局部变量槽来存储,long/double两个槽位,其余的只有一个,编译时确定,运行时不会发生改变)线程请求栈深度大于虚拟机所允许深度,将抛出StackOverflowError,如虚拟机栈容量可动态扩展,则当栈扩展时无法申请足够内存时抛出OOM。
本地方法栈:线程私有。为虚拟机使用到的本地方法服务。也有可能StackOverflowError或OOM。
Java堆:线程共享。内存管理最大的一块。存放对象实例,垃圾收集器管理的内存区域。物理上可不连续,但在逻辑上是连续的。可根据-Xmx和-Xms设定固定大小或可扩展的。无空间完成对象实例分配且无法再扩展时,将抛出OOM。
方法区:线程共享。存储被虚拟机加载的类型信息、常量、静态变量、即时编译器后的代码缓存等数据。如果方法区无法满足新内存分配需求时,也会抛出OOM,一般垃圾回收器不对其进行处理。
new对象的原理/生命周期
a、检查类是否加载,没有则先加载类。(懒加载,会在堆区有class对象,方法区会有类的相关元数据信息)
b、分配内存。jvm根据大小分配内存空间;空闲列表(空间不规整,容易形成碎片空间)和指针碰撞方式(空间比较规整,默认使用)。并发问题,用CAS+重试机制或本地线程分配缓冲(每个线程预先分配一块堆内存)。
c、初始化。实例赋零值或null等操作。
d、设置对象头。hashcode、分代年龄、锁状态等信息。
e、执行初始化方法。对实例设置程序指定的初始值,并执行构造方法。
当对象不再被使用时,就需要进行垃圾回收,为其他对象腾出空间。
一般分配在eden区,不够用时会触发minor gc,存活对象被移动到survivor区,eden:survivor:survivor=8:1:1,如果survivor满了或年龄到了15后则被移动到老年代。
大对象和长期存活的对象都将进入老年代。如果老年代满了则会触发full gc,回收整个堆。
垃圾回收算法有哪些?新生代用的是哪种?哪种容易引起full GC?TLAB是什么?
标记清除:从gc root链上标记所有被引用的对象;遍历整个堆,把未标记的对象清除。(需暂停整个应用,并会产生内存碎片)。缺点:执行效率不稳定,会因为对象数量增长,效率变低;标记清除后会有大量的不连续的内存碎片,空间碎片太多就会导致无法分配较大对象,无法找到足够大的连续内存,而发生gc;
标记整理:算法分为”标记-整理-清除“阶段,首先需要先标记出存活的对象,然后把他们整理到一边,最后把存活边界外的内存空间都清除一遍。这个算法的好处就是不会产生内存碎片,但是由于整理阶段移动了对象,所以需要更新对象的引用。
复制:复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。缺点:可用内存缩短一半,浪费空间。
尽管看起来问题很大,但分代理论说大多数对象生命周期短,这种情况下标记复制就很适合了(复制的存活对象少)。至于内存消耗太大的问题,java虚拟机通过将新生代分为一个Eden区与2个Survivo区,其中一个Survivo区用来复制,这样一来极大得提高了内存空间利用率。
所以新生代用的是标记复制算法。
容易引起Full GC的是标记清除算法,因为空间碎片太多导致无法分配大对象。
线程本地局部缓存TLAB(Thread Local Allocation Buffer),JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB, 其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配, 在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
TLAB仅作用于新生代的Eden Space,所以通常多个小的对象比大的对象分配起来更加高效。
虽然总体来说堆是线程共享的,但是在堆的年轻代中的Eden区可以分配给专属于线程的局部缓存区TLAB,也可以用来存放对象。相当于线程私有的对象。所以这块内存分配是线程独占的,读取、使用和回收是线程共享的。
垃圾对象的判定方式?哪些可以做gc root对象
引用计数器:被引用一次+1,为0时表示无引用,可以被回收了。但相互引用但外部无引用的情况下不会被回收,容易造成内存泄漏。
可达性分析:从gc root开始扫描堆中的对象,被扫描到的都是存活对象。没有扫到的则需要被回收。gc root对象:虚拟机栈中的引用对象、方法区中的静态属性引用对象、方法区中的常量引用对象和本地方法栈中JNI引用对象、被锁持有的对象、虚拟机内部引用对象等。
引用有哪些?各有什么特点?
强引用(strong reference)
强引用就是我们最常见的普通对象引用(如new 一个对象),只要还有强引用指向一个对象,就表明此对象还“活着”。在强引用面前,即使JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),让程序异常终止,也不会靠回收强引用对象来解决内存不足的问题。(不符合垃圾收集)对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就意味着此对象可以被垃圾收集了。但要注意的是,并不是赋值为null后就立马被垃圾回收,具体的回收时机还是要看垃圾收集策略的。
软引用(soft reference)
软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。(垃圾收集可能会执行,但会作为最后的选择)
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
在使用软引用的时候必须检查引用是否为null。因为垃圾收集器可能在任意时刻回收软引用,如果不做是否null的判断,可能会出现NullPointerException的异常。
弱引用(weak reference)
弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。弱引用指向的对象是一种十分临近finalize状态的情况,当弱引用被清除的时候,就符合finalize的条件了。弱引用与软引用最大的区别就是弱引用比软引用的生命周期更短暂。垃圾回收器会扫描它所管辖的内存区域的过程中,只要发现弱引用的对象,不管内存空间是否有空闲,都会立刻回收它。(符合垃圾收集)具体的回收时机还是要看垃圾回收策略的,因此那些弱引用的对象并不是说只要达到弱引用状态就会立马被回收。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列。这一点和软引用一样。
虚引用(phantom reference)
虚引用并不会决定对象的生命周期。即如果一个对象仅持有虚引用,就相当于没有任何引用一样,在任何时候都可能被垃圾回收器回收。(符合垃圾收集)不能通过它访问对象,虚引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制(如做所谓的Post-Mortem清理机制),也有人利用虚引用监控对象的创建和销毁。
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态。所以对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以确保处于弱引用状态的对象没有改变为强引用。
但是有个问题,如果我们错误的保持了强引用(比如,赋值给了static变量),那么对象可能就没有机会变回类似弱引用的可达性状态了,就会产生内存泄露。所以,检查弱引用指向对象是否被垃圾收集,也是诊断是否有特定内存泄露的一个思路,我们的框架使用到弱引用又怀疑有内存泄露,就可以从这个角度检查。
什么是内存溢出,什么是内存泄漏
内存溢出是指程序在申请内存时,没有足够的空间供其使用。而内存泄漏是指在程序在申请空间后,无法释放已申请的空间。一次泄漏不会造成什么影响,但内存泄漏堆积会耗尽所有内存。
OOM如何排查?如何调优gc? (没经验的话可以说自己会按照什么方式去排查)
1、根据top命令查看各个进程的使用情况,通过ctrl+m找到消耗最高的几个进程;根据pid查看性能消耗较高的是什么服务在运行;
2、通过jstat虚拟机统计信息命令行工具查看进程的类加载、内存、垃圾收集、即时编译等运行数据。
如 -gc 进程号 间隔时间 输出条数来监视Java堆情况;-gcutil 进程号查看空间占比;
3、jmap内存映射工具来生成堆转储快照,即dump文件,jmap -heap 进程号。也可通过-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/log/risk-manager-magpie.hprof配置在OOM时自动生成。
调优:堆大小的设置;新生代老年代的比例设置;回收器设置;(仅做了解,没实践过)
用的是什么jdk版本?什么垃圾回收机制?G1和CMS的回收流程与算法是什么样的?有啥优缺点?(一定要提前了解线上用的是哪种,面试官会根据这个来引入)
jdk1.8,用的是G1收集器。
获取最短停顿时间为目标的收集器。基于标记-清除算法实现,老年代收集器。步骤为:
1、初始标记(CPU停顿,很短):标记gcroot直连对象,速度很快;
2、并发标记(收集垃圾和用户程序一起执行):进行GC root对象图遍历的过程;
3、重新标记(CPU停顿,比初始标记稍长,但比并发标记短):修正并发标记中因用户线程继续运作而导致标记变更的记录;
4、并发清除-清除算法(不需移动存活对象,与用户线程同时):清除已死亡对象。
优点:并发收集,低停顿,
缺点:对CPU资源敏感,总吞吐量下降;无法处理浮动垃圾;空间碎片对对象分配不利;
面向服务端应用的垃圾收集器,基于region的堆内存布局,化整为零,大小相等的region根据需要作为新生代或老年代,不同的region采用不同的策略处理,分代收集器。步骤:
1、初始标记(CPU停顿):标记直连对象;停顿很短,利用minorGC完成,实际上并没有额外停顿;
2、并发标记(与用户线程并发执行):可达性分析,找出要回收的对象;耗时长;
3、最终标记(CPU停顿):处理原始快照SATB中并发阶段结束后的遗留记录;
4、筛选回收:(可根据用户期望的GC停顿时间回收):对各个region的回收价值和成本排序,基于用户所期望停顿的时间来回收对应的内存。
优点:并行与并发,多CPU下可通过并发继续执行从而缩短停顿时间;分代收集:不需其他收集器配合就能独立管理整个GC堆;空间整合:整体基于标记-整理,局部复制来实现;可预测的停顿;
双亲委派模式是什么?为什么要有双亲委派?如何破坏双亲委派?
所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
Java中提供的这四种类型的加载器,是有各自的职责的:
那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。
1、通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
2、通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
因为双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。
因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。
如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
Spring如何实现自定义注解
Java注解是附加在代码中的一些元信息,用于一些工具在编译时、运行时进行解析和使用,起到说明、配置的功能。其本质是继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。反射获取注解时,返回的是java运行时生成的动态代理对象$Proxy1。
1、创建一个自定义注解和创建一个接口类似。但自定义注解需要使用@interface;
2、添加元注解信息;
3、注解方法不能带有参数;
4、注解方法返回值为基本类型、String、Enums、Annotation或其数组;
5、注解可以有默认值;
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface CarName {
String value() default "";
}
@Target:注解用于什么地方:ElementType.Constructor\Field\local_variable\method\package\parameter\type(类或接口或枚举声明);
@Document:注解是否会包含在javadoc中;
@Retention:什么时候用该注解。SOURCE(编译阶段就丢弃)/CLASS(类加载时丢弃)/RUNTIME(始终不会丢弃);
@Inherited:定义该注解与子类的关系。子类是否能使用。
Spring beanFactory和Factory Bean的区别?
BeanFactory:所有SpringBean的容器根接口,定义了Spring容器的规范,如getBean\isSingleton等方法;实现类诸如XmlBeanFactory、AbstructBeanFactory;
FactoryBean:Spring容器创建Bean的一种形式,可让用户通过实现该接口来自定义该Bean接口的实例化过程;让调用者无需关心具体实现细节。方法有getObject/getObjectType/isSingleton;常用类有ProxyFactoryBean(AOP代理Bean).
SpringBean的循环依赖如何处理的?有几级缓存?
循环依赖,即A依赖B,B又依赖A;或者ABC三者的依赖关系。
解决循环依赖:主要是针对单例Bean对象而言的。原型的会抛出异常提示。
1、创建原始Bean对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
假设BeanA先被创建。创建后的原始对象是BeanA1。上述代码中的bean即BeanA1。
2、暴露早期引用
addSingletonFactory(beanName, new ObjectFactory
通过暴露早期引用,BeanA指向的原始对象BeanA1创建好后,就会把原始对象的引用通过ObjectFactory暴露出去,在getObject的时候,其getEarlyBeanReference第三个参数就是原始对象暴露的bean。
3、解析依赖
populateBean(beanName, mbd, instanceWrapper);
解析依赖阶段,会先对BeanA对象进行属性填充,当检测到BeanA依赖于BeanB时,就会先去实例化B。而BeanB也会在此处解析自己的依赖。就可以直接调用BeanFactory.getBean("beanA")方法获取beanA;
4、获取早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 从缓存中获取早期引用
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 从 SingletonFactory 中获取早期引用
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}}}}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
在上一步中,getBean("beanA")会先调用getSingleton("beanA"),尝试从缓存中获取;由于beanA 还未实例化好,则返回的是null,接着getEarlySingletonObject也返回空,因为早期引用还没有放入缓存中。因此调用singletonFactory.getObject,由于已经有了早期引用,则实际上指向了BeanA1。beanB获取了这个原始对象的引用,就可以顺利完成实例化,这样beanA也就能顺利完成实例化了。由于beanB.beanA和beanA指向的是同一个对象beanA1,所以beanB中的beanA也处于可用状态了。
Spring有三级缓存。处于最上层的缓存是singletonObjects,它其中存储的对象是完全创建好,可以正常使用的bean,二级缓存叫做earlySingletonObjects,它其中存储的bean是仅执行了第一步通过构造方法实例化,并没有填充属性和初始化,第三级缓存singletonFactories存储的是对应bean的一个工场。
/** 一级缓存,保存singletonBean实例: bean name --> bean instance */
private final Map singletonObjects = new ConcurrentHashMap(256);
/** 二级缓存,保存早期未完全创建的Singleton实例: bean name --> bean instance */
private final Map earlySingletonObjects = new HashMap(16);
/** 三级缓存,保存singletonBean生产工厂: bean name --> ObjectFactory */
private final Map> singletonFactories = new HashMap>(16);
Spring尝试获取单例bean时,首先会在三级缓存中查找。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 查询一级缓存
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//若一级缓存内不存在,查询二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
//若二级缓存内不存在,查询三级缓存
ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//若三级缓存中的,则通过工厂获得对象,并清除三级缓存,提升至二级缓存
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
当Spring容器试图获得单例bean时,首先会在三层缓存中查找。查找位置从一级缓存至三级缓存,注意若三级缓存查找成功,其返回的bean对象并不一定是完全体,而可能是仅完成实例化,还未完成属性装填的提前暴露引用。当三级缓存内都未找到目标,getSingleton方法则会返回null,之后Spring将会执行一系列逻辑,最终将调用以下方法新创建bean对象:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
//此处略过 做某些事
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// 早期缓存单例对象以解决循环引用问题
// 即使问题是在如BeanFactoryAware的生命周期阶段接口处发生的
// 允许早期暴露参数
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 将实例化完成但还未填装属性的bean引用暴露出来,方法为将beanName和对应singletonFactory加入第三级缓存Map
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
// 此处省略部分代码
//填装属性,在此方法内尝试获得循环引用的被引用bean,方法与自身bean获得流程一致
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 此处省略部分代码
使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于IOC的考虑,而是出于AOP的考虑,即若使用二级缓存,在AOP情形下,注入到其他bean的,不是最终的代理对象,而是原始对象。
AOP的原理、使用场景、是否可以更改入出参?如何实现一个AOP?cglib和jdk代理的区别?(如果有时间,可以自己手动实践下)
AOP,面向切面编程,可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性。SpringAop基于动态代理实现。
①JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
可以通过ProceedingJoinPoint.getArgs()获取方法调用参数,对其进行修改,然后通过ProceedingJoinPoint.proceed(Object[] args)来传入修改过的参数继续调用。
AOP的通知类型
前置通知(Before):在目标方法被调用之前调用通知功能
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
返回通知(After-returning):在目标方法成功执行之后调用通知
异常通知(After-throwing):在目标方法抛出异常后调用通知
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为。
后置通知和返回通知的区别是,后置通知是不管方法是否有异常,都会执行该通知;而返回通知是方法正常结束时才会执行。
IOC是什么?控制的是什么?反转的是什么?
IoC
(Inversion of Control
,控制倒转),spring
的核心,就是由spring
来负责控制对象的生命周期和对象间的关系。
传统开发如果在一个对象中使用另外的对象,就必须得到它(自己new
一个,或者从JNDI
中查询一个),使用完后还需销毁(比如Connection
等),对象始终会和其他的接口或类藕合起来。Spring
所倡导的开发方式是所有的类都会在spring
容器中登记,告诉spring
你是什么,你需要什么,然后spring
会在系统运行到适当的时候,把你要的主动给你,同时也把你交给其他需要你的。所有的类的创建、销毁都由spring
来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring
。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring
控制,所以这叫控制反转。
IoC
的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI
(Dependency Injection
,依赖注入)来实现的。比如对象A
需要操作数据库,以前我们总是要在A
中自己编写代码来获得一个Connection
对象,有了 spring
我们就只需要告诉spring
,A
中需要一个Connection
,至于这个Connection
怎么构造,何时构造,A
不需要知道。在系统运行时,spring
会在适当的时候制造一个Connection
,然后像打针一样,注射到A
当中,这样就完成了对各个对象之间关系的控制。A
需要依赖 Connection
才能正常运行,而这个Connection
是由spring
注入到A
中的,依赖注入的名字就这么来的。
谁控制谁,控制什么:传统是在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
==和equals的区别是什么? Integer 1 == Integer 1比较的是什么?
对于基础数据类型的话,==比较的是值;对于引用数据类型,==比较的是引用的地址;
equals如果是Object默认的,则是比较地址值;否则是根据重写的方法来比较相应的内容。
字符串的equals比较就是比较的内容。
因为在-127-128的范围内,Integer会自动拆箱,所以比较的是值。
订单ID如何生成以保证唯一? (这个和电商业务的项目关联比较大)
可以采用自增ID,也可以采用雪花算法。
雪花算法是Twitter开源的分布式ID生成算法,以64bit的Long型作为全局唯一ID,引入了时间戳,基本上保持自增。
第一部分:1位,为0,无意义,保持自增ID是正数;
第二部分:41位,时间戳;多达2^41-1个毫秒值,大概69年;
第三部分:5位,机房ID,2^5个机房(32);
第四部分:5位,机器ID,每个机房有2^5个机器,所以第三第四部分代表1024个机器;
第五部分:12位,序号,某机房某台机器这一毫秒内生成的ID序号,共2^12-1个(4096),如是第12个请求,就以12作为最后几位的数字。
优点:高性能高可用,生成时不依赖数据库,完全内存中生成;容量大,每秒可生成百万的自增ID,ID自增存入数据库,索引效率高;
缺点:依赖与系统时间的一致性,如系统被回调时间,则可能造成ID重复。
jsf架构用的是什么?rpc的哪种?rpc接口和http接口的区别?(了解即可)
rpc框架。常用的rpc框架有thrift、dubbo、SpringCloud等。我们用的应该是dubbo.
RPC接口与HTTP对比
1、传输协议。RPC:可以基于TCP协议,也可以基于HTTP协议;HTTP:基于HTTP协议
2、传输效率。RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率;HTTP:如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装下是可以作为一个RPC来使用的。
3、性能消耗。RPC:可以基于thrift实现高效的二进制传输;HTTP:大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能。
4、负载均衡。RPC:基本都自带了负载均衡策略;HTTP:需要配置Nginx,HAProxy来实现。
5、服务治理。RPC:能做到自动通知,不影响上游;HTTP:需要事先通知,修改Nginx/HAProxy配置。
网络有了解么?有哪些层?各是干啥的?IP在哪一层?TCP/UDP在哪一层? (除此之外还有三次握手四次挥手,知道过程,知道为什么不能两次为什么不能四次等)
第一层:应用层。定义了用于在网络中进行通信和传输数据的接口; (五层是表示层和会话层合并到了应用层)HTTP
第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等;
第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断;
第四层:传输层。管理着网络中的端到端的数据传输; TCP/UDP
第五层:网络层。定义网络设备间如何传输数据; IP
第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输;
第七层:物理层。这一层主要就是传输这些二进制数据。
不可变类的定义、使用
可变类:当你获得这个类的一个实例引用时,你可以改变这个实例的内容。
不可变类:当你获得这个类的一个实例引用时,你不可以改变这个实例的内容。不可变类的实例一但创建,其内在成员变量的值就不能被修改。
举个例子:String和StringBuilder,String是immutable的,每次对于String对象的修改都将产生一个新的String对象,而原来的对象保持不变,而StringBuilder是mutable,因为每次对于它的对象的修改都作用于该对象本身,并没有产生新的对象。
1、所有成员都是private final
2、不提供对成员的改变方法,例如:setXXXX
3、确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
4、如果某一个类成员不是原始变量(primitive)或者不可变类,必须通过在成员初始化(in)或者get方法(out)时通过深度clone方法,来确保类的不可变。
Java的string类为什么是不可变的: 效率和安全。
字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
相对于可变对象,不可变对象有很多优势:
1).不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“ABC”的其他变量也不会造成影响。
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。
2).不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
3)安全性:如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
浅拷贝和深拷贝
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
例如:
对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。
深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。
若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝。
分布式的CAP理论和BASE理论
网络分区:俗称“脑裂”。当网络发生异常情况,导致分布式系统中部分节点之间的网络延时不断变大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够进行正常通信,而另一些节点则不能。当网络分区出现时,分布式系统会出现局部小集群。
CAP理论指的是在一个分布式系统中,不可能同时满足Consistency(一致性)、Availablity(可用性)、Partition tolerance(分区容错性)这三个基本需求,最多只能满足其中的两项。
一致性(C):数据在多个副本之间是否能够保持一致的特性。当执行数据更新操作后,仍然可保证数据处于一致的状态。
可用性(A):系统提供的服务必须一直处于可用的状态。对于用户的每一个操作情况总是能够在有限的时间内返回结果。这个有限时间是系统设计之初就指定好的系统运行指标。返回的结果指的是系统返回用户的一个正常响应结果,而不是系统错误信息。
分区容错性(P):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
组成分布式系统的每个节点的加入与退出都可以看成是一个特殊的网络分区。
注:一个分布式系统无法同时满足这三个条件,只能满足两个,意味着我们要抛弃其中一项。
(1)CA,放弃P:将所有数据都放在一个分布式节点上,这同时放弃了系统的可扩展性。
(2)CP,放弃A:一旦系统遇到故障,受影响的服务器需要等待一段时间,在恢复期间无法对外提供正常的服务。
(3)AP,放弃C:这里的放弃一致性是指放弃数据强一致性,而保留数据的最终一致性。系统无法实时保持数据的一致性,但承诺在一个限定的时间内,数据最终能够达到一致的状态。
对于分布式系统而言,分区容错性是一个最基本的要求,因为分布式系统中的组件必然需要部署到不同的节点 ,必然会出现子网络,在分布式系统中,网络问题是必定会出现的异常。
因此分布式系统只能在一致性(C)和可用性(A)之间进行权衡。
BASE理论是指,Basically Available(基本可用),Soft-state(软状态),Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。
核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。
基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。
软状态:软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。
最终一致性:系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。
BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。
ACID是传统数据库常用的概念设计,追求强一致性模型。ACID,指数据库事务正确执行的四个基本要素的:原子性(Atomicty)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
哈希冲突的解决方式
Hash算法解决冲突的四种方法 - Lyf凤 - 博客园
线程的实现方式?为什么有了tread类还需runnable?
创建线程的方式的四种方式
多线程时如何保证一个变量的可见性?
synchronized关键字
synchronized语义规范:
1.进入同步块前,先清空工作内存中的共享变量,从主内存中重新加载;
2.解锁前必须把修改的共享变量同步回主内存。
synchronized如何做到线程安全的?
1.锁机制保护共享资源,只有获得锁的线程才可操作共享资源;
2.synchronized语义规范保证了修改共享资源后,会同步回主存,就做到了线程安全。
volatile关键字(比synchronized更轻量级,性能更好)
volatile语义规范:
1.使用volatile变量时,必须重新从主内存加载,并且read 、load是连续的。
2.修改volatile变量后,必须立马同步回主内存,并且store、 write是连续的。
volatile能做到线程安全吗?
1.不能,因为它没有锁机制,线程可并发操作共享资源。除非对变量的写操作不依赖于当前值。
final关键字:修饰类、方法、变量,类不能被继承、方法不能重写、变量只能赋值一次
synchronized如何解决并发问题的?
类锁:所有对象共用一个锁;对象锁:一个对象一把锁,多个对象多把锁。
synchronized修饰实例方法,对当前实例对象加锁;修饰静态方法,对当前类加锁;修饰代码块,对synchronized括号内的对象/类加锁。
方法块的同步是隐式的,JVM通过方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标识区分一个方法是否是同步方法,如果是,执行线程先持有monitor,执行完再释放monitor。
代码块同步是利用monitorenter和monitorexit这两个字节码指令来时间的。当前线程试图获取monitor对象的所有权,未加锁或已持有,则锁计数器+1,否则-1,为0时释放,获取失败则进入阻塞,等待其他线程释放锁。
synchronized修饰静态变量会怎样?
无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
所以加在静态变量上就是获取类锁。
锁升级是什么?有锁降级么?为什么是重量级锁?是否存在内核态切换?
线程的阻塞和唤醒需要CPU从用户态转为内核态。为了减少获得锁和释放锁带来的性能消耗,JDK1.6引入了偏向锁和轻量级锁,一共有4种状态,从低到高依次为:无锁状态、偏向锁状态、轻量级锁、重量级锁状态,会随着竞争情况逐渐升级。锁升级但不能降级,是为了提高锁获得和释放的效率。
偏向锁:无锁竞争情况下为减少锁竞争的资源开销,引入偏向锁。线程获得了锁,则进入偏向模式,会在对象头中记录对应的线程ID,再次请求锁时,则无需任何同步操作,只需检查对象头的线程ID是否匹配,省去大量有关锁申请的操作。偏向锁不会主动释放锁。如果不匹配,则需看对象头的线程是否存活,若不存活,则锁对象变成无锁状态,其他线程可以竞争变为偏向锁;如存活,则需看线程1当前栈帧,如果还需要继续持有该锁对象,则暂停线程1,撤销偏向锁,升级为轻量级锁;否则变成无锁状态重新偏向。
轻量级锁:偏向锁升级而来。会先把锁对象的对象头复制一份到当前线程的栈帧中的锁空间内,然后CAS把对象头重的内容替换为当前线程存储的锁记录的地址;如果此时另一个线程的CAS失败,则尝试自旋等待1释放锁(CPU消耗)。若自旋10次货100次还未等到,或又有线程来竞争,则轻量级锁就会膨胀成重量级锁。
重量级锁:除了拥有锁的线程外都将阻塞,防止CPU空转。
为啥重量级?JDK1.6以前,重量级锁是需要依靠操作系统来实现互斥锁的,这导致大量上下文切换,消耗大量CPU,影响性能。阻塞和唤醒操作又涉及到了上下文操作,大量消耗CPU,降低性能。1.6引入锁升级。
ReentrantLock什么场景下使用?和synchronized有什么区别?
就是因为synchronized性能低,有人就开发了ReentrantLock可重入锁,大大提高了性能。
1、底层实现:synchronized是JVM层面的锁,Java关键字,同步块或同步方法中调用,涉及锁升级;ReenTrantLock是JDK1.5以后的JUC提供的API层面的锁,利用ASQ实现的,使用改进的CLH队列,实现CAS自旋+阻塞+唤醒。
2、synchronized不需主动释放,而ReenTrantLock需要手动释放,否则会出现死锁;lock/unlock配合try/finally来完成,使用释放更灵活;
3、synchronized不可中断,ReenTrantLock可中断;
4、synchronized为非公平锁。ReenTrantLock默认非公平,也可以通过构造函数设置为公平锁;trylock可设置尝试等待;
5、synchronized不可绑定条件Condition,而ReenTrantLock可以,结合await/singal实现线程的精确唤醒。
6、synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
场景:1、若发现某操作已经在执行,则trylock(5,SECOND)尝试等待,等待超时则不执行;2、公平锁,等待着一个个执行;3、发现某操作在执行中,则不再执行trylock();4、lockInterruptibly中断正在执行的操作立刻释放锁进入下一个操作。
线程池参数有哪些?分别代表什么含义?如何进行调优?线程复用是怎么实现的?队列中无线程时的核心线程处于什么状态? (最好自己实践下,怎么写的,有哪些分类,参数等处理)
参数及其含义
1、corePoolSize:核心线程数。核心线程会一直存活,即使没有任务需要执行。当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理;设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
2、workQueue:存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。
3、maximumPoolSize:最大线程数。当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务;当线程数=maximumPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间。当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。如果allowCoreThreadTimeout=true,则会直到线程数量=0;
5、allowCoreThreadTimeout:允许核心线程超时;
6、rejectedExecutionHandler:任务拒绝处理器;两种情况会拒绝处理任务:当线程数已经达到maximumPoolSize,切队列已满,会拒绝新任务;当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
实现RejectedExecutionHandler接口,可自定义处理器
新提交一个任务时的处理流程
1、如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;
2、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
3、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时;
3.1、当前poolSize
3.2、当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要线程池的饱和策略RejectedExecutionHandler来拒绝新增加的任务。
优化时主要针对corePoolSize、maximumPoolSize、workQueue这三个参数。
corePoolSize:CPU密集型CPU数+1(偶尔的内存页失效等额外的线程也能确保CPU时钟周期不会被浪费),IO密集型CPU*2(一个等待IO时其他的还可以继续执行);最大线程数=CPU*25;
实现线程复用:核心线程会一直处于阻塞状态,等待任务来时被唤醒使用。
为什么不建议使用Executors创建线程池的处理?
不允许使用Executors去创建,而是通过ThreadPoolExecutor方式,一是明确线程池运行原理,二是规避资源耗尽风险,Executors的弊端在于其等待队列长度最大是Integer.Max_Value,可能会堆积大量的线程/请求而导致OOM。
保证线程的顺序执行
Java中的Runnable、Callable、Future、FutureTask的区别与示例
Java中的Runnable、Callable、Future、FutureTask的区别与示例_Mr.Simple的专栏-CSDN博客
慢查询/SQL优化是怎么做的?联合索引(a,b,c)中为何使用c不走索引?为什么不使用多个单列索引?
慢SQL优化:1、检查索引是否生效,是否需要重建索引;2、是否数据量太大,考虑分批次读区;3、是否返回了不必要的字段,尽量不要用select *;4、查询语句是否合理;
最左前缀匹配原则,mysql会一直从左向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整,由优化器进行优化;
联合索引(a,b,c)实际上是(a)/(a,b)/(a,b,c)。所以c是无法走索引的。
对于多个单列索引来说,优化器会计算成本,选择成本最小的索引,而不是走所有的索引。
事务有哪些隔离级别?innodb的默认隔离级别是什么?分别对应的哪些场景?
脏读:脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
可重复读可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
不可重复读:对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
幻读:幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交(默认) | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
SQL如何保证并发的?讲一讲MVCC。MVCC能否读到另一个事务提交的数据?两个事务更新时如何进行版本控制的?
读未提交,它是性能最好,也最野蛮,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
再来说串行化。读的时候加共享锁,其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
实现可重复读,采用MVCC多版本并发控制。可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:当前事务内的更新,可以读到;版本未提交,不能读到;版本已提交,但是却在快照创建后提交的,不能读到;版本已提交,且是在快照创建前提交的,可以读到;
并发写问题:存在这的情况,两个事务,对同一条数据做修改。最后结果应该是时间靠后的那个。并且更新之前要先读数据,这里所说的读和上面说到的读不一样,更新之前的读叫做“当前读”,总是当前版本的数据,也就是多版本中最新一次提交的那版。假设事务A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常。
解决幻读:解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。
假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。
在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行 在不加事务的情况下,更新数据会加锁么? 更新时得看是否走索引。如果走索引的话就锁行。如果不走索引,MySQL 无法直接定位到这行数据。MySQL 会为这张表中所有行加行锁,没错,是所有行。但在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。 Innodb自增ID的原理?是否会加锁?重启后ID如何处理的? AutoIncrement最新值的获取涉及到锁,有三种锁模式,对应 innodb_autoinc_lock_mode 的值, 0 ,1,2. MySQL 8.0 之后,默认为 2, 在这之前,默认为 1。innodb_autoinc_lock_mode=0(traditional lock mode):传统的auto_increment机制,这种模式下所有针对auto_increment列的插入操作都会加表级别的AUTO-INC锁,在语句执行结束则会释放,分配的值也是一个个分配,是连续的,正常情况下也不会有间隙(当然如果事务rollback了这个auto_increment值就会浪费掉,从而造成间隙)。 innodb_autoinc_lock_mode=1(consecutive lock mode):这种情况下,针对未知数量批量插入(例如INSERT ... SELECT, REPLACE ... SELECT和LOAD DATA)才会采用AUTO-INC锁这种方式,而针对已知数量的普通插入,则采用了一种新的轻量级的互斥锁来分配auto_increment列的值。这种锁,只会持续到获取一定数量的 id,不会等待语句执行结束在释放。也就是拿轻量级锁提前分配好所需数量的 id 之后释放锁,再执行语句。当然,如果其他事务已经持有了AUTO-INC锁,则simple inserts需要等待。当然,这种情况下,可能产生的间隙更多。 innodb_autoinc_lock_mode=2(interleaved lock mode):这种模式下任何类型的inserts都不会采用AUTO-INC锁,性能最好,但是在同一条语句内部产生auto_increment值间隙。其实这个就是所有语句对于同一个值进行 Compare-And-Set 更新,类似于乐观锁。这个锁模式对statement-based replication的主从同步都有一定问题。因为同步传输的是语句,而不是行值,语句执行后的差异导致主从可能主键不一致。 一般情况下,一张表最大能放多少数据量级? InnoDB存储引擎也有自己的最小储存单元--页(Page),一个页的大小是16K。页可以用于存放数据也可以用于存放键值+指针,在B+树中叶子节点存放数据,非叶子节点存放键值+指针。 假设一行数据的大小是1k,那么一个页可以存放16行这样的数据。 假设键值是Long型的自增ID,则为8字节,指针默认是6字节,这样非叶子节点的一个键值+指针为14字节,一页可存放16K/14=1170。 假设B+数高度是N,则叶子节点数= 1170^(n-1);所以数据行数=叶子节点数*16=16*1170^(N-1); 一般层高都是3-5层,按照3层算,数据行数=16*1170*1170=21902400,2千万量级。 性别适合索引么? 访问索引需要付出额外的IO开销,性别不适合做聚簇索引。但普通索引的话相当于拿到的是对应的ID,还需回表找数据。假如要从表的100万行数据中取几个数据,那么利用索引迅速定位,访问索引的这IO开销就非常值了。但如果是从100万行数据中取50万行数据,就比如性别字段,那相对需要访问50万次索引,再访问50万次表,加起来的开销并不会比直接对表进行一次完整扫描小。 为什么不建议使用select *? 1, 业务方面 a.假设某一天修改了表结构,如果用select *,返回的数据必然会会变化,客户端是否对数据库变化作适配,是否所有地方都做了适配,这都是问题。 b. 可能会存在不需要的列,传输过程中有不必要的性能损耗; c. 客户端解析查询结果也需要更多损耗 2,数据库原理方面原因(此处以MySql为例): a. 使用了select ,必然导致数据库需要先解析代表哪写字段,从数据字段中将*转化为具体的字段含义,存在性能开销; b. 不可能对所有字段建索引,在索引优化必然会有局限性,导致查询时性能差; 数据库的分库分表 仅作了解 数据库分库分表思路 - butterfly100 - 博客园 mybatis如何防止SQL注入的 mybatis中的#和$的区别: 1、#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。 mybatis框架作为一款半自动化的持久层框架,其SQL语句都要我们自己手动编写,这个时候当然需要防止SQL注入。MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译,#会被替换为?;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。 【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。 es的优缺点 首先ES是基于Lucene这个非常成熟的索引方案,另加上一些分布式的实现:集群,sharding,replication等。 1、横向可扩展性:只需要增加一台服务器,做一点儿配置,启动一下ES进程就可以并入集群; 2、分片机制提供更好的分布性:同一个索引分成多个分片(sharding),这点类似于HDFS的块机制;分而治之的方式来提升处理效率,相信大家都不会陌生; 3、高可用:提供复制(replica)机制,一个分片可以设置多个复制,使得某台服务器宕机的情况下,集群仍旧可以照常运行,并会把由于服务器宕机丢失的复制恢复到其它可用节点上;这点也类似于HDFS的复制机制(HDFS中默认是3份复制); 1、各节点的一致性问题:其默认的机制是通过多播机制,同步元数据信息,但是在比较繁忙的集群中,可能会由于网络的阻塞,或者节点处理能力达到饱和导致各节点元数据不一致——也就是所谓的脑裂问题,这样会使集群处于不一致状态。目前并没有一个彻底的解决方案来解决这个问题,但是可以通过将工作节点与元数据节点分开的部署方案来缓解这种情况。 2、没有细致的权限管理机制,也就是说,没有像MySQL那样的分各种用户,每个用户又有不同的权限。所以在操作上的限制需要自己开发一个系统来完成; es如何做到高并发读写与同步的?如何保证数据的一致性? es分为一个主节点和多个数据节点/候选主节点、协调节点,每个节点中存储数据,参与索引与搜索功能。每个节点包含主分片和副本,且同一主分片与其副本不在同一节点上。在进行数据写入时,每个节点都可以写,但根据路由规则计算后只写入相应的主分片,并并发同步到副本当中,同步完成后才通知成功。为了保证写入速度,采用延迟写策略,先写内存,每隔1s写入文件缓存,这时就可以被读取到了,再等5s写磁盘。读取时是随机读取。同时es集群可扩展上百台的服务器,所以能够做到高并发的读写与同步。 为了保证数据的一致性,首先数据同步时要副本都成功后才返回成功;其次,添加事务日志translog记录还未写入到磁盘的数据,translog先写入os cache的,默认每隔5秒刷一次到磁盘中去,因为日志每隔5秒从文件缓存系统flush一次到磁盘,所以最多会丢5秒的数据。 es的使用场景有哪些?有哪些关键字,作用是什么? 检索、搜索、统计数据。 1、term查询用来查询某个关键字在文档里是否存在,所以Term需要是文档切分的一个关键字; 2、terms查询用来查询某几个个关键字在文档里是否存在,Terms可以同时对一个字段检索多个关键字; 3、match查询和queryString有点类似,就是先对查询内容做分词,然后再去进行匹配; 4、match_all的查询方式简单粗暴,就是匹配所有,不需要传递任何参数; 5、match_phrase属于短语匹配,能保证分词间的邻近关系,相当于对文档的关键词进行重组以匹配查询内容,对于匹配了短语"森 小 林"的文档,下面的条件必须为true:森 、小、 林必须全部出现在某个字段中;小的位置必须比森的位置大1;林的位置必须比森的位置大2; 7、range查询,顾明思意就是范围查询,例如我们这里要查询年龄在19到28的人的数据;gt: > 大于(greater than)lt: < 小于(less than)gte: >= 大于或等于(greater than or equal to);lte: <= 小于或等于(less than or equal to); 8、exists允许你过滤文档,只查找那些在特定字段有值的文档,无论其值是多少,为了验证,需要注意,这里的有值即使是空值也算有值,只要不是null; 9、wildcard,通配符查询,其中【?】代表任意一个字符【*】代表任意的一个或多个字符,例如我们想查名字结尾为林的文档: 10、prefix,前缀查询,我们为了找到所有姓名以森开头的文档,可以使用这种方式: 11、regexp,正则匹配,ES兼容了正则的查询方式 12、fuzzy,纠错检索,让输入条件有容错性,例如我要检索性别为woman的数据,但是我拼错了,输入的是wman,用fuzzy照样可以检索到; 13、filter:只过滤符合条件的文档,与must唯一的区别是:不计算相关系得分,但因为有缓存,所以性能高; 14、must:用must连接的多个条件必须都满足,是and的关系,逻辑&与的关系; 15、should:用should连接的多个条件只要满足一个即可,是or的关系,逻辑||或的关系; 16、must_not:用must_not绑定的条件表示一定不能满足该条件,是not的关系,逻辑^非的关系。用这些条件的连接词将多个查询条件连接起来就能进行复杂的复合查询了。Boolean在同时有must和should的时候,should就被过滤掉了,因为should表示有也可以没有也可以,所以我们常把must放到should字句里,确保should的子句能执行; es的分片和副本数设置的是多少?16:1?有几个主节点?分片副本的作用? 16:1,1个主节点。水平拓展,容灾。 Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上,允许水平分割/扩展内容容量,在分片之上进行分布式的、并行的操作来提高性能/吞吐量,提供了高可扩展及高并发能力。 分片故障时故障转移机制非常必要。Elasticsearch允许创建分片的一份或多份拷贝,这些拷贝叫做复制分片。在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的,同时复制分片还能提提高并发量。所以复制分片的作用是高可用\高并发,副本越多消耗越大,也越保险,集群的可用性就越高。 准实时的原因是什么? 为了保证写入速度,采用延迟写策略,先写内存,此时不可被读取,每隔1s写入文件缓存形成新的段文件,这时就可以被读取到了,所以会有1s的时间差,因此被称为准实时。 es的脑裂现象是什么?如何解决的? 脑裂现象是指在选举过程中出现多个master竞争时,主分片和副本的识别也发生了分歧,对一些分歧中的分片标识为了坏片,更新的时候造成数据混乱或其它非预期结果。其实按照选举规则,能选举出一个确定的master是一定的,就算clusterStateVersion一样,也不可能有两个节点id一致,总会有大有小,按照此规则,所有节点其实是能达成共识的。 “脑裂”问题可能有以下几个原因造成: 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片; 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 内存回收:主节点的角色既为master又为data,当data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。 为了避免脑裂现象的发生,我们可以从根源着手通过以下几个方面来做出优化措施: 适当调大响应时间,减少误判:通过参数discovery.zen.ping_timeout设置节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。 角色分离:即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。 选举触发:在候选集群中的节点的配置文件中设置参数discovery.zen.munimum_master_nodes的值,这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是1,官方建议取值(master_eligibel_nodes/2) + 1,这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。 什么是倒排索引? 传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。倒排索引结构是根据内容(词语)找文档。词典就是term的集合,每个term【域和关键词的组合】会索引一连串满足条件的文档id,检索时通过term检索可以找到这串id,进而找到满足条件的文档集合 redis的优缺点是什么? 优点 1、高性能的key-value内存数据库 – 读速度是 110000 次/s,写速度是 81000 次/s 。 2、丰富的数据类型 – String, List, Hash, Set 及zset 数据类型操作。 3、原子 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC指令包起来。 4、丰富的特性 – 可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除。 5、Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。 缺点 1、数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 2、主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。 3、可能会存在缓存击穿、缓存雪崩、缓存穿透等问题; redis为什么这么快? 1、redis是基于内存的,内存的读写速度非常快, 数据存放在内存中,内存的响应时间大约是 100纳秒 ,这是Redis每秒万亿级别访问的重要基础。 2、redis是单线程的,省去了很多上下文切换线程的时间,避免了线程切换和竞态产生的消耗; 3、redis使用多路复用技术,可以处理并发的连接,多个socket连接复用redis的单个存取线程。IO多路复用内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性实现了对多个事件的监控,但在处理时通过队列一个个单线程处理,绝不在io上浪费一点时间。 4、数据结构简单,操作节省时间。 高可用有哪些方式?当存储不了数据时怎么处理? (熟记淘汰策略) redis 实现高并发主要依靠主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。 如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。 Redis 提供 6 种数据淘汰策略: volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据 分布式锁是怎么用的?是否存在缺点/问题?如果使用setNx命令有什么问题? redis的set的带过期时间的命令来使用的,在删除时需要先判断是否是自己来操作的。 缺点 1、客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题;(可在一定时间内延长过期时间) 2、redis服务器时钟漂移问题:如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。 3、单点实例安全问题:如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁。该问题可以通过redLock算法解决。 使用setNx命令加锁则需手动加过期时间,并发时会出现未写入过期时间而导致无法释放锁。 还知道其他哪些分布式锁?如何选型? 基于数据库实现分布式锁 在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行后删除对应的行数据释放锁。 缺点:不具备可重入性;不具备阻塞锁特性,需手动循环;没有锁失效机制; 基于缓存(Redis等)实现分布式锁 优点:Redis有很高的性能;Redis命令对此支持较好,实现起来比较方便。 setNx/expire/delete key;也可以用set命令。2.6.9的版本就支持了。 Rdis只保证最终一致性,副本间的数据复制是异步进行(Set是写,Get是读,Reids集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有部分数据没有复制过去可能会丢失锁情况,故强一致性要求的业务不推荐使用Reids,推荐使用zk。 基于Zookeeper实现分布式锁 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: 1、创建一个目录mylock; 2、线程A想获取锁就在mylock目录下创建临时顺序节点; 3、获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 4、线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 5、线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。 使用ZooKeeper集群,锁原理是使用ZooKeeper的临时节点,临时节点的生命周期在Client与集群的Session结束时结束。因此如果某个Client节点存在网络问题,与ZooKeeper集群断开连接,Session超时同样会导致锁被错误的释放(导致被其他线程错误地持有),因此ZooKeeper也无法保证完全一致。 ZK具有较好的稳定性;响应时间抖动很小,没有出现异常。但是随着并发量和业务数量的提升其响应时间和qps会明显下降。 redis如何做到高并发的?是否会出现脑裂现象? 单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。 脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。此时虽然某个 slave 被切换成了master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。 解决方式:两个参数(根据需要配置):min-slaves-to-write 1/min-slaves-max-lag 10 要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。 存储方式在redis和db中如何选型?如何保证redis和数据库的一致性? MySQL用于持久化的存储数据到硬盘,功能强大,但是速度较慢。Redis用于存储使用较为频繁的数据到缓存中,读取速度快。Redis适合放一些频繁使用,比较热的数据,因为是放在内存中,读写速度都非常快,一般会应用在下面一些场景:排行榜、计数器、消息队列推送、好友关注、粉丝。 1、采用延迟双删策略保证redis与数据库的一致性:1、先淘汰缓存;2、再写数据库;3、休眠1秒,再次淘汰缓存。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 2、删除缓存重试机制:1、写请求更新数据库;2、删除缓存,但可能删除失败;3、删除失败的key放入消息队列,消费消息获取要删除的key;4、重试删除缓存的操作; 3、同步binlog异步删除缓存:以mysql为例 可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性; 以上三种,1、2即可,比较好说明。 哨兵模式大概讲一讲。 哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。 1、故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。 2、即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。 Redis 的持久化机制是什么?各自的优缺点? Redis提供两种持久化机制 RDB 和 AOF 机制: RDB(Redis DataBase):是指用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。 优点:1、只有一个文件 dump.rdb,方便持久化。2、容灾性好,一个文件可以保存到安全的磁盘。3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis的高性能;4、相对于数据集大时,比 AOF 的启动效率更高。 缺点:数据安全性低。RDB 是间隔一段时间(每隔五分钟)进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。 AOF(Append-only file):是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储保存为 aof 文件。数据日志,记录操作命令。 优点:1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。everysec每秒同步一次;2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题。3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall) 缺点:AOF 文件比 RDB 文件大,且恢复速度慢。数据集大的时候,比 rdb 启动效率低。 如何延长过期时间? 1、用一个 2、在加锁成功之后将 3、每次续期任务完成并且成功之后,就再次启动延迟任务。 4、释放前,先将任务从map中移除; 接口对接处理:超时处理,重试处理,数据不一致处理 保证接口的幂等性:同一个操作执行多次,产生的效果一样。 2、状态机幂等:如订单状态有待支付,支付中,支付成功,支付失败。设计时最好只支持状态的单向改变。这样在更新的时候就可以加上条件,多次调用也只会执行一次。例如想把订单状态更新为支持成功,则之前的状态必须为支付中; 3、乐观锁实现幂等:查询数据获得版本号,通过版本号去更新,版本号匹配则更新,版本号不匹配则不更新; 4、防重表:增加一个防重表,业务唯一的id作为唯一索引,如订单号,当想针对订单做一系列操作时,可以向防重表中插入一条记录,插入成功,执行后续操作,插入失败,则不执行后续操作。本质上可以看成是基于MySQL实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据; 5、分布式锁实现幂等:执行方法时,先根据业务唯一的id获取分布式锁,获取成功,则执行,失败则不执行。分布式锁可以基于redis,zookeeper,mysql来实现,分布式锁的细节就不介绍了。 6、全局唯一号实现幂等:通过source(来源)+ seq(序列号)来判断请求是否重复,重复则直接返回请求重复提交,否则执行。如当多个三方系统调用服务的时候,就可以采用这种方式。 redis集群有哪些 redis有三种集群方式:主从复制,哨兵模式和集群。 主从复制原理: 主从复制优缺点: 优点: 缺点: 当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。 (1)监控主服务器和从服务器是否正常运行。 (2)主服务器出现故障时自动将从服务器转换为主服务器。 哨兵的工作方式: 哨兵模式的优缺点 优点:哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有;主从可以自动切换,系统更健壮,可用性更高。 缺点:Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。 3.Redis-Cluster集群 redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。 Redis-Cluster采用无中心结构,它的特点如下: 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。 节点的fail是通过集群中超过半数的节点检测失效时才生效。 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。 工作方式: 在redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。 为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。 redis的一致性哈希 场景 在一个分布式系统中,要将数据存储到具体某个节点,或者将来自客户端的请求分配到某个服务器节点做负载均衡,如果采用普通的hash取模算法进行映射,使用上能达到预期效果。 但是如果此时要下线一个服务器或者上线一个新的服务器,那么原来的映射将全部失效。如果是做分布式存储,则需要做数据迁移;如果是做分布式缓存,则原来的缓存失效,需要让新增或下线的节点生效就需要做rehash,数据较大会比较消耗时间(其实这点对HashMap了解的,很熟悉这一点,HashMap在动态扩容进行rehash,数据量过大时很消耗时间影响性能)。这时,一致性哈希就派上用场了。 Redis:一致性Hash算法_Master-TJ的个人博客-CSDN博客_redis一致性hash MQ/kafka的基本原理/使用场景。 MQ 消息队列作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性。主要具有以下优势: 使用场景:异步、解藕、削峰; kafka基本原理 Kafka是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编写。 Kafka中发布订阅的对象是topic。我们可以为每类数据创建一个topic,把向topic发布消息的客户端称作producer,从topic订阅消息的客户端称作consumer。Producers和consumers可以同时从多个topic读写数据。一个kafka集群由一个或多个broker服务器组成,它负责持久化和备份具体的kafka消息。 kafka使用场景 1、日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。 2、消息系统:解耦和生产者和消费者、缓存消息等。 3、用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。 4、运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 5、流式处理:比如spark streaming和storm 6、事件源 kafka特性 1、高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作; 2、可扩展性:kafka集群支持热扩展; 3、持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失; 4、容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败); 5、高并发:支持数千个客户端同时读写; 6、支持实时在线处理和离线处理:可以使用Storm这种实时流处理系统对消息进行实时进行处理,同时还可以使用Hadoop这种批处理系统进行离线处理; Zookeeper在kafka的作用 MQ/Kafka的区别 在Kafka中,是1个topic有多个partition,每个partition有1个master + 多个slave。 在RocketMQ里面,1台机器只能要么是Master,要么是Slave。这个在初始的机器配置里面,就定死了。 Master/Slave概念 Kafka: Master/Slave是个逻辑概念,1台机器,同时具有Master角色和Slave角色。 RocketMQ: Master/Slave是个物理概念,1台机器,只能是Master或者Slave。在集群初始配置的时候,指定死的。其中Master的broker id = 0,Slave的broker id > 0。 Broker概念: Kafka: Broker是个物理概念,1个broker就对应1台机器。 RocketMQ:Broker是个逻辑概念,1个broker = 1个master + 多个slave。所以才有master broker, slave broker这样的概念。Kafka是先有Broker,然后产生出Master/Slave;RokcetMQ是先定义Master/Slave,然后组合出Broker。 在Kafka里面,Maser/Slave是选举出来的!RocketMQ不需要选举! 具体来说,在Kafka里面,Master/Slave的选举,有2步:第1步,先通过ZK在所有机器中,选举出一个KafkaController;第2步,再由这个Controller,决定每个partition的Master是谁,Slave是谁。这里的Master/Slave是动态的,也就是说:当Master挂了之后,会有1个Slave切换成Master。 而在RocketMQ中,不需要选举,Master/Slave的角色也是固定的。当一个Master挂了之后,你可以写到其他Master上,但不会说一个Slave切换成Master。这种简化,使得RocketMQ可以不依赖ZK就很好的管理Topic/queue和物理机器的映射关系了,也实现了高可用。 在Kafka里面,一个partition必须与1个Master有严格映射关系,这个Master挂了,就要从其他Slave里面选举出一个Master;而在RocketMQ里面,这个限制放开了,一个queue对应的Master挂了,它会切到其他Master,而不是选举出来一个。 总结,RocketMQ不需要像Kafka那样有很重的选举逻辑,它把这个问题简化了。剩下的就是topic/queue的路由信息,那用个简单的NameServer就搞定了,很轻量,还无状态,可靠性也能得到很好保证。 RocketMQ支持异步实时刷盘,同步刷盘,同步Replication,异步Replication Kafka单机写入TPS约在百万条/秒,消息大小10个字节 Kafka使用短轮询方式,实时性取决于轮询间隔时间,0.8以后版本支持长轮询。 这里的重试需要可靠的重试,即失败重试的消息不因为Consumer宕机导致丢失。 Kafka支持消息顺序,但是一台Broker宕机后,就会产生消息乱序 Kafka理论上可以按照Offset来回溯消息 理论上Kafka要比RocketMQ的堆积能力更强,不过RocketMQ单机也可以支持亿级的消息堆积能力,我们认为这个堆积能力已经完全可以满足业务需求。 MQ出现消息积压了怎么办? 假设一个 MQ 消费者可以一秒处理 1000 条消息,三个 MQ 消费者可以一秒处理 3000 条消息,那么一分钟的处理量是 18 万条。如果 MQ 中积压了几百万到上千万的数据,即使消费者恢复了,也需要大概很长的时间才能恢复过来。 对于产线环境来说,漫长的等待是不可接受的,所以面临这种窘境时,只能临时紧急扩容以应对了,具体操作步骤和思路如下: kafka写超时怎么处理? 1、报错:: java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.NotLeaderForPartitionException: This server is not the leader for that topic-partition. 报错原因:producer在向kafka broker写的时候,刚好发生选举,本来是向broker0上写的,选举之后broker1成为leader,所以无法写成功,就抛异常了。 解决办法:修改producer的重试参数retries参数,默认是0, 一般设置为3, 我在生产环境配置的retries=10 2、java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.TimeoutException: Expiring 1 record(s) for binlogCsbbroker-2 due to 30026 ms has passed since batch creation plus linger time 报错原因:具体原因我自己还没有找到,但是网友们都说是因为kafka在批量写的时候,这一批次的数据没有在30s内还处理完,(30s为request.timeout.ms默认值),这一批次的数据就过期了,所以抛出异常 解决办法:增大request.timeout.ms, 我在生产环境配置的是request.timeout.ms=60000 // 由原来默认的30s改成60s 3、java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.NetworkException: The server disconnected before a response was received. 报错原因:kafka client与broker断开连接了 解决办法:重启服务 MQ/Kafka会丢消息么?如何保证数据一致性? 从Producer端看: Kafka是这么处理的,当一个消息被发送后,Producer会等待broker成功接收到消息的反馈(可通过参数控制等待时间),如果消息在途中丢失或是其中一个broker挂掉,Producer会重新发送(Kafka有备份机制,可通过参数控制是否等待所有备份节点都收到消息)。 broker端记录了partition中的一个offset值,这个值指向Consumer下一个即将消费message。当Consumer收到了消息,但却在处理过程中挂掉,此时Consumer可以通过这个offset值重新找到上一个消息再进行处理。Consumer还有权限控制这个offset值,对持久化到broker端的消息做任意处理。 kafka保证数据一致性和可靠性 是否会出现脑裂现象? 如果controller Broker 挂掉了,Kafka集群必须找到可以替代的controller,集群将不能正常运转。这里面存在一个问题,很难确定Broker是挂掉了,还是仅仅只是短暂性的故障。但是,集群为了正常运转,必须选出新的controller。如果之前被取代的controller又正常了,他并不知道自己已经被取代了,那么此时集群中会出现两台controller。 其实这种情况是很容易发生。比如,某个controller由于GC而被认为已经挂掉,并选择了一个新的controller。在GC的情况下,在最初的controller眼中,并没有改变任何东西,该Broker甚至不知道它已经暂停了。因此,它将继续充当当前controller,这是分布式系统中的常见情况,称为脑裂。 Kafka是通过使用epoch number(纪元编号,也称为隔离令牌)来完成的。epoch number只是单调递增的数字,第一次选出Controller时,epoch number值为1,如果再次选出新的Controller,则epoch number将为2,依次单调递增。 每个新选出的controller通过Zookeeper 的条件递增操作获得一个全新的、数值更大的epoch number 。其他Broker 在知道当前epoch number 后,如果收到由controller发出的包含较旧(较小)epoch number的消息,就会忽略它们,即Broker根据最大的epoch number来区分当前最新的controller。 topic分区过多会有什么问题 kafka分区数过多引发的弊端 - 云+社区 - 腾讯云 ArrayList的扩容机制说一说。(准备了hashmap没准备它,尴尬了) 如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时(即添加第一个元素时),才真正分配容量,默认分配容量为10;当容量不足时(容量为size,添加第size+1个元素时),先判断按照1.5倍(位运算)的比例扩容能否满足最低容量要求,若能,则以1.5倍扩容,否则以最低容量要求进行扩容。 执行add方法时,先判断ArrayList当前容量是否满足size+1的容量;在判断是否满足size+1的容量时,先判断ArrayList是否为空,若为空,则先初始化ArrayList初始容量为10,再判断初始容量是否满足最低容量要求;若不为空,则直接判断当前容量是否满足最低容量要求;若满足最低容量要求,则直接添加;若不满足,则先扩容,再添加。 ArrayList的最大容量为Integer.MAX_VALUE。 说一说hashMap put的详细过程,越详细越好。 这个就需要自行去研究下源码,在理解的基础上来阐述了。1.7和1.8要分别说。最好是熟悉put/get/扩容机制等。 可借鉴一篇文章 看完这篇 HashMap,和面试官扯皮就没问题了 - 程序员cxuan - 博客园 介绍一下hashMap,1.7和1.8的区别是什么? (1)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候(泊松分布,概率非常小),也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率) (2)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。 (3)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(hash值 & length-1);而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式。 (4)在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在1.8的时候是先插入再扩容的(可能是到了红黑树上),优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容,但是在1.7的时候就会急造成扩容; (5)扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。 concurrentHashMap的1.7和1.8的区别?1.7为什么要优化到1.8?是否会发生全表锁(size计算) (1)JDK1.7:ReentrantLock+Segment+HashEntry, JDK1.8:取消segments字段,直接采用transient volatile HashEntry (2)JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点) 1.7的size的ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。 在JDK1.8版本中,对于size的计算,在扩容和addCount()方法就已经有处理了,可以注意一下Put函数,里面就有addCount()函数,早就计算好的,然后你size的时候直接给你。 有时间可以去着重熟悉下put/get/扩容机制的详细过程并加以理解。 可借鉴 ConcurrentHashMap 1.7和1.8源码分析 - 知乎 hashMap如何快速提取数据? HashEntry的方式一次性取到key和值。 为什么扩容是2倍? 向集合中添加元素时,会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置; 而HashMap扩容时调用resize()方法中会新建一个tab,然后遍历旧的tab,将旧的元素经过e.hash & (newCap - 1)的计算添加进新的tab中,还是用(n - 1) & hash的计算方法,其中n是集合的容量,hash是添加的元素经过hash函数计算出来的hash值。 可见这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算,这是位运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。 并发场景下是否会出现数据的丢失? HashMap在并发场景下可能存在问题:数据丢失;数据重复;死循环。 在Java8之前的版本中之所以出现死循环是因为在resize的过程中对链表进行了头插法处理;在Java8中是尾插法,自然也不会出现死循环。 如果两条线程同时执行到语句 table[i]=null时两个线程都会区创建Entry,这样存入会出现数据丢失。 如果有两个线程同时发现自己都key不存在,而这两个线程的key实际是相同的,在向链表中写入的时候第一线程将e设置为了自己的Entry,而第二个线程执行到了e.next,此时拿到的是最后一个节点,依然会将自己持有是数据插入到链表中,这样就出现了数据重复。 顺序map的实现 即LinkedHashMap.图解LinkedHashMap原理 - 简书 两个有序集合的合并(leetcode上是有序数组,从后往前处理的,我采用的是从前往后,以两个变量分别记录下标,找好边界) 链表反转。 删除链表倒数第N个节点。 熟悉的排序算法,简要介绍一些。(快排/冒泡/归并等,需要知道怎么排,以及时间空间复杂度) 海量大文件如何排序?(分割排序+topN+多路归并) 二叉树层序遍历(借助队列)/深度遍历/前中后序遍历; 升序链表的合并。(递归判断) 如何判断两个单链表是否有交点(尾节点相等)判断是否有环(快慢指针) 系统和下游系统对接时,rpc接口应该如何处理?超时了怎么处理?重试怎么处理?数据不一致怎么处理? 接口对接应该注意几点:1、出入参;2、返回结果中的异常处理;3、超时时间的设置与重试设置;4、异常报警机制;5、一次传输的数据量级的卡控; 超时的话,一是超时时间的调整;一是看数据量级的调整; 重试的话需要根据业务场景来制定,有些场景不允许自动重拾,需要手动下发;有些是允许重拾的,主要是分辨重试的数据是否会对下游的数据造成影响而导致不一致的情况发生; 数据不一致时,可以考虑将下游数据删除,上游重新下发一次。 项目的时间把控 先根据项目的需求的优先级及难易程度进行排期,如果有风险: 一是协调研发和测试时间。研发资源与测试资源是否可以增加。 二是看上线日期是否可调整,如可以调整,则延长到合理的日期; 三如果上线日期不能调整,也没有研发测试资源,那就只能老老实实地加班咯。update user set name='风筝2号’ where age = 10;
的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10
AutoIncrement 计数器在 MySQL 8.0 之前,存储在内存中,每次启动时通过以下语句初始化:SELECT MAX(ai_col) FROM table_name FOR UPDATE;在 MySQL 8.0 之后,持久化存储到磁盘。通过每次更新写入 Redo Log,并在检查点刷入 innodb 引擎表中记录下来。所以,在MySQL 8.0 之前,如果 rollback 导致某些值没有使用,重启后,这些值还是会使用。但是在 MySQL 8.0 之后就不会了。
如:where username=#{username},如果传入的值是111,那么解析成sql时的值为where username="111", 如果传入的值是id,则解析成的sql为where username="id".
2、$将传入的数据直接显示生成在sql中。
如:where username=${username},如果传入的值是111,那么解析成sql时的值为where username=111;如果传入的值是;drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user;
3、#方式能够很大程度防止sql注入,$方式无法防止Sql注入。
4、$方式一般用于传入数据库对象,例如传入表名.
5、一般能用#的就别用$,若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止sql注入攻击。
6、在MyBatis中,“${xxx}”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“${xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。
【结论】在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止SQL注入攻击。mybatis是如何做到防止sql注入的
Es
优点:
缺点:
6、multi_match表示多字段匹配关键词,我们试着在name和sex里找,只要包含男的我们就返回该数据;
Redis
redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。
Redis集群各方法的响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明显上升(公有集群影响因素偏大),但是极限qps可以达到最大且基本无异常。
有了 min-slaves-max-lag 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。
如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。
Map
来存储需要续期的任务信息
。任务信息
放入Map
,并启动延迟任务,延迟任务在执行延期动作
前先检查下Map
里锁数据是不是还是被当前任务持有。
1、使用唯一索引:当数据重复时,插入数据库会抛异常;
主从复制
哨兵模式
Kafka/MQ
1、无论是kafka集群,还是producer和consumer都依赖于zookeeper来保证系统可用性集群保存一些meta信息。
2、Kafka使用zookeeper作为其分布式协调框架,很好的将消息生产、消息存储、消息消费的过程结合在一起。
3、同时借助zookeeper,kafka能够生产者、消费者和broker在内的所有组件在无状态的情况下,建立起生产者和消费者的订阅关系,并实现生产者与消费者的负载均衡。
架构
Master/Slave/Broker概念
Master/Slave/Broker特性
数据可靠性
Kafka使用异步刷盘方式,异步Replication/同步Replication
总结:RocketMQ的同步刷盘在单机可靠性上比Kafka更高,不会因为操作系统Crash,导致数据丢失。性能对比
RocketMQ单机写入TPS单实例约7万条/秒,单机部署3个Broker,可以跑到最高12万条/秒,消息大小10个字节
总结:Kafka的TPS跑到单机百万,主要是由于Producer端将多个小消息合并,批量发向Broker。
消息投递实时性
RocketMQ使用长轮询,同Push方式实时性一致,消息的投递延时通常在几个毫秒。
消费失败重试
Kafka消费失败不支持重试。
RocketMQ消费失败支持定时重试,每次重试间隔时间顺延
总结:例如充值类应用,当前时刻调用运营商网关,充值失败,可能是对方压力过多,稍后再调用就会成功,如支付宝到银行扣款也是类似需求。严格的消息顺序
RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序
Mysql Binlog分发需要严格的消息顺序消息回溯
RocketMQ支持按照时间来回溯消息,精度毫秒,例如从一天之前的某时某分某秒开始重新消费消息
总结:典型业务场景如consumer做订单分析,但是由于程序逻辑或者依赖的系统发生故障等原因,导致今天消费的消息全部无效,需要重新从昨天零点开始消费,那么以时间为起点的消息重放功能对于业务非常有帮助。
消息堆积能力
从Consumer端看:
集合(基本上围绕着hashmap\ArrayList)
(3)JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
(4)JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
(5)JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
算法(代码后续我再贴出)
项目(个人理解,仅作参考)