3天时间,我是如何解决redis bigkey 删除问题的?( 三 )


  • 缺点
需要知道具体的淘汰策略
对内存是不够友好的
可能要根据业务进行调整 , 比如本来显式删除 , 可以放在凌晨 。
如果使用定期删除 , 被淘汰的时间就变得不固定了 。
实战代码示例/** * 刪除 BIG key * 应用场景:对于 big key , 可以使用 hscan 首先分批次删除 , 最后统一删除 * (1)比直接删除的耗时变长 , 但是不会产生慢操作 。* (2)新业务实现尽可能拆开 , 不要依赖此方法 。* @param key key * @param scanCount 单次扫描总数(建议值:100) * @param intervalMills 分批次的等待时间(建议值:5) */void removeBigKey(final String key, final int scanCount, final long intervalMills)实现
JedisCluster jedisCluster = redisClusterTemplate.getJedisClusterInstance();// 游标初始值为0String cursor = ScanParams.SCAN_POINTER_START;ScanParams scanParams = new ScanParams();scanParams.count(scanCount);while (true) { // 每次扫描后获取新的游标 ScanResult scanResult = jedisCluster.hscan(key, cursor, scanParams); cursor = scanResult.getStringCursor(); // 获取扫描结果为空 List list = scanResult.getResult(); if (CollectionUtils.isEmpty(list)) {break; } // 构建多个删除的 key String[] fields = getFieldsKeyArray(list); jedisCluster.hdel(key, fields); // 游标为0时停止 if (ScanParams.SCAN_POINTER_START.equals(cursor)) {break; } // 沉睡等待 , 避免对 redis 压力太大 DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS);}// 执行 key 本身的删除jedisCluster.del(key);
  • 构建的 key
/** * 获取对应的 keys 信息 * @param list 列表 * @return 结果 */private String[] getFieldsKeyArray(List 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 进行了一次封装 , 发现还是存在很多坑 。
语法如下:
/** * 获取集合的游标 。 通过游标可以遍历整个集合 。* ScanOptions 这个类中使用了构造者 工厂方法 单例 。通过它可以配置返回的元素 * 个数 count与正则匹配元素 match. 不过count设置后不代表一定返回的就是count个 。 这个只是参考 * 意义 * * @param key * @param options* @return * @since 1.4 */Cursor scan(K key, ScanOptions options);注意的坑实际上这个方法存在很多需要注意的坑:
(1)cursor 要关闭 , 否则会内存泄漏
(2)cursor 不要重复关闭 , 或者会报错
(3)cursor 经测试 , 直接指定的 count 设置后 , 返回的结果其实是全部 , 所以需要自己额外处理
参考代码如下:
  • 声明
@Autowiredprivate StringRedisTemplate template;
  • 核心代码
public void removeBigKey(String key, int scanCount, long intervalMills) throws CacheException {final ScanOptions scanOptions = ScanOptions.scanOptions().count(scanCount).build();//TRW 避免内存泄漏 try(Cursor> cursor =template.opsForHash().scan(key, scanOptions)) {if(ObjectUtil.isNotNull(cursor)) {// 执行循环删除List 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();// 沉睡等待 , 避免对 redis 压力太大DateUtil.sleepInterval(intervalMills, TimeUnit.MILLISECONDS);}}}// 最后 fieldKeyList 中可能还有剩余 , 不过一般数量不大 , 直接删除速度不会很慢// 执行 key 本身的删除this.opsForValueDelete(key); } catch (Exception e) {// log.error(); }}这里我们使用 TRW 保证 cursor 被关闭 , 自己实现 scanCount 一次进行删除 , 避免 1 个 1 个删除网络交互较多 。
使用睡眠保证对 Redis 压力不要过大 。
测试验证当然上线之前需要测试充分的验证 , 这里我是自己做了自测 , 然后让测试帮我做了回归 。
确认不影响功能 , 删除性能等方面都没有问题之后 , 才进行了上线发布 。
上线之后做了连续3天的观察 , 一切稳定 , 这个问题也算是告一段落 。
小结redis 作为一个高性能的缓存服务 , 使用的时候确实会存在各种各样的问题 。
有时候是我们自己使用不留意 , 有时候是前人埋下的坑 。 (比如我这个)
遇到这种问题 , 找到原因 , 并且找到合适的解决方案 , 才是最重要的 。
这个 redis bigkey 我前后分析+编码+自测+上线 , 差不多共计 3 天左右 , 还算顺利 。 其中两条是分析原因+讨论解决方案 , 编码测试反倒比较简单 。