Java面试

Spring

  • BeanFactory 和 ApplicationContext 的区别
    • BeanFactory 可以理解为含有 bean 集合的工厂类。BeanFactory 包含了种 bean 的定义,以便在接收到客户端请求时将对应的 bean 实例化。还能在实例化对象时生成协作类之间的关系。此举将 bean 自身与 bean 客户端的配置中解放出来。BeanFactory 还包含了 bean 生命周期的控制,调用客户端的初始化方法(initialization methods)和销毁方法(destruction methods)
    • ApplicationContext 如同 BeanFactory 一样具有 bean 定义、bean 关联关系的设置,根据请求分发 bean 的功能。但 ApplicationContext 在此基础上还提供了其他的功能:提供了支持国际化的文本消息、统一的资源文件读取方式、已在监听器中注册的 bean 的事件。
  • Spring Bean 的生命周期:在一个 bean 实例被初始化时,需要执行一系列的初始化操作以达到可用的状态。同样的,当一个 bean 不在被调用时需要进行相关的析构操作,并从 bean 容器中移除。Spring bean factory 负责管理在 spring 容器中被创建的 bean 的生命周期。Bean 的生命周期由两组回调(call back)方法组成。初始化之后调用的回调方法。销毁之前调用的回调方法。Spring 框架提供了以下四种方式来管理 bean 的生命周期事件:
    • InitializingBean 和 DisposableBean 回调接口
    • 针对特殊行为的其他 Aware 接口
    • Bean 配置文件中的 Custom init() 方法和 destroy() 方法
    • @PostConstruct 和 @PreDestroy 注解方式
  • Spring IOC 如何实现
    • Spring 中的 org.springframework.beans 包和 org.springframework.context 包构成了 Spring 框架 IoC 容器的基础。
    • BeanFactory 接口提供了一个先进的配置机制,使得任何类型的对象的配置成为可能。ApplicationContext 接口对 BeanFactory(是一个子接口)进行了扩展,在 BeanFactory 的基础上添加了其他功能,比如与 Spring 的 AOP 更容易集成,也提供了处理 message resource 的机制(用于国际化)、事件传播以及应用层的特别配置,比如针对 Web 应用的 WebApplicationContext。
    • org.springframework.beans.factory.BeanFactory 是 Spring IoC 容器的具体实现,用来包装和管理前面提到的各种 bean。BeanFactory 接口是 Spring IoC 容器的核心接口。
  • Spring AOP:面向切面编程,在我们的应用中,经常需要做一些事情,但是这些事情与核心业务无关,比如,要记录所有 update 方法的执行时间时间,操作人等等信息,记录到日志, 通过 Spring 的 AOP 技术,就可以在不修改 update 的代码的情况下完成该需求。
  • Spring AOP 实现原理:Spring AOP 中的动态代理主要有两种方式,JDK 动态代理 和 CGLIB 动态代理。JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。
  • 动态代理(CGLIB 与 JDK):JDK 动态代理类和委托类需要都实现同一个接口。也就是说只有实现了某个接口的类可以使用 Java 动态代理机制。但是,事实上使用中并不是遇到的所有类都会给你实现一个接口。因此,对于没有实现接口的类,就不能使用该机制。而 CGLIB 则可以实现对类的动态代理。
  • Spring 事务实现方式
    • 编码方式:所谓编程式事务指的是通过编码方式实现事务,即类似于 JDBC 编程实现事务管理
    • 声明式事务管理方式:基于 xml 配置文件的方式、另一个是在业务方法上进行 @Transaction 注解,将事务规则应用到业务逻辑中;
  • Spring 事务底层原理
    • 划分处理单元 IOC:由于 Spring 解决的问题是对单个数据库进行局部事务处理的,具体的实现首相用 Spring 中的 IOC 划分了事务处理单元。并且将对事务的各种配置放到了 IOC 容器中(设置事务管理器,设置事务的传播特性及隔离机制)
    • AOP 拦截需要进行事务处理的类:Spring 事务处理模块是通过 AOP 功能来实现声明式事务处理的,具体操作(比如事务实行的配置和读取,事务对象的抽象),用 TransactionProxyFactoryBean 接口来使用 AOP 功能,生成 proxy 代理对象,通过 TransactionInterceptor 完成对代理方法的拦截,将事务处理的功能编织到拦截的方法中。读取 IOC 容器事务配置属性,转化为 Spring 事务处理需要的内部数据结构(TransactionAttributeSourceAdvisor),转化为 TransactionAttribute 表示的数据对象
    • 对事物处理实现(事务的生成、提交、回滚、挂起):Spring 委托给具体的事务处理器实现。实现了一个抽象和适配。适配的具体事务处理器:DataSource 数据源支持、Hibernate 数据源事务处理支持、JDO 数据源事务处理支持,JPA、JTA 数据源事务处理支持。这些支持都是通过设计 PlatformTransactionManager、AbstractPlatforTransaction 一系列事务处理的支持。 为常用数据源支持提供了一系列的 TransactionManager
    • 结合:PlatformTransactionManager 实现了 TransactionInterception 接口,让其与 TransactionProxyFactoryBean 结合起来,形成一个 Spring 声明式事务处理的设计体系。
  • 如何自定义注解实现功能
    • 创建自定义注解和创建一个接口相似,但是注解的 interface 关键字需要以 @ 符号开头。
    • 注解方法不能带有参数;
    • 注解方法返回值类型限定为:基本类型、String、Enums、Annotation 或者是这些类型的数组;
    • 注解方法可以有默认值;
    • 注解本身能够包含元注解,元注解被用来注解其它注解。
  • Spring MVC 运行流程
    • Spring MVC 将所有的请求都提交给 DispatcherServlet,它会委托应用系统的其他模块负责对请求进行真正的处理工作。
    • DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller.
    • DispatcherServlet 请求提交到目标 Controller
    • Controller 进行业务逻辑处理后,会返回一个 ModelAndView
    • Dispatcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象
    • 视图对象负责渲染返回给客户端。
  • Spring MVC 启动流程:在 web.xml 文件中给 Spring MVC 的 Servlet 配置了 load-on-startup,所以程序启动的时候会初始化 Spring MVC,在 HttpServletBean 中将配置的 contextConfigLocation 属性设置到 Servlet 中,然后在 FrameworkServlet 中创建了 WebApplicationContext,DispatcherServlet 根据 contextConfigLocation 配置的 classpath 下的 xml 文件初始化了 Spring MVC 总的组件
  • Spring 的单例实现原理:Spring 对 Bean 实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是 ConcurrentHashMap 对象。
  • Spring 框架中用到了哪些设计模式
    • 代理模式:在 AOP 和 Remoting 中被用的比较多。
    • 单例模式:在 Spring 配置文件中定义的 Bean 默认为单例模式。
    • 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
    • 前端控制器:Spring 提供了 DispatcherServlet 来对请求进行分发。
    • 视图帮助(View Helper ):Spring 提供了一系列的 JSP 标签,高效宏来辅助将分散的代码整合在视图里。
    • 依赖注入:贯穿于 BeanFactory / ApplicationContext 接口的核心理念。
    • 工厂模式:BeanFactory 用来创建对象的实例。

