谈谈对spring的理解:

Spring是一个开源框架,为简化企业级应用开发而生。Spring可以使简单的JavaBean实现以前只有EJB才能实现的功能。Spring是一个IOC和AOP容器框架。Spring容器的主要核心是:
(回答下面的)


Spring的ioc、di、aop分别是什么,ioc和di有什么关系

  1. IOC(控制翻转)和AOP(面向切面编程) DI(依赖注入) ;
  2. IOC控制反转,简单来说就是将原本是由使用者来控制对象的创建的,变成了由Spring容器来进行控制对象的创建权限和时机,这种控制权由内部交给外部的思想,就是控制反转;这种思想有利于使对象与对象之间松散耦合;也有利于功能的复用,当我们需要使用对象的时候,直接在Spring容器中获取即可;
  3. DI依赖注入,将对应的属性注入到对应的对象中,通过这几种方式来注入:@Autowired,@Resource,populateBean方法来完成依赖的注入;
  4. AOP:一般我们称为:面向切面编程,作为面向对象编程的一种补充;将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可用的模块,这个模块则被称为切面;(后面还可以讲AOP用到的动态代理);
  5. IOC和DI的关系:

spring中@Autowired和@Resource的区别:

1、@AutoWired:是spring中提供的注解,@Resource:是jdk中定义的注解,依靠的是java的标准

2、@AutoWired默认是按照类型进行装配(如果类型找不到,就按照名称ID。),默认情况下要求依赖的对象必须存在。@Resource默认是按照名字进行匹配的(先按名称ID,再按类型。如果名字找不到就按照类型找。),同时可以指定name属性。

3、@AutoWired只适合spring框架,而@Resource可以在其他的框架中也可以用,所以@Resource扩展性更好


spring bean的生命周期:

image.png

  1. 实例化对象,通过反射的方式来生成,在源码中会有一个createBeanInstance的方法来专门生成对象;
  2. 当Bean对象创建完成后,对象的属性值都是默认值,所有现在要对Bean的属性进行填充,通过populateBean方法来对Bean对象的属性来进行填充,中间会涉及到循环依赖的问题;
  3. 向Bean对象设置容器属性,会调用invokeAwareMethods方法来将具体的容器对象设置到具体的Bean对象中;
  4. 调用BeanPostProcessor中的前置(Before)处理方法来对Bean对象进行前置处理工作,如ApplicationContext对象处理上下文属性数据;
  5. 调用invokeInitMethods方法来完成初始化方法的调用,在此方法调用过程中,需要判断当前bean对象是否实现了InitializingBean接口,如果实现了就调用aftePropertiesSet方法来最后设置Bean对象;
  6. 调用BeanPostProcessor的后置(after)处理方法,完成对Bean对象的后置处理工作;(aop就是在这里实现的,实现的接口名称叫:AbstractAutoProxyCreator;)
  7. 获取到完整的Bean对象,通过getBean方法就能进行对bean的获取与使用;
  8. 当对象使用完成后,在容器关闭的时候,会销毁Bean对象,首先会判断是否实现了DisposableBean接口,然后再去执行destory方法完成销毁;
  9. (注意)补充一下:只有单例模式下创建bean对象时销毁方法才会伴随容器的销毁而执行,如果是多例模式下创建Bean的话就相反,Spring会根据上下文的内容进行校验该bean在后续是否还会被引用,如果没有的话,会直接销毁,释放内存;目的呢就是优化性能;

springcloud常用组件:

注册中心(nacos),Gateway服务网关,配置中心(nacos),Feign远程服务调用,消息队列(rabbitmq,kafka,emq),Ribbon负载均衡,服务保护组件sentinel(熔断降级),seata分布式事务;


springboot的常用注解:

