java 面试题1

hashCode()和equals()

两者区别
  • equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
  • hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
为什么equals()相等,hashCode就一定要相等,而hashCode相等,却不要求equals相等?

​ 之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equals为false。

为什么需要hashCode?
  • 通过hashCode可以很快的查到小内存块。
  • hashMap、hashset等hash开头的数据结构,都要用到hash,来确定储存位置,理论上一个内存位置的数据应该是相等的,但是hashcode算法可能不是唯一的,就会导致不相同的数据被储存在一个内存位置上。

ArrayList、LinkedList

两者区别
  1. ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。
  2. 对于查找操作,ArrayList要优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作,LinkedList比较占优势,因为ArrayList要移动数据。
  4. LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
使用场景

​ ArrayList使用在查询比较多,但是插入和删除比较少的情况,而LinkedList用在查询比较少而插入删除比较多的情况。

实现原理

​ 略~

HashMap、HashTable、ConcurrentHashMap

HashTable
  1. 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化。
HashMap
  1. 底层数组+链表实现,可以存储null键和null值,线程不安全

  2. HashMap如何保证线程安全?

    1. ConcurrentHashMap

      Map concurrentHashMap = new ConcurrentHashMap<>();
      
    2. 通过Collections.synchronizedMap()返回一个新的map。(要习惯基于接口编程,因为返回的不是hashmap,而是map的实现)

      Map synchronizedHashMap = Collections.synchronizedMap(new HashMap());
      
ConcurrentHashMap
  1. 底层采用分段的数组+链表实现,线程安全
  2. 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  3. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术

Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。

​ 在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用**containsKey()**方法来判断。而在Hashtable中,无论是key还是value都不能为null。

多线程

并发编程三要素?

原子性

​ 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

可见性

​ 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

有序性

​ 有序性,即程序的执行顺序按照代码的先后顺序来执行。

实现可见性的方法有哪些?

​ synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

创建线程的有哪些方式?
  1. 继承Thread类创建线程类
  2. 通过Runnable接口创建线程类
  3. 通过Callable和Future创建线程
  4. 通过线程池创建
创建线程的三种方式的对比?
  1. 采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

  2. 使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

Runnable和Callable的区别

​ Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。 Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。 Call方法可以抛出异常,run方法不可以。 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

什么是线程池?有哪几种创建方式?

​ 线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

​ java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

四种线程池的创建
  1. newCachedThreadPool创建一个可缓存线程池
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
线程池构造方法中的参数
  • corePoolSize:线程池的大小。线程池创建之后不会立即去创建线程,而是等待线程的到来。当当前执行的线程数大于改值是,线程会加入到缓冲队列;
  • maximumPoolSize:线程池中创建的最大线程数;
  • keepAliveTime:空闲的线程多久时间后被销毁。默认情况下,改值在线程数大于corePoolSize时,对超出corePoolSize值得这些线程起作用。
  • unit:TimeUnit枚举类型的值,代表keepAliveTime时间单位,可以取下列值: TimeUnit.DAYS; //天  TimeUnit.HOURS; //小时  TimeUnit.MINUTES; //分钟  TimeUnit.SECONDS; //秒  TimeUnit.MILLISECONDS; //毫秒  TimeUnit.MICROSECONDS; //微妙  TimeUnit.NANOSECONDS; //纳秒
  • workQueue:阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略,有以下取值:  ArrayBlockingQueue;  LinkedBlockingQueue;  SynchronousQueue;  threadFactory:线程工厂,是用来创建线程的。默认new Executors.DefaultThreadFactory();
  • handler:线程拒绝策略。当创建的线程超出maximumPoolSize,且缓冲队列已满时,新任务会拒绝,有以下取值  ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。  ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。  ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)  ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
线程池的优点?
  1. 重用存在的线程,减少对象创建销毁的开销。
  2. 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  3. 提供定时执行、定期执行、单线程、并发数控制等功能。
常用的并发工具类有哪些?

​ CountDownLatch、CyclicBarrier、Semaphore、Exchanger

什么是CAS

​ CAS是compare and swap的缩写,即我们所说的比较交换。

​ cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

​ CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)
ReadWriteLock是什么

