java面试题

java

linkedList和ArrayList的区别
  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)
如何让linkedList变为线程安全

LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
List list=Collections.synchronizedList(new LinkedList(…));

List list=Collections.synchronizedList(new LinkedList(...));
arraylist扩容原理

首先说jdk1.7:

当实例化ArrayList时,创建长度为10的object[ ] ;
当add添加到11个的时候,扩容,扩容为原来的1.5倍。
将原来的数据复制到新的数组中。
建议使用new ArrayList(int capacity)直接声明数组的大小;

然后说一下1.8的变化:

当实例化是,创建object[ ] ,初始化为 { },并没有长度。
当添加第一个元素时,创建长度为10的数组。
后续一致。

1.7类似,饿汉式
1.8类似,懒汉式
1.8的优点:延迟数组的创建,节省内存

vector的分析:首先1.71.8创建长度都为10,扩容方面是原来的2倍。
String,StringBuffer,StringBuild区别

String,StringBuild 线程不安全
StringBuffer 线程安全

String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的

在大部分情况下 StringBuffer > String

在大部分情况下 StringBuilder > StringBuffer

HashMap在1.7和1.8的变化

HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。

HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。扩容加载因子0.75f,第一次扩容16*0.75=12,0.75是官方给出的一个比较好的临界值

1.8之前

  • 数组+链表
  • 扰动函数四次位移20>12>7>4
  • 拉链法(将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可)

1.8之后

  • 数组+链表(当链表长度大于8或者数组长度大于64转换为红黑树,减少搜索时间,当小于6的时候会转回链表)
  • 扰动函数一次位移(因为官方认为扰动太多次结果也差不多,而且性能会差点,所以选了个比较合适的值扰动)16

spring

SpringBean的生命周期

Bean自身的方法

Bean级生命周期接口方法

容器级生命周期接口方法

工厂后处理器接口方法

Spring如何解决循环依赖

什么是循环依赖

2个或以上bean互相持有对方,最终形成闭环

singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】

singletonObjects:单例对象的cache

img

img

通过上面的源码我们可以看到,在获取单例Bean的时候,会先从一级缓存singletonObjects里获取,如果没有获取到(说明不存在或没有实例化完成),会去第二级缓存earlySingletonObjects中去找,如果还是没有找到的话,就会三级缓存中获取单例工厂singletonFactory,通过从singletonFactory中获取正在创建中的引用,将singletonFactory存储在earlySingletonObjects 二级缓存中,这样就将创建中的单例引用从三级缓存中升级到了二级缓存中,二级缓存earlySingletonObjects,是会提前暴露已完成构造,还可以执行属性注入的单例bean的。 这个时候如何还有其他的bean也是需要属性注入,那么就可以直接从earlySingletonObjects中获取了

Spring中的事务隔离级别

img

Spring中的ioc和aop原理,并说出它们的使用场景

IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP

Spring中注入的几种方式

1.autowire 注解

即标识符就是bean的名字。这个需要autowire的后置处理器。可参考前面的后置处理器的文章。

2.set方法注入

即添加属性后,只需要给该属性添加一个set方法即可。然后在xml文件中注入的时候,配置property属性进行注入,即property name=“beanname” value=""或者ref=“”某个bean。

3.构造器注入

即通过构造函数传入,这样就不需要set方法。传入的时候就是在xml文件中配置

<constructor-argindex="0"ref=“beanname”>

其实也是类似set方式注入的。

springboot

SpringBoot的核心注解

@SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
  • @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilterAutoConfigurationExcludeFilter
SpringBoot的自动装配原理

EnableAutoConfiguration 只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector

可以看出,AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中

img

并不是所有被扫描到的配置都会加载,这一步有经历了一遍筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效

jvm

JDK,JRE,jvm,JIT的区别

JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
  Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
  JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。

JIT也称为即时编译器。调用方法时使用。JIT将被调用方法的字节码编译成本机代码。当一个方法在本机代码中编译时,JVM直接调用该方法的编译代码,而不是解释它。

类加载流程

img

什么是双亲委派机制

img

工作原理

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器
  • 如果父类加载器可以完成类加载,则返回成功,若父类加载器无法完成类加载,则子类加载器才会尝试进行加载,这就是双亲委派机制

优势

  • 避免类的重复加载
  • 保护程序安全,防止核心api被随意篡改:比如java.lang.String
什么是OOM,怎么解决

1、代码中可能存在大对象分配

2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法

1、检查是否存在大对象的分配,最有可能的是大数组分配

2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题

3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存

4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

jvm中新生代,老年代,永久代区别

堆=新生代+老年代,不包括永久代(方法区)。

新生代分为三个区域,一个Eden区和两个Survivor区,它们之间的比例为(8:1:1),这个比例也是可以修改的。

通常情况下,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中。Java虚拟机每次使用新生代中的Eden和其中一块Survivor(From),在经过一次Minor GC后,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上(这里使用的复制算法进行GC),最后清理掉Eden和刚才用过的Survivor(From)空间。将此时在Survivor空间存活下来的对象的年龄设置为1,以后这些对象每在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

总结:

1、Minor GC是发生在新生代中的垃圾收集,采用的复制算法;

2、新生代中每次使用的空间不超过90%,主要用来存放新生的对象;

3、Minor GC每次收集后Eden区和一块Survivor区都被清空;

4、老年代中使用Full GC,采用的标记-清除算法

请列举你所知道的垃圾收集器,以及各自的优缺点
清楚算法

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)
复制算法

为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收

整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

请解释一下强引用,软引用,弱引用,虚引用
  • 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它

  • 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

  • 弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

  • 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

synchronized1.6前后变化

在JDK1.6以前,使用synchronized就只有一种方式即重量级锁,而在JDK1.6以后,引入了偏向锁,轻量级锁,重量级锁,来减少竞争带来的上下文切换。

synchronized和lock的区别

这里写图片描述

乐观锁和悲观锁

悲观锁的实现:

  1. 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
  2. Java 里面的同步 synchronized 关键字的实现

悲观锁主要分为共享锁和排他锁

  • 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改

乐观锁的实现:

  1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

死锁的产生和解决

死锁产生条件

​ 1.互斥
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

​ 2.占有等待

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

​ 3.循环等待

存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。

​ 4.不可抢占

进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测
读写锁

顾名思义『读写锁』就是对于临界区区分读和写。在读多写少的场景下,不加区分的使用互斥量显然是有点浪费的。此时便该上演读写锁的拿手好戏。

特性:

  • 当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)
  • 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功
自旋锁的优缺点

优点

自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间

缺点

在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁

cas是什么,aba怎么解决

通过对atomic包的分析我们知道了CAS机制,我们在看一下CAS的公式。

CAS(V,A,B)
1:V表示内存中的地址
2:A表示预期值
3:B表示要修改的新值

CAS的原理就是预期值A与内存中的值相比较,如果相同则将内存中的值改变成新值B。这样比较有两类:

第一类:如果操作的是基本变量,则比较的是 值 是否相等。

第二类:如果操作的是对象的引用,则比较的是对象在 内存的地址 是否相等。

其实**CAS是Java乐观锁的一种实现机制,**在Java并发包中,大部分类就是通过CAS机制实现的线程安全,它不会阻塞线程,如果更改失败则可以自旋重试,但是它也存在很多问题:

1:ABA问题,也就是说从A变成B,然后就变成A,但是并不能说明其他线程并没改变过它,利用CAS就发现不了这种改变。
2:由于CAS失败后会继续重试,导致一致占用着CPU。

ABA可以用AtomicStampedReference加版本号解决

volatile的作用以及和synchronized有什么区别

volatile关键字的作用就是保证了可见性和有序性(不保证原子性),volatile能禁止指令重新排序,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。

synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了