注解: 作用:
@SpringBootApplication (类注解) 它封装了核心的@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan这三个类,大大节省了程序员配置时间,这就是SpringBoot的核心设计思想.
@EnableScheduling(类注解) 是通过@Import将Spring调度框架相关的bean定义都加载到IoC容器
@MapperScan (类注解) 支持mybatis组件的一个注解,通过此注解指定mybatis接口类的路径,即可完成对mybatis接口的扫描
@EnableAutoConfiguration 帮助SpringBoot程序将所有符合条件的@Configuration配置都加载到当前SpringBoot,并创建对应配置类的Bean,并把该Bean实体交给IoC容器进行管理
@RestController(类注解) @Controller @ResponseBody的结合,一个类被加上@RestController注解,数据接口中就不再需要添加@ResponseBody,更加简洁。
@RequestMapping 需要明确请求的路径
@GetMappping,@PostMapping, @PutMapping, @DeleteMapping 结合@RequestMapping使用, 是Rest风格的, 指定更明确的子路径.
@PathVariable 路径变量注解,用{}来定义url部分的变量名.
@Service (类注解) 这个注解用来标记业务层的组件,我们会将业务逻辑处理的类都会加上这个注解交给spring容器。事务的切面也会配置在这一层。当让 这个注解不是一定要用。有个泛指组件的注解,当我们不能确定具体作用的时候 可以用泛指组件的注解托付给spring容器(切记:在Service实现类上一定要加上这个)
@Component(类注解) 和spring的注解功能一样, 注入到IOC容器中.
@ControllerAdvice(类注解) 和 @ExceptionHandler 配合完成统一异常拦截处理.
@Mapper (类注解) 添加了@Mapper注解之后这个接口在编译时交给SpringBoot自动生成相应的代理对象;

springmvc和springboot的关系:

1、Spring
Spring是一个开源容器框架,可以接管web层、业务层、dao层、持久层的组件,并可以配置各种bean,且能维护bean与bean之间的关系。其核心就是控制反转(IOC)和面向切面编程(AOP),简单的说就是一个分层的轻量级开源框架。
2、SpringMVC
Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。SpringMVC是一种web层mvc框架,用于替代servlet(处理|响应请求,获取表单参数,表单校验等)。SpringMVC是一个MVC的开源框架,SpringMVC=struts2+spring,即springMVC相当于是Struts2加上Spring的整合。
3、SpringBoot
Springboot是一个微服务框架,延续了spring框架的核心思想IOC和AOP,简化了应用的开发和部署。Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。它提供了一堆依赖打包,并已经按照使用习惯解决了依赖问题。


SpringMVC的执行流程:

image.png
服务器启动就会创建mvc的上下文域(子容器)然后把spring容器的东西也获取返回上下文域中:

  1. 用户发送请求到前端控制器(DispatcherServlet),前端控制器接收并拦截请求;
  2. HandlerMapping处理器映射器会被前端控制器调用,处理器映射器就会根据url找到对应的Handler处理器;
  3. 返回处理器执行链,根据url由处理器适配器查找控制器controller,并且将解析后的数据信息返回到前端控制器;
  4. HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler;
  5. 执行Handler查找到的具体处理器;
  6. 控制器controller将具体的执行信息返回给处理器适配器,如:视图与模型(Model and View);
  7. 处理器适配器将视图逻辑名或者模型返回给前端控制器;
  8. 前端控制器会调用视图解析器来对处理器适配器传来的视图逻辑名进行解析;
  9. 视图解析器处理完毕后会将解析好的视图View对象返回;
  10. 前端控制器会根据视图解析器解析视图的结果,调用具体的视图,进行视图渲染;
  11. 完成渲染后将响应数据返回用户;

什么是事务:

(1) 事务的定义
事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元原子性


事务的四大特性和隔离级别:

1. 原子性(Atomicity)
事务包含的所有数据库操作要么全部成功,要不全部失败回滚。
2. 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。一致性规定事务提交前后只存在两个状态,提交前的状态和提交后的状态,绝对不会出现中间的状态。最典型的例子就是银行转账,A和B之间互相转账,账面加起来总和5000元,无论A和B之间怎么转,转几次,成功与否,事务结束后A和B账面总和还是5000元。
3. 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
4. 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

四个隔离级别
1. 读未提交(Read uncommitted)
所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。因为采用这种隔离级别只能防止更新丢失问题(这个问题现代关系型数据库已经不会发生),不能解决脏读不可重复读幻读问题。
2. 读已提交(Read committed)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别可以防止脏读问题,但会出现不可重复读幻读问题。
3. 可重复读(Repeatable read)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这种隔离级别可以防止除幻读外的其他问题。
4. 可串行化(Serializable )
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读、第二类更新丢失问题。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常数据库不会用这个隔离级别,我们需要其他的机制来解决这些问题:乐观锁和悲观锁。


