精华内容
下载资源
问答
  • 第1节 关系模型的好坏 ER模型转换的关系是否...如果存在冗余,就需要对关系模式进行优化主要优化技术: 模式分解 (比如, ABCD 用 AB和BCD, 或者 ACD 和 ABD替代)。 如何正确地使用模式分解: 是否有必要分解一

    第1节 关系模型的好坏

    • ER模型转换的关系是否就是最优的关系?不一定。

      关系模型潜在的问题:

      • 添加异常
    • 修改异常

      • 删除异常
      • 数据冗余

    冗余

    • 当数据的某些部分能由其他部分推导出来,就意味着存在冗余
    • 冗余的存在是因为存在完整性约束

    如何解决冗余问题

    • 由于存在约束,特别是函数依赖,导致冗余的产生。
    • 如果存在冗余,就需要对关系模式进行优化。
      • 主要优化技术: 模式分解 (比如, ABCD 用 AB和BCD, 或者 ACD 和 ABD替代)。
    • 如何正确地使用模式分解:
      • 是否有必要分解一个模式?
      • 分解以后会不会出现什么问题?

    如何评价一个关系是好的还是坏的?

    • 坏关系到好关系

      • 对关系模式进行优化
      • 主要优化技术:模式分解(比如ABCD用AB和BCD,或者ACD和ABD代替)

    第2节 函数依赖的概念

    • 函数依赖概念
      • 函数依赖可形式化表示为X->Y, 其中X and Y是属性集
      • 例如:rating->hrly_wage, AB->C
      • 如果对于关系实例r中的任意一对元组t1和t2,有t1.X = t2.X逻辑蕴含t1.Y = t2.Y,那么函数依赖X->Y在关系r中成立
      • 即关系r中给定两个元组,如果X属性值相等则Y的值也必须相等(X和Y是属性集)
      • 如果对关系R中的每个实例r,都满足函数依赖,则该函数依赖在关系R上成立。
        • 函数依赖必须由应用的语义所决定。
        • 对于给定关系R的某实例r,我们能判断它是否违反了某个函数依赖。
        • 但是不能仅仅根据一个满足函数依赖的实例来断定该函数依赖在关系模式R上成立。
    • 函数依赖起到检测冗余是否存在的作用

    完全函数依赖

    在一张表中,若 X → Y X \rightarrow Y XY,且对于 X 的任何一个真子集(假如属性组 X 包含超过一个属性的话), X ′ → Y X' \rightarrow Y XY 不成立,那么我们称 Y 对于 X 完全函数依赖,记作 X → F Y X\mathop{\rightarrow}\limits ^F Y XFY

    例如:

    • 学号 → F \mathop{\rightarrow}\limits ^F F姓名
    • (学号,课名) → F \mathop{\rightarrow}\limits ^F F分数 (注:因为同一个的学号对应的分数不确定,同一个课名对应的分数也不确定)

    部分函数依赖

    假如 Y 函数依赖于 X,但同时 Y 并不完全函数依赖于 X,那么我们就称 Y 部分函数依赖于 X,记作 X → P Y X \mathop{\rightarrow}\limits ^P Y XPY

    例如:

    • (学号,课名) → P \mathop{\rightarrow}\limits ^P P姓名

    主属性

    包含在任意一个码中的属性称为主属性。

    非主属性

    不包含在任何一个码中的属性称为非主属性。

    第3节 范式和Armstrong公理

    范式

    如果一个关系满足一种范式(BCNF,3NF等),就能判断该关系模式是否避免了某类问题。这样就能知道该关系是否需要分解。

    • 任何符合BCNF的关系也符合 2NF

    第一范式(1NF)

    • 每个属性都是原子属性
    • 本质上所有关系都满足第一范式

    第二范式(2NF)

    • 任何满足第二范式的关系满足第一范式
    • 所有非主属性必须依赖于整个主码而不能依赖于主码的部分属性
    • 例如:关系模式 Inventory(part, warehouse, quantity,warehouse_address).
      假设 {part, warehouse}是主码. warehsouse_address 单独依赖于 warehouse -则违反了第二范式。 解决方法: 分解关系模式

    第三范式(3NF)

    • 任何符合BCNF的关系也符合 3NF
    • 符合3NF要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。
    • 有一些情况
      • BCNF 不能保持函数依赖,
      • 在更新上有效的检查函数依赖是否违背是非常重要的。
    • 解决方法: 定义一个弱的关系, 叫做第三范式 (3NF)
      • 允许存在一些冗余

      • 函数依赖是否保持可以在单独的关系上检查,而不需要进行连接计算.

      • 3NF一般是保持无损连接分解和保持函数依赖

    • R符合BCNF, 那么R符合3NF
    • 如果 R 符合 3NF, 可能会存在一定的冗余,它是分解和性能的一种折中
    • 将R分解为满足3NF的关系集合,是保持了无损连接分解和保持函数依赖的
    3NF分解
    • 显然, 分解为BCNF的无损连接分解的算法能够用来获取无损连接分解的3NF.
    • 为了保持函数依赖, 一个思想是:
      • 如果 X → Y 不保持, 增加关系XY.
      • 问题是XY可能会违背 3NF!
    • 细化: 不考虑 FDs F, 而是使用F的最小的函数依赖集.

    图片42

    3NF分解算法的其它例子

    图片43

    Boyce-Codd 范式(BCNF)

    • 对于 R 有函数依赖集 FDs F ,如果R符合BCNF 当且仅当每一个非平凡的 FD

      • X → A in F , X 是R的超键 (i.e., X → R in F+).

      • 对于 FD X → A 是平凡的,当且仅当 A ⊆ X.

    • 也就是说, R 符合BCNF 当且仅当非平凡的FDs 箭头左侧是键.
      BCNF:

    • R中没有数据能够使用FDs预测.

    • Why:

      • X是超键, 不会有两个元组的X值相同
    • 如果R只有两个属性, 那么它符合BCNF

    • 如果F只包括R中的属性:

      • R 符合BCNF 当且仅当每一个F中的函数依赖 X → Y (not F+!), X 是 R的超键, i.e., X → R is in F+ (not F!).
    BCNF的检测
    • 列出所有的非平凡函数依赖
    • 确认每一个函数依赖箭头左边的属性集是R的超键。
    • 注意:我们需要首先找出R的超键!

    例:Courses(course_num, dept_name, course_name, classroom, enrollment, student_name, address) 符合BCNF?
    FDs 包括:

    • course_num, dept_name → course_name

    • course_num, dept_name → classroom

    • course_num, dept_name → enrollment

    那么(course_num, dept_name)+?

    • {course_num, dept_name, course_name, classroom, enrollm ent}
      Therefore, the key is {course_num, dept_name, student_name}

    不符合BCNF

    BCNF范式分解
    • 当一个关系不符合BCNF: 那么分解它.
    • 假定关系R 包含属性A1 … An. R的分解会分解为两个或者多个关系:
      • 每个新的关系的属性为R属性的子集 (不会有属性不属于 R), 并且R 中的每一个属性至少会在一个新的关系中.
    • 考虑关系R 和函数依赖集 FDs F. 如果F中的函数依赖 X → A 违背 BCNF, 那么: 分解R 为R – A 和XA.
    • 重复这个思想,我们会得到一个符合BCNF的关系集合.
    • 保持无损连接分解。
    将关系模式R<U,F>分解为一个BCNF的基本步骤

    image-20200608143938557

    快速求候选码的方法

    首先对于给定的R(U)和函数依赖集F,可以将它的属性划分为4类:

    • L类,仅出现在F的函数依赖左部的属性。
    • R类,仅出现在F的函数依赖右部的属性。
    • N类,在F的函数依赖左部和右部均未出现的属性。
    • LR类,在F的函数依赖左部和右部两部均出现的属性。

    根据以下定理和推论来求解候选码。

    • 定理1:对于给定的关系模式R及其函数依赖集F,若X(X∈R)是L类属性,则X必为R的任一候选码的成员。
    • 推论1:对于给定的关系模式R及其函数依赖集F,若X(X∈R)是L类属性,且X+包含了R的全部属性,则X必为R的唯一候选码。
    • 定理2:对于给定的关系模式R及其函数依赖集F,若X(X∈R)是R类属性,则X不在任何候选码中。
    • 定理3:设有关系模式R及其函数依赖集F,如果X是R的N类属性,则X必包含在R的任一候选码中。
    • 推论2:对于给定的关系模式R及其函数依赖集F,如果X是R的N类和L类组成的属性集,且X+包含了R的有属性,则X是R的唯一候选码。

    例:

    如设有关系模式R(U),其函数依赖集为F,其中:U={A,B,C,D,E}, F={A→C,C→A,B→AC,D→AC} 求R的候选码。
    解:
    根据函数依赖可得:
    属性B、D为L类,E为N类,因此属性B、D、E必为候选码的成员,
    且此三个属性的闭包:B+=ABC,(BD)+=ABCD,(BDE)+=ABCDE,根据推论2可得BDE是R的唯一候选码。
    所以R的候选码为BDE。
    

    如果把例题中关系模式R(U)中的属性E去掉,那么再求R的候选码的话可以根据推论1得出BD为R的唯一候选码。

    快速求解方法适用于判断有属性是属于L类、N类或其中一种的情况下求解。如果有L类和N类的属性,则求解候选码速度非常快。

    简而言之:

    L、R、N、LR类。根据定理,L、N类必为侯选码之一,如果L+包含全部R,则L为唯一侯选。R类不在任何侯选码中。

    L+N类且(L+N)+包含所有R,则L+N为唯一侯选。(适于有L、N类至少一种的情况。)

    例题:
    设有关系模式R(A,B,C,D,E),其函数依赖集F={A→BC,CD→E,B→D,E→A},求R的所有候选码。
    解:
    (1)Fm={A→B, A→C,CD→E,B→D,E→A}
    (2)A,B,C,D,E五个属性在F中各个函数依赖的右边和左边都出现了,所以候选码中可能包含A,B,C,D,E。
    (3)A+=ABCDE,即A→U,所以A是一个候选码   B+,C+,D+→U,所以B,C,D不是候选码   E+=ABCDE,即E→U,所以也E是一个候选码
    (4)除去A,E两个候选码,在B,C,D中查找两个属性的候选码   (BC)+=ABCDE,即BC→U,所以BC是一个候选码   (BD)+=BD,即BC→U,所以BD不是一个候选码   (CD)+=ABCDE,即CD→U,所以CD是一个候选码
    候选码有:A,E,BC,CD
    

    BCNF 和 依赖保持

    • 分解得到的关系,符合BCNF,但可能不满足保持函数依赖.
    • 例子: 关系CSZ (city, street_name, zip_code) 函数依赖 FDs: CS → Z, Z → C
      (city, street_name) → zip_code
      zip_code → city
    • 要保持CS → Z,则不能分解, 但是CSZ 不符合BCNF.

    函数依赖理论:Armstrong公理

    从一个给定的函数依赖集推导出它所逻辑蕴含的所有函数依赖

    图片28

    图片29

    图片30

    第4节 函数依赖集

    4.1 函数依赖和键

    • 给定R(A,B,C)

      • A->ABC意味着A是一个键(码)
    • 通常,

      • X → R 意味着 X 是一个超键.
    • 键的约束

      • ssn → did

    图片31

    4.2 函数依赖集的闭包F+

    • 函数依赖集的闭包?由F逻辑蕴含的所有函数依赖的集合
    • 计算函数依赖集的闭包:

    图片32

    F+例子

    F = {A → B, B → C, C D → E }

    • Step 1: F中的每一个函数依赖, 使用自反律
      • 得到:CD → C; CD → D

      • 加到F上: F = {A → B, B → C, C D → E; CD → C; CD → D }

    • Step 2: F中的每一个函数依赖, 使用增补率
      • A → B 得到: A → AB; AB → B; AC → BC; AD→ BD; ABC →BC; ABD → BD; ACD →BCD
      • B → C 得到: AB → AC; BC → C; BD → CD; ABC → AC; ABD → ACD, etc.
    • Step 3: 使用传递率
    • 重复1~3步骤…

    可以看出计算F+代价太高.

    4.3 求最小依赖集

    关系模式R(U,F)中,R=ABCDEG F={BE→G,BD→G,CD→A,CE→G,CDE→AB,BC→A,B→D}

    1. 首先把右边的属性都变成单个属性

      函数依赖集F={BE→G,BD→G,CD→A,CE→G,CDE→A,CDE→B,BC→A,B→D}

    2. 对于函数依赖F中的每个函数X->A,设G=F-{X->A},如果A属于关于函数依赖集G的闭包,将X->A从F中删除,否则保留,然后得出新的F。

      BE+=BEDG,包含G,删除。

      BD+=BD,不包含G,保留。

      CD+=CD,不包含A,保留。

      CE+=CE,不包含G,保留。

      CDE+=CDEAGBA,包含A,删除。

      CDE+=CDEAG,不包含B,保留。

      BC+=BCDGA,包含A,删除。

      B+=B,不包含D,保留。

      得到新的F={BD→G,CD→A,CE→G,CDE→B,B→D}

    3. 对于F中每一个左端包含多个属性的X->A(即去掉各依赖左部多余的属性。一个一个地检查函数依赖左部非单个属性的依赖),选择X的每个子集Z,如果A属于Z的闭包,则用Z->A代替X->A。

      BD→G:B+=BDG,包含G,去掉;D+=D,不包含G,保留。

      CD→A:C+=C,不包含A,保留;D+=D,不包含A,保留。

      CE→G:C+=C,不包含G,保留;E+=E,不包含G,保留。

      CDE→B:C+=C,不包含B,保留;D+=DG,不包含B,保留;E+=E,不包含B,保留。

    最后得出关系模式R(U,F)的最小依赖集Fm={D→G,CD→A,CE→G,CDE→B,B→D}

    4.3 属性集闭包

    • (函数依赖集闭包的大小是(属性的)指数级的)
    • 很多时候, 我们仅仅是想判断一个 FD X →Y 是否在F的闭包中. 一个有效的方式是:
      • 计算属性X的闭包 (记为X+) :
        • X的闭包 就是由X在F上蕴含的所有属性的集合。
        • 计算属性的闭包仅仅需要一个线性的时间算法就够了.
      • F = {A → B, B → C, C D → E } A → E成立吗?

    属性集闭包的作用

    1. 测试超键
      • – 判断X是否是一个超键?只需要计算 X+, 检查 X+ 是否包括R的所有属性.
    2. 检测函数依赖
      • 判断X → Y 是否成立 (或者说, 是否在F+中), 只需要判断Y ⊆ X+.
      • 因此, 我们计算X+, 然后检测这个属性集闭包是否包括 Y.
      • 简单有用的方法
    3. 计算F的函数依赖集闭包

    图片33

    图片34

    图片35

    图片37

    第5节 模式分解的基本标准

    分解的问题

    • 模式分解可能存在三种问题:

      1. 一些查询可能会代价变高.
        e.g., Attishoo 挣了多少钱? (earn = W*H)
      2. 分解后,根据分解的实例, 我们可能不能重新构建分解前的实例!
      3. 检查某些依赖需要考虑分解后的多个关系.
    • 折中: 考虑这些问题 vs. 冗余.

    分解

    • 将分解符合3NF或更高的范式是一种很好的保证方法.
    • 所有分解应该是无损的! (Avoids Problem (2))

    无损连接分解

    • 将R 分解为 R1 和R2 ,如果是无损连接分解,那么应该满足:

    图片38

    例子(不是无损的)

    图片39

    例子(无损的)

    图片40

    保持函数依赖

    • 保持函数依赖的分解(直观上):
      • R 分解为X, Y 和Z, 函数依赖集FDs在X,Y,Z上成立,那么FDs也会在R上成立。

    分解后的函数依赖?

    • 函数依赖集的投影: R 分解为 X, … F 在X上的投影 (denoted FX ) 是如下的 FDs U → V in F+ (closure of F ) ,U, V 是X中的属性.

    保持函数依赖的分解

    • 将R分解为X和Y是保持函数依赖的,当且仅当 ( F X ∪ F Y ) + = F + (F_X \cup F_Y)^+ = F^+ (FXFY)+=F+
    • 注意是 F + F^+ F+,而不是 F F F
    • 保持函数依赖并不能保证保持无损连接分解
    • 反之亦然

    判断两个函数依赖集是否等价

    • 如果 F1+ = F2+, 那么F1和F2等价.
    • 例如, F1={A →B, A →C} 和 F2={A → BC}等价

    例子

    • F={ A → BC, B →C }. 判断 C →AB 是否在 F+?
    • Answer: 不在.
      Reason 1) C+=C, 不包括 AB.
      Reason 2) 反例,不存在 C → AB.

    BCNF和3NF的比较

    • 将一个关系分解为符合3NF的关系集合:
      • 分解是无损的
      • 分解是保持函数依赖的
    • 将一个关系分解为符合BCNF的关系集合:
      • 分解是无损的
      • 可能不保持函数依赖

    图片44

    设计目标

    • BCNF
    • 无损连接
    • 保持函数依赖

    如果不能得到这些

    • 函数依赖的保持
    • 使用3NF产生冗余

    Example

    图片45

    • R(A,B,F), F = {AC → E, B → F}.
      • Candidate key? AB
      • BCNF? No, because of B → F (B is not a superkey).
      • 3NF? No, because of B → F (F is not part of a candidate key).
    • R(D, C, H, G), F = {A → I, I → A}
      • Candidate key? DCHG
      • BCNF? Yes
      • 3NF? Yes

    图片46

    • R(A,B,C,D,E,F),F={A->C,C->A,B->AC,D->AC}
      • 找出R的键
      • 是否符合3NF,如果是,说明原因,如果否,分解。
    (1)R的候选码为BD
    (2)
    ①将F中的函数依赖都分解为右部为单属性的函数依赖.
    F={A→C,C→A,B→A,B→C,D→A,D→C,BD→A}
    ②去掉F中冗余的函数依赖.
    判断A→C是否冗余.
    设:G1={C→A,B→A,B→C,D→A,D→C,BD→A},得(A)G1+=A
    ∵C不属于(A)G1+   ∴ A→C不冗余
    判断C→A是否冗余.
    设:G2={A→C,B→A,B→C,D→A,D→C,BD→A},得(C)G2+=C
    ∵A不属于(C)G1+   ∴ C→A不冗余
    判断B→A是否冗余.
    设:G3={A→C,C→A,B→C,D→A,D→C,BD→A},得(B)G3+=BCA
    ∵A属于(B)G3+   ∴ B→A冗余
    判断B→C是否冗余.
    设:G4={A→C,C→A,D→A,D→C,BD→A},得(B)G4+=B
    ∵C不属于(B)G4+   ∴ B→C不冗余
    判断D→A是否冗余.
    设:G5={A→C,C→A,B→C,D→C,BD→A},得(D)G5+=DCA
    ∵A不属于(D)G5+   ∴ D→A冗余
    判断A→C是否冗余.
    设:G6={A→C,C→A,B→C,BD→A},得(D)G6+=D
    ∵C不属于(D)G6+   ∴ D→C不冗余
    判断BD→A是否冗余.
    设:G7={A→C,C→A,B→C,D→C},得(BD)G7+=BDCA
    ∵A不属于(BD)G7+   ∴ BD→A冗余
    F={A→C,C→A,B→C,D→C}
    ③由于各函数依赖左部都为单属性,故:
    Fm={A→C,C→A,B→C,D→C}
    (3)τ={AC,BC,DC,BD}
    
    • 解法一

    图片47

    • 解法二

    图片48

    展开全文
  • 4.数据冗余(主要关系中存在了相互之间的约束依赖,使得某一属性的值确定后,另一个属性的值也就确定了) 坏关系关系中存在约束,特别是函数依赖,导致冗余,异常等发生就是一个坏关系,如果...

    关系模型潜在的问题
    1.添加异常(当在关系中添加数据时可能会导致数据的不一致)
    2.修改异常(随意的修改关系中的一行记录也可能导致数据的不一致)
    3.删除异常(当删除一定数量的记录时可能会导致一些其他信息的丢失)
    4.数据冗余(主要是关系中存在了相互之间的约束依赖,使得某一属性的值确定后,另一个属性的值也就确定了)
    坏关系
    当关系中存在约束,特别是函数依赖,导致冗余,异常等发生就是一个坏关系,如果存在这些问题就需要对关系模式进行优化,而主要的优化技术就是“模式分解”。

    第一范式 第二范式 第三范式 BCNF范式–详解连接

    函数依赖

    函数依赖 (FD):可形式化表示为:X→Y,其中X和Y是属性集(读为:X决定Y或者Y依赖于X)。即关系R中给定两个元组,如果X属性值相等,则Y的值也必须相等。

    函数依赖的作用:函数依赖起到检测冗余是否存在的作用,通过冗余的检测,就能很容易判断出这个关系是不是存在相应的插入,删除,更新异常。

    函数依赖集(F):由关系上一些函数依赖组成的集合叫函数依赖集

    函数依赖集的闭包(F+):由函数依赖集F所蕴含的所有函数依赖的集合

    函数依赖和关系的键给定关系R(A,B,C),A→ABC表示A属性决定了R的所有属性,A决定了R,X→R意味着X是一个超键。
    超码、候选码、主码 与 外码

    不管是判断关系的好坏还是范式的分解都离不开函数依赖,Armstrong公理能够帮助我们从一个给定的函数依赖集推导出它所蕴含的所有的函数依赖。

    Armstrong公理(X,Y,Z是属性集)
    自反律:若 α为一属性集且 β ⊆ α,则 α→β成立。(比如CD→C,CD→D)
    增补律:若α→β成立且γ为一属性集,则γα→γβ成立。
    传递律:若α→β和 β→γ成立,则 α→γ成立。
    Armstrong公理的重要推论
    合并律:若 α→β和 α→γ成立,则α→βγ成立。
    分解律:若 α→β成立,则α→β 和α→γ成立。
    伪传递律:若α→β和 γβ→δ成立,αγ→δ则成立。
    使用Armstrong公理计算F+
    在这里插入图片描述

    属性集闭包

    属性集闭包定义
    令α为一个属性集。我们将函数依赖集 F 下被α函数确定的所有属性的集合,称为 F 下α的 闭包,记为α+。
    属性集闭包的求解
    求 A+:将 A 置入 A+。对每一 FD,若左部属于 A+,则将右部置入 A+,一直重复至 A+不能扩大。
    在这里插入图片描述
    属性集闭包的作用
    1.判断α是否为超键(即α决定R中的所有属性):计算α+,检查α+是否包含R 中的所有属性。
    2.判断函数依赖α→β是否成立:检查是否有β⊆α+ 。
    3.计算函数依赖集的闭包F+:对任意γ⊆ R,找出其属性闭包γ+,对任意的S⊆γ+,输出一个函数依赖γ→S。

    通过属性集闭包求函数依赖集F+的例子
    已知 F={A→B, B→C },求 F+
    第一步:构建一个空的二维表,行和列分别列处所有可能的属性组合
    在这里插入图片描述
    第二步:计算所有属性组合的属性集闭包
    已A+为例:
    已知 F={A→B, B→C },求 A+
    A+ = A
    考虑 A→B,A+ = AB
    考虑 B→C,B 在 A+中,A+ = ABC
    所以可求:A+= ABC ,同理可求
    在这里插入图片描述
    第三步:将包括在对应属性闭包里的属性都勾选出来
    在这里插入图片描述
    任何一个勾都表示一个函数依赖,比如第一行第六列这个勾表示A→BC(A决定BC)的函数依赖,所有的函数依赖关系组成了函数依赖集的闭包。

    模式分解

    模式分解的基本标准
    1.无损连接分解(如果将R分解为R1和R2,应该满足分解后的两个关系实例能够通过连接组合为原来的关系,使信息不发生改变)
    在这里插入图片描述
    满足条件:
    若R分解为R1和R2是无损分解,则下面至少一个成立的话,那么分解就是无损分解:
    R1∩R2→R1(函数依赖)
    R1∩R2→R2(函数依赖)
    实际上将R分解为(UV)和(R-V),如果U→V在R上成立,那么分解时无损连接分解。
    2.保持函数依赖
    保持函数依赖的分解(直观上)就是R分解为X,Y,Z,函数依赖集(FDs)F在X,Y,Z上成立,那么FDs也会在R上成立。直观上无法在分解后的单一关系上验证原关系上的函数依赖,就表示无法保持函数依赖。

    BCNF范式和第三范式

    BCNF范式

    对于R有函数依赖集F,如果R符合BCNF当且仅当每一个非平凡的FD满足:
    1.对于每一个非平凡函数依赖X→Y中,X是R的超键(即X能决定R中的所有属性,所有非平凡函数依赖集F中的箭头左边是键)
    2.函数依赖是平凡的(X→Y且Y是X的子集就是平凡函数依赖)

    性质
    如果R(A,B)只有两个属性,那么它符合BCNF(因为A,B的关系:1.AB之间没有相互一个依赖的关系2.A决定B3.B决定A,第一种情况是平凡函数依赖,第二种A是主键,第三种B是主键,箭头的左边都是我们的键,所以只要关系是两个属性的必然满足BCNF范式)

    BCNF范式分解
    当一个关系不符合BCNF:那么分解它时考虑到关系R和函数依赖集F,如果F中的函数依赖X→A违背BCNF,那么分解R为R-A和XA,重复这个操作就可以得到一个符合BCNF的关系集合。
    BCNF分解一定是保持无损连接分解,通常情况下有多个函数违背BCNF,那么我们处理它们的顺序不同会导致分解的结果不同,得到不同的都符合BCNF的关系集。BCNF范式分解是符合函数依赖的,但是可能不满足保持函数依赖,有的也可能满足保持函数依赖。
    例题:
    关系模式 R,有U={A,B,C,D,E,G},F={B→G,CE→B,C→A,CE→G,B→D,C→D}
    1.候选键
    因为 B+ =(B,D,G);C+=(A,C,D);CE+=(A,B,C,D,E,G) 所以可知其候选码为 CE
    2. 违反 BCNF 的函数依赖:C→A,B→G,B→D,C→D
    3. 分解
    1)R1={AC};F1={C→A}
    R1’={BCDEG};F1’={B→G,CE→B,CE→G,B→D,C→D};不满足 BCNF
    2)R2={BG};F2={B→G} R2’={BCDE};F2’={CE→B,B→D,C→D};不满足 BCNF
    3)R3={BD};F3={B→D}R3’={BCE};F3’={CE→B};

    满足 BCNF 最终答案:R1={AC};R2={BG};R3={BD};R4={BCE};

    3NF(三范式)

    BCNF范式在有些情况是不能保持函数依赖的,而在更新上有效的检查函数依赖是否违背是很重要的,在BCNF范式的基础上将限制放松一点,从而让分解后的关系保持函数依赖,定义一个弱的关系—3NF。

    R符合BCNF,那么R符合3NF,如果R符合3NF,可能存在一定的冗余,它是分解和性能的一种折中。3NF中函数依赖是否保持可以在单独的关系上检查,而不需要进行连接计算,3NF一般是保持无损连接分解和保持函数依赖的。

    函数依赖集F符合3NF,当且仅当F中每一个函数依赖X→A(X⊆R and A⊆R)
    符合下列描述中的一个:
    1.A⊆X(平凡的FD)
    2.X是超键
    3.A是R的键的一部分

    3NF分解

    首先第一步是要计算正则覆盖Fc,然后将Fc中每一个函数依赖左右两边属性组合为一个新的关系Ri,再然后判断Ri是否包括R的某个候选键,如果没有包括,则将R的一个候选键属性集单独作为一个新的关系,最后判断一下所有的新关系是否有包含关系,如果有则合并。
    例题1:
    设有关系模式:R={A,B,C,G,H,I}
    函数依赖集F={A→B A→C CG→H CG→I B→H},R如何“双保持”分解为一组3NF?(双保持:无损分解,函数依赖)
    第一步:求F的正则覆盖(用合并律)
    Fc={A→BC CG→HI B→H}
    由于去掉任何属性都和原来的F不等价,所以没有多余的属性。
    第二步:求R的3NF分解
    A→BC R1=(A,B,C) F1={A→BC}
    CG→HI R2=(CGHI) F2={CG→HI}
    B→H R3={BH} F3={B→H}
    第三步:确定是否添加R候选键做成的子模式
    仅一个候选键AG
    因为:(AG)+=AGBCHI
    而:A+=ABCH G+=G
    因该候选键未包含在R1,R2,R3中,需要产生子模式R4=(AG) F4=Ø

    例题2
    关系cust_bancker_branch(customer_id,employee_id,branch_name,type)
    函数依赖集为
    F={customer_id,employee_id->branch_name,type
    employee_id->branch_name
    customer_id,branch_name->employee_id}
    首先计算函数依赖集的正则覆盖Fc,branch_name是多余的属性
    Fc={customer_id,employee_id->type
    employee_id->branch_name
    customer_id,branch_name->employee_id}
    然后根据Fc生成3个子模式
    R1={customer_id,employee_id,type}
    R2={employee_id,branch_name}
    R3={customer_id,branch_name,employee_id}
    候选键customer_id,employee_id是候选键,并且包括在R1中,因此不需要添加新的关系模式
    又因为R3包括R2,因此删除R2得到最终的关系分解结果:
    R1={customer_id,employee_id,type}
    R3={customer_id,branch_name,employee_id}
    例题3
    关系模式 R,有 U={A,B,C,D,E,G},F={B→G,CE→B,C→A,CE→G,B→D,C→D},求3NF分解
    0.候选码
    因为 B+=(B,D,G);C+=(A,C,D);CE+=(A,B,C,D,E,G)
    所以可知其候选码为 CE
    1.正则覆盖
    1)运用算法和定义,可以合并 B→G,B→D 为 B→DG;合并
    C→A,C→D 为 C→AD;合并 CE→B,CE→G 为 CE→BG。 得到函数依赖集 FC={B→DG,C→AD,CE→BG}
    2) 消除无关属性
    验证 G 是 CE→BG 的右方无关属性
    FC’={B→DG,C→AD,CE→B}, (CE)FC’+=(CEBDG),所以 FC’蕴含 FC 的 CE→BG,G 是 CE→BG 的右方无关属性,可以消除 正则覆盖 FC={B→DG,C→AD,CE→B}
    2. 由FC={B→DG,C→AD,CE→B}可以分解 R1={BDG},F1={B→DG}
    R2={ACD},F2={C→AD} R3={BCE},F3={CE→B}
    3.检查 候选键 CE 包含在 R3 中,不需要添加子模式,每个子模式之间无包含关系 最后答案:R1={BDG} ,R2={ACD},R3={BCE};

    正则覆盖

    正则覆盖的定义
    函数依赖集F 的正则覆盖 Fc,是 F 的最小函数依赖集,使得 F 与 Fc 相互蕴含。
    正则覆盖必须满足的性质
    1.Fc中任何函数依赖都不包含无关属性
    2.Fc 中函数依赖的左半边都是唯一的
    无关属性的定义
    如果去除函数依赖中的一个属性,不改变该函数依赖集的闭包,则称该属性是无关的
    无关属性的形式化定义
    设函数依赖集 F 中有α→β 
    1.如果 A∈α并且 F 逻辑蕴含(F – {α→β})∪{(α- A) →β},则属性 A 在α中是无关的。 
    2.如果 A∈β并且函数依赖集( F - { α→β} )∪{ α→( β- A ) }逻辑蕴含 F,则属性 A 在β中是无关的。(A逻辑蕴含B我感觉就是A能够推导出B)

    正则覆盖计算过程
    在这里插入图片描述
    在这里插入图片描述
    例题:
    求 F={A→B,B→A,B→C,A→C,C→A},最小函数依赖集合Fc
    1利用分解规则,将所有的函数依赖变成右边都是单个属性的函数依赖
    从题目来看,F中的任何一个函数依赖的右部仅含有一个属性{A→B,B→A,B→C,A→C,C→A}
    2去掉F 中多余的函数依赖
    (即假设某个依赖冗余,删除该依赖后的函数依赖集中,在删除的依赖关系箭头左边的属性的闭包能够包含箭头右边的属性,说明冗余则删除,否则进行还原,最终得到的最小函数依赖集使用合并律进行合并得到正则覆盖)
    (1)设A→B冗余
    从F 中去掉A→B,则F1={B→A,B→C,A→C,C→A}。
    根据F1 计算A+ =AC,但AC 不包含B,故A->B 不能从F中去掉。
    (2)设B→A冗余从F 中去掉B→A,则F2={A→B,B→C,A→C,C→A}。
    根据F2 计算B+ =ABC,包含所有属性,故B→A可从F中去掉。
    (3)设B→C冗余
    从F 中去掉B→C,则F3={A→B,A→C,C→A}。
    根据F3 计算B+= B,不包含C,故B→C不是冗余的函数依赖,不能从F 中去掉。
    (4)设A→C冗余
    从F 中去掉A→C,则F4={A→B,B→C,C→A}。
    根据F4 计算A+ =ABC,包含所有属性,故A→C可从F中去掉。
    (5)设C→A冗余
    从F 中去掉C→A,则F5={A→B,B→C}。
    根据F5 计算C+= C 不包含A,故C→A不是冗余的函数依赖,不能从F 中去掉。
    (6)至此
    所有依赖均以验算完毕,故
    F 最小函数依赖集合为:{A→B,B→C,C→A}

    BCNF和3NF的比较
    将一个关系分解为符合3NF的关系集合:
    1.分解是无损的
    2.分解是保持函数依赖的
    将一个关系分解为符合BCNF的关系集合
    1.分解是无损的
    2.可能不保持函数依赖

    总结

    1.若分解无损分解为BCNF,可能不能保存函数依赖,若为保持函数依赖而不分解,可能存在数据冗余现象,各有利弊,需因应用需求而定。数据库优化的过程,一般是首先分解为BCNF
    ,如果分解成BCNF,发现丢失了重要的函数依赖,则返回到3NF,如果某些多表连接查询也非常重要,则返回到2NF甚至是1NF。
    2.范式分解的方法
    BCNF分解:
    首先需要做的事情是分析关系的函数依赖集F中的函数依赖,找出不符合BCNF的函数依赖,将其箭头左右两端的属性取出,组合成一个新的关系,而从关系R中除掉箭头右端的属性后剩余的属性组成另外一个关系,之后再分析这两个关系是否符合BCNF,如果不符合需要进一步分解。
    在这里插入图片描述
    3NF分解
    首先需要做的事情是找出关系的正则覆盖Fc,然后将正则覆盖的每一个函数依赖箭头两端的 属性组合成新的关系,之后检测关系R的候选键是否已经存在于新的关系中,如果没有则将候选键作为新的关系,最后审查一下所有关系,看是否有包含关系,有则合并。
    在这里插入图片描述

    展开全文
  • 性能优化原则和优化模式

    千次阅读 2018-10-02 12:08:32
    一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高...

     

    摘要

    性能优化涉及面很广。一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高服务可用性三者相互矛盾,不可兼得。例如:增加缓存可以降低平均响应时间,但是处理线程数量会因为缓存过大而有所限制,从而降低系统吞吐量;为了提高服务可用性,对异常请求重复调用是一个常用的做法,但是这会提高响应时间并降低系统吞吐量。

    对于很多像联想,京东这样的公司,它们的系统会面临如下三个挑战:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据。这些挑战对于性能优化而言表现为:在保持和降低系统TP95响应时间(指的是将一段时间内的请求响应时间从低到高排序,高于95%请求响应时间的下确界)的前提下,不断提高系统吞吐量,提升流量高峰时期的服务可用性。这种场景下,三者的目标和改进方法取得了比较好的一致。本文主要目标是为类似的场景提供优化方案,确保系统在流量高峰时期的快速响应和高可用。

    文章第一部分是介绍,包括采用模式方式讲解的优点,文章所采用案例的说明,以及后面部分用到的一些设计原则;第二部分介绍几种典型的“性能恶化模式”,阐述导致系统性能恶化,服务可用性降低的典型场景以及形成恶化循环的过程;第三部分是文章重点,阐述典型的“性能优化模式”,这些模式或者可以使服务远离“恶化模式”,或者直接对服务性能进行优化;文章最后一部分进行总结,并对未来可能出现的新模式进行展望。

    本文性能设计原则有四个:1 最小可用;2经济;3 代码复用;4 奥卡姆剃刀。

    本文性能恶化模式有三个:1 长请求拥塞反模式;2 多请求杠杆反模式;3 复制缓存反模式。

    本文性能优化模式有七个:1 水平分割;2 垂直分割;3 局部化数据模式;4 实时离线分离模式;5 避免大炮打蚊子模式;6 恒变模式;7 降级模式。


    介绍

    模式讲解方式

    关于性能优化的文章和图书已有很多,但就我所知,还没有采用模式的方式去讲解的。本文借鉴《设计模式》("Design Patterns-Elements of Reusable Object-Oriented Software")对设计模式的阐述方式,首先为每一种性能优化模式取一个贴切的名字,便于读者快速理解和深刻记忆,接着讲解该模式的动机和原理,然后结合作者在联想,京东的具体工作案例进行深度剖析,最后总结采用该模式的优点以及需要付出的代价。简而言之,本文采用“命名-->原理和动机-->具体案例-->缺点和优点”的四阶段方式进行性能优化模式讲解。与其他方式相比,采用模式进行讲解有两个方面的优点:一方面,读者不仅仅能够掌握优化手段,而且能够了解采用该手段进行性能优化的场景以及所需付出的代价,这有利于读者全面理解和灵活应用;另一方面,模式解决的是特定应用场景下的一类问题,所以应用场景描述贯穿于模式讲解之中。如此,即使读者对原理不太了解,只要碰到的问题符合某个特定模式的应用场景(这往往比理解原理要简单),就可以采用对应的手段进行优化,进一步促进读者对模式的理解和掌握。

    案例说明

    文章的所有案例都来自于联想,京东的真实项目。出于两方面的考虑,作者做了一定的简化和抽象:一方面,系统可以优化的问题众多,而一个特定的模式只能解决几类问题,所以在案例分析过程中会突出与模式相关的问题;另一方面,任何一类问题都需要多维度数据去描述,而应用性能优化模式的前提是多维度数据的组合值超过了某个临界点,但是精确定义每个维度数值的临界点是一件很难的事情,更别说多维度数据组合之后临界点。因此有必要对案例做一些简化,确保相关取值范围得到满足。基于以上以及其他原因,作者所给出的解决方案只是可行性方案,并不保证其是所碰到问题的最佳解决方案。

    案例涉及的所有项目都是基于Java语言开发的,严格地讲,所有模式适用的场景是基于Java语言搭建的服务。从另外一方面讲,Java和C++的主要区别在于垃圾回收机制,所以,除去和垃圾回收机制紧密相关的模式之外,文章所描述的模式也适用于采用C++语言搭建的服务。对于基于其他语言开发的服务,读者在阅读以及实践的过程中需要考虑语言之间的差别。

    设计原则

    必须说明,本文中各种模式所要解决的问题之所以会出现,部分是因为工程师运用了某些深层次的设计原则。有些设计原则看上去和优秀的设计理念相悖,模式所解决的问题似乎完全可以避免,但是它们却被广泛使用。“存在即合理”,世界上没有完美的设计方案,任何方案都是一系列设计原则的妥协结果,所以本文主要关注点是解决所碰到的问题而不是如何绕过这些设计原则。下面对文中重要的设计原则进行详细阐述,在后面需要运用该原则时将不再解释。

    最小可用原则

    最小可用原则(快速接入原则)有两个关注点:1. 强调快速接入,快速完成;2. 实现核心功能可用。这是一个被普遍运用的原则,其目标是缩短测试周期,增加试错机会,避免过度设计。为了快速接入就必须最大限度地利用已有的解决方案或系统。从另外一个角度讲,一个解决方案或系统只要能够满足基本需求,就满足最小可用原则的应用需求。过度强调快速接入原则会导致重构风险的增加,原则上讲,基于该原则去设计系统需要为重构做好准备。

    经济原则

    经济原则关注的是成本问题,看起来很像最小可用原则,但是它们之间关注点不同。最小可用原则的目标是通过降低开发周期,快速接入而实现风险可控,而快速接入并不意味着成本降低,有时候为了实现快速接入可能需要付出巨大的成本。软件项目的生命周期包括:预研、设计、开发、测试、运行、维护等阶段。最小可用原则主要运用在预研阶段,而经济原则可以运用在整个软件生命周期里,也可以只关注某一个或者几个阶段。例如:运行时经济原则需要考虑的系统成本包括单次请求的CPU、内存、网络、磁盘消耗等;设计阶段的经济原则要求避免过度设计;开发阶段的经济原则可能关注代码复用,工程师资源复用等。

    代码复用原则

    代码复用原则分为两个层次:第一个层次使用已有的解决方案或调用已存在的共享库(Shared Library),也称为方案复用;第二个层次是直接在现有的代码库中开发,也称之为共用代码库。

    方案复用是一个非常实用主义的原则,它的出发点就是最大限度地利用手头已有的解决方案,即使这个方案并不好。方案的形式可以是共享库,也可以是已存在的服务。方案复用的例子参见避免蚊子大炮模式的具体案例。用搜索引擎服务来解决查找附近商家的问题是一个性能很差的方案,但仍被很多工程师使用。方案复用原则的一个显著优点就是提高生产效率,例如:Java之所以能够得到如此广泛应用,原因之一就是有大量可以重复利用的开源库。实际上“Write once, run anywhere”是Java语言最核心的设计理念之一。基于Java语言开发的代码库因此得以在不同硬件平台、不同操作系统上更广泛地使用。

    共用代码库要求在同一套代码库中完成所有功能开发。采用这个原则,代码库中的所有功能编译时可见,新功能代码可以无边界的调用老代码。另外,原代码库已存在的各种运行、编译、测试、配置环境可复用。主要有两个方面地好处:1. 充分利用代码库中已有的基础设施,快速接入新业务;2. 直接调用原代码中的基础功能或原語,避免网络或进程间调用开销,性能更佳。共用代码库的例子参见垂直分割模式的具体案例。

    从设计的角度上讲,方案复用类似于微服务架构(Microservice Architecture,有些观点认为这是一种形式的SOA),而共用代码库和Monolithic Architecture很接近。总的来说,微服务倾向于面向接口编程,要求设计出可重用性的组件(Library或Service),通过分层组织各层组件来实现良好的架构。与之相对应,Monolith Architecture则希望尽可能在一套代码库中开发,通过直接调用代码中的基础功能或原語而实现性能的优化和快速迭代。使用Monolith Architecture有很大的争议,被认为不符合“设计模式”的理念。参考文献[4],Monolithic Design主要的缺点包括:1. 缺乏美感;2. 很难重构;3. 过早优化(参见文献[6]Optimize judiciously); 4. 不可重用;5. 限制眼界。微服务架构是很多互联网公司的主流架构,典型的运用公司包括Amazon、联想,京东等。Monolithic Architecture也有其忠实的粉丝,例如:Tripadvisor的全球网站就共用一套代码库;基于性能的考虑,Linux最终选择的也是Monolithic kernel的模式。

    奥卡姆剃刀原则

    系统设计以及代码编写要遵循奥卡姆剃刀原则:Entities should not be multiplied unnecessarily。一般而言,一个系统的代码量会随着其功能增加而变多。系统的健壮性有时候也需要通过编写异常处理代码来实现。异常考虑越周全,异常处理代码量越大。但是随着代码量的增大,引入Bug的概率也就越大,系统也就越不健壮。从另外一个角度来讲,异常流程处理代码也要考虑健壮性问题,这就形成了无限循环。所以在系统设计和代码编写过程中,奥卡姆剃刀原则要求:一个功能模块如非必要,就不要;一段代码如非必写,就不写。

    奥卡姆剃刀原则和最小可用原则有所区别。最小可用原则主要运用于产品MVP阶段,本文所指的奥卡姆剃刀原则主要指系统设计和代码编写两个方面,这是完全不同的两个概念。MVP包含系统设计和代码编写,但同时,系统设计和代码编写也可以发生在成熟系统的迭代阶段。


    性能恶化模式

    在讲解性能优化模式之前,有必要先探讨一下性能恶化模式,因为:

    1. 很多性能优化模式的目标之一就是避免系统进入性能恶化模式;
    2. 不同性能优化模式可能是避免同一种性能恶化模式;
    3. 同一种性能优化模式可能在不同阶段避免不同的性能恶化模式。
      在此统一阐述性能恶化模式,避免下文重复解释。为了便于读者清晰识别恶化模式和优化模式,恶化模式采用“XXX反模式”的方式进行命名。

    长请求拥塞反模式(High Latency Invocating AntiPattern)

    这是一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增加,最终可能会通过如下三种方式之一恶化系统性能:

    1. 线程数目变多导致线程之间CPU资源使用冲突,反过来进一步延长了单次请求时间;
    2. 线程数量增多以及线程中缓存变大,内存消耗随之剧增,对于基于Java语言的服务而言,又会更频繁地full GC,反过来单次请求时间会变得更长;
    3. 内存使用增多,会使操作系统内存不足,必须使用Swap,可能导致服务彻底崩溃。
      典型恶化流程图如下图:
      长请求拥塞反模式

    长请求拥塞反模式所导致的性能恶化现象非常普遍,所以识别该模式非常重要。典型的场景如下:某复杂业务系统依赖于多个服务,其中某个服务的响应时间变长,随之系统整体响应时间变长,进而出现CPU、内存、Swap报警。系统进入长请求拥塞反模式的典型标识包括:被依赖服务可用性变低、响应时间变长、服务的某段计算逻辑时间变长等。

    多次请求杠杆反模式(Levered Multilayer Invocating AntiPattern)

    客户端一次用户点击行为往往会触发多次服务端请求,这是一次请求杠杆;每个服务端请求进而触发多个更底层服务的请求,这是第二次请求杠杆。每一层请求可能导致一次请求杠杆,请求层级越多,杠杆效应就越大。在多次请求杠杆反模式下运行的分布式系统,处于深层次的服务需要处理大量请求,容易会成为系统瓶颈。与此同时,大量请求也会给网络带来巨大压力,特别是对于单次请求数据量很大的情况,网络可能会成为系统彻底崩溃的导火索。典型恶化流程图如下图:
    多次请求杠杆反模式
    多次请求杠杆所导致的性能恶化现象非常常见,例如:对于联想,京东推荐系统,一个用户列表请求会有多个算法参与,每个算法会召回多个列表单元(商家或者团购),每个列表单元有多种属性和特征,而这些属性和特征数据服务又分布在不同服务和机器上面,所以客户端的一次用户展现可能导致了成千上万的最底层服务调用。对于存在多次请求杠杆反模式的分布式系统,性能恶化与流量之间往往遵循指数曲线关系。这意味着,在平常流量下正常运行服务系统,在流量高峰时通过线性增加机器解决不了可用性问题。所以,识别并避免系统进入多次请求杠杆反模式对于提高系统可用性而言非常关键。

    反复缓存反模式(Recurrent Caching AntiPattern)

    为了降低响应时间,系统往往在本地内存中缓存很多数据。缓存数据越多,命中率就越高,平均响应时间就越快。为了降低平均响应时间,有些开发者会不加限制地缓存各种数据,在正常流量情况下,系统响应时间和吞吐量都有很大改进。但是当流量高峰来临时,系统内存使用开始增多,触发了JVM进行full GC,进而导致大量缓存被释放(因为主流Java内存缓存都采用SoftReference和WeakReference所导致的),而大量请求又使得缓存被迅速填满,这就是反复缓存。反复缓存导致了频繁的full GC,而频繁full GC往往会导致系统性能急剧恶化。典型恶化流程图如下图:
    反复缓存反模式
    反复缓存所导致性能恶化的原因是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据完全缓存的情况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提高流量高峰时可用性,而降低平均响应时间的一个例子)。反复缓存反模式往往发生在流量高峰时候,通过线性增加机器和提高机器内存可以大大减少系统崩溃的概率。


    性能优化模式

    水平分割模式(Horizontal partitioning Pattern)

    原理和动机

    典型的服务端运行流程包含四个环节:接收请求、获取数据、处理数据、返回结果。在一次请求中,获取数据和处理数据往往多次发生。在完全串行运行的系统里,一次请求总响应时间满足如下公式:

    一次请求总耗时=解析请求耗时 + ∑(获取数据耗时+处理数据耗时) + 组装返回结果耗时

    大部分耗时长的服务主要时间都花在中间两个环节,即获取数据和处理数据环节。对于非计算密集性的系统,主要耗时都用在获取数据上面。获取数据主要有三个来源:本地缓存,远程缓存或者数据库,远程服务。三者之中,进行远程数据库访问或远程服务调用相对耗时较长,特别是对于需要进行多次远程调用的系统,串行调用所带来的累加效应会极大地延长单次请求响应时间,这就增大了系统进入长请求拥塞反模式的概率。如果能够对不同的业务请求并行处理,请求总耗时就会大大降低。例如下图中,Client需要对三个服务进行调用,如果采用顺序调用模式,系统的响应时间为18ms,而采用并行调用只需要7ms。
    水平分割模式

    水平分割模式首先将整个请求流程切分为必须相互依赖的多个Stage,而每个Stage包含相互独立的多种业务处理(包括计算和数据获取)。完成切分之后,水平分割模式串行处理多个Stage,但是在Stage内部并行处理。如此,一次请求总耗时等于各个Stage耗时总和,每个Stage所耗时间等于该Stage内部最长的业务处理时间。

    水平分割模式有两个关键优化点:减少Stage数量和降低每个Stage耗时。为了减少Stage数量,需要对一个请求中不同业务之间的依赖关系进行深入分析并进行解耦,将能够并行处理的业务尽可能地放在同一个Stage中,最终将流程分解成无法独立运行的多个Stage。降低单个Stage耗时一般有两种思路:1. 在Stage内部再尝试水平分割(即递归水平分割),2. 对于一些可以放在任意Stage中进行并行处理的流程,将其放在耗时最长的Stage内部进行并行处理,避免耗时较短的Stage被拉长。

    水平分割模式不仅可以降低系统平均响应时间,而且可以降低TP95响应时间(这两者有时候相互矛盾,不可兼得)。通过降低平均响应时间和TP95响应时间,水平分割模式往往能够大幅度提高系统吞吐量以及高峰时期系统可用性,并大大降低系统进入长请求拥塞反模式的概率。

    具体案例

    我们的挑战来自为用户提供高性能的优质个性化列表服务,每一次列表服务请求会有多个算法参与,而每个算法基本上都采用“召回->特征获取->计算”的模式。 在进行性能优化之前,算法之间采用顺序执行的方式。伴随着算法工程师的持续迭代,算法数量越来越多,随之而来的结果就是客户端响应时间越来越长,系统很容易进入长请求拥塞反模式。曾经有一段时间,一旦流量高峰来临,出现整条服务链路的机器CPU、内存报警。在对系统进行分析之后,我们采取了如下三个优化措施,最终使得系统TP95时间降低了一半:

    1. 算法之间并行计算;
    2. 每个算法内部,多次特征获取进行了并行处理;
    3. 在调度线程对工作线程进行调度的时候,耗时最长的线程最先调度,最后处理。

    缺点和优点

    对成熟系统进行水平切割,意味着对原系统的重大重构,工程师必须对业务和系统非常熟悉,所以要谨慎使用。水平切割主要有两方面的难点:

    1. 并行计算将原本单一线程的工作分配给多线程处理,提高了系统的复杂度。而多线程所引入的安全问题让系统变得脆弱。与此同时,多线程程序测试很难,因此重构后系统很难与原系统在业务上保持一致。
    2. 对于一开始就基于单线程处理模式编写的系统,有些流程在逻辑上能够并行处理,但是在代码层次上由于相互引用已经难以分解。所以并行重构意味着对共用代码进行重复撰写,增大系统的整体代码量,违背奥卡姆剃刀原则。
      对于上面提到的第二点,举例如下:A和B是逻辑可以并行处理的两个流程,基于单线程设计的代码,假定处理完A后再处理B。在编写处理B逻辑代码时候,如果B需要的资源已经在处理A的过程中产生,工程师往往会直接使用A所产生的数据,A和B之间因此出现了紧耦合。并行化需要对它们之间的公共代码进行拆解,这往往需要引入新的抽象,更改原数据结构的可见域。

    在如下两种情况,水平切割所带来的好处不明显:

    1. 一个请求中每个处理流程需要获取和缓存的数据量很大,而不同流程之间存在大量共享的数据,但是请求之间数据共享却很少。在这种情况下,流程处理完之后,数据和缓存都会清空。采用顺序处理模式,数据可以被缓存在线程局部存储(ThreadLocal)中而减少重复获取数据的成本;如果采用水平切割的模式,在一次请求中,不同流程会多次获取并缓存的同一类型数据,对于内存原本就很紧张的系统,可能会导致频繁full GC,进入反复缓存反模式。
    2. 某一个处理流程所需时间远远大于其他所有流程所需时间的总和。这种情况下,水平切割不能实质性地降低请求响应时间。

    采用水平切割的模式可以降低系统的平均响应时间和TP95响应时间,以及流量高峰时系统崩溃的概率。虽然进行代码重构比较复杂,但是水平切割模式非常容易理解,只要熟悉系统的业务,识别出可以并行处理的流程,就能够进行水平切割。有时候,即使少量的并行化也可以显著提高整体性能。对于新系统而言,如果存在可预见的性能问题,把水平分割模式作为一个重要的设计理念将会大大地提高系统的可用性、降低系统的重构风险。总的来说,虽然存在一些具体实施的难点,水平分割模式是一个非常有效、容易识别和理解的模式。

    垂直分割模式(Vertical partitioning Pattern)

    原理和动机

    对于移动互联网节奏的公司,新需求往往是一波接一波。基于代码复用原则,工程师们往往会在一个系统实现大量相似却完全不相干的功能。伴随着功能的增强,系统实际上变得越来越脆弱。这种脆弱可能表现在系统响应时间变长、吞吐量降低或者可用性降低。导致系统脆弱原因主要来自两方面的冲突:资源使用冲突和可用性不一致冲突。

    资源使用冲突是导致系统脆弱的一个重要原因。不同业务功能并存于同一个运行系统里面意味着资源共享,同时也意味着资源使用冲突。可能产生冲突的资源包括:CPU、内存、网络、I/O等。例如:一种业务功能,无论其调用量多么小,都有一些内存开销。对于存在大量缓存的业务功能,业务功能数量的增加会极大地提高内存消耗,从而增大系统进入反复缓存反模式的概率。对于CPU密集型业务,当产生冲突的时候,响应时间会变慢,从而增大了系统进入长请求拥塞反模式的可能性。

    不加区别地将不同可用性要求的业务功能放入一个系统里,会导致系统整体可用性变低。当不同业务功能糅合在同一运行系统里面的时候,在运维和机器层面对不同业务的可用性、可靠性进行调配将会变得很困难。但是,在高峰流量导致系统濒临崩溃的时候,最有效的解决手段往往是运维,而最有效手段的失效也就意味着核心业务的可用性降低。

    垂直分割思路就是将系统按照不同的业务功能进行分割,主要有两种分割模式:部署垂直分割和代码垂直分割。部署垂直分割主要是按照可用性要求将系统进行等价分类,不同可用性业务部署在不同机器上,高可用业务单独部署;代码垂直分割就是让不同业务系统不共享代码,彻底解决系统资源使用冲突问题。

    具体案例

    我们的挑战来自于联想,京东推荐系统,联想,京东客户端的多个页面都有推荐列表。虽然不同的推荐产品需求来源不同,但是为了实现快速的接入,基于共用代码库原则,所有的推荐业务共享同一套推荐代码,同一套部署。在一段时间内,我们发现push推荐和首页“猜你喜欢推荐”的资源消耗巨大。特别是在push推荐的高峰时刻,CPU和内存频繁报警,系统不停地full GC,造成联想,京东用户进入客户端时,首页出现大片空白。

    在对系统进行分析之后,得出两个结论:

    1. 首页“猜你喜欢”对用户体验影响更大,应该给予最高可用性保障,而push推荐给予较低可用性保障;
    2. 首页“猜你喜欢”和push推荐都需要很大的本地缓存,有较大的内存使用冲突,并且响应时间都很长,有严重的CPU使用冲突。

    因此我们采取了如下措施,一方面,解决了首页“猜你喜欢”的可用性低问题,减少了未来出现可用性问题的概率,最终将其TP95响应时间降低了40%;另一方面也提高了其他推荐产品的服务可用性和高峰吞吐量。

    1. 将首页“猜你喜欢”推荐进行单独部署,而将push推荐和其他对系统资源要求不高的推荐部署在另一个集群上面;
    2. 对于新承接的推荐业务,新建一套代码,避免影响首页推荐这种最高可用性的业务。

    缺点和优点

    垂直分割主要的缺点主要有两个:

    1. 增加了维护成本。一方面代码库数量增多提高了开发工程师的维护成本,另一方面,部署集群的变多会增加运维工程师的工作量;
    2. 代码不共享所导致的重复编码工作。

    解决重复编码工作问题的一个思路就是为不同的系统提供共享库(Shared Library),但是这种耦合反过来可能导致部署机器中引入未部署业务的开销。所以在共享库中要减少静态代码的初始化开销,并将类似缓存初始化等工作交给上层系统。总的来说,通过共享库的方式引入的开销可以得到控制。但是对于业务密集型的系统,由于业务往往是高度定制化的,共用一套代码库的好处是开发工程师可以采用Copy-on-write的模式进行开发,需要修改的时候随时拷贝并修改。共享库中应该存放不容易变化的代码,避免使用者频繁升级,所以并不适合这种场景。因此,对于业务密集型的系统,分代码所导致的重复编码量是需要权衡的一个因素。

    垂直分割是一个非常简单而又有效的性能优化模式,特别适用于系统已经出现问题而又需要快速解决的场景。部署层次的分割既安全又有效。需要说明的是部署分割和简单意义上的加机器不是一回事,在大部分情况下,即使不增加机器,仅通过部署分割,系统整体吞吐量和可用性都有可能提升。所以就短期而言,这几乎是一个零成本方案。对于代码层次的分割,开发工程师需要在业务承接效率和系统可用性上面做一些折衷考虑。

    恒变分离模式(Runtime 3NF Pattern)

    原理和动机

    基于性能的设计要求变化的数据和不变的数据分开,这一点和基于面向对象的设计原则相悖。在面向对象的设计中,为了便于对一个对象有整体的把握,紧密相关的数据集合往往被组装进一个类,存储在一个数据库表,即使有部分数据冗余(关于面向对象与性能冲突的讨论网上有很多文章,本文不细讲)。很多系统的主要工作是处理变化的数据,如果变化的数据和不变的数据被紧密组装在一起,系统对变化数据的操作将引入额外的开销。而如果易变数据占总数据比例非常小,这种额外开销将会通过杠杆效应恶化系统性能。分离易变和恒定不变的数据在对象创建、内存管理、网络传输等方面都有助于性能提高。

    恒变分离模式的原理非常类似与数据库设计中的第三范式(3NF):第三范式主要解决的是静态存储中重复存储的问题,而恒变分离模式解决的是系统动态运行时候恒定数据重复创建、传输、存储和处理的问题。按照3NF,如果一个数据表的每一记录都依赖于一些非主属性集合,而这些非主属性集合大量重复出现,那么应该考虑对被依赖的非主属性集合定义一个新的实体(构建一个新的数据表),原数据库的记录依赖于新实体的ID。如此一来数据库重复存储数据量将大大降低。类似的,按照恒变分离模式,对于一个实体,如果系统处理的只是这个实体的少量变化属性,应该将不变的属性定义为一个新实体(运行时的另一个类,数据库中的另一个表),原来实体通过ID来引用新实体,那么原有实体在运行系统中的数据传输、创建、网络开销都会大大降低。

    案例分析

    我们的挑战是提供一个高性能、高一致性要求的团购服务(DealService)。系统存在一些多次请求杠杆反模式问题,客户端一次请求会导致几十次DealService读取请求,每次获取上百个团购详情信息,服务端单机需要支持每秒万次级别的吞吐量。基于需求,系统大体框架设计如下:
    恒变分离模式
    每个DealService定期从持久层同步所有发生变化的deal信息,所有的deal信息保存在内存里面。在最初的设计里面,数据库只有一个数据表DealModelTable,程序里面也只有一个实体类DealModel。由于销量、价格、用户评价等信息的频发变化,为了达到高一致性要求,服务系统每分钟需要从数据库同步几万条记录。随着联想,京东团购数量的增多和用户活跃度的增加,系统出现了三个问题:

    1. 团购服务网卡频繁报警,由于这是高性能低延时服务,又导致了大量的客户端超时异常;
    2. 频繁的full GC,这是由于每条数据库记录更新都会导致运行系统里面老的DealModel实体被销毁,新的DealModels实体被创建;
    3. 数据库从库滞后主库,使得服务数据一致性降低,原因是数据库系统写数据量巨大。

    在对系统进行分析之后,我们采用了如下措施,大大降低了网络传输的数据量,缓解了主从数据库同步压力,使得客户端的超时异常从高峰时候的9%降低到了小于0.01%(低于万分之一):

    1. 将DealModelTable中的销量、价格、用户评价等常变的信息单独构建一张数据表VariableDealModel;
    2. 同时在代码中为销量、价格、用户评价等常变数据创建一个单独的类VariableDealModel;
    3. DealService对两张表进行分别同步;
    4. 如果DealModelTable的记录产生了更新,运行系统销毁老的DealModel实体并创建新的DealModel实体;
    5. 如果只是VariableDealModel的记录产生了更新,只对VariableDealModel的属性进行更改。

    缺点和优点

    采用恒变分离模式,主要有三个缺点:

    1. 不符合面向对象的设计原则。原本概念上统一的实体被切分成多个实体,会给开发工程师带来一些理解上的困难,因此增加维护成本。进一步而言,这会增加引入额外Bug的概率(实际上面向对象之所以如此受欢迎的一个重要原因就是容易理解)。
    2. 增加了类不变量(Class invariant)的维护难度。很多情况下,Class invariant是通过语言所提供的封装(Encapsulation)特性来维护的。当一个类变成多个类,Class invariant可能会被破坏。如果必须维护Class invariant,而这种Class invariant又发生在不同实体之间,那么往往是把不变的属性从不变实体移到易变的实体中去。
    3. 一张数据库表变成多张,也会增加维护成本。

    在如下两种场景下,恒变分离模式所带来的好处有限:

    1. 易变数据导致的操作和传输并不频繁,不是系统主要操作;
    2. 易变数据占整体数据的比例很高,杠杆效应不显著,通过恒变分离模式不能根本性地解决系统性能问题。

    总的来说,恒变分离模式非常容易理解,其应用往往需要满足两个条件:易变数据占整体数据比例很低(比例越低,杠杆效应越大)和易变数据所导致的操作又是系统的主要操作。在该场景下,如果系统性能已经出现问题,牺牲一些可维护性就显得物有所值。

    大部分系统都是由多种类型的数据构成,大多数数据类型的都包含易变、少变和不变的属性。盲目地进行恒变分离会导致系统的复杂度指数级别的增加,系统变得很难维护,所以系统设计者必须在高性能和高维护性之间找到一个平衡点。作者的建议是:对于复杂的业务系统,尽量按照面向对象的原则进行设计,只有在性能出现问题的时候才开始考虑恒变分离模式;而对于高性能,业务简单的基础数据服务,恒变分离模式应该是设计之初的一个重要原则。

    数据局部性模式(Locality Pattern)

    原理和动机

    数据局部性模式是多次请求杠杆反模式的针对性解决方案。在大数据和强调个性化服务的时代,一个服务消费几十种不同类型数据的现象非常常见,同时每一种类型的数据服务都有可能需要一个大的集群(多台机器)提供服务。这就意味着客户端的一次请求有可能会导致服务端成千上万次调用操作,很容易使系统进入多次请求杠杆反模式。在具体开发过程中,导致数据服务数量暴增的主要原因有两个:1. 缓存滥用以及缺乏规划,2. 数据量太大以至于无法在一台机器上提供全量数据服务。数据局部性模的核心思想是合理组织数据服务,减少服务调用次数。具体而言,可以从服务端和客户端两个方面进行优化。

    服务端优化方案的手段是对服务进行重新规划。对于数据量太大以至于无法在一台机器上存储全量数据的场景,建议采用Bigtable或类似的解决方案提供数据服务。典型的Bigtable的实现包括Hbase、Google Cloud Bigtable等。实际上数据局部性是Bigtable的一个重要设计原则,其原理是通过Row key和Column key两个主键来对数据进行索引,并确保同一个Row key索引的所有数据都在一台服务器上面。通过这种数据组织方式,一次网络请求可以获取同一个Row key对应的多个Column key索引的数据。缺乏规划也是造成服务数量剧增的一个重要原因。很多通过统计和挖掘出来的特征数据往往是在漫长的时间里由不同team独立产生的。而对于每种类型数据,在其产生之初,由于不确定其实际效果以及生命周期,基于快速接入原则,服务提供者往往会用手头最容易实施的方案,例如采用Redis Cache(不加选择地使用缓存会导致缓存滥用)。数据服务之间缺乏联动以及缺乏标准接入规划流程就会导致数据服务数量膨胀。数据局部性原则对规划的要求,具体而言是指:1. 数据由尽可能少的服务器来提供,2. 经常被一起使用的数据尽可能放在同一台服务器上。

    客户端优化有如下几个手段:

    1. 本地缓存,对于一致性要求不高且缓存命中率较高的数据服务,本地缓存可以减少服务端调用次数;
    2. 批处理,对于单机或者由等价的机器集群提供的数据服务,尽可能采用批处理方式,将多个请求合成在一个请求中;
    3. 客户端Hash,对于需要通过Hash将请求分配到不同数据服务机器的服务,尽量在客户端进行Hash,对于落入同一等价集群的请求采用批处理方式进行调用。

    案例分析

    我们的挑战来自于联想,京东的推荐、个性化列表和个性化搜索服务。这些个性化系统需要获取各种用户、商家和团购信息。信息类型包括基本属性和统计属性。最初,不同属性数据由不同的服务提供,有些是RPC服务,有些是Redis服务,有些是HBase或者数据库,参见下图:
    数据局部性模式1

    通常而言,客户端每个用户请求都会触发多个算法。一方面,每个算法都会召回几十甚至几百个团购或者商家ID,团购和商家基础属性被均匀地分配到几十台Redis里面(如下图),产生了大量的Redis请求,极端情况下,一次客户端请求所触发的团购基础数据请求就超过了上千次;另一方面,用户特征属性信息有十几种,每种属性也由单独的服务提供,服务端网络调用次数暴增。在一段时间里,很多系统都进入了多次请求杠杆反模式,Redis服务器的网卡经常被打死,多次进行扩容,提高线程池线程数量,丝毫没有改善。
    数据局部性模式2
    在对系统进行分析之后,按照数据局部性模式的原则,我们采用了如下手段,彻底解决了系统多次请求杠杆反模式的问题:

    1. 采用大内存服务器存储所有的团购和商家基础信息,每个算法只要一次网络请求就可以获取所有的信息;
    2. 服务端采用多线程方式提供服务,避免了Redis单一线程模式下单个请求慢所带来的连锁效应;
    3. 借鉴类似Bigtable的数据组织方式,将用户的多种特征采用两个维度(用户维度和特征类型)进行索引,确保同一用户的信息只存放在一台机器上面,减少网络调用数量。

    缺点和优点

    数据局部性模式并不适用于系统初级阶段。在初级阶段,最小可用原则往往是主要设计原则之一,出于两方面的考虑:一方面,在初级阶段,很难预测所要提供服务的数据是否有效而且能够长期使用,以及未来的调用量;另一方面,在初级阶段,工程师可能无法预测最终的调用模式,而不同的调用模式会导致数据局部性方案的设计不同。对于已经大量使用的数据服务,采用数据局部性模式进行重构必然要改变老的调用模式,这一方面会引入新的Bug,另一方面也意味着巨大的工作量。需要特别强调的是,数据处于系统的最底层,对于结构复杂而又重要的数据,重构所带来可靠性、一致性和工作量都是需要权衡的因素。对于请求量比较小的数据服务,即使一次请求会触发严重的请求杠杆效应,但是如果原始触发请求数量在可预见的时间内没有明显变多的迹象,进行数据服务重构可能得不偿失。

    数据局部性模式能够解决多次请求杠杆反模式所导致的问题,但它并非大数据的产物,CPU、编译器的设计理念里早就融入了该模式,所以很容易被工程师理解。虽然过度设计在系统初级阶段是一个要尽量避免的事情,但是理解和掌握数据局部性模式对于设计出一个可扩展、可重用的系统有很大帮助。很多成熟的系统因为多次请求杠杆反模式而导致系统频繁崩溃,理解数据局部性模式的原则有助于提高工程师分析解决问题的能力,而在确认了系统存在请求杠杆问题后,数据局部性原则是一件非常锐利的武器。

    避免蚊子大炮模式(Avoiding Over-generalized Solution Pattern)

    原理和动机

    “用大炮打蚊子”本来是大材小用的意思,但是细致想一想,用大炮打蚊子,成功率不高。对于开发工程师而言,一方面为了快速承接业务,按照方案复用原则,总是尽可能地利用现有系统,这使得系统功能越来越强大;另一方面,提高系统的通用性或可重用性也是工程师们在设计系统的一个重要目标。随着这两个过程的相互独立演化,采用通用方案解决特定问题的现象随处可见,形象地说,这就像大炮打蚊子。大炮成本很高,蚊子的数量众多,最终的结局往往是蚊子战胜了大炮。

    “避免蚊子大炮模式”是经济原则在运行时系统的运用,它要求采用最节省资源(CPU、内存等)的方法来解决所面临的问题,资源浪费会带来未来潜在的风险。工程师接到一个需求的时候,需要思考的不仅仅是如何复用现有的系统,减少开发时间,还需要考虑现有系统为处理每个新需求访问所需运行时成本,以及新需求的预期访问量。否则,不加辨别地利用现有系统,不仅仅增大了重构风险,还有可能交叉影响,对现有系统所支持的服务造成影响。从另外一个角度讲,工程师在构建一个可重用系统的时候,要明确其所不能解决和不建议解决的问题,而对于不建议解决的问题,在文档中标明潜在的风险。

    案例分析

    我们的挑战是为移动用户寻找其所在位置附近的商家信息。联想,京东有非常完善的搜索系统,也有资深的搜索工程师,所以一个系统需要查找附近的商家的时候,往往第一方案就是调用搜索服务。但是在联想,京东,太多的服务有基于LBS的查询需求,导致搜索请求量直线上升,这本来不属于搜索的主营业务,在一段时间里面反倒成了搜索的最多请求来源。而搜索引擎在如何从几十万商家里面找最近的几百商家方面的性能非常差,因此一段时间里,搜索服务频繁报警。不仅仅搜索服务可用性受到了影响,所有依赖于LBS的服务的可用性都大大降低。

    在对系统分析之后,我们认为更适合解决最短直线距离的算法应该是k-d tree,在快速实现了基于k-d tree的LBS Search解决方案之后,我们用4台服务器轻松解决了30多台搜索服务器无法解决的问题,平均响应时间从高峰时的100ms降低到300ns,性能取得了几百倍的提高。

    缺点和优点

    避免蚊子大炮模式的问题和数据局部性模式类似,都与最小可用原则相冲突。在系统设计初级阶段,寻求最优方案往往意味着过度设计,整个项目在时间和成本变得不可控,而为每个问题去找最优秀的解决方案是不现实的奢求。最优化原则的要求是全面的,不仅仅要考虑的运行时资源,还需要考虑工程师资源和时间成本等,而这些点往往相互矛盾。在如下情况下,避免蚊子大炮模式所带来的好处有限:在可预见的未来,某个业务请求量非常小,这时候花大量精力去找最优技术方案效果不明显。

    在设计阶段,避免蚊子大炮模式是一个需要工程师去权衡的选择,需要在开发成本和系统运行成本之间保持一个平衡点。当很多功能融入到一个通用系统里而出现性能问题的时候,要拆分出来每一个功能点所造成的影响也不是件轻易的事情,所以采用分开部署而共用代码库的原则可以快速定位问题,然后有针对性地解决“蚊子大炮”问题。总的来说,在设计阶段,避免蚊子大炮模式是工程师们进行分析和设计的一个重要准则,工程师可以暂时不解决潜在的问题,但是一定要清楚潜在的危害。构建可重用系统或方案,一定要明确其所不能解决和不建议解决的问题,避免过度使用。

    实时离线分离模式(Sandbox Pattern)

    原理和动机

    本模式的极端要求是:离线服务永远不要调用实时服务。该模式比较简单也容易理解,但是,严格地讲它不是一种系统设计模式,而是一种管理规范。离线服务和在线服务从可用性、可靠性、一致性的要求上完全不同。原则上,工程师在编写离线服务代码的时候,应该遵循的就是离线服务编程规范,按照在线服务编程规范要求,成本就会大大提高,不符合经济原则;从另外一方面讲,按照离线服务的需求去写在线服务代码,可用性、可靠性、一致性等往往得不到满足。

    具体而言,实时离线分离模式建议如下几种规范:

    1. 如果离线程序需要访问在线服务,应该给离线程序单独部署一套服务;
    2. 类似于MapReduce的云端多进程离线程序禁止直接访问在线服务;
    3. 分布式系统永远不要直接写传统的DBMS。

    案例分析

    因为违反实时离线分离模式而导致的事故非常常见。有一次,因为一个离线程序频繁的向Tair集群写数据,每一次写10M数据,使得整个Tair集群宕机。另一次,因为Storm系统直接写MySQL数据库导致数据库连接数耗尽,从而使在线系统无法连接数据库。

    缺点和优点

    为了实现实时在线分离,可能需要为在线环境和离线环境单独部署,维护多套环境所带来运维成本是工程师需要考虑的问题。另一方面,在线环境的数据在离线环境中可能很难获取,这也是很多离线系统直接访问在线系统的原因。但是,遵从实时离线分离模式是一个非常重要的安全管理准则,任何违背这个准则的行为都意味着系统性安全漏洞,都会增大线上故障概率。

    降级模式(Degradation Pattern)

    原理和动机

    降级模式是系统性能保障的最后一道防线。理论上讲,不存在绝对没有漏洞的系统,或者说,最好的安全措施就是为处于崩溃状态的系统提供预案。从系统性能优化的角度来讲,不管系统设计地多么完善,总会有一些意料之外的情况会导致系统性能恶化,最终可能导致崩溃,所以对于要求高可用性的服务,在系统设计之初,就必须做好降级设计。根据作者的经验,良好的降级方案应该包含如下措施:

    1. 在设计阶段,确定系统的开始恶化数值指标(例如:响应时间,内存使用量);
    2. 当系统开始恶化时,需要第一时间报警;
    3. 在收到报警后,或者人工手动控制系统进入降级状态,或者编写一个智能程序让系统自动降级;
    4. 区分系统所依赖服务的必要性,一般分为:必要服务和可选服务。必要服务在降级状态下需要提供一个快速返回结果的权宜方案(缓存是常见的一种方案),而对于可选服务,在降级时系统果断不调用;
    5. 在系统远离恶化情况时,需要人工恢复,或者智能程序自动升级。

    典型的降级策略有三种:流量降级、效果降级和功能性降级。流量降级是指当通过主动拒绝处理部分流量的方式让系统正常服务未降级的流量,这会造成部分用户服务不可用;效果降级表现为服务质量的降级,即在流量高峰时期用相对低质量、低延时的服务来替换高质量、高延时的服务,保障所有用户的服务可用性;功能性降级也表现为服务质量的降级,指的是通过减少功能的方式来提高用户的服务可用性。效果降级和功能性降级比较接近,效果降级强调的是主功能服务质量的下降,功能性降级更多强调的是辅助性功能的缺失。做一个类比如下:计划将100个工程师从北京送到罗威度假,但是预算不够。采用流量降级策略,只有50工程师做头等舱去了罗威度假,其余工程师继续编写程序(这可不好);效果降级策略下,100个工程师都坐经济舱去罗威;采用功能性降级策略,100个工程师都坐头等舱去罗威,但是飞机上不提供食品和饮料。

    案例分析

    我们的系统大量使用了智能降级程序。在系统恶化的时候,智能降级程序自动降级部分流量,当系统恢复的时候,智能降级程序自动升级为正常状态。在采用智能降级程序之前,因为系统降级问题,整体系统不可用的情况偶尔发生。采用智能降级程序之后,基本上没有因为性能问题而导致的系统整体不可用。我们的智能降级程序的主要判定策略是服务响应时间,如果出现大量长时间的响应异常或超时异常,系统就会走降级流程,如果异常数量变少,系统就会自动恢复。

    缺点和优点

    为了使系统具备降级功能,需要撰写大量的代码,而降级代码往往比正常业务代码更难写,更容易出错,所以并不符合奥卡姆剃刀原则。在确定使用降级模式的前提下,工程师需要权衡这三种降级策略的利弊。大多数面向C端的系统倾向于采用效果降级和功能性降级策略,但是有些功能性模块(比如下单功能)是不能进行效果和功能性降级的,只能采用流量降级策略。对于不能接受降级后果的系统,必须要通过其他方式来提高系统的可用性。

    总的来说,降级模式是一种设计安全准则,任何高可用性要求的服务,必须要按照降级模式的准则去设计。对于违背这条设计原则的系统,或早或晚,系统总会因为某些问题导致崩溃而降低可用性。不过,降级模式并非不需要成本,也不符合最小可用原则,所以对于处于MVP阶段的系统,或者对于可用性要求不高的系统,降级模式并非必须采纳的原则。

    其他性能优化建议

    对于无法采用系统性的模式方式讲解的性能优化手段,作者也给出一些总结性的建议:

    1. 删除无用代码有时候可以解决性能问题,例如:有些代码已经不再被调用但是可能被初始化,甚至占有大量内存;有些代码虽然在调用但是对于业务而言已经无用,这种调用占用CPU资源。
    2. 避免跨机房调用,跨机房调用经常成为系统的性能瓶颈,特别是那些伪batch调用(在使用者看起来是一次性调用,但是内部实现采用的是顺序单个调用模式)对系统性能影响往往非常巨大

    总结

    Christopher Alexander曾说过:"Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice" 。 尽管Christopher Alexander指的是建筑模式,软件设计模式适用,基于同样的原因,性能优化模式也适用。每个性能优化模式描述的都是工程师们日常工作中经常出现的问题,一个性能优化模式可以解决确定场景下的某一类型的问题。所以要理解一个性能优化模式不仅仅要了解性能模式的所能解决的问题以及解决手段,还需要清楚该问题所发生的场景和需要付出的代价。

    最后,本文所描述的性能优化模式只是作者的工作经验总结,都是为了解决由以下三种情况所造成的性能问题:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据,但是这些远非该领域里面的所有模式。对于文章中提到的其他性能优化建议,以及现在和将来可能碰到的性能问题,作者还会不断抽象,在未来总结出更多的模式。性能问题涉及领域非常广泛,而模式是一个非常好的讲解性能问题以及解决方案的方式,作者有理由相信,无论是在作者所从事的工作领域里面还是在其他的领域里面,新的性能优化模式会不断涌现。希望通过本文的讲述,对碰到同样问题的工程师们有所帮助,同时也抛砖引玉,期待出现更多的基于模式方式讲解性能优化的文章。

    参考文献:
    [1] Chang F, Dean J, Ghemawat S, et al. Bigtable: A Distributed Storage System for Structured Data
    [2] Gamma E, Helm R, Johnson R, et al. Design Patterns-Elements of Reusable Object-Oriented Software. Machinery Industry, 2003
    [3] Motlik F. Monolithic Core Versus Full Microservice Architecture
    [4] Monolithic Design WikiWikiWeb.
    [5] Bovet D P, Cesati M. Understanding the Linux Kernel. 3rd ed. O'Reilly Media, 2005.
    [6] Bloch J. Effective Java. 2nd ed. Addison-Wesley, 2008.
    [7] Alexander C, Ishikawa S, Silverstein M. A Pattern Language: Towns, Buildings, Construction. Oxford University Press, 1977.

    展开全文
  • 在main方法里面的测试demo,可以看到通过不同的type类型,可以实现不同的策略,这就是策略模式主要思想。 在Context里面定义了两种写法: 第一种是维护了一个strategies的Map容器。用这种方式就需要判断每种策略...

    有情怀,有干货,微信搜索【三太子敖丙】关注这个有一点点东西的程序员。

    本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

    最近有一个学妹在跟我沟通如何有效的去避免代码中一长串的if else判断或者switch条件判断?针对更多的回答就是合理的去使用设计来规避这个问题。

    在设计模式中,可以使用工厂模式或者策略模式来处理这类问题,之前已经分享了工厂模式,感兴趣的同学可以去复习一下。

    设计模式系列往期文章:

    那么工厂模式和策略模式有什么区别呢?

    • 工厂模式是属于创建型设计模式,主要用来针对不同类型创建不同的对象,达到解偶类对象。
    • 策略模式是属于行为型设计模式,主要是针对不同的策略做出对应行为,达到行为解偶

    本次就来具体聊聊策略模式它是如何做到行为解耦

    大纲

    定义

    什么是策略模式?它的原理实现是怎么样的?

    定义一系列算法,封装每个算法,并使他们可以互换,不同的策略可以让算法独立于使用它们的客户而变化。 以上定义来自设计模式之美

    感觉有点抽象?那就来看一张结构图吧

    • Strategy(抽象策略):抽象策略类,并且定义策略执行入口
    • ConcreteStrategy(具体策略):实现抽象策略,实现algorithm方法
    • Context(环境):运行特定的策略类。

    这么看结构其实还是不复杂的,而且跟状态模式类似。

    那么这个代码怎么实现?

    举个例子,汽车大家肯定都不陌生,愿大家早日完成汽车梦,汽车的不同档(concreteStrategy)就好比不同的策略,驾驶者选择几档则汽车按几档的速度前进,整个选择权在驾驶者(context)手中。

    public interface GearStrategy {
    
        // 定义策略执行方法
        void algorithm(String param);
    }
    

    首先还是先定义抽象策略

    这里是用接口的形式,还有一种方式可以用抽象方法abstract来写也是一样的。具体就看大家自己选择了。

    public abstract class GearStrategyAbstract {
     // 定义策略执行方法
     abstract void algorithm(String param);
    }
    
    public class GearStrategyOne implements GearStrategy {
    
        @Override
        public void algorithm(String param) {
            System.out.println("当前档位" + param);
        }
    }
    

    其次定义具体档位策略,实现algorithm方法。

    public class Context {
    		// 缓存所有的策略,当前是无状态的,可以共享策略类对象
        private static final Map<String, GearStrategy> strategies = new HashMap<>();
    
        // 第一种写法
        static {
            strategies.put("one", new GearStrategyOne());
        }
    
        public static GearStrategy getStrategy(String type) {
            if (type == null || type.isEmpty()) {
                throw new IllegalArgumentException("type should not be empty.");
            }
            return strategies.get(type);
        }
    
        // 第二种写法
        public static GearStrategy getStrategySecond(String type) {
            if (type == null || type.isEmpty()) {
                throw new IllegalArgumentException("type should not be empty.");
            }
            if (type.equals("one")) {
                return new GearStrategyOne();
            }
            return null;
        }
    
    
        public static void main(String[] args) {
            // 测试结果
            GearStrategy strategyOne = Context.getStrategy("one");
            strategyOne.algorithm("1档");
             // 结果:当前档位1档
            GearStrategy strategyTwo = Context.getStrategySecond("one");
            strategyTwo.algorithm("1档");
            // 结果:当前档位1档
        }
    
    }
    

    最后就是实现运行时环境(Context),你可以定义成StrategyFactory,但都是一个意思。

    在main方法里面的测试demo,可以看到通过不同的type类型,可以实现不同的策略,这就是策略模式主要思想。

    在Context里面定义了两种写法:

    • 第一种是维护了一个strategies的Map容器。用这种方式就需要判断每种策略是否可以共享使用,它只是作为算法的实现。
    • 第二种是直接通过有状态的类,每次根据类型new一个新的策略类对象。这个就需要根据实际业务场景去做的判断。

    框架的应用

    策略模式在框架中也在一个很常见的地方体现出来了,而且大家肯定都有使用过。

    那就是JDK中的线程池ThreadPoolExecutor

    首先都是类似于这样定义一个线程池,里面实现线程池的异常策略。

    这个线程池的异常策略就是用的策略模式的思想。

    在源码中有RejectedExecutionHandler这个抽象异常策略接口,同时它也有四种拒绝策略。关系图如下:

    这就是在框架中的体现了,根据自己的业务场景,合理的选择线程池的异常策略。

    业务改造举例

    在真实的业务场景中策略模式也还是应用很多的。

    在社交电商中分享商品是一个很重要的环节,假设现在要我们实现一个分享图片功能,比如当前有 单商品、多商品、下单、会场、邀请、小程序链接等等多种分享场景。

    针对上线这个流程图先用if else语句做一个普通业务代码判断,就像下面的这中方式:

    public class SingleItemShare {
        // 单商品
        public void algorithm(String param) {
            System.out.println("当前分享图片是" + param);
        }
    }
    public class MultiItemShare {
        // 多商品
        public void algorithm(String param) {
            System.out.println("当前分享图片是" + param);
        }
    }
    public class OrderItemShare {
        // 下单
        public void algorithm(String param) {
            System.out.println("当前分享图片是" + param);
        }
    }
    public class ShareFactory {
    
        public static void main(String[] args) throws Exception {
            Integer shareType = 1;
           // 测试业务逻辑
            if (shareType.equals(ShareType.SINGLE.getCode())) {
                SingleItemShare singleItemShare = new SingleItemShare();
                singleItemShare.algorithm("单商品");
            } else if (shareType.equals(ShareType.MULTI.getCode())) {
                MultiItemShare multiItemShare = new MultiItemShare();
                multiItemShare.algorithm("多商品");
            } else if (shareType.equals(ShareType.ORDER.getCode())) {
                OrderItemShare orderItemShare = new OrderItemShare();
                orderItemShare.algorithm("下单");
            } else {
                throw new Exception("未知分享类型");
            }
            // .....省略更多分享场景
        }
    
        enum ShareType {
            SINGLE(1, "单商品"),
            MULTI(2, "多商品"),
            ORDER(3, "下单");
            /**
             * 场景对应的编码
             */
            private Integer code;
            /**
             * 业务场景描述
             */
            private String desc;
            ShareType(Integer code, String desc) {
                this.code = code;
                this.desc = desc;
            }
            public Integer getCode() {
                return code;
            }
           // 省略 get set 方法
        }
    }
    

    这里大家可以看到每新加一种分享类型,就需要加一次if else 判断,当如果有十几种场景的时候那代码整体就会非常的长,看起来给人的感觉也不是很舒服。

    接下来就看看如何用策略模式进行重构:

    public interface ShareStrategy {
        // 定义分享策略执行方法
        void shareAlgorithm(String param);
    }
    
    public class OrderItemShare implements ShareStrategy {
        @Override
        public void shareAlgorithm(String param) {
            System.out.println("当前分享图片是" + param);
        }
    }
    
    // 省略 MultiItemShare以及SingleItemShare策略
    
    // 分享工厂
    public class ShareFactory {
    		// 定义策略枚举
        enum ShareType {
            SINGLE("single", "单商品"),
            MULTI("multi", "多商品"),
            ORDER("order", "下单");
            // 场景对应的编码
            private String code;
           
            // 业务场景描述
            private String desc;
            ShareType(String code, String desc) {
                this.code = code;
                this.desc = desc;
            }
            public String getCode() {
                return code;
            }
           // 省略 get set 方法
        }
    		// 定义策略map缓存
        private static final Map<String, ShareStrategy> shareStrategies = new       HashMap<>();
        static {
            shareStrategies.put("order", new OrderItemShare());
            shareStrategies.put("single", new SingleItemShare());
            shareStrategies.put("multi", new MultiItemShare());
        }
        // 获取指定策略
        public static ShareStrategy getShareStrategy(String type) {
            if (type == null || type.isEmpty()) {
                throw new IllegalArgumentException("type should not be empty.");
            }
            return shareStrategies.get(type);
        }
    	
        public static void main(String[] args) {
            // 测试demo
            String shareType = "order";
            ShareStrategy shareStrategy = ShareFactory.getShareStrategy(shareType);
            shareStrategy.shareAlgorithm("order");
            // 输出结果:当前分享图片是order
        }
    }
    
    

    这里策略模式就已经改造完了。在client请求端,根本看不到那么多的if else判断,只需要传入对应的策略方式即可,这里我们维护了一个策略缓存map,在直接调用的ShareFactory获取策略的时候就直接是从换种获取策略类对象。

    这就已经达到了行为解偶的思想。同时也避免了长串的if else 判断。

    优点:

    • 算法策略可以自由实现切换
    • 扩展性好,加一个策略,只需要增加一个类

    缺点:

    • 策略类数量多
    • 需要维护一个策略枚举,让别人知道你当前具有哪些策略

    总结

    以上就讲完了策略模式,整体看上去其实还是比较简单的,还是那句话学习设计模式我们还是要学习每种设计模式的思想,任何一种设计模式存在即合理。当然也不要因为设计模式而设计代码,那样反而得不偿失。


    敖丙把自己的面试文章整理成了一本电子书,共 1630页!

    干货满满,字字精髓。目录如下,还有我复习时总结的面试题以及简历模板,现在免费送给大家。

    链接:https://pan.baidu.com/s/1ZQEKJBgtYle3v-1LimcSwg 密码:wjk6

    我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞收藏评论,我们下期见!


    文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

    展开全文
  • 面向中文专利信息的关系数据库检索优化策略研究应用目 录1 引言... 32 中文专利信息检索优化概述... 42.1 中文信息检索的概念... 42.2 中文信息检索的现状... 42.3 中文专利信息检索概述... 52.3.1 中文专利...
  • OpenCV常见的优化方法和技巧总结

    万次阅读 多人点赞 2018-02-24 15:53:36
    OpenCV常见的优化方法和技巧总结 【尊重原创,转载请注明出处】http://blog.csdn.net/guyuealian/article/details/78540206 目录 OpenCV常见的优化方法和技巧总结 一、OpenCV常见的优化方法总结 1.1 cv::...
  • 前端性能优化方法总结

    万次阅读 多人点赞 2017-03-20 10:25:37
    前端性能优化(一) 前端是庞大的,包括 HTML、 CSS、 Javascript、Image 、Flash等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。那么,前端优化的目的是什么 ?  1. 从用户角度...
  • 对象-关系元数据映射模式是用来描述数据库中域是如何对应到内存对象中的域的,它包括元数据映射、查询对象、资源库三种模式。 元数据映射:在元数据中保持对象-关系映射的详细信息 该模式主要的决策是如何根据...
  • 实体关系抽取任务方法及SOTA模型总结

    千次阅读 多人点赞 2020-05-31 21:02:08
    对于实体关系抽取任务,最容易想到的方法就是先抽取句子中的实体,然后在对实体对进行关系分类,从而找出spo三元组,这种思想被称作管道模型(Pipeline)。管道模型把实体关系抽取分成了两个子任务,实体识别和关系...
  • 无约束最优化方法学习笔记

    万次阅读 多人点赞 2015-08-11 23:12:24
    这一段时间学习优化理论中的一些经典方法,写下一些要点和体会,帮助自己和有需要的人理解最优化方法。1.基础知识首先来看无约束最优化问题: minf(x)\begin{equation} \min f(x) \end{equation} 其中函数 f:Rn...
  • 图像模式识别的方法

    万次阅读 2015-07-05 11:53:22
    图像模式识别的方法很多,从图像模式识别提取的特征对象来看,图像识别方法可分为以下几种:基于形状特征的识别技术、基于色彩特征的识别技术以及基于纹理特征的识别技术。其中,基于形状特征的识别方法,其关键是...
  • 关系数据库查询优化分析

    千次阅读 2008-01-07 12:52:00
    因此人们往往通过对查询语句进行优化来提高整个数据库的性能。举例来说,如果一个数据库表信息积累到上百万甚至是上千万条记录,全表扫描一次需要数十分钟,甚至数小时;但如果采用比全表扫描更好的查询策略,往往...
  • 设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人...
  • 本分开始之前。咱先提出来几个疑问: 接口有什么用途? 面向接口编程的好处? 它和抽象类有什么区别? 能不能用抽象类代替接口呢?...6.面向接口编程(Demo终极优化,扩展性超强,抽象工厂模式) Demo地址(github) 1
  • 性能优化模式-美团技术团队

    千次阅读 2015-12-13 20:28:03
    一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高...
  • 因此研究员又提出来更先进的方法,其中包括比较有名的Gap Statistics方法,Gap Statistics方法的优点是不需要肉眼判断,只需要找到最大的Gap Statistics所对应的K即可,因此该方法也适用于批量优化作业。 (3)...
  • 性能优化方法论建设

    千次阅读 2018-04-12 10:27:08
    计算机的核心就是由这五大部分构成,网络io的输入、输出主要是网卡、带宽等资源,这是已经确定了的,由计算机底层优化,能优化的空间不大(也可以通过改变tcp缓冲区的大小来特地优化某些场景的性能,这个以后再说)...
  • 模式识别-经典聚类方法

    千次阅读 2015-01-15 22:16:10
    基本思想:假设有N个样本{X1,X2...Xn},要求按距离阈值T分类到聚类中心{Z1,Z2,Z3…} 步骤: step1:将第一个样本作为第一类的中心,Z1=X1. step2:将剩下的样本计算||Xi-Z1 ||,若大于阈值T,则Xi作为新的一类的中心...
  • 一、设计模式(Design Patterns)    设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性...
  • Unity大场景数据加载及优化方案

    万次阅读 多人点赞 2018-08-07 16:29:55
    前段时间,有几个虚拟仿真公司跟我请教关于大地形的加载优化问题,它们使用的引擎都是自己研发的,引擎对于开发者来说,大同小异,它们的基本构造是一样的,关键是在于解决问题的方法,正是基于这个前提写了这个课程...
  • 模式识别方法总结

    千次阅读 2016-03-21 14:33:21
    这学期选了门模式识别的课。发现最常见的一种情况就是,书上写的老师ppt上写的都看不懂,然后绕了一大圈去自己查资料理解,回头看看发现,Ah-ha,原来本质的原理那么简单,自己一开始只不过被那些看似formidable的...
  • 目录 一、概述 二、7个设计原则 1、单一职责原则 ( SRP ) 2、开闭原则 ( OCP ) 3、里氏替换原则 ( LSP ) ...4、依赖倒置原则 ( DIP ) ...2.工厂方法模式 3.抽象工厂模式 4.建造者模式 5.原型模...
  • 概要 Facade模式所涵盖的范围虽然可大可小,但更多的还是被当作一种架构型的模式来考虑,所以它更多的说明的是一种思想,而不是一种实现方式。...而Facade模式就是一种帮助优化模块或类间复杂关系的一种思想。那
  • 23种设计模式及java实现

    千次阅读 2016-05-27 14:02:07
    一、设计模式的分类 总体来说设计模式分为三大类: ...行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
  • 管线优化的一种大致思路是,先将渲染速度最大化,然后使得非瓶颈部分和瓶颈部分消耗同样多的时间(如上文所述,这里的思想是,既然要等,不等白不等,不妨多给速度快的部分分配更多工作量,来达到更好的画面效果)。...
  • 这一次谈谈以小米为代表的互联网硬件新势力,其背后的估值模式,这是一切颠覆变革开始的源头,也以此作为观察雷军方法论的终结篇。 不久前,受邀前往华为终端参观,发现这家手机制造巨头正在发生一些静悄悄的变化,...
  • 简单工厂模式思想

    千次阅读 2016-04-09 22:46:57
     简单工厂模式(Simple Factory Pattern)属于类的创新型模式,又叫静态工厂方法模式(Static FactoryMethod Pattern),是通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。...
  • 典型的神经网络模型主要分3大类:以感知机、BP反向传播模型、函数型网络为代表的,用于分类、预测和模式识别的前馈式神经网络模型;以Hopfield的离散模型和连续模型为代表的,分别用于联想记忆和优化计算的反馈式神经...
  • 主要敏捷开发方法

    千次阅读 2009-09-24 16:59:00
    主要的敏捷方法极限编程(XP) 极限编程(XP)的主要目的是降低需求变化的成本。它引入一系列优秀的软件开发方法,并将它们发挥到极致。比如,为了能及时得到用户的反馈,XP要求客户代表每天都必须与开发团队在一起...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 123,594
精华内容 49,437
关键字:

关系模式优化主要思想及方法