{T}

分布式系统核心问题

核心要点:分布式系统通过网络协调多个节点完成业务能力,带来了网络不可靠、数据一致性、幂等性、分布式锁等核心挑战。理解这些问题是构建可靠分布式系统的基础。

一、分布式系统概述

1.1 什么是分布式系统

分布式系统(Distributed System) 是由多个独立的计算节点组成的系统,这些节点通过网络通信和协作,对外呈现为一个统一的系统。

核心特征

  • 多节点:系统由多个独立的计算节点组成
  • 网络通信:节点之间通过网络进行通信和协作
  • 透明性:对用户而言,系统看起来像是一个单一的系统
  • 可扩展性:可以通过增加节点来扩展系统容量
  • 容错性:部分节点故障不影响整个系统的运行

单体架构 vs 分布式架构

特性单体架构分布式架构
部署单一进程多个独立进程
扩展垂直扩展(升级硬件)水平扩展(增加节点)
通信方法调用网络调用(RPC/HTTP)
数据一致性本地事务分布式事务
故障影响全局故障局部故障
开发复杂度

1.2 为什么需要分布式系统

1. 业务规模增长

code
单体架构的限制:
- 代码库庞大,编译部署缓慢
- 单机性能瓶颈,无法满足高并发需求
- 团队协作困难,冲突频繁
- 技术栈受限,无法灵活选择

分布式架构的优势:
- 服务拆分,独立部署和扩展
- 水平扩展,应对流量增长
- 团队独立开发,提高效率
- 技术选型灵活,因地制宜

2. 高可用需求

code
单体架构:单点故障
┌─────┐
│ App │ ──── 挂掉 → 全系统不可用
└─────┘

分布式架构:容错设计
┌─────┐
│ App │ ──── 挂掉 → 其他节点接管
└─────┘
     ↓
┌─────┐
│ App │ ──── 继续服务
└─────┘

3. 技术演进

code
技术栈演进:
单体应用 → 垂直拆分 → 分布式服务 → 微服务架构 → 云原生架构

1.3 分布式系统的核心挑战

真正难的地方,不是"把应用拆成多个服务",而是拆开之后的问题:

  1. 网络不可靠

    • 网络延迟、超时、丢包
    • 网络分区、脑裂
  2. 数据一致性

    • 多节点数据同步
    • 分布式事务
  3. 幂等性

    • 重复请求处理
    • 消息重复消费
  4. 分布式锁

    • 资源竞争协调
    • 锁的可靠性
  5. 故障处理

    • 节点故障检测
    • 故障转移和恢复

二、网络不可靠

2.1 为什么网络不可靠

单机 vs 分布式

code
单机场景:
┌────────────┐
│   应用     │
│  ┌──────┐  │
│  │方法A │  │ ← 调用可靠,立即返回
│  └──────┘  │
│  ┌──────┐  │
│  │方法B │  │ ← 内存访问,稳定
│  └──────┘  │
└────────────┘

分布式场景:
┌─────┐      网络      ┌─────┐
│服务A│ ←──────────────→ │服务B│
└─────┘   ↑ 网络不可靠   └─────┘
          │
          ├─ 超时
          ├─ 丢包
          ├─ 延迟
          ├─ 网络分区
          └─ 部分失败

网络不可靠的表现

  1. 超时(Time Out)

    java
    // 服务调用超时
    try {
        userService.getUser(id); // 超时 3 秒
    } catch (TimeoutException e) {
        // 不知道服务是否执行成功
        // 1. 服务未执行
        // 2. 服务执行成功,但响应超时
        // 3. 服务执行失败
    }
  2. 丢包(Packet Loss)

    java
    // 数据包丢失
    // 请求发送,但未到达目标
    // 响应发送,但在途中丢失
  3. 延迟(Latency)

    java
    // 网络延迟导致响应缓慢
    // 影响用户体验
    // 可能触发超时机制
  4. 网络分区(Network Partition)

    java
    // 网络分区导致部分节点无法通信
    // ┌────────┐   ╳   ┌────────┐
    // │ 节点A  │       │ 节点B  │
    // └────────┘       └────────┘
    //   ↑ 网络分区,无法通信

2.2 超时问题

1. 超时的三种情况

java
// 调用远程服务
public User getUser(Long id) {
    try {
        // 超时时间 3 秒
        return userService.getUser(id);
    } catch (TimeoutException e) {
        // 三种情况:
        // 1. 请求未到达服务提供方
        // 2. 服务提供方已执行,但响应超时
        // 3. 服务提供方执行失败,未返回响应
        
        // 如何处理?
        // - 重试?可能导致重复执行
        // - 查询?可能查不到结果
        // - 放弃?可能导致数据不一致
    }
}

2. 超时策略

java
// 设置合理的超时时间
public class TimeoutConfig {
    // 连接超时
    private int connectTimeout = 3000; // 3 秒
    
