开篇诗两句:

莫道谗言如浪深,莫言迁客似沙沉。

千淘万漉虽辛苦,吹尽狂沙始到金。 ——刘禹锡《浪淘沙》



书籍链接《凤凰架构

RPC vs REST

RPC

远程服务调用(Remote Procedure Call,RPC)的目的,就是为了让计算机能够跟调用本地方法去调用远程方法。而REST本质上并不是一种协议,只能说是一种风格。

RPC在思想上是面向过程的编程,各种类型的RPC框架本质上都是在解决一下三个基本问题:

  • 如何表示数据:数据包括传递给方法的参数值,以及方法的返回值,这些数据从一个进程传递给另一个进程,都需要有特定的表示方式。常规的就是序列化和反序列化操作。

  • 如何传递数据:如何通过网络进行交换数据,这里的传递数据不是简单的投递个序列化数据流就可以了,需要有很多额外的操作,例如异常,超时,安全,授权,事务等。

  • 如何确定方法:需要针对”如何表示同一个方法”,”如何找到对应的方法”提供跨语言的标准。

当前RPC框架主流的发展方向:

  • 面向对象:希望能够在分布式系统中能够进行跨进程的面向对象编程,代表为 RMI,.NET Remoting。
  • 性能:追求RPC框架的性能,其中决定RPC框架性能的因素主要有两个:序列化效率和信息密度。序列化效率可以理解成序列化之后的容量越小越好,信息密度可以理解成有效荷载(Payload)占比越高越好。代表为 gRPC 和 Thrift。
  • 简化:通过牺牲功能和效率,换取协议的简单轻便,接口和格式都更加通用,代表为 JSON-RPC。

REST

REST(Representational State Transfer)最初源于Roy Thomas Fielding 在 2000 年发表的博士论文《Architectural Styles and the Design of Network-based Software Architectures》。

Fielding认为,一套理想的,完全满足REST风格的系统应该满足一下六个原则:

  1. 服务端与客户端分离:将客户端关注的逻辑与数据存储相关逻辑分离开,提高跨平台的移植性。
  2. 无状态:是REST的一条核心原则,REST希望服务端不维护状态。
  3. 可缓存:无状态服务虽然提升了系统的可见性,可靠性和可伸缩性,但是降低了系统的网络性,原本只需要一次或者少量请求就可以完成的功能,无状态服务可能需要多次请求才能完成。为了解决这个矛盾,REST允许客户端将服务端的应答缓存起来。
  4. 分层系统:客户端不需要知道是否直接链接到了最终服务器上。
  5. 同一接口:REST的另一条核心原则,REST希望开发者面向资源编程,将重点放在系统该有哪些资源而不是有哪些行为。
  6. 按需代码:按需代码被 Fielding 列为一条可选原则,它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。

REST提出以资源为主体,带来了不少好处:首先便是降低了服务接口的学习成本,资源天然具有集合和层次结构,此外,由于REST是绑定HTTP协议的,所以如何解决数据传递问题已经不用考虑了,当然也正是绑定了HTTP协议,也带来了一些缺点,对于HTTP协议不支持的特性,REST是没有解决办法的。

REST与HTTP绑定不适用于高性能传输场景,没有传输可靠性支持,需要实现幂等,同时缺乏对资源的批量处理能力。

事务处理

说到事务,一定会提到事务的ACID特性:

(1)原子性(Atomic),(2)一致性(Consistency),(3)隔离性(Isolation),(4)持久性(Durability)

C是事务所要实现的目的,A,I,D是实现C的手段。

事务的概念起源于数据库系统,但是当前已经不再局限于数据库本身了。对于数据库,缓存,消息队列以及分布式存储都可能会用到事务,但是对于不同的场景,事务所表示的含义也有所不同。

  • 一个服务只使用一个数据源时,保证一致性的最经典做法就是同过A,I,D的手段。当多个事务并发执行时,并发事务的执行时间顺序完全是由数据源控制,这种事务间一致性称为”内部一致性”。
  • 一个事务使用多个数据源时,保证多个数据源之间的数据一致性就相对复杂许多。这种数据一致性称为”外部一致性”。

本地事务

本地事务是单服务单数据源的场景,是最基础的事务解决方案。经典的手段就是保证事务的ACID特性。

保证原子性和持久性

原子性所要保证的是事务的多个操作,要么同时生效,要么都不生效。持久性所要保证的是一旦事务提交,无论任何原因都不能导致已经提交的数据撤销或者丢失。

由于写入时的中间状态和崩溃都是无法避免的,为了保证原子性和持久性,就只能在崩溃之后采用恢复的方式,这种数据恢复操作称为”崩溃恢复”(Crash Recovery或Failure Recovery 或 Transaction Recovery)

Commit Logging

当前的主流方式是通过日志来实现事务的原子性和持久性。一种方式是将修改操作通过顺序追加的方式记录到磁盘中,只有所有的日志记录全部落盘之后,才会提交事务,之后才会对数据进行真正的修改,这种事务实现的方式称为”Commit Logging”(提交日志)。

但是Commit Logging方式存在一个缺陷就是所有的数据修改都是在事务提交之后进行的,所以出现的问题就是即使系统磁盘IO充足,即使事务操作数据量非常庞大,占用大量内存,都不允许在事务提交之前修改数据,对提升数据库性能的十分不利。

Write-Ahead Logging

为了解决上面Commit Logging的缺陷,Write-Ahead Logging方式增加了Undo Log日志,变动数据之前必须先写Undo Log日志。采用这种方式当执行崩溃恢复时,主要分为三个阶段,简单概括就是(1)分析需要恢复的事务集合(2)已经提交的事务,重演历史(3)未提交的事务,根据Undo Log进行回滚操作。

