分布式系统设计:技术解析与实践

分布式系统设计:技术解析与实践

一、分布式锁

1. 为什么需要分布式锁

单体应用中,synchronizedReentrantLock 可以保证同一 JVM 内的线程安全。但在微服务多实例部署时,不同 JVM 之间这些锁完全失效:

复制代码
实例A: synchronized(orderId) { 扣库存 }
实例B: synchronized(orderId) { 扣库存 }
// 两个实例可能同时进入临界区,导致超卖

分布式锁的核心:在分布式系统中,对共享资源的互斥访问

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

2. 常见实现方案

基于 Redis(最常用)
java 复制代码
// 加锁:SET key value NX EX timeout
// NX=不存在才设置  EX=过期时间(秒)
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("lock:order:" + orderId, requestId, 30, TimeUnit.SECONDS);

// 解锁:Lua 脚本保证原子性(只能释放自己的锁)
String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                "then return redis.call('del',KEYS[1]) " +
                "else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
    Collections.singletonList("lock:order:" + orderId), requestId);
基于 Redisson(生产推荐)
java 复制代码
@Resource
private RedissonClient redisson;

public void processOrder(Integer orderId) {
    RLock lock = redisson.getLock("lock:order:" + orderId);
    try {
        // 尝试加锁,等待10秒,持有30秒自动释放
        if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            orderService.doProcess(orderId);
        } else {
            throw new BusinessException("获取锁超时");
        }
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Redisson 的优势:

  • 看门狗机制:持有锁期间自动续期,防止业务未完成锁就过期
  • 可重入:同一线程可以多次加锁
  • 公平锁:按请求顺序获取锁
  • 红锁(RedLock):多 Redis 节点防止单点故障
基于 ZooKeeper
java 复制代码
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order/" + orderId);
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        orderService.doProcess(orderId);
    }
} finally {
    lock.release();
}

ZooKeeper 锁的原理:

  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)
  • 最小序号的节点持有锁
  • 节点删除时 Watch 通知下一个等待者
  • 客户端断连后临时节点自动删除(自动释放锁)
基于数据库
sql 复制代码
-- 加锁(利用唯一索引)
INSERT INTO distributed_lock (lock_key, owner, expire_time) 
VALUES ('order:123', 'instance-A', NOW() + INTERVAL 30 SECOND);

-- 解锁
DELETE FROM distributed_lock WHERE lock_key = 'order:123' AND owner = 'instance-A';

3. 方案对比

方案 性能 可靠性 实现复杂度 适用场景
Redis (Redisson) 中高 大多数业务
ZooKeeper 强一致性要求
数据库 简单场景、无 Redis
etcd 云原生环境

4. 锁粒度设计

java 复制代码
// 粒度太粗:整个服务锁住,并发度极低
distributedLock.lock("order-service");

// 粒度适中:按客户维度锁(同一客户的操作串行)
distributedLock.lock("member:" + memberId);

// 粒度精细:按订单维度锁(只锁单个订单)
distributedLock.lock("order:" + orderId);

// 粒度太细:按 SKU 维度锁(可能出现死锁)
distributedLock.lock("sku:" + skuId);

原则:锁的粒度应该等于业务操作的最小冲突单元。

5. 分布式锁的常见陷阱

陷阱 后果 解决方案
锁过期但业务未完成 两个线程同时进入临界区 看门狗自动续期
释放了别人的锁 互斥失效 加锁时记录 owner,释放时验证
Redis 主从切换丢锁 锁失效 RedLock 或 ZooKeeper
获取锁后异常未释放 死锁 finally 中释放 + 过期时间兜底
锁内调用 RPC 超时 持锁时间过长 合理设置超时 + 异步化

二、幂等设计

1. 什么是幂等

同一个操作执行一次和执行多次的效果相同。

复制代码
f(x) = f(f(x))

为什么需要幂等

  • MQ 消息重复投递(At Least Once 语义)
  • 用户重复点击提交
  • 网络超时后客户端重试
  • 服务间调用超时重试(Feign retry)

2. 幂等实现方案

方案1:唯一约束(数据库层面)
sql 复制代码
-- 业务唯一键约束
CREATE UNIQUE INDEX uk_order_code ON outbound_record_master(outbound_record_code);

