refactor: rename files to Chinese and organize by category
Organized 50 interview questions into 12 categories: - 01-分布式系统 (9 files): 分布式事务, 分布式锁, 一致性哈希, CAP理论, etc. - 02-数据库 (2 files): MySQL索引优化, MyBatis核心原理 - 03-缓存 (5 files): Redis数据结构, 缓存问题, LRU算法, etc. - 04-消息队列 (1 file): RocketMQ/Kafka - 05-并发编程 (4 files): 线程池, 设计模式, 限流策略, etc. - 06-JVM (1 file): JVM和垃圾回收 - 07-系统设计 (8 files): 秒杀系统, 短链接, IM, Feed流, etc. - 08-算法与数据结构 (4 files): B+树, 红黑树, 跳表, 时间轮 - 09-网络与安全 (3 files): TCP/IP, 加密安全, 性能优化 - 10-中间件 (4 files): Spring Boot, Nacos, Dubbo, Nginx - 11-运维 (4 files): Kubernetes, CI/CD, Docker, 可观测性 - 12-面试技巧 (1 file): 面试技巧和职业规划 All files renamed to Chinese for better accessibility and organized into categorized folders for easier navigation. Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
439
questions/01-分布式系统/CAP理论和BASE理论.md
Normal file
439
questions/01-分布式系统/CAP理论和BASE理论.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# CAP 理论和 BASE 理论
|
||||
|
||||
## 问题
|
||||
|
||||
1. 什么是 CAP 理论?CAP 三者为什么不可兼得?
|
||||
2. 什么是 BASE 理论?
|
||||
3. CP、AP、AP 架构分别适用于什么场景?
|
||||
4. Zookeeper、Eureka、Nacos、Consul 分别是 CP 还是 AP?
|
||||
5. 在实际项目中如何权衡一致性、可用性、分区容错性?
|
||||
|
||||
---
|
||||
|
||||
## 标准答案
|
||||
|
||||
### 1. CAP 理论
|
||||
|
||||
#### **定义**
|
||||
|
||||
CAP 是指分布式系统中的三个核心指标:
|
||||
|
||||
| 指标 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **Consistency(一致性)** | 所有节点在同一时间看到相同的数据 | 写入后,所有节点立即读取到新数据 |
|
||||
| **Availability(可用性)** | 系统持续提供服务,每个请求都能得到响应 | 即使部分节点故障,系统仍能响应 |
|
||||
| **Partition Tolerance(分区容错性)** | 系统在网络分区时仍能继续运行 | 节点间网络断开,系统仍能工作 |
|
||||
|
||||
---
|
||||
|
||||
#### **CAP 定理**
|
||||
|
||||
**一个分布式系统最多只能同时满足两项**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ CAP 三选二 │
|
||||
│ │
|
||||
│ CA ────┐ │
|
||||
│ │ │
|
||||
│ ╱ │ ╲ │
|
||||
│ ╱ │ ╲ │
|
||||
│ ╱ │ ╲ │
|
||||
│ ●────────●──────● │
|
||||
│ C P A │
|
||||
│ │
|
||||
│ CP AP CA │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**为什么?**(证明)
|
||||
```
|
||||
场景:两个节点 N1、N2,网络分区(P)
|
||||
|
||||
情况 1:保证 C(一致性)
|
||||
├─ N1 写入数据
|
||||
└─ 为了保证一致性,N2 必须拒绝读取(牺牲 A)
|
||||
|
||||
情况 2:保证 A(可用性)
|
||||
├─ N1 写入数据
|
||||
└─ 为了保证可用性,N2 返回旧数据(牺牲 C)
|
||||
|
||||
结论:在有分区(P)的情况下,C 和 A 无法同时满足
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **CP、AP、CA 架构**
|
||||
|
||||
**1. CP(一致性 + 分区容错)**
|
||||
|
||||
**特点**:
|
||||
- 保证数据一致性
|
||||
- 分区时部分节点不可用
|
||||
|
||||
**适用场景**:
|
||||
- 金融系统(转账、支付)
|
||||
- 库存系统(超卖不可接受)
|
||||
|
||||
**代表系统**:
|
||||
- Zookeeper(CP)
|
||||
- HBase(CP)
|
||||
- Redis Cluster(CP,主从切换时短暂不可用)
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// Zookeeper 写入流程
|
||||
client.setData("/node", data);
|
||||
// 等待大多数节点确认
|
||||
// 如果网络分区,部分节点无法写入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**2. AP(可用性 + 分区容错)**
|
||||
|
||||
**特点**:
|
||||
- 保证系统可用
|
||||
- 分区时可能读到脏数据
|
||||
|
||||
**适用场景**:
|
||||
- 社交媒体(点赞、评论)
|
||||
- 内容分发(CDN)
|
||||
- 用户行为统计
|
||||
|
||||
**代表系统**:
|
||||
- Cassandra(AP)
|
||||
- DynamoDB(AP)
|
||||
- Eureka(AP)
|
||||
- DNS(AP)
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// Cassandra 写入
|
||||
session.execute("INSERT INTO users (id, name) VALUES (1, 'Alice')");
|
||||
// 写入成功立即返回
|
||||
// 数据可能尚未复制到其他节点(最终一致性)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3. CA(一致性 + 可用性)**
|
||||
|
||||
**注意**:**在分布式系统中,CA 不存在**(因为网络分区不可避免)。
|
||||
|
||||
**CA 存在于**:
|
||||
- 单机系统(RDBMS)
|
||||
- 传统关系型数据库(MySQL、PostgreSQL)
|
||||
|
||||
---
|
||||
|
||||
### 2. BASE 理论
|
||||
|
||||
#### **定义**
|
||||
|
||||
BASE 是对 CAP 中 AP 方案的补充,通过**牺牲强一致性**来获得**高可用性**。
|
||||
|
||||
| 指标 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **Basically Available(基本可用)** | 系统出现故障时,允许损失部分可用性 | 秒杀时拒绝部分请求 |
|
||||
| **Soft state(软状态)** | 允许数据存在中间状态 | 订单状态:待支付 → 已支付 |
|
||||
| **Eventually consistent(最终一致性)** | 数据最终会达到一致状态 | 支付后 1 秒内到账 |
|
||||
|
||||
---
|
||||
|
||||
#### **BASE vs ACID**
|
||||
|
||||
| 特性 | ACID(传统数据库) | BASE(NoSQL) |
|
||||
|------|-------------------|---------------|
|
||||
| **一致性** | 强一致性(立即) | 最终一致性(延迟) |
|
||||
| **可用性** | 可能(锁、事务) | 高(无锁、异步) |
|
||||
| **隔离性** | 严格(锁) | 松散 |
|
||||
| **持久性** | 强 | 弱(可能丢失) |
|
||||
|
||||
---
|
||||
|
||||
#### **最终一致性的实现**
|
||||
|
||||
**1. 读时修复(Read Repair)**:
|
||||
```java
|
||||
// 读取时检查一致性
|
||||
public User getUser(Long userId) {
|
||||
// 从多个节点读取
|
||||
User user1 = node1.get(userId);
|
||||
User user2 = node2.get(userId);
|
||||
|
||||
// 发现不一致,修复
|
||||
if (!user1.equals(user2)) {
|
||||
// 使用版本号或时间戳决定哪个更新
|
||||
User latest = user1.getVersion() > user2.getVersion() ? user1 : user2;
|
||||
|
||||
// 异步修复旧数据
|
||||
asyncRepair(latest);
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
return user1;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**2. 写时修复(Write Repair)**:
|
||||
```java
|
||||
// 写入时同步到所有节点
|
||||
public void saveUser(User user) {
|
||||
// 写入主节点
|
||||
masterNode.save(user);
|
||||
|
||||
// 异步同步到从节点
|
||||
for (Node slave : slaves) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
slave.save(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3. 异步复制**:
|
||||
```
|
||||
主节点收到写入
|
||||
↓
|
||||
立即返回成功
|
||||
↓
|
||||
异步复制到从节点
|
||||
↓
|
||||
最终一致
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 主流注册中心对比
|
||||
|
||||
#### **Zookeeper(CP)**
|
||||
|
||||
**特点**:
|
||||
- 保证一致性(ZAB 协议)
|
||||
- Leader 挂了会重新选举(期间不可用)
|
||||
|
||||
**适用场景**:
|
||||
- 需要强一致性
|
||||
- 对可用性要求不高(如配置中心)
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// Zookeeper 注册
|
||||
zk.create("/services/order/192.168.1.10:8080", data, ZooDefs.Ids.OPEN, NodeMode.EPHEMERAL);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Eureka(AP)**
|
||||
|
||||
**特点**:
|
||||
- 保证可用性
|
||||
- 客户端缓存注册信息
|
||||
- 网络分区时仍能提供服务
|
||||
|
||||
**适用场景**:
|
||||
- 对可用性要求高
|
||||
- 可容忍短暂的服务发现不准确
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// Eureka 注册
|
||||
@Bean
|
||||
public EurekaInstanceBeanBean eurekaInstanceBean(InetAddress inetAddress) {
|
||||
EurekaInstanceBeanBean bean = new EurekaInstanceBeanBean();
|
||||
bean.setHostname(inetAddress.getHostAddress());
|
||||
bean.setAppName("order-service");
|
||||
bean.setNonSecurePort(8080);
|
||||
return bean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Nacos(AP + CP)**
|
||||
|
||||
**特点**:
|
||||
- 支持 AP 和 CP 切换
|
||||
- 默认 AP(临时实例)
|
||||
- 可配置 CP(持久化实例)
|
||||
|
||||
**配置**:
|
||||
```yaml
|
||||
# Nacos 配置
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
ephemeral: true # true=AP, false=CP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Consul(CP)**
|
||||
|
||||
**特点**:
|
||||
- 保证一致性(Raft 协议)
|
||||
- 支持 KV 存储
|
||||
- 支持健康检查
|
||||
|
||||
**适用场景**:
|
||||
- 服务发现
|
||||
- 配置中心
|
||||
- 分布式锁
|
||||
|
||||
---
|
||||
|
||||
### 4. 实际项目应用
|
||||
|
||||
#### **场景 1:订单系统(CP)**
|
||||
|
||||
**需求**:
|
||||
- 不能超卖
|
||||
- 库存数据必须准确
|
||||
|
||||
**方案**:
|
||||
```
|
||||
1. 使用分布式锁(Redis、Zookeeper)
|
||||
2. 数据库使用强一致性事务
|
||||
3. 库存扣减使用串行化
|
||||
```
|
||||
|
||||
**代码**:
|
||||
```java
|
||||
@Transactional
|
||||
public void createOrder(Order order) {
|
||||
// 1. 获取分布式锁
|
||||
RLock lock = redissonClient.getLock("product:" + order.getProductId());
|
||||
lock.lock();
|
||||
|
||||
try {
|
||||
// 2. 查询库存
|
||||
Product product = productMapper.selectById(order.getProductId());
|
||||
if (product.getStock() < order.getQuantity()) {
|
||||
throw new BusinessException("库存不足");
|
||||
}
|
||||
|
||||
// 3. 扣减库存
|
||||
product.setStock(product.getStock() - order.getQuantity());
|
||||
productMapper.updateById(product);
|
||||
|
||||
// 4. 创建订单
|
||||
orderMapper.insert(order);
|
||||
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **场景 2:社交点赞(AP)**
|
||||
|
||||
**需求**:
|
||||
- 高并发点赞
|
||||
- 允许点赞数短暂不准确
|
||||
|
||||
**方案**:
|
||||
```
|
||||
1. 先更新缓存(Redis)
|
||||
2. 异步同步到数据库
|
||||
3. 最终一致(1 秒内)
|
||||
```
|
||||
|
||||
**代码**:
|
||||
```java
|
||||
public void like(Long userId, Long postId) {
|
||||
// 1. 更新缓存(立即返回)
|
||||
redisTemplate.opsForSet().add("post:" + postId + ":likes", userId);
|
||||
|
||||
// 2. 异步更新数据库
|
||||
CompletableFuture.runAsync(() -> {
|
||||
likeMapper.insert(new Like(userId, postId));
|
||||
});
|
||||
}
|
||||
|
||||
public Long getLikeCount(Long postId) {
|
||||
// 1. 先查缓存
|
||||
Long count = redisTemplate.opsForSet().size("post:" + postId + ":likes");
|
||||
|
||||
// 2. 如果缓存不存在,查数据库
|
||||
if (count == null || count == 0) {
|
||||
count = likeMapper.countByPostId(postId);
|
||||
redisTemplate.opsForValue().set("post:" + postId + ":count", count, 1, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **场景 3:库存同步(最终一致性)**
|
||||
|
||||
**需求**:
|
||||
- 多个仓库库存同步
|
||||
- 允许短暂不一致
|
||||
|
||||
**方案**:
|
||||
```
|
||||
1. 使用消息队列(RocketMQ)
|
||||
2. 事务消息保证不丢失
|
||||
3. 消费者重试保证最终一致
|
||||
```
|
||||
|
||||
**代码**:
|
||||
```java
|
||||
// 1. 发送事务消息
|
||||
@Transactional
|
||||
public void updateStock(Long productId, int quantity) {
|
||||
// 更新数据库
|
||||
productMapper.updateStock(productId, quantity);
|
||||
|
||||
// 发送事务消息
|
||||
Message msg = new Message("StockTopic", "StockUpdate",
|
||||
JSON.toJSONString(new StockUpdateEvent(productId, quantity)).getBytes());
|
||||
rocketMQTemplate.sendMessageInTransaction(msg, null);
|
||||
}
|
||||
|
||||
// 2. 消费消息
|
||||
@RocketMQMessageListener(topic = "StockTopic", consumerGroup = "stock-consumer")
|
||||
public class StockConsumer implements RocketMQListener<StockUpdateEvent> {
|
||||
@Override
|
||||
public void onMessage(StockUpdateEvent event) {
|
||||
// 同步到其他仓库
|
||||
warehouseService.syncStock(event.getProductId(), event.getQuantity());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 阿里 P7 加分项
|
||||
|
||||
**深度理解**:
|
||||
- 理解 CAP 理论的局限性(如网络延迟、时钟问题)
|
||||
- 理解各种一致性协议(Paxos、Raft、ZAB)
|
||||
- 理解分布式事务的权衡
|
||||
|
||||
**实战经验**:
|
||||
- 有设计 CP 或 AP 系统的经验
|
||||
- 有处理数据不一致问题的经验
|
||||
- 有最终一致性调优的经验
|
||||
|
||||
**架构能力**:
|
||||
- 能根据业务特点选择合适的架构
|
||||
- 能设计混合架构(部分 CP、部分 AP)
|
||||
- 能设计数据修复和补偿机制
|
||||
|
||||
**技术选型**:
|
||||
- 了解各种注册中心和存储系统的 CAP 特性
|
||||
- 能根据业务特点选择合适的技术
|
||||
- 有分布式系统设计经验
|
||||
1378
questions/01-分布式系统/MySQL主从延迟.md
Normal file
1378
questions/01-分布式系统/MySQL主从延迟.md
Normal file
File diff suppressed because it is too large
Load Diff
427
questions/01-分布式系统/一致性哈希.md
Normal file
427
questions/01-分布式系统/一致性哈希.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# 一致性哈希算法
|
||||
|
||||
## 问题
|
||||
|
||||
1. 什么是一致性哈希?解决了什么问题?
|
||||
2. 一致性哈希的原理是什么?
|
||||
3. 什么是虚拟节点?为什么需要虚拟节点?
|
||||
4. 一致性哈希在负载均衡、分布式缓存中的应用
|
||||
5. 一致性哈希有哪些优缺点?
|
||||
6. 在实际项目中如何实现一致性哈希?
|
||||
|
||||
---
|
||||
|
||||
## 标准答案
|
||||
|
||||
### 1. 传统哈希的问题
|
||||
|
||||
#### **场景:分布式缓存**
|
||||
|
||||
假设有 3 台缓存服务器:
|
||||
```
|
||||
Server A, Server B, Server C
|
||||
```
|
||||
|
||||
使用传统哈希:`hash(key) % N`
|
||||
```java
|
||||
int serverIndex = hash(key) % 3; // 3 台服务器
|
||||
```
|
||||
|
||||
**问题:服务器扩容/缩容**
|
||||
|
||||
新增一台服务器(Server D):
|
||||
```
|
||||
原来:hash(key) % 3
|
||||
现在:hash(key) % 4
|
||||
|
||||
结果:大部分 key 的路由都变了!
|
||||
- 缓存全部失效
|
||||
- 数据库压力激增
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
3 台服务器时:
|
||||
hash("user:1") % 3 = 1 → Server B
|
||||
hash("user:2") % 3 = 2 → Server C
|
||||
|
||||
4 台服务器时(新增 Server D):
|
||||
hash("user:1") % 4 = 2 → Server C(变了!)
|
||||
hash("user:2") % 4 = 3 → Server D(变了!)
|
||||
|
||||
影响:75% 的缓存失效
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 一致性哈希原理
|
||||
|
||||
#### **核心思想**
|
||||
|
||||
将服务器和数据都映射到**哈希环**上:
|
||||
- 顺时针查找最近的服务器
|
||||
- 服务器变化时,只影响相邻数据
|
||||
|
||||
**哈希环**(0 - 2^32-1):
|
||||
```
|
||||
0
|
||||
↓
|
||||
Server A ──────────── Server B
|
||||
↗ ↘
|
||||
↗ ↘
|
||||
↑ ↓
|
||||
| ↓
|
||||
Server D ←────────────→ Server C
|
||||
↑
|
||||
2^32-1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **算法步骤**
|
||||
|
||||
**步骤 1:映射服务器到环**
|
||||
```java
|
||||
// 服务器 IP → 哈希值
|
||||
hash("192.168.1.10") → 1000 → Server A
|
||||
hash("192.168.1.11") → 5000 → Server B
|
||||
hash("192.168.1.12") → 10000 → Server C
|
||||
```
|
||||
|
||||
**步骤 2:映射数据到环**
|
||||
```java
|
||||
// 数据 Key → 哈希值
|
||||
hash("user:1") → 2000
|
||||
hash("user:2") → 6000
|
||||
hash("user:3") → 15000
|
||||
```
|
||||
|
||||
**步骤 3:顺时针查找服务器**
|
||||
```
|
||||
user:1 (2000) → Server B (5000) // 顺时针第一个服务器
|
||||
user:2 (6000) → Server C (10000)
|
||||
user:3 (15000) → Server A (1000 环绕)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Java 实现**
|
||||
|
||||
```java
|
||||
public class ConsistentHash<T> {
|
||||
|
||||
private final TreeMap<Long, T> ring = new TreeMap<>();
|
||||
private final int virtualNodes; // 虚拟节点数
|
||||
|
||||
public ConsistentHash(int virtualNodes) {
|
||||
this.virtualNodes = virtualNodes;
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
public void addNode(T node) {
|
||||
for (int i = 0; i < virtualNodes; i++) {
|
||||
String virtualNodeName = node.toString() + "#" + i;
|
||||
long hash = hash(virtualNodeName);
|
||||
ring.put(hash, node);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除节点
|
||||
public void removeNode(T node) {
|
||||
for (int i = 0; i < virtualNodes; i++) {
|
||||
String virtualNodeName = node.toString() + "#" + i;
|
||||
long hash = hash(virtualNodeName);
|
||||
ring.remove(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点
|
||||
public T getNode(String key) {
|
||||
if (ring.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long hash = hash(key);
|
||||
|
||||
// 顺时针查找
|
||||
Map.Entry<Long, T> entry = ring.ceilingEntry(hash);
|
||||
if (entry == null) {
|
||||
// 环绕到第一个节点
|
||||
entry = ring.firstEntry();
|
||||
}
|
||||
|
||||
return entry.getValue();
|
||||
}
|
||||
|
||||
// 哈希函数(FNV1_32_HASH)
|
||||
private long hash(String key) {
|
||||
final long p = 16777619;
|
||||
long hash = 2166136261L;
|
||||
|
||||
for (byte b : key.getBytes()) {
|
||||
hash = (hash ^ b) * p;
|
||||
}
|
||||
|
||||
hash += hash << 13;
|
||||
hash ^= hash >> 7;
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 17;
|
||||
hash += hash << 5;
|
||||
|
||||
return hash & 0xffffffffL;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
```java
|
||||
// 创建一致性哈希环
|
||||
ConsistentHash<String> consistentHash = new ConsistentHash<>(150);
|
||||
|
||||
// 添加服务器
|
||||
consistentHash.addNode("192.168.1.10"); // Server A
|
||||
consistentHash.addNode("192.168.1.11"); // Server B
|
||||
consistentHash.addNode("192.168.1.12"); // Server C
|
||||
|
||||
// 获取路由
|
||||
String server = consistentHash.getNode("user:1001");
|
||||
System.out.println("路由到服务器: " + server);
|
||||
|
||||
// 新增服务器
|
||||
consistentHash.addNode("192.168.1.13"); // Server D
|
||||
// 只有部分数据受影响
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 虚拟节点
|
||||
|
||||
#### **问题:数据倾斜**
|
||||
|
||||
**场景**:
|
||||
```
|
||||
哈希环:
|
||||
Server A (1000)
|
||||
Server B (5000)
|
||||
Server C (10000)
|
||||
|
||||
数据分布:
|
||||
A: 2000 条
|
||||
B: 500 条
|
||||
C: 500 条
|
||||
|
||||
数据倾斜:A 的负载远大于 B、C
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 节点少,哈希分布不均
|
||||
- 真实服务器数量有限
|
||||
|
||||
---
|
||||
|
||||
#### **解决方案:虚拟节点**
|
||||
|
||||
**原理**:
|
||||
每个真实节点映射多个虚拟节点:
|
||||
```
|
||||
真实节点 A:
|
||||
├─ 虚拟节点 A#1 → 1000
|
||||
├─ 虚拟节点 A#2 → 3000
|
||||
├─ 虚拟节点 A#3 → 7000
|
||||
└─ ...
|
||||
|
||||
真实节点 B:
|
||||
├─ 虚拟节点 B#1 → 2000
|
||||
├─ 虚拟节点 B#2 → 4000
|
||||
└─ ...
|
||||
|
||||
真实节点 C:
|
||||
├─ 虚拟节点 C#1 → 5000
|
||||
└─ ...
|
||||
```
|
||||
|
||||
**效果**:
|
||||
```
|
||||
数据分布:
|
||||
A: 1000 条(25%)
|
||||
B: 1000 条(25%)
|
||||
C: 1000 条(25%)
|
||||
D: 1000 条(25%)
|
||||
|
||||
均衡度:高
|
||||
```
|
||||
|
||||
**虚拟节点数量**:
|
||||
- 一般:100-150 个
|
||||
- 更多:更均衡,但内存占用大
|
||||
|
||||
---
|
||||
|
||||
### 4. 一致性哈希的应用
|
||||
|
||||
#### **应用 1:分布式缓存(Redis Cluster)**
|
||||
|
||||
```java
|
||||
// Redis 集群路由
|
||||
public class RedisClusterRouter {
|
||||
|
||||
private final ConsistentHash<JedisPool> consistentHash;
|
||||
|
||||
public RedisClusterRouter(List<JedisPool> pools) {
|
||||
this.consistentHash = new ConsistentHash<>(150);
|
||||
for (JedisPool pool : pools) {
|
||||
consistentHash.addNode(pool);
|
||||
}
|
||||
}
|
||||
|
||||
public Jedis getJedis(String key) {
|
||||
JedisPool pool = consistentHash.getNode(key);
|
||||
return pool.getResource();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 扩容/缩容影响小
|
||||
- 数据迁移量小
|
||||
|
||||
---
|
||||
|
||||
#### **应用 2:负载均衡(Nginx)**
|
||||
|
||||
```nginx
|
||||
# Nginx 一致性哈希配置
|
||||
upstream backend {
|
||||
consistent_hash $request_uri;
|
||||
|
||||
server 192.168.1.10:8080;
|
||||
server 192.168.1.11:8080;
|
||||
server 192.168.1.12:8080;
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 相同请求路由到同一服务器
|
||||
- 会话保持(无需 Sticky Session)
|
||||
|
||||
---
|
||||
|
||||
#### **应用 3:分库分表**
|
||||
|
||||
```java
|
||||
// 分库路由
|
||||
public class ShardingRouter {
|
||||
|
||||
private final ConsistentHash<String> dbRouter;
|
||||
|
||||
public ShardingRouter(List<String> databases) {
|
||||
this.dbRouter = new ConsistentHash<>(100);
|
||||
for (String db : databases) {
|
||||
dbRouter.addNode(db);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDatabase(String userId) {
|
||||
return dbRouter.getNode(userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 一致性哈希的优缺点
|
||||
|
||||
#### **优点**
|
||||
|
||||
1. **最小化数据迁移**:
|
||||
- 新增节点:只影响相邻节点数据
|
||||
- 移除节点:只影响该节点数据
|
||||
|
||||
2. **良好的容错性**:
|
||||
- 节点故障:数据自动迁移到下一个节点
|
||||
- 平滑恢复:节点恢复后数据自动迁移回来
|
||||
|
||||
3. **可扩展性**:
|
||||
- 支持动态增删节点
|
||||
- 适合大规模分布式系统
|
||||
|
||||
---
|
||||
|
||||
#### **缺点**
|
||||
|
||||
1. **数据倾斜**:
|
||||
- 节点少时分布不均
|
||||
- 需要虚拟节点解决
|
||||
|
||||
2. **实现复杂**:
|
||||
- 相比简单哈希复杂度高
|
||||
- 需要维护哈希环
|
||||
|
||||
3. **内存占用**:
|
||||
- 虚拟节点占用内存
|
||||
- 节点多时内存开销大
|
||||
|
||||
---
|
||||
|
||||
### 6. 实际项目经验
|
||||
|
||||
#### **案例:Redis 集群扩容**
|
||||
|
||||
**场景**:
|
||||
- 现有 3 台 Redis,每台 10 万 key
|
||||
- 新增 2 台 Redis
|
||||
|
||||
**不使用一致性哈希**:
|
||||
```
|
||||
迁移量:10 万 × 5/6 ≈ 8.3 万 key(83%)
|
||||
```
|
||||
|
||||
**使用一致性哈希**:
|
||||
```
|
||||
迁移量:10 万 × 2/5 ≈ 4 万 key(40%)
|
||||
```
|
||||
|
||||
**实现**:
|
||||
```java
|
||||
// 1. 新节点上线
|
||||
JedisPool newPool1 = new JedisPool("192.168.1.13");
|
||||
JedisPool newPool2 = new JedisPool("192.168.1.14");
|
||||
|
||||
consistentHash.addNode(newPool1);
|
||||
consistentHash.addNode(newPool2);
|
||||
|
||||
// 2. 数据迁移
|
||||
for (String key : allKeys) {
|
||||
JedisPool newPool = consistentHash.getNode(key);
|
||||
JedisPool oldPool = oldMapping.get(key);
|
||||
|
||||
if (newPool != oldPool) {
|
||||
// 迁移数据
|
||||
migrateData(key, oldPool, newPool);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 阿里 P7 加分项
|
||||
|
||||
**深度理解**:
|
||||
- 理解一致性哈希的数学原理(哈希函数、分布均匀性)
|
||||
- 理解虚拟节点数量对均衡度的影响
|
||||
- 了解其他哈希算法(如 Rendezvous Hash)
|
||||
|
||||
**实战经验**:
|
||||
- 有使用一致性哈希实现分库分表的经验
|
||||
- 有处理数据倾斜和迁移的经验
|
||||
- 有一致性哈希在生产环境的调优经验
|
||||
|
||||
**架构能力**:
|
||||
- 能设计支持平滑扩容的分片集群
|
||||
- 能设计数据迁移的灰度方案
|
||||
- 有一致性哈希的监控和告警经验
|
||||
|
||||
**技术选型**:
|
||||
- 了解 Redis Cluster、Cassandra 等系统的一致性哈希实现
|
||||
- 了解 Nginx、HAProxy 等负载均衡器的一致性哈希配置
|
||||
- 能根据业务特点选择合适的哈希算法
|
||||
1514
questions/01-分布式系统/事务隔离级别.md
Normal file
1514
questions/01-分布式系统/事务隔离级别.md
Normal file
File diff suppressed because it is too large
Load Diff
731
questions/01-分布式系统/分布式ID生成.md
Normal file
731
questions/01-分布式系统/分布式ID生成.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# 分布式 ID 生成方案
|
||||
|
||||
## 问题
|
||||
|
||||
1. 为什么需要分布式 ID?分布式 ID 有哪些要求?
|
||||
2. 常见的分布式 ID 生成方案有哪些?各自的优缺点是什么?
|
||||
3. Snowflake 算法的原理是什么?有什么坑?
|
||||
4. 数据库自增 ID 如何实现分布式?如何优化性能?
|
||||
5. Redis 如何生成分布式 ID?有哪些优缺点?
|
||||
6. 在实际项目中,你是如何设计分布式 ID 的?
|
||||
|
||||
---
|
||||
|
||||
## 标准答案
|
||||
|
||||
### 1. 分布式 ID 的要求和特点
|
||||
|
||||
#### **为什么需要分布式 ID?**
|
||||
|
||||
在分布式系统中,需要保证 ID 的**全局唯一性**:
|
||||
- 订单号、用户 ID、支付流水号
|
||||
- 分库分表后的主键冲突问题
|
||||
- 微服务间的数据关联
|
||||
|
||||
**单机 ID 的问题**:
|
||||
```
|
||||
单机数据库自增 ID:
|
||||
- 实例 1:1, 2, 3, 4, 5
|
||||
- 实例 2:1, 2, 3, 4, 5 ← 冲突!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **分布式 ID 的核心要求**
|
||||
|
||||
| 要求 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **全局唯一性** | 不能重复 | 订单号不能重复 |
|
||||
| **有序性** | 趋势递增(可选) | 按时间排序的订单号 |
|
||||
| **高性能** | 生成速度快 | 支持 10 万+/秒 |
|
||||
| **高可用** | 服务不中断 | 宕机后仍可生成 |
|
||||
| **信息安全** | 不暴露业务信息 | 不暴露订单总量 |
|
||||
|
||||
---
|
||||
|
||||
### 2. 常见分布式 ID 方案对比
|
||||
|
||||
| 方案 | 唯一性 | 有序性 | 性能 | 复杂度 | 适用场景 |
|
||||
|------|--------|--------|------|--------|----------|
|
||||
| **UUID** | ✅ | ❌ | ⭐⭐⭐⭐⭐ | 低 | 非主键、内部 ID |
|
||||
| **数据库自增** | ✅ | ✅ | ⭐⭐ | 低 | 小规模、并发低 |
|
||||
| **Redis INCR** | ✅ | ✅ | ⭐⭐⭐ | 低 | 中小规模 |
|
||||
| **Snowflake** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 中 | 大规模、高并发 |
|
||||
| **号段模式** | ✅ | ✅ | ⭐⭐⭐⭐ | 中 | 大规模、高性能 |
|
||||
| **美团 Leaf** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 高 | 金融级、高可用 |
|
||||
|
||||
---
|
||||
|
||||
### 3. UUID
|
||||
|
||||
#### **原理**
|
||||
|
||||
UUID(Universally Unique Identifier)是 128 位的唯一标识符。
|
||||
|
||||
**格式**:
|
||||
```
|
||||
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
**Java 示例**:
|
||||
```java
|
||||
import java.util.UUID;
|
||||
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
// 输出:550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **优缺点**
|
||||
|
||||
**优点**:
|
||||
- 性能高:本地生成,无网络开销
|
||||
- 简单:JDK 自带,无需额外组件
|
||||
|
||||
**缺点**:
|
||||
- **无序**:无法按时间排序
|
||||
- **过长**:36 字符,存储空间大
|
||||
- **不安全**:暴露 MAC 地址(UUID v1)
|
||||
- **索引性能差**:无序 ID 导致 B+ 树频繁分裂
|
||||
|
||||
**B+ 树分裂问题**:
|
||||
```
|
||||
有序 ID:
|
||||
1 → 100 → 1000 → 10000
|
||||
└─ 顺序插入,B+ 树叶子节点顺序填充
|
||||
|
||||
无序 ID(UUID):
|
||||
abc → xyz → 123 → 999
|
||||
└─ 随机插入,B+ 树频繁分裂,性能差
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **适用场景**
|
||||
|
||||
- ✅ 非数据库主键(如请求 ID)
|
||||
- ✅ 临时标识、会话 ID
|
||||
- ✅ 内部系统、不需要排序
|
||||
- ❌ 订单号、用户 ID(需要有序)
|
||||
|
||||
---
|
||||
|
||||
### 4. 数据库自增 ID
|
||||
|
||||
#### **方案 1:单机自增**
|
||||
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_no VARCHAR(32),
|
||||
created_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
**问题**:单机性能瓶颈,无法扩展。
|
||||
|
||||
---
|
||||
|
||||
#### **方案 2:多机步长模式(Flickr 方案)**
|
||||
|
||||
**原理**:不同数据库实例设置不同的起始值和步长。
|
||||
|
||||
```
|
||||
实例 1:起始 1,步长 2 → 1, 3, 5, 7, 9
|
||||
实例 2:起始 2,步长 2 → 2, 4, 6, 8, 10
|
||||
```
|
||||
|
||||
**配置**:
|
||||
```sql
|
||||
-- 实例 1
|
||||
SET auto_increment_increment = 2;
|
||||
SET auto_increment_offset = 1;
|
||||
|
||||
-- 实例 2
|
||||
SET auto_increment_increment = 2;
|
||||
SET auto_increment_offset = 2;
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 实现简单
|
||||
- ID 有序
|
||||
|
||||
**缺点**:
|
||||
- 扩容困难:需要重新计算步长
|
||||
- 性能瓶颈:数据库写入限制
|
||||
|
||||
---
|
||||
|
||||
#### **方案 3:号段模式(批量获取)**
|
||||
|
||||
**原理**:一次从数据库获取一批 ID(号段),缓存在本地。
|
||||
|
||||
**表结构**:
|
||||
```sql
|
||||
CREATE TABLE id_segment (
|
||||
biz_type VARCHAR(32) PRIMARY KEY, -- 业务类型
|
||||
max_id BIGINT, -- 当前最大 ID
|
||||
step INT, -- 步长(批量大小)
|
||||
version INT, -- 版本号(乐观锁)
|
||||
updated_at DATETIME
|
||||
);
|
||||
|
||||
-- 初始化数据
|
||||
INSERT INTO id_segment (biz_type, max_id, step, version)
|
||||
VALUES ('order', 0, 1000, 1);
|
||||
```
|
||||
|
||||
**获取 ID 逻辑**:
|
||||
```java
|
||||
@Service
|
||||
public class IdSegmentService {
|
||||
|
||||
@Autowired
|
||||
private IdSegmentMapper segmentMapper;
|
||||
|
||||
private final Map<String, IdSegment> localCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Transactional
|
||||
public synchronized Long nextId(String bizType) {
|
||||
IdSegment segment = localCache.get(bizType);
|
||||
|
||||
// 本地号段用完,从数据库获取
|
||||
if (segment == null || segment.getCurrentId() >= segment.getMaxId()) {
|
||||
segment = fetchSegmentFromDb(bizType);
|
||||
}
|
||||
|
||||
// 返回下一个 ID
|
||||
return segment.getNextId();
|
||||
}
|
||||
|
||||
private IdSegment fetchSegmentFromDb(String bizType) {
|
||||
// 使用 CAS 更新数据库
|
||||
IdSegment dbSegment = segmentMapper.selectByType(bizType);
|
||||
|
||||
// 更新 max_id = max_id + step
|
||||
int updated = segmentMapper.updateMaxId(
|
||||
bizType,
|
||||
dbSegment.getMaxId() + dbSegment.getStep(),
|
||||
dbSegment.getVersion()
|
||||
);
|
||||
|
||||
if (updated == 0) {
|
||||
throw new RuntimeException("并发冲突,请重试");
|
||||
}
|
||||
|
||||
// 缓存到本地
|
||||
IdSegment newSegment = new IdSegment();
|
||||
newSegment.setMaxId(dbSegment.getMaxId() + dbSegment.getStep());
|
||||
newSegment.setCurrentId(dbSegment.getMaxId());
|
||||
|
||||
localCache.put(bizType, newSegment);
|
||||
return newSegment;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mapper**:
|
||||
```java
|
||||
@Update("UPDATE id_segment SET max_id = #{maxId}, version = version + 1 " +
|
||||
"WHERE biz_type = #{bizType} AND version = #{version}")
|
||||
int updateMaxId(@Param("bizType") String bizType,
|
||||
@Param("maxId") Long maxId,
|
||||
@Param("version") Integer version);
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 性能高:本地缓存,减少数据库访问
|
||||
- 有序:ID 趋势递增
|
||||
|
||||
**缺点**:
|
||||
- 宕机丢 ID:本地缓存的 ID 未使用完就丢失
|
||||
- 实现复杂
|
||||
|
||||
**优化**:使用双缓冲(Double Buffer)机制预加载号段。
|
||||
|
||||
---
|
||||
|
||||
### 5. Redis INCR
|
||||
|
||||
#### **原理**
|
||||
|
||||
使用 Redis 的 `INCR` 和 `INCRBY` 命令生成全局唯一 ID。
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
# 初始化
|
||||
SET order:id 1
|
||||
|
||||
# 获取下一个 ID
|
||||
INCR order:id
|
||||
# 返回:2
|
||||
|
||||
# 批量获取(步长 1000)
|
||||
INCRBY order:id 1000
|
||||
# 返回:1001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Java 实现**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class RedisIdGenerator {
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
public Long nextId(String key) {
|
||||
// 使用 redisTemplate 的 opsForValue
|
||||
Long id = redisTemplate.opsForValue().increment(key);
|
||||
|
||||
if (id == null) {
|
||||
throw new RuntimeException("生成 ID 失败");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// 批量获取(优化性能)
|
||||
public Long[] batchNextId(String key, int count) {
|
||||
Long startId = redisTemplate.opsForValue().increment(key, count);
|
||||
|
||||
Long[] ids = new Long[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = startId - count + i + 1;
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **优缺点**
|
||||
|
||||
**优点**:
|
||||
- 性能高:Redis 内存操作
|
||||
- 有序:ID 趋势递增
|
||||
- 实现简单
|
||||
|
||||
**缺点**:
|
||||
- 依赖 Redis:需要维护 Redis 集群
|
||||
- 宕机丢数据:未持久化会丢失
|
||||
|
||||
**持久化配置**:
|
||||
```properties
|
||||
# 开启 AOF 持久化
|
||||
appendonly yes
|
||||
appendfsync everysec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **适用场景**
|
||||
|
||||
- 中小规模、性能要求高
|
||||
- 已有 Redis 集群
|
||||
- 可容忍短期 ID 丢失
|
||||
|
||||
---
|
||||
|
||||
### 6. Snowflake 算法
|
||||
|
||||
#### **原理**
|
||||
|
||||
Snowflake 是 Twitter 开源的分布式 ID 算法,生成 64 位的 Long 型 ID。
|
||||
|
||||
**结构**(64 位):
|
||||
```
|
||||
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
|
||||
↑ ↑ ↑ ↑ ↑
|
||||
│ │ │ │ │
|
||||
│ └─ 41 位时间戳(毫秒) │ │ │
|
||||
│ │ │ │
|
||||
│ └─ 5 位数据中心 ID │
|
||||
│ └─ 12 位序列号
|
||||
1 位符号位(始终为 0)
|
||||
```
|
||||
|
||||
**组成部分**:
|
||||
- **1 位符号位**:始终为 0(正数)
|
||||
- **41 位时间戳**:毫秒级,可用 69 年(`2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69`)
|
||||
- **5 位数据中心 ID**:支持 32 个数据中心
|
||||
- **5 位机器 ID**:每个数据中心 32 台机器
|
||||
- **12 位序列号**:每毫秒可生成 4096 个 ID
|
||||
|
||||
**理论 QPS**:
|
||||
```
|
||||
单机:4096 / 毫秒 = 409.6 万/秒
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Java 实现**
|
||||
|
||||
```java
|
||||
public class SnowflakeIdGenerator {
|
||||
|
||||
// 起始时间戳(2024-01-01 00:00:00)
|
||||
private final long twepoch = 1704067200000L;
|
||||
|
||||
// 各部分位数
|
||||
private final long workerIdBits = 5L;
|
||||
private final long datacenterIdBits = 5L;
|
||||
private final long sequenceBits = 12L;
|
||||
|
||||
// 最大值
|
||||
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
|
||||
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
|
||||
private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095
|
||||
|
||||
// 位移
|
||||
private final long workerIdShift = sequenceBits; // 12
|
||||
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
|
||||
private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
|
||||
|
||||
private final long workerId;
|
||||
private final long datacenterId;
|
||||
private long sequence = 0L;
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
public SnowflakeIdGenerator(long workerId, long datacenterId) {
|
||||
if (workerId > maxWorkerId || workerId < 0) {
|
||||
throw new IllegalArgumentException("workerId 无效");
|
||||
}
|
||||
if (datacenterId > maxDatacenterId || datacenterId < 0) {
|
||||
throw new IllegalArgumentException("datacenterId 无效");
|
||||
}
|
||||
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) & maxSequence;
|
||||
// 序列号溢出,等待下一毫秒
|
||||
if (sequence == 0) {
|
||||
timestamp = tilNextMillis(lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
// 新毫秒,序列号重置
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// 组装 ID
|
||||
return ((timestamp - twepoch) << timestampShift)
|
||||
| (datacenterId << datacenterIdShift)
|
||||
| (workerId << workerIdShift)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
// 等待下一毫秒
|
||||
private long tilNextMillis(long lastTimestamp) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// 解析 ID(用于调试)
|
||||
public static void parseId(long id) {
|
||||
long timestamp = (id >> 22) + 1704067200000L;
|
||||
long datacenterId = (id >> 17) & 0x1F;
|
||||
long workerId = (id >> 12) & 0x1F;
|
||||
long sequence = id & 0xFFF;
|
||||
|
||||
System.out.println("ID: " + id);
|
||||
System.out.println("时间戳: " + new Date(timestamp));
|
||||
System.out.println("数据中心 ID: " + datacenterId);
|
||||
System.out.println("机器 ID: " + workerId);
|
||||
System.out.println("序列号: " + sequence);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
```java
|
||||
// 初始化(workerId=1, datacenterId=1)
|
||||
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
|
||||
|
||||
// 生成 ID
|
||||
long id = idGenerator.nextId();
|
||||
System.out.println("生成的 ID: " + id);
|
||||
|
||||
// 解析 ID
|
||||
SnowflakeIdGenerator.parseId(id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **Snowflake 的坑**
|
||||
|
||||
##### **问题 1:时钟回拨**
|
||||
|
||||
**原因**:
|
||||
- 系统时钟不准确(NTP 同步)
|
||||
- 人工修改系统时间
|
||||
|
||||
**后果**:
|
||||
```
|
||||
时间:10:00:00.100,生成 ID(时间戳=T1)
|
||||
时钟回拨:时间 → 09:59:59.900
|
||||
时间:09:59:59.950,生成 ID(时间戳=T2 < T1)← 冲突!
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **拒绝服务**(简单):
|
||||
```java
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new RuntimeException("时钟回拨,拒绝生成 ID");
|
||||
}
|
||||
```
|
||||
|
||||
2. **等待时钟追上**(推荐):
|
||||
```java
|
||||
if (timestamp < lastTimestamp) {
|
||||
long offset = lastTimestamp - timestamp;
|
||||
if (offset <= 5) {
|
||||
// 等待 5 毫秒
|
||||
try {
|
||||
Thread.sleep(offset << 1);
|
||||
timestamp = System.currentTimeMillis();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException("时钟回拨,等待失败");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("时钟回拨过多,拒绝生成 ID");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用备用 workerId**(美团 Leaf 方案):
|
||||
```java
|
||||
if (timestamp < lastTimestamp) {
|
||||
// 切换到备用 workerId
|
||||
workerId = backupWorkerId;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### **问题 2:机器 ID 分配**
|
||||
|
||||
**问题**:如何保证 workerId 全局唯一?
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **配置文件**(简单):
|
||||
```yaml
|
||||
application.yml:
|
||||
snowflake:
|
||||
worker-id: 1
|
||||
datacenter-id: 1
|
||||
```
|
||||
|
||||
2. **数据库配置**:
|
||||
```sql
|
||||
CREATE TABLE worker_config (
|
||||
id INT PRIMARY KEY,
|
||||
worker_id INT,
|
||||
datacenter_id INT,
|
||||
ip VARCHAR(32),
|
||||
used BOOLEAN
|
||||
);
|
||||
|
||||
-- 启动时申请 workerId
|
||||
INSERT INTO worker_config (worker_id, datacenter_id, ip, used)
|
||||
VALUES (1, 1, '192.168.1.10', TRUE);
|
||||
```
|
||||
|
||||
3. **Zookeeper 顺序节点**(动态):
|
||||
```java
|
||||
// 在 Zookeeper 中创建临时顺序节点
|
||||
String path = zk.create("/snowflake/worker-", null, ZooDefs.Ids.EPHEMERAL_SEQUENTIAL);
|
||||
// 获取序号作为 workerId
|
||||
int workerId = Integer.parseInt(path.split("-")[1]);
|
||||
```
|
||||
|
||||
4. **Redis INCR**(动态):
|
||||
```java
|
||||
Long workerId = redisTemplate.opsForValue().increment("snowflake:worker:id");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### **问题 3:序列号溢出**
|
||||
|
||||
**场景**:高并发下,1 毫秒内请求超过 4096 个。
|
||||
|
||||
**解决**:
|
||||
```java
|
||||
// 序列号溢出,等待下一毫秒
|
||||
if (sequence == 0) {
|
||||
timestamp = tilNextMillis(lastTimestamp);
|
||||
}
|
||||
```
|
||||
|
||||
**优化**:使用 13 位序列号(每毫秒 8192 个 ID)。
|
||||
|
||||
---
|
||||
|
||||
#### **优缺点**
|
||||
|
||||
**优点**:
|
||||
- 性能极高:409 万 QPS/单机
|
||||
- 有序:趋势递增(按时间排序)
|
||||
- 不依赖数据库、Redis
|
||||
|
||||
**缺点**:
|
||||
- 时钟回拨问题
|
||||
- 机器 ID 分配复杂
|
||||
- ID 较长(18 位数字)
|
||||
|
||||
---
|
||||
|
||||
#### **适用场景**
|
||||
|
||||
- 大规模、高并发场景
|
||||
- 需要有序 ID
|
||||
- 可容忍时钟问题(或已有解决方案)
|
||||
|
||||
**实际应用**:
|
||||
- 百度 UidGenerator
|
||||
- 美团 Leaf(Snowflake 模式)
|
||||
- Etsy)
|
||||
|
||||
---
|
||||
|
||||
### 7. 美团 Leaf
|
||||
|
||||
#### **Leaf-segment(号段模式)**
|
||||
|
||||
**原理**:优化版号段模式,使用双缓冲机制。
|
||||
|
||||
**架构**:
|
||||
```
|
||||
Leaf Server
|
||||
├─ Buffer 1:当前使用号段 [1000, 2000)
|
||||
└─ Buffer 2:预加载号段 [2000, 3000)(后台异步加载)
|
||||
|
||||
当 Buffer 1 用完:
|
||||
└─ 切换到 Buffer 2
|
||||
└─ 异步加载 Buffer 1 的下一个号段
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 无停顿:双缓冲无缝切换
|
||||
- 高性能:本地缓存
|
||||
|
||||
**缺点**:
|
||||
- 宕机丢 ID:未使用的号段丢失
|
||||
|
||||
---
|
||||
|
||||
#### **Leaf-snowflake(优化版 Snowflake)**
|
||||
|
||||
**优化点**:
|
||||
1. **Zookeeper 生成 workerId**:动态分配,无需配置
|
||||
2. **时钟回拨优化**:
|
||||
- 回拨 5ms 内:等待时钟追上
|
||||
- 回拨 5ms 外:告警并拒绝服务
|
||||
|
||||
**架构**:
|
||||
```
|
||||
Leaf Server 1(workerId=1)
|
||||
Leaf Server 2(workerId=2)
|
||||
Leaf Server 3(workerId=3)
|
||||
↓
|
||||
Zookeeper(协调)
|
||||
```
|
||||
|
||||
**GitHub**:https://github.com/Meituan-Dianping/Leaf
|
||||
|
||||
---
|
||||
|
||||
### 8. 百度 UidGenerator
|
||||
|
||||
**特点**:
|
||||
- 基于 Snowflake 优化
|
||||
- 使用 22 位序列号(每秒 400 万 ID)
|
||||
- 支持跨毫秒分配序列号
|
||||
|
||||
**GitHub**:https://github.com/baidu/uid-generator
|
||||
|
||||
---
|
||||
|
||||
### 9. 实际项目选型建议
|
||||
|
||||
#### **决策树**
|
||||
|
||||
```
|
||||
是否需要有序?
|
||||
├─ 否 → UUID(最简单)
|
||||
└─ 是 → 继续判断
|
||||
│
|
||||
├─ QPS < 1000?
|
||||
│ ├─ 是 → Redis INCR(简单)
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 已有 Redis?
|
||||
│ ├─ 是 → 号段模式(高性能)
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 可容忍时钟回拨问题?
|
||||
│ ├─ 是 → Snowflake(性能最高)
|
||||
│ └─ 否 → 美团 Leaf-snowflake
|
||||
│
|
||||
└─ 金融级可靠性?
|
||||
└─ 美团 Leaf-segment + 监控
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **性能对比**
|
||||
|
||||
| 方案 | 单机 QPS | 延迟 | 依赖 |
|
||||
|------|---------|------|------|
|
||||
| UUID | 1000 万+ | 0.001ms | 无 |
|
||||
| 数据库自增 | 1000 | 10ms | 数据库 |
|
||||
| Redis INCR | 10 万 | 1ms | Redis |
|
||||
| 号段模式 | 100 万 | 0.1ms | 数据库 |
|
||||
| Snowflake | 400 万 | 0.01ms | 无 |
|
||||
|
||||
---
|
||||
|
||||
### 10. 阿里 P7 加分项
|
||||
|
||||
**深度理解**:
|
||||
- 理解 Snowflake 的时间戳回拨问题的根本原因
|
||||
- 理解号段模式的双缓冲机制和 CAS 原理
|
||||
|
||||
**实战经验**:
|
||||
- 有处理 Snowflake 时钟回拨的线上故障经验
|
||||
- 有号段模式宕机丢 ID 的解决方案
|
||||
- 有分布式 ID 迁移经验(如从数据库自增迁移到 Snowflake)
|
||||
|
||||
**架构能力**:
|
||||
- 能设计支持多业务类型的分布式 ID 系统
|
||||
- 能设计分布式 ID 的监控和告警体系
|
||||
- 有分布式 ID 容灾方案(多机房容灾)
|
||||
|
||||
**技术选型**:
|
||||
- 能根据业务特点选择合适的方案
|
||||
- 了解美团 Leaf、百度 UidGenerator 等开源方案
|
||||
- 有自研分布式 ID 生成器的经验
|
||||
298
questions/01-分布式系统/分布式事务.md
Normal file
298
questions/01-分布式系统/分布式事务.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 分布式事务
|
||||
|
||||
## 问题
|
||||
|
||||
**背景**:在微服务架构中,我们经常遇到跨服务的数据一致性问题。
|
||||
|
||||
**问题**:
|
||||
1. 请描述分布式事务的常见解决方案(至少 3 种)
|
||||
2. 它们的优缺点和适用场景是什么?
|
||||
3. 你在实际项目中是如何选择的?有没有遇到过什么坑?
|
||||
|
||||
---
|
||||
|
||||
## 标准答案
|
||||
|
||||
### 1. 常见解决方案
|
||||
|
||||
#### **方案一:2PC (Two-Phase Commit,两阶段提交)**
|
||||
|
||||
**原理**:
|
||||
- 准备阶段:协调者询问所有参与者是否可以提交
|
||||
- 提交阶段:如果所有参与者都回复"可以",则发送提交指令;否则发送回滚指令
|
||||
|
||||
**优点**:
|
||||
- 强一致性
|
||||
- 原理简单,易于理解
|
||||
|
||||
**缺点**:
|
||||
- **同步阻塞**:所有参与者在事务提交前都处于阻塞状态
|
||||
- **单点故障**:协调者故障会导致参与者一直阻塞
|
||||
- **数据不一致**:在第二阶段,部分节点收到提交指令,部分未收到
|
||||
|
||||
**适用场景**:
|
||||
- 传统关系型数据库(XA 协议)
|
||||
- 对一致性要求极高,可接受性能损耗的场景
|
||||
|
||||
**实际应用**:
|
||||
- MySQL XA 事务
|
||||
- Java JTA (Java Transaction API)
|
||||
|
||||
---
|
||||
|
||||
#### **方案二:3PC (Three-Phase Commit,三阶段提交)**
|
||||
|
||||
**原理**:
|
||||
在 2PC 基础上增加 CanCommit 阶段:
|
||||
1. CanCommit:协调者询问参与者是否可以执行
|
||||
2. PreCommit:参与者预执行并回复
|
||||
3. DoCommit:正式提交
|
||||
|
||||
**优点**:
|
||||
- 相比 2PC 减少了阻塞时间
|
||||
- 引入超时机制,参与者可以自动决策
|
||||
|
||||
**缺点**:
|
||||
- 仍然存在数据不一致风险
|
||||
- 协议更复杂,实现成本高
|
||||
- 性能提升有限
|
||||
|
||||
**适用场景**:
|
||||
- 很少在实际生产中使用,更多是理论意义
|
||||
|
||||
---
|
||||
|
||||
#### **方案三:TCC (Try-Confirm-Cancel,补偿事务)**
|
||||
|
||||
**原理**:
|
||||
- **Try 阶段**:尝试执行业务,完成资源的检查和预留
|
||||
- **Confirm 阶段**:确认执行业务,使用 Try 阶段预留的资源
|
||||
- **Cancel 阶段**:取消执行业务,释放 Try 阶段预留的资源
|
||||
|
||||
**代码示例**:
|
||||
```java
|
||||
// Try 阶段
|
||||
public boolean try() {
|
||||
// 检查账户余额
|
||||
// 冻结相应金额(预留资源)
|
||||
// return true/false
|
||||
}
|
||||
|
||||
// Confirm 阶段
|
||||
public boolean confirm() {
|
||||
// 扣除冻结金额
|
||||
// 真正完成转账
|
||||
}
|
||||
|
||||
// Cancel 阶段
|
||||
public boolean cancel() {
|
||||
// 释放冻结金额
|
||||
// 恢复原始状态
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- **最终一致性**
|
||||
- 性能较好,相比 2PC 没有长时间锁资源
|
||||
- 业务可控性强
|
||||
|
||||
**缺点**:
|
||||
- **代码侵入性强**:每个业务都需要写三个接口
|
||||
- **开发成本高**:需要考虑各种异常情况
|
||||
- **容易遗漏**:Cancel 接口如果实现不完整会导致资源泄露
|
||||
|
||||
**适用场景**:
|
||||
- 对性能有一定要求
|
||||
- 业务逻辑清晰,可以拆分成 Try/Confirm/Cancel
|
||||
- 高并发场景
|
||||
|
||||
**实际应用**:
|
||||
- 阿里巴巴 Seata 的 TCC 模式
|
||||
- 支付系统、订单系统
|
||||
|
||||
---
|
||||
|
||||
#### **方案四:本地消息表(异步确保)**
|
||||
|
||||
**原理**:
|
||||
1. 上游服务在同一本地事务中:
|
||||
- 完成业务操作
|
||||
- 存储一条消息到本地消息表(状态为"待发送")
|
||||
2. 定时任务扫描消息表,发送消息到 MQ
|
||||
3. 下游服务消费 MQ,执行业务逻辑
|
||||
4. 下游服务成功后通知上游更新消息状态
|
||||
|
||||
**优点**:
|
||||
- 实现简单
|
||||
- 可靠性高(消息持久化)
|
||||
- 支持重试
|
||||
|
||||
**缺点**:
|
||||
- 需要维护本地消息表
|
||||
- 定时任务有延迟
|
||||
- 需要处理消息重复消费(幂等性)
|
||||
|
||||
**适用场景**:
|
||||
- 可以接受最终一致性
|
||||
- 对实时性要求不高
|
||||
- 高并发场景
|
||||
|
||||
**实际应用**:
|
||||
- 支付宝到账通知
|
||||
- 订单创建后的物流通知
|
||||
|
||||
---
|
||||
|
||||
#### **方案五:MQ 事务消息(RocketMQ 方案)**
|
||||
|
||||
**原理**:
|
||||
1. 发送半消息(Half Message)到 MQ(消息对消费者不可见)
|
||||
2. 执行本地事务
|
||||
3. 提交/回滚消息:
|
||||
- 本地事务成功 → 提交消息(消息对消费者可见)
|
||||
- 本地事务失败 → 删除消息
|
||||
4. MQ 提供反查机制:如果长时间未收到确认,主动查询业务方事务状态
|
||||
|
||||
**优点**:
|
||||
- 解耦性强
|
||||
- 性能好
|
||||
- 支持大规模分布式事务
|
||||
|
||||
**缺点**:
|
||||
- 依赖特定 MQ(如 RocketMQ)
|
||||
- 需要实现反查接口
|
||||
- 消息可能有延迟
|
||||
|
||||
**适用场景**:
|
||||
- 高并发、大规模分布式系统
|
||||
- 可以接受最终一致性
|
||||
- 需要解耦上下游服务
|
||||
|
||||
**实际应用**:
|
||||
- RocketMQ 事务消息
|
||||
- 双11 大促场景
|
||||
|
||||
---
|
||||
|
||||
#### **方案六:Saga 模式**
|
||||
|
||||
**原理**:
|
||||
将长事务拆分为多个本地短事务,每个短事务都有对应的补偿操作:
|
||||
- 正向操作:T1, T2, T3, ..., Tn
|
||||
- 补偿操作:Cn, ..., C3, C2, C1(反向补偿)
|
||||
|
||||
**示例**:
|
||||
```
|
||||
预订行程 Saga:
|
||||
1. 预订航班 (T1)
|
||||
2. 预订酒店 (T2)
|
||||
3. 预订租车 (T3)
|
||||
|
||||
如果 T2 失败:
|
||||
1. 取消航班 (C1)
|
||||
2. 返回失败给用户
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 适合长事务、业务流程复杂的场景
|
||||
- 最终一致性
|
||||
- 可以跨多个服务
|
||||
|
||||
**缺点**:
|
||||
- 需要为每个操作设计补偿逻辑
|
||||
- 补偿操作可能失败,需要处理
|
||||
- 无法保证隔离性(脏读问题)
|
||||
|
||||
**适用场景**:
|
||||
- 业务流程长、涉及多个服务
|
||||
- 旅行预订、电商下单
|
||||
- 微服务编排
|
||||
|
||||
**实际应用**:
|
||||
- Apache ServiceComb Saga
|
||||
- Netflix Conductor
|
||||
|
||||
---
|
||||
|
||||
### 2. 方案对比总结
|
||||
|
||||
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|
||||
|------|--------|------|--------|----------|
|
||||
| 2PC | 强一致性 | 低(同步阻塞) | 低 | 传统数据库 |
|
||||
| 3PC | 强一致性 | 中 | 中 | 很少使用 |
|
||||
| TCC | 最终一致性 | 高 | 高(业务侵入) | 高并发、强业务控制 |
|
||||
| 本地消息表 | 最终一致性 | 中 | 低 | 高可靠性、可接受延迟 |
|
||||
| MQ 事务消息 | 最终一致性 | 高 | 中 | 大规模、高并发、解耦 |
|
||||
| Saga | 最终一致性 | 高 | 高(补偿逻辑) | 长事务、业务编排 |
|
||||
|
||||
---
|
||||
|
||||
### 3. 实际项目选择建议
|
||||
|
||||
**选择决策树**:
|
||||
```
|
||||
是否需要强一致性?
|
||||
├─ 是 → 2PC(XA 事务)
|
||||
└─ 否 → 最终一致性
|
||||
│
|
||||
├─ 业务可以拆分为 Try/Confirm/Cancel?
|
||||
│ ├─ 是 → TCC(高并发、强控制)
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 使用 RocketMQ?
|
||||
│ ├─ 是 → MQ 事务消息
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 业务流程长、涉及多服务?
|
||||
│ ├─ 是 → Saga
|
||||
│ └─ 否 → 本地消息表
|
||||
```
|
||||
|
||||
**常见坑和注意事项**:
|
||||
|
||||
1. **幂等性问题**(所有方案都需要考虑)
|
||||
- 重复请求导致的重复扣款、重复发货
|
||||
- 解决:使用唯一业务 ID、Redis 分布式锁
|
||||
|
||||
2. **空补偿问题**(TCC)
|
||||
```java
|
||||
// Cancel 被调用时,Try 可能还没执行
|
||||
public void cancel() {
|
||||
// 需要检查是否有冻结记录
|
||||
if (没有冻结记录) {
|
||||
return; // 空补偿,直接返回
|
||||
}
|
||||
// 执行取消逻辑
|
||||
}
|
||||
```
|
||||
|
||||
3. **悬挂问题**(TCC)
|
||||
- Confirm 比 Cancel 先到
|
||||
- 解决:记录事务状态,拒绝后续操作
|
||||
|
||||
4. **消息丢失**(MQ 方案)
|
||||
- 网络抖动导致消息丢失
|
||||
- 解决:ACK 机制 + 重试 + 死信队列
|
||||
|
||||
5. **资源锁定时间**(2PC)
|
||||
- 长时间锁资源导致性能下降
|
||||
- 解决:控制事务规模,拆分大事务
|
||||
|
||||
---
|
||||
|
||||
### 4. 阿里 P7 加分项
|
||||
|
||||
**实际项目经验**:
|
||||
- 设计并实现过千万级用户的分布式事务系统
|
||||
- 处理过分布式事务的性能瓶颈(如连接池优化、并发度控制)
|
||||
- 有 TCC/Saga 的踩坑经验和解决方案
|
||||
|
||||
**深度理解**:
|
||||
- 理解 CAP 理论在实际场景中的权衡
|
||||
- 能根据业务特点选择合适的一致性级别
|
||||
- 有监控和告警体系,能快速定位分布式事务问题
|
||||
|
||||
**架构能力**:
|
||||
- 能设计支持多种分布式事务模式的统一框架
|
||||
- 考虑降级和熔断策略
|
||||
- 有混沌工程实践(注入故障测试系统恢复能力)
|
||||
456
questions/01-分布式系统/分布式锁.md
Normal file
456
questions/01-分布式系统/分布式锁.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# 分布式锁
|
||||
|
||||
## 问题
|
||||
|
||||
1. 什么是分布式锁?为什么需要分布式锁?
|
||||
2. 如何用 Redis 实现分布式锁?
|
||||
3. Redis 分布式锁有哪些坑?如何解决?
|
||||
4. Zookeeper 如何实现分布式锁?
|
||||
5. Redis 分布式锁和 Zookeeper 分布式锁的区别?
|
||||
6. Redisson 是如何实现分布式锁的?
|
||||
7. 在实际项目中如何使用分布式锁?
|
||||
|
||||
---
|
||||
|
||||
## 标准答案
|
||||
|
||||
### 1. 为什么需要分布式锁
|
||||
|
||||
#### **场景**
|
||||
|
||||
**单机锁(synchronized、ReentrantLock)**:
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
private synchronized void createOrder(Order order) {
|
||||
// 单机环境下有效
|
||||
// 分布式环境下无效(不同 JVM)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 单机锁只对单个 JVM 有效
|
||||
- 分布式环境下,多个实例的锁互不排斥
|
||||
- 需要跨 JVM、跨机器的锁机制
|
||||
|
||||
---
|
||||
|
||||
### 2. Redis 分布式锁
|
||||
|
||||
#### **基本实现**
|
||||
|
||||
```java
|
||||
public class RedisDistributedLock {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
// 加锁
|
||||
public boolean lock(String key, String value, long expireTime) {
|
||||
// SET key value NX PX expireTime
|
||||
Boolean success = redisTemplate.opsForValue()
|
||||
.setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
|
||||
|
||||
return success != null && success;
|
||||
}
|
||||
|
||||
// 释放锁
|
||||
public boolean unlock(String key, String value) {
|
||||
// Lua 脚本:保证原子性
|
||||
String luaScript =
|
||||
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
|
||||
" return redis.call('del', KEYS[1]) " +
|
||||
"else " +
|
||||
" return 0 " +
|
||||
"end";
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
|
||||
Long result = redisTemplate.execute(script, Collections.singletonList(key), value);
|
||||
|
||||
return result != null && result == 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **使用示例**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
@Autowired
|
||||
private RedisDistributedLock lock;
|
||||
|
||||
public void createOrder(Order order) {
|
||||
String lockKey = "order:" + order.getProductId();
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
// 加锁(过期时间 30 秒)
|
||||
boolean locked = lock.lock(lockKey, lockValue, 30000);
|
||||
|
||||
if (!locked) {
|
||||
throw new BusinessException("系统繁忙,请稍后再试");
|
||||
}
|
||||
|
||||
// 执行业务逻辑
|
||||
// 1. 查询库存
|
||||
// 2. 扣减库存
|
||||
// 3. 创建订单
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
lock.unlock(lockKey, lockValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Redis 分布式锁的坑
|
||||
|
||||
#### **坑 1:锁超时问题**
|
||||
|
||||
**场景**:
|
||||
```
|
||||
线程 A 获取锁(设置 30 秒过期)
|
||||
线程 A 执行业务(耗时 50 秒)
|
||||
30 秒后,锁自动过期
|
||||
线程 B 获取锁
|
||||
线程 A 执行完毕,释放锁(把线程 B 的锁释放了!)
|
||||
```
|
||||
|
||||
**解决方案:看门狗(Watchdog)**
|
||||
|
||||
**原理**:
|
||||
- 启动后台线程
|
||||
- 定期检查锁是否还存在
|
||||
- 如果存在,续期(延长过期时间)
|
||||
|
||||
**Redisson 实现**:
|
||||
```java
|
||||
RLock lock = redisson.getLock("myLock");
|
||||
lock.lock(); // 自动续期(默认 30 秒)
|
||||
|
||||
try {
|
||||
// 业务逻辑
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **坑 2:主从切换导致锁丢失**
|
||||
|
||||
**场景**:
|
||||
```
|
||||
线程 A 在 Master 获取锁
|
||||
锁未同步到 Slave
|
||||
Master 宕机,Slave 升级为 Master
|
||||
线程 B 在新 Master 上获取锁(冲突!)
|
||||
```
|
||||
|
||||
**解决方案:Redlock(Redis 分布式锁算法)**
|
||||
|
||||
**原理**:
|
||||
- 向多个独立的 Redis 节点请求加锁
|
||||
- 大多数节点加锁成功才算成功
|
||||
|
||||
```java
|
||||
// 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 {
|
||||
redLock.lock();
|
||||
// 业务逻辑
|
||||
} finally {
|
||||
redLock.unlock();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **坑 3:释放锁时误删**
|
||||
|
||||
**场景**:
|
||||
```
|
||||
线程 A 获取锁
|
||||
线程 A 执行时间过长,锁超时过期
|
||||
线程 B 获取锁
|
||||
线程 A 执行完毕,释放锁(删除了线程 B 的锁!)
|
||||
```
|
||||
|
||||
**解决方案:Lua 脚本 + 唯一标识**
|
||||
|
||||
```lua
|
||||
-- Lua 脚本(原子操作)
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
```
|
||||
|
||||
**Java 实现**:
|
||||
```java
|
||||
public boolean unlock(String key, String value) {
|
||||
String luaScript =
|
||||
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
|
||||
" return redis.call('del', KEYS[1]) " +
|
||||
"else " +
|
||||
" return 0 " +
|
||||
"end";
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
|
||||
Long result = redisTemplate.execute(script, Collections.singletonList(key), value);
|
||||
|
||||
return result != null && result == 1;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Zookeeper 分布式锁
|
||||
|
||||
#### **原理**
|
||||
|
||||
利用 Zookeeper 的**临时顺序节点**:
|
||||
|
||||
```
|
||||
/lock
|
||||
/lock/node-0000000001 (客户端 1)
|
||||
/lock/node-0000000002 (客户端 2)
|
||||
/lock/node-0000000003 (客户端 3)
|
||||
```
|
||||
|
||||
**流程**:
|
||||
1. 客户端创建临时顺序节点
|
||||
2. 获取所有子节点,判断自己是否是序号最小的
|
||||
3. 如果是最小节点,获取锁
|
||||
4. 如果不是,监听前一个节点的删除事件
|
||||
|
||||
---
|
||||
|
||||
#### **Curator 实现**
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class ZkDistributedLock {
|
||||
|
||||
private final InterProcessMutex lock;
|
||||
|
||||
public ZkDistributedLock(CuratorFramework curatorFramework) {
|
||||
this.lock = new InterProcessMutex(curatorFramework, "/lock");
|
||||
}
|
||||
|
||||
public void acquire() {
|
||||
try {
|
||||
lock.acquire();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("获取锁失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
try {
|
||||
lock.release();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("释放锁失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用**:
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
@Autowired
|
||||
private ZkDistributedLock lock;
|
||||
|
||||
public void createOrder(Order order) {
|
||||
try {
|
||||
lock.acquire();
|
||||
|
||||
// 业务逻辑
|
||||
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Redis vs Zookeeper
|
||||
|
||||
| 特性 | Redis | Zookeeper |
|
||||
|------|-------|-----------|
|
||||
| **性能** | 高(内存操作) | 低(磁盘 + ZAB 协议) |
|
||||
| **可靠性** | 中(可能丢锁) | 高(CP 一致性) |
|
||||
| **实现复杂度** | 简单 | 复杂 |
|
||||
| **获取锁方式** | 轮询 | Watcher 通知(事件驱动) |
|
||||
| **锁释放** | 超时自动释放 | 会话结束自动释放 |
|
||||
| **适用场景** | 高并发、对一致性要求不高 | 严格一致性要求 |
|
||||
|
||||
---
|
||||
|
||||
### 6. Redisson 原理
|
||||
|
||||
#### **看门狗(自动续期)**
|
||||
|
||||
```java
|
||||
RLock lock = redisson.getLock("myLock");
|
||||
lock.lock(); // 默认 leaseTime = -1(启用看门狗)
|
||||
```
|
||||
|
||||
**原理**:
|
||||
```
|
||||
1. 加锁成功,启动看门狗线程
|
||||
2. 看门狗每 10 秒检查一次
|
||||
3. 如果锁还存在,续期 30 秒
|
||||
4. 客户端宕机,会话结束,看门狗停止,锁自动过期
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **公平锁**
|
||||
|
||||
```java
|
||||
RLock fairLock = redisson.getFairLock("myLock");
|
||||
fairLock.lock();
|
||||
```
|
||||
|
||||
**原理**:
|
||||
- 加锁时,添加到队列尾部
|
||||
- 按照请求顺序获取锁
|
||||
- 性能比非公平锁低
|
||||
|
||||
---
|
||||
|
||||
#### **读写锁**
|
||||
|
||||
```java
|
||||
RReadWriteLock rwLock = redisson.getReadWriteLock("myLock");
|
||||
rwLock.readLock().lock(); // 读锁(共享)
|
||||
rwLock.writeLock().lock(); // 写锁(独占)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 实际项目应用
|
||||
|
||||
#### **场景 1:秒杀系统**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class SeckillService {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redisson;
|
||||
|
||||
public void seckill(Long userId, Long productId) {
|
||||
RLock lock = redisson.getLock("seckill:" + productId);
|
||||
|
||||
try {
|
||||
// 尝试加锁(最多等待 3 秒,锁自动释放时间 10 秒)
|
||||
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
|
||||
|
||||
if (!locked) {
|
||||
throw new BusinessException("抢购人数过多,请稍后再试");
|
||||
}
|
||||
|
||||
// 1. 查询库存
|
||||
Integer stock = redisTemplate.opsForValue().get("stock:" + productId);
|
||||
if (stock == null || stock <= 0) {
|
||||
throw new BusinessException("库存不足");
|
||||
}
|
||||
|
||||
// 2. 扣减库存
|
||||
redisTemplate.opsForValue().decrement("stock:" + productId);
|
||||
|
||||
// 3. 创建订单
|
||||
createOrder(userId, productId);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new BusinessException("系统异常");
|
||||
} finally {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **场景 2:定时任务分布式锁**
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class ScheduledTask {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redisson;
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟
|
||||
public void execute() {
|
||||
RLock lock = redisson.getLock("scheduled-task");
|
||||
|
||||
try {
|
||||
// 只有一个实例执行
|
||||
boolean locked = lock.tryLock(0, 5, TimeUnit.MINUTES);
|
||||
|
||||
if (!locked) {
|
||||
return; // 其他实例正在执行
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
doTask();
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. 阿里 P7 加分项
|
||||
|
||||
**深度理解**:
|
||||
- 理解 CAP 理论在分布式锁中的体现
|
||||
- 理解 Redis 的 ZSet 实现延迟队列的原理
|
||||
- 理解 ZAB 协议和 ZK 的一致性保证
|
||||
|
||||
**实战经验**:
|
||||
- 有处理分布式锁死锁的经验
|
||||
- 有分布式锁性能优化的经验
|
||||
- 有分布式锁监控的经验
|
||||
|
||||
**架构能力**:
|
||||
- 能设计高可用的分布式锁方案
|
||||
- 能设计分布式锁的容灾方案
|
||||
- 能设计分布式锁的降级方案
|
||||
|
||||
**技术选型**:
|
||||
- 了解 Redis、Zookeeper、Etcd 等多种分布式锁实现
|
||||
- 能根据业务特点选择合适的方案
|
||||
- 有自研分布式锁的经验
|
||||
854
questions/01-分布式系统/数据库分库分表.md
Normal file
854
questions/01-分布式系统/数据库分库分表.md
Normal file
@@ -0,0 +1,854 @@
|
||||
# 数据库分库分表面试指南
|
||||
|
||||
## 1. 垂直分库、水平分库的区别
|
||||
|
||||
### 垂直分库
|
||||
|
||||
**定义**:按照业务模块将表拆分到不同的数据库中。
|
||||
|
||||
**特点**:
|
||||
- 每个数据库包含不同的业务表
|
||||
- 解决单表数据量过大问题
|
||||
- 便于数据管理和权限控制
|
||||
- 减少单个数据库的连接数压力
|
||||
|
||||
**图解**:
|
||||
```
|
||||
单数据库 垂直分库后
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ 用户表 │ │ 用户DB │
|
||||
├─────────┤ ├─────────┤
|
||||
│ 订单表 │ → │ 订单DB │
|
||||
├─────────┤ ├─────────┤
|
||||
│ 商品表 │ │ 商品DB │
|
||||
├─────────┤ ├─────────┤
|
||||
│ 支付表 │ │ 支付DB │
|
||||
└─────────┴──────────────┴─────────┘
|
||||
```
|
||||
|
||||
**代码示例**:
|
||||
```java
|
||||
// 垂直分库配置
|
||||
@Configuration
|
||||
public class VerticalShardingConfig {
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.datasource.user")
|
||||
public DataSource userDataSource() {
|
||||
return DataSourceBuilder.create().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.datasource.order")
|
||||
public DataSource orderDataSource() {
|
||||
return DataSourceBuilder.create().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ShardingDataSource shardingDataSource(
|
||||
@Qualifier("userDataSource") DataSource userDataSource,
|
||||
@Qualifier("orderDataSource") DataSource orderDataSource) {
|
||||
|
||||
Map<String, DataSource> dataSourceMap = new HashMap<>();
|
||||
dataSourceMap.put("user_ds", userDataSource);
|
||||
dataSourceMap.put("order_ds", orderDataSource);
|
||||
|
||||
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
|
||||
|
||||
// 用户表路由规则
|
||||
TableRuleConfiguration userTableRule = new TableRuleConfiguration("user", "user_ds.user_$->{user_id % 4}");
|
||||
shardingRuleConfig.getTableRuleConfigs().add(userTableRule);
|
||||
|
||||
// 订单表路由规则
|
||||
TableRuleConfiguration orderTableRule = new TableRuleConfiguration("order", "order_ds.order_$->{order_id % 4}");
|
||||
shardingRuleConfig.getTableRuleConfigs().add(orderTableRule);
|
||||
|
||||
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 水平分库
|
||||
|
||||
**定义**:将同一个表的数据按照某种规则拆分到不同的数据库中。
|
||||
|
||||
**特点**:
|
||||
- 每个数据库包含相同的表结构
|
||||
- 解决单表数据量过大问题
|
||||
- 提升查询性能和并发能力
|
||||
- 解决单机存储瓶颈
|
||||
|
||||
**图解**:
|
||||
```
|
||||
单数据库 水平分库后
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ 用户表 │ │ user_0 │
|
||||
│ 100W │ ├─────────┤
|
||||
├─────────┤ │ user_1 │
|
||||
│ 订单表 │ → ├─────────┤
|
||||
│ 500W │ │ user_2 │
|
||||
├─────────┤ ├─────────┤
|
||||
│ 商品表 │ │ user_3 │
|
||||
│ 200W │ └─────────┘
|
||||
└─────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
**代码示例**:
|
||||
```java
|
||||
// 水平分库配置
|
||||
@Configuration
|
||||
public class HorizontalShardingConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource horizontalShardingDataSource() {
|
||||
Map<String, DataSource> dataSourceMap = new HashMap<>();
|
||||
|
||||
// 创建4个分库
|
||||
for (int i = 0; i < 4; i++) {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(String.format("jdbc:mysql://127.0.0.1:3306/user_%d", i));
|
||||
dataSource.setUsername("root");
|
||||
dataSource.setPassword("password");
|
||||
dataSourceMap.put(String.format("user_ds_%d", i), dataSource);
|
||||
}
|
||||
|
||||
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
|
||||
|
||||
// 水平分库规则
|
||||
ShardingTableRuleConfiguration tableRule = new ShardingTableRuleConfiguration();
|
||||
tableLogicTable = "user";
|
||||
actualDataNodes = "user_ds_$->{0..3}.user_$->{user_id % 4}";
|
||||
shardingRuleConfig.getTableRuleConfigs().add(tableRule);
|
||||
|
||||
// 分片算法
|
||||
StandardShardingAlgorithm<Integer> shardingAlgorithm = new CustomModShardingAlgorithm();
|
||||
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(
|
||||
new StandardShardingStrategyConfiguration("user_id", shardingAlgorithm));
|
||||
|
||||
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig);
|
||||
}
|
||||
|
||||
public static class CustomModShardingAlgorithm implements StandardShardingAlgorithm<Integer> {
|
||||
@Override
|
||||
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) {
|
||||
int index = shardingValue.getValue() % 4;
|
||||
return availableTargetNames.stream()
|
||||
.filter(target -> target.endsWith("_" + index))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("no database available"));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 分库分表的策略
|
||||
|
||||
### 范围分片
|
||||
|
||||
**特点**:
|
||||
- 按照 ID 或时间范围进行分片
|
||||
- 查询效率高,范围查询方便
|
||||
- 数据分布不均匀,热点问题
|
||||
|
||||
**示例**:
|
||||
```sql
|
||||
-- 按用户ID范围分片
|
||||
CREATE TABLE user (
|
||||
id BIGINT,
|
||||
name VARCHAR(50),
|
||||
age INT
|
||||
) PARTITION BY RANGE (id) (
|
||||
PARTITION p0 VALUES LESS THAN (1000000),
|
||||
PARTITION p1 VALUES LESS THAN (2000000),
|
||||
PARTITION p2 VALUES LESS THAN (3000000),
|
||||
PARTITION p3 VALUES LESS THAN MAXVALUE
|
||||
);
|
||||
```
|
||||
|
||||
### Hash 分片
|
||||
|
||||
**特点**:
|
||||
- 数据分布均匀
|
||||
- 范围查询需要全表扫描
|
||||
- 扩容困难,数据迁移量大
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// Hash分片算法
|
||||
public class HashShardingAlgorithm implements StandardShardingAlgorithm<Integer> {
|
||||
private final int shardingCount;
|
||||
|
||||
public HashShardingAlgorithm(int shardingCount) {
|
||||
this.shardingCount = shardingCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) {
|
||||
int hash = shardingValue.getValue() % shardingCount;
|
||||
if (hash < 0) {
|
||||
hash = Math.abs(hash);
|
||||
}
|
||||
String target = availableTargetNames.stream()
|
||||
.filter(name -> name.endsWith("_" + hash))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("no database available"));
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 一致性哈希分片
|
||||
|
||||
**特点**:
|
||||
- 扩容时数据迁移量小
|
||||
- 节点增减时只需迁移少量数据
|
||||
- 实现相对复杂
|
||||
|
||||
**图解**:
|
||||
```
|
||||
一致性哈希环
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 节点1 │ │ 节点2 │ │ 节点3 │ │ 节点4 │
|
||||
│ Hash: │ │ Hash: │ │ Hash: │ │ Hash: │
|
||||
│ 1000 │ │ 3000 │ │ 5000 │ │ 7000 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
↓ ↓ ↓ ↓
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 1500 │ │ 3500 │ │ 5500 │ │ 7500 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
↓ ↓ ↓ ↓
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 2000 │ │ 4000 │ │ 6000 │ │ 8000 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
**代码示例**:
|
||||
```java
|
||||
public class ConsistentHashSharding {
|
||||
private final SortedMap<Integer, String> circle = new TreeMap<>();
|
||||
private final int virtualNodeCount;
|
||||
|
||||
public ConsistentHashSharding(List<String> nodes, int virtualNodeCount) {
|
||||
this.virtualNodeCount = virtualNodeCount;
|
||||
for (String node : nodes) {
|
||||
addNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
private void addNode(String node) {
|
||||
for (int i = 0; i < virtualNodeCount; i++) {
|
||||
String virtualNode = node + "#" + i;
|
||||
int hash = hash(virtualNode);
|
||||
circle.put(hash, virtualNode);
|
||||
}
|
||||
}
|
||||
|
||||
public String getNode(String key) {
|
||||
if (circle.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int hash = hash(key);
|
||||
SortedMap<Integer, String> tailMap = circle.tailMap(hash);
|
||||
if (tailMap.isEmpty()) {
|
||||
return circle.get(circle.firstKey());
|
||||
}
|
||||
return tailMap.get(tailMap.firstKey());
|
||||
}
|
||||
|
||||
private int hash(String key) {
|
||||
final int p = 16777619;
|
||||
int hash = (int) 2166136261L;
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
hash = (hash ^ key.charAt(i)) * p;
|
||||
}
|
||||
hash += hash << 13;
|
||||
hash ^= hash >> 7;
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 17;
|
||||
hash += hash << 5;
|
||||
return hash < 0 ? -hash : hash;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 地理位置(GeoHash)分片
|
||||
|
||||
**特点**:
|
||||
- 按地理位置进行分片
|
||||
- 适合有地理属性的业务
|
||||
- 查询效率高
|
||||
|
||||
**示例**:
|
||||
```java
|
||||
// GeoHash分片算法
|
||||
public class GeoHashSharding {
|
||||
private final Geohash geohash = new Geohash();
|
||||
private final Map<String, String> geoToShard = new HashMap<>();
|
||||
|
||||
public String getShardByLocation(double lat, double lng) {
|
||||
String geoCode = geohash.encode(lat, lng, 8);
|
||||
return geoToShard.get(geoCode.substring(0, 2)); // 前两位决定分片
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 分库分表后的问题
|
||||
|
||||
### 跨库 JOIN 问题
|
||||
|
||||
**问题**:无法直接跨库执行 JOIN 操作
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **应用层 JOIN**
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
public OrderDTO getOrderWithUser(Long orderId) {
|
||||
// 1. 查询订单
|
||||
Order order = orderMapper.selectById(orderId);
|
||||
|
||||
// 2. 查询用户
|
||||
User user = userMapper.selectById(order.getUserId());
|
||||
|
||||
// 3. 组装结果
|
||||
OrderDTO dto = new OrderDTO();
|
||||
BeanUtils.copyProperties(order, dto);
|
||||
dto.setUser(user);
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **中间件自动路由**
|
||||
```java
|
||||
// 使用 MyCAT 自动路由
|
||||
@Configuration
|
||||
@ShardingTable("order_detail")
|
||||
public class OrderShardingConfig {
|
||||
|
||||
@Bean
|
||||
public TableRule orderDetailRule() {
|
||||
TableRule rule = new TableRule();
|
||||
rule.setLogicTable("order_detail");
|
||||
rule.setActualDataNodes("order_ds_$->{0..3}.order_detail_$->{order_id % 4}");
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **ER 分片**
|
||||
```sql
|
||||
-- 使用 ER 分片,保证父子表在同一分片
|
||||
CREATE TABLE user (
|
||||
id BIGINT AUTO_INCREMENT,
|
||||
name VARCHAR(50),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE order (
|
||||
id BIGINT AUTO_INCREMENT,
|
||||
user_id BIGINT,
|
||||
amount DECIMAL(10,2),
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (user_id) REFERENCES user(id)
|
||||
) PARTITION BY HASH(user_id);
|
||||
```
|
||||
|
||||
### 分布式事务问题
|
||||
|
||||
**问题**:跨多个数据库的事务一致性
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **TCC 模式**
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
@Transactional
|
||||
public void createOrder(OrderDTO orderDTO) {
|
||||
// Try 阶段
|
||||
orderRepository.createOrder(orderDTO);
|
||||
|
||||
// 预扣库存
|
||||
inventoryService.reserveInventory(orderDTO.getItems());
|
||||
|
||||
// 预扣账户余额
|
||||
paymentService.reserveAmount(orderDTO.getUserId(), orderDTO.getTotalAmount());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void confirmOrder(Long orderId) {
|
||||
// Confirm 阶段
|
||||
orderRepository.confirmOrder(orderId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelOrder(Long orderId) {
|
||||
// Cancel 阶段
|
||||
orderRepository.cancelOrder(orderId);
|
||||
inventoryService.cancelReserve(orderId);
|
||||
paymentService.cancelReserve(orderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **XA 分布式事务**
|
||||
```java
|
||||
// 使用 Atomikos 实现XA事务
|
||||
@Component
|
||||
public class XATransactionManager {
|
||||
|
||||
@Resource
|
||||
private UserTransactionManager userTransactionManager;
|
||||
|
||||
@Resource
|
||||
private UserTransaction userTransaction;
|
||||
|
||||
public void execute(Runnable operation) throws SystemException {
|
||||
userTransaction.begin();
|
||||
try {
|
||||
operation.run();
|
||||
userTransaction.commit();
|
||||
} catch (Exception e) {
|
||||
userTransaction.rollback();
|
||||
throw new SystemException("XA transaction failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页问题
|
||||
|
||||
**问题**:跨分页查询和排序
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **Limit 分页**
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
public PageResult<UserVO> getPageUsers(int page, int size) {
|
||||
// 查询所有分库
|
||||
List<UserVO> allUsers = new ArrayList<>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
List<UserVO> users = userMapper.selectByShard(i, page, size);
|
||||
allUsers.addAll(users);
|
||||
}
|
||||
|
||||
// 内存分页
|
||||
int fromIndex = (page - 1) * size;
|
||||
int toIndex = Math.min(fromIndex + size, allUsers.size());
|
||||
|
||||
List<UserVO> pageUsers = allUsers.subList(fromIndex, toIndex);
|
||||
return new PageResult<>(pageUsers, allUsers.size());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **游标分页**
|
||||
```java
|
||||
@Service
|
||||
public class CursorPagingService {
|
||||
|
||||
public List<OrderVO> getOrdersByCursor(Long lastId, int limit) {
|
||||
List<OrderVO> orders = new ArrayList<>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
List<OrderVO> shardOrders = orderMapper.selectByIdGreaterThan(lastId, limit, i);
|
||||
orders.addAll(shardOrders);
|
||||
}
|
||||
|
||||
// 按ID排序并去重
|
||||
orders.sort(Comparator.comparingLong(OrderVO::getId));
|
||||
return orders.stream().limit(limit).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 分库分表的中间件
|
||||
|
||||
### MyCAT
|
||||
|
||||
**特点**:
|
||||
- 支持 SQL 路由和读写分离
|
||||
- 支持分片规则和分片算法
|
||||
- 兼容 MySQL 协议
|
||||
|
||||
**配置示例**:
|
||||
```xml
|
||||
<!-- schema.xml -->
|
||||
<schema name="TESTDB" sqlMaxLimit="100">
|
||||
<!-- 表规则配置 -->
|
||||
<table name="user" dataNode="dn1,dn2,dn3" rule="sharding-by-id"/>
|
||||
|
||||
<!-- 分片规则 -->
|
||||
<rule name="sharding-by-id">
|
||||
<rule-columns>id</rule-columns>
|
||||
<algorithm-class>io.mycat.route.function.PartitionByMod</algorithm-class>
|
||||
<param name="count">3</param>
|
||||
</rule>
|
||||
</schema>
|
||||
|
||||
<dataNode name="dn1" dataHost="localhost1" database="db1"/>
|
||||
<dataNode name="dn2" dataHost="localhost1" database="db2"/>
|
||||
<dataNode name="dn3" dataHost="localhost1" database="db3"/>
|
||||
|
||||
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native">
|
||||
<heartbeat>select user()</heartbeat>
|
||||
<writeHost host="hostM1" url="192.168.1.100:3306" user="root" password="password"/>
|
||||
</dataHost>
|
||||
```
|
||||
|
||||
### ShardingSphere
|
||||
|
||||
**特点**:
|
||||
- 轻量级、可插拔
|
||||
- 支持多种数据库
|
||||
- 提供治理和监控能力
|
||||
|
||||
**配置示例**:
|
||||
```java
|
||||
// Java 配置
|
||||
@Configuration
|
||||
public class ShardingSphereConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource shardingDataSource() {
|
||||
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
|
||||
|
||||
// 分片规则
|
||||
TableRuleConfiguration userTableRule = new TableRuleConfiguration("user",
|
||||
"user_ds_$->{0..3}.user_$->{user_id % 4}");
|
||||
shardingRuleConfig.getTableRuleConfigs().add(userTableRule);
|
||||
|
||||
// 绑定表
|
||||
shardingRuleConfig.getBindingTableGroups().add("user,order");
|
||||
|
||||
// 广播表
|
||||
shardingRuleConfig.getBroadcastTables().add("dict");
|
||||
|
||||
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig);
|
||||
}
|
||||
|
||||
private Map<String, DataSource> createDataSourceMap() {
|
||||
Map<String, DataSource> result = new HashMap<>();
|
||||
|
||||
// 创建4个分片数据源
|
||||
for (int i = 0; i < 4; i++) {
|
||||
result.put("user_ds_" + i, createDataSource("localhost", 3306 + i, "root", "password", "user_" + i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private DataSource createDataSource(String host, int port, String username, String password, String database) {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf-8", host, port, database));
|
||||
dataSource.setUsername(username);
|
||||
dataSource.setPassword(password);
|
||||
return dataSource;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Proxy 模式 vs JDBC 模式
|
||||
|
||||
**Proxy 模式**:
|
||||
- 通过代理层转发SQL
|
||||
- 无需修改应用代码
|
||||
- 性能损耗较大
|
||||
|
||||
**JDBC 模式**:
|
||||
- 直接集成JDBC驱动
|
||||
- 性能更高
|
||||
- 需要修改应用配置
|
||||
|
||||
## 5. 实际项目中的分库分表设计
|
||||
|
||||
### 电商系统分片设计
|
||||
|
||||
**业务场景**:
|
||||
- 用户表:按用户ID哈希分片
|
||||
- 订单表:按用户ID分片,保证用户订单在同一分片
|
||||
- 商品表:按商品ID范围分片
|
||||
- 交易表:按时间分片
|
||||
|
||||
**配置示例**:
|
||||
```java
|
||||
// 电商系统分片配置
|
||||
@Configuration
|
||||
public class EcommerceShardingConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource ecommerceShardingDataSource() {
|
||||
Map<String, DataSource> dataSourceMap = new HashMap<>();
|
||||
|
||||
// 创建分库
|
||||
for (int i = 0; i < 8; i++) {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(String.format("jdbc:mysql://127.0.0.1:3306/ecommerce_%d", i));
|
||||
dataSource.setUsername("root");
|
||||
dataSource.setPassword("password");
|
||||
dataSourceMap.put("ecommerce_ds_" + i, dataSource);
|
||||
}
|
||||
|
||||
ShardingRuleConfiguration ruleConfig = new ShardingRuleConfiguration();
|
||||
|
||||
// 用户表 - 哈希分片
|
||||
TableRuleConfiguration userRule = new TableRuleConfiguration("user",
|
||||
"ecommerce_ds_$->{0..7}.user_$->{user_id % 8}");
|
||||
ruleConfig.getTableRuleConfigs().add(userRule);
|
||||
|
||||
// 订单表 - 用户ID分片
|
||||
TableRuleConfiguration orderRule = new TableRuleConfiguration("order",
|
||||
"ecommerce_ds_$->{0..7}.order_$->{user_id % 8}");
|
||||
ruleConfig.getTableRuleConfigs().add(orderRule);
|
||||
|
||||
// 商品表 - 范围分片
|
||||
TableRuleConfiguration productRule = new TableRuleConfiguration("product",
|
||||
"ecommerce_ds_$->{0..7}.product_$->{product_id % 8}");
|
||||
ruleConfig.getTableRuleConfigs().add(productRule);
|
||||
|
||||
// 交易表 - 时间分片
|
||||
TableRuleConfiguration tradeRule = new TableRuleConfiguration("trade",
|
||||
"ecommerce_ds_$->{0..7}.trade_$->{YEAR(create_time) * 12 + MONTH(create_time)}");
|
||||
ruleConfig.getTableRuleConfigs().add(tradeRule);
|
||||
|
||||
// 绑定表
|
||||
ruleConfig.getBindingTableGroups().add("user,order");
|
||||
ruleConfig.getBindingTableGroups().add("product,trade_detail");
|
||||
|
||||
// 广播表
|
||||
ruleConfig.getBroadcastTables().add("sys_config");
|
||||
|
||||
return ShardingDataSourceFactory.createDataSource(dataSourceMap, ruleConfig);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 社交系统分片设计
|
||||
|
||||
**业务场景**:
|
||||
- 用户表:按用户ID分片
|
||||
- 好友关系表:按用户ID哈希分片
|
||||
- 动态表:按用户ID分片
|
||||
- 评论表:按业务ID分片
|
||||
|
||||
**设计原则**:
|
||||
1. **数据访问模式**:根据查询模式选择分片键
|
||||
2. **数据量均衡**:确保各分片数据量大致相等
|
||||
3. **跨分片查询少**:减少需要跨分片的查询
|
||||
4. **分片键选择**:选择区分度高的字段
|
||||
|
||||
### 扩容方案
|
||||
|
||||
**垂直扩容**:
|
||||
- 增加分库数量
|
||||
- 重新分配数据
|
||||
- 需要数据迁移
|
||||
|
||||
**水平扩容**:
|
||||
- 增加分片数量
|
||||
- 使用一致性哈希减少迁移
|
||||
|
||||
**代码示例**:
|
||||
```java
|
||||
// 动态扩容的哈希分片算法
|
||||
public class DynamicShardingAlgorithm implements StandardShardingAlgorithm<Integer> {
|
||||
private volatile int shardingCount = 4;
|
||||
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
|
||||
|
||||
public void updateShardingCount(int newCount) {
|
||||
rwLock.writeLock().lock();
|
||||
try {
|
||||
this.shardingCount = newCount;
|
||||
} finally {
|
||||
rwLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) {
|
||||
rwLock.readLock().lock();
|
||||
try {
|
||||
int index = shardingValue.getValue() % shardingCount;
|
||||
if (index < 0) {
|
||||
index = Math.abs(index);
|
||||
}
|
||||
|
||||
return availableTargetNames.stream()
|
||||
.filter(name -> name.endsWith("_" + index))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("no database available"));
|
||||
} finally {
|
||||
rwLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 阿里 P7 加分项
|
||||
|
||||
### 分库分表监控体系
|
||||
|
||||
```java
|
||||
// 分库分表监控组件
|
||||
@Component
|
||||
public class ShardingMonitor {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final ShardingDataSource shardingDataSource;
|
||||
|
||||
@Scheduled(fixedRate = 5000)
|
||||
public void monitorShardingMetrics() {
|
||||
// 监控各分片性能
|
||||
for (int i = 0; i < 8; i++) {
|
||||
DataSource dataSource = shardingDataSource.getDataSource("ecommerce_ds_" + i);
|
||||
|
||||
// 连接池监控
|
||||
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
|
||||
meterRegistry.gauge("sharding.pool.active", i, hikariDataSource::getHikariPoolMXBean);
|
||||
meterRegistry.gauge("sharding.pool.idle", i, hikariDataSource::getHikariPoolMXBean);
|
||||
|
||||
// 慢查询监控
|
||||
monitorSlowQueries(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void monitorSlowQueries(int shardIndex) {
|
||||
// 查询慢查询
|
||||
List<Map<String, Object>> slowQueries = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM slow_log WHERE execution_time > 1000 ORDER BY execution_time DESC LIMIT 10");
|
||||
|
||||
slowQueries.forEach(query -> {
|
||||
meterRegistry.counter("sharding.slow.query",
|
||||
"shard", String.valueOf(shardIndex),
|
||||
"sql", (String) query.get("query"))
|
||||
.increment();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动化运维平台
|
||||
|
||||
```java
|
||||
// 分库分表自动化迁移工具
|
||||
@Service
|
||||
public class ShardingMigrationService {
|
||||
|
||||
private final ShardingDataSource shardingDataSource;
|
||||
private final ExecutorService executorService;
|
||||
|
||||
public void migrateData(String table, int oldShardCount, int newShardCount) {
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
|
||||
// 并行迁移各分片
|
||||
for (int oldShard = 0; oldShard < oldShardCount; oldShard++) {
|
||||
for (int newShard = 0; newShard < newShardCount; newShard++) {
|
||||
final int fOldShard = oldShard;
|
||||
final int fNewShard = newShard;
|
||||
|
||||
futures.add(executorService.submit(() -> {
|
||||
migrateShardData(table, fOldShard, fNewShard);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待迁移完成
|
||||
for (Future<?> future : futures) {
|
||||
try {
|
||||
future.get();
|
||||
} catch (Exception e) {
|
||||
log.error("Migration failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateShardData(String table, int oldShard, int newShard) {
|
||||
// 查询源数据
|
||||
List<Map<String, Object>> sourceData = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM " + table + " WHERE id % ? = ?", oldShard, oldShard);
|
||||
|
||||
// 目标数据源
|
||||
DataSource targetDataSource = shardingDataSource.getDataSource("ecommerce_ds_" + newShard);
|
||||
JdbcTemplate targetJdbcTemplate = new JdbcTemplate(targetDataSource);
|
||||
|
||||
// 批量插入
|
||||
BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() {
|
||||
@Override
|
||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||
Map<String, Object> row = sourceData.get(i);
|
||||
// 设置参数
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatchSize() {
|
||||
return sourceData.size();
|
||||
}
|
||||
};
|
||||
|
||||
targetJdbcTemplate.batchUpdate("INSERT INTO " + table + " VALUES (?, ?, ?)", setter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 高级分片策略
|
||||
|
||||
```java
|
||||
// 基于业务规则的复合分片策略
|
||||
@Component
|
||||
public class BusinessRuleShardingAlgorithm implements StandardShardingAlgorithm<BusinessKey> {
|
||||
|
||||
@Override
|
||||
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<BusinessKey> shardingValue) {
|
||||
BusinessKey businessKey = shardingValue.getValue();
|
||||
|
||||
// 复合分片规则:用户ID + 时间 + 业务类型
|
||||
String shardKey = businessKey.getUserId() + "_" +
|
||||
businessKey.getCreateTime() + "_" +
|
||||
businessKey.getBusinessType();
|
||||
|
||||
// 使用加密哈希保证分布均匀
|
||||
int hash = murmurHash(shardKey);
|
||||
int index = Math.abs(hash % availableTargetNames.size());
|
||||
|
||||
return availableTargetNames.stream()
|
||||
.filter(name -> name.endsWith("_" + index))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("no database available"));
|
||||
}
|
||||
|
||||
private int murmurHash(String key) {
|
||||
// MurmurHash 实现
|
||||
return key.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
// 分片键实体
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class BusinessKey {
|
||||
private Long userId;
|
||||
private LocalDateTime createTime;
|
||||
private String businessType;
|
||||
}
|
||||
```
|
||||
|
||||
### 总结
|
||||
|
||||
分库分表是大型数据库架构的必经之路,需要:
|
||||
|
||||
1. **合理选择分片策略**:根据业务特点选择合适的分片算法
|
||||
2. **解决技术难题**:重点关注跨库JOIN、分布式事务、分页等问题
|
||||
3. **完善监控体系**:建立完善的监控和告警机制
|
||||
4. **自动化运维**:实现自动化的分片迁移和扩容
|
||||
5. **性能优化**:持续优化查询性能和系统稳定性
|
||||
|
||||
在面试中,除了技术细节,还要体现对业务的理解、系统的架构能力和性能优化的经验。
|
||||
1909
questions/01-分布式系统/数据库锁机制.md
Normal file
1909
questions/01-分布式系统/数据库锁机制.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user