敏捷开发与DevOps的对比

敏捷开发与DevOps的区别

敏捷与 DevOps 之间的主要区别在于:敏捷是关于如何开发和交付软件的哲学,而 DevOps 则描述了如何通过使用现代工具和自动化流程来持续部署代码。

敏捷宣言

如果软件开发人员是敏捷的,他们的行为方式与敏捷宣言中定义的价值观和原则相一致。

敏捷宣言于 2001 年由软件开发领导者撰写并签署,它定义了敏捷从业者必须遵循的十二项原则和四项基本价值观,包括:

  • 个体和互动高于流程和工具
  • 工作的软件高于全面的文档
  • 客户合作高于合同谈判
  • 响应变化高于遵循计划

虽然敏捷宣言对软件开发社区的重要性不言而喻,但它的篇幅却非常短。 总共不到500字。

除了坚持将软件持续将会给客户是敏捷的最高优先级之外,宣言并没有提供任何简明的指导,也没有推荐任何可以遵循的具体流程。 这纯粹是一种哲学练习。

如何定义 DevOps?

与敏捷相比,DevOps 有定义文档。 DevOps 没有普遍接受的定义。

我们甚至不清楚 DevOps 是什么时候进入公共词典的,尽管许多人指出 John Allspaw 和 Paul Hammond 在 2009 年 Velocity 大会上的演讲《每天10+个部署:Flickr上的开发和运维合作》是它的开端。许多人也认为 Gene Kim 的《凤凰计划》一书也是 DevOps 普及的一个推动因素。

比较 敏捷开发 DevOps
启始时间 2001 2007
创始人 J约翰·克恩、马丁·福勒等人 Flickr 的 John Allspaw 和 Paul Hammond 以及 Gene Kim 的凤凰计划
最高优先级 软件持续交付 软件持续部署
创始神器 敏捷宣言 《每天10+个部署:Flickr上的开发与运维合作》
实施框架 Scrum、看板、ScrumBan、精益、XP CAMS, CALMS, DORA
备选方案 瀑布开发模型 竖井模式的开发与部署
团队规模 由 10 人以下的小团队使用 作为公司范围内的战略实施
范围 专注于单个应用程序的开发 公司范围内的软件部署方法

在 2009 年的 Velocity 演示中,Allspaw 和 Hammond 描述了他们对的软件测试例程的信心怎样使得在开发和运营之间建立了诸多信任,该公司甚至已经实施了一个流程,可以每天将代码自动部署到生产环境中 10 次以上。自动化这样的事情在当时被认为是石破惊天的。

自 2009 年以来,软件开发行业发生了许多变化,但 DevOps 的基石仍然是:

  • 开发和运维团队之间的信任和协作
  • 严重依赖全面的软件测试例程
  • 集成现代工具以简化开发和运营任务
  • 无需任何人工干预或检查点即可自动部署到生产中

敏捷和 DevOps 文化

尽管许多人争论 DevOps 到底是什么,但大多数定义都包含“文化”一词。这就引出了一个问题:什么是文化?

一般来说,文化可以定义为任何一群志同道合的人,他们使用一套通用的工具并遵循一套可重复的流程。

从本质上讲,文化可以归结为以下三点:

  • 流程
  • 及他们所用的工具

敏捷和 DevOps 的目标和成果是紧密相连的

DevOps 工具和流程

在 DevOps 的世界中,从业者使用的流行工具包括:

  • Git 和 GitHub
  • Docker 和容器
  • Jenkins 其他 CI 工具
  • 用于编排的 Kubernetes
  • Chef 和 Puppet
  • 静态代码分析
  • Terraform
  • 公有云

使用这些产生的过程是代码的持续集成和部署(CI/CD)。

敏捷和 DevOps 有何相似之处?

从事 DevOps 的人有什么独特之处?

数字化转型成功的先决条件是什么?

这正是敏捷和 DevOps 的交汇点。

为了使 DevOps 发挥作用,所有 DevOps 从业者都必须接受敏捷思维。

所有 DevOps 从业者都相信:

  • 手动任务的自动化和未完成工作的重要性
  • 与积极的个人组成的自组织团队合作的重要性
  • 将软件持续交付给客户作为最高优先级

这些要点中的每一个都直接映射到敏捷宣言中列出的十二条原则之一。

DevOps 无限循环显示了敏捷团队所采用的迭代开发过程

如何将敏捷和 DevOps 结合起来?

DevOps 人员必须是敏捷的。

要正确执行 DevOps,DevOps 从业者必须接受敏捷思维。

敏捷和 DevOps 从业者不仅有共同的思维模式,而且他们的目标也很一致。

DevOps 过渡的最终目标是将工作代码完全自动化部署到生产中。这代表了 DevOps 的完整启示。

敏捷宣言毫不含糊地指出,其最高优先级是持续向客户交付软件。

敏捷和 DevOps 都相信构建软件、确保透明度和促进可持续发展的最佳方式是将可工作的软件交到客户手中。

敏捷和 DevOps 有着完全相同的目标,就是让开发和运营团队使用现代工具并遵照流程,将软件尽快交到客户手中。

敏捷与 DevOps 的异同

总结一下,下面是 DevOps 和敏捷之间最常见的区别和相似之处:

  1. 敏捷由敏捷宣言定义,而 DevOps 没有普遍接受的定义

  2. DevOps 定义了一种工作文化,而敏捷是一种软件开发理念

  3. 敏捷的最高优先级是持续交付,而 DevOps 则是持续部署

  4. DevOps 坚持所有手动任务的自动化,而敏捷则重视“未完成的工作量”

  5. DevOps 从业者拥护敏捷思维,而敏捷则要求参与者自组织和激励

敏捷和 DevOps 相结合

敏捷和 DevOps 并不是相互冲突的概念。事实上,情况恰恰相反。

拥有敏捷思维并接受 DevOps 文化的人都有一个共同的目标,那就是向客户持续交付和部署有价值的软件。

要正确执行 DevOps,所有参与者都必须接受敏捷思维。只有这样,基于 DevOps 的数字化转型才会成功。


【注】本文译自:Agile vs DevOps: What’s the difference?

架构师软考学习笔记【1】:渐入佳境(课程基础)

渐入佳境(课程基础)

技术类-计算机基础知识

第1章 计算机基础

1-1 计算机基础:冯·诺依曼计算机

冯·诺依曼计算机五大特征:
  1. 计算机由五大部件组成:
    • 控制器
    • 运算器
    • 存储器
    • 输入设备
    • 输出设备
      注:

      • 后期"控制器"和"运算器"被统一到CPU中
      • 存储器可分为内部存储器和外部存储器
  2. 指令和数据以同等地位存于存储器
  3. 指令和数据用二进制表示
  4. 指令由操作码和地址码组成
  5. 存储程序
  6. 以运算器为中心

    1-2 现代计算机硬件图和CPU(一)

    计算机主要硬件组成:
    • 主机:
    • 运算器ALU
    • 控制器
    • 存储器:
    • 主存
    • 辅存
    • I/O设备:
    • 输入设备
    • 输出设备
      注:CPU构成:运算器ALU、控制器、主存

      1-3 现代计算机硬件图和CPU(二)

      CPU主要构成
    • 运算器的组成:
      ① 自述逻辑单元ALU:数据的算术运算和逻辑运算;
      ② 累加寄存器AC:通用寄存器,为ALU提供一个工作区,用来缓存数据;
      ③ 数据缓冲寄存器DR:写内存时,暂存指令或数据;
      ④ 状态条件寄存器PSW:存状态标志与控制标志。
    • 控制器的组成:
      ① 程序计数器PC:存储下一条要执行指令的地址;
      ② 指令寄存器IR:存储即将执行的指令;
      ③ 指令译码器ID:对指令中的操作码字段进行分析解释;
      ④ 地址寄存大AR:用来保存当前CPU所访问的内在单元的地址;
      ⑤ 时序部件:提供时序控制信号。

      1-4 主存

    • 存储器的基本单位是存储单元,一般以8位二进制为一个存储单元。每个存储器单元都有一个地址,一般用十六进制数表示。
    • 地址总线:假如需要n位二进制数来表示所有的地址,则地址总线个数为n。
    • 数据总线:一次处理n位的数据,则数据总线的长度n。n的位数为一个字的长度。
    • 寻址空间范围算法:最大地址码-最小地址码+1。

      1-5 存储器

      存储器分类:
    • 按所处位置分:
    • 内存(主存):CPU当前使用的指令和数据
    • 外存(辅存):存放后备程序和数据
    • 按构成材料分:
    • 半导体存储器:
      • 静态存储器:双稳态触发器
      • 动态存储器:依靠电容上的电荷存储,主存
    • 磁存储器:利用磁性材料两种不同的状态长期保存信息,外存
    • 光存储器:利用光斑、晶像的变化表示信息,外存
    • 按工作方式分:
    • 读/写存储器:RAM
    • 只读存储器:
      • 固定只读存储器ROM:用户不能写数据
      • 可编程的只读存储器PROM:用户可写入一次
      • 可擦除可编程的只读存储器EPROM:可多次编程,紫外线擦除
      • 电可擦除可编程的只读存储器EEPROM:可多次编程,电擦除
      • 闪存:接近EEPROM,U盘
    • 按访问方式分:
    • 按地址访问的存储器
    • 按内容访问的存储器
    • 按寻址方式分:
    • 随机存储器RAM:按地址访问存储器的任一单元,主存
    • 顺序存储器SAM:访问时按顺序查找目标地趣,磁带
    • 直接存储器DAM:按照数据块所在位置访问,磁盘
    • 相联存储器:按照内容进行访问,Cache
存储器比较

寄存器\rightarrowCache\rightarrow主存\rightarrow磁盘
【容量】小\rightarrow
【速度】快\rightarrow
【价格】高\rightarrow

1-6 校验码概念

  • 信息出错:信息保存在电容中,如遇电磁环境干扰,会导致电容的充放电或触发器的翻转,存在存储器的信息可能会出错。
  • 码距与检错纠错:一个编码系统的码距就是整个编码系统中任意(所有)两个码字的最小距离。
    • 在一个码组内为了检测e个误码,要求最小dcgd距应该满足:d \geq e + 1
    • 在一个码组内为了纠正t个误码,要求最小码距应该满足:d \geq 2t + 1
    • 同时纠错检错:d \geq e + t + 1

      1-7 奇偶校验码

  • 奇偶校验方法:保证信息位上的1的个数为奇(偶)数个
  • 局限:只能发现奇数个们出错的情况

    1-8 海明码(一)

  • 海明码:基于奇偶校验、分组校验
  • 海明码的校验码的位置必须是在2^n位置(n从0开始,分别代表从右边数起分别是1、2、4、8、16……),信息码也就是在非2^n位置

    1-9 海明码(二)

  • 设数据位是n位,校验位是k位,则n和k必须满足以下关系:
    2^k \geq n + k + 1

    1-10 循环冗余校验码CRC

  • 采用CRC进行差错校验,生成多项式为G(X) = X^4 + X + 1,信息码字为10111,则计算出来的CRC校验码为(    )。
    • 解题步骤:
      (1) 化解多项式为10011
      X^4 + X + 1 = 2^4 + 2^1 + 2^0 \rightarrow 2^4 + 2^3 + 2^2 + 2^1 + 2^0 \rightarrow 10011
      (2) 信息码加0做模二除运算(不进位加法/异或运算)\rightarrow补(4{多项式最高次幂}个零);异或运算:相同为0,不同为1
      (3) 得到的余数即为校验码

      1-11 指令的流水线(一)

      指令
  • 指令周期:取出(解释)并执行一条指令所需的全部时间
  • 完成一条指令(一个指令周期)可以分为:取指周期、分析周期、执行周期
  • 指令流水技术:指令步骤的并行、提高处理器执行指令的效率
  • 指令的三种执行方式:
    (1) 顺序方式
    (2) 重叠方式
    (3) 并行方式

    1-12 指令的流水线(二)

  • 流水线周期:【并行指令中(取指、分析、执行)】执行时间最长的一段
  • 公式:(k为流水线执行阶段数,n为指令数)
    ① 理论公式:(t1 + t2 + t3 + ... + tk) + (n-1) * \Delta t
    ② 实践公式:(k + n - 1) * \Delta t
  • 流水线的吞吐率(TP)和最大吞吐率($$TP_{max}$$):
    • TP = \frac{n}{t}
      (n=指令条数,t=流水线执行时间)
    • TP_{max}=\lim_{n\rightarrow\infty}\frac{n}{(k+n-1)\Delta t}=\frac{1}{\Delta t}
  • 流水线加速比:S = \frac{a}{b}
    (a = 不执行流水线的时间,b = 执行流水线的时间)
    注:大数比小数

    1-13 高速缓冲储存器

    局部性原理:
  • 时间局部性
  • 空间局部性
    Cacher的映射方法
  • 直接映像
    • 优点:地址变换很简单
    • 缺点:不灵活、块冲突率高
  • 全相联映像
    • 优点:位置不受限制,十分灵活
    • 缺点:无法从主存块号中直接获得Cache的块号,变换比较复杂,速度比较慢
  • 组相联映像
    距离CPU较近位置可以采用直接映像或者组相联映像;距离CPU较远的可以采用全相联映像

    Cache的性能

    如果以Hc为代表对Cache的访问命中率,tc为Cache的存取时间,tm为主存的访问时间,则Cache的平均访问时间ta为:
    ta=Hc * tc + (1 - Hc ) * tm

    写策略。
    1. 写回法(write-back)
      当CPU对Cache写命中时,只修改Cache的内容不立即写入主存,只当此行被换出时才写回主存。这种策略使Cache在CPU-主存之间,不仅在读方向而且在写方向上都起到高速缓存作用。
    2. 写直达法(write-through)
      又称全写法,写透。是当Cache写命中时,Cache与主存同时发生写修改。
    3. 标记法
      数据进入Cache后,有效位置1,当CPU对该数据修改时,数据只写入主存并将该有效位置0。要从Cache中读取数据时要测试其有效位,若为1则直接从Cache中取数,否则从主存中取数。

      Cache替换策略

      (1) 随机算法。最简单。
      (2) 先进先出(First In and First Out, FIFO)算法。容易实现,系统开销小。缺点是可能会把一些需要经常使用的数据替换掉。
      (3) 近期最少使用(Least Recently Used, LRU)算法。需要对每一块设置一个“年龄计数器”的硬件或软件,用以记录其被使用的情况。
      (4) 最不经常使用页置换(Least Frequently Used, LFU)算法。计数器位数多,实现困难。

      1-14 磁盘储存器

  • 存储容量=n * t * s * b,其中:n为保存数据的总记录面数,t为每面磁道数,s为每道的扇区数,b为每个扇区存储的字节数
  • 硬盘存取时间=寻道时间+等待时间+读/写时间,其中读/写时间可忽略不计。

    1-15 计算机系统结构的分类&指令系统

    计算机系统结构的分类
    Flynn分类

    (1) 指令流:指机器执行的指令序列;
    (2) 数据流:指由指令流调用的数据序列,包括输入数据和中间结果,但不包括输出数据。
        Flynn根据不同的指令流-数据流组织方式,把计算机系统分成以下四类:
    (1) 单指令流单数据流(Single Instruction stream and Single Data stream, SISD)
    (2) 单指令流多数据流(Single Instruction stream and Multiple Data stream, SIMD):SIMD以并行处理机(矩阵处理机)为代表
    (3) 多指令流单数据流(Multiple Instruction stream and Single Data stream, MISD):不常见
    (4) 多指令流多数据流(Multiple Instruction stream and Multiple Data stream, MIMD):多核处理器。

    指令系统
  • 复杂指令系统CISC的特点
    (1) 指令数据众多
    (2) 指令使用频率相差悬殊
    (3) 支持很多种寻址方式
    (4) 变长的指令
    (5) 指令可以对主存单元中的数据直接进行处理
    (6) 以微程序控制为主
  • 精简指令系统RISC的特点
    (1) 指令数量少
    (2) 指令的寻址方式少
    (3) 指令的长度固定
    (4) 以硬布线逻辑控制为主
    (5) 单周期指令执行,采用流水线技术
    (6) 优化的编译器
    (7) CPU中的能用寄存器数量多,一般在32个以上,有的可达上千个

    1-16 总线

    总线是一组能为多个部件分时共享的公共信息传送线路

  • 按相对于CPU或其他芯片的位置可分为内部总线外部总线
  • 按总线功能分,可分为地址总线数据总线控制总线
  • 按总线中数据线的多少,可分为并行总线串行总线
名称 数据线 特点 应用
并行总线 多条双向数据线 有传输延迟,适合近距离连接 系统总线(计算机各部件)
串行总线 一条双向数据线,或两条单身数据线 速率不高,但适合长距离连接 通信总线(计算机之间或计算机与其他系统间)

1-17 磁盘阵列

  1. RAID 0(无冗余和无校验的数据分块)代表了所有RAID级别中最高的存储性能。
  2. RAID 1(磁盘镜像阵列)磁盘空间利用率为50%。
  3. RAID 2(采用纠错海明码的磁盘阵列):增加了海明码纠错技术,适用于大量数据传输,很少使用。
  4. RAID 3和RAID 4(采用奇偶校验码的磁盘阵列)RAID 3采用位交叉奇偶校验—适用于大型文件且I/O需求不频繁的应用,RAID 4采用块交叉奇偶校验码—适用于大型文件的读取。
  5. RAID 5(无独立校验盘的奇偶校验码的磁盘阵列):适用于I/O需求频繁的应用。当有N块阵列盘时,用户空间为N-1块盘容量。
  6. RAID 6(独立的数据硬盘与两人独立的分布式校验方案):RAID 5的扩展,增加了数据保护。当有N块阵列盘时,用户空间为N-2块盘容量。
  7. RAID 7(最优化的异步高I/O速率和高数据传输率):自带操作系统和管理工具,可独立运行。
  8. RAID 10(最可靠与高性能):将RAID 1和RAID 0标准结合.

    第2章 操作系统基础

    2-1 操作系统基础:概述

    分类
    • Windows
    • macOS
    • Unix
    • Linux
      考点分布
    • 处理机处理
    • 进程的状态
    • 前驱图
    • PV操作
    • 存储器管理
    • 逻辑地址
    • 物理地址
    • 存储方案
    • 设备管理
    • 输入输出控制方式
    • 文件管理
    • 文件的索引
    • 用户接口

      2-2 进程

    • 进程的三状态模型:
              运行
          \nearrow\swarrow \searrow
        就绪\longleftarrow阻塞
    • 进程通常由程序、数据集合、进程控制块PCB组成。PCB是一种数据结构,是进程存在的唯一标识。
    • PCB组织方式
    • 线性方式
    • 链接方式
    • 索引方式
    • 前驱图是一个有向无循环图。描述多个程序或进程之间的执行顺序关系。

      2-3 PV操作01

    • 互斥问题
    • 进入临界区之前先执行P操作(可能阻塞当前进程)
    • 离开临界区之后执行V操作(可能唤醒某个进程)
    • P操作
      ① 将信号量S的值减1,即S–;
      ① 如果S>=0,则该进程继续;否则该进程置为等待状态。
    • V操作
      ① 将信号量的值加1,即S++;
      ① 如果S>0,则该进程继续执行;否则说明有等队列中有等待进程,需要唤醒等待进程。

      2-4 PV操作02

    • 同步问题
    • 运行条件不满足时,能让进程暂停(在关键操作之前执行P操作)
    • 运行条件满足时,能让进程继续(在关键操作之后执行V操作)
    • 规则:
      • 不能向满缓冲区存产品
      • 不能向空缓冲区取产品
      • 每个时刻仅允许1个生产者或消费者存或取1个产品

        2-5 存储管理01

              _____
             \downarrow                        \downarrow
           CPU\longleftrightarrow缓存\longleftrightarrow内存\longleftrightarrow辅存

  • 当内存太小不够用时,用辅存来支援内存
  • 暂时不运行的模块换出到辅存上,必要时再换入内存
    地址重定位

    是指将程序中的虚拟地址(逻辑地址)变换成内存的真实地址(物理地址)的过程

  • 逻辑地址
    • 相对地址
    • CPU所生成的地址。内部和编程使用、并不唯一
  • 物理地址
    • 绝对地址
    • 加载到内存地址寄存器中的地址,内存单元的真正地址
      静态重定位
  • 绝对地址=相对地址+程序存放的内存地址
  • 特点:
    • 程序运行前就确定映射关系
    • 程序装入后不能移动
    • 程序战胜连续的内存空间
      动态重定位
  • 绝对地址=重定位寄存器器的值(BR)+逻辑寄存器的值(VR)
  • 特点
    • 程序占用的内存空间可动态变化
    • 程序不要求连续的内存空间
    • 便于多个进程共享代码

      2-6 存储管理02

      存储管理的主要目的是解决多个用户使用主存的问题

  • 分区存储管理
  • 分页存储管理
  • 分段存储管理
  • 段页式存储管理
  • 虚拟存储管理
    分区管理

    把主存的用户区划分成若干个区域,每个区域分配给一个用户作业使用,并限定它们只能在自己的区域运行。

  • 可重定位分区
  • 可变分区
  • 固定分区
    页式存储管理

    分页的基本思想是把程序的逻辑空间和内存空间的物理空间按照同样的大小划分成若干页面,并以页面为单位进行分配。
    常用的页面调度算法:

  • 最优(OPT)算法
  • 随机(RAND)算法
  • 先进先出(FIFO)算法
  • 最近最少使用(Least Recently Used,LRU)算法
    段式存储管理

    分段的基本思想是把用户作业按逻辑意义上有完整意义的段(主程序、子程序、数据段等)来划分,并以段为单位作为内外存交换的空间尺度。

    段页式存储管理

    根据程序模块分段,段内再分页,内存被划分成定长的页。可提高内存空间的利用率。

    2-7 设备管理

  • 外围设备和内存之间的数据控制方式
    • 程序控制方式
    • 中断方式
    • 直接存储访问(Direct Memory Access,DMA)
  • 虚设备与SPOOLING技术
    • 假脱机(Simulataneous Peripheral Operating On Line, SPOOLING)的意思是外部设备同时联机操作,又称为假脱机输入/输出操作,采用一组程序撒气一台输入/输出处理器。
    • 用于将低速独占设备改造成可共享设备。

      2-8 文件存储管理

      文件的逻辑结构
  • 无结构的字符流文件
  • 有结构的记录文件
    (1)顺序文件
    (2)索引顺序文件
    (3)索引文件
    (4)直接文件

    文件的物理结构

    常用的文件分配策略:
    (1)顺序分配(连续分配)
    (2)链接分配(串联分配)
    (3)索引分配

    • 一级索引(n个地址)
    • 二级间接索引(n^2个地址)
    • 三级间接索引(n^3个地址)

      2-9 文件存储设备管理

      有三种不同的空闲块管理方法:
      (1)索引法
      (2)链接法
      (3)位示图法在外存上建立一张位示图(Bitmap),记录文件的存储器的使用情况。每一位仅对应文件存储器上的一个物理块,取值0和1分别表示空闲和占用。

      第3章 计算机网格基础

      3-1 计算机风格基础:网络互联模型&常见网络协议

  • 1977,国际标准化组织制定了“开放系统互联参考模型(Open System Interconnection/Reference Model, OSI/RM)”。七层以模型,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。口决:“巫术忘传会飚鹰”。
  • 常见的网络协议
    • 应用层协议:
      • FTP(File TransportProtocol,文件传输协议)
      • TFTP(Trivial FileTransfer Protocol,简单文件传输协议)
      • HTTP(Hypertext TransferProtocol,超文本传输协议)
    • 传输层协议:
      • TCP:可靠的、面向连接的、全双工的数据传输服务。用于传输数据量比较少,且对可靠性要求高的场合。
      • UDP:不可靠的、无连接的协议,可以保证应用程序进程间的通信。用于传输数据量大,对可靠性要求不是很高,但要求速度快的场合。
    • 网络层协议:
      • IP:无连接的和不可靠的,它将差错检测和流量控 制之类的服务授权给了其他的各层协议,这正是 TCP/IP 能够高效率工作的一个重要保证。
      • ICMP(Internet Control Message Protocol, 网际控制报文协议)
      • IGMP(Internet Group Management Protocol,网际 组管理协议)
      • ARP(Address Resolution Protocol,地址解析协议)
      • RARP(Reverse Address Resolution Protocol,反向地址解析协议)

        3-2 IP地址及其表示方法

        IP(IPV4)地址是一个32位的二进制数的逻辑地址,分成4个字节,每个字节间以“.”区分。IP地址由两个部分组成,网络号+主机号
        网络号(4位)分类:

  • A类:1~126
  • B类:128~191
  • C类:192~223
  • D类:224~255+组播地址
  • E类:240~255+保留

    3-3 子网与子网掩码

    三级IP地址:网络号+子网号+主机号
    子网掩码也是32位二进制数,网络与子网标识全为1,主机标识全部为0。

  • A类地址的子网掩码:255.0.0.0
  • B类地址的子网掩码:255.255.0.0
  • C类地址的子网掩码:255.255.255.0