​ 首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

​ 因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

什么是乐观锁和悲观锁

​ **乐观锁:**就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

​ **悲观锁:**还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

线程B怎么知道线程A修改了变量
  1. volatile修饰变量
  2. synchronized修饰修改变量的方法
  3. wait/notify
  4. while轮询
ThreadLocal是什么?有什么用?

​ ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

​ 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

多线程同步有哪几种方法?

​ Synchronized关键字,Lock锁实现,分布式锁等。

死锁的原因
  1. 是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

  2. 默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

如果你提交任务时,线程池队列已满,这时会发生什么

这里区分一下:

  1. 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

  2. 如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

Java中用到的线程调度算法是什么

​ 抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

什么是自旋

​ 很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

同步方法和同步块,哪个是更好的选择?

​ 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

Java线程数过多会造成什么异常?
  1. 线程的生命周期开销非常高

  2. 消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

  3. 降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常

JVM

什么情况下会发生栈内存溢出

​ 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。 如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多) 参数 -Xss 去调整JVM栈的大小

详解JVM内存模型

img

​ 程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。

​ Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。

​ Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。

​ Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。

​ 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享

新生代中为什么要分为Eden和Survivor
  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

SQL

SQL优化
  1. 语句优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
  2. 语句优化,用select field_1,field_2,field_3… form table代替select * from table
  3. 语句优化,使用连接(JOIN)代替子查询
  4. 语句优化,使用联合(UNION)代替手动创建的临时表
  5. 建表优化,尽量减少字段宽度,使用ENUM存储固定数据,例如性别
  6. 语句优化,避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  7. 建表优化,字段选用优先级,整形>date,time>enum,char>varchar>blob,text
  8. 语句优化,尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描
  9. 语句优化,尽量使用全文检索代替like查询
  10. 语句优化,Update语句尽量只set需要修改的字段,否则带来没必要的开销
  11. 语句优化,数据量大时(1万以上)尽量避免使用,游标
  12. 持续更新中。。。

Spring

列举一些重要的Spring模块?
  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IOC 依赖注入功能。
  • Spring Aspects : 该模块为与AspectJ的集成提供支持。
  • Spring AOP :提供了面向方面的编程实现。
  • Spring JDBC : Java数据库连接。
  • Spring JMS :Java消息服务。
  • Spring ORM : 用于支持Hibernate等ORM工具。
  • Spring Web : 为创建Web应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。
IOC

​ IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

Spring IOC的初始化过程:

img

AOP

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

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

关于面向切面编程的一些术语

  • 切面(Aspect): 切面用于组织多个Advice,Advice放在切面中定义
  • 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用
  • 增强处理(Advice): AOP框架在特定的切入点执行的增强处理。处理有“around”、“before”和“after”等类型
  • 切入点(Pointcut): 可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点Spring的AOP支持
Spring 中的单例 bean 的线程安全问题了解吗?

​ 大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。

常见的有两种解决办法:

  1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
  2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
什么是ThreadLocal?

​ ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

大致的实现思路:

​ Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

SpringMVC 工作原理了解吗?
  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. View 返回给请求者(浏览器)
Spring 框架中用到了哪些设计模式?
  • 工厂设计模式 : Spring使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller
  • ……
@Component 和 @Bean 的区别是什么?
  1. 作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。

  2. @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的实例,当我需要用它的时候还给我。

  3. @Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。

    @Bean注解使用示例:

    @Configuration
    public class AppConfig {
        @Bean
            public TransferService transferService() {
                return new TransferServiceImpl();
            }
    }
    

    上面的代码相当于下面的 xml 配置

    <beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
    </beans>    
    

    下面这个例子是通过 @Component 无法实现的。

    @Bean
    public OneService getService(status) {
        case (status)  {
            when 1:
            return new serviceImpl1();
            when 2:
            return new serviceImpl2();
            when 3:
            return new serviceImpl3();
        }
    }                                                    
    
将一个类声明为Spring的 bean 的注解有哪些?

​ 我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于拿个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
Spring 管理事务的方式有几种?
  1. 编程式事务,在代码中硬编码。(不推荐使用)
  2. 声明式事务,在配置文件中配置(推荐使用)

