CodeWalk

Redis实现排行榜与ZSet跳表原理

作者:孤独的心 · 2026-05-30 12:55

如何使用Redis的ZSet(有序集合)实现实时排行榜?ZSet底层为什么使用跳表(SkipList)而不是红黑树?跳表的查找/插入/删除的时间复杂度是多少?如何实现同分按时间排序?

回答

孤独的心

ZSet实现排行榜

// 1. 添加玩家分数
redisTemplate.opsForZSet()
    .add("game:rank", "player_1", 1500);

// 2. 获取Top10(按分数降序)
Set<String> top10 = redisTemplate.opsForZSet()
    .reverseRange("game:rank", 0, 9);

// 3. 获取玩家排名
Long rank = redisTemplate.opsForZSet()
    .reverseRank("game:rank", "player_1");

// 4. 获取玩家分数
Double score = redisTemplate.opsForZSet()
    .score("game:rank", "player_1");

// 5. 分数增加
redisTemplate.opsForZSet()
    .incrementScore("game:rank", "player_1", 100);

跳表(SkipList)原理

  • 多层链表结构,上层是下层的'快速通道'
  • 每层链表有序,节点随机决定层数
  • 查找时从最高层开始,逐层向下,类似二分搜索
  • 时间复杂度:查找O(logN)、插入O(logN)、删除O(logN)
  • 空间复杂度:O(N)(期望1.33倍原数据)

为什么用跳表不用红黑树

  1. 范围查询高效:跳表找到范围起点后,沿链表向后遍历即可;红黑树范围查询需中序遍历
  2. 实现简单:跳表代码约200行,红黑树约1000行(平衡操作复杂)
  3. 无锁并发友好:跳表更容易实现无锁并发(如Java ConcurrentSkipListMap)
  4. 内存效率:跳表平均每个节点1.33个指针,红黑树每个节点3个指针(左/右/父)

同分按时间排序

// 技巧:Score = 实际分数 + (1 - 时间戳/最大时间戳)
// 分数相同时间早的排在前面(Score更大)
local maxTimestamp = 9999999999999
local compositeScore = score + (maxTimestamp - currentTimestamp) / maxTimestamp
redis.call('ZADD', KEYS[1], compositeScore, member)

或者使用double精度,将时间戳作为分数的小数部分。