    // 读取超时
    private int readTimeout = 5000; // 5 秒
    
    // 总超时
    private int totalTimeout = 10000; // 10 秒
}

// 超时重试策略
public class RetryPolicy {
    private int maxRetries = 3;        // 最大重试次数
    private long retryInterval = 1000; // 重试间隔
    
    public <T> T execute(Callable<T> task) {
        for (int i = 0; i < maxRetries; i++) {
            try {
                return task.call();
            } catch (TimeoutException e) {
                if (i == maxRetries - 1) {
                    throw new RuntimeException("重试失败", e);
                }
                Thread.sleep(retryInterval);
            }
        }
    }
}

2.3 网络分区

网络分区(Network Partition) 是指网络故障导致部分节点之间无法通信。

1. 网络分区的后果

code
正常情况:
┌──────┐ ←─────→ ┌──────┐
│节点A │         │节点B │
└──────┘ ←─────→ └──────┘
   ↑                 ↑
   └────────┬────────┘
         数据同步

网络分区:
┌──────┐    ╳    ┌──────┐
│节点A │         │节点B │
└──────┘         └──────┘
   ↑                 ↑
   │                 │
   └───── 数据不一致 ─┘

网络分区导致的问题

  1. 数据不一致(节点 A 和节点 B 的数据不同)
  2. 脑裂(两个节点都认为自己是主节点)
  3. 服务不可用(部分节点无法提供服务)

2. 应对策略

java
// 1. 多数派原则(Quorum)
// 写入需要多数节点确认
public boolean write(String key, String value) {
    int successCount = 0;
    int totalNodes = 5;
    int quorum = totalNodes / 2 + 1; // 3
    
    for (Node node : nodes) {
        try {
            node.write(key, value);
            successCount++;
            if (successCount >= quorum) {
                return true; // 写入成功
            }
        } catch (Exception e) {
            // 忽略单个节点失败
        }
    }
    return false; // 写入失败
}

// 2. 故障检测和隔离
public class FailureDetector {
    private Map<Node, Long> lastHeartbeat = new ConcurrentHashMap<>();
    
    public void checkNodes() {
        for (Node node : nodes) {
            long lastTime = lastHeartbeat.get(node);
            if (System.currentTimeMillis() - lastTime > timeout) {
                // 标记节点为故障
                markAsFailed(node);
            }
        }
    }
}

2.4 分布式调用原则

任何远程调用都要有

1. 超时意识

java
// × 错误:没有设置超时
public User getUser(Long id) {
    return userService.getUser(id); // 可能永远阻塞
}

// √ 正确:设置超时
public User getUser(Long id) {
    try {
        return userService.getUser(id)
            .get(3, TimeUnit.SECONDS); // 超时 3 秒
    } catch (TimeoutException e) {
        log.error("调用超时", e);
        return null;
    }
}

2. 重试意识

java
// × 错误:失败后直接放弃
public User getUser(Long id) {
    try {
        return userService.getUser(id);
    } catch (Exception e) {
        return null; // 可能只是临时故障
    }
}

// √ 正确:合理重试
@Retryable(
    value = {TimeoutException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000)
)
public User getUser(Long id) {
    return userService.getUser(id);
}

3. 降级意识

java
// × 错误:依赖服务不可用导致整个系统不可用
public User getUser(Long id) {
    return userService.getUser(id); // userService 挂了,整个系统不可用
}

// √ 正确:降级处理
public User getUser(Long id) {
    try {
        return userService.getUser(id);
    } catch (Exception e) {
        // 降级:返回缓存数据或默认值
        return cacheService.getUser(id);
        // 或返回默认用户
        // return User.defaultUser();
    }
}

三、数据一致性

3.1 一致性问题的本质

多个服务、多个存储节点之间的数据,不可能永远像单机事务一样简单。

1. 单机事务(ACID)

java
// 单机事务:简单、可靠
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // 1. 扣减账户A余额
    accountMapper.decreaseBalance(fromId, amount);
    
    // 2. 增加账户B余额
    accountMapper.increaseBalance(toId, amount);
    
    // 要么全部成功,要么全部回滚
}

2. 分布式事务的复杂性

java
// 分布式事务:复杂、不可靠
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // 1. 扣减账户A余额(服务A)
    accountServiceA.decreaseBalance(fromId, amount);
    
    // 2. 增加账户B余额(服务B)
    accountServiceB.increaseBalance(toId, amount);
    
    // 问题:
    // - 服务A成功,服务B失败 → 数据不一致
    // - 服务A成功,网络超时 → 不知道服务B是否成功
    // - 服务A成功,服务B超时 → 不知道是否成功
}

3.2 一致性级别

1. 强一致性(Strong Consistency)

定义: 任何时刻所有节点的数据都是一致的。

特点:

  • 数据立即同步到所有节点
  • 读取操作总是返回最新数据
  • 性能较低,延迟较高

适用场景:

  • 金融交易
  • 库存扣减
  • 订单创建

