禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Go基础

  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 聊聊我说如何用go语言实现redis列表操作
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
      • 写在文章开头
      • 关于跳表的知识导读
      • 详解跳表的实现思路
        • 定义基础数据结构
        • 实现初始化方法
        • 跳表插入操作
        • 跳表查询操作
        • 跳表删除操作
      • 小结
      • 参考
    • Go 语言版 Redis 有序集合指令复刻探索
    • mini-redis复刻Redis的INCR指令
    • 聊聊我基于dict优化mini-redis数据性能这件事
  • Go语言
  • mini-redis实战
sharkchili
2024-12-12
目录

Go 语言下的 Redis 跳表设计与实现

# 写在文章开头

在现代高性能数据库和缓存系统中,跳表(Skip List)作为一种高效的有序数据结构,被广泛应用于快速查找、插入和删除操作。Redis 是一个开源的键值对存储系统,它支持多种数据类型,并以其出色的性能而闻名。其中,Redis 使用了跳表来实现有序集合(Sorted Set),以保证其高效的数据处理能力。

本文将详细介绍如何使用 Go 语言从零开始实现一个类似于 Redis 的跳表。我们将探讨跳表的基本原理、设计思路以及具体的实现方法。通过本篇文章的学习,你不仅能够了解跳表的工作机制,还能够在实际项目中应用这一强大的数据结构。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。

同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 关于跳表的知识导读

# 详解跳表的实现思路

在正式阅读这篇文章之前,建议读者阅读笔者这篇文章熟悉一下跳表的实现细节,以保证更好的理解笔者复刻思路。

高效索引的秘密:redis跳表设计与实现:https://mp.weixin.qq.com/s/7iMwsunZC_UyBBrLTvx8OA (opens new window)

# 定义基础数据结构

redis中跳表通过score标识元素的大小,通过redis obj维护节点的信息,与此同时为了保证查询的高效,它会为每个节点维护一份随机高度的索引记录当前节点的某个前驱节点:

对应我们给出节点的代码实现:

/*
*
跳表节点的定义
*/
type zskiplistNode struct {
	//记录元素的redis指针
	obj *robj
	//记录当前元素的数值,代表当前元素的优先级
	score float64
	//指向当前元素的前驱节点,即小于当前节点的元素
	backward *zskiplistNode
	//用一个zskiplistLevel数组维护本届点各层索引信息
	level    []zskiplistLevel
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

zskiplistLevel的代码实现比较简单,通过forward 记录本层索引的前驱节点,并用span维护当前节点需要跨几步才能走到该前驱节点:

type zskiplistLevel struct {
	//记录本层索引的前驱节点的指针
	forward *zskiplistNode
	//标识节点的本层索引需要跨几步才能到达该节点
	span    int64
}
1
2
3
4
5
6

通过上述概念构成无数个节点即称为跳表,如下图所示,各个节点都用一个level数组记录本层索引到前驱节点的地址和跨度,而跳表也用一个header和tail指针维护跳表的头尾节点:

对应的跳表结构体的代码如下所示:

type zskiplist struct {
	//指向跳表的头节点
	header *zskiplistNode
	//指向跳表的尾节点
	tail *zskiplistNode
	//维护跳表的长度
	length int64
	//维护跳表当前索引的最高高度
	level int
}
1
2
3
4
5
6
7
8
9
10

# 实现初始化方法

对应的我们也给出跳表的初始化代码,大体逻辑是初始化跳表之后,初始化一个全空的索引和维护跳表的各种初始化信息,对应的笔者也对此代码做了详尽的注释,读者可自行参阅:

func zslCreate() *zskiplist {
	var j int
	//初始化跳表结构体
	zsl := new(zskiplist)
	//索引默认高度为1
	zsl.level = 1
	//跳表元素初始化为0
	zsl.length = 0
	//初始化一个头节点socre为0,元素为空
	zsl.header = zslCreateNode(ZSKIPLIST_MAXLEVEL, 0, nil)

	/**
	基于跳表最大高度32初始化头节点的索引,
	使得前驱指针指向null 跨度也设置为0
	*/
	for j = 0; j < ZSKIPLIST_MAXLEVEL; j++ {
		zsl.header.level[j].forward = nil
		zsl.header.level[j].span = 0
	}
	//头节点的前驱节点指向null,代表头节点之前没有任何元素
	zsl.header.backward = nil
	//初始化尾节点
	zsl.tail = nil
	return zsl
}
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

# 跳表插入操作

插入新节点时,本质上就是通过各层索引找到小于插入节点x的score的最大值,并记录到update数组中,同时将头节点跨到update数组元素的跨度值记录到rank数组中,如下图所示,假如我们插入节点1.5,那么对应各层索引的在update和rank两个数组中维护的信息是:

  1. level2级中update记录header节点,所以跨度为0。
  2. level1级中update记录的是节点1,跨度为1。

然后基于此信息将x插入:

对应的代码和上述图解逻辑一致,对应的实现细节笔者都做好了标注:

func zslInsert(zsl *zskiplist, score float64, obj *robj) *zskiplistNode {
	//创建一个update数组,记录插入节点每层索引中小于该score的最大值
	update := make([]*zskiplistNode, ZSKIPLIST_MAXLEVEL)
	//记录各层索引走到小于score最大节点的跨区
	rank := make([]int64, ZSKIPLIST_MAXLEVEL)
	//x指向跳表走节点
	x := zsl.header
	var i int
	//从跳表当前最高层索引开始,查找每层小于当前score的节点的最大值节点
	for i = zsl.level - 1; i >= 0; i-- {
		//如果当前索引是最高层索引,那么rank从0开始算
		if i == zsl.level-1 {
			rank[i] = 0
		} else { //反之本层索引直接从上一层的跨度开始往后查找
			rank[i] = rank[i+1]
		}
		/**
		如果前驱节点不为空,且符合以下条件,则指针前移:
		1. 节点小于当前插入节点的score
		2. 节点score一致,且元素值小于或者等于当前score
		*/
		if x.level[i].forward != nil &&
			(x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() < obj.String())) {
			//记录本层索引前移跨度
			rank[i] += x.level[i].span
			//索引指针先前移动
			x = x.level[i].forward

		}
		//记录本层小于当前score的最大节点
		update[i] = x
	}
	//随机生成新插入节点的索引高度
	level := zslRandomLevel()
	/**
	如果大于当前索引高度,则进行初始化,将这些高层索引的update数组都指向header节点,跨度设置为跳表中的元素数
	意为这些高层索引小于插入节点的最大值就是header
	*/
	if level > zsl.level {
		for i := zsl.level; i < level; i++ {
			rank[i] = 0
			update[i] = zsl.header
			update[i].level[i].span = zsl.length
		}
		//更新一下跳表索引的高度
		zsl.level = level
	}
	//基于入参生成一个节点
	x = zslCreateNode(level, score, obj)
	//从底层到当前最高层索引处理节点关系
	for i = 0; i < level; i++ {
		//将小于当前节点的最大节点的forward指向插入节点x,同时x指向这个节点的前向节点
		x.level[i].forward = update[i].level[i].forward
		update[i].level[i].forward = x
		//维护x和update所指向节点之间的跨度信息
		x.level[i].span = update[i].level[i].span - (rank[0] - rank[i])
		update[i].level[i].span = rank[0] - rank[i] + 1
	}
	/**
	考虑到当前插入节点生成的level小于当前跳表最高level的情况
	该逻辑会将这些区间的update索引中的元素到其前方节点的跨度+1,即代表这些层级索引虽然没有指向x节点,
	但因为x节点插入的缘故跨度要加1
	*/
	for i = level; i < zsl.level; i++ {
		update[i].level[i].span++
	}
	//如果1级索引是header,则x后继节点不指向该节点,反之指向
	if update[0] == zsl.header {
		x.backward = nil
	} else {
		x.backward = update[0]
	}
	//如果x前向节点不为空,则让前向节点指向x
	if x.level[0].forward != nil {
		x.level[0].forward.backward = x
	} else {//反之说明x是尾节点,tail指针指向它
		zsl.tail = x
	}
	//维护跳表长度信息
	zsl.length++
	return x
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

# 跳表查询操作

有了插入操作的基础后,查询操作实现也比较容易了,即从头节点的最高索引开始不断向前找,如果没有则往下一级索引前向找,找到后返回经过的跨度即可。

如下图,我们希望查找元素2,直接从头节点的2级索引开始看,就是元素2比对一致,返回跨度2,即跨2步就能到达:

对应代码如下,和笔者说明一致,这里笔者也做了详尽的标注提供参考:

func zslGetRank(zsl *zskiplist, score float64, obj *robj) int64 {
	var rank int64
	//从索引最高节点开始进行查找
	x := zsl.header
	for i := zsl.level - 1; i >= 0; i-- {
		//如果前向节点不为空且score小于查找节点,或者score相等,但是元素字符序比值小于或者等于则前移,同时用rank记录跨度
		for x.level[i].forward != nil &&
			(x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() <= obj.String())) {
			rank += x.level[i].span
			x = x.level[i].forward
		}
		//上述循环结束,比对一直,则返回经过的跨度
		if x.obj != nil && x.obj.String() == obj.String() {
			return rank
		}
	}
	return 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 跳表删除操作

删除操作本质上也是找到要删除节点索引的前后节点,然后将这些节点关联,并修改其之间跨度,如下图我们要删除1.5节点,对应各层查找结果为:

  1. 3级索引找到头节点,因为前方不是1.5的节点索引,直接跨度减1即。
  2. 2级索引找到头节点,前方就是1.5的索引,删除掉后跨度改为header索引到1.5+1.5到前向节点跨度减去1,这里的减去1代表删除了节点1.5的跨步。
  3. 1级索引同2级索引,不多做赘述。

对应的代码示例如下,整体逻辑和笔者描述基本一致,先通过update找到删除节点x的前一个元素,然后调用zslDeleteNode进行删除:

func zslDelete(zsl *zskiplist, score float64, obj *robj) int64 {
	update := make([]*zskiplistNode, ZSKIPLIST_MAXLEVEL)
	//找到每层索引要删除节点的前一个节点
	x := zsl.header
	for i := zsl.level - 1; i >= 0; i-- {
		for x.level[i].forward != nil &&
			(x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() < obj.String())) {
			x = x.level[i].forward
		}
		update[i] = x
	}
	//查看1级索引前面是否就是要删除的节点,如果是则直接调用zslDeleteNode删除节点,并断掉前后节点关系
	x = x.level[0].forward
	if x != nil && x.obj.String() == obj.String() {
		zslDeleteNode(zsl, x, update)
		return 1
	}
	return 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

对应zslDeleteNode细节就如笔者上图所讲解的步骤,读者可参考注释进行阅读:

func zslDeleteNode(zsl *zskiplist, x *zskiplistNode, update []*zskiplistNode) {

	var i int
	for i = 0; i < zsl.level; i++ {
		/*
		如果索引前方就是删除节点,当前节点span为:
		当前节点到x +x到x前向节点 -1
		 */
		if update[i].level[i].forward == x {
			update[i].level[i].span += x.level[i].span - 1
			update[i].level[i].forward = x.level[i].forward
		} else {
			//反之说明该节点前方不是x的索引,直接减去x的跨步1即
			update[i].level[i].span -= 1
		}
	}
	//维护删除后的节点前后关系
	if x.level[0].forward != nil {
		x.level[0].forward.backward = x.backward
	} else {
		zsl.tail = x.backward
	}
	//将全空层的索引删除
	for zsl.level > 1 && zsl.header.level[zsl.level-1].forward == nil {
		zsl.level--
	}
	//维护跳表节点信息
	zsl.length--


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

# 小结

通过本文的详细讲解,我们从零开始使用 Go 语言实现了一个类似于 Redis 的跳表。我们首先介绍了跳表的基本原理和设计思路,然后逐步实现了跳表的各种核心操作,包括插入、查找和删除。最后,我们对跳表的性能进行了分析,并探讨了其在 Redis 有序集合和其他场景中的应用。

我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。

同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考

《redis设计与实现》

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
动手复刻redis之go语言下的字典的设计与落地
Go 语言版 Redis 有序集合指令复刻探索

← 动手复刻redis之go语言下的字典的设计与落地 Go 语言版 Redis 有序集合指令复刻探索→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×