将IP地址和其对应的子网掩码逐位进行“与”运算,可得到对应的子网的网络地址。
131.1.123.24/27:/27代表前27位都是网络号,主机号是5位,主机号要去掉全0和全1。

3-4 IPV4数据报/IPV6

IPV4数据报
报名 含义
版本 IP协议的版本。这里版本号为4。
首部长度 可表示的最大数值是15个单位(4 字节为一个单位),60字节。
区分服务 不同优先级服务质量不同,只有在使用区分服务(DiffServ) 时有效。
总长度 首部与数据之和的长度,最大长度为2^16-1=65535字节。
标识 唯一标识数据报的标识符。
片偏移 指明该段处于原来数据报中的位置。
生存时间 记为TTL(Time To Live),指示数据报在网络中可通过的路 由器的最大值。
协议 数据报携带的协议(TCP、UDP、IGMP等)
首部检验和 只检验首部,不检验数据。采用16位二进制反码求和算法。
可选字段 可记录时间戳 ,通过路径,安全信息等。
填充 填充为4的倍数
IPV6
  • 1995年12月推出。
  • 共128位,以16位为一段,共为8段,每段的16位转换为 一个4位的十六进制数,每段之间用“:”分开。
  • IPV6的优势:
    • IPv6有更大的地址空间
    • IPv6使用更小的路由表
    • IPv6增加了组播支持与对流支持
    • IPv6加入了自动配置的支持
    • IPv6具有更高的安全性
  • IPv4/IPv6 过渡技术:
    (1) 双协议栈技术: 支持两种业务的共存。
    (2) 隧道技术: 实现在 IPv4 网络上对 IPv6 业务的承载。具体的隧道技术包括: 6to4 隧道;6over4 隧道;ISATAP 隧道。
    (3) NAT-PT 技术:NAT – PT 使用网关设备连接 IPv6 和 IPv4 网络。
  • IPV6数据报
报名 含义
版本 IP协议的版本。这里版本号为6。
流量分类 通信类型,相当于IPV4服务类型字段。
流标签 从源点到终点的一系列数据报,同一个流上的数据报标签相同,保证服务质量。
有效负载长度 除基本首部以外的字节数(所有扩展首部都算在有效负载内),最大 值64KB
下一头部 相当于IPV4的协议字段或可选字段。
跳数限制 用于检测路由循环,路由器在转发数据报时对这个字段减1,变成0丢弃该数据报。
  • IPv6数据报的目的地址可以是以下三种基本类型地址之一∶ (1)单播((unicast)∶传统的点对点通信。 (2)多播/组播(multicast)∶一点对多点的通信。 (3)任播(anycast)∶这是 IPv6增加的一种类型。任播的目的站是一组 计算机,但数据报在交付时只交付其中的一个,通常是距离最近的一个。

    3-5 TCP与UDP

    TCP协议
  • 传输控制协议(Transmission Control Protocol,TCP)是一种可靠的、面向 连接的字节流服务。
  • TCP建立在无连接的IP基础之上,因此使用了3种机制实现面向连接的服务。
    1) 使用序号对数据报进行标记。
    2) TCP使用确认、校验和定时器系统提供可靠性。
    3) TCP使用窗口机制调整数据流量。
  • TCP只有一种类型的PDU,叫作TCP段
    UDP协议
  • 用户数据报协议(User Datagram Protocol,UDP)是一种不可靠的、无连接的数据报服务。
  • UDP特点:
    (1) UDP是无连接的,发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。
    (2) UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂
    的连接状态表。
    (3) UDP是面向报文的。UDP对应用层交下来的报文,既不合并,也不拆分,而
    是保留这些报文的边界。UDP一次交付一个完整的报文。
    (4) UDP没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。
    这对某些实时应用是很重要的。很适合多媒体通信的要求。
    (5) UDP支持一对一、一对多、多对一和多对多的交互通信。
    (6) UDP的首部开销小,只有8个字节,比TCP的 20个字节的首部要短。

    3-6 网络设计&综合布线系统

    网格设计
  • 接入层:通常将网络中直接面向用户连接或访问网络的部分称为接入层,将 位于接入层和核心层之间的部分称为分布层或汇聚层。
  • 汇聚层:是核心层和接入层的分界面,完成网络访问策略控制、数据包处理、过滤、寻址,以及其他数据处理的任务。
  • 核心层:网络主干部分称为核心层,核心层的主要目的在于通过高速转发通信,提供优化、可靠的骨干传输结构,因此,核心层交换机应拥有 更高的可靠性,性能和吞吐量。
    综合布线系统

    综合布线系统是一个用于传输语音、数据、影像和其他信息的标准结 构化布线系统,是建筑物或建筑群的传输网络,它使语言和数据通信设备、 交换设备和其他信息管理系统彼此相连接。
    综合布线的热物理结构一般采用模块化设计和分层星型拓扑结构。系统结构有6个独立的子系统:

    1. 工作区子系统:它是工作区内终端设备连接到信息插座之间的设备组成,包括信息插座、连接软线、适配器、计算机、网络集散器、电话、报警探头、摄像机、监视器、音响等。
    2. 水平子系统:水平子系统是布置在同一楼层上,一端接在信息插座,另一端接在配线间的跳线架上,它的功能是将干线子系统线路延伸到用户工作区,将用户工作区引至管理子系统,并为用户提供一个符合国际标准,满足语音及高速数据传输要求的信息点出口。
    3. 管理子系统:安装有线路管理器件及各种公用设备,实现整个系统集中管理,它是干线子系统和水平子系统的桥梁,同时又可为同层组网提供条件。其中包括双绞线跳线架、跳线(有快接式跳线和简易跳线之分)。
    4. 垂直(干线)子系统:通常它是由主设备间至各层管理间,特别是在位于中央点的公共系统设备处提供多个线路设施,采用大对数的电缆馈线或光缆,两端分别端接在设备间和管理间的跳线架上,目的是实现计算机设备、程控交换机(PBX)、控制中心与各管理子系统间的连接,是建筑物干线电缆的路由。
    5. 设备间子系统:该子系统是由设备间中的电缆、连接跳线架及相关支撑件、防雷电保护装置等构成。可以说是整个配线系统的中心单元,因此它的布放、造型及环境条件的考虑适当与否,直接影响到将来信息系统的正常运行及维护和使用的灵活性。电话交换机、计算机主机设备及入口设施也可与配线设备安装在一起。
    6. 建筑群子系统:它是将多个建筑物的数据通信信号连接成一体的布线系统,它采用架空或地下电缆管道或直埋敷设的室外电缆和光缆互连起来,是结构化布线系统的一部分,支持提供楼群之间通信所需的硬件。

      3-7 地址和域名

      Internet地址

      Internet地址分为3级,可表示为“网络地址.主机地址.端口地址”的形式。其中,网络和主机地址即 IP地址;端口地址就是TCP或UDP地址,用于表示上层进程的服务访问点。TCP/IP 网络中的大多数公共应用进程都有专用的端口号,这些端口号是IANA (Internet Assigned Numbers Authority)指定的,其值小于1024,而用户进程的端口号一般大于1024。

      域名系统
  • 域名系统(Domain Name System,DNS)是把主机域名解析为IP 地址的系统,解决了IP地址难记的问题。该系统是由解析器和域名服务器组成的。
  • DNS使用UDP协议,较少情况下使用TCP协议,端口号均为53。
  • 域名系统由三部分构成:DNS名字空间、域名服务器、DNS客户机。
    1) 根域:根域处于Internet上域名空间结构树的最高端,是树的根,提供根域名服务。根域用“.”来表示。
    2) 顶级域名(To p Level Domain,TLD):顶级域名在根域名之下,分为三大类:国家顶级域名、通用顶级域名和国际顶级域名。
    3) 主机:属于最低层域名,处于域名树的叶子端,代表各类主机提供的服务。
  • 通用顶级域名
域名 使用对象
com 商业机构等盈利性组织
edu 教育机构、学术组织和国家科研中心等
gov 美国非军事性的政府机关
mil 美国的军事组织
net 网络信息中心(NIC)和网络操作中心(BIC)等
org 非盈利性组织、例如技术支持小组、计算机用户小组等
int 国际组织
  • 顶级域下面是二级域,这是正式注册给组织和个人的唯一名称,例如
    www.microsoft.com 中的microsoft 就是微软注册的域名。
  • 在二级域之下,组织机构还可以划分子域,使其各个分支部门都获得一个专用的名称标识, 例如www.sales.microsoft.com中的sales是微软销售部门的子域名称。划分子域的工作可以一直延续下去,直到满足组织机构的管理需要为止。但是标准规定,一个域名的长度通常不超过63个字符,最多不能超过255个字符。
    域名服务器
    名称 定义 作用
    主域名服务器 维护本区所有域名信息,信息存于磁盘文件和数据库中 提供本区域名解析,区内域名信 息的权威。具有域名数据库。一个域有且只有一个主域名服务器
    辅助域名服务器 主域名服务器的备份服务器 提供域名解析服务,信息存于磁盘文件和数据库中主域名服务器备份,可进行域名 解析的负载均衡。具有域名数据库
    缓存域名服务器 向其他域名服务器进行域名查询,将查询结果保存在缓存中的域名服务器 改善网络中DNS服务器的性能,减少反复查询相同域名的时间,提高解析速度,节约出口带宽。 获取解析结果耗时最短,没有域名数据库
    转发域名服务器 负责非本地和缓存中无法查到的域名。接收域名查询请求,首先查询自身缓存,如果找不到对应的,则转发到指定的域名服务器查询 负责域名转发,由于转发域名服务器同样可以有缓存,因此可以 减少流量和查询次数。具有域名数据库
    域名查询

    DNS查询过程的两种方法:
    (1) 递归查询:当用户发出查询请求时,本地服务器要进行递归查询。这种查询方式 要求服务器彻底地进行名字解析,并返回最后的结果一一IP地址或错误信息。
    (2) 迭代查询:服务器与服务器之间的查询采用迭代的方式进行,发出查询请求的服 务器得到的响应可能不是目标的IP地址,而是其他服务器的引用(名字和地址),那么本地服务器就要访问被引用的服务器,做进一步的查询。如此反复多次,每次都更接近目标的授权服务器,直至得到最 后的结果一一目标的IP地址或错误信息。

    网络互连与常用设备
    互联设备 工作层次 主要功能
    中继器 物理层 对接收信号进行再生和发送,对高层协议是透明的,但使用个数有限(例如,在以太网中只能使用4个
    网桥 数据链路层 根据帧物理地址进行网络之间的信息转发,可缓解网络通信繁忙度,提高效率。只能够连接相同MAC层的网络
    路由器 网络层 通过逻辑地址进行网络之间的信息转发,可完成异构网络之间的互联互通,只能连接使用相同网络层协议的子网
    网关 高层(第4~7层) 最复杂的网络互联设备。用于连接网络层以上执行不同协议的子网
    继线器 物理层 多端口中继器
    二层交换机 数据链路层 是指传统意义上的交换机,多端口网桥
    三层交换机 网络层 带路由功能的二层交换机
    多层交换机 高层(第4~7层) 带协议转换的交换机
    网络存储技术

    目前,主流的网络存储技术主要有三种,分别是直接附加存储(Direct Attached Storage, DAS)、网络附加存储(Network Attached Storage, NAS)和存储区域网络(Storage Area Network,SAN)。

    1. 直接附加存储
      DAS 是将存储设备通过 SCSI(Small Computer System Interface,小 型计算机系统接口)电缆直接连到服务器,其本身是硬件的堆叠,存储操作依赖 于服务器,不带有任何存储操作系统。因此,有些文献也把 DAS 称为 SAS (Server Attached Storage,服务器附加存储)。
    2. 网络附加存储
      采用 NAS 技术的存储设备不再通过 I/O 总线附属于某个特定的服务器,而是通过网络接口与网络直接相连,由用户通过网络访问。
      NAS 存储支持即插即用,可以在网络的任一位置建立存储。基于 Web 管理,从而使设备的安装、使用和管理更加容易。NAS 可以很经济地解决存储容量不足的问题,但难以获得满意的性能。
    3. 存储区域网络
      SAN 是通过专用交换机将磁盘阵列与服务器连接起来的高速专用子网。它没有采用文件共享存取方式,而是采用块(block)级别存储。SAN 是通过专用高速网将一个或多个网络存储设备和服务器连接起来的专用存储系统,其最大特点是将存储设备从传统的以太网中分离出来,成为独立的存储区域网络。

      网络系统建设

      网络设计的原则

    4. 采用先进、成熟的技术。
    5. 遵循国际标准,坚持开放性原则。
    6. 网络的可管理性。
    7. 系统的安全性。
    8. 灵活性和可扩充性。
    9. 系统的稳定性和可靠性。
    10. 经济性。
    11. 实用性。

      第4章 数据库系统的结构

      4-1 数据库基础:数据库系统的结构

  • 应用开发人员的角度
    • 三级抽象
      • 用户级数据库:用户视图,可相互重叠。
      • 概念级数据库:DBA视图,所有用户视图的最小并集。
      • 物理级数据库:内部视图,对应于内模式,接近于物理存储。
    • 三级模式:
      • 概念模式:模式、逻辑模式,是数据项值的框架。一个数据库只有一个概念模式。
      • 外模式:子模式、用户模式,描述用户视图的数据结构。
      • 内模式:数据物理结构和存储方式的描述,数据库内部的数据表示方式。一个数据库只有一个内模式。
  • 最终用户角度
    • 单用户结构
    • 主从结构
    • 分布式结构
    • 客户-服务器结构
    • 浏览器-应用服务器/数据库服务器
  • 两级独立性
    • 物理独立性:物理存储改变时,应用程序不需要改变。
    • 逻辑独立性:数据的逻辑结构改变时,应用程序不需要改变。(更难实现)

      4-2 数据模型

  • 概念数据模型:按用户的观点进行数据建模,E-R模型。
  • 基本数据模型:按照计算机系统的观点进行数据建模,常用的模型有层次模型、网状模型、关系模型和面向对象模型。
  • 数据的约束条件:
    • 实体完整性—主属性不能取空值。
    • 参照完整性—外键要么为空,要么必须是其他关系的主属性。
    • 用户定义完整性—具体应用所定义的约束条件。

      4-3 关系型数据库

概念 名词解释
关系 可以理解为一张二维表,每个关系都具有一个关系名,即表名
元组 可以理解为二维表中的一行,在数据库中被称为记录
属性 可以理解为二维表中的一列,在数据库中被称为字段
属性的取值范围,即数据库中某一列的取值限制
关键字 一组可以唯一标识元组的属性,数据库中称为主键,由一列或多列组成
关系模式 对关系的描述。格式为:关系名(属性1,属性2,……,属性N),在数据库中成为表结构。

4-4 关系代数01

集合运算符 含义 名词解释
\cup 关系R与S的并是由属于R或属于S的元组构成的集合。
- 关系R与S的差是由属于R但不属于S的元组
\cap 关系R与关系S的交是由属于R同时又属于S的元组构成的集合
\times 笛卡尔积 两个元组分别为n目和m目的关系R和关系S的笛卡尔积是一个(n+m)列的元组的集合。元组的前n列是关系R的一个元组,后m列是关系S的一个元组。
专门关系的运算符 含义 名词解释
\sigma 选择 取得关系R中符合条件的行
\pi 投影 取得关系R中符合条件的列
\Join 连接 等值连接:关系R、S,取两者笛卡尔积中属性值相 等的元组。自然连接:一种特殊的等值连接,它要求比较的属性列必须是相同的属性组,并且把结果中重复属性去掉。注意与笛卡尔积的区别。

4-5 关系代数02

  • 外连接:两个关系R和S进行自然连接时,选择两个关系R和S公共属性相等的元组,去掉重复的属性列构成新关系。
    1) 左外连接:R和S进行自然连接时,只把R中舍弃的元组放到新关系中。
    2) 右外连接:R和S进行自然连接时,只把S中舍弃的元组放到新关系中。
    3) 完全外连接:R和S进行自然连接时,只把R和S中舍弃的元组都放到新关系中。

    4-6 函数依赖

    定义
  • 设R(U)是在属性U上的关系模式,X,Y是U的子集,若对于R(U)的任意的一个可能的关系r,r中的任意两个元组在X上的属性值相等,那么在Y上的属性值也相等,则称“X函数确定Y“或”Y函数依赖于X“,记作X \rightarrow Y。X称为这个函数依赖的决定属性组,也称为决定因素
  • 若Y不函数依赖于X,则记为X \nrightarrow Y
  • 函数依赖是语义范畴内的概念。如Sname \rightarrow Sno函数依赖只有在“学生不允许有重名”的条件下成立。
  • X \rightarrow YY \nsubseteq X,则称X \rightarrow Y非平凡的函数依赖
  • X \rightarrow YY \subseteq X,则称X \rightarrow Y平凡的函数依赖
  • 例如:关系式Student(Sno,Sdept,Mname,Cno,Grade)
名称 定义
完全函数依赖 (Sno,Cno) \rightarrow Grade是完全函数依赖
部分函数依赖 (Sno,Cno) \rightarrow Sdept是完全函数依赖
传递依赖 Sno \rightarrow Sdept,Sdept \rightarrow Mname,则称Sno传递依赖于Mname
Armstrong公理