实现方式:

java
// 两阶段提交(2PC)
public class TwoPhaseCommit {
    
    // 阶段1:准备阶段
    public boolean prepare(List<Service> services) {
        for (Service service : services) {
            if (!service.prepare()) {
                return false; // 准备失败
            }
        }
        return true;
    }
    
    // 阶段2:提交阶段
    public void commit(List<Service> services) {
        for (Service service : services) {
            service.commit(); // 提交事务
        }
    }
    
    // 阶段2:回滚阶段
    public void rollback(List<Service> services) {
        for (Service service : services) {
            service.rollback(); // 回滚事务
        }
    }
}

2. 最终一致性(Eventual Consistency)

定义: 系统保证在没有新的更新的情况下,最终所有节点的数据会达到一致状态。

特点:

  • 允许短暂的数据不一致
  • 最终数据会收敛到一致状态
  • 性能较高,延迟较低

适用场景:

  • 社交动态
  • 消息通知
  • 日志收集

实现方式:

java
// 基于消息的最终一致性
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);
        
        // 2. 发送消息到消息队列
        mqService.send("order.created", order);
        
        // 订单服务和库存服务异步处理
    }
}

public class InventoryService {
    
    @RabbitListener(queues = "order.created")
    public void handleOrderCreated(Order order) {
        // 异步扣减库存
        inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
    }
}

3.3 CAP 定理

CAP 定理: 一个分布式系统不可能同时满足一致性(C)、可用性(A)和分区容错性(P)。

code
         C (Consistency)
         一致性
        / \
       /   \
      /     \
     /  CAP  \
    /         \
   A ───────── P
可用性      分区容错性

1. CAP 三要素

要素含义说明
C (Consistency)一致性所有节点看到的数据是一致的
A (Availability)可用性每个请求都能在合理时间内得到响应
P (Partition Tolerance)分区容错性网络分区发生时,系统仍能运行

2. 为什么不能同时满足

code
场景:网络分区发生

节点A ──────╳────── 节点B
  │                  │
  │                  │
  ↓                  ↓
客户端1            客户端2

问题:
- 如果选择 C:拒绝客户端请求,保证一致性,但牺牲可用性
- 如果选择 A:接受客户端请求,保证可用性,但牺牲一致性

3. 常见系统的 CAP 选择

系统选择说明
CA单机数据库不考虑分区容错(单机不存在分区问题)
CPZookeeper、HBase保证一致性和分区容错,牺牲可用性
APCassandra、DynamoDB保证可用性和分区容错,牺牲一致性

3.4 BASE 理论

BASE 理论 是 CAP 定理的补充,更贴近互联网业务场景。

1. BASE 三要素

要素含义说明
BA (Basically Available)基本可用系统出现故障时,允许损失部分可用性
S (Soft State)软状态允许系统存在中间状态,不影响整体可用性
E (Eventually Consistent)最终一致系统最终会达到一致状态

2. BASE vs ACID

特性ACIDBASE
一致性强一致性最终一致性
可用性
性能
复杂度
适用场景金融交易互联网应用

3. BASE 的实现

java
// 基本可用:降级处理
public User getUser(Long id) {
    try {
        return userService.getUser(id);
    } catch (Exception e) {
        // 基本可用:返回缓存数据
        return cacheService.getUser(id);
    }
}

// 软状态:允许中间状态
public void updateOrderStatus(Long orderId, String status) {
    // 中间状态:订单状态可能短暂不一致
    orderMapper.updateStatus(orderId, status);
    
    // 异步通知其他服务
    mqService.send("order.status.updated", orderId, status);
}

// 最终一致:补偿机制
@Scheduled(fixedDelay = 60000)
public void checkConsistency() {
    // 定期检查数据一致性
    List<Order> inconsistentOrders = orderMapper.findInconsistentOrders();
    
    for (Order order : inconsistentOrders) {
        // 补偿操作
        compensate(order);
    }
}

3.5 一致性方案选择

业务里更重要的问题不是"哪种理论更高级",而是:

  1. 这个场景到底需要多强的一致性?

    • 金融交易:强一致性
    • 社交动态:最终一致性
    • 日志收集:弱一致性
  2. 业务能接受多长时间的不一致窗口?

    • 实时:几秒内
    • 准实时:几分钟内
    • 延迟:几小时内

一致性级别选择决策树:

code
是否需要强一致性?
├─ 是 → 选择 CP 方案
│       ├─ 数据库主从复制
│       ├─ 分布式锁
│       └─ 两阶段提交
│
└─ 否 → 选择 AP 方案
        ├─ 允许不一致时间窗口?
        │   ├─ 短(秒级) → 最终一致性
        │   │           ├─ 消息队列
        │   │           ├─ 异步复制
        │   │           └─ 补偿机制
        │   │
        │   └─ 长(分钟级) → 弱一致性
        │                   ├─ 定时同步
        │                   └─ 定期补偿

