架构设计
难度等级:⭐⭐⭐⭐ 前置知识:Web 后端开发、数据存储 后续衔接:微服务架构
学习路径
- 入门阶段:掌握常用设计模式,理解分层架构
- 进阶阶段:能够应用架构模式解决实际问题
- 精通阶段:能够进行系统级架构设计和技术决策
一、设计模式
设计模式是软件工程中反复出现的问题的通用解决方案。它们不是可以直接编译的代码,而是经过验证的设计经验的总结。掌握设计模式能够帮助开发者写出更易扩展、更易维护的代码。
1.1 创建型模式
创建型模式关注对象的创建过程,将对象的创建与使用分离,降低系统耦合度。
工厂方法模式(Factory Method)
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类中进行。
核心思想:定义一个创建对象的接口,让实现类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
典型应用场景:日志框架中,根据配置创建不同格式的日志输出器;支付系统中,根据支付方式创建对应的支付处理器。
// 产品接口
public interface Logger {
void log(String message);
}
// 具体产品
public class JsonLogger implements Logger {
public void log(String message) {
System.out.println("{\"level\":\"INFO\",\"msg\":\"" + message + "\"}");
}
}
public class TextLogger implements Logger {
public void log(String message) {
System.out.println("[INFO] " + message);
}
}
// 工厂接口
public interface LoggerFactory {
Logger createLogger();
}
// 具体工厂
public class JsonLoggerFactory implements LoggerFactory {
public Logger createLogger() {
return new JsonLogger();
}
}
public class TextLoggerFactory implements LoggerFactory {
public Logger createLogger() {
return new TextLogger();
}
}
使用工厂方法模式的好处是,当需要新增一种日志格式时,只需新增对应的产品和工厂类,无需修改现有代码,符合开闭原则。
抽象工厂模式(Abstract Factory)
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。与工厂方法的区别在于,抽象工厂创建的是产品族,而非单一产品。
典型场景:UI 组件库中,需要同时创建按钮、文本框、下拉框等多种组件,且不同主题(Windows、Mac、Linux)下的组件风格不同。
public interface Button { void render(); }
public interface TextBox { void display(); }
// Windows 产品族
public class WindowsButton implements Button {
public void render() { System.out.println("Windows style button"); }
}
public class WindowsTextBox implements TextBox {
public void display() { System.out.println("Windows style textbox"); }
}
// Mac 产品族
public class MacButton implements Button {
public void render() { System.out.println("Mac style button"); }
}
public class MacTextBox implements TextBox {
public void display() { System.out.println("Mac style textbox"); }
}
// 抽象工厂
public interface UIFactory {
Button createButton();
TextBox createTextBox();
}
public class WindowsFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextBox createTextBox() { return new WindowsTextBox(); }
}
public class MacFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public TextBox createTextBox() { return new MacTextBox(); }
}
单例模式(Singleton)
单例模式确保一个类只有一个实例,并提供一个全局访问点。
实现要点:
- 私有化构造函数
- 提供静态方法获取唯一实例
- 保证线程安全
// 双重检查锁定(推荐实现)
public class ConfigManager {
private static volatile ConfigManager instance;
private Properties config;
private ConfigManager() {
// 加载配置
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
}
// 枚举实现(最简洁、最安全)
public enum ConfigManagerEnum {
INSTANCE;
private Properties config;
// 初始化逻辑
}
单例模式的适用场景:配置管理器、连接池、缓存管理器、日志管理器。
建造者模式(Builder)
建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。适用于参数众多、且部分参数可选的复杂对象创建。
public class ServerConfig {
private final String host;
private final int port;
private final int timeout;
private final int maxConnections;
private final boolean sslEnabled;
private ServerConfig(Builder builder) {
this.host = builder.host;
this.port = builder.port;
this.timeout = builder.timeout;
this.maxConnections = builder.maxConnections;
this.sslEnabled = builder.sslEnabled;
}
public static class Builder {
private String host;
private int port;
private int timeout = 3000;
private int maxConnections = 100;
private boolean sslEnabled = false;
public Builder(String host, int port) {
this.host = host;
this.port = port;
}
public Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}
public Builder maxConnections(int maxConnections) {
this.maxConnections = maxConnections;
return this;
}
public Builder sslEnabled(boolean sslEnabled) {
this.sslEnabled = sslEnabled;
return this;
}
public ServerConfig build() {
return new ServerConfig(this);
}
}
}
// 使用
ServerConfig config = new ServerConfig.Builder("localhost", 8080)
.timeout(5000)
.maxConnections(200)
.sslEnabled(true)
.build();
1.2 结构型模式
结构型模式关注类和对象的组合,通过继承和组合来构建更大的结构。
适配器模式(Adapter)
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
典型场景:第三方支付接口适配、旧系统接口兼容、多数据源适配。
// 目标接口
public interface PaymentProcessor {
void processPayment(double amount);
}
// 已有类(不兼容目标接口)
public class AlipayGateway {
public void pay(String amount) {
System.out.println("Alipay: " + amount);
}
}
public class WechatPayGateway {
public void sendPay(double amount) {
System.out.println("Wechat: " + amount);
}
}
// 适配器
public class AlipayAdapter implements PaymentProcessor {
private AlipayGateway alipay;
public AlipayAdapter() { this.alipay = new AlipayGateway(); }
public void processPayment(double amount) {
alipay.pay(String.valueOf(amount));
}
}
public class WechatPayAdapter implements PaymentProcessor {
private WechatPayGateway wechat;
public WechatPayAdapter() { this.wechat = new WechatPayGateway(); }
public void processPayment(double amount) {
wechat.sendPay(amount);
}
}
装饰器模式(Decorator)
装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。它通过创建一个包装对象(装饰器)来包裹原有的对象。
典型场景:Java I/O 流(BufferedInputStream、DataInputStream)、HTTP 请求增强(添加认证、日志、缓存)。
public interface Coffee {
double getCost();
String getDescription();
}
public class SimpleCoffee implements Coffee {
public double getCost() { return 10; }
public String getDescription() { return "Simple coffee"; }
}
// 装饰器基类
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public double getCost() { return decoratedCoffee.getCost(); }
public String getDescription() { return decoratedCoffee.getDescription(); }
}
// 具体装饰器
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return super.getCost() + 3; }
public String getDescription() { return super.getDescription() + ", milk"; }
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return super.getCost() + 1; }
public String getDescription() { return super.getDescription() + ", sugar"; }
}
// 使用:可以任意组合装饰
Coffee coffee = new MilkDecorator(new SugarDecorator(new SimpleCoffee()));
代理模式(Proxy)
代理模式为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用。
代理的几种类型:
- 远程代理:为远程对象提供本地代表
- 虚拟代理:延迟加载大对象
- 保护代理:控制访问权限
- 智能引用代理:访问时执行额外操作
public interface Image {
void display();
}
public class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + filename);
}
public void display() {
System.out.println("Displaying " + filename);
}
}
// 虚拟代理(延迟加载)
public class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) { this.filename = filename; }
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
外观模式(Facade)
外观模式为子系统中的一组接口提供一个一致的界面,定义了一个高层接口,使得子系统更加容易使用。
// 子系统
public class CPU { public void freeze() {} public void execute() {} }
public class Memory { public void load(long pos, byte[] data) {} }
public class HardDrive { public byte[] read(long lba, int size) { return new byte[0]; } }
// 外观
public class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.execute();
}
}
1.3 行为型模式
行为型模式关注对象之间的通信和职责分配。
策略模式(Strategy)
策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法独立于使用它的客户而变化。
典型场景:排序算法选择、压缩算法切换、支付方式选择。
public interface SortStrategy {
void sort(int[] data);
}
public class QuickSortStrategy implements SortStrategy {
public void sort(int[] data) { /* 快速排序实现 */ }
}
public class MergeSortStrategy implements SortStrategy {
public void sort(int[] data) { /* 归并排序实现 */ }
}
public class SortContext {
private SortStrategy strategy;
public void setStrategy(SortStrategy strategy) { this.strategy = strategy; }
public void sort(int[] data) { strategy.sort(data); }
}
观察者模式(Observer)
观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
典型场景:事件驱动系统、消息订阅、GUI 事件监听。
import java.util.ArrayList;
import java.util.List;
public interface Observer {
void update(String event);
}
public class Subject {
private List<Observer> observers = new ArrayList<>();
private String state;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void setState(String state) {
this.state = state;
notifyAll(state);
}
private void notifyAll(String event) {
for (Observer o : observers) {
o.update(event);
}
}
}
public class EmailNotifier implements Observer {
public void update(String event) {
System.out.println("Sending email notification: " + event);
}
}
public class SmsNotifier implements Observer {
public void update(String event) {
System.out.println("Sending SMS notification: " + event);
}
}
责任链模式(Chain of Responsibility)
责任链模式使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
典型场景:Servlet Filter、Spring Interceptor、审批流程。
public abstract class Handler {
protected Handler next;
public void setNext(Handler next) { this.next = next; }
public abstract void handleRequest(String request);
}
public class AuthHandler extends Handler {
public void handleRequest(String request) {
if (request.contains("auth")) {
System.out.println("AuthHandler: processing authentication");
} else if (next != null) {
next.handleRequest(request);
}
}
}
public class LogHandler extends Handler {
public void handleRequest(String request) {
System.out.println("LogHandler: logging request - " + request);
if (next != null) {
next.handleRequest(request);
}
}
}
// 构建链
Handler auth = new AuthHandler();
Handler log = new LogHandler();
auth.setNext(log);
auth.handleRequest("user login");
模板方法模式(Template Method)
模板方法模式定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现,使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
public abstract class DataProcessor {
// 模板方法
public final void process() {
readData();
processData();
writeData();
}
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
}
public class CsvProcessor extends DataProcessor {
protected void readData() { System.out.println("Reading CSV file"); }
protected void processData() { System.out.println("Processing CSV data"); }
protected void writeData() { System.out.println("Writing CSV output"); }
}
public class JsonProcessor extends DataProcessor {
protected void readData() { System.out.println("Reading JSON file"); }
protected void processData() { System.out.println("Processing JSON data"); }
protected void writeData() { System.out.println("Writing JSON output"); }
}
命令模式(Command)
命令模式将请求封装成对象,从而允许使用不同的请求、队列或者日志来参数化其他对象。
public interface Command {
void execute();
}
public class Light {
public void on() { System.out.println("Light is ON"); }
public void off() { System.out.println("Light is OFF"); }
}
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) { this.light = light; }
public void execute() { light.on(); }
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) { this.command = command; }
public void pressButton() { command.execute(); }
}
二、架构风格
架构风格定义了系统的组织结构,决定了系统如何被分解为组件以及组件之间如何交互。
2.1 分层架构
分层架构是最常见的架构风格之一,将系统按照职责划分为不同的层次,每一层只与相邻层通信。
典型分层:
┌─────────────────────────────────┐
│ 表现层 (Presentation) │ ← 处理用户交互、HTTP 请求
├─────────────────────────────────┤
│ 业务层 (Business/Service) │ ← 核心业务逻辑、规则验证
├─────────────────────────────────┤
│ 数据访问层 (Data Access) │ ← 数据库操作、外部服务调用
├─────────────────────────────────┤
│ 持久层 (Persistence) │ ← 数据库、文件系统
└─────────────────────────────────┘
分层架构的核心原则是依赖倒置:上层依赖下层接口,但不依赖具体实现。每一层只能调用相邻下层的服务,不能跨层调用。
分层架构的优缺点:
- 优点:职责清晰、易于维护、层间解耦、便于团队分工
- 缺点:过度分层导致代码分散、性能损耗(调用链路过长)
- 适用场景:企业级应用、后台管理系统、CRUD 业务
2.2 六边形架构(Hexagonal Architecture)
六边形架构(又称端口与适配器架构)由 Alistair Cockburn 提出,核心思想是将业务逻辑(核心)与外部依赖(适配器)分离。
┌──────────────────┐
│ 用户界面适配器 │
└────────┬─────────┘
│
┌────────────────┐ ┌──────▼──────┐ ┌────────────────┐
│ 数据库适配器 │◄──►│ 核心领域 │◄──►│ 消息队列适配器 │
└────────────────┘ └──────▲──────┘ └────────────────┘
│
┌────────┴─────────┐
│ REST API 适配器 │
└──────────────────┘
核心概念:
- 端口(Ports):核心定义的服务接口,分为入站端口(核心对外暴露的能力)和出站端口(核心需要的外部服务)
- 适配器(Adapters):实现端口的具体类,负责与外部系统交互
- 核心(Domain):纯业务逻辑,不依赖任何外部框架或基础设施
六边形架构的优势:
- 业务逻辑完全独立于技术选型
- 可以轻松替换外部依赖(如切换数据库、消息队列)
- 单元测试无需启动外部服务
- 支持领域驱动设计(DDD)
// 入站端口(核心对外暴露)
public interface OrderService {
Order createOrder(CreateOrderCommand cmd);
Order getOrder(String orderId);
}
// 出站端口(核心需要的外部服务)
public interface OrderRepository {
void save(Order order);
Order findById(String id);
}
public interface PaymentGateway {
boolean charge(Order order);
}
// 核心实现(不依赖任何框架)
public class OrderServiceImpl implements OrderService {
private final OrderRepository repository;
private final PaymentGateway paymentGateway;
public OrderServiceImpl(OrderRepository repository, PaymentGateway paymentGateway) {
this.repository = repository;
this.paymentGateway = paymentGateway;
}
public Order createOrder(CreateOrderCommand cmd) {
Order order = new Order(cmd);
if (paymentGateway.charge(order)) {
repository.save(order);
}
return order;
}
}
// 适配器层(依赖框架)
@Repository
public class JpaOrderRepository implements OrderRepository {
@Autowired private OrderJpaRepository jpaRepo;
public void save(Order order) { jpaRepo.save(toEntity(order)); }
public Order findById(String id) { return toDomain(jpaRepo.findById(id)); }
}
2.3 整洁架构(Clean Architecture)
整洁架构由 Robert C. Martin(Uncle Bob)提出,是对六边形架构的扩展,强调依赖规则:源代码依赖必须指向更内层。
┌─────────────────────────────────────────────────────┐
│ Frameworks & Drivers │ ← 最外层:数据库、UI、外部 API
│ ┌─────────────────────────────────────────────┐ │
│ │ Interface Adapters │ ← 控制器、网关、展示器
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Use Cases │ ← 应用业务规则
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ Entities │ ← ← 企业业务规则
│ │ │ └─────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
四层结构:
- Entities(实体):企业级业务规则,最不具体的、最高层次的业务逻辑
- Use Cases(用例):应用特定的业务规则,协调实体完成具体任务
- Interface Adapters(接口适配器):将外部数据格式转换为内部使用的格式
- Frameworks & Drivers(框架与驱动):所有具体实现细节
依赖规则(Dependency Rule):
- 内层不能知道外层的任何信息
- 外层可以依赖内层
- 跨层通信通过依赖倒置(接口定义在内层,实现放在外层)
2.4 CQRS(Command Query Responsibility Segregation)
CQRS 模式将系统的写操作(Command)和读操作(Query)分离为两个独立的模型。
┌─────────────┐ ┌──────────────┐
│ Command │ │ Query │
│ Model │ │ Model │
│ │ │ │
│ 写操作 │ │ 读操作 │
│ 创建/更新 │ │ 查询/展示 │
└──────┬──────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Write DB │ │ Read DB │
│ (规范化) │ │ (反规范化) │
└──────┬──────┘ └──────┬───────┘
│ │
└───────────┬───────────┘
│
┌──────▼──────┐
│ Event Bus │
│ (同步数据) │
└─────────────┘
CQRS 的适用场景:
- 读写比例悬殊(如读多写少)
- 读写模型的复杂度差异大
- 需要独立扩展读或写
- 需要多视图展示同一数据
CQRS 的优势:
- 读写模型可以独立优化
- 读库可以使用反规范化提升查询性能
- 写库可以保持规范化保证数据一致性
- 可以独立扩展读节点和写节点
注意事项:
- 增加了系统复杂度
- 数据存在延迟(最终一致性)
- 需要处理读写数据同步问题
- 不适合简单的 CRUD 应用
2.5 Event Sourcing(事件溯源)
Event Sourcing 不直接存储当前状态,而是存储导致状态变化的一系列事件。当前状态通过重放事件重建。
┌─────────────────────────────────────────────────────┐
│ Event Store │
│ │
│ Event 1: OrderCreated {id: 1, amount: 100} │
│ Event 2: ItemAdded {orderId: 1, item: "A"} │
│ Event 3: OrderPaid {orderId: 1, method: "CC"} │
│ Event 4: OrderShipped {orderId: 1, carrier: "SF"} │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ State Reconstruction │
│ │
│ replay(Event 1) → Order {id: 1, status: CREATED} │
│ replay(Event 2) → Order {items: ["A"]} │
│ replay(Event 3) → Order {status: PAID} │
│ replay(Event 4) → Order {status: SHIPPED} │
└─────────────────────────────────────────────────────┘
Event Sourcing 的核心概念:
- 事件(Event):不可变的事实记录,包含时间戳和变更内容
- 事件存储(Event Store):持久化所有事件的存储系统
- 聚合(Aggregate):通过重放事件重建当前状态
- 快照(Snapshot):定期保存状态快照,避免全量重放
Event Sourcing 的优势:
- 完整的审计日志(天然支持)
- 可以回溯到任意时间点(时间旅行)
- 支持事件重播和状态重建
- 与 CQRS 天然配合
Event Sourcing 的挑战:
- 事件版本管理(schema evolution)
- 性能问题(大量事件的重放)
- 事件删除困难(合规要求)
- 学习曲线陡峭
三、分布式系统设计
分布式系统是由多个独立计算机组成的系统,这些计算机通过网络通信和协调,对外表现为一个统一的整体。分布式系统设计的核心挑战是处理网络延迟、节点故障和数据一致性。
3.1 CAP 定理
CAP 定理由 Eric Brewer 提出,指出在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)三者不可兼得,最多只能同时满足两个。
Consistency (C)
▲
/ \
/ \
/ \
/ \
/ \
CA ◄──┤ ├──► CP
/ \ / \
/ \ / \
/ \ / \
/ \ / \
/ \ / \
└───────────┴───────────►
Partition (P) Availability (A)
三个特性的含义:
- 一致性(C):所有节点在同一时间看到的数据是相同的
- 可用性(A):每个请求都能收到响应(不保证数据最新)
- 分区容忍性(P):系统在出现网络分区时仍能继续运行
CAP 定理的实践指导:
- CP 系统(如 ZooKeeper、HBase):保证一致性和分区容忍性,牺牲可用性。在网络分区时,系统可能拒绝部分请求。
- AP 系统(如 Dynamo、Cassandra):保证可用性和分区容忍性,牺牲强一致性。在网络分区时,系统继续服务,但可能返回旧数据。
- CA 系统:理论上存在,但实际中不可能实现,因为分布式系统必然面临网络分区。
现代分布式系统通常选择 AP + 最终一致性 或 CP + 强一致性,根据业务场景权衡。
3.2 BASE 理论
BASE 理论是对 CAP 定理中 AP 方案的补充,由 eBay 提出,是一种通过牺牲强一致性来保证可用性的设计哲学。
BASE 的三个要素:
- Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,但保证核心功能可用
- Soft State(软状态):系统状态可以在一段时间内不同步,允许存在中间状态
- Eventually Consistent(最终一致性):经过一段时间后,系统最终会达到一致状态
最终一致性的实现方式:
- 因果一致性:有因果关系的操作保证顺序
- 读己之所写:用户写入的数据自己能立即读到
- 会话一致性:同一会话内保证一致性
- 单调读一致性:不会读到比之前更旧的数据
- 单调写一致性:写操作按顺序执行
BASE 理论的典型应用:DNS 系统、电商库存系统、社交网络 feed 流。
3.3 分布式锁
分布式锁用于在分布式系统中保证多个节点对共享资源的互斥访问。
Redis Redlock
Redlock 是 Redis 官方提出的分布式锁算法,通过在多个独立的 Redis 实例上获取锁来保证可靠性。
算法步骤:
- 记录开始时间
- 依次向 N 个 Redis 实例发送 SET 命令(带 NX 和 EX 选项)
- 如果在超过半数实例上成功获取锁,且总耗时小于锁有效期,则获取锁成功
- 否则,向所有实例发送 DEL 命令释放锁
// 使用 Redisson 实现 Redlock
RLock lock1 = redisson1.getLock("myLock");
RLock lock2 = redisson2.getLock("myLock");
RLock lock3 = redisson3.getLock("myLock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean success = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (success) {
// 执行业务逻辑
}
} finally {
redLock.unlock();
}
Redlock 的争议:分布式系统专家 Martin Kleppmann 指出 Redlock 依赖系统时钟,在时钟跳变时可能出现问题。
ZooKeeper 分布式锁
ZooKeeper 通过顺序节点和 Watch 机制实现分布式锁。
实现原理:
- 在指定路径下创建临时顺序节点
- 获取该路径下所有子节点并排序
- 如果自己创建的节点序号最小,则获取锁
- 否则,监听前一个节点的删除事件
- 前一个节点被删除后,重新检查自己是否序号最小
// 使用 Curator 实现 ZooKeeper 分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
try {
lock.acquire();
// 执行业务逻辑
} finally {
lock.release();
}
ZooKeeper 锁的优势:不依赖系统时钟,可靠性高;劣势:性能相对较低,依赖 ZooKeeper 集群。
数据库锁
数据库锁是最直接的分布式锁实现方式。
-- 悲观锁(SELECT ... FOR UPDATE)
BEGIN;
SELECT * FROM locks WHERE resource_id = 1 FOR UPDATE;
-- 执行业务逻辑
COMMIT;
-- 乐观锁(版本号机制)
UPDATE resources SET data = 'new_data', version = version + 1
WHERE id = 1 AND version = 5;
-- 唯一约束(INSERT 方式)
INSERT INTO distributed_locks (lock_name, expire_at)
VALUES ('myLock', NOW() + INTERVAL 30 SECOND);
3.4 分布式 ID 生成
在分布式系统中,需要生成全局唯一的 ID 来标识实体。
雪花算法(Snowflake)
雪花算法由 Twitter 提出,生成 64 位 Long 型 ID。
┌─────────────────┬──────────────┬──────────────┬─────────────┐
│ 1 bit (符号位) │ 41 bit 时间戳 │ 10 bit 机器ID │ 12 bit 序列号 │
│ 0 │ 毫秒级 │ 数据中心+机器 │ 每毫秒计数 │
└─────────────────┴──────────────┴──────────────┴─────────────┘
- 41 位时间戳:可使用约 69 年
- 10 位机器 ID:支持 1024 个节点
- 12 位序列号:每毫秒每节点可生成 4096 个 ID
雪花算法的优势:
- 全局唯一
- 趋势递增(有利于数据库索引)
- 高性能(本地生成,无需网络调用)
- 高可用(不依赖外部服务)
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// 时间起点(2020-01-01)
private static final long EPOCH = 1577836800000L;
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID 超出范围");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID 超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = waitNextMillis();
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis() {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
UUID
UUID(Universally Unique Identifier)是 128 位的标识符,标准格式为 8-4-4-4-12 的十六进制字符串。
UUID 的优势:全局唯一、无需中心服务、生成简单。 UUID 的劣势:无序(不适合作为数据库主键)、长度较长、可读性差。
Leaf / TinyID
美团 Leaf 和滴滴 TinyID 是工业级的分布式 ID 生成方案。
Leaf 的两种模式:
- 号段模式:从数据库获取号段(如 1000-2000),在本地分配,性能高
- Snowflake 模式:类似雪花算法,但依赖 ZooKeeper 管理节点信息
TinyID 的核心:基于数据库号段模式,支持多 db 负载均衡,提供 HTTP 和 Java SDK 两种调用方式。
3.5 缓存架构
缓存是提升系统性能的重要手段,但使用不当会导致数据不一致、系统崩溃等问题。
缓存穿透
缓存穿透指查询一个不存在的数据,缓存不命中,请求直接打到数据库。
解决方案:
- 布隆过滤器(Bloom Filter):在缓存之前加一层布隆过滤器,快速判断数据是否存在
- 缓存空值:将查询结果为空的数据也缓存,设置较短的过期时间
// 缓存空值示例
public User getUser(String id) {
User user = cache.get(id);
if (user != null) {
return user == NULL_USER ? null : user;
}
user = db.findById(id);
if (user == null) {
cache.set(id, NULL_USER, 60); // 缓存空值 1 分钟
} else {
cache.set(id, user, 3600);
}
return user;
}
缓存击穿
缓存击穿指某个热点 key 在过期瞬间,大量请求同时到达数据库。
解决方案:
- 互斥锁:只有一个请求去加载数据库,其他请求等待
- 逻辑过期:缓存中存储过期时间,后台异步更新
// 互斥锁方案
public User getUserWithMutex(String id) {
User user = cache.get(id);
if (user != null) {
return user;
}
// 获取分布式锁
String lockKey = "lock:user:" + id;
boolean locked = redis.setnx(lockKey, "1", 10);
if (locked) {
try {
user = db.findById(id);
if (user != null) {
cache.set(id, user, 3600);
}
} finally {
redis.del(lockKey);
}
} else {
// 等待后重试
Thread.sleep(50);
return getUserWithMutex(id);
}
return user;
}
缓存雪崩
缓存雪崩指大量缓存在同一时间过期,导致数据库压力骤增。
解决方案:
- 随机过期时间:在过期时间上加上一个随机值
- 多级缓存:L1(本地缓存)+ L2(Redis 缓存)+ L3(数据库)
- 缓存永不过期:后台异步更新,逻辑过期
// 随机过期时间
int expireTime = 3600 + new Random().nextInt(300); // 3600~3900 秒
cache.set(key, value, expireTime);
缓存一致性
缓存与数据库数据不一致是常见问题,常见的保证一致性的方案:
- Cache Aside Pattern(旁路缓存):读时先读缓存,缓存不命中则读数据库并写入缓存;写时先更新数据库,再删除缓存
- 订阅 Binlog:通过 Canal 等工具订阅数据库 Binlog,异步更新缓存
- 延迟双删:先删除缓存,再更新数据库,最后再删除一次缓存(解决并发问题)
// Cache Aside Pattern
public void updateUser(User user) {
db.update(user);
cache.delete("user:" + user.getId()); // 删除缓存,而非更新
}
3.6 负载均衡
负载均衡是将请求分发到多个后端服务器,以提高系统的处理能力和可用性。
客户端负载均衡
客户端负载均衡由服务调用方决定请求发送到哪个服务实例。
典型实现:Spring Cloud Ribbon + Feign
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 使用
restTemplate.getForObject("http://user-service/users/1", User.class);
服务端负载均衡
服务端负载均衡由独立的负载均衡器(如 Nginx、HAProxy)分发请求。
upstream backend {
server 192.168.1.10:8080 weight=3;
server 192.168.1.11:8080 weight=2;
server 192.168.1.12:8080 backup;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
负载均衡算法
- 轮询(Round Robin):依次分配请求,简单公平
- 加权轮询(Weighted Round Robin):根据服务器性能分配权重
- 最少连接(Least Connections):将请求分配给当前连接数最少的服务器
- 一致性 Hash(Consistent Hash):相同请求总是路由到同一服务器,适用于有状态服务
- 随机(Random):随机选择,简单但可能不均衡
四、架构决策与权衡
架构设计的核心不是选择最好的技术,而是在约束条件下做出最合适的权衡。
4.1 技术选型原则
技术选型应考虑以下因素:
- 业务需求:技术必须服务于业务目标,而非相反
- 团队能力:选择团队熟悉的技术,降低学习成本
- 生态成熟度:选择有活跃社区、丰富文档和工具的技术
- 长期维护:评估技术的生命周期,避免选择即将淘汰的技术
- 成本:包括开发成本、运维成本、授权费用
- 可扩展性:技术是否能支撑未来 1-3 年的业务增长
- 安全性:技术是否有成熟的安全实践
技术选型的常见陷阱:
- 简历驱动开发:为了学习新技术而选择不成熟的技术
- 过度设计:在业务初期引入复杂架构
- 技术债务:为了快速上线而选择不合适的技术
4.2 架构演进路径
系统的架构通常经历以下阶段:
单体应用 → 垂直拆分 → 服务化 → 微服务 → Service Mesh
- 单体应用:所有功能部署在一个进程中,适合初创团队和简单业务
- 垂直拆分:按业务模块拆分为多个独立应用,解决单体部署慢的问题
- 服务化(SOA):抽取共享服务,通过 ESB(企业服务总线)集成
- 微服务:细粒度服务拆分,去中心化治理,独立部署
- Service Mesh:将服务间通信、负载均衡、熔断等基础设施下沉到 Sidecar
架构演进的关键原则:
- 不要过早拆分:单体应用能解决就不要拆分
- 演进式架构:架构应该能够平滑演进,而非推倒重来
- 数据驱动决策:根据监控数据决定是否需要架构升级
4.3 架构文档
良好的架构文档是团队协作和知识传承的基础。
C4 模型
C4 模型由 Simon Brown 提出,从四个层次描述软件架构:
- Context(上下文):系统在整个业务环境中的位置
- Container(容器):系统的顶层组件(应用、数据库、文件系统)
- Component(组件):容器内的主要组件及其职责
- Code(代码):组件的代码实现(类图、时序图)
┌─────────────────────────────────────────────┐
│ System Context │ ← 系统与外部系统的关系
│ ┌─────────────────────────────────────┐ │
│ │ Containers │ ← 系统的可部署单元
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Components │ ← ← 容器内的模块
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ Code │ ← ← ← 代码实现
│ │ │ └─────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
ADR(Architecture Decision Record)
ADR 是一种轻量级的架构决策记录方式,记录架构决策的背景、选项和结果。
模板:
# ADR-001: 选择 PostgreSQL 作为主数据库
## 状态
已接受
## 背景
系统需要支持复杂查询和事务,且团队有 PostgreSQL 经验。
## 决策
使用 PostgreSQL 14 作为主数据库。
## 后果
- 优点:支持 JSONB、全文搜索、成熟的事务支持
- 缺点:水平扩展能力不如 NoSQL
- 风险:单点故障,需要配置主从复制
五、学习资源推荐
书籍
- 《设计模式:可复用面向对象软件的基础》(GoF)
- 《Head First 设计模式》
- 《架构整洁之道》(Clean Architecture)
- 《企业应用架构模式》(PoEAA)
- 《数据密集型应用系统设计》(DDIA)
- 《微服务架构设计模式》
在线资源
- Refactoring Guru(设计模式教程)
- Martin Fowler 博客(架构模式)
- InfoQ 架构频道
- 极客时间《架构师训练营》
实践项目
- 使用设计模式重构现有代码
- 将单体应用改造为微服务架构
- 实现一个简单的分布式系统
- 设计并实现 CQRS + Event Sourcing 系统
总结
架构设计是一项需要持续学习和实践的技能。掌握设计模式可以帮助你写出更优雅的代码,理解架构风格可以帮助你更好地组织系统,而分布式系统设计的知识则是构建大规模系统的基础。
关键要点:
- 没有银弹:每种架构风格都有其适用场景和局限性
- 权衡取舍:架构决策是在约束条件下做出的最优选择
- 持续演进:架构应该随着业务的发展而不断调整
- 简单优先:不要过度设计,简单的方案往往是最好的
- 数据驱动:用监控和指标来验证架构决策的正确性