实现隔离性

当前数据库的提供的锁主要分3中:(1)读锁(2)写锁(3)范围锁(这个范围内的数据不能被写入)

数据库提供的四种事务隔离级别:

(1)可串行化:简单理解就是对所有的读写数据操作都加上读锁,写锁,范围锁,是强度最高的隔离级别。但缺点就是严重影响性能。

(2)可重复读:可串行化的下一个隔离级别。对事务涉及的数据加读锁和写锁,但不加范围锁。带来的问题就是会有幻读的情况(范围查询到不同的结果集)。

(3)读已提交:可重复读的下一个隔离级别,事务涉及的数据一直加写锁,但是只有在查询数据的时候添加读锁,查询完成就释放读锁,比可重复读增加的问题就是不可重复读

(4)读未提交:读已提交的下一个隔离级别,事务涉及的数据只加写锁,完全不加读锁,增加的问题就是脏读

除了上面四种隔离级别,其实还有完全不隔离的方式,就是连写锁也不添加,但是带来的问题就是会出现脏写的问题,这种方式甚至连原子性都不能保证。

除了加锁的方式来实现事务隔离外,还有一种名未”多版本并发控制的方式”(Multi-Version Concurrency Control,MVCC)来实现事务隔离,这里不详细描述了。MVCC也只是针对”读+写”的场景进行优化。对于”写+写”的方式,加锁可能是唯一的办法了。

全局事务

单个服务使用多个数据源的场景,比较典型的是”两段式提交”(2PC)和”三段式提交”(3PC)协议。

两阶段提交(2PC)

2PC的过程分为两个阶段:

  • 准备阶段:参与者做好提交事务的准备则返回prepared,否则,恢复non-prepared。两阶段提交的准备阶段操作很重,需要在重做日志中记录所有的事务操作,并且完成数据的持久化,只是不进行事务最后的提交工作,仍然需要持有锁,保证事务对其他事务的隔离状态。
  • 提交阶段:当所有的参与者返回prepared之后,协调者将本地事务持久化为Commit,然后向所有参与者发送Commit指令,参与者接收到Commit指令之后,进行事务提交,此时Commit指令的处理是非常轻量的,只是增加一条Commit Record,因为数据持久化操作已经在准备阶段完成了。如果有参与者返回non-prepared之后,协调者将本地事务持久化为Abort,然后向所有参与者发送Abort指令,参与者接收到Abort指令之后,执行回滚操作,仍然是比较重的操作。

2PC能够保证一致性的前提条件是:(1)必须假设网络在提交阶段的短时间内是可靠的 (2)必须假设失联节点最终能够恢复。因为在提交阶段出现网络问题或者机器宕机,可能出现部分参与者提交了事务,而部分参与者由于网络问题无法接收到提交请求,由于2PC不会改变已经提交或者回滚的结果,所以只能等待崩溃节点恢复。

2PC存在的三个明显的缺点:(1)单点问题(协调者宕机,参与者必须一直等待) (2)性能问题(三次持久化,必须等待执行最慢的参与者执行完成) (3)一致性风险

三阶段提交(3PC)

3PC是为了缓解2PC准备阶段的缺陷(单点问题和性能问题),将原本的提交阶段拆分成两个阶段CanCommit和PreCommit,最后的提交阶段称为DoCommit。由于原本的2PC中准备阶段是重负载阶段,所以增加一个CanCommit阶段来让各个参与者评估是否能够完成事务操作,降低回滚操作带来重负载操作的风险。

在需要事务回滚的场景重,3PC比2PC性能要好很多。但是性能方面仍然很差,甚至由于多了一次请求,性能比2PC还要差一些,同时2PC中的数据一致性问题仍然没有解决,甚至加重了产生一致性的风险。3PC中为了解决单点问题,当参与者长时间接收不到协调者的DoCommit消息,会默认提交事务,带来的问题就是当协调者发送Abort命令时,部分参与者没有收到请求,导致错误的提交了事务而不是回滚事务,进而导致数据的不一致。

分布式事务

事务是保证所有的操作要么都成功,要么都失败,事务的特点在于操作失败可以回滚。分布式一致性协议重点是保障数据存储的可靠性,分布式事务和分布式数据一致性协议都是保证分布式数据一致性的手段。

CAP原理:(1)C一致性 (2)A可用性 (3)P分区容错性

由于在分布式系统中,同时满足3个特性是不可能的,所以目前绝大多数的分布式系统都是在CP和AP中进行抉择。由于分布式系统中保证CAP,ACID这种”强一致性”是非常困难的,所以人们针对一致性的要求开始降级,开始追求所谓的”最终一致性”。从追求ACID这种”刚性事务”转变为追求”柔性事务”。

BASE原理:基本可用性(Basically Available)、柔性事务(Soft State)和最终一致性(Eventually Consistent)

当前几种”柔性事务”的实现方式:

  1. 消息队列:最大努力交付,对没有确认成功的消息自动重发。缺点,完全没有任何事务隔离性
  2. TCC事务:Try-Confirm-Cancel,最大努力交付 (1)Try,冻结资源,保证事务隔离性 (2)Confirm,使用Try阶段的资源,直接执行事务逻辑,需要保证幂等 (3)Cancel,释放Try阶段的资源,取消执行,需要保证幂等。缺点,业务侵入性很强。
  3. SAGA:将一个大的分布式事务拆成一些列本地事务。如果分布式事务正常提交与一系列子事务顺序提交结果等价(最终一致性)。事务恢复策略分为两种:(1)正向提交,重试失败子事务,最大努力交付 (2)反向恢复,回滚所有已提交事务,最大努力交付。这种拆分的方式会降低事务的隔离性。