八股小知识
八股小知识
Springboot
过滤器和拦截器的区别?
- 过滤器依赖于servlet容器,它的拦截范围更广,可以拦截所有的请求和静态资源访问,它是在请求进入servlet容器之后,进入servlet之前进行拦截。需要实现javax.servlet.filter接口,并重写init,doFilter,doDestory方法,可以访问servletRequest和responsse对象,但不能访问spring容器的bean。
- 拦截器依赖于Spring,只能拦截springMvc控制器的请求,无法拦截静态资源访问。它是在请求进入控制器之前,控制器处理请求之后以及视图渲染之前处理。需要实现handlerInterceptor接口,并重写perHandler,postHandler和aftercondition方法,可以访问servletRequest,response对象以及Spring容器的bean
Spring有哪些设计模式?
我了解的有工厂模式,单例模式和代理模式
- 工厂模式:常见的工厂模式主要就是BeanFactory(延迟注入)和ApplicationContext(完全注入),我们无需知道类如何创建,直接从工厂中获取即可
- 单例模式:ioc默认的就是单例模式,每次获取相同类时,获取的对象都是同一个。单例模式还分需要时创建和自动创建两种
- 代理模式:aop就是采用代理模式来实现的,当要代理的对象实现接口时,就会通过JDK Proxy来生成代理对象。如果代理的对象没有实现接口,就会使用CGLIb生成一个被代理对象的子类作为代理对象
什么是AOP
AOP是面向切面编程,它将一些与业务无关但是复用性比较高的代码抽离出来,降低代码耦合度
Spring中的事务是怎么实现的?
本质是通过AOP实现的,通过环绕通知的方式进行前后拦截,在执行方法前开启事务,在执行后提交业务,会对此过程进行try/catch,如果报错直接回滚
Spring中事务失效有哪些场景?
- try/catch捕获异常后,并没有手动抛出异常就会导致事务失效。解决办法:在catch中手动抛出异常
- 抛出检查时异常,Spring的事务只会对runtime异常进行回滚,比如not found exception就是一个检查异常。解决方法:在@transacional中添加属性rollbackFor=Exception.class,对所有异常进行回滚
- 非public方法,解决方法:将方法改为publc
- 在非事务方法中调用事务方法
@Transactional的原理有了解过吗
事务注解是基于AOP来实现的,通过环绕通知,在执行方法前开启事务,执行后提交事务,如果try/catch捕获异常,则回滚
什么是SpringTask
SpringTask是Spring框架提供的一个任务调度工具,它可以在约定时间自动实行一段程序
使用时,只需要在启动类添加@EnableScheduling注解,然后通过@Scheduled注解写定时方法即可
什么时SpringCache
SpringCache是Spring提供的一种缓存的解决方案,基于AOP原理,实现了基于注解的缓存功能,对代码的入侵性更小
它提供了@EnableCaching,@Cachable,@CacheEvict,@CachePut等注解实现了开启SpringCache,查询缓存,修改缓存,更新缓存功能
数据结构
HashMap
HashMap底层是哈希表,jdk1.8之前是由数组+链表组成,jdk1.8之后由数组+链表+红黑树实现
元素加入HashMap,先计算出元素的哈希值,其映射出元素对应在数组的下标。如果哈希值重复,则存放在数组对应的链表上。jdk1.8之后,如果链表长度大于阈值,则自动转化为红黑树,同样的,如果长度小于阈值,就会由红黑树转化为链表。
在什么情况下HashMap会扩容?(HashMap扩容机制)
HashMap初始化大小为16,在内容达容量0.75时自动扩容,扩容大小为2倍。扩容后会创建一个新的数组,需要把旧数据挪到新数组中
为什么HashMap是线程不安全的?怎么实现一个安全的HashMap?
HashMap的所有方法不是同步的。在多线程环境下,多个线程同时修改HashMap可能导致数据不一致或出现死循环。
如果想使用线程安全的HashMap,可通过**Collections.synchronizedMap(new HashMap<>())**创建一个线程安全的HashMap
HashMap什么样的类适合作为键?
不可变性是必要的,且需要实现方法equals和hashcode。因为key的存取要一致,如果不一致就取不到数据了
HashMap和HashTable的区别
主要区别有,线程安全性和可存入Null值
- HashTable是线程安全的,HashMap是非线程安全的
- HashTable不能存入以Null为值的key和value ,而HashMap可以
- 虽然HashTable是线程安全的,但不建议在多线程环境下使用,因为它是一个古老的API,性能不好,如果要在多线程下使用,推荐用ConcurrentMap
int和Integer的区别
int是java中的 基本对象,integer是java对int的一个 封装类,大小和默认值不同,int默认值为0,Integer默认值为null
ArrayList,Vector,LinkedList的区别
ArrayList和Vector底层由数组实现,支持随机索引,查找快而修改慢。
ArrayList可以自动扩容,线程不安全
Vector可以自动扩容,线程安全,但是性能太低已经弃用了
LinkedList底层由双向链表实现,不支持随即索引,查找需要向前或向后遍历,修改快。线程不安全。
如果要多线程访问,可以用Collections.synchronizedList()
Collection和Collections的区别
Collection是集合类的上级接口,继承它的主要是List和Set
Collections是集合类的一个帮助类,提供了集合的搜索,排序等方法
List和Map的区别
- list存储 单列数据集合,存储循序有序且可重复
- Map存储 键值对双列数据集合,存储顺序无需且键不可重复值可重复
Sleep()和wait()的区别
- sleep是Thread类的,睡眠时不需要释放锁,可以在任意地方使用
- wait是Object类的,睡眠时释放锁,必须在synchronized修饰的代码块或方法中使用
抽象类和接口类的区别
抽象类内部可以有属性,构造方法,静态方法等,而接口不可以。
一个类可以继承一个类,但是可以实现多个接口
OverLoad(重载)和Override(重写)的区别
OverLoad重载可以使一个方法名有多个不同参数的是西安
Override用在继承方面,方法名和参数完全一致,子类在使用时优先使用子类的方法
什么是序列化?
序列化是一种处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象传输于网络之间。通过实现接口Serializable接口来实现序列化
String,StringBuilder,StringBuffer的区别
- String,内容不可变,每次拼接操作都会创建一个新的对象。线程安全,但是由于每次拼接字符都会创建一个新的对象,所以性能不高。适用于字符串直接量,少量的字符拼接操作
- StringBuilder,内容可变,功能与StingBuffer类似,但线程不安全,适用于单线程下的大量字符串拼接操作
- SringBuffer,内容可变,内部通过char[]动态扩容,自动修改内容。所有方法由Synchronized修饰,线程安全,适用于多线程下的字符串操作
==与equals()的区别
- ==比较基本数据类型时,比较值是否相等,比较引用对象时,比较的是其在内存中的位置是否相等。但是如果用于比较引用对象,就算两个对象内容相同,结果也不一定为true,因为地址可能不一样。
- equals()默认比较内存地址,但可以重写方法为比较内容相等
hashCode()和equals()的区别,为什么重写equals()就要重写hashCode()
- hashCode()计算哈希码,equals()比较两对象是否相等
- 为什么两个要同时重写?因为这是java对象契约规定的,重写equals()就一定要重写hashCode(),否则在操作哈希表时可能会出现一些逻辑错误。
- java对象契约核心规定,两个对象equals()返回true,那么他们的hashCode方法返回值一定相等,反之则不成立
- 如果两个方法不同时重写,因为哈希表是通过哈希码来判断数据的存储位置的。内容相同的两个对象,由于哈希码不同,所以在哈希表中的位置就不一样,如果存储在hashSet,就会被认为是两个不同的对象,就导致hashSet存入重复值
java的异常处理机制
- java的异常处理机制可分为三部分,异常处理,抛出异常,异常跟踪栈
- 处理异常的语句由try,catch,finally三部分组成,try包裹业务代码,catch捕获异常,处理异常,finally用于释放资源,无论什么情况下都会执行finally
- 程序出现异常,系统自动生成一个异常对象,并交给JVM处理,JVM寻找处理该异常的chatch块并交给它处理,如果没找到,则终止程序
- 此外,java还允许程序手动抛出异常
说说你对java反射的了解
java反射就是在程序运行期间动态的获取类的方法和属性。反射的操作内容主要是:
- 获取Class对象,可以通过类.class、对象.class或forName(全限定名)来获取类对象
- 创建对象实例,获取到类对象后,通过getDeclaredConstructor()来实现它的无参或有参构造方法,创建实例
- 获取方法,通过getDeclaredMethod来获取方法,用invoke调用方法
说一说多线程
线程是执行程序的最小单位,一个进程可以有多个线程。各个线程之间共享程序的内存空间和系统资源分配,但是各个线程都有自己的栈空间。多线程的优点:减少程序响应时间,提高cpu利用率,创建和切换开销小。
说说线程的创建
我了解到的创建线程的方法有两种
一种是继承Thread类,并重写run方法,通过start方法运行
另一种是实现Runnable接口,并重写run方法,通过start方法运行
说说你对集合的了解
java中的集合主要包含Collection和Map两大类,其中collection又派生出了List,Set,Queue。所有集合类都是List,Set,Queue,Map这四个接口的实现类。List代表有序,可重复,Set代表无序,不可重复,Queue代表现先进先出队列,Map代表键值对关系集合。
Redis
Redis提供了哪几种持久化方式
Redis提供了两种持久化方式,即RDB和AOF
- RDB,在不同的时间点,将redis存储的数据生成快照存储在磁盘中
- AOF,将redis执行过程中的所有命令记录下来,在下次redis重启时,再重新执行一遍所有程序
- RDb和AOF这两种可以同时使用,但是重启Redis,会优先采用AOF,因为AOF恢复的数据完整度更高
RDB和AOF的区别
RDB是对整个内存做一个快照,而AOF是记录所有运行的指令。
RDB在可能会导致数据完整性低,而AOF的完整性高
RDB的文件比较小,而AOF会记录所有执行命令,文件比较大
RDB恢复数据速度快,而AOF需要一条一条的执行命令,所以速度比较慢
Redis的key过期后,会立即删除吗
Redis有两种数据过期策略,惰性删除和定期删除
- 惰性删除:只有在数据被访问时,才会去判断数据的ttl是否过期,过期就删除,否则直接返回,这意味着,就算数据已经过期,但只要数据一直没有被访问,就会占用内存空间,会造成内存资源浪费
- 定期删除:定期去判断一定数量的key是否过期,如果过期则删除。优点是内存占用低,缺点是查询key需要占用一些性能
加入缓存过多,内存有限,内存满了怎么办?
Redis有八种数据淘汰策略,默认是不淘汰数据直接报错,但可以修改配置文件来选择淘汰策略
淘汰策略中有两种重要的思想
- LRU:淘汰当前最久不适用的数据
- LFU:淘汰当前使用最不频繁的数据
如果数据库有100W条数据,而Redis只能存储20W条数据怎么办?
选择LRU淘汰策略,删除最久未使用的数据,保证近期热点访问的数据缓存在内存中
了解过双写一致性吗?
当数据库的数据发生修改时,需要更新缓存中的数据,使缓存数据与数据库数据保持一致,在写操作时,会采用延迟双删
在延迟双删中为什么要延迟删除缓存?
因为数据库采用主从模式,遵循读写分离,需要一些时间将数据库中的数据同步到从数据库,但还是有可能出现脏数据
在延迟双删中,为什么要删除两次缓存?
如果只删除一次缓存,可能出现两种情况
1.先删除缓存再更新数据。线程A删除缓存,再更新数据库的时候,线程B访问缓存,缓存没有数据,去访问数据库,获取旧数据并且更新缓存,此时线程A更新数据库成功,就会导致数据库数据与缓存数据不一致
2.先更新数据再删除缓存。当缓存中没有数据时,线程A访问缓存失败,去访问数据库的时候,线程B修改数据库,删除缓存(此时缓存没有数据),然后线程A将访问到的旧数据添加到缓存,还会造成数据不一致
Redis和Mysql是如何保证数据一致性的
Mysql可以通过事务来确保数据一致性。通过事务,确保所有的数据库操作要么全部成功执行,要么全部失败回滚
Redis和Mysql保持数据一致,需要延迟双删。
先删除缓存,再更新数据库,在这期间,所有访问都去数据库,而非旧数据。数据库更新后,再让线程休眠一段时间,这个时间是给数据库充足的时间去修改数据。最后,再次删除缓存,这是因为在线程休眠期间,可能有别的线程读取到了旧数据并写入缓存,造成数据不一致。但是延迟双删并不能完全保证数据的一致性。
延迟双删是没法保证强一致性的,有什么强一致性的方法吗?
1.分布式锁。在对数据库进行写操作的时候,添加分布式锁,保证同一时刻只有一个线程访问修改数据库,但是这样会影响性能。
2.使用RabbitMQ,在修改数据时,通过MQ发送信息给Radis,异步的修改缓存。因为MQ的消息是按照顺序消费的,如果事务发生了回滚,那么消息会被重新放入队列。
Redis为什么快?
Redis将内容存储在内存,而内存的读写速度远大于硬盘,所以快
另外,Redis还对一些数据结构做了优化比如String,Redis采用单线程模式来处理客户端请求,这看似限制了Redis的并发处理能力,但是Redis是操作内存的,且避免了多线程的上下文切换以及抢锁环节,使得Redis处理单个请求时非常高效
Redis是单线程的,为什么那么快呢?
Redis虽然是单线程的,这看似限制了它的高并发能力,但也得益于单线程,省去了多线程的上下文切换和争抢锁的时间,使其对于单个服务的处理效率很高
简单说说布隆过滤器
布隆过滤器类似hashmap,存储二进制数据,1代表存在,0代表不存在。通过hash函数计算存储位置,在查询缓存前,先查询布隆过滤器中数据是否存在,如果存在,在去查询缓存,否则直接返回空
布隆过滤器的优点是,存储二进制数据,占用空间小,查询速度快,但是可能存在误判,不能做删除操作。
因为布隆过滤器是根据哈希值来存储的,可能存在相同的哈希值,所以为了降低误判率,可以采用多哈希值。
简单说说分布式锁
Redisson分布式锁底层方法主要是通过Lua脚本实现,确保了操作的原子性。如果给锁添加了时间,则分布式锁会有watch dog来自动给锁续期。
还可以用Redis的setnx来实现,但是由于不知道具体要给锁添加多长时间,所以不常用。
介绍一下Redis的主从复制流程
主从复制主要分为全量同步和增量同步
两个Redis,slave向master发送同步请求,携带上自己的applicationId和offset给master,master判断appID与自己是否一致,如果不一致,则认为slave是第一次发送同步请求,则进行全量同步,master运行bgsave程序,生成RDB文件发送给slave进行全量同步,在此过程中,master可能进行新的指令,master会将这些指令存储在日志文件中,等slave同步完成后再发送给slave进行最终同步,然后master同步自己的appid和offset给slave。
如果appID一致,则认为不是第一次同步,则进行增量同步,master从日志文件中获取offset位置,将之后的数据发送给slave进行同步
哨兵模式是怎样的?Redis有几种模式?
我了解到的模式有主从模式、哨兵模式
哨兵模式是为了解决主从复制的缺点,即哨兵模式还是基于主从模式,只不过多了一个”哨兵“,当master挂掉后,烧饼会从salve中选出一个作为master,若master重新启动,那么该master则会转化为slave,并通过发布订阅方式,更改所有slave的master。
但是哨兵也存在单点故障问题,所以在一个一主多从的Redis系统中,可以创建多个哨兵
主节点宕机了怎么办?
可以设置哨兵模式,当主节点宕机后,哨兵模式自动从其他节点中选出一个节点作为主节点
说说你对哨兵模式的理解
哨兵模式的实现是通过sentinel的心跳机制,sentinel每秒会向所有节点发送一个ping命令,当master失效后,就会选择出新的master
Redis的分片集群有什么用?
- 可以有多个master,这些master存储不同的数据,多个master可以解决并发写的问题
- 一个master可以有多个slave,可以解决并发读的问题
- 分片集群中,不再通过sentinel检测节点的状态,而是master之间ping状态,判断各master的健康状态,实现了哨兵模式
Mybatis
有了解过Mybatis的一级缓存,二级缓存吗?
一级缓存:基于PrepetualCache的HashMap的本地缓存,默认是开启的,其作用域是session,当session进行了close,flush后缓存从session中删除
二级缓存:其作用域是namespace和mapper,基于PrepetualCache,hashmap进行缓存,默认不开启,可以在Mybatis的配置文件中配置CacheEnable设置为true,mapper中添加标签cache开启
Mybatis的二级缓存什么时候会被清除?
当某个作用域(一级缓存的session/二级缓存的namespace)进行了增删改查后,就会清楚select中的缓存
#{}和${}的区别?
- #{}主要就是占用符的替换,mysql中用?进行占位,而${}主要做文本替换
- #{}发生在数据库访问层之内,${}数据替换发生在数据库访问层之外
- #{}可以防止sql注入,而${}替换的文本数据存在sql注入的风险
SpringCloud
了解过SpringCloud的五大件吗?
了解过SpringCloudAlibaba
- nacos:注册中心,配置中心
- openFeign:远程调用
- gateWay:网关
- sentinel:服务保护
- 负载均衡:了解过nacos的负载均衡
服务的注册与发现是什么意思?SpringCloud是如何实现服务的注册与发现的
拿Nacos举例
每一个服务的提供者,都会向Nacos发送一份自己的信息,Nacos会将这些信息,比如服务名,ip,端口号存储到一个信息表中,这就是服务注册
每一个消费者,如果想要获取某一个服务,会先向Nacos发送服务请求,Nacos根据它的请求服务,去信息表中查询相关信息并返回给消费者,消费者通过这些信息再去获取服务,这就是服务发现。
服务的提供者每30秒还会向Nacos发送心跳,来证明自己的健康状态,如果一个信息超过90秒都没有发送心跳,那么Nacos就会移除这个服务
你了解到的负载均衡有哪几种方法?
- 第一种,通过RestTemplate和Nacos实现,需要自定义RestTemplate并且添加注解@LoadBalanced,然后添加Nacos的负载均衡依赖,之后使用RestTeamplate就会实现负载均衡
- OpenFeign的负载均衡默认是开启的,只需要配置好feign的客户端,负载均衡就会自动生效
什么是服务雪崩?解决办法是什么
当某个服务调用失败或者长时间未响应时,可能导致一系列的服务调用都发生失败, 造成服务雪崩
解决办法:熔断降级
当服务长时间未响应或者调用失败,应该立刻断开连接,并发送一个错误数据。通过OpenFeign的兜底返回方法,返回一个默认数据
分布式事务的解决方案?
Seata分布式事务
Seata实现分布式事务,得益于它的二阶提交协议
分布式事务开始前,添加@GlobalTransactional注解,向seataSetver注册一个全局事务id,开启全局事务
第一阶段,开启分支事务。分支事务中,先解析sql语句,然后从数据库中获取到要修改数据的前镜像,然后执行sql语句,获取数据的后镜像,然后将前后镜像插入到undo_log回滚日志中,准备提交事务,但是为了防止外界其他事务也操作这个数据,所以本地事务提交前会向seateServer注册一个分支事务,并且申请一个全局锁,防止外界去修改这个数据,然后再去提交事务,也就是提交数据跟undo_log日志,然后向tc发送自己的完成状态。
第二阶段,所有的分支事务都完成后,都向tc发送一个完成的信息,如果所有分支事务都完成了,那么就添加异步任务,删除第一阶段生成的undo_log日志记录
但是,如果某一个任务发生了回滚,tc就会通知所有的事务开启回滚。
本地事务收到回滚通知,开启本地事务,去undo_log日志中查找到自己的前后镜像,先判断后镜像数据与当前数据是否一致,如果不值一,说明有别的操作修改了数据,此时需要在配置文件中设置策略。如果一致,则回滚,执行结束后,删除undo_log日志。
简单介绍一下OpenFeign
OpenFeign的作用主要是远程调用,它不用显示的写出调用的地址,而是写出调用的微服务的名字和方法即可。
OpenFeign还可以配置超时信息,包括连接超时和响应超时等。
OpenFeign还提供了重试机制,默认连接一次,失败就不再尝试,可以通过配置文件,进行失败后重试。
另外,还提供了兜底返回机制,通过实现ProduceFeignClient接口,自定义兜底返回方法,在调用失败后返回默认数据
简单介绍一下Sentinel
sentinel功能主要有熔断降级,流量控制,资源监控等功能。
服务保护就比如它的异常处理机制,如下
熔断降级
如果服务A访问B,迟迟没有得到响应,那么就可能造成服务雪崩,为了避免这一情况,当服务迟迟未响应时,应该断开连接并返回一个错误数据。
熔断降级的关键组件是断路器
断路器有三种状态,开,半开,关
默认是关,服务A可以直接访问服务B,但如果B处理效率不佳,比如一段时间内大量请求都失败了,那么断路器就会打开,阻止所有请求。等待一段时间后,断路器变为半开,再发送一条探路数据,如果此时效果还是不好,就继续断开,否则就关上
流量限制
分为流控模式和流控效果
流控模式分为直接、关联、链路
流控效率分为快速失败,warmUp,排队等待
Sentinel的异常处理机制?
Sentinel在发生异常时,会抛出一个BlockException异常,这个异常包括了流控、热点参数、熔断降级、权限控制、系统阻塞异常等。
BlockExcetion会根据发生异常的资源类型,进行不同的异常处理
Web接口异常
如果是Web接口抛出异常,它会执行SentinelWebInterceptor方法,这个方法继承了AbstractSentinelInterceptor方法,它是一个web拦截器,里面有一个preHandler方法,在进入Web接口前,先获取到了接口信息,然后调用Sphu.entry方法去校验这个方法是否违背规则,如果没有违背,则返回true,如果违反规则,那么就会抛出一个BlockException异常,最后通过一个默认的BlockExceptionHandler处理异常。如果要自定义web异常,可以定义类去实现BlockExceptionHandler,自定义异常处理类。
@SentinelResource异常
如果是@SentinelResource异常,系统调用SentinelResourceAspect切面方法,切点是所有添加了@SentinelResource注解的方法,在切面方法中,获取方法名,然后通过Sphu.entry方法判断是否违规,如果没有返回true,如果有,抛出BlockException异常,并调用handlerBlockException方法去处理,如果注解里标注了blockHandler方法,则默认执行blockHandler方法,如果没有,则去看看有没有添加兜底返回的fallback方法,同样的,如果都没有,那么就会向上抛出异常,把异常交给springboot处理。
OpenFeign异常
如果OpenFeign发生异常,就调用OpenFeign的兜底回调方法,如果没有,就将异常交给springboot处理
Sphu.entry
用try/catch手动处理异常
说一下GateWay
功能有:统一入口,请求路由,负载均衡,流量控制,身份认证,系统监控等功能
所有接口都要经过网关,网关通过请求路径再转发请求到对应的服务。
因此,可以再网关层做一些身份校验,或者是日志记录等等
RabbitMQ
什么是MQ
MQ是一种消息中间件,用于跨线程信息传递,传递上下游信息。
MQ的优点
异步、削峰、解耦
- 异步,通过异步操作
- 削峰:把消息放到消息队列,至于之后每秒消费多少消息,就看服务器的消费能力,流量高峰下去了,服务也就没有压力了
- 解耦:每添加一个下游,都不必修改上游代码,下游直接订阅MQ即可
为什么要使用MQ
当业务增加时,比如支付功能,有可能支付后要进行添加积分等操作,如果都写到支付功能中,可能在造成响应时间过长,影响用户体验。这时通过MQ发送消息去异步执行,用户只需要等待支付功能完成即可,剩下的添加积分的业务异步执行即可
RabbitMQ如何保证数据的不丢失?
可以从三个方面进行考虑
- 生产者确认机制:生产者发送信息给MQ,MQ根据情况返回ack和nack
- 消费者确认机制:将消费者确认机制设置为auto,在spring成功处理消息后返回ack,删除消息,如果返回nack,则开启重试机制,如果重试多次仍然失败,则将消息投递到异常消息队列,由人工处理异常消息
- 消息持久化:创建持久化的交换机,队列,消息
RabbitMQ的消息重复消费如何解决?
- 设置唯一id,在消费消息时,先查数据库是否存在id,如果不存在,说明没有消费,则消费信息,反之不消费
- 设置分布式锁,确保只有一个线程消费信息,但是性能比较低
RabbitMQ的死信交换机有了解过吗(延迟发送机制?)
通过死信队列和ttl实现
给消息队列通过属性dead-letter-exchange绑定一个死信交换机,自定义死信交换机以及绑定的死信消息队列,给需要延迟发送的信息设置ttl,当消息队列中的消息过期,或者消息被拒绝消费,队列满时,消息进入死信队列。消费者去消费死信队列,以此达到延迟发送的目的。
如果消息队列中的数据太多了怎么办?
说明消息的消费速度远小于生产速度,可以多添加几个消费者或者开启线程池加快消费速度
Dubbo
什么是Dubbo?
Dubbo是一种RPC框架,用于解决微服务架构下的远程通信
RPC是一种远程调用协议