volatile关键字和synchronized关键字的区别
(1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
(2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
(3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

锁升级过程

java中对象锁有4种状态:(级别从低到高)

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

锁升级的方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

1.偏向锁
偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

2.轻量级锁
如果明显存在其它线程申请锁,那么偏向锁将很快升级为轻量级锁。

3.自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

4.重量级锁
指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

什么是AQS

AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表

AQS核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。

如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
AQS定义了两种资源获取方式:独占(只有一个线程能访问执行)和共享(多个线程可同时访问执行)

redis

缓存穿透,缓存击穿,缓存雪崩

缓存穿透

描述:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:

  1. 设置热点数据永远不过期。
  2. 加互斥锁

缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。
数据的应用场景

String:

应用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。计数器(字符串的内容为整数的时候可以使用)

list:

应用场景: 发布与订阅或者说消息队列、慢查询

hash:

应用场景: 系统中对象数据的存储。

set:

应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

sorted set:

应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。

bitmap:

应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。

如何保证数据都是热点数据

限定 Redis 占用的内存,Redis 会根据自身数据淘汰策略,留下热数据到内存。所以,计算一下 50W 数据大约占用的内存,然后设置一下 Redis 内存限制即可,并将淘汰策略为volatile-lru或者allkeys-lru

什么是布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

从容器的角度来说:

如果布隆过滤器判断元素在集合中存在,不一定存在

如果布隆过滤器判断不存在,一定不存在

从元素的角度来说:

如果元素实际存在,布隆过滤器一定判断存在

如果元素实际不存在,布隆过滤器可能判断存在

redis如何实现分布式锁
//获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX  30000

//释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

加锁代码分析

首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,用来标识这把锁是属于哪个请求加的,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

解锁代码分析

将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,如果相等则解锁(删除key)。

存在的风险

如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,导致出现多个客户端持有锁的情况,这样就不能实现资源的独享了。

  1. 客户端A从master获取到锁

  2. 在master将锁同步到slave之前,master宕掉了(Redis的主从同步通常是异步的)。
    主从切换,slave节点被晋级为master节点

  3. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的情况

    SET key value [EX seconds] [PX milliseconds] [NX|XX]
    EX second :设置键的过期时间为second秒
    PX millisecond :设置键的过期时间为millisecond毫秒
    NX :只在键不存在时,才对键进行设置操作
    XX:只在键已经存在时,才对键进行设置操作
    SET操作成功完成时,返回OK ,否则返回nil

过期删除策略有哪些
  1. 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除

内存淘汰机制有哪些

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
如何保证缓存和数据库数据一致性

双写一致性

  1. 先更新数据库,再更新缓存:会造成脏数据
  2. 先删除缓存,再更新数据库:会造成脏数据,因为一直查询不到缓存,会把数据库旧值写入缓存
  3. 先更新数据库,再删除缓存:会造成脏数据,并发情况下会出现读写不一致

采用延时双删策略

先删除缓存,然后设置休眠更新数据库再删除缓存

什么是cap原则,什么是base理论,redis实现了其中的哪些

Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CA 传统Oracle数据库

AP 大多数网站架构的选择

CP Redis、Mongodb

什么是acid原则

1、A (Atomicity) 原子性
原子性很容易理解,也就是说事务里的所有操作要么全部做完,要么都不做,事务成功的条件是事务里的所有操作都成功,只要有一个操作失败,整个事务就失败,需要回滚。比如银行转账,从A账户转100元至B账户,分为两个步骤:1)从A账户取100元;2)存入100元至B账户。这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。

2、C (Consistency) 一致性
一致性也比较容易理解,也就是说数据库要一直处于一致的状态,事务的运行不会改变数据库原本的一致性约束。

3、I (Isolation) 独立性
所谓的独立性是指并发的事务之间不会互相影响,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。比如现有有个交易是从A账户转100元至B账户,在这个交易还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元的

4、D (Durability) 持久性
持久性是指一旦事务提交后,它所做的修改将会永久的保存在数据库上,即使出现宕机也不会丢失。

Redis的rdb和aof

快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

AOF(append-only file)持久化

与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

redis6为什么会引入多线程

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。

redis和memcached的区别

共同点

  1. 都是基于内存的数据库,一般都用来当做缓存使用。
  2. 都有过期策略。
  3. 两者的性能都非常高。

区别

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  5. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
  7. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
  8. Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
redis的哨兵模式

哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

多哨兵模式下在master宕机时会进行投票选举,选一个新的临时作为master,当master恢复后切换回去(建议3个以上,并且为奇数)

redis的发布订阅

redis的列表类型天生支持用作消息队列(类似于mq的队列模型:任何时候每条消息只能消费一次)

在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除

如何用redis确定两个人之间的距离

redis3.2版本里面新增的一个功能就是对GEO(地理位置)的支持。

地理位置大概提供了6个命令,分别为:

  • GEOADD
  • GEODIST
  • GEOHASH
  • GEOPOS
  • GEORADIUS
  • GEORADIUSBYMEMBER

只需要存入两个人的经度纬度然后通过geodist来获取两个人之间的距离

springCloud

springcloud和springboot的区别

1、SpringBoot只是一个快速开发框架,使用注解简化了xml配置,内置了Servlet容器,以Java应用程序进行执行。

2、SpringCloud是一系列框架的集合,可以包含SpringBoot。

SpringCloud具备微服务开发的核心技术:RPC远程调用技术;SpringBoot的web组件默认集成了SpringMVC,可以实现HTTP+JSON的轻量级传输,编写微服务接口,所以SpringCloud依赖SpringBoot框架实现微服务开发。

cloud中有哪些组件,它们的作用是什么

服务发现注册配置中心消息总线负载均衡断路器数据监控

1、Eureka实现服务治理;

2、Ribbon主要提供客户侧的软件负载均衡算法;

3、Hystrix断路器,保护系统,控制故障范围;

4、Zuul,api网关,路由,负载均衡等多种作用;

5、Config配置管理

hystrix它如何实现容错,他的使用场景有哪些

Hystrix 就是一个能进行 熔断降级 的库,通过使用它能提高整个系统的弹性

熔断 就是指的 Hystrix 中的 断路器模式 ,你可以使用简单的@HystrixCommand 注解来标注某个方法,这样 Hystrix 就会使用 断路器 来“包装”这个方法,每当调用时间超过指定时间时(默认为1000ms),断路器将会中断对这个方法的调用

降级是为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复

使用场景

雪崩效应常见场景

  • 硬件故障:如服务器宕机,机房断电,光纤被挖断等。
  • 流量激增:如异常流量,重试加大流量等。
  • 缓存穿透:一般发生在应用重启,所有缓存失效时,以及短时间内大量缓存失效时。大量的缓存不命中,使请求直击后端服务,造成服务提供者超负荷运行,引起服务不可用。
  • 程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。
  • 同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽

代码请求超时

zuul和gateway的区别
相同点:

1、底层都是servlet

2、两者均是web网关,处理的是http请求

不同点:

1、内部实现:

gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件
  zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等。
2、是否支持异步
  zuul仅支持同步
  gateway支持异步。理论上gateway则更适合于提高系统吞吐量(但不一定能有更好的性能),最终性能还需要通过严密的压测来决定
3、框架设计的角度
  gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的
4、性能
  WebFlux 模块的名称是 spring-webflux,名称中的 Flux 来源于 Reactor 中的类 Flux。Spring webflux 有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。使用非阻塞API。 Websockets得到支持,并且由于它与Spring紧密集成,所以将会是一个更好的 开发 体验。
  Zuul 1.x,是一个基于阻塞io的API Gateway。Zuul已经发布了Zuul 2.x,基于Netty,也是非阻塞的,支持长连接,但Spring Cloud暂时还没有整合计划。

eureka的底层实现原理

服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。

当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。

服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例

什么是微服务,什么是分布式

微服务是一种面向服务的架构(SOA)风格(Java开发人员最重要的技能之一),其中,应用程序被构建为多个不同的小型服务的集合而不是单个应用程序。与单个程序不同的是,微服务让你可以同时运行多个独立的应用程序,而这些独立的应用程序可以使用不同的编码或编程语言来创建。庞大而又复杂的应用程序可以由多个可自行执行的简单而又独立的程序所组成。这些较小的程序组合在一起,可以提供庞大的单程序所具备的所有功能。

微服务是一种面向服务的架构风格,具有灵活性和低成本两个特点.
灵活性:由于这些较小的应用程序无需使用相同的编程语言,因此,开发人员可以使用他们最熟悉的语言,这是灵活性.

低成本:由于他们都用自己擅长的语言去开发,所以效率会高,相应的开发成本会降低.

所谓分布式,无非就是将一个系统拆分成多个子系统并分布到多个服务器上.

rest和rpc的区别

img

REST和RPC都常用于微服务架构中。

1)HTTP相对更规范,更标准,更通用,无论哪种语言都支持http协议。如果你是对外开放API,例如开放平台,外部的编程语言多种多样,你无法拒绝对每种语言的支持,现在开源中间件,基本最先支持的几个协议都包含RESTful。

