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:
yasinshaw
2026-03-01 00:10:53 +08:00
parent fe2e6dc2f2
commit 0e46a367c4
47 changed files with 0 additions and 0 deletions

View 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一致性 + 分区容错)**
**特点**
- 保证数据一致性
- 分区时部分节点不可用
**适用场景**
- 金融系统(转账、支付)
- 库存系统(超卖不可接受)
**代表系统**
- ZookeeperCP
- HBaseCP
- Redis ClusterCP主从切换时短暂不可用
**示例**
```java
// Zookeeper 写入流程
client.setData("/node", data);
// 等待大多数节点确认
// 如果网络分区,部分节点无法写入
```
---
**2. AP可用性 + 分区容错)**
**特点**
- 保证系统可用
- 分区时可能读到脏数据
**适用场景**
- 社交媒体(点赞、评论)
- 内容分发CDN
- 用户行为统计
**代表系统**
- CassandraAP
- DynamoDBAP
- EurekaAP
- DNSAP
**示例**
```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传统数据库 | BASENoSQL |
|------|-------------------|---------------|
| **一致性** | 强一致性(立即) | 最终一致性(延迟) |
| **可用性** | 可能(锁、事务) | 高(无锁、异步) |
| **隔离性** | 严格(锁) | 松散 |
| **持久性** | 强 | 弱(可能丢失) |
---
#### **最终一致性的实现**
**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. 主流注册中心对比
#### **ZookeeperCP**
**特点**
- 保证一致性ZAB 协议)
- Leader 挂了会重新选举(期间不可用)
**适用场景**
- 需要强一致性
- 对可用性要求不高(如配置中心)
**示例**
```java
// Zookeeper 注册
zk.create("/services/order/192.168.1.10:8080", data, ZooDefs.Ids.OPEN, NodeMode.EPHEMERAL);
```
---
#### **EurekaAP**
**特点**
- 保证可用性
- 客户端缓存注册信息
- 网络分区时仍能提供服务
**适用场景**
- 对可用性要求高
- 可容忍短暂的服务发现不准确
**示例**
```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;
}
```
---
#### **NacosAP + CP**
**特点**
- 支持 AP 和 CP 切换
- 默认 AP临时实例
- 可配置 CP持久化实例
**配置**
```yaml
# Nacos 配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
ephemeral: true # true=AP, false=CP
```
---
#### **ConsulCP**
**特点**
- 保证一致性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 特性
- 能根据业务特点选择合适的技术
- 有分布式系统设计经验

File diff suppressed because it is too large Load Diff

View 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 万 key83%
```
**使用一致性哈希**
```
迁移量10 万 × 2/5 ≈ 4 万 key40%
```
**实现**
```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 等负载均衡器的一致性哈希配置
- 能根据业务特点选择合适的哈希算法

File diff suppressed because it is too large Load Diff

View 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
- 实例 11, 2, 3, 4, 5
- 实例 21, 2, 3, 4, 5 ← 冲突!
```
---
#### **分布式 ID 的核心要求**
| 要求 | 说明 | 示例 |
|------|------|------|
| **全局唯一性** | 不能重复 | 订单号不能重复 |
| **有序性** | 趋势递增(可选) | 按时间排序的订单号 |
| **高性能** | 生成速度快 | 支持 10 万+/秒 |
| **高可用** | 服务不中断 | 宕机后仍可生成 |
| **信息安全** | 不暴露业务信息 | 不暴露订单总量 |
---
### 2. 常见分布式 ID 方案对比
| 方案 | 唯一性 | 有序性 | 性能 | 复杂度 | 适用场景 |
|------|--------|--------|------|--------|----------|
| **UUID** | ✅ | ❌ | ⭐⭐⭐⭐⭐ | 低 | 非主键、内部 ID |
| **数据库自增** | ✅ | ✅ | ⭐⭐ | 低 | 小规模、并发低 |
| **Redis INCR** | ✅ | ✅ | ⭐⭐⭐ | 低 | 中小规模 |
| **Snowflake** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 中 | 大规模、高并发 |
| **号段模式** | ✅ | ✅ | ⭐⭐⭐⭐ | 中 | 大规模、高性能 |
| **美团 Leaf** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 高 | 金融级、高可用 |
---
### 3. UUID
#### **原理**
UUIDUniversally 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+ 树叶子节点顺序填充
无序 IDUUID
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
- 美团 LeafSnowflake 模式)
- 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 1workerId=1
Leaf Server 2workerId=2
Leaf Server 3workerId=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 生成器的经验

View 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. 实际项目选择建议
**选择决策树**
```
是否需要强一致性?
├─ 是 → 2PCXA 事务)
└─ 否 → 最终一致性
├─ 业务可以拆分为 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 理论在实际场景中的权衡
- 能根据业务特点选择合适的一致性级别
- 有监控和告警体系,能快速定位分布式事务问题
**架构能力**
- 能设计支持多种分布式事务模式的统一框架
- 考虑降级和熔断策略
- 有混沌工程实践(注入故障测试系统恢复能力)

View 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 上获取锁(冲突!)
```
**解决方案RedlockRedis 分布式锁算法)**
**原理**
- 向多个独立的 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 等多种分布式锁实现
- 能根据业务特点选择合适的方案
- 有自研分布式锁的经验

View 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. **性能优化**:持续优化查询性能和系统稳定性
在面试中,除了技术细节,还要体现对业务的理解、系统的架构能力和性能优化的经验。

File diff suppressed because it is too large Load Diff