-- 插入时冲突则忽略
INSERT IGNORE INTO process_log (biz_key, status) VALUES ('order_123', 'PROCESSING');
public void processOrder(String orderCode) {
    try {
        processLogRepository.save(new ProcessLog(orderCode, "PROCESSING"));
    } catch (DuplicateKeyException e) {
        log.info("订单已处理,幂等跳过, orderCode={}", orderCode);
        return;
    }
    // 执行业务逻辑...
}
方案2:状态机校验
java 复制代码
public void csGcReject(OutboundRecordMaster outbound, ...) {
    // 出库单已完成则跳过(状态机幂等)
    if (OrderStatusEnums.COMPLATE.getValue().equals(outbound.getOrderStatus())) {
        log.info("出库单已完成,跳过处理");
        return;
    }
    // 执行拒收逻辑...
}

状态机幂等的核心:只有特定状态才能触发操作,操作完成后状态变更,重复调用时状态不匹配直接跳过

复制代码
待处理(5) → 处理中 → 已完成(7)
   ↑                      │
   └── 重复调用时状态=7 → 跳过
方案3:Token 机制(防重复提交)
java 复制代码
// 第一步:获取 token
@GetMapping("/token")
public String getToken() {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set("submit:" + token, "1", 5, TimeUnit.MINUTES);
    return token;
}

// 第二步:提交时携带 token,用完即删
@PostMapping("/submit")
public Result submit(@RequestHeader("X-Submit-Token") String token, @RequestBody Order order) {
    Boolean deleted = redisTemplate.delete("submit:" + token);
    if (!Boolean.TRUE.equals(deleted)) {
        return Result.fail("请勿重复提交");
    }
    return orderService.createOrder(order);
}
方案4:乐观锁(CAS)
sql 复制代码
-- 更新时带版本号条件
UPDATE stock SET quantity = quantity - 1, version = version + 1
WHERE sku_id = 123 AND version = 5;
-- 如果 affected_rows = 0,说明版本号已变(被别人改过),操作失败
@Version
private Integer version;  // JPA 乐观锁注解

// Hibernate 自动在 UPDATE 语句加 WHERE version = ?
// 并发修改时抛 OptimisticLockException
方案5:去重表
java 复制代码
public void consumeMessage(String messageId, String body) {
    // 插入去重表(唯一索引 messageId)
    try {
        deduplicationRepository.save(new Deduplication(messageId));
    } catch (DuplicateKeyException e) {
        log.info("消息已消费,跳过, messageId={}", messageId);
        return;
    }
    // 执行业务...
}

3. 方案对比

方案 适用场景 优点 缺点
唯一约束 创建类操作 简单可靠 需要明确的唯一键
状态机 状态流转类操作 业务语义清晰 需要设计状态机
Token 前端表单防重 用户无感知 需要额外请求获取 token
乐观锁 更新类操作 不需要额外存储 高并发下重试多
去重表 MQ 消费去重 通用性强 额外存储开销

4. 幂等设计的层次

复制代码
┌─────────────────────────┐
│  接口层幂等(Token/去重)  │  ← 防止客户端重复调用
├─────────────────────────┤
│  业务层幂等(状态机校验)  │  ← 防止内部重复处理
├─────────────────────────┤
│  数据层幂等(唯一约束/CAS)│  ← 最后一道防线
└─────────────────────────┘

三、最终一致性

1. CAP 定理

复制代码
     C(一致性)
    / \
   /   \
  P ─── A
 (分区容忍) (可用性)

分布式系统三选二,网络分区必然存在,所以实际是 CP 或 AP 选择。
  • CP:保证一致性,牺牲可用性(如 ZooKeeper)
  • AP:保证可用性,牺牲一致性(如 Eureka)
  • 大多数业务系统选择 AP + 最终一致性

2. 什么是最终一致性

系统不保证任意时刻数据一致,但保证在没有新更新的情况下,最终所有节点数据会收敛一致。

复制代码
时刻T1: 服务A更新成功,服务B还是旧数据  ← 暂时不一致
时刻T2: 异步同步/重试/补偿完成后         ← 最终一致

3. 实现最终一致性的模式

模式1:本地消息表
java 复制代码
@Transactional
public void createOrder(Order order) {
    // 1. 本地业务操作
    orderRepository.save(order);
    
    // 2. 同事务写入消息表
    messageRepository.save(new LocalMessage(
        "stock.deduct", JSON.toJSONString(new DeductStock(order.getSkuId(), order.getQty())),
        "PENDING"
    ));
}