分布式

  • 业务中使用分布式的场景:随着用户量的增加,并发量增加,单项目难以承受如此大的并发请求导致的性能瓶颈;随着业务的发展,数据库压力越来越大,导致的性能瓶颈:
    • 应用系统集群的 Session 共享:应用系统集群最简单的就是服务器集群,比如:Tomcat 集群。应用系统集群的时候,比较凸显的问题是 Session 共享,Session 共享我们一是可以通过服务器插件来解决。另外一种也可以通过 Redis 等中间件实现。
    • 应用系统的服务化拆分:服务化拆分,现在都在提微服务。通过对传统项目进行服务化拆分,达到服务独立解耦,单服务又可以横向扩容。服务化拆分遇到的经典问题就是分布式事务问题。目前,比较常用的分布式事务解决方案有几种:消息最终一致性、TCC 补偿型事务等
    • 底层数据库的压力分摊:读写分离、分库分表等方案进行解决
  • Session 分布式方案
    • 基于 nfs(net filesystem) 的 Session 共享:将共享服务器目录 mount 各服务器的本地 session 目录,session 读写受共享服务器 io 限制,不能满足高并发
    • 基于关系数据库的 Session 共享:这种方案普遍使用。使用关系数据库存储 session 数据,对于 mysql 数据库,建议使用 heap 引擎。这种方案性能取决于数据库的性能,在高并发下容易造成表锁(虽然可以采用行锁的存储引擎,性能会下降),并且需要自己实现 session 过期淘汰机制
    • 基于 Cookie 的 Session 共享:这种方案也在大型互联网中普遍使用,将用户的 session 加密序列化后以 cookie 的方式保存在网站根域名下(比如 taobao.com),当用户访问所有二级域名站点时,浏览器会传递所有匹配的根域名的 cookie 信息,这样实现了用户 cookie 化 session 的多服务共享。此方案能够节省大量服务器资源,缺点是存储的信息长度受到 http 协议限制;cookie 的信息还需要做加密解密;请求任何资源时都会将 cookie 附加到 http 头上传到服务器,占用了一定带宽
    • 基于 Web 容器的 Session 机制:利用容器机制,通过配置即可实现
    • 基于 Zookeeper 的分布式 Session 存储
    • 基于 Redis/Memcached 的 Session 共享存储:这些 key/value 非关系存储有较高的性能,轻松达到 2000 左右的 qps,内置的过期机制正好满足 session 的自动实效特性
  • 分布式锁的场景与实现
    • 数据库实现:加 xx 锁、唯一键
    • Zookeeper 实现:获取锁:先有一个锁跟节点,lockRootNode,这可以是一个永久的节点;客户端获取锁,先在 lockRootNode 下创建一个顺序的瞬时节点,保证客户端断开连接,节点也自动删除;调用 lockRootNode 父节点的 getChildren() 方法,获取所有的节点,并从小到大排序,如果创建的最小的节点是当前节点,则返回 true,获取锁成功,否则,关注比自己序号小的节点的释放动作(exist watch),这样可以保证每一个客户端只需要关注一个节点,不需要关注所有的节点,避免羊群效应;如果有节点释放操作,重复步骤 3。释放锁:只需要删除步骤 2 中创建的节点即可
    • Redis 实现
    • Tair 实现:通过 Tair 来实现分布式锁和 Redis 的实现核心差不多,不过 Tair 有个很方便的 api,感觉是实现分布式锁的最佳配置,就是 Put api 调用的时候需要传入一个 version,就和数据库的乐观锁一样,修改数据之后,版本会自动累加,如果传入的版本和当前数据版本不一致,就不允许修改

