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主进程,且这种慢查询不会再主节点的慢查询中记录,但可以在从节点的慢查询中记录。
如何安全删除非字符串的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)连接池参数配置,如是否开启空闲资源监测、空闲资源监测周期、池中资源最小空闲时间、做空闲资源检查时每次的采样数等。
4)如何预估最大连接池
maxTotal如何设置?maxIdle接近maxTotal。
考虑因素:业务并发量、客户端执行命令的时间、redis server最大连接数
例子:
jedis一次命令(包括获取归还连接、网络传输时间、redis执行命令时间)的平均耗时为1毫秒,那么一个连接的QPS为1000,业务期望的QPS是50000,那么理论上maxTotal=50000/1000=50个,可以在这个基础上进行适当的增加,如100个,而不用 设置为500、1000等。
当然jedis一次命令的平均耗时可能需要多次测试才能确定。