• 凤凰价格——架构师的视角


    访问远程服务

    远程服务调用

    进程间通信 (Inter-Process Communication,IPC) 的办法

    • 管道(Pipe)或者具名管道(Named Pipe): 传递少量的字符流或字节流, 普通管道只用于有亲缘关系进程(由一个进程启动的另外一个进程)间的通信,具名管道摆脱了普通管道没有名字的限制,除具有管道所有的功能外,它还允许无亲缘关系进程间的通信
    • 信号(Signal):通知目标进程有某种事件发生
    • 信号量(Semaphore): 两个进程之间同步协作手段
    • 消息队列(Message Queue):可以传送大量信息,但是实时性相对受限
    • 共享内存(Shared Memory):允许多个进程访问同一块公共的内存空间
    • 套接字接口(Socket): 可用于不同机器之间的进程通信

    RPC的三个基本问题

    • 如何表示数据:将交互双方所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输,将数据流转换回不同语言中对应的数据类型来进行使用(比如序列化与反序列化)
    • 如何传递数据:通过网络,在两个服务的 Endpoint 之间相互操作、交换数据(应用层协议)。
    • 如何确定方法:不同语言如何表示同一方法

    不同的RPC

    没有一个RPC能完美满足上面三点,所以出现了不同的RPC

    • 朝着面向对象发展:RMI,这条线有一个别名叫做分布式对象
    • 朝着性能发展(序列化效率和信息密度):gRPC (基于 HTTP/2 的,支持多路复用和 Header 压缩)和 Thrift(基于传输层的 TCP 协议来实现,省去了额外应用层协议的开销)
    • 朝着简化发展,代表为 JSON-RPC(协议的简单轻便,接口与格式都更为通用)

    REST 设计风格

    RPC 面向过程调用,REST 面向资源调用

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

    1. 服务端与客户端分离(Client-Server):将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性
    2. 无状态(Stateless):客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑
    3. 可缓存(Cacheability):运作良好的缓存机制可以减少客户端、服务器之间的交互
    4. 分层系统(Layered System):客户端一般不需要知道是否直接连接到了最终的服务器
    5. 统一接口(Uniform Interface):
    6. 按需代码(Code-On-Demand):客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度

    优点和不足

    优点

    1. 降低的服务接口的学习成本:对资源的标准操作都映射到了标准的 HTTP 方法上去
    2. 资源天然具有集合与层次结构:以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。GET /users/icyfenix/cart/2
    3. REST 绑定于 HTTP 协议。

    不足与争议

    1. 面向资源的编程思想只适合做 CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑:面向资源的编程思想与另外两种主流编程思想只是抽象问题时所处的立场不同,只有选择问题,没有高下之分
    2. REST 与 HTTP 完全绑定,不适合应用于要求高性能传输的场景中:面向资源编程与协议无关,但是 REST的确依赖着 HTTP 协议的标准方法、状态码、协议头等各个方面,对于需要直接控制传输,如二进制细节、编码形式、报文格式、连接方式等细节的场景中,REST 确实不合适,这些场景往往存在于服务集群的内部节点之间
    3. REST 不利于事务支持:如果放弃刚性事务,使用 REST 肯定不会有什么阻碍
    4. REST 没有传输可靠性支持
    5. REST 缺乏对资源进行“部分”和“批量”的处理能力

    事务处理

    本地事务

    ARIES理论(Algorithms for Recovery and Isolation Exploiting Semantics,ARIES): 基于语义的恢复与隔离算法

    实现原子性和持久性

    由于写入中间状态与崩溃都是无法避免的,为了保证原子性和持久性,就只能在崩溃后采取恢复的补救措施,这种数据恢复操作被称为“崩溃恢复”(Crash Recovery)

    为了能够顺利地完成崩溃恢复,必须将修改数据这个操作所需的全部信息,以顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。日志记录落盘——》数据库在日志看见Commit Record——》对数据进行修改——》日志中加入一条End Record表示事务已完成持久化——》这种实现方式被称为 Commit Logging

    Shadow Paging
    先将数据复制一份副本,保留原数据,修改副本数据。当事务成功提交,所有数据的修改都成功持久化之后,最后一步是去修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本,最后的“修改指针”这个操作将被认为是原子操作。

    Commit Logging的缺点

    • 所有对数据的真实修改都必须发生在事务提交以后,即日志写入了 Commit Record 之后。

    ARIES提出“Write-Ahead Logging” 提前写入

    Write-Ahead Logging 先将何时写入变动数据,按照事务提交时点为界,划分为 FORCE 和 STEAL 两类情况。

    • FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。
    • STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。

    **Commit Logging 允许 NO-FORCE,但不允许 STEAL。**因为假如事务提交前就有部分变动数据写入磁盘,那一旦事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。

    **Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL。**它给出的解决办法是增加了另一种被称为 Undo Log 的日志类型,当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值,等等。以便在事务回滚或者崩溃恢复时根据 Undo Log 对提前写入的数据变动进行擦除。Undo Log 现在一般被翻译为“回滚日志”,此前记录的用于崩溃恢复时重演数据变动的日志就相应被命名为 Redo Log,一般翻译为“重做日志”。由于 Undo Log 的加入,Write-Ahead Logging 在崩溃恢复时会执行以下三个阶段的操作。

    • 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合,这个集合至少会包括 Transaction Table 和 Dirty Page Table 两个组成部分。
    • 重做阶段(Redo):该阶段依据分析阶段中产生的待恢复的事务集合来重演历史(Repeat History),具体操作为:找出所有包含 Commit Record 的日志,将这些日志修改的数据写入磁盘,写入完成后在日志中增加一条 End Record,然后移除出待恢复事务集合。
    • 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务,它们被称为 Loser,根据 Undo Log 中的信息,将已经提前写入磁盘的信息重新改写回去,以达到回滚这些 Loser 事务的目的。

    在这里插入图片描述

    实现隔离性

    • 写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。

    • 读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。

    • 范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。SELECT * FROM books WHERE price < 100 FOR UPDATE;

    • 可串行化(Serializable):对事务所有读、写的数据全都加上读锁、写锁和范围锁即可做到可串行化(“即可”是简化理解,实际还是很复杂的,要分成 Expanding 和 Shrinking 两阶段去处理读锁、写锁与数据间的关系,称为Two-Phase Lock,2PL)

    • 可重复读(Repeatable Read):对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁,相比串行化会存在幻读问题(事务执行过程中,两个完全相同的范围查询得到了不同的结果集),没有范围锁来禁止在该范围内插入新的数据。

    Innodb在只读事务可以完全避免幻读,读写事务中,MySQL 仍然会出现幻读问题

    • 读已提交(Read Committed):对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。,会引起不可重复读问题

    • 读未提交(Read Uncommitted):对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。,会引起脏读问题

    多版本并发控制 MVCC

    针对“一个事务读+另一个事务写”的隔离问题,MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本

    版本——》数据库中每一行记录都存在两个看不见的字段:CREATE_VERSION 和 DELETE_VERSION,这两个字段记录的值都是事务 ID,事务 ID 是一个全局严格递增的数值,然后根据以下规则写入数据。

    • 插入数据时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空。
    • 删除数据时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空。
    • 修改数据时:将修改数据视为“删除旧数据,插入新数据”的组合,即先将原有数据复制一份,原有数据的 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空。复制出来的新数据的 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空。

    如有另外一个事务要读取这些发生了变化的数据,将根据隔离级别来决定到底应该读取哪个版本的数据。

    • 隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。
    • 隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。

    另外两个隔离级别都没有必要用到 MVCC,因为读未提交直接修改原始数据即可,其他事务查看数据的时候立刻可以看到,根本无须版本字段。可串行化本来的语义就是要阻塞其他事务的读取操作,而 MVCC 是做读取时无锁优化的,自然就不会放到一起用。

    MVCC 是只针对“读+写”场景的优化,如果是两个事务同时修改数据,即“写+写”的情况,那就没有多少优化的空间了,此时加锁几乎是唯一可行的解决方案,稍微有点讨论余地的是加锁的策略是“乐观加锁”(Optimistic Locking)还是“悲观加锁”(Pessimistic Locking)。前面笔者介绍的加锁都属于悲观加锁策略,即认为如果不先做加锁再访问数据,就肯定会出现问题。相对地,乐观加锁策略认为事务之间数据存在竞争是偶然情况,没有竞争才是普遍情况,这样就不应该在一开始就加锁,而是应当在出现竞争时再找补救措施。这种思路被称为“乐观并发控制”(Optimistic Concurrency Control,OCC),没有必要迷信什么乐观锁要比悲观锁更快的说法,这纯粹看竞争的剧烈程度,如果竞争剧烈的话,乐观锁反而更慢。

  • 相关阅读:
    八股八股八
    2022中国大健康展,山东大健康展,济南健康展,健康产业展
    解决Java异常java.sql.SQLException: Unexpected exception encountered during query.
    NumPy基础知识
    如何配置nginx的转发?
    MPC入门与Matlab实现
    出现 conda虚拟环境默认放在C盘 解决方法
    C/C++的刷题练习之牛客网,一个友好的网站
    DataFrame.empty 与 DataFrame is None 的区别是?
    使用宝塔面板部署商城项目到云服务器的案例
  • 原文地址:https://blog.csdn.net/lxsxkf/article/details/125522794