分布式事务

  • 分布式一致性:在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的 CRUD 等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol) 和 Paxos 算法
  • 分布式事务:是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)。常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行。
  • XA 规范:是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。
    • X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。 通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。 所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。 一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。
    • 二阶提交协议和三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现 XA 分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)
  • 二阶段提交(Two-phaseCommit):在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。第一阶段:准备阶段(投票阶段) 和第二阶段:提交阶段(执行阶段)
    • 准备阶段:协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。参与者节点执行询问发起为止的所有事务操作,并将 Undo 信息和 Redo 信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)。各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
    • 提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚( Rollback )消息;否则,发送提交( Commit )消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
    • 二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷
  • 三阶段提交(Three-phase commit):是二阶段提交(2PC)的改进版本 。引入超时机制,同时在协调者和参与者中都引入超时机制;在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
    • CanCommit 阶段:事务询问、响应反馈
    • PreCommit 阶段:假如协调者从所有的参与者获得的反馈都是 Yes 响应,那么就会执行事务的预执行;假如有任何一个参与者向协调者发送了 No 响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
    • doCommit 阶段:执行提交、中断事务
  • 2PC 与 3PC 的区别:相对于 2PC,3PC 主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行 commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的 abort 响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作。这样就和其他接到 abort 命令并执行回滚的参与者之间存在数据不一致的情况。

