尼恩说在前面
如何 使用ZSET 排序统计 ? 亿级用户的排行榜,如何设计?
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文目录
- 尼恩说在前面
- 首先回顾一下:redis 四大统计(基数统计,二值统计,排序统计、聚合统计)的原理 和应用场景
- 第1大统计:基数统计
- 第2大统计:二值统计
- 第3大统计:排序统计
- 第4大统计:聚合统计
- 入门篇:Redis 的有序集合(Sorted Set)
- 1. ZADD
- 2. ZSCORE
- 3. ZINCRBY
- 4. ZRANK / ZREVRANK
- 5. ZRANGE / ZREVRANGE
- 6. ZCOUNT
- 7. ZREM
- 8. ZCARD
- 9. ZREMRANGEBYSCORE
- 社交点赞 的设计与实现
- 社交点赞 数据结构设计
- 社交点赞的核心操作
- 社交点赞涉及的Redis 命令
- 社交点赞的 redis 命令实现
- 游戏玩家排行榜案例
- 游戏玩家排行榜 数据模型
- 游戏玩家排行榜 Redis 操作命令
- 游戏玩家排行榜Java 代码示例
- 超高并发下 Redis 热key 分治原理
- 什么是Redis 热点key 拆分?
- 亿级用户排行榜,如何设计范围分片, 路由到 不同的set
- 理解范围分片的基本概念
-确定分片键和范围边界
-设计路由算法
- 动态范围调整的 数据迁移策略:
- 动态范围调整的数据一致性保证:
- 排行榜 Redis Cluster 集群持久化方式
- 方式1:RDB 持久化(Redis Database)
- 方式2:AOF 持久化(Append - Only File)
- Redis Cluster 集群恢复过程
- 结合亿级用户排行榜选择合适的持久化策略
- 为什么 Reids 的 ZRANK 那么快?
- 尼恩架构团队塔尖的redis 面试题
- 说在最后:有问题找老架构取经
首先回顾一下:redis 四大统计(基数统计,二值统计,排序统计、聚合统计)的原理 和应用场景
第1大统计:基数统计
第2大统计:二值统计
第3大统计:排序统计
可以 使用ZSET对文章的点赞数排序并分页展示 对评论根据时间进行排序 亿级用户的排行榜,如何设计?
第4大统计:聚合统计
入门篇:Redis 的有序集合(Sorted Set)
1. ZADD
ZADD key score member [score member ...]
ZADD myzset 1 "one"
ZADD myzset 1 "one" 2 "two" 3 "three"
2. ZSCORE
ZSCORE key member
ZSCORE myzset "two"
3. ZINCRBY
ZINCRBY key increment member
ZINCRBY myzset 2 "two"
4. ZRANK / ZREVRANK
ZRANK key member
ZREVRANK key member
ZRANK myzset "three" # 返回 "three" 升序排名
ZREVRANK myzset "three" # 返回 "three" 降序排名
5. ZRANGE / ZREVRANGE
ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]
ZRANGE
命令用于返回有序集合中指定区间内的成员列表,区间是有序集合的索引范围,可以是正数或负数,正数表示从大到小的顺序,负数表示从小到大的顺序。ZRANGE key start stop [WITHSCORES]
key
:有序集合的键名。start
:区间的起始索引。stop
:区间的结束索引。WITHSCORES
:(可选)如果指定这个选项,命令将返回成员及其分数。
如果没有指定 WITHSCORES
,则返回一个列表,包含指定区间内的成员。如果指定了 WITHSCORES
,则返回一个列表,包含指定区间内的成员及其分数,成员和分数是成对出现的。
myzset
的有序集合,并且已经添加了一些成员和分数,如下所示:ZADD myzset 1 "one" 2 "two" 3 "three" 4 "four" 5 "five"
ZRANGE myzset 0 2
"one"
、"two"
和 "three"
。WITHSCORES
选项:ZRANGE myzset 0 2 WITHSCORES
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
6. ZCOUNT
ZCOUNT key min max
ZCOUNT myzset 0 3
7. ZREM
ZREM key member [member ...]
ZREM myzset "one"
8. ZCARD
ZCARD key
ZCARD myzset
9. ZREMRANGEBYSCORE
ZREMRANGEBYSCORE key min max
ZREMRANGEBYSCORE myzset 0 2
社交点赞 的设计与实现
社交点赞 数据结构设计
有序集合(zset):用于存储所有内容(如文章)的点赞总数,并按照点赞数进行排序。 key
:likes:all,表示所有文章的点赞数的一个排序集合。member
:文章的唯一标识符,比如post:<content_id>
。score
:点赞的次数,用于 点赞的排序。哈希(hash):用于存储每个用户点赞过的所有内容。 key
:user:<user_id>:likes
,其中<user_id>
是用户的唯一标识符。field
:内容的唯一标识符,比如<content_id>
。value
:点赞的时间戳,用于记录用户点赞该内容的具体时间。
社交点赞的核心操作
点赞:当用户对内容点赞时,更新有序集合和哈希表。 取消点赞:当用户取消点赞时,从有序集合和哈希表中移除相应的记录。 获取点赞数:获取特定内容的点赞数。 获取用户点赞的内容:获取一个用户点赞过的所有内容。 获取热门内容:根据点赞数获取最热门或最新的内容列表。
社交点赞涉及的Redis 命令
ZADD:向有序集合添加成员。 ZREM:从有序集合移除成员。 HSET:向哈希表添加字段。 HDEL:从哈希表删除字段。 ZSCORE:获取有序集合中成员的分数。 ZCARD:获取有序集合的成员数。 ZRANGE:获取有序集合中指定区间的成员。
社交点赞的 redis 命令实现
# 用户点赞
ZADD likes:all 1 post:<content_id>
HSET user:<user_id>:likes <content_id> <timestamp>
# 用户取消点赞
ZADD likes:all -1 post:<content_id>
HDEL user:<user_id>:likes <content_id>
# 获取特定内容的点赞数
ZSCORE likes:all post:<content_id>
# 获取用户点赞过的所有内容
HGETALL user:<user_id>:likes
# 获取热门内容列表 ToP 10
ZRANGE likes:all 0 9 WITHSCORES
# 获取文章的排名
ZRANK likes:all post:<content_id>
游戏玩家排行榜案例
游戏玩家排行榜 数据模型
玩家标识 每个玩家都有一个唯一的标识,如玩家 ID。这个标识将作为 Redis 有序集合中的元素。 排名分数计算因素
得分:玩家在游戏中的得分是一个重要的排名因素。例如,在射击游戏中,杀敌数、完成任务获得的奖励分数等都可以计入总分。 游戏完成时间:对于一些有时间限制的游戏关卡或模式,完成时间越短排名越靠前。可以根据一定的算法将完成时间转换为分数的一部分,比如,用时越短分数越高。
游戏玩家排行榜 Redis 操作命令
ZADD "leaderboard" 1000 "player123"
获取 top N 玩家 使用 ZREVRANGE 命令可以获取排名靠前的 N 个玩家信息。 例如,要获取排行榜前 10 名玩家,可以使用 ZREVRANGE "leaderboard" 0 9
命令。这个命令将返回排名前 10 的玩家 ID。获取玩家排名 使用 ZRANK 或 ZREVRANK 命令可以获取某个玩家在排行榜中的位置。 如果要获取 “player123” 的排名,可以使用 ZRANK "leaderboard" "player123"
(正序排名)或ZREVRANK "leaderboard" "player123"
(逆序排名,即从高到低的排名)。
游戏玩家排行榜Java 代码示例
import redis.clients.jedis.Jedis;
import java.util.Map;
public class GameLeaderboardSystem {
private Jedis jedis;
public GameLeaderboardSystem() {
jedis = new Jedis("localhost", 6379);
}
// 根据得分和时间计算玩家的综合排名分数(这里只是示例算法)
private double calculateRankScore(int score, long completionTime) {
// 假设得分权重为0.7,时间权重为0.3,时间越短分数越高,可以根据实际情况调整
double timeScore = 1000.0 / completionTime;
return score * 0.7 + timeScore * 0.3;
}
public void updatePlayerRank(String playerId, int score, long completionTime) {
double rankScore = calculateRankScore(score, completionTime);
jedis.zadd("leaderboard", rankScore, playerId);
}
public void showTopPlayers(int topN) {
System.out.println("Top " + topN + " players in the leaderboard:");
jedis.zrevrange("leaderboard", 0, topN - 1).forEach(System.out::println);
}
public long getPlayerRank(String playerId) {
Long rank = jedis.zrevrank("leaderboard", playerId);
return rank == null? -1 : rank;
}
public static void main(String[] args) {
GameLeaderboardSystem system = new GameLeaderboardSystem();
// 模拟玩家数据更新
system.updatePlayerRank("player1", 100, 120000);
system.updatePlayerRank("player2", 120, 100000);
system.updatePlayerRank("player1", 150, 110000);
// 展示排行榜前5名玩家
system.showTopPlayers(5);
// 获取player1的排名
System.out.println("Player1's rank: " + system.getPlayerRank("player1"));
}
}
GameLeaderboardSystem
类负责管理游戏玩家排行榜。在构造函数中初始化Jedis
连接到本地 Redis 服务器。calculateRankScore
方法根据玩家得分和游戏完成时间计算综合排名分数。这里只是一个简单的示例算法,可以根据游戏实际情况进行更复杂的设计。updatePlayerRank
方法在玩家完成游戏后,计算排名分数并使用ZADD
命令更新玩家在排行榜中的位置。showTopPlayers
方法使用ZREVRANGE
命令获取并展示排行榜上前N
名玩家。getPlayerRank
方法使用ZREVRANK
命令获取指定玩家的排名。在main
方法中,模拟了玩家数据更新、排行榜展示和获取玩家排名的操作,展示了整个排行榜系统的基本功能实现。
超高并发下 Redis 热key 分治原理
什么是Redis 热点key 拆分?
亿级用户排行榜,如何设计范围分片, 路由到 不同的set
理解范围分片的基本概念
范围分片是一种数据分片策略,它根据数据的某个特定范围将数据划分到不同的存储单元(在这个例子中是不同的 set)。 对于亿级用户排行榜来说,这个范围可以基于用户的某个属性,积分属性。 类似的,其他场景可以是活跃度等。 假设以用户积分来进行范围分片,首先需要确定积分的范围。 例如,可以将积分范围划分为多个区间,如 0 - 1000,1001 - 2000,2001 - 3000 ,2001 - 无穷大 等。
确定分片键和范围边界
选择分片键:分片键是用于决定数据存储位置的关键属性。 在用户排行榜场景中,如前面提到的积分、用户等级等属性都可以作为分片键。 以积分作为分片键为例,它能很好地反映用户在排行榜中的位置。 确定范围边界: 需要考虑数据的分布情况。 如果积分分布比较均匀,可以采用等距的范围边界划分。 比如,整个用户积分范围是 0 - 100000,要划分成 10 个 set,可以将每个 set 的积分范围设为 10000(0 - 9999,10000 - 19999,...,90000 - 99999)。 如果数据分布不均匀,可能需要根据实际数据的统计情况来划分。 例如,大部分用户积分集中在 0 - 5000,那么可以将 0 - 5000 这个区间划分得更细,比如 0 - 1000,1001 - 2000,2001 - 3000,3001 - 4000,4001 - 5000, 而对于 5001 - 100000 可以划分得粗一些,如 5001 - 20000,20001 - 40000,40001 - 60000,60001 - 80000,80001 - 100000。
设计路由算法
简单映射算法: 当确定了分片键和范围边界后,路由算法可以是一个简单的映射函数。 例如,以积分范围分片为例,假设有一个函数 getSetIDByScore(score)
,如果积分范围是 0 - 999 对应 set1,1000 - 1999 对应 set2 等,那么函数可以这样实现:
public class SetIDRouter {
public static int getSetIDByScore(int score) {
if (score >= 0 && score <= 999) {
return 1;
} else if (score >= 1000 && score <= 1999) {
return 2;
}
// 这里可以根据实际需求继续添加更多的条件判断来确定返回的Set ID
return -1; // 如果没有匹配的条件,可以返回一个默认值,这里返回 -1 作为示例
}
public static void main(String[] args) {
int score = 1500;
int setID = getSetIDByScore(score);
System.out.println("对于积分 " + score + ",对应的Set ID为:" + setID);
}
}
定义了一个名为 getSetIDByScore
的静态方法,它接受一个整数类型的score
参数,根据score
的值所在的范围返回对应的Set ID
。在 main
方法中,示例了如何调用getSetIDByScore
方法并输出结果,你可以根据实际情况修改main
方法中的score
值或者在其他地方调用getSetIDByScore
方法来满足具体的业务需求。同时,如果实际情况中有更多的积分范围判断,需要在getSetIDByScore
方法中继续添加相应的if-else
条件语句。
动态范围调整的 数据迁移策略:
先在新的 set 中插入数据, 动态的切换 路由算法,把老的路由算法,切换为新的路由算法。 可以使用 nacos/zk的发布订阅机制,进行算法的切换 然后逐步删除旧 set 中的数据。
动态范围调整的数据一致性保证:
排行榜 Redis Cluster 集群持久化方式
RDB 持久化(Redis Database) AOF 持久化(Append - Only File)
方式1:RDB 持久化(Redis Database)
工作原理:RDB 持久化是通过对 Redis 中的数据进行周期性的快照来实现的。它会在满足一定条件时,将内存中的数据集快照写入磁盘。这些条件包括:用户通过配置文件指定的时间间隔(例如, save 900 1
表示 900 秒内如果有 1 个键被修改就进行快照),或者执行SAVE
或BGSAVE
命令。SAVE
命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕;而BGSAVE
命令会在后台异步进行快照操作,不会阻塞服务器的正常读写操作。RDB 文件格式:RDB 文件是一个二进制文件,它以非常紧凑的格式存储了 Redis 数据。文件头包含了一些关于文件格式的版本信息和其他元数据。然后,数据部分按照键值对的形式存储,不同的数据类型有不同的存储格式。例如,字符串类型会存储其长度和内容,列表类型会存储列表长度和每个元素等。 优势:RDB 文件非常紧凑,适合用于备份和灾难恢复场景。它能够快速地将数据恢复到某个时间点的状态,并且在进行数据传输时,因为文件小,传输速度也比较快。例如,将 RDB 文件传输到另一个数据中心进行恢复。 劣势:由于它是周期性地进行快照,如果 Redis 在两次快照之间发生故障,那么这期间的数据修改将会丢失。
方式2:AOF 持久化(Append - Only File)
工作原理:AOF 持久化是通过记录服务器所执行的写命令来实现的。当 Redis 执行一个写命令(如 SET
、LPUSH
等)时,这个命令会被追加到 AOF 文件的末尾。AOF 文件的内容是纯文本格式,可以通过文本编辑器查看。例如,执行SET key value
命令后,AOF 文件中会追加一行*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue
,其中*3
表示命令有 3 个参数,$3
表示参数长度为 3 字节,以此类推。文件重写机制:随着时间的推移,AOF 文件会变得越来越大,因为它会不断地追加写命令。为了解决这个问题,Redis 引入了 AOF 文件重写机制。当 AOF 文件大小超过一定阈值时,Redis 会在后台启动一个子进程对 AOF 文件进行重写。重写的过程不是简单地复制原文件,而是根据当前内存中的数据状态,重新生成一个最小化的 AOF 文件。例如,如果对一个键进行了多次修改,重写后的 AOF 文件只会记录这个键的最终状态。 优势:AOF 持久化提供了更好的数据安全性,因为它可以记录每一个写操作,只要 AOF 文件不丢失,数据就可以最大限度地被恢复。它还可以通过配置 appendfsync
选项来控制数据同步到磁盘的频率,例如设置为always
可以保证每次写操作都立即同步到磁盘,数据安全性最高。劣势:AOF 文件通常比 RDB 文件大,因为它记录了所有的写命令。而且,AOF 文件的恢复速度相对较慢,因为需要重新执行 AOF 文件中的所有写命令来恢复数据。
Redis Cluster 集群恢复过程
RDB 恢复 单个节点恢复:当一个 Redis 节点需要从 RDB 文件恢复数据时,它会在启动时检查是否存在 RDB 文件(默认文件名是 dump.rdb
,存储在配置文件指定的目录下)。如果存在,它会将 RDB 文件中的数据加载到内存中。这个过程是将二进制数据解析成 Redis 内部的数据结构,例如,将存储的字符串还原成SDS
(Simple Dynamic String)结构,将列表数据还原成quicklist
结构等。集群恢复场景:在 Redis Cluster 集群中,如果某个节点故障后重新启动,它会独立地从自己的 RDB 文件恢复数据。但是,在恢复后,它还需要与集群中的其他节点进行通信,重新同步一些集群相关的信息,比如槽位(slot)的分配情况。因为在节点故障期间,集群的状态可能已经发生了变化,例如其他节点可能已经接管了故障节点负责的槽位。 AOF 恢复 单个节点恢复:在启动时,如果 Redis 发现 AOF 文件(默认文件名是 appendonly.aof
)存在,并且配置为优先使用 AOF 恢复(可以通过配置appendonly yes
开启 AOF 持久化并设置恢复优先级),它会读取 AOF 文件并重新执行其中的写命令来恢复数据。这个过程是顺序读取 AOF 文件中的每一行,解析命令并执行。例如,读取到上面提到的*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue
命令,Redis 会执行SET key value
操作。集群恢复场景:在 Redis Cluster 集群中,AOF 恢复的过程和单个节点类似。不过,恢复后同样需要与集群中的其他节点进行信息同步。而且,由于 AOF 文件记录的是写操作,如果在集群中某个写操作涉及到多个节点(例如跨槽位的事务操作),需要确保这些操作在恢复后不会破坏集群的一致性。在实际操作中,Redis Cluster 会根据集群状态和 AOF 记录的操作,合理地调整数据状态,保证集群的正常运行。
结合亿级用户排行榜选择合适的持久化策略
对于排行榜数据更新频率较低的情况: 如果亿级用户排行榜的数据更新不是很频繁,例如,排行榜只是每天或者每周更新一次,RDB 持久化可能是一个比较好的选择。 可以将 save
配置参数设置为适当的值,以平衡数据安全性和性能。同时,可以定期手动执行 SAVE
或BGSAVE
命令来创建数据集快照。SAVE
命令会阻塞 Redis 服务器,直到快照保存完成;BGSAVE
命令则是在后台执行快照保存,不会阻塞服务器,但可能会占用一定的系统资源。对于排行榜数据更新频繁的情况: 如果排行榜数据更新频繁,如实时更新用户积分和排名,AOF 持久化可能更合适。 在 Redis 配置文件中,可以设置 AOF 的相关参数。 首先,开启 AOF 持久化,将 appendonly
选项设置为yes
。然后,可以通过设置 appendfsync
参数来控制 AOF 文件的同步频率。appendfsync always
会在每个写命令执行后立即将命令同步到 AOF 文件,这种方式数据安全性最高,但性能损耗也最大;appendfsync everysec
表示每秒同步一次 AOF 文件,这是一种在性能和数据安全性之间比较好的平衡;appendfsync no
表示由操作系统决定何时将 AOF 文件同步到磁盘,这种方式性能最好,但数据丢失的风险也相对较高。
为什么 Reids 的 ZRANK 那么快?
尼恩架构团队塔尖的redis 面试题
说在最后:有问题找老架构取经
如何 使用ZSET 排序统计 ? 亿级用户的排行榜,如何设计?
空窗1年/空窗2年,如何通过一份绝世好简历, 起死回生 ?
空窗8月:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
空窗2年:42岁被裁2年,天快塌了,急救1个月,拿到开发经理offer,起死回生
空窗半年:35岁被裁6个月, 职业绝望,转架构急救上岸,DDD和3高项目太重要了
空窗1.5年:失业15个月,学习40天拿offer, 绝境翻盘,如何实现?
100W 年薪 大逆袭, 如何实现 ?
100W案例,100W年薪的底层逻辑是什么? 如何实现年薪百万? 如何远离 中年危机?
如何 评价一份绝世好简历, 实现逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