声明式事务又分为两种:

  1. 基于XML的声明式事务
  2. 基于注解的声明式事务
Spring 事务中的隔离级别有哪几种?

​ DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别. 未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生 已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生 可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生. 串行化的 (serializable) :避免以上所有读问题. Mysql 默认:可重复读 Oracle 默认:读已提交

Spring 事务中哪几种事务传播行为?
  • 保证同一个事务中 PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认) PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务 PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常
  • 保证没有在同一个事务中 PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务 PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务 PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常 PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行

Spring Boot

什么是springboot
  1. 用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(约定大于配置properties或yml文件)
  2. 创建独立的spring引用程序 main方法运行
  3. 嵌入的servlet容器(Tomcat) 无需部署war文件
  4. 简化maven配置
  5. 自动配置spring添加对应功能starter自动化配置
Spring Boot有哪些优点?
  • 独立运行
  • 简化配置
  • 自动配置
  • 无代码生成和XML配置
  • 应用监控
  • 上手容易
Spring Boot自动配置的原理

​ 在Spring程序main方法中 添加@SpringBootApplication或者@EnableAutoConfiguration。会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean。

​ Spring Boot 在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR,根据spring.factories配置加载AutoConfigure类,根据@Conditional注解的条件,进行自动配置并将Bean注入Spring Context

SpringCloud

什么是 spring cloud?

​ spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。

spring cloud 的核心组件有哪些?
  • Eureka:服务注册于发现。
  • Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。
  • Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。
  • Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。
  • Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。
spring cloud 断路器的作用是什么?

​ 在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延

​ 当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应 当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应) 断路器有完全打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务 半开:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭 关闭:当服务一直处于正常状态 能正常调用

什么是服务熔断?什么是服务降级?

​ 在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩

​ 为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。

​ 所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝"当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。

​ 服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值

什么是Hystrix?

​ 防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控(Hystrix Dashboard) 服务降级 双十一 提示 哎哟喂,被挤爆了。 app秒杀 网络开小差了,请稍后再试。 优先核心服务,非核心服务不可用或弱可用。通过HystrixCommand注解指定。

负载均衡的意义是什么?

​ 在计算中,负载均衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载均衡旨在优化资源使用,最大吞吐量,最小响应时间并避免任何单一资源的过载。使用多个组件进行负载均衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务进程。

springcloud如何实现服务的注册?
  1. 服务发布时,指定对应的服务名,将服务注册到 注册中心(eureka zookeeper)
  2. 注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用ribbon或feign进行服务直接的调用发现。
eureka自我保护机制是什么?

​ 当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

什么是Ribbon?

​ ribbon是一个负载均衡客户端,可以很好的控制htt和tcp的一些行为。feign默认集成了ribbon。

什么是feigin?它的优点是什么?
  1. feign采用的是基于接口的注解
  2. feign整合了ribbon,具有负载均衡的能力
  3. 整合了Hystrix,具有熔断的能力
Ribbon和Feign的区别?
  1. Ribbon都是调用其他服务的,但方式不同。
  2. 启动类注解不同,Ribbon是@RibbonClient feign的是@EnableFeignClients
  3. 服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
  4. 调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。
什么是Spring Cloud Bus?(rabbimq)

​ spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。 如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。

为什么要使用MQ消息中间件?

解耦、异步、削峰

  1. 解耦 传统模式:

img

​ 传统模式的缺点:

​ 系统间耦合性太强,如上图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

中间件模式:

中间件模式的的优点:

将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。

  1. 异步 传统模式:

​ 传统模式的缺点:

​ 一些非必要的业务逻辑以同步的方式运行,太耗费时间。

中间件模式:

img

​ 中间件模式的的优点:

​ 将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

  1. 削峰 传统模式

​ 传统模式的缺点:

​ 并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

中间件模式:

img

​ 中间件模式的的优点:

​ 系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。

使用了消息队列会有什么缺点?

系统可用性降低: 你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。

现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低

系统复杂性增加: 要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。

因此,需要考虑的东西更多,系统复杂性增大。

如何保证消息不被重复消费?

​ 因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