集群与负载均衡的算法与实现

  • 负载均衡:用户输入的流量通过负载均衡器按照某种负载均衡算法把流量均匀的分散到后端的多个服务器上,接收到请求的服务器可以独立的响应请求,达到负载分担的目的。从应用场景上来说,常见的负载均衡模型有全局负载均衡和集群内负载均衡,从产品形态角度来说,又可以分为硬件负载均衡和软件负载均衡。全局负载均衡一般通过DNS实现,通过将一个域名解析到不同VIP,来实现不同的region调度能力;硬件负载均衡器常见的有F5、A10、Array,它们的优缺点都比较明显,优点是功能强大,有专门的售后服务团队,性能比较好,缺点是缺少定制的灵活性,维护成本较高;现在的互联网更多的思路是通过软件负载均衡来实现,这样可以满足各种定制化需求,常见的软件负载均衡有 LVS、Nginx、Haproxy。
  • 阿里云高性能负载均衡使用 LVS 和 Tengine,我们在一个 region 区分不同的机房,每个机房都有 LVS 集群和 Tengine 集群,对于用户配置的四层监听,LVS 后面会直接挂载用户 ECS,七层用户监听 ECS 则挂载在 Tengine 上,四层监听的流量直接由 LVS 转发到 ECS,而 7 层监听的流量会经过 LVS 到 Tenigine 再到用户 ECS。每一个 region 里都会有多个可用区,达到主备容灾目的,每一个集群里都有多台设备,第一是为了提升性能,第二也是基于容灾考虑。
    • LVS:早期 LVS 支持三种模式,DR 模式、TUN 模式和 NAT 模式
      DR 模式、TUN 模式和 NAT 模式
      • DR 模式:经过 LVS 之后,LVS 会将 MAC 地址更改、封装 MAC 头,内层 IP 报文不动,报文经过 LVS 负载均衡查找到 RS 之后,将源 MAC 头改成自己的,目的 MAC 改成 RS 地址,MAC 寻址是在二层网络里,对网络部署有一定的限定,在大规模分布式集群部署里,这种模式的灵活性没有办法满足需求;
      • TUN 模式:走在 LVS 之后,LVS 会在原有报文基础上封装 IP 头,到了后端 RS 之后,RS 需要解开 IP 报文封装,才能拿到原始报文,不管是 DR 模式还是 TUN 模式,后端 RS 都可以看到真实客户源 IP,目的 IP 是自己的 VIP,VIP 在 RS 设备上需要配置,这样可以直接绕过 LVS 返回给用户,TUN 模式问题在于需要在后端 ECS 上配置解封装模块,在 Linux 上已经支持这种模块,但是 Windows 上还没有提供支持,所以会对用户系统镜像选择有限定。
      • NAT 模式:用户访问的是 VIP,LVS 查找完后会将目的 IP 做 DNAT 转换,选择出 RS 地址,因为客户端的 IP 没变,在回包的时候直接向公网真实客户端 IP 去路由,NAT 的约束是因为 LVS 做了 DNAT 转换,所以回包需要走 LVS,把报文头转换回去,由于 ECS 看到的是客户端真实的源地址,我们需要在用户 ECS 上配置路由,将到 ECS 的默认路由指向 LVS 上,这对用户场景也做了限制
    • Tengine:Tengine 在应用过程中也遇到了各种问题,最严重的就是性能问题,我们发现随着 CPU 数量越来越多,QPS 值并没有线性提升;Nginx 本身是多 worker 模型,每个 worker 是单进程模式,多 worker 架构做 CPU 亲和,内部基于事件驱动的模型,其本身已经提供了很高的性能,单核 Nginx 可以跑到 1W5~2W QPS。Nginx 往下第一层是 socket API,socket 往下有一层 VFS,再往下是 TCP、IP,socket 层比较薄,经过量化的分析和评估,性能开销最大的是 TCP 协议栈和 VFS 部分,因为同步开销大,我们发现横向扩展不行,对此,我们做了一些优化

