1. key设计

    可读性和可管理性:数据库名:表名:id,如用户中心的用户表里的数据,usersystemconter:usertb:128,表示用户中心系统里的用户表里id为128这条数据。

    简洁性:在保证语义的前提下,控制key的长度,在数据量很大的情况下,也能节省一定的内存,如usersystemconter:usertb:128可以简化成usys:utb:128。

    key不要包含特殊字符:如空格、换行、单双引号以及其他转义字符。

2. value设计

   拒绝bigkey:建议string类型控制在10KB以内;hash、list、set、zset元素个数不要超过5000,超过这个可以认为是bigkey。

    危害:

    网络阻塞:如果一个网卡为千兆网卡,那么实际带宽为1024M/8=128M,如果一个bigkey为1M,那么并发不会超过128QPS。

    redis阻塞:bigkey容易导致慢查询,从而导致redis阻塞。

    集群节点数据不均匀:bigkey导致集群节点数据分布不均匀

    虚拟化和反序列化:应用程序频繁的序列化和反序列化容易消耗过多的CPU

3. 发现bigkey

    3.1 应用方发现:读写超时异常、获取不到连接池中连接异常等,有可能就是由于bigkey导致。

    3.2 redis-cli -h -p --bigkeys:

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest hash   found so far 's_9329222' with 3 fields
[00.00%] Biggest string found so far 'url_http://mini.eastday.com/mobile/170722090206890.html?qid=sgllq&ch=east_sogou_push&pushid=13' with 8 bytes
[00.00%] Biggest string found so far 'foo' with 40 bytes
[00.00%] Biggest hash   found so far 's_9329084' with 4 fields
[00.23%] Biggest zset   found so far 'region_hot_菏泽地' with 625 members
[00.23%] Biggest zset   found so far 'region_hot_葫芦岛' with 914 members
[00.47%] Biggest string found so far 'top_notice_list' with 135193 bytes
[00.73%] Biggest zset   found so far 'region_hot_自贡' with 2092 members
[01.90%] Biggest hash   found so far 'uno_facet_2018-12-20' with 59 fields
[11.87%] Biggest zset   found so far 'region_hot_上海' with 2233 members
[27.05%] Biggest set    found so far 'blacklist_set_key' with 31832 members
[73.87%] Biggest string found so far 'PUSH_NEWS' with 3104237 bytes
[86.18%] Biggest zset   found so far 'region_hot_北京' with 2688 members

-------- summary -------

Sampled 4263 keys in the keyspace!
Total key length in bytes is 174847 (avg len 41.02)

Biggest string found 'PUSH_NEWS' has 3104237 bytes
Biggest    set found 'blacklist_set_key' has 31832 members
Biggest   hash found 'uno_facet_2018-12-20' has 59 fields
Biggest   zset found 'region_hot_北京' has 2688 members

1616 strings with 3771161 bytes (37.91% of keys, avg size 2333.64)
0 lists with 0 items (00.00% of keys, avg size 0.00)
1 sets with 31832 members (00.02% of keys, avg size 31832.00)
2353 hashs with 7792 fields (55.20% of keys, avg size 3.31)
293 zsets with 333670 members (06.87% of keys, avg size 1138.81)

    注意:

    1)该命令使用scan方式对key进行统计,所以使用时无需担心对redis造成阻塞。

    2)输出大概分为两部分,summary之上的部分,只是显示了扫描的过程。summary部分给出了每种数据结构中最大的Key。

    3)统计出的最大key只有string类型是以字节长度为衡量标准的。list,set,zset等都是以元素个数作为衡量标准,原始个数多不代表其占的内存就一定多。

    3.3 debug object key:

127.0.0.1:6379> hmset myhash k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> debug object myhash
Value at:0x7f005c6920a0 refcount:1 encoding:ziplist serializedlength:36 lru:3341677 lru_seconds_idle:2
#Value at:key的内存地址 refcount:引用次数 encoding:编码类型 serializedlength:序列化长度 lru_seconds_idle:空闲时间
#serializedlength是key序列化后的长度,并不是key在内存中的真正长度。
#redis的官方文档不是特别建议在客户端使用该命令,可能因为计算serializedlength的代价相对高。

3.4 其他开源工具

4. 删除bigkey

    非字符串的bigkey,不要使用del删除,可能会阻塞redis主进程,应该使用hscan、sscan、zscan方式渐进式删除。

    注意:bigkey过期自动删除,和del删除一样,可能会阻塞redis主进程,且这种慢查询不会再主节点的慢查询中记录,但可以在从节点的慢查询中记录。

redis tag key设计 redis key设计技巧_使用建议

        如何安全删除非字符串的bigkey?采用hscan、sscan、zscan方式渐进式删除,如下代码所示:(redis 4.0已经支持key的异步删除)。

#Hash删除: 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);
}
#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;
    }
    //最终删除key
    jedis.del(bigListKey);
}
#Set删除: 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);
}
#SortedSet删除: 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);
}

5. 选择合理的数据结构

    合理使用数据结构及其内存编码,但要注意节省内存和性能之间的平衡。

    对于结构化对象,如数据库表中的数据,建议使用hash结构,不建议用格式化的字符串(value为数据的json串),更不建议用每个条数据的属性作为一个key。

    例子:假设有N多图片,每个图片有一个图片id(picId),每个图片对应一个用户id(userId),用户量100万,如果想通过picId获取userId,那么有如下3种设计方案:

    1)全部string:set picId userId      #优点:编程简单         缺点:key过多 浪费内存,全量获取较为复杂

    2)一个hash:set allPics picId userId    #优点:无            缺点:bigkey问题,占用内存最多

    3)若干个小hash:hset picId/100 picId%100 userId     #优点:节省内存        缺点:编程复杂,超时问题(无法设置过期时间)

6. 键值生命周期的管理

    周期数据需要设置过期时间,object idle time 可以找到垃圾key-value。

    过期时间不宜集中,否则可能造成缓存穿透和后端存储雪崩等问题。

7. 命令优化技巧

    1)O(N)命令需要关注N的数量,如hgetall、lrang等,如果需要遍历可以使用hscan、sscan、zscan代替。

    2)禁用相关命令,如keys、flushall、flushdb等,使用rename机制。

    3)不要使用多数据库,redis本身提供了16个数据库,建议就使用db0。

    4)不建议使用redis的事务功能,redis事务不能回滚,集群时要求一次事务的key必须在一个槽上等。

    5)redis集群版本在使用lua上有特殊要求,建议不要过多依赖lua

    6)必要情况下使用monitor命令,注意不要长时间使用,在并发大的情况下,它可能会产生过多的 输出缓冲数据。

8. jedis客户端优化

    1)避免多个应用使用一个redis实例。

    2)使用连接池,必须使用标准的try catch finally模式

    3)连接池参数配置,如是否开启空闲资源监测、空闲资源监测周期、池中资源最小空闲时间、做空闲资源检查时每次的采样数等。

redis tag key设计 redis key设计技巧_List_02

    4)如何预估最大连接池

    maxTotal如何设置?maxIdle接近maxTotal。

    考虑因素:业务并发量、客户端执行命令的时间、redis server最大连接数

    例子:

    jedis一次命令(包括获取归还连接、网络传输时间、redis执行命令时间)的平均耗时为1毫秒,那么一个连接的QPS为1000,业务期望的QPS是50000,那么理论上maxTotal=50000/1000=50个,可以在这个基础上进行适当的增加,如100个,而不用 设置为500、1000等。

    当然jedis一次命令的平均耗时可能需要多次测试才能确定。