悲观锁和乐观锁的区别和应用场景:

  1. 悲观锁(写):每次获取数据的时候,都担心数据被修改,因此,在每次获取数据的时候都会进行加锁,确保别人在使用的时候不会被别人修改;获取数据完毕后进行数据解锁;在此期间其他线程都会等待;
  2. 乐观锁(读):每次获取数据的时候,不担心数据被修改,因此,没有加锁,但是在获取数据前要先进行判断数据是否被修改过,如果数据被其他线程修改,那么则不会进行数据更新,没有则更新,期间数据可以被其他线程操作;如:数据库设置一个版本号 version,在每次更新数据前先对该版本号进行校验,判断当前数据是否被修改;
  3. 应用场景
    1. 悲观锁:适合写入操作比较频繁的场景如果出现大量的读取操作,每次读取的时候都会进行加锁,会增加锁的开销,降低了性能,而且具有强一致性;
    2. 乐观锁:适合读取操作比较频繁的场景如果出现大量的写入操作,可能会出现数据冲突,不能保证数据一致性。而为了保证数据的一致性,上层应用需要不断重获数据,会大大增加查询操作,降低性能

redis的数据类型,持久化方式:

  1. 字符串String;
  2. 哈希Hash;
  3. List集合;
  4. Set集合;
  5. 有序集合Zset;(可充当消息延迟队列)在zadd方法中,参数可以设置延迟时间

1) RDB和AOF
2) RDB原理是对整个当前内存数据进行快照备份,体积小;AOF原理是每条操作指令都会持久化到文件,导致文件体积比较大.RDB的两次备份时间(默认时间)间隔最短1min,时间长,容易导致数据丢失;而AOF默认间隔时间是1s,时间短,数据完整性高!恢复速度上来说,RDB要比AOF速度快些,因为体积小.
RDB:内存数据,体积小,1min,备份时间长,数据容易丢失; AOF:每条操作指令,体积大,1s,数据完整性好;
RDB要比AOF快,因为体积小;


redis缓存穿透、雪崩、击穿:

  • redis的缓存穿透缓存穿透指当用户在查询一条数据的时候,缓存和数据库都没有这条数据记录;在缓存中找不到的时候,就会向数据库发送请求查询数据,在用户拿不到数据就会一直发送请求,查询数据库;这样的话就会给数据库造成很大的压力;

    • 解决方案

      1. 方案一:设置缓存空对象,如果当前数据库不存在当前数据,在redis中会将它设置为一个空对象,返回用户。
      2. 方案二:使用布隆过滤器;先将需要缓存的所有key,放入布隆过滤器,当一个查询请求过来时,现金过布隆过滤器查询,如果判断该key存在,则继续查询(访问缓存或数据库);如果判断该key不存在,则直接丢弃.我大概了解布隆过滤器的原理是这样,先将所有key利用特定算法存储到一个二进制变量中,当请求某个key时,利用n个哈希函数计算得到该key的n个位置值,一但位置值包含0,代表该key必须不存在.如果所有位置值为1,代表该key大概率存在(布隆过滤器有一定的误判率)

        1. 简单说说,布隆过滤器相当于在查询缓存之前先在集合中查询key是否存在,如果不存在就直接返回
  • 缓存雪崩:缓存雪崩就是指在某个时间段内,缓存集中过期失效,导致大量的请求打到数据库,数据库的调用量暴增,数据库压力过大导致宕机;

    • 解决方案
      1. redis高可用;设置多几台redis实例或者几个redis集群;
      2. 限流降级;在缓存失效后,通过加锁或者队列来控制读取数据库写缓存的线程数量,对某个key只允许一个线程查询数据库和写缓存;
      3. 数据预热:就是在正式部署前,先要把可能的数据先预先访问一遍,这样部分可能大量访问的数据先加载到缓存中,在即将高并发访问前手动触发加载缓存不同的key;避免大量的请求找不到而打到数据库中造成压力过大即可;
      4. 不同的过期时间:设置不同的过期时间,让缓存失效的时间尽可能均匀;
      5. 缓存永不过期;设置缓存永不过期,简单粗暴;
  • 缓存击穿:就是在短时间内,大量的访问请求直接穿透缓存,请求数据库,瞬间导致数据库访问压力过大,可能导致数据库宕机的风险
    • 造成的原因
      • 一个冷门key,突然被大量用户访问;
      • 一个热门key,在缓存中的时间刚好过期,这时有大量的用户进行访问;
    • 解决方案:
      • 加锁:对于key过期的时候,当key要查询数据库的时候加上一把锁,这时只能让第一个请求进行查询数据库,然后把从数据库中查询到的值存储到缓存中,而第二个请求会进行等待,直到第一个请求查询完毕后将锁释放;对于剩下的相同的key,可以直接从缓存中获取即可。
        • 在单机环境下,使用java常用的锁即可;
        • 在分布式环境下,可以使用分布式锁;如:数据库,redis的分布式锁等;
      • 设置热点数据永不过期;
      • 采用多级缓存,并且设置缓存过期时间尽可能平均;

