前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
进阶篇:
接上期内容:上期完成了Redis单多线程以及IO多路复用方面的学习。下面开始学习redis处理Bigkey和MoreKey的知识,话不多说,直接发车。
一、什么是BigKey、MoreKey
(一)、BigKey定义
BigKey,简单来说,就是指那些值(value)特别大的键(key)。这里的 “大”,主要体现在两个方面:
- 数据大小:对于字符串类型的键值对,如果单个value值很大,一般认为超过10KB就是 BigKey。(参考阿里云Redis开发规范)
- 成员数量:当键对应的值是哈希、列表、集合、有序集合等非字符串类型时,如果这些数据结构中的元素个数太多,也会被视为 BigKey,一般认为元素超过5000个就是BigKey。(参考阿里云Redis开发规范)
(二)、MoreKey定义
MoreKey通常指的是当数据库中的key数量非常多时,使用如KEYS *这样的命令去检索所有的 key,会导致 Redis 服务阻塞,影响正常业务。
二、BigKey如何产生和危害?
(一)、BigKey如何产生?
- 业务规划不合理:在业务开发初期,如果缺乏对数据规模和访问模式的充分预估,可能会导致将大量相关数据存储在一个 key 中。
- 数据结构使用不当:Redis 的 String 类型虽然简单易用,但如果用它来存储大体积的二进制文件(如图片、视频的二进制数据)或者非常长的文本数据,就容易形成 BigKey。
- 数据清理机制失效或缺失:如果系统没有建立有效的数据清理机制或机制失效,过期或不再使用的数据会一直占用内存,导致 key 的大小不断增长。
(二)、BigKey产生后造成的危害
- 影响性能:当一个key是BigKey,那么对这个key的读、写都是会带来严重的性能问题。如果这个key足够大,甚至会阻塞redis服务器。
- 占用内存:BigKey会占用大量的连续内存空间。如果一个redis集群中,存在大量的Bigkey,还可能导致整个集群的负载不均衡。
- 影响业务稳定性:BigKey 的存在可能会增加数据一致性维护的难度。当对 BigKey 进行更新操作时,如果操作过程中出现异常(如网络中断、Redis 服务器故障等),可能会导致部分数据更新成功,部分失败,从而破坏数据的一致性,影响数据一致性。
三、MoreKey、BigKey实操
(一)、MoreKey实操
1、模拟redis多key场景
通过脚本,生成1000万条redis命令
for((i=1;i<=1000*10000;i++)); do echo "set k$i v$i" >> /myredis/temp.txt ;done;
在通过--pipe命令批量往redis里面存入1000万key。
2、keys *命令的弊端
如果在1000w的redis中,使用keys * 命令来查看某个key是否存在,结果是啥我就不多说。
如果在生产上,redis阻塞74s,是什么概念!!(⊙o⊙)
keys *,这个指令没有 ofset、limit 参数,是要一次性吐出所有满足条件的 key,由于 redis 是单线程的,其所有操作都是原子的,而 keys 算法是遍历算法,复杂度是 0(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机。
3、SCAN命令实操
3.1、Q & A
Q:但是又想要查看或者查找某个key怎么做?
A:使用scan命令。
SCAN cursor [MATCH pattern] [COUNT count]
参数说明:
cursor
:- 这是一个必需的参数,是一个整数值,作为游标使用。首次调用scan命令时,cursor要设置为
0
,表示从数据库的起始位置开始迭代。每次调用SCAN
命令后,会返回一个新的游标值,后续的迭代需要使用这个新的游标值继续进行,直到返回的游标值为0
,这意味着迭代结束。
- 这是一个必需的参数,是一个整数值,作为游标使用。首次调用scan命令时,cursor要设置为
[MATCH pattern]
:- 可选参数,用于指定键的匹配模式。可以使用通配符,如
*
表示匹配任意数量的任意字符,?表示匹配单个任意字符。例如,MATCH user:* 会返回所有以 user: 开头的键。
- 可选参数,用于指定键的匹配模式。可以使用通配符,如
[COUNT count]
:- 可选参数,用于提示 Redis 每次迭代要返回的键的大致数量。不过,这只是一个提示,Redis 并不一定会严格按照这个数量返回键。通常,count的默认值是 10。
返回结果:
- 第一个元素是用于进行下一次迭代的新游标。
- 第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。
*注意:SCAN的遍历顺序非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
3.2、scan的使用
(二)、BigKey实操
1、发现BigKey
①、--Bigkeys命令
bigkeys命令能给出每种数据结构,同时给出每种数据类型的键值个数+平均大小。但是它没法给出每个具体key的大小。
redis-cli -a 密码 --bigkeys
②、memory usage命令
MEMORY USAGE key
MEMORY USAGE 命令给出一个 key 和它的值在 RAM 中所占用的字节数。返回的结果是 key 的值以及为管理该 key 分配的内存总字节数。
这不妥妥的BigKey么。。。
2、删除BigKey
①、第一种情况
如果key为String类型,普通删除用del即可,BigKey使用unlink。
②、第二种情况
非字符串的bigkey,不要使用del删除,而使用hscan、sscan、zscan方式渐进式删除。
删除Hash:使用hscan每次获取少量field-value,再使用hdel删除每个field,hscan+hdel。
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigHashKey);
}
删除Set:使用sscan每次获取部分元素,再使用srem命令删除每个元素,sscan + srem。
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigSetKey);
}
删除List:使用ltrim渐进式逐步删除,直到全部删除完成。
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while (counter < llen) {
//每次从左侧截掉100个
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最终删除bigKey
jedis.del(bigListKey);
}
删除zSet: 使用zscan每次获取部分元素,再使用zrem命令删除每个元素,zscan+zrem。
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigZsetKey);
}
四、BigKey调优
(一)、惰性删除
惰性删除(Lazy Free)是 Redis 为了优化内存释放操作、避免阻塞主线程而引入的一种机制。在 Redis 中,键的删除操作可能会涉及大量内存的释放,如果这些操作都在主线程中同步执行,可能会导致主线程阻塞,影响 Redis 的性能和响应能力。
(二)、配置项解释
解释:
- lazyfree-lazy-eviction
:
控制当 Redis 执行内存淘汰策略时,是否采用异步方式释放被淘汰键占用的内存。设置为 yes 时,采用异步方式;设置为 no 时,采用同步方式。 - lazyfree-lazy-expire
:
当 Redis 中的键过期时,是否使用异步方式删除过期键。设置为 yes 时,异步删除;设置为 no 时,同步删除。 - lazyfree-lazy-server-del
:
控制在使用 del、unlink 等命令删除键时,是否采用异步方式释放键占用的内存。设置为 yes 时,异步释放;设置为 no 时,同步释放。 - replica-lazy-flush:用于控制从节点在进行全量同步时,是否使用异步方式清空数据库。设置为
yes
时,异步清空;设置为 no 时,同步清空 - lazyfree-lazy-user-del
:
控制用户使用DEL
命令删除键时,是否采用异步方式释放键占用的内存。设置为 yes 时,del 命令会以异步方式工作,类似于 unlink 命令;设置为 no 时,DEL
命令保持默认的同步删除行为。 - lazyfree-lazy-user-flush
:
此配置项用于控制在用户执行FLUSHDB
、FLUSHALL
、SCRIPT FLUSH
和FUNCTION FLUSH
命令,且未指定SYNC
或ASYNC
标志时,这些命令的数据删除操作是采用异步还是同步方式。设置为 yes 时,若执行上述命令且未指定删除模式时则采用异步清空;设置为 no 时,则采用同步清空。
五、关于MoreKey、BigKey的经典面试题
(一)、在海量数据中如何查找固定前缀的key?如何遍历key?
1、查找固定前缀的key
针对于key类型为String,可以使用命令是 Redis 提供的用于渐进式迭代键空间的命令,可配合MATCH选项来查找具有固定前缀的 key。
针对于hash、set、zset类型的数据,可以使用hscan、sscan、zscan命令来查找固定前缀的key。
2、遍历key
基本原理和查找固定前缀的 key 类似,只是不使用MATCH选项。通过不断迭代游标,逐步遍历整个键空间。
(二)、如何在生产上限制keys*/flushdb/flushall等危险命令的使用?
1、配置ACL
从 Redis 6.0 版本开始,支持通过ACL(访问控制列表)来限制用户对特定命令的使用。例如:可以通过命令创建,
# 创建一个新的用户,只允许执行部分安全命令
ACL SETUSER test on > test ~* +get +set +del
也可以在配置文件配置相
2、rename-command
可以通过redis重命名功能完全禁止某些命令。
(三)、Memory usage命令有使用过吗?能干嘛?
该命令用于返回一个键所占用的内存字节数,主要用途:
①、分析内存占用:了解每个键具体占用了多少内存,有助于找出占用大量内存的 bigkey,进行针对性的优化。
②、内存优化决策:根据键的内存使用情况,决定是否需要对键进行拆分、压缩或者删除等操作,以优化 Redis 的内存使用。
(四)、什么是BigKey?多大算BigKey?如何发现、删除?
1、什么是 BigKey
BigKey 指的是在 Redis 中占用大量内存或者包含大量元素的键。
2、多大算 BigKey
并没有一个固定的标准来定义多大算 BigKey,在以下情况可以认为是 BigKey:
①、字符串类型键值大小超过10KB。
②、hash、list、set、zset等类型包含的元素数量超过 5000 个。
3、如何发现 BigKey
可以使用两个命令来发现BigKey:
①、Memory usage计算key占据内存大小。
②、--bigkeys:统计每个类型key占据内存情况。
4、如何删除BigKey
①、推荐使用unlink命令异步删除。
②、开启异步删除配置后在使用DEL命令删除。
③、通过scan命令分批获取bigkey中的字段,再用del命令逐个删除这些字段,最后删除整个键。比如hash就是使用hscan+hdel来进行key的删除。
(五)、BigKey调优之惰性删除
Redis 的惰性删除机制是为了避免在删除大键时阻塞主线程。当执行删除操作时,主线程只负责标记该键为已删除,然后将释放内存的任务交给后台线程处理,主线程可以继续处理其他请求。Redis 提供了多个配置项来控制不同场景下的惰性删除行为,如lazyfree-lazy-eviction,lazyfree-lazy-expire,lazyfree-lazy-server-del等。通过将这些配置项设置为yes,可以开启相应场景下的惰性删除功能。
六、总结
在了解了 BigKey 和 MoreKey 相关知识后,我在实际工作里面对此类问题时能够更加从容地应对,处理起来游刃有余。而且在面试场景中,当遇到涉及这方面的问题时,我也更有底气,回答起来更加得心应手。
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。