生产环境应直接使用 github.com/huandu/skiplist,因其在并发安全、内存布局、层级控制、迭代器语义和 GC 压力等方面远超手写实现,且 compare 函数必须是纯函数以保障一致性。

别自己手写跳表 —— 生产环境直接用 github.com/huandu/skiplist,99% 的场景它比你写的更稳、更快、更安全。
为什么不用自己实现?
跳表看似结构简单,但并发安全、内存布局、层级概率控制、迭代器语义这些细节极易出错。比如:
- 手写
randLevel()时没设上限,导致某节点意外生成 20 层指针,内存暴涨且 cache line 友好性崩坏 - 忘记在
compare函数里做类型检查,a.(int)panic 在高并发下随机炸开 - Range 查询返回的迭代器不是 snapshot,边遍历边删节点会漏数据或 panic,而你自己写的版本大概率没处理这个契约
- 没考虑 GC 压力:每层都存指针,层数多 + 节点密 = 频繁触发 STW
skiplist.New() 的 level 参数到底控制什么?
它不是“最大层数”,而是晋升概率因子 —— 新节点每向上一层的概率是 1 / (2^level)。
-
skiplist.New(4)≈ 平均 4 层,单节点可能 1 层,也可能 12 层(概率 - 设成
16:内存翻倍再翻倍,P99 查找延迟几乎不降,但 heap profile 里*skiplist.Node占比飙升 - 设成
1:退化成链表,Find()从 O(log n) 慢成 O(n),压测时 P99 > 50ms - 实操建议:默认
skiplist.New(4)足够;嵌入式等极端内存受限场景才试3,且必须压测确认延迟
并发安全的关键不在锁,而在 compare 函数
github.com/huandu/skiplist 本身无锁,但所有 goroutine 会并发调用你传入的 compare 函数 —— 它必须是纯函数。
立即学习“go语言免费学习笔记(深入)”;
- ❌ 错误写法:
func(a, b interface{}) int { counter++ ; return a.(int) - b.(int) }→ 竞态 + panic - ✅ 正确写法:
func(a, b interface{}) int { return a.(int) - b.(int) },或更安全地用switch分支做类型判断 - 复合排序(如按时间戳+ID)建议提前 encode 成
[]byte存进 value,compare只做字节比较,避免运行时解析开销 - 绝对不要在
compare里调time.Now()、rand.Intn()或读写全局 map
什么时候真该上跳表?
不是“有序 + 并发”就自动选跳表。先看替代方案是否更省心:
- QPS 不高、写少读多、key 是 int:用
sync.RWMutex + []int + sort.SearchInts,代码 20 行,无 GC 压力,P99 - 需要范围查询(
Range(100, 200))、频繁插入删除、或压测发现写操作 P99 > 10ms —— 这时跳表才值得投入 - Redis ZSet 底层用跳表,不是因为炫技,是因为它要同时扛住百万级
ZRANGEBYSCORE和ZADD,且不能卡主进程
跳表真正的复杂点从来不在“怎么建索引”,而在于“怎么让所有 goroutine 对中间态达成一致”——这个一致性,藏在 compare 的纯度里、在 level 的取舍里、也在你是否愿意跳过它去用现成轮子的判断里。


