2)RPC 框架作为架构微服务化的基础组件,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节。让调用方感觉就像调用本地函数一样调用远端函数、让服务提供方感觉就像实现一个本地函数一样来实现服务

cloud是如何实现服务的注册和发现

Eureka 提供了服务注册和服务发现的功能,服务注册是让所有微服务将自己的信息注册到注册中心,服务发现是让微服务可以拉取注册中心里的服务列表,方便结合feign进行远程调用,由于所有服务都在 Eureka 服务器上注册并通过调用 Eureka 服务器完成查找, 因此无需处理服务地点的任何更改和处理。

项目中zuul常用的功能

过滤和路由最常用

鉴权:对于访问每个服务的请求进行鉴权,拒绝鉴权失败的请求。

监控:监控请求及服务的运行情况,提供生产服务运行数据参考。

压力测试:帮助对整个集群进行可控的压力测试。

金丝雀测试:帮助完成应用的热部署,达到用户无感升级。

动态路由:基于请求路径,将请求可控的分发到指定的客户端。

负载控制:统一控制各客户端请求压力,超过压力的请求直接拒绝。

等……

ribbon的负载均衡策略
策略类命名描述
RandomRule随机策略随机选择server
RoundRobinRule轮询策略轮询选择, 轮询index,选择index对应位置的Server;
RetryRule重试策略对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的server;
BestAvailableRule最低并发策略逐个考察server,如果server断路器打开,则忽略,再选择其中并发链接最低的server
AvailabilityFilteringRule可用过滤策略过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server(active connections超过配置的阈值)或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个Server的运行状态;
ResponseTimeWeightedRule响应时间加权重策略根据server的响应时间分配权重,响应时间越长,权重越低,被选择到的概率也就越低。响应时间越短,权重越高,被选中的概率越高,这个策略很贴切,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间
ZoneAvoidanceRule区域权重策略综合判断server所在区域的性能,和server的可用性,轮询选择server并且判断一个AWS Zone的运行性能是否可用,剔除不可用的Zone中的所有server

zookeeper

四种节点类型CP原则

img

zookeeper应用场景

ZooKeeper 概览中,我们介绍到使用其通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

下面选 3 个典型的应用场景来专门说说:

  1. 分布式锁 : 通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。
  2. 命名服务 :可以通过 ZooKeeper 的顺序节点生成全局唯一 ID
  3. 数据发布/订阅 :通过 Watcher 机制 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
zookeeper加分布式锁的优点

1)优点:ZooKeeper分布式锁(如InterProcessMutex),能有效的解决分布式问题,不可重入问题,使用起来也较为简单。

(2)缺点:ZooKeeper实现的分布式锁,性能并不太高。为啥呢?
因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。大家知道,ZK中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同步到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。

总之,在高性能,高并发的场景下,不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性,所以在并发量不是太高的场景,推荐使用ZooKeeper的分布式锁。

zookeeper如何保证事务顺序一致性

zookeeper 采用了递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid, zxid 实际上是一个 64 位的数字,高 32 位是 epoch (时期; 纪元; 世; 新时代)用来标识 leader 是否发生改变,如果有新的 leader 产生出来,epoch 会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

如何解决脑裂问题

zookeeper集群中,各个节点间的网络通信不良时,容易出现脑裂(split-brain)现象;

集群中的节点监听不到leader节点的心跳,就会认为leader节点出了问题,此时集群将分裂为不同的小集群,这些小集群会各自选举出自己的leader节点,导致原有的集群中出现多个leader节点,这就是脑裂现象。