四、幂等性

4.1 什么是幂等性

幂等性(Idempotency) 是指同一个操作执行多次与执行一次的效果相同。

公式表示:

code
f(x) = f(f(x))

实际意义:

  • 消息重试不会导致业务结果反复变化
  • 请求超时重发不会导致重复操作
  • 前端重复点击不会导致重复提交

4.2 为什么需要幂等性

1. 消息重试

java
// 消息队列可能重复投递
@RabbitListener(queues = "order.created")
public void handleOrderCreated(Order order) {
    // 问题:如果这条消息重复投递,会导致重复扣减库存
    inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
}

2. 请求超时重发

java
// 客户端超时重试
public void createOrder(Order order) {
    try {
        orderService.createOrder(order);
    } catch (TimeoutException e) {
        // 客户端重试,可能导致重复创建订单
        createOrder(order);
    }
}

3. 前端重复点击

java
// 用户快速点击两次按钮
// 可能导致重复提交
$("#submit").click(function() {
    $.post("/order/create", orderData);
});

4.3 幂等性实现方案

1. 业务单号(唯一 ID)

java
// 使用业务单号实现幂等
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 检查订单号是否已存在
        Order existingOrder = orderMapper.selectByOrderNo(order.getOrderNo());
        if (existingOrder != null) {
            // 订单已存在,直接返回(幂等)
            return existingOrder;
        }
        
        // 2. 创建订单
        orderMapper.insert(order);
        
        // 3. 扣减库存
        inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
        
        return order;
    }
}

// 客户端生成唯一订单号
public class OrderClient {
    public void createOrder() {
        // 生成唯一订单号
        String orderNo = UUID.randomUUID().toString();
        
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setProductId(123L);
        order.setQuantity(1);
        
        orderService.createOrder(order);
    }
}

2. 唯一索引

sql
-- 数据库唯一索引
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64) UNIQUE,  -- 唯一索引
    product_id BIGINT,
    quantity INT,
    create_time DATETIME
);

-- 幂等插入
INSERT INTO orders (order_no, product_id, quantity, create_time)
VALUES ('ORD123456', 123, 1, NOW())
ON DUPLICATE KEY UPDATE order_no = order_no; -- 重复插入时忽略

3. 幂等 Token

java
// 幂等 Token 机制
public class IdempotentService {
    
    // 1. 获取 Token
    public String getToken() {
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(token, "1", 10, TimeUnit.MINUTES);
        return token;
    }
    
    // 2. 验证 Token
    public boolean checkToken(String token) {
        // 使用 Lua 脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(token),
            "1"
        );
        
        return result != null && result == 1;
    }
    
    // 3. 业务处理
    public void process(String token, Order order) {
        // 验证 Token
        if (!checkToken(token)) {
            throw new RuntimeException("重复请求");
        }
        
        // 执行业务逻辑
        createOrder(order);
    }
}

4. 状态机约束

java
// 使用状态机实现幂等
public class OrderStateMachine {
    
    // 订单状态
    public enum OrderStatus {
        CREATED,    // 已创建
        PAID,       // 已支付
        SHIPPED,    // 已发货
        COMPLETED,  // 已完成
        CANCELLED   // 已取消
    }
    
    // 状态转换规则
    private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new HashMap<>();
    
    static {
        TRANSITIONS.put(CREATED, Set.of(PAID, CANCELLED));
        TRANSITIONS.put(PAID, Set.of(SHIPPED, CANCELLED));
        TRANSITIONS.put(SHIPPED, Set.of(COMPLETED));
    }
    
    // 状态转换
    @Transactional
    public void updateStatus(Long orderId, OrderStatus newStatus) {
        Order order = orderMapper.selectById(orderId);
        
        // 检查状态转换是否合法
        if (!TRANSITIONS.get(order.getStatus()).contains(newStatus)) {
            throw new IllegalStateException("非法状态转换");
        }
        
        // 更新状态
        orderMapper.updateStatus(orderId, newStatus);
    }
    
    // 幂等支付
    public void pay(Long orderId) {
        Order order = orderMapper.selectById(orderId);
        
        // 如果已经是已支付状态,直接返回(幂等)
        if (order.getStatus() == OrderStatus.PAID) {
            return;
        }
        
        // 状态转换
        updateStatus(orderId, OrderStatus.PAID);
    }
}

4.4 幂等性设计原则

幂等不是"尽量少重复",而是"重复了也不改变最终业务结果"

1. 天然幂等操作

java
// 查询操作:天然幂等
public User getUser(Long id) {
    return userMapper.selectById(id);
}

// 删除操作:幂等
public void deleteUser(Long id) {
    userMapper.deleteById(id); // 多次删除结果相同
}

// 更新为固定值:幂等
public void updateStatus(Long id, String status) {
    userMapper.updateStatus(id, status); // 多次更新结果相同
}

2. 非幂等操作需要设计

