9.1 Dubbo框架
9.1.1 简述 Dubbo和SpringCloud的优缺点
Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。
而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了 Spirng、Spirng Boot 的优势之上,两个框架在开始目标就不一致,Dubbo 定位服务治理、Spirng Cloud 是一个生态。
两者最大的区别是 Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传输的,配合以Hession 序列化完成 RPC 通信。而 SpringCloud 是基于 Http 协议+Rest 接口调用远程过程的通信,相对来说,Http 请求会有更大的报文,占的带宽也会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。
9.1.2 Dubbo的服务请求失败怎么处理
Dubbo 是一个 RPC 框架,它为我们的应用提供了远程通信能力的封装,同时,Dubbo 在 RPC 通信的基础上,涵盖了服务注册、动态路由、容错、服务降级、负载均衡等能力,
基本上在微服务架构下面临的问题,Dubbo 都可以解决。
而对于 Dubbo 服务请求失败的场景,默认提供了重试的容错机制,也就是说,如果基于 Dubbo 进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发起重试,确保这次请求成功,默认的额外重试次数是 2 次。
除此之外,Dubbo 还提供了更多的容错策略,我们可以根据不同的业务场景来进行选择。
- 快速失败策略,服务消费者只发起一次请求,如果请求失败,就直接把错误抛出去。这种比较适合在非幂等性场景中使用
- 失败安全策略,如果出现服务通信异常,直接把这个异常吞掉不做任何处理
- 失败自动恢复策略,后台记录失败请求,然后通过定时任务来对这个失败的请求进行重发。
- 并行调用多个服务策略,就是把这个消息广播给服务提供者集群,只要有任何一个节点返回,就表示请求执行成功。
- 广播调用策略,逐个调用服务提供者集群,只要集群中任何一个节点出现异常,就表示本次请求失败
要注意的是,默认基于重试策略的容错机制中,需要注意幂等性的处理,否则在事务型的操作中,容易出现多源数据变更的问题
9.1.3 Dubbo是如何感知服务下线的
Dubbo 默认采用 Zookeeper 实现服务的注册与服务发现,简单来说啊,就是多个 Dubbo 服务之间的通信地址,是使用 Zookeeper 来维护的。
(如图)在 Zookeeper 上,会采用树形结构的方式来维护 Dubbo 服务提供端的协议地址,
Dubbo 服务消费端会从 Zookeeper Server 上去查找目标服务的地址列表,从而完成服务的注册和消费的功能。
Zookeeper 会通过心跳检测机制,来判断 Dubbo 服务提供端的运行状态,来决定是否应该把这个服务从地址列表剔除。
当 Dubbo 服务提供方出现故障导致 Zookeeper 剔除了这个服务的地址,那么 Dubbo 服务消费端需要感知到地址的变化,从而避免后续的请求发送到故障节点,导致请求失败。
也就是说 Dubbo 要提供服务下线的动态感知能力。
(如图)这个能力是通过 Zookeeper 里面提供的 Watch 机制来实现的,简单来说呢,Dubbo 服务消费端会使用 Zookeeper 里面的 Watch 来针对 Zookeeper Server 端的/providers 节点注册监听,
一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知 Dubbo Client 端
Dubbo Client 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续就不会把请求发送到失败的节点上,完成服务下线感知。
Dubbo 的整体架构设计有哪些分层?
接口服务层( Service ):该层与业务逻辑相关,根据 provider 和 consumer 的业务设 计对应
的接口和实现
配置层( Config ):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心
服务代理层( Proxy ):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton ,以 Service Proxy 为中心 , 扩展接口为 ProxyFactory
服务注册层( Registry ) :封装服务地址的注册和发现 , 以服务 URL 为中心, 扩展 接口为RegistryFactory、 Registry、 RegistryService
路由层( Cluster ) : 封装多个提供者的路由和负载均衡 , 并桥接注册中心 , 以 Invoker 为
中心 , 扩展接口为 Cluster 、 Directory 、 Router 和 Load Blancce
监控层( Monitor ) :RPC 调用次数和调用时间监控 , 以 Statistics 为中心 , 扩 展接口为
Monitor Factory 、 Monitor 和 MonitorService
远程调用层( Protocal) :封装 RPC 调用,以 Invocation 和 Result 为 中 心 , 扩展接口
为 Protocal 、 Invoker 和 Exporter
信息交换层( Exchange ): 封装请求响应模式 ,同步转异步。 以 Request 和 Response 为
中心 ,扩展接口为 Exchanger 、 ExchangeChannel、ExchangeClient 和 ExchangeServer
网络传输层( Transport ):抽象 mina 和 netty 为统一接口,以 Message 为中心, 扩展接
口为 Channel、Transporter、Client、 Server 和 Codec
数据序列化层( Serialize ) : 可复用的一些工具 , 扩展接口为 Serialization、 ObjectInput 、ObjectOutput 和 Thread Pool
9.2 MyBatis框架
9.2.1 谈谈你对MyBatis缓存机制的理解
Mybatis 里面设计了两级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。
一级缓存,它是 SqlSession 级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用SqlSession来执行,为了避免每次都去查数据库,MyBatis把查询出来的数据保存到SqlSession的本地缓存中,后续的 SQL 如果命中缓存,就可以直接从本地缓存读取。
那如果想要实现跨 SqlSession 级别的缓存?一级缓存就无法实现了,因此,MyBatis 引入了二级缓存。
当多个用户在查询数据的时候,只要有任何一个 SqlSession 拿到了数据就会放入到二级缓存里面,其他的 SqlSession 就可以从二级缓存加载数据。
2、原理分析
接下来,我给大家详细分析一下 MyBatis 缓存机制的实现原理。先来看一级缓存的实现原理:
在 SqlSession 里面持有一个 Executor 的对象,每个 Executor 中有一个 LocalCache 对象。
当用户发起查询的时候,MyBatis 会根据执行语句在 Local Cache 里面查询,如果没命中,再去查询数据库并写入到 LocalCache,否则直接返回。
所以,一级缓存的生命周期只在 SqlSession 级别,而且在多个 SqlSession 或者分布式环境下,可能会导致数据库写操作出现脏数据。
那这个时候,就要使用二级缓存。下面来看二级缓存的具体实现原理:
二级缓存使用了一个叫做 CachingExecutor 的对象,对 Executor 进行了装饰,在进入一级缓存的查询流程之前,会先通过 CachingExecutor 进行二级缓存的查询。
开启二级缓存以后,会被多个 SqlSession 共享,所以它是一个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。
另外,MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时缓存粒度也能够控制到 Name Space 级别,并且还可以通过 Cache 接口实现类不同的组合,对 Cache的可控性也更强。
9.2.2 MyBatis中 #号和$号的区别
1、两者区别
Mybatis 提供到的#号和KaTeX parse error: Expected 'EOF', got '#' at position 74: …这两种占位符进行动态解析。 #̲号,等同于 JDBC 里面的?…号,相当于直接把参数拼接到了原始的 SQL 里面,MyBatis 不会对它进行特殊处理。
所以KaTeX parse error: Expected 'EOF', got '#' at position 2: 和#̲最大的区别在于,前者是动态参数…符号的动态传参,可以适合应用在一些动态 SQL 场景中,比如动态传递表名、动态设置排序字段等。
9.2.3 MyBatis是如何进行分页的
问题解析
数据进行分页是最基础的功能,一般可以把分页分成两类:
逻辑分页,先查询出所有的数据缓存到内存,再根据业务相关需求,从内存数据中筛选出合适的数据进行分页。
物理分页 ,直接利用数据库支持的分页语法来实现,比如 Mysql 里面提供了分页关键词 Limit
Mybatis 提供了四种分页方式:
在 Mybatis Mapper 配置文件里面直接写分页 SQL,这种方式比较灵活,实现也简单。
RowBounds 实现逻辑分页,也就是一次性加载所有符合查询条件的目标数据,根据分页参数值在内存中实现分页。
当然,在数据量比较大的情况下,JDBC 驱动本身会做一些优化,也就是不会把所有结果存储在ResultSet 里面,而是只加载一部分数据,再根据需求去数据库里面加载。
这种方式不适合数据量较大的场景,而且有可能会频繁访问数据库造成比较大的压力。
Interceptor 拦截器实现,通过拦截需要分页的 select 语句,然后在这个 sql 语句里面动态拼接分
页关键字,从而实现分页查询。
(如图)Interceptor 是 Mybatis 提供的一种针对不同生命周期的拦截器,比如:
拦截执行器方法
拦截参数的处理
拦截结果集的处理
拦截SQL 语法构建的处理
我们可以拦截不同阶段的处理,来实现 Mybatis 相关功能的扩展。
这种方式的好处,就是可以提供统一的处理机制,不需要我们再单独去维护分页相关的功能。
插件(PageHelper)及(MyBaits-Plus、tkmybatis)框架实现 这些插件本质上也是使用 Mybatis 的拦截器来实现的。
只是他们帮我们实现了扩展和封装,节省了分页扩展封装的工作量,在实际开发中,只需要拿来即用即可。
回答
我认为有三种方式来实现分页:
第一种,直接在 Select 语句上增加数据库提供的分页关键字,然后在应用程序里面传递当前页,
以及每页展示条数即可。
第二种,使用 Mybatis 提供的 RowBounds 对象,实现内存级别分页。
第三种,基于 Mybatis 里面的 Interceptor 拦截器,在 select 语句执行之前动态拼接分页关键字。