ZooKeeper默认采用了Quorums(法定人数)的方式: *只有获得超过半数节点的投票, 才能选举出leader,这种方式可以确保要么选出唯一的leader,要么选举失败*

zookeeper是不会有脑裂问题

它底层实现了ZAB协议,只有半数以上的节点同意,才会写入成功,否则报错。比如,现在我们三个点,那半数以上也就是有两个节点同意。对于新leader刚好两个(算是它自己),而对于久leader的,就只有一个节点,所以,它是不会写入成功的

nginx

什么是正向代理,什么是反向代理

1.什么叫做正向代理

是一个位于客户端和原始服务器之间的服务器,为了从原始服务器之间的服务器,为了原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始目标),然后代理向原始服务器转交请求并将获得内容返回给客户端。客户端才能使用正向代理。

2.什么是反向代理

反向代理是作用在服务器,是一个虚拟ip,对于用户的一个请求,会转发给多个后端处理器中的一台来处理该具体要求。

nginx是如何实现高并发的

异步,非阻塞,使用了epoll 和大量的底层代码优化。

如果一个server采用一个进程负责一个request的方式,那么进程数就是并发数。正常情况下,会有很多进程一直在等待中。

而nginx采用一个master进程,多个woker进程的模式。

  • master进程主要负责收集、分发请求。每当一个请求过来时,master就拉起一个worker进程负责处理这个请求。
  • 同时master进程也负责监控woker的状态,保证高可靠性
  • woker进程一般设置为跟cpu核心数一致。nginx的woker进程在同一时间可以处理的请求数只受内存限制,可以处理多个请求。

Nginx 的异步非阻塞工作方式正把当中的等待时间利用起来了。在需要等待的时候,这些进程就空闲出来待命了,因此表现为少数几个进程就解决了大量的并发问题。

什么是动静分离,为什么要做动静分离

Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。

img

  • 对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。
  • 若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。
什么是CDN

CDN应用广泛,支持多种行业、多种场景内容加速,例如:图片小文件、大文件下载、视音频点播、直播流媒体、全站加速、安全加速。

nginx有哪些负载均衡策略

轮询:默认方式

weight:权重方式

ip_hash:依据ip分配方式

least_conn:最少连接方式

fair:(第三方)响应时间方式

url_hash:(第三方)依据URL分配方式

RabbitMq

rabbitmq的特点,以及优缺点

RabbitMQ的特点

1、保证可靠性(Reliability):使用持久化、传输确认、发布确认等机制

2、灵活的路由功能(Flexible Routing):在消息进入队列之前,通过Exchange(交换器)来路由消息,对应典型的路由功能,RabbitMQ提供了内置的一些Exchange来实现、针对复杂的路由功能,可以将多个Exchange绑定在一起,也可以通过插件来实现自己的Exchange

3、支持消息集群(Clustering):多台RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker

4、具有高可用性(Highly Available):队列可以在集群中的机器进行镜像,在部分节点出现问题的情况下队列仍然可用

5、支持多种协议(Multi-protocol):RabbitMQ除了支持AMQP协议之外,还通过插件方式支持其它消息队列协议,比如STOMP、MQTT等

6、支持多语言客户端(Many Client):几乎支持所有常用的语言

7、提供管理界面(Management UI):RabbitMQ提供了一个简单的用户页面,用户可以监控和管理消息

8、提供跟踪机制(Tracing):RabbitMQ提供了消息跟踪机制,如果消息异常,使用者可以查出发生了什么情况

9、提供插件机制(Plugin System):RabbitMQ提供了许多插件,从多方面进行扩展,也可以自己编写自己的插件

优缺点

1.优点:在特殊场景下其对应的好处,详见上篇文章:为什么要使用RabbitMQ(业务场景) 应用异步 应用解耦 流量削峰

2.缺点: 系统可用性降低:系统引入外部依赖越多,越容易挂掉。本来A系统调用BCD系统好好的,加一个MQ统一连接BCD系统,万一MQ挂掉,整套系统就崩溃了。

AMQP是什么?请说说它的三层协议

高级消息队列,是为了弥补当前应用大量使用异步消息模型

Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。

Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。

TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。

怎么理解生产者和消费者

生产者

  • 消息生产者,就是投递消息的一方。
  • 消息一般包含两个部分:消息体(payload)和标签(Label)。

消费者

  • 消费消息,也就是接收消息的一方。
  • 消费者连接到RabbitMQ服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
如何保证消息的可靠投递

消息到MQ的过程中搞丢,MQ自己搞丢,MQ到消费过程中搞丢。

生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。

RabbitMQ自身:持久化、集群、普通模式、镜像模式。

RabbitMQ到消费者:basicAck机制、死信队列、消息补偿机制。

两种方式:1.事务模式 2.确认模式

生产者消息发送以及消费者消息接收过程
生产者消息运转?

1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。

2.Producer声明一个交换器并设置好相关属性。

3.Producer声明一个队列并设置好相关属性。

4.Producer通过路由键将交换器和队列绑定起来。

5.Producer发送消息到Broker,其中包含路由键、交换器等信息。

