发布时间:2026/7/31 11:55:22
pythonimport randomimport mathclass BPlusTreeNode: """B+树节点,模拟磁盘页""" def __init__(self, is_leaf=True): self.is_leaf = is_leaf self.keys = [] # 键列表 self.children = [] # 子节点(内部节点)或数据(叶子节点) self.next_leaf = None # 叶子节点链表指针class BPlusTree: """简化版B+树,演示范围查询""" def __init__(self, order=4): self.order = order # 节点最大键数 self.root = BPlusTreeNode(is_leaf=True) def insert(self, key, value): """插入键值对,模拟磁盘写入""" # 实际中会触发磁盘页分裂,这里简化 node = self._find_leaf(key) node.keys.append(key) node.children.append(value) node.keys.sort() # 模拟节点分裂(当超过order时) if len(node.keys) > self.order: self._split_node(node) def _find_leaf(self, key): """查找叶子节点(模拟磁盘读取)""" node = self.root while not node.is_leaf: # 二分查找确定分支(实际磁盘页会缓存) idx = len(node.keys) for i, k in enumerate(node.keys): if key < k: idx = i break node = node.children[idx] return node def range_query(self, low, high): """范围查询:利用叶子链表顺序扫描""" leaf = self._find_leaf(low) result = [] while leaf: for k, v in zip(leaf.keys, leaf.children): if low <= k <= high: result.append((k, v)) elif k > high: return result leaf = leaf.next_leaf return result# 模拟B+树范围查询bpt = BPlusTree(order=4)for i in range(1, 21): bpt.insert(i, f"value_{i}")print("B+树范围查询 [5,15]:")result = bpt.range_query(5, 15)print(result) # 输出连续有序的结果### 2.2 模拟跳表的插入与查询(内存友好)pythonimport randomclass SkipNode: """跳表节点""" def __init__(self, key, value, level): self.key = key self.value = value self.forward = [None] * (level + 1) # 各层前进指针class SkipList: """实现有序集合的跳表""" def __init__(self, max_level=16): self.max_level = max_level self.head = SkipNode(-float('inf'), None, max_level) self.level = 0 # 当前最高层 def _random_level(self): """随机生成层数""" level = 0 while random.random() < 0.5 and level < self.max_level: level += 1 return level def insert(self, key, value): """插入键值对(内存操作)""" update = [None] * (self.max_level + 1) current = self.head # 从最高层向下查找插入位置 for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < key: current = current.forward[i] update[i] = current # 确定新节点层数 new_level = self._random_level() if new_level > self.level: for i in range(self.level + 1, new_level + 1): update[i] = self.head self.level = new_level new_node = SkipNode(key, value, new_level) for i in range(new_level + 1): new_node.forward[i] = update[i].forward[i] update[i].forward[i] = new_node def range_query(self, low, high): """范围查询""" current = self.head # 定位到low附近 for i in range(self.level, -1, -1): while current.forward[i] and current.forward[i].key < low: current = current.forward[i] current = current.forward[0] # 移动到第一层 result = [] while current and current.key <= high: result.append((current.key, current.value)) current = current.forward[0] return result# 模拟跳表范围查询sl = SkipList()for i in range(1, 21): sl.insert(i, f"val_{i}")print("\n跳表范围查询 [5,15]:")result = sl.range_query(5, 15)print(result)## 3. 为什么InnoDB不用跳表?### 3.1 磁盘I/O优化需求B+树每个节点大小固定(通常16KB),与磁盘页对齐。一次读取可获取一个节点内的所有键,极大减少I/O次数。而跳表节点分散存储,每个节点只存一个键,范围查询需要多次随机读取。性能对比(模拟100万条记录):- B+树:树高约3-4层,范围查询仅需读取少量页- 跳表:节点分散,范围查询需大量随机I/O### 3.2 范围查询效率B+树的叶子节点通过双向链表连接,范围扫描只需顺序读取相邻叶子节点,磁盘预读效果好。跳表虽然也能范围遍历,但节点内存地址不连续,无法利用磁盘预读。### 3.3 页分裂与合并B+树插入时页分裂会影响相邻页,但MySQL的缓冲池(Buffer Pool)能缓存热点页。跳表的节点动态分配,会导致频繁的内存碎片,在磁盘场景下加剧随机I/O。## 4. 为什么Redis不用B+树?### 4.1 纯内存场景的取舍Redis所有数据驻留内存,无需考虑磁盘页对齐。B+树的节点大小固定优势消失,反而增加了内存碎片。跳表节点按需分配,内存利用率更高。### 4.2 简单性与并发性能跳表实现比B+树简单得多,不需要复杂的页分裂/合并逻辑。Redis是单线程模型,跳表的无锁设计(通过随机化避免复杂平衡)更适合单线程环境。### 4.3 有序集合的特殊需求Redis的ZSET需要支持:- O(log n)的插入、删除、更新- 范围查询(ZRANGE)- 排名查询(ZRANK)跳表天然支持这些操作,且实现代码仅约300行。B+树实现复杂度高,且需要维护平衡,在内存中优势不大。性能对比(Redis源码分析):c// Redis跳表插入核心代码(简化)zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; unsigned int rank[ZSKIPLIST_MAXLEVEL]; // 从最高层向下查找,时间复杂度O(log n) x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { // 比较score和ele,保证稳定性 while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { rank[i] += x->level[i].span; x = x->level[i].forward; } update[i] = x; } // 插入新节点,调整各层指针 // ...}## 5. 实战对比:大数据量下的性能差异### 5.1 模拟测试代码pythonimport timeimport randomdef benchmark_inserts(ds, count=10000): """测试插入性能""" start = time.time() for i in range(count): ds.insert(random.randint(1, 100000), f"data_{i}") return time.time() - startdef benchmark_range_query(ds, count=100): """测试范围查询性能""" start = time.time() for _ in range(count): low = random.randint(1, 50000) high = low + 1000 ds.range_query(low, high) return time.time() - start# 对比测试(注意:跳表在内存中,B+树模拟磁盘)bpt = BPlusTree(order=16)sl = SkipList()bpt_time = benchmark_inserts(bpt, 5000)sl_time = benchmark_inserts(sl, 5000)print(f"B+树插入5000条耗时: {bpt_time:.4f}s")print(f"跳表插入5000条耗时: {sl_time:.4f}s")# 实际中B+树因磁盘I/O更慢,但内存中跳表更快### 5.2 结果分析在纯内存环境下,跳表插入更快(无需处理页分裂)。但B+树在磁盘场景下,通过缓冲池和预读机制,范围查询性能远超跳表。## 总结| 特性 | B+树(InnoDB) | 跳表(Redis) ||------|---------------|--------------|| 适用场景 | 磁盘存储 | 内存存储 || 节点大小 | 固定(对齐磁盘页) | 动态分配 || 范围查询 | 顺序扫描叶子链表,磁盘预读 | 逐节点遍历,无预读 || 实现复杂度 | 高(页分裂/合并) | 低(概率平衡) || 插入性能 | 受页分裂影响 | O(log n)稳定 || 内存利用率 | 低(节点有冗余) | 高(按需分配) |核心结论:InnoDB选择B+树是因为它完美适配磁盘特性——节点对齐页、顺序扫描友好、树高稳定。Redis选择跳表则是因为内存场景不需要磁盘优化,且跳表实现简单、并发性好,完美契合单线程模型的需求。两者都是各自领域的最优解,而不是技术上的优劣之分。