凤凰架构-远程服务、事务1
远程服务
RPC 远程调用
rpc 本质是 将本地方法调用的思路迁移到远程方法,同时为了解决异构机器之间的问题,需要设计不同的协议或者接口。
A1 :RPC 是怎么被提起的?它遇到什么困难?
远程服务将计算机工作从单机扩展到网络,从本地延展到远程。不可避免地,如何在 caller, callee 不是同一进程空间的条件下仍然能够像普通调用函数一样执行。如果是本机,可以借助共享内存,让多个进程同时访问一个地址空间;跨机器的话,利用 socket 让函数参数经过网络协议栈,打包解包,计算校验和,维护序列号和应答号。
设计之初,rpc 还有很多的问题亟待解决:
- 跨机器之间的网络通信成本(rpc 不等于 ipc)
- 进程间交换数据
- 机器之间支持的不同指令集、不同编程语言
总的来说,需要解决三个基本问题:
- 如何表示数据(序列化与反序列化,跨语言跨机器之间用约定好的中立数据格式传输:JSON XML)
- 如何传递数据(应用层协议交换信息协议,解决类似超时、异常、安全、认证、事务等需求)
- 如何表示方法(统一的跨语言标准,能够找到准确的 callee)
总结
TODO
REST 设计
REST 风格是一种面向资源编程思想,让开发者专注于抽象资源对应到 HTTP 协议的操作中,强调统一接口和面向资源而非服务或者动作,具有很强的可读性和统一性,但性能和处理逻辑上有欠缺。
A1 :如何确认一个系统是不是 REST 风格?
REST 风格有如下特点:
- 客户端无状态原则:“REST 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。” 客户端把历史的状态信息隐藏到每次的请求(头部?)中,传递给服务端。大型的系统要求传递的状态可能是巨大的,所以服务端本地的状态缓存也有必要.
- 与 Http 1.1 协议的的高度绑定:这两个的核心思想都是 “ 面向资源” ,设计了 GET POST PUT DELETE PUTCH 这样对资源的操作行为。把服务中的实体抽象出来成为一种资源,对资源进行设计统一的接口
- 建议:对资源的操作通过 ID 进行;通过返回消息的超文本驱动状态的转变,指向下一个操作的接口
- ……
A2:REST 的争议?
- 资源粒度大小:REST 缺乏对资源进行“部分”和“批量”的处理能力:限制了资源的大小,每次请求返回的是资源对象的所有属性信息,可能并不需要这么多,然而 http 协议没有对请求资源的结构化描述,所以只能在 endpoint 后面加上参数或者表单上加上字段限制;
- 批量获取资源的能力:批量则是指如果对于一个重复性的 request 请求,为了避免大量的传输请求,服务端要设计一个批量的 ”资源“ (可能已经脱离了面向一个实体类型资源的讨论,而是 抽象一种批量处理的动作,称之为”事务“ 的资源 )来对这个批量的资源请求。
A3:对比一下 rpc,rest?
可以看到 rest 风格主要是关注资源怎么被高效地抽象出来,能够覆盖更多的操作,也就是更加的 restful 风格。它把设计的统一接口固定了,绑定的协议也固定了,HTTP 规范是优点也是缺点。开发者能够不用关心更多的细节、接口的繁琐;而 rpc 提供了更自由、更灵活的配置,当然它的代价也更大。
事务处理
本地事务
实现原子性和持久性
原子性指一个事务要么都成功,要么都失败,不存在中间状态;持久性指事务生效后,不会因为其他的原因导致修改失效。然而这和实际的磁盘读写和不符的,磁盘读写可能存在只写了一半出现崩溃的情况。所以我们需要辅助机制来帮助实现这两个目标。
“基于语义的恢复与隔离算法”ARIES 提出了日志辅助的方法,它的核心思想是,对每一个事务的读写都要经过日志,状态包括 start, commit, endlog 状态,读写数据要在事务 commit 之后才能进行原子性的读写,持久化则可以扫描日志异步持久化到磁盘以提高 IO。
崩溃时间点可以分为:
- 未提交事务,写入后崩溃:程序还没修改完三个数据,但数据库已经将其中一个或两个数据的变动写入磁盘,此时出现崩溃,一旦重启之后,需要回滚撤销已修改的数据
- 已提交事务,写入前崩溃:程序已经修改完三个数据,但数据库还未将全部三个数据的变动都写入到磁盘,此时出现崩溃,一旦重启之后,根据日志继续把所有数据的改动写入磁盘
Write-Ahead Logging 先将何时写入变动数据,按照事务提交时点为界,划分为 FORCE 和 STEAL 两类情况。
- FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。
- STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。
例子分析
1 | |
从上一次的 checkpoint 开始扫描,如果发现 commit 但未 end 日志,加入到 redo 日志,异步完成重做;如果发现 start 但是未 commit,加入到 undo log 撤销操作。
WAL 崩溃恢复进行下面三个阶段的操作:
- 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合,这个集合至少会包括 Transaction Table 和 Dirty Page Table 两个组成部分。
- 重做阶段(Redo):该阶段依据分析阶段中产生的待恢复的事务集合来重演历史(Repeat History),具体操作为:找出所有包含 Commit Record 的日志,将这些日志修改的数据写入磁盘,写入完成后在日志中增加一条 End Record,然后移除出待恢复事务集合。
- 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务,它们被称为 Loser,根据 Undo Log 中的信息,将已经提前写入磁盘的信息重新改写回去,以达到回滚这些 Loser 事务的目的。
实现隔离性
隔离性是要求不同事务的并发读写互相不干扰,一般使用加锁的方式实现隔离。
- 写锁又称排他锁,如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁
- 读锁,多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。
- 范围锁,对一个范围加排他锁,不能在这个范围内修改、新增、删除数据
隔离分为 可串行化、可重复读、读已提交、读未提交 三个主要的级别,差别在于加锁的范围和时机不同,达到了不同的效果。本质还是依靠上面的三种类型的锁实现隔离。在思考下面几种隔离级别带来的问题时,要特别注意对象是不同的事务,在内部事务没有 commit 的情况下,锁是否还生效导致其他的事务会不会读/写同一范围的数据,而一个事务包含的 sql 语句是很多的。
- 可串行化:并发事务的读写可以等效为某一种串行的读写请求。对事务所有读、写的数据全都加上读锁、写锁和范围锁
- 可重复读:对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。带来的问题是幻读,如果在两次范围读之间插入写,那么范围查询是不同的集合。MySQL 的引擎 Innodb 默认的隔离级别是可重复读,在只读事务的条件下可以避免幻读
- 读已提交:对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。带来了不可重复读问题,缺乏贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化,在下一次读取可能会读到其他事务写的数据。
- 读未提交:对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。意味着事务写后回滚,可能会有别的事务在其中读到了回滚前的数据,造成脏读。