CAP

对于任何分布式系统而言,我们只能保证下面三种属性中的两个:

  • 一致性
  • 可用性
  • 分区容错性

一致性

每一个读请求都能获取最新的数据。

可用性

任何一个请求都有一个相应,哪怕不是最新的数据。

分区容错性

即使存在网络故障,系统仍然能够正常运行。

举例

分布式数据库中由于网络故障(分区),导致一些服务器无法和其他服务器通信。

如果强调一致性,那么一些用户会收到报错,因为不是所有的服务器都有最新的数据。

如果强调可用性,所有的请求都会有响应,那一部分用户获取到的数据不是最新数据。

PACELC 理论

CAP 理论太过狭隘,PACELC 在 CAP 理论上额外引入了延迟概念。

如果网络分区发生 P,那么你必须在 A 和 C 中选择,如果没有发生 E,你必须在延迟 L 和一致性 C 间选择。

BASE 理论

在 NoSql 数据库中,强一致性通常情况下不是可行的,特别是在高性能应用场景下。

BASE = Basically Available + Soft State + Eventual Consistency。

Basically Avaiable:系统在大部分时间内可用,即使系统的一些部分崩溃。

Soft State:数据可以在一段时间内暂时不一致,不需要通过严格的同步机制保证所有节点强制一致,最终会通过系统自身的逻辑自动收敛到一致状态。

Eventual Consistency:不确保所有的事务都立即被所有的用户看到。但是最终,所有的读都会读到最新的值。

SOLID

Single Responsibility

一个类只有一个修改的原因。

例如下面将员工数据管理和员工通知分离开来。

为什么之前的逻辑违反了单一职责原则,因为修改 Employee 类的原因可能有两个:

  1. 修改计算薪水的逻辑 (数据管理职责)。
  2. 修改发送 Email 的逻辑 (消息通知职责)。

拆分后,修改这两个类只有一个原因了。

关于单一职责原则,有个简单的判断方法:

如果删除/修改其中一个方法,是否影响其他方法的核心目的?

如果不影响,说明大概率是多个职责。

一个 Calculator 类,包含 add(加)、subtract(减)、multiply(乘)、divide(除)4 个方法:

  • 所有方法的核心目的都是 “数学计算”,修改任何一个方法的原因(如优化除法的精度、修复乘法的溢出问题)都属于 “计算逻辑的调整”,因此符合单一职责;
  • 但如果 Calculator 类再加一个 printResult(打印计算结果到文件)的方法: printResult 的核心目的是 “文件 IO”,修改它的原因(如改变文件格式、保存路径)与 “数学计算” 无关,此时类就违反了单一职责。

Open Close Principle

类应该对扩展开放,对修改关闭。

  • 对扩展开放:新增功能时,只需新增实现类(实现接口/继承抽象类),而无需修改抽象层或者已有实现。
  • 对修改关闭:抽象层和已有实现类一旦确定,就无需修改(除非抽象层设计有误),避免引入风险。

需要指出,对修改关闭指的是修改功能的核心逻辑,不是完全不能碰旧代码。

  • 如果旧代码存在 bug,此时修改 bug 属于修复错误,而不是变更功能,不违反开闭规则。
  • 如果抽象层设计不合理,例如确实了某个参数,那说明这个是设计缺陷,应该尽量避免。

最佳实践:将核心逻辑抽象成接口,后续所有的定制逻辑都实现这个接口,所有对具体实现的依赖都转换为对接口的依赖。

Liskov Substitution Principle

父类的对象可以被替换为子类的对象而不影响程序正确性。

子类在继承父类时,不能破坏父类原有的行为约定(如方法的输入输出、异常抛出、业务逻辑语义等),必须严格遵循父类定义的 “契约”。

LSP 的本质是约束继承关系的合理性,确保子类是对父类的 “扩展” 而非 “篡改”,从而保证系统的稳定性 —— 无论使用父类还是子类,程序的核心逻辑都能正确运行,避免因子类替换导致的隐性 bug。

经典反例:“正方形不是长方形” 假设父类 Rectangle 有 setWidth(int w) 和 setHeight(int h) 方法,核心语义是 “宽和高可独立修改”;子类 Square 继承 Rectangle,但正方形的 “宽 = 高”,因此重写 setWidth 和 setHeight 为 “修改宽时同步修改高,修改高时同步修改宽”。

LSP 是 OCP 的 “基础保障”—— 只有当子类可以无缝替换父类时,才能通过 “新增子类” 扩展系统功能(符合 OCP 的 “对扩展开放”),而无需修改依赖父类的高层代码(符合 OCP 的 “对修改关闭”)。