// 定时任务轮询消息表,发送未成功的消息
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
    List<LocalMessage> pendingList = messageRepository.findByStatus("PENDING");
    for (LocalMessage msg : pendingList) {
        try {
            rabbitTemplate.convertAndSend(msg.getTopic(), msg.getBody());
            msg.setStatus("SENT");
        } catch (Exception e) {
            msg.setRetryCount(msg.getRetryCount() + 1);
        }
        messageRepository.save(msg);
    }
}
本地事务(原子性)          异步(最终一致)
┌──────────────────┐     ┌──────────────┐
│ 1.写业务数据      │     │ 3.发送MQ      │
│ 2.写消息表(PENDING)│ →→→ │ 4.下游消费处理 │
└──────────────────┘     └──────────────┘
模式2:事务消息(RocketMQ)
java 复制代码
// RocketMQ 事务消息
TransactionSendResult result = producer.sendMessageInTransaction(msg, null);

// 本地事务执行器
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    try {
        orderService.createOrder(parseOrder(msg));
        return LocalTransactionState.COMMIT_MESSAGE;   // 提交:下游可消费
    } catch (Exception e) {
        return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚:消息丢弃
    }
}

// 事务回查(本地事务结果未知时 Broker 主动回查)
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    Order order = orderRepository.findByOrderCode(parseOrderCode(msg));
    return order != null 
        ? LocalTransactionState.COMMIT_MESSAGE 
        : LocalTransactionState.ROLLBACK_MESSAGE;
}
模式3:Saga 模式(补偿事务)
java 复制代码
// 正向操作
public void bookTrip() {
    hotelService.reserve();      // 步骤1:预订酒店
    flightService.book();        // 步骤2:预订机票
    paymentService.charge();     // 步骤3:扣款
}

// 任一步骤失败 → 执行补偿
public void compensate(int failedStep) {
    if (failedStep <= 3) paymentService.refund();     // 退款
    if (failedStep <= 2) flightService.cancel();      // 取消机票
    if (failedStep <= 1) hotelService.cancelReserve();// 取消酒店
}
模式4:事务后置通知 + 日志表 + 重试
java 复制代码
// 1. 主事务内完成核心业务(拒收做账)
@Transactional
public void xxxReject(...) {
    // 业务操作...
    
    // 2. 注册事务提交后回调
    AfterTransactionActionCollector collector = new AfterTransactionActionCollector();
    TransactionSynchronizationManager.registerSynchronization(collector);
    collector.addCommitSyncAction(() -> {
        // 3. 事务提交后通知外部系统
        this.notifyxxx(...);
    });
}

// 4. 通知方法内记日志,失败不影响主流程
private void notifyxxx(...) {
    xxxNotifyLog log = new xxxNotifyLog(...);
    try {
        String response = openApiClient.execute(...);
        log.setStatus("T");
    } catch (Exception e) {
        log.setStatus("F");
        log.setErrorMsg(e.getMessage());
    }
    logRepository.save(log);
}

// 5. 运维接口支持重试
public void retryNotify(List<Integer> ids) {
    // 从日志表取失败记录重新通知
}

4. 模式对比

模式 一致性保证 复杂度 适用场景
本地消息表 可靠 跨服务数据同步
事务消息 可靠 低(需 RocketMQ) 订单+库存等核心链路
Saga 补偿 依赖补偿完整性 长流程多步骤事务
后置通知+重试 最终一致 通知类、非核心链路

5. 最终一致性的关键要素

复制代码
┌───────────────────────────────────────────┐
│  最终一致性 = 幂等 + 重试 + 日志 + 监控   │
└───────────────────────────────────────────┘
  • 幂等:重试不会产生副作用
  • 重试:定时任务/运维接口驱动
  • 日志:记录每次尝试的结果,支持排查
  • 监控:告警未达到一致的数据

四、容错设计

1. 超时与重试

java 复制代码
// Feign 超时配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000   # 连接超时 5s
        readTimeout: 10000     # 读超时 10s

// Feign 重试配置
@Bean
public Retryer feignRetryer() {
    // 初始间隔100ms,最大间隔1s,最多重试3次
    return new Retryer.Default(100, 1000, 3);
}

重试的前提是幂等:非幂等接口重试会导致重复操作。

2. 熔断(Circuit Breaker)

java 复制代码
// Resilience4j 熔断配置
resilience4j:
  circuitbreaker:
    instances:
      orderService:
        failureRateThreshold: 50        # 失败率达 50% 触发熔断
        waitDurationInOpenState: 30000   # 熔断 30s 后尝试半开
        slidingWindowSize: 10           # 统计窗口 10 次调用

// 使用
@CircuitBreaker(name = "orderService", fallbackMethod = "fallback")
public OrderInfo getOrder(Integer orderId) {
    return orderFeign.getOrderInfo(orderId);
}