6.相应的交换器根据接收到的路由键查找匹配的队列。

7.如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。

8.关闭信道。

9.管理连接。

消费者接收消息过程?

1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。

2.向Broker请求消费响应的队列中消息,可能会设置响应的回调函数。

3.等待Broker回应并投递相应队列中的消息,接收消息。

4.消费者确认收到的消息,ack

5.RabbitMq从队列中删除已经确定的消息。

6.关闭信道。

7.关闭连接。

什么是死信,出现的原因是什么

DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。

死信原因

  • 消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false
  • 消息TTL过期。
  • 队列满了,无法再添加。
什么是延迟队列和优先级队列

存储对应的延迟消息,指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

优先级队列

  • 优先级高的队列会先被消费。
  • 可以通过x-max-priority参数来实现。
  • 当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。
rabbitmq的使用场景

rabbitmq主要功能,异步/解耦/削峰

  • 接口之间耦合比较严重
  • 面对大流量并发时,容易被冲垮
  • 存在性能问题
消息一致性

如果消费者确实宕机了,带着代码出现了问题,导致无法正常消费,在我们尝试多次重复后,消息最终都没有处理,可以做记录,日志、数据库等,然后进行人工补偿,比如可以人工修改数据库数据进行数据一致性的补偿。

如何保证不被重复消费

幂等

rabbitmq的六种工作模式

simple简单模式

在这里插入图片描述

work工作模式(资源的竞争)

在这里插入图片描述

publish/subscribe发布订阅(共享资源)

在这里插入图片描述

routing路由模式

在这里插入图片描述

topic 主题模式(路由模式的一种)

在这里插入图片描述

RPC

在这里插入图片描述

mysql

innodb和myisam的区别

MyISAM

  • 不需要事务支持(不支持)
  • 并发相对较低(锁定机制问题)
  • 数据修改相对较少(阻塞问题),以读为主
  • 数据一致性要求不是非常高
  1. 尽量索引(缓存机制)
  2. 调整读写优先级,根据实际需求确保重要操作更优先
  3. 启用延迟插入改善大批量写入性能
  4. 尽量顺序操作让insert数据都写入到尾部,减少阻塞
  5. 分解大的操作,降低单个操作的阻塞时间
  6. 降低并发数,某些高并发场景通过应用来进行排队机制
  7. 对于相对静态的数据,充分利用Query Cache可以极大的提高访问效率
  8. MyISAM的Count只有在全表扫描的时候特别高效,带有其他条件的count都需要进行实际的数据访问

InnoDB

  • 需要事务支持(具有较好的事务特性)
  • 行级锁定对高并发有很好的适应能力,但需要确保查询是通过索引完成
  • 数据更新较为频繁的场景
  • 数据一致性要求较高
  • 硬件设备内存较大,可以利用InnoDB较好的缓存能力来提高内存利用率,尽可能减少磁盘 IO
  1. 主键尽可能小,避免给Secondary index带来过大的空间负担
  2. 避免全表扫描,因为会使用表锁
  3. 尽可能缓存所有的索引和数据,提高响应速度
  4. 在大批量小插入的时候,尽量自己控制事务而不要使用autocommit自动提交
  5. 合理设置innodb_flush_log_at_trx_commit参数值,不要过度追求安全性
  6. 避免主键更新,因为这会带来大量的数据移动
为什么阿里手册不推荐使用外键

以学生和成绩的关系为例,学生表中的 student id 是主键,那么成绩表中的 student id
则为外键。如果更新学生表中的 student id ,同时触发成绩表中的 student id 更新,即为
级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群 ; 级联更新是强阻
塞,存在数据库更新风暴的风险 ; 外键影响数据库的插入速度。

什么是行锁和表锁?怎么加锁
  • 表锁:一次性锁一张表整体加锁,如myISAM存储引擎使用表锁,开销小,加锁快,无死锁,但锁范围大,容易发生锁冲突,并发度低

  • 行锁:一次性锁一行数据加锁,如innoDB存储引擎使用行锁,开销大,加锁慢,容易出现死锁,锁的范围较小,不易发生锁冲突,并发度高(很小概率发生并发问题:脏读,幻读,不可重复读,丢失更新)

    增加锁
    lock table 表1 read/write, 表2 read/write …

    表锁通过unlock tables解锁,也可以通过事务解锁,行锁通过事务解锁

    select 使用 for update加锁

mysql为什么不太建议使用自己的缓存

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。

mysql并发事务会带来哪些问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读区别:

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

事务隔离级别有哪些

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
sql的解析顺序

from … on … join … where … group by … having … select … dinstinct … order by … limit

如何做冷热分离

这个冷热分离的好处,就是将不常用的数据放在历史库中,当然,这个历史库也可以是多个,也就是一个生产库,多个历史库,每个历史库都存放某一时间段的数据

减少磁盘 IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的 IO);

更有效的利用缓存,避免读入无用的冷数据;

以下提供几种数据迁移的思路
1.1.执行一个job,定时每天凌晨开始自动迁移,每次迁移若干条,这样就会在不知不觉中将数据迁移完,这样最保险,但不是效率最高
1.2.直接用一个线程池,最多开五个线程(具体能开几个,看自己的机器性能),然后每个线程每次只跑一天的量,这样其实也是很快的

