Redis实现排行榜与ZSet跳表原理
如何使用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倍原数据)
为什么用跳表不用红黑树:
- 范围查询高效:跳表找到范围起点后,沿链表向后遍历即可;红黑树范围查询需中序遍历
- 实现简单:跳表代码约200行,红黑树约1000行(平衡操作复杂)
- 无锁并发友好:跳表更容易实现无锁并发(如Java ConcurrentSkipListMap)
- 内存效率:跳表平均每个节点1.33个指针,红黑树每个节点3个指针(左/右/父)
同分按时间排序:
// 技巧:Score = 实际分数 + (1 - 时间戳/最大时间戳)
// 分数相同时间早的排在前面(Score更大)
local maxTimestamp = 9999999999999
local compositeScore = score + (maxTimestamp - currentTimestamp) / maxTimestamp
redis.call('ZADD', KEYS[1], compositeScore, member)
或者使用double精度,将时间戳作为分数的小数部分。