微服务

  • 前后端分离:在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的 API 服务即可。前后端之间通过 HTTP 请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器
  • 解决跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源(域名,协议,端口均相同)策略造成的,是浏览器对 JavaScript 施加的安全限制
    • 使用 CORS(跨资源共享)解决跨域问题:CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
    • CORS 与 JSONP 的比较:CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
  • 微服务框架
    • Dubbo:是阿里巴巴服务化治理的核心框架,并被广泛应用于阿里巴巴集团的各成员站点。阿里巴巴近几年对开源社区的贡献不论在国内还是国外都是引人注目的,比如:JStorm 捐赠给 Apache 并加入 Apache 基金会等,为中国互联网人争足了面子,使得阿里巴巴在国人眼里已经从电商升级为一家科技公司了。
    • Spring Cloud:从命名我们就可以知道,它是 Spring Source 的产物,Spring 社区的强大背书可以说是 Java 企业界最有影响力的组织了,除了 Spring Source 之外,还有 Pivotal 和 Netflix 是其强大的后盾与技术输出。其中 Netflix 开源的整套微服务架构套件是 Spring Cloud 的核心。
  • RPC 框架:RPC 是指远程过程调用,也就是说两台服务器 A,B 一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数或方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据
    • 要解决通讯的问题,主要是通过在客户端和服务器之间建立 TCP 连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
    • 要解决寻址的问题,也就是说,A 服务器上的应用怎么告诉底层的 RPC 框架,如何连接到 B 服务器(如主机或 IP 地址)以及特定的端口,方法的名称是什么,这样才能完成调用。比如基于 Web 服务协议栈的 RPC,就要提供一个 endpoint URI,或者是从 UDDI 服务上查找。如果是 RMI 调用的话,还需要一个 RMI Registry 来注册服务的地址。
    • 当 A 服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如 TCP 传递到 B 服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给 B 服务器。
    • B 服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。
    • 返回值还要发送回服务器 A 上的应用,也要经过序列化的方式发送,服务器 A 接到后,再反序列化,恢复为内存中的表达方式,交给 A 服务器上的应用。
  • RPC 的实现原理:首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。
  • Dubbo 的实现原理:Dubbo 作为 RPC 框架,实现的效果就是调用远程的方法就像在本地调用一样
    • 本地有对远程方法的描述,包括方法名、参数、返回值,在 Dubbo 中是远程和本地使用同样的接口
    • 要有对网络通信的封装,要对调用方来说通信细节是完全不可见的,网络通信要做的就是将调用方法的属性通过一定的协议(简单来说就是消息格式)传递到服务端
    • 服务端按照协议解析出调用的信息;执行相应的方法;在将方法的返回值通过协议传递给客户端;客户端再解析;在调用方式上又可以分为同步调用和异步调用;
  • RESTful:“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”。利用一个面向最终用户的 Web 应用来对这个概念进行简单阐述:这里所谓的应用状态(Application State)表示 Web 应用的客户端的状态,简单起见可以理解为会话状态。资源在浏览器中以超媒体的形式呈现,通过点击超媒体中的链接可以获取其它相关的资源或者对当前资源进行相应的处理,获取的资源或者针对资源处理的响应同样以超媒体的形式再次呈现在浏览器上。由此可见,超媒体成为了驱动客户端会话状态的转换的引擎。借助于超媒体这种特殊的资源呈现方式,应用状态的转换体现为浏览器中呈现资源的转换。如果将超媒体进一步抽象成一般意义上的资源呈现(Representation )方式,那么应用状态变成了可被呈现的状态(REpresentational State)。应用状态之间的转换就成了可被呈现的状态装换(REpresentational State Transfer),这就是 REST。

设计一个良好的 API

  • 版本号
    • 在 RESTful API 中,API 接口应该尽量兼容之前的版本。但是,在实际业务开发场景中,可能随着业务需求的不断迭代,现有的 API 接口无法支持旧版本的适配,此时如果强制升级服务端的 API 接口将导致客户端旧有功能出现故障。实际上,Web 端是部署在服务器,因此它可以很容易为了适配服务端的新的 API 接口进行版本升级,然而像 Android 端、IOS 端、PC 端等其他客户端是运行在用户的机器上,因此当前产品很难做到适配新的服务端的 API 接口,从而出现功能故障,这种情况下,用户必须升级产品到最新的版本才能正常使用。为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。
      【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口
      【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口

    • 现在,我们可以不改变版本 v1 的查询用户列表的 API 接口的情况下,新增版本 v2 的查询用户列表的 API 接口以满足新的业务需求,此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。“不改变版本 v1 的查询用户列表的 API 接口”主要指的是对于客户端的调用者而言它看起来是没有改变。而实际上,如果业务变化太大,服务端的开发人员需要对旧版本的 API 接口使用适配器模式将请求适配到新的API 接口上。

  • 资源路径:RESTful API 的设计以资源为核心,每一个 URI 代表一种资源。因此,URI 不能包含动词,只能是名词。注意的是,形容词也是可以使用的,但是尽量少用。一般来说,不论资源是单个还是多个,API 的名词要以复数进行命名。此外,命名名词的时候,要使用小写、数字及下划线来区分多个单词。这样的设计是为了与 json 对象及属性的命名方案保持一致。例如,一个查询系统标签的接口可以进行如下设计。
    • 资源的路径应该从根到子依次如下
      /{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}
    • 当一个资源变化难以使用标准的 RESTful API 来命名,可以考虑使用一些特殊的 actions 命名。/{resources}/{resource_id}/actions/{action}
  • 请求方式:通过 GET、 POST、 PUT、 PATCH、 DELETE 等方式对服务端的资源进行操
    • GET:用于查询资源
    • POST:用于创建资源
    • PUT:用于更新服务端的资源的全部信息
    • PATCH:用于更新服务端的资源的部分信息
    • DELETE:用于删除服务端的资源。
