Redis篇-第13章 BigKey问题

第一节 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、第三方工具

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 指令的处理方式一致

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值