查询

1、数据库分库而不是分表,分表需要考虑后期的查询问题,此外还需要注意分表的算法(哈希算法)。

2、热数据只占全部数据的一部分,因此每次优先查询热库,以下情况才查询冷库

  • 当查询条件未命中(结果集为空)时,查询冷库。

  • 当查询条件部分命中时,查询冷库。

3、为了区分部分命中和全部命中,可以在热库中建一张R表存放每次查询冷库的查询条件和查询结果数量和查询结果的主键,每次查询热库时,对比相同查询条件的查询结果数量是否一致。一致,则本次查询结束。不一致,则需要到冷库中进行查询。

4、更优方案:不一致的情况,只到冷库中查询未查到的数据。此时R表需要存放的不仅是查询结果数量,还有查询结果的所有主键。

5、举例说明:100条中80条还是热数据 20条变成了冷数据,其实应该只是对冷数据库发起这20条数据的请求。此时需要将R表数据拿出来比对,只查一部分冷数据。

6、热库=>冷库 : 查询和使用热数据时,将一段时间不再使用的热数据移到冷库。

7、冷库=>热库 :查询冷库时,将本次查询的结果移到热库,附上最新查询日期。

8、数据同步(每次查询进行或达到一定量级进行)

9.针对单条数据的查询,单条数据的查询一般发生在刚刚下单后,所以优先查询生产库,生产库没有,再去查询历史库。

10.针对某一时间段内,多条数据查询list,这里我们可以预先定义一个分割线,这个分割线是一个日期,这个日期就是生产库最早一条数据的日期,有了这个分割线,那我们只需要拿要查询的日期区间和这个分割线做比较,即可确定

11.针对多个分散订单的查询list,理论上没有任何规律,但是由于历史数据发现,这种情况一般有数量不多,数据多在近期的特征,所以还是优先查询生产库,查不到再查询历史库

如何做主从分离

MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。

数据库字段设计需要考虑哪些因素
  1. 字段的类型和以后可能会出现的最大长度
  2. 字段是否具有唯一性
  3. 哪些字段常用哪些不常用,是否需要进行分表
  4. 尽量把所有列定义为not null
  5. 使用comment从句添加表和列的备注,从一开始就进行数据字典的维护
  6. 禁止在表中建立预留字段(预留字段的命名很难做到见名识意,也无法选择合适的类型)
  7. 禁止在数据库中存储图片,文件等大的二进制文件
  8. 优先使用符合存储需要最小的数据类型
  9. 避免使用text,blob数据类型(建议把blob或text分离到单独的扩展表中)
  10. 避免使用enum类型,可以用tinyint代替
  11. 使用timestamp(4字节)或datetime(8字节)
  12. 金额类数据必须使用decimal类型
索引设计需要考虑哪些因素
  1. 每张表索引不超过5个

  2. 禁止给表中的每列都建立单独的索引

  3. 每个innodb表都必须有主键

  4. 复合索引顺序必须保持和查询一致

  5. 避免建立冗余索引和重复索引(会增加查询优化器生成执行时间)

  6. 对于频繁的查询优先考虑使用覆盖索引,防止回表查询

  7. 复合索引不要跨列或者无序使用(最佳左前缀)

  8. 不要在索引上进行任何操作(计算,函数,类型转换)

  9. 选择合适的字段建立索引

    不为null的字段,被频繁查询的字段,被作为查询条件的字段,频繁需要排序的字段,频繁用于连接的字段

  10. 频繁更新的字段慎用索引

  11. 尽可能考虑联合索引而不是单列索引

说说你所知道的索引类型

聚集索引:主键索引

辅助索引:普通索引,全文索引,复合索引,唯一索引

索引不生效有哪些原因
  • 复合索引,不要跨列或无序使用(最佳左前缀)

  • 复合索引,尽量使用全索引匹配(有abc三个复合索引,最好三个都用上)

  • 不要在索引上进行任何操作(计算,函数,类型转换),否则索引失效(对于复合索引,如果左边失效,右侧则全部失效,不能断)

  • 复合索引不能使用不等于 != <> 或is null (is not null),否则自身或者右侧所有全部失效

  • 尽量使用索引覆盖(using index)

  • like尽量以“常量开头”,不要以“%”开头,否则索引失效

    select * from xx where name like “%x%” –name索引失效

    select * from xx where name like “x%” --正常

    select * from xx where name like “%x%” –如果必须使用like进行模糊查询,可以使用索引覆盖挽救一部分

  • 尽量不要使用类型转换(显示/隐式),否则索引失效

    select * from xx where name = 123 // 程序底层将123转换为“123”,即进行了类型转换

  • 尽量不要使用or,否则索引失效(左侧和右侧都失效)

如果不走索引优化一个查询特别慢的sql你会怎么优化

分库分表,冷热分离,读写分离

什么是回表

所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。

二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后,再查询到想要的数据,这种通过二级索引查询出来的过程,就叫做回表

不是出现using where就是回表,using where只是过滤
如果出现using index condition就是二级索引回表