如何解决?这个问题针对业务场景来答分以下几点

  1. 比如,你拿到这个消息做数据库的insert操作。

    ​ 那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

  2. 再比如,你拿到这个消息做redis的set的操作

    ​ 那就容易了,不用解决。因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

  3. 如果上面两种情况还不行,上大招。

    ​ 准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

什么是SpringCloudConfig?

​ 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config 组件中,分两个角色,一是config server,二是config client。

微服务的优缺点分别是什么?说下你在项目开发中碰到的坑

优点

  • 每一个服务足够内聚,代码容易理解
  • 开发效率提高,一个服务只做一件事
  • 微服务能够被小团队单独开发
  • 微服务是松耦合的,是有功能意义的服务
  • 可以用不同的语言开发,面向接口编程
  • 易于与第三方集成
  • 微服务只是业务逻辑的代码,不会和HTML,CSS或者其他界面组合

​ 开发中,两种开发模式 前后端分离 全栈工程师

  • 可以灵活搭配,连接公共库/连接独立库

缺点

  • 分布式系统的负责性
  • 多服务运维难度,随着服务的增加,运维的压力也在增大
  • 系统部署依赖
  • 服务间通信成本
  • 数据一致性
  • 系统集成测试
  • 性能监控
微服务之间是如何独立通讯的
  1. 远程过程调用(Remote Procedure Invocation)

​ 也就是我们常说的服务的注册与发现

​ 直接通过远程过程调用来访问别的service。

优点:

 简单,常见,因为没有中间件代理,系统更简单

缺点:

​ 只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应

​ 降低了可用性,因为客户端和服务端在请求过程中必须都是可用的

  1. 消息

​ 使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。

优点:

​ 把客户端和服务端解耦,更松耦合

​ 提高可用性,因为消息中间件缓存了消息,直到消费者可以消费

​ 支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应

缺点:

​ 消息中间件有额外的复杂

Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别
  1. ZooKeeper保证的是CP,Eureka保证的是AP

    ZooKeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的

    Eureka各个节点是平等关系,只要有一台Eureka就可以保证服务可用,而查询到的数据并不是最新的

    自我保护机制会导致

    Eureka不再从注册列表移除因长时间没收到心跳而应该过期的服务

    Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点(高可用)

    当网络稳定时,当前实例新的注册信息会被同步到其他节点中(最终一致性)

    Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper一样使得整个注册系统瘫痪

  2. ZooKeeper有Leader和Follower角色,Eureka各个节点平等

  3. ZooKeeper采用过半数存活原则,Eureka采用自我保护机制解决分区问题

  4. Eureka本质上是一个工程,而ZooKeeper只是一个进程

SpringBoot和SpringCloud

​ SpringBoot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务 而SpringCloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等 技术维度并相同,并且SpringCloud是依赖于SpringBoot的,而SpringBoot并不是依赖与SpringCloud,甚至还可以和Dubbo进行优秀的整合开发

总结:

  • SpringBoot专注于快速方便的开发单个个体的微服务
  • SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务
  • SpringBoot不依赖于SpringCloud,SpringCloud依赖于SpringBoot,属于依赖关系
  • SpringBoot专注于快速,方便的开发单个的微服务个体,SpringCloud关注全局的服务治理框架
微服务架构?

​ 在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、断路器、智能路由、配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统

​ 在Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(zuul、Ngnix),再到达服务网关(zuul集群),然后再到具体的服。,服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理,配置服务的配置文件放在git仓库,方便开发人员随时改配置。

Redis

Redis支持的数据类型?

String字符串

​ 格式: set key value

​ string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。

​ string类型是Redis最基本的数据类型,一个键最大能存储512MB。

Hash(哈希)

​ 格式: hmset name key1 value1 key2 value2

​ Redis hash 是一个键值(key=>value)对集合。

​ Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

List(列表)

​ Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

​ 格式: lpush name value

​ 在 key 对应 list 的头部添加字符串元素

​ 格式: rpush name value

​ 在 key 对应 list 的尾部添加字符串元素

​ 格式: lrem name index

​ key 对应 list 中删除 count 个和 value 相同的元素

​ 格式: llen name

