第一节 MoreKey案例
1、数据准备
大批量往Redis中插入100万条测试数据
# 生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
# 通过redis 提供的管道 --pipe 命令将数据插入 redis
cat /tmp/redisTest.txt | /opt/redis-7.0.0/src/redis-cli -h 127.0.0.1 -p 6379 -a 111111 --pipe
2、操作大量key的写法
(1)查询 morekey 的错误写法
使用 keys * 指令查询所有的 key
这个指令有个致命的弊端,没有 offset、limit 参数,一次性吐出所有满足条件的 key,由于 redis 指令是单线程原子性的,数据量大的话会导致所有其它查询 redis 的指令被延后甚至超时报错,可能会引起缓存雪崩或服务器宕机。
(2)查询 morekey 的正确写法
使用 SCAN 指令增量遍历集合中的元素
语法:SCAN cursor [MATCH pattern] [COUNT count]
scan 返回一个包含两个元素的数组, 第一个元素是用于进行下一次遍历的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被遍历的元素。当 scan 命令的游标参数被设置为 0
时, 服务器将开始一次新的遍历,而当服务器向用户返回值为 0
的游标时, 表示遍历已结束。
(3)如何限制危险命令防止误删误用?
生产上需禁用 keys * / flushdb / flushall 等危险命令,将这些命令重命名为 ""
在 redis.conf 配置文件 security 一项中设置指令禁用
第二节 什么是BigKey
BigKey 通常以 Key 的大小和 Key 中成员的数量来综合判定,例如:
-
Key 本身的数据量过大:一个 String 类型的 Key,它的值为5 MB
-
Key中的成员数过多:一个 ZSET 类型的 Key,它的成员数量为10,000个
-
Key中成员的数据量过大:一个 Hash 类型的 Key,它的成员数量虽然只有1,000个但这些成员的 Value(值)总大小为100 MB
如何判断元素的大小呢?redis 也给我们提供了命令
// 可以使用 MEMORY USAGE 指令查看指定 key 及其 value 占用内存字节数大小,一般不推荐使用 MEMORY USAGE 指令,因为对 CPU 的使用率比较高
MEMORY USAGE KEY
// 字符串可以使用 STRLEN 指令,列表可以使用 LLEN 指令,只需要衡量值或值的个数就可以
STRLEN KEY
LLEN KEY
推荐值:
-
单个 key 的 value 小于10KB
-
对于集合类型的 key,建议元素数量小于5000
第三节 BigKey的危害
-
网络阻塞
-
对 BigKey 执行读请求时,少量的 QPS 就可能导致带宽使用率被占满,导致 Redis 实例乃至所在物理机变慢
-
-
数据倾斜
-
BigKey 所在的 Redis 实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
-
-
Redis阻塞
-
对元素较多的 hash、list、zset 等做运算会耗时较久,使主线程被阻塞
-
-
CPU压力
-
对 BigKey 的数据序列化和反序列化会导致 CPU 的使用率飙升,影响 Redis 实例和本机其它应用
-
第四节 如何发现BigKey
1、redis-cli --bigkeys
利用 redis-cli 提供的 --bigkeys 参数,可以遍历分析所有 key,并返回 Key 的整体统计信息与每个数据的 Top1 的 big key
命令:redis-cli -a 密码 --bigkeys
2、scan扫描
自己编程,利用 scan 扫描 Redis 中的所有 key,利用 strlen、hlen 等命令判断 key 的长度(此处不建议使用 MEMORY USAGE,因为对 CPU 的使用率比较高)
scan 命令调用完后每次会返回2个元素,第一个是下一次迭代的光标,第一次光标会设置为0,当最后一次scan 返回的光标等于0时,表示整个scan遍历结束了,第二个返回的是List,一个匹配的key的数组
// 第一个数为下标(第一次为0),第二个数为每次返回元素的个数
127.0.0.1:6379> scan 0 count 2
1) "2"
2) 1) "key1"
2) "key2"
127.0.0.1:6379> scan 2 count 2
1) "1"
2) 1) "key4"
2) "key3"
测试代码实现:
import com.txwh.jedis.util.JedisConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
// 1.建立连接
jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123321");
// 3.选择库
jedis.select(0);
}
// bigKey的指标
final static int STR_MAX_LEN = 10 * 1024;
final static int HASH_MAX_LEN = 500;
@Test
void testScan() {
int maxLen = 0;
long len = 0;
String cursor = "0";
do {
// 扫描并获取一部分key
ScanResult<String> result = jedis.scan(cursor);
// 记录cursor
cursor = result.getCursor();
List<String> list = result.getResult();
if (list == null || list.isEmpty()) {
break;
}
// 遍历
for (String key : list) {
// 判断key的类型
String type = jedis.type(key);
switch (type) {
case "string":
len = jedis.strlen(key);
maxLen = STR_MAX_LEN;
break;
case "hash":
len = jedis.hlen(key);
maxLen = HASH_MAX_LEN;
break;
case "list":
len = jedis.llen(key);
maxLen = HASH_MAX_LEN;
break;
case "set":
len = jedis.scard(key);
maxLen = HASH_MAX_LEN;
break;
case "zset":
len = jedis.zcard(key);
maxLen = HASH_MAX_LEN;
break;
default:
break;
}
if (len >= maxLen) {
System.out.printf("Found big key : %s, type: %s, length or size: %d %n", key, type, len);
}
}
} while (!cursor.equals("0"));
}
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
}
3、第三方工具
-
利用第三方工具,如 Redis-Rdb-Tools 分析 RDB 快照文件,全面分析内存使用情况
4、网络监控
-
自定义工具,监控进出 Redis 的网络数据,超出预警值时主动告警
-
一般阿里云搭建的云服务器就有相关监控页面
第五节 如何删除BigKey
BigKey 内存占用较多,删除这样的 key 也需要耗费很长时间,会导致 Redis 主线程阻塞,引发一系列问题。
阿里开发规范:非字符串的 bigkey,不要使用 del 删除(del 指令是阻塞性的),使用 hscan、sscan、zscan 方式渐进式删除,同时要注意防止 bigkey 过期时间自动删除问题(例如一个200万的 zset 设置1小时过期,会触发 del 操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查))
1、String 类型
一般用 del 指令,如果过于庞大用 unlink 指令
2、hash 类型
(1)方法:使用 hscan 每次获取少量的 field-value,再使用 hdel 删除每一个 field
(2)代码示例:
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);
}
3、list 类型
(1)方法:使用 ltrim 指令渐进式删除,直到全部删除完毕
(2)代码示例:
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;
}
//最终删除key
jedis.del(bigListKey);
}
4、set 类型
(1)方法:使用 sscan 指令每次获取部分元素,再使用 srem 指令删除每个元素
(2)代码示例:
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);
}
5、zset 类型
(1)方法:使用 zscan 指令每次获取部分元素,再使用 ZREMRANGEBYRANK 指令删除指定范围的元素
(2)代码示例:
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);
}
第六节 生产调优
惰性释放:将释放内存的操作交给后台线程异步的进行处理,也就意味着一个 key 真正意义上的删除,具有一定的延迟。
redis.conf 配置文件 LAZY FREEING (惰性释放)一项中的相关说明
lazy free 应用于被动删除中(主动删除有 del、unlink),目前有4种场景,每种场景对应一个配置参数; 默认都是关闭,表示同步删除。
lazyfree-lazy-eviction 当 redis 内存达到阈值 maxmemory 时,将执行内存淘汰
lazyfree-lazy-expire 当设置了过期 key 的过期时间到了,将删除 key
lazyfree-lazy-server-del Server 端的一些内部删除操作。举例:使用 pop 指令弹出所有元素后,server端会删除这个key
replica-lazy-flush 主要用于复制过程中,全量同步的场景,从节点需要删除整个 db
优化方案:修改下面3个配置
(1)replica-lazy-flush no 当设置为 yes 时,进行全量同步前,整个 DB 进行 FLUSH 时会进行异步操作,内存变化不大时建议开启,可减少全量同步耗时
(2)lazyfree-lazy-user-del no 当设置为 yes 时,用户使用 del 指令本质来说和 unlink 指令的处理方式一致