public OrderInfo fallback(Integer orderId, Exception e) {
    log.warn("订单服务熔断, orderId={}", orderId);
    return OrderInfo.empty();  // 返回兜底数据
}

熔断器状态机:

复制代码
CLOSED(正常)→ 失败率超阈值 → OPEN(熔断,快速失败)
                                    │
                              等待超时后
                                    ↓
                              HALF_OPEN(试探)
                                    │
                    ├─ 试探成功 → CLOSED
                    └─ 试探失败 → OPEN

3. 降级(Fallback)

java 复制代码
// Feign Fallback
@FeignClient(value = "order-service", fallbackFactory = OrderFeignFallbackFactory.class)
public interface OrderFeign {
    @GetMapping("/order/{id}")
    OrderInfo getOrder(@PathVariable Integer id);
}

@Component
public class OrderFeignFallbackFactory implements FallbackFactory<OrderFeign> {
    @Override
    public OrderFeign create(Throwable cause) {
        return orderId -> {
            log.warn("订单服务降级, orderId={}, cause={}", orderId, cause.getMessage());
            return null;  // 返回 null,调用方判断处理
        };
    }
}

4. 隔离(Bulkhead)

java 复制代码
// 线程池隔离:不同服务调用使用独立线程池
resilience4j:
  thread-pool-bulkhead:
    instances:
      orderService:
        maxThreadPoolSize: 10
        coreThreadPoolSize: 5
        queueCapacity: 20

// 信号量隔离:限制并发数
resilience4j:
  bulkhead:
    instances:
      paymentService:
        maxConcurrentCalls: 20
        maxWaitDuration: 500ms

目的:一个下游服务故障不会拖垮整个系统(线程池耗尽)。

5. 异常分级处理

java 复制代码
try {
    externalService.call();
} catch (TimeoutException e) {
    // 临时故障,可重试
    retry(e);
} catch (CircuitBreakerOpenException e) {
    // 服务不可用,走降级
    return fallback();
} catch (BusinessException e) {
    // 业务异常,记录日志不重试
    log.error("业务失败", e);
    throw e;
} catch (Exception e) {
    // 未知异常,告警 + 记录
    alert(e);
    throw e;
}

6. 容错设计的层次

复制代码
┌─────────────────────────────────────┐
│           客户端层                    │
│  超时设置 → 重试 → 熔断 → 降级       │
├─────────────────────────────────────┤
│           服务层                      │
│  限流 → 隔离 → 优雅降级             │
├─────────────────────────────────────┤
│           数据层                      │
│  主从切换 → 读写分离 → 缓存兜底      │
└─────────────────────────────────────┘

五、总结:分布式系统设计核心原则

原则 含义 实现手段
互斥性 共享资源同一时刻只有一个操作者 分布式锁
幂等性 重复操作不产生副作用 状态机/唯一键/Token
最终一致性 允许短暂不一致,最终收敛 消息表/补偿/重试
容错性 部分失败不影响整体 熔断/降级/隔离
可观测性 能发现和定位问题 日志/监控/告警
可运维性 问题可人工干预恢复 重试接口/补偿工具
相关推荐
ACP广源盛139246256733 小时前
蚂蚁百灵 Ling‑3.0‑flash 开源 + 昇腾 0‑Day 原生适配@ACP#GSV9001E 在国产算力矩阵中的机会与落地场景
大数据·人工智能·分布式·单片机·嵌入式硬件
汽车仪器仪表相关领域7 小时前
KRYPTON坚固型IP67 EtherCAT总线数据采集模块:工业级分布式采集方案
分布式·功能测试·汽车·压力测试·可用性测试
霸道流氓气质9 小时前
Java中信号量(Semaphore):从本地到分布式
java·开发语言·分布式
naierfengdian11 小时前
分布式风电助力乡村能源转型的技术路径
分布式·能源
霸道流氓气质12 小时前
RedLock:Redis 分布式锁的高可用方案
数据库·redis·分布式
hhzz12 小时前
《深度学习框架PyTorch入门与实践》系列:09-分布式与并行训练之DataParallel、DDP与Horovod
pytorch·分布式·深度学习
城管不管1 天前
重生——第九次面试2026.8.13某车一面
分布式·ai·面试·职场和发展·rabbitmq
ly76891 天前
分布式一致性算法详解:从 2PC、3PC 到 Paxos、Raft、ZAB
分布式·算法
珠***格1 天前
通信链路全打通:西格电力四可装置的 4G/5G + 加密认证技术详解
大数据·数据库·分布式·5g·架构·能源