java
// × 非幂等:累加操作
public void increaseBalance(Long id, BigDecimal amount) {
    userMapper.increaseBalance(id, amount); // 多次累加导致余额错误
}

// √ 幂等:使用业务单号
public void increaseBalance(Long id, BigDecimal amount, String transactionNo) {
    // 检查交易号是否已存在
    if (transactionMapper.exists(transactionNo)) {
        return; // 已处理,幂等
    }
    
    // 增加余额
    userMapper.increaseBalance(id, amount);
    
    // 记录交易
    transactionMapper.insert(transactionNo, amount);
}

五、分布式锁

5.1 什么是分布式锁

分布式锁(Distributed Lock) 用于协调多个节点之间对共享资源的竞争,保证同一时刻只有一个节点能够访问共享资源。

应用场景:

  • 秒杀活动
  • 定时任务调度
  • 分布式事务
  • 配置更新

5.2 分布式锁的实现方式

1. 数据库锁

sql
-- 基于数据库的唯一索引
CREATE TABLE distributed_lock (
    lock_key VARCHAR(64) PRIMARY KEY,
    lock_value VARCHAR(64),
    expire_time DATETIME
);

-- 加锁
INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES ('lock:order:123', 'uuid', DATE_ADD(NOW(), INTERVAL 30 SECOND));

-- 解锁
DELETE FROM distributed_lock 
WHERE lock_key = 'lock:order:123' AND lock_value = 'uuid';

优点: 简单,易于理解和实现
缺点: 性能低,不支持锁自动过期

2. Redis 分布式锁

java
// Redis 分布式锁
public class RedisDistributedLock {
    
    private StringRedisTemplate redisTemplate;
    
    // 加锁
    public boolean lock(String key, String value, long expireTime) {
        // 使用 SETNX 命令
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        
        return result != null && result;
    }
    
    // 解锁
    public boolean unlock(String key, String value) {
        // 使用 Lua 脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key),
            value
        );
        
        return result != null && result == 1;
    }
    
    // 使用示例
    public void processOrder(Long orderId) {
        String lockKey = "lock:order:" + orderId;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 加锁
            if (lock(lockKey, lockValue, 30)) {
                // 执行业务逻辑
                createOrder(orderId);
            } else {
                throw new RuntimeException("获取锁失败");
            }
        } finally {
            // 解锁
            unlock(lockKey, lockValue);
        }
    }
}

3. Zookeeper 分布式锁

java
// Zookeeper 分布式锁
public class ZkDistributedLock {
    
    private CuratorFramework client;
    
    // 加锁
    public InterProcessMutex acquire(String lockPath, long timeout, TimeUnit unit) 
            throws Exception {
        InterProcessMutex lock = new InterProcessMutex(client, lockPath);
        
        if (lock.acquire(timeout, unit)) {
            return lock;
        } else {
            throw new RuntimeException("获取锁超时");
        }
    }
    
    // 使用示例
    public void processOrder(Long orderId) throws Exception {
        String lockPath = "/locks/order/" + orderId;
        InterProcessMutex lock = null;
        
        try {
            // 加锁
            lock = acquire(lockPath, 10, TimeUnit.SECONDS);
            
            // 执行业务逻辑
            createOrder(orderId);
        } finally {
            // 解锁
            if (lock != null) {
                lock.release();
            }
        }
    }
}

5.3 分布式锁的对比

实现方式优点缺点适用场景
数据库锁简单,易于理解性能低,不支持自动过期低并发场景
Redis 锁性能高,支持自动过期可能丢失锁(单节点故障)高并发,允许少量锁丢失
Zookeeper 锁可靠性高,支持锁重入性能较低,部署复杂高可靠性要求

5.4 分布式锁的常见误区

误区一:一遇到并发问题就想上分布式锁

问题: 很多人把分布式锁当成解决并发问题的万能钥匙。

实际情况: 很多问题更适合用业务约束和状态机解决。

java
// × 错误:使用分布式锁
public void createOrder(Order order) {
    String lockKey = "lock:order:" + order.getUserId();
    distributedLock.lock(lockKey);
    
    try {
        // 创建订单
        orderMapper.insert(order);
    } finally {
        distributedLock.unlock(lockKey);
    }
}

// √ 正确:使用幂等设计
public void createOrder(Order order) {
    // 检查订单号是否已存在
    if (orderMapper.exists(order.getOrderNo())) {
        return; // 幂等
    }
    
    // 创建订单
    orderMapper.insert(order);
}

误区二:分布式锁是银弹

问题: 认为有了分布式锁就能保证数据一致性。

实际情况: 分布式锁本身也有可靠性问题。

java
//  分布式锁的问题:
// 1. Redis 单节点故障,锁丢失
// 2. 客户端获取锁后崩溃,锁无法释放
// 3. 锁超时时间设置不合理

// 解决方案:
// 1. 使用 RedLock 算法(多节点)
// 2. 设置合理的超时时间
// 3. 使用看门狗机制自动续期