/java的基本数据类型

Arraylist和Linkedlist的区别:

  1. 1. **ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。**
  2. 1. 对于随机访问(查询元素)ArrayList要优于LinkedList。(ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。) ArrayList的查询性能要优于LinkedList
  3. 1. 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。 LinkedList的插入和删除要优于ArrayList
  4. 1. **LinkedListArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。**

hashtable和hashmap的区别:

线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

/jvm内存溢出:

内存溢出:就是常见的OOM(out of memeory);


jvm调优:

我在开发中一般很少需要进行JVM调优,但我了解过一点JVM调优的参数:

OOM(Out of Memeory 内存溢出)

1)设定堆内存大小(比较常用的)
-Xmx:堆内存最大限制。-Xms:内存大小;最大内存值要尽量等于内存大小;因为,在java虚拟机中,每当jvm的垃圾回收器每次对垃圾缓存进行回收的时候,会重新去计算堆内存的大小来进行分配,如果当内存值等于最大内存值的时候,就不需要去重新计算,就能节省了伸缩堆内存大小的算力,保证性能;

2)设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

3)设定垃圾回收器算法
年轻代用 -XX:+UseParNewGC
年老代用-XX:+UseConcMarkSweepGC


创建线程的方式:

  1. - 创建线程有三种方式,分别是**继承Thread类、实现Runnable接口、实现Callable接口**。
  2. 1. **继承Thread类:**通过继承Thread类来创建线程的步骤如下 - 定义Thread类的子类,并重写该类的**run()方法,**该方法将作为线程执行体。 - 创建Thread子类的实例,即创建了线程对象。 - 调用线程对象的**start()方法**来启动该线程。
  3. 1. **实现Runnable接口:**通过实现Runnable接口来创建线程的步骤如下 - 定义Runnable接口的实现类,并实现该接口的run()方法,该方法将作为线程执行体。 - 创建Runnable实现类的实例,并**将其作为参数来创建Thread对象**,Thread对象为线程对象。 - 调用线程对象的start()方法来启动该线程。
  4. 1. **实现Callable接口:**通过实现Callable接口来创建线程的步骤如下 - 定义Callable接口的实现类,并实现call()方法,该方法将作为线程执行体。- 创建Callable实现类的实例,并以该实例作为参数,创建FutureTask对象。 - 使用FutureTask对象作为参数,创建Thread对象,然后启动线程。 - 调用FutureTask对象的get()方法,获得子线程执行结束后的返回值。 归纳起来,创建线程的方式实际只有两种:继承父类和实现接口。
  5. - **而使用Runnable接口和Callable接口的方式,区别在于前者不能获得线程执行结束的返回值,后者可以获得线程执行结束的返回值。**

线程池有几种 :

  • 如果是Java提供的话就是5种:
  • Java通过Executors提供四种线程池,分别为:
    • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

mysql的引擎有几种 :

MySQL表引擎常用:
1)InnoDB
2)MyISAM

