分布式系统设计:技术解析与实践
一、分布式锁
1. 为什么需要分布式锁
单体应用中,synchronized 或 ReentrantLock 可以保证同一 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 |
| 最终一致性 | 允许短暂不一致,最终收敛 | 消息表/补偿/重试 |
| 容错性 | 部分失败不影响整体 | 熔断/降级/隔离 |
| 可观测性 | 能发现和定位问题 | 日志/监控/告警 |
| 可运维性 | 问题可人工干预恢复 | 重试接口/补偿工具 |