误区三:长事务适合用分布式锁

问题: 在长事务中使用分布式锁,导致锁持有时间过长。

实际情况: 长事务会导致锁超时,引发数据不一致。

java
// × 错误:长事务 + 分布式锁
public void processOrder(Long orderId) {
    distributedLock.lock("order:" + orderId);
    
    try {
        // 长时间业务处理
        createOrder(orderId);      // 1 秒
        decreaseInventory(orderId); // 2 秒
        sendEmail(orderId);         // 3 秒
        // 总共 6 秒,可能超过锁超时时间
    } finally {
        distributedLock.unlock("order:" + orderId);
    }
}

// √ 正确:缩短锁持有时间
public void processOrder(Long orderId) {
    // 只锁关键操作
    distributedLock.lock("inventory:" + productId);
    try {
        decreaseInventory(orderId);
    } finally {
        distributedLock.unlock("inventory:" + productId);
    }
    
    // 其他操作不需要锁
    createOrder(orderId);
    sendEmail(orderId);
}

5.5 分布式锁最佳实践

1. 锁的超时时间要合理

java
// 设置合理的超时时间
public class LockConfig {
    // 业务执行时间 + 缓冲时间
    private long lockTimeout = businessTimeout * 1.5;
}

2. 锁的粒度要细

java
// × 粗粒度锁
String lockKey = "lock:order";

// √ 细粒度锁
String lockKey = "lock:order:" + orderId;

3. 使用锁重试机制

java
// 带重试的加锁
public boolean lockWithRetry(String key, String value, long expireTime, int maxRetries) {
    for (int i = 0; i < maxRetries; i++) {
        if (lock(key, value, expireTime)) {
            return true;
        }
        
        try {
            Thread.sleep(100); // 等待后重试
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    return false;
}

4. 使用看门狗机制

java
// Redisson 看门狗机制
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认 30 秒,看门狗自动续期

// 业务执行期间,看门狗会自动续期
// 业务执行完毕,主动释放锁
lock.unlock();

六、常见误区与注意事项

6.1 误区一:把重试当成万能修复手段

错误认识: 遇到失败就重试,重试能解决所有问题。

实际情况:

  1. 重试可能导致重复执行
  2. 重试可能放大故障(雪崩效应)
  3. 重试需要配合幂等设计
java
// × 错误:无限重试
public void callService() {
    while (true) {
        try {
            remoteService.call();
            break;
        } catch (Exception e) {
            // 无限重试,可能导致系统崩溃
            continue;
        }
    }
}

// √ 正确:有限重试 + 幂等设计
@Retryable(
    value = {TimeoutException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000, multiplier = 2)
)
public void callService(String requestId) {
    // 使用 requestId 实现幂等
    remoteService.call(requestId);
}

6.2 误区二:只会背 CAP,不会落到业务场景

错误认识: 记住了 CAP 定理,但不知道如何应用到实际业务。

实际情况: 不同业务场景对一致性和可用性的要求不同。

示例:

code
电商系统的不同场景:

1. 下单场景
   - 需求:强一致性(不能超卖)
   - 选择:CP 方案
   - 实现:分布式锁 + 数据库事务

2. 商品浏览场景
   - 需求:高可用性
   - 选择:AP 方案
   - 实现:缓存 + 异步更新

3. 订单查询场景
   - 需求:最终一致性
   - 选择:AP 方案
   - 实现:读写分离 + 异步复制

6.3 误区三:没有补偿机制,却说系统是最终一致

错误认识: 使用异步消息就说系统是最终一致性,但没有补偿机制。

实际情况: 最终一致性需要补偿机制保证数据最终收敛。

java
// × 错误:只发消息,没有补偿
public void createOrder(Order order) {
    // 创建订单
    orderMapper.insert(order);
    
    // 发送消息
    mqService.send("order.created", order);
    
    // 如果消息丢失,库存永远不会扣减
}

// √ 正确:增加补偿机制
public void createOrder(Order order) {
    // 创建订单
    orderMapper.insert(order);
    
    // 发送消息
    mqService.send("order.created", order);
}

// 定时补偿任务
@Scheduled(fixedDelay = 60000)
public void compensateOrders() {
    // 查找未处理的订单
    List<Order> pendingOrders = orderMapper.findPendingOrders();
    
    for (Order order : pendingOrders) {
        // 重新发送消息
        mqService.send("order.created", order);
    }
}

6.4 误区四:把"理论一致性"当成"业务真的可落地"

错误认识: 理论上可行的一致性方案,实际上难以落地。

实际情况: 一致性方案需要考虑成本、复杂度、团队技术能力。

示例:

code
Paxos 算法 vs Raft 算法:

Paxos 算法:
- 理论:正确性已证明
- 实际:难以理解和实现
- 落地:很少有团队能正确实现

Raft 算法:
- 理论:正确性已证明
- 实际:易于理解和实现
- 落地:被广泛使用(Etcd、Consul)

结论:选择适合团队能力的方案

七、实战场景

7.1 场景一:订单、库存、支付协作

问题描述: 一个下单操作往往会跨多个系统,如何保证数据一致性?

挑战:

  1. 订单创建成功,库存扣减失败
  2. 库存扣减成功,支付失败
  3. 支付成功,订单状态未更新

解决方案:

1. 基于 TCC 的分布式事务

java
// TCC (Try-Confirm-Cancel) 分布式事务
public class OrderTccService {
    
    // Try 阶段:预留资源
    public void tryCreateOrder(Order order) {
        // 1. 创建订单(待确认状态)
        orderMapper.insert(order.setStatus("TRYING"));
        
        // 2. 预留库存
        inventoryService.freezeStock(order.getProductId(), order.getQuantity());
        
        // 3. 预留余额
        accountService.freezeBalance(order.getUserId(), order.getAmount());
    }
    
    // Confirm 阶段:确认提交
    public void confirmCreateOrder(Order order) {
        // 1. 确认订单
        orderMapper.updateStatus(order.getId(), "CONFIRMED");
        
        // 2. 扣减库存
        inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
        
        // 3. 扣减余额
        accountService.decreaseBalance(order.getUserId(), order.getAmount());
    }
    
    // Cancel 阶段:取消回滚
    public void cancelCreateOrder(Order order) {
        // 1. 取消订单
        orderMapper.updateStatus(order.getId(), "CANCELLED");
        
        // 2. 释放库存
        inventoryService.unfreezeStock(order.getProductId(), order.getQuantity());
        
        // 3. 释放余额
        accountService.unfreezeBalance(order.getUserId(), order.getAmount());
    }
}

2. 基于消息的最终一致性

java
// 基于消息的最终一致性
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);
        
        // 2. 发送消息到库存服务
        InventoryEvent event = new InventoryEvent();
        event.setOrderId(order.getId());
        event.setProductId(order.getProductId());
        event.setQuantity(order.getQuantity());
        
        // 保存消息到本地消息表
        eventMapper.insert(event);
        
        // 异步发送消息
        mqService.send("inventory.decrease", event);
    }
}

