Bigkey 是指当 Redis 的字符串类型过大,非字符串类型元素过多。
危害 | 内存空间不均匀(平衡) 例如在 Redis Cluster 中,大量 bigkey 落在其中一个 Redis 节点上,会造成该节点的内存空间使用率比其他节点高,造成内存空间使用不均匀。
| 请求倾斜 对于非字符串类型的 bigkey 的请求,由于其元素较多,很可能对于这些元素的请求都落在 Redis cluster 的同一个节点上,造成请求不均匀,压力过大。
| 超时阻塞 由于 Redis 单线程的特性,操作 bigkey 比较耗时,也就意味着阻塞 Redis 可能性增大。这就是造成生产事故的罪魁祸首!导致 Redis 间歇性卡死、影响线上正常下单!
| 网络拥塞 每次获取 bigkey 产生的网络流量较大,假设一个 bigkey 为 1MB,每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通的千兆网卡(按照字节算是 128MB/s)的服务器来说简直是灭顶之灾。
而且一般服务器会采用单机多实例的方式来部署,也就是说一个 bigkey 可能会对其他实例造成影响,其后果不堪设想。
| 过期删除 有个 bigkey,它安分守己(只执行简单的命令,例如 hget、lpop、zscore 等),但它设置了过期时间,当它过期后,会被删除,如果没有使用 Redis 4.0 的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞 Redis 的可能性。
排查 查看bigkeys,
对于 String 类型来说,会输出最大 bigkey 的字节长度,对于集合类型来说,会输出最大 bigkey 的元素个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $ redis -p 6666 --bigkeys -a <pass> Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. # 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 string found so far 'SP-TENANT:1xxxxxxxxx' with 139 bytes [00.00%] Biggest hash found so far 'idempotent' with 1 fields [76.92%] Biggest zset found so far 'redisson__timeout__set:{idempotent}' with 1 members -------- summary ------- Sampled 13 keys in the keyspace! Total key length in bytes is 365 (avg len 28.08) Biggest string found 'SP-TENANT:1xxxxxxxxx' has 139 bytes Biggest hash found 'idempotent' has 1 fields Biggest zset found 'redisson__timeout__set:{idempotent}' has 1 members 11 strings with 1411 bytes (84.62% of keys, avg size 128.27) 0 lists with 0 items (00.00% of keys, avg size 0.00) 0 sets with 0 members (00.00% of keys, avg size 0.00) 1 hashs with 1 fields (07.69% of keys, avg size 1.00) 1 zsets with 1 members (07.69% of keys, avg size 1.00) 0 streams with 0 entries (00.00% of keys, avg size 0.00)
判断一个 key 是否为 bigkey,只需要执行 debug object key 查看 serializedlength 属性即可,它表示 key 对应的 value 序列化之后的字节数。
1 2 127.0.0.1:6666> debug object SP-MEMBER:1xxxxxxxxx Value at:0x7f6a5901f3f0 refcount:1 encoding:raw serializedlength:126 lru:6454313 lru_seconds_idle:8
可以看到 encoding 是 raw,也就是字符串类型,那么可以通过 strlen 来看一下字符串的字节数
1 2 > strlen SP-MEMBER:1xxxxxxxxx (integer) 139
解决 开启Lazy Free delete(被动删除,过期策略) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no slave-lazy-flush no
主动删除(转自石杉老师) | 如何提升删除的效率 既然不能用 del 命令,那有没有比较优雅的方式进行删除呢?Redis 提供了一些和 scan 命令类似的命令:sscan、hscan、zscan。
①string
字符串删除一般不会造成阻塞:
②hash、list、set、sorted set
下面以 hash 为例子,使用 hscan 命令,每次获取部分(例如 100 个)fieldvalue,再利用 hdel 删除每个 field(为了快速可以使用 Pipeline):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void delBigHash (String bigKey) { Jedis jedis = new Jedis (“127.0 .0 .1 ”, 6379 ); String cursor = “0 ”; while (true ) { ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor, new ScanParams ().count(100 )); cursor = scanResult.getStringCursor(); List<Entry<String, String>> list = scanResult.getResult(); if (list == null || list.size() == 0 ) { continue ; } String[] fields = getFieldsFrom(list); jedis.hdel(bigKey, fields); if (cursor.equals(“0 ”)) { break ; } } jedis.del(bigKey); } private String[] getFieldsFrom(List<Entry<String, String>> list) { List<String> fields = new ArrayList <String>(); for (Entry<String, String> entry : list) { fields.add(entry.getKey()); } return fields.toArray(new String [fields.size()]); }
请勿忘记每次执行到最后执行 del key 操作。
| 实战代码 ①JedisCluster 示例:
1 2 3 4 5 6 7 8 9 10 void removeBigKey (final String key, final int scanCount, final long intervalMills)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 实现: JedisCluster jedisCluster = redisClusterTemplate.getJedisClusterInstance();String cursor = ScanParams.SCAN_POINTER_START;ScanParams scanParams = new ScanParams ();scanParams.count(scanCount); while (true ) { ScanResult<Map.Entry<String, String>> scanResult = jedisCluster.hscan(key, cursor, scanParams); cursor = scanResult.getStringCursor(); List<Map.Entry<String, String>> list = scanResult.getResult(); if (CollectionUtils.isEmpty(list)) { break ; } String[] fields = getFieldsKeyArray(list); jedisCluster.hdel(key, fields); if (ScanParams.SCAN_POINTER_START.equals(cursor)) { break ; } DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS); } jedisCluster.del(key);
构建的 key:
1 2 3 4 5 6 7 8 9 10 11 12 private String[] getFieldsKeyArray(List<Map.Entry<String, String>> list) { String[] strings = new String [list.size()]; for (int i = 0 ; i < list.size(); i++) { strings[i] = list.get(i).getKey(); } return strings; }
①redisTemplate 的写法
估计是 redis 进行了一次封装,发现还是存在很多坑。
语法如下:
1 2 3 4 5 6 7 8 9 10 11 12 Cursor<V> scan (K key, ScanOptions options) ;
②注意的坑
实际上这个方法存在很多需要注意的坑:
cursor 要关闭,否则会内存泄漏 cursor 不要重复关闭,或者会报错 cursor 经测试,直接指定的 count 设置后,返回的结果其实是全部,所以需要自己额外处理 参考代码如下:
声明 StringRedisTemplate:
1 2 @Autowired private StringRedisTemplate template;
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void removeBigKey (String key, int scanCount, long intervalMills) throws CacheException { final ScanOptions scanOptions = ScanOptions.scanOptions().count(scanCount).build(); try (Cursor<Map.Entry<Object,Object>> cursor = template.opsForHash().scan(key, scanOptions)) { if (ObjectUtil.isNotNull(cursor)) { List<String> fieldKeyList = new ArrayList <>(); while (cursor.hasNext()) { String fieldKey = String.valueOf(cursor.next().getKey()); fieldKeyList.add(fieldKey); if (fieldKeyList.size() >= scanCount) { Object[] fields = fieldKeyList.toArray(); template.opsForHash().delete(key, fields); logger.info("[Big key] remove key: {}, fields size: {}" , key, fields.length); fieldKeyList.clear(); DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS); } } } this .opsForValueDelete(key); } catch (Exception e) { } }
这里我们使用 TRW 保证 cursor 被关闭,自己实现 scanCount 一次进行删除,避免一个一个删除网络交互较多。使用睡眠保证对 Redis 压力不要过大。