​ 返回 key 对应 list 的长度

Set(集合)

​ 格式: sadd name value

​ Redis的Set是string类型的无序集合。

​ 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

zset(sorted set:有序集合)

​ 格式: zadd name score value

​ Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

​ 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

​ zset的成员是唯一的,但分数(score)却可以重复。

什么是Redis持久化?Redis有哪几种持久化方式?优缺点是什么?

​ 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

​ Redis 提供了两种持久化方式:RDB(默认) 和AOF

RDB:

​ rdb是Redis DataBase缩写

​ 功能核心函数rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数

img

AOF:

​ Aof是Append-only file缩写

img

​ 每当执行服务器(定时)任务或者函数时flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作

aof写入保存:

​ WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件

​ SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中

两者比较

  1. aof文件比rdb更新频率高,优先使用aof还原数据。
  2. aof比rdb更安全也更大
  3. rdb性能比aof好
  4. 如果两个都配了优先加载AOF
Redis 有哪些架构模式?讲讲各自的特点

单机版

img

特点:简单

问题:

  1. 内存容量有限
  2. 处理能力有限
  3. 无法高可用

主从复制

img

​ Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。 只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。

特点:

  1. master/slave 角色
  2. master/slave 数据相同
  3. 降低 master 读压力在转交从库

问题:

​ 无法保证高可用

​ 没有解决 master 写的压力

哨兵

img

​ Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:

​ 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。

​ 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

​ 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。

特点:

  1. 保证高可用

  2. 监控各个节点

  3. 自动故障迁移

    缺点:

    主从模式,切换需要时间丢数据

    没有解决 master 写的压力

集群(proxy 型):

img

​ Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。

特点:

  1. 多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
  2. 支持失败节点自动删除
  3. 后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致

缺点:

​ 增加了新的 proxy,需要维护其高可用。

​ failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预

集群(直连型)

img

​ 从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

特点:

  1. 无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
  2. 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  3. 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
  4. 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
  5. 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。

缺点:

  1. 资源隔离性较差,容易出现相互影响的情况。
  2. 数据通过异步复制,不保证数据的强一致性
Redis常用命令?
  • Keys pattern

    *表示区配所有

    以bit开头的

    查看Exists key是否存在

  • Set

    设置 key 对应的值为 string 类型的 value。

  • setnx

    设置 key 对应的值为 string 类型的 value。如果 key 已经存在,返回 0,nx 是 not exist 的意思。

    删除某个key

    第一次返回1 删除了 第二次返回0

    Expire 设置过期时间(单位秒)

  • Setex

    设置 key 对应的值为 string 类型的 value,并指定此键值对应的有效期。

  • Mset

    一次设置多个 key 的值,成功返回 ok 表示所有的值都设置了,失败返回 0 表示没有任何值被设置。

  • Getset

    设置 key 的值,并返回 key 的旧值。

  • Mget

    一次获取多个 key 的值,如果对应 key 不存在,则对应返回 nil。

  • Incr

    对 key 的值做加加操作,并返回新的值。注意 incr 一个不是 int 的 value 会返回错误,incr 一个不存在的 key,则设置 key 为 1

  • incrby

    同 incr 类似,加指定值 ,key 不存在时候会设置 key,并认为原来的 value 是 0

  • Decr

    对 key 的值做的是减减操作,decr 一个不存在 key,则设置 key 为-1

  • Decrby

    同 decr,减指定值。

  • Append

    给指定 key 的字符串值追加 value,返回新字符串值的长度。

  • Strlen

    取指定 key 的 value 值的长度。

  • persist xxx(取消过期时间)

    选择数据库(0-15库)

  • Select 0 //选择数据库

  • move age 1//把age 移动到1库

  • Randomkey随机返回一个key

  • Rename重命名

  • Type 返回数据类型

使用过Redis分布式锁么,它是怎么实现的?

​ 先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

​ set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的

使用过Redis做异步队列么,你是怎么用的?有什么缺点?

​ 一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

缺点:

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?

缓存穿透

​ 一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

如何避免?

  1. 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
  2. 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

缓存雪崩

​ 当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

如何避免?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  2. 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
  3. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值