支付系统的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 父类:支付抽象类(定义契约)
abstract class Payment {
    // 契约1:发起支付,返回支付订单号(输入:金额;返回:订单号;抛支付异常)
    public abstract String pay(double amount) throws PaymentException;
    // 契约2:查询支付状态(输入:订单号;返回:支付状态)
    public abstract PaymentStatus queryStatus(String orderNo);
}

// 子类1:微信支付(扩展父类,不破坏契约)
class WechatPayment extends Payment {
    @Override
    public String pay(double amount) throws PaymentException {
        if (amount <= 0) {
            throw new PaymentException("金额必须大于0"); // 符合父类异常约定
        }
        // 微信支付的具体逻辑(如调用微信支付接口)
        String orderNo = "WECHAT_" + System.currentTimeMillis();
        System.out.println("微信支付:" + amount + "元,订单号:" + orderNo);
        return orderNo; // 返回订单号,符合父类返回值约定
    }

    @Override
    public PaymentStatus queryStatus(String orderNo) {
        // 微信支付查询逻辑
        return PaymentStatus.SUCCESS; // 返回支付状态,符合父类语义
    }
}

// 子类2:支付宝支付(扩展父类,不破坏契约)
class AlipayPayment extends Payment {
    @Override
    public String pay(double amount) throws PaymentException {
        if (amount <= 0) {
            throw new PaymentException("金额必须大于0"); // 符合父类异常约定
        }
        // 支付宝支付的具体逻辑(如调用支付宝接口)
        String orderNo = "ALIPAY_" + System.currentTimeMillis();
        System.out.println("支付宝支付:" + amount + "元,订单号:" + orderNo);
        return orderNo; // 符合父类返回值约定
    }

    @Override
    public PaymentStatus queryStatus(String orderNo) {
        // 支付宝查询逻辑
        return PaymentStatus.SUCCESS; // 符合父类语义
    }
}

// 测试:子类替换父类,程序逻辑正确
public class PaymentTest {
    public static void main(String[] args) throws PaymentException {
        // 用微信支付替换父类
        Payment payment1 = new WechatPayment();
        String orderNo1 = payment1.pay(100);
        System.out.println("微信支付状态:" + payment1.queryStatus(orderNo1));

        // 用支付宝支付替换父类
        Payment payment2 = new AlipayPayment();
        String orderNo2 = payment2.pay(200);
        System.out.println("支付宝支付状态:" + payment2.queryStatus(orderNo2));
    }
}

无论是 WechatPayment 还是 AlipayPayment,都严格遵循父类 Payment 的契约(输入输出、异常、语义),替换父类后程序逻辑完全正确,符合 LSP。

Interface Segregation Principle

客户端不应该依赖他们不需要的接口。

通俗来说,ISP 主张将 “庞大臃肿的接口” 拆分为 “多个小型、专一的接口”,每个接口只包含特定客户端(类或模块)所需的方法,避免客户端被迫实现它用不到的方法,从而降低接口与客户端之间的耦合度,提升系统的灵活性和可维护性。

Dependency Inversion Principle

  1. 高层模块不能依赖低层模块,两者都应该依赖抽象。
  2. 抽象不能依赖具体实现,具体实现应该依赖抽象。
  • 低层模块:负责具体的 “实现细节”,比如数据库操作(MySQL/MongoDB 读写)、文件 IO、第三方接口调用(短信发送、支付对接)等;
  • 高层模块:负责 “业务逻辑整合”,依赖低层模块的功能完成复杂业务,比如电商的 “订单创建流程”(依赖 “订单存储”“金额计算”“短信通知” 等低层功能)、政务系统的 “用户审批流程”(依赖 “用户信息查询”“审批记录存储” 等低层功能)。

传统 “高层依赖低层” 的设计会导致两大核心问题,而 DIP 正是为了规避这些问题:

  • 低层模块变更时,高层模块必须跟着改:比如 “订单存储” 从 MySQL 迁移到 MongoDB,如果高层模块直接依赖 MySQLOrderRepository(具体实现),则所有调用该类的高层代码都要修改(替换为 MongoOrderRepository),牵一发而动全身;
  • 难以替换低层实现,扩展性差:比如 “短信通知” 需要从 “阿里云短信” 切换到 “腾讯云短信”,如果高层代码直接依赖 AliSmsSender(具体实现),则必须修改所有调用短信的逻辑,无法灵活切换。

核心步骤:先定义抽象接口,再让低层实现抽象,最后让高层依赖抽象(而非具体实现)。

KISS