// 库存服务消费消息
public class InventoryService {
    
    @RabbitListener(queues = "inventory.decrease")
    public void handleDecreaseStock(InventoryEvent event) {
        // 幂等检查
        if (eventMapper.exists(event.getId())) {
            return; // 已处理
        }
        
        // 扣减库存
        inventoryMapper.decreaseStock(event.getProductId(), event.getQuantity());
        
        // 标记消息已处理
        eventMapper.markAsProcessed(event.getId());
    }
}

7.2 场景二:分布式任务调度

问题描述: 多个实例同时运行定时任务,如何避免重复执行?

挑战:

  1. 同一个任务被多个实例执行
  2. 某个节点挂掉后任务无法继续

解决方案:

java
// 使用分布式锁实现任务调度
public class ScheduledTaskService {
    
    @Scheduled(cron = "0 0 2 * * ?")
    public void executeTask() {
        String lockKey = "lock:task:daily-cleanup";
        String lockValue = UUID.randomUUID().toString();
        
        // 获取分布式锁
        if (distributedLock.lock(lockKey, lockValue, 300)) {
            try {
                // 执行任务
                doCleanup();
            } finally {
                // 释放锁
                distributedLock.unlock(lockKey, lockValue);
            }
        } else {
            log.info("其他实例正在执行任务");
        }
    }
    
    private void doCleanup() {
        // 清理过期数据
        // 生成报表
        // 发送通知
    }
}

7.3 场景三:消息驱动系统

问题描述: 消息系统通常会带来重复消费、顺序问题、补偿问题。

挑战:

  1. 消息重复消费
  2. 消息顺序问题
  3. 消息丢失问题

解决方案:

1. 重复消费:幂等设计

java
// 消息幂等处理
@RabbitListener(queues = "order.created")
public void handleOrderCreated(OrderEvent event) {
    // 幂等检查
    if (eventMapper.exists(event.getEventId())) {
        log.info("消息已处理: {}", event.getEventId());
        return;
    }
    
    // 处理订单
    createOrder(event);
    
    // 标记消息已处理
    eventMapper.insert(event.getEventId());
}

2. 顺序问题:分区队列

java
// 顺序消息:同一订单的消息发送到同一队列
public void sendOrderMessage(Long orderId, OrderEvent event) {
    // 根据 orderId 计算队列编号
    int queueIndex = (int) (orderId % queueCount);
    
    String queueName = "order.queue." + queueIndex;
    
    rabbitTemplate.convertAndSend(queueName, event);
}

3. 消息丢失:确认机制