【GET】          /users                # 查询用户信息列表
【GET】          /users/1001           # 查看某个用户信息
【POST】         /users                # 新建用户信息
【PUT】          /users/1001           # 更新用户信息(全部字段)
【PATCH】        /users/1001           # 更新用户信息(部分字段)
【DELETE】       /users/1001           # 删除用户信息
  • 查询参数
    • RESTful API 接口应该提供参数,过滤返回结果。其中,offset 指定返回记录的开始位置。一般情况下,它会结合 limit 来做分页的查询,这里 limit 指定返回记录的数量:【GET】 /{version}/{resources}/{resource_id}?offset=0&limit=20
    • orderby 可以用来排序,但仅支持单个字符的排序,如果存在多个字段排序,需要业务中扩展其他参数进行支持:【GET】 /{version}/{resources}/{resource_id}?orderby={field} [asc|desc]
    • 支持查询总数,可以使用 count 字段,count 表示返回数据是否包含总条数,它的默认值为 false:【GET】 /{version}/{resources}/{resource_id}?count=[true|false]
    • 业务场景中还存在许多个性化的参数:【GET】 /v1/categorys/{category_id}/apps/{app_id}?enable=[1|0]&os_type={field}&device_ids={field,field,…}
  • 状态码
  • 异常响应:当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
    "code": "INVALID_ARGUMENT",
    "message": "{error message}",
    "cause": "{cause message}",
    "request_id": "01234567-89ab-cdef-0123-456789abcdef",
    "host_id": "{server identity}",
    "server_time": "2014-01-01T12:00:00Z"
}
  • 请求参数:需要对请求参数进行限制说明;在设计新增或修改接口时,需要在文档中明确告诉调用者哪些参数是必填项,哪些是选填项,以及它们的边界值的限制
  • 响应参数:如果是单条数据,则返回一个对象的 JSON 字符串;如果是列表数据,则返回一个封装的结构体。
  • 完整的案例