什么是索引下推

索引条件下推优化(Index Condition Pushdown (ICP) )是MySQL5.6添加的,用于优化数据查询

不使用索引条件下推优化时存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件。
当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数

不使用索引条件下推优化时的查询过程

获取下一行,首先读取索引信息,然后根据索引将整行数据读取出来。
然后通过where条件判断当前数据是否符合条件,符合返回数据。

使用索引条件下推优化时的查询过程

获取下一行的索引信息。
检查索引中存储的列信息是否符合索引条件,如果符合将整行数据读取出来,如果不符合跳过读取下一行。
用剩余的判断条件,判断此行数据是否符合要求,符合要求返回数据。
什么是索引覆盖

1.什么是覆盖索引?

1)只需要在一棵索引树上就可以获取sql所需所有的列数据,不需要回表,较之回表速度要更快。

2)explain输出结果extra字段为Using index时,触发了索引覆盖。

2.如何实现覆盖索引?

办法:将被查询的字段建立到联合索引中

什么是慢查询日志

慢查询日志默认是关闭的:建议,开发调优时打开,而最终部署时关闭

检查是否开启了慢查询日志:show variables like “%slow_query_log%”

临时开启:set global slow_query_log = 1 //在内存中开启

永久开启:etc/my.cnf 中追加配置:slow_query_log =1 slow_query_log_file=/var/lib/mysql/localhost-slow.log

慢查询阀值:show variables like “%long_query_time%”

临时开启:set global long_query_time = 5 // 设置完毕后,重新登陆后才生效

永久开启:etc/my.cnf 中追加配置:long_query_time =3

  • 慢查询的sql被记录到了日志中,可以通过日志查看具体的慢sql
  • 通过mysqldumpslow 工具查看慢sql
@Transactional的类型

一共有7种:(默认:REQUIRED)

REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);

1.REQUIRED

如果方法在运行的时候存在事务,已经存在了一个事务,那么会加入到该事务中,否则的话自己创建一个新的事务。(spring的默认传播行为就是REQUIRED)

2.SUPPORTS

如果有事务就在事务的环境下执行,否则反之不在事务的环境下执行

3.MANDATORY

当前的方法必须在事务中运行,否则的话就抛出异常

4.REQUIRES_NEW

当前的方法必须在自己的事务内运行,如果有其他的事务就将它挂起

5.NOT_SUPPORTED

当前的方法不会在事务中运行,如果有事务就会将它挂起。

6.NEVER

当前的方法如果在事务中运行,则抛出异常

7.NESTED

如果存在事务运行,那么这个方法就是在这个事务的嵌套事务内运行,嵌套事务是独立提交或回滚;

事务失效的情况

  1. 方法不是public
  2. 异常类型不是unchecked
  3. 数据库引擎要支持事务,比如mysql的innodb
  4. spring能否扫描到包
  5. 在同一个类不要使用this.XX(方法),因为这个this并不是spring用cglib增强的类,没有被代理,那就没有事务
B树和B+树

B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。

目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

B 树& B+树两者有何异同呢?

  • B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。

  • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。

  • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。

多线程

线程的创建方式
  1. 继承Thread类
  2. 实现Runnable接口
  3. Callable回调接口,futureTask获取回调值
  4. 线程池
线程的状态

创建,就绪,阻塞,运行,死亡

什么是线程礼让
  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看cpu心情
为什么要用线程池

背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具
好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止
    • newFixedThreadPool 参数位:线程池大小
线程常用辅助类有哪些

CountDownLatch 计数器

count.countDown()// 数量-1

count.await(); // 等待计数器归零,然后再向下执行

CyclicBarrier

CyclicBarrier cyclic=new CyclicBarrier(7,()->{
    System.out.println("唤醒成功");
});
cyclic.await();

Semaphore 信号量

sem.acquire(); 获得,假设如果已经满了,等待,直到被释放为止

sem.release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数
线程池的三大方法,七大参数,四种解决策略

三大方法

// 单个线程
ExecutorService threadPool=Executors.newSingleThreadExecutor();
// 固定线程池的大小
ExecutorService threadPool= Executors.newFixedThreadPool(3);
// 可伸缩的
ExecutorService threadPool= Executors.newCachedThreadPool();

七大参数

public ThreadPoolExecutor(
    int corePoolSize, // 核心线程池大小
    int maximumPoolSize, // 最大的线程池大小
    long keepAliveTime, // 存活时间
    TimeUnit unit, // 时间单位
    BlockingQueue<Runnable> workQueue, // 阻塞队列
    ThreadFactory threadFactory, // 线程工厂 创建线程的,一般不动
    RejectedExecutionHandler handler // 拒绝策略
) {}

img

四种拒绝策略

// 四种拒绝策略
new ThreadPoolExecutor.AbortPolicy() 不处理,直接抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() 哪来的去哪里
new ThreadPoolExecutor.DiscardPolicy()   队列满了不会抛出异常,会丢掉任务
new ThreadPoolExecutor.DiscardOldestPolicy()  队列满了,尝试去和最早的竞争,也不会抛出异常
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值