阿里面试:亿级 redis 排行榜,如何设计?
本文原文链接
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
阿里面试:亿级 redis 排行榜,如何设计?
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
- 如何 使用ZSET 排序统计 ?
- 亿级用户的排行榜,如何设计?
最近有小伙伴在面试 阿里,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
首先回顾一下:redis 四大统计(基数统计,二值统计,排序统计、聚合统计)的原理 和应用场景
尼恩这边的文章都有一个 基本的规则,从最为基础的讲起。
先看看 常见的四大统计。
第1大统计:基数统计
基数(Cardinality)是指一个集合中不同元素的数量, 或者说统计一个集合中不重复元素的个数。
简单来说: 基数(Cardinality)就是去除重复后的数的个数
例如,对于一个包含重复元素的集合{1, 2, 2, 3, 4, 4, 4},其基数为4,即不同元素的个数。
在Redis中,HashSet /bitmap/ HyperLogLog数据结构都能提供 高效的基数统计,而HyperLogLog算法可以在不保存原始数据的情况下快速计算出一个集合的基数。
希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)
第2大统计:二值统计
二值统计通常涉及到将数据分为两个类别或状态,比如成功与失败、是与非等,并对这些类别进行计数和分析。
这种统计方法在处理二分类问题时非常常见,比如在质量控制、用户行为分析等领域。
亿级海量数据查重,如何实现 ? 涉及的是四大统计其中的 二值统计
亿级海量数据黑名单 ,如何存储?涉及的也是四大统计其中的 二值统计
京东面试:亿级 数据查重,亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)
第3大统计:排序统计
排序统计涉及将数据按照一定的顺序(如升序或降序)进行排列,以便于分析和比较。
排序统计的例子,比如,可以使用ZSET对排序统计
- 可以 使用ZSET对文章的点赞数排序并分页展示
- 对评论根据时间进行排序
- 亿级用户的排行榜,如何设计?
都属于排序统计, 此文,彻底介绍一下排序统计。
第4大统计:聚合统计
聚合统计是一种数据处理技术,它将多个数据记录组合成一个集合,并计算该集合的统计信息,如总和、平均值、最大值和最小值等。
聚合操作通常用于数据仓库和数据分析中,以简化数据并提取有用的信息。
聚合统计的核心在于对数据进行分组(grouping),然后对每个组应用聚合函数(如sum, avg, max, min等)来计算统计值。
此文,45岁老架构师给大家彻底介绍一下第3大统计: 排序统计。
入门篇:Redis 的有序集合(Sorted Set)
Redis 的有序集合(Sorted Set)是一个复合数据类型,它能够存储不重复的字符串成员,并且每个成员都关联了一个浮点数分数,这个分数用于对集合中的成员进行排序。
有序集合是唯一的,新成员可以添加,分数可以更新,但成员必须是唯一的。
以下是有序集合的一些主要操作及其实例:
1. ZADD
向有序集合添加一个或多个成员,或者更新已存在成员的分数。
ZADD key score member [score member ...]
实例:
ZADD myzset 1 "one"
"one" 的分数是 1
ZADD myzset 1 "one" 2 "two" 3 "three"
"one" 的分数是 1 , "two" 的分数是2 , "three" 的分数是3 ,
2. ZSCORE
获取有序集合中指定成员的分数。
ZSCORE key member
实例:
ZSCORE myzset "two"
这里返回 "two" 的分数 2
3. ZINCRBY
为有序集合中已存在的成员的分数加上一个增量值。
ZINCRBY key increment member
实例:
ZINCRBY myzset 2 "two"
"two" 的原始分数是 2,现在将变为 4
4. ZRANK / ZREVRANK
获取有序集合中指定成员的排名。ZRANK 是升序排名,ZREVRANK 是降序排名。
ZRANK key member
ZREVRANK key member
实例:
ZRANK myzset "three" # 返回 "three" 升序排名
ZREVRANK myzset "three" # 返回 "three" 降序排名
5. ZRANGE / ZREVRANGE
获取有序集合中指定区间的成员,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"
现在,如果你想要获取索引从 0 到 2 的成员(即分数最低的三个成员),你可以执行以下命令:
ZRANGE myzset 0 2
执行这个命令后,Redis 将返回一个列表,包含成员 "one"
、"two"
和 "three"
。
如果你还想要获取这些成员的分数,可以添加 WITHSCORES
选项:
ZRANGE myzset 0 2 WITHSCORES
执行这个命令后,Redis 将返回一个列表,包含成员及其分数,如下所示:
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
这些操作使得有序集合成为实现排行榜、计分板等需要排序和排名功能的应用的理想选择。通过这些命令,你可以有效地管理和查询有序集合中的数据。
社交点赞 的设计与实现
在当今的社交媒体时代,点赞功能是一个非常重要的交互元素。
它不仅可以让用户表达对内容的喜爱,还能帮助平台突出受欢迎的内容,增强用户的参与感和平台的活跃度。本文将详细介绍一种基于 Redis 的社交点赞系统的设计与实现方法。
在构建社交媒体平台时,需要为用户提供对文章、照片等各类内容进行点赞的功能。
平台要能够准确统计每个内容的点赞数量,并实时展示最受欢迎的内容,以满足用户对内容热度的关注和平台对优质内容推荐的需求。
使用 Redis 的有序集合(zset)和哈希(hash)来设计一个社交点赞系统,可以有效地存储和检索点赞数据。
以下是如何使用这两种数据结构来实现点赞系统的具体步骤:
社交点赞 数据结构设计
- 有序集合(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 命令实现
以下是使用 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>
游戏玩家排行榜案例
在游戏中,玩家排行榜是一个重要的功能,它可以激发玩家的竞争意识,增加游戏的趣味性和玩家的粘性。
在一个多人在线游戏中,我们希望能够实时追踪和显示玩家的排行榜,以鼓励玩家参与并提升游戏的竞争性。
通过排行榜,玩家可以直观地了解自己在游戏中的位置,与其他玩家进行比较。以下是一个基于特定技术的游戏玩家排行榜系统的设计与实现方案。
游戏玩家排行榜 数据模型
可以选择使用 Redis 作为主要的数据存储来实现排行榜功能。 Redis 的有序集合(Sorted Set)数据结构非常适合存储排行榜数据。
因为有序集合中的元素可以根据一个分数值进行排序,而这个分数值可以根据游戏中的排名因素(如得分、时间等)来计算。
- 玩家标识
每个玩家都有一个唯一的标识,如玩家 ID。这个标识将作为 Redis 有序集合中的元素。 - 排名分数计算因素
- 得分:玩家在游戏中的得分是一个重要的排名因素。例如,在射击游戏中,杀敌数、完成任务获得的奖励分数等都可以计入总分。
- 游戏完成时间:对于一些有时间限制的游戏关卡或模式,完成时间越短排名越靠前。可以根据一定的算法将完成时间转换为分数的一部分,比如,用时越短分数越高。
- 综合排名分数
根据上述因素,通过一个特定的算法计算出每个玩家的综合排名分数。例如,可以将得分乘以一个权重系数,再加上根据完成时间计算出的分数,得到最终的综合排名分数。
游戏玩家排行榜 Redis 操作命令
玩家数据更新 Redis 操作命令
当玩家完成一局游戏后,根据游戏结果计算出玩家的新排名分数。
然后使用 Redis 的 ZADD 命令将玩家 ID 和新的排名分数添加到有序集合中。
例如,如果玩家 ID 是 “player123”,新的排名分数是 1000,对应的 ZADD 命令就是
ZADD "leaderboard" 1000 "player123"
获取排行榜信息 Redis 操作命令
-
获取 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 的高性能特性能够保证在大量玩家数据和高并发操作下系统的稳定运行。
超高并发下 Redis 热key 分治原理
众所周知Redis扛并发的能力是非常强的,所以高并发场景下经常会使用Redis,
如果在亿级用户、超高并发下场景, 一个key 落到一个 redis 节点,单 节点的Redis 扛不住的,如下图所示:
因为, Redis单分片的 吞吐量 瓶颈在2w左右 。
如果 Redis做了集群部署呢? 没有。 实际上,一个Redis的key只会存在一个分片上,此时超高并发下redis2很有可能会被打垮。
那么在超高的并发如何解决,某个热key带来的单分片被打垮,甚至系统雪崩的问题呢?
具体请参见 45岁老架构师尼恩的文章 史上最全:Redis热点Key,如何 彻底解决问题
下面聊聊Redis分key来解决这个问题的方案。
什么是Redis 热点key 拆分?
Redis分key就是将一个热点key通过拆分成若干key,然后让这若干个key分散到Redis集群的不同节点,
如下图所示:
上图,将热点key拆分成3个小key,这样由原先的一个key拆分成3个小key,在超高并发下由这3个key共同的来承担原先一个热点key扛的流量。
当然, 通过Redis的key 到 node的 对应关系, 是通过 hash 取模 (crc16算法)获取的槽位值,再对应到redis 节点上
hash 取模 (crc16算法)获取的槽位值 ,算法将这3个key分散到Redis集群的分片上。由hash 取模 (crc16算法) 保证数据 的大致均匀,不发生数据倾斜。
(1)Redis分可以的原理就是将一个热点key拆分成若干个小key后分散不集群的不同节点上(集群有几个节点就拆分成几个小key)
(2)为保证key可以分散到集群节点上,采用的是手动尝试的方式来获取拆分后的小key。
(3)分key的方案常用于超高并发业务场景下,如抢优惠券、实时榜单等。
亿级用户排行榜,如何设计范围分片, 路由到 不同的set
王者荣耀,妥妥的亿级用户, 这个亿级用户排行榜的需求也得用分治的思想。
其实,45岁老架构师尼恩提示大家,一般面试遇到什么千万条数据、几个 G 文件、上亿的数据啥的,首先想到的方案就是分而治之。
关键是,亿级用户 ,如何设计分片?
这里,我们可以设计范围分片/分桶,比如按照下面的 模式去拆
那么, 分片的基本原理和思想是什么,尼恩给大家慢慢道来。
理解范围分片的基本概念
-
范围分片是一种数据分片策略,它根据数据的某个特定范围将数据划分到不同的存储单元(在这个例子中是不同的 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);
}
}
在上述 Java 代码中:
- 定义了一个名为
getSetIDByScore
的静态方法,它接受一个整数类型的score
参数,根据score
的值所在的范围返回对应的Set ID
。 - 在
main
方法中,示例了如何调用getSetIDByScore
方法并输出结果,你可以根据实际情况修改main
方法中的score
值或者在其他地方调用getSetIDByScore
方法来满足具体的业务需求。同时,如果实际情况中有更多的积分范围判断,需要在getSetIDByScore
方法中继续添加相应的if-else
条件语句。
靠虑边界情况:
要注意边界值的处理。
比如,当积分正好是边界值(如 1000)时,需要明确它应该被路由到哪个 set。
可以采用左闭右开区间([a, b))的方式,即积分 1000 应该被路由到 set2。
动态调整范围:
随着用户数据的变化,积分范围可能会发生改变。
例如,随着用户活跃度的提高,整体积分范围可能会上升。
此时,需要有一种机制来动态调整范围分片的边界。
可以定期重新统计用户积分的分布情况,根据新的分布来调整范围边界和路由算法。这就设计
动态范围调整的 数据迁移策略:
当调整范围分片边界时,可能需要进行数据迁移。
如果积分范围从 0 - 9999 和 10000 - 19999 两个区间调整为 0 - 14999 和 15000 - 19999,那么原本在 10000 - 14999 区间的数据需要从原来的 set 迁移到新的 set。
可以采用异步迁移的方式,分成3步:
-
先在新的 set 中插入数据,
-
动态的切换 路由算法,把老的路由算法,切换为新的路由算法。 可以使用 nacos/zk的发布订阅机制,进行算法的切换
-
然后逐步删除旧 set 中的数据。
动态范围调整的数据一致性保证:
在数据迁移过程中,可以有的key 已经迁移到新的 分片,但是 前端写入用的是老的 路由算法, 会导致数据的丢失。
所以说,这个是弱一致性。
但是在排行榜这样的场景,这个是允许的。
排行榜 Redis Cluster 集群持久化方式
如何进行 排行榜的 持久化?
首先来看 Redis Cluster 集群持久化方式。
- RDB 持久化(Redis Database)
- AOF 持久化(Append - Only File)
RDB是通过快照的形式保存内存中的数据到硬盘上的二进制文件中,而AOF是将Redis的操作日志以追加的方式保存到硬盘上的AOF文件中。
可以根据实际需求选择适合的持久化策略,以保证数据的可靠性和性能的平衡
方式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)的分配情况。因为在节点故障期间,集群的状态可能已经发生了变化,例如其他节点可能已经接管了故障节点负责的槽位。
- 单个节点恢复:当一个 Redis 节点需要从 RDB 文件恢复数据时,它会在启动时检查是否存在 RDB 文件(默认文件名是
- 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 记录的操作,合理地调整数据状态,保证集群的正常运行。
- 单个节点恢复:在启动时,如果 Redis 发现 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 那么快?
最后理解了上面的zset之后,面试官一般会问,为什么 Reids 的 ZRANK 那么快?
zset 底层用跳表来实现实时排行榜功能。
Java 中,ConcurrentSkipListMap、ConcurrentSkipListSet 就是基于跳表的。
那么 跳表为啥块呢 , Redis Zset 作者是这么解释的:
Redis 源码中还有非常多的精妙绝伦的设计,后面我们一起来揭开它的更多面纱吧!
关于跳表的结构, 请参见 45岁老架构尼恩的深度文章,手写一个跳表。
尼恩架构团队塔尖的redis 面试题
京东面试: 亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)
希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)
史上最全: Redis: 缓存击穿、缓存穿透、缓存雪崩 ,如何彻底解决?
史上最全: Redis锁如何续期 ?Redis锁超时,任务没完怎么办?
说在最后:有问题找老架构取经
回到开始的时候的面试题:
- 如何 使用ZSET 排序统计 ?
- 亿级用户的排行榜,如何设计?
按照此文的套路去回答,一定会 吊打面试官,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。前段时间,刚指导一个小伙 暴涨200%(涨2倍),29岁/7年/双非一本 , 从13K一次涨到 37K ,逆天改命。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》