从已知的一些函数依赖,可以推导出另外一些函数依赖,这就需要一些推理规则,这些规则常被称作“Armstrong 公理”。

  • 设关系式R(U,F),U是关系模式R的属性集,F是U上一组函数依赖,则有以下三条推理规则:
    • A1自反律:若Y \subseteq X \subseteq U,则X \rightarrow Y为F所蕴含;
    • A2增广律:若X \rightarrow Y为F所蕴含,且Z \subseteq U,则XZ \rightarrow YZ
    • A3传递律:若X \rightarrow YY \rightarrow Z为F所蕴含,则X \rightarrow Z为F所蕴含。
  • 根据上面三条推理规则,又可推出下面三条推理规则:
    • 合并规则:若X \rightarrow YY \rightarrow Z,则X \rightarrow YZ为F所蕴含;
    • 伪传递规则:若X \rightarrow YWY \rightarrow Z,则XW \rightarrow YZ为F所蕴含;
    • 分解规则:若X \rightarrow YZ \subseteq Y,则X \rightarrow Z为F所蕴含。
      键值
  • 例如:关系式S1(Sno,Sdept,Sage)
    • 超键:(Sno,Sdept)、(Sno,Sage)、(Sno,Sdept,Sage)是超键
    • 主键:Sno \rightarrow SdeptSno \rightarrow Sage,Sno是主键(码);若有关系式SC(Sno,Cno,Grade)中,(Sno,Cno)是主键
  • 例如:关系式S2(Sno,Sname,Sdept,Sage)
    • 候选键:Sno、Sname是候选键,选择Sno为主键。不含有多余属性的超键称为候选键。
  • 外键
    如果关系模式R中的某些属性集不是R的主键,而是关系模式S的主键,则这个属性集对模式R而言是外键。

    属性

    包含在任何一个主键中,称为主属性,否则为非主属性。

    全码

    例如:关系模式R(P,W,A)中,P是演奏者,W是作品,A是听众,该关系模式只有一个包含了全部属性的主键,是全码。

    4-7 规范化01

    关系数据库设计的方法之一就是设计满足适当范式的模式,通常可以通过判断分解后的模式达到几范式来评价模式的规范化程度。

  • 第一范式(1NF)
  • 第二范式(2NF)
  • 第三范式(3NF)
  • BC范式(BCNF)
  • 第四范式(4NF)
  • 第五范式(5NF)

    4-8 规范化02

  • 第一范式(1NF):若关系模式R的每一个分量是不可再分的数据项,则关系式R属于第一范式。
    1. 冗余较大
    2. 修改异常
    3. 插入异常
    4. 删除异常
  • 第二范式(2NF):若关系式R \in 1NF,且每一个非主属性完全依赖主健时,则关系式R是2NF(第二范式)

    4-9 规范化03

  • 第三范式(3NF):即当2NF消除了非属性以码的传递函数依赖,则称为3NF。
  • BC范式(BCNF):R属于BCNF当且当其F中每个依赖的决定因素必定包含R的某个候选键

    4-10 数据库设计&需求分析

    数据库设计
  • 规划阶段:建立数据库的必要性、可行性
  • 需求分析:收集需求,理解需求,需求规格说明书、数据字典
  • 概念设计:建立概念模型,E-R图
  • 逻辑设计:建立逻辑模型,关系模式
  • 建立物理:建立物理模型,“create table”,依赖于DBMS
    需求分析
  • 需要分析的目标是通过调查研究,了解用户的数据和处理要求,并按照一定格式整理成需求规格说明书
    • 充分了解原系统(手工或计算机)工作概况
    • 详细调查待开发系统的组织/部门/企业等
    • 明确用户的各种需求
    • 确定新系统的功能(注意今后的扩充和改变)
  • 调查的重点是“数据”和“处理”:【数据库的元数据交由数据字典来进行管理】
    1. 数据库需要哪些数据?如:数据名、属性及其类型、数据量估计等。
    2. 数据处理要求?如:更改要求、使用频率和等。
    3. 安全性与完整性要求?如:保密 要求、完整性约束条件(主键属性) 等。
  • 数据字典的内容:数据项数据流数据存储数据加工(处理过程)

    4-11 概念设计&逻辑设计

    概念设计

    其任务是在需求分析阶段产生的需求说明书的基础上,按照特定的方法将它们抽象为一个不依赖于任何DBMS的数据模型,即概念模型
    【需求分析】
            \downarrow
    【确定局部视图范围】
            \downarrow
    【识别实体及其标识】
            \downarrow
    【确定实体间的联系】
            \downarrow
    【分配实体及联系的属性】
            \downarrow
    【全局E-R模式设计】(消除冲突)

  • 属性冲突
    • 属性域冲突(不同学校编码方式不同)
    • 属性值冲突(重量采用千克、磅)
  • 结构冲突
    • 同一对象在不同应用中的抽象不同(一个应用是实体,另一个是属性)
    • 同一实体在不同E-R图中属性个数和排列次序不同
  • 命名冲突
    • 同名异义
    • 异名同义
      逻辑设计

      也称为逻辑结构设计,其任务是将概念模型转化为某个特定的逻辑模型(层次模型、网状模型、关系模型

      物理设计
      1. 设计存储记录结构,包括记录的组成、数据项的类型和长度,以及逻辑记录到存储记录的映射。
      2. 确定数据存储安排。
      3. 设计访问方法,为存储在物理设备上的数据提供存储和检索的能力。
      4. 进行完整性和安全性的分析与设计。
      5. 数据库程序设计。
        反规范化

        常见的反规范化技术:

      6. 增加冗余列
      7. 增加派生列
      8. 重新组表
      9. 分割表
    • 水平分割:根据一列或多列数据的值把数据行放到两个独立的表中。使用场景:
      • 情况1:表很大,提高查询效率
      • 情况2:表中的数据有独立性
      • 情况3:需要把数据存放到多个介质上
    • 垂直分割:把主码和一些列放到一个表,然后把主码和另外的列放到另一个表中。优点是在查询时就会减少 I/O 次数,缺点是需要管理冗余列,查询所有数据需要连接操作。

      4-12 事务管理&并发控制

      事务管理

      数据库系统运行的基本工作单位是事务,事务相当于操作系统中的进程,是用户定义的一个数据库操作序列,这些操作序列要么全做要么全不做,是一个不可分割的工作单位。

事务管理的ACID原则:

  1. 原子性(Atomicity)—操作:操作序列要么全 做要么全不做。
  2. 一致性(Consistency)—数据:数据库从一个一致性状态变到另一个一致性状态。
  3. 隔离性(Isolation)—执行:不能被其他事务干扰。
  4. 持续性(永久性)(Durability)—变化:一旦提交,改变就是永久性的。
    并发控制

    处理并发控制的主要方法是采用封锁技术。它有两种类型:排他型封锁(X封 锁)和共享型封锁(S封锁):
    1) 排他型封锁(简称 X 封锁)。如果事务 T 对数据 A(可以是数据项、 记录、数据集,乃至整个数据库)实现了 X 封锁,那么只允许事务 T 读取和 修改数据 A,其他事务要等事务 T 解除 X 封锁以后,才能对数据 A 实现任何类型的封锁。可见 X 封锁只允许一个事务独锁某个数据,具有排他性。
    2) 共享型封锁(简称 S 封锁)。如果事务 T 对数据 A 实现了 S 封锁,那么 允许事务 T 读取数据 A,但不能修改数据 A,在所有 S 封锁解除之前绝不允许任何事务对数据 A 实现 X 封锁。

封锁协议: 协议 说明 作用
一级封锁协议 事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放 可防止丢失修改
二级封锁协议 一级封锁协议加上事务T在读取数据R之前先 对其加S锁,读完后即可释放S。 可防止丢失修改,还可防止读“脏”数据
三级封锁协议 一级封锁协议加上事务T在读取数据R之前先 对其加S锁,直到事务结束才释放 可防止丢失修改,还可防止读“脏”数据与防止数据重复读
两段锁协议 分为封锁阶段(扩展)和释放阶段(收缩)。封锁阶段只能加锁、扩展阶段只能解锁 可串行化,可能发生死锁

4-13 分布式数据库&故障恢复

分布式数据库
分布式数据库系统的特点

1) 数据的分布性。布式数据库中的数据分布于网络中的各个结点。
2) 统一性。主要表现在数据在逻辑上的统一性和数据在管理上的统一性两个方面。
3) 透明性。用户无须知道数据的存放位置。

分布式数据库的优点

1) 坚固性好。多台服务器构成,容错能力强,可靠性和可用性好。
2) 可扩充性好。
3) 可改善性能。
4) 自治性好。

分布透明性

1) 分片透明性是分布透明性的最最高层次。分片透明性是指用户或应用程序只对全局关系进行操作而不必考虑数据的分片。
2) 位置透明性是分布透明性的下一层次。位置透明性是指,用户或应用程序应当了解分片情况,但不必了解片段的存储场地。
3) 局部数据模型(逻辑透明)透明性。局部数据透明性是指用户或应用程序应当了解分片及各片断存储的场地,但不必了解局部场地上使用的是何种数据模型。

故障恢复
  • 数据库的故障可用事务的故障来表示,主要分为四类
    1) 事务故障。逻辑、数据错误导致事务未正常终止就被撤销。
    2) 系统故障。操作失误、三件错误、停电等买不到内存信息丢失。
    3) 介质故障。磁盘损坏、操作系统问题导致数据损失。
    4) 计算机病毒。
  • 故障的恢复
    1) 事务故障的恢复。由系统自动完成,步骤如下:

    • 反向扫描文件日志,查找该事务的更新操作。
    • 对该事务的更新操作执行逆操作。
    • 继续反向扫描日志文件,查找该事务的其他更新操作,并做同样处理。
    • 如此处理下去,直至读到此事务的开始标记,事务故障恢复完成。
      2) 系统故障的恢复。在重新启动时自动完成,步骤如下:
    • 正向扫描日志文件,找出在故障发生前已经提交的事务,将其事务标识记入 重做(Redo)队列。同时找出故障发生时尚未完成的事务,将其事务标识记 入撤销(Undo)队列。
    • 对撤销队列中的各个事务进行撤销处理:反向扫描日志文件,对每个 Undo 事务的更新操作执行逆操作。
    • 对重做队列中的各个事务进行重做处理:正向扫描日志文件,对每个 Redo 事务重新执行日志文件登记的操作。
      3) 介质故障与病毒破坏的的恢复。磁盘上的物理数据库被破坏,这时的恢复操作可分为三步:
    • 装入最新的数据库后备副本,使数据库恢复到最近一次转储时的一致性状态。
    • 从故障点开始反向读日志文件,找出已提交事务标识将其记入重做队列。
    • 从起始点开始正向阅读日志文件,根据重做队列中的记录,重做所有已完成 事务,将数据库恢复至故障前某一时刻的一致状态。
      4) 具有检查点的恢复技术。检查点记录的内容可包括:
    • 建立检查点时刻所有正在执行的事务清单。
    • 这些事务最近一个日志记录的地址。
    • 采用检查点的恢复步骤如下:
      • 从重新开始文件中找到最后一个检查点记录在日志文件中的地址,由该地址在日志文件中找到最后一个检查点记录。
      • 由该检查点记录得到检查点建立时所有正在执行的事务清单队列(A)。
      • 建立重做队列(R)和撤销队列(U),把 A 队列放入 U 队列中,R 队列为空。
        备份

        数据库备份按照不同方式可分为多种,这里按照备份内容分为物理备份逻辑备份两类。
        物理备份是在操作系统层面上对数据库的数据文件进行备份,物理备份分为冷备份热备份两种。

        4-14 数据仓库

        数据仓库定义

        著名的数据仓库专家 W.H.Inmon 在《Building the Data Warehouse》一书中 将数据仓库定义为:数据仓库(Data Warehouse)是一个面向主题的、集成的、 相对稳定的、反映历史变化的数据集合,用于支持管理决策。

        数据仓库与传统数据库的比较
        比较项目 传统数据库 数据仓库
        数据内容 当前值 历史的、归档的、归纳的、计算机的数据(处
        理过的
        数据目标 面向业务操作程序、重复操作 面向主体域、分析应用
        数据特性 动态变化、更新 静态、不能直接更新,只能定时添加、更新
        数据结构 高度结构化、复杂、适合操作计算 简单、适合分析
        使用频率
        数据访问量 每个事务一般只访问少量记录 每个事务一般访问大量记录
        对响应时间的要求 计时单位小、如秒 计时单位相对较大,除了秒,还有分钟、小时
        数据仓库的结构
        1. 数据源。是数据仓库系统的基础,是整个系统的数据源泉。
        2. 数据的存储与管理。整个数据仓库系统的核心。数据仓库的真正关键是数据的存储和管理。
        3. OLAP服务器。对分析需要的数据进行有效集成,按多维模型予以组织,以便进行多角度、多层次的分析,并发现趋势。其具体实现可以为:ROLAP、 MOLAP 和 HOLAP。
        4. 前端工具。主要包括各种报表工具、查询工具、数据分析工具、数据挖掘工具及各种基于数据仓库或数据集市的应用开发工具。
          数据仓库的实现方法
        5. 自顶向下法
        6. 自底向上法
        7. 混合法

          4-15 数据挖掘

          数据挖掘定义

          数据挖掘(Data Mining)技术是人们长期对数据库技术进行研究和开发的结果。数据挖掘与传统的数据分析(如查询、报表、联机应用分析)的本质区 别是数据挖掘是在没有明确假设的前提下去挖掘信息、发现知识。数据挖掘所得到的信息应具有先知,有效和可实用三个特征。

          数据挖掘的流程
        8. 问题定义。在开始数据挖掘之前,最先的也是最重要的要求就是熟悉背 景知识,弄清用户的需求。
        9. 建立数据挖掘库。要进行数据挖掘必须收集要挖掘的数据资源。一般建议把要挖掘的数据都 收集到一个数据库中,而不是采用原有的数据库或数据仓库。
        10. 分析数据。分析数据就是通常所进行的对数据深入调查的过程。
        11. 调整数据。通过上述步骤的操作,对数据的状态和趋势有了进一步的了解,这时要尽 可能对问题解决的要求能进一步明确化、进一步量化。
        12. 模型化。在问题进一步明确,数据结构和内容进一步调整的基础上,就可以建立形 成知识的模型。
        13. 评价和解释。上面得到的模式模型,有可能是没有实际意义或没有实用价值的,也有可 能是其不能准确反映数据的真实意义,甚至在某些情况下是与事实相反的,因 此需要评估,确定哪些是有效的、有用的模式。
          常用的数据挖掘技术
        14. 关联分析。关联分析主要用于发现不同事件之间的关联性,即一个事件发生的同时,另一个事件也经常发生。
        15. 序列分析。序列分析技术主要用于发现一定时间间隔内接连发生的事件。
        16. 分类分析。分类分析通过分析具有类别的样本的特点,得到决定样本属于各种类别的规则或方法。其主要方法有基于统计学的贝叶斯方法、神经网络方法、决策树方法及支持向量机(support vector machines)等。
        17. 聚类分析。聚类分析是根据物以类聚的原理,将本身没有类别的样本聚集成不同的组,并且对每一个这样的组进行描述的过程。
        18. 预测。预测与分类类似,但预测是根据样本的已知特征估算某个连续类型的变量的取值的过程,而分类则只是用于判别样本所属的离散类别而已。预测常用的技术是回归分析。
        19. 时间序列。分析时间序列分析的是随时间而变化的事件序列,目的是预测未来发展趋势,或者寻找相似发展模式或者是发现周期性发展规律。

          4-16 NoSql数据库:关系型数据库的缺点

  • 不满足高并发读写需求
  • 不满足海量数据的高效率读写
  • 不满足高扩展性和可用性

    集群方式虽然可以缓解上述问题,但仍然存在下列缺陷:
    • 复杂性–集群配置、部署、管理都和复杂。
    • 延迟性–主数据库压力较大时,会产生较大延迟。主备切换时候可能
    需要人工参与。
    • 扩容性–集群中增加新机器时,对整个数据集重新分区,非常复杂。

    4-17 ACID理论、CAP理论、BASE理论

    ACID理论

    ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:

    • 原子性(Atomicity)
    • 一致性(Consistency)
    • 隔离性(Isolation)
    • 持续性(永久性)(Durability)
Nosql数据库

NoSQL(NoSQL = Not Only SQL ),意即 “不仅仅是SQL”。

NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来 的挑战,尤其是大数据应用难题。
1) 灵活的可扩展性
2) 灵活的数据模型
3) 与云计算结合

CAP理论
主要概念 解释
C(Consistency)一致性 一致性是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,与ACID的 C完全不同。
A(Availability)可用性 可用性是指服务一直可用,而且是正常响应时间。
P(Partition tolerance)分区容 错性 分区容错性是指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

鱼与熊掌不可兼得。一个分布式系统不可能同时满足一致性、可用性、分区容忍性这三个需求,最多只能同时满足其中两个。

CA CP AP
优先保证一致性和可用性,放弃分区容错。缺点:不再是分布式系统 优先保证一致性和分区容错性,放弃可用性。缺点:牺牲用户体验 优先保证可用性和分区容错性,放弃一致性。缺点:全局数据的不一致性
BASE理论
基本可用(BasicallyAvailable) 软状态(Soft state) 最终一致性(Eventuallyconsistent)
指分布式系统在出现不可预知故障的时候,允许数许损失部分可用性。允许分区失败的情形出现。 硬状态:数据库状态必须一直保持数据库一致性。软状态:状态可以有一段时间不同步 系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。
Nosql数据库与sql数据库的比较
特征 SQL数据库 NoSql数据库
数据类型 结构化 非结构化
数据一致性 强一致性 弱一致性
事务 高事务性 弱事务性
扩展性 一般
数据容量 有限数据 海量数据
标准化
技术支持
可维护性 复杂 复杂
NoSql数据库的主要类型
  1. 键值数据库
  2. 列族数据库
  3. 文档数据库
  4. 图形数据库

    4-18 NoSQL的主要类型:键值数据库

    键可以是一个字符串对象,值可 以是任意类型的数据。如整型、 字符型、数组、列表、集合等。

相关产品 Redis、Riak、SimpleDB、Chordless、Scalaris、Memcached
数据模型 键/值对:键是一个字符串对象 值是可以任意类型的数据,比如整型、字符型、数组、列表、集合等
典型应用 涉及频繁读写、拥有简单数据模型的应用内容缓存,比如会话、配置文件、参数、购物车等存储配置和用户数据信息的移动应用
优点 扩展性好,灵活性好,大量写操作时性能高
缺点 无法存储结构化信息,条件查询效率较低
不适用情形 不是通过键而是通过值来查:键值数据库根本没有通过值查询的途径;需要存储数据之间的关系:在键值数据库中,不能通过两个获两个以上的键来关联数据;需要事务的支持:在一些键值数据库中,产生故障时,不可以回滚
使用者 百度云数据库(Redis)、GitHub(Riak)、BestBuy(Riak)、Twitter (Redis和Memcached)、StackOverFlow(Redis)、Instagram (Redis)、Youtube(Memcached)、Wikipedia(Memcached)

4-19 NoSQL的主要类型:列族数据库

相关产品 BigTable、Hbase、Cassandra、HadoopDB、GreenPlum、PNUTS
数据模型 列族
典型应用 分布式数据存储与管理;数据在地理上分布于多个数据中心的应用程序;可以容忍副本中存在短期不一致情况等的应用程序;拥有动态字段的应用程序;拥有潜在大量数据的应用程序,大到几百TB的数据
优点 查找速度快,可扩展性强,容易进行分布式扩展,复杂性低
缺点 功能较少,大都不支持强事务一致性
不适用情形 需要ACID事务支持的情形,Cassandra等产品就不适用
使用者 Ebay(Cassandra)、Instagram(Cassandra)、NASA(Cassandra) Twitter(Cassandra and HBase)、Facebook(HBase)、Yahoo! (HBase)

4-20 NoSQL的主要类型:文档数据库

相关产品 MongoDB、CouchDB、Terrastore、ThruDB、RavenDB、SisoDB、CloudKit、Perservere、Jackrabbit
数据模型 键/值 值(value)是版本化的文档
典型应用 存储、索引并管理面向文档的数据或者类似的半结构化数据比如,用于后台具有大量读写操作的网站、使用JSON数据结构的应用、 使用嵌套结构等非规范化数据的应用程序
优点 性能好(高并发),灵活性高,复杂性低,数据结构灵活提供嵌入式文档功能,将经常查询的数据存储在同一个文档中既可以根据键来构建索引,也可以根据内容构建索引
缺点 缺乏统一的查询语法
不适用情形 在不同的文档上添加事务。文档数据库并不支持文档间的事务,如果对这方面有需求则不应该选用这个解决方案
使用者 百度云数据库(MongoDB)、SAP(MongoDB)、Codecademy(MongoDB)、Foursquare(MongoDB)、NBC News(RavenDB)

4-21 NoSQL的主要类型:图形数据库

相关产品 Neo4J、OrientDB、InfoGrid、Infinite Granph、GraphDB
数据模型 图结构
典型应用 专门用于处理具有高度相互关联关系的数据,比较适合于社交网络、模式识别、依赖分析、推荐系统以及路径寻找等问题
优点 灵活性高,支持复杂的图形算法,可用于构建复杂的关系图谱
缺点 复杂性高,只能支持一定的数据规模
使用者 Adobe(Neo4J)、Cisco(Neo4J)、T-Mobile(Neo4J)

搭建 Restful Web 服务

1. 理解 REST

  REST 全称是 Representational State Transfer,中文意思是表征性状态转移。它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。

  理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。