MySQL表引擎的区别:
1)InnoDB支持事务,InnoDB支持表级和行级锁,InnoDB是采用聚簇索引
2)MyISAM不支持事务,MyISAM只支持表级锁,不支持行级锁,MyISAM采用非聚簇索引
InnoDB采用聚集(聚簇)索引存储,索引和数据存储一个文件中,默认使用表的主键列值来构建B+树(表没定义主键也会包含隐式主键
.frm
MyISAM采用非聚集(非聚簇)索引存储,索引和数据文件是分两个文件存储的可以没有主键索引
.myi .myk


/项目中有没有设计表,都有哪些字段


sql优化

1)SQL语句优化,如:需要什么查询什么避免用 * , 尽量比较时用于>= <= 尽可能用exits not exits,少用in not in like的百分号不要放左边
2)需要考虑对查询频繁的字段建立索引或联合(复合)索引。
但是要避免写SQL语句时索引失效的情况,如何得知SQL是否执行索引?使用explain查询执行计划
常见的可能导致索引失效的情况:
使用or,in like查询%放在左边
如在使用联合索引,遵守最左前缀原则
最左前缀原则是这样: (name,age,sex)
where name = jack ok
where name = jack and age>=10 ok
where age >10 and sex = “男” 不ok

3)如果说单表数据太多(过千万),可以采用水平分库分表(采用ShardingJdbc技术);
4)如果业务数据用于复杂的查询场景,把MySQL数据导入到Elasticsearch中,进行全文检索,从而效率更高。


eureka和nacos的区别:

相同点
1)两者支持服务注册服务拉取
2)两者都支持服务者心跳机制实现健康检测(续约机制)

不同点
1)Nacos可以实现服务注册发现,也可以做配置管理;Eureka只能做服务注册发现。
2)Nacos临时实例心跳不正常会被剔除,非临时实例(永久实例)则不会被剔除;而Eureka只能注册临时实例,实例失效会被剔除(Eureka不支持永久实例)
spring.cloud.nacos.discovery.ephemeral=false 创建永久实例
3)Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式;而Eureka只有心跳模式;
4)Nacos支持服务列表变更消息主动通知模式,服务列表更新更及时减少服务调用失败的机率;而Eureka采用被动定时服务列表拉取更新


/项目中用到什么设计模式,单例有几种:


mybatis的$和#:

他们两者的最大区别是:

  • #:传入的参数在sql显示为字符串;$:传入的参数在sql中直接显示传入的值;
  • #:底层是preparedstatement,在传入参数后会先对参数拼接为字符串后再执行查询;不会出现sql注入的问题;
  • $:底层是statement,会直接在sql中显示参数值直接进行查询,会有sql注入的风险;
  • 在考虑安全性的前提下,我们要用#;一般是能用#号就用#号,除非是特殊情况下要用到$,如排序的时候order by需要动态参数的时候需要用到$;

为什么要用es,es的倒排索引是什么:

为什么要使用es?
因为elasticSearch采用的倒排索引进行搜索,效率极高,而且在这个引擎中拥有丰富的针对复杂搜索场景的api,更加适合在海量数据场景下的搜索处理;
倒排索引的原理:
image.png
1)首先,Elasticsearch将文档数据进行索引构建。将文档数据需要分词的字段内容使用分词器进行分词,并记录每个词条和原文档的出现位置和出现频率等信息,构建出文档的索引库
2)然后,用户搜索时,可以对关键词进行分词,使用分词后的词条来匹配索引库,在索引库匹配到记录后,通过文档位置频率信息,反查具体的文档数据。


rocketmq、kafka的了解:

我没有用过rocketmq,但是用过rabbitmq;
RabbitMQ:
优势:
1)支持语言非常广
2)稳定性很好,采用Erlang语言开发
3)吞吐量不算低,万级
4)RabbitMQ官方提供7种消息发送模式,开发者轻松选择合适的模式进行开发即可
缺点:
1)采用Erlang,太小众,研究源码很难

Kafka:
优势:
1)高吞吐量,百万级
2)稳定性好,采用zookeeper进行注册(Zookeep采用CP模式,高一致模式)
3)可以应用在大数据数据处理领域(KafkaStream)
缺点:
1)支持的开发语言比较少
2)耦合zk,依赖zookeeper进行注册