【GET】     /v1/users?[&keyword=xxx][&enable=1][&offset=0][&limit=20] 获取用户列表
功能说明:获取用户列表
请求方式:GET
参数说明
- keyword: 模糊查找的关键字。[选填]
- enable: 启用状态[1-启用 2-禁用]。[选填]
- offset: 获取位置偏移,从 0 开始。[选填]
- limit: 每次获取返回的条数,缺省为 20 条,最大不超过 100。 [选填]
响应内容
HTTP/1.1 200 OK
{
    "count":100,
    "items":[
        {
            "id" : "01234567-89ab-cdef-0123-456789abcdef",
            "name" : "example",
            "created_time": 1496676420000,
            "updated_time": 1496676420000,
            ...
        },
        ...
    ]
}
失败响应
HTTP/1.1 403 UC/AUTH_DENIED
Content-Type: application/json
{
    "code": "INVALID_ARGUMENT",
    "message": "{error message}",
    "cause": "{cause message}",
    "request_id": "01234567-89ab-cdef-0123-456789abcdef",
    "host_id": "{server identity}",
    "server_time": "2014-01-01T12:00:00Z"
}
错误代码
- 403 UC/AUTH_DENIED    授权受限
  • HTTP幂等性:指无论调用多少次都不会有不同结果的 HTTP 方法。不管你调用一次,还是调用一百次,一千次,结果都是相同的。
    • HTTP GET 方法:用于获取资源,不管调用多少次接口,结果都不会改变,只是查询数据,不会影响到资源的变化,因此我们认为它幂等,所以是幂等的
    • HTTP POST 方法:是一个非幂等方法,因为调用多次,它会对资源本身产生影响,每次调用都会有新的资源产生
    • HTTP PUT 方法:它直接把实体部分的数据替换到服务器的资源,我们多次调用它,只会产生一次影响,但是有相同结果的 HTTP 方法,所以满足幂等性
    • HTTP PATCH 方法:非幂等的
    • HTTP DELETE 方法:调用一次和多次对资源产生影响是相同的,所以也满足幂等性
    • HTTP GET vs HTTP POST:GET 方式通过 URL 提交数据,数据在 URL 中可以看到;POST 方式,数据放置在 HTML HEADER 内提交。但是,我们现在从 RESTful 的资源角度来看待问题,HTTP GET 方法是幂等的,所以它适合作为查询操作,HTTP POST 方法是非幂等的,所以用来表示新增操作。但是,也有例外,我们有的时候可能需要把查询方法改造成 HTTP POST 方法。比如,超长(1k)的 GET URL 使用 POST 方法来替代,因为 GET 受到 URL 长度的限制。虽然,它不符合幂等性,但是它是一种折中的方案。
    • HTTP POST vs HTTP PUT: POST 表示创建资源,PUT 表示更新资源。
    • HTTP PUT vs HTTP PATCH:我们一般的理解是 PUT 表示更新全部资源,PATCH 表示更新部分资源。首先,这个是我们遵守的第一准则。根据上面的描述,PATCH 方法是非幂等的,因此我们在设计我们服务端的 RESTful API 的时候,也需要考虑。如果,我们想要明确的告诉调用者我们的资源是幂等的,我的设计更倾向于使用 HTTP PUT 方法。
  • 保证接口的幂等性
    • 当通过调用创建实例接口在负载均衡中创建云服务器时,如果遇到了请求超时或服务器内部错误时,客户端可能会尝试重发请求,这时客户端可以通过提供可选参数 ClientToken 避免服务器创建出比预期要多的实例,也就是通过提供 ClientToken 参数保证请求的幂等性。ClientToken 是一个由客户端生成的唯一的、大小写敏感、不超过 64 个 ASCII 字符的字符串。
    • 如果用户使用同一个 ClientToken 值调用创建实例接口,则服务端会返回相同的请求结果,包含相同的 InstanceId。因此用户在遇到错误进行重试的时候,可以通过提供相同的 ClientToken 值,来确保负载均衡只创建一个实例,并得到这个实例的 InstanceId。
    • 如果用户提供了一个已经使用过的 ClientToken,但其他请求参数不同,则负载均衡会返回 IdempotentParameterMismatch 的错误代码。但需要注意的是,SignatureNonce、Timestamp 和 Signature 参数在重试时是需要变化的,因为负载均衡使用 SignatureNonce 来防止重放攻击,使用 Timestamp 来标记每次请求时间,所以再次请求必须提供不同的 SignatureNonce 和 Timestamp 参数值,这同时也会导致 Signature 值的变化。
    • 通常,客户端只需要在 500(InternetError)或 503(ServiceUnavailable)错误、或者无法得到响应结果的情况下进行重试操作。返回结果是 200 时,重试可以得到上次相同的结果,但不会对服务端状态带来任何影响。而对 4xx 的返回错误,通常重试也是不能成功的。
  • CAP 定理:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项
    • 一致性:all nodes see the same data at the same time,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致
    • 可用性:Reads and writes always succeed,即服务一直可用,而且是正常响应时间
    • 分区容错性:the system continues to operate despite arbitrary message loss or failure of part of the system,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务
    • CAP 权衡:对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9,即保证 P 和 A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度;对于涉及到钱财这样不能有一丝让步的场景,C 必须保证。网络发生故障宁可停止服务,这是保证 CA,舍弃 P。貌似这几年国内银行业发生了不下 10 起事故,但影响面不大,报到也不多,广大群众知道的少。还有一种是保证 CP,舍弃 A。例如网络故障是只读不写。
  • BASE 理论:即使无法做到强一致性(Strong Consistency,CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)
    • 基本可用(Basically Available):是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现
    • 软状态(Soft State):是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。
    • 最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况
    • ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。
  • 数据一致性问题:Saga 比两阶段提交更易扩展。在事务可补偿的情况下,相比 TCC,saga 对业务逻辑几乎没有改动的需要,而且性能更高。集中式的 saga 设计解耦了服务与数据一致性逻辑及其持久化设施,并使排查事务中的问题更容易。
  • 最终一致性的实现方案:
  • 微服务与 SOA 的区别:微服务是 SOA 发展出来的产物,它是一种比较现代化的细粒度的 SOA 实现方式
  • 如何拆分服务
    • 生命周期:什么时候创建或停止服务?我们什么时候需要将它们分开?
    • 服务实现:我们应该在每个服务中使用哪些工具、语言和架构?
    • 系统架构:服务如何引导他人?开发人员如何了解?
    • 数据架构:服务之间如何共享数据?
    • 变更过程:什么时候可以改变服务?部署和 QA 的工具和过程?
    • 团队管理:谁在哪个团队服务?每个团队负责什么?团队成员做了什么?
    • 人事管理:人员如何被雇用和解雇?员工如何激励和奖励?
    • 安全管理:我们如何降低安全事故的风险?需要做些什么来改善整个系统的安全性?
    • 采购过程:可以购买什么软件?使用开源软件需要哪些保护?
  • 微服务如何进行数据库管理:分布式数据管理(CRUD),需要采用事件驱动架构(event-driven architecture):当某件重要事情发生时,微服务会发布一个事件,当订阅这些事件的微服务接收此事件时,就可以更新自己的业务实体,也可能会引发更多的事件发布,让其他相关服务进行数据更新,最终实现分布式数据最终一致性。可以使用事件来实现跨多服务的业务交易。交易一般由一系列步骤构成,每一步骤都由一个更新业务实体的微服务和发布激活下一步骤的事件构成。
    • 事件驱动架构之分布式数据更新:可靠的事件投递和避免事件的重复消费
      • 可靠的事件投递:每个服务原子性的完成业务操作和发布事件,消息代理确保事件投递至少一次(at least once);
      • 避免事件重复消费则要求消费事件的服务实现幂等性
        • 实现事件投递操作原子性:使用本地事务发布事件(在本地建立一个 EVENT 表,此表在存储业务实体数据库中起到消息列表功能)、使用事件源(使用以事件中心的数据存储方式来保证业务实体的一致性)
        • 避免事件重复消费:需要消费事件的服务实现服务幂等
    • 事件驱动架构之分布式数据查询:借助物化视图(包括一个查询结果的数据库对像,它是远程数据的的本地副本,或者用来生成基于数据表求和的汇总表)和命令查询职责分离技术(使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来),使用事件来维护不同微服务拥有数据预连接(pre-join)的物化视图
  • 应对微服务的链式调用异常:服务端可能会在多个微服务之间产生一条链式调用,并把整合后的信息返回给客户端。在调用过程中,如果某个服务宕机或者网络不稳定可能造成整个请求失败。因此,为了应对微服务的链式调用异常,我们需要在设计微服务调用链时不宜过长,以免客户端长时间等待,以及中间环节出现错误造成整个请求失败。此外,可以考虑使用消息队列进行业务解耦,并且使用缓存避免微服务的链式调用从而提高该接口的可用性。
  • 快速追踪与定位问题:一种比较好的方案是,当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。其中,code 字段用来表示某类错误的错误码,在微服务中应该加上“{biz_name}/”前缀以便于定位错误发生在哪个业务系统上。
  • 微服务的安全:OAuth 是一个关于授权的开放网络标准,它允许第三方网站在用户授权的前提下访问用户在服务商那里存储的各种信息。实际上,OAuth 2.0 允许用户提供一个令牌给第三方网站,一个令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的资源。用户在客户端使用用户名和密码在用户中心获得授权,然后客户端在访问应用是附上 Token 令牌。此时,应用接收到客户端的 Token 令牌到用户中心进行认证。一般情况下,access token 会添加到 HTTP Header 的 Authorization 参数中使用,其中经常使用到的是 Bearer Token 与 Mac Token。其中,Bearer Token 适用于安全的网络下 API 授权。MAC Token 适用于不安全的网络下 API 授权。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值