1.1. REST 原则

  • 资源 可通过目录结构样式的 URIs 暴露

  • 表述 可以通过 JSON 或 XML 表达的数据对象或属性来传递

  • 消息 使用统一的 HTTP 方法(例如:GET、POST、PUT 和 DELETE)

  • 无状态 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息

    1.2. HTTP 方法

      使用 HTTP 将 CRUD(create, retrieve, update, delete <创建、获取、更新、删除—增删改查>)操作映射为 HTTP 请求。如果按照HTTP方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如GET和HEAD请求都是安全的, 无论请求多少次,都不会改变服务器状态。而GET、HEAD、PUT和DELETE请求都是幂等的,无论对资源操作多少次, 结果总是一样的,后面的请求并不会产生比第一次更多的影响。

    1.2.1. GET

  • 安全且幂等

  • 获取信息

    1.2.2. POST

  • 不安全且不幂等

  • 使用请求中提供的实体执行操作,可用于创建资源或更新资源

    1.2.3. PUT

  • 不安全但幂等

  • 使用请求中提供的实体执行操作,可用于创建资源或更新资源

    1.2.4. DELETE

  • 不安全但幂等

  • 删除资源
      POST和PUT在创建资源的区别在于,所创建的资源的名称(URI)是否由客户端决定。 例如为我的博文增加一个java的分类,生成的路径就是分类名/categories/java,那么就可以采用PUT方法。不过很多人直接把POST、GET、PUT、DELETE直接对应上CRUD,例如在一个典型的rails实现的RESTful应用中就是这么做的。

    1.3. HTTP status codes

      状态码指示 HTTP 请求的结果:

  • 1XX:信息

  • 2XX:成功

  • 3XX:转发

  • 4XX:客户端错误

  • 5XX:服务端错误

    1.4. 媒体类型

      HTTP头中的 Accept 和 Content-Type 可用于描述HTTP请求中发送或请求的内容。如果客户端请求JSON响应,那么可以将 Accept 设为 application/json。相应地,如果发送的内容是XML,那么可以设置 Content-Type 为 application/xml 。

    2. REST API 设计最佳实践

      这里介绍一些设计 REST API 的最佳实践,大家先记住下面这句话:

    URL 是个句子,其中资源是名词、HTTP 方法是动词。

    2.1. 使用名词来表示资源

      下面是一些例子:

  • GET – /users:返回用户列表

  • GET – /users/100:返回一个特定用户

  • POST – /users:创建一个新用户

  • PUT – /users/200:更新一个特定用户

  • DELETE – /users/711:删除一个特定用户
      不要使用动词:

  • /getAllsers

  • /getUserById

  • /createNewUser

  • /updateUser

  • /deleteUser

    2.2 在 HTTP 头中使用适当的序列化格式

      客户端和服务端都需要知道通信所用的格式,这个格式要在 HTTP 头中指定:

  • Content-Type 定义请求格式

  • Accept 定义一个可接受的响应格式列表

    2.3 Get 方法和查询参数不应当改变状态

      使用 PUT, POST 和 DELETE 方法来改变状态,不要使用 GET 方法来改变状态:

  • GET /users/711?activate

  • GET /users/711/activate

    2.4. 使用子资源表示关联

      如果一个资源与另一个资源关联,使用子资源:

  • GET /cars/711/drivers/ 返回711号汽车的驾驶员列表

  • GET /cars/711/drivers/4 返回711号汽车的第4号驾驶员

    2.5. 使用适当的 HTTP 方法 (动词)

      再回顾一下这句话:

    URL 是个句子,其中资源是名词、HTTP 方法是动词。

  • GET:获取在URI资源中指定的表述,响应消息体包含所请求资源的细节。

  • POST:创建一个URI指定的新资源,请求消息体提供新资源的细节。注意,POST也可以触发一些操作,而不一定是要创建新资源。

  • PUT:创建或替代指定URI的资源。请求消息体指定要创建或更新的资源。

  • DELETE:移除指定URI的资源。

    2.6. HTTP 响应状态码

      当客户端通过API向服务端发起一个请求时,客户端应当知道反馈:是否失败、通过或者请求错误。HTTP 状态码是一批标准化代码,在不同的场景下有不同的解释。服务器应当总是返回正确的状态码。
      下面是重要的HTTP代码分类:

  • 2xx (成功分类):这些状态码代码请求动作被接收且被服务器成功处理。

    • 200:Ok 表示 GET、PUT 或 POST 请求的标准状态码。
    • 201:Created(已创建)表示实例已被创建,多用于 POST 方法。
    • 204:No Content(无内容)表示请求已被成功处理但没有返回任何内容。常用于 DELETE 方法返回。
  • 3xx (转发分类)

    • 304:Not Modified(无修改)表示客户端已经缓存此响应,无须再次传输相同内容。
  • 4xx (客户端错误分类):这些状态码代表客户端提交了一个错误请求。

    • 400:Bad Request(错误请求)表示客户端请求没被处理,因为服务端无法理解客户端请求。
    • 401:Unauthorized(无授权)表示客户端无权访问资源,应当加上所需的认证信息后再次请求。
    • 403:Forbidden(禁止访问)表示请求有效且客户端已获授权,但客户端无权访问该资源。
    • 404:Not Found(没发现)表示所请求的资源现在不可用。
    • 410:Gone(移除)表示所请求的资源已被移除。
  • 5xx (服务端错误分类)

    • 500:Internal Server Error(内部服务器错误)表示请求有效,但是服务端发生了异常。
    • 503:Service Unavailable(服务不可用)表示服务器关闭或不可用,通常是指服务器处于维护状态。

      2.7. 名称规约

        你可以遵循任何名称规约,只要保持跨应用一致性即可。如果请求体和响应体是 JSON 类型,那么请遵循驼峰名称规约。

      2.8. 搜索、排序、过滤与分页

        上面一些示例都是在一个数据集上的简单查询,对于复杂的数据,我们需要在 GET 方法 API 上加一些参数来处理。下面是一些示例:

  • 排序:这个例子中,客户想获取排序的公司列表,GET /companies 应当在查询时接受多种排序参数。譬如 GET /companies?sort=rank_asc 将以等级升序的方式对公司进行排序。

  • 过滤:要过滤数据集,我们可以通过查询参数传递不同的选项。比如 GET /companies?category=banking&location=india 将过滤分类为银行且位于印度的公司。

  • 搜索:在公司列表中搜索公司名的 API 端点应当是 GET /companies?search=Digital。

  • 分页:当数据集太大时,我们应当将数据集分割成小的数据块,这样有利于提升服务端性能,也方便客户端处理响应。如 GET /companies?page=23 意味着获取公司列表的第 23 页数据。

    2.9. Restful API 版本

      一般使用不带点的简单数字表示版本,数字前加字母v代表版本号,如下所示:

  • /blog/api/v1

  • http://api.yourservice.com/v1/companies/34/employees

    2.10. 处理 JSON 错误体

      API 错误处理机制是很重要的,而且要好好规划。极力推荐总是在返回字段中包含错误消息。一个 JSON 错误体应当为开发者提供一些有用的信息:错误消息、错误代码以及详细描述。下面是一个较好的示例:

    {
    "code": 1234,
    "message": "Something bad happened :(",
    "description": "More details about the error here"
    }

    2.11. 如何创建 Rest API URL

      推荐使用下面格式的 URL:

  • http(s)://{域名(:端口号)}/{表示REST API的值}/{API版本}/{识别资源的路径}

  • http(s)://{表示REST API的域名(:端口号)}/{API 版本}/{识别资源的路径}
      如下所示:

  • http://example.com/api/v1/members/M000000001

  • http://api.example.com/v1/members/M000000001

    3. 开发基于 Spring Boot 的 Restful Web 服务

      Spring Boot 提供了构建企业应用中 RESTful Web 服务的极佳支持。

    3.1. 引入依赖

      要构建 RESTful Web 服务,我们需要在构建配置文件中加上 Spring Boot Starter Web 依赖。
      对于 Maven 用户,使用以下的代码在 pom.xml 文件中加入依赖:

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>    
    </dependency>

      对于 Gradle 用户,使用以下的代码在 build.gradle 文件中加入依赖:

    compile('org.springframework.boot:spring-boot-starter-web')

    3.2. Rest 相关注解

      在继续构建 RESTful web 服务前,建议你先要熟悉下面的注解:

    Rest Controller

      @RestController 注解用于定义 RESTful web 服务。它提供 JSON、XML 和自定义响应。语法如下所示:

    @RestController
    public class ProductServiceController {
    }

    Request Mapping

      @RequestMapping 注解用于定义请求 URI 以访问 REST 端点。我们可以定义 Request 方法来消费 produce 对象。默认的请求方法是 GET:

    @RequestMapping(value = "/products")
    public ResponseEntity<Object> getProducts() { }
    Request Body
    @RequestBody 注解用于定义请求体内容类型。
    public ResponseEntity<Object> createProduct(@RequestBody Product product) {
    }

    Path Variable

      @PathVariable 注解被用于定义自定义或动态的请求 URI,Path variable 被放在请求 URI 中的大括号内,如下所示:

    public ResponseEntity<Object> updateProduct(@PathVariable("id") String id) {
    }

    Request Parameter

      @RequestParam 注解被用于从请求 URL 中读取请求参数。缺省情况下是必须的,也可以为请求参数设置默认值。如下所示:
    public ResponseEntity getProduct(
    @RequestParam(value = "name", required = false, defaultValue = "honey") String name) {
    }

    3.3. 编写 REST API

    GET API

      下面的示例代码定义了 HTTP GET 请求方法。在这个例子里,我们使用 HashMap 来在存储 Product。注意我们使用了 POJO 类来存储产品。
      在这里,请求 URI 是 /products,它会从 HashMap 仓储中返回产品列表。下面的控制器类文件包含了 GET 方法的 REST 端点:

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
       static {
          Product honey = new Product();
          honey.setId("1");
          honey.setName("Honey");
          productRepo.put(honey.getId(), honey);
    
          Product almond = new Product();
          almond.setId("2");
          almond.setName("Almond");
          productRepo.put(almond.getId(), almond);
       }
       @RequestMapping(value = "/products")
       public ResponseEntity<Object> getProduct() {
          return new ResponseEntity<>(productRepo.values(), HttpStatus.OK);
       }
    }

    POST API

      HTTP POST 请求用于创建资源。这个方法包含请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的示例代码定义了 HTTP POST 请求方法。在这个例子中,我们使用 HashMap 来存储 Product,这里产品是一个 POJO 类。
      这里,请求 URI 是 /products,在产品被存入 HashMap 仓储后,它会返回字符串。

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products", method = RequestMethod.POST)
       public ResponseEntity<Object> createProduct(@RequestBody Product product) {
          productRepo.put(product.getId(), product);
          return new ResponseEntity<>("Product is created successfully", HttpStatus.CREATED);
       }
    }

    PUT API

      HTTP PUT 请求用于更新已有的资源。这个方法包含请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的例子展示了如何定义 HTTP PUT 请求方法。在这个例子中,我们使用 HashMap 更新现存的产品。此处,产品是一个 POJO 类。
      这里,请求 URI 是 /products/{id},在产品被存入 HashMap 仓储后,它会返回字符串。注意我们使用路径变量 {id} 定义需要更新的产品 ID:

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT)
       public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
          productRepo.remove(id);
          product.setId(id);
          productRepo.put(id, product);
          return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
       }   
    }

    DELETE API

      HTTP Delete 请求用于删除存在的资源。这个方法不包含任何请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的例子展示如何定义 HTTP DELETE 请求方法。这个例子中,我们使用 HashMap 来移除现存的产品,用 POJO 来表示。
      请求 URI 是 /products/{id} 在产品被从 HashMap 仓储中删除后,它会返回字符串。 我们使用路径变量 {id} 来定义要被删除的产品 ID。

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE)
       public ResponseEntity<Object> delete(@PathVariable("id") String id) {
          productRepo.remove(id);
          return new ResponseEntity<>("Product is deleted successsfully", HttpStatus.OK);
       }
    }

    2022年软件开发趋势:远程工作已成主流

    看看明年会发生什么。

    2020 年 3 月,工作世界发生了翻天覆地的变化。到 2020 年 4 月,大约一半的公司报告称,由于新冠病毒,其 80% 以上的员工在家工作。大多数人再也没有回到办公室——远程工作将继续存在。

    被迫在网上生活,技术变得至关重要。 数字化转型现在是任何想要跟上步伐的组织的基本要求。以前就很抢手的技术工作者,现在更受追捧,以帮助建立一个我们都依赖技术进行最基本活动的世界。

    2022 年技术将如何支持远程工作

    Gartner 的一项调查显示,由于新冠病毒,69% 的董事会加速了数字化转型。这一趋势将继续存在,组织的重点是非接触式服务 (60.1%)、迁移到云 (52.25%) 以及 DevOps 活动 (51.75%)。

    根据数据,我们预测了2022年的一些软件发展趋势:

    1. 云将变得越来越重要

    云将在 2022 年及未来几年在技术中发挥越来越重要的作用。可以迁移到云端的所有东西都将迁移到云端。

    以公司新开发人员的入职为例。他们通常会花费几周的时间来尝试让所有东西都在本地机器上运行。这非常耗时,不仅对于新员工来说,而且对于需要在此过程中提供帮助的经验丰富的工程师来说也是如此。

    到目前为止,大多数自动化构建、模拟环境和正在运行的生产应用程序已经迁移到云端。下一步是本地开发环境。

    微软和亚马逊已经开始着手这方面的工作,并且在 2021 年都发布了解决方案(微软的 GitHub Codespaces 和亚马逊的 AWS Cloud9),这些解决方案可以在几秒钟内提供可在浏览器上访问的开发环境。

    2. DevOps 将发挥重要作用

    谷歌的 DORA 进行的研究表明,“优秀的执行工程组织实现其组织目标的可能性是一般组织的两倍,并在三年内实现了 50% 的高增长率”。

    为了加快管道并快速交付新功能,团队需要确保他们的流程和工具尽可能好,消除障碍和瓶颈。因此,DevOps 和实现持续交付的实践至关重要。

    3. 人工智能辅助开发

    2021 年,我们已经见证了人工智能开始进入开发工具。GitHub Copilot、IBM AI for Code 和 Oracle 的新查询语言生成器是指向 AI 辅助开发方向的一些创新。

    2022 年,Forrester “预计几乎所有开发工具中都会出现 AI 机器人,为开发人员的工具箱添加自然语言和其他功能”。

    4. 低代码平台的崛起

    2017 年,《福布斯》将低代码平台归类为“极具颠覆性”,而且这一趋势还在加速。Gartner 预测:“到 2022 年,低代码应用平台预计仍将是低代码开发技术市场的最大组成部分,比 2020 年增长近 30%,到 2021 年达到 58 亿美元”。并补充说“到 2024 年,低代码 应用开发将占应用开发活动的 65% 以上。”

    公司如何支持远程工作者?

    “新常态”将继续存在。但这对雇主和雇员意味着什么?

    研究表明,拥有积极体验的远程员工的工作效率提高了 28%,敬业度提高了 46%。对公司来说有一个明显的好处:那些提供优质远程员工体验的公司可以提高 25% 的利润,降低 37% 的流失率。

    这些数字看起来很乐观,但它们仅适用于远程工作员工体验是正确的,并作为公司的优先考虑。但是如何才能做到这一点呢?

    管理团队工作量的技巧

    远程工作带来了许多挑战,并加剧了雇主和雇员之前已经面临的斗争。 研究表明,在2020 年,71% 的员工经历过倦怠,87% 的员工不得不加班。

    倦怠远非个人问题。仅在美国,职业倦怠员工的心理和身体问题每年就花费 125 至 1900 亿美元的医疗保健费用。再加上生产力的下降、高流动率和组织中人才的流失。 而且,最重要的成本是员工的福祉。

    为了解决这个问题,公司需要掌握工作量管理。但这不仅仅是为了确保您的员工不会过度劳累。相反,它是关于在团队之间战略性地分配工作以实现尽可能高的生产力水平,利用个人优势,并承认每个成员的弱点。以下是一些将其付诸实践的技巧:

    1. 定义优先级

    如果员工不知道哪些任务是紧急的,哪些不是,他们就无法自我管理,也不能在确定工作优先级时做出明智的决定。试着为每项任务设定最后期限,因为这将为人们提供指导,以了解他们是否在正轨上。

    2. 制定轮班时间表

    朝九晚五的工作时间表早已过时。现在,大多数公司允许员工管理他们的工作时间,提供灵活性,让人们将工作融入他们的生活,而不是相反。

    但这会在团队成员之间造成压力和工作量不平衡。例如,在某个时间没有足够的团队成员工作可能意味着其他人的工作量更大。

    分析公司需求,并在允许灵活性的同时组织人员的日程安排,以确保灵活性不会破坏生产力和团队成员之间的健康平衡。

    3. 让人们了解情况

    尤其是在远程环境中,保持信息可用很重要。请记住,远程工作人员不会随便在咖啡机旁聊天,也不会在走廊里偶遇。积极努力确保每个人都知道他们需要知道什么。

    4. 保持持续和开放的沟通

    在前一点的基础上,确保人们获得他们需要的信息。当他们这样做时,一切都会运行得更快、更顺畅。考虑举行定期会议,人们可以与他们的团队或整个组织共享相关信息。

    另一方面,保持沟通渠道向另一个方向开放。让您的员工觉得他们可以表达自己的感受和意见。这项研究表明,74% 的员工表示,当他们感到被倾听时,他们的工作效率更高。

    通过推行健康的工作量管理,您可以让员工和团队更快乐,让他们更有效率并愿意为公司的成功做出贡献。

    员工如何促进工作与生活的平衡

    公司在员工福利方面发挥着重要作用,但员工也必须发挥自己的作用。美国精神病学协会于 2021 年初对远程工作者进行了一项在线调查。研究结果令人担忧:“大多数在家工作的员工表示,他们经历了负面的心理健康影响,包括孤立、孤独,以及在一天结束后难以离开工作。”

    如果你在远程工作,甚至是混合工作,你需要注意自己的身心健康。在大流行之初广泛传播的建议仍然有效且至关重要,尤其是当您因数月乃至数年的远程工作而感到疲倦时。以下是在家工作时要记住的一些技巧:

    1. 创造一个合适的家庭办公环境

    不要在床上或者沙发上工作。在房子里指定一个特定的空间作为你的家庭办公室,并这样对待它。确保你有一张合适的桌子、一把舒适的椅子和自然光。

    2. 使用质量技术

    随着远程工作的采用,许多公司为员工提供资金来装备他们的家庭工作空间。利用这一点并购买必要的设备,以保证尽可能好的工作空间。如果您的公司不提供支持,那么也许值得与 HR 提出这个话题。

    3. 保持一致的工作时间

    虽然远程工作通常具有灵活工作时间的好处,但请注意其缺点:如果您没有时间表,工作将占据您的一整天。

    要充分利用灵活性来安排一些活动:诸如送孩子上学或去看医生,但不要让其他所有事情都进入您的工作时间,否则您会有一直在值班的感觉。

    4. 吃好睡好

    时刻给予身体适当的休息和营养。在家工作时,很容易养成不健康的习惯,例如熬夜或全天吃零食。小心,因为这些会影响您的工作效率和整体幸福感。

    5. 移动你的身体

    特别是疫情期间,很容易在空闲时间坐在同一张桌子前,看着屏幕完成工作。

    人的身体不是整天坐着的。它是用来移动、感受刺激并与他人互动的。确保您在工作时间休息并有适当的午餐时间,您甚至可以去那里散步。工作完成后,让你的身体动起来。出去,做一些运动,或者尝试瑜伽来伸展你的肌肉,减轻坐姿不良引起的疼痛。

    结论

    GitHub 的 2021 年 Octoverse 状态报告显示,虽然 41% 的受访者在疫情之前曾在同一办公室办公,但预计只有 10.7% 的受访者在疫情结束后会留在办公室。此外,与疫情前相比,完全接受远程工作的公司预计将增加 46%。

    这些趋势表明,数字化转型是生存的关键。然而,70% 的数字化转型计划未能实现其目标。


    【注】本文译自:Software Development Trends for 2022 – DZone Agile

    开发人员的编程心理学

    向开发人员提供建议的编程心理学

    我之前写过,编程有两个受众:CPU 和你的编程伙伴。

    还有一些优秀的文章,比如《面向苦难编程》 ,可以帮助你在编程时调整目标——让它工作、让它漂亮、让它快速,这是那篇文章的建议。

    “让它工作、让它漂亮、让它快速”是绝妙的编程建议,也是我从第一次读它开始就一直牢记在心的建议。

    编程建议程序首先以 CPU 为目标——即“使其工作”。

    1. 合理的编程建议

    然后建议针对您的编程伙伴,即必须维护或查看代码的人,让代码漂亮。

    一旦您的代码成功满足其计算要求,并满足与我们一起共事的普通人能够理解的要求,那么,如有必要,我们就可以专心致志地不断完善它。事实上, 代码“漂亮”意味着有可能更容易找到改进的机会,因为在大多数情况下,使代码“漂亮”意味着更小的、独立的函数,从而使得更易优化。

    我最近在网上和一位朋友聊天,他提出了一个问题,这也反映了我的经历:他的任务是集成一组代码,这些代码是在没有团队监督的情况下创建的。这些代码缺乏测试,并且是独立编写的,没有遵循与主项目相同的编码标准。

    这是一个艰难的处境。集成这样的代码意味着要试图弄清切入点——这是测试的职责——但由于没经过测试,你不得不相信编码者实际上满足了需求,因为在理想情情形下,测试也要证明这一点。

    你会怎么做?你会给什么编程建议?

    2. 无冲突面对

    如我所言,我遇到过类似情形,虽然我承认还可以处理得更好,但同时我也认为自己已经做得够好了。

    人们不喜欢被面对,面对什么并不重要。迪特里希·邦霍费尔(Dietrich Bonhoeffer)有一个深刻的洞察,即个体可能很愚笨,但群体可能超级愚笨——更糟糕的是,抵触挑战,抵抗力会随着参与人数的增加而增加。(你可以很容易地改变一个朋友的想法——但是改变一群七个人的想法却难于上青天。)

    所以我所做的就是把自己描绘成一个受到他们的代码库挑战的人。我没有评论实际代码或它们有多可怕:我让自己专注是于学习代码,因为我不理解它。

    这个功能的测试在哪里?”我问道,尽管我知道没有测试。毕竟,我可能是错的……问测试在哪里是对他们的温和刺激,以便我能从他们那里有所收获。

    这个问题给了他们很大的回旋余地。

    3 强迫自我反省

    他们可以指出测试在哪里确实满足了我的需求;也许我只是没看到?(在这种情况下,不存在测试,我知道,但这不是重点。我需要他们考虑可能性。)

    他们也可以自己观察到,也许测试不存在,作为交接的要求,也许他们可以写一个

    当您还没有设计用于测试的代码时,测试很困难,但在您接受代码之前,这不是您的问题,这让他们有机会修改自己对代码的理解,*而不必在意对他们代码的看法*。

    当然,也许你没有权限要求这样的测试。在这种情况下,您可能需要向利益相关者(负责交付代码的人)请求帮助,并指出未测试代码的集成引入了可变的可靠性(即,它是不可靠的,因为您不能假设它是可靠的)。

    与利益相关者就可靠性进行对话可能会您有权回去进行结构和测试的对话。

    4. 编程建议心理学

    再者说,这种对话完全可以颠倒过来。如果他们编写了意大利面条式的代码并为此感到自豪——谁不会呢?—您会简单地要求他们编写它,以便您的小脑袋可以像理解根据组织约定编写的代码一样容易地理解它。

    “哇,那个243行的函数太厉害了,就是看不懂。你能告诉我我们如何将它分解并重构为具有更小的函数和组合吗? 而这个对“j”的引用,是一个索引吗?我们能把它命名为它实际代表的东西吗?是窗口句柄吗? 请帮帮我,我真的不明白。”

    这是基本的心理学原理。在某种程度上,这是一种操纵,但我们每天和每次互动都以温和(希望是善良)的方式操纵人们:当我们与人打招呼时,我们会微笑,以触发特定的内啡肽,我们首先提到好消息(或许不是)创造有利于我们想要的特定心态。为自己的目的使用人们的思维和感知方式并不奇怪,如果结果是正面的,那么这样做也不是坏事。

    不要害怕用心理学来帮助你编程。这可能很难,因为有时它意味着你不能对那些可能真的需要大喊大叫的人大喊大叫——但大喊大叫往往会适得其反,如果目标是富有成效,那么我们需要考虑如何创造我们的工作环境,而不是通过按住别人来满足我们的私欲。


    注:本文译自:The psychology of offering programming advice to developers – Coffee Talk: Java, News, Stories and Opinions (theserverside.com)

    软件开发中的常见的15个定律和原则释义及应用

    在围绕软件开发的讨论中,几乎不可能避免引用一两条定律。

    “这行不通,因为‘X法则’!” 你可能听过人们说。或者“你不知道‘Y原则’吗? 你是哪种软件开发人员?”。

    有许多法律和原则可以引用,其中大部分都基于真理。然而,盲目地使用像上面这样的绝对陈述来应用它们肯定会导致自负和失败。

    本文列举了一些可以应用于软件开发的最流行的规律和原则。对于每条规律,我们将快速讨论其主要命题,然后探讨如何将其应用于软件开发(也许何时不应该)。

    帕累托原则(80/20 规则)

    解释

    帕累托原则指出,通常 80% 的结果来自 20% 的原因。数字 80 和 20 无论如何都不是精确的,但该原理的总体思路是结果通常分布不均。

    我们可以在生活的许多领域遵守这条规则,例如:

    • 世界上最富有的 20% 的人创造了世界上 80% 的收入,
    • 80%的犯罪是由20%的罪犯所为
    • 自 2020 年以来,我们知道 80% 的病毒传播来自 20% 的受感染人群。

    在软件开发中的应用

    我们可以从帕累托原则中获得的主要好处是专注。它可以帮助我们专注于重要的事情(20%),而不是在不重要的事情(其他 80%)上浪费时间和精力。不重要的事情对我们来说似乎很重要,因为有太多(而且看起来很紧急)。但最好的结果往往是通过专注于重要的少数来实现的。

    在软件开发中,我们可以基于这个原则来专注于构建正确的功能,例如:

    • 专注于构成产品价值 80% 的 20% 的产品功能,
    • 专注于导致 80% 用户沮丧的 20% 错误,
    • 专注于 80% 的产品功能需要 20% 的总时间来构建,
    • ……

    只是问“现在最重要的事情是什么?” 能够帮助你完成下一个最重要的事情,而不是下一个最紧急的事情。

    顺便说一下,敏捷和 DevOps 等现代开发方法有助于获得这种关注!具有定期用户反馈的快速迭代允许对重要事项进行数据驱动的决策。诸如基于主干的带有功能标记的开发(例如使用 LaunchDarkly)之类的实践可以帮助软件团队实现目标。

    破窗定理

    解释

    一扇被打破的窗户会招来恶意破坏,所以用不了多久,所有的窗户都被打破了。

    一般来说:混乱会带来更多的混乱

    如果我们的环境是原始的,我们就会有动力保持这种状态。环境中的混乱越多,我们添加混乱的门槛就越低。毕竟已经混乱了……谁在乎我们是否再添加一点呢?

    我们可以从这条规则中获得的主要好处是我们应该意识到我们周围的混乱。如果人们习惯于它,不再关心它了,那么最好为混乱带来一些秩序。

    在软件开发中的应用

    在软件开发中,我们可以将其应用于代码质量:我们引入代码库的每一种代码异味都会降低我们添加更多代码异味的门槛。我们应该 [[开始清理]] 并保持代码库干净以避免这种情况发生。许多代码库如此难以理解和维护的原因是,破窗已经悄然出现并且没有足够快地修复。

    我们也可以将这个原则应用到测试覆盖率上:一旦有一定数量的代码进入了未被测试覆盖的代码库,就会添加更多未被覆盖的代码。这是保持 100% 代码覆盖率(应该覆盖的代码的)的论据,因此我们可以在窗口破裂之前看到裂缝。

    奥卡姆剃刀

    解释

    剃刀哲学是一种原理,它通过消除(或“削除”)不可能的解释来帮助解释某些事情。

    奥卡姆剃刀指出,如果有多个假设,我们应该选择假设最少的假设(这很可能是解释最简单的假设)。

    在软件开发中的应用

    我们可以在事件分析中应用奥卡姆剃刀。您可能遇到过这样的情况:用户报告了您的应用程序存在问题,但您不知道导致问题的原因。因此,您搜索日志和指标,试图找到根本原因。

    下次用户报告错误时,维护一个事件调查文档。写下您对导致问题的原因的假设。然后,对于每个假设,列出事实和假设。如果一个假设被证明是正确的,则将其标记为事实。如果某个假设被证明是错误的,请将其从文档中删除或将其标记为错误。在任何时候,您都可以将时间集中在最可能的假设上,而不是浪费时间寻找不相干的东西。

    达克效应

    解释

    邓宁-克鲁格效应表明,没有经验的人往往会高估自己的能力,而有经验的人往往会低估自己的能力

    如果你不擅长某件事,你会认为你擅长它。如果你擅长某事,你认为你不擅长 – 这可能导致骗子综合症,这让你非常怀疑自己的能力,以至于你在其他具有相似技能的人中感到不舒服 – 不必要地害怕被质疑是一个骗子。

    在软件开发中的应用

    意识到这种认知偏差已经是朝着正确方向迈出的重要一步。它将帮助您更好地评估自己的技能,以便您可以寻求帮助,或克服自我怀疑并自己动手。

    有助于消除达克效应和骗子综合症的一种做法是结对或群体编程。你不是独自工作,沉浸在自我怀疑或优越感中,而是与其他人密切合作,边工作边交流思想、学习和教学。

    不过,这只适用于安全的环境。在个人主义被美化的环境中,结对或群体编程会导致更多的自我怀疑或更多的优越感妄想。

    彼得原则

    解释

    彼得原则指出,只要你成功,你就会得到晋升,直到你最终得到一份你不胜任的工作。由于您不再成功,您将不再获得晋升,这意味着您将生活在一份不会给您带来满足感或成功的工作中,通常这种感觉将在一直伴随在您的余生。

    前景黯淡。

    在软件开发中的应用

    在软件开发中,当您将角色从开发人员职业转换为管理职业时,彼得原则通常适用。然而,成为一名优秀的开发人员并不一定意味着你是一名优秀的经理。或者,您可能是一名优秀的经理,但却不能从经理工作中获得开发工作中所能获得的满足感,这意味着您没有全力以赴(这就是我的情况)。在任何情况下,你都很悲惨,在你面前的职业道路上看不到任何未来的发展。

    在这种情况下,退后一步,决定你想要什么样的职业生涯。然后,转换角色(或公司,如果需要)以获得您想要的角色。

    帕金森定律

    解释

    帕金森定律指出,工作总是会占据分配给它的时间。如果您的项目在两周的截止日期,则该项目将不会在此之前完成。 可能需要更长的时间,是的,但绝不会少于我们为它分配的时间,因为我们正在用不必要的工作或拖延来填补时间。

    在软件开发中的应用

    帕金森定律的主要驱动因素是:

    • 拖延症(“截止日期太远了,所以我现在不需要赶时间……”),还有
    • 范围蔓延(“当然,我们可以添加这个小功能,它不会花费我们太多时间……”)。

    为了战胜拖延症,我们可以把最后期限设置为几天而不是几周或内个月。在接下来的 2-3 天内需要做什么才能朝着目标前进?一个(健康的!)截止日期可以给我们足够的动力,让我们不要陷入拖延症的泥潭。

    为了防止范围蔓延,我们应该非常清楚地知道我们想要通过项目实现什么。成功的衡量标准是什么? 这个新功能是否会增强这些指标?那么如果每个人都明白这项工作需要更长的时间,我们应该添加它。如果新功能不符合使命,那就不用管它。

    霍夫施塔特定律

    解释

    霍夫施塔特定律指出:“即使考虑了霍夫施塔特定律,它所花的时间也比你预期的长”。

    即使您了解了这条法律,并增加了项目的时间分配,它仍然会比您预期的要长。这与帕金森定律密切相关,即工作总是会填满分配给它的时间。只是霍夫施塔特定律说它填充的时间超过了分配的时间。

    这条定律得到了心理学的支持。我们容易犯所谓的“计划谬误”,即在估算工作量时,我们通常不会考虑所有可用信息,即使我们认为我们已经考虑了。我们的估计几乎总是主观的,很少是正确的。

    在软件开发中的应用

    在软件开发(以及任何其他基于项目的工作)中,我们人类的乐观情绪发挥了很大作用。评估几乎总是过于乐观。

    为了减少霍夫施塔特定律的影响,我们可以尝试尽可能客观地进行估计。

    写下关于项目的假设和事实。将每个项目标记为假设或事实,以使数据质量可见并管理预期。

    不要依赖直觉,因为每个人的感受都不一样。写下估算值,让你的大脑思考它们。将它们与其他人的估计进行比较,然后讨论差异。

    即便如此,它仍然只是一个估计,很可能不能反映现实。如果估算不是基于统计数据或其他历史数据,那么它的价值就非常低,因此与要求您估算的人一起管理预期总是好的——这总是会出错的。如果你让它尽可能客观,它就会减少错误。

    康威定律

    解释

    康威定律指出,一个组织创建的任何系统都与该组织的团队和沟通结构相似。系统将在构建系统的团队有接口的地方具备接口。如果你有 10 个团队在一个系统上工作,你很可能会得到 10 个相互通信的子系统。

    在软件开发中的应用

    我们可以应用所谓的逆康威策略:创建最能支持我们想要构建的系统架构的组织结构。

    没有固定的团队结构,而是要有足够的灵活性来创建和解散团队,这对系统的当前状态是最好的。

    墨菲定律

    解释

    墨菲定律说,任何可能出错的事情,都会出错。它经常在意外发生后被引用。

    在软件开发中的应用

    软件开发是一个容易出错的职业。出错的主要来源是错误。没有任何一款软件不存在漏洞或事故,从而考验测试用户的耐心。

    我们可以通过在日常软件开发实践中养成减少错误影响的习惯来抵御墨菲定律。我们无法完全避免错误,但我们可以而且应该减少它们对用户的影响。

    对抗墨菲定律最有用的做法是特征标记。如果我们使用像 LaunchDarkly 这样的功能标记平台,我们可以在功能标记后面将更改部署到生产中。然后,我们可以使用有针对性的推出来激活内部 dogfooding 的标志,然后为少量友好的 Beta 用户激活它,最后将其发布给所有用户。这样,我们可以从越来越挑剔的用户群体那里获得关于变更的反馈。如果更改出错(并且在某些时候会出错)影响就很小,因为只有一小部分用户组会受到它的影响。而且,该标志可以快速关闭。

    布鲁克定律

    解释

    在经典著作《人月神话》中,弗雷德·布鲁克 (Fred Brook) 有句名言:为延期的项目增加人力会使项目延期更多

    尽管本书讨论的是软件项目,但它适用于大多数类型的项目,甚至是软件开发之外的项目。

    添加人员不会提高项目速度的原因是项目的通信开销随着添加到项目中的每个人呈指数增长。2 个人有 1 条通信路径,5个人已经有 5! = 120 条可能的通信路径。新人安顿下来并确定他们需要的沟通路径需要时间,这就是为什么在项目中添加新人时,迟到的项目会更晚。

    在软件开发中的应用

    很简单。改变截止日期,而不是在已经延期的项目中增加人力。

    对于向软件项目中增加新人的期望要切合实际。将人员添加到项目中可能会在某个时候提高速度,但并非总是如此,当然也不是立竿见影。人员和团队需要时间来适应日常工作,而在某些时候,工作无法充分并行化,因此增加更多人是没有意义的。 仔细考虑一个新人应该完成什么任务,以及在将该人添加到项目中时您期望什么。

    波斯特定律

    解释

    波斯特定律也被称为稳健性原则,它指出你应该“在你所做的事情上保守,在你接受别人的事情上自由”。

    换句话说,您可以接受多种不同形式的数据,以使您的软件尽可能灵活,但您在处理这些数据时应该非常小心,以免因无效或恶意数据而损害您的软件。

    在软件开发中的应用

    该定律源于软件开发,因此非常适于直接使用。

    为了增强健壮性,您的软件与其他软件或人之间的接口应允许不同形式的输入:

    • 为了向后兼容,新版本的接口应该接受旧版本和新版本的数据,
    • 为了更好的用户体验,UI 中的表单应该接受不同格式的数据,这样用户就不必担心格式。

    但是,如果我们愿意接受不同格式的数据,我们在处理这些数据时就必须保守。我们必须审查无效值,并确保我们不会因为允许太多不同的格式而损害系统的安全性。SQL 注入是一种可能的攻击,它是通过对用户输入过于宽松而启用的。

    克希霍夫原理

    解释

    克希霍夫原理指出,加密系统应该是安全的,即使它的方法是公知的。只有您用来解密某些东西的密钥才需要是私有的。

    在软件开发中的应用

    这很简单,真的。永远不要相信要求其方法是私有的加密系统。这被称为“隐藏的安全”。像这样的系统本质上是不安全的。一旦该方法向公众公开,它就容易受到攻击。

    相反,依靠公开审查和可信的对称和非对称加密系统,这些系统是在可以公开审查的开源包中实现的。每个想知道他们内部如何工作的人都可以查看代码并验证它们是否安全。

    莱纳斯定律

    解释

    在关于 Linux 内核开发的《教堂与集市》一书中,埃里克·雷蒙德 (Eric Raymond) 写道:“只要有足够的眼光,所有 bug 都是微不足道的”。他将此称为“莱纳斯定律”以纪念莱纳斯·托瓦兹。

    意思是,如果很多人看代码,那么相比很少人看代码而言,可以更好地揭露代码中的错误。

    在软件开发中的应用

    如果您想摆脱 bug,请让其他人查看您的代码。

    源于开源社区的一种常见做法是让开发人员提出包含代码更改的拉取请求(pull request),然后让其他开发人员在将拉取请求合并到主分支之前审查该拉取请求。这种做法也进入了闭源开发,但根据 Linus 定律,拉取请求在闭源环境(只有少数人查看它)中的作用不如在开源环境中(其中 可能很多贡献者都在看它)。

    其他为代码添加更多眼球的做法是结对编程和群体编程。至少在闭源环境中,这些在避免错误方面比拉取请求审查更有效,因为每个人都参与了代码的初始阶段,这为每个人提供了更好的上下文来理解代码和潜在的错误。

    沃斯定律

    解释

    沃斯定律指出,软件变慢的速度比硬件变快的速度要快

    在软件开发中的应用

    不要依赖强大的硬件来运行性能不佳的代码。相反,代码要加强性能优化。

    这必须与 [[软件开发定律#Knuth 的优化原则]] 的格言相平衡,该格言说“过早的优化是万恶之源”。要把精力花在为用户构建新功能上,而不是用于代码的性能优化上。

    通常,这是一种平衡的艺术。

    克努斯**优化原则**

    解释

    唐纳德·克努斯 (Donald Knuth) 在他的一部作品中写下了“过早优化是万恶之源”这句话,这句话经常断章取意,并被用作根本不关心优化代码的借口。

    在软件开发中的应用

    根据克努斯定律,我们不应该浪费精力过早地优化代码。然而,根据沃斯定律,我们也不应该依赖硬件足够快来执行未经优化的代码。

    最后,这就是我从这些原则中得出的结论:

    • 优化可以轻松完成且不需要太多努力的代码:例如,编写几行额外代码以避免经历可能有很多项的循环
    • 优化一直在执行的代码路径中的代码
    • 除此之外,不要在优化代码上花太多精力,除非你已经确定了一个性能瓶颈。

    保持怀疑

    定律和原则是好的。它允许我们从某个角度评估某些情况,如果没有它们,我们可能不会有这些情况。

    然而,盲目地将法律和原则应用于每种情况是行不通的。每一种情况都会带来微妙的变化,这可能意味着某个原则不能或不应该被应用。

    对你遇到的原则和定律保持怀疑。世界并不是非黑即白的。


    本文译自: Laws and Principles of Software Development – Reflectoring

    Java 项目中使用 Resilience4j 框架实现隔断机制/断路器


    到目前为止,在本系列中,我们已经了解了 Resilience4j 及其 Retry, RateLimiter, TimeLimiter, 和 Bulkhead 模块。在本文中,我们将探索 CircuitBreaker 模块。我们将了解何时以及如何使用它,并查看一些示例。

    代码示例

    本文附有 GitHub 上的工作代码示例。

    什么是 Resilience4j?

    请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理

    什么是断路器?

    断路器的思想是,如果我们知道调用可能会失败或超时,则阻止对远程服务的调用。我们这样做是为了不会在我们的服务和远程服务中不必要地浪费关键资源。这样的退出也给了远程服务一些时间来恢复。

    我们怎么知道一个调用可能会失败? 通过跟踪对远程服务发出的先前请求的结果。例如,如果前 10 次调用中有 8 次导致失败或超时,则下一次调用也可能会失败。

    断路器通过包装对远程服务的调用来跟踪响应。在正常运行期间,当远程服务成功响应时,我们说断路器处于“闭合”状态。当处于关闭状态时,断路器正常将请求传递给远程服务。

    当远程服务返回错误或超时时,断路器会增加一个内部计数器。如果错误计数超过配置的阈值,断路器将切换到“断开”状态。当处于断开状态时,断路器立即向调用者返回错误,甚至无需尝试远程调用。

    经过一段配置的时间后,断路器从断开状态切换到“半开”状态。在这种状态下,它允许一些请求传递到远程服务以检查它是否仍然不可用或缓慢。 如果错误率或慢呼叫率高于配置的阈值,则切换回断开状态。但是,如果错误率或慢呼叫率低于配置的阈值,则切换到关闭状态以恢复正常操作。

    断路器的类型

    断路器可以基于计数或基于时间。如果最后 N 次调用失败或缓慢,则基于计数的断路器将状态从关闭切换为断开。如果最后 N 秒的响应失败或缓慢,则基于时间的断路器将切换到断开状态。在这两个断路器中,我们还可以指定失败或慢速调用的阈值。

    例如,如果最近 25 次调用中有 70% 失败或需要 2 秒以上才能完成,我们可以配置一个基于计数的断路器来“断开电路”。同样,如果过去 30 秒内 80% 的调用失败或耗时超过 5 秒,我们可以告诉基于时间的断路器断开电路。

    Resilience4j 的 CircuitBreaker 概念

    resilience4j-circuitbreaker 的工作原理与其他 Resilience4j 模块类似。我们提供想要作为函数构造执行的代码——一个进行远程调用的 lambda 表达式或一个从远程服务中检索到的某个值的 Supplier,等等——并且断路器用代码修饰它 如果需要,跟踪响应并切换状态。

    Resilience4j 同时支持基于计数和基于时间的断路器。

    我们使用 slidingWindowType() 配置指定断路器的类型。此配置可以采用两个值之一 –
    SlidingWindowType.COUNT_BASED
    SlidingWindowType.TIME_BASED

    failureRateThreshold()slowCallRateThreshold() 以百分比形式配置失败率阈值和慢速调用率。

    slowCallDurationThreshold() 以秒为单位配置调用被认为慢的时间。

    我们可以指定一个 minimumNumberOfCalls(),在断路器可以计算错误率或慢速调用率之前需要它。

    如前所述,断路器在一定时间后从断开状态切换到半断开状态,以检查远程服务的情况。waitDurationInOpenState() 指定断路器在切换到半开状态之前应等待的时间。

    permittedNumberOfCallsInHalfOpenState() 配置在半开状态下允许的调用次数,
    maxWaitDurationInHalfOpenState() 确定断路器在切换回开状态之前可以保持在半开状态的时间。

    此配置的默认值 0 意味着断路器将无限等待,直到所有
    permittedNumberOfCallsInHalfOpenState() 完成。

    默认情况下,断路器将任何异常视为失败。但是我们可以对此进行调整,以使用 recordExceptions() 配置指定应视为失败的异常列表和使用 ignoreExceptions() 配置忽略的异常列表。

    如果我们在确定异常应该被视为失败还是忽略时想要更精细的控制,我们可以提供 Predicate<Throwable> 作为 recordException()ignoreException() 配置。

    当断路器拒绝处于断开状态的呼叫时,它会抛出 CallNotPermittedException。我们可以使用 writablestacktraceEnabled() 配置控制 CallNotPermittedException 的堆栈跟踪中的信息量。

    使用 Resilience4j CircuitBreaker模块

    让我们看看如何使用
    resilience4j-circuitbreaker 模块中可用的各种功能。

    我们将使用与本系列前几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

    使用 Resilience4j 断路器时,CircuitBreakerRegistryCircuitBreakerConfigCircuitBreaker 是我们使用的主要抽象。

    CircuitBreakerRegistry 是用于创建和管理 CircuitBreaker 对象的工厂。

    CircuitBreakerConfig 封装了上一节中的所有配置。每个 CircuitBreaker 对象都与一个 CircuitBreakerConfig 相关联。

    第一步是创建一个 CircuitBreakerConfig

    CircuitBreakerConfig config = CircuitBreakerConfig.ofDefaults();

    这将创建一个具有以下默认值的 CircuitBreakerConfig:

    配置 默认值
    slidingWindowType COUNT_BASED
    failureRateThreshold 50%
    slowCallRateThreshold 100%
    slowCallDurationThreshold 60s
    minimumNumberOfCalls 100
    permittedNumberOfCallsInHalfOpenState 10
    maxWaitDurationInHalfOpenState 0s

    基于计数的断路器

    假设我们希望断路器在最近 10 次调用中有 70% 失败时断开:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.COUNT_BASED)
      .slidingWindowSize(10)
      .failureRateThreshold(70.0f)
      .build();

    然后我们用这个配置创建一个 CircuitBreaker

    CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
    CircuitBreaker circuitBreaker = registry.circuitBreaker("flightSearchService");

    现在让我们表达我们的代码以作为 Supplier 运行航班搜索并使用 circuitbreaker 装饰它:

    Supplier<List<Flight>> flightsSupplier =
      () -> service.searchFlights(request);
    Supplier<List<Flight>> decoratedFlightsSupplier =
      circuitBreaker.decorateSupplier(flightsSupplier);

    最后,让我们调用几次修饰操作来了解断路器的工作原理。我们可以使用 CompletableFuture 来模拟来自用户的并发航班搜索请求:

    for (int i=0; i<20; i++) {
      try {
        System.out.println(decoratedFlightsSupplier.get());
      }
      catch (...) {
        // Exception handling
      }
    }

    输出显示前几次飞行搜索成功,然后是 7 次飞行搜索失败。此时,断路器断开并为后续调用抛出 CallNotPermittedException

    Searching for flights; current time = 12:01:12 884
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 12:01:12 954
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 12:01:12 957
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 12:01:12 958
    io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Error occurred during flight search
    ... stack trace omitted ...
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ... other lines omitted ...
    io.reflectoring.resilience4j.circuitbreaker.Examples.countBasedSlidingWindow_FailedCalls(Examples.java:56)
      at io.reflectoring.resilience4j.circuitbreaker.Examples.main(Examples.java:229)

    现在,假设我们希望断路器在最后 10 个调用中有 70% 需要 2 秒或更长时间才能完成:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.COUNT_BASED)
      .slidingWindowSize(10)
      .slowCallRateThreshold(70.0f)
      .slowCallDurationThreshold(Duration.ofSeconds(2))
      .build();

    示例输出中的时间戳显示请求始终需要 2 秒才能完成。在 7 次缓慢响应后,断路器断开并且不允许进一步调用:

    Searching for flights; current time = 12:26:27 901
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 12:26:29 953
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 12:26:31 957
    Flight search successful
    ... other lines omitted ...
    Searching for flights; current time = 12:26:43 966
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ... stack trace omitted ...
            at io.reflectoring.resilience4j.circuitbreaker.Examples.main(Examples.java:231)
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ... stack trace omitted ...
            at io.reflectoring.resilience4j.circuitbreaker.Examples.main(Examples.java:231)

    通常我们会配置一个具有故障率和慢速调用率阈值的断路器:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.COUNT_BASED)
      .slidingWindowSize(10)
      .failureRateThreshold(70.0f)
      .slowCallRateThreshold(70.0f)
      .slowCallDurationThreshold(Duration.ofSeconds(2))
      .build();

    基于时间的断路器

    假设我们希望断路器在过去 10 秒内 70% 的请求失败时断开:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.COUNT_BASED)
      .slidingWindowSize(10)
      .failureRateThreshold(70.0f)
      .slowCallRateThreshold(70.0f)
      .slowCallDurationThreshold(Duration.ofSeconds(2))
      .build();

    我们创建了 CircuitBreaker,将航班搜索调用表示为 Supplier<List<Flight>> 并使用 CircuitBreaker 对其进行装饰,就像我们在上一节中所做的那样。

    以下是多次调用修饰操作后的示例输出:

    Start time: 18:51:01 552
    Searching for flights; current time = 18:51:01 582
    Flight search successful
    [Flight{flightNumber='XY 765', ... }]
    ... other lines omitted ...
    Searching for flights; current time = 18:51:01 631
    io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Error occurred during flight search
    ... stack trace omitted ...
    Searching for flights; current time = 18:51:01 632
    io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Error occurred during flight search
    ... stack trace omitted ...
    Searching for flights; current time = 18:51:01 633
    ... other lines omitted ...
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ... other lines omitted ...

    前 3 个请求成功,接下来的 7 个请求失败。此时断路器断开,后续请求因抛出 CallNotPermittedException 而失败。

    现在,假设我们希望断路器在过去 10 秒内 70% 的调用需要 1 秒或更长时间才能完成:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.TIME_BASED)
      .minimumNumberOfCalls(10)
      .slidingWindowSize(10)
      .slowCallRateThreshold(70.0f)
      .slowCallDurationThreshold(Duration.ofSeconds(1))
      .build();

    示例输出中的时间戳显示请求始终需要 1 秒才能完成。在 10 个请求(minimumNumberOfCalls)之后,当断路器确定 70% 的先前请求花费了 1 秒或更长时间时,它断开电路:

    Start time: 19:06:37 957
    Searching for flights; current time = 19:06:37 979
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 19:06:39 066
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 19:06:40 070
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 19:06:41 070
    ... other lines omitted ...
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ... stack trace omitted ...

    通常我们会配置一个具有故障率和慢速调用率阈值的基于时间的断路器:

    指定断开状态下的等待时间

    假设我们希望断路器处于断开状态时等待 10 秒,然后转换到半断开状态并让一些请求传递到远程服务:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.TIME_BASED)
      .slidingWindowSize(10)
      .minimumNumberOfCalls(10)
      .failureRateThreshold(70.0f)
      .slowCallRateThreshold(70.0f)
      .slowCallDurationThreshold(Duration.ofSeconds(2))
      .build();

    示例输出中的时间戳显示断路器最初转换为断开状态,在接下来的 10 秒内阻止一些调用,然后更改为半断开状态。后来,在半开状态时一致的成功响应导致它再次切换到关闭状态:

    Searching for flights; current time = 20:55:58 735
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 20:55:59 812
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 20:56:00 816
    ... other lines omitted ...
    io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Flight search failed
        at
    ... stack trace omitted ...
    2020-12-13T20:56:03.850115+05:30: CircuitBreaker 'flightSearchService' changed state from CLOSED to OPEN
    2020-12-13T20:56:04.851700+05:30: CircuitBreaker 'flightSearchService' recorded a call which was not permitted.
    2020-12-13T20:56:05.852220+05:30: CircuitBreaker 'flightSearchService' recorded a call which was not permitted.
    2020-12-13T20:56:06.855338+05:30: CircuitBreaker 'flightSearchService' recorded a call which was not permitted.
    ... other similar lines omitted ...
    2020-12-13T20:56:12.862362+05:30: CircuitBreaker 'flightSearchService' recorded a call which was not permitted.
    2020-12-13T20:56:13.865436+05:30: CircuitBreaker 'flightSearchService' changed state from OPEN to HALF_OPEN
    Searching for flights; current time = 20:56:13 865
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    ... other similar lines omitted ...
    2020-12-13T20:56:16.877230+05:30: CircuitBreaker 'flightSearchService' changed state from HALF_OPEN to CLOSED
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 20:56:17 879
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    ... other similar lines omitted ...

    指定回退方法

    使用断路器时的常见模式是指定在电路断开时要调用的回退方法。回退方法可以为不允许的远程调用提供一些默认值或行为

    我们可以使用 Decorators 实用程序类进行设置。Decorators 是来自 resilience4j-all 模块的构建器,具有 withCircuitBreaker()withRetry()withRateLimiter() 等方法,可帮助将多个 Resilience4j 装饰器应用于 SupplierFunction 等。

    当断路器断开并抛出 CallNotPermittedException 时,我们将使用它的 withFallback() 方法从本地缓存返回航班搜索结果:

    Supplier<List<Flight>> flightsSupplier = () -> service.searchFlights(request);
    Supplier<List<Flight>> decorated = Decorators
      .ofSupplier(flightsSupplier)
      .withCircuitBreaker(circuitBreaker)
      .withFallback(Arrays.asList(CallNotPermittedException.class),
                    e -> this.getFlightSearchResultsFromCache(request))
      .decorate();

    以下示例输出显示了断路器断开后从缓存中返回的搜索结果:

    Searching for flights; current time = 22:08:29 735
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 22:08:29 854
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 22:08:29 855
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Searching for flights; current time = 22:08:29 855
    2020-12-13T22:08:29.856277+05:30: CircuitBreaker 'flightSearchService' recorded an error: 'io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Error occurred during flight search'. Elapsed time: 0 ms
    Searching for flights; current time = 22:08:29 912
    ... other lines omitted ...
    2020-12-13T22:08:29.926691+05:30: CircuitBreaker 'flightSearchService' changed state from CLOSED to OPEN
    Returning flight search results from cache
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    Returning flight search results from cache
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... }]
    ... other lines omitted ...

    减少 Stacktrace 中的信息

    每当断路器断开时,它就会抛出 CallNotPermittedException

    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
        at io.github.resilience4j.circuitbreaker.CallNotPermittedException.createCallNotPermittedException(CallNotPermittedException.java:48)
    ... other lines in stack trace omitted ...
    at io.reflectoring.resilience4j.circuitbreaker.Examples.timeBasedSlidingWindow_SlowCalls(Examples.java:169)
        at io.reflectoring.resilience4j.circuitbreaker.Examples.main(Examples.java:263)

    除了第一行,堆栈跟踪中的其他行没有增加太多价值。如果 CallNotPermittedException 发生多次,这些堆栈跟踪行将在我们的日志文件中重复。

    我们可以通过将 writablestacktraceEnabled() 配置设置为 false 来减少堆栈跟踪中生成的信息量:

    CircuitBreakerConfig config = CircuitBreakerConfig
      .custom()
      .slidingWindowType(SlidingWindowType.COUNT_BASED)
      .slidingWindowSize(10)
      .failureRateThreshold(70.0f)
      .writablestacktraceEnabled(false)
      .build();

    现在,当 CallNotPermittedException 发生时,堆栈跟踪中只存在一行:

    Searching for flights; current time = 20:29:24 476
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    Searching for flights; current time = 20:29:24 540
    Flight search successful
    [Flight{flightNumber='XY 765', flightDate='12/31/2020', from='NYC', to='LAX'}, ... ]
    ... other lines omitted ...
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'flightSearchService' is OPEN and does not permit further calls
    ...

    其他有用的方法

    Retry 模块类似,CircuitBreaker 也有像 ignoreExceptions()recordExceptions() 等方法,让我们可以指定 CircuitBreaker 在跟踪调用结果时应该忽略和考虑哪些异常。

    例如,我们可能不想忽略来自远程飞行服务的 SeatsUnavailableException – 在这种情况下,我们真的不想断开电路。

    与我们见过的其他 Resilience4j 模块类似,CircuitBreaker 还提供了额外的方法,如 decorateCheckedSupplier()decorateCompletionStage()decorateRunnable()decorateConsumer() 等,因此我们可以在 Supplier 之外的其他结构中提供我们的代码。

    断路器事件

    CircuitBreaker 有一个 EventPublisher 可以生成以下类型的事件:

    • CircuitBreakerOnSuccessEvent,
    • CircuitBreakerOnErrorEvent,
    • CircuitBreakerOnStateTransitionEvent,
    • CircuitBreakerOnResetEvent,
    • CircuitBreakerOnIgnoredErrorEvent,
    • CircuitBreakerOnCallNotPermittedEvent,
    • CircuitBreakerOnFailureRateExceededEvent 以及
    • CircuitBreakerOnSlowCallRateExceededEvent.

    我们可以监听这些事件并记录它们,例如:

    circuitBreaker.getEventPublisher()
      .onCallNotPermitted(e -> System.out.println(e.toString()));
    circuitBreaker.getEventPublisher()
      .onError(e -> System.out.println(e.toString()));
    circuitBreaker.getEventPublisher()
      .onFailureRateExceeded(e -> System.out.println(e.toString()));
    circuitBreaker.getEventPublisher().onStateTransition(e -> System.out.println(e.toString()));

    以下是示例的日志输出:

    2020-12-13T22:25:52.972943+05:30: CircuitBreaker 'flightSearchService' recorded an error: 'io.reflectoring.resilience4j.circuitbreaker.exceptions.FlightServiceException: Error occurred during flight search'. Elapsed time: 0 ms
    Searching for flights; current time = 22:25:52 973
    ... other lines omitted ...
    2020-12-13T22:25:52.974448+05:30: CircuitBreaker 'flightSearchService' exceeded failure rate threshold. Current failure rate: 70.0
    2020-12-13T22:25:52.984300+05:30: CircuitBreaker 'flightSearchService' changed state from CLOSED to OPEN
    2020-12-13T22:25:52.985057+05:30: CircuitBreaker 'flightSearchService' recorded a call which was not permitted.
    ... other lines omitted ...

    CircuitBreaker指标

    CircuitBreake 暴露了许多指标,这些是一些重要的条目:

    • 成功、失败或忽略的调用总数 (resilience4j.circuitbreaker.calls)
    • 断路器状态 (resilience4j.circuitbreaker.state)
    • 断路器故障率 (resilience4j.circuitbreaker.failure.rate)
    • 未被允许的调用总数 (resilience4.circuitbreaker.not.permitted.calls)
    • 断路器的缓慢调用 (resilience4j.circuitbreaker.slow.call.rate)

    首先,我们像往常一样创建 CircuitBreakerConfigCircuitBreakerRegistryCircuitBreaker。然后,我们创建一个 MeterRegistry 并将 CircuitBreakerRegistry 绑定到它:

    MeterRegistry meterRegistry = new SimpleMeterRegistry();
    TaggedCircuitBreakerMetrics.ofCircuitBreakerRegistry(registry)
      .bindTo(meterRegistry);

    运行几次断路器修饰操作后,我们显示捕获的指标。这是一些示例输出:

    The number of slow failed calls which were slower than a certain threshold - resilience4j.circuitbreaker.slow.calls: 0.0
    The states of the circuit breaker - resilience4j.circuitbreaker.state: 0.0, state: metrics_only
    Total number of not permitted calls - resilience4j.circuitbreakernot.permitted.calls: 0.0
    The slow call of the circuit breaker - resilience4j.circuitbreaker.slow.call.rate: -1.0
    The states of the circuit breaker - resilience4j.circuitbreaker.state: 0.0, state: half_open
    Total number of successful calls - resilience4j.circuitbreaker.calls: 0.0, kind: successful
    The failure rate of the circuit breaker - resilience4j.circuitbreaker.failure.rate: -1.0

    在实际应用中,我们会定期将数据导出到监控系统并在仪表板上进行分析。

    结论

    在本文中,我们学习了如何使用 Resilience4j 的 Circuitbreaker 模块在远程服务返回错误时暂停向其发出请求。我们了解了为什么这很重要,还看到了一些有关如何配置它的实际示例。

    您可以使用 GitHub 上的代码来演示一个完整的应用程序。


    本文译自:Implementing a Circuit Breaker with Resilience4j – Reflectoringhttps://reflectoring.io/circuitbreaker-with-resilience4j/)

    Java 项目中使用 Resilience4j 框架实现故障隔离

    Java 项目中使用 Resilience4j 框架实现故障隔离

    到目前为止,在本系列中,我们已经了解了 Resilience4j 及其 Retry, RateLimiterTimeLimiter 模块。在本文中,我们将探讨 Bulkhead 模块。我们将了解它解决了什么问题,何时以及如何使用它,并查看一些示例。

    代码示例

    本文附有 GitHub 上的工作代码示例。

    什么是 Resilience4j?

    请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理

    什么是故障隔离?

    几年前,我们遇到了一个生产问题,其中一台服务器停止响应健康检查,负载均衡器将服务器从池中取出。

    就在我们开始调查这个问题的时候,还有第二个警报——另一台服务器已经停止响应健康检查,也被从池中取出。

    几分钟后,每台服务器都停止响应健康探测,我们的服务完全关闭。

    我们使用 Redis 为应用程序支持的几个功能缓存一些数据。正如我们后来发现的那样,Redis 集群同时出现了一些问题,它已停止接受新连接。我们使用 Jedis 库连接到 Redis,该库的默认行为是无限期地阻塞调用线程,直到建立连接。

    我们的服务托管在 Tomcat 上,它的默认请求处理线程池大小为 200 个线程。因此,通过连接到 Redis 的代码路径的每个请求最终都会无限期地阻塞线程。

    几分钟之内,集群中的所有 2000 个线程都无限期地阻塞了——甚至没有空闲线程来响应负载均衡器的健康检查。

    该服务本身支持多项功能,并非所有功能都需要访问 Redis 缓存。但是当这一方面出现问题时,它最终影响了整个服务。

    这正是故障隔离要解决的问题——它可以防止某个服务区域的问题影响整个服务。

    虽然我们的服务发生的事情是一个极端的例子,但我们可以看到缓慢的上游依赖如何影响调用服务的不相关区域。

    如果我们在每个服务器实例上对 Redis 设置了 20 个并发请求的限制,那么当 Redis 连接问题发生时,只有这些线程会受到影响。剩余的请求处理线程可以继续为其他请求提供服务。

    故障隔离背后的想法是对我们对远程服务进行的并发调用数量设置限制。我们将对不同远程服务的调用视为不同的、隔离的池,并对可以同时进行的调用数量设置限制。

    术语舱壁本身来自它在船舶中的使用,其中船舶的底部被分成彼此分开的部分。如果有裂缝,并且水开始流入,则只有该部分会充满水。这可以防止整艘船沉没。

    Resilience4j 隔板概念

    resilience4j-bulkhead 的工作原理类似于其他 Resilience4j 模块。我们为它提供了我们想要作为函数构造执行的代码——一个进行远程调用的 lambda 表达式或一个从远程服务中检索到的某个值的 Supplier,等等——并且隔板用代码装饰它以控制并发调用数。

    Resilience4j 提供两种类型的隔板 – SemaphoreBulkhead ThreadPoolBulkhead

    SemaphoreBulkhead 内部使用
    java.util.concurrent.Semaphore 来控制并发调用的数量并在当前线程上执行我们的代码。

    ThreadPoolBulkhead 使用线程池中的一个线程来执行我们的代码。它内部使用
    java.util.concurrent.ArrayBlockingQueue
    java.util.concurrent.ThreadPoolExecutor 来控制并发调用的数量。

    SemaphoreBulkhead

    让我们看看与信号量隔板相关的配置及其含义。

    maxConcurrentCalls 确定我们可以对远程服务进行的最大并发调用数。我们可以将此值视为初始化信号量的许可数。

    任何尝试超过此限制调用远程服务的线程都可以立即获得 BulkheadFullException 或等待一段时间以等待另一个线程释放许可。这由 maxWaitDuration 值决定。

    当有多个线程在等待许可时,fairCallHandlingEnabled 配置确定等待的线程是否以先进先出的顺序获取许可。

    最后, writableStackTraceEnabled 配置让我们可以在 BulkheadFullException 发生时减少堆栈跟踪中的信息量。这很有用,因为如果没有它,当异常多次发生时,我们的日志可能会充满许多类似的信息。通常在读取日志时,只知道发生了 BulkheadFullException 就足够了。

    ThreadPoolBulkhead

    coreThreadPoolSizemaxThreadPoolSizekeepAliveDurationqueueCapacity 是与 ThreadPoolBulkhead 相关的主要配置。ThreadPoolBulkhead 内部使用这些配置来构造一个 ThreadPoolExecutor

    internalThreadPoolExecutor 使用可用的空闲线程之一执行传入的任务。 如果没有线程可以自由执行传入的任务,则该任务将排队等待线程可用时稍后执行。如果已达到 queueCapacity,则远程调用将被拒绝并返回 BulkheadFullException

    ThreadPoolBulkhead 也有 writableStackTraceEnabled 配置来控制 BulkheadFullException 的堆栈跟踪中的信息量。

    使用 Resilience4j 隔板模块

    让我们看看如何使用 resilience4j-bulkhead 模块中可用的各种功能。

    我们将使用与本系列前几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

    SemaphoreBulkhead

    使用基于信号量的隔板时,BulkheadRegistryBulkheadConfigBulkhead 是我们使用的主要抽象。

    BulkheadRegistry 是一个用于创建和管理 Bulkhead 对象的工厂。

    BulkheadConfig 封装了 maxConcurrentCallsmaxWaitDurationwritableStackTraceEnabledfairCallHandlingEnabled 配置。每个 Bulkhead 对象都与一个 BulkheadConfig 相关联。

    第一步是创建一个 BulkheadConfig

    BulkheadConfig config = BulkheadConfig.ofDefaults();

    这将创建一个 BulkheadConfig,其默认值为 maxConcurrentCalls(25)、maxWaitDuration(0s)、writableStackTraceEnabled(true) 和 fairCallHandlingEnabled(true)。

    假设我们希望将并发调用的数量限制为 2,并且我们愿意等待 2 秒让线程获得许可:

    BulkheadConfig config = BulkheadConfig.custom()
      .maxConcurrentCalls(2)
      .maxWaitDuration(Duration.ofSeconds(2))
      .build();

    然后我们创建一个 Bulkhead

    BulkheadRegistry registry = BulkheadRegistry.of(config);
    
    Bulkhead bulkhead = registry.bulkhead("flightSearchService");

    现在让我们表达我们的代码以作为 Supplier 运行航班搜索并使用 bulkhead 装饰它:

    BulkheadRegistry registry = BulkheadRegistry.of(config);
    Bulkhead bulkhead = registry.bulkhead("flightSearchService");

    最后,让我们调用几次装饰操作来了解隔板的工作原理。我们可以使用 CompletableFuture 来模拟来自用户的并发航班搜索请求:

    for (int i=0; i<4; i++) {
      CompletableFuture
        .supplyAsync(decoratedFlightsSupplier)
        .thenAccept(flights -> System.out.println("Received results"));
    }

    输出中的时间戳和线程名称显示,在 4 个并发请求中,前两个请求立即通过:

    Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-3
    Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-5
    Flight search successful at 11:42:13 226
    Flight search successful at 11:42:13 226
    Received results
    Received results
    Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-9
    Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-7
    Flight search successful at 11:42:14 239
    Flight search successful at 11:42:14 239
    Received results
    Received results

    第三个和第四个请求仅在 1 秒后就能够获得许可,在之前的请求完成之后。

    如果线程无法在我们指定的 2s maxWaitDuration 内获得许可,则会抛出 BulkheadFullException

    Caused by: io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
        at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:49)
        at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:164)
        at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateSupplier$5(Bulkhead.java:194)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
        ... 6 more

    除了第一行,堆栈跟踪中的其他行没有增加太多价值。如果 BulkheadFullException 发生多次,这些堆栈跟踪行将在我们的日志文件中重复。

    我们可以通过将 writableStackTraceEnabled 配置设置为 false 来减少堆栈跟踪中生成的信息量:

    BulkheadConfig config = BulkheadConfig.custom()
        .maxConcurrentCalls(2)
        .maxWaitDuration(Duration.ofSeconds(1))
        .writableStackTraceEnabled(false)
    .build();

    现在,当 BulkheadFullException 发生时,堆栈跟踪中只存在一行:

    Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3
    Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
    io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
    Flight search successful at 12:27:58 699
    Flight search successful at 12:27:58 699
    Received results
    Received results

    与我们见过的其他 Resilience4j 模块类似,Bulkhead 还提供了额外的方法,如 decorateCheckedSupplier()decorateCompletionStage()decorateRunnable()decorateConsumer() 等,因此我们可以在 Supplier 供应商之外的其他结构中提供我们的代码。

    ThreadPoolBulkhead

    当使用基于线程池的隔板时,
    ThreadPoolBulkheadRegistryThreadPoolBulkheadConfigThreadPoolBulkhead 是我们使用的主要抽象。

    ThreadPoolBulkheadRegistry 是用于创建和管理 ThreadPoolBulkhead 对象的工厂。

    ThreadPoolBulkheadConfig 封装了 coreThreadPoolSizemaxThreadPoolSizekeepAliveDurationqueueCapacity 配置。每个 ThreadPoolBulkhead 对象都与一个 ThreadPoolBulkheadConfig 相关联。

    第一步是创建一个 ThreadPoolBulkheadConfig

    ThreadPoolBulkheadConfig config =
      ThreadPoolBulkheadConfig.ofDefaults();

    这将创建一个 ThreadPoolBulkheadConfig,其默认值为 coreThreadPoolSize(可用处理器数量 -1)、maxThreadPoolSize(可用处理器最大数量)、keepAliveDuration(20ms)和 queueCapacity(100)。

    假设我们要将并发调用的数量限制为 2:

    ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(2)
      .coreThreadPoolSize(1)
      .queueCapacity(1)
      .build();

    然后我们创建一个 ThreadPoolBulkhead

    ThreadPoolBulkheadRegistry registry = ThreadPoolBulkheadRegistry.of(config);
    ThreadPoolBulkhead bulkhead = registry.bulkhead("flightSearchService");

    现在让我们表达我们的代码以作为 Supplier 运行航班搜索并使用 bulkhead 装饰它:

    Supplier<List<Flight>> flightsSupplier =
      () -> service.searchFlightsTakingOneSecond(request);
    Supplier<CompletionStage<List<Flight>>> decoratedFlightsSupplier =
      ThreadPoolBulkhead.decorateSupplier(bulkhead, flightsSupplier);

    与返回一个 Supplier<List<Flight>>
    SemaphoreBulkhead.decorateSupplier() 不同,
    ThreadPoolBulkhead.decorateSupplier() 返回一个 Supplier<CompletionStage<List<Flight>>。这是因为 ThreadPoolBulkHead 不会在当前线程上同步执行代码。

    最后,让我们调用几次装饰操作来了解隔板的工作原理:

    for (int i=0; i<3; i++) {
      decoratedFlightsSupplier
        .get()
        .whenComplete((r,t) -> {
          if (r != null) {
            System.out.println("Received results");
          }
          if (t != null) {
            t.printStackTrace();
          }
        });
    }

    输出中的时间戳和线程名称显示,虽然前两个请求立即执行,但第三个请求已排队,稍后由释放的线程之一执行:

    Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-1
    Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-2
    Flight search successful at 16:15:00 136
    Flight search successful at 16:15:00 135
    Received results
    Received results
    Searching for flights; current time = 16:15:01 151; current thread = bulkhead-flightSearchService-2
    Flight search successful at 16:15:01 151
    Received results

    如果队列中没有空闲线程和容量,则抛出 BulkheadFullException

    Exception in thread "main" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
     at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:64)
     at io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead.submit(FixedThreadPoolBulkhead.java:157)
    ... other lines omitted ...

    我们可以使用 writableStackTraceEnabled 配置来减少堆栈跟踪中生成的信息量:

    ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(2)
      .coreThreadPoolSize(1)
      .queueCapacity(1)
      .writableStackTraceEnabled(false)
      .build();

    现在,当 BulkheadFullException 发生时,堆栈跟踪中只存在一行:

    Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3
    Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
    io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
    Flight search successful at 12:27:58 699
    Flight search successful at 12:27:58 699
    Received results
    Received results

    上下文传播

    有时我们将数据存储在 ThreadLocal 变量中并在代码的不同区域中读取它。我们这样做是为了避免在方法链之间显式地将数据作为参数传递,尤其是当该值与我们正在实现的核心业务逻辑没有直接关系时。

    例如,我们可能希望将当前用户 ID 或事务 ID 或某个请求跟踪 ID 记录到每个日志语句中,以便更轻松地搜索日志。对于此类场景,使用 ThreadLocal 是一种有用的技术。

    使用 ThreadPoolBulkhead 时,由于我们的代码不在当前线程上执行,因此我们存储在 ThreadLocal 变量中的数据在其他线程中将不可用。

    让我们看一个例子来理解这个问题。首先我们定义一个 RequestTrackingIdHolder 类,一个围绕 ThreadLocal 的包装类:

    class RequestTrackingIdHolder {
      static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
      static String getRequestTrackingId() {
        return threadLocal.get();
      }
    
      static void setRequestTrackingId(String id) {
        if (threadLocal.get() != null) {
          threadLocal.set(null);
          threadLocal.remove();
        }
        threadLocal.set(id);
      }
    
      static void clear() {
        threadLocal.set(null);
        threadLocal.remove();
      }
    }

    静态方法可以轻松设置和获取存储在 ThreadLocal 上的值。我们接下来在调用隔板装饰的航班搜索操作之前设置一个请求跟踪 ID:

    for (int i=0; i<2; i++) {
      String trackingId = UUID.randomUUID().toString();
      System.out.println("Setting trackingId " + trackingId + " on parent, main thread before calling flight search");
      RequestTrackingIdHolder.setRequestTrackingId(trackingId);
      decoratedFlightsSupplier
        .get()
        .whenComplete((r,t) -> {
            // other lines omitted
        });
    }

    示例输出显示此值在隔板管理的线程中不可用:

    Setting trackingId 98ff99df-466a-47f7-88f7-5e31fc8fcb6b on parent, main thread before calling flight search
    Setting trackingId 6b98d73c-a590-4a20-b19d-c85fea783caf on parent, main thread before calling flight search
    Searching for flights; current time = 19:53:53 799; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
    Flight search successful at 19:53:53 824
    Received results
    Searching for flights; current time = 19:53:54 836; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
    Flight search successful at 19:53:54 836
    Received results

    为了解决这个问题,ThreadPoolBulkhead 提供了一个 ContextPropagatorContextPropagator 是一种用于跨线程边界检索、复制和清理值的抽象。它定义了一个接口,其中包含从当前线程 (retrieve()) 获取值、将其复制到新的执行线程 (copy()) 并最终在执行线程 (clear()) 上进行清理的方法。

    让我们实现一个
    RequestTrackingIdPropagator

    class RequestTrackingIdPropagator implements ContextPropagator {
      @Override
      public Supplier<Optional> retrieve() {
        System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName());
        return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId());
      }
    
      @Override
      Consumer<Optional> copy() {
        return optional -> {
          System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName());
          optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString()));
        };
      }
    
      @Override
      Consumer<Optional> clear() {
        return optional -> {
          System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName());
          optional.ifPresent(s -> RequestTrackingIdHolder.clear());
        };
      }
    }

    我们通过在 ThreadPoolBulkheadConfig 上的设置来为 ThreadPoolBulkhead 提供 ContextPropagator

    class RequestTrackingIdPropagator implements ContextPropagator {
      @Override
      public Supplier<Optional> retrieve() {
        System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName());
        return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId());
      }
    
      @Override
      Consumer<Optional> copy() {
        return optional -> {
          System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName());
          optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString()));
        };
      }
    
      @Override
      Consumer<Optional> clear() {
        return optional -> {
          System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName());
          optional.ifPresent(s -> RequestTrackingIdHolder.clear());
        };
      }
    }

    现在,示例输出显示请求跟踪 ID 在隔板管理的线程中可用:

    Setting trackingId 71d44cb8-dab6-4222-8945-e7fd023528ba on parent, main thread before calling flight search
    Getting request tracking id from thread: main
    Setting trackingId 5f9dd084-f2cb-4a20-804b-038828abc161 on parent, main thread before calling flight search
    Getting request tracking id from thread: main
    Setting request tracking id 71d44cb8-dab6-4222-8945-e7fd023528ba on thread: bulkhead-flightSearchService-1
    Searching for flights; current time = 20:07:56 508; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 71d44cb8-dab6-4222-8945-e7fd023528ba
    Flight search successful at 20:07:56 538
    Clearing request tracking id on thread: bulkhead-flightSearchService-1
    Received results
    Setting request tracking id 5f9dd084-f2cb-4a20-804b-038828abc161 on thread: bulkhead-flightSearchService-1
    Searching for flights; current time = 20:07:57 542; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 5f9dd084-f2cb-4a20-804b-038828abc161
    Flight search successful at 20:07:57 542
    Clearing request tracking id on thread: bulkhead-flightSearchService-1
    Received results

    Bulkhead事件

    Bulkhead 和 ThreadPoolBulkhead 都有一个 EventPublisher 来生成以下类型的事件:

    • BulkheadOnCallPermittedEvent
    • BulkheadOnCallRejectedEvent 和
    • BulkheadOnCallFinishedEvent

    我们可以监听这些事件并记录它们,例如:

    Bulkhead bulkhead = registry.bulkhead("flightSearchService");
    bulkhead.getEventPublisher().onCallPermitted(e -> System.out.println(e.toString()));
    bulkhead.getEventPublisher().onCallFinished(e -> System.out.println(e.toString()));
    bulkhead.getEventPublisher().onCallRejected(e -> System.out.println(e.toString()));

    示例输出显示了记录的内容:

    2020-08-26T12:27:39.790435: Bulkhead 'flightSearch' permitted a call.
    ... other lines omitted ...
    2020-08-26T12:27:40.290987: Bulkhead 'flightSearch' rejected a call.
    ... other lines omitted ...
    2020-08-26T12:27:41.094866: Bulkhead 'flightSearch' has finished a call.

    Bulkhead 指标

    SemaphoreBulkhead

    Bulkhead 暴露了两个指标:

    • 可用权限的最大数量(resilience4j.bulkhead.max.allowed.concurrent.calls),和
    • 允许的并发调用数(resilience4j.bulkhead.available.concurrent.calls)。

    bulkhead.available 指标与我们在 BulkheadConfig 上配置的 maxConcurrentCalls 相同。

    首先,我们像前面一样创建 BulkheadConfigBulkheadRegistryBulkhead。然后,我们创建一个 MeterRegistry 并将 BulkheadRegistry 绑定到它:

    MeterRegistry meterRegistry = new SimpleMeterRegistry();
    TaggedBulkheadMetrics.ofBulkheadRegistry(registry)
      .bindTo(meterRegistry);

    运行几次隔板装饰操作后,我们显示捕获的指标:

    Consumer<Meter> meterConsumer = meter -> {
      String desc = meter.getId().getDescription();
      String metricName = meter.getId().getName();
      Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
        .filter(m -> m.getStatistic().name().equals("VALUE"))
        .findFirst()
        .map(m -> m.getValue())
        .orElse(0.0);
      System.out.println(desc + " - " + metricName + ": " + metricValue);};meterRegistry.forEachMeter(meterConsumer);

    这是一些示例输出:

    The maximum number of available permissions - resilience4j.bulkhead.max.allowed.concurrent.calls: 8.0
    The number of available permissions - resilience4j.bulkhead.available.concurrent.calls: 3.0

    ThreadPoolBulkhead

    ThreadPoolBulkhead 暴露五个指标:

    • 队列的当前长度(resilience4j.bulkhead.queue.depth),
    • 当前线程池的大小(resilience4j.bulkhead.thread.pool.size),
    • 线程池的核心和最大容量(resilience4j.bulkhead.core.thread.pool.sizeresilience4j.bulkhead.max.thread.pool.size),以及
    • 队列的容量(resilience4j.bulkhead.queue.capacity)。

    首先,我们像前面一样创建 ThreadPoolBulkheadConfig
    ThreadPoolBulkheadRegistryThreadPoolBulkhead。然后,我们创建一个 MeterRegistry 并将
    ThreadPoolBulkheadRegistry 绑定到它:

    MeterRegistry meterRegistry = new SimpleMeterRegistry();
    TaggedThreadPoolBulkheadMetrics.ofThreadPoolBulkheadRegistry(registry).bindTo(meterRegistry);

    运行几次隔板装饰操作后,我们将显示捕获的指标:

    The queue capacity - resilience4j.bulkhead.queue.capacity: 5.0
    The queue depth - resilience4j.bulkhead.queue.depth: 1.0
    The thread pool size - resilience4j.bulkhead.thread.pool.size: 5.0
    The maximum thread pool size - resilience4j.bulkhead.max.thread.pool.size: 5.0
    The core thread pool size - resilience4j.bulkhead.core.thread.pool.size: 3.0

    在实际应用中,我们会定期将数据导出到监控系统并在仪表板上进行分析。

    实施隔板时的陷阱和良好实践

    使隔板成为单例

    对给定远程服务的所有调用都应通过同一个 Bulkhead 实例。对于给定的远程服务,Bulkhead 必须是单例。

    如果我们不强制执行此操作,我们代码库的某些区域可能会绕过 Bulkhead 直接调用远程服务。为了防止这种情况,远程服务的实际调用应该在一个核心、内部层和其他区域应该使用内部层暴露的隔板装饰器。

    我们如何确保未来的新开发人员理解这一意图? 查看 Tom 的文章,该文章展示了解决此类问题的一种方法,即通过组织包结构来明确此类意图。此外,它还展示了如何通过在 ArchUnit 测试中编码意图来强制执行此操作。

    与其他 Resilience4j 模块结合

    将隔板与一个或多个其他 Resilience4j 模块(如重试和速率限制器)结合使用会更有效。例如,如果有 BulkheadFullException,我们可能希望在一些延迟后重试。

    结论

    在本文中,我们学习了如何使用 Resilience4j 的 Bulkhead 模块对我们对远程服务进行的并发调用设置限制。我们了解了为什么这很重要,还看到了一些有关如何配置它的实际示例。

    您可以使用 GitHub 上的代码演示一个完整的应用程序。


    本文译自: Implementing Bulkhead with Resilience4j – Reflectoring

    Java 项目中使用 Resilience4j 框架实现异步超时处理

    到目前为止,在本系列中,我们已经了解了 Resilience4j 及其 RetryRateLimiter 模块。在本文中,我们将通过 TimeLimiter 继续探索 Resilience4j。我们将了解它解决了什么问题,何时以及如何使用它,并查看一些示例。

    代码示例

    本文附有 GitHub 上的工作代码示例。

    什么是 Resilience4j?

    请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理

    什么是限时?

    对我们愿意等待操作完成的时间设置限制称为时间限制。如果操作没有在我们指定的时间内完成,我们希望通过超时错误收到通知。

    有时,这也称为“设定最后期限”。

    我们这样做的一个主要原因是确保我们不会让用户或客户无限期地等待。不提供任何反馈的缓慢服务可能会让用户感到沮丧。

    我们对操作设置时间限制的另一个原因是确保我们不会无限期地占用服务器资源。我们在使用 Spring 的 @Transactional 注解时指定的 timeout 值就是一个例子——在这种情况下,我们不想长时间占用数据库资源。

    什么时候使用 Resilience4j TimeLimiter?

    Resilience4j 的 TimeLimiter 可用于设置使用 CompleteableFutures 实现的异步操作的时间限制(超时)。

    Java 8 中引入的 CompletableFuture 类使异步、非阻塞编程变得更容易。可以在不同的线程上执行慢速方法,释放当前线程来处理其他任务。 我们可以提供一个当 slowMethod() 返回时执行的回调:

    int slowMethod() {
      // time-consuming computation or remote operation
    return 42;
    }
    
    CompletableFuture.supplyAsync(this::slowMethod)
    .thenAccept(System.out::println);

    这里的 slowMethod() 可以是一些计算或远程操作。通常,我们希望在进行这样的异步调用时设置时间限制。我们不想无限期地等待 slowMethod() 返回。例如,如果 slowMethod() 花费的时间超过一秒,我们可能想要返回先前计算的、缓存的值,甚至可能会出错。

    在 Java 8 的 CompletableFuture 中,没有简单的方法来设置异步操作的时间限制。CompletableFuture 实现了 Future 接口,Future 有一个重载的 get() 方法来指定我们可以等待多长时间:

    CompletableFuture<Integer> completableFuture = CompletableFuture
      .supplyAsync(this::slowMethod);
    Integer result = completableFuture.get(3000, TimeUnit.MILLISECONDS);
    System.out.println(result);

    但是这里有一个问题—— get() 方法是一个阻塞调用。所以它首先违背了使用 CompletableFuture 的目的,即释放当前线程。

    这是 Resilience4j 的 TimeLimiter 解决的问题——它让我们在异步操作上设置时间限制,同时保留在 Java 8 中使用 CompletableFuture 时非阻塞的好处。

    CompletableFuture 的这种限制已在 Java 9 中得到解决。我们可以在 Java 9 及更高版本中使用 CompletableFuture 上的 orTimeout()completeOnTimeout() 等方法直接设置时间限制。然而,凭借 Resilience4J指标事件,与普通的 Java 9 解决方案相比,它仍然提供了附加值。

    Resilience4j TimeLimiter 概念

    TimeLimiter支持 FutureCompletableFuture。但是将它与 Future 一起使用相当于 Future.get(long timeout, TimeUnit unit)。因此,我们将在本文的其余部分关注 CompletableFuture

    与其他 Resilience4j 模块一样,TimeLimiter 的工作方式是使用所需的功能装饰我们的代码 – 如果在这种情况下操作未在指定的 timeoutDuration 内完成,则返回 TimeoutException

    我们为 TimeLimiter 提供 timeoutDurationScheduledExecutorService 和异步操作本身,表示为 CompletionStageSupplier。它返回一个 CompletionStage 的装饰 Supplier

    在内部,它使用调度器来调度一个超时任务——通过抛出一个 TimeoutException 来完成 CompletableFuture 的任务。如果操作先完成,TimeLimiter 取消内部超时任务。

    除了 timeoutDuration 之外,还有另一个与 TimeLimiter 关联的配置 cancelRunningFuture。此配置仅适用于 Future 而不适用于 CompletableFuture。当超时发生时,它会在抛出 TimeoutException 之前取消正在运行的 Future

    使用 Resilience4j TimeLimiter 模块

    TimeLimiterRegistryTimeLimiterConfigTimeLimiterresilience4j-timelimiter 的主要抽象。

    TimeLimiterRegistry 是用于创建和管理 TimeLimiter 对象的工厂。

    TimeLimiterConfig 封装了 timeoutDurationcancelRunningFuture 配置。每个 TimeLimiter 对象都与一个 TimeLimiterConfig 相关联。

    TimeLimiter 提供辅助方法来为 FutureCompletableFuture Suppliers 创建或执行装饰器。

    让我们看看如何使用 TimeLimiter 模块中可用的各种功能。我们将使用与本系列前几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

    第一步是创建一个 TimeLimiterConfig

    TimeLimiterConfig config = TimeLimiterConfig.ofDefaults();

    这将创建一个 TimeLimiterConfig,其默认值为 timeoutDuration (1000ms) 和 cancelRunningFuture (true)。

    假设我们想将超时值设置为 2s 而不是默认值:

    TimeLimiterConfig config = TimeLimiterConfig.custom()
      .timeoutDuration(Duration.ofSeconds(2))
      .build();

    然后我们创建一个 TimeLimiter

    TimeLimiterRegistry registry = TimeLimiterRegistry.of(config);
    
    TimeLimiter limiter = registry.timeLimiter("flightSearch");

    我们想要异步调用
    FlightSearchService.searchFlights(),它返回一个 List<Flight>。让我们将其表示为 Supplier<CompletionStage<List<Flight>>>

    Supplier<List<Flight>> flightSupplier = () -> service.searchFlights(request);
    Supplier<CompletionStage<List<Flight>>> origCompletionStageSupplier =
    () -> CompletableFuture.supplyAsync(flightSupplier);

    然后我们可以使用 TimeLimiter 装饰 Supplier

    ScheduledExecutorService scheduler =
      Executors.newSingleThreadScheduledExecutor();
    Supplier<CompletionStage<List<Flight>>> decoratedCompletionStageSupplier =  
      limiter.decorateCompletionStage(scheduler, origCompletionStageSupplier);

    最后,让我们调用装饰的异步操作:

    decoratedCompletionStageSupplier.get().whenComplete((result, ex) -> {
      if (ex != null) {
        System.out.println(ex.getMessage());
      }
      if (result != null) {
        System.out.println(result);
      }
    });

    以下是成功飞行搜索的示例输出,其耗时少于我们指定的 2 秒 timeoutDuration

    Searching for flights; current time = 19:25:09 783; current thread = ForkJoinPool.commonPool-worker-3
    
    Flight search successful
    
    [Flight{flightNumber='XY 765', flightDate='08/30/2020', from='NYC', to='LAX'}, Flight{flightNumber='XY 746', flightDate='08/30/2020', from='NYC', to='LAX'}] on thread ForkJoinPool.commonPool-worker-3

    这是超时的航班搜索的示例输出:

    Exception java.util.concurrent.TimeoutException: TimeLimiter 'flightSearch' recorded a timeout exception on thread pool-1-thread-1 at 19:38:16 963
    
    Searching for flights; current time = 19:38:18 448; current thread = ForkJoinPool.commonPool-worker-3
    
    Flight search successful at 19:38:18 461

    上面的时间戳和线程名称表明,即使异步操作稍后在另一个线程上完成,调用线程也会收到 TimeoutException。

    如果我们想创建一个装饰器并在代码库的不同位置重用它,我们将使用decorateCompletionStage()。如果我们想创建它并立即执行 Supplier<CompletionStage>,我们可以使用 executeCompletionStage() 实例方法代替:

    CompletionStage<List<Flight>> decoratedCompletionStage =  
      limiter.executeCompletionStage(scheduler, origCompletionStageSupplier);

    TimeLimiter 事件

    TimeLimiter 有一个 EventPublisher,它生成 TimeLimiterOnSuccessEventTimeLimiterOnErrorEventTimeLimiterOnTimeoutEvent 类型的事件。我们可以监听这些事件并记录它们,例如:

    TimeLimiter limiter = registry.timeLimiter("flightSearch");
    
    limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));
    
    limiter.getEventPublisher().onError(e -> System.out.println(e.toString()));
    
    limiter.getEventPublisher().onTimeout(e -> System.out.println(e.toString()));

    示例输出显示了记录的内容:

    2020-08-07T11:31:48.181944: TimeLimiter 'flightSearch' recorded a successful call.
    
    ... other lines omitted ...
    
    2020-08-07T11:31:48.582263: TimeLimiter 'flightSearch' recorded a timeout exception.

    TimeLimiter 指标

    TimeLimiter 跟踪成功、失败和超时的调用次数。

    首先,我们像往常一样创建 TimeLimiterConfigTimeLimiterRegistryTimeLimiter。然后,我们创建一个 MeterRegistry 并将 TimeLimiterRegistry 绑定到它:

    MeterRegistry meterRegistry = new SimpleMeterRegistry();
    TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(registry)
      .bindTo(meterRegistry);

    运行几次限时操作后,我们显示捕获的指标:

    Consumer<Meter> meterConsumer = meter -> {
      String desc = meter.getId().getDescription();
      String metricName = meter.getId().getName();
      String metricKind = meter.getId().getTag("kind");
      Double metricValue =
        StreamSupport.stream(meter.measure().spliterator(), false)
        .filter(m -> m.getStatistic().name().equals("COUNT"))
        .findFirst()
        .map(Measurement::getValue)
        .orElse(0.0);
      System.out.println(desc + " - " +
                         metricName +
                         "(" + metricKind + ")" +
                         ": " + metricValue);
    };
    meterRegistry.forEachMeter(meterConsumer);

    这是一些示例输出:

    The number of timed out calls - resilience4j.timelimiter.calls(timeout): 6.0
    
    The number of successful calls - resilience4j.timelimiter.calls(successful): 4.0
    
    The number of failed calls - resilience4j.timelimiter.calls(failed): 0.0

    在实际应用中,我们会定期将数据导出到监控系统并在仪表板上进行分析。

    实施时间限制时的陷阱和良好实践

    通常,我们处理两种操作 – 查询(或读取)和命令(或写入)。对查询进行时间限制是安全的,因为我们知道它们不会改变系统的状态。我们看到的 searchFlights() 操作是查询操作的一个例子。

    命令通常会改变系统的状态。bookFlights() 操作将是命令的一个示例。在对命令进行时间限制时,我们必须记住,当我们超时时,该命令很可能仍在运行。例如,bookFlights() 调用上的 TimeoutException 并不一定意味着命令失败。

    在这种情况下,我们需要管理用户体验——也许在超时时,我们可以通知用户操作花费的时间比我们预期的要长。然后我们可以查询上游以检查操作的状态并稍后通知用户。

    结论

    在本文中,我们学习了如何使用 Resilience4j 的 TimeLimiter 模块为异步、非阻塞操作设置时间限制。我们通过一些实际示例了解了何时使用它以及如何配置它。

    您可以使用 GitHub 上的代码演示一个完整的应用程序来说明这些想法。


    本文译自:
    https://reflectoring.io/time-limiting-with-resilience4j/

    Java 项目中使用 Resilience4j 框架实现客户端 API 调用的限速/节流机制


    在本系列的上一篇文章中,我们了解了 Resilience4j 以及如何使用其 Retry 模块。现在让我们了解 RateLimiter – 它是什么,何时以及如何使用它,以及在实施速率限制(或者也称为“节流”)时要注意什么。

    代码示例

    本文附有GitHub 上的工作代码示例。

    什么是 Resilience4j?

    请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理

    什么是限速?

    我们可以从两个角度来看待速率限制——作为服务提供者和作为服务消费者。

    服务端限速

    作为服务提供商,我们实施速率限制以保护我们的资源免受过载和拒绝服务 (DoS) 攻击

    为了满足我们与所有消费者的服务水平协议 (SLA),我们希望确保一个导致流量激增的消费者不会影响我们对他人的服务质量。

    我们通过设置在给定时间单位内允许消费者发出多少请求的限制来做到这一点。我们通过适当的响应拒绝任何超出限制的请求,例如 HTTP 状态 429(请求过多)。这称为服务器端速率限制。

    速率限制以每秒请求数 (rps)、每分钟请求数 (rpm) 或类似形式指定。某些服务在不同的持续时间(例如 50 rpm 且不超过 2500 rph)和一天中的不同时间(例如,白天 100 rps 和晚上 150 rps)有多个速率限制。该限制可能适用于单个用户(由用户 ID、IP 地址、API 访问密钥等标识)或多租户应用程序中的租户。

    客户端限速

    作为服务的消费者,我们希望确保我们不会使服务提供者过载。此外,我们不想招致意外的成本——无论是金钱上的还是服务质量方面的。

    如果我们消费的服务是有弹性的,就会发生这种情况。服务提供商可能不会限制我们的请求,而是会因额外负载而向我们收取额外费用。有些甚至在短时间内禁止行为不端的客户。消费者为防止此类问题而实施的速率限制称为客户端速率限制。

    何时使用 RateLimiter?

    resilience4j-ratelimiter 用于客户端速率限制。

    服务器端速率限制需要诸如缓存和多个服务器实例之间的协调之类的东西,这是 resilience4j 不支持的。对于服务器端的速率限制,有 API 网关和 API 过滤器,例如 Kong API GatewayRepose API Filter。Resilience4j 的 RateLimiter 模块并不打算取代它们。

    Resilience4j RateLimiter 概念

    想要调用远程服务的线程首先向 RateLimiter 请求许可。如果 RateLimiter 允许,则线程继续。 否则,RateLimiter 会停放线程或将其置于等待状态。

    RateLimiter 定期创建新权限。当权限可用时,线程会收到通知,然后可以继续。

    一段时间内允许的调用次数称为 limitForPeriod。RateLimiter 刷新权限的频率由 limitRefreshPeriod 指定。timeoutDuration 指定线程可以等待多长时间获取权限。如果在等待时间结束时没有可用的权限,RateLimiter 将抛出 RequestNotPermitted 运行时异常。

    使用Resilience4j RateLimiter 模块

    RateLimiterRegistryRateLimiterConfigRateLimiterresilience4j-ratelimiter 的主要抽象。

    RateLimiterRegistry 是一个用于创建和管理 RateLimiter 对象的工厂。

    RateLimiterConfig 封装了 limitForPeriodlimitRefreshPeriodtimeoutDuration 配置。每个 RateLimiter 对象都与一个 RateLimiterConfig 相关联。

    RateLimiter 提供辅助方法来为包含远程调用的函数式接口或 lambda 表达式创建装饰器。

    让我们看看如何使用 RateLimiter 模块中可用的各种功能。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

    基本示例

    第一步是创建一个 RateLimiterConfig

    RateLimiterConfig config = RateLimiterConfig.ofDefaults();

    这将创建一个 RateLimiterConfig,其默认值为 limitForPeriod (50)、limitRefreshPeriod(500ns) 和 timeoutDuration (5s)。

    假设我们与航空公司服务的合同规定我们可以以 1 rps 调用他们的搜索 API。然后我们将像这样创建 RateLimiterConfig

    RateLimiterConfig config = RateLimiterConfig.custom()
      .limitForPeriod(1)
      .limitRefreshPeriod(Duration.ofSeconds(1))
      .timeoutDuration(Duration.ofSeconds(1))
      .build();

    如果线程无法在指定的 1 秒 timeoutDuration 内获取权限,则会出错。

    然后我们创建一个 RateLimiter 并装饰 searchFlights() 调用:

    RateLimiterRegistry registry = RateLimiterRegistry.of(config);
    RateLimiter limiter = registry.rateLimiter("flightSearchService");
    // FlightSearchService and SearchRequest creation omitted
    Supplier<List<Flight>> flightsSupplier =
      RateLimiter.decorateSupplier(limiter,
        () -> service.searchFlights(request));

    最后,我们多次使用装饰过的 Supplier<List<Flight>>

    for (int i=0; i<3; i++) {
      System.out.println(flightsSupplier.get());
    }

    示例输出中的时间戳显示每秒发出一个请求:

    Searching for flights; current time = 15:29:40 786
    ...
    [Flight{flightNumber='XY 765', ... }, ... ]
    Searching for flights; current time = 15:29:41 791
    ...
    [Flight{flightNumber='XY 765', ... }, ... ]

    如果超出限制,我们会收到 RequestNotPermitted 异常:

    Exception in thread "main" io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'flightSearchService' does not permit further calls at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43)
    
     at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:580)
    
    ... other lines omitted ...

    装饰方法抛出已检异常

    假设我们正在调用
    FlightSearchService.searchFlightsThrowingException() ,它可以抛出一个已检 Exception。那么我们就不能使用
    RateLimiter.decorateSupplier()。我们将使用
    RateLimiter.decorateCheckedSupplier() 代替:

    CheckedFunction0<List<Flight>> flights =
      RateLimiter.decorateCheckedSupplier(limiter,
        () -> service.searchFlightsThrowingException(request));
    
    try {
      System.out.println(flights.apply());
    } catch (...) {
      // exception handling
    }

    RateLimiter.decorateCheckedSupplier() 返回一个 CheckedFunction0,它表示一个没有参数的函数。请注意对 CheckedFunction0 对象的 apply() 调用以调用远程操作。

    如果我们不想使用 SuppliersRateLimiter 提供了更多的辅助装饰器方法,如 decorateFunction()decorateCheckedFunction()decorateRunnable()decorateCallable() 等,以与其他语言结构一起使用。decorateChecked* 方法用于装饰抛出已检查异常的方法。

    应用多个速率限制

    假设航空公司的航班搜索有多个速率限制:2 rps 和 40 rpm。 我们可以通过创建多个 RateLimiters 在客户端应用多个限制:

    RateLimiterConfig rpsConfig = RateLimiterConfig.custom().
      limitForPeriod(2).
      limitRefreshPeriod(Duration.ofSeconds(1)).
      timeoutDuration(Duration.ofMillis(2000)).build();
    
    RateLimiterConfig rpmConfig = RateLimiterConfig.custom().
      limitForPeriod(40).
      limitRefreshPeriod(Duration.ofMinutes(1)).
      timeoutDuration(Duration.ofMillis(2000)).build();
    
    RateLimiterRegistry registry = RateLimiterRegistry.of(rpsConfig);
    RateLimiter rpsLimiter =
      registry.rateLimiter("flightSearchService_rps", rpsConfig);
    RateLimiter rpmLimiter =
      registry.rateLimiter("flightSearchService_rpm", rpmConfig);  
    然后我们使用两个 RateLimiters 装饰 searchFlights() 方法:
    
    Supplier<List<Flight>> rpsLimitedSupplier =
      RateLimiter.decorateSupplier(rpsLimiter,
        () -> service.searchFlights(request));
    
    Supplier<List<Flight>> flightsSupplier
      = RateLimiter.decorateSupplier(rpmLimiter, rpsLimitedSupplier);

    示例输出显示每秒发出 2 个请求,并且限制为 40 个请求:

    Searching for flights; current time = 15:13:21 246
    ...
    Searching for flights; current time = 15:13:21 249
    ...
    Searching for flights; current time = 15:13:22 212
    ...
    Searching for flights; current time = 15:13:40 215
    ...
    Exception in thread "main" io.github.resilience4j.ratelimiter.RequestNotPermitted:
    RateLimiter 'flightSearchService_rpm' does not permit further calls
    at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43)
    at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:580)

    在运行时更改限制

    如果需要,我们可以在运行时更改 limitForPeriodtimeoutDuration 的值:

    limiter.changeLimitForPeriod(2);
    limiter.changeTimeoutDuration(Duration.ofSeconds(2));

    例如,如果我们的速率限制根据一天中的时间而变化,则此功能很有用 – 我们可以有一个计划线程来更改这些值。新值不会影响当前正在等待权限的线程。

    RateLimiter和 Retry一起使用

    假设我们想在收到 RequestNotPermitted 异常时重试,因为它是一个暂时性错误。我们会像往常一样创建 RateLimiterRetry 对象。然后我们装饰一个 Supplier 的供应商并用 Retry 包装它:

    Supplier<List<Flight>> rateLimitedFlightsSupplier =
      RateLimiter.decorateSupplier(rateLimiter,
        () -> service.searchFlights(request));
    
    Supplier<List<Flight>> retryingFlightsSupplier =
      Retry.decorateSupplier(retry, rateLimitedFlightsSupplier);

    示例输出显示为 RequestNotPermitted 异常重试请求:

    Searching for flights; current time = 15:29:39 847
    Flight search successful
    [Flight{flightNumber='XY 765', ... }, ... ]
    Searching for flights; current time = 17:10:09 218
    ...
    [Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]
    2020-07-27T17:10:09.484: Retry 'rateLimitedFlightSearch', waiting PT1S until attempt '1'. Last attempt failed with exception 'io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'flightSearchService' does not permit further calls'.
    Searching for flights; current time = 17:10:10 492
    ...
    2020-07-27T17:10:10.494: Retry 'rateLimitedFlightSearch' recorded a successful retry attempt...
    [Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]

    我们创建装饰器的顺序很重要。如果我们将 RetryRateLimiter 包装在一起,它将不起作用。

    RateLimiter 事件

    RateLimiter 有一个 EventPublisher,它在调用远程操作时生成 RateLimiterOnSuccessEventRateLimiterOnFailureEvent 类型的事件,以指示获取权限是否成功。我们可以监听这些事件并记录它们,例如:

    RateLimiter limiter = registry.rateLimiter("flightSearchService");
    limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));
    limiter.getEventPublisher().onFailure(e -> System.out.println(e.toString()));

    日志输出示例如下:

    RateLimiterEvent{type=SUCCESSFUL_ACQUIRE, rateLimiterName='flightSearchService', creationTime=2020-07-21T19:14:33.127+05:30}
    ... other lines omitted ...
    RateLimiterEvent{type=FAILED_ACQUIRE, rateLimiterName='flightSearchService', creationTime=2020-07-21T19:14:33.186+05:30}

    RateLimiter 指标

    假设在实施客户端节流后,我们发现 API 的响应时间增加了。这是可能的 – 正如我们所见,如果在线程调用远程操作时权限不可用,RateLimiter 会将线程置于等待状态。

    如果我们的请求处理线程经常等待获得许可,则可能意味着我们的 limitForPeriod 太低。也许我们需要与我们的服务提供商合作并首先获得额外的配额。

    监控 RateLimiter 指标可帮助我们识别此类容量问题,并确保我们在 RateLimiterConfig 上设置的值运行良好。

    RateLimiter 跟踪两个指标:可用权限的数量(
    resilience4j.ratelimiter.available.permissions)和等待权限的线程数量(
    resilience4j.ratelimiter.waiting.threads)。

    首先,我们像往常一样创建 RateLimiterConfigRateLimiterRegistryRateLimiter。然后,我们创建一个 MeterRegistry 并将 RateLimiterRegistry 绑定到它:

    MeterRegistry meterRegistry = new SimpleMeterRegistry();
    TaggedRateLimiterMetrics.ofRateLimiterRegistry(registry)
      .bindTo(meterRegistry);

    运行几次限速操作后,我们显示捕获的指标:

    Consumer<Meter> meterConsumer = meter -> {
      String desc = meter.getId().getDescription();
      String metricName = meter.getId().getName();
      Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
        .filter(m -> m.getStatistic().name().equals("VALUE"))
        .findFirst()
        .map(m -> m.getValue())
        .orElse(0.0);
      System.out.println(desc + " - " + metricName + ": " + metricValue);};meterRegistry.forEachMeter(meterConsumer);

    这是一些示例输出:

    The number of available permissions - resilience4j.ratelimiter.available.permissions: -6.0
    The number of waiting threads - resilience4j.ratelimiter.waiting_threads: 7.0

    resilience4j.ratelimiter.available.permissions 的负值显示为请求线程保留的权限数。在实际应用中,我们会定期将数据导出到监控系统,并在仪表板上进行分析。

    实施客户端速率限制时的陷阱和良好实践

    使速率限制器成为单例

    对给定远程服务的所有调用都应通过相同的 RateLimiter 实例。对于给定的远程服务,RateLimiter 必须是单例。

    如果我们不强制执行此操作,我们代码库的某些区域可能会绕过 RateLimiter 直接调用远程服务。为了防止这种情况,对远程服务的实际调用应该在核心、内部层和其他区域应该使用内部层暴露的限速装饰器。

    我们如何确保未来的新开发人员理解这一意图?查看 Tom 的文章,其中揭示了一种解决此类问题的方法,即通过组织包结构来明确此类意图。此外,它还展示了如何通过在 ArchUnit 测试中编码意图来强制执行此操作。

    为多个服务器实例配置速率限制器

    为配置找出正确的值可能很棘手。如果我们在集群中运行多个服务实例,limitForPeriod 的值必须考虑到这一点

    例如,如果上游服务的速率限制为 100 rps,而我们的服务有 4 个实例,那么我们将配置 25 rps 作为每个实例的限制。

    然而,这假设我们每个实例上的负载大致相同。 如果情况并非如此,或者如果我们的服务本身具有弹性并且实例数量可能会有所不同,那么 Resilience4j 的 RateLimiter 可能不适合

    在这种情况下,我们需要一个速率限制器,将其数据保存在分布式缓存中,而不是像 Resilience4j RateLimiter 那样保存在内存中。但这会影响我们服务的响应时间。另一种选择是实现某种自适应速率限制。尽管 Resilience4j 可能会支持它,但尚不清楚何时可用。

    选择正确的超时时间

    对于 timeoutDuration 配置值,我们应该牢记 API 的预期响应时间

    如果我们将 timeoutDuration 设置得太高,响应时间和吞吐量就会受到影响。如果它太低,我们的错误率可能会增加。

    由于此处可能涉及一些反复试验,因此一个好的做法是将我们在 RateLimiterConfig 中使用的值(如 timeoutDurationlimitForPeriodlimitRefreshPeriod)作为我们服务之外的配置进行维护。然后我们可以在不更改代码的情况下更改它们。

    调优客户端和服务器端速率限制器

    实现客户端速率限制并不能保证我们永远不会受到上游服务的速率限制

    假设我们有来自上游服务的 2 rps 的限制,并且我们将 limitForPeriod 配置为 2,将 limitRefreshPeriod 配置为 1s。如果我们在第二秒的最后几毫秒发出两个请求,在此之前没有其他调用,RateLimiter 将允许它们。如果我们在下一秒的前几毫秒内再进行两次调用,RateLimiter 也会允许它们,因为有两个新权限可用。但是上游服务可能会拒绝这两个请求,因为服务器通常会实现基于滑动窗口的速率限制。

    为了保证我们永远不会从上游服务中获得超过速率,我们需要将客户端中的固定窗口配置为短于服务中的滑动窗口。因此,如果我们在前面的示例中将 limitForPeriod 配置为 1 并将 limitRefreshPeriod 配置为 500ms,我们就不会出现超出速率限制的错误。但是,第一个请求之后的所有三个请求都会等待,从而增加响应时间并降低吞吐量。

    结论

    在本文中,我们学习了如何使用 Resilience4j 的 RateLimiter 模块来实现客户端速率限制。 我们通过实际示例研究了配置它的不同方法。我们学习了一些在实施速率限制时要记住的良好做法和注意事项。

    您可以使用 GitHub 上的代码演示一个完整的应用程序来说明这些想法。


    本文译自: Implementing Rate Limiting with Resilience4j – Reflectoring