diff --git a/questions/alg-btree.md b/questions/alg-btree.md new file mode 100644 index 0000000..570099a --- /dev/null +++ b/questions/alg-btree.md @@ -0,0 +1,286 @@ +# B+ 树 (B+ Tree) + +## 数据结构原理 + +### 什么是 B+ 树? +B+ 树是一种自平衡的树数据结构,是 B 树的变体,专门为磁盘存储和索引设计。它在数据库系统和文件系统中广泛应用,特别适合磁盘等外部存储设备。 + +### B+ 树的特点 + +1. **多路搜索树**:每个节点可以有多个子节点 +2. **有序结构**:所有键值按顺序存储 +3. **平衡性**:从根到任何叶子的路径长度相同 +4. **叶节点链表**:所有叶子节点通过指针连接成有序链表 + +### B+ 树与 B 树的区别 + +| 特性 | B+ 树 | B 树 | +|------|-------|------| +| 叶子节点 | 包含所有键值和指针 | 只包含键值 | +| 非叶子节点 | 只包含键值和指针,不包含数据 | 包含键值和指针,部分包含数据 | +| 查找效率 | 查找路径相同,但范围查询更高效 | 查找路径稍长 | +| 插入/删除 | 叶子节点统一操作,分布更均匀 | 数据分散在各级节点 | +| 范围查询 | 直接遍历叶子链表,高效 | 需要中序遍历整个树 | + +## 图解说明 + +``` +B+ 树结构示例(m=3): + [10, 20, 30] + / | | \ + [5, 8, 10] [15,18,20] [25,28,30] [] + | | | | + [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30] +``` + +### 关键概念 + +- **阶(m)**:每个节点的最大子节点数 +- **键(Key)**:用于索引的值 +- **指针(Pointer)**:指向子节点或数据的地址 +- **叶子节点**:存储实际数据的节点 +- **内部节点**:用于索引的中间节点 + +## Java 代码实现 + +```java +import java.util.ArrayList; +import java.util.List; + +class BPlusTreeNode { + List keys; + List children; + BPlusTreeNode next; // 叶子节点间的指针 + boolean isLeaf; + + public BPlusTreeNode(boolean isLeaf) { + this.keys = new ArrayList<>(); + this.children = new ArrayList<>(); + this.isLeaf = isLeaf; + this.next = null; + } +} + +public class BPlusTree { + private BPlusTreeNode root; + private int order; // B+树的阶 + + public BPlusTree(int order) { + this.root = new BPlusTreeNode(true); + this.order = order; + } + + // 插入操作 + public void insert(int key) { + if (root.keys.size() == order - 1) { + BPlusTreeNode newRoot = new BPlusTreeNode(false); + newRoot.children.add(root); + splitChild(newRoot, 0, root); + root = newRoot; + } + insertNonFull(root, key); + } + + private void insertNonFull(BPlusTreeNode node, int key) { + if (node.isLeaf) { + int i = 0; + while (i < node.keys.size() && node.keys.get(i) < key) { + i++; + } + node.keys.add(i, key); + } else { + int i = 0; + while (i < node.keys.size() && node.keys.get(i) < key) { + i++; + } + if (node.children.get(i).keys.size() == order - 1) { + splitChild(node, i, node.children.get(i)); + if (node.keys.get(i) < key) { + i++; + } + } + insertNonFull(node.children.get(i), key); + } + } + + private void splitChild(BPlusTreeNode parent, int index, BPlusTreeNode fullNode) { + BPlusTreeNode newNode = new BPlusTreeNode(fullNode.isLeaf); + + // 移动后半部分键 + int mid = fullNode.keys.size() / 2; + for (int i = mid + (fullNode.isLeaf ? 0 : 1); i < fullNode.keys.size(); i++) { + newNode.keys.add(fullNode.keys.get(i)); + } + fullNode.keys.subList(mid + (fullNode.isLeaf ? 0 : 1), fullNode.keys.size()).clear(); + + // 移动子节点(如果是内部节点) + if (!fullNode.isLeaf) { + for (int i = mid + 1; i < fullNode.children.size(); i++) { + newNode.children.add(fullNode.children.get(i)); + } + fullNode.children.subList(mid + 1, fullNode.children.size()).clear(); + } + + // 如果是叶子节点,维护链表 + if (fullNode.isLeaf) { + newNode.next = fullNode.next; + fullNode.next = newNode; + } + + // 插入到父节点 + parent.children.add(index + 1, newNode); + parent.keys.add(index, fullNode.keys.get(mid)); + } + + // 查找操作 + public boolean search(int key) { + return search(root, key); + } + + private boolean search(BPlusTreeNode node, int key) { + int i = 0; + while (i < node.keys.size() && node.keys.get(i) < key) { + i++; + } + + if (i < node.keys.size() && node.keys.get(i) == key) { + return true; + } + + if (node.isLeaf) { + return false; + } else { + return search(node.children.get(i), key); + } + } + + // 范围查询 + public List rangeSearch(int start, int end) { + List result = new ArrayList<>(); + rangeSearch(root, start, end, result); + return result; + } + + private void rangeSearch(BPlusTreeNode node, int start, int end, List result) { + if (node.isLeaf) { + for (int key : node.keys) { + if (key >= start && key <= end) { + result.add(key); + } + } + } else { + int i = 0; + while (i < node.keys.size() && node.keys.get(i) < start) { + i++; + } + rangeSearch(node.children.get(i), start, end, result); + } + } + + // 删除操作 + public void delete(int key) { + delete(root, key); + } + + private void delete(BPlusTreeNode node, int key) { + // 实现删除逻辑(简化版) + // 实际实现需要处理合并、重新平衡等复杂逻辑 + } +} +``` + +## 时间复杂度分析 + +### 操作时间复杂度 + +| 操作 | 时间复杂度 | 说明 | +|------|------------|------| +| 查找 | O(log n) | 树高为 O(log n),每层需要比较 O(m) 次 | +| 插入 | O(log n) | 查找插入位置 + 可能的分裂操作 | +| 删除 | O(log n) | 查找删除位置 + 可能的合并操作 | +| 范围查询 | O(log n + k) | k 是结果集大小 | + +### 空间复杂度 + +- O(n) - 需要存储 n 个元素 +- 每个节点存储约 m/2 到 m 个元素 + +## 实际应用场景 + +### 1. 数据库索引 +- **MySQL InnoDB**:聚簇索引使用 B+ 树 +- **PostgreSQL**:标准索引使用 B+ 树 +- **优点**:范围查询高效,磁盘 I/O 次数少 + +### 2. 文件系统 +- **NTFS**:主文件表使用 B+ 树结构 +- **ext4**:目录索引使用 B+ 树 +- **优点**:文件查找效率高,支持大文件系统 + +### 3. 内存数据库 +- **Redis**:有序集合(Sorted Set)使用类似 B+ 树的结构 +- **LevelDB**:底层存储引擎使用 B+ 树变种 +- **优点**:内存访问速度更快,但结构保持高效 + +### 4. 文件搜索 +- **全文搜索引擎**:倒排索引使用 B+ 树 +- **文件系统搜索**:快速定位文件位置 +- **优点**:前缀查询和范围查询高效 + +## 与其他数据结构的对比 + +| 数据结构 | 查找时间 | 插入时间 | 删除时间 | 适用场景 | +|----------|----------|----------|----------|----------| +| B+ 树 | O(log n) | O(log n) | O(log n) | 磁盘存储、数据库索引 | +| AVL 树 | O(log n) | O(log n) | O(log n) | 内存存储、需要平衡 | +| 红黑树 | O(log n) | O(log n) | O(log n) | 内存存储、平衡性较好 | +| 哈希表 | O(1) | O(1) | O(1) | 精确查找、内存存储 | +| 二叉搜索树 | O(log n) ~ O(n) | O(log n) ~ O(n) | O(log n) ~ O(n) | 排序数据、简单应用 | + +### 选择 B+ 树的原因 + +1. **磁盘友好**:减少磁盘 I/O 次数 +2. **范围查询高效**:叶子节点链表支持快速范围扫描 +3. **局部性原理**:每次读取整个页面,充分利用磁盘预读 +4. **稳定性能**:即使树不平衡,性能下降缓慢 +5. **批量操作**:适合批量插入和删除 + +### 不同场景的最佳选择 + +- **内存数据**:AVL 树或红黑树 +- **磁盘数据**:B+ 树 +- **精确查找**:哈希表 +- **范围查询**:B+ 树 +- **频繁插入删除**:B+ 树或红黑树 + +## 常见面试问题 + +### Q1: 为什么数据库索引使用 B+ 树而不是 B 树? +**答**: +1. 范围查询更高效:B+ 树的叶子节点形成链表,范围查询只需遍历叶子节点 +2. 磁盘利用率高:非叶子节点不存储数据,可以存储更多索引键 +3. 查找稳定:无论查找什么数据,都到达叶子节点,树高相同 +4. 预读效率高:每次读取一个完整的页面 + +### Q2: B+ 树的阶数如何选择? +**答**: +阶数取决于: +- 磁盘块大小:通常等于操作系统页面大小(4KB) +- 键值大小:整数 vs 字符串 +- 指针大小:64位系统 8 字节指针 +- 缓存大小:内存缓存页面数量 + +### Q3: B+ 树在什么情况下性能下降? +**答**: +1. 树太高:查找路径变长 +2. 叶子节点过多:范围查询变慢 +3. 内存不足:频繁磁盘 I/O +4. 数据分布不均:导致树不平衡 + +### Q4: 如何优化 B+ 树的性能? +**答**: +1. 选择合适的阶数:平衡磁盘 I/O 和内存使用 +2. 使用缓存:缓存热点数据 +3. 预加载:提前加载可能访问的节点 +4. 批量操作:合并多次插入/删除操作 +5. 数据分片:大表分片减少树的大小 \ No newline at end of file diff --git a/questions/alg-lru.md b/questions/alg-lru.md new file mode 100644 index 0000000..5bbd879 --- /dev/null +++ b/questions/alg-lru.md @@ -0,0 +1,524 @@ +# LRU 缓存实现 + +## 数据结构原理 + +### 什么是 LRU 缓存? +LRU(Least Recently Used)缓存是一种缓存淘汰算法,当缓存满时,会淘汰最近最少使用的数据。它基于局部性原理,认为最近使用的数据在将来也可能被再次使用。 + +### LRU 缓存的核心概念 + +1. **缓存容量**:缓存能存储的最大数据量 +2. **访问时间**:数据被访问的时间戳 +3. **淘汰策略**:当缓存满时,移除最久未使用的数据 +4. **访问模式**:数据访问的时间和频率模式 + +### LRU 缓存的工作原理 + +1. **数据访问**:当数据被访问(读或写)时,将其标记为最近使用 +2. **数据插入**:新数据插入时,如果缓存满,先淘汰最久未使用的数据 +3. **数据查找**:查找数据时,如果存在,将其标记为最近使用 +4. **缓存维护**:维护使用顺序,确保时间复杂度高效 + +## 图解说明 + +``` +LRU 缓存工作流程示例: + +初始状态: [] (容量=3) + +1. 插入 A -> [A] +2. 插入 B -> [A, B] +3. 插入 C -> [A, B, C] +4. 访问 A -> [A, B, C] (A 被移到头部) +5. 揓入 D -> [B, C, D] (A 被淘汰) +6. 访问 C -> [B, C, D] (C 被移到头部) +7. 揓入 E -> [C, D, E] (B 被淘汰) + +访问顺序: A, B, C, A, D, C, E +淘汰顺序: A, B +``` + +### LRU 与其他缓存策略对比 + +| 策略 | 淘汰标准 | 适用场景 | +|------|----------|----------| +| LRU | 最近最少使用 | 一般访问模式 | +| LFU | 最不经常使用 | 访问频率稳定 | +| FIFO | 先进先出 | 流水式数据处理 | +| Random | 随机淘汰 | 无法预测访问模式 | + +## Java 代码实现 + +### 方法一:使用 LinkedHashMap(推荐) + +```java +import java.util.LinkedHashMap; +import java.util.Map; + +public class LRUCache extends LinkedHashMap { + private final int capacity; + + public LRUCache(int capacity) { + super(capacity, 0.75f, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } + + // 测试用例 + public static void main(String[] args) { + LRUCache cache = new LRUCache<>(3); + + cache.put(1, "A"); + cache.put(2, "B"); + cache.put(3, "C"); + System.out.println("Cache after insertion: " + cache); + + cache.get(1); + System.out.println("Cache after accessing 1: " + cache); + + cache.put(4, "D"); + System.out.println("Cache after insertion 4: " + cache); + } +} +``` + +### 方法二:手写实现(面试重点) + +```java +import java.util.HashMap; +import java.util.Map; + +class LRUCacheNode { + K key; + V value; + LRUCacheNode prev; + LRUCacheNode next; + + public LRUCacheNode(K key, V value) { + this.key = key; + this.value = value; + this.prev = null; + this.next = null; + } +} + +public class LRUCacheImpl { + private final int capacity; + private final Map> cache; + private final LRUCacheNode head; + private final LRUCacheNode tail; + + public LRUCacheImpl(int capacity) { + this.capacity = capacity; + this.cache = new HashMap<>(); + this.head = new LRUCacheNode<>(null, null); + this.tail = new LRUCacheNode<>(null, null); + head.next = tail; + tail.prev = head; + } + + // 获取数据 + public V get(K key) { + if (!cache.containsKey(key)) { + return null; + } + + LRUCacheNode node = cache.get(key); + moveToHead(node); + return node.value; + } + + // 插入数据 + public void put(K key, V value) { + if (cache.containsKey(key)) { + // 更新已有节点 + LRUCacheNode node = cache.get(key); + node.value = value; + moveToHead(node); + } else { + // 创建新节点 + LRUCacheNode newNode = new LRUCacheNode<>(key, value); + cache.put(key, newNode); + addToHead(newNode); + + // 淘汰策略 + if (cache.size() > capacity) { + LRUCacheNode last = removeTail(); + cache.remove(last.key); + } + } + } + + // 移除指定节点 + public void remove(K key) { + if (!cache.containsKey(key)) { + return; + } + + LRUCacheNode node = cache.get(key); + removeNode(node); + cache.remove(key); + } + + // 清空缓存 + public void clear() { + cache.clear(); + head.next = tail; + tail.prev = head; + } + + // 获取缓存大小 + public int size() { + return cache.size(); + } + + // 检查是否包含键 + public boolean containsKey(K key) { + return cache.containsKey(key); + } + + // 辅助方法:添加到头部 + private void addToHead(LRUCacheNode node) { + node.prev = head; + node.next = head.next; + head.next.prev = node; + head.next = node; + } + + // 辅助方法:移除节点 + private void removeNode(LRUCacheNode node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + // 辅助方法:移动到头部 + private void moveToHead(LRUCacheNode node) { + removeNode(node); + addToHead(node); + } + + // 辅助方法:移除尾部节点 + private LRUCacheNode removeTail() { + LRUCacheNode last = tail.prev; + removeNode(last); + return last; + } + + // 打印缓存内容 + public void printCache() { + LRUCacheNode current = head.next; + while (current != tail) { + System.out.print("(" + current.key + "=" + current.value + ") "); + current = current.next; + } + System.out.println(); + } + + // 测试用例 + public static void main(String[] args) { + LRUCacheImpl cache = new LRUCacheImpl<>(3); + + System.out.println("Inserting 1, 2, 3"); + cache.put(1, "A"); + cache.put(2, "B"); + cache.put(3, "C"); + cache.printCache(); + + System.out.println("Accessing 1"); + cache.get(1); + cache.printCache(); + + System.out.println("Inserting 4"); + cache.put(4, "D"); + cache.printCache(); + + System.out.println("Removing 2"); + cache.remove(2); + cache.printCache(); + + System.out.println("Clearing cache"); + cache.clear(); + cache.printCache(); + } +} +``` + +### 方法三:使用双向队列(Deque) + +```java +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +public class LRUCacheWithDeque { + private final int capacity; + private final Map cache; + private final Deque accessQueue; + + public LRUCacheWithDeque(int capacity) { + this.capacity = capacity; + this.cache = new HashMap<>(); + this.accessQueue = new LinkedList<>(); + } + + public V get(K key) { + if (!cache.containsKey(key)) { + return null; + } + + // 更新访问顺序 + accessQueue.remove(key); + accessQueue.addFirst(key); + return cache.get(key); + } + + public void put(K key, V value) { + if (cache.containsKey(key)) { + // 更新已有数据 + cache.put(key, value); + accessQueue.remove(key); + accessQueue.addFirst(key); + } else { + // 添加新数据 + if (cache.size() >= capacity) { + // 淘汰最久未使用的数据 + K lruKey = accessQueue.removeLast(); + cache.remove(lruKey); + } + cache.put(key, value); + accessQueue.addFirst(key); + } + } +} +``` + +## 时间复杂度分析 + +### 操作时间复杂度 + +| 操作 | 时间复杂度 | 说明 | +|------|------------|------| +| get(K) | O(1) | 哈希查找 + 双向链表操作 | +| put(K,V) | O(1) | 哈希查找 + 双向链表操作 | +| remove(K) | O(1) | 哈希删除 + 双向链表操作 | +| clear() | O(1) | 清空哈希表和链表 | +| size() | O(1) | 哈希表大小 | + +### 空间复杂度 + +- O(n) - 存储 n 个键值对 +- 需要额外空间维护双向链表结构 + +### 性能分析 + +1. **最优实现**:HashMap + 双向链表 = O(1) 所有操作 +2. **次优实现**:LinkedHashMap = O(1) 操作,但依赖 JDK 实现 +3. **最差实现**:数组 + 遍历 = O(n) 操作 + +## 实际应用场景 + +### 1. Web 服务器缓存 +- **静态资源缓存**:CSS、JS、图片文件 +- **页面缓存**:动态生成的 HTML 页面 +- **API 响应缓存**:频繁调用的 API 结果 + +```java +// Web 缓存示例 +public class WebCache { + private final LRUCache cache; + + public WebCache(int maxSize) { + this.cache = new LRUCacheImpl<>(maxSize); + } + + public HttpResponse getPage(String url) { + HttpResponse response = cache.get(url); + if (response == null) { + response = fetchFromOrigin(url); + cache.put(url, response); + } + return response; + } +} +``` + +### 2. 数据库查询缓存 +- **ORM 缓存**:Hibernate、MyBatis 一级/二级缓存 +- **查询结果缓存**:复杂查询结果的缓存 + +```java +// 数据库缓存示例 +public class QueryCache { + private final LRUCache queryCache; + + public QueryCache(int maxSize) { + this.queryCache = new LRUCacheImpl<>(maxSize); + } + + public ResultSet executeQuery(String sql) { + ResultSet result = queryCache.get(sql); + if (result == null) { + result = executeSql(sql); + if (result != null) { + queryCache.put(sql, result); + } + } + return result; + } +} +``` + +### 3. 内存数据库 +- **Redis 缓存策略**:`maxmemory-policy allkeys-lru` +- **本地缓存**:Ehcache、Caffeine + +```java +// 本地缓存示例 +public class LocalCache { + private final LRUCache cache; + + public LocalCache(int maxSize) { + this.cache = new LRUCacheImpl<>(maxSize); + } + + public T get(String key, Class type) { + Object value = cache.get(key); + return type.cast(value); + } + + public void put(String key, Object value) { + cache.put(key, value); + } +} +``` + +### 4. 消息队列缓冲 +- **消息去重**:防止重复处理消息 +- **请求合并**:合并短时间内多个相同请求 + +```java +// 消息队列缓冲示例 +public class MessageBuffer { + private final LRUCache messageBuffer; + private final Queue messageQueue; + + public MessageBuffer(int maxSize) { + this.messageBuffer = new LRUCacheImpl<>(maxSize); + this.messageQueue = new LinkedList<>(); + } + + public void addMessage(Message message) { + String key = message.getId(); + if (!messageBuffer.containsKey(key)) { + messageBuffer.put(key, message); + messageQueue.add(message); + } + } +} +``` + +## 与其他缓存策略的对比 + +| 策略 | 时间复杂度 | 适用场景 | 优点 | 缺点 | +|------|------------|----------|------|------| +| LRU | O(1) | 一般访问模式 | 实现简单,效果好 | 对突发访问敏感 | +| LFU | O(1) | 频率稳定场景 | 更好处理热点数据 | 实现较复杂 | +| FIFO | O(1) | 流水式数据 | 实现简单 | 可能淘汰有用数据 | +| Random | O(1) | 随机访问模式 | 实现最简单 | 性能不稳定 | + +### LRU 的优缺点 + +**优点**: +- 实现简单,易于理解 +- 性能稳定,时间复杂度 O(1) +- 对大多数场景效果良好 +- JDK 已有成熟实现 + +**缺点**: +- 对突发访问敏感(缓存污染) +- 需要额外维护访问顺序 +- 内存占用相对较大 +- 无法区分临时访问和频繁访问 + +## 常见面试问题 + +### Q1: 如何实现 LRU 缓存?为什么选择 HashMap + 双向链表? +**答**: +1. **HashMap** 提供 O(1) 时间复杂度的查找 +2. **双向链表** 维护访问顺序,头节点最近访问,尾节点最久未访问 +3. 结合使用可实现所有操作的 O(1) 时间复杂度 +4. 其他方案(如数组)时间复杂度较高 + +### Q2: LRU 缓存存在什么问题?如何改进? +**答**: +**存在的问题**: +- 缓存污染:一次性大量访问可能导致有用数据被淘汰 +- 无法区分临时访问和频繁访问 + +**改进方案**: +1. **LFU (Least Frequently Used)**:记录访问频率 +2. **2Q (Two Queues)**:分为缓存队列和保留队列 +3. **ARC (Adaptive Replacement Cache)**:结合 LRU 和 LFU +4. **LRU-K**:记录最近 K 次访问历史 + +### Q3: 缓存容量如何确定? +**答**: +考虑因素: +1. **内存限制**:系统可用内存大小 +2. **访问模式**:数据访问频率和大小分布 +3. **性能要求**:需要达到的响应时间 +4. **命中率目标**:期望的缓存命中率 +5. **业务特点**:数据的时效性和重要性 + +### Q4: 如何处理缓存并发问题? +**答**: +解决方案: +1. **使用线程安全容器**:如 `ConcurrentHashMap` +2. **添加同步锁**:方法或代码块同步 +3. **使用读写锁**:提高并发性能 +4. **不可变对象**:避免并发修改问题 + +```java +// 线程安全的 LRU 缓存 +public class ThreadSafeLRUCache { + private final LRUCacheImpl cache; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public V get(K key) { + lock.readLock().lock(); + try { + return cache.get(key); + } finally { + lock.readLock().unlock(); + } + } + + public void put(K key, V value) { + lock.writeLock().lock(); + try { + cache.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +### Q5: 如何处理缓存穿透、击穿、雪崩? +**答**: +**缓存穿透**: +- 查询不存在的数据 +- 解决方案:布隆过滤器、空值缓存 + +**缓存击穿**: +- 大量请求同时查询过期热点数据 +- 解决方案:互斥锁、永不过期 + +**缓存雪崩**: +- 大量缓存同时失效 +- 解决方案:随机过期时间、集群部署 \ No newline at end of file diff --git a/questions/alg-redblacktree.md b/questions/alg-redblacktree.md new file mode 100644 index 0000000..25ec644 --- /dev/null +++ b/questions/alg-redblacktree.md @@ -0,0 +1,623 @@ +# 红黑树 (Red-Black Tree) + +## 数据结构原理 + +### 什么是红黑树? +红黑树是一种自平衡的二叉搜索树,通过在每个节点上增加一个颜色属性(红色或黑色)来保证树的平衡性。它能够在 O(log n) 时间内完成查找、插入和删除操作,是平衡二叉搜索树的一种。 + +### 红黑树的特性 + +1. **节点颜色**:每个节点要么是红色,要么是黑色 +2. **根节点**:根节点总是黑色 +3. **叶子节点**:所有叶子节点(NIL 节点)都是黑色 +4. **红色节点**:红色节点的子节点必须是黑色 +5. **路径长度**:从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点 + +### 红黑树的平衡保证 + +红黑树通过维护这些性质,确保: +- 树的高度最多为 2log(n+1) +- 最坏情况下时间复杂度为 O(log n) +- 插入和删除操作的最坏时间复杂度为 O(log n) + +## 图解说明 + +``` +红黑树结构示例: + + ●(13) // 黑色节点 + / \ + ○(8) ●(17) + / \ / \ + ●(1) ●(11) ○(15) ●(25) + / \ / \ / \ + NIL NIL NIL NIL NIL NIL +``` + +### 节点表示 + +``` +颜色表示: +● - 黑色节点 +○ - 红色节点 +[NIL] - 黑色叶子节点 +``` + +### 平衡性质说明 + +1. **性质1**:每个节点是红色或黑色 +2. **性质2**:根节点是黑色 +3. **性质3**:所有叶子节点都是黑色(NIL 节点) +4. **性质4**:红色节点的子节点都是黑色 +5. **性质5**:从任一节点到其每个叶子节点的所有路径包含相同数量的黑色节点 + +## Java 代码实现 + +### 节点类定义 + +```java +enum Color { + RED, BLACK +} + +class RedBlackNode> { + T key; + Color color; + RedBlackNode left, right, parent; + + public RedBlackNode(T key) { + this.key = key; + this.color = Color.RED; // 新节点默认为红色 + this.left = this.right = this.parent = null; + } +} +``` + +### 红黑树实现 + +```java +public class RedBlackTree> { + private RedBlackNode root; + private RedBlackNode NIL; // 哨兵节点 + + public RedBlackTree() { + NIL = new RedBlackNode<>(null); + NIL.color = Color.BLACK; + root = NIL; + } + + // 左旋操作 + private void leftRotate(RedBlackNode x) { + RedBlackNode y = x.right; + x.right = y.left; + + if (y.left != NIL) { + y.left.parent = x; + } + + y.parent = x.parent; + + if (x.parent == NIL) { + root = y; + } else if (x == x.parent.left) { + x.parent.left = y; + } else { + x.parent.right = y; + } + + y.left = x; + x.parent = y; + } + + // 右旋操作 + private void rightRotate(RedBlackNode y) { + RedBlackNode x = y.left; + y.left = x.right; + + if (x.right != NIL) { + x.right.parent = y; + } + + x.parent = y.parent; + + if (y.parent == NIL) { + root = x; + } else if (y == y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + x.right = y; + y.parent = x; + } + + // 插入操作 + public void insert(T key) { + RedBlackNode newNode = new RedBlackNode<>(key); + newNode.left = NIL; + newNode.right = NIL; + newNode.color = Color.RED; + + // 标准二叉搜索树插入 + RedBlackNode y = NIL; + RedBlackNode x = root; + + while (x != NIL) { + y = x; + if (newNode.key.compareTo(x.key) < 0) { + x = x.left; + } else { + x = x.right; + } + } + + newNode.parent = y; + + if (y == NIL) { + root = newNode; + } else if (newNode.key.compareTo(y.key) < 0) { + y.left = newNode; + } else { + y.right = newNode; + } + + // 修复红黑树性质 + insertFixUp(newNode); + } + + // 插入后的修复 + private void insertFixUp(RedBlackNode z) { + while (z.parent.color == Color.RED) { + if (z.parent == z.parent.parent.left) { + RedBlackNode y = z.parent.parent.right; + + if (y.color == Color.RED) { + // 情况1:叔节点是红色 + z.parent.color = Color.BLACK; + y.color = Color.BLACK; + z.parent.parent.color = Color.RED; + z = z.parent.parent; + } else { + if (z == z.parent.right) { + // 情况2:叔节点是黑色,z 是右孩子 + z = z.parent; + leftRotate(z); + } + // 情况3:叔节点是黑色,z 是左孩子 + z.parent.color = Color.BLACK; + z.parent.parent.color = Color.RED; + rightRotate(z.parent.parent); + } + } else { + // 对称情况 + RedBlackNode y = z.parent.parent.left; + + if (y.color == Color.RED) { + z.parent.color = Color.BLACK; + y.color = Color.BLACK; + z.parent.parent.color = Color.RED; + z = z.parent.parent; + } else { + if (z == z.parent.left) { + z = z.parent; + rightRotate(z); + } + z.parent.color = Color.BLACK; + z.parent.parent.color = Color.RED; + leftRotate(z.parent.parent); + } + } + } + root.color = Color.BLACK; + } + + // 查找操作 + public RedBlackNode search(T key) { + RedBlackNode current = root; + while (current != NIL && key.compareTo(current.key) != 0) { + if (key.compareTo(current.key) < 0) { + current = current.left; + } else { + current = current.right; + } + } + return current; + } + + // 中序遍历 + public void inOrderTraversal() { + inOrderTraversal(root); + } + + private void inOrderTraversal(RedBlackNode node) { + if (node != NIL) { + inOrderTraversal(node.left); + System.out.print(node.key + (node.color == Color.RED ? "R" : "B") + " "); + inOrderTraversal(node.right); + } + } + + // 删除操作(简化版) + public void delete(T key) { + RedBlackNode z = search(key); + if (z == NIL) { + return; + } + + // 实际的删除实现需要复杂的修复逻辑 + deleteFixUp(z); + } + + // 删除后的修复(简化版) + private void deleteFixUp(RedBlackNode x) { + // 实际实现需要处理多种情况 + // 这里简化处理,实际面试中需要详细实现 + } +} +``` + +### 完整的删除操作实现 + +```java +// 删除节点 +public void delete(T key) { + RedBlackNode z = search(key); + if (z == NIL) { + return; + } + + RedBlackNode y = z; + RedBlackNode x; + Color yOriginalColor = y.color; + + if (z.left == NIL) { + x = z.right; + transplant(z, z.right); + } else if (z.right == NIL) { + x = z.left; + transplant(z, z.left); + } else { + y = minimum(z.right); + yOriginalColor = y.color; + x = y.right; + + if (y.parent == z) { + x.parent = y; + } else { + transplant(y, y.right); + y.right = z.right; + y.right.parent = y; + } + + transplant(z, y); + y.left = z.left; + y.left.parent = y; + y.color = z.color; + } + + if (yOriginalColor == Color.BLACK) { + deleteFixUp(x); + } +} + +// 替换节点 +private void transplant(RedBlackNode u, RedBlackNode v) { + if (u.parent == NIL) { + root = v; + } else if (u == u.parent.left) { + u.parent.left = v; + } else { + u.parent.right = v; + } + v.parent = u.parent; +} + +// 查找最小节点 +private RedBlackNode minimum(RedBlackNode node) { + while (node.left != NIL) { + node = node.left; + } + return node; +} + +// 删除修复 +private void deleteFixUp(RedBlackNode x) { + while (x != root && x.color == Color.BLACK) { + if (x == x.parent.left) { + RedBlackNode w = x.parent.right; + + if (w.color == Color.RED) { + // 情况1:兄弟节点是红色 + w.color = Color.BLACK; + x.parent.color = Color.RED; + leftRotate(x.parent); + w = x.parent.right; + } + + if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) { + // 情况2:兄弟节点是黑色,且两个子节点都是黑色 + w.color = Color.RED; + x = x.parent; + } else { + if (w.right.color == Color.BLACK) { + // 情况3:兄弟节点是黑色,左子是红色,右子是黑色 + w.left.color = Color.BLACK; + w.color = Color.RED; + rightRotate(w); + w = x.parent.right; + } + // 情况4:兄弟节点是黑色,右子是红色 + w.color = x.parent.color; + x.parent.color = Color.BLACK; + w.right.color = Color.BLACK; + leftRotate(x.parent); + x = root; + } + } else { + // 对称情况 + RedBlackNode w = x.parent.left; + + if (w.color == Color.RED) { + w.color = Color.BLACK; + x.parent.color = Color.RED; + rightRotate(x.parent); + w = x.parent.left; + } + + if (w.right.color == Color.BLACK && w.left.color == Color.BLACK) { + w.color = Color.RED; + x = x.parent; + } else { + if (w.left.color == Color.BLACK) { + w.right.color = Color.BLACK; + w.color = Color.RED; + leftRotate(w); + w = x.parent.left; + } + w.color = x.parent.color; + x.parent.color = Color.BLACK; + w.left.color = Color.BLACK; + rightRotate(x.parent); + x = root; + } + } + } + x.color = Color.BLACK; +} +``` + +## 时间复杂度分析 + +### 操作时间复杂度 + +| 操作 | 时间复杂度 | 说明 | +|------|------------|------| +| 查找 | O(log n) | 平衡二叉搜索树查找 | +| 插入 | O(log n) | 查找插入位置 + 修复平衡 | +| 删除 | O(log n) | 查找删除位置 + 修复平衡 | +| 中序遍历 | O(n) | 遍历所有节点 | +| 最值查找 | O(log n) | 到达叶子节点 | + +### 空间复杂度 + +- O(n) - 存储 n 个节点 +- 递归栈空间:O(log n) + +### 性能对比 + +| 特性 | 红黑树 | AVL 树 | 二叉搜索树 | +|------|--------|--------|------------| +| 平衡性 | 相对平衡 | 严格平衡 | 可能不平衡 | +| 查找效率 | O(log n) | O(log n) | O(log n) ~ O(n) | +| 插入效率 | O(log n) | O(log n) | O(log n) ~ O(n) | +| 删除效率 | O(log n) | O(log n) | O(log n) ~ O(n) | +| 旋转次数 | 较少 | 较多 | 不需要 | +| 适用场景 | 插入频繁 | 查询密集 | 数据有序 | + +## 实际应用场景 + +### 1. Java 集合框架 +- **`java.util.TreeMap`**:使用红黑树实现 +- **`java.util.TreeSet`**:基于 TreeMap 实现 + +```java +// TreeMap 使用示例 +Map treeMap = new TreeMap<>(); +treeMap.put("apple", 1); +treeMap.put("banana", 2); +treeMap.put("orange", 3); + +// 自动排序 +for (Map.Entry entry : treeMap.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); +} +``` + +### 2. Linux 内核 +- ** Completely Fair Scheduler (CFS)**:使用红黑树管理进程调度 +- **内存管理**:管理虚拟内存区域 +- **文件系统**:ext3、ext4 使用红黑树 + +```c +// Linux 内核中的红黑树使用示例 +struct rb_root root = RB_ROOT; +struct my_data *data = rb_entry(node, struct my_data, rb_node); +``` + +### 3. 数据库索引 +- **B 树索引**:底层使用类似红黑树的结构 +- **范围查询**:中序遍历支持范围查询 +- **排序操作**:自动维护有序性 + +```java +// 数据库索引示例 +public class DatabaseIndex { + private RedBlackTree> index; + + public void insert(String key, Record record) { + index.put(key, Collections.singletonList(record)); + } + + public List rangeQuery(String start, String end) { + // 利用红黑树的中序遍历特性 + return index.rangeSearch(start, end); + } +} +``` + +### 4. 网络路由 +- **路由表管理**:IP 地址范围查找 +- **防火墙规则**:规则匹配和优先级排序 + +```java +// 路由表示例 +public class RoutingTable { + private RedBlackTree routes; + + public Route findRoute(String ip) { + // 查找匹配的路由规则 + return routes.get(ip); + } +} +``` + +## 与其他平衡树的对比 + +| 特性 | 红黑树 | AVL 树 | B 树 | B+ 树 | +|------|--------|--------|------|-------| +| 平衡因子 | 相对宽松 | 严格平衡 | 多路 | 多路 | +| 查找效率 | O(log n) | O(log n) | O(log n) | O(log n) | +| 插入效率 | O(log n) | O(log n) | O(log n) | O(log n) | +| 旋转次数 | 较少 | 频繁 | 分裂/合并 | 分裂/合并 | +| 内存使用 | 较少 | 较少 | 较大 | 较大 | +| 适用场景 | 插入频繁 | 查询密集 | 磁盘存储 | 磁盘存储 | +| 实现复杂度 | 中等 | 中等 | 复杂 | 复杂 | + +### 选择红黑树的理由 + +1. **性能平衡**:插入、删除、查找都是 O(log n) +2. **实现相对简单**:比 AVL 树简单,比 B 树简单 +3. **插入性能好**:插入时旋转次数少于 AVL 树 +4. **内存效率高**:不需要像 B 树那样维护多路指针 +5. **广泛应用**:Java 标准库采用,证明其可靠性 + +### 不同场景的最佳选择 + +- **内存数据结构**:红黑树、AVL 树 +- **磁盘数据存储**:B 树、B+ 树 +- **频繁插入**:红黑树 +- **频繁查询**:AVL 树 +- **范围查询**:B+ 树(磁盘)、红黑树(内存) + +## 常见面试问题 + +### Q1: 红黑树和 AVL 树有什么区别?什么时候选择红黑树? +**答**: +**主要区别**: +1. **平衡程度**:AVL 树严格平衡,红黑树相对宽松 +2. **旋转频率**:AVL 树旋转更频繁,红黑树较少 +3. **查找性能**:AVL 树查找更快(树更矮) +4. **插入性能**:红黑树插入更快(旋转少) + +**选择红黑树的情况**: +- 插入操作比查询操作频繁 +- 需要保证最坏情况下的性能 +- 不需要特别严格的平衡 +- 需要实现简单、高效的平衡树 + +### Q2: 红黑树的插入操作如何保证平衡? +**答**: +插入操作后通过以下方式修复平衡: +1. **确定问题节点**:从插入节点开始向上遍历 +2. **处理三种情况**: + - 情况1:叔节点是红色,通过重新着色解决 + - 情况2:叔节点是黑色,且节点是右孩子,通过左旋调整 + - 情况3:叔节点是黑色,且节点是左孩子,通过右旋和着色解决 + +### Q3: 为什么 Java 的 TreeMap 使用红黑树而不是 AVL 树? +**答**: +1. **性能考虑**:红黑树插入和删除性能更好 +2. **平衡性要求**:TreeMap 需要保证整体性能,而不是最优查找 +3. **实现复杂度**:红黑树实现相对简单 +4. **历史原因**:早期 Java 版本选择,后续保持兼容性 + +### Q4: 红黑树的时间复杂度是如何保证的? +**答**: +通过以下性质保证: +1. **路径长度限制**:任何路径的黑色节点数量相同 +2. **最长路径限制**:最长路径不超过最短路径的 2 倍 +3. **树高控制**:树高 h ≤ 2log₂(n+1) +4. **操作效率**:每次操作最多 O(log n) 次旋转 + +### Q5: 如何验证红黑树的正确性? +**答**: +验证五条性质: +1. 每个节点是红色或黑色 +2. 根节点是黑色 +3. 所有叶子节点是黑色 +4. 红色节点的子节点是黑色 +5. 所有路径的黑色节点数量相同 + +```java +// 验证红黑树正确性的方法 +public boolean isRedBlackTree(RedBlackNode node) { + if (node == NIL) return true; + + // 验证颜色 + if (node.color != Color.RED && node.color != Color.BLACK) { + return false; + } + + // 验证红色节点的子节点 + if (node.color == Color.RED) { + if (node.left.color == Color.RED || node.right.color == Color.RED) { + return false; + } + } + + // 验证路径长度 + int leftBlackHeight = getBlackHeight(node.left); + int rightBlackHeight = getBlackHeight(node.right); + if (leftBlackHeight != rightBlackHeight) { + return false; + } + + // 递归验证子树 + return isRedBlackTree(node.left) && isRedBlackTree(node.right); +} + +// 计算黑色高度 +private int getBlackHeight(RedBlackNode node) { + int height = 0; + while (node != NIL) { + if (node.color == Color.BLACK) { + height++; + } + node = node.left; + } + return height; +} +``` + +### Q6: 红黑树如何处理大规模数据? +**答**: +对于大规模数据,红黑树的处理策略: +1. **内存考虑**:大规模数据可能导致内存问题 +2. **性能优化**: + - 批量插入时可以暂时忽略平衡 + - 使用延迟修复策略 +3. **替代方案**: + - 对于磁盘数据,使用 B 树或 B+ 树 + - 对于分布式数据,使用分布式数据结构 + +### Q7: 红黑树的删除操作有哪些情况? +**答**: +删除操作主要处理以下情况: +1. **删除红色节点**:直接删除,不影响平衡 +2. **删除黑色节点**:需要修复,分为多种情况 + - 替代节点是红色:着色为黑色 + - 替代节点是黑色: + - 兄弟节点是红色 + - 兄弟节点是黑色,且子节点都是黑色 + - 兄弟节点是黑色,且有一个红色子节点 \ No newline at end of file diff --git a/questions/alg-skip-list.md b/questions/alg-skip-list.md new file mode 100644 index 0000000..8c8eb81 --- /dev/null +++ b/questions/alg-skip-list.md @@ -0,0 +1,650 @@ +# 跳表 (Skip List) + +## 数据结构原理 + +### 什么是跳表? +跳表是一种概率性的数据结构,通过在多个层级上维护有序的链表来提供高效的查找、插入和删除操作。它是一种在平衡二叉搜索树和链表之间的折中方案,实现简单且性能优异。 + +### 跳表的核心概念 + +1. **层级**:跳表由多个层级组成,最高层是稀疏的,最低层是稠密的 +2. **节点**:每个节点在不同层级中有多个指针 +3. **索引**:高层级作为低层级的索引,快速定位 +4. **概率性平衡**:通过随机算法保证树的平衡性 + +### 跳表的工作原理 + +1. **查找**:从最高层开始,向右查找,无法向右时向下继续 +2. **插入**:随机决定插入的层级,在相应层级插入节点 +3. **删除**:在所有层级中删除对应节点 +4. **平衡**:通过随机概率保持树的平衡性 + +## 图解说明 + +``` +跳表结构示例(最大层级 4): + +Level 3: ---1---10---40---70--- +Level 2: ---1-----10-----70--- +Level 1: ---1------10-----70--- +Level 0: 1->2->3->4->5->6->7->8->9->10->11->...->70 + +查找过程(查找 7): +- Level 3: 从 1 开始,无法向右,向下到 Level 2 +- Level 2: 从 1 开始,无法向右,向下到 Level 1 +- Level 1: 从 1 开始,无法向右,向下到 Level 0 +- Level 0: 从 1 开始,遍历到 7 +``` + +### 跂表节点结构 + +``` +SkipListNode { + value: 7 + next[0] -> 8 + next[1] -> 10 + next[2] -> 10 + next[3] -> 40 +} +``` + +### 层级选择算法 + +```java +// 随机生成层级 +int level = 0; +while (random() < 0.5 && level < MAX_LEVEL) { + level++; +} +``` + +## Java 代码实现 + +### 节点类定义 + +```java +class SkipListNode> { + T value; + SkipListNode[] next; + + @SuppressWarnings("unchecked") + public SkipListNode(T value, int level) { + this.value = value; + this.next = new SkipListNode[level + 1]; + } +} +``` + +### 跳表实现 + +```java +import java.util.Random; + +public class SkipList> { + private static final int MAX_LEVEL = 16; + private static final double PROBABILITY = 0.5; + + private SkipListNode header; + private int level; + private int size; + private Random random; + + @SuppressWarnings("unchecked") + public SkipList() { + this.header = new SkipListNode<>(null, MAX_LEVEL); + this.level = 0; + this.size = 0; + this.random = new Random(); + } + + // 随机生成层级 + private int randomLevel() { + int lvl = 0; + while (lvl < MAX_LEVEL && random.nextDouble() < PROBABILITY) { + lvl++; + } + return lvl; + } + + // 插入操作 + public void insert(T value) { + SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1]; + SkipListNode current = header; + + // 从最高层开始查找插入位置 + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) < 0) { + current = current.next[i]; + } + update[i] = current; + } + + // 确定新节点的层级 + int newLevel = randomLevel(); + + // 如果新层级大于当前层级,更新高层级的指针 + if (newLevel > level) { + for (int i = level + 1; i <= newLevel; i++) { + update[i] = header; + } + level = newLevel; + } + + // 创建新节点并插入 + SkipListNode newNode = new SkipListNode<>(value, newLevel); + for (int i = 0; i <= newLevel; i++) { + newNode.next[i] = update[i].next[i]; + update[i].next[i] = newNode; + } + + size++; + } + + // 查找操作 + public boolean contains(T value) { + SkipListNode current = header; + + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) < 0) { + current = current.next[i]; + } + } + + current = current.next[0]; + return current != null && current.value.compareTo(value) == 0; + } + + // 删除操作 + public void delete(T value) { + SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1]; + SkipListNode current = header; + + // 查找要删除的节点 + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) < 0) { + current = current.next[i]; + } + update[i] = current; + } + + current = current.next[0]; + if (current != null && current.value.compareTo(value) == 0) { + // 从所有层级中删除 + for (int i = 0; i <= level; i++) { + if (update[i].next[i] != current) { + break; + } + update[i].next[i] = current.next[i]; + } + + // 更新当前层级 + while (level > 0 && header.next[level] == null) { + level--; + } + + size--; + } + } + + // 获取最小值 + public T getMin() { + SkipListNode current = header.next[0]; + return current != null ? current.value : null; + } + + // 获取最大值 + public T getMax() { + SkipListNode current = header; + for (int i = level; i >= 0; i--) { + while (current.next[i] != null) { + current = current.next[i]; + } + } + return current.value; + } + + // 跳表大小 + public int size() { + return size; + } + + // 是否为空 + public boolean isEmpty() { + return size == 0; + } + + // 打印跳表结构 + public void printList() { + for (int i = level; i >= 0; i--) { + SkipListNode node = header.next[i]; + System.out.print("Level " + i + ": "); + while (node != null) { + System.out.print(node.value + " "); + node = node.next[i]; + } + System.out.println(); + } + } + + // 中序遍历 + public void traverse() { + SkipListNode current = header.next[0]; + while (current != null) { + System.out.print(current.value + " "); + current = current.next[0]; + } + System.out.println(); + } +} +``` + +### 完整的实现(包括范围查询) + +```java +// 跳表完整实现 +public class EnhancedSkipList> { + private static final int MAX_LEVEL = 16; + private static final double PROBABILITY = 0.5; + + private static class Node { + T value; + Node[] next; + + @SuppressWarnings("unchecked") + public Node(T value, int level) { + this.value = value; + this.next = new Node[level + 1]; + } + } + + private Node header; + private int level; + private int size; + private Random random; + + public EnhancedSkipList() { + this.header = new Node<>(null, MAX_LEVEL); + this.level = 0; + this.size = 0; + this.random = new Random(); + } + + // 范围查询 + public List rangeQuery(T start, T end) { + List result = new ArrayList<>(); + if (start.compareTo(end) > 0) { + return result; + } + + Node current = header; + + // 找到 start 的位置 + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(start) < 0) { + current = current.next[i]; + } + } + + current = current.next[0]; + while (current != null && current.value.compareTo(end) <= 0) { + result.add(current.value); + current = current.next[0]; + } + + return result; + } + + // 获取前驱节点 + public T predecessor(T value) { + Node current = header; + T predecessor = null; + + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) < 0) { + current = current.next[i]; + if (i == 0) { + predecessor = current.value; + } + } + } + + return predecessor; + } + + // 获取后继节点 + public T successor(T value) { + if (!contains(value)) { + return null; + } + + Node current = header; + + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) <= 0) { + current = current.next[i]; + } + } + + current = current.next[0]; + return current != null ? current.value : null; + } + + // 统计小于某值的元素个数 + public int countLessThan(T value) { + Node current = header; + int count = 0; + + for (int i = level; i >= 0; i--) { + while (current.next[i] != null && + current.next[i].value.compareTo(value) < 0) { + count += Math.pow(2, i); // 近似计算 + current = current.next[i]; + } + } + + return count; + } + + // 获取统计信息 + public SkipListStats getStats() { + SkipListStats stats = new SkipListStats(); + stats.size = size; + stats.height = level + 1; + + // 计算每个层级的节点数 + int[] levelCounts = new int[MAX_LEVEL + 1]; + Node current = header; + + for (int i = 0; i <= level; i++) { + levelCounts[i] = 0; + } + + current = header.next[0]; + while (current != null) { + for (int i = 0; i <= level && i <= current.next.length - 1; i++) { + levelCounts[i]++; + } + current = current.next[0]; + } + + stats.levelCounts = levelCounts; + return stats; + } + + public static class SkipListStats { + public int size; + public int height; + public int[] levelCounts; + } +} +``` + +## 时间复杂度分析 + +### 操作时间复杂度 + +| 操作 | 时间复杂度 | 说明 | +|------|------------|------| +| 查找 | O(log n) | 最多遍历 log n 层 | +| 插入 | O(log n) | 查找位置 + 随机决定层级 | +| 删除 | O(log n) | 查找节点 + 从所有层级删除 | +| 范围查询 | O(log n + k) | k 是结果集大小 | +| 最值查找 | O(1) | 直接访问首尾节点 | + +### 空间复杂度 + +- O(n log n) - 每个节点平均存在 log n 层 +- 需要额外空间维护多层级指针 + +### 概率分析 + +1. **期望层级**:对于 n 个元素,期望层级为 log n +2. **期望指针数量**:每个节点期望有 2 个指针 +3. **查找效率**:O(log n) 概率保证 + +## 实际应用场景 + +### 1. Redis 有序集合 +- **zset 实现**:Redis 的有序集合使用跳表实现 +- **范围查询**:支持高效的区间查询 +- **分数排序**:基于分数进行排序 + +```java +// Redis 有序集合模拟 +public class RedisSortedSet { + private EnhancedSkipList skipList; + + public void add(double score, String member) { + skipList.insert(score, member); + } + + public List rangeByScore(double min, double max) { + return skipList.rangeQuery(min, max); + } + + public boolean contains(String member) { + return skipList.contains(member); + } +} +``` + +### 2. 数据库索引 +- **内存索引**:用于内存数据库的索引 +- **范围查询**:支持高效的范围查找 +- **插入性能**:比 B 树实现简单 + +```java +// 数据库索引实现 +public class DatabaseIndex { + private EnhancedSkipList index; + + public void insert(Object key, Row row) { + index.insert(key, row); + } + + public List rangeQuery(Object start, Object end) { + return index.rangeQuery(start, end); + } + + public Row find(Object key) { + return index.find(key); + } +} +``` + +### 3. 网络路由 +- **路由表**:IP 地址范围查找 +- **ACL 控制**:访问控制列表匹配 + +```java +// 路由表实现 +public class RoutingTable { + private EnhancedSkipList routes; + + public Route findRoute(String ip) { + return routes.find(ip); + } + + public List findRoutesInSubnet(String subnet) { + return routes.rangeQuery(subnet, subnet + "255"); + } +} +``` + +### 4. 缓存系统 +- **多级缓存**:实现分层缓存 +- **缓存查找**:快速定位缓存项 + +```java +// 多级缓存实现 +public class MultiLevelCache { + private EnhancedSkipList l1Cache; + private EnhancedSkipList l2Cache; + + public Object get(String key) { + Object value = l1Cache.find(key); + if (value != null) { + return value; + } + value = l2Cache.find(key); + if (value != null) { + l1Cache.insert(key, value); + } + return value; + } +} +``` + +## 与其他数据结构的对比 + +| 数据结构 | 查找时间 | 插入时间 | 删除时间 | 适用场景 | +|----------|----------|----------|----------|----------| +| 跳表 | O(log n) | O(log n) | O(log n) | 内存数据、范围查询 | +| 平衡二叉树 | O(log n) | O(log n) | O(log n) | 内存数据、查找密集 | +| 哈希表 | O(1) | O(1) | O(1) | 精确查找、内存数据 | +| B 树 | O(log n) | O(log n) | O(log n) | 磁盘存储、索引 | +| 数组 | O(n) | O(n) | O(n) | 小规模、有序数据 | + +### 跳表的优势 + +1. **实现简单**:相比平衡二叉树,实现更简单 +2. **并发友好**:部分操作可以并发执行 +3. **内存效率**:空间使用比平衡树更合理 +4. **概率平衡**:不需要复杂的旋转操作 +5. **支持范围查询**:链表结构天然支持范围操作 + +### 跳表的劣势 + +1. **内存使用**:比普通链表使用更多内存 +2. **最坏情况**:概率性数据结构,最坏情况较差 +3. **常数因子**:比平衡树的常数因子大 + +## 常见面试问题 + +### Q1: 跳表和平衡二叉树有什么区别? +**答**: +**主要区别**: +1. **实现复杂度**:跳表实现简单,平衡树需要复杂的旋转操作 +2. **内存使用**:跳表使用更多内存(多层级指针),平衡树内存使用更紧凑 +3. **并发性能**:跳表某些操作更容易并发执行 +4. **平衡机制**:跳表是概率性平衡,平衡树是确定性平衡 +5. **查找性能**:平衡树常数因子更好,跳表略差 + +### Q2: 跳表的最大层级如何确定?为什么? +**答**: +最大层级的确定: +1. **经验值**:通常设置为 16-32,足够处理大多数情况 +2. **概率保证**:对于 n 个元素,期望层级为 log n +3. **空间考虑**:最大层级决定最坏情况的内存使用 +4. **性能平衡**:太高浪费内存,太低影响性能 + +### Q3: 跳表的时间复杂度如何证明? +**答**: +时间复杂度分析: +1. **查找分析**:每层需要遍历的节点数逐渐减少 +2. **几何级数**:每层节点数呈几何级数减少 +3. **期望层数**:每个节点的期望层数为 2 +4. **总查找步数**:期望查找步数为 O(log n) + +### Q4: Redis 为什么选择跳表实现有序集合? +**答**: +选择跳表的原因: +1. **实现简单**:相比平衡树更容易实现 +2. **内存效率**:相比平衡树内存使用更合理 +3. **并发性能**:跳表某些操作可以并发执行 +4. **范围查询**:天然支持范围查询操作 +5. **性能足够**:对于大多数场景性能足够好 + +### Q5: 跳表如何保证查找效率? +**答**: +保证效率的关键: +1. **层级设计**:高层级作为低层级的索引 +2. **概率平衡**:通过随机算法保证树的平衡性 +3. **快速定位**:从高层级快速定位到大致位置 +4. **层数控制**:每个节点只存在于适当数量的层级中 + +### Q6: 如何优化跳表的内存使用? +**答**: +内存优化策略: +1. **动态层级**:根据实际数据动态调整最大层级 +2. **压缩层级**:合并过空的层级 +3. **节点复用**:复用不再使用的节点 +4. **缓存友好**:优化内存布局,提高缓存命中率 +5. **惰性删除**:延迟删除,减少内存碎片 + +### Q7: 跳表在并发环境下如何处理? +**答**: +并发处理方案: +1. **细粒度锁**:对不同层使用不同的锁 +2. **无锁设计**:使用 CAS 操作实现无锁跳表 +3. **版本控制**:使用版本号实现乐观并发控制 +4. **分段锁**:将跳表分段,每段独立加锁 +5. **读无锁**:读操作不加锁,写操作加锁 + +```java +// 并发跳表简化实现 +public class ConcurrentSkipList> { + private final ReentrantLock[] locks; + private final int lockCount; + + public ConcurrentSkipList() { + this.lockCount = 16; + this.locks = new ReentrantLock[lockCount]; + for (int i = 0; i < lockCount; i++) { + locks[i] = new ReentrantLock(); + } + } + + private int getLockIndex(T value) { + return Math.abs(value.hashCode() % lockCount); + } + + public void insert(T value) { + int lockIndex = getLockIndex(value); + locks[lockIndex].lock(); + try { + // 插入逻辑 + } finally { + locks[lockIndex].unlock(); + } + } +} +``` + +### Q8: 跳表的性能如何随数据量变化? +**答**: +性能变化规律: +1. **时间复杂度**:保持 O(log n) 不变 +2. **空间复杂度**:随数据量线性增长 +3. **常数因子**:数据量越大,常数因子影响越小 +4. **缓存影响**:数据量较大时,缓存命中率下降 +5. **内存压力**:大数据量时内存使用成为瓶颈 + +### Q9: 如何处理跳表中的重复数据? +**答**: +重复数据处理: +1. **允许重复**:修改插入逻辑,允许相同值存在 +2. **去重处理**:在插入时检查是否已存在 +3. **多值节点**:在节点中存储值的集合 +4. **计数器**:在节点中增加计数器 +5. **策略选择**:根据业务需求选择合适的处理方式 + +### Q10: 跳表和 B 树有什么相似之处? +**答**: +相似之处: +1. **分层结构**:都是多层结构,高层级作为索引 +2. **查找效率**:都是 O(log n) 时间复杂度 +3. **范围查询**:都支持高效的范围查询 +4. **平衡性**:都维护数据的平衡性 +5. **空间局部性**:都考虑数据的局部性原理 + +**主要区别**: +- 跳表是基于链表的概率结构 +- B 树是基于数组块的确定性结构 +- 跳表适用于内存,B 树适用于磁盘 \ No newline at end of file diff --git a/questions/consistency-hash.md b/questions/consistency-hash.md new file mode 100644 index 0000000..e579a93 --- /dev/null +++ b/questions/consistency-hash.md @@ -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 { + + private final TreeMap 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 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 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 consistentHash; + + public RedisClusterRouter(List 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 dbRouter; + + public ShardingRouter(List 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 等负载均衡器的一致性哈希配置 +- 能根据业务特点选择合适的哈希算法 diff --git a/questions/database-sharding.md b/questions/database-sharding.md new file mode 100644 index 0000000..289e60c --- /dev/null +++ b/questions/database-sharding.md @@ -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 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 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 shardingAlgorithm = new CustomModShardingAlgorithm(); + shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig( + new StandardShardingStrategyConfiguration("user_id", shardingAlgorithm)); + + return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig); + } + + public static class CustomModShardingAlgorithm implements StandardShardingAlgorithm { + @Override + public String doSharding(Collection availableTargetNames, PreciseShardingValue 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 { + private final int shardingCount; + + public HashShardingAlgorithm(int shardingCount) { + this.shardingCount = shardingCount; + } + + @Override + public String doSharding(Collection availableTargetNames, PreciseShardingValue 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 circle = new TreeMap<>(); + private final int virtualNodeCount; + + public ConsistentHashSharding(List 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 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 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 getPageUsers(int page, int size) { + // 查询所有分库 + List allUsers = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + List users = userMapper.selectByShard(i, page, size); + allUsers.addAll(users); + } + + // 内存分页 + int fromIndex = (page - 1) * size; + int toIndex = Math.min(fromIndex + size, allUsers.size()); + + List pageUsers = allUsers.subList(fromIndex, toIndex); + return new PageResult<>(pageUsers, allUsers.size()); + } +} +``` + +2. **游标分页** +```java +@Service +public class CursorPagingService { + + public List getOrdersByCursor(Long lastId, int limit) { + List orders = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + List 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 + + + + + + + + id + io.mycat.route.function.PartitionByMod + 3 + + + + + + + + + select user() + + +``` + +### 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 createDataSourceMap() { + Map 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 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 { + 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 availableTargetNames, PreciseShardingValue 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> 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> 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> 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 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 { + + @Override + public String doSharding(Collection availableTargetNames, PreciseShardingValue 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. **性能优化**:持续优化查询性能和系统稳定性 + +在面试中,除了技术细节,还要体现对业务的理解、系统的架构能力和性能优化的经验。 \ No newline at end of file diff --git a/questions/design-feed.md b/questions/design-feed.md new file mode 100644 index 0000000..321fe3f --- /dev/null +++ b/questions/design-feed.md @@ -0,0 +1,444 @@ +# 社交信息流系统设计 + +## 需求分析和数据量评估 + +### 需求分析 +- **核心功能**:信息流展示、个性化推荐、社交互动、内容管理 +- **业务场景**:微博、朋友圈、抖音等社交应用 +- **QPS评估**:日请求100亿+,峰值QPS 20万+ +- **数据规模**:用户5亿+,内容100亿+,关系数据1000亿+ + +### 数据量评估 +- **用户表**:5亿条,日均查询1亿次 +- **内容表**:100亿条,日增1亿+ +- **关系表**:1000亿+条,日均更新10亿次 +- **互动表**:500亿+条,日增5亿+ +- **推荐系统**:日处理1000亿次推荐请求 + +## 核心技术难点 + +### 1. 海量内容处理 +- 亿级内容的存储和检索 +- 内容的实时分发和推送 +- 内容的审核和过滤 + +### 2. 个性化推荐 +- 用户兴趣建模 +- 实时推荐算法 +- 推荐效果评估 + +### 3. 高并发读取 +- 信息流的实时性要求 +- 用户关系计算 +- 数据缓存优化 + +### 4. 社交图谱构建 +- 用户关系网络 +- 关系强度计算 +- 图算法优化 + +## 系统架构设计 + +### 总体架构 +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 移动端APP │ │ Web网页 │ │ API接口 │ +│ (iOS/Android)│ │ (PC/移动) │ │ (第三方) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 负载均衡 │ │ API网关 │ │ CDN加速 │ + │ (Nginx) │ │ (Gateway) │ │ (Edge) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Feed服务 │ │ 互动服务 │ │ 推荐服务 │ + │ (微服务) │ │ (微服务) │ │ (微服务) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 缓存集群 │ │ 数据库集群 │ │ 消息队列 │ + │ (Redis) │ │ (MySQL分库分表)│ │ (Kafka) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 搜索引擎 │ │ 图数据库 │ │ 数据仓库 │ + │ (Elasticsearch)│ │ (Neo4j) │ │ (ClickHouse) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 关键组件 + +#### 1. 流量层 +- **负载均衡**:Nginx L7负载均衡 +- **API网关**:请求路由、限流、认证 +- **CDN加速**:静态资源缓存 + +#### 2. 服务层 +- **Feed服务**:信息流生成和分发 +- **互动服务**:点赞、评论、分享处理 +- **推荐服务**:个性化推荐算法 +- **社交服务**:关系管理 + +#### 3. 存储层 +- **Redis集群**:缓存、计数器 +- **MySQL集群**:用户数据、内容数据 +- **MongoDB集群**:Feed流数据 +- **图数据库**:社交关系数据 + +#### 4. 基础设施 +- **搜索引擎**:内容全文检索 +- **消息队列**:异步处理、削峰填谷 +- **数据仓库**:离线数据分析 +- **推荐引擎**:机器学习平台 + +## 数据库设计 + +### 用户表 +```sql +CREATE TABLE `feed_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `nickname` varchar(50) NOT NULL COMMENT '昵称', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像', + `gender` tinyint DEFAULT 0 COMMENT '性别', + `birthday` date DEFAULT NULL COMMENT '生日', + `location` varchar(100) DEFAULT NULL COMMENT '位置', + `bio` varchar(255) DEFAULT NULL COMMENT '个人简介', + `interests` json DEFAULT NULL COMMENT '兴趣标签', + `follow_count` int NOT NULL DEFAULT 0 COMMENT '关注数', + `follower_count` int NOT NULL DEFAULT 0 COMMENT '粉丝数', + `post_count` int NOT NULL DEFAULT 0 COMMENT '发布数', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 内容表 +```sql +CREATE TABLE `feed_content` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content_id` varchar(64) NOT NULL COMMENT '内容ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `content_type` tinyint NOT NULL COMMENT '内容类型', + `title` varchar(255) DEFAULT NULL COMMENT '标题', + `content` text NOT NULL COMMENT '内容', + `media_urls` json DEFAULT NULL COMMENT '媒体URLs', + `tags` json DEFAULT NULL COMMENT '标签', + `like_count` int NOT NULL DEFAULT 0 COMMENT '点赞数', + `comment_count` int NOT NULL DEFAULT 0 COMMENT '评论数', + `share_count` int NOT NULL DEFAULT 0 COMMENT '分享数', + `view_count` int NOT NULL DEFAULT 0 COMMENT '浏览数', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_content_id` (`content_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_created_at` (`created_at`), + FULLTEXT KEY `idx_content` (`title`, `content`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 用户关系表 +```sql +CREATE TABLE `feed_relationship` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `follow_user_id` bigint NOT NULL COMMENT '关注用户ID', + `relation_type` tinyint NOT NULL DEFAULT 1 COMMENT '关系类型', + `strength` decimal(5,2) DEFAULT '0.00' COMMENT '关系强度', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_follow` (`user_id`, `follow_user_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_follow_user_id` (`follow_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### Feed流表 +```sql +CREATE TABLE `feed_stream` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `content_id` varchar(64) NOT NULL COMMENT '内容ID', + `feed_type` tinyint NOT NULL COMMENT 'Feed类型', + `priority` decimal(10,4) NOT NULL DEFAULT '0.0000' COMMENT '优先级', + `is_read` tinyint NOT NULL DEFAULT 0 COMMENT '是否已读', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_priority` (`priority`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 互动记录表 +```sql +CREATE TABLE `feed_interaction` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `target_user_id` bigint DEFAULT NULL COMMENT '目标用户ID', + `content_id` varchar(64) DEFAULT NULL COMMENT '内容ID', + `interaction_type` tinyint NOT NULL COMMENT '互动类型', + `content` text DEFAULT NULL COMMENT '互动内容', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_target_user_id` (`target_user_id`), + KEY `idx_content_id` (`content_id`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +## 缓存策略 + +### Redis缓存设计 +```typescript +// 用户信息缓存 +const USER_INFO_PREFIX = 'user:'; +const USER_INFO_TTL = 3600; // 1小时 + +// 内容信息缓存 +const CONTENT_INFO_PREFIX = 'content:'; +const CONTENT_INFO_TTL = 86400; // 24小时 + +// Feed流缓存 +const FEED_STREAM_PREFIX = 'feed:'; +const FEED_STREAM_TTL = 1800; // 30分钟 + +// 互动计数器 +const INTERACTION_PREFIX = 'interaction:'; +const INTERACTION_TTL = 300; // 5分钟 + +// 推荐结果缓存 +const RECOMMEND_PREFIX = 'recommend:'; +const RECOMMEND_TTL = 300; // 5分钟 + +// 社交关系缓存 +const RELATION_PREFIX = 'relation:'; +const RELATION_TTL = 3600; // 1小时 +``` + +### 缓存策略 +1. **多级缓存**: + - 本地缓存:Caffeine + - 分布式缓存:Redis Cluster + - CDN缓存:静态资源 + +2. **缓存更新策略**: + - Write Behind:异步更新 + - Refresh Ahead:预加载热点数据 + - Cache Invalidation:定时失效 + +3. **Feed流缓存**: + - 分页缓存 + - 用户个性化缓存 + - 实时更新策略 + +### Feed流生成算法 +```java +public class FeedGenerator { + + // 基于时间线的基础Feed + public List generateTimelineFeed(String userId, int offset, int limit) { + // 从Redis获取用户关注列表 + List followUsers = getFollowUsers(userId); + + // 获取关注用户的最新内容 + List feeds = new ArrayList<>(); + for (String followUser : followUsers) { + List userFeeds = getUserRecentFeeds(followUser, offset, limit / followUsers.size()); + feeds.addAll(userFeeds); + } + + // 按时间排序 + return feeds.stream() + .sorted(Comparator.comparing(FeedItem::getCreatedAt).reversed()) + .skip(offset) + .limit(limit) + .collect(Collectors.toList()); + } + + // 个性化推荐Feed + public List generateRecommendFeed(String userId, int offset, int limit) { + // 获取用户兴趣标签 + List interests = getUserInterests(userId); + + // 基于协同过滤推荐内容 + List recommendContentIds = collaborativeFiltering(userId, interests); + + // 基于内容的推荐 + List contentBasedRecommend = contentBasedRecommend(userId); + + // 合并推荐结果 + Set allRecommend = new HashSet<>(); + allRecommend.addAll(recommendContentIds); + allRecommend.addAll(contentBasedRecommend); + + // 获取推荐内容详情 + List feeds = getContentDetails(new ArrayList<>(allRecommend)); + + // 排序并返回 + return feeds.stream() + .sorted(Comparator.comparing(FeedItem::getScore).reversed()) + .skip(offset) + .limit(limit) + .collect(Collectors.toList()); + } + + // 混合Feed流 + public List generateHybridFeed(String userId, int offset, int limit) { + List timelineFeeds = generateTimelineFeed(userId, 0, limit / 2); + List recommendFeeds = generateRecommendFeed(userId, 0, limit / 2); + + // 合并并去重 + Set seenContentIds = new HashSet<>(); + List result = new ArrayList<>(); + + for (FeedItem feed : timelineFeeds) { + if (!seenContentIds.contains(feed.getContentId())) { + result.add(feed); + seenContentIds.add(feed.getContentId()); + } + } + + for (FeedItem feed : recommendFeeds) { + if (!seenContentIds.contains(feed.getContentId())) { + result.add(feed); + seenContentIds.add(feed.getContentId()); + } + } + + // 按权重排序 + return result.stream() + .sorted(Comparator.comparing(FeedItem::getScore).reversed()) + .skip(offset) + .limit(limit) + .collect(Collectors.toList()); + } +} +``` + +## 扩展性考虑 + +### 1. 水平扩展 +- **无状态服务**:Feed服务、推荐服务无状态化 +- **数据分片**:按用户ID分片 +- **读写分离**:主库写入,从库读取 + +### 2. 垂直扩展 +- **服务拆分**:Feed服务、推荐服务、互动服务 +- **数据分层**:热数据、温数据、冷数据 +- **多级缓存**:本地、Redis、CDN + +### 3. 推荐算法扩展 +- **实时推荐**:基于实时行为更新 +- **离线推荐**:批量处理推荐结果 +- **冷启动**:新用户推荐策略 + +### 4. 容灾备份 +- **多活架构**:多机房部署 +- **故障转移**:自动故障检测和转移 +- **数据备份**:定时备份和实时同步 + +## 实际项目经验 + +### 1. 技术栈选择 +- **前端**:React + TypeScript + Mobile +- **后端**:Spring Boot + Node.js + Python +- **数据库**:MySQL + MongoDB + Redis +- **消息队列**:Kafka + Pulsar +- **搜索引擎**:Elasticsearch + ClickHouse + +### 2. 性能优化 +- **Feed缓存**:多级缓存策略 +- **数据库优化**:分库分表、索引优化 +- **算法优化**:推荐算法优化 +- **网络优化**:HTTP/2、Keep-Alive + +### 3. 运维部署 +- **容器化**:Docker + Kubernetes +- **CI/CD**:Jenkins + GitLab +- **监控告警**:ELK Stack + AlertManager +- **压测**:JMeter + Locust + +### 4. 安全设计 +- **内容安全**:内容审核、过滤 +- **用户隐私**:数据脱敏、权限控制 +- **防刷机制**:频率限制、行为分析 + +## 阿里P7加分项 + +### 1. 架构设计能力 +- **高可用架构**:99.99%可用性 +- **高性能架构**:支持亿级QPS +- **扩展性架构**:弹性扩缩容 + +### 2. 技术深度 +- **推荐系统**:协同过滤、深度学习 +- **实时计算**:Flink、Kafka Streams +- **图算法**:社交图谱、关系计算 + +### 3. 业务理解 +- **社交业务**:理解社交网络特性 +- **用户行为**:分析用户互动模式 +- **内容生态**:掌握内容分发逻辑 + +### 4. 团队管理 +- **技术团队**:带领50人+团队 +- **项目管控**:管理亿级用户项目 +- **技术方案**:主导架构设计 + +### 5. 前沿技术 +- **AI应用**:智能推荐、内容生成 +- **边缘计算**:边缘节点处理 +- **Serverless**:函数化服务 + +## 面试常见问题 + +### 1. 如何生成个性化信息流? +- **用户画像**:用户兴趣建模 +- **推荐算法**:协同过滤、基于内容 +- **实时更新**:实时行为追踪 +- **多目标优化**:点击率、停留时间 + +### 2. 如何处理高并发Feed请求? +- **缓存策略**:多级缓存 +- **数据预加载**:Feed预生成 +- **异步处理**:异步更新 +- **分页优化**:游标分页 + +### 3. 如何保证Feed流实时性? +- **实时推送**:WebSocket推送 +- **增量更新**:实时增量计算 +- **事件驱动**:事件总线 +- **缓存预热**:热点数据预热 + +### 4. 如何实现推荐算法? +- **协同过滤**:用户行为相似度 +- **内容分析**:内容特征提取 +- **深度学习**:神经网络模型 +- **多臂老虎机**:探索与利用 + +### 5. 如何处理海量数据? +- **分库分表**:按用户ID分片 +- **数据归档**:冷热数据分离 +- **缓存优化**:热点数据缓存 +- **计算优化**:批量处理、并行计算 \ No newline at end of file diff --git a/questions/design-im.md b/questions/design-im.md new file mode 100644 index 0000000..d355a49 --- /dev/null +++ b/questions/design-im.md @@ -0,0 +1,424 @@ +# 即时通讯系统设计 + +## 需求分析和数据量评估 + +### 需求分析 +- **核心功能**:单聊、群聊、消息推送、在线状态 +- **业务场景**:社交应用、企业通讯、客服系统 +- **QPS评估**:日消息100亿+,峰值QPS 10万+ +- **数据规模**:用户1亿+,好友关系10亿+,历史消息1000亿+ + +### 数据量评估 +- **用户表**:1亿条,日均查询1000万次 +- **好友关系表**:10亿条,日均更新100万次 +- **消息表**:1000亿+条,日增1亿+ +- **群组表**:1亿+条,日均查询100万次 +- **离线消息**:100亿+条,日均推送1亿+ + +## 核心技术难点 + +### 1. 高并发消息处理 +- 亿级用户同时在线 +- 消息的实时性要求 +- 消息的可靠性保证 + +### 2. 消息存储优化 +- 海量消息数据存储 +- 消息的快速检索 +- 历史消息清理 + +### 3. 在线状态管理 +- 实时在线状态同步 +- 心跳检测机制 +- 离线状态管理 + +### 4. 消息推送优化 +- 推送延迟控制 +- 消息去重 +- 推送失败重试 + +## 系统架构设计 + +### 总体架构 +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 移动端APP │ │ PC客户端 │ │ Web网页 │ +│ (iOS/Android)│ │ (Windows) │ │ (Web) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 负载均衡 │ │ API网关 │ │ CDN加速 │ + │ (Nginx) │ │ (Gateway) │ │ (Edge) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 网关服务 │ │ 业务服务 │ │ 推送服务 │ + │ (Gateway) │ │ (微服务) │ │ (Service) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 消息队列 │ │ Redis集群 │ │ 数据库集群 │ + │ (Kafka/Pulsar)│ │ (缓存+pub/sub)│ │ (MySQL分库分表)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 消息存储 │ │ 文件存储 │ │ 搜索引擎 │ + │ (MongoDB) │ │ (MinIO/S3) │ │ (Elasticsearch)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 关键组件 + +#### 1. 流量层 +- **负载均衡**:Nginx L7负载均衡 +- **API网关**:请求路由、限流、认证 +- **CDN加速**:静态资源缓存 + +#### 2. 服务层 +- **网关服务**:连接管理、协议转换 +- **业务服务**:消息处理、关系管理 +- **推送服务**:消息推送、状态同步 +- **通知服务**:系统通知、消息提醒 + +#### 3. 存储层 +- **Redis集群**:在线状态、会话管理 +- **MySQL集群**:用户数据、关系数据 +- **MongoDB集群**:消息存储、历史记录 +- **消息队列**:异步处理、削峰填谷 + +#### 4. 基础设施 +- **消息存储**:分布式文件系统 +- **搜索引擎**:消息全文检索 +- **监控系统**:实时监控告警 +- **日志系统**:业务日志记录 + +## 数据库设计 + +### 用户表 +```sql +CREATE TABLE `im_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `nickname` varchar(50) NOT NULL COMMENT '昵称', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像', + `gender` tinyint DEFAULT 0 COMMENT '性别', + `birthday` date DEFAULT NULL COMMENT '生日', + `signature` varchar(255) DEFAULT NULL COMMENT '个性签名', + `mobile` varchar(20) DEFAULT NULL COMMENT '手机号', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `last_login` timestamp DEFAULT NULL COMMENT '最后登录时间', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 好友关系表 +```sql +CREATE TABLE `im_friends` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `friend_id` bigint NOT NULL COMMENT '好友ID', + `remark` varchar(50) DEFAULT NULL COMMENT '备注', + `group_name` varchar(50) DEFAULT NULL COMMENT '分组名', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_friend_id` (`friend_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 消息表 +```sql +CREATE TABLE `im_message` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `message_id` varchar(64) NOT NULL COMMENT '消息ID', + `from_user_id` bigint NOT NULL COMMENT '发送者ID', + `to_user_id` bigint DEFAULT NULL COMMENT '接收者ID', + `group_id` bigint DEFAULT NULL COMMENT '群组ID', + `message_type` tinyint NOT NULL COMMENT '消息类型', + `content` text COMMENT '消息内容', + `is_read` tinyint NOT NULL DEFAULT 0 COMMENT '是否已读', + `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否已删除', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_message_id` (`message_id`), + KEY `idx_from_user` (`from_user_id`), + KEY `idx_to_user` (`to_user_id`), + KEY `idx_group_id` (`group_id`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 群组表 +```sql +CREATE TABLE `im_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `group_id` bigint NOT NULL COMMENT '群组ID', + `group_name` varchar(100) NOT NULL COMMENT '群名称', + `avatar` varchar(255) DEFAULT NULL COMMENT '群头像', + `creator_id` bigint NOT NULL COMMENT '创建者ID', + `member_count` int NOT NULL DEFAULT 0 COMMENT '成员数', + `max_members` int DEFAULT 500 COMMENT '最大成员数', + `description` text COMMENT '群描述', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 群组成员表 +```sql +CREATE TABLE `im_group_member` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `group_id` bigint NOT NULL COMMENT '群组ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `role` tinyint NOT NULL DEFAULT 0 COMMENT '角色', + `nickname` varchar(50) DEFAULT NULL COMMENT '群昵称', + `join_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_user` (`group_id`, `user_id`), + KEY `idx_group_id` (`group_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +## 缓存策略 + +### Redis缓存设计 +```typescript +// 用户在线状态 +const ONLINE_STATUS_PREFIX = 'online:'; +const ONLINE_STATUS_TTL = 300; // 5分钟 + +// 会话信息 +const SESSION_PREFIX = 'session:'; +const SESSION_TTL = 3600; // 1小时 + +// 未读消息计数 +const UNREAD_PREFIX = 'unread:'; +const UNREAD_TTL = 86400; // 24小时 + +// 消息已读状态 +const READ_PREFIX = 'read:'; +const READ_TTL = 604800; // 7天 + +// 最近会话 +const RECENT_SESSION_PREFIX = 'recent:'; +const RECENT_SESSION_TTL = 86400; // 24小时 +``` + +### 缓存策略 +1. **多级缓存**: + - 本地缓存:Caffeine + - 分布式缓存:Redis Cluster + - 内存缓存:热点数据缓存 + +2. **缓存更新策略**: + - Write Through:写入同时更新缓存 + - Write Behind:异步更新缓存 + - Cache Invalidation:定时失效 + +3. **消息缓存**: + - 最近消息缓存 + - 群组信息缓存 + - 用户状态缓存 + +### WebSocket实现 +```java +public class WebSocketHandler extends TextWebSocketHandler { + + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + // 用户上线 + String userId = getUserIdFromSession(session); + sessions.put(userId, session); + + // 更新在线状态 + redisTemplate.opsForValue().set(ONLINE_STATUS_PREFIX + userId, "1", ONLINE_STATUS_TTL); + + // 通知好友用户上线 + notifyFriendsOnline(userId); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String userId = getUserIdFromSession(session); + Message msg = parseMessage(message.getPayload()); + + // 处理消息 + handleMessage(userId, msg); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 用户下线 + String userId = getUserIdFromSession(session); + sessions.remove(userId); + + // 更新在线状态 + redisTemplate.delete(ONLINE_STATUS_PREFIX + userId); + + // 通知好友用户下线 + notifyFriendsOffline(userId); + } + + private void handleMessage(String fromUserId, Message msg) { + switch (msg.getType()) { + case SINGLE_CHAT: + sendSingleMessage(fromUserId, msg); + break; + case GROUP_CHAT: + sendGroupMessage(fromUserId, msg); + break; + case TYPING: + sendTypingMessage(fromUserId, msg); + break; + } + } + + private void sendSingleMessage(String fromUserId, Message msg) { + String toUserId = msg.getToUserId(); + WebSocketSession session = sessions.get(toUserId); + + if (session != null && session.isOpen()) { + // 用户在线,直接推送 + session.sendMessage(new TextMessage(msg.toJson())); + } else { + // 用户离线,存储离线消息 + saveOfflineMessage(fromUserId, toUserId, msg); + } + + // 更新未读消息计数 + updateUnreadCount(fromUserId, toUserId); + } +} +``` + +## 扩展性考虑 + +### 1. 水平扩展 +- **无状态服务**:业务服务无状态化 +- **数据分片**:按用户ID分片 +- **读写分离**:主库写入,从库读取 + +### 2. 垂直扩展 +- **服务拆分**:网关服务、业务服务、推送服务 +- **数据分层**:热数据、温数据、冷数据 +- **多级缓存**:本地、Redis、CDN + +### 3. 消息可靠性 +- **消息持久化**:消息队列持久化 +- **重试机制**:消息发送失败重试 +- **消息去重**:ID去重机制 + +### 4. 容灾备份 +- **多活架构**:多机房部署 +- **故障转移**:自动故障检测和转移 +- **数据备份**:定时备份和实时同步 + +## 实际项目经验 + +### 1. 技术栈选择 +- **前端**:React Native + Flutter +- **后端**:Spring Boot + Node.js +- **数据库**:MySQL + MongoDB + Redis +- **消息队列**:Kafka + Pulsar +- **通信协议**:WebSocket + MQTT + +### 2. 性能优化 +- **消息压缩**:Gzip压缩消息内容 +- **批量处理**:批量消息处理 +- **连接池**:数据库连接池优化 +- **缓存优化**:多级缓存策略 + +### 3. 运维部署 +- **容器化**:Docker + Kubernetes +- **CI/CD**:Jenkins + GitLab +- **监控告警**:ELK Stack + AlertManager +- **压测**:JMeter + Locust + +### 4. 安全设计 +- **消息加密**:端到端加密 +- **身份认证**:JWT Token认证 +- **消息防刷**:频率限制 +- **数据脱敏**:敏感信息过滤 + +## 阿里P7加分项 + +### 1. 架构设计能力 +- **高可用架构**:99.99%可用性 +- **高性能架构**:支持亿级消息 +- **扩展性架构**:弹性扩缩容 + +### 2. 技术深度 +- **分布式系统**:分布式缓存、分布式消息 +- **通信协议**:WebSocket、MQTT协议 +- **实时系统**:实时消息处理 + +### 3. 业务理解 +- **社交业务**:理解社交应用场景 +- **企业通讯**:掌握企业通讯需求 +- **用户行为**:分析消息使用模式 + +### 4. 团队管理 +- **技术团队**:带领30人+团队 +- **项目管控**:管理亿级用户项目 +- **技术方案**:主导架构设计 + +### 5. 前沿技术 +- **AI应用**:智能回复、消息分类 +- **边缘计算**:边缘节点处理 +- **Serverless**:函数化服务 + +## 面试常见问题 + +### 1. 如何保证消息不丢失? +- **持久化存储**:消息队列持久化 +- **重试机制**:失败消息重试 +- **确认机制**:消息确认ACK +- **补偿机制**:定时补偿任务 + +### 2. 如何处理海量消息存储? +- **分库分表**:按时间分片 +- **数据归档**:冷热数据分离 +- **压缩存储**:消息内容压缩 +- **生命周期管理**:自动清理过期数据 + +### 3. 如何实现消息实时性? +- **长连接**:WebSocket长连接 +- **推送机制**:实时推送 +- **心跳检测**:连接保持 +- **故障转移**:自动重连 + +### 4. 如何处理消息去重? +- **消息ID**:全局唯一ID +- **幂等设计**:处理重复消息 +- **去重表**:已处理消息记录 +- **时间窗口**:时间窗口去重 + +### 5. 如何优化消息推送性能? +- **批量推送**:批量消息推送 +- **连接池**:连接复用 +- **异步处理**:非阻塞IO +- **缓存优化**:推送结果缓存 \ No newline at end of file diff --git a/questions/design-lbs.md b/questions/design-lbs.md new file mode 100644 index 0000000..7ca2fb0 --- /dev/null +++ b/questions/design-lbs.md @@ -0,0 +1,379 @@ +# LBS 附近的人系统设计 + +## 需求分析和数据量评估 + +### 需求分析 +- **核心功能**:位置搜索、附近的人、距离计算、实时更新 +- **业务场景**:社交软件、外卖服务、打车应用 +- **QPS评估**:日查询10亿次,峰值QPS 5万+ +- **数据规模**:用户1亿+,日均位置更新1000万+ + +### 数据量评估 +- **用户位置表**:1亿条,实时更新频率高 +- **位置历史表**:100亿+条,存储轨迹信息 +- **地理索引表**:全球空间索引,数据量大 +- **社交关系表**:10亿+条,好友关系数据 + +## 核心技术难点 + +### 1. 海量数据处理 +- 亿级用户位置数据存储 +- 实时数据写入性能要求 +- 空间索引构建和维护 + +### 2. 实时性要求 +- 位置信息实时更新 +- 查询响应毫秒级 +- 数据一致性问题 + +### 3. 距离计算优化 +- 快速计算两点间距离 +- 批量距离计算优化 +- 空间索引查询优化 + +### 4. 隐私保护 +- 用户位置隐私 +- 数据脱敏处理 +- 访问权限控制 + +## 系统架构设计 + +### 总体架构 +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 移动端APP │ │ Web管理后台 │ │ 第三方API │ +│ (iOS/Android)│ │ (PC) │ │ (SDK) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ API网关 │ │ 负载均衡 │ │ CDN加速 │ + │ (Gateway) │ │ (Nginx) │ │ (Edge) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 位置服务 │ │ 搜索服务 │ │ 计算服务 │ + │ (微服务) │ │ (微服务) │ │ (微服务) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────────┼───────────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Redis集群 │ │ 数据库集群 │ │ 消息队列 │ + │ (缓存+pub/sub)│ │ (PostGIS) │ │ (Kafka/RabbitMQ)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────────┼───────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 地理计算 │ │ 数据仓库 │ │ 地图渲染 │ + │ (Geospatial) │ │ (ClickHouse) │ │ (Mapbox) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 关键组件 + +#### 1. 流量层 +- **API网关**:请求路由、限流、认证 +- **负载均衡**:Nginx L7负载均衡 +- **CDN加速**:静态资源缓存 + +#### 2. 服务层 +- **位置服务**:位置更新、订阅管理 +- **搜索服务**:附近的人查询 +- **计算服务**:距离计算、路线规划 +- **社交服务**:好友关系管理 + +#### 3. 存储层 +- **Redis集群**:位置缓存、订阅频道 +- **PostgreSQL**:空间数据存储 +- **ClickHouse**:地理位置分析 +- **MongoDB**:轨迹数据存储 + +#### 4. 分析层 +- **数据仓库**:离线数据分析 +- **实时计算**:Flink/Kafka Streams +- **地图服务**:地图渲染和展示 + +## 数据库设计 + +### 用户位置表 +```sql +CREATE TABLE `user_location` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `latitude` decimal(10,8) NOT NULL COMMENT '纬度', + `longitude` decimal(11,8) NOT NULL COMMENT '经度', + `accuracy` decimal(10,2) DEFAULT NULL COMMENT '定位精度(米)', + `device_type` varchar(20) DEFAULT NULL COMMENT '设备类型', + `app_version` varchar(50) DEFAULT NULL COMMENT '应用版本', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `expire_at` timestamp NOT NULL COMMENT '过期时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`), + KEY `idx_location` (`latitude`, `longitude`), + KEY `idx_expire_at` (`expire_at`), + SPATIAL KEY `idx_spatial` (`latitude`, `longitude`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 位置历史表 +```sql +CREATE TABLE `location_history` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `latitude` decimal(10,8) NOT NULL COMMENT '纬度', + `longitude` decimal(11,8) NOT NULL COMMENT '经度', + `accuracy` decimal(10,2) DEFAULT NULL COMMENT '定位精度(米)', + `timestamp` datetime NOT NULL COMMENT '时间戳', + `device_type` varchar(20) DEFAULT NULL COMMENT '设备类型', + PRIMARY KEY (`id`), + KEY `idx_user_timestamp` (`user_id`, `timestamp`), + KEY `idx_location` (`latitude`, `longitude`), + SPATIAL KEY `idx_spatial` (`latitude`, `longitude`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 地理索引表 +```sql +CREATE TABLE `geo_index` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `grid_id` varchar(20) NOT NULL COMMENT '网格ID', + `min_lat` decimal(10,8) NOT NULL COMMENT '最小纬度', + `max_lat` decimal(10,8) NOT NULL COMMENT '最大纬度', + `min_lng` decimal(11,8) NOT NULL COMMENT '最小经度', + `max_lng` decimal(11,8) NOT NULL COMMENT '最大经度', + `user_count` int NOT NULL DEFAULT 0 COMMENT '用户数量', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_grid_id` (`grid_id`), + KEY `idx_bounds` (`min_lat`, `max_lat`, `min_lng`, `max_lng`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 好友关系表 +```sql +CREATE TABLE `user_friends` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `friend_id` bigint NOT NULL COMMENT '好友ID', + `distance` decimal(10,2) DEFAULT NULL COMMENT '距离(公里)', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_friend_id` (`friend_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +## 缓存策略 + +### Redis缓存设计 +```typescript +// 用户位置缓存 +const USER_LOCATION_PREFIX = 'location:'; +const USER_LOCATION_TTL = 60; // 1分钟 + +// 地理网格缓存 +const GRID_PREFIX = 'grid:'; +const GRID_TTL = 300; // 5分钟 + +// 用户订阅缓存 +const SUBSCRIPTION_PREFIX = 'subscription:'; +const SUBSCRIPTION_TTL = 3600; // 1小时 + +// 距离计算缓存 +const DISTANCE_PREFIX = 'distance:'; +const DISTANCE_TTL = 60; // 1分钟 +``` + +### 缓存策略 +1. **多级缓存**: + - 本地缓存:Caffeine + - 分布式缓存:Redis Cluster + - CDN缓存:静态资源 + +2. **缓存更新策略**: + - Write Behind:异步更新 + - Refresh Ahead:预加载热点数据 + - Cache Invalidation:定时失效 + +3. **空间索引缓存**: + - 网格索引预加载 + - 热点区域缓存 + - 分层缓存策略 + +### GeoHash实现 +```python +import math + +def geohash_encode(latitude, longitude, precision=12): + # 地球半径(米) + earth_radius = 6371000 + + # GeoHash字符集 + chars = "0123456789bcdefghjkmnpqrstuvwxyz" + + # 计算精度对应的网格大小 + grid_size = 5e6 / (2 ** (precision / 2)) + + # 计算经纬度范围 + lat_min = -90 + lat_max = 90 + lng_min = -180 + lng_max = 180 + + geo_hash = [] + for i in range(precision): + # 纬度二进制 + mid_lat = (lat_min + lat_max) / 2 + if latitude >= mid_lat: + lat_min = mid_lat + lat_bit = 1 + else: + lat_max = mid_lat + lat_bit = 0 + + # 经度二进制 + mid_lng = (lng_min + lng_max) / 2 + if longitude >= mid_lng: + lng_min = mid_lng + lng_bit = 1 + else: + lng_max = mid_lng + lng_bit = 0 + + # 组合成字符索引 + index = lat_bit * 2 + lng_bit + geo_hash.append(chars[index]) + + return ''.join(geo_hash) + +def calculate_distance(lat1, lng1, lat2, lng2): + # Haversine公式计算距离 + R = 6371000 # 地球半径(米) + + dLat = math.radians(lat2 - lat1) + dLng = math.radians(lng2 - lng1) + + a = (math.sin(dLat/2) * math.sin(dLat/2) + + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * + math.sin(dLng/2) * math.sin(dLng/2)) + + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + distance = R * c + + return distance +``` + +## 扩展性考虑 + +### 1. 水平扩展 +- **无状态服务**:位置服务和搜索服务无状态化 +- **数据分片**:按用户ID分片 +- **读写分离**:主库写入,从库读取 + +### 2. 垂直扩展 +- **服务拆分**:位置更新服务、查询服务、计算服务 +- **数据分层**:热数据、温数据、冷数据 +- **多级缓存**:本地、Redis、CDN + +### 3. 全球化部署 +- **地域化服务**:按地域部署服务 +- **数据同步**:跨地域数据同步 +- **灾备切换**:多机房容灾 + +### 4. 性能优化 +- **空间索引**:R-Tree、Quadtree +- **批量处理**:批量查询优化 +- **异步处理**:消息队列异步化 + +## 实际项目经验 + +### 1. 技术栈选择 +- **前端**:React Native + Flutter +- **后端**:Spring Boot + Node.js +- **数据库**:PostgreSQL + Redis +- **缓存**:Redis Cluster +- **消息队列**:Kafka +- **监控**:Prometheus + Grafana + +### 2. 性能优化 +- **空间索引优化**:GeoHash、R-Tree +- **缓存优化**:多级缓存策略 +- **数据库优化**:分库分表、索引优化 +- **网络优化**:HTTP/2、Keep-Alive + +### 3. 运维部署 +- **容器化**:Docker + Kubernetes +- **CI/CD**:Jenkins + GitLab +- **监控告警**:ELK Stack + AlertManager +- **压测**:JMeter + Locust + +### 4. 安全设计 +- **位置隐私**:数据脱敏、权限控制 +- **数据加密**:传输加密、存储加密 +- **访问控制**:API鉴权、黑白名单 + +## 阿里P7加分项 + +### 1. 架构设计能力 +- **高可用架构**:99.99%可用性 +- **高性能架构**:支持亿级查询 +- **全球化架构**:全球多机房部署 + +### 2. 技术深度 +- **空间算法**:GeoHash、R-Tree算法 +- **分布式系统**:分布式缓存、分布式计算 +- **数据库优化**:PostGIS优化、空间索引 + +### 3. 业务理解 +- **社交业务**:理解社交应用场景 +- **位置服务**:掌握LBS业务模式 +- **用户行为**:分析用户移动模式 + +### 4. 团队管理 +- **技术团队**:带领20人+团队 +- **项目管控**:管理千万级用户项目 +- **技术方案**:主导技术架构设计 + +### 5. 前沿技术 +- **边缘计算**:边缘节点处理 +- **AI应用**:轨迹预测、位置推荐 +- **Serverless**:函数化服务 + +## 面试常见问题 + +### 1. 如何高效查询附近的人? +- **空间索引**:GeoHash、R-Tree +- **网格划分**:地理网格索引 +- **缓存策略**:多级缓存优化 + +### 2. 如何保证位置数据实时性? +- **推送机制**:WebSocket实时推送 +- **订阅模式**:用户订阅位置变化 +- **数据过期**:定时清理过期数据 + +### 3. 如何处理海量位置数据? +- **分库分表**:按用户ID分片 +- **数据归档**:冷热数据分离 +- **压缩存储**:轨迹数据压缩 + +### 4. 如何保护用户隐私? +- **数据脱敏**:位置信息模糊化 +- **权限控制**:基于关系的访问控制 +- **加密存储**:敏感信息加密 + +### 5. 如何优化距离计算? +- **近似计算**:GeoHash过滤 +- **批量计算**:向量运算优化 +- **缓存机制**:距离结果缓存 \ No newline at end of file diff --git a/questions/design-seckill.md b/questions/design-seckill.md new file mode 100644 index 0000000..7c8ea81 --- /dev/null +++ b/questions/design-seckill.md @@ -0,0 +1,318 @@ +# 秒杀系统设计 + +## 需求分析和数据量评估 + +### 需求分析 +- **核心功能**:商品秒杀、库存管理、下单支付、用户限流 +- **业务场景**:双十一、618等大促活动,商品短时间内高并发抢购 +- **QPS评估**:假设10万用户同时抢购,QPS可达10万+ +- **峰值预期**:高峰期QPS可达50万+ +- **数据规模**:商品10万+,用户1000万+,订单日峰值1亿+ + +### 数据量评估 +- **商品表**:10万条,日均查询100万次 +- **库存表**:10万条,秒杀期间读写10万+/秒 +- **订单表**:日峰值1亿条,历史数据10亿+ +- **用户表**:1000万条,日均查询500万次 +- **Redis缓存**:商品信息100万条,库存信息10万条 + +## 核心技术难点 + +### 1. 高并发库存管理 +- 传统数据库锁无法支撑高并发 +- 需要分布式锁配合缓存实现 +- 库存扣减的原子性问题 + +### 2. 超卖问题 +- 库存与订单不一致 +- 需要最终一致性保证 +- 重复下单处理 + +### 3. 限流策略 +- 全局限流、用户限流、商品限流 +- 限流算法选择(令牌桶、漏桶) +- 限流后的用户体验 + +### 4. 数据一致性 +- 缓存与数据库的一致性 +- 分布式事务处理 +- 幂等性保证 + +## 系统架构设计 + +### 总体架构 +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ CDN/Static │ │ Load Balance │ │ API Gateway │ +│ Cache │◄──►│ (Nginx) │◄──►│ (Gateway) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌───────────────────────────────────┼───────────────────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 业务服务 │ │ 业务服务 │ │ 业务服务 │ + │ (微服务) │ │ (微服务) │ │ (微服务) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────────────────┼───────────────────────────────────┘ + │ + ┌───────────────────────────────────┼───────────────────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Redis集群 │ │ 消息队列 │ │ 数据库集群 │ + │ (缓存+分布式锁)│ │ (Kafka/RocketMQ)│ │ (MySQL分库分表)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 关键组件 + +#### 1. 流量层 +- **CDN**:静态资源加速 +- **Web服务器**:Nginx负载均衡 +- **API网关**:限流、路由、认证 + +#### 2. 服务层 +- **秒杀服务**:核心业务逻辑 +- **订单服务**:订单处理 +- **库存服务**:库存管理 +- **支付服务**:支付处理 + +#### 3. 存储层 +- **Redis集群**:缓存和分布式锁 +- **数据库集群**:MySQL分库分表 +- **消息队列**:异步处理 + +#### 4. 监控层 +- **监控系统**:实时监控 +- **告警系统**:异常告警 +- **日志系统**:业务日志 + +## 数据库设计 + +### 商品表 +```sql +CREATE TABLE `seckill_product` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `product_id` bigint NOT NULL COMMENT '商品ID', + `product_name` varchar(255) NOT NULL COMMENT '商品名称', + `product_desc` text COMMENT '商品描述', + `original_price` decimal(10,2) NOT NULL COMMENT '原价', + `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价', + `stock_count` int NOT NULL COMMENT '库存数量', + `seckill_count` int NOT NULL DEFAULT 0 COMMENT '已秒杀数量', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_product_id` (`product_id`), + KEY `idx_start_time` (`start_time`), + KEY `idx_end_time` (`end_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 订单表 +```sql +CREATE TABLE `seckill_order` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `order_no` varchar(64) NOT NULL COMMENT '订单号', + `user_id` bigint NOT NULL COMMENT '用户ID', + `product_id` bigint NOT NULL COMMENT '商品ID', + `product_name` varchar(255) NOT NULL COMMENT '商品名称', + `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order_no` (`order_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_product_id` (`product_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 用户表 +```sql +CREATE TABLE `seckill_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `email` varchar(100) NOT NULL COMMENT '邮箱', + `phone` varchar(20) NOT NULL COMMENT '手机号', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +## 缓存策略 + +### Redis缓存设计 +```typescript +// 商品信息缓存 +const PRODUCT_CACHE_PREFIX = 'product:'; +const PRODUCT_CACHE_TTL = 3600; // 1小时 + +// 库存缓存 +const STOCK_CACHE_PREFIX = 'stock:'; +const STOCK_CACHE_TTL = 300; // 5分钟 + +// 分布式锁 +const LOCK_PREFIX = 'lock:'; +const LOCK_TTL = 10; // 10秒 + +// 用户限流 +const RATE_LIMIT_PREFIX = 'rate_limit:'; +const RATE_LIMIT_TTL = 1; // 1秒 +``` + +### 缓存策略 +1. **多级缓存**: + - CDN缓存静态资源 + - Redis缓存热点数据 + - 本地缓存减少网络IO + +2. **缓存更新策略**: + - 主动更新:秒杀开始前预加载 + - 异步更新:异步刷新数据库 + - 失效策略:设置合理的TTL + +3. **缓存预热**: + - 秒杀开始前加载商品信息 + - 预热库存信息到Redis + - 预加载热门商品 + +### 分布式锁实现 +```java +public boolean acquireDistributedLock(String lockKey, String requestId, long expireTime) { + String result = redisTemplate.opsForValue().set( + lockKey, + requestId, + expireTime, + TimeUnit.MILLISECONDS + ); + return "OK".equals(result); +} + +public boolean releaseDistributedLock(String lockKey, String requestId) { + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + + "return redis.call('del', KEYS[1]) " + + "else " + + "return 0 " + + "end"; + Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), + Collections.singletonList(lockKey), + requestId); + return result != null && result > 0; +} +``` + +## 扩展性考虑 + +### 1. 水平扩展 +- **无状态服务**:服务实例可以水平扩展 +- **数据分片**:按商品ID分片 +- **读写分离**:主库写入,从库读取 + +### 2. 垂直扩展 +- **服务拆分**:按业务域拆分微服务 +- **数据分层**:热数据、温数据、冷数据分离 +- **缓存层扩展**:Redis集群扩容 + +### 3. 异步处理 +- **消息队列**:订单处理异步化 +- **事件驱动**:使用事件总线 +- **最终一致性**:保证数据最终一致 + +### 4. 容灾备份 +- **多活架构**:多机房部署 +- **故障转移**:自动故障检测和转移 +- **数据备份**:定时备份和实时同步 + +## 实际项目经验 + +### 1. 技术栈选择 +- **前端**:Vue.js + React +- **后端**:Spring Boot + Node.js +- **数据库**:MySQL + Redis +- **缓存**:Redis Cluster +- **消息队列**:Kafka +- **监控**:Prometheus + Grafana + +### 2. 性能优化 +- **JVM调优**:调整堆大小和GC策略 +- **MySQL优化**:索引优化、慢SQL优化 +- **Redis优化**:集群扩容、内存优化 +- **网络优化**:连接池配置、超时设置 + +### 3. 运维部署 +- **容器化**:Docker + Kubernetes +- **CI/CD**:Jenkins + GitLab +- **监控告警**:ELK Stack + AlertManager +- **压测工具**:JMeter + Locust + +### 4. 灰度发布 +- **蓝绿部署**:无缝切换 +- **金丝雀发布**:逐步放量 +- **流量控制**:按比例分配流量 + +## 阿里P7加分项 + +### 1. 架构设计能力 +- **高可用架构**:设计99.99%可用性的系统 +- **高性能架构**:支持百万级QPS +- **高扩展架构**:支持弹性扩缩容 + +### 2. 技术深度 +- **分布式事务**:Seata、TCC、Saga +- **分布式缓存**:Redis集群、一致性哈希 +- **分布式锁**:Redis、Zookeeper、Etcd + +### 3. 业务理解 +- **电商业务**:理解秒杀业务场景 +- **用户行为**:分析用户抢购习惯 +- **风控系统**:防刷、防作弊机制 + +### 4. 团队管理 +- **技术团队**:带领10人+技术团队 +- **项目管控**:管理千万级用户项目 +- **技术方案评审**:评审核心技术方案 + +### 5. 前沿技术 +- **Serverless**:秒杀函数化 +- **云原生**:K8s微服务架构 +- **AI应用**:智能推荐、风控 + +## 面试常见问题 + +### 1. 秒杀系统如何防止超卖? +- 使用Redis预减库存 +- 分布式锁控制并发 +- 数据库唯一约束 +- 消息队列异步处理 + +### 2. 如何实现限流策略? +- 令牌桶算法 +- 漏桶算法 +- Redis计数器 +- Nginx限流模块 + +### 3. 分布式锁的实现方式? +- Redis RedLock +- Zookeeper +- Etcd +- 数据库悲观锁 + +### 4. 如何保证数据一致性? +- 最终一致性 +- 消息队列补偿 +- 定时任务对账 +- 幂等性设计 + +### 5. 秒杀系统的瓶颈在哪里? +- 库存查询 +- 下单逻辑 +- 支付处理 +- 库存同步 \ No newline at end of file diff --git a/questions/design-shorturl.md b/questions/design-shorturl.md new file mode 100644 index 0000000..b5dff21 --- /dev/null +++ b/questions/design-shorturl.md @@ -0,0 +1,359 @@ +# 短链接系统设计 + +## 需求分析和数据量评估 + +### 需求分析 +- **核心功能**:长链接转短链接、短链接跳转、统计分析 +- **业务场景**:短信营销、社交媒体分享、广告推广 +- **QPS评估**:日点击量10亿次,峰值QPS 3万+ +- **数据规模**:短链接10亿+,日生成1000万+ + +### 数据量评估 +- **短链接表**:10亿条,日均写入1000万次 +- **原始链接表**:10亿条,日均读取10亿次 +- **访问统计表**:100亿+条,日增1亿+ +- **用户表**:1000万+,日均查询100万次 + +## 核心技术难点 + +### 1. 高并发写入 +- 短链接生成需要高性能 +- 避免数据库写入瓶颈 +- 分布式ID生成 + +### 2. 高性能读取 +- 毫秒级响应时间 +- 缓存命中率优化 +- 全球CDN加速 + +### 3. 长链接查重 +- 重复链接检测 +- 去重策略设计 +- 一致性保证 + +### 4. 统计准确性 +- 实时统计延迟 +- 统计数据准确性 +- 分布式计数器 + +## 系统架构设计 + +### 总体架构 +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ CDN/Edge │ │ Load Balance │ │ API Gateway │ +│ Cache │◄──►│ (Anycast) │◄──►│ (Gateway) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌───────────────────────────────────┼───────────────────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 短链接服务 │ │ 统计服务 │ │ 监控服务 │ + │ (微服务) │ │ (微服务) │ │ (微服务) │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────────────────┼───────────────────────────────────┘ + │ + ┌───────────────────────────────────┼───────────────────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Redis集群 │ │ 消息队列 │ │ 数据库集群 │ + │ (缓存+计数器)│ │ (Kafka) │ │ (MySQL分库分表)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌───────────────────────────────────┼───────────────────────────────────┐ + │ │ │ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ 时序数据库 │ │ 数据仓库 │ │ 搜索引擎 │ + │ (InfluxDB) │ │ (ClickHouse) │ │ (Elasticsearch)│ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 关键组件 + +#### 1. 流量层 +- **CDN/Edge**:全球加速,缓存热点短链接 +- **负载均衡**:Anycast IP,就近接入 +- **API网关**:限流、路由、认证 + +#### 2. 服务层 +- **短链接服务**:核心业务逻辑 +- **统计服务**:访问统计和报表 +- **管理服务**:后台管理系统 +- **监控服务**:实时监控告警 + +#### 3. 存储层 +- **Redis集群**:缓存和计数器 +- **MySQL集群**:主从复制,分库分表 +- **时序数据库**:时序数据存储 +- **搜索引擎**:链接检索和分析 + +#### 4. 分析层 +- **数据仓库**:离线数据分析 +- **OLAP引擎**:实时查询分析 +- **报表系统**:业务报表展示 + +## 数据库设计 + +### 短链接表 +```sql +CREATE TABLE `short_url` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `short_code` varchar(10) NOT NULL COMMENT '短链接编码', + `long_url` text NOT NULL COMMENT '原始长链接', + `domain` varchar(255) NOT NULL COMMENT '自定义域名', + `title` varchar(255) DEFAULT NULL COMMENT '页面标题', + `description` text COMMENT '页面描述', + `keywords` varchar(500) DEFAULT NULL COMMENT '关键词', + `user_id` bigint DEFAULT NULL COMMENT '用户ID', + `is_custom` tinyint NOT NULL DEFAULT 0 COMMENT '是否自定义', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `expire_at` datetime DEFAULT NULL COMMENT '过期时间', + `click_count` int NOT NULL DEFAULT 0 COMMENT '点击次数', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_short_code` (`short_code`), + KEY `idx_long_url` (`long_url`(255)), + KEY `idx_user_id` (`user_id`), + KEY `idx_expire_at` (`expire_at`), + KEY `idx_domain` (`domain`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 访问记录表 +```sql +CREATE TABLE `url_access_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `short_code` varchar(10) NOT NULL COMMENT '短链接编码', + `ip` varchar(45) NOT NULL COMMENT '访问IP', + `user_agent` text COMMENT '用户代理', + `referer` text COMMENT '来源页面', + `country` varchar(50) DEFAULT NULL COMMENT '国家', + `region` varchar(50) DEFAULT NULL COMMENT '地区', + `city` varchar(50) DEFAULT NULL COMMENT '城市', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_short_code` (`short_code`), + KEY `idx_created_at` (`created_at`), + KEY `idx_ip` (`ip`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 统计表 +```sql +CREATE TABLE `url_stats` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `short_code` varchar(10) NOT NULL COMMENT '短链接编码', + `date` date NOT NULL COMMENT '统计日期', + `total_clicks` int NOT NULL DEFAULT 0 COMMENT '总点击次数', + `unique_clicks` int NOT NULL DEFAULT 0 COMMENT '独立点击次数', + `by_country` json DEFAULT NULL COMMENT '国家分布', + `by_region` json DEFAULT NULL COMMENT '地区分布', + `by_device` json DEFAULT NULL COMMENT '设备分布', + `by_browser` json DEFAULT NULL COMMENT '浏览器分布', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_date_code` (`date`, `short_code`), + KEY `idx_short_code` (`short_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 用户表 +```sql +CREATE TABLE `short_url_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `email` varchar(100) NOT NULL COMMENT '邮箱', + `domain` varchar(255) DEFAULT NULL COMMENT '自定义域名', + `api_key` varchar(64) DEFAULT NULL COMMENT 'API密钥', + `quota` int NOT NULL DEFAULT 1000 COMMENT '配额', + `used` int NOT NULL DEFAULT 0 COMMENT '已使用', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`), + UNIQUE KEY `uk_api_key` (`api_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +## 缓存策略 + +### Redis缓存设计 +```typescript +// 短链接缓存 +const SHORT_URL_CACHE_PREFIX = 'short_url:'; +const SHORT_URL_CACHE_TTL = 86400; // 24小时 + +// 访问计数器 +const CLICK_COUNT_PREFIX = 'click:'; +const CLICK_COUNT_TTL = 60; // 1分钟 + +// 布隆过滤器 +const BLOOM_FILTER_PREFIX = 'bloom:'; +const BLOOM_FILTER_SIZE = 1000000000; // 10亿 + +// 限流计数器 +const RATE_LIMIT_PREFIX = 'rate_limit:'; +const RATE_LIMIT_TTL = 60; // 1分钟 +``` + +### 缓存策略 +1. **多级缓存**: + - 本地缓存:Caffeine + - 分布式缓存:Redis Cluster + - CDN缓存:边缘节点 + +2. **缓存更新策略**: + - Write Through:写入同时更新缓存 + - Write Back:异步更新缓存 + - Refresh Ahead:预加载热点数据 + +3. **缓存预热**: + - 热门短链接预加载 + - 统计数据预计算 + - 静态资源预缓存 + +### 布隆过滤器实现 +```java +public class BloomFilter { + private final BitSet bitSet; + private final int size; + private final int[] hashSeeds; + + public BloomFilter(int size, int hashCount) { + this.size = size; + this.bitSet = new BitSet(size); + this.hashSeeds = new int[hashCount]; + Random random = new Random(); + for (int i = 0; i < hashCount; i++) { + hashSeeds[i] = random.nextInt(); + } + } + + public void add(String key) { + for (int seed : hashSeeds) { + int hash = Math.abs((key.hashCode() ^ seed) % size); + bitSet.set(hash, true); + } + } + + public boolean mightContain(String key) { + for (int seed : hashSeeds) { + int hash = Math.abs((key.hashCode() ^ seed) % size); + if (!bitSet.get(hash)) { + return false; + } + } + return true; + } +} +``` + +## 扩展性考虑 + +### 1. 水平扩展 +- **无状态服务**:短链接服务无状态化 +- **数据分片**:按短链接编码分片 +- **读写分离**:主库写入,从库读取 + +### 2. 垂直扩展 +- **服务拆分**:API服务、统计服务、管理服务 +- **数据分层**:热数据、温数据、冷数据 +- **多级缓存**:本地、Redis、CDN + +### 3. 全球化部署 +- **CDN加速**:全球节点部署 +- **地域化存储**:按地域分片 +- **灾备切换**:多机房容灾 + +### 4. 监控告警 +- **实时监控**:QPS、响应时间、错误率 +- **业务监控**:点击量、转化率 +- **异常告警**:服务异常、数据异常 + +## 实际项目经验 + +### 1. 技术栈选择 +- **前端**:React + TypeScript +- **后端**:Spring Boot + Node.js +- **数据库**:MySQL + Redis +- **缓存**:Redis Cluster +- **消息队列**:Kafka +- **监控**:Prometheus + Grafana + +### 2. 性能优化 +- **短链接生成**:Snowflake算法 +- **缓存优化**:多级缓存策略 +- **数据库优化**:分库分表、索引优化 +- **网络优化**:HTTP/2、Keep-Alive + +### 3. 运维部署 +- **容器化**:Docker + Kubernetes +- **CI/CD**:Jenkins + GitLab +- **监控告警**:ELK Stack + AlertManager +- **压测**:JMeter + Locust + +### 4. 安全设计 +- **HTTPS**:全链路加密 +- **API限流**:防刷、防攻击 +- **数据脱敏**:敏感信息加密 +- **访问控制**:API密钥认证 + +## 阿里P7加分项 + +### 1. 架构设计能力 +- **高可用架构**:99.99%可用性 +- **高性能架构**:支持亿级QPS +- **全球化架构**:全球CDN加速 + +### 2. 技术深度 +- **分布式算法**:一致性哈希、布隆过滤器 +- **缓存优化**:多级缓存策略 +- **数据库优化**:分库分表、读写分离 + +### 3. 业务理解 +- **营销业务**:理解短链接在营销中的应用 +- **用户行为**:分析点击行为模式 +- **数据统计**:实时统计和离线分析 + +### 4. 团队管理 +- **技术团队**:带领15人+团队 +- **项目管控**:管理亿级用户项目 +- **技术方案**:主导技术架构设计 + +### 5. 前沿技术 +- **Serverless**:短链接函数化 +- **边缘计算**:边缘节点处理 +- **AI应用**:智能推荐、异常检测 + +## 面试常见问题 + +### 1. 短链接如何生成? +- **随机字符**:生成随机字符串 +- **自增序列**:数据库自增ID +- **哈希算法**:MD5/SHA1取前几位 +- **Base62编码**:数字转62进制 + +### 2. 如何避免短链接冲突? +- **布隆过滤器**:快速检测重复 +- **数据库唯一索引**:保证唯一性 +- **重试机制**:冲突时重新生成 + +### 3. 如何实现高并发生成? +- **预生成**:批量生成短链接 +- **分布式ID**:Snowflake算法 +- **内存缓存**:减少数据库访问 + +### 4. 如何统计点击数据? +- **实时统计**:Redis计数器 +- **异步处理**:消息队列存储 +- **离线分析**:数据仓库计算 + +### 5. 如何保证短链接安全? +- **链接过滤**:过滤恶意链接 +- **访问控制**:黑白名单 +- **HTTPS加密**:防止劫持 +- **IP限制**:防刷机制 \ No newline at end of file diff --git a/questions/distributed-id.md b/questions/distributed-id.md new file mode 100644 index 0000000..260b696 --- /dev/null +++ b/questions/distributed-id.md @@ -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 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 生成器的经验 diff --git a/questions/rate-limiting.md b/questions/rate-limiting.md new file mode 100644 index 0000000..8b051e8 --- /dev/null +++ b/questions/rate-limiting.md @@ -0,0 +1,667 @@ +# 限流策略与算法 + +## 问题 + +1. 为什么需要限流?常见的限流场景有哪些? +2. 有哪些常见的限流算法?各自的原理和优缺点是什么? +3. 固定窗口算法有什么问题?如何优化? +4. 滑动窗口算法是如何实现的? +5. 令牌桶和漏桶算法的区别是什么? +6. 分布式限流如何实现?(Redis、Sentinel) +7. 在实际项目中,你是如何设计限流策略的? + +--- + +## 标准答案 + +### 1. 限流的目的和场景 + +#### **为什么需要限流?** + +**保护系统**: +- 防止系统过载(CPU、内存、数据库) +- 防止雪崩效应(服务级联失败) +- 保护核心资源(数据库连接数、API 配额) + +**保证服务质量**: +- 保证大部分用户的正常使用 +- 防止恶意攻击(爬虫、DDoS) +- 实现公平性(防止单个用户占用资源) + +--- + +#### **常见限流场景** + +| 场景 | 限流对象 | 目的 | +|------|---------|------| +| **API 接口** | QPS、TPS | 保护后端服务 | +| **数据库** | 连接数、QPS | 防止数据库打挂 | +| **第三方接口** | 调用次数 | 控制成本(如短信接口) | +| **用户行为** | 操作次数 | 防止刷单、恶意抢购 | +| **爬虫防护** | IP 请求频率 | 保护数据 | + +--- + +### 2. 限流算法对比 + +| 算法 | 原理 | 优点 | 缺点 | 适用场景 | +|------|------|------|------|----------| +| **固定窗口** | 固定时间窗口计数 | 简单 | 临界突变、不精确 | 低要求场景 | +| **滑动窗口** | 滑动时间窗口计数 | 精确 | 内存占用大 | 高精度要求 | +| **漏桶** | 恒定速率流出 | 平滑流量 | 无法应对突发 | 恒定速率场景 | +| **令牌桶** | 恒定速率放入令牌 | 允许突发 | 实现复杂 | 通用场景 | + +--- + +### 3. 固定窗口算法 + +#### **原理** + +将时间划分为固定窗口,每个窗口内计数,超过阈值则拒绝。 + +**示例**: +``` +窗口大小:1 分钟 +阈值:100 次请求 + +10:00:00 - 10:00:59 → 100 次请求 +10:01:00 - 10:01:59 → 重置计数器,重新开始 +``` + +--- + +#### **Java 实现** + +```java +public class FixedWindowRateLimiter { + + private final int limit; // 阈值 + private final long windowSizeMs; // 窗口大小(毫秒) + + private int count; // 当前计数 + private long windowStart; // 窗口开始时间 + + public FixedWindowRateLimiter(int limit, long windowSizeMs) { + this.limit = limit; + this.windowSizeMs = windowSizeMs; + this.windowStart = System.currentTimeMillis(); + } + + public synchronized boolean allowRequest() { + long now = System.currentTimeMillis(); + + // 超出窗口,重置 + if (now - windowStart >= windowSizeMs) { + windowStart = now; + count = 0; + } + + // 检查是否超限 + if (count < limit) { + count++; + return true; + } + + return false; + } +} +``` + +**使用示例**: +```java +// 限制:每分钟 100 次请求 +FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(100, 60 * 1000); + +for (int i = 0; i < 150; i++) { + boolean allowed = limiter.allowRequest(); + System.out.println("请求 " + i + ": " + (allowed ? "通过" : "限流")); +} +``` + +--- + +#### **问题:临界突变** + +**场景**: +``` +阈值:100 / 分钟 + +10:00:59 → 100 次请求(窗口 1 满) +10:01:00 → 100 次请求(窗口 2 满) + ↓ +10:01:00 前后 1 秒内,实际处理了 200 次请求! +``` + +**图解**: +``` +时间 10:00:59 10:01:01 + ↓ ↓ +窗口1 █████████████████ (100 请求) +窗口2 █████████████████ (100 请求) + ↑ + 临界点突变 +``` + +--- + +### 4. 滑动窗口算法 + +#### **原理** + +将时间窗口划分为多个小窗口,滑动计数。 + +**示例**: +``` +大窗口:1 分钟,阈值 100 +小窗口:10 秒 + +10:00:00 - 10:00:10 → 10 次 +10:00:10 - 10:00:20 → 15 次 +10:00:20 - 10:00:30 → 20 次 +10:00:30 - 10:00:40 → 25 次 +10:00:50 - 10:01:00 → 20 次 + +10:00:35 时,统计最近 1 分钟: + 10:00:00 - 10:00:10 → 10 次 + 10:00:10 - 10:00:20 → 15 次 + 10:00:20 - 10:00:30 → 20 次 + 10:00:30 - 10:00:35 → 12.5 次(估算) + 总计:57.5 次 < 100,通过 +``` + +--- + +#### **Java 实现(环形数组)** + +```java +public class SlidingWindowRateLimiter { + + private final int limit; // 阈值 + private final int slotCount; // 槽位数量 + private final long slotSizeMs; // 槽位大小(毫秒) + + private final int[] counters; // 计数器数组 + private long lastSlotTime; // 上次槽位时间 + + public SlidingWindowRateLimiter(int limit, long windowSizeMs, int slotCount) { + this.limit = limit; + this.slotCount = slotCount; + this.slotSizeMs = windowSizeMs / slotCount; + this.counters = new int[slotCount]; + this.lastSlotTime = System.currentTimeMillis(); + } + + public synchronized boolean allowRequest() { + long now = System.currentTimeMillis(); + + // 计算当前槽位索引 + int currentSlot = (int) ((now / slotSizeMs) % slotCount); + + // 清理过期槽位 + int slotsToClear = (int) ((now - lastSlotTime) / slotSizeMs); + if (slotsToClear >= slotCount) { + // 全部过期,清空所有槽位 + Arrays.fill(counters, 0); + } else { + // 部分过期,清理过期槽位 + for (int i = 0; i < slotsToClear; i++) { + int slotToClear = (currentSlot - i + slotCount) % slotCount; + counters[slotToClear] = 0; + } + } + + lastSlotTime = now; + + // 计算当前窗口内总请求数 + int totalCount = 0; + for (int count : counters) { + totalCount += count; + } + + // 检查是否超限 + if (totalCount < limit) { + counters[currentSlot]++; + return true; + } + + return false; + } +} +``` + +**使用示例**: +```java +// 限制:每分钟 100 次请求,分为 6 个槽位(每 10 秒一个) +SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(100, 60 * 1000, 6); +``` + +--- + +#### **Redis 实现(Redisson 的 RRateLimiter)** + +```java +@Autowired +private RedissonClient redisson; + +public boolean allowRequest(String key, int rate, RateIntervalUnit interval) { + RRateLimiter rateLimiter = redisson.getRateLimiter(key); + + // 初始化:每分钟 100 次 + rateLimiter.trySetRate(RateType.OVERALL, rate, interval); + + // 尝试获取许可 + return rateLimiter.tryAcquire(1); +} +``` + +--- + +### 5. 漏桶算法 + +#### **原理** + +想象一个底部有孔的桶: +- 请求像水一样流入桶 +- 桶底以恒定速率漏水 +- 桶满时拒绝请求 + +**图解**: +``` +请求流入 + ↓ + ┌───┐ + │ ███│ ← 桶(容量 = C) + │ ███│ + └───┘ ↓ + 恒定速率(R)流出 +``` + +**特点**: +- **恒定速率**:无论请求多快,流出速率固定 +- **平滑流量**:削峰填谷 + +--- + +#### **Java 实现** + +```java +public class LeakyBucketRateLimiter { + + private final int capacity; // 桶容量 + private final double leakRate; // 漏水速率(请求/毫秒) + + private double currentWater; // 当前水量 + private long lastLeakTime; // 上次漏水时间 + + public LeakyBucketRateLimiter(int capacity, double leakRatePerSec) { + this.capacity = capacity; + this.leakRate = leakRatePerSec / 1000.0; + this.lastLeakTime = System.currentTimeMillis(); + } + + public synchronized boolean allowRequest() { + long now = System.currentTimeMillis(); + + // 漏水 + double leaked = (now - lastLeakTime) * leakRate; + currentWater = Math.max(0, currentWater - leaked); + lastLeakTime = now; + + // 检查是否超限 + if (currentWater < capacity) { + currentWater += 1; + return true; + } + + return false; + } +} +``` + +**使用示例**: +```java +// 容量:100,漏水速率:10 请求/秒 +LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(100, 10); +``` + +--- + +#### **优缺点** + +**优点**: +- 平滑流量,恒定速率 +- 保护下游系统 + +**缺点**: +- 无法应对突发流量 +- 参数调整困难 + +--- + +### 6. 令牌桶算法 + +#### **原理** + +系统以恒定速率向桶中放入令牌: +- 请求到达时,从桶中获取令牌 +- 有令牌则通过,无令牌则拒绝 +- 桶满时,令牌溢出 + +**图解**: +``` +恒定速率放入令牌 + ↓ + ┌───┐ + │ ○○○│ ← 令牌桶(容量 = C) + │ ○○○│ + └───┘ ↓ + 请求获取令牌 +``` + +**特点**: +- **允许突发**:桶中有令牌时可突发处理 +- **恒定平均速率**:长期平均速率恒定 + +--- + +#### **Java 实现** + +```java +public class TokenBucketRateLimiter { + + private final int capacity; // 桶容量 + private final double refillRate; // 放入速率(令牌/毫秒) + + private double currentTokens; // 当前令牌数 + private long lastRefillTime; // 上次放入时间 + + public TokenBucketRateLimiter(int capacity, double refillRatePerSec) { + this.capacity = capacity; + this.refillRate = refillRatePerSec / 1000.0; + this.currentTokens = capacity; + this.lastRefillTime = System.currentTimeMillis(); + } + + public synchronized boolean allowRequest() { + long now = System.currentTimeMillis(); + + // 放入令牌 + double refillTokens = (now - lastRefillTime) * refillRate; + currentTokens = Math.min(capacity, currentTokens + refillTokens); + lastRefillTime = now; + + // 检查是否有令牌 + if (currentTokens >= 1) { + currentTokens -= 1; + return true; + } + + return false; + } +} +``` + +**使用示例**: +```java +// 容量:100,放入速率:10 令牌/秒 +TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100, 10); +``` + +--- + +#### **Guava RateLimiter(令牌桶实现)** + +```java +import com.google.common.util.concurrent.RateLimiter; + +// 创建限流器:每秒 100 个 permits +RateLimiter rateLimiter = RateLimiter.create(100.0); + +// 尝试获取 permit +if (rateLimiter.tryAcquire()) { + // 通过 + processRequest(); +} else { + // 被限流 + rejectRequest(); +} + +// 阻塞式获取(会等待) +rateLimiter.acquire(); // 获取 1 个 permit +rateLimiter.acquire(5); // 获取 5 个 permits +``` + +--- + +#### **优缺点** + +**优点**: +- 允许突发流量 +- 灵活配置 + +**缺点**: +- 实现复杂 +- 突发流量可能影响下游 + +--- + +### 7. 漏桶 vs 令牌桶 + +| 特性 | 漏桶 | 令牌桶 | +|------|------|--------| +| **速率** | 恒定流出 | 恒定放入 | +| **突发** | 不允许突发 | 允许突发 | +| **适用** | 保护下游系统 | 通用场景 | +| **平滑性** | 高 | 中 | + +**选择建议**: +- 保护数据库等脆弱系统 → **漏桶** +- API 接口限流 → **令牌桶** + +--- + +### 8. 分布式限流 + +#### **基于 Redis(滑动窗口)** + +```java +@Service +public class RedisRateLimiter { + + @Autowired + private StringRedisTemplate redisTemplate; + + public boolean allowRequest(String key, int limit, int windowSizeSec) { + long now = System.currentTimeMillis(); + long windowStart = now - windowSizeSec * 1000; + + // Lua 脚本(原子操作) + String luaScript = + "local key = KEYS[1]\n" + + "local now = tonumber(ARGV[1])\n" + + "local windowStart = tonumber(ARGV[2])\n" + + "local limit = tonumber(ARGV[3])\n" + + + // 删除过期记录 + "redis.call('zremrangebyscore', key, '-inf', windowStart)\n" + + + // 获取当前窗口内计数 + "local count = redis.call('zcard', key)\n" + + + // 检查是否超限 + "if count < limit then\n" + + " redis.call('zadd', key, now, now)\n" + + " redis.call('expire', key, windowStart)\n" + + " return 1\n" + + "else\n" + + " return 0\n" + + "end"; + + // 执行 Lua 脚本 + DefaultRedisScript script = new DefaultRedisScript<>(luaScript, Long.class); + Long result = redisTemplate.execute(script, Collections.singletonList(key), + String.valueOf(now), String.valueOf(windowStart), String.valueOf(limit)); + + return result == 1; + } +} +``` + +**使用示例**: +```java +// 限制:每个 IP 每分钟 100 次请求 +boolean allowed = redisRateLimiter.allowRequest("rate:limit:ip:" + ip, 100, 60); +``` + +--- + +#### **基于 Sentinel(阿里巴巴)** + +**引入依赖**: +```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + +``` + +**配置限流规则**: +```java +@Configuration +public class SentinelConfig { + + @PostConstruct + public void initFlowRules() { + List rules = new ArrayList<>(); + + // 定义规则:QPS 限制 1000 + FlowRule rule = new FlowRule(); + rule.setResource("api"); // 资源名 + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 限流阈值类型 + rule.setCount(1000); // 阈值 + rule.setStrategy(RuleConstant.STRATEGY_DIRECT); // 流控策略 + + rules.add(rule); + FlowRuleManager.loadRules(rules); + } +} +``` + +**使用注解**: +```java +@RestController +public class ApiController { + + @GetMapping("/api") + @SentinelResource(value = "api", blockHandler = "handleBlock") + public String api() { + return "success"; + } + + // 限流降级 + public String handleBlock(BlockException ex) { + return "Too many requests"; + } +} +``` + +**配置文件(动态规则)**: +```yaml +# application.yml +spring: + cloud: + sentinel: + transport: + dashboard: localhost:8080 # Sentinel Dashboard + datasource: + flow: + nacos: + server-addr: localhost:8848 + data-id: ${spring.application.name}-flow-rules + rule-type: flow +``` + +--- + +### 9. 实际项目应用 + +#### **多级限流策略** + +``` +用户级限流(单用户 QPS = 10) + ↓ +接口级限流(总 QPS = 10000) + ↓ +应用级限流(CPU < 80%) + ↓ +数据库级限流(连接数 < 500) +``` + +--- + +#### **用户级限流(防刷)** + +```java +@Aspect +@Component +public class RateLimitAspect { + + @Autowired + private RedisTemplate redisTemplate; + + @Around("@annotation(rateLimit)") + public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { + String key = "rate:limit:user:" + getCurrentUserId(); + + int limit = rateLimit.limit(); + int duration = rateLimit.duration(); + + // Redis + Lua 限流 + boolean allowed = allowRequest(key, limit, duration); + + if (!allowed) { + throw new RateLimitException("请求过于频繁,请稍后再试"); + } + + return joinPoint.proceed(); + } +} +``` + +--- + +#### **接口级限流** + +```java +// Sentinel 配置不同接口的限流规则 +FlowRule apiRule = new FlowRule(); +apiRule.setResource("userApi"); +apiRule.setCount(1000); + +FlowRule orderRule = new FlowRule(); +orderRule.setResource("orderApi"); +orderRule.setCount(500); +``` + +--- + +### 10. 阿里 P7 加分项 + +**深度理解**: +- 理解各种限流算法的适用场景和权衡 +- 理解分布式限流的一致性问题 + +**实战经验**: +- 有处理线上突发流量导致系统崩溃的经验 +- 有设计多级限流策略的经验 +- 有限流参数调优的经验(如何确定限流阈值) + +**架构能力**: +- 能设计支持动态调整的限流系统 +- 能设计限流的监控和告警体系 +- 有灰度发布和降级预案 + +**技术选型**: +- 了解 Sentinel、Hystrix、Resilience4j 等框架 +- 有自研限流组件的经验 +- 能根据业务特点选择合适的限流算法 diff --git a/questions/replication-delay.md b/questions/replication-delay.md new file mode 100644 index 0000000..30c3ae8 --- /dev/null +++ b/questions/replication-delay.md @@ -0,0 +1,1378 @@ +# MySQL 主从延迟面试指南 + +## 1. 主从复制原理 + +### MySQL 主从复制架构 + +**主从复制的三种架构模式**: + +1. **主从复制**(Master-Slave) +``` +Master → Slave1 + → Slave2 + → Slave3 +``` + +2. **主主复制**(Master-Master) +``` +Master1 ↔ Master2 + ↓ + Slave1 + ↓ + Slave2 +``` + +3. **级联复制**(Master-Slave-Slave) +``` +Master → Slave1 → Slave2 + ↓ + Slave3 +``` + +### 主从复制流程 + +MySQL 主从复制基于二进制日志(Binary Log),主要分为三个步骤: + +``` +1. 主库写操作: + ↓ + 主库执行 SQL 事务 + ↓ + 写入 binlog + ↓ + 发送 binlog 到从库 + +2. 从库读取: + ↓ + 从库 I/O 线程读取 binlog + ↓ + 写入中继日志(Relay Log) + ↓ + 更新 master-info + +3. 从库应用: + ↓ + 从库 SQL 线程执行中继日志 + ↓ + 更新 slave-relay-info + ↓ + 应用完成 +``` + +### 复制的核心组件 + +**主端组件**: +- **binlog**:记录所有更改操作 +- **dump thread**:发送 binlog 到从库 + +**从端组件**: +- **I/O thread**:接收 binlog +- **SQL thread**:执行中继日志 +- **relay log**:中继日志 +- **master.info**:记录主库连接信息 +- **relay-log.info**:记录中继日志位置 + +### 复制的配置示例 + +**主库配置(my.cnf)**: +```ini +[mysqld] +# 启用二进制日志 +server-id = 1 +log-bin = mysql-bin +binlog-format = ROW +binlog-row-image = FULL +expire_logs_days = 7 +max_binlog_size = 1G + +# GTID 配置 +gtid_mode = ON +enforce_gtid_consistency = ON + +# 复制过滤 +replicate-wild-ignore-table = mysql.% +replicate-wild-ignore-table = test.% +``` + +**从库配置(my.cnf)**: +```ini +[mysqld] +# 从库配置 +server-id = 2 +relay-log = mysql-relay-bin +read-only = 1 + +# GTID 配置 +gtid_mode = ON +enforce_gtid_consistency = ON + +# 中继日志自动清理 +relay_log_purge = 1 +``` + +**主从复制建立**: +```sql +-- 主库创建复制用户 +CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; +GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; + +-- 从库配置主库连接 +CHANGE REPLICATION SOURCE TO + SOURCE_HOST = '192.168.1.100', + SOURCE_PORT = 3306, + SOURCE_USER = 'repl', + SOURCE_PASSWORD = 'password', + SOURCE_AUTO_POSITION = 1; -- 使用 GTID + +-- 启动复制 +START REPLICA; + +-- 查看复制状态 +SHOW REPLICA STATUS \G +``` + +## 2. 主从延迟的原因 + +### 硬件层面 + +**1. 磁盘 I/O 瓶颈** +```bash +# 查看磁盘性能 +iostat -x 1 10 + +# 监控磁盘使用情况 +df -h +``` + +**2. 网络延迟** +```bash +# 网络延迟测试 +ping 192.168.1.100 +traceroute 192.168.1.100 +mtr 192.168.1.100 + +# 网络带宽监控 +iftop +nload +``` + +**3. CPU 负载过高** +```bash +# CPU 使用率监控 +top +htop +mpstat 1 10 + +# MySQL 相关进程 +ps aux | grep mysql +``` + +### 配置层面 + +**1. 复制参数配置不当** +```ini +# 主库配置优化 +[mysqld] +# 二进制日志相关 +sync_binlog = 1 # 1: 每次事务提交都同步,0: 操作系统决定 +binlog_cache_size = 4M # binlog 缓冲区大小 +binlog_stmt_cache_size = 4M # 语句缓存大小 + +# 从库配置优化 +[mysqld] +# SQL 线程配置 +slave_parallel_workers = 4 # MySQL 5.7+ 并行复制 +slave_parallel_type = LOGICAL_CLOCK # 并行复制类型 +slave_pending_jobs_size_max = 2G # 待处理任务队列大小 + +# 中继日志相关 +relay_log_space_limit = 8G # 中继日志限制 +``` + +**2. 存储引擎配置** +```sql +-- 主库使用 InnoDB 配置 +SET GLOBAL innodb_flush_log_at_trx_commit = 1; -- 1: 每次事务提交都刷新 +SET GLOBAL innodb_buffer_pool_size = 8G; -- 50-70% 内存 +SET GLOBAL innodb_io_capacity = 2000; -- 根据 IOPS 调整 +SET GLOBAL innodb_io_capacity_max = 4000; -- 最大 I/O capacity + +-- 从库优化配置 +SET GLOBAL read_only = 1; -- 只读模式 +SET GLOBAL innodb_flush_log_at_trx_commit = 1; -- 主从一致性 +``` + +### 业务层面 + +**1. 大事务处理** +```sql +-- 问题示例:大事务导致延迟 +BEGIN; +-- 执行大量更新操作 +UPDATE order_table SET status = 'completed' WHERE create_time < '2023-01-01'; +UPDATE order_table SET status = 'shipped' WHERE create_time < '2023-02-01'; +... -- 大量操作 +COMMIT; + +-- 优化方案:批量处理 +BEGIN; +-- 分批处理 +UPDATE order_table SET status = 'completed' WHERE create_time < '2023-01-01' LIMIT 1000; +COMMIT; + +-- 或者使用事件调度 +CREATE EVENT batch_update_order_status +ON SCHEDULE EVERY 1 MINUTE +DO +BEGIN + UPDATE order_table SET status = 'completed' + WHERE create_time < '2023-01-01' + LIMIT 1000; +END; +``` + +**2. 复杂查询影响复制** +```sql +-- 复杂查询可能导致 SQL 线程阻塞 +SELECT o.* FROM order o +JOIN user u ON o.user_id = u.id +WHERE o.amount > 10000 +AND u.create_time > '2023-01-01' +AND o.status IN ('pending', 'processing') +ORDER BY o.create_time DESC +LIMIT 1000; + +-- 优化方案:创建索引 +CREATE INDEX idx_order_user_status ON order(user_id, status, create_time); +CREATE INDEX idx_user_create_time ON user(create_time); + +-- 或者使用物化视图 +CREATE MATERIALIZED VIEW mv_order_user_status AS +SELECT o.*, u.name +FROM order o +JOIN user u ON o.user_id = u.id; + +-- 定期刷新 +CREATE EVENT refresh_mv_order_user_status +ON SCHEDULE EVERY 5 MINUTE +DO + REFRESH MATERIALIZED VIEW mv_order_user_status; +``` + +### 复制模式影响 + +**1. 语句复制(STATEMENT)** +```sql +-- 语句复制的问题 +CREATE PROCEDURE update_order_amount(IN p_user_id INT, IN p_factor DECIMAL) +BEGIN + DECLARE done INT DEFAULT FALSE; + DECLARE v_order_id INT; + DECLARE v_amount DECIMAL; + DECLARE cur CURSOR FOR SELECT id, amount FROM order WHERE user_id = p_user_id; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; + + OPEN cur; + read_loop: LOOP + FETCH cur INTO v_order_id, v_amount; + IF done THEN + LEAVE read_loop; + END IF; + UPDATE order SET amount = v_amount * p_factor WHERE id = v_order_id; + END LOOP; + CLOSE cur; +END; + +-- 每次执行都会在从库重复执行,导致不同结果 +CALL update_order_amount(1, 1.1); +``` + +**2. 行复制(ROW)** +```sql +-- 行复制配置 +[mysqld] +binlog_format = ROW +binlog_row_image = FULL + +-- 优点:数据一致性好 +-- 缺点:binlog 体积大,复制性能较低 +``` + +**3. 混合复制(MIXED)** +```ini +[mysqld] +binlog_format = MIXED +``` + +## 3. 如何监控主从延迟 + +### 基础监控命令 + +**1. 查看复制延迟** +```sql +-- MySQL 8.0+ +SHOW REPLICA STATUS\G + +-- 关键字段 +Seconds_Behind_Master: 延迟秒数 +Replica_IO_Running: I/O 线程状态 +Replica_SQL_Running: SQL 线程状态 + +-- MySQL 5.7 +SHOW SLAVE STATUS\G +``` + +**2. GTID 延迟监控** +```sql +-- 使用 GTID 监控延迟 +SELECT + master_executed_gtid_set, + received_gtid_set, + SUBSTRING(master_executed_gtid_set, 1, 20) as master_gtid, + SUBSTRING(received_gtid_set, 1, 20) as slave_gtid +FROM performance_schema.replication_connection_status +WHERE channel_name = ''; +``` + +### 延迟监控脚本 + +**1. Python 监控脚本** +```python +#!/usr/bin/env python3 +import pymysql +import time +import sys +from datetime import datetime + +class MySQLReplicationMonitor: + def __init__(self, host, user, password, port=3306): + self.host = host + self.user = user + self.password = password + self.port = port + + def get_replication_status(self): + try: + conn = pymysql.connect(host=self.host, user=self.user, password=self.password, port=self.port) + cursor = conn.cursor() + + query = """ + SELECT + Seconds_Behind_Master, + Slave_IO_Running, + Slave_SQL_Running, + Last_IO_Error, + Last_SQL_Error, + Last_IO_Error_Timestamp, + Last_SQL_Error_Timestamp + FROM information_schema.replica_status + """ + + cursor.execute(query) + result = cursor.fetchone() + + return { + 'delay': result[0] if result[0] is not None else 0, + 'io_running': result[1] == 'Yes', + 'sql_running': result[2] == 'Yes', + 'io_error': result[3], + 'sql_error': result[4], + 'io_error_time': result[5], + 'sql_error_time': result[6] + } + except Exception as e: + print(f"Error: {e}") + return None + finally: + if 'conn' in locals(): + conn.close() + + def monitor(self, interval=60, threshold=300): + while True: + status = self.get_replication_status() + if status: + print(f"[{datetime.now()}] Delay: {status['delay']}s, IO: {status['io_running']}, SQL: {status['sql_running']}") + + if status['delay'] > threshold: + print(f"ALERT: Replication delay exceeds threshold: {status['delay']}s") + + if not status['io_running']: + print("ERROR: IO thread stopped") + + if not status['sql_running']: + print("ERROR: SQL thread stopped") + + if status['io_error']: + print(f"IO Error: {status['io_error']}") + + if status['sql_error']: + print(f"SQL Error: {status['sql_error']}") + + time.sleep(interval) + +# 使用示例 +if __name__ == "__main__": + monitor = MySQLReplicationMonitor( + host="192.168.1.200", + user="monitor", + password="password" + ) + monitor.monitor(interval=30, threshold=60) +``` + +**2. Shell 监控脚本** +```bash +#!/bin/bash +# replication_monitor.sh + +MYSQL_HOST="192.168.1.200" +MYSQL_USER="monitor" +MYSQL_PASSWORD="password" +MYSQL_PORT="3306" +THRESHOLD=300 + +while true; do + DELAY=$(mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASSWORD -P$MYSQL_PORT -e "SHOW REPLICA STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}') + + if [ -z "$DELAY" ]; then + DELAY="0" + fi + + TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") + echo "[$TIMESTAMP] Replication Delay: $DELAY seconds" + + if [ "$DELAY" -gt "$THRESHOLD" ]; then + echo "ALERT: Replication delay exceeds threshold: $DELAY seconds" + # 发送告警 + # curl -X POST -H "Content-Type: application/json" -d '{"text":"Replication delay: '$DELAY' seconds"}' https://your-webhook-url + fi + + sleep 30 +done +``` + +### 监控系统集成 + +**1. Prometheus + Grafana 监控** +```yaml +# prometheus.yml +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'mysql_replication' + static_configs: + - targets: ['192.168.1.100:9104', '192.168.1.200:9104'] +``` + +**2. Exporter 配置** +```python +# mysql_exporter 配置 +collector_groups: + - replication + - process + - schema + - global_innodb_metrics + +# 查询示例 +SELECT + variable_value as seconds_behind_master +FROM performance_schema.global_status + WHERE variable_name = 'Seconds_Behind_Master'; + +SELECT + variable_name, + variable_value +FROM performance_schema.global_status + WHERE variable_name IN ( + 'Slave_running', + 'Slave_io_running', + 'Slave_sql_running' + ); +``` + +**3. Grafana Dashboard** +```json +{ + "dashboard": { + "title": "MySQL Replication Monitor", + "panels": [ + { + "title": "Replication Delay", + "type": "graph", + "targets": [ + { + "expr": "mysql_global_status_seconds_behind_master", + "legendFormat": "{{instance}}" + } + ] + }, + { + "title": "IO Thread Status", + "type": "singlestat", + "targets": [ + { + "expr": "mysql_global_status_slave_io_running", + "legendFormat": "{{instance}}" + } + ] + }, + { + "title": "SQL Thread Status", + "type": "singlestat", + "targets": [ + { + "expr": "mysql_global_status_slave_sql_running", + "legendFormat": "{{instance}}" + } + ] + } + ] + } +} +``` + +### 延迟告警配置 + +**1. Alertmanager 配置** +```yaml +# alertmanager.yml +groups: +- name: mysql_replication + rules: + - alert: MySQLReplicationLag + expr: mysql_global_status_seconds_behind_master > 300 + for: 5m + labels: + severity: warning + annotations: + summary: "MySQL replication lag is {{ $value }} seconds" + description: "Replication delay exceeds 5 minutes" + + - alert: MySQLReplicationStopped + expr: mysql_global_status_slave_io_running == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "MySQL replication IO thread stopped" + description: "IO thread is not running" + + - alert: MySQLSQLThreadStopped + expr: mysql_global_status_slave_sql_running == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "MySQL replication SQL thread stopped" + description: "SQL thread is not running" +``` + +**2. 企业级监控告警** +```python +# 企业级监控服务 +class EnterpriseReplicationMonitor: + def __init__(self, config): + self.config = config + self.alert_channels = [] + + def add_alert_channel(self, channel): + self.alert_channels.append(channel) + + def check_replication_health(self): + status = self.get_replication_status() + + alerts = [] + if status['delay'] > self.config['threshold']: + alerts.append({ + 'level': 'warning', + 'message': f"Replication delay: {status['delay']}s", + 'timestamp': datetime.now() + }) + + if not status['io_running']: + alerts.append({ + 'level': 'critical', + 'message': "IO thread stopped", + 'timestamp': datetime.now() + }) + + if not status['sql_running']: + alerts.append({ + 'level': 'critical', + 'message': "SQL thread stopped", + 'timestamp': datetime.now() + }) + + # 发送告警 + for alert in alerts: + self.send_alert(alert) + + def send_alert(self, alert): + for channel in self.alert_channels: + channel.send(alert) + +# 邮件告警 +class EmailAlertChannel: + def send(self, alert): + # 发送邮件逻辑 + pass + +# 钉钉告警 +class DingTalkAlertChannel: + def send(self, alert): + # 发送钉钉消息 + pass + +# 企业微信告警 +class WeChatAlertChannel: + def send(self, alert): + # 发送企业微信消息 + pass +``` + +## 4. 如何解决主从延迟 + +### 读写分离策略 + +**1. 基础读写分离** +```java +// Java 读写分离实现 +public class DataSourceRouter { + private final DataSource masterDataSource; + private final List slaveDataSources; + private final AtomicInteger counter = new AtomicInteger(0); + + public DataSource getDataSource(boolean isWrite) { + if (isWrite) { + return masterDataSource; + } else { + int index = counter.getAndIncrement() % slaveDataSources.size(); + return slaveDataSources.get(index); + } + } + + // 使用注解 + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ReadOnly { + } +} + +@Service +public class UserService { + @Autowired + @ReadOnly + public List getUsers() { + // 从从库读取 + } + + @Autowired + public void createUser(User user) { + // 写入主库 + } +} +``` + +**2. 动态数据源路由** +```java +@Configuration +public class DynamicDataSourceConfig { + + @Bean + @ConfigurationProperties("spring.datasource.master") + public DataSource masterDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + @ConfigurationProperties("spring.datasource.slave") + public DataSource slaveDataSource1() { + return DataSourceBuilder.create().build(); + } + + @Bean + @ConfigurationProperties("spring.datasource.slave") + public DataSource slaveDataSource2() { + return DataSourceBuilder.create().build(); + } + + @Bean + public DataSource dynamicDataSource() { + Map targetDataSources = new HashMap<>(); + targetDataSources.put("master", masterDataSource()); + targetDataSources.put("slave1", slaveDataSource1()); + targetDataSources.put("slave2", slaveDataSource2()); + + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); + dynamicDataSource.setTargetDataSources(targetDataSources); + + return dynamicDataSource; + } +} + +public class DynamicDataSource extends AbstractRoutingDataSource { + private static final ThreadLocal dataSourceKey = new ThreadLocal<>(); + + public static void setDataSourceKey(String key) { + dataSourceKey.set(key); + } + + public static String getDataSourceKey() { + return dataSourceKey.get(); + } + + @Override + protected Object determineCurrentLookupKey() { + return getDataSourceKey(); + } +} +``` + +**3. 中间件实现读写分离** +```yaml +# MyCat 配置 +# schema.xml + +
+
+ + + + + + + + select user() + + + + + + select user() + + + +``` + +### 并行复制优化 + +**1. MySQL 5.7+ 并行复制** +```sql +-- 从库配置 +SET GLOBAL slave_parallel_workers = 4; +SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK'; +SET GLOBAL slave_pending_jobs_size_max = 1024M; + +-- 查看并行复制状态 +SHOW VARIABLES LIKE '%parallel%'; +SHOW STATUS LIKE '%slave_parallel%'; +``` + +**2. 基于库的并行复制** +```ini +# my.cnf 配置 +[mysqld] +# MySQL 5.7.2+ 支持库级别并行复制 +slave_parallel_workers = 8 +slave_parallel_type = DATABASE +replicate_wild_ignore_table=mysql.% +replicate_wild_ignore_table=test.% +``` + +**3. 基于组提交的并行复制** +```ini +# 主库配置 +[mysqld] +# 启用组提交 +binlog_group_commit_sync_delay = 1000 +binlog_group_commit_sync_no_delay_count = 10 + +# 优化二进制日志 +sync_binlog = 1 +innodb_flush_log_at_trx_commit = 1 + +# 从库配置 +[mysqld] +# 启用并行复制 +slave_parallel_workers = 16 +slave_parallel_type = LOGICAL_CLOCK +slave_preserve_commit_order = 1 +``` + +### 半同步复制 + +**1. 半同步复制配置** +```sql +-- 主库配置 +-- 安装插件 +INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; +SET GLOBAL rpl_semi_sync_master_enabled = 1; + +-- 从库配置 +-- 安装插件 +INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; +SET GLOBAL rpl_semi_sync_slave_enabled = 1; + +-- 查看半同步状态 +SHOW STATUS LIKE 'Rpl_semi_sync%'; +``` + +**2. 半同步复制超时设置** +```sql +-- 主库超时设置 +SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 毫秒 + +-- 从库超时设置 +SET GLOBAL rpl_semi_sync_slave_timeout = 1000; + +-- 主库等待从库确认数 +SET GLOBAL rpl_semi_sync_master_wait_no_slave = 1; +``` + +**3. 半同步复制监控** +```java +// 半同步复制监控组件 +@Component +public class SemiSyncMonitor { + + @Scheduled(fixeedRate = 5000) + public void monitorSemiSync() { + // 检查半同步状态 + boolean isMasterSemiSync = checkMasterSemiSyncStatus(); + boolean isSlaveSemiSync = checkSlaveSemiSyncStatus(); + + // 监控延迟 + long delay = getReplicationDelay(); + + // 监控等待时间 + long waitTime = getSemiSyncWaitTime(); + + // 告警检查 + if (!isMasterSemiSync) { + alert("Master semi-sync disabled"); + } + + if (delay > 60) { + alert("Replication delay too high: " + delay + "s"); + } + + if (waitTime > 1000) { + alert("Semi-sync wait time too long: " + waitTime + "ms"); + } + } +} +``` + +### 优化主库性能 + +**1. 主库配置优化** +```ini +# my.cnf 主库优化 +[mysqld] +# 缓冲池 +innodb_buffer_pool_size = 16G +innodb_buffer_pool_instances = 8 + +# 日志配置 +innodb_log_file_size = 4G +innodb_log_buffer_size = 64M +innodb_flush_log_at_trx_commit = 1 + +# I/O 配置 +innodb_io_capacity = 2000 +innodb_io_capacity_max = 4000 +innodb_read_io_threads = 16 +innodb_write_io_threads = 16 + +# 二进制日志 +binlog_format = ROW +sync_binlog = 1 +binlog_cache_size = 32M +binlog_stmt_cache_size = 32M +expire_logs_days = 7 +max_binlog_size = 1G + +# 连接配置 +max_connections = 1000 +thread_cache_size = 100 +``` + +**2. 主库SQL优化** +```sql +-- 优化主库查询 +-- 避免全表扫描 +CREATE INDEX idx_user_id_status ON order(user_id, status); +CREATE INDEX idx_order_time ON order(create_time); + +-- 优化大表更新 +-- 使用批量更新 +UPDATE order SET status = 'completed' +WHERE status = 'pending' +AND create_time < NOW() - INTERVAL 1 DAY +LIMIT 1000; + +-- 使用临时表处理大操作 +CREATE TEMPORARY TABLE temp_order_update AS +SELECT id, user_id FROM order +WHERE status = 'pending' +AND create_time < NOW() - INTERVAL 1 DAY +LIMIT 1000; + +UPDATE temp_order_update t +JOIN order o ON t.id = o.id +SET o.status = 'completed'; + +-- 定期优化表 +ANALYZE TABLE order, user; +OPTIMIZE TABLE order; +``` + +### 从库优化策略 + +**1. 从库配置优化** +```ini +# my.cnf 从库优化 +[mysqld] +# 只读模式 +read_only = 1 +super_read_only = 1 + +# 缓冲池(通常比主库大) +innodb_buffer_pool_size = 24G +innodb_buffer_pool_instances = 8 + +# 读取优化 +innodb_read_io_threads = 32 +innodb_write_io_threads = 16 + +# 中继日志 +relay_log_space_limit = 8G +relay_log_purge = 1 + +# 复制优化 +slave_parallel_workers = 16 +slave_parallel_type = LOGICAL_CLOCK +slave_pending_jobs_size_max = 2G + +# 查询缓存(MySQL 8.0已移除) +query_cache_size = 0 +``` + +**2. 从库SQL优化** +```sql +-- 从库专用索引 +CREATE INDEX idx_query_user ON user(create_time); +CREATE INDEX idx_report_order ON order(create_time, amount); + +-- 优化复杂查询 +-- 使用覆盖索引 +SELECT SQL_NO_CACHE id, name, email +FROM user +WHERE create_time > '2023-01-01' +AND status = 'active'; + +-- 使用物化视图 +CREATE MATERIALIZED VIEW mv_user_active AS +SELECT id, name, email, create_time +FROM user +WHERE status = 'active' +AND create_time > '2023-01-01'; + +-- 定期刷新 +CREATE EVENT refresh_mv_user_active +ON SCHEDULE EVERY 5 MINUTE +DO + REFRESH MATERIALIZED VIEW mv_user_active; +``` + +### 故障恢复方案 + +**1. 主从切换自动化** +```java +// 主从切换服务 +@Service +public class MasterSlaveFailoverService { + + @Autowired + private DataSource masterDataSource; + + @Autowired + private List slaveDataSources; + + @Autowired + private NotificationService notificationService; + + public void failover() { + // 1. 检测主库故障 + if (!checkMasterHealth()) { + // 2. 选择新的主库 + DataSource newMaster = selectNewMaster(); + + // 3. 执行主从切换 + executeFailover(newMaster); + + // 4. 通知应用 + notifyApplication(newMaster); + + // 5. 发送告警 + notificationService.sendAlert("Master-Slave failover completed"); + } + } + + private boolean checkMasterHealth() { + try { + Connection conn = masterDataSource.getConnection(); + return conn.isValid(5); + } catch (Exception e) { + return false; + } + } + + private DataSource selectNewMaster() { + // 选择最健康的从库 + return slaveDataSources.stream() + .filter(this::checkSlaveHealth) + .findFirst() + .orElseThrow(() -> new RuntimeException("No healthy slave available")); + } + + private void executeFailover(DataSource newMaster) { + // 1. 停止从库复制 + stopReplication(newMaster); + + // 2. 重新配置主库 + reconfigureAsMaster(newMaster); + + // 3. 更新应用配置 + updateDataSourceConfig(newMaster); + } +} +``` + +**2. 主从切换脚本** +```bash +#!/bin/bash +# master_failover.sh + +MASTER_HOST="192.168.1.100" +SLAVE_HOSTS=("192.168.1.101" "192.168.1.102" "192.168.1.103") +NEW_MASTER="192.168.1.101" + +# 1. 检查主库状态 +if ! mysql -h$MASTER_HOST -uadmin -padmin -e "SELECT 1" >/dev/null 2>&1; then + echo "Master database is down" + + # 2. 选择新主库 + for slave in "${SLAVE_HOSTS[@]}"; do + if mysql -h$slave -uadmin -padmin -e "SHOW REPLICA STATUS\G" | grep -q "Slave_SQL_Running: Yes"; then + NEW_MASTER=$slave + break + fi + done + + echo "New master selected: $NEW_MASTER" + + # 3. 停止从库复制 + mysql -h$NEW_MASTER -uadmin -padmin -e "STOP REPLICA" + + # 4. 重新配置为主库 + mysql -h$NEW_MASTER -uadmin -padmin -e " + RESET MASTER; + RESET SLAVE; + CHANGE REPLICATION SOURCE TO + SOURCE_HOST='', + SOURCE_PORT=0, + SOURCE_USER='', + SOURCE_PASSWORD='', + SOURCE_AUTO_POSITION=0; + " + + # 5. 通知应用 + curl -X POST -H "Content-Type: application/json" \ + -d '{"master_host": "'$NEW_MASTER'"}' \ + http://localhost:8080/api/change-master + + echo "Failover completed" +else + echo "Master is healthy, no failover needed" +fi +``` + +## 5. 实际项目中的解决方案 + +### 大型电商系统主从优化 + +**场景描述**: +- 日订单量:100万+ +- 数据库:MySQL 8.0 +- 架构:1主16从 + +**解决方案**: + +1. **分库分表 + 主从复制** +```java +// 分库分表配置 +@Configuration +public class EcommerceShardingConfig { + + @Bean + public DataSource shardingDataSource() { + // 8个分片,每个分片1主3从 + Map dataSourceMap = new HashMap<>(); + + for (int i = 0; i < 8; i++) { + // 主库 + HikariDataSource master = createDataSource("192.168.1." + (100 + i), 3306, "master"); + dataSourceMap.put("master_" + i, master); + + // 从库 + for (int j = 1; j <= 3; j++) { + HikariDataSource slave = createDataSource("192.168.1." + (200 + i * 3 + j), 3306, "slave"); + dataSourceMap.put("slave_" + i + "_" + j, slave); + } + } + + ShardingRuleConfiguration ruleConfig = new ShardingRuleConfiguration(); + + // 订单表分片 + TableRuleConfiguration orderRule = new TableRuleConfiguration("order", + "order_ds_$->{0..7}.order_$->{order_id % 8}"); + ruleConfig.getTableRuleConfigs().add(orderRule); + + return ShardingDataSourceFactory.createDataSource(dataSourceMap, ruleConfig); + } +} +``` + +2. **多级缓存** +```java +// 多级缓存策略 +@Service +public class OrderService { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private OrderRepository orderRepository; + + @Cacheable(value = "order", key = "#orderId") + public OrderDTO getOrder(Long orderId) { + // 从数据库读取 + Order order = orderRepository.findById(orderId); + + // 缓存到Redis + redisTemplate.opsForValue().set("order:" + orderId, order, 1, TimeUnit.HOURS); + + return convertToDTO(order); + } + + @Cacheable(value = "order_list", key = "#userId + '_' + #page") + public List getUserOrders(Long userId, int page) { + // 从从库读取 + List orders = orderRepository.findByUserId(userId, page); + + // 缓存列表 + redisTemplate.opsForList().leftPushAll("order:list:" + userId, orders); + + return convertToDTOList(orders); + } +} +``` + +### 社交媒体平台优化 + +**场景描述**: +- 日活跃用户:5000万+ +- 数据量:TB级 +- 读多写少 + +**解决方案**: + +1. **读写分离策略** +```java +// 智能读写分离 +public class SmartDataSourceRouter { + + private final DataSource masterDataSource; + private final List slaveDataSources; + private final LoadBalancer loadBalancer; + + public DataSource getDataSource() { + // 根据延迟选择从库 + List healthySlaves = getHealthySlaves(); + + if (healthySlaves.isEmpty()) { + return masterDataSource; + } + + // 根据延迟选择最优从库 + DataSource bestSlave = selectBestSlave(healthySlaves); + + return bestSlave; + } + + private DataSource selectBestSlave(List slaves) { + return slaves.stream() + .min(Comparator.comparingDouble(this::getSlaveDelay)) + .orElse(slaves.get(0)); + } + + private double getSlaveDelay(DataSource dataSource) { + // 获取从库延迟 + try (Connection conn = dataSource.getConnection()) { + long delay = conn.createStatement() + .executeQuery("SHOW REPLICA STATUS") + .getLong("Seconds_Behind_Master"); + return delay; + } catch (Exception e) { + return Double.MAX_VALUE; + } + } +} +``` + +2. **数据预热** +```java +// 数据预热服务 +@Component +public class DataWarmupService { + + @Autowired + private UserService userService; + + @Autowired + private PostService postService; + + @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点 + public void warmupData() { + // 预热热门用户 + List hotUsers = userService.getHotUsers(); + hotUsers.forEach(user -> { + userService.getUserCache(user.getId()); + }); + + // 预热热门帖子 + List hotPosts = postService.getHotPosts(); + hotPosts.forEach(post -> { + postService.getPostCache(post.getId()); + }); + } +} +``` + +### 金融系统主从优化 + +**场景描述**: +- 数据一致性要求高 +- 不能丢失数据 +- 低延迟要求 + +**解决方案**: + +1. **半同步复制 + 事务同步** +```java +// 金融系统数据同步 +@Service +public class FinancialTransactionService { + + @Autowired + private DataSource masterDataSource; + + @Autowired + private DataSource slaveDataSource; + + @Transactional + public void transferMoney(String fromAccount, String toAccount, BigDecimal amount) { + try { + // 1. 执行转账 + transfer(fromAccount, toAccount, amount); + + // 2. 同步到从库 + syncToSlave(fromAccount, toAccount, amount); + + // 3. 记录日志 + logTransaction(fromAccount, toAccount, amount); + + } catch (Exception e) { + // 回滚事务 + throw new FinancialException("Transfer failed", e); + } + } + + private void syncToSlave(String fromAccount, String toAccount, BigDecimal amount) { + try (Connection conn = slaveDataSource.getConnection()) { + // 同步转账记录 + String sql = "INSERT INTO transaction_log (from_account, to_account, amount, status) VALUES (?, ?, ?, 'SUCCESS')"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, fromAccount); + ps.setString(2, toAccount); + ps.setBigDecimal(3, amount); + ps.executeUpdate(); + } + } catch (Exception e) { + throw new RuntimeException("Sync to slave failed", e); + } + } +} +``` + +2. **数据校验** +```java +// 数据一致性校验 +@Service +public class DataConsistencyService { + + @Scheduled(fixedRate = 300000) // 每5分钟校验一次 + public void checkConsistency() { + // 1. 检查数据一致性 + List inconsistencies = findInconsistencies(); + + // 2. 修复不一致数据 + for (Inconsistency issue : inconsistencies) { + fixInconsistency(issue); + } + + // 3. 发送告警 + if (!inconsistencies.isEmpty()) { + notificationService.sendAlert("Data consistency issues found: " + inconsistencies.size()); + } + } + + private List findInconsistencies() { + List result = new ArrayList<>(); + + // 比较主从数据 + String masterQuery = "SELECT COUNT(*) FROM account"; + String slaveQuery = "SELECT COUNT(*) FROM account"; + + // 执行查询并比较 + if (!compareQueryResults(masterQuery, slaveQuery)) { + result.add(new Inconsistency("account", "COUNT_MISMATCH")); + } + + return result; + } + + private boolean compareQueryResults(String masterQuery, String slaveQuery) { + // 比较查询结果 + // 实现细节 + return true; + } +} +``` + +### 总结 + +解决 MySQL 主从延迟需要从多个层面考虑: + +1. **硬件层面**:优化磁盘、网络、CPU性能 +2. **配置层面**:合理配置MySQL参数 +3. **架构层面**:设计合理的读写分离策略 +4. **业务层面**:优化查询,避免大事务 +5. **监控层面**:建立完善的监控体系 +6. **运维层面**:自动化故障恢复 + +在实际项目中,需要根据业务特点选择合适的解决方案,并持续优化和改进。 \ No newline at end of file