java
// 消息确认机制
@RabbitListener(queues = "order.created")
public void handleOrderCreated(OrderEvent event, Channel channel, 
                                @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
    try {
        // 处理订单
        createOrder(event);
        
        // 确认消息
        channel.basicAck(tag, false);
    } catch (Exception e) {
        // 拒绝消息,重新入队
        channel.basicNack(tag, false, true);
    }
}

八、面试要点

8.1 分布式系统核心面试题

Q1:幂等和防重的区别是什么?

答案:

特性防重(Deduplication)幂等(Idempotency)
层次入口限制结果保证
实现方式Token、唯一索引、状态检查业务单号、状态机
作用范围请求层业务层
重复请求直接拒绝正常处理,结果相同

示例:

java
// 防重:Token 机制
if (!checkToken(token)) {
    throw new RuntimeException("重复请求");
}

// 幂等:业务单号
if (orderMapper.exists(orderNo)) {
    return; // 已存在,直接返回
}
createOrder(order);

Q2:为什么分布式系统里超时很重要?

答案:

  1. 避免资源耗尽: 不设置超时,线程可能永远阻塞
  2. 快速失败: 超时可以快速发现故障,避免雪崩
  3. 故障隔离: 超时后可以降级,避免影响其他服务
  4. 用户体验: 超时可以给用户更快的反馈

最佳实践:

java
// 设置合理的超时时间
public class TimeoutConfig {
    private int connectTimeout = 3000;  // 连接超时
    private int readTimeout = 5000;     // 读取超时
    private int totalTimeout = 10000;   // 总超时
}

Q3:为什么一致性方案要按业务分级?

答案: 不同业务对一致性的要求不同,需要根据业务特点选择合适的方案。

一致性分级:

级别一致性要求典型场景解决方案
强一致数据必须立即一致金融交易、库存扣减分布式事务、分布式锁
最终一致允许短暂不一致订单状态、消息通知消息队列、补偿机制
弱一致允许不一致日志收集、统计报表异步同步、定期补偿

Q4:为什么分布式锁不是默认答案?

答案: 很多并发问题更适合用业务约束和状态机解决,分布式锁只是其中一种方案。

方案对比:

方案适用场景优点缺点
分布式锁临界区保护实现简单性能低,可靠性依赖锁服务
状态机状态转换控制可靠性高需要设计状态转换规则
幂等设计重复请求处理性能高需要额外字段

最佳实践:

java
// 优先选择业务约束
if (order.getStatus() == PAID) {
    return; // 已支付,幂等
}

// 其次选择状态机
if (!canTransition(order.getStatus(), newStatus)) {
    throw new IllegalStateException("非法状态转换");
}

// 最后选择分布式锁
distributedLock.lock("order:" + orderId);

九、总结

9.1 核心知识点总结

分布式系统核心挑战

  1. 网络不可靠: 超时、丢包、延迟、分区
  2. 数据一致性: 强一致、最终一致、弱一致
  3. 幂等性: 重复请求处理、消息重复消费
  4. 分布式锁: 资源竞争协调、可靠性保证

理论基础

  1. CAP 定理: 一致性、可用性、分区容错性三者只能满足其二
  2. BASE 理论: 基本可用、软状态、最终一致性

实现方案

  1. 一致性: 2PC、TCC、消息队列、补偿机制
  2. 幂等性: 业务单号、唯一索引、幂等 Token、状态机
  3. 分布式锁: 数据库锁、Redis 锁、Zookeeper 锁

9.2 学习路径建议

初级阶段

  1. 理解分布式系统的基本概念
  2. 掌握 CAP 和 BASE 理论
  3. 了解常见的一致性问题

中级阶段

  1. 掌握幂等性设计和实现
  2. 学会使用分布式锁
  3. 理解分布式事务的实现方式

高级阶段

  1. 掌握分布式系统的设计模式
  2. 学会分布式系统的性能优化
  3. 理解分布式系统的可靠性保障

9.3 实战理解题

题目 1:设计一个秒杀系统

要求: 保证不超卖,支持高并发。

提示:

  • 使用分布式锁保证库存扣减的原子性
  • 使用幂等设计防止重复下单
  • 使用消息队列削峰填谷

题目 2:实现分布式事务

要求: 跨多个服务的业务操作,保证数据一致性。

提示:

  • 使用 TCC 模式实现分布式事务
  • 设计 Try、Confirm、Cancel 三个阶段
  • 实现补偿机制

题目 3:实现消息幂等消费

要求: 消息可能重复投递,保证业务不会重复执行。

提示:

  • 使用消息 ID 实现幂等
  • 在数据库中记录已处理的消息
  • 使用唯一索引防止重复插入

参考资料

  1. 经典论文

  2. 经典书籍

    • 《分布式系统原理与范型》- Andrew S. Tanenbaum
    • 《数据密集型应用系统设计》- Martin Kleppmann
  3. 开源项目


最后更新: 2026-03-29
维护人: AI 助手

版本差异(技术原理说明)

维度说明
技术原理分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效
落地选型新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容)
Java 版本示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容

本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。