前言
在现今的互联网系统中,Redis以其优秀的性能和简洁的设计,被广泛使用,尤其是在分布式系统中;Redis最主要的应用场景在于为分布式系统提供公用内存服务,而在使用的过程中,尤其是线上的高可用环境,需要时刻注意其内存占用情况,一旦其内存吃紧,就会导致Redis性能下降,并引发服务不可用;此时除了硬件扩展,一个经常使用的手段就是线上数据清理
如何清理线上环境Redis数据
一般有正规运营团队的公司里,运营团队都会针对Redis出一套规范,这里面一般就包括:如无特殊情况,所有数据必须设置超时时间,以防数据无限堆积;即便如此,还是无法保证所有开发人员百分百遵守,同时即便遵守,理论上也还是有内存吃紧的情况,此时除了增加硬件资源外,常用的做法就是根据业务对Redis无用数据进行清理
另一方面,由于Redis是基于单线程事件模型(为什么是单线程,可以参照Redis FAQ),为了保持线上高可用,一般决不允许单一命令阻塞过长,如:KEYS
,SMEMBERS
等命令就需要限制不能随便使用;此时如何对Redis的数据进行清理呢?针对这种情况,Redis给出了「迭代扫描」系列命令,先来看看Redis官方对该系列命令的描述:
The SCAN command and the closely related commands SSCAN, HSCAN and ZSCAN are used in order to incrementally iterate over a collection of elements.
- SCAN iterates the set of keys in the currently selected Redis database.
- SSCAN iterates elements of Sets types.
- HSCAN iterates fields of Hash types and their associated values.
- ZSCAN iterates elements of Sorted Set types and their associated scores.
Since these commands allow for incremental iteration, returning only a small number of elements per call, they can be used in production without the downside of commands like KEYS or SMEMBERS that may block the server for a long time (even several seconds) when called against big collections of keys or elements
大思就是SCAN系列命令主要包含四个:SCAN
,SSCAN
,HSCAN
,ZSCAN
,分别针对不同的数据类型,它们的主要特点在于支持对数据进行迭代式扫描,每次只返回部分数据(返回多少可定制),这就有效屏蔽了全量扫描命令如KEYS
,SMEMBERS
等单一命令执行时间过长造成的服务器阻塞
下面就如何利用HSCAN
进行数据扫描并清理进行举例说明,实现语言为Java,其他语言逻辑大同小异,具体如下:
/**
* 迭代式删除Hash数据
* @param hashKey, hash key
* @param iterSize, 每次迭代元素数量,过大会可能会导致阻塞,过小会导致清理效率底下
* @return
*/
public int delHash(String hashKey, int iterSize) {
int cursor = 0;
ScanParams scanParams = new ScanParams().match("*").count(iterSize > 0 ? iterSize : 1000);
ScanResult<Entry<String, String>> scanResult;
List<Entry<String, String>> processList = new ArrayList<>();
int delCount = 0;
do {
try {
log.info("begin scan, cursor is {}", cursor);
scanResult = jedis.hscan(hashKey, String.valueOf(cursor), scanParams);
processList.addAll(scanResult.getResult());
List<String> dList = processList.stream().map(Entry::getKey).collect(Collectors.toList());
//批量删除
jedis.hdel(hashKey, dList.toArray(new String[dList.size()]));
delCount += processList.size();
processList.clear();
//获取游标位置,若大于0,则代表还有数据,需要继续迭代
cursor = Integer.parseInt(scanResult.getStringCursor());
log.info("scan next, cursor is {}", cursor);
}catch (Exception e) {
log.error(e.getLocalizedMessage(), e);
}
} while (cursor > 0);
log.info("delete hash {} over, {} key be deleted", hashKey, delCount);
return delCount;
}
使用方式不复杂,如果使用过Java迭代器的同学应该对这种形式很熟悉,主要流程就是通过判断标识(Java Iterator 中的 hasNext()
和next()
)对整体数据进行切分并操作,如此循环,直至覆盖全部数据;这里有一个点需要注意的就是,每次迭代元素数量需要设置一个合适的值,过大会可能会导致阻塞,过小会导致处理效率底下,我在实际使用的时候发现每次处理1000个效率较高,当然,实际情况可能会根据硬件和带宽不同而有差异
总结
本文只从纯技术角度简单的阐述了Redis线上数据清理需要注意的点及如何操作,实际的过程中还需要有很多其他考虑,如:
- 与业务团队沟通,哪些数据可以被清理,是否需要对相关数据做另行备份,以防万一误删
- 与保障团队沟通,以防清理过程中出现问题,需要及时恢复
等