分布式系统核心问题
核心要点:分布式系统通过网络协调多个节点完成业务能力,带来了网络不可靠、数据一致性、幂等性、分布式锁等核心挑战。理解这些问题是构建可靠分布式系统的基础。
一、分布式系统概述
1.1 什么是分布式系统
分布式系统(Distributed System) 是由多个独立的计算节点组成的系统,这些节点通过网络通信和协作,对外呈现为一个统一的系统。
核心特征:
- 多节点:系统由多个独立的计算节点组成
- 网络通信:节点之间通过网络进行通信和协作
- 透明性:对用户而言,系统看起来像是一个单一的系统
- 可扩展性:可以通过增加节点来扩展系统容量
- 容错性:部分节点故障不影响整个系统的运行
单体架构 vs 分布式架构:
| 特性 | 单体架构 | 分布式架构 |
|---|---|---|
| 部署 | 单一进程 | 多个独立进程 |
| 扩展 | 垂直扩展(升级硬件) | 水平扩展(增加节点) |
| 通信 | 方法调用 | 网络调用(RPC/HTTP) |
| 数据一致性 | 本地事务 | 分布式事务 |
| 故障影响 | 全局故障 | 局部故障 |
| 开发复杂度 | 低 | 高 |
1.2 为什么需要分布式系统
1. 业务规模增长
单体架构的限制:
- 代码库庞大,编译部署缓慢
- 单机性能瓶颈,无法满足高并发需求
- 团队协作困难,冲突频繁
- 技术栈受限,无法灵活选择
分布式架构的优势:
- 服务拆分,独立部署和扩展
- 水平扩展,应对流量增长
- 团队独立开发,提高效率
- 技术选型灵活,因地制宜2. 高可用需求
单体架构:单点故障
┌─────┐
│ App │ ──── 挂掉 → 全系统不可用
└─────┘
分布式架构:容错设计
┌─────┐
│ App │ ──── 挂掉 → 其他节点接管
└─────┘
↓
┌─────┐
│ App │ ──── 继续服务
└─────┘3. 技术演进
技术栈演进:
单体应用 → 垂直拆分 → 分布式服务 → 微服务架构 → 云原生架构1.3 分布式系统的核心挑战
真正难的地方,不是"把应用拆成多个服务",而是拆开之后的问题:
-
网络不可靠
- 网络延迟、超时、丢包
- 网络分区、脑裂
-
数据一致性
- 多节点数据同步
- 分布式事务
-
幂等性
- 重复请求处理
- 消息重复消费
-
分布式锁
- 资源竞争协调
- 锁的可靠性
-
故障处理
- 节点故障检测
- 故障转移和恢复
二、网络不可靠
2.1 为什么网络不可靠
单机 vs 分布式:
单机场景:
┌────────────┐
│ 应用 │
│ ┌──────┐ │
│ │方法A │ │ ← 调用可靠,立即返回
│ └──────┘ │
│ ┌──────┐ │
│ │方法B │ │ ← 内存访问,稳定
│ └──────┘ │
└────────────┘
分布式场景:
┌─────┐ 网络 ┌─────┐
│服务A│ ←──────────────→ │服务B│
└─────┘ ↑ 网络不可靠 └─────┘
│
├─ 超时
├─ 丢包
├─ 延迟
├─ 网络分区
└─ 部分失败网络不可靠的表现:
-
超时(Time Out)
java// 服务调用超时 try { userService.getUser(id); // 超时 3 秒 } catch (TimeoutException e) { // 不知道服务是否执行成功 // 1. 服务未执行 // 2. 服务执行成功,但响应超时 // 3. 服务执行失败 } -
丢包(Packet Loss)
java// 数据包丢失 // 请求发送,但未到达目标 // 响应发送,但在途中丢失 -
延迟(Latency)
java// 网络延迟导致响应缓慢 // 影响用户体验 // 可能触发超时机制 -
网络分区(Network Partition)
java// 网络分区导致部分节点无法通信 // ┌────────┐ ╳ ┌────────┐ // │ 节点A │ │ 节点B │ // └────────┘ └────────┘ // ↑ 网络分区,无法通信
2.2 超时问题
1. 超时的三种情况
// 调用远程服务
public User getUser(Long id) {
try {
// 超时时间 3 秒
return userService.getUser(id);
} catch (TimeoutException e) {
// 三种情况:
// 1. 请求未到达服务提供方
// 2. 服务提供方已执行,但响应超时
// 3. 服务提供方执行失败,未返回响应
// 如何处理?
// - 重试?可能导致重复执行
// - 查询?可能查不到结果
// - 放弃?可能导致数据不一致
}
}2. 超时策略
// 设置合理的超时时间
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. 网络分区的后果
正常情况:
┌──────┐ ←─────→ ┌──────┐
│节点A │ │节点B │
└──────┘ ←─────→ └──────┘
↑ ↑
└────────┬────────┘
数据同步
网络分区:
┌──────┐ ╳ ┌──────┐
│节点A │ │节点B │
└──────┘ └──────┘
↑ ↑
│ │
└───── 数据不一致 ─┘网络分区导致的问题:
- 数据不一致(节点 A 和节点 B 的数据不同)
- 脑裂(两个节点都认为自己是主节点)
- 服务不可用(部分节点无法提供服务)
2. 应对策略
// 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. 超时意识
// × 错误:没有设置超时
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. 重试意识
// × 错误:失败后直接放弃
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. 降级意识
// × 错误:依赖服务不可用导致整个系统不可用
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)
// 单机事务:简单、可靠
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 1. 扣减账户A余额
accountMapper.decreaseBalance(fromId, amount);
// 2. 增加账户B余额
accountMapper.increaseBalance(toId, amount);
// 要么全部成功,要么全部回滚
}2. 分布式事务的复杂性
// 分布式事务:复杂、不可靠
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)
定义: 任何时刻所有节点的数据都是一致的。
特点:
- 数据立即同步到所有节点
- 读取操作总是返回最新数据
- 性能较低,延迟较高
适用场景:
- 金融交易
- 库存扣减
- 订单创建
实现方式:
// 两阶段提交(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)
定义: 系统保证在没有新的更新的情况下,最终所有节点的数据会达到一致状态。
特点:
- 允许短暂的数据不一致
- 最终数据会收敛到一致状态
- 性能较高,延迟较低
适用场景:
- 社交动态
- 消息通知
- 日志收集
实现方式:
// 基于消息的最终一致性
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)。
C (Consistency)
一致性
/ \
/ \
/ \
/ CAP \
/ \
A ───────── P
可用性 分区容错性1. CAP 三要素
| 要素 | 含义 | 说明 |
|---|---|---|
| C (Consistency) | 一致性 | 所有节点看到的数据是一致的 |
| A (Availability) | 可用性 | 每个请求都能在合理时间内得到响应 |
| P (Partition Tolerance) | 分区容错性 | 网络分区发生时,系统仍能运行 |
2. 为什么不能同时满足
场景:网络分区发生
节点A ──────╳────── 节点B
│ │
│ │
↓ ↓
客户端1 客户端2
问题:
- 如果选择 C:拒绝客户端请求,保证一致性,但牺牲可用性
- 如果选择 A:接受客户端请求,保证可用性,但牺牲一致性3. 常见系统的 CAP 选择
| 系统 | 选择 | 说明 |
|---|---|---|
| CA | 单机数据库 | 不考虑分区容错(单机不存在分区问题) |
| CP | Zookeeper、HBase | 保证一致性和分区容错,牺牲可用性 |
| AP | Cassandra、DynamoDB | 保证可用性和分区容错,牺牲一致性 |
3.4 BASE 理论
BASE 理论 是 CAP 定理的补充,更贴近互联网业务场景。
1. BASE 三要素
| 要素 | 含义 | 说明 |
|---|---|---|
| BA (Basically Available) | 基本可用 | 系统出现故障时,允许损失部分可用性 |
| S (Soft State) | 软状态 | 允许系统存在中间状态,不影响整体可用性 |
| E (Eventually Consistent) | 最终一致 | 系统最终会达到一致状态 |
2. BASE vs ACID
| 特性 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致性 | 最终一致性 |
| 可用性 | 低 | 高 |
| 性能 | 低 | 高 |
| 复杂度 | 低 | 高 |
| 适用场景 | 金融交易 | 互联网应用 |
3. BASE 的实现
// 基本可用:降级处理
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 一致性方案选择
业务里更重要的问题不是"哪种理论更高级",而是:
-
这个场景到底需要多强的一致性?
- 金融交易:强一致性
- 社交动态:最终一致性
- 日志收集:弱一致性
-
业务能接受多长时间的不一致窗口?
- 实时:几秒内
- 准实时:几分钟内
- 延迟:几小时内
一致性级别选择决策树:
是否需要强一致性?
├─ 是 → 选择 CP 方案
│ ├─ 数据库主从复制
│ ├─ 分布式锁
│ └─ 两阶段提交
│
└─ 否 → 选择 AP 方案
├─ 允许不一致时间窗口?
│ ├─ 短(秒级) → 最终一致性
│ │ ├─ 消息队列
│ │ ├─ 异步复制
│ │ └─ 补偿机制
│ │
│ └─ 长(分钟级) → 弱一致性
│ ├─ 定时同步
│ └─ 定期补偿四、幂等性
4.1 什么是幂等性
幂等性(Idempotency) 是指同一个操作执行多次与执行一次的效果相同。
公式表示:
f(x) = f(f(x))实际意义:
- 消息重试不会导致业务结果反复变化
- 请求超时重发不会导致重复操作
- 前端重复点击不会导致重复提交
4.2 为什么需要幂等性
1. 消息重试
// 消息队列可能重复投递
@RabbitListener(queues = "order.created")
public void handleOrderCreated(Order order) {
// 问题:如果这条消息重复投递,会导致重复扣减库存
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
}2. 请求超时重发
// 客户端超时重试
public void createOrder(Order order) {
try {
orderService.createOrder(order);
} catch (TimeoutException e) {
// 客户端重试,可能导致重复创建订单
createOrder(order);
}
}3. 前端重复点击
// 用户快速点击两次按钮
// 可能导致重复提交
$("#submit").click(function() {
$.post("/order/create", orderData);
});4.3 幂等性实现方案
1. 业务单号(唯一 ID)
// 使用业务单号实现幂等
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. 唯一索引
-- 数据库唯一索引
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
// 幂等 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. 状态机约束
// 使用状态机实现幂等
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. 天然幂等操作
// 查询操作:天然幂等
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. 非幂等操作需要设计
// × 非幂等:累加操作
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. 数据库锁
-- 基于数据库的唯一索引
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 分布式锁
// 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 分布式锁
// 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 分布式锁的常见误区
误区一:一遇到并发问题就想上分布式锁
问题: 很多人把分布式锁当成解决并发问题的万能钥匙。
实际情况: 很多问题更适合用业务约束和状态机解决。
// × 错误:使用分布式锁
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);
}误区二:分布式锁是银弹
问题: 认为有了分布式锁就能保证数据一致性。
实际情况: 分布式锁本身也有可靠性问题。
// 分布式锁的问题:
// 1. Redis 单节点故障,锁丢失
// 2. 客户端获取锁后崩溃,锁无法释放
// 3. 锁超时时间设置不合理
// 解决方案:
// 1. 使用 RedLock 算法(多节点)
// 2. 设置合理的超时时间
// 3. 使用看门狗机制自动续期误区三:长事务适合用分布式锁
问题: 在长事务中使用分布式锁,导致锁持有时间过长。
实际情况: 长事务会导致锁超时,引发数据不一致。
// × 错误:长事务 + 分布式锁
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. 锁的超时时间要合理
// 设置合理的超时时间
public class LockConfig {
// 业务执行时间 + 缓冲时间
private long lockTimeout = businessTimeout * 1.5;
}2. 锁的粒度要细
// × 粗粒度锁
String lockKey = "lock:order";
// √ 细粒度锁
String lockKey = "lock:order:" + orderId;3. 使用锁重试机制
// 带重试的加锁
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. 使用看门狗机制
// Redisson 看门狗机制
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认 30 秒,看门狗自动续期
// 业务执行期间,看门狗会自动续期
// 业务执行完毕,主动释放锁
lock.unlock();六、常见误区与注意事项
6.1 误区一:把重试当成万能修复手段
错误认识: 遇到失败就重试,重试能解决所有问题。
实际情况:
- 重试可能导致重复执行
- 重试可能放大故障(雪崩效应)
- 重试需要配合幂等设计
// × 错误:无限重试
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 定理,但不知道如何应用到实际业务。
实际情况: 不同业务场景对一致性和可用性的要求不同。
示例:
电商系统的不同场景:
1. 下单场景
- 需求:强一致性(不能超卖)
- 选择:CP 方案
- 实现:分布式锁 + 数据库事务
2. 商品浏览场景
- 需求:高可用性
- 选择:AP 方案
- 实现:缓存 + 异步更新
3. 订单查询场景
- 需求:最终一致性
- 选择:AP 方案
- 实现:读写分离 + 异步复制6.3 误区三:没有补偿机制,却说系统是最终一致
错误认识: 使用异步消息就说系统是最终一致性,但没有补偿机制。
实际情况: 最终一致性需要补偿机制保证数据最终收敛。
// × 错误:只发消息,没有补偿
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 误区四:把"理论一致性"当成"业务真的可落地"
错误认识: 理论上可行的一致性方案,实际上难以落地。
实际情况: 一致性方案需要考虑成本、复杂度、团队技术能力。
示例:
Paxos 算法 vs Raft 算法:
Paxos 算法:
- 理论:正确性已证明
- 实际:难以理解和实现
- 落地:很少有团队能正确实现
Raft 算法:
- 理论:正确性已证明
- 实际:易于理解和实现
- 落地:被广泛使用(Etcd、Consul)
结论:选择适合团队能力的方案七、实战场景
7.1 场景一:订单、库存、支付协作
问题描述: 一个下单操作往往会跨多个系统,如何保证数据一致性?
挑战:
- 订单创建成功,库存扣减失败
- 库存扣减成功,支付失败
- 支付成功,订单状态未更新
解决方案:
1. 基于 TCC 的分布式事务
// 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. 基于消息的最终一致性
// 基于消息的最终一致性
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 场景二:分布式任务调度
问题描述: 多个实例同时运行定时任务,如何避免重复执行?
挑战:
- 同一个任务被多个实例执行
- 某个节点挂掉后任务无法继续
解决方案:
// 使用分布式锁实现任务调度
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. 重复消费:幂等设计
// 消息幂等处理
@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. 顺序问题:分区队列
// 顺序消息:同一订单的消息发送到同一队列
public void sendOrderMessage(Long orderId, OrderEvent event) {
// 根据 orderId 计算队列编号
int queueIndex = (int) (orderId % queueCount);
String queueName = "order.queue." + queueIndex;
rabbitTemplate.convertAndSend(queueName, event);
}3. 消息丢失:确认机制
// 消息确认机制
@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、唯一索引、状态检查 | 业务单号、状态机 |
| 作用范围 | 请求层 | 业务层 |
| 重复请求 | 直接拒绝 | 正常处理,结果相同 |
示例:
// 防重:Token 机制
if (!checkToken(token)) {
throw new RuntimeException("重复请求");
}
// 幂等:业务单号
if (orderMapper.exists(orderNo)) {
return; // 已存在,直接返回
}
createOrder(order);Q2:为什么分布式系统里超时很重要?
答案:
- 避免资源耗尽: 不设置超时,线程可能永远阻塞
- 快速失败: 超时可以快速发现故障,避免雪崩
- 故障隔离: 超时后可以降级,避免影响其他服务
- 用户体验: 超时可以给用户更快的反馈
最佳实践:
// 设置合理的超时时间
public class TimeoutConfig {
private int connectTimeout = 3000; // 连接超时
private int readTimeout = 5000; // 读取超时
private int totalTimeout = 10000; // 总超时
}Q3:为什么一致性方案要按业务分级?
答案: 不同业务对一致性的要求不同,需要根据业务特点选择合适的方案。
一致性分级:
| 级别 | 一致性要求 | 典型场景 | 解决方案 |
|---|---|---|---|
| 强一致 | 数据必须立即一致 | 金融交易、库存扣减 | 分布式事务、分布式锁 |
| 最终一致 | 允许短暂不一致 | 订单状态、消息通知 | 消息队列、补偿机制 |
| 弱一致 | 允许不一致 | 日志收集、统计报表 | 异步同步、定期补偿 |
Q4:为什么分布式锁不是默认答案?
答案: 很多并发问题更适合用业务约束和状态机解决,分布式锁只是其中一种方案。
方案对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 分布式锁 | 临界区保护 | 实现简单 | 性能低,可靠性依赖锁服务 |
| 状态机 | 状态转换控制 | 可靠性高 | 需要设计状态转换规则 |
| 幂等设计 | 重复请求处理 | 性能高 | 需要额外字段 |
最佳实践:
// 优先选择业务约束
if (order.getStatus() == PAID) {
return; // 已支付,幂等
}
// 其次选择状态机
if (!canTransition(order.getStatus(), newStatus)) {
throw new IllegalStateException("非法状态转换");
}
// 最后选择分布式锁
distributedLock.lock("order:" + orderId);九、总结
9.1 核心知识点总结
分布式系统核心挑战
- 网络不可靠: 超时、丢包、延迟、分区
- 数据一致性: 强一致、最终一致、弱一致
- 幂等性: 重复请求处理、消息重复消费
- 分布式锁: 资源竞争协调、可靠性保证
理论基础
- CAP 定理: 一致性、可用性、分区容错性三者只能满足其二
- BASE 理论: 基本可用、软状态、最终一致性
实现方案
- 一致性: 2PC、TCC、消息队列、补偿机制
- 幂等性: 业务单号、唯一索引、幂等 Token、状态机
- 分布式锁: 数据库锁、Redis 锁、Zookeeper 锁
9.2 学习路径建议
初级阶段
- 理解分布式系统的基本概念
- 掌握 CAP 和 BASE 理论
- 了解常见的一致性问题
中级阶段
- 掌握幂等性设计和实现
- 学会使用分布式锁
- 理解分布式事务的实现方式
高级阶段
- 掌握分布式系统的设计模式
- 学会分布式系统的性能优化
- 理解分布式系统的可靠性保障
9.3 实战理解题
题目 1:设计一个秒杀系统
要求: 保证不超卖,支持高并发。
提示:
- 使用分布式锁保证库存扣减的原子性
- 使用幂等设计防止重复下单
- 使用消息队列削峰填谷
题目 2:实现分布式事务
要求: 跨多个服务的业务操作,保证数据一致性。
提示:
- 使用 TCC 模式实现分布式事务
- 设计 Try、Confirm、Cancel 三个阶段
- 实现补偿机制
题目 3:实现消息幂等消费
要求: 消息可能重复投递,保证业务不会重复执行。
提示:
- 使用消息 ID 实现幂等
- 在数据库中记录已处理的消息
- 使用唯一索引防止重复插入
参考资料
-
经典论文
-
经典书籍
- 《分布式系统原理与范型》- Andrew S. Tanenbaum
- 《数据密集型应用系统设计》- Martin Kleppmann
-
开源项目
最后更新: 2026-03-29
维护人: AI 助手
版本差异(技术原理说明)
| 维度 | 说明 |
|---|---|
| 技术原理 | 分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效 |
| 落地选型 | 新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容) |